ThinkingContent.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useRef } from 'react';
  16. import { Typography } from '@douyinfe/semi-ui';
  17. import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
  18. import { ChevronRight, ChevronUp, Brain, Loader2 } from 'lucide-react';
  19. import { useTranslation } from 'react-i18next';
  20. const ThinkingContent = ({
  21. message,
  22. finalExtractedThinkingContent,
  23. thinkingSource,
  24. styleState,
  25. onToggleReasoningExpansion
  26. }) => {
  27. const { t } = useTranslation();
  28. const scrollRef = useRef(null);
  29. const lastContentRef = useRef('');
  30. const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
  31. const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
  32. useEffect(() => {
  33. if (scrollRef.current && finalExtractedThinkingContent && message.isReasoningExpanded) {
  34. scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  35. }
  36. }, [finalExtractedThinkingContent, message.isReasoningExpanded]);
  37. useEffect(() => {
  38. if (!isThinkingStatus) {
  39. lastContentRef.current = '';
  40. }
  41. }, [isThinkingStatus]);
  42. if (!finalExtractedThinkingContent) return null;
  43. let prevLength = 0;
  44. if (isThinkingStatus && lastContentRef.current) {
  45. if (finalExtractedThinkingContent.startsWith(lastContentRef.current)) {
  46. prevLength = lastContentRef.current.length;
  47. }
  48. }
  49. if (isThinkingStatus) {
  50. lastContentRef.current = finalExtractedThinkingContent;
  51. }
  52. return (
  53. <div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
  54. <div
  55. className="flex items-center justify-between p-3 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all"
  56. style={{
  57. background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
  58. position: 'relative'
  59. }}
  60. onClick={() => onToggleReasoningExpansion(message.id)}
  61. >
  62. <div className="absolute inset-0 overflow-hidden">
  63. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  64. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  65. </div>
  66. <div className="flex items-center gap-2 sm:gap-4 relative">
  67. <div className="w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center shadow-lg">
  68. <Brain style={{ color: 'white' }} size={styleState.isMobile ? 12 : 16} />
  69. </div>
  70. <div className="flex flex-col">
  71. <Typography.Text strong style={{ color: 'white' }} className="text-sm sm:text-base">
  72. {headerText}
  73. </Typography.Text>
  74. {thinkingSource && (
  75. <Typography.Text style={{ color: 'white' }} className="text-xs mt-0.5 opacity-80 hidden sm:block">
  76. 来源: {thinkingSource}
  77. </Typography.Text>
  78. )}
  79. </div>
  80. </div>
  81. <div className="flex items-center gap-2 sm:gap-3 relative">
  82. {isThinkingStatus && !message.isThinkingComplete && (
  83. <div className="flex items-center gap-1 sm:gap-2">
  84. <Loader2 style={{ color: 'white' }} className="animate-spin" size={styleState.isMobile ? 14 : 18} />
  85. <Typography.Text style={{ color: 'white' }} className="text-xs sm:text-sm font-medium opacity-90">
  86. 思考中
  87. </Typography.Text>
  88. </div>
  89. )}
  90. {(!isThinkingStatus || message.isThinkingComplete) && (
  91. <div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center">
  92. {message.isReasoningExpanded ?
  93. <ChevronUp size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} /> :
  94. <ChevronRight size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} />
  95. }
  96. </div>
  97. )}
  98. </div>
  99. </div>
  100. <div
  101. className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
  102. } overflow-hidden bg-gradient-to-br from-purple-50 via-indigo-50 to-violet-50`}
  103. >
  104. {message.isReasoningExpanded && (
  105. <div className="p-3 sm:p-5 pt-2 sm:pt-4">
  106. <div
  107. ref={scrollRef}
  108. className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll"
  109. style={{
  110. maxHeight: '200px',
  111. scrollbarWidth: 'thin',
  112. scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent'
  113. }}
  114. >
  115. <div className="prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm">
  116. <MarkdownRenderer
  117. content={finalExtractedThinkingContent}
  118. className=""
  119. animated={isThinkingStatus}
  120. previousContentLength={prevLength}
  121. />
  122. </div>
  123. </div>
  124. </div>
  125. )}
  126. </div>
  127. </div>
  128. );
  129. };
  130. export default ThinkingContent;