MiniChat.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <template>
  2. <div class="mini-chat">
  3. <!-- 左侧折叠区域 -->
  4. <div class="left-side">
  5. <SvgIcon name="add" size="20" />
  6. <Popover trigger="click" :overlay-inner-style="{ padding: '1px' }">
  7. <template #content>
  8. <AIChat style="width: 700px; height: 500px" :visible="true" />
  9. </template>
  10. <SvgIcon :name="dialogVisible ? 'zoom-out' : 'zoom-in'" size="20" @click="openDialog" />
  11. </Popover>
  12. </div>
  13. <!-- 右侧对话框 -->
  14. <div class="right-side">
  15. <!-- 对话区域 -->
  16. <div class="dialog-area">
  17. <div
  18. v-for="message in sortedMessages"
  19. :key="message.id"
  20. class="flex items-center"
  21. :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
  22. >
  23. <SvgIcon v-if="message.type !== 'user'" size="30" class="ml-2px mr-2px" name="ai-logo" />
  24. <div v-if="message.type !== 'user'" class="answer-message" v-html="formatMessage(message.content)"></div>
  25. <div v-if="message.type === 'user'" class="ask-message">{{ message.content }}</div>
  26. </div>
  27. </div>
  28. <!-- 底部操作栏 -->
  29. <div class="input-area">
  30. <TextArea v-model="inputText" placeholder="请输入你的问题" />
  31. <div class="action-bar">
  32. <!-- 左侧深度思考按钮 -->
  33. <div class="think-btn" :class="{ active: isThinking }" @click="toggleThinking"> <span>深度思考</span> </div>
  34. <!-- 右侧操作按钮 -->
  35. <Space>
  36. <SvgIcon name="send-image" />
  37. <SvgIcon name="send-file" />
  38. <Button type="primary" shape="circle" size="small" :loading="spinning" @click="handleSend">
  39. <template #icon>
  40. <SvgIcon name="send" />
  41. </template>
  42. </Button>
  43. </Space>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. </template>
  49. <script lang="ts" setup>
  50. import { ref, onMounted, unref, computed, defineComponent } from 'vue';
  51. import { useUserStore } from '/@/store/modules/user';
  52. import { SvgIcon } from '../Icon';
  53. import { Space, Button, Popover, Input } from 'ant-design-vue';
  54. import AIChat from './index.vue';
  55. const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
  56. // 响应式变量声明
  57. const dialogVisible = ref(true);
  58. const isFold = ref(true); // 是否折叠
  59. const inputText = ref(''); // 输入框内容
  60. const historySessions = ref([]); // 消会话历史
  61. const spinning = ref(false); // 加载状态
  62. const systemMessage = ref(''); // 系统返回信息
  63. const session_id = ref(''); // 会话id
  64. const hasCreated = ref(false); // 标志位,防止重复调用create接口
  65. const hasAdd = ref(false); // 标志位,防止重复调用create接口
  66. const userStore = useUserStore(); //获取用户信息
  67. const editingId = ref<number | null>(null);
  68. const editText = ref('');
  69. const isThinking = ref(false);
  70. interface ListItem {
  71. id: number;
  72. title?: string;
  73. }
  74. let userId = unref(userStore.getUserInfo).id;
  75. // const userId = ref(0);
  76. type MessageItem = {
  77. id: string; // 唯一标识(可用时间戳生成)
  78. type: 'user' | 'system';
  79. content: string;
  80. timestamp: number; // 排序依据
  81. };
  82. const messageList = ref<MessageItem[]>([]);
  83. const sortedMessages = computed(() => {
  84. const list = messageList.value;
  85. return list.sort((a, b) => a.timestamp - b.timestamp);
  86. });
  87. const vFocus = {
  88. mounted: (el: HTMLElement) => el.querySelector('input')?.focus(),
  89. };
  90. const scrollToBottom = () => {
  91. const dialogArea = document.querySelector('.dialog-area');
  92. if (dialogArea) {
  93. dialogArea.scrollTop = dialogArea.scrollHeight;
  94. }
  95. };
  96. const openDialog = () => {
  97. dialogVisible.value = !dialogVisible.value;
  98. };
  99. const fold = () => {
  100. // isFold.value = !isFold.value;
  101. // if (!isFold.value) {
  102. // sessionsHistoryList();
  103. // }
  104. };
  105. //启用深度思考
  106. const toggleThinking = () => {
  107. isThinking.value = !isThinking.value;
  108. };
  109. //创建新对话
  110. async function addNew() {
  111. hasAdd.value = !hasAdd.value;
  112. const params = {
  113. user_id: userId,
  114. };
  115. let response = await fetch('http://182.92.126.35:6005/sessions/create', {
  116. method: 'post',
  117. headers: {
  118. 'Content-Type': 'application/json',
  119. },
  120. body: JSON.stringify(params),
  121. });
  122. const data = await response.json();
  123. session_id.value = data.id;
  124. messageList.value = [];
  125. }
  126. //编辑标题
  127. const startEditing = (item: ListItem) => {
  128. editingId.value = item.id;
  129. editText.value = item.title || '';
  130. };
  131. // 保存修改
  132. const handleSave = async (item: ListItem) => {
  133. const params = {
  134. chat_session_id: item.id,
  135. new_title: editText.value,
  136. };
  137. try {
  138. let response = await fetch('http://182.92.126.35:6005/sessions/change_title', {
  139. method: 'POST',
  140. headers: {
  141. 'Content-Type': 'application/json',
  142. },
  143. body: JSON.stringify(params),
  144. });
  145. if (!response.ok) {
  146. throw new Error('Network response was not ok');
  147. }
  148. item.title = editText.value;
  149. } catch (error) {
  150. console.error('保存失败:', error);
  151. }
  152. editingId.value = null;
  153. };
  154. //获取消息列表
  155. async function handleSend() {
  156. if (session_id.value === '') {
  157. await addNew();
  158. createSessionTitle({ session_id: session_id.value, title: inputText.value });
  159. sendMessage1();
  160. } else {
  161. createSessionTitle({ session_id: session_id.value, title: inputText.value });
  162. sendMessage1();
  163. }
  164. }
  165. //发送消息
  166. async function sendMessage() {
  167. spinning.value = true;
  168. // 添加用户消息
  169. messageList.value.push({
  170. id: `user_${Date.now()}`,
  171. type: 'user',
  172. content: inputText.value,
  173. timestamp: Date.now(),
  174. });
  175. const params = {
  176. chat_session_id: session_id.value,
  177. prompt: inputText.value,
  178. ref_file_ids: [],
  179. thinking_enabled: false,
  180. };
  181. inputText.value = ''; // 清空输入框
  182. //将用户输入的内容发送到后端
  183. try {
  184. // 将用户输入的内容发送到后端
  185. let response = await fetch('http://182.92.126.35:6005/chat', {
  186. method: 'POST',
  187. headers: {
  188. 'Content-Type': 'application/json',
  189. },
  190. body: JSON.stringify(params),
  191. });
  192. if (!response.ok) {
  193. throw new Error('Network response was not ok');
  194. }
  195. const data = await response.json();
  196. const assistantReply = data.reply.content; // 获取助手回复
  197. // formatMessage(assistantReply);
  198. systemMessage.value = assistantReply;
  199. // 添加系统回答
  200. messageList.value.push({
  201. id: `system_${Date.now()}`,
  202. type: 'system',
  203. content: systemMessage.value,
  204. timestamp: Date.now(),
  205. });
  206. } catch (error) {
  207. // 请求失败时设置系统消息为"服务器异常"
  208. systemMessage.value = '服务器异常';
  209. console.error('请求失败:', error);
  210. } finally {
  211. spinning.value = false; // 无论请求成功与否,都停止加载指示器
  212. }
  213. }
  214. //发送消息 流式响应
  215. const sendMessage1 = async () => {
  216. spinning.value = true; // 开始加载
  217. messageList.value.push({
  218. id: `user_${Date.now()}`,
  219. type: 'user',
  220. content: inputText.value,
  221. timestamp: Date.now(),
  222. });
  223. // 构造请求参数
  224. const params = {
  225. chat_session_id: session_id.value, // 替换为实际的会话 ID
  226. prompt: inputText.value,
  227. ref_file_ids: [],
  228. thinking_enabled: isThinking.value,
  229. };
  230. inputText.value = ''; // 清空输入框
  231. try {
  232. // 发送 POST 请求
  233. const response = await fetch('http://182.92.126.35:6005/chat_stream', {
  234. method: 'POST',
  235. headers: {
  236. 'Content-Type': 'application/json',
  237. },
  238. body: JSON.stringify(params),
  239. });
  240. // 检查响应是否成功
  241. if (!response.ok) {
  242. throw new Error('Network response was not ok');
  243. }
  244. // 获取可读流
  245. const reader = response.body.getReader();
  246. // 创建一条新的消息对象
  247. const newMessage = {
  248. id: `response_${Date.now()}`,
  249. type: 'response', // 消息类型
  250. content: '',
  251. timestamp: Date.now(), // 时间戳用来排序
  252. };
  253. // 将新消息添加到消息列表
  254. messageList.value.push(newMessage);
  255. // 读取流式数据
  256. while (true) {
  257. const { done, value } = await reader.read();
  258. if (done) {
  259. console.log('Stream complete');
  260. break;
  261. }
  262. // 将流数据转换为字符串
  263. const chunk = new TextDecoder().decode(value);
  264. console.log('Received chunk:', chunk);
  265. // 使用正则表达式匹配完整的 JSON 对象
  266. const jsonRegex = /{.*?}/g;
  267. const matches = chunk.match(jsonRegex);
  268. if (matches) {
  269. matches.forEach((match) => {
  270. try {
  271. const data = JSON.parse(match);
  272. if (data.type === 'text') {
  273. // 找到当前消息对象并更新 content
  274. const targetMessage = messageList.value.find((msg) => msg.id === newMessage.id);
  275. if (targetMessage) {
  276. targetMessage.content += data.content; // 追加内容
  277. scrollToBottom();
  278. }
  279. }
  280. } catch (error) {
  281. console.error('Failed to parse JSON:', error);
  282. }
  283. });
  284. }
  285. }
  286. } catch (error) {
  287. // 请求失败时设置系统消息
  288. if (!response || !response.ok) {
  289. systemMessage.value = '服务器异常';
  290. messageList.value.push({
  291. id: `system_${Date.now()}`,
  292. type: 'system',
  293. content: systemMessage.value,
  294. timestamp: Date.now(),
  295. });
  296. console.error('请求失败:', error);
  297. }
  298. } finally {
  299. spinning.value = false; // 停止加载
  300. }
  301. };
  302. //创建标题
  303. async function createSessionTitle({ session_id, title }) {
  304. const params = {
  305. chat_session_id: session_id,
  306. prompt: title,
  307. };
  308. let response = await fetch('http://182.92.126.35:6005/sessions/title', {
  309. method: 'post',
  310. headers: {
  311. 'Content-Type': 'application/json',
  312. },
  313. body: JSON.stringify(params),
  314. });
  315. const data = await response.json();
  316. }
  317. //获取会话历史
  318. async function sessionsHistoryList() {
  319. const params = {
  320. user_id: userId,
  321. };
  322. let response = await fetch(`http://182.92.126.35:6005/sessions`, {
  323. method: 'post',
  324. headers: {
  325. 'Content-Type': 'application/json',
  326. },
  327. body: JSON.stringify(params),
  328. });
  329. const data = await response.json();
  330. historySessions.value = data.chat_sessions;
  331. }
  332. //获取具体会话记录
  333. async function sessionsHistory(id: string) {
  334. let response = await fetch(`http://182.92.126.35:6005/sessions/history_chat/?chat_session_id=${id}`, {
  335. method: 'get',
  336. headers: {
  337. 'Content-Type': 'application/json',
  338. },
  339. });
  340. const data = await response.json();
  341. if (data.chat_messages.length > 0) {
  342. messageList.value = [];
  343. data.chat_messages.forEach((item: any) => {
  344. // role== user 用户提问
  345. if (item.role === 'user') {
  346. messageList.value.push({
  347. id: `user_${Date.now()}`,
  348. type: 'user',
  349. content: item.content,
  350. timestamp: Date.now(),
  351. });
  352. } else {
  353. // role== assistant 机器回答
  354. messageList.value.push({
  355. id: `system_${Date.now()}`,
  356. type: 'system',
  357. content: item.content,
  358. timestamp: Date.now(),
  359. });
  360. }
  361. });
  362. }
  363. }
  364. //格式化消息
  365. function formatMessage(text: string) {
  366. let formatted = text
  367. // 处理换行
  368. .replace(/\n\n/g, '<br>')
  369. .replace(/\n###/g, '<br> ')
  370. .replace(/###/g, '')
  371. .replace(/---/g, '')
  372. // 处理粗体
  373. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  374. // 处理斜体
  375. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  376. // 处理行内代码
  377. .replace(/`([^`]+)`/g, '<code>$1</code>');
  378. return formatted;
  379. }
  380. // 初始化按钮定位
  381. onMounted(() => {
  382. sessionsHistoryList();
  383. });
  384. </script>
  385. <style lang="less" scoped>
  386. .mini-chat {
  387. display: flex;
  388. }
  389. .left-side {
  390. width: 40px; /* 折叠时宽度 */
  391. background: #0c2842;
  392. transition: width 0.5s ease; /* 平滑过渡动画 */
  393. display: flex;
  394. flex-direction: column;
  395. justify-content: space-around;
  396. align-items: center;
  397. }
  398. .right-side {
  399. flex: 1; /* 占据剩余空间 */
  400. background: #09172c;
  401. display: flex;
  402. flex-direction: column;
  403. .dialog-area {
  404. flex: 1; /* 占据剩余空间 */
  405. gap: 10px; /* 消息块间隔统一控制 */
  406. overflow-y: auto; /* 垂直滚动条 */
  407. padding: 5px;
  408. display: flex;
  409. flex-direction: column;
  410. color: #fff;
  411. .ask-message {
  412. padding: 10px;
  413. border-radius: 5px;
  414. background: #0c2842;
  415. }
  416. .answer-message {
  417. padding: 10px;
  418. border-radius: 5px;
  419. background: #0c2842;
  420. }
  421. }
  422. .input-area {
  423. background-color: #043256;
  424. padding: 5px;
  425. textarea {
  426. background-color: transparent;
  427. width: 100%;
  428. height: 40px;
  429. border: none;
  430. resize: none;
  431. outline: none;
  432. overflow: hidden;
  433. padding: 10px; /* 统一内边距 */
  434. color: #fff;
  435. }
  436. .action-bar {
  437. height: 30px;
  438. display: flex;
  439. align-items: center;
  440. justify-content: space-between;
  441. .think-btn {
  442. border: 1px solid #ccc;
  443. width: 100px;
  444. height: 20px;
  445. line-height: 20px;
  446. text-align: center;
  447. border-radius: 5px;
  448. cursor: pointer;
  449. background: transparent;
  450. color: white;
  451. transition: background 0.3s;
  452. }
  453. .think-btn.active {
  454. background: #1890ff;
  455. color: white;
  456. border-color: #1890ff;
  457. }
  458. }
  459. }
  460. }
  461. </style>
  462. <style>
  463. .zxm-popover-inner-content {
  464. padding: 1px;
  465. }
  466. </style>