independent.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. <script setup lang="tsx">
  2. import {
  3. Attachments,
  4. type AttachmentsProps,
  5. Bubble,
  6. type BubbleListProps,
  7. Conversations,
  8. type ConversationsProps,
  9. Prompts,
  10. type PromptsProps,
  11. Sender,
  12. Welcome,
  13. useXAgent,
  14. useXChat,
  15. type BubbleProps
  16. } from 'ant-design-x-vue';
  17. import { Typography } from 'ant-design-vue';
  18. import {
  19. CloudUploadOutlined,
  20. CommentOutlined,
  21. EllipsisOutlined,
  22. FireOutlined,
  23. HeartOutlined,
  24. PaperClipOutlined,
  25. PlusOutlined,
  26. ReadOutlined,
  27. ShareAltOutlined,
  28. SmileOutlined,
  29. } from '@ant-design/icons-vue';
  30. import { Badge, Button, Space, theme } from 'ant-design-vue';
  31. import { computed, ref, watch, type VNode, h, onMounted } from 'vue';
  32. import MarkdownIt from 'markdown-it';
  33. defineOptions({ name: 'PlaygroundIndependent' });
  34. const sleep = () => new Promise((resolve) => setTimeout(resolve, 500));
  35. const renderTitle = (icon: VNode, title: string) => (
  36. <Space align="start">
  37. {icon}
  38. <span>{title}</span>
  39. </Space>
  40. );
  41. const defaultConversationsItems = [
  42. {
  43. key: '0',
  44. label: 'What is Ant Design X?',
  45. },
  46. ];
  47. const placeholderPromptsItems: PromptsProps['items'] = [
  48. {
  49. key: '1',
  50. label: renderTitle(<FireOutlined style={{ color: '#FF4D4F' }} />, 'Hot Topics'),
  51. description: 'What are you interested in?',
  52. children: [
  53. {
  54. key: '1-1',
  55. description: `What's new in X?`,
  56. },
  57. {
  58. key: '1-2',
  59. description: `What's AGI?`,
  60. },
  61. {
  62. key: '1-3',
  63. description: `Where is the doc?`,
  64. },
  65. ],
  66. },
  67. {
  68. key: '2',
  69. label: renderTitle(<ReadOutlined style={{ color: '#1890FF' }} />, 'Design Guide'),
  70. description: 'How to design a good product?',
  71. children: [
  72. {
  73. key: '2-1',
  74. icon: <HeartOutlined />,
  75. description: `Know the well`,
  76. },
  77. {
  78. key: '2-2',
  79. icon: <SmileOutlined />,
  80. description: `Set the AI role`,
  81. },
  82. {
  83. key: '2-3',
  84. icon: <CommentOutlined />,
  85. description: `Express the feeling`,
  86. },
  87. ],
  88. },
  89. ];
  90. const senderPromptsItems: PromptsProps['items'] = [
  91. {
  92. key: '1',
  93. description: 'Hot Topics',
  94. icon: <FireOutlined style={{ color: '#FF4D4F' }} />,
  95. },
  96. {
  97. key: '2',
  98. description: 'Design Guide',
  99. icon: <ReadOutlined style={{ color: '#1890FF' }} />,
  100. },
  101. ];
  102. const roles: BubbleListProps['roles'] = {
  103. ai: {
  104. placement: 'start',
  105. typing: { step: 5, interval: 20 },
  106. styles: {
  107. content: {
  108. borderRadius: '16px',
  109. },
  110. },
  111. },
  112. local: {
  113. placement: 'end',
  114. variant: 'shadow',
  115. },
  116. };
  117. // ==================== Style ====================
  118. const { token } = theme.useToken();
  119. const styles = computed(() => {
  120. return {
  121. layout: {
  122. width: '100%',
  123. 'min-width': '1000px',
  124. height: '100vh',
  125. 'border-radius': `${token.value.borderRadius}px`,
  126. display: 'flex',
  127. background: `${token.value.colorBgContainer}`,
  128. 'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`,
  129. },
  130. menu: {
  131. background: `${token.value.colorBgLayout}80`,
  132. width: '280px',
  133. height: '100%',
  134. display: 'flex',
  135. 'flex-direction': 'column',
  136. },
  137. conversations: {
  138. padding: '0 12px',
  139. flex: 1,
  140. 'overflow-y': 'auto',
  141. },
  142. chat: {
  143. height: '100%',
  144. width: '100%',
  145. 'max-width': '700px',
  146. margin: '0 auto',
  147. 'box-sizing': 'border-box',
  148. display: 'flex',
  149. 'flex-direction': 'column',
  150. padding: `${token.value.paddingLG}px`,
  151. gap: '16px',
  152. },
  153. messages: {
  154. flex: 1,
  155. },
  156. placeholder: {
  157. 'padding-top': '32px',
  158. },
  159. sender: {
  160. 'box-shadow': token.value.boxShadow,
  161. },
  162. logo: {
  163. display: 'flex',
  164. height: '72px',
  165. 'align-items': 'center',
  166. 'justify-content': 'start',
  167. padding: '0 24px',
  168. 'box-sizing': 'border-box',
  169. },
  170. 'logo-img': {
  171. width: '24px',
  172. height: '24px',
  173. display: 'inline-block',
  174. },
  175. 'logo-span': {
  176. display: 'inline-block',
  177. margin: '0 8px',
  178. 'font-weight': 'bold',
  179. color: token.value.colorText,
  180. 'font-size': '16px',
  181. },
  182. addBtn: {
  183. background: '#1677ff0f',
  184. border: '1px solid #1677ff34',
  185. width: 'calc(100% - 24px)',
  186. margin: '0 12px 24px 12px',
  187. }
  188. } as const
  189. });
  190. // ==================== State ====================
  191. const headerOpen = ref(false);
  192. const content = ref('');
  193. const conversationsItems = ref(defaultConversationsItems);
  194. const activeKey = ref(defaultConversationsItems[0].key);
  195. const attachedFiles = ref<AttachmentsProps['items']>([]);
  196. const agentRequestLoading = ref(false);
  197. const pagination = ref({
  198. total: 0,
  199. page: 1,
  200. pageSize: 20,
  201. totalPages: 0
  202. });
  203. const loadingHistory = ref(false);
  204. // 添加文件类型分类存储
  205. const imageUrls = ref<string[]>([]);
  206. const documentUrls = ref<string[]>([]);
  207. // 添加 API 基础 URL 常量
  208. const API_BASE_URL = 'http://192.168.66.187:8082';
  209. // ==================== Runtime ====================
  210. const [ agent ] = useXAgent({
  211. request: async ({ message, attachments }, { onSuccess, onError }) => {
  212. agentRequestLoading.value = true;
  213. try {
  214. // 调用多模态聊天接口
  215. const response = await fetch(`${API_BASE_URL}/chatbot/multimodal`, {
  216. method: 'POST',
  217. headers: {
  218. 'Content-Type': 'application/json',
  219. },
  220. body: JSON.stringify({
  221. message: message,
  222. chat_config_id: "17",
  223. user_id: 4,
  224. session_id: "",
  225. source: "pc",
  226. image_urls: imageUrls.value,
  227. documents: documentUrls.value,
  228. merchant_id: "3",
  229. model_type: "REMOTE_CLAUDE",
  230. model_name: "claude-3-7-sonnet-20250219"
  231. }),
  232. });
  233. if (!response.ok) {
  234. throw new Error(`API request failed with status ${response.status}`);
  235. }
  236. const responseData = await response.json();
  237. // 检查响应状态码
  238. if (responseData.code !== 2000) {
  239. throw new Error(`API returned error code: ${responseData.code}, message: ${responseData.message}`);
  240. }
  241. // 从响应中提取内容
  242. if (responseData.choices && responseData.choices[0].message.content) {
  243. // 将AI回复内容传递给onSuccess
  244. onSuccess(responseData.choices[0].message.content);
  245. // 可选:记录模型信息和token使用情况
  246. console.log(`Model used: ${responseData.data.model}`);
  247. if (responseData.usage) {
  248. console.log(`Token usage: ${JSON.stringify(responseData.usage)}`);
  249. }
  250. } else {
  251. throw new Error('Response data is missing content');
  252. }
  253. } catch (error) {
  254. console.error('Error calling multimodal API:', error);
  255. onError(error instanceof Error ? error : new Error('Unknown error occurred'));
  256. } finally {
  257. agentRequestLoading.value = false;
  258. // 清空URL数组
  259. imageUrls.value = [];
  260. documentUrls.value = [];
  261. }
  262. },
  263. });
  264. const { onRequest, messages, setMessages } = useXChat({
  265. agent: agent.value,
  266. onRequest: (message, attachments) => {
  267. return agent.value.request({
  268. message,
  269. attachments,
  270. });
  271. }
  272. });
  273. watch(activeKey, () => {
  274. if (activeKey.value !== undefined) {
  275. setMessages([]);
  276. }
  277. }, { immediate: true });
  278. // ==================== Event ====================
  279. const onSubmit = (nextContent: string) => {
  280. if (!nextContent) return;
  281. // 检查是否有正在上传的文件
  282. const hasUploadingFiles = attachedFiles.value.some(file => file.status === 'uploading');
  283. if (hasUploadingFiles) {
  284. // 可以显示一个提示,告诉用户等待文件上传完成
  285. console.warn('Please wait for file uploads to complete');
  286. return;
  287. }
  288. // 将附件文件传递给请求,同时传递分类后的URLs
  289. onRequest(nextContent, attachedFiles.value);
  290. // 发送后清空输入和附件
  291. content.value = '';
  292. attachedFiles.value = [];
  293. // 清空URL数组
  294. imageUrls.value = [];
  295. documentUrls.value = [];
  296. headerOpen.value = false;
  297. };
  298. const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
  299. onRequest(info.data.description as string);
  300. };
  301. const onAddConversation = () => {
  302. // 添加一个特殊的"新会话"选项
  303. const newConversationKey = 'new';
  304. // 检查是否已经有新会话选项
  305. if (!conversationsItems.value.some(item => item.key === newConversationKey)) {
  306. conversationsItems.value = [
  307. {
  308. key: newConversationKey,
  309. label: '新会话',
  310. },
  311. ...conversationsItems.value,
  312. ];
  313. }
  314. // 选中新会话
  315. activeKey.value = newConversationKey;
  316. // 清空消息
  317. setMessages([]);
  318. };
  319. const onConversationClick: ConversationsProps['onActiveChange'] = async (key) => {
  320. activeKey.value = key;
  321. // 如果是新会话,清空消息
  322. if (key === 'new') {
  323. setMessages([]);
  324. return;
  325. }
  326. // 查找选中的会话
  327. const selectedSession = conversationsItems.value.find(item => item.key === key);
  328. if (!selectedSession) return;
  329. // 这里可以添加加载特定会话消息的逻辑
  330. // 例如:
  331. try {
  332. const response = await fetch(`${API_BASE_URL}/api/chat/messages?session_id=${key}`, {
  333. method: 'GET',
  334. headers: {
  335. 'Content-Type': 'application/json',
  336. }
  337. });
  338. if (!response.ok) {
  339. throw new Error(`Failed to fetch messages: ${response.status}`);
  340. }
  341. const result = await response.json();
  342. if (result.code !== 2000) {
  343. throw new Error(`API returned error: ${result.message}`);
  344. }
  345. // 假设API返回的消息格式与您的消息格式兼容
  346. if (result.data && result.data.messages) {
  347. // 转换消息格式
  348. const formattedMessages = result.data.messages.map((msg: any) => ({
  349. id: msg.id || Math.random().toString(36).substring(2, 9),
  350. message: msg.content,
  351. status: msg.role === 'assistant' ? 'ai' : 'local'
  352. }));
  353. setMessages(formattedMessages);
  354. }
  355. } catch (error) {
  356. console.error('Error fetching messages:', error);
  357. // 如果获取失败,清空消息
  358. setMessages([]);
  359. }
  360. };
  361. const handleFileChange: AttachmentsProps['onChange'] = (info) => {
  362. console.log('File change event:', info);
  363. // 获取新添加的文件
  364. const newFile = info.file;
  365. // 保存文件列表,包括原始文件对象
  366. attachedFiles.value = info.fileList.map(file => ({
  367. ...file,
  368. // 确保保留原始文件对象以便上传
  369. originFileObj: file.originFileObj
  370. }));
  371. // 检查是否有新文件添加
  372. if (newFile && newFile.status === 'uploading' && newFile.originFileObj) {
  373. console.log('Starting upload for file:', newFile.name);
  374. // 立即上传文件
  375. uploadFile(newFile.originFileObj)
  376. .then(fileUrl => {
  377. console.log('File uploaded successfully:', fileUrl);
  378. // 更新文件状态为已上传成功
  379. attachedFiles.value = attachedFiles.value.map(file => {
  380. if (file.uid === newFile.uid) {
  381. return {
  382. ...file,
  383. status: 'done',
  384. url: fileUrl,
  385. response: { url: fileUrl }
  386. };
  387. }
  388. return file;
  389. });
  390. })
  391. .catch(error => {
  392. console.error('File upload failed:', error);
  393. // 更新文件状态为上传失败
  394. attachedFiles.value = attachedFiles.value.map(file => {
  395. if (file.uid === newFile.uid) {
  396. return {
  397. ...file,
  398. status: 'error',
  399. error
  400. };
  401. }
  402. return file;
  403. });
  404. });
  405. }
  406. };
  407. // 修改文件上传函数
  408. const uploadFile = async (file: File): Promise<string> => {
  409. const formData = new FormData();
  410. formData.append('file', file);
  411. try {
  412. const uploadResponse = await fetch(`${API_BASE_URL}/upload/file`, {
  413. method: 'POST',
  414. body: formData,
  415. });
  416. if (!uploadResponse.ok) {
  417. throw new Error(`File upload failed with status ${uploadResponse.status}`);
  418. }
  419. const uploadResult = await uploadResponse.json();
  420. // 检查上传响应状态码
  421. if (uploadResult.status !== 2000) {
  422. throw new Error(`Upload API returned error: ${uploadResult.message}`);
  423. }
  424. // 从响应中获取文件URL
  425. const fileUrl = uploadResult.data?.fileUrl;
  426. if (!fileUrl) {
  427. throw new Error('Upload response missing file URL');
  428. }
  429. // 根据文件URL后缀或文件类型判断是否为图片
  430. const isImage = (url: string, fileType: string): boolean => {
  431. // 检查URL后缀
  432. const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
  433. const hasImageExtension = imageExtensions.some(ext => url.toLowerCase().endsWith(ext));
  434. // 检查MIME类型
  435. const isImageMimeType = fileType.startsWith('image/');
  436. return hasImageExtension || isImageMimeType;
  437. };
  438. // 根据判断结果分类存储URL
  439. if (isImage(fileUrl, file.type)) {
  440. imageUrls.value.push(fileUrl);
  441. console.log('Added image URL:', fileUrl);
  442. console.log('Current image URLs:', imageUrls.value);
  443. } else {
  444. documentUrls.value.push(fileUrl);
  445. console.log('Added document URL:', fileUrl);
  446. console.log('Current document URLs:', documentUrls.value);
  447. }
  448. return fileUrl;
  449. } catch (error) {
  450. console.error('Error uploading file:', error);
  451. throw error;
  452. }
  453. };
  454. // ==================== Nodes ====================
  455. const placeholderNode = computed(() => (
  456. <Space direction="vertical" size={16} style={styles.value.placeholder}>
  457. <Welcome
  458. variant="borderless"
  459. icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
  460. title="Hello, I'm Ant Design X"
  461. description="Base on Ant Design, AGI product interface solution, create a better intelligent vision~"
  462. extra={
  463. <Space>
  464. <Button icon={<ShareAltOutlined />} />
  465. <Button icon={<EllipsisOutlined />} />
  466. </Space>
  467. }
  468. />
  469. <Prompts
  470. title="Do you want?"
  471. items={placeholderPromptsItems}
  472. styles={{
  473. list: {
  474. width: '100%',
  475. },
  476. item: {
  477. flex: 1,
  478. },
  479. }}
  480. onItemClick={onPromptsItemClick}
  481. />
  482. </Space>
  483. ));
  484. // 创建 markdown-it 实例
  485. const md = new MarkdownIt({
  486. html: true,
  487. linkify: true,
  488. typographer: true,
  489. breaks: true
  490. });
  491. const renderMarkdown: BubbleProps['messageRender'] = (content) => (
  492. <Typography>
  493. <div v-html={md.render(content)} />
  494. </Typography>
  495. );
  496. const items = computed<BubbleListProps['items']>(() => messages.value.map(({ id, message, status }) => {
  497. // 检查消息是否是 markdown 格式
  498. /* const isMarkdown = status === 'ai' &&
  499. (message.includes('#') || message.includes('**') || message.includes('\n')); */
  500. let content;
  501. /* if (status === 'ai') { */
  502. // 使用 Typography 组件包装 markdown 内容
  503. content = h('div', {
  504. class: 'markdown-content',
  505. innerHTML: md.render(message),
  506. typing: {
  507. step: 5, // 每次打印字符数
  508. interval: 20, // 打印间隔时间(ms)
  509. }
  510. });
  511. /* } else {
  512. // 普通文本内容
  513. content = message;
  514. } */
  515. return {
  516. key: id,
  517. loading: status === 'loading',
  518. role: status === 'local' ? 'local' : 'ai',
  519. content: content,
  520. };
  521. }));
  522. const attachmentsNode = computed(() => (
  523. <Badge dot={attachedFiles.value.length > 0 && !headerOpen.value}>
  524. <Button type="text" icon={<PaperClipOutlined />} onClick={() => headerOpen.value = !headerOpen.value} />
  525. </Badge>
  526. ));
  527. const senderHeader = computed(() => (
  528. <Sender.Header
  529. title="Attachments"
  530. open={headerOpen.value}
  531. onOpenChange={(open) => headerOpen.value = open}
  532. styles={{
  533. content: {
  534. padding: 0,
  535. },
  536. }}
  537. >
  538. <Attachments
  539. action={`${API_BASE_URL}/upload/file`}
  540. name="file"
  541. items={attachedFiles.value}
  542. onChange={handleFileChange}
  543. customRequest={({ file, onSuccess, onError, onProgress }) => {
  544. // 手动处理文件上传
  545. if (file && file.originFileObj) {
  546. console.log('Custom request triggered for file:', file.name);
  547. uploadFile(file.originFileObj)
  548. .then(fileUrl => {
  549. console.log('Custom upload successful:', fileUrl);
  550. onSuccess({ url: fileUrl }, new XMLHttpRequest());
  551. })
  552. .catch(err => {
  553. console.error('Custom upload failed:', err);
  554. onError(err);
  555. });
  556. }
  557. }}
  558. placeholder={(type) =>
  559. type === 'drop'
  560. ? { title: 'Drop file here' }
  561. : {
  562. icon: <CloudUploadOutlined />,
  563. title: 'Upload files',
  564. description: 'Click or drag files to this area to upload',
  565. }
  566. }
  567. />
  568. </Sender.Header>
  569. ));
  570. const logoNode = computed(() => (
  571. <div style={styles.value.logo}>
  572. <img
  573. src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
  574. draggable={false}
  575. alt="logo"
  576. style={styles.value['logo-img']}
  577. />
  578. </div>
  579. ));
  580. //<span style={styles.value['logo-span']}>Ant Design X Vue</span>
  581. // 添加 markdown 样式
  582. const markdownStyles = `
  583. .markdown-content {
  584. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  585. }
  586. .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4 {
  587. margin-top: 1em;
  588. margin-bottom: 0.5em;
  589. font-weight: 600;
  590. line-height: 1.25;
  591. }
  592. .markdown-content p {
  593. margin-bottom: 0.8em;
  594. line-height: 1.6;
  595. }
  596. .markdown-content ul, .markdown-content ol {
  597. padding-left: 1.5em;
  598. margin-bottom: 1em;
  599. }
  600. .markdown-content li {
  601. margin-bottom: 0.3em;
  602. }
  603. .markdown-content code {
  604. background-color: rgba(0, 0, 0, 0.05);
  605. padding: 0.2em 0.4em;
  606. border-radius: 3px;
  607. font-family: Consolas, Monaco, 'Andale Mono', monospace;
  608. }
  609. .markdown-content pre {
  610. background-color: rgba(0, 0, 0, 0.05);
  611. padding: 1em;
  612. border-radius: 4px;
  613. overflow-x: auto;
  614. }
  615. .markdown-content blockquote {
  616. border-left: 4px solid rgba(0, 0, 0, 0.1);
  617. padding-left: 1em;
  618. margin: 1em 0;
  619. color: rgba(0, 0, 0, 0.65);
  620. }
  621. .markdown-content a {
  622. color: #1890ff;
  623. text-decoration: none;
  624. }
  625. .markdown-content a:hover {
  626. text-decoration: underline;
  627. }
  628. `;
  629. // 修改 fetchChatHistory 函数,处理 last_message 和 user_message
  630. const fetchChatHistory = async () => {
  631. loadingHistory.value = true;
  632. try {
  633. const response = await fetch(`${API_BASE_URL}/api/chat/history`, {
  634. method: 'POST',
  635. headers: {
  636. 'Content-Type': 'application/json',
  637. },
  638. body: JSON.stringify({
  639. merchant_id: "3",
  640. user_id: 4,
  641. page: pagination.value.page,
  642. page_size: pagination.value.pageSize
  643. }),
  644. });
  645. if (!response.ok) {
  646. throw new Error(`获取聊天历史失败: ${response.status}`);
  647. }
  648. const result = await response.json();
  649. if (result.code !== 2000) {
  650. throw new Error(`API返回错误: ${result.message}`);
  651. }
  652. // 更新会话列表
  653. if (result.data && result.data.sessions) {
  654. conversationsItems.value = result.data.sessions.map((session: any) => {
  655. // 优先使用用户消息作为会话标签,如果没有则使用AI回复
  656. const displayContent = session.user_message?.content || session.last_message?.content || '新会话';
  657. return {
  658. key: session.session_id,
  659. // 截取前20个字符作为标签,如果超过则添加省略号
  660. label: displayContent.substring(0, 20) + (displayContent.length > 20 ? '...' : ''),
  661. // 保存时间戳
  662. timestamp: new Date(session.last_message?.created_at || Date.now()),
  663. // 保存用户消息和AI回复,以便在UI中显示
  664. userMessage: session.user_message,
  665. aiMessage: session.last_message,
  666. // 保存完整会话数据以备后用
  667. sessionData: session
  668. };
  669. });
  670. // 如果有会话,默认选中第一个
  671. if (conversationsItems.value.length > 0) {
  672. activeKey.value = conversationsItems.value[0].key;
  673. // 加载第一个会话的消息
  674. await loadSessionMessages(conversationsItems.value[0].key);
  675. }
  676. // 更新分页信息
  677. if (result.data.pagination) {
  678. pagination.value = {
  679. total: result.data.pagination.total,
  680. page: result.data.pagination.page,
  681. pageSize: result.data.pagination.page_size,
  682. totalPages: result.data.pagination.total_pages
  683. };
  684. }
  685. }
  686. } catch (error) {
  687. console.error('获取聊天历史出错:', error);
  688. // 如果获取失败,使用默认会话列表
  689. conversationsItems.value = defaultConversationsItems;
  690. } finally {
  691. loadingHistory.value = false;
  692. }
  693. };
  694. // 修改加载会话消息的函数,处理用户消息和AI回复
  695. const loadSessionMessages = async (sessionId: string) => {
  696. try {
  697. // 显示加载状态
  698. setMessages([{ id: 'loading', message: '加载消息中...', status: 'loading' }]);
  699. const response = await fetch(`${API_BASE_URL}/api/chat/messages?session_id=${sessionId}`, {
  700. method: 'GET',
  701. headers: {
  702. 'Content-Type': 'application/json',
  703. }
  704. });
  705. if (!response.ok) {
  706. throw new Error(`获取消息失败: ${response.status}`);
  707. }
  708. const result = await response.json();
  709. if (result.code !== 2000) {
  710. throw new Error(`API返回错误: ${result.message}`);
  711. }
  712. // 处理消息数据
  713. if (result.data && result.data.messages) {
  714. // 转换消息格式并按时间排序
  715. const formattedMessages = [];
  716. // 遍历消息,每组包含用户消息和AI回复
  717. for (const msg of result.data.messages) {
  718. // 如果有用户消息,添加到列表
  719. if (msg.user_message && msg.user_message.content) {
  720. formattedMessages.push({
  721. id: `user_${msg.id || Math.random().toString(36).substring(2, 9)}`,
  722. message: msg.user_message.content,
  723. status: 'local',
  724. timestamp: new Date(msg.user_message.created_at || Date.now())
  725. });
  726. }
  727. // 如果有AI回复,添加到列表
  728. if (msg.last_message && msg.last_message.content) {
  729. formattedMessages.push({
  730. id: `ai_${msg.id || Math.random().toString(36).substring(2, 9)}`,
  731. message: msg.last_message.content,
  732. status: 'ai',
  733. timestamp: new Date(msg.last_message.created_at || Date.now())
  734. });
  735. }
  736. }
  737. // 按时间排序
  738. formattedMessages.sort((a, b) => a.timestamp - b.timestamp);
  739. // 更新消息列表
  740. setMessages(formattedMessages);
  741. } else {
  742. // 如果没有消息,显示空列表
  743. setMessages([]);
  744. }
  745. } catch (error) {
  746. console.error('加载会话消息出错:', error);
  747. // 显示错误消息
  748. setMessages([{
  749. id: 'error',
  750. message: '加载消息失败,请重试',
  751. status: 'ai'
  752. }]);
  753. }
  754. };
  755. // 在组件挂载时获取聊天历史
  756. onMounted(() => {
  757. /* fetchChatHistory(); */
  758. });
  759. defineRender(() => {
  760. return (
  761. <div style={styles.value.layout}>
  762. {/* 添加 markdown 样式 */}
  763. <style>{markdownStyles}</style>
  764. <div style={styles.value.menu}>
  765. {/* 🌟 Logo */}
  766. {logoNode.value}
  767. {/* 🌟 添加会话 */}
  768. <Button
  769. onClick={onAddConversation}
  770. type="link"
  771. style={styles.value.addBtn}
  772. icon={<PlusOutlined />}
  773. >
  774. New Conversation
  775. </Button>
  776. {/* 🌟 会话管理 */}
  777. <Conversations
  778. items={conversationsItems.value}
  779. style={styles.value.conversations}
  780. activeKey={activeKey.value}
  781. onActiveChange={onConversationClick}
  782. />
  783. </div>
  784. <div style={styles.value.chat}>
  785. {/* 🌟 消息列表 */}
  786. <Bubble.List
  787. items={items.value.length > 0 ? items.value : [{ content: placeholderNode.value, variant: 'borderless' }]}
  788. roles={roles}
  789. style={styles.value.messages}
  790. />
  791. {/* 🌟 提示词 */}
  792. <Prompts style={{ color: token.value.colorText }} items={senderPromptsItems} onItemClick={onPromptsItemClick} />
  793. {/* 🌟 输入框 */}
  794. <Sender
  795. value={content.value}
  796. header={senderHeader.value}
  797. onSubmit={onSubmit}
  798. onChange={(value) => content.value = value}
  799. prefix={attachmentsNode.value}
  800. loading={agentRequestLoading.value}
  801. style={styles.value.sender}
  802. />
  803. </div>
  804. </div>
  805. )
  806. });
  807. </script>