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