index.vue 23 KB

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