|
@@ -0,0 +1,486 @@
|
|
|
+<template>
|
|
|
+ <div class="mini-chat">
|
|
|
+ <!-- 左侧折叠区域 -->
|
|
|
+ <div class="left-side">
|
|
|
+ <SvgIcon name="add" size="20" />
|
|
|
+ <SvgIcon name="zoom-out" size="20" />
|
|
|
+ </div>
|
|
|
+ <!-- 右侧对话框 -->
|
|
|
+ <div class="right-side">
|
|
|
+ <!-- 对话区域 -->
|
|
|
+ <div class="dialog-area">
|
|
|
+ <div
|
|
|
+ v-for="message in sortedMessages"
|
|
|
+ :key="message.id"
|
|
|
+ class="flex items-center"
|
|
|
+ :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
|
|
|
+ >
|
|
|
+ <SvgIcon v-if="message.type !== 'user'" size="30" class="ml-2px mr-2px" name="ai-logo" />
|
|
|
+ <div v-if="message.type !== 'user'" class="answer-message" v-html="formatMessage(message.content)"></div>
|
|
|
+ <div v-if="message.type === 'user'" class="ask-message">{{ message.content }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 底部操作栏 -->
|
|
|
+ <div class="input-area">
|
|
|
+ <Textarea v-model:value="inputText" placeholder="请输入你的问题" />
|
|
|
+ <div class="action-bar">
|
|
|
+ <!-- 左侧深度思考按钮 -->
|
|
|
+ <div class="think-btn" :class="{ active: isThinking }" @click="toggleThinking"> <span>深度思考</span> </div>
|
|
|
+
|
|
|
+ <!-- 右侧操作按钮 -->
|
|
|
+ <Space>
|
|
|
+ <SvgIcon name="send-image" />
|
|
|
+ <SvgIcon name="send-file" />
|
|
|
+ <Button type="primary" shape="circle" size="small" :loading="spinning" @click="handleSend">
|
|
|
+ <template #icon>
|
|
|
+ <SvgIcon name="send" />
|
|
|
+ </template>
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+ import { ref, onMounted, unref, nextTick, computed } from 'vue';
|
|
|
+ import { useUserStore } from '/@/store/modules/user';
|
|
|
+ import { EditOutlined } from '@ant-design/icons-vue';
|
|
|
+ import { SvgIcon } from '../Icon';
|
|
|
+ import { CheckableTag, Space, Button, Textarea, Switch } from 'ant-design-vue';
|
|
|
+ // import SelfComponent from '/@/components/AIChat/index.vue';
|
|
|
+ // 响应式变量声明
|
|
|
+ const dialogVisible = ref(false);
|
|
|
+ const isFold = ref(true); // 是否折叠
|
|
|
+ const inputText = ref(''); // 输入框内容
|
|
|
+ const historySessions = ref([]); // 消会话历史
|
|
|
+ const spinning = ref(false); // 加载状态
|
|
|
+ const systemMessage = ref(''); // 系统返回信息
|
|
|
+ const session_id = ref(''); // 会话id
|
|
|
+ const hasCreated = ref(false); // 标志位,防止重复调用create接口
|
|
|
+ const hasAdd = ref(false); // 标志位,防止重复调用create接口
|
|
|
+ const userStore = useUserStore(); //获取用户信息
|
|
|
+ const editingId = ref<number | null>(null);
|
|
|
+ const editText = ref('');
|
|
|
+ const isThinking = ref(false);
|
|
|
+ interface ListItem {
|
|
|
+ id: number;
|
|
|
+ title?: string;
|
|
|
+ }
|
|
|
+ let userId = unref(userStore.getUserInfo).id;
|
|
|
+ // const userId = ref(0);
|
|
|
+ type MessageItem = {
|
|
|
+ id: string; // 唯一标识(可用时间戳生成)
|
|
|
+ type: 'user' | 'system';
|
|
|
+ content: string;
|
|
|
+ timestamp: number; // 排序依据
|
|
|
+ };
|
|
|
+ const messageList = ref<MessageItem[]>([]);
|
|
|
+ const sortedMessages = computed(() => {
|
|
|
+ const list = messageList.value;
|
|
|
+ return list.sort((a, b) => a.timestamp - b.timestamp);
|
|
|
+ });
|
|
|
+ const vFocus = {
|
|
|
+ mounted: (el: HTMLElement) => el.querySelector('input')?.focus(),
|
|
|
+ };
|
|
|
+ const scrollToBottom = () => {
|
|
|
+ const dialogArea = document.querySelector('.dialog-area');
|
|
|
+ if (dialogArea) {
|
|
|
+ dialogArea.scrollTop = dialogArea.scrollHeight;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const openMenu = () => {
|
|
|
+ dialogVisible.value = !dialogVisible.value;
|
|
|
+ if (dialogVisible.value) {
|
|
|
+ // addNew();
|
|
|
+ hasCreated.value = true;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const fold = () => {
|
|
|
+ // isFold.value = !isFold.value;
|
|
|
+ // if (!isFold.value) {
|
|
|
+ // sessionsHistoryList();
|
|
|
+ // }
|
|
|
+ };
|
|
|
+ //启用深度思考
|
|
|
+ const toggleThinking = () => {
|
|
|
+ isThinking.value = !isThinking.value;
|
|
|
+ };
|
|
|
+ //创建新对话
|
|
|
+ async function addNew() {
|
|
|
+ hasAdd.value = !hasAdd.value;
|
|
|
+ const params = {
|
|
|
+ user_id: userId,
|
|
|
+ };
|
|
|
+ let response = await fetch('http://182.92.126.35:6005/sessions/create', {
|
|
|
+ method: 'post',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+ const data = await response.json();
|
|
|
+ session_id.value = data.id;
|
|
|
+ messageList.value = [];
|
|
|
+ }
|
|
|
+ //编辑标题
|
|
|
+ const startEditing = (item: ListItem) => {
|
|
|
+ editingId.value = item.id;
|
|
|
+ editText.value = item.title || '';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 保存修改
|
|
|
+ const handleSave = async (item: ListItem) => {
|
|
|
+ const params = {
|
|
|
+ chat_session_id: item.id,
|
|
|
+ new_title: editText.value,
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ let response = await fetch('http://182.92.126.35:6005/sessions/change_title', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Network response was not ok');
|
|
|
+ }
|
|
|
+ item.title = editText.value;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存失败:', error);
|
|
|
+ }
|
|
|
+ editingId.value = null;
|
|
|
+ };
|
|
|
+ //获取消息列表
|
|
|
+ async function handleSend() {
|
|
|
+ if (session_id.value === '') {
|
|
|
+ await addNew();
|
|
|
+ createSessionTitle({ session_id: session_id.value, title: inputText.value });
|
|
|
+ sendMessage1();
|
|
|
+ } else {
|
|
|
+ createSessionTitle({ session_id: session_id.value, title: inputText.value });
|
|
|
+ sendMessage1();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //发送消息
|
|
|
+ async function sendMessage() {
|
|
|
+ spinning.value = true;
|
|
|
+ // 添加用户消息
|
|
|
+ messageList.value.push({
|
|
|
+ id: `user_${Date.now()}`,
|
|
|
+ type: 'user',
|
|
|
+ content: inputText.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ const params = {
|
|
|
+ chat_session_id: session_id.value,
|
|
|
+ prompt: inputText.value,
|
|
|
+ ref_file_ids: [],
|
|
|
+ thinking_enabled: false,
|
|
|
+ };
|
|
|
+ inputText.value = ''; // 清空输入框
|
|
|
+ //将用户输入的内容发送到后端
|
|
|
+ try {
|
|
|
+ // 将用户输入的内容发送到后端
|
|
|
+ let response = await fetch('http://182.92.126.35:6005/chat', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Network response was not ok');
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ const assistantReply = data.reply.content; // 获取助手回复
|
|
|
+ // formatMessage(assistantReply);
|
|
|
+ systemMessage.value = assistantReply;
|
|
|
+
|
|
|
+ // 添加系统回答
|
|
|
+ messageList.value.push({
|
|
|
+ id: `system_${Date.now()}`,
|
|
|
+ type: 'system',
|
|
|
+ content: systemMessage.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ // 请求失败时设置系统消息为"服务器异常"
|
|
|
+ systemMessage.value = '服务器异常';
|
|
|
+ console.error('请求失败:', error);
|
|
|
+ } finally {
|
|
|
+ spinning.value = false; // 无论请求成功与否,都停止加载指示器
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //发送消息 流式响应
|
|
|
+ const sendMessage1 = async () => {
|
|
|
+ spinning.value = true; // 开始加载
|
|
|
+ messageList.value.push({
|
|
|
+ id: `user_${Date.now()}`,
|
|
|
+ type: 'user',
|
|
|
+ content: inputText.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 构造请求参数
|
|
|
+ const params = {
|
|
|
+ chat_session_id: session_id.value, // 替换为实际的会话 ID
|
|
|
+ prompt: inputText.value,
|
|
|
+ ref_file_ids: [],
|
|
|
+ thinking_enabled: isThinking.value,
|
|
|
+ };
|
|
|
+ inputText.value = ''; // 清空输入框
|
|
|
+ try {
|
|
|
+ // 发送 POST 请求
|
|
|
+ const response = await fetch('http://182.92.126.35:6005/chat_stream', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 检查响应是否成功
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Network response was not ok');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取可读流
|
|
|
+ const reader = response.body.getReader();
|
|
|
+
|
|
|
+ // 创建一条新的消息对象
|
|
|
+ const newMessage = {
|
|
|
+ id: `response_${Date.now()}`,
|
|
|
+ type: 'response', // 消息类型
|
|
|
+ content: '',
|
|
|
+ timestamp: Date.now(), // 时间戳用来排序
|
|
|
+ };
|
|
|
+
|
|
|
+ // 将新消息添加到消息列表
|
|
|
+ messageList.value.push(newMessage);
|
|
|
+
|
|
|
+ // 读取流式数据
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read();
|
|
|
+ if (done) {
|
|
|
+ console.log('Stream complete');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将流数据转换为字符串
|
|
|
+ const chunk = new TextDecoder().decode(value);
|
|
|
+ console.log('Received chunk:', chunk);
|
|
|
+
|
|
|
+ // 使用正则表达式匹配完整的 JSON 对象
|
|
|
+ const jsonRegex = /{.*?}/g;
|
|
|
+ const matches = chunk.match(jsonRegex);
|
|
|
+
|
|
|
+ if (matches) {
|
|
|
+ matches.forEach((match) => {
|
|
|
+ try {
|
|
|
+ const data = JSON.parse(match);
|
|
|
+ if (data.type === 'text') {
|
|
|
+ // 找到当前消息对象并更新 content
|
|
|
+ const targetMessage = messageList.value.find((msg) => msg.id === newMessage.id);
|
|
|
+ if (targetMessage) {
|
|
|
+ targetMessage.content += data.content; // 追加内容
|
|
|
+ scrollToBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to parse JSON:', error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 请求失败时设置系统消息
|
|
|
+ if (!response || !response.ok) {
|
|
|
+ systemMessage.value = '服务器异常';
|
|
|
+ messageList.value.push({
|
|
|
+ id: `system_${Date.now()}`,
|
|
|
+ type: 'system',
|
|
|
+ content: systemMessage.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ console.error('请求失败:', error);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ spinning.value = false; // 停止加载
|
|
|
+ }
|
|
|
+ };
|
|
|
+ //创建标题
|
|
|
+ async function createSessionTitle({ session_id, title }) {
|
|
|
+ const params = {
|
|
|
+ chat_session_id: session_id,
|
|
|
+ prompt: title,
|
|
|
+ };
|
|
|
+ let response = await fetch('http://182.92.126.35:6005/sessions/title', {
|
|
|
+ method: 'post',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+ const data = await response.json();
|
|
|
+ }
|
|
|
+ //获取会话历史
|
|
|
+ async function sessionsHistoryList() {
|
|
|
+ const params = {
|
|
|
+ user_id: userId,
|
|
|
+ };
|
|
|
+ let response = await fetch(`http://182.92.126.35:6005/sessions`, {
|
|
|
+ method: 'post',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+ const data = await response.json();
|
|
|
+ historySessions.value = data.chat_sessions;
|
|
|
+ }
|
|
|
+ //获取具体会话记录
|
|
|
+ async function sessionsHistory(id: string) {
|
|
|
+ let response = await fetch(`http://182.92.126.35:6005/sessions/history_chat/?chat_session_id=${id}`, {
|
|
|
+ method: 'get',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ const data = await response.json();
|
|
|
+ if (data.chat_messages.length > 0) {
|
|
|
+ messageList.value = [];
|
|
|
+ data.chat_messages.forEach((item: any) => {
|
|
|
+ // role== user 用户提问
|
|
|
+ if (item.role === 'user') {
|
|
|
+ messageList.value.push({
|
|
|
+ id: `user_${Date.now()}`,
|
|
|
+ type: 'user',
|
|
|
+ content: item.content,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // role== assistant 机器回答
|
|
|
+ messageList.value.push({
|
|
|
+ id: `system_${Date.now()}`,
|
|
|
+ type: 'system',
|
|
|
+ content: item.content,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //格式化消息
|
|
|
+ function formatMessage(text: string) {
|
|
|
+ let formatted = text
|
|
|
+ // 处理换行
|
|
|
+ .replace(/\n\n/g, '<br>')
|
|
|
+ .replace(/\n###/g, '<br> ')
|
|
|
+ .replace(/###/g, '')
|
|
|
+ .replace(/---/g, '')
|
|
|
+ // 处理粗体
|
|
|
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
|
+ // 处理斜体
|
|
|
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
|
+ // 处理行内代码
|
|
|
+ .replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
|
+ return formatted;
|
|
|
+ }
|
|
|
+ // 初始化按钮定位
|
|
|
+ onMounted(() => {
|
|
|
+ sessionsHistoryList();
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+ .mini-chat {
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-side {
|
|
|
+ width: 40px; /* 折叠时宽度 */
|
|
|
+ background: #0c2842;
|
|
|
+ transition: width 0.5s ease; /* 平滑过渡动画 */
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: space-around;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right-side {
|
|
|
+ flex: 1; /* 占据剩余空间 */
|
|
|
+ background: #09172c;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ .dialog-area {
|
|
|
+ flex: 1; /* 占据剩余空间 */
|
|
|
+ gap: 10px; /* 消息块间隔统一控制 */
|
|
|
+ overflow-y: auto; /* 垂直滚动条 */
|
|
|
+ padding: 5px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ color: #fff;
|
|
|
+
|
|
|
+ .ask-message {
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 5px;
|
|
|
+ background: #0c2842;
|
|
|
+ }
|
|
|
+ .answer-message {
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 5px;
|
|
|
+ background: #0c2842;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-area {
|
|
|
+ background-color: #043256;
|
|
|
+ padding: 5px;
|
|
|
+
|
|
|
+ textarea {
|
|
|
+ background-color: transparent;
|
|
|
+ width: 100%;
|
|
|
+ height: 40px;
|
|
|
+ border: none;
|
|
|
+ resize: none;
|
|
|
+ outline: none;
|
|
|
+ overflow: hidden;
|
|
|
+ padding: 10px; /* 统一内边距 */
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-bar {
|
|
|
+ height: 30px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ .think-btn {
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ width: 100px;
|
|
|
+ height: 20px;
|
|
|
+ line-height: 20px;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ background: transparent;
|
|
|
+ color: white;
|
|
|
+ transition: background 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .think-btn.active {
|
|
|
+ background: #1890ff;
|
|
|
+ color: white;
|
|
|
+ border-color: #1890ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|