Commit 70990881 by User

AI聊天集成

parent 083691ba
# backend service base url, prod environment # backend service base url, prod environment
VITE_SERVICE_BASE_URL='http://localhost:80' VITE_SERVICE_BASE_URL=''
# other backend service base url, prod environment # other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{ VITE_OTHER_SERVICE_BASE_URL= `{
......
# backend service base url, test environment # backend service base url, test environment
VITE_SERVICE_BASE_URL='http://localhost:80' VITE_SERVICE_BASE_URL=''
# other backend service base url, test environment # other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{ VITE_OTHER_SERVICE_BASE_URL= `{
......
<!doctype html> <!doctype html>
<html lang="zh-cmn-Hans"> <html lang="zh-cmn-Hans">
<head> <head>
<meta name="buildTime" content="2025-06-10 15:32:11"> <meta name="buildTime" content="2025-06-10 20:28:50">
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<title>VueDashboard</title> <title>VueDashboard</title>
<script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-BV5IaHCk.js"></script> <script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-B_dt3SP8.js"></script>
<link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-BFEuYhFr.css"> <link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-BLjiwC98.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
......
...@@ -13,49 +13,49 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform'; ...@@ -13,49 +13,49 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route * @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/ */
const customRoutes: CustomRoute[] = [ const customRoutes: CustomRoute[] = [
{ // {
name: 'exception', // name: 'exception',
path: '/exception', // path: '/exception',
component: 'layout.base', // component: 'layout.base',
meta: { // meta: {
title: 'exception', // title: 'exception',
i18nKey: 'route.exception', // i18nKey: 'route.exception',
icon: 'ant-design:exception-outlined', // icon: 'ant-design:exception-outlined',
order: 7 // order: 7
}, // },
children: [ // children: [
{ // {
name: 'exception_403', // name: 'exception_403',
path: '/exception/403', // path: '/exception/403',
component: 'view.403', // component: 'view.403',
meta: { // meta: {
title: 'exception_403', // title: 'exception_403',
i18nKey: 'route.exception_403', // i18nKey: 'route.exception_403',
icon: 'ic:baseline-block' // icon: 'ic:baseline-block'
} // }
}, // },
{ // {
name: 'exception_404', // name: 'exception_404',
path: '/exception/404', // path: '/exception/404',
component: 'view.404', // component: 'view.404',
meta: { // meta: {
title: 'exception_404', // title: 'exception_404',
i18nKey: 'route.exception_404', // i18nKey: 'route.exception_404',
icon: 'ic:baseline-web-asset-off' // icon: 'ic:baseline-web-asset-off'
} // }
}, // },
{ // {
name: 'exception_500', // name: 'exception_500',
path: '/exception/500', // path: '/exception/500',
component: 'view.500', // component: 'view.500',
meta: { // meta: {
title: 'exception_500', // title: 'exception_500',
i18nKey: 'route.exception_500', // i18nKey: 'route.exception_500',
icon: 'ic:baseline-wifi-off' // icon: 'ic:baseline-wifi-off'
} // }
} // }
] // ]
}, // },
// 以下是iframe-page的示例 // 以下是iframe-page的示例
...@@ -194,82 +194,82 @@ const customRoutes: CustomRoute[] = [ ...@@ -194,82 +194,82 @@ const customRoutes: CustomRoute[] = [
// 以下是获取菜单的示例 // 以下是获取菜单的示例
// ${window.uiGlobalConfig.InternalCode} // ${window.uiGlobalConfig.InternalCode}
// const { data: menus } = await getRootMenu(`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=dashboard`); // const { data: menus } = await getRootMenu(`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=dashboard`);
// // const { data: menus } = await getRootMenu( const { data: menus } = await getRootMenu(
// // `/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=${window.uiGlobalConfig.InternalCode}` `/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=${window.uiGlobalConfig.InternalCode}`
// // ); );
// const MenuThree = await getMenuThree(menus?.MenusMain?.Results); const MenuThree = await getMenuThree(menus?.MenusMain?.Results);
// const MenuRoot = menus?.MenuRoot; const MenuRoot = menus?.MenuRoot;
// // console.log(MenuRoot); // // console.log(MenuRoot);
// // 存储 MenuRoot 到 store // // 存储 MenuRoot 到 store
// if (MenuRoot) { if (MenuRoot) {
// setTimeout(() => { setTimeout(() => {
// const routeStore = useRouteStore(); const routeStore = useRouteStore();
// routeStore.setMenuRoot(MenuRoot); routeStore.setMenuRoot(MenuRoot);
// }, 1000); }, 1000);
// } }
// const MenuThree2 = generateRoutes(MenuThree); const MenuThree2 = generateRoutes(MenuThree);
// if (MenuThree2.length > 0) { if (MenuThree2.length > 0) {
// for (let i = 0; i < MenuThree2.length; i++) { for (let i = 0; i < MenuThree2.length; i++) {
// customRoutes.push(MenuThree2[i]); customRoutes.push(MenuThree2[i]);
// } }
// } }
// function getMenuThree(data: any) { function getMenuThree(data: any) {
// const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆 const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆
// return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => { return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => {
// const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid === child.ParentKvid); const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid === child.ParentKvid);
// // eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
// branchArr.length > 0 ? (father.children = branchArr) : ''; branchArr.length > 0 ? (father.children = branchArr) : '';
// // eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
// return father.ParentKvid == undefined; return father.ParentKvid == undefined;
// }); });
// } }
// function generateRoutes(data: any[]) { function generateRoutes(data: any[]) {
// return data.map(item => { return data.map(item => {
// const route: any = { const route: any = {
// name: item.Type === 'System' ? `${item.Type}` : item.Kvid, name: item.Type === 'System' ? `${item.Type}` : item.Kvid,
// path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`, path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`,
// component: 'layout.base', component: 'layout.base',
// meta: { meta: {
// title: item.DisplayName, title: item.DisplayName,
// icon: 'mdi:file-document-multiple-outline', icon: 'mdi:file-document-multiple-outline',
// order: item.SortId, order: item.SortId,
// keepAlive: true keepAlive: true
// }, },
// children: [] children: []
// }; };
// if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
// route.children = item.children.map( route.children = item.children.map(
// (child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => { (child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => {
// const sanitizedRemark = const sanitizedRemark =
// child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark; child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark;
// return { return {
// name: child.Type === 'System' ? `${item.Type}_${sanitizedRemark}` : `${item.Kvid}_${child.Kvid}`, name: child.Type === 'System' ? `${item.Type}_${sanitizedRemark}` : `${item.Kvid}_${child.Kvid}`,
// path: child.Type === 'System' ? `/${item.Type}/${sanitizedRemark}` : `/${item.Kvid}/${child.Kvid}`, path: child.Type === 'System' ? `/${item.Type}/${sanitizedRemark}` : `/${item.Kvid}/${child.Kvid}`,
// component: 'view.iframe-page', component: 'view.iframe-page',
// props: { props: {
// url: child.Type === 'System' ? sanitizedRemark : '', url: child.Type === 'System' ? sanitizedRemark : '',
// kvid: child.Kvid, kvid: child.Kvid,
// type: child.Type type: child.Type
// }, },
// meta: { meta: {
// title: child.DisplayName, title: child.DisplayName,
// icon: child.Icon, icon: child.Icon,
// order: child.SortId, order: child.SortId,
// keepAlive: true, keepAlive: true,
// type: 'iframe' type: 'iframe'
// } }
// }; };
// } }
// ); );
// } }
// return route; return route;
// }); });
// } }
export function createStaticRoutes() { export function createStaticRoutes() {
const constantRoutes: ElegantRoute[] = []; const constantRoutes: ElegantRoute[] = [];
......
...@@ -7,6 +7,7 @@ import { loadModule } from 'vue3-sfc-loader'; ...@@ -7,6 +7,7 @@ import { loadModule } from 'vue3-sfc-loader';
import { getSelectMenu } from '@/service/api'; import { getSelectMenu } from '@/service/api';
import NotFound from '@/views/_builtin/404/index.vue'; // 引入 404 组件 import NotFound from '@/views/_builtin/404/index.vue'; // 引入 404 组件
import { useTabStore } from '@/store/modules/tab'; import { useTabStore } from '@/store/modules/tab';
import ChatComponent from '@/views/chat/chat-component.vue'; // 导入聊天组件
import ExtJsComponent from './extJs.vue'; import ExtJsComponent from './extJs.vue';
import WebviewComponent from './webview.vue'; import WebviewComponent from './webview.vue';
import VueComponent from './vueComponent.vue'; // 添加新组件导入 import VueComponent from './vueComponent.vue'; // 添加新组件导入
...@@ -62,19 +63,22 @@ const cleanupResources = () => { ...@@ -62,19 +63,22 @@ const cleanupResources = () => {
hasError.value = false; hasError.value = false;
}; };
// 监听标签关闭事件 // 监听函数引用
tabCloseEventBus.on(closedTabId => { const tabCloseHandler = (closedTabId: string) => {
// 只有当关闭的是当前标签时才清理资源 // 只有当关闭的是当前标签时才清理资源
if (closedTabId === tabStore.activeTabId) { if (closedTabId === tabStore.activeTabId) {
cleanupResources(); cleanupResources();
} }
}); };
// 监听标签关闭事件
tabCloseEventBus.on(tabCloseHandler);
// 组件卸载前清理 // 组件卸载前清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
cleanupResources(); cleanupResources();
// 移除事件监听 // 移除事件监听
tabCloseEventBus.off(); tabCloseEventBus.off(tabCloseHandler);
}); });
// 修复console[type]的类型错误 // 修复console[type]的类型错误
...@@ -90,8 +94,22 @@ const safeConsoleLog = (type: string, ...args: any[]) => { ...@@ -90,8 +94,22 @@ const safeConsoleLog = (type: string, ...args: any[]) => {
} }
}; };
// 定义加载外部组件的函数 // 内部组件映射表
const internalComponents: Record<string, any> = {
'chat-component.vue': ChatComponent
};
// 定义加载组件的函数(优先加载内部组件)
const loadExternalComponent = async (url: string) => { const loadExternalComponent = async (url: string) => {
// 首先检查是否为内部组件
const componentName = url.split('/').pop() || url;
if (internalComponents[componentName]) {
asyncComponent.value = internalComponents[componentName];
console.log('Loaded internal component:', componentName);
return;
}
// 如果不是内部组件,则从服务端加载
const options = { const options = {
moduleCache: { moduleCache: {
vue: await import('vue') vue: await import('vue')
......
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { type ModelOption, deepSeekService } from '@/service/api/deepseek';
import { renderMarkdownWithMath as renderContentWithMath } from '@/utils/katex-renderer';
interface ChatMessage {
id: string;
from: 'user' | 'ai';
content: string;
timestamp: number;
thinkingTime?: number;
contentType?: 'text' | 'markdown';
thinkingContent?: string;
isShowingThinking?: boolean;
currentThinkingText?: string;
displayContent?: string; // 用于渐进显示内容
isTyping?: boolean; // 是否正在打字
phase?: 'thinking' | 'answering' | 'completed'; // 当前阶段
}
interface ChatHistory {
id: string;
title: string;
lastMessage: string;
timestamp: number;
messages: ChatMessage[];
}
// 响应式数据
const searchText = ref('');
const inputValue = ref('');
const currentChatId = ref('1');
const showStartPage = ref(true);
const messages = ref<ChatMessage[]>([]);
const isAiThinking = ref(false);
const thinkingStartTime = ref(0);
const thinkingTime = ref(0);
const thinkingInterval = ref<NodeJS.Timeout | null>(null);
const lastThinkingTime = ref(0);
// 配置相关
const showConfig = ref(false);
const selectedModelId = ref('deepseek');
const testing = ref(false);
const modelOptions = ref<ModelOption[]>([]);
// 聊天历史数据
const chatHistory = ref<ChatHistory[]>([
{
id: '1',
title: '新对话',
lastMessage: '',
timestamp: Date.now(),
messages: []
}
]);
// 计算属性
const filteredChatHistory = computed(() => {
if (!searchText.value) return chatHistory.value;
return chatHistory.value.filter(chat => chat.title.toLowerCase().includes(searchText.value.toLowerCase()));
});
const currentChat = computed(() => {
return chatHistory.value.find(chat => chat.id === currentChatId.value);
});
const isApiConfigured = computed(() => {
return selectedModelId.value !== '';
});
const currentModelName = computed(() => {
if (!selectedModelId.value) return 'AI助手';
const currentModel = modelOptions.value.find(model => model.id === selectedModelId.value);
return currentModel?.name || 'AI助手';
});
// 聊天容器引用
const chatContentRef = ref<HTMLElement | null>(null);
// 滚动到底部
const scrollToBottom = () => {
if (chatContentRef.value) {
setTimeout(() => {
if (chatContentRef.value) {
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight;
}
}, 50);
}
};
const newChat = () => {
const newChatId = `chat_${Date.now()}`;
const newChatItem: ChatHistory = {
id: newChatId,
title: '新对话',
lastMessage: '',
timestamp: Date.now(),
messages: []
};
chatHistory.value.unshift(newChatItem);
currentChatId.value = newChatId;
showStartPage.value = true;
messages.value = [];
inputValue.value = '';
};
const switchChat = (chatId: string) => {
const chat = chatHistory.value.find(c => c.id === chatId);
if (chat) {
currentChatId.value = chatId;
messages.value = [...chat.messages];
showStartPage.value = chat.messages.length === 0;
}
};
// 配置相关方法
const onModelChange = () => {
if (selectedModelId.value) {
deepSeekService.switchModel(selectedModelId.value);
}
};
const saveConfig = () => {
localStorage.setItem('deepseek-selected-model', selectedModelId.value);
deepSeekService.switchModel(selectedModelId.value);
showConfig.value = false;
// alert('配置已保存!');
};
const testConnection = async () => {
testing.value = true;
try {
deepSeekService.switchModel(selectedModelId.value);
await deepSeekService.chat([{ role: 'user', content: 'Hello' }], { maxTokens: 10 });
// alert('连接测试成功!');
} catch (error) {
// alert(`连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
testing.value = false;
}
};
// 思考相关方法
const startThinking = () => {
isAiThinking.value = true;
thinkingStartTime.value = Date.now();
thinkingTime.value = 0;
thinkingInterval.value = setInterval(() => {
thinkingTime.value = Math.floor((Date.now() - thinkingStartTime.value) / 1000);
}, 1000);
};
const stopThinking = () => {
isAiThinking.value = false;
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
thinkingInterval.value = null;
}
};
// 模拟思考过程显示
const showThinkingProcess = (message: ChatMessage, content: string) => {
let currentIndex = 0;
message.currentThinkingText = '';
message.isShowingThinking = true;
const typeNextChar = () => {
if (currentIndex < content.length) {
message.currentThinkingText += content[currentIndex];
currentIndex += 1;
setTimeout(typeNextChar, 15); // 快速显示思考过程
} else {
message.isShowingThinking = false;
}
};
typeNextChar();
};
// 更新聊天历史
const updateChatHistory = () => {
const currentChatItem = chatHistory.value.find(chat => chat.id === currentChatId.value);
if (currentChatItem) {
currentChatItem.messages = [...messages.value];
currentChatItem.lastMessage = messages.value[messages.value.length - 1]?.content || '';
currentChatItem.timestamp = Date.now();
// 如果是第一条消息,更新聊天标题
if (currentChatItem.title === '新对话' && messages.value.length > 0) {
const firstUserMessage = messages.value.find(msg => msg.from === 'user');
if (firstUserMessage) {
currentChatItem.title =
firstUserMessage.content.slice(0, 20) + (firstUserMessage.content.length > 20 ? '...' : '');
}
}
}
};
// 混合方案:思考过程 + 真正流式输出
const onSubmit = async () => {
if (!inputValue.value.trim() || !isApiConfigured.value) return;
showStartPage.value = false;
// 添加用户消息
const userMessage: ChatMessage = {
id: `msg_${Date.now()}`,
from: 'user',
content: inputValue.value,
timestamp: Date.now(),
contentType: 'text'
};
messages.value.push(userMessage);
scrollToBottom();
const question = inputValue.value;
inputValue.value = '';
// 开始思考状态
startThinking();
// 创建AI消息,初始为思考阶段
const aiMessage: ChatMessage = {
id: `msg_ai_${Date.now()}`,
from: 'ai',
content: '',
timestamp: Date.now(),
contentType: 'markdown',
phase: 'thinking',
isShowingThinking: true,
currentThinkingText: '',
displayContent: '',
isTyping: false
};
messages.value.push(aiMessage);
scrollToBottom();
// 显示思考过程
const thinkingContent = `分析用户问题:${question}
让我仔细思考这个问题的关键要素和可能的解答方向...
首先需要理解问题的核心内容和用户的真实需求。
接下来我会从多个角度来分析这个问题:
1. 基础概念和定义
2. 相关背景信息
3. 可能的解决方案
4. 具体的建议和步骤
正在整理相关信息和最佳回答方案...`;
showThinkingProcess(aiMessage, thinkingContent);
try {
// 等待思考过程显示一段时间后开始流式输出
setTimeout(async () => {
stopThinking();
aiMessage.thinkingTime = Math.floor((Date.now() - thinkingStartTime.value) / 1000);
// 切换到答案阶段
aiMessage.phase = 'answering';
aiMessage.isShowingThinking = false;
aiMessage.isTyping = true;
// console.log('🚀 开始流式输出答案...');
// 更详细的流式日志和强制刷新
await deepSeekService.streamChat(
[{ role: 'user', content: question }],
// onChunk: 每收到一个数据块立即更新
async (chunk: string) => {
// console.log(`📦 [${chunkCount}] 收到数据块:`, JSON.stringify(chunk));
// 累加内容
aiMessage.content += chunk;
// 找到消息在数组中的位置并完全替换
const messageIndex = messages.value.findIndex(m => m.id === aiMessage.id);
if (messageIndex !== -1) {
// 创建新的消息对象强制触发响应式更新
messages.value[messageIndex] = {
...aiMessage,
content: aiMessage.content,
displayContent: aiMessage.content
};
scrollToBottom();
}
// console.log(
// `📝 更新后总长度: ${aiMessage.content.length}, 显示长度: ${aiMessage.displayContent || aiMessage.content.length}`
// );
},
// onComplete: 流式传输完成
() => {
aiMessage.isTyping = false;
aiMessage.phase = 'completed';
lastThinkingTime.value = aiMessage.thinkingTime || 0;
// console.log(`✅ 流式传输完成,共收到 ${chunkCount} 个数据块,最终内容长度: ${aiMessage.content.length}`);
updateChatHistory();
},
// onError: 处理错误
(error: Error) => {
// console.error('❌ 流式传输错误:', error);
aiMessage.content = `抱歉,AI回复失败:${error.message}`;
aiMessage.displayContent = aiMessage.content;
aiMessage.isTyping = false;
aiMessage.phase = 'completed';
}
);
}, 3000); // 3秒思考时间
} catch (error) {
stopThinking();
// console.error('AI回复失败:', error);
aiMessage.content = `抱歉,AI回复失败:${error instanceof Error ? error.message : '未知错误'}`;
aiMessage.isTyping = false;
aiMessage.phase = 'completed';
}
};
// 其他功能方法
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
onSubmit();
}
};
const copyMessage = async (content: string) => {
try {
await navigator.clipboard.writeText(content);
// alert('已复制到剪贴板');
} catch (error) {
// console.error('复制失败:', error);
}
};
// 格式化内容,将思考过程和回答内容组合
const getFormattedContent = (message: ChatMessage): string => {
let content = '';
// 如果有思考过程就直接显示
if (message.currentThinkingText) {
content += `<think>
${message.currentThinkingText}
</think>
`;
}
// 添加最终答案内容
if (message.content && (message.phase === 'completed' || message.phase === 'answering')) {
content += message.displayContent || message.content;
}
return content;
};
// 使用自定义 markdown 渲染器渲染内容
const renderMarkdownWithMath = (content: string): string => {
try {
const result = renderContentWithMath(content);
console.log('Markdown 渲染结果:', result);
return result;
} catch (error) {
console.error('Markdown 渲染错误:', error);
return content; // 返回原内容作为fallback
}
};
const regenerateMessage = async (message: ChatMessage) => {
// 找到对应的用户消息
const messageIndex = messages.value.findIndex(msg => msg.id === message.id);
if (messageIndex > 0) {
const userMessage = messages.value[messageIndex - 1];
if (userMessage.from === 'user') {
// 删除AI消息,重新生成
messages.value.splice(messageIndex, 1);
inputValue.value = userMessage.content;
onSubmit();
}
}
};
const showChatMenu = (_chatId: string) => {
// 实现聊天菜单功能(删除、重命名等)
// console.log('显示聊天菜单', _chatId);
};
// 生命周期
onMounted(() => {
// 加载可用模型选项
modelOptions.value = deepSeekService.getModelOptions();
// 加载保存的模型选择
const savedModel = localStorage.getItem('deepseek-selected-model');
if (savedModel) {
selectedModelId.value = savedModel;
deepSeekService.switchModel(savedModel);
}
});
onUnmounted(() => {
// 清理定时器
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
}
});
</script>
<template>
<div class="chat-container">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<!-- 顶部工具栏 -->
<div class="sidebar-header">
<div class="logo-section">
<div class="logo">
<i class="icon-user"></i>
</div>
<span class="title">{{ currentModelName }}</span>
</div>
<div class="header-actions">
<i class="icon-add action-btn" @click="newChat"></i>
<i class="icon-settings action-btn" title="API配置" @click="showConfig = !showConfig"></i>
</div>
</div>
<!-- 模型配置面板 -->
<div v-if="showConfig" class="config-panel">
<div class="config-item">
<label>选择模型:</label>
<select v-model="selectedModelId" @change="onModelChange">
<option v-for="model in modelOptions" :key="model.id" :value="model.id">
{{ model.name }}
</option>
</select>
<div v-if="selectedModelId" class="model-description">
{{ modelOptions.find(m => m.id === selectedModelId)?.description }}
</div>
</div>
<div class="config-actions">
<button :disabled="!selectedModelId" class="save-btn" @click="saveConfig">保存</button>
<button :disabled="testing || !selectedModelId" class="test-btn" @click="testConnection">
{{ testing ? '测试中...' : '测试连接' }}
</button>
</div>
</div>
<!-- 搜索框 -->
<div class="search-section">
<div class="search-input">
<i class="icon-search"></i>
<input v-model="searchText" placeholder="搜索对话" />
</div>
</div>
<!-- 历史对话列表 -->
<div class="chat-history">
<div class="history-section">
<div class="section-title">
<span>对话历史</span>
</div>
<div
v-for="chat in filteredChatHistory"
:key="chat.id"
class="chat-item"
:class="{ active: currentChatId === chat.id }"
@click="switchChat(chat.id)"
>
<i class="icon-message"></i>
<span class="chat-title">{{ chat.title }}</span>
<i class="icon-more" @click.stop="showChatMenu(chat.id)"></i>
</div>
</div>
</div>
<!-- 状态指示 -->
<div class="sidebar-footer">
<div class="status-indicator" :class="{ connected: isApiConfigured }">
<span>{{ isApiConfigured ? '✅ 模型已选择' : '❌ 未选择模型' }}</span>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="chat-main">
<!-- 聊天区域头部 -->
<div class="chat-header">
<div class="chat-title-section">
<span class="chat-title">{{ currentChat?.title || 'DeepSeek AI助手' }}</span>
<span class="chat-model">DeepSeek Chat</span>
</div>
<div class="chat-actions">
<span v-if="lastThinkingTime" class="token-count">上次思考用时 {{ lastThinkingTime }}</span>
<i class="icon-more"></i>
</div>
</div>
<!-- 聊天内容区域 -->
<div v-if="!showStartPage" ref="chatContentRef" class="chat-content">
<template v-for="msg in messages" :key="`${msg.id}-${msg.phase}-${Date.now()}`">
<!-- 用户消息 -->
<div v-if="msg.from === 'user'" class="message user-message">
<div class="message-content">{{ msg.content }}</div>
<div class="message-avatar">👤</div>
</div>
<!-- AI消息 - 统一样式:思考过程 + 最终答案 -->
<div v-else class="message ai-message unified-response">
<div class="message-avatar">🤖</div>
<div class="message-content">
<!-- 顶部:深度思考状态 -->
<div v-if="msg.thinkingTime" class="thinking-status-header">
🧠 已深度思考(用时{{ msg.thinkingTime }}秒)
</div>
<div v-else-if="msg.phase === 'thinking'" class="thinking-status-header thinking-active">
🧠 正在深度思考...
</div>
<!-- 统一使用 MarkdownCard 展示思考过程和答案 -->
<div class="unified-markdown-section">
<!-- 使用 McMarkdownCard 统一渲染 -->
<div
class="unified-content markdown-content"
v-html="renderMarkdownWithMath(getFormattedContent(msg))"
></div>
</div>
<!-- 底部:操作按钮 -->
<div
v-if="(msg.phase === 'completed' || msg.phase === 'answering') && msg.content"
class="message-actions"
>
<i class="icon-copy" title="复制" @click="copyMessage(msg.content)"></i>
<i class="icon-refresh" title="重新生成" @click="regenerateMessage(msg)"></i>
<i class="icon-like" title="点赞"></i>
<i class="icon-dislike" title="点踩"></i>
</div>
</div>
</div>
</template>
<!-- AI思考状态 -->
<div v-if="isAiThinking" class="message ai-message thinking-message">
<div class="message-avatar">🤖</div>
<div class="message-content">
<div class="thinking-animation">
<div class="thinking-dots">
<span></span>
<span></span>
<span></span>
</div>
<div class="thinking-text">正在思考中...({{ thinkingTime }}秒)</div>
</div>
</div>
</div>
</div>
<!-- 启动页 -->
<div v-else class="start-page">
<div class="welcome-content">
<h2>🤖 DeepSeek AI助手</h2>
<p>基于DeepSeek强大AI模型的智能对话助手</p>
<div class="features">
<div class="feature-item">✨ 深度思考过程展示</div>
<div class="feature-item">💻 专业代码生成</div>
<div class="feature-item">📝 Markdown格式化回复</div>
<div class="feature-item">🧮 数学公式渲染支持</div>
</div>
<div v-if="!isApiConfigured" class="config-tip">
<p>⚠️ 请先在左侧选择AI模型</p>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-container">
<input
v-model="inputValue"
placeholder="输入您的问题... (Enter发送, Shift+Enter换行)"
:disabled="!isApiConfigured || isAiThinking"
class="chat-input"
@keydown="handleKeyDown"
/>
<button :disabled="!inputValue.trim() || !isApiConfigured || isAiThinking" class="send-btn" @click="onSubmit">
发送
</button>
</div>
<div class="input-footer">
<span class="input-counter">{{ inputValue.length }}/2000</span>
<div class="input-actions">
<i class="icon-paperclip" title="附件"></i>
<i class="icon-mic" title="语音"></i>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.n-config-provider {
height: 100% !important;
}
.chat-container {
display: flex;
height: 90vh!important;
background: #f5f5f5;
.sidebar {
width: 300px;
background: #ffffff;
border-right: 1px solid #ddd;
display: flex;
flex-direction: column;
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
.logo-section {
display: flex;
align-items: center;
gap: 8px;
.logo {
width: 32px;
height: 32px;
border-radius: 50%;
background: #5e7ce0;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.title {
font-weight: 500;
color: #252b3a;
}
}
.header-actions {
display: flex;
gap: 8px;
.action-btn {
cursor: pointer;
padding: 4px;
border-radius: 4px;
&:hover {
background: #f0f0f0;
}
}
}
}
.config-panel {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #eee;
.config-item {
margin-bottom: 12px;
label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #666;
}
input,
select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
background: #ffffff !important;
color: #252b3a !important;
&:focus {
border-color: #5e7ce0;
outline: none;
}
&::placeholder {
color: #999;
}
option {
background: #ffffff;
color: #252b3a;
}
}
.model-description {
margin-top: 6px;
padding: 6px 8px;
background: #f0f0f0;
border-radius: 4px;
font-size: 11px;
color: #666;
line-height: 1.4;
}
}
.config-actions {
display: flex;
gap: 8px;
button {
flex: 1;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
&.save-btn {
background: #5e7ce0;
color: white;
}
&.test-btn {
background: #f0f0f0;
color: #333;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
.search-section {
padding: 16px;
.search-input {
position: relative;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 8px;
padding: 8px 12px;
.icon-search {
color: #999;
margin-right: 8px;
}
input {
flex: 1;
border: none;
background: transparent;
outline: none;
color: #252b3a;
&::placeholder {
color: #999;
}
}
}
}
.chat-history {
flex: 1;
overflow-y: auto;
.history-section {
padding: 0 16px;
.section-title {
padding: 8px 0;
font-size: 12px;
color: #999;
border-bottom: 1px solid #eee;
margin-bottom: 8px;
}
.chat-item {
display: flex;
align-items: center;
padding: 12px 8px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 4px;
color: #252b3a; // 默认黑色字体
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f3ff;
color: #5e7ce0;
}
.icon-message {
margin-right: 8px;
color: #999;
// 选中状态下的图标颜色
.chat-item.active & {
color: #5e7ce0;
}
}
.chat-title {
flex: 1;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: inherit; // 继承父元素颜色
}
.icon-more {
opacity: 0;
transition: opacity 0.2s;
color: #999;
// 选中状态下的图标颜色
.chat-item.active & {
color: #5e7ce0;
}
}
&:hover .icon-more {
opacity: 1;
}
}
}
}
.sidebar-footer {
padding: 16px;
border-top: 1px solid #eee;
.status-indicator {
padding: 8px;
border-radius: 6px;
font-size: 12px;
text-align: center;
background: #ffd6d6;
color: #d32f2f;
&.connected {
background: #d4edda;
color: #155724;
}
}
}
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: #ffffff;
.chat-header {
padding: 16px 24px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
.chat-title-section {
display: flex;
flex-direction: column;
.chat-title {
font-size: 16px;
font-weight: 500;
color: #252b3a;
margin-bottom: 4px;
}
.chat-model {
font-size: 12px;
color: #999;
}
}
.chat-actions {
display: flex;
align-items: center;
gap: 12px;
.token-count {
font-size: 12px;
color: #999;
}
.icon-more {
cursor: pointer;
padding: 4px;
border-radius: 4px;
&:hover {
background: #f0f0f0;
}
}
}
}
.chat-content {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
.message {
display: flex;
gap: 12px;
max-width: 85%;
&.user-message {
align-self: flex-end;
flex-direction: row-reverse;
.message-content {
background: #5e7ce0;
color: white;
border-radius: 18px 4px 18px 18px;
}
}
&.ai-message {
align-self: flex-start;
.message-content {
background: #f5f5f5;
color: #252b3a;
border-radius: 4px 18px 18px 18px;
}
&.thinking-message .message-content {
background: #e8f4fd;
border: 1px solid #b3d7f0;
}
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
margin-top: 4px;
}
.message-content {
padding: 12px 16px;
word-wrap: break-word;
position: relative;
}
}
}
.start-page {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
.welcome-content {
h2 {
color: #252b3a;
margin-bottom: 16px;
}
p {
color: #666;
margin-bottom: 24px;
font-size: 16px;
}
.features {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
.feature-item {
color: #5e7ce0;
font-size: 14px;
}
}
.config-tip {
p {
color: #d32f2f;
font-weight: 500;
}
}
.math-demo {
margin: 24px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e9ecef;
h3 {
color: #252b3a;
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.math-demo-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 16px;
}
}
}
}
.input-section {
padding: 16px 24px;
border-top: 1px solid #eee;
background: #ffffff !important;
.input-container {
display: flex;
gap: 12px;
margin-bottom: 8px;
background: #ffffff !important;
.chat-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
outline: none;
font-size: 14px;
background: #ffffff !important;
color: #252b3a !important;
&:focus {
border-color: #5e7ce0;
background: #ffffff !important;
color: #252b3a !important;
}
&:disabled {
background: #f5f5f5 !important;
color: #666 !important;
cursor: not-allowed;
}
&::placeholder {
color: #999 !important;
}
}
.send-btn {
padding: 12px 24px;
background: #5e7ce0;
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-size: 14px;
&:hover {
background: #4c6ed7;
}
&:disabled {
background: #ccc;
cursor: not-allowed;
}
}
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
.input-counter {
font-size: 12px;
color: #999;
}
.input-actions {
display: flex;
gap: 12px;
i {
cursor: pointer;
color: #666;
padding: 4px;
border-radius: 4px;
&:hover {
background: #f0f0f0;
}
}
}
}
}
}
}
// 图标样式
.icon-user::before {
content: '👤';
}
.icon-add::before {
content: '➕';
}
.icon-settings::before {
content: '⚙️';
}
.icon-search::before {
content: '🔍';
}
.icon-message::before {
content: '💬';
}
.icon-more::before {
content: '⋯';
}
.icon-copy::before {
content: '📋';
}
.icon-refresh::before {
content: '🔄';
}
.icon-like::before {
content: '👍';
}
.icon-dislike::before {
content: '👎';
}
.icon-paperclip::before {
content: '📎';
}
.icon-mic::before {
content: '🎤';
}
.icon-brain::before {
content: '🧠';
}
// 思考状态样式
.thinking-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding: 6px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
&.completed {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
}
}
.thinking-process-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 12px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
color: #333;
white-space: pre-wrap;
min-height: 40px;
.thinking-cursor {
color: #5e7ce0;
animation: blink 1s infinite;
}
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
.thinking-animation {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 12px;
.thinking-dots {
display: flex;
gap: 6px;
span {
width: 8px;
height: 8px;
background: #5e7ce0;
border-radius: 50%;
animation: thinking-dot 1.4s ease-in-out infinite both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
&:nth-child(3) {
animation-delay: 0s;
}
}
}
.thinking-text {
color: #666;
font-size: 13px;
text-align: center;
}
}
@keyframes thinking-dot {
0%,
80%,
100% {
transform: scale(0.6);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
// AI消息统一样式 - 匹配截图效果
.ai-message.unified-response {
.message-content {
background: #f8f9fa;
border-radius: 12px;
padding: 16px;
margin: 12px 0;
border: 1px solid #e3e3e3;
}
// 统一内容区域
.unified-content {
margin: 0;
}
// 顶部思考状态头
.thinking-status-header {
color: #6b7280;
font-size: 13px;
margin-bottom: 12px;
padding: 6px 0;
border-bottom: 1px solid #e5e7eb;
&.thinking-active {
color: #3b82f6;
animation: pulse 2s infinite;
}
}
// 中间思考过程区域
.thinking-process-section {
margin: 12px 0;
.thinking-content {
background: transparent;
border: none;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #4b5563;
white-space: pre-wrap;
margin-bottom: 16px;
.thinking-cursor {
color: #3b82f6;
animation: blink 1s infinite;
}
}
}
// 底部最终答案区域(加粗)
.final-answer-section {
margin-top: 16px;
padding-top: 12px;
.final-answer {
font-weight: 600;
font-size: 14px;
line-height: 1.6;
color: #111827;
// 加粗markdown内容
p {
font-weight: 600;
margin: 8px 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700;
}
strong,
b {
font-weight: 700;
}
}
}
// 操作按钮
.message-actions {
display: flex;
gap: 12px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #e5e7eb;
opacity: 0;
transition: opacity 0.2s ease;
i {
cursor: pointer;
color: #6b7280;
padding: 6px;
border-radius: 6px;
font-size: 16px;
transition: all 0.2s ease;
&:hover {
background: #f3f4f6;
color: #374151;
transform: scale(1.1);
}
}
}
&:hover .message-actions {
opacity: 1;
}
}
// 脉搏动画
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
// Markdown 内容样式
.markdown-content {
line-height: 1.6;
color: #252b3a;
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 16px 0 8px 0;
color: #252b3a;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
h4 {
font-size: 14px;
}
p {
margin: 8px 0;
}
ul,
ol {
margin: 8px 0;
padding-left: 20px;
}
li {
margin: 4px 0;
}
blockquote {
margin: 12px 0;
padding: 8px 12px;
background: #f8f9fa;
border-left: 3px solid #5e7ce0;
border-radius: 4px;
color: #666;
font-style: italic;
}
code {
background: #f1f3f4;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
color: #e83e8c;
}
pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
code {
background: none;
padding: 0;
color: #d4d4d4;
font-size: 13px;
}
}
strong {
font-weight: 600;
color: #252b3a;
}
em {
font-style: italic;
}
a {
color: #5e7ce0;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// KaTeX 数学公式样式
.katex {
font-size: 1.1em;
}
.katex-display {
margin: 16px 0;
text-align: center;
}
.math-display {
margin: 16px 0;
text-align: center;
overflow-x: auto;
}
// 思考过程样式
think {
display: block;
background: #f0f8ff;
border-left: 4px solid #5e7ce0;
padding: 12px 16px;
margin: 16px 0;
border-radius: 0 8px 8px 0;
font-style: italic;
color: #4a6cf7;
white-space: pre-wrap;
}
// 数学公式错误样式
.math-error {
color: #cc0000;
background: #ffe6e6;
padding: 4px 8px;
border-radius: 4px;
font-family: monospace;
border: 1px solid #ffcccc;
}
}
.text-content {
line-height: 1.6;
color: #252b3a;
}
.message-actions {
display: flex;
gap: 8px;
margin-top: 8px;
opacity: 0;
transition: opacity 0.2s;
i {
cursor: pointer;
color: #666;
padding: 4px;
border-radius: 4px;
font-size: 12px;
&:hover {
background: #e0e0e0;
}
}
}
.message:hover .message-actions {
opacity: 1;
}
// 全局输入框样式重写,确保所有输入元素都有正确的颜色
.chat-container {
input {
background: #ffffff !important;
color: #252b3a !important;
&::placeholder {
color: #999 !important;
}
&:focus {
background: #ffffff !important;
color: #252b3a !important;
}
&:disabled {
background: #f5f5f5 !important;
color: #666 !important;
}
}
select {
background: #ffffff !important;
color: #252b3a !important;
option {
background: #ffffff !important;
color: #252b3a !important;
}
}
.input-section {
background: #ffffff !important;
.input-container {
background: #ffffff !important;
}
}
}
</style>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
import { NConfigProvider, darkTheme } from 'naive-ui'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import IframePage from '@/views/_builtin/iframe-page/[url].vue';
import { type ModelOption, deepSeekService } from '@/service/api/deepseek';
import { renderMarkdownWithMath as renderContentWithMath } from '@/utils/katex-renderer';
defineOptions({ name: 'ChatPage' }); const appStore = useAppStore();
// 控制组件显示状态的变量
interface ChatMessage {
id: string;
from: 'user' | 'ai';
content: string;
timestamp: number;
thinkingTime?: number;
contentType?: 'text' | 'markdown';
thinkingContent?: string;
isShowingThinking?: boolean;
currentThinkingText?: string;
displayContent?: string; // 用于渐进显示内容
isTyping?: boolean; // 是否正在打字
phase?: 'thinking' | 'answering' | 'completed'; // 当前阶段
}
interface ChatHistory {
id: string;
title: string;
lastMessage: string;
timestamp: number;
messages: ChatMessage[];
}
// 页面激活状态管理
const isActive = ref(true); const isActive = ref(true);
const containerId = `chat-${Math.random().toString(36).substr(2, 9)}`; const extjsContainerId = `menuRoot-${Math.random().toString(36).substr(2, 9)}`;
// 主题相关
const themeStore = useThemeStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
// 响应式数据
const searchText = ref('');
const inputValue = ref('');
const currentChatId = ref('1');
const showStartPage = ref(true);
const messages = ref<ChatMessage[]>([]);
const isAiThinking = ref(false);
const thinkingStartTime = ref(0);
const thinkingTime = ref(0);
const thinkingInterval = ref<NodeJS.Timeout | null>(null);
const lastThinkingTime = ref(0);
// 配置相关
const showConfig = ref(false);
const selectedModelId = ref('deepseek');
const testing = ref(false);
const modelOptions = ref<ModelOption[]>([]);
// 聊天历史数据
const chatHistory = ref<ChatHistory[]>([
{
id: '1',
title: '新对话',
lastMessage: '',
timestamp: Date.now(),
messages: []
}
]);
// 计算属性
const filteredChatHistory = computed(() => {
if (!searchText.value) return chatHistory.value;
return chatHistory.value.filter(chat => chat.title.toLowerCase().includes(searchText.value.toLowerCase()));
});
const currentChat = computed(() => {
return chatHistory.value.find(chat => chat.id === currentChatId.value);
});
const isApiConfigured = computed(() => {
return selectedModelId.value !== '';
});
const currentModelName = computed(() => {
if (!selectedModelId.value) return 'AI助手';
const currentModel = modelOptions.value.find(model => model.id === selectedModelId.value);
return currentModel?.name || 'AI助手';
});
// 聊天容器引用
const chatContentRef = ref<HTMLElement | null>(null);
// 清理函数
function cleanup() {
try {
// 清理定时器
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
thinkingInterval.value = null;
}
// 清空状态
isAiThinking.value = false;
thinkingTime.value = 0;
// 安全地清空容器,避免DOM操作冲突 // 设置默认的自动启动项,直接加载chat-component.vue
nextTick(() => { const autoStartProps = ref({
try { type: 'System',
const container = document.getElementById(containerId); kvid: 'chat-default',
if (container && container.parentNode) { url: 'chat-component.vue'
// 检查容器是否仍然在DOM中
container.innerHTML = '';
}
} catch (domError) {
// 忽略DOM操作错误,因为容器可能已被移除
console.warn('清理容器时出现DOM错误,可能容器已被移除:', domError);
}
});
} catch (e) {
console.error('清理聊天组件时出错:', e);
}
}
// 暴露清理方法给父组件
defineExpose({
cleanup
}); });
// 滚动到底部 // 添加 extjs-root 就绪状态
const scrollToBottom = () => { const isExtjsRootReady = ref(false);
if (chatContentRef.value) {
setTimeout(() => {
if (chatContentRef.value) {
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight;
}
}, 50);
}
};
const newChat = () => {
const newChatId = `chat_${Date.now()}`;
const newChatItem: ChatHistory = {
id: newChatId,
title: '新对话',
lastMessage: '',
timestamp: Date.now(),
messages: []
};
chatHistory.value.unshift(newChatItem);
currentChatId.value = newChatId;
showStartPage.value = true;
messages.value = [];
inputValue.value = '';
};
const switchChat = (chatId: string) => {
const chat = chatHistory.value.find(c => c.id === chatId);
if (chat) {
currentChatId.value = chatId;
messages.value = [...chat.messages];
showStartPage.value = chat.messages.length === 0;
}
};
// 配置相关方法
const onModelChange = () => {
if (selectedModelId.value) {
deepSeekService.switchModel(selectedModelId.value);
}
};
const saveConfig = () => {
localStorage.setItem('deepseek-selected-model', selectedModelId.value);
deepSeekService.switchModel(selectedModelId.value);
showConfig.value = false;
// alert('配置已保存!');
};
const testConnection = async () => {
testing.value = true;
try {
deepSeekService.switchModel(selectedModelId.value);
await deepSeekService.chat([{ role: 'user', content: 'Hello' }], { maxTokens: 10 });
// alert('连接测试成功!');
} catch (error) {
// alert(`连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
testing.value = false;
}
};
// 思考相关方法
const startThinking = () => {
isAiThinking.value = true;
thinkingStartTime.value = Date.now();
thinkingTime.value = 0;
thinkingInterval.value = setInterval(() => {
thinkingTime.value = Math.floor((Date.now() - thinkingStartTime.value) / 1000);
}, 1000);
};
const stopThinking = () => {
isAiThinking.value = false;
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
thinkingInterval.value = null;
}
};
// 模拟思考过程显示
const showThinkingProcess = (message: ChatMessage, content: string) => {
let currentIndex = 0;
message.currentThinkingText = '';
message.isShowingThinking = true;
const typeNextChar = () => {
if (currentIndex < content.length) {
message.currentThinkingText += content[currentIndex];
currentIndex += 1;
setTimeout(typeNextChar, 15); // 快速显示思考过程
} else {
message.isShowingThinking = false;
}
};
typeNextChar();
};
// 更新聊天历史
const updateChatHistory = () => {
const currentChatItem = chatHistory.value.find(chat => chat.id === currentChatId.value);
if (currentChatItem) {
currentChatItem.messages = [...messages.value];
currentChatItem.lastMessage = messages.value[messages.value.length - 1]?.content || '';
currentChatItem.timestamp = Date.now();
// 如果是第一条消息,更新聊天标题
if (currentChatItem.title === '新对话' && messages.value.length > 0) {
const firstUserMessage = messages.value.find(msg => msg.from === 'user');
if (firstUserMessage) {
currentChatItem.title =
firstUserMessage.content.slice(0, 20) + (firstUserMessage.content.length > 20 ? '...' : '');
}
}
}
};
// 混合方案:思考过程 + 真正流式输出
const onSubmit = async () => {
if (!inputValue.value.trim() || !isApiConfigured.value) return;
showStartPage.value = false;
// 添加用户消息
const userMessage: ChatMessage = {
id: `msg_${Date.now()}`,
from: 'user',
content: inputValue.value,
timestamp: Date.now(),
contentType: 'text'
};
messages.value.push(userMessage);
scrollToBottom();
const question = inputValue.value;
inputValue.value = '';
// 开始思考状态
startThinking();
// 创建AI消息,初始为思考阶段
const aiMessage: ChatMessage = {
id: `msg_ai_${Date.now()}`,
from: 'ai',
content: '',
timestamp: Date.now(),
contentType: 'markdown',
phase: 'thinking',
isShowingThinking: true,
currentThinkingText: '',
displayContent: '',
isTyping: false
};
messages.value.push(aiMessage);
scrollToBottom();
// 显示思考过程
const thinkingContent = `分析用户问题:${question}
让我仔细思考这个问题的关键要素和可能的解答方向...
首先需要理解问题的核心内容和用户的真实需求。
接下来我会从多个角度来分析这个问题:
1. 基础概念和定义
2. 相关背景信息
3. 可能的解决方案
4. 具体的建议和步骤
正在整理相关信息和最佳回答方案...`;
showThinkingProcess(aiMessage, thinkingContent);
try {
// 等待思考过程显示一段时间后开始流式输出
setTimeout(async () => {
stopThinking();
aiMessage.thinkingTime = Math.floor((Date.now() - thinkingStartTime.value) / 1000);
// 切换到答案阶段
aiMessage.phase = 'answering';
aiMessage.isShowingThinking = false;
aiMessage.isTyping = true;
// console.log('🚀 开始流式输出答案...');
// 更详细的流式日志和强制刷新
await deepSeekService.streamChat(
[{ role: 'user', content: question }],
// onChunk: 每收到一个数据块立即更新
async (chunk: string) => {
// console.log(`📦 [${chunkCount}] 收到数据块:`, JSON.stringify(chunk));
// 累加内容
aiMessage.content += chunk;
// 找到消息在数组中的位置并完全替换
const messageIndex = messages.value.findIndex(m => m.id === aiMessage.id);
if (messageIndex !== -1) {
// 创建新的消息对象强制触发响应式更新
messages.value[messageIndex] = {
...aiMessage,
content: aiMessage.content,
displayContent: aiMessage.content
};
scrollToBottom();
}
// console.log(
// `📝 更新后总长度: ${aiMessage.content.length}, 显示长度: ${aiMessage.displayContent || aiMessage.content.length}`
// );
},
// onComplete: 流式传输完成
() => {
aiMessage.isTyping = false;
aiMessage.phase = 'completed';
lastThinkingTime.value = aiMessage.thinkingTime || 0;
// console.log(`✅ 流式传输完成,共收到 ${chunkCount} 个数据块,最终内容长度: ${aiMessage.content.length}`);
updateChatHistory();
},
// onError: 处理错误
(error: Error) => {
// console.error('❌ 流式传输错误:', error);
aiMessage.content = `抱歉,AI回复失败:${error.message}`;
aiMessage.displayContent = aiMessage.content;
aiMessage.isTyping = false;
aiMessage.phase = 'completed';
}
);
}, 3000); // 3秒思考时间
} catch (error) {
stopThinking();
// console.error('AI回复失败:', error);
aiMessage.content = `抱歉,AI回复失败:${error instanceof Error ? error.message : '未知错误'}`;
aiMessage.isTyping = false;
aiMessage.phase = 'completed';
}
};
// 其他功能方法
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
onSubmit();
}
};
const copyMessage = async (content: string) => {
try {
await navigator.clipboard.writeText(content);
// alert('已复制到剪贴板');
} catch (error) {
// console.error('复制失败:', error);
}
};
// 格式化内容,将思考过程和回答内容组合
const getFormattedContent = (message: ChatMessage): string => {
let content = '';
// 如果有思考过程就直接显示
if (message.currentThinkingText) {
content += `<think>
${message.currentThinkingText}
</think>
`;
}
// 添加最终答案内容
if (message.content && (message.phase === 'completed' || message.phase === 'answering')) {
content += message.displayContent || message.content;
}
return content;
};
// 使用自定义 markdown 渲染器渲染内容
const renderMarkdownWithMath = (content: string): string => {
try {
const result = renderContentWithMath(content);
console.log('Markdown 渲染结果:', result);
return result;
} catch (error) {
console.error('Markdown 渲染错误:', error);
return content; // 返回原内容作为fallback
}
};
const regenerateMessage = async (message: ChatMessage) => {
// 找到对应的用户消息
const messageIndex = messages.value.findIndex(msg => msg.id === message.id);
if (messageIndex > 0) {
const userMessage = messages.value[messageIndex - 1];
if (userMessage.from === 'user') {
// 删除AI消息,重新生成
messages.value.splice(messageIndex, 1);
inputValue.value = userMessage.content;
onSubmit();
}
}
};
const showChatMenu = (_chatId: string) => { // 检查 extjs-root 是否存在
// 实现聊天菜单功能(删除、重命名等) const checkExtjsRoot = () => {
// console.log('显示聊天菜单', _chatId); const extjsRoot = document.getElementById('extjs-root');
}; isExtjsRootReady.value = Boolean(extjsRoot);
if (!extjsRoot) {
// 检查Teleport目标容器是否存在 setTimeout(checkExtjsRoot, 100); // 继续检查
const checkTeleportTarget = () => {
const target = document.getElementById('extjs-root');
if (!target) {
console.warn('Teleport目标容器 #extjs-root 不存在,正在创建...');
const container = document.createElement('div');
container.id = 'extjs-root';
document.body.appendChild(container);
} }
}; };
// 生命周期
onMounted(() => { onMounted(() => {
// 确保Teleport目标容器存在 checkExtjsRoot();
checkTeleportTarget();
isActive.value = true; isActive.value = true;
// 加载可用模型选项
modelOptions.value = deepSeekService.getModelOptions();
// 加载保存的模型选择
const savedModel = localStorage.getItem('deepseek-selected-model');
if (savedModel) {
selectedModelId.value = savedModel;
deepSeekService.switchModel(savedModel);
}
}); });
onActivated(() => { onActivated(() => {
...@@ -476,1211 +38,28 @@ onActivated(() => { ...@@ -476,1211 +38,28 @@ onActivated(() => {
onDeactivated(() => { onDeactivated(() => {
isActive.value = false; isActive.value = false;
// 延迟清理,避免与路由切换冲突
setTimeout(() => {
if (!isActive.value) {
cleanup();
}
}, 100);
}); });
onBeforeUnmount(() => { const gap = computed(() => (appStore.isMobile ? 0 : 16));
// 立即设置为非激活状态
isActive.value = false;
// 清理定时器(同步操作)
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
thinkingInterval.value = null;
}
// 清空状态
isAiThinking.value = false;
thinkingTime.value = 0;
});
onUnmounted(() => {
// 最后的清理检查
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
thinkingInterval.value = null;
}
});
</script> </script>
<template> <template>
<Teleport to="#extjs-root" :disabled="!isActive"> <NSpace vertical :size="16">
<div <NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
v-if="isActive" <NGi span="24 s:24 m:14">
:id="containerId" <!-- <ProjectNews /> -->
class="chat-container-wrapper" </NGi>
:data-theme="themeStore.darkMode ? 'dark' : 'light'" <NGi span="24 s:24 m:10">
> <!-- <CreativityBanner /> -->
<NConfigProvider :theme="naiveDarkTheme" :theme-overrides="themeStore.naiveTheme"> </NGi>
<div class="chat-container"> </NGrid>
<!-- 左侧侧边栏 --> <!-- 默认加载 chat-component.vue,通过 IframePage 加载 -->
<div class="sidebar"> <template v-if="isExtjsRootReady">
<!-- 顶部工具栏 --> <div v-show="isActive" :id="extjsContainerId" style="width: 100%; height: 100%">
<div class="sidebar-header"> <IframePage :type="autoStartProps.type" :kvid="autoStartProps.kvid" :url="autoStartProps.url" />
<div class="logo-section"> </div>
<div class="logo"> </template>
<i class="icon-user"></i> </NSpace>
</div>
<span class="title">{{ currentModelName }}</span>
</div>
<div class="header-actions">
<i class="icon-add action-btn" @click="newChat"></i>
<i class="icon-settings action-btn" title="API配置" @click="showConfig = !showConfig"></i>
</div>
</div>
<!-- 模型配置面板 -->
<div v-if="showConfig" class="config-panel">
<div class="config-item">
<label>选择模型:</label>
<select v-model="selectedModelId" @change="onModelChange">
<option v-for="model in modelOptions" :key="model.id" :value="model.id">
{{ model.name }}
</option>
</select>
<div v-if="selectedModelId" class="model-description">
{{ modelOptions.find(m => m.id === selectedModelId)?.description }}
</div>
</div>
<div class="config-actions">
<button :disabled="!selectedModelId" class="save-btn" @click="saveConfig">保存</button>
<button :disabled="testing || !selectedModelId" class="test-btn" @click="testConnection">
{{ testing ? '测试中...' : '测试连接' }}
</button>
</div>
</div>
<!-- 搜索框 -->
<div class="search-section">
<div class="search-input">
<i class="icon-search"></i>
<input v-model="searchText" placeholder="搜索对话" />
</div>
</div>
<!-- 历史对话列表 -->
<div class="chat-history">
<div class="history-section">
<div class="section-title">
<span>对话历史</span>
</div>
<div
v-for="chat in filteredChatHistory"
:key="chat.id"
class="chat-item"
:class="{ active: currentChatId === chat.id }"
@click="switchChat(chat.id)"
>
<i class="icon-message"></i>
<span class="chat-title">{{ chat.title }}</span>
<i class="icon-more" @click.stop="showChatMenu(chat.id)"></i>
</div>
</div>
</div>
<!-- 状态指示 -->
<div class="sidebar-footer">
<div class="status-indicator" :class="{ connected: isApiConfigured }">
<span>{{ isApiConfigured ? '✅ 模型已选择' : '❌ 未选择模型' }}</span>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="chat-main">
<!-- 聊天区域头部 -->
<div class="chat-header">
<div class="chat-title-section">
<span class="chat-title">{{ currentChat?.title || 'DeepSeek AI助手' }}</span>
<span class="chat-model">DeepSeek Chat</span>
</div>
<div class="chat-actions">
<span v-if="lastThinkingTime" class="token-count">上次思考用时 {{ lastThinkingTime }}</span>
<i class="icon-more"></i>
</div>
</div>
<!-- 聊天内容区域 -->
<div v-if="!showStartPage" ref="chatContentRef" class="chat-content">
<template v-for="msg in messages" :key="`${msg.id}-${msg.phase}-${Date.now()}`">
<!-- 用户消息 -->
<div v-if="msg.from === 'user'" class="message user-message">
<div class="message-content">{{ msg.content }}</div>
<div class="message-avatar">👤</div>
</div>
<!-- AI消息 - 统一样式:思考过程 + 最终答案 -->
<div v-else class="message ai-message unified-response">
<div class="message-avatar">🤖</div>
<div class="message-content">
<!-- 顶部:深度思考状态 -->
<div v-if="msg.thinkingTime" class="thinking-status-header">
🧠 已深度思考(用时{{ msg.thinkingTime }}秒)
</div>
<div v-else-if="msg.phase === 'thinking'" class="thinking-status-header thinking-active">
🧠 正在深度思考...
</div>
<!-- 统一使用 MarkdownCard 展示思考过程和答案 -->
<div class="unified-markdown-section">
<!-- 使用 McMarkdownCard 统一渲染 -->
<div
class="unified-content markdown-content"
v-html="renderMarkdownWithMath(getFormattedContent(msg))"
></div>
</div>
<!-- 底部:操作按钮 -->
<div
v-if="(msg.phase === 'completed' || msg.phase === 'answering') && msg.content"
class="message-actions"
>
<i class="icon-copy" title="复制" @click="copyMessage(msg.content)"></i>
<i class="icon-refresh" title="重新生成" @click="regenerateMessage(msg)"></i>
<i class="icon-like" title="点赞"></i>
<i class="icon-dislike" title="点踩"></i>
</div>
</div>
</div>
</template>
<!-- AI思考状态 -->
<div v-if="isAiThinking" class="message ai-message thinking-message">
<div class="message-avatar">🤖</div>
<div class="message-content">
<div class="thinking-animation">
<div class="thinking-dots">
<span></span>
<span></span>
<span></span>
</div>
<div class="thinking-text">正在思考中...({{ thinkingTime }}秒)</div>
</div>
</div>
</div>
</div>
<!-- 启动页 -->
<div v-else class="start-page">
<div class="welcome-content">
<h2>🤖 DeepSeek AI助手</h2>
<p>基于DeepSeek强大AI模型的智能对话助手</p>
<div class="features">
<div class="feature-item">✨ 深度思考过程展示</div>
<div class="feature-item">💻 专业代码生成</div>
<div class="feature-item">📝 Markdown格式化回复</div>
<div class="feature-item">🧮 数学公式渲染支持</div>
</div>
<div v-if="!isApiConfigured" class="config-tip">
<p>⚠️ 请先在左侧选择AI模型</p>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-container">
<input
v-model="inputValue"
placeholder="输入您的问题... (Enter发送, Shift+Enter换行)"
:disabled="!isApiConfigured || isAiThinking"
class="chat-input"
@keydown="handleKeyDown"
/>
<button
:disabled="!inputValue.trim() || !isApiConfigured || isAiThinking"
class="send-btn"
@click="onSubmit"
>
发送
</button>
</div>
<div class="input-footer">
<span class="input-counter">{{ inputValue.length }}/2000</span>
<div class="input-actions">
<i class="icon-paperclip" title="附件"></i>
<i class="icon-mic" title="语音"></i>
</div>
</div>
</div>
</div>
</div>
</NConfigProvider>
</div>
</Teleport>
</template> </template>
<style scoped lang="scss"> <style scoped></style>
.chat-container-wrapper {
width: 100%;
height: 100%;
// 浅色主题变量
--text-color: #252b3a;
--input-bg: #fff;
--placeholder-color: #999;
--disabled-text-color: #999;
--border-color: #ddd;
--sidebar-bg: #ffffff;
--main-bg: #f5f5f5;
// 深色主题适配
&[data-theme='dark'] {
--text-color: #e1e5e9;
--input-bg: #2d3748;
--placeholder-color: #718096;
--disabled-text-color: #718096;
--border-color: #4a5568;
--sidebar-bg: #1a202c;
--main-bg: #2d3748;
}
}
.chat-container {
display: flex;
height: 85vh;
background: var(--main-bg);
.sidebar {
width: 300px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
.logo-section {
display: flex;
align-items: center;
gap: 8px;
.logo {
width: 32px;
height: 32px;
border-radius: 50%;
background: #5e7ce0;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.title {
font-weight: 500;
color: #252b3a;
}
}
.header-actions {
display: flex;
gap: 8px;
.action-btn {
cursor: pointer;
padding: 4px;
border-radius: 4px;
&:hover {
background: #f0f0f0;
}
}
}
}
.config-panel {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #eee;
.config-item {
margin-bottom: 12px;
label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #666;
}
input,
select {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 12px;
color: var(--text-color, #252b3a);
background: var(--input-bg, #fff);
&:focus {
border-color: #5e7ce0;
outline: none;
}
&::placeholder {
color: var(--placeholder-color, #999);
}
}
.model-description {
margin-top: 6px;
padding: 6px 8px;
background: #f0f0f0;
border-radius: 4px;
font-size: 11px;
color: #666;
line-height: 1.4;
}
}
.config-actions {
display: flex;
gap: 8px;
button {
flex: 1;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
&.save-btn {
background: #5e7ce0;
color: white;
}
&.test-btn {
background: #f0f0f0;
color: #333;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
.search-section {
padding: 16px;
.search-input {
position: relative;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 8px;
padding: 8px 12px;
.icon-search {
color: #999;
margin-right: 8px;
}
input {
flex: 1;
border: none;
background: transparent;
outline: none;
color: #252b3a;
&::placeholder {
color: #999;
}
}
}
}
.chat-history {
flex: 1;
overflow-y: auto;
.history-section {
padding: 0 16px;
.section-title {
padding: 8px 0;
font-size: 12px;
color: #999;
border-bottom: 1px solid #eee;
margin-bottom: 8px;
}
.chat-item {
display: flex;
align-items: center;
padding: 12px 8px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 4px;
color: #252b3a; // 默认黑色字体
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f3ff;
color: #5e7ce0;
}
.icon-message {
margin-right: 8px;
color: #999;
// 选中状态下的图标颜色
.chat-item.active & {
color: #5e7ce0;
}
}
.chat-title {
flex: 1;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: inherit; // 继承父元素颜色
}
.icon-more {
opacity: 0;
transition: opacity 0.2s;
color: #999;
// 选中状态下的图标颜色
.chat-item.active & {
color: #5e7ce0;
}
}
&:hover .icon-more {
opacity: 1;
}
}
}
}
.sidebar-footer {
padding: 16px;
border-top: 1px solid #eee;
.status-indicator {
padding: 8px;
border-radius: 6px;
font-size: 12px;
text-align: center;
background: #ffd6d6;
color: #d32f2f;
&.connected {
background: #d4edda;
color: #155724;
}
}
}
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: #ffffff;
.chat-header {
padding: 16px 24px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
.chat-title-section {
display: flex;
flex-direction: column;
.chat-title {
font-size: 16px;
font-weight: 500;
color: #252b3a;
margin-bottom: 4px;
}
.chat-model {
font-size: 12px;
color: #999;
}
}
.chat-actions {
display: flex;
align-items: center;
gap: 12px;
.token-count {
font-size: 12px;
color: #999;
}
.icon-more {
cursor: pointer;
padding: 4px;
border-radius: 4px;
&:hover {
background: #f0f0f0;
}
}
}
}
.chat-content {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
.message {
display: flex;
gap: 12px;
max-width: 85%;
&.user-message {
align-self: flex-end;
flex-direction: row-reverse;
.message-content {
background: #5e7ce0;
color: white;
border-radius: 18px 4px 18px 18px;
}
}
&.ai-message {
align-self: flex-start;
.message-content {
background: #f5f5f5;
color: #252b3a;
border-radius: 4px 18px 18px 18px;
}
&.thinking-message .message-content {
background: #e8f4fd;
border: 1px solid #b3d7f0;
}
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
margin-top: 4px;
}
.message-content {
padding: 12px 16px;
word-wrap: break-word;
position: relative;
}
}
}
.start-page {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
.welcome-content {
h2 {
color: #252b3a;
margin-bottom: 16px;
}
p {
color: #666;
margin-bottom: 24px;
font-size: 16px;
}
.features {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
.feature-item {
color: #5e7ce0;
font-size: 14px;
}
}
.config-tip {
p {
color: #d32f2f;
font-weight: 500;
}
}
.math-demo {
margin: 24px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e9ecef;
h3 {
color: #252b3a;
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.math-demo-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 16px;
}
}
}
}
.input-section {
padding: 16px 24px;
border-top: 1px solid #eee;
.input-container {
display: flex;
gap: 12px;
margin-bottom: 8px;
.chat-input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 24px;
outline: none;
font-size: 14px;
color: var(--text-color, #252b3a);
background: var(--input-bg, #fff);
&:focus {
border-color: #5e7ce0;
}
&:disabled {
background: #f5f5f5;
cursor: not-allowed;
color: var(--disabled-text-color, #999);
}
&::placeholder {
color: var(--placeholder-color, #999);
}
}
.send-btn {
padding: 12px 24px;
background: #5e7ce0;
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-size: 14px;
&:hover {
background: #4c6ed7;
}
&:disabled {
background: #ccc;
cursor: not-allowed;
}
}
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
.input-counter {
font-size: 12px;
color: #999;
}
.input-actions {
display: flex;
gap: 12px;
i {
cursor: pointer;
color: #666;
padding: 4px;
border-radius: 4px;
&:hover {
background: #f0f0f0;
}
}
}
}
}
}
}
// 图标样式
.icon-user::before {
content: '👤';
}
.icon-add::before {
content: '➕';
}
.icon-settings::before {
content: '⚙️';
}
.icon-search::before {
content: '🔍';
}
.icon-message::before {
content: '💬';
}
.icon-more::before {
content: '⋯';
}
.icon-copy::before {
content: '📋';
}
.icon-refresh::before {
content: '🔄';
}
.icon-like::before {
content: '👍';
}
.icon-dislike::before {
content: '👎';
}
.icon-paperclip::before {
content: '📎';
}
.icon-mic::before {
content: '🎤';
}
.icon-brain::before {
content: '🧠';
}
// 思考状态样式
.thinking-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding: 6px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
&.completed {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
}
}
.thinking-process-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 12px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
color: #333;
white-space: pre-wrap;
min-height: 40px;
.thinking-cursor {
color: #5e7ce0;
animation: blink 1s infinite;
}
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
.thinking-animation {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 12px;
.thinking-dots {
display: flex;
gap: 6px;
span {
width: 8px;
height: 8px;
background: #5e7ce0;
border-radius: 50%;
animation: thinking-dot 1.4s ease-in-out infinite both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
&:nth-child(3) {
animation-delay: 0s;
}
}
}
.thinking-text {
color: #666;
font-size: 13px;
text-align: center;
}
}
@keyframes thinking-dot {
0%,
80%,
100% {
transform: scale(0.6);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
// AI消息统一样式 - 匹配截图效果
.ai-message.unified-response {
.message-content {
background: #f8f9fa;
border-radius: 12px;
padding: 16px;
margin: 12px 0;
border: 1px solid #e3e3e3;
}
// 统一内容区域
.unified-content {
margin: 0;
}
// 顶部思考状态头
.thinking-status-header {
color: #6b7280;
font-size: 13px;
margin-bottom: 12px;
padding: 6px 0;
border-bottom: 1px solid #e5e7eb;
&.thinking-active {
color: #3b82f6;
animation: pulse 2s infinite;
}
}
// 中间思考过程区域
.thinking-process-section {
margin: 12px 0;
.thinking-content {
background: transparent;
border: none;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #4b5563;
white-space: pre-wrap;
margin-bottom: 16px;
.thinking-cursor {
color: #3b82f6;
animation: blink 1s infinite;
}
}
}
// 底部最终答案区域(加粗)
.final-answer-section {
margin-top: 16px;
padding-top: 12px;
.final-answer {
font-weight: 600;
font-size: 14px;
line-height: 1.6;
color: #111827;
// 加粗markdown内容
p {
font-weight: 600;
margin: 8px 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700;
}
strong,
b {
font-weight: 700;
}
}
}
// 操作按钮
.message-actions {
display: flex;
gap: 12px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #e5e7eb;
opacity: 0;
transition: opacity 0.2s ease;
i {
cursor: pointer;
color: #6b7280;
padding: 6px;
border-radius: 6px;
font-size: 16px;
transition: all 0.2s ease;
&:hover {
background: #f3f4f6;
color: #374151;
transform: scale(1.1);
}
}
}
&:hover .message-actions {
opacity: 1;
}
}
// 脉搏动画
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
// Markdown 内容样式
.markdown-content {
line-height: 1.6;
color: #252b3a;
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 16px 0 8px 0;
color: #252b3a;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
h4 {
font-size: 14px;
}
p {
margin: 8px 0;
}
ul,
ol {
margin: 8px 0;
padding-left: 20px;
}
li {
margin: 4px 0;
}
blockquote {
margin: 12px 0;
padding: 8px 12px;
background: #f8f9fa;
border-left: 3px solid #5e7ce0;
border-radius: 4px;
color: #666;
font-style: italic;
}
code {
background: #f1f3f4;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
color: #e83e8c;
}
pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
code {
background: none;
padding: 0;
color: #d4d4d4;
font-size: 13px;
}
}
strong {
font-weight: 600;
color: #252b3a;
}
em {
font-style: italic;
}
a {
color: #5e7ce0;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// KaTeX 数学公式样式
.katex {
font-size: 1.1em;
}
.katex-display {
margin: 16px 0;
text-align: center;
}
.math-display {
margin: 16px 0;
text-align: center;
overflow-x: auto;
}
// 思考过程样式
think {
display: block;
background: #f0f8ff;
border-left: 4px solid #5e7ce0;
padding: 12px 16px;
margin: 16px 0;
border-radius: 0 8px 8px 0;
font-style: italic;
color: #4a6cf7;
white-space: pre-wrap;
}
// 数学公式错误样式
.math-error {
color: #cc0000;
background: #ffe6e6;
padding: 4px 8px;
border-radius: 4px;
font-family: monospace;
border: 1px solid #ffcccc;
}
}
.text-content {
line-height: 1.6;
color: #252b3a;
}
.message-actions {
display: flex;
gap: 8px;
margin-top: 8px;
opacity: 0;
transition: opacity 0.2s;
i {
cursor: pointer;
color: #666;
padding: 4px;
border-radius: 4px;
font-size: 12px;
&:hover {
background: #e0e0e0;
}
}
}
.message:hover .message-actions {
opacity: 1;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment