index.vue 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008
  1. <template>
  2. <div class="chat-container">
  3. <!-- 左侧边栏 -->
  4. <ChatSidebar
  5. :conversations="conversations"
  6. :active-id="activeConversationId"
  7. :has-more="hasMore"
  8. :is-loading-more="isLoadingMore"
  9. @new-chat="handleNewChat"
  10. @select-conversation="handleSelectConversation"
  11. @conv-command="handleConvCommand"
  12. @load-more="loadMoreConversations"
  13. />
  14. <!-- 右侧聊天区域 -->
  15. <div class="chat-main">
  16. <ChatHeader :title="activeConversationTitle">
  17. <template #actions>
  18. <!-- 展示/隐藏表单 -->
  19. <el-popover
  20. :visible="showForm"
  21. placement="bottom"
  22. trigger="manual"
  23. :width="520"
  24. popper-class="workflow-form-popper"
  25. >
  26. <div v-loading="workflowFormLoading" class="workflow-form">
  27. <div class="workflow-form__title">{{ t('pages.chat.workflowChatFormTitle') }}</div>
  28. <InputTab
  29. :start-node="workflowFormStartNode"
  30. :visible-variables="workflowVisibleVariables"
  31. :input-values="workflowInputValues"
  32. :json-drafts="workflowJsonDrafts"
  33. :validation-errors="workflowValidationErrors"
  34. :is-running="isLoading"
  35. :show-action-bar="false"
  36. :pane-style="workflowFormPaneStyle"
  37. />
  38. </div>
  39. <template #reference>
  40. <IconButton
  41. v-if="hasVisibleForm"
  42. icon="lucide:sliders-horizontal"
  43. :type="showForm ? 'primary' : 'default'"
  44. :icon-color="showForm ? '#fff' : undefined"
  45. height="18"
  46. width="18"
  47. @click="showForm = !showForm"
  48. size="small"
  49. square
  50. />
  51. </template>
  52. </el-popover>
  53. <!-- 创建连接 -->
  54. <el-button v-if="!isPresetMode" @click="shareConversation">
  55. <span class="flex items-center gap-4px">
  56. <Icon icon="lucide:share-2" />
  57. <span>{{ t('pages.chat.generateLink') }}</span>
  58. </span>
  59. </el-button>
  60. </template>
  61. </ChatHeader>
  62. <MessageList
  63. ref="messageListRef"
  64. :messages="messages"
  65. :loading="isLoading"
  66. @retry="handleRetry"
  67. @add-to-kb="handleAddKb"
  68. @card-submit="handleSend"
  69. >
  70. <template #message-extra="{ item }">
  71. <div class="min-w-400px">
  72. <WorkflowTraceBubble v-if="item.workflowTraceVisible" :message="item" />
  73. </div>
  74. </template>
  75. <div
  76. v-if="settingsDraft.type === 'agent' && agentPromptsItems?.length"
  77. style="margin-top: 12px; width: 80%"
  78. >
  79. <Prompts
  80. :title="t('pages.chat.quickStartTitle')"
  81. :items="agentPromptsItems"
  82. @item-click="handlePromptItemClick"
  83. wrap
  84. />
  85. </div>
  86. </MessageList>
  87. <!-- 对话框 -->
  88. <ChatInput
  89. v-model="senderValue"
  90. ref="chatInputRef"
  91. :loading="isLoading"
  92. :attachments="currentAttachments"
  93. @submit="handleSend"
  94. @cancel="handleCancel"
  95. >
  96. <template #header v-if="!isPresetMode && settingsDraft.type !== 'flow'">
  97. <div class="px-8px py-12px flex items-center gap-12px">
  98. <div class="flex items-center gap-4px">
  99. <!-- <div class="title">知识库:</div> -->
  100. <el-select
  101. class="w-180px!"
  102. :placeholder="t('pages.chat.selectKnowledgeBasePlaceholder')"
  103. v-model="settingsDraft.knowledgeBaseIds"
  104. :options="knowledgeBaseOptions"
  105. @change="fetchKnowledgeOptions"
  106. multiple
  107. />
  108. </div>
  109. <div class="flex items-center gap-4px">
  110. <!-- <div class="title">知识:</div> -->
  111. <el-select
  112. class="w-180px!"
  113. :placeholder="t('pages.chat.selectKnowledgePlaceholder')"
  114. v-model="settingsDraft.knowledgeIds"
  115. :options="knowledgeOptions"
  116. multiple
  117. />
  118. </div>
  119. </div>
  120. </template>
  121. <template #prefix-extra>
  122. <!-- 智能体与智能体编排 -->
  123. <el-cascader
  124. v-if="!isPresetMode"
  125. v-model="targetSelection"
  126. :options="getTargetOptions"
  127. :placeholder="t('pages.chat.selectPlaceholder')"
  128. popper-class="target-popper"
  129. class="chat-target-cascader"
  130. @change="handleChangeTarget"
  131. filterable
  132. />
  133. <!-- 附件 -->
  134. <el-badge :value="currentAttachments.length" :hidden="!currentAttachments.length">
  135. <el-button
  136. v-if="showImageUploadButton"
  137. round
  138. plain
  139. color="#626aef"
  140. @click="imageUploadDialogVisible = true"
  141. >
  142. <el-icon>
  143. <PictureFilled />
  144. </el-icon>
  145. </el-button>
  146. </el-badge>
  147. </template>
  148. <template #action>
  149. <el-select
  150. v-if="!isPresetMode && settingsDraft.type !== 'flow'"
  151. :placeholder="t('pages.chat.selectModelPlaceholder')"
  152. placement="top"
  153. v-model="settingsDraft.summaryModelId"
  154. :options="modelOptions"
  155. class="w-120px!"
  156. />
  157. </template>
  158. </ChatInput>
  159. </div>
  160. <!-- 重命名对话弹窗 -->
  161. <el-dialog
  162. v-model="renameDialogVisible"
  163. :title="t('pages.chat.renameDialogTitle')"
  164. width="400px"
  165. >
  166. <el-input v-model="renameInput" :placeholder="t('pages.chat.renamePlaceholder')" />
  167. <template #footer>
  168. <el-button @click="renameDialogVisible = false">{{ t('pages.chat.cancel') }}</el-button>
  169. <el-button type="primary" @click="handleRename">{{ t('common.confirm') }}</el-button>
  170. </template>
  171. </el-dialog>
  172. <!-- 图片上传 -->
  173. <el-dialog
  174. v-model="imageUploadDialogVisible"
  175. :title="t('pages.chat.imageUploadTitle')"
  176. width="560px"
  177. >
  178. <FileUploadInput
  179. v-model="currentAttachments"
  180. :multiple="true"
  181. :allow-link-input="false"
  182. :tip="t('pages.chat.imageUploadTip')"
  183. style="width: 100%"
  184. />
  185. <template #footer>
  186. <el-button @click="imageUploadDialogVisible = false">{{
  187. t('pages.chat.cancel')
  188. }}</el-button>
  189. <el-button type="primary" @click="imageUploadDialogVisible = false">{{
  190. t('pages.chat.imageUploadDone')
  191. }}</el-button>
  192. </template>
  193. </el-dialog>
  194. <el-dialog v-model="shareDialogVisible" :title="t('pages.chat.shareDialogTitle')" width="620px">
  195. <div class="share-dialog">
  196. <div class="share-dialog__tip">
  197. {{ t('pages.chat.shareDialogTip') }}
  198. </div>
  199. <el-input v-model="shareUrl" type="textarea" :rows="4" readonly />
  200. </div>
  201. <template #footer>
  202. <el-button @click="shareDialogVisible = false">{{ t('common.close') }}</el-button>
  203. <el-button type="primary" @click="copyShareUrl">{{
  204. t('pages.chat.copyShareLink')
  205. }}</el-button>
  206. </template>
  207. </el-dialog>
  208. </div>
  209. <AddKbModal ref="addkbRef" />
  210. </template>
  211. <script setup lang="ts">
  212. import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
  213. import { useRoute } from 'vue-router'
  214. import { ElMessage, ElMessageBox } from 'element-plus'
  215. import { PictureFilled } from '@element-plus/icons-vue'
  216. import { useI18n } from '@/composables/useI18n'
  217. import FileUploadInput from '@/features/fileUpload/FileUploadInput.vue'
  218. import InputTab from '@/features/RunWorkflow/components/InputTab.vue'
  219. import { buildExecuteParams, createEmptyValue } from '@/features/RunWorkflow/utils'
  220. import { useChatStream } from './composables/useChatStream'
  221. import { Prompts } from 'vue-element-plus-x'
  222. import {
  223. buildChatRequestBody,
  224. createSession,
  225. deleteSession,
  226. getAgentInfo,
  227. getAgentOptions,
  228. getFlowOptions,
  229. getChatUrl,
  230. getKnowledgeBaseOptions,
  231. getModelOptions,
  232. getSessionList,
  233. getSessionMessages,
  234. updateSessionName,
  235. type ChatOptionItem
  236. } from './api/chat.api'
  237. import ChatHeader from './components/ChatHeader.vue'
  238. import ChatInput from '@/components/Chat/ChatInput.vue'
  239. import ChatSidebar from './components/ChatSidebar.vue'
  240. import MessageList from '@/components/Chat/MessageList.vue'
  241. import AddKbModal from './components/AddKbModal.vue'
  242. import WorkflowTraceBubble from '@/features/ChatDrawer/WorkflowTraceBubble.vue'
  243. import { agent, agentApplication, knowledge, aiChat } from '@repo/api-service'
  244. import type {
  245. BubbleMessage,
  246. ChatReference,
  247. ChatSseMessage,
  248. ChatTargetConfig,
  249. ChatTargetType,
  250. ChatToolCall,
  251. ChatToolResult,
  252. Conversation
  253. } from './types'
  254. import type { PromptsItemsProps } from 'vue-element-plus-x/types/Prompts'
  255. import type { WorkflowUploadFile } from '@/features/fileUpload/shared'
  256. import type { CSSProperties } from 'vue'
  257. import type { IWorkflowNode } from '@repo/workflow'
  258. import type { FileType, FormType, StartVariable } from '@/nodes/src/start'
  259. import type { RunnerNodeState } from '@/store/modules/runner.store'
  260. import { Icon, IconButton } from '@repo/ui'
  261. const { t } = useI18n()
  262. const { isLoading, cancelRequest, streamChat } = useChatStream()
  263. const route = useRoute()
  264. const conversations = ref<Conversation[]>([])
  265. const sessionConfigMap = reactive<Record<string, ChatTargetConfig>>({})
  266. const activeConversationId = ref('')
  267. const activeTargetType = ref<ChatTargetType>('agent')
  268. const imageUploadDialogVisible = ref(false)
  269. const currentAttachments = ref<WorkflowUploadFile[]>([])
  270. const allowImageUpload = ref(false)
  271. const agentOptions = ref<ChatOptionItem[]>([])
  272. const flowOptions = ref<ChatOptionItem[]>([])
  273. const targetSelection = ref<[ChatTargetType, string]>(['agent', ''])
  274. const knowledgeBaseOptions = ref<ChatOptionItem[]>([])
  275. const knowledgeOptions = ref<{ label: string; value: string }[]>([])
  276. const modelOptions = ref<ChatOptionItem[]>([])
  277. const currentPage = ref(1)
  278. const pageSize = ref(20)
  279. const hasMore = ref(true)
  280. const isLoadingMore = ref(false)
  281. const messages = ref<BubbleMessage[]>([])
  282. const renameDialogVisible = ref(false)
  283. const renameInput = ref('')
  284. const renamingConvId = ref('')
  285. const senderValue = ref('')
  286. const messageListRef = ref()
  287. const activeStreamToken = ref('')
  288. const chatInputRef = ref<InstanceType<typeof ChatInput>>()
  289. const agentPromptsItems = ref<PromptsItemsProps[]>()
  290. const addkbRef = ref<InstanceType<typeof AddKbModal>>()
  291. const shareDialogVisible = ref(false)
  292. const shareUrl = ref('')
  293. const presetConfig = ref<ChatTargetConfig | null>(null)
  294. const isPresetMode = computed(() => !!presetConfig.value)
  295. const showForm = ref(false)
  296. const workflowFormLoading = ref(false)
  297. const workflowStartVariables = ref<StartVariable[]>([])
  298. const workflowInputValues = reactive<Record<string, any>>({})
  299. const workflowJsonDrafts = reactive<Record<string, string>>({})
  300. const workflowValidationErrors = reactive<Record<string, string>>({})
  301. const workflowFormPaneStyle: CSSProperties = {
  302. marginTop: '0',
  303. padding: '0'
  304. }
  305. let scrollToBottomPending = false
  306. // 联动下拉列表
  307. const getTargetOptions = computed(() => {
  308. return [
  309. {
  310. label: t('pages.chat.settingsAgent'),
  311. value: 'agent',
  312. children: agentOptions.value
  313. },
  314. {
  315. label: t('sidebar.menu.orchestration'),
  316. value: 'flow',
  317. children: flowOptions.value
  318. }
  319. ]
  320. })
  321. /**
  322. * 解析当前级联选择器的显示信息(类型标签、名称、Tag 颜色)
  323. */
  324. const selectionDisplayInfo = computed(() => {
  325. const [type, id] = targetSelection.value
  326. if (!type || !id) return null
  327. const isAgent = type === 'agent'
  328. const typeLabel = isAgent ? t('pages.chat.settingsAgent') : t('sidebar.menu.orchestration')
  329. const options = isAgent ? agentOptions.value : flowOptions.value
  330. const matched = options.find((o) => o.value === id)
  331. return {
  332. typeLabel,
  333. name: matched?.label || id,
  334. tagType: isAgent ? 'success' : 'warning'
  335. }
  336. })
  337. const scrollToBottom = async () => {
  338. if (scrollToBottomPending) return
  339. scrollToBottomPending = true
  340. await nextTick()
  341. scrollToBottomPending = false
  342. messageListRef.value?.scrollToBottom?.()
  343. }
  344. const scrollToBottomIfNearBottom = () => {
  345. if (messageListRef.value?.isNearBottom?.() === false) return
  346. scrollToBottom()
  347. }
  348. /**
  349. * 创建知识
  350. */
  351. const handleAddKb = (txt: string) => {
  352. addkbRef.value?.open(txt)
  353. }
  354. /**
  355. * 计算当前激活会话的标题
  356. */
  357. const activeConversationTitle = computed(() => {
  358. const conv = conversations.value.find((c) => c.id === activeConversationId.value)
  359. return conv ? conv.title : t('pages.chat.newConversation')
  360. })
  361. /**
  362. * 判断是否显示图片上传按钮
  363. * agent 模式:根据智能体配置决定是否显示
  364. * flow 模式:始终显示
  365. */
  366. const showImageUploadButton = computed(
  367. () =>
  368. (activeTargetType.value === 'agent' && allowImageUpload.value) ||
  369. activeTargetType.value === 'flow'
  370. )
  371. /**
  372. * 计算当前聊天配置对象,用于发送消息时使用
  373. */
  374. const currentChatConfig = computed<ChatTargetConfig>(() => ({
  375. type: activeTargetType.value,
  376. knowledgeBaseIds: activeTargetType.value === 'flow' ? [] : [...settingsDraft.knowledgeBaseIds],
  377. knowledgeIds: activeTargetType.value === 'flow' ? [] : [...settingsDraft.knowledgeIds],
  378. summaryModelId: activeTargetType.value === 'flow' ? '' : settingsDraft.summaryModelId,
  379. disableTitle: settingsDraft.disableTitle,
  380. enableMemory: settingsDraft.enableMemory,
  381. agentId: settingsDraft.agentId,
  382. agentEnabled: settingsDraft.agentEnabled,
  383. webSearchEnabled: settingsDraft.webSearchEnabled,
  384. images: currentAttachments.value.map((item) => item.path || item.id || item.name).filter(Boolean)
  385. }))
  386. /**
  387. * 获取默认的智能体 ID
  388. * @returns 第一个智能体的 value,若无则返回空字符串
  389. */
  390. const getDefaultAgentId = () => {
  391. return agentOptions.value[0]?.value || ''
  392. }
  393. const getDefaultFlowId = () => {
  394. return flowOptions.value[0]?.value || ''
  395. }
  396. onMounted(async () => {
  397. presetConfig.value = parsePresetConfigFromRoute()
  398. await loadChatOptions()
  399. if (presetConfig.value) {
  400. await initializePresetConversation(presetConfig.value)
  401. return
  402. }
  403. await loadConversations()
  404. // 默认选中第一个会话
  405. // if (conversations.value.length > 0 && !activeConversationId.value) {
  406. // activeConversationId.value = conversations.value?.[0]?.id!
  407. // await loadConversationMessages(activeConversationId.value)
  408. // await scrollToBottom()
  409. // }
  410. })
  411. /**
  412. * 创建默认的聊天目标配置
  413. * @param type - 聊天目标类型,默认为 'agent'
  414. * @returns 默认配置对象
  415. */
  416. const createDefaultConfig = (type: ChatTargetType = 'agent'): ChatTargetConfig => {
  417. return {
  418. type,
  419. knowledgeBaseIds: [],
  420. knowledgeIds: [],
  421. summaryModelId: '',
  422. disableTitle: false,
  423. enableMemory: true,
  424. agentId: type === 'flow' ? getDefaultFlowId() : type === 'agent' ? getDefaultAgentId() : '',
  425. agentEnabled: true,
  426. webSearchEnabled: false,
  427. images: []
  428. }
  429. }
  430. const settingsDraft = reactive<ChatTargetConfig>(createDefaultConfig('agent'))
  431. const workflowVisibleVariables = computed(() =>
  432. workflowStartVariables.value.filter((item) => !item.is_hide)
  433. )
  434. const workflowFormStartNode = computed<IWorkflowNode | null>(() =>
  435. settingsDraft.type === 'flow'
  436. ? ({
  437. id: 'chat-workflow-start',
  438. type: 'canvas-node',
  439. position: { x: 0, y: 0 },
  440. data: {
  441. nodeType: 'start',
  442. variables: workflowStartVariables.value
  443. }
  444. } as unknown as IWorkflowNode)
  445. : null
  446. )
  447. const hasVisibleForm = computed(
  448. () => settingsDraft.type === 'flow' && workflowVisibleVariables.value.length > 0
  449. )
  450. const resetWorkflowFormValidation = () => {
  451. Object.keys(workflowValidationErrors).forEach((key) => {
  452. delete workflowValidationErrors[key]
  453. })
  454. }
  455. const resetWorkflowFormState = () => {
  456. workflowStartVariables.value = []
  457. Object.keys(workflowInputValues).forEach((key) => {
  458. delete workflowInputValues[key]
  459. })
  460. Object.keys(workflowJsonDrafts).forEach((key) => {
  461. delete workflowJsonDrafts[key]
  462. })
  463. resetWorkflowFormValidation()
  464. }
  465. const formatJsonDraft = (value: unknown, fallback = '{}') => {
  466. if (value === undefined || value === null || value === '') return fallback
  467. try {
  468. return JSON.stringify(value, null, 2)
  469. } catch {
  470. return fallback
  471. }
  472. }
  473. const normalizeFormType = (formType?: string): FormType => {
  474. const allowed = new Set<FormType>([
  475. 'text-input',
  476. 'text-area',
  477. 'select',
  478. 'number',
  479. 'checkbox',
  480. 'file',
  481. 'file-list',
  482. 'json_object'
  483. ])
  484. return allowed.has(formType as FormType) ? (formType as FormType) : 'text-input'
  485. }
  486. const normalizeWorkflowVariable = (item: Record<string, any>): StartVariable | null => {
  487. const name = `${item?.name || ''}`.trim()
  488. if (!name) return null
  489. const formType = normalizeFormType(item.formType)
  490. return {
  491. name,
  492. label: item.label || name,
  493. max_length: item.max_length,
  494. default_value: item.default_value,
  495. json: item.json,
  496. is_require: !!item.is_require,
  497. is_hide: !!item.is_hide,
  498. formType,
  499. options: Array.isArray(item.options) ? item.options : [],
  500. file_types: Array.isArray(item.file_types) ? (item.file_types as FileType[]) : [],
  501. file_extensions: Array.isArray(item.file_extensions) ? item.file_extensions : [],
  502. allow_link_input: item.allow_link_input
  503. }
  504. }
  505. const initializeWorkflowFormValues = () => {
  506. Object.keys(workflowInputValues).forEach((key) => {
  507. delete workflowInputValues[key]
  508. })
  509. Object.keys(workflowJsonDrafts).forEach((key) => {
  510. delete workflowJsonDrafts[key]
  511. })
  512. workflowStartVariables.value.forEach((variable) => {
  513. const initialValue =
  514. variable.default_value !== undefined
  515. ? structuredClone(variable.default_value)
  516. : createEmptyValue(variable.formType)
  517. if (variable.formType === 'json_object') {
  518. workflowInputValues[variable.name] =
  519. initialValue && typeof initialValue === 'object' && !Array.isArray(initialValue)
  520. ? initialValue
  521. : {}
  522. workflowJsonDrafts[variable.name] = formatJsonDraft(workflowInputValues[variable.name], '{}')
  523. return
  524. }
  525. workflowInputValues[variable.name] = initialValue
  526. })
  527. }
  528. const loadWorkflowExecuteInfo = async (id: string) => {
  529. resetWorkflowFormState()
  530. if (!id) return
  531. workflowFormLoading.value = true
  532. try {
  533. const res = await agent.postAgentGetExecuteInfo({ id })
  534. if (!res?.isSuccess || !res.result) {
  535. throw new Error('load workflow execute info failed')
  536. }
  537. workflowStartVariables.value = (res.result.form_variables || [])
  538. .map((item) => normalizeWorkflowVariable(item as Record<string, any>))
  539. .filter(Boolean) as StartVariable[]
  540. initializeWorkflowFormValues()
  541. showForm.value = workflowVisibleVariables.value.length > 0
  542. } catch (error) {
  543. console.error('loadWorkflowExecuteInfo error', error)
  544. ElMessage.error(t('pages.editor.loadAgentInfoFailed'))
  545. } finally {
  546. workflowFormLoading.value = false
  547. }
  548. }
  549. const buildWorkflowChatParams = () => {
  550. if (settingsDraft.type !== 'flow') return {}
  551. const params = buildExecuteParams({
  552. startVariables: workflowStartVariables.value,
  553. inputValues: workflowInputValues,
  554. jsonDrafts: workflowJsonDrafts,
  555. validationErrors: workflowValidationErrors,
  556. translateFieldRequired: (name) =>
  557. t('pages.runWorkflow.fieldRequired', {
  558. name
  559. }),
  560. translateInvalidJson: () => t('pages.runWorkflow.invalidJson'),
  561. translateFieldTooLong: (name, max) =>
  562. t('pages.runWorkflow.fieldTooLong', {
  563. name,
  564. max
  565. })
  566. })
  567. if (!params) {
  568. showForm.value = true
  569. ElMessage.warning(t('pages.runWorkflow.inputPanel.completeRequired'))
  570. return false
  571. }
  572. return params
  573. }
  574. /**
  575. * 深拷贝聊天配置对象
  576. * @param config - 源配置对象
  577. * @returns 拷贝后的新配置对象
  578. */
  579. const cloneConfig = (config: ChatTargetConfig): ChatTargetConfig => {
  580. const isFlow = config.type === 'flow'
  581. return {
  582. type: config.type,
  583. knowledgeBaseIds: isFlow ? [] : [...config.knowledgeBaseIds],
  584. knowledgeIds: isFlow ? [] : [...config.knowledgeIds],
  585. summaryModelId: isFlow ? '' : config.summaryModelId,
  586. disableTitle: config.disableTitle,
  587. enableMemory: config.enableMemory,
  588. agentId: config.agentId,
  589. agentEnabled: config.agentEnabled,
  590. webSearchEnabled: config.webSearchEnabled,
  591. images: [...config.images]
  592. }
  593. }
  594. /**
  595. * 选择智能体活智能编排
  596. */
  597. const handleChangeTarget = async (value: [ChatTargetType, string] | string[]) => {
  598. const [type, id] = value as [ChatTargetType, string]
  599. if (!type || !id) return
  600. activeTargetType.value = type
  601. settingsDraft.type = type
  602. settingsDraft.agentId = id
  603. if (type === 'flow') {
  604. settingsDraft.knowledgeBaseIds = []
  605. settingsDraft.knowledgeIds = []
  606. settingsDraft.summaryModelId = ''
  607. settingsDraft.agentEnabled = true
  608. settingsDraft.webSearchEnabled = false
  609. knowledgeOptions.value = []
  610. currentAttachments.value = []
  611. agentPromptsItems.value = undefined
  612. allowImageUpload.value = true
  613. await loadWorkflowExecuteInfo(id)
  614. return
  615. }
  616. showForm.value = false
  617. resetWorkflowFormState()
  618. await refreshAgentUploadCapability(id)
  619. }
  620. const normalizeConfig = (config: Partial<ChatTargetConfig>): ChatTargetConfig => {
  621. const base = createDefaultConfig(config.type || 'agent')
  622. const type = config.type || base.type
  623. const isFlow = type === 'flow'
  624. return {
  625. ...base,
  626. ...config,
  627. type,
  628. knowledgeBaseIds:
  629. !isFlow && Array.isArray(config.knowledgeBaseIds) ? config.knowledgeBaseIds : [],
  630. knowledgeIds: !isFlow && Array.isArray(config.knowledgeIds) ? config.knowledgeIds : [],
  631. summaryModelId: isFlow ? '' : config.summaryModelId || '',
  632. disableTitle: config.disableTitle ?? base.disableTitle,
  633. enableMemory: config.enableMemory ?? base.enableMemory,
  634. agentId:
  635. config.agentId || (isFlow ? getDefaultFlowId() : type === 'agent' ? getDefaultAgentId() : ''),
  636. agentEnabled: config.agentEnabled ?? base.agentEnabled,
  637. webSearchEnabled: config.webSearchEnabled ?? base.webSearchEnabled,
  638. images: []
  639. }
  640. }
  641. const parsePresetConfigFromRoute = () => {
  642. if (route.query.preset !== '1' || typeof route.query.chatConfig !== 'string') return null
  643. try {
  644. const rawConfig = JSON.parse(decodeURIComponent(route.query.chatConfig))
  645. return normalizeConfig(rawConfig)
  646. } catch (error) {
  647. console.error('Failed to parse chat preset config', error)
  648. ElMessage.error(t('pages.chat.invalidShareLink'))
  649. return null
  650. }
  651. }
  652. const buildShareUrl = () => {
  653. const config = normalizeConfig(currentChatConfig.value)
  654. const url = new URL(window.location.href)
  655. const hashPath = route.path || '/chat'
  656. const params = new URLSearchParams()
  657. params.set('preset', '1')
  658. params.set('chatConfig', encodeURIComponent(JSON.stringify(config)))
  659. url.hash = `${hashPath}?${params.toString()}`
  660. return url.toString()
  661. }
  662. const shareConversation = () => {
  663. shareUrl.value = buildShareUrl()
  664. shareDialogVisible.value = true
  665. }
  666. const copyShareUrl = async () => {
  667. if (!shareUrl.value) return
  668. try {
  669. await navigator.clipboard.writeText(shareUrl.value)
  670. ElMessage.success(t('pages.chat.shareLinkCopied'))
  671. } catch {
  672. ElMessage.warning(t('pages.chat.shareLinkCopyFailed'))
  673. }
  674. }
  675. const initializePresetConversation = async (config: ChatTargetConfig) => {
  676. syncSettingsDraft(config)
  677. if (config.type === 'flow') {
  678. await loadWorkflowExecuteInfo(config.agentId)
  679. } else {
  680. await fetchKnowledgeOptions(config.knowledgeBaseIds)
  681. await refreshAgentUploadCapability(config.agentId)
  682. }
  683. await loadConversations(true)
  684. const res = await createSession()
  685. if (!res.isSuccess || !res.result) return
  686. await loadConversations(true)
  687. const targetConv = conversations.value.find((c) => c.id === res.result)
  688. if (targetConv) {
  689. sessionConfigMap[targetConv.id] = cloneConfig(config)
  690. await handleSelectConversation(targetConv.id)
  691. }
  692. }
  693. /**
  694. * 同步配置到当前的设置草稿中
  695. * @param config - 要同步的配置对象
  696. */
  697. const syncSettingsDraft = (config: ChatTargetConfig) => {
  698. const next = createDefaultConfig(config.type)
  699. Object.assign(next, config)
  700. activeTargetType.value = next.type
  701. Object.assign(settingsDraft, next)
  702. targetSelection.value = [next.type, next.agentId]
  703. }
  704. /**
  705. * 加载聊天所需的选项数据(智能体、知识库、模型)
  706. * 并在加载完成后设置默认的模型和智能体
  707. */
  708. const loadChatOptions = async () => {
  709. try {
  710. const [agents, knowledgeBases, models, flows] = await Promise.all([
  711. getAgentOptions(),
  712. getKnowledgeBaseOptions(),
  713. getModelOptions('', 'KnowledgeQA'),
  714. getFlowOptions()
  715. ])
  716. agentOptions.value = agents
  717. knowledgeBaseOptions.value = knowledgeBases
  718. modelOptions.value = models
  719. flowOptions.value = flows
  720. if (activeTargetType.value === 'agent' && !settingsDraft.agentId) {
  721. settingsDraft.agentId = getDefaultAgentId()
  722. }
  723. if (activeTargetType.value === 'flow' && !settingsDraft.agentId) {
  724. settingsDraft.agentId = getDefaultFlowId()
  725. }
  726. targetSelection.value = [settingsDraft.type, settingsDraft.agentId]
  727. if (settingsDraft.type === 'flow' && settingsDraft.agentId) {
  728. await loadWorkflowExecuteInfo(settingsDraft.agentId)
  729. }
  730. } catch (error) {
  731. console.error('Failed to load chat options', error)
  732. }
  733. }
  734. /**
  735. * 刷新智能体的图片上传能力配置
  736. * @param agentId - 智能体 ID
  737. */
  738. const refreshAgentUploadCapability = async (agentId: string) => {
  739. allowImageUpload.value = false
  740. if (!agentId || activeTargetType.value !== 'agent') return
  741. try {
  742. const res = await getAgentInfo(agentId)
  743. allowImageUpload.value = !!res.result?.config?.img_vlm_config?.image_upload_enabled
  744. if (!allowImageUpload.value) {
  745. currentAttachments.value = []
  746. }
  747. } catch (error) {
  748. console.error('Failed to load agent info', error)
  749. }
  750. }
  751. watch(
  752. () => [settingsDraft.type, chatInputRef.value],
  753. ([type, chatRef]) => {
  754. if (type && chatRef) {
  755. const chat = chatInputRef.value?.getInstance()
  756. if (type !== 'model' && type !== 'flow') {
  757. chat?.openHeader()
  758. } else {
  759. chat?.closeHeader()
  760. }
  761. }
  762. },
  763. {
  764. immediate: true
  765. }
  766. )
  767. /**
  768. * 处理智能体选择变更事件
  769. * @param value - 选中的智能体 ID
  770. */
  771. const handleAgentSelectChange = async (value: string) => {
  772. settingsDraft.agentId = value
  773. targetSelection.value = [settingsDraft.type, value]
  774. await refreshAgentUploadCapability(value)
  775. }
  776. /**
  777. * 获取建议问题列表并更新提示项
  778. */
  779. const getSuggestQuestion = async () => {
  780. if (!settingsDraft.agentId) return
  781. // 获取建议问题
  782. const res = await agentApplication.postAiAgentSuggestedQuestions({
  783. id: settingsDraft.agentId,
  784. limit: 6
  785. })
  786. if (res.result) {
  787. agentPromptsItems.value = res.result.map((item, index) => {
  788. return {
  789. key: index,
  790. label: item.question,
  791. itemStyle: {
  792. height: 'auto',
  793. width: 'calc(50% - 6px)',
  794. boxSizing: 'border-box'
  795. }
  796. }
  797. })
  798. }
  799. }
  800. watch(
  801. () => settingsDraft.agentId,
  802. async () => {
  803. if (settingsDraft.type === 'agent') getSuggestQuestion()
  804. },
  805. {
  806. immediate: true
  807. }
  808. )
  809. /**
  810. * 处理提示项点击事件
  811. * @param item - 被点击的提示项
  812. */
  813. const handlePromptItemClick = (item: PromptsItemsProps) => {
  814. handleSend(item.label)
  815. }
  816. /**
  817. * 追加思考内容到消息对象
  818. * @param message - 消息对象
  819. * @param text - 要追加的思考文本
  820. */
  821. const appendThinking = (message: BubbleMessage, text?: string) => {
  822. const thought = text
  823. if (!thought) return
  824. message.eventThinkingText = `${message.eventThinkingText || ''}${thought}`
  825. updateMessageThinking(message)
  826. }
  827. /**
  828. * 解析回答中的思考状态
  829. * 提取 <think> 标签内的内容和标签外的回答内容
  830. * @param text - 原始文本
  831. * @returns 包含思考内容、回答内容及标签状态的對象
  832. */
  833. const parseAnswerThinkState = (text?: string) => {
  834. const raw = `${text || ''}`
  835. if (!raw) return { thinking: '', answer: '', hasThinkStart: false, hasThinkEnd: false }
  836. const hasThinkStart = /^<think\b[^>]*>/i.test(raw.trimStart())
  837. const thinking: string[] = []
  838. let answer = raw
  839. if (hasThinkStart) {
  840. answer = answer.replace(
  841. /^\s*<think\b[^>]*>([\s\S]*?)(?:<\/think>|$)/i,
  842. (_match, inner: string) => {
  843. const piece = inner
  844. if (piece) thinking.push(piece)
  845. return ''
  846. }
  847. )
  848. }
  849. return {
  850. thinking: thinking.join('\n\n'),
  851. answer: answer,
  852. hasThinkStart,
  853. hasThinkEnd: /<\/think>/i.test(raw)
  854. }
  855. }
  856. /**
  857. * 更新消息对象的思考内容字段
  858. * @param message - 消息对象
  859. * @param answerThinking - 回答中的思考内容
  860. */
  861. const updateMessageThinking = (message: BubbleMessage, answerThinking = '') => {
  862. message.answerThinkingText = answerThinking
  863. message.thinking = [message.eventThinkingText, message.answerThinkingText]
  864. .map((item) => item)
  865. .filter(Boolean)
  866. .join('\n\n')
  867. }
  868. /**
  869. * 更新消息对象的内容字段,并处理思考状态
  870. * @param message - 消息对象
  871. */
  872. const updateMessageContent = (message: BubbleMessage) => {
  873. const answerParts = parseAnswerThinkState(message.answerText || message.output)
  874. message.content = answerParts.answer
  875. updateMessageThinking(message, answerParts.thinking)
  876. if (answerParts.hasThinkStart) {
  877. message.thinkingOpen = !answerParts.hasThinkEnd
  878. } else if (message.streamCompleted) {
  879. message.thinkingOpen = false
  880. }
  881. }
  882. /**
  883. * 判断消息是否具有可渲染的内容
  884. * @param message - 消息对象
  885. * @returns 是否有可渲染内容
  886. */
  887. const hasRenderableContent = (message: BubbleMessage) => {
  888. return !!(
  889. message.content ||
  890. message.answerText ||
  891. message.thinking ||
  892. (message.toolCalls || []).length ||
  893. (message.toolResults || []).length ||
  894. (message.references || []).length ||
  895. message.error
  896. )
  897. }
  898. const appendInlineError = (message: BubbleMessage, error: string) => {
  899. const trimmedError = error.trim()
  900. if (!trimmedError) return
  901. const inlineErrors = message.inlineErrors || []
  902. if (!inlineErrors.includes(trimmedError)) {
  903. message.inlineErrors = [...inlineErrors, trimmedError]
  904. }
  905. }
  906. /**
  907. * 创建用户消息对象
  908. * @param content - 消息内容
  909. * @param updateTime - 更新时间
  910. * @returns 用户消息对象
  911. */
  912. const createUserMessage = (content: string, updateTime?: string): BubbleMessage => {
  913. const id = `user-${Date.now()}-${Math.random()}`
  914. return {
  915. id,
  916. key: id,
  917. role: 'user',
  918. placement: 'end',
  919. content: content,
  920. rawText: content,
  921. loading: false,
  922. shape: 'corner',
  923. variant: 'outlined',
  924. isMarkdown: false,
  925. typing: false,
  926. isFog: false,
  927. streamCompleted: true,
  928. updateTime
  929. }
  930. }
  931. /**
  932. * 获取当前附件的文件 ID 列表
  933. * @returns 文件 ID 数组
  934. */
  935. const getAttachmentFileIds = () => {
  936. return currentAttachments.value
  937. .map((item) => item.path || item.id || item.name)
  938. .filter(Boolean) as string[]
  939. }
  940. /**
  941. * 创建带有附件的用户消息对象
  942. * @param content - 消息内容
  943. * @param updateTime - 更新时间
  944. * @returns 用户消息对象
  945. */
  946. const createUserMessageWithAttachments = (content: string, updateTime?: string): BubbleMessage => {
  947. const message = createUserMessage(content, updateTime)
  948. const fileIds = getAttachmentFileIds()
  949. if (fileIds.length) {
  950. message.message_files = fileIds
  951. }
  952. return message
  953. }
  954. /**
  955. * 创建 AI 消息对象
  956. * @param updateTime - 更新时间
  957. * @returns AI 消息对象
  958. */
  959. const createAiMessage = (updateTime?: string): BubbleMessage => {
  960. const id = `ai-${Date.now()}-${Math.random()}`
  961. return {
  962. id,
  963. msgId: '',
  964. key: id,
  965. role: 'ai',
  966. placement: 'start',
  967. content: '',
  968. answerText: '',
  969. thinking: '',
  970. thinkingOpen: false,
  971. toolCalls: [],
  972. toolResults: [],
  973. references: [],
  974. assistantMessageId: '',
  975. loading: true,
  976. shape: 'corner',
  977. variant: 'filled',
  978. isMarkdown: true,
  979. typing: false,
  980. isFog: true,
  981. streamCompleted: false,
  982. updateTime,
  983. workflowTraceVisible: activeTargetType.value === 'flow',
  984. workflowTraceNodes: []
  985. }
  986. }
  987. /**
  988. * 同步消息身份
  989. * @param message
  990. * @param event
  991. */
  992. const syncMessageIdentity = (message: BubbleMessage, event: ChatSseMessage) => {
  993. if (event?.assistant_message_id) {
  994. message.assistantMessageId = event.assistant_message_id
  995. message.msgId = event.id
  996. }
  997. }
  998. /**
  999. * 标准化工具调用事件数据
  1000. * @param event - SSE 事件对象
  1001. * @returns 标准化的工具调用对象
  1002. */
  1003. const normalizeToolCall = (event: ChatSseMessage): ChatToolCall => {
  1004. const data = event.data || {}
  1005. return {
  1006. id: data.id || event.id,
  1007. toolCallId: data.tool_call_id || data.toolCallId,
  1008. toolName: data.tool_name || data.toolName,
  1009. arguments: data.arguments || data.params || data.args || {},
  1010. content: event.content || data.content
  1011. }
  1012. }
  1013. /**
  1014. * 标准化工具结果事件数据
  1015. * @param event - SSE 事件对象
  1016. * @returns 标准化的工具结果对象
  1017. */
  1018. const normalizeToolResult = (event: ChatSseMessage): ChatToolResult => {
  1019. const data = event.data || {}
  1020. return {
  1021. id: data.id || event.id,
  1022. toolCallId: data.tool_call_id || data.toolCallId,
  1023. toolName: data.tool_name || data.toolName,
  1024. success: data.success,
  1025. error: data.error,
  1026. output: data.output,
  1027. thought: data.thought,
  1028. durationMs: data.duration_ms || data.durationMs,
  1029. displayType: data.display_type || data.displayType,
  1030. contentItems: data.content_items || data.contentItems
  1031. }
  1032. }
  1033. /**
  1034. * 标准化引用文献事件数据
  1035. * @param event - SSE 事件对象
  1036. * @returns 标准化的引用文献数组
  1037. */
  1038. const normalizeReferences = (event: ChatSseMessage): ChatReference[] => {
  1039. const data = event.data || {}
  1040. const refs = data.knowledge_references || data.references || []
  1041. if (!Array.isArray(refs)) return []
  1042. return refs.map((item: Record<string, any>) => ({
  1043. id: item.id,
  1044. knowledgeBaseId: item.knowledge_base_id,
  1045. knowledgeId: item.knowledge_id,
  1046. knowledgeTitle: item.knowledge_title,
  1047. knowledgeFilename: item.knowledge_filename,
  1048. knowledgeDescription: item.knowledge_description,
  1049. content: item.content,
  1050. matchedContent: item.matched_content || item.matchedContent,
  1051. score: item.score,
  1052. chunkType: item.chunk_type || item.chunkType
  1053. }))
  1054. }
  1055. const normalizeChatContentEvent = (raw: Record<string, any>): ChatSseMessage => {
  1056. const data = raw.data && typeof raw.data === 'object' ? raw.data : raw
  1057. const content =
  1058. raw.content ??
  1059. raw.answer ??
  1060. raw.output ??
  1061. raw.delta ??
  1062. raw.text ??
  1063. data.content ??
  1064. data.answer ??
  1065. data.output ??
  1066. data.delta ??
  1067. data.text ??
  1068. ''
  1069. return {
  1070. id: raw.id || data.id,
  1071. response_type:
  1072. raw.response_type ||
  1073. raw.responseType ||
  1074. raw.type ||
  1075. raw.event ||
  1076. (content ? 'agent_query' : 'complete'),
  1077. content: `${content || ''}`,
  1078. done: raw.done || raw.is_end || raw.finished,
  1079. data
  1080. }
  1081. }
  1082. const normalizeWorkflowRunnerEvent = (event: ChatSseMessage): ChatSseMessage => {
  1083. const raw = event as ChatSseMessage & {
  1084. cmd?: string
  1085. msg?: Record<string, any>
  1086. result?: unknown
  1087. errorMsg?: string
  1088. }
  1089. switch (raw.cmd) {
  1090. case 'CMD_AGENT_CHAT_MSG':
  1091. return normalizeChatContentEvent(raw.msg || {})
  1092. case 'CMD_AGENT_FINISH_MSG':
  1093. return {
  1094. response_type: 'complete',
  1095. done: true,
  1096. data: { result: raw.result }
  1097. }
  1098. case 'CMD_AGENT_ERROR_MSG':
  1099. case 'CMD_CONNECT_ERROR_MSG':
  1100. return {
  1101. response_type: 'error',
  1102. content: raw.errorMsg || t('pages.chat.unknownError'),
  1103. done: true,
  1104. data: raw as unknown as Record<string, any>
  1105. }
  1106. case 'CMD_AGENT_SUSPEND_MSG':
  1107. return {
  1108. response_type: 'error',
  1109. content: t('pages.runWorkflow.suspended'),
  1110. done: true,
  1111. data: raw as unknown as Record<string, any>
  1112. }
  1113. default:
  1114. return event
  1115. }
  1116. }
  1117. const createRunnerNodeState = (
  1118. node: Record<string, any>,
  1119. status: RunnerNodeState['status']
  1120. ): RunnerNodeState => ({
  1121. nodeId: node.nodeId || node.id || '',
  1122. nodeName: node.nodeName || node.name || '',
  1123. nodeType: node.nodeType || node.type || '',
  1124. status,
  1125. lastUpdateTime: node.time,
  1126. track: node.track
  1127. })
  1128. const syncWorkflowTraceNode = (
  1129. message: BubbleMessage,
  1130. node: Record<string, any> | undefined,
  1131. partial: Partial<RunnerNodeState>
  1132. ) => {
  1133. if (!node) return
  1134. const nodeId = node.nodeId || node.id
  1135. if (!nodeId) return
  1136. message.workflowTraceVisible = true
  1137. const nodes: RunnerNodeState[] = Array.isArray(message.workflowTraceNodes)
  1138. ? message.workflowTraceNodes
  1139. : []
  1140. const index = nodes.findIndex((item) => item.nodeId === nodeId)
  1141. const existing = index >= 0 ? nodes[index] : undefined
  1142. const next: RunnerNodeState = {
  1143. ...createRunnerNodeState(node, existing?.status || 'idle'),
  1144. ...existing,
  1145. ...partial,
  1146. nodeId
  1147. }
  1148. if (index >= 0) {
  1149. nodes[index] = next
  1150. } else {
  1151. nodes.push(next)
  1152. }
  1153. message.workflowTraceNodes = nodes
  1154. }
  1155. const applyWorkflowRunnerTraceEvent = (message: BubbleMessage, event: ChatSseMessage) => {
  1156. const raw = event as ChatSseMessage & {
  1157. cmd?: string
  1158. time?: string
  1159. node?: Record<string, any>
  1160. track?: Record<string, any>
  1161. }
  1162. switch (raw.cmd) {
  1163. case 'CMD_AGENT_RUNNING_MSG':
  1164. message.workflowTraceVisible = true
  1165. break
  1166. case 'CMD_NODE_RUNNING_MSG':
  1167. case 'CMD_NODE_ITERATION_RUNNING_MSG':
  1168. case 'CMD_NODE_ITERATION_STEP_MSG':
  1169. syncWorkflowTraceNode(message, raw.node, {
  1170. status: 'running',
  1171. lastUpdateTime: raw.time
  1172. })
  1173. break
  1174. case 'CMD_NODE_FINISH_MSG':
  1175. syncWorkflowTraceNode(message, raw.node, {
  1176. status: raw.track?.is_success ? 'success' : 'failed',
  1177. lastUpdateTime: raw.time,
  1178. track: raw.track
  1179. })
  1180. break
  1181. case 'CMD_NODE_ITERATION_FINISH_MSG':
  1182. syncWorkflowTraceNode(message, raw.node, {
  1183. status: 'success',
  1184. lastUpdateTime: raw.time
  1185. })
  1186. break
  1187. case 'CMD_AGENT_SUSPEND_MSG':
  1188. message.workflowTraceVisible = true
  1189. message.workflowTraceNodes = (message.workflowTraceNodes || []).map(
  1190. (node: RunnerNodeState) =>
  1191. node.status === 'running'
  1192. ? {
  1193. ...node,
  1194. status: 'suspended',
  1195. lastUpdateTime: raw.time || node.lastUpdateTime
  1196. }
  1197. : node
  1198. )
  1199. break
  1200. default:
  1201. break
  1202. }
  1203. }
  1204. /**
  1205. * 将结构化事件应用到消息对象上
  1206. * 根据事件类型更新消息的不同字段(如回答、思考、工具调用等)
  1207. * @param message - 消息对象
  1208. * @param event - SSE 事件对象
  1209. */
  1210. const applyStructuredEventToMessage = (message: BubbleMessage, event: ChatSseMessage) => {
  1211. const data = event.data || {}
  1212. switch (event.response_type) {
  1213. case 'agent_query':
  1214. syncMessageIdentity(message, event)
  1215. if (event.content) {
  1216. message.answerText = `${message.answerText || ''}${event.content}`
  1217. }
  1218. break
  1219. case 'answer':
  1220. case 'message':
  1221. case 'delta':
  1222. if (event.content) {
  1223. message.answerText = `${message.answerText || ''}${event.content}`
  1224. }
  1225. break
  1226. case 'thinking': {
  1227. const thought = event.content || data.thought || data.content
  1228. appendThinking(message, thought)
  1229. message.thinkingOpen = true
  1230. break
  1231. }
  1232. case 'tool_call':
  1233. message.toolCalls = [...(message.toolCalls || []), normalizeToolCall(event)]
  1234. break
  1235. case 'tool_result': {
  1236. message.toolResults = [...(message.toolResults || []), normalizeToolResult(event)]
  1237. appendThinking(message, data.thought)
  1238. break
  1239. }
  1240. case 'references':
  1241. message.references = [...(message.references || []), ...normalizeReferences(event)]
  1242. break
  1243. case 'error':
  1244. syncMessageIdentity(message, event)
  1245. appendInlineError(
  1246. message,
  1247. event.content || data.error || data.message || t('pages.chat.unknownError')
  1248. )
  1249. message.streamCompleted = true
  1250. break
  1251. case 'session_title': {
  1252. const title = data.title || event.content
  1253. if (title) {
  1254. const conv = conversations.value.find((item) => item.id === activeConversationId.value)
  1255. if (conv) {
  1256. conv.title = title
  1257. }
  1258. }
  1259. break
  1260. }
  1261. case 'complete':
  1262. message.streamCompleted = true
  1263. message.total_duration_ms = data?.total_duration_ms
  1264. message.total_steps = data?.total_steps
  1265. break
  1266. default:
  1267. if (event.content) {
  1268. message.answerText = `${message.answerText || ''}${event.content}`
  1269. }
  1270. break
  1271. }
  1272. updateMessageContent(message)
  1273. message.loading =
  1274. !message.streamCompleted &&
  1275. !event.done &&
  1276. !hasRenderableContent(message) &&
  1277. event.response_type !== 'complete'
  1278. message.typing = false
  1279. message.isFog = false
  1280. }
  1281. /**
  1282. * 解析历史记录条目为消息对象数组
  1283. * @param item - 历史记录条目
  1284. * @returns 消息对象数组
  1285. */
  1286. const parseHistoryRecord = (item: Record<string, any>): BubbleMessage[] => {
  1287. const updateTime = item.updateTime || item.creationTime
  1288. const messagesForRow: BubbleMessage[] = []
  1289. const attachmentFiles = (item?.message_files || '').split(',')
  1290. if (item.query) {
  1291. const userMessage = createUserMessage(item.query, updateTime)
  1292. if (attachmentFiles.length) {
  1293. userMessage.message_files = attachmentFiles
  1294. }
  1295. messagesForRow.push(userMessage)
  1296. }
  1297. const hasStructuredData =
  1298. item.response_type ||
  1299. item.thinking ||
  1300. item.tool_call ||
  1301. item.tool_result ||
  1302. item.knowledge_references ||
  1303. item.references ||
  1304. item.answer ||
  1305. item.content
  1306. if (!hasStructuredData) {
  1307. return messagesForRow
  1308. }
  1309. const aiMessage = createAiMessage(updateTime)
  1310. aiMessage.id = item.id || item.msgId || aiMessage.id
  1311. aiMessage.key = aiMessage.id
  1312. aiMessage.msgId = item.msgId || item.id || ''
  1313. aiMessage.assistantMessageId = item.assistant_message_id || item.assistantMessageId || ''
  1314. aiMessage.answerText = item.answer || item.content || item.output
  1315. aiMessage.thinking = item.thinking || item.thought
  1316. aiMessage.toolCalls = Array.isArray(item.tool_calls)
  1317. ? item.tool_calls
  1318. : Array.isArray(item.toolCalls)
  1319. ? item.toolCalls
  1320. : []
  1321. aiMessage.toolResults = Array.isArray(item.tool_results)
  1322. ? item.tool_results
  1323. : Array.isArray(item.toolResults)
  1324. ? item.toolResults
  1325. : []
  1326. aiMessage.references = Array.isArray(item.knowledge_references)
  1327. ? normalizeReferences({ data: { knowledge_references: item.knowledge_references } })
  1328. : Array.isArray(item.references)
  1329. ? normalizeReferences({ data: { references: item.references } })
  1330. : []
  1331. const answerParts = parseAnswerThinkState(aiMessage.answerText)
  1332. if (!aiMessage.thinking && answerParts.thinking) {
  1333. aiMessage.thinking = answerParts.thinking
  1334. }
  1335. aiMessage.answerText = answerParts.answer
  1336. aiMessage.thinkingOpen = false
  1337. updateMessageContent(aiMessage)
  1338. aiMessage.loading = false
  1339. aiMessage.isFog = false
  1340. aiMessage.streamCompleted = true
  1341. messagesForRow.push(aiMessage)
  1342. return messagesForRow
  1343. }
  1344. /**
  1345. * 加载会话列表
  1346. * @param isRefresh - 是否为刷新操作,如果是则重置分页和列表
  1347. */
  1348. const loadConversations = async (isRefresh = false) => {
  1349. if (isRefresh) {
  1350. currentPage.value = 1
  1351. conversations.value = []
  1352. hasMore.value = true
  1353. }
  1354. try {
  1355. const res = await getSessionList(currentPage.value, pageSize.value)
  1356. if (res.isSuccess && res.result?.model) {
  1357. const newList = res.result.model.map((item) => {
  1358. const config = sessionConfigMap[item.id]
  1359. return {
  1360. id: item.id,
  1361. sessionId: item.sessionId,
  1362. title: item.name,
  1363. updatedAt: new Date(item.updateTime).getTime(),
  1364. targetType: config?.type,
  1365. targetConfig: config ? cloneConfig(config) : undefined
  1366. }
  1367. })
  1368. conversations.value = isRefresh ? newList : [...conversations.value, ...newList]
  1369. if (newList.length < pageSize.value) {
  1370. hasMore.value = false
  1371. } else {
  1372. currentPage.value += 1
  1373. }
  1374. } else {
  1375. hasMore.value = false
  1376. }
  1377. } catch (error) {
  1378. console.error('Failed to load conversations', error)
  1379. ElMessage.error(t('common.error.network'))
  1380. }
  1381. }
  1382. /**
  1383. * 加载更多会话
  1384. */
  1385. const loadMoreConversations = async () => {
  1386. if (isLoadingMore.value || !hasMore.value) return
  1387. isLoadingMore.value = true
  1388. try {
  1389. await loadConversations(false)
  1390. } finally {
  1391. isLoadingMore.value = false
  1392. }
  1393. }
  1394. /**
  1395. * 加载指定会话的消息历史
  1396. * @param conversationId - 会话 ID
  1397. */
  1398. const loadConversationMessages = async (conversationId: string) => {
  1399. if (!conversationId) {
  1400. messages.value = []
  1401. return
  1402. }
  1403. try {
  1404. const res = await getSessionMessages(conversationId)
  1405. if (res.isSuccess && res.result?.model) {
  1406. messages.value = res.result.model.flatMap((item) =>
  1407. parseHistoryRecord(item as Record<string, any>)
  1408. )
  1409. } else {
  1410. messages.value = []
  1411. }
  1412. } catch (error) {
  1413. console.error('Failed to load messages', error)
  1414. messages.value = []
  1415. }
  1416. }
  1417. /**
  1418. * 处理选择会话事件
  1419. * @param bizId - 会话业务 ID
  1420. */
  1421. const handleSelectConversation = async (bizId: string) => {
  1422. activeConversationId.value = bizId
  1423. messages.value = []
  1424. const targetConv = conversations.value.find((c) => c.id === bizId)
  1425. if (!targetConv) {
  1426. console.warn('Selected conversation not found in local list')
  1427. return
  1428. }
  1429. if (presetConfig.value) {
  1430. sessionConfigMap[targetConv.id] = cloneConfig(presetConfig.value)
  1431. }
  1432. await loadConversationMessages(targetConv.id)
  1433. await scrollToBottom()
  1434. }
  1435. const ensureActiveConversation = async () => {
  1436. if (activeConversationId.value) {
  1437. sessionConfigMap[activeConversationId.value] = cloneConfig(currentChatConfig.value)
  1438. return true
  1439. }
  1440. try {
  1441. const config = cloneConfig(currentChatConfig.value)
  1442. const res = await createSession()
  1443. if (!res.isSuccess || !res.result) {
  1444. ElMessage.error(t('common.error.network'))
  1445. return false
  1446. }
  1447. await loadConversations(true)
  1448. const targetConv = conversations.value.find((c) => c.id === res.result)
  1449. if (targetConv) {
  1450. sessionConfigMap[targetConv.id] = config
  1451. activeConversationId.value = targetConv.id
  1452. messages.value = []
  1453. return true
  1454. }
  1455. ElMessage.error(t('common.error.network'))
  1456. return false
  1457. } catch (error) {
  1458. console.error(error)
  1459. ElMessage.error(t('common.error.network'))
  1460. return false
  1461. }
  1462. }
  1463. /**
  1464. * 处理新建会话事件
  1465. */
  1466. const handleNewChat = async () => {
  1467. try {
  1468. // const defaultName = `${t('pages.chat.newConversation')} ${new Date().toLocaleTimeString()}`
  1469. const res = await createSession()
  1470. if (res.isSuccess && res.result) {
  1471. await loadConversations(true)
  1472. const targetConv = conversations.value.find((c) => c.id === res.result)
  1473. if (targetConv) {
  1474. sessionConfigMap[targetConv.id] = presetConfig.value
  1475. ? cloneConfig(presetConfig.value)
  1476. : cloneConfig(currentChatConfig.value)
  1477. await handleSelectConversation(targetConv.id)
  1478. ElMessage.success(t('pages.chat.createSuccess'))
  1479. } else if (conversations.value.length > 0) {
  1480. await handleSelectConversation(conversations.value?.[0]?.id!)
  1481. }
  1482. }
  1483. } catch (error) {
  1484. console.error(error)
  1485. ElMessage.error(t('common.error.network'))
  1486. }
  1487. }
  1488. /**
  1489. * 处理会话命令(重命名、删除等)
  1490. * @param command - 命令名称
  1491. * @param bizId - 会话业务 ID
  1492. */
  1493. const handleConvCommand = (command: string | number, bizId: string) => {
  1494. const cmd = String(command)
  1495. if (cmd === 'rename') {
  1496. renamingConvId.value = bizId
  1497. const conv = conversations.value.find((c) => c.id === bizId)
  1498. renameInput.value = conv?.title || ''
  1499. renameDialogVisible.value = true
  1500. return
  1501. }
  1502. if (cmd === 'delete') {
  1503. ElMessageBox.confirm(t('pages.chat.deleteConfirm'), t('common.dialog.tip'), {
  1504. confirmButtonText: t('common.confirm'),
  1505. cancelButtonText: t('common.cancel'),
  1506. type: 'warning'
  1507. })
  1508. .then(async () => {
  1509. try {
  1510. await deleteSession(bizId)
  1511. ElMessage.success(t('pages.chat.deleteSuccess'))
  1512. if (activeConversationId.value === bizId) {
  1513. activeConversationId.value = ''
  1514. messages.value = []
  1515. }
  1516. await loadConversations(true)
  1517. if (conversations.value.length > 0 && !activeConversationId.value) {
  1518. activeConversationId.value = conversations.value?.[0]?.id!
  1519. await loadConversationMessages(activeConversationId.value)
  1520. await scrollToBottom()
  1521. }
  1522. } catch (error) {
  1523. ElMessage.error(t('common.error.network'))
  1524. }
  1525. })
  1526. .catch(() => {})
  1527. }
  1528. }
  1529. /**
  1530. * 根据选中的知识库 ID 获取知识选项
  1531. * @param baseIds - 知识库 ID 数组
  1532. */
  1533. const fetchKnowledgeOptions = async (baseIds: string[]) => {
  1534. if (!baseIds.length) {
  1535. knowledgeOptions.value = []
  1536. return
  1537. }
  1538. const results = await Promise.all(
  1539. baseIds.map((knowledgeBaseId) =>
  1540. knowledge.postAiKnowledgeSelectList({
  1541. knowledge_base_id: knowledgeBaseId,
  1542. title: ''
  1543. })
  1544. )
  1545. )
  1546. const optionMap: Record<string, { label: string; value: string }> = {}
  1547. results.forEach((res: any) => {
  1548. if (!res?.isSuccess) return
  1549. ;(res.result || []).forEach((item: any) => {
  1550. if (!item.id) return
  1551. optionMap[item.id] = {
  1552. label: item.title!,
  1553. value: item.id
  1554. }
  1555. })
  1556. })
  1557. knowledgeOptions.value = Object.values(optionMap)
  1558. }
  1559. /**
  1560. * 处理重命名会话
  1561. */
  1562. const handleRename = async () => {
  1563. if (!renameInput.value.trim()) {
  1564. ElMessage.warning(t('pages.chat.renameEmpty'))
  1565. return
  1566. }
  1567. try {
  1568. await updateSessionName(renamingConvId.value, renameInput.value)
  1569. renameDialogVisible.value = false
  1570. ElMessage.success(t('pages.chat.renameSuccess'))
  1571. const conv = conversations.value.find((c) => c.id === renamingConvId.value)
  1572. if (conv) conv.title = renameInput.value
  1573. } catch (error) {
  1574. ElMessage.error(t('common.error.network'))
  1575. }
  1576. }
  1577. /**
  1578. * 处理发送消息
  1579. * @param content - 可选的消息内容,若不提供则使用 senderValue
  1580. */
  1581. const handleSend = async (content?: string) => {
  1582. if (!content || !content.trim()) {
  1583. ElMessage.warning(t('pages.chat.inputRequired'))
  1584. return
  1585. }
  1586. if (activeTargetType.value === 'agent' && !settingsDraft.agentId) {
  1587. ElMessage.warning(t('pages.chat.selectAgentFirst'))
  1588. return
  1589. }
  1590. if (activeTargetType.value === 'flow' && !settingsDraft.agentId) {
  1591. ElMessage.warning(t('pages.chat.selectWorkflowFirst'))
  1592. return
  1593. }
  1594. const workflowParams = buildWorkflowChatParams()
  1595. if (workflowParams === false) return
  1596. const hasConversation = await ensureActiveConversation()
  1597. if (!hasConversation) return
  1598. senderValue.value = ''
  1599. const userMsg = createUserMessageWithAttachments(content)
  1600. messages.value.push(userMsg)
  1601. await scrollToBottom()
  1602. const aiMsg = createAiMessage()
  1603. messages.value.push(aiMsg)
  1604. const aiMessageKey = aiMsg.key as string
  1605. const streamToken = `${aiMessageKey}-${Date.now()}`
  1606. activeStreamToken.value = streamToken
  1607. await scrollToBottom()
  1608. const getAiMessage = () => messages.value.find((item) => item.key === aiMessageKey)
  1609. const isActiveStream = () => activeStreamToken.value === streamToken
  1610. const requestBody = buildChatRequestBody(
  1611. activeTargetType.value,
  1612. currentChatConfig.value,
  1613. activeConversationId.value,
  1614. content,
  1615. workflowParams
  1616. )
  1617. const url = getChatUrl(activeTargetType.value)
  1618. currentAttachments.value = []
  1619. streamChat(
  1620. url,
  1621. requestBody,
  1622. // onChunk
  1623. (event: ChatSseMessage) => {
  1624. if (!isActiveStream()) return
  1625. const msg = getAiMessage()
  1626. if (!msg) return
  1627. applyWorkflowRunnerTraceEvent(msg, event)
  1628. applyStructuredEventToMessage(msg, normalizeWorkflowRunnerEvent(event))
  1629. scrollToBottomIfNearBottom()
  1630. },
  1631. // onComplete
  1632. () => {
  1633. if (!isActiveStream()) return
  1634. const msg = getAiMessage()
  1635. if (msg) {
  1636. msg.loading = false
  1637. msg.isFog = false
  1638. msg.typing = false
  1639. msg.streamCompleted = true
  1640. updateMessageContent(msg)
  1641. }
  1642. },
  1643. // onError
  1644. (err) => {
  1645. if (!isActiveStream()) return
  1646. console.error('Stream chat error:', err)
  1647. const msg = getAiMessage()
  1648. if (msg) {
  1649. msg.loading = false
  1650. msg.isFog = false
  1651. msg.typing = false
  1652. msg.streamCompleted = true
  1653. msg.inlineErrors = [
  1654. ...(msg.inlineErrors || []),
  1655. err?.message || err?.name || t('common.error.network')
  1656. ]
  1657. updateMessageContent(msg)
  1658. }
  1659. }
  1660. )
  1661. }
  1662. /**
  1663. * 发送消息的包装方法
  1664. * @param content - 消息内容
  1665. */
  1666. const sendMessage = async (content: string) => {
  1667. await handleSend(content)
  1668. }
  1669. /**
  1670. * 处理重试消息
  1671. * @param message - 要重试的消息对象
  1672. */
  1673. const handleRetry = async (message: BubbleMessage) => {
  1674. if (isLoading.value) {
  1675. ElMessage.warning(t('pages.chat.requestInProgress'))
  1676. return
  1677. }
  1678. const messageIndex = messages.value.findIndex((item) => item.id === message.id)
  1679. if (messageIndex <= 0) return
  1680. const userMessage = [...messages.value.slice(0, messageIndex)]
  1681. .reverse()
  1682. .find((item) => item.role === 'user')
  1683. const query = userMessage?.rawText || userMessage?.content
  1684. if (!query) {
  1685. ElMessage.warning(t('pages.chat.retrySourceNotFound'))
  1686. return
  1687. }
  1688. messages.value = messages.value.slice(0, messageIndex)
  1689. senderValue.value = query
  1690. await sendMessage(query)
  1691. }
  1692. /**
  1693. * 处理取消请求
  1694. */
  1695. const handleCancel = async () => {
  1696. cancelRequest()
  1697. activeStreamToken.value = ''
  1698. const msg = [...messages.value]
  1699. .reverse()
  1700. .find((item) => item.role === 'ai' && !item.streamCompleted)
  1701. if (!msg) return
  1702. msg.loading = false
  1703. msg.isFog = false
  1704. msg.typing = false
  1705. msg.streamCompleted = true
  1706. msg.stopped = true
  1707. updateMessageContent(msg)
  1708. if (msg.assistantMessageId) {
  1709. try {
  1710. await aiChat.postChatStopAnswer({
  1711. session_id: activeConversationId.value,
  1712. msgId: msg.assistantMessageId
  1713. })
  1714. } catch (error) {
  1715. console.error('stop answer error', error)
  1716. }
  1717. }
  1718. }
  1719. </script>
  1720. <style lang="less" scoped>
  1721. .chat-container {
  1722. display: flex;
  1723. height: 100vh;
  1724. background: var(--bg-page);
  1725. .chat-main {
  1726. flex: 1;
  1727. display: flex;
  1728. flex-direction: column;
  1729. min-height: 0;
  1730. overflow: hidden;
  1731. background: var(--bg-page);
  1732. }
  1733. }
  1734. .title {
  1735. font-size: 12px;
  1736. color: var(--text-secondary);
  1737. flex-shrink: 0;
  1738. }
  1739. :deep(.el-bubble-end .el-bubble-content) {
  1740. background-color: var(--el-color-primary-light-9);
  1741. border-radius: 8px 0px 8px 8px;
  1742. color: var(--text-primary);
  1743. border: none;
  1744. }
  1745. :deep(.el-bubble-start .el-bubble-content) {
  1746. background-color: var(--bg-container);
  1747. border: none;
  1748. color: var(--text-primary);
  1749. }
  1750. .chat-target-select {
  1751. width: 132px;
  1752. }
  1753. .settings-switches {
  1754. display: flex;
  1755. align-items: center;
  1756. flex-wrap: wrap;
  1757. gap: 18px;
  1758. }
  1759. .settings-note {
  1760. margin-top: 10px;
  1761. }
  1762. .workflow-form {
  1763. width: 100%;
  1764. max-width: 100%;
  1765. max-height: min(360px, 42vh);
  1766. margin: 0;
  1767. padding: 16px;
  1768. overflow-y: auto;
  1769. overflow-x: hidden;
  1770. border: 1px solid var(--border-light);
  1771. border-radius: 8px;
  1772. background: var(--bg-base);
  1773. box-sizing: border-box;
  1774. }
  1775. .workflow-form__title {
  1776. margin-bottom: 12px;
  1777. font-size: 15px;
  1778. font-weight: 600;
  1779. color: var(--text-primary);
  1780. }
  1781. :global(.workflow-form-popper) {
  1782. max-width: calc(100vw - 24px);
  1783. border-radius: 20px !important;
  1784. }
  1785. :global(.workflow-form-popper .el-popover__inner) {
  1786. padding: 0;
  1787. }
  1788. .workflow-form :deep(.input-form),
  1789. .workflow-form :deep(.el-form),
  1790. .workflow-form :deep(.el-form-item),
  1791. .workflow-form :deep(.el-form-item__content),
  1792. .workflow-form :deep(.el-input),
  1793. .workflow-form :deep(.el-select),
  1794. .workflow-form :deep(.el-textarea) {
  1795. min-width: 0;
  1796. max-width: 100%;
  1797. }
  1798. /* Prompts 组件暗黑模式适配 */
  1799. :deep(.el-prompts-title) {
  1800. color: var(--text-tertiary) !important;
  1801. }
  1802. :deep(.el-prompts-item) {
  1803. background: var(--bg-container) !important;
  1804. border-color: var(--border-light) !important;
  1805. }
  1806. :deep(.el-prompts-item.hovered) {
  1807. background: var(--bg-overlay) !important;
  1808. }
  1809. :deep(.el-prompts-item.actived) {
  1810. background: var(--bg-page) !important;
  1811. }
  1812. :deep(.el-prompts-item-label) {
  1813. color: var(--text-primary) !important;
  1814. }
  1815. :deep(.el-prompts-item-description) {
  1816. color: var(--text-tertiary) !important;
  1817. }
  1818. :global(.target-popper .el-cascader-node) {
  1819. gap: 8px;
  1820. }
  1821. /* 隐藏级联选择器默认的标签 chip,保留自定义 #tag slot 内容 */
  1822. .chat-target-cascader :deep(.el-cascader__tags > .el-tag) {
  1823. display: none;
  1824. }
  1825. .chat-target-tag {
  1826. display: inline-flex;
  1827. align-items: center;
  1828. white-space: nowrap;
  1829. }
  1830. .chat-target-tag__name {
  1831. font-size: 13px;
  1832. color: var(--text-primary);
  1833. max-width: 120px;
  1834. overflow: hidden;
  1835. text-overflow: ellipsis;
  1836. }
  1837. </style>