independent.vue 23 KB

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