ChatMessages.vue 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168
  1. <template>
  2. <div class="chat-messages-container">
  3. <!-- 左侧对话缩略导航 -->
  4. <div
  5. v-if="qaPairs.length > 0"
  6. class="conversation-nav"
  7. ref="navRef"
  8. @mouseenter="onNavMove"
  9. @mousemove="onNavMove"
  10. @mouseleave="onNavLeave"
  11. @scroll="onNavScroll"
  12. >
  13. <div
  14. v-for="(pair, pIdx) in qaPairs"
  15. :key="pIdx"
  16. :class="['nav-line', { 'nav-line-hot': nearestLineIndex === pIdx, active: activeMessage && activeMessage === messages[pair.startIndex] }]"
  17. :style="{ width: `${navLineWidth(pIdx)}px` }"
  18. @click="scrollToPair(pair)"
  19. ></div>
  20. </div>
  21. <Transition name="tooltip-fade">
  22. <div v-if="hoveredPair" ref="tooltipRef" class="conversation-tooltip" :style="{ top: `${tooltipTop}px` }">
  23. <span class="tooltip-arrow" :style="{ top: `${tooltipArrowTop}px` }"></span>
  24. <div v-if="hoveredPair.question" class="tooltip-row">
  25. <span class="tooltip-label tooltip-label-q">问</span>
  26. <span class="tooltip-text">{{ hoveredPair.question }}</span>
  27. </div>
  28. <div v-if="hoveredPair.answer" class="tooltip-row">
  29. <span class="tooltip-label tooltip-label-a">答</span>
  30. <span class="tooltip-text">{{ hoveredPair.answer }}</span>
  31. </div>
  32. </div>
  33. </Transition>
  34. <div class="chat-messages" ref="messagesRef">
  35. <!-- 新对话缺省图 -->
  36. <div v-if="messages.length === 0" class="welcome-empty">
  37. <img :src="getCssUrl('--img-chat-welcome-img')" class="welcome-img" />
  38. </div>
  39. <div v-for="(message, index) in messages" :key="index" :class="['message-wrapper', message.type]" @mouseleave="copiedMessageIndex = -1">
  40. <!-- AI消息 -->
  41. <div v-if="message.type === 'ai'" class="ai-message">
  42. <div class="message-body">
  43. <!-- 思考过程折叠卡片 -->
  44. <div v-if="message.thinkingSteps && message.thinkingSteps.length > 0" class="thinking-card">
  45. <div class="thinking-duration-bar" @click="toggleThinkingCard(index)">
  46. <span v-if="getDurationText(message)" class="thinking-duration">{{ getDurationText(message) }}</span>
  47. <div :class="['thinking-arrow', { expanded: !collapsedThinking[index] }]">
  48. <img :src="getCssUrl('--img-chat-arrow-icon')" width="12" height="12" />
  49. </div>
  50. </div>
  51. <Transition name="collapse">
  52. <div v-show="!collapsedThinking[index]" class="thinking-collapse-wrap">
  53. <div class="thinking-header" @click="toggleThinkingContent(index)">
  54. <div class="thinking-header-left">
  55. <div class="thinking-icon"></div>
  56. <span class="thinking-title">思考过程</span>
  57. </div>
  58. <div v-if="message.thinkingContent" :class="['thinking-arrow', { expanded: !collapsedThinkingContent[index] }]">
  59. <img :src="getCssUrl('--img-chat-arrow-icon')" width="10" height="10" />
  60. </div>
  61. </div>
  62. <!-- thinking_token 思考内容 -->
  63. <Transition name="collapse">
  64. <div v-if="message.thinkingContent && !collapsedThinkingContent[index]" class="thinking-content-body">
  65. {{ message.thinkingContent }}
  66. </div>
  67. </Transition>
  68. <div class="thinking-body">
  69. <div
  70. v-for="(group, groupIdx) in groupStepsByAgent(message.thinkingSteps)"
  71. :key="groupIdx"
  72. class="agent-group"
  73. @click="handleAgentGroupClick(index, group)"
  74. >
  75. <div class="agent-group-header">
  76. <div class="agent-group-header-left">
  77. <div class="agent-group-icon"></div>
  78. <span class="agent-group-name">{{ group.agent }}</span>
  79. </div>
  80. <span v-if="getLatestStepMessage(group)" class="agent-group-latest">
  81. {{ getLatestStepMessage(group) }}
  82. </span>
  83. <div class="thinking-arrow agent-arrow">
  84. <img :src="getCssUrl('--img-chat-next-icon')" width="10" height="10" />
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </Transition>
  91. </div>
  92. <div
  93. v-if="message.content"
  94. class="message-text"
  95. v-html="
  96. renderMarkdown(
  97. message.content,
  98. tableIcons,
  99. message.citations?.items?.length ? Math.max(...message.citations.items.map((i) => i.id)) : undefined
  100. )
  101. "
  102. @click="handleFootnoteClick($event, index)"
  103. ></div>
  104. <!-- 数据来源卡片 citations -->
  105. <div v-if="message.citations" class="citations-card">
  106. <div v-if="message.citations.model_only" class="citations-header citations-header-simple">
  107. <span class="citations-title">本回答依据模型知识生成,仅供参考</span>
  108. </div>
  109. <template v-else-if="message.citations.items?.length">
  110. <div class="citations-header" @click="toggleCitations(index)">
  111. <div class="citations-header-left">
  112. <span class="citations-title">数据来源</span>
  113. <span class="citations-count">{{ message.citations.items.length }}</span>
  114. </div>
  115. <div :class="['thinking-arrow', { expanded: !collapsedCitations[index] }]">
  116. <img :src="getCssUrl('--img-chat-arrow-icon')" width="10" height="10" />
  117. </div>
  118. </div>
  119. <div v-show="!collapsedCitations[index]" class="citations-body">
  120. <div v-for="item in message.citations.items" :key="item.id" :id="`citation-${index}-${item.id}`" class="citation-item">
  121. <div class="citation-item-row">
  122. <span :class="['citation-badge', `citation-badge-${item.type}`]">{{ item.type_cn }}</span>
  123. <span class="citation-title-text">{{ item.title }}</span>
  124. <span v-if="item.search_url" class="citation-link">知识库检索</span>
  125. <span v-if="item.url" class="citation-link">链接</span>
  126. </div>
  127. <div class="citation-item-attrs">
  128. <span v-if="item.source_system" class="citation-attr">{{ item.source_system }}</span>
  129. <span v-if="item.fetched_at" class="citation-attr">{{ item.fetched_at }}</span>
  130. </div>
  131. <div v-if="item.snippet" class="citation-snippet" :title="item.snippet">{{ item.snippet }}</div>
  132. </div>
  133. </div>
  134. </template>
  135. </div>
  136. <div v-if="message.wordDownloadUrl" class="word-download-area">
  137. <a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
  138. </div>
  139. <!-- 文件操作卡片 -->
  140. <div v-if="message.downloadFiles?.length" class="download-file-cards">
  141. <div v-for="(file, fIdx) in message.downloadFiles" :key="fIdx" class="download-file-card" @click="handlePreviewFile(file)">
  142. <div class="file-card-info">
  143. <div class="file-card-icon" :style="{ backgroundImage: getFileIconVar(file.filename) }"></div>
  144. <div class="file-card-text">
  145. <span class="file-card-name">{{ file.filename }}</span>
  146. <span class="file-card-hint">点击预览文件</span>
  147. </div>
  148. </div>
  149. <div class="file-card-actions">
  150. <button class="file-action-btn download-btn" @click.stop="handleDownloadFile(file.url, file.filename)">
  151. <img :src="getCssUrl('--img-chat-download-icon-btn')" width="14" height="14" />
  152. <span>下载</span>
  153. </button>
  154. <button v-if="isEditableFile(file.filename)" class="file-action-btn edit-btn" @click.stop="handleEditFile(file.url, file.filename)">
  155. <img :src="getCssUrl('--img-chat-edit-icon')" width="14" height="14" />
  156. <span>编辑</span>
  157. </button>
  158. </div>
  159. </div>
  160. </div>
  161. <div v-show="message.isLoading" class="loading-dots">
  162. <span class="dot"></span>
  163. <span class="dot"></span>
  164. <span class="dot"></span>
  165. </div>
  166. <!-- ask_user 交互提问 UI(分步向导) -->
  167. <div v-if="message.isPendingApproval && message.pendingApprovalData?.ask_user" class="approval-card">
  168. <div class="approval-icon"></div>
  169. <div class="approval-content">
  170. <div class="approval-title">{{ message.pendingApprovalData?.message }}</div>
  171. <!-- 计划内容 -->
  172. <div v-if="message.pendingApprovalData?.plan" class="approval-plan">
  173. <div class="approval-plan-label">执行计划</div>
  174. <div class="approval-plan-content">{{ message.pendingApprovalData.plan }}</div>
  175. </div>
  176. <!-- 步骤指示器 -->
  177. <div v-if="message.pendingApprovalData.ask_user.questions.length > 1" class="ask-user-stepper">
  178. <div
  179. v-for="(_, sIdx) in message.pendingApprovalData.ask_user.questions"
  180. :key="sIdx"
  181. :class="[
  182. 'ask-user-step-dot',
  183. {
  184. active: sIdx === (askUserCurrentStep[index] ?? 0),
  185. completed: sIdx < (askUserCurrentStep[index] ?? 0) || !!(askUserAnswers[index]?.[sIdx] && askUserAnswers[index][sIdx].trim()),
  186. },
  187. ]"
  188. @click="askUserCurrentStep[index] = sIdx"
  189. ></div>
  190. <span class="ask-user-step-text"
  191. >{{ (askUserCurrentStep[index] ?? 0) + 1 }} / {{ message.pendingApprovalData.ask_user.questions.length }}</span
  192. >
  193. </div>
  194. <!-- 当前问题 -->
  195. <div v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]" class="ask-user-question">
  196. <div class="ask-user-question-text">
  197. {{ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].question }}
  198. <span
  199. v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].required !== false"
  200. class="ask-user-required"
  201. >*</span
  202. >
  203. </div>
  204. <!-- multiple_choice 类型 -->
  205. <div
  206. v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'multiple_choice'"
  207. class="ask-user-choices"
  208. >
  209. <div
  210. v-for="(choice, cIdx) in message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].choices"
  211. :key="cIdx"
  212. :class="[
  213. 'ask-user-choice',
  214. {
  215. selected:
  216. askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0] === getChoiceValue(choice) &&
  217. !askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0],
  218. },
  219. ]"
  220. @click="selectChoice(index, askUserCurrentStep[index] ?? 0, choice, message.pendingApprovalData.ask_user.questions.length)"
  221. >
  222. <span>{{ getChoiceValue(choice) }}</span>
  223. </div>
  224. <!-- Other 选项 -->
  225. <div
  226. :class="['ask-user-choice', { selected: askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0] }]"
  227. @click="selectOther(index, askUserCurrentStep[index] ?? 0)"
  228. >
  229. <span>其他</span>
  230. </div>
  231. <input
  232. v-if="askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0]"
  233. class="ask-user-other-input"
  234. placeholder="请输入..."
  235. :value="askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0]"
  236. @input="askUserAnswers[index][askUserCurrentStep[index] ?? 0] = ($event.target as HTMLInputElement).value"
  237. />
  238. </div>
  239. <!-- text 类型 -->
  240. <div
  241. v-else-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'text'"
  242. class="ask-user-text"
  243. >
  244. <textarea
  245. class="ask-user-textarea"
  246. placeholder="请输入..."
  247. :value="askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0]"
  248. @input="askUserAnswers[index][askUserCurrentStep[index] ?? 0] = ($event.target as HTMLTextAreaElement).value"
  249. ></textarea>
  250. </div>
  251. </div>
  252. <!-- 操作按钮 -->
  253. <div class="approval-actions">
  254. <button v-if="(askUserCurrentStep[index] ?? 0) > 0" class="approval-btn nav-btn" @click="goPrevStep(index)">上一步</button>
  255. <button
  256. v-if="(askUserCurrentStep[index] ?? 0) < message.pendingApprovalData.ask_user.questions.length - 1"
  257. class="approval-btn approve-btn"
  258. :disabled="
  259. !isCurrentStepAnswered(
  260. index,
  261. askUserCurrentStep[index] ?? 0,
  262. message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]
  263. )
  264. "
  265. @click="goNextStep(index, message.pendingApprovalData.ask_user.questions.length)"
  266. >下一步</button
  267. >
  268. <button
  269. v-if="(askUserCurrentStep[index] ?? 0) === message.pendingApprovalData.ask_user.questions.length - 1"
  270. class="approval-btn approve-btn"
  271. :disabled="
  272. !isCurrentStepAnswered(
  273. index,
  274. askUserCurrentStep[index] ?? 0,
  275. message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]
  276. )
  277. "
  278. @click="handleSubmitAskUser(index)"
  279. >提交</button
  280. >
  281. <button class="approval-btn reject-btn" @click="handleCancelAskUser">取消</button>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. <!-- AI消息操作栏 -->
  287. <div class="message-actions">
  288. <div class="action-btn" @click="copyMessageContent(message.content, index)" :title="copiedMessageIndex === index ? '复制成功' : '复制'">
  289. <img :src="copyIconUrl(index)" width="14" height="14" />
  290. </div>
  291. <div v-if="message.id" class="action-btn" @click="emit('delete-message', message)" title="删除消息">
  292. <img :src="getCssUrl('--img-chat-delete-icon')" width="14" height="14" />
  293. </div>
  294. <span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
  295. </div>
  296. </div>
  297. <!-- 用户消息 -->
  298. <div v-else class="user-message">
  299. <div class="message-body">
  300. <div class="message-text" v-html="renderMarkdown(message.content, tableIcons)"></div>
  301. <div v-if="message.attachedFile" class="attached-file-card" @click="emit('file-preview', message.attachedFile)">
  302. <div class="file-icon-card" :style="{ backgroundImage: getFileIconVar(message.attachedFile.name) }"></div>
  303. <div class="file-info-card">
  304. <div class="file-name-card">{{ message.attachedFile.name }}</div>
  305. <div class="file-size-card">{{ formatFileSize(message.attachedFile.size) }}</div>
  306. </div>
  307. </div>
  308. </div>
  309. <!-- 用户消息操作栏 -->
  310. <div class="message-actions">
  311. <div class="action-btn" @click="copyMessageContent(message.content, index)" :title="copiedMessageIndex === index ? '复制成功' : '复制'">
  312. <img :src="copyIconUrl(index)" width="14" height="14" />
  313. </div>
  314. <div v-if="message.id" class="action-btn" @click="emit('delete-message', message)" title="删除消息">
  315. <img :src="getCssUrl('--img-chat-delete-icon')" width="14" height="14" />
  316. </div>
  317. <span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
  318. </div>
  319. </div>
  320. </div>
  321. </div>
  322. <!-- 回到底部按钮 -->
  323. <div v-if="!isAtBottom" class="scroll-to-bottom" @click="scrollToBottom">
  324. <div class="scroll-to-bottom-icon"></div>
  325. </div>
  326. </div>
  327. </template>
  328. <script setup lang="ts">
  329. import { ref, reactive, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
  330. import dayjs from 'dayjs';
  331. import {
  332. renderMarkdown,
  333. formatFileSize,
  334. tableHtmlToMarkdown,
  335. tableHtmlToCsv,
  336. isMdFile,
  337. isTextFile,
  338. isWordFile,
  339. isExcelFile,
  340. isImageFile,
  341. isPdfFile,
  342. getFileIconVar,
  343. } from './utils';
  344. import { getToken } from '/@/utils/auth';
  345. import { message } from 'ant-design-vue';
  346. import type { Message, AttachedFile, ThinkingStep } from './types';
  347. // import katex from 'katex';
  348. // import 'katex/dist/katex.min.css';
  349. // Cache CSS variable lookups to avoid repeated getComputedStyle calls on every render
  350. const cssUrlCache = new Map<string, string>();
  351. const getCssUrl = (varName: string) => {
  352. if (!cssUrlCache.has(varName)) {
  353. const val = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
  354. cssUrlCache.set(varName, val.replace(/^url\(["']?/, '').replace(/["']?\)$/, ''));
  355. }
  356. return cssUrlCache.get(varName)!;
  357. };
  358. // Clear cache when theme changes (observed via MutationObserver on html class)
  359. const themeObserver = new MutationObserver(() => cssUrlCache.clear());
  360. themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
  361. onBeforeUnmount(() => themeObserver.disconnect());
  362. const tableIcons = {
  363. copy: getCssUrl('--img-chat-copy-icon'),
  364. download: getCssUrl('--img-chat-download-icon-btn'),
  365. preview: getCssUrl('--img-chat-preview-icon'),
  366. copySuccess: getCssUrl('--img-chat-copy-success-icon'),
  367. };
  368. const copyIconUrl = computed(() => {
  369. const icon = getCssUrl('--img-chat-copy-icon');
  370. const successIcon = getCssUrl('--img-chat-copy-success-icon');
  371. return (index: number) => (copiedMessageIndex.value === index ? successIcon : icon);
  372. });
  373. // 全局计时器 - 每秒刷新一次,用于实时显示思考耗时
  374. const nowTs = ref(Date.now());
  375. let timerInterval: ReturnType<typeof setInterval> | null = null;
  376. const getDurationText = (message: Message): string => {
  377. // 优先使用后端返回的 duration_ms(已包含 baseDurationMs 累加)
  378. if (message.durationMs) {
  379. const totalSec = Math.round(message.durationMs / 1000);
  380. const min = Math.floor(totalSec / 60);
  381. const sec = totalSec % 60;
  382. return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
  383. }
  384. // 审批等待中 - 暂停显示中断前的累计时长
  385. if (message.isPendingApproval && message.pendingApprovalData?.elapsedMs) {
  386. const totalSec = Math.round(message.pendingApprovalData.elapsedMs / 1000);
  387. const min = Math.floor(totalSec / 60);
  388. const sec = totalSec % 60;
  389. return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
  390. }
  391. // 前端实时计时(含中断前累计时长)
  392. if (message.generateStartTime && message.isLoading) {
  393. const baseMs = message.baseDurationMs || 0;
  394. const elapsed = Math.floor((nowTs.value - message.generateStartTime + baseMs) / 1000);
  395. const min = Math.floor(elapsed / 60);
  396. const sec = elapsed % 60;
  397. return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
  398. }
  399. return '';
  400. };
  401. const props = defineProps<{
  402. messages: Message[];
  403. }>();
  404. // Track the last pendingApprovalData reference to detect new interrupts
  405. const lastApprovalDataRef = new Map<number, any>();
  406. // 自动初始化 ask_user 答案状态
  407. watch(
  408. () => props.messages,
  409. (msgs) => {
  410. msgs.forEach((msg, idx) => {
  411. if (msg.isPendingApproval && msg.pendingApprovalData?.ask_user?.questions) {
  412. // Only reinitialize if pendingApprovalData object reference changed (new interrupt)
  413. const lastRef = lastApprovalDataRef.get(idx);
  414. if (lastRef !== msg.pendingApprovalData) {
  415. lastApprovalDataRef.set(idx, msg.pendingApprovalData);
  416. initAskUserAnswers(idx, msg.pendingApprovalData.ask_user.questions);
  417. }
  418. } else {
  419. lastApprovalDataRef.delete(idx);
  420. }
  421. });
  422. },
  423. { immediate: true, deep: true }
  424. );
  425. export interface AgentDetailData {
  426. messageIndex: number;
  427. agent: string;
  428. agentId?: string;
  429. }
  430. const emit = defineEmits<{
  431. (e: 'file-preview', file: AttachedFile): void;
  432. (e: 'download-word', url: string): void;
  433. (e: 'open-file-preview', file: AttachedFile): void;
  434. (e: 'ask-user-submit', answers: string[]): void;
  435. (e: 'ask-user-cancel'): void;
  436. (e: 'agent-detail-click', data: AgentDetailData): void;
  437. (e: 'open-markdown-tab', payload: { title: string; content: string }): void;
  438. (e: 'delete-message', message: Message): void;
  439. }>();
  440. const isEditableFile = (filename: string) => isMdFile(filename) || isTextFile(filename);
  441. const handleDownloadFile = (url: string, filename: string) => {
  442. if (!url) return;
  443. const a = document.createElement('a');
  444. a.href = url;
  445. a.download = filename;
  446. document.body.appendChild(a);
  447. a.click();
  448. document.body.removeChild(a);
  449. };
  450. const handleEditFile = async (url: string, filename: string) => {
  451. try {
  452. const resp = await fetch(url, { headers: { 'X-Access-Token': getToken() } });
  453. if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
  454. const content = await resp.text();
  455. const ext = filename.split('.').pop()?.toLowerCase() || '';
  456. const mimeMap: Record<string, string> = {
  457. md: 'text/markdown',
  458. txt: 'text/plain',
  459. json: 'application/json',
  460. xml: 'text/xml',
  461. yaml: 'text/yaml',
  462. yml: 'text/yaml',
  463. csv: 'text/csv',
  464. };
  465. const file: AttachedFile = {
  466. id: `report-${url}`,
  467. name: filename,
  468. size: content.length,
  469. type: mimeMap[ext] || 'text/plain',
  470. content,
  471. uploadTime: new Date().toISOString(),
  472. };
  473. emit('open-file-preview', file);
  474. } catch (err) {
  475. console.error('获取文件内容失败:', err);
  476. message.error('获取文件内容失败');
  477. }
  478. };
  479. // 点击文件卡片:按类型拉取内容并在右侧辅助面板打开预览标签
  480. const handlePreviewFile = async (file: { url: string; filename: string }) => {
  481. if (!file.url) return;
  482. try {
  483. const resp = await fetch(file.url, { headers: { 'X-Access-Token': getToken() } });
  484. if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
  485. const ext = file.filename.split('.').pop()?.toLowerCase() || '';
  486. const mimeMap: Record<string, string> = {
  487. md: 'text/markdown',
  488. txt: 'text/plain',
  489. json: 'application/json',
  490. xml: 'text/xml',
  491. yaml: 'text/yaml',
  492. yml: 'text/yaml',
  493. csv: 'text/csv',
  494. };
  495. const id = `report-${file.url}`;
  496. const uploadTime = new Date().toISOString();
  497. let previewFile: AttachedFile;
  498. if (mimeMap[ext]) {
  499. const content = await resp.text();
  500. previewFile = { id, name: file.filename, size: content.length, type: mimeMap[ext], content, uploadTime };
  501. } else if (isWordFile(file.filename) || isExcelFile(file.filename)) {
  502. const arrayBuffer = await resp.arrayBuffer();
  503. previewFile = { id, name: file.filename, size: arrayBuffer.byteLength, type: 'application/octet-stream', arrayBuffer, uploadTime };
  504. } else if (isImageFile(file.filename) || isPdfFile(file.filename)) {
  505. const blob = await resp.blob();
  506. const dataUrl = await new Promise<string>((resolve, reject) => {
  507. const reader = new FileReader();
  508. reader.onload = () => resolve(reader.result as string);
  509. reader.onerror = () => reject(reader.error);
  510. reader.readAsDataURL(blob);
  511. });
  512. previewFile = { id, name: file.filename, size: blob.size, type: blob.type, preview: dataUrl, uploadTime };
  513. } else {
  514. // 不支持的格式回退为直接下载
  515. handleDownloadFile(file.url, file.filename);
  516. return;
  517. }
  518. emit('file-preview', previewFile);
  519. } catch (err) {
  520. console.error('获取文件内容失败:', err);
  521. message.error('文件预览失败,请尝试下载查看');
  522. }
  523. };
  524. // ask_user 答案状态(按消息索引存储)
  525. const askUserAnswers = reactive<Record<number, string[]>>({});
  526. const askUserOtherSelected = reactive<Record<number, boolean[]>>({});
  527. const askUserCurrentStep = reactive<Record<number, number>>({});
  528. // choice 可能是字符串或 { value: "..." } 对象,统一提取字符串
  529. const getChoiceValue = (choice: any): string => {
  530. if (typeof choice === 'string') return choice;
  531. if (choice && typeof choice === 'object' && 'value' in choice) return choice.value;
  532. return String(choice);
  533. };
  534. const initAskUserAnswers = (index: number, questions: { question: string; type: string; choices?: any[] }[]) => {
  535. // Always reinitialize when called - handles repeated interrupts from backend
  536. askUserAnswers[index] = questions.map(() => '');
  537. askUserOtherSelected[index] = questions.map(() => false);
  538. askUserCurrentStep[index] = 0;
  539. };
  540. const isCurrentStepAnswered = (msgIndex: number, qIndex: number, question: { required?: boolean; type: string }) => {
  541. const answers = askUserAnswers[msgIndex];
  542. if (!answers) return false;
  543. if (question.required === false && !answers[qIndex]) return true;
  544. return !!(answers[qIndex] && answers[qIndex].trim() !== '');
  545. };
  546. const goNextStep = (msgIndex: number, totalQuestions: number) => {
  547. if (askUserCurrentStep[msgIndex] < totalQuestions - 1) {
  548. askUserCurrentStep[msgIndex]++;
  549. }
  550. };
  551. const goPrevStep = (msgIndex: number) => {
  552. if (askUserCurrentStep[msgIndex] > 0) {
  553. askUserCurrentStep[msgIndex]--;
  554. }
  555. };
  556. const selectChoice = (msgIndex: number, qIndex: number, choice: any, totalQuestions: number) => {
  557. if (!askUserAnswers[msgIndex]) return;
  558. askUserAnswers[msgIndex][qIndex] = getChoiceValue(choice);
  559. askUserOtherSelected[msgIndex][qIndex] = false;
  560. // 单选自动跳到下一题(非最后一题时)
  561. if (qIndex < totalQuestions - 1) {
  562. goNextStep(msgIndex, totalQuestions);
  563. }
  564. };
  565. const selectOther = (msgIndex: number, qIndex: number) => {
  566. if (!askUserOtherSelected[msgIndex]) return;
  567. askUserOtherSelected[msgIndex][qIndex] = true;
  568. askUserAnswers[msgIndex][qIndex] = '';
  569. };
  570. const handleSubmitAskUser = (index: number) => {
  571. // 防御性初始化:如果答案数组未初始化,从 pendingApprovalData 回退
  572. if (!askUserAnswers[index]) {
  573. const msg = props.messages[index];
  574. if (msg?.pendingApprovalData?.ask_user?.questions) {
  575. initAskUserAnswers(index, msg.pendingApprovalData.ask_user.questions);
  576. } else {
  577. return;
  578. }
  579. }
  580. const answers = askUserAnswers[index];
  581. if (!answers) return;
  582. emit('ask-user-submit', [...answers]);
  583. };
  584. const handleCancelAskUser = () => {
  585. emit('ask-user-cancel');
  586. };
  587. const messagesRef = ref<HTMLElement>();
  588. const collapsedThinking = reactive<Record<number, boolean>>({});
  589. const collapsedThinkingContent = reactive<Record<number, boolean>>({});
  590. const collapsedCitations = reactive<Record<number, boolean>>({});
  591. const isAtBottom = ref(true);
  592. const copiedMessageIndex = ref(-1);
  593. const handleTableAction = (e: Event) => {
  594. const target = (e.target as HTMLElement).closest('.table-action-btn') as HTMLElement;
  595. if (!target) return;
  596. const action = target.dataset.action;
  597. const encodedTable = target.dataset.table;
  598. if (!action || !encodedTable) return;
  599. const tableHtml = decodeURIComponent(encodedTable);
  600. switch (action) {
  601. case 'copy': {
  602. const markdown = tableHtmlToMarkdown(tableHtml);
  603. const doCopy = async () => {
  604. try {
  605. await navigator.clipboard.writeText(markdown);
  606. } catch {
  607. const ta = document.createElement('textarea');
  608. ta.value = markdown;
  609. ta.style.position = 'fixed';
  610. ta.style.opacity = '0';
  611. document.body.appendChild(ta);
  612. ta.select();
  613. document.execCommand('copy');
  614. document.body.removeChild(ta);
  615. }
  616. };
  617. doCopy().then(() => {
  618. const btn = target;
  619. const img = btn.querySelector('img');
  620. const successIcon = btn.dataset.iconSuccess;
  621. const originalIcon = btn.dataset.icon;
  622. if (img && successIcon) {
  623. img.src = successIcon;
  624. btn.title = '复制成功';
  625. const reset = () => {
  626. img.src = originalIcon!;
  627. btn.title = '复制Markdown';
  628. wrapper.removeEventListener('mouseleave', reset);
  629. };
  630. const wrapper = btn.closest('.markdown-table-wrapper')!;
  631. wrapper.addEventListener('mouseleave', reset);
  632. }
  633. });
  634. break;
  635. }
  636. case 'csv': {
  637. const csv = tableHtmlToCsv(tableHtml);
  638. const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
  639. const url = URL.createObjectURL(blob);
  640. const a = document.createElement('a');
  641. a.href = url;
  642. a.download = `table_${Date.now()}.csv`;
  643. a.click();
  644. URL.revokeObjectURL(url);
  645. break;
  646. }
  647. case 'preview': {
  648. // 在右侧面板新建标签页展示表格渲染效果
  649. emit('open-markdown-tab', { title: '表格预览', content: tableHtmlToMarkdown(tableHtml) });
  650. break;
  651. }
  652. }
  653. };
  654. const checkScrollBottom = () => {
  655. const el = messagesRef.value;
  656. if (!el) return;
  657. const threshold = 50;
  658. isAtBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
  659. };
  660. const scrollToBottom = () => {
  661. const el = messagesRef.value;
  662. if (el) {
  663. el.scrollTop = el.scrollHeight - el.clientHeight;
  664. }
  665. };
  666. const forceScrollToBottom = () => {
  667. const el = messagesRef.value;
  668. if (el) {
  669. el.scrollTop = el.scrollHeight - el.clientHeight;
  670. // 使用 requestAnimationFrame 确保 DOM 渲染完成后再滚动一次
  671. requestAnimationFrame(() => {
  672. el.scrollTop = el.scrollHeight - el.clientHeight;
  673. });
  674. }
  675. isAtBottom.value = true;
  676. };
  677. onMounted(() => {
  678. messagesRef.value?.addEventListener('click', handleTableAction);
  679. messagesRef.value?.addEventListener('scroll', checkScrollBottom);
  680. timerInterval = setInterval(() => {
  681. nowTs.value = Date.now();
  682. }, 1000);
  683. });
  684. onBeforeUnmount(() => {
  685. messagesRef.value?.removeEventListener('click', handleTableAction);
  686. messagesRef.value?.removeEventListener('scroll', checkScrollBottom);
  687. if (timerInterval) {
  688. clearInterval(timerInterval);
  689. timerInterval = null;
  690. }
  691. });
  692. const toggleThinkingCard = (index: number) => {
  693. collapsedThinking[index] = !collapsedThinking[index];
  694. };
  695. const toggleThinkingContent = (index: number) => {
  696. collapsedThinkingContent[index] = !collapsedThinkingContent[index];
  697. };
  698. const handleAgentGroupClick = (messageIndex: number, group: AgentGroup) => {
  699. emit('agent-detail-click', {
  700. messageIndex,
  701. agent: group.agent,
  702. agentId: group.agentId,
  703. });
  704. };
  705. const toggleCitations = (index: number) => {
  706. collapsedCitations[index] = !collapsedCitations[index];
  707. };
  708. const handleFootnoteClick = (e: Event, msgIndex: number) => {
  709. const target = (e.target as HTMLElement).closest('.footnote-ref') as HTMLElement;
  710. if (!target) return;
  711. const n = target.dataset.n;
  712. if (!n) return;
  713. const el = document.getElementById(`citation-${msgIndex}-${n}`);
  714. if (el) {
  715. el.scrollIntoView({ behavior: 'smooth', block: 'center' });
  716. el.classList.add('citation-highlight');
  717. setTimeout(() => el.classList.remove('citation-highlight'), 1500);
  718. }
  719. };
  720. const getLatestStepMessage = (group: AgentGroup): string => {
  721. if (group.steps.length === 0) return '';
  722. const last = group.steps[group.steps.length - 1];
  723. if (last.type === 'executing') return last.message || '正在执行...';
  724. if (last.type === 'tool_call') return last.message || last.tool || '';
  725. if (last.type === 'tool_result') return last.message || '';
  726. if (last.type === 'agent_start') return last.message || '';
  727. if (last.type === 'agent_done') return last.message || '';
  728. if (last.type === 'updated_todo_list') return '任务列表已更新';
  729. return '';
  730. };
  731. interface AgentGroup {
  732. agent: string;
  733. agentId?: string;
  734. steps: ThinkingStep[];
  735. }
  736. const groupStepsByAgent = (steps: ThinkingStep[]): AgentGroup[] => {
  737. const groupMap = new Map<string, AgentGroup>();
  738. for (const step of steps) {
  739. const agentName = step.agent || '系统';
  740. const groupKey = step.agentId || agentName;
  741. if (!groupMap.has(groupKey)) {
  742. groupMap.set(groupKey, { agent: agentName, agentId: step.agentId, steps: [] });
  743. }
  744. groupMap.get(groupKey)!.steps.push(step);
  745. }
  746. return Array.from(groupMap.values());
  747. };
  748. const formatFullTime = (createdAt: string) => {
  749. return dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss');
  750. };
  751. const copyMessageContent = async (content: string, index: number) => {
  752. if (!content) return;
  753. try {
  754. await navigator.clipboard.writeText(content);
  755. copiedMessageIndex.value = index;
  756. } catch {
  757. const textarea = document.createElement('textarea');
  758. textarea.value = content;
  759. textarea.style.position = 'fixed';
  760. textarea.style.opacity = '0';
  761. document.body.appendChild(textarea);
  762. textarea.select();
  763. document.execCommand('copy');
  764. document.body.removeChild(textarea);
  765. copiedMessageIndex.value = index;
  766. }
  767. };
  768. // ===== 左侧对话缩略导航 =====
  769. interface QaPair {
  770. startIndex: number;
  771. question: string;
  772. answer: string;
  773. }
  774. const toPlainText = (md: string): string =>
  775. md
  776. .replace(/```[\s\S]*?```/g, ' [代码] ')
  777. .replace(/`[^`]*`/g, ' ')
  778. .replace(/<[^>]+>/g, '')
  779. .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
  780. .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
  781. .replace(/^#{1,6}\s*/gm, '')
  782. .replace(/[*_~>|]/g, '')
  783. .replace(/\s+/g, ' ')
  784. .trim();
  785. const truncateText = (text: string, max = 50): string => (text.length > max ? `${text.slice(0, max)}…` : text);
  786. // 一轮问答 = 一条用户消息 + 其后的连续 AI 消息
  787. const qaPairs = computed<QaPair[]>(() => {
  788. const pairs: QaPair[] = [];
  789. let current: QaPair | null = null;
  790. props.messages.forEach((msg, index) => {
  791. if (msg.type === 'user') {
  792. if (current) pairs.push(current);
  793. current = { startIndex: index, question: truncateText(toPlainText(msg.content)), answer: '' };
  794. } else {
  795. if (!current) current = { startIndex: index, question: '', answer: '' };
  796. const part = msg.isLoading && !msg.content ? '正在生成...' : toPlainText(msg.content);
  797. if (part) current.answer = truncateText(current.answer ? `${current.answer} ${part}` : part);
  798. }
  799. });
  800. if (current) pairs.push(current);
  801. return pairs;
  802. });
  803. const navRef = ref<HTMLElement>();
  804. const tooltipRef = ref<HTMLElement>();
  805. const hoveredPair = ref<QaPair | null>(null);
  806. const tooltipTop = ref(0);
  807. const tooltipArrowTop = ref(0);
  808. const activeMessage = ref<Message | null>(null);
  809. // 鼠标在导航列内的 Y 坐标(导航列内容坐标系,随滚动补偿);null 表示未悬浮
  810. const navMouseY = ref<number | null>(null);
  811. // 各横线中心 Y(导航列内容坐标系),消息变化/列滚动后重建
  812. const lineCenters = ref<number[]>([]);
  813. const updateLineCenters = () => {
  814. const nav = navRef.value;
  815. if (!nav) {
  816. lineCenters.value = [];
  817. return;
  818. }
  819. const centers: number[] = [];
  820. nav.querySelectorAll<HTMLElement>('.nav-line').forEach((line) => centers.push(line.offsetTop + line.offsetHeight / 2));
  821. lineCenters.value = centers;
  822. };
  823. // 宽度随与鼠标的距离连续衰减(高斯),形成平滑的"长-中-短"波形
  824. const navLineWidth = (pIdx: number): number => {
  825. const mouseY = navMouseY.value;
  826. if (mouseY === null) return 10;
  827. const dist = Math.abs((lineCenters.value[pIdx] ?? 0) - mouseY);
  828. return 10 + 8 * Math.exp(-(dist * dist) / (2 * 14 * 14));
  829. };
  830. const nearestLineIndex = computed(() => {
  831. const mouseY = navMouseY.value;
  832. if (mouseY === null) return -1;
  833. let best = -1;
  834. let bestDist = Infinity;
  835. lineCenters.value.forEach((center, i) => {
  836. const dist = Math.abs(center - mouseY);
  837. if (dist < bestDist) {
  838. bestDist = dist;
  839. best = i;
  840. }
  841. });
  842. return best;
  843. });
  844. let lastTooltipIndex = -1;
  845. const applyTooltip = async (pIdx: number) => {
  846. const pair = qaPairs.value[pIdx];
  847. if (!pair) {
  848. hoveredPair.value = null;
  849. return;
  850. }
  851. hoveredPair.value = pair;
  852. await nextTick();
  853. const tooltip = tooltipRef.value;
  854. const nav = navRef.value;
  855. const container = nav?.parentElement as HTMLElement | null;
  856. if (!tooltip || !nav || !container) return;
  857. const lineCenterY = nav.offsetTop + (lineCenters.value[pIdx] ?? 0) - nav.scrollTop;
  858. const tooltipH = tooltip.offsetHeight;
  859. const maxTop = Math.max(8, container.clientHeight - tooltipH - 8);
  860. tooltipTop.value = Math.max(8, Math.min(maxTop, lineCenterY - tooltipH / 2));
  861. tooltipArrowTop.value = Math.max(6, Math.min(tooltipH - 16, lineCenterY - tooltipTop.value - 5));
  862. };
  863. const onNavMove = (e: MouseEvent) => {
  864. const nav = navRef.value;
  865. if (!nav) return;
  866. // 横线数量变化(新增问答/首次悬浮)时重建坐标
  867. if (lineCenters.value.length !== qaPairs.value.length) updateLineCenters();
  868. navMouseY.value = e.clientY - nav.getBoundingClientRect().top + nav.scrollTop;
  869. const nearest = nearestLineIndex.value;
  870. if (nearest >= 0 && nearest !== lastTooltipIndex) {
  871. lastTooltipIndex = nearest;
  872. applyTooltip(nearest);
  873. }
  874. };
  875. const onNavLeave = () => {
  876. navMouseY.value = null;
  877. hoveredPair.value = null;
  878. lastTooltipIndex = -1;
  879. };
  880. const onNavScroll = () => {
  881. updateLineCenters();
  882. if (lastTooltipIndex >= 0) applyTooltip(lastTooltipIndex);
  883. };
  884. const scrollToPair = (pair: QaPair) => {
  885. activeMessage.value = props.messages[pair.startIndex];
  886. const wrappers = messagesRef.value?.querySelectorAll('.message-wrapper');
  887. const el = wrappers?.[pair.startIndex] as HTMLElement | undefined;
  888. if (!el) return;
  889. el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  890. el.classList.add('nav-located-highlight');
  891. setTimeout(() => el.classList.remove('nav-located-highlight'), 1500);
  892. };
  893. defineExpose({ messagesRef, isAtBottom, forceScrollToBottom, initAskUserAnswers });
  894. </script>
  895. <style scoped lang="less">
  896. .chat-messages-container {
  897. flex: 1;
  898. position: relative;
  899. display: flex;
  900. flex-direction: row;
  901. overflow: hidden;
  902. }
  903. .conversation-nav {
  904. position: relative;
  905. width: 28px;
  906. flex-shrink: 0;
  907. display: flex;
  908. flex-direction: column;
  909. align-items: center;
  910. overflow-y: auto;
  911. padding: 6px 0;
  912. scrollbar-width: none;
  913. &::-webkit-scrollbar {
  914. display: none;
  915. }
  916. .nav-line {
  917. width: 10px;
  918. height: 3px;
  919. border-radius: 2px;
  920. background: rgba(139, 148, 158, 0.4);
  921. margin: 5px 0;
  922. flex-shrink: 0;
  923. cursor: pointer;
  924. transition:
  925. width 0.15s ease-out,
  926. background 0.2s;
  927. &:first-child {
  928. margin-top: auto;
  929. }
  930. &:last-child {
  931. margin-bottom: auto;
  932. }
  933. &.nav-line-hot {
  934. background: #0a84ff;
  935. }
  936. &.active {
  937. background: #0a84ff;
  938. }
  939. }
  940. }
  941. .conversation-tooltip {
  942. position: absolute;
  943. left: 36px;
  944. width: 260px;
  945. background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
  946. border: 1px solid #1a4a6f;
  947. border-radius: 8px;
  948. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  949. padding: 10px 12px;
  950. z-index: 20;
  951. pointer-events: none;
  952. .tooltip-arrow {
  953. position: absolute;
  954. left: -6px;
  955. width: 10px;
  956. height: 10px;
  957. transform: rotate(45deg);
  958. background: #0d2137;
  959. border-left: 1px solid #1a4a6f;
  960. border-bottom: 1px solid #1a4a6f;
  961. }
  962. .tooltip-row {
  963. display: flex;
  964. align-items: flex-start;
  965. gap: 8px;
  966. font-size: 12px;
  967. line-height: 1.5;
  968. & + .tooltip-row {
  969. margin-top: 8px;
  970. padding-top: 8px;
  971. border-top: 1px solid rgba(63, 80, 106, 0.3);
  972. }
  973. .tooltip-label {
  974. flex-shrink: 0;
  975. width: 16px;
  976. height: 16px;
  977. line-height: 16px;
  978. margin-top: 1px;
  979. text-align: center;
  980. border-radius: 3px;
  981. font-size: 11px;
  982. &-q {
  983. color: #79c0ff;
  984. background: rgba(10, 132, 255, 0.15);
  985. }
  986. &-a {
  987. color: #8b949e;
  988. background: rgba(139, 148, 158, 0.15);
  989. }
  990. }
  991. .tooltip-text {
  992. color: #c9d1d9;
  993. word-break: break-word;
  994. }
  995. }
  996. }
  997. .tooltip-fade-enter-active,
  998. .tooltip-fade-leave-active {
  999. transition: opacity 0.15s ease;
  1000. }
  1001. .tooltip-fade-enter-from,
  1002. .tooltip-fade-leave-to {
  1003. opacity: 0;
  1004. }
  1005. .chat-messages {
  1006. flex: 1;
  1007. overflow-y: auto;
  1008. padding: 20px;
  1009. position: relative;
  1010. .welcome-empty {
  1011. position: absolute;
  1012. top: 50%;
  1013. left: 50%;
  1014. transform: translate(-50%, -50%);
  1015. display: flex;
  1016. align-items: center;
  1017. justify-content: center;
  1018. .welcome-img {
  1019. max-width: 300px;
  1020. max-height: 300px;
  1021. object-fit: contain;
  1022. }
  1023. }
  1024. .message-wrapper {
  1025. // margin-bottom: 20px;
  1026. &.nav-located-highlight {
  1027. background: rgba(10, 132, 255, 0.08);
  1028. border-radius: 6px;
  1029. }
  1030. .ai-message {
  1031. display: flex;
  1032. flex-wrap: wrap;
  1033. .message-agent {
  1034. display: flex;
  1035. margin-bottom: 8px;
  1036. opacity: 0;
  1037. }
  1038. .message-body {
  1039. padding: 12px 12px 4px 12px;
  1040. border-radius: 6px;
  1041. width: 100%;
  1042. min-width: 0;
  1043. overflow: hidden;
  1044. /* 思考过程卡片 */
  1045. .thinking-card {
  1046. margin-bottom: 12px;
  1047. overflow: hidden;
  1048. .thinking-duration-bar {
  1049. display: flex;
  1050. align-items: center;
  1051. justify-content: space-between;
  1052. padding: 8px 14px;
  1053. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  1054. cursor: pointer;
  1055. user-select: none;
  1056. transition: background 0.2s;
  1057. &:hover {
  1058. background: rgba(10, 132, 255, 0.12);
  1059. }
  1060. .thinking-duration {
  1061. font-size: 16px;
  1062. color: #8b949e;
  1063. }
  1064. .thinking-arrow {
  1065. color: #8b949e;
  1066. display: flex;
  1067. align-items: center;
  1068. transition: transform 0.2s;
  1069. &.expanded {
  1070. transform: rotate(180deg);
  1071. }
  1072. }
  1073. }
  1074. .thinking-header {
  1075. display: flex;
  1076. align-items: center;
  1077. justify-content: space-between;
  1078. padding: 10px 14px;
  1079. cursor: pointer;
  1080. user-select: none;
  1081. transition: background 0.2s;
  1082. &:hover {
  1083. background: rgba(10, 132, 255, 0.05);
  1084. }
  1085. .thinking-header-left {
  1086. display: flex;
  1087. align-items: center;
  1088. gap: 8px;
  1089. }
  1090. .thinking-arrow {
  1091. display: flex;
  1092. align-items: center;
  1093. transition: transform 0.2s;
  1094. &.expanded {
  1095. transform: rotate(180deg);
  1096. }
  1097. }
  1098. .thinking-icon {
  1099. width: 16px;
  1100. height: 16px;
  1101. background-image: var(--img-chat-thinking-icon);
  1102. background-repeat: no-repeat;
  1103. background-size: 100% 100%;
  1104. }
  1105. .thinking-title {
  1106. font-size: 13px;
  1107. font-weight: 500;
  1108. color: #8b949e;
  1109. }
  1110. .thinking-count {
  1111. font-size: 12px;
  1112. color: #8b949e;
  1113. background: rgba(10, 132, 255, 0.1);
  1114. padding: 1px 6px;
  1115. border-radius: 10px;
  1116. }
  1117. }
  1118. .thinking-content-body {
  1119. margin: 0 14px 8px;
  1120. padding: 8px 12px;
  1121. max-height: 200px;
  1122. overflow-y: auto;
  1123. font-size: 13px;
  1124. line-height: 1.6;
  1125. color: #8b949e;
  1126. white-space: pre-wrap;
  1127. word-break: break-word;
  1128. background: rgba(10, 13, 23, 0.3);
  1129. border-radius: 6px;
  1130. }
  1131. .thinking-body {
  1132. padding: 8px 14px;
  1133. .agent-group {
  1134. margin-bottom: 8px;
  1135. border: 1px solid transparent;
  1136. border-radius: 6px;
  1137. overflow: hidden;
  1138. transition: border-color 0.2s;
  1139. cursor: pointer;
  1140. &:hover {
  1141. border-color: rgba(63, 80, 106, 0.25);
  1142. background: rgba(10, 132, 255, 0.05);
  1143. }
  1144. &:last-child {
  1145. margin-bottom: 0;
  1146. }
  1147. .agent-group-header {
  1148. display: flex;
  1149. align-items: center;
  1150. justify-content: space-between;
  1151. padding: 8px 12px;
  1152. user-select: none;
  1153. .agent-group-header-left {
  1154. display: flex;
  1155. align-items: center;
  1156. gap: 8px;
  1157. }
  1158. .agent-group-icon {
  1159. width: 14px;
  1160. height: 14px;
  1161. background-image: var(--img-chat-agent-icon);
  1162. background-repeat: no-repeat;
  1163. background-size: 100% 100%;
  1164. flex-shrink: 0;
  1165. }
  1166. .agent-group-name {
  1167. font-size: 13px;
  1168. font-weight: 500;
  1169. color: #fff;
  1170. }
  1171. .agent-group-latest {
  1172. font-size: 12px;
  1173. color: #8b949e;
  1174. margin-left: 8px;
  1175. overflow: hidden;
  1176. text-overflow: ellipsis;
  1177. white-space: nowrap;
  1178. flex: 1;
  1179. min-width: 0;
  1180. }
  1181. .agent-arrow {
  1182. margin-left: 8px;
  1183. flex-shrink: 0;
  1184. display: flex;
  1185. align-items: center;
  1186. }
  1187. }
  1188. }
  1189. }
  1190. }
  1191. .collapse-enter-active,
  1192. .collapse-leave-active {
  1193. transition: all 0.25s ease;
  1194. overflow: hidden;
  1195. }
  1196. .collapse-enter-from,
  1197. .collapse-leave-to {
  1198. opacity: 0;
  1199. max-height: 0;
  1200. padding-top: 0;
  1201. padding-bottom: 0;
  1202. margin-top: 0;
  1203. margin-bottom: 0;
  1204. }
  1205. .collapse-enter-to,
  1206. .collapse-leave-from {
  1207. opacity: 1;
  1208. max-height: 500px;
  1209. }
  1210. .message-text {
  1211. font-size: 14px;
  1212. font-weight: 400;
  1213. line-height: 1.6;
  1214. color: #e0e6ed;
  1215. word-break: break-word;
  1216. overflow-wrap: break-word;
  1217. :deep(.session-ref-icon) {
  1218. width: 14px;
  1219. height: 14px;
  1220. vertical-align: -2px;
  1221. }
  1222. :deep(h1),
  1223. :deep(h2),
  1224. :deep(h3),
  1225. :deep(h4),
  1226. :deep(h5),
  1227. :deep(h6) {
  1228. color: #fff;
  1229. }
  1230. :deep(p) {
  1231. margin: 0;
  1232. }
  1233. :deep(code) {
  1234. background: rgba(10, 132, 255, 0.1);
  1235. padding: 1px 4px;
  1236. border-radius: 3px;
  1237. font-size: 12px;
  1238. }
  1239. :deep(pre) {
  1240. background: rgba(10, 132, 255, 0.1);
  1241. padding: 8px;
  1242. border-radius: 4px;
  1243. overflow-x: auto;
  1244. margin: 6px 0;
  1245. white-space: pre-wrap;
  1246. }
  1247. :deep(pre code) {
  1248. background: transparent;
  1249. padding: 0;
  1250. }
  1251. :deep(ul),
  1252. :deep(ol) {
  1253. padding-left: 18px;
  1254. margin: 4px 0;
  1255. }
  1256. :deep(blockquote) {
  1257. border-left: 3px solid rgba(10, 132, 255, 0.3);
  1258. padding-left: 10px;
  1259. margin: 6px 0;
  1260. color: #8b949e;
  1261. }
  1262. :deep(table) {
  1263. border-collapse: collapse;
  1264. margin: 6px 0;
  1265. }
  1266. :deep(th),
  1267. :deep(td) {
  1268. border: 1px solid rgba(63, 80, 106, 0.5);
  1269. padding: 4px 8px;
  1270. }
  1271. :deep(th) {
  1272. background: rgba(10, 132, 255, 0.1);
  1273. }
  1274. :deep(strong) {
  1275. font-weight: 600;
  1276. }
  1277. :deep(.markdown-table-wrapper) {
  1278. position: relative;
  1279. margin: 6px 0;
  1280. max-width: 100%;
  1281. overflow-x: auto;
  1282. .table-actions {
  1283. position: absolute;
  1284. top: 4px;
  1285. right: 4px;
  1286. display: flex;
  1287. gap: 2px;
  1288. opacity: 0;
  1289. transition: opacity 0.2s;
  1290. z-index: 1;
  1291. }
  1292. &:hover .table-actions {
  1293. opacity: 1;
  1294. }
  1295. .table-action-btn {
  1296. width: 26px;
  1297. height: 26px;
  1298. display: flex;
  1299. align-items: center;
  1300. justify-content: center;
  1301. border-radius: 4px;
  1302. color: #8b949e;
  1303. cursor: pointer;
  1304. background: rgba(22, 27, 34, 0.8);
  1305. backdrop-filter: blur(4px);
  1306. transition: all 0.2s;
  1307. &:hover {
  1308. color: #e0e6ed;
  1309. background: rgba(10, 132, 255, 0.3);
  1310. }
  1311. }
  1312. }
  1313. }
  1314. }
  1315. .loading-dots {
  1316. display: flex;
  1317. align-items: center;
  1318. gap: 6px;
  1319. padding: 4px 0;
  1320. min-height: 16px;
  1321. .dot {
  1322. width: 8px;
  1323. height: 8px;
  1324. border-radius: 50%;
  1325. background: rgba(10, 132, 255, 0.4);
  1326. animation: dot-bounce 1.4s ease-in-out infinite both;
  1327. &:nth-child(1) {
  1328. animation-delay: 0s;
  1329. }
  1330. &:nth-child(2) {
  1331. animation-delay: 0.2s;
  1332. }
  1333. &:nth-child(3) {
  1334. animation-delay: 0.4s;
  1335. }
  1336. }
  1337. }
  1338. @keyframes dot-bounce {
  1339. 0%,
  1340. 80%,
  1341. 100% {
  1342. transform: scale(0.6);
  1343. opacity: 0.4;
  1344. }
  1345. 40% {
  1346. transform: scale(1);
  1347. opacity: 1;
  1348. }
  1349. }
  1350. .approval-card {
  1351. display: flex;
  1352. align-items: flex-start;
  1353. gap: 12px;
  1354. padding: 14px 16px;
  1355. margin-top: 12px;
  1356. background: #002f4a;
  1357. border: 1px solid #005482;
  1358. border-radius: 8px;
  1359. .approval-icon {
  1360. width: 36px;
  1361. height: 36px;
  1362. background-image: var(--img-chat-session-approve-icon);
  1363. background-repeat: no-repeat;
  1364. background-size: 100% 100%;
  1365. flex-shrink: 0;
  1366. }
  1367. .approval-content {
  1368. flex: 1;
  1369. .approval-title {
  1370. font-size: 14px;
  1371. font-weight: 600;
  1372. color: #e0e6ed;
  1373. margin-bottom: 6px;
  1374. }
  1375. .approval-plan {
  1376. margin-bottom: 14px;
  1377. padding: 10px 12px;
  1378. background: rgba(10, 132, 255, 0.08);
  1379. border: 1px solid rgba(10, 132, 255, 0.2);
  1380. border-radius: 6px;
  1381. .approval-plan-label {
  1382. font-size: 12px;
  1383. color: #0a84ff;
  1384. font-weight: 600;
  1385. margin-bottom: 6px;
  1386. }
  1387. .approval-plan-content {
  1388. font-size: 13px;
  1389. color: #c9d1d9;
  1390. line-height: 1.6;
  1391. white-space: pre-wrap;
  1392. }
  1393. }
  1394. .ask-user-question {
  1395. margin-bottom: 14px;
  1396. .ask-user-question-text {
  1397. font-size: 13px;
  1398. color: #c9d1d9;
  1399. margin-bottom: 8px;
  1400. line-height: 1.5;
  1401. .ask-user-required {
  1402. color: #f85149;
  1403. margin-left: 2px;
  1404. }
  1405. }
  1406. .ask-user-choices {
  1407. display: flex;
  1408. flex-direction: column;
  1409. gap: 6px;
  1410. .ask-user-choice {
  1411. display: flex;
  1412. align-items: center;
  1413. padding: 8px 12px;
  1414. background: rgba(255, 255, 255, 0.04);
  1415. border: 1px solid rgba(63, 80, 106, 0.4);
  1416. border-radius: 6px;
  1417. cursor: pointer;
  1418. font-size: 13px;
  1419. color: #c9d1d9;
  1420. transition: all 0.2s;
  1421. &:hover {
  1422. background: rgba(10, 132, 255, 0.1);
  1423. border-color: rgba(10, 132, 255, 0.4);
  1424. }
  1425. &.selected {
  1426. background: rgba(10, 132, 255, 0.15);
  1427. border-color: #0a84ff;
  1428. color: #fff;
  1429. }
  1430. }
  1431. .ask-user-other-input {
  1432. margin-top: 4px;
  1433. padding: 8px 12px;
  1434. font-size: 13px;
  1435. color: #e0e6ed;
  1436. background: rgba(255, 255, 255, 0.06);
  1437. border: 1px solid rgba(63, 80, 106, 0.5);
  1438. border-radius: 6px;
  1439. outline: none;
  1440. width: 100%;
  1441. &:focus {
  1442. border-color: #0a84ff;
  1443. background: rgba(10, 132, 255, 0.08);
  1444. }
  1445. &::placeholder {
  1446. color: #6e7681;
  1447. }
  1448. }
  1449. }
  1450. .ask-user-text {
  1451. .ask-user-textarea {
  1452. width: 100%;
  1453. min-height: 60px;
  1454. padding: 8px 12px;
  1455. font-size: 13px;
  1456. color: #e0e6ed;
  1457. background: rgba(255, 255, 255, 0.06);
  1458. border: 1px solid rgba(63, 80, 106, 0.5);
  1459. border-radius: 6px;
  1460. outline: none;
  1461. resize: vertical;
  1462. font-family: inherit;
  1463. &:focus {
  1464. border-color: #0a84ff;
  1465. background: rgba(10, 132, 255, 0.08);
  1466. }
  1467. &::placeholder {
  1468. color: #6e7681;
  1469. }
  1470. }
  1471. }
  1472. }
  1473. .ask-user-stepper {
  1474. display: flex;
  1475. align-items: center;
  1476. gap: 6px;
  1477. margin-bottom: 14px;
  1478. .ask-user-step-dot {
  1479. width: 8px;
  1480. height: 8px;
  1481. border-radius: 50%;
  1482. background: rgba(63, 80, 106, 0.5);
  1483. cursor: pointer;
  1484. transition: all 0.2s;
  1485. &.active {
  1486. background: #0a84ff;
  1487. width: 20px;
  1488. border-radius: 4px;
  1489. }
  1490. &.completed {
  1491. background: rgba(10, 132, 255, 0.5);
  1492. }
  1493. }
  1494. .ask-user-step-text {
  1495. margin-left: 6px;
  1496. font-size: 12px;
  1497. color: #6e7681;
  1498. }
  1499. }
  1500. .approval-actions {
  1501. display: flex;
  1502. gap: 10px;
  1503. .approval-btn {
  1504. padding: 6px 20px;
  1505. border-radius: 6px;
  1506. font-size: 13px;
  1507. font-weight: 500;
  1508. cursor: pointer;
  1509. border: none;
  1510. transition: all 0.2s;
  1511. &.approve-btn {
  1512. background: #0a84ff;
  1513. color: #fff;
  1514. &:hover:not(:disabled) {
  1515. background: #3a9bfd;
  1516. }
  1517. &:disabled {
  1518. opacity: 0.4;
  1519. cursor: not-allowed;
  1520. }
  1521. }
  1522. &.nav-btn {
  1523. background: rgba(255, 255, 255, 0.08);
  1524. color: #c9d1d9;
  1525. border: 1px solid rgba(63, 80, 106, 0.5);
  1526. &:hover {
  1527. background: rgba(255, 255, 255, 0.15);
  1528. }
  1529. }
  1530. &.reject-btn {
  1531. background: rgba(255, 255, 255, 0.08);
  1532. color: #e0e6ed;
  1533. border: 1px solid rgba(63, 80, 106, 0.5);
  1534. &:hover {
  1535. background: rgba(255, 255, 255, 0.15);
  1536. }
  1537. }
  1538. }
  1539. }
  1540. }
  1541. }
  1542. .citations-card {
  1543. margin-bottom: 12px;
  1544. overflow: hidden;
  1545. .citations-header {
  1546. display: flex;
  1547. align-items: center;
  1548. justify-content: space-between;
  1549. padding: 8px 14px;
  1550. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  1551. cursor: pointer;
  1552. user-select: none;
  1553. transition: background 0.2s;
  1554. &:hover {
  1555. background: rgba(10, 132, 255, 0.08);
  1556. }
  1557. &.citations-header-simple {
  1558. cursor: default;
  1559. &:hover {
  1560. background: transparent;
  1561. }
  1562. }
  1563. .citations-header-left {
  1564. display: flex;
  1565. align-items: center;
  1566. gap: 8px;
  1567. }
  1568. .citations-title {
  1569. font-size: 13px;
  1570. font-weight: 500;
  1571. color: #8b949e;
  1572. }
  1573. .citations-count {
  1574. font-size: 12px;
  1575. color: #8b949e;
  1576. background: rgba(10, 132, 255, 0.1);
  1577. padding: 1px 6px;
  1578. border-radius: 10px;
  1579. }
  1580. .thinking-arrow {
  1581. display: flex;
  1582. align-items: center;
  1583. transition: transform 0.2s;
  1584. &.expanded {
  1585. transform: rotate(180deg);
  1586. }
  1587. }
  1588. }
  1589. .citations-body {
  1590. padding: 4px 14px;
  1591. .citation-item {
  1592. padding: 6px 0;
  1593. border-bottom: 1px solid rgba(63, 80, 106, 0.15);
  1594. &:last-child {
  1595. border-bottom: none;
  1596. }
  1597. &.citation-highlight {
  1598. background: rgba(10, 132, 255, 0.08);
  1599. border-radius: 4px;
  1600. padding: 6px 8px;
  1601. margin: 0 -8px;
  1602. }
  1603. .citation-item-row {
  1604. display: flex;
  1605. align-items: center;
  1606. gap: 8px;
  1607. line-height: 1.5;
  1608. .citation-badge {
  1609. font-size: 12px;
  1610. color: #79c0ff;
  1611. font-family: monospace;
  1612. background: rgba(10, 132, 255, 0.1);
  1613. padding: 1px 5px;
  1614. border-radius: 3px;
  1615. white-space: nowrap;
  1616. flex-shrink: 0;
  1617. }
  1618. .citation-badge-knowledge_base {
  1619. color: #3fb950;
  1620. background: rgba(46, 160, 67, 0.1);
  1621. }
  1622. .citation-badge-web {
  1623. color: #d29922;
  1624. background: rgba(210, 153, 34, 0.1);
  1625. }
  1626. .citation-badge-calc {
  1627. color: #a371f7;
  1628. background: rgba(163, 113, 247, 0.1);
  1629. }
  1630. .citation-title-text {
  1631. font-size: 13px;
  1632. color: #b0b8c4;
  1633. overflow: hidden;
  1634. text-overflow: ellipsis;
  1635. white-space: nowrap;
  1636. min-width: 0;
  1637. }
  1638. .citation-link {
  1639. font-size: 12px;
  1640. color: #0a84ff;
  1641. text-decoration: none;
  1642. white-space: nowrap;
  1643. flex-shrink: 0;
  1644. }
  1645. }
  1646. .citation-item-attrs {
  1647. display: flex;
  1648. align-items: center;
  1649. flex-wrap: wrap;
  1650. gap: 4px 10px;
  1651. padding-left: 0;
  1652. margin-top: 2px;
  1653. .citation-attr {
  1654. font-size: 12px;
  1655. color: #8b949e;
  1656. white-space: nowrap;
  1657. }
  1658. .citation-low-score {
  1659. color: #d29922;
  1660. }
  1661. }
  1662. .citation-snippet {
  1663. margin-top: 4px;
  1664. font-size: 12px;
  1665. color: #8b949e;
  1666. line-height: 1.5;
  1667. overflow: hidden;
  1668. text-overflow: ellipsis;
  1669. white-space: nowrap;
  1670. }
  1671. }
  1672. }
  1673. }
  1674. .download-file-cards {
  1675. display: flex;
  1676. flex-direction: column;
  1677. gap: 8px;
  1678. margin-top: 12px;
  1679. .download-file-card {
  1680. display: flex;
  1681. align-items: center;
  1682. justify-content: space-between;
  1683. padding: 12px 16px;
  1684. background: linear-gradient(135deg, rgba(0, 47, 74, 0.8) 0%, rgba(0, 60, 90, 0.6) 100%);
  1685. border: 1px solid rgba(0, 84, 130, 0.6);
  1686. border-radius: 10px;
  1687. cursor: pointer;
  1688. transition: all 0.2s ease;
  1689. &:hover {
  1690. border-color: rgba(10, 132, 255, 0.4);
  1691. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
  1692. }
  1693. .file-card-info {
  1694. display: flex;
  1695. align-items: center;
  1696. gap: 12px;
  1697. min-width: 0;
  1698. flex: 1;
  1699. .file-card-icon {
  1700. width: 36px;
  1701. height: 36px;
  1702. background-image: var(--img-chat-download-icon);
  1703. background-repeat: no-repeat;
  1704. background-size: 100% 100%;
  1705. flex-shrink: 0;
  1706. opacity: 0.9;
  1707. }
  1708. .file-card-text {
  1709. display: flex;
  1710. flex-direction: column;
  1711. gap: 2px;
  1712. min-width: 0;
  1713. .file-card-name {
  1714. font-size: 13px;
  1715. font-weight: 500;
  1716. color: #e0e6ed;
  1717. overflow: hidden;
  1718. text-overflow: ellipsis;
  1719. white-space: nowrap;
  1720. }
  1721. .file-card-hint {
  1722. font-size: 11px;
  1723. color: #6e7681;
  1724. }
  1725. }
  1726. }
  1727. .file-card-actions {
  1728. display: flex;
  1729. gap: 8px;
  1730. flex-shrink: 0;
  1731. .file-action-btn {
  1732. display: inline-flex;
  1733. align-items: center;
  1734. gap: 5px;
  1735. padding: 6px 14px;
  1736. border-radius: 7px;
  1737. font-size: 12px;
  1738. font-weight: 500;
  1739. cursor: pointer;
  1740. border: none;
  1741. transition: all 0.2s ease;
  1742. white-space: nowrap;
  1743. &.download-btn {
  1744. background: rgba(255, 255, 255, 0.08);
  1745. color: #c9d1d9;
  1746. border: 1px solid rgba(63, 80, 106, 0.5);
  1747. &:hover {
  1748. background: rgba(255, 255, 255, 0.14);
  1749. color: #e0e6ed;
  1750. border-color: rgba(63, 80, 106, 0.8);
  1751. }
  1752. }
  1753. &.edit-btn {
  1754. background: linear-gradient(135deg, #0a84ff 0%, #0066cc 100%);
  1755. color: #fff;
  1756. border: 1px solid rgba(10, 132, 255, 0.5);
  1757. box-shadow: 0 2px 6px rgba(10, 132, 255, 0.2);
  1758. &:hover {
  1759. background: linear-gradient(135deg, #3a9bfd 0%, #0a84ff 100%);
  1760. box-shadow: 0 3px 10px rgba(10, 132, 255, 0.35);
  1761. }
  1762. }
  1763. }
  1764. }
  1765. }
  1766. }
  1767. }
  1768. .user-message {
  1769. display: flex;
  1770. flex-wrap: wrap;
  1771. justify-content: flex-end;
  1772. align-items: flex-start;
  1773. .message-agent {
  1774. display: flex;
  1775. margin-bottom: 8px;
  1776. margin-left: 5px;
  1777. }
  1778. .message-body {
  1779. background: rgba(10, 132, 255, 0.15);
  1780. padding: 12px;
  1781. border-radius: 6px 0 6px 6px;
  1782. max-width: 700px;
  1783. min-width: 0;
  1784. overflow: hidden;
  1785. .message-text {
  1786. font-size: 14px;
  1787. font-weight: 400;
  1788. line-height: 1.6;
  1789. color: #e0e6ed;
  1790. word-break: break-word;
  1791. overflow-wrap: break-word;
  1792. :deep(.session-ref-icon) {
  1793. width: 14px;
  1794. height: 14px;
  1795. vertical-align: -2px;
  1796. }
  1797. :deep(h1, h2, h3, h4, h5, h6) {
  1798. color: #fff;
  1799. }
  1800. :deep(p) {
  1801. margin: 0;
  1802. }
  1803. :deep(code) {
  1804. background: rgba(0, 0, 0, 0.2);
  1805. padding: 1px 4px;
  1806. border-radius: 3px;
  1807. font-size: 12px;
  1808. }
  1809. :deep(pre) {
  1810. background: rgba(0, 0, 0, 0.2);
  1811. padding: 8px;
  1812. border-radius: 4px;
  1813. overflow-x: auto;
  1814. margin: 6px 0;
  1815. white-space: pre-wrap;
  1816. }
  1817. :deep(pre code) {
  1818. background: transparent;
  1819. padding: 0;
  1820. }
  1821. }
  1822. .attached-file-card {
  1823. margin-top: 10px;
  1824. background: rgba(255, 255, 255, 0.08);
  1825. border-radius: 6px;
  1826. display: flex;
  1827. align-items: center;
  1828. gap: 10px;
  1829. padding: 8px;
  1830. cursor: pointer;
  1831. transition: all 0.3s;
  1832. &:hover {
  1833. background: rgba(255, 255, 255, 0.15);
  1834. }
  1835. .file-icon-card {
  1836. width: 32px;
  1837. height: 32px;
  1838. background-image: var(--img-chat-attach-icon);
  1839. background-repeat: no-repeat;
  1840. background-size: 100% 100%;
  1841. flex-shrink: 0;
  1842. }
  1843. .file-info-card {
  1844. flex: 1;
  1845. min-width: 0;
  1846. .file-name-card {
  1847. font-size: 12px;
  1848. font-weight: 400;
  1849. line-height: 1.4;
  1850. color: #e0e6ed;
  1851. overflow: hidden;
  1852. text-overflow: ellipsis;
  1853. white-space: nowrap;
  1854. margin-bottom: 2px;
  1855. }
  1856. .file-size-card {
  1857. font-size: 11px;
  1858. font-weight: 400;
  1859. line-height: 1.3;
  1860. color: #8b949e;
  1861. }
  1862. }
  1863. }
  1864. }
  1865. }
  1866. .ai-message,
  1867. .user-message {
  1868. .message-actions {
  1869. display: flex;
  1870. align-items: center;
  1871. gap: 8px;
  1872. width: 100%;
  1873. opacity: 0;
  1874. transition: opacity 0.2s;
  1875. .action-btn {
  1876. width: 26px;
  1877. height: 26px;
  1878. display: flex;
  1879. align-items: center;
  1880. justify-content: center;
  1881. border-radius: 4px;
  1882. color: #8b949e;
  1883. cursor: pointer;
  1884. transition: all 0.2s;
  1885. &:hover {
  1886. color: #e0e6ed;
  1887. background: rgba(10, 132, 255, 0.15);
  1888. }
  1889. }
  1890. .action-time {
  1891. font-size: 12px;
  1892. color: #8b949e;
  1893. }
  1894. }
  1895. &:hover .message-actions {
  1896. opacity: 1;
  1897. }
  1898. }
  1899. .ai-message .message-actions {
  1900. padding: 0 0 0 10px;
  1901. }
  1902. .user-message .message-actions {
  1903. padding: 4px 0 0 0;
  1904. justify-content: flex-end;
  1905. }
  1906. }
  1907. }
  1908. .scroll-to-bottom {
  1909. position: absolute;
  1910. bottom: 20px;
  1911. left: 50%;
  1912. transform: translateX(-50%);
  1913. z-index: 10;
  1914. .scroll-to-bottom-icon {
  1915. width: 40px;
  1916. height: 40px;
  1917. border-radius: 50%;
  1918. background: rgba(255, 255, 255, 0.15);
  1919. backdrop-filter: blur(4px);
  1920. background-image: var(--img-chat-scroll-bottom);
  1921. background-repeat: no-repeat;
  1922. background-size: 20px 20px;
  1923. background-position: center;
  1924. cursor: pointer;
  1925. transition: all 0.2s;
  1926. &:hover {
  1927. background-color: rgba(255, 255, 255, 0.25);
  1928. }
  1929. }
  1930. }
  1931. </style>