Commit 75526240 by User

添加聊天模块和数学公式展示功能

parent 3faf04c4
......@@ -61,6 +61,10 @@
"dayjs": "1.11.12",
"echarts": "5.5.1",
"font-awesome": "4.7.0",
"highlight.js": "^11.11.1",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3",
"naive-ui": "2.39.0",
"nprogress": "0.2.0",
"pinia": "2.2.0",
......
......@@ -155,19 +155,20 @@ const local: App.I18n.Schema = {
500: 'Server Error',
'iframe-page': 'Iframe',
home: 'Home',
exception: '异常页',
chat: 'Chat',
exception: 'Exception',
exception_403: '403',
exception_404: '404',
exception_500: '500',
document: '文档',
document_project: '项目文档',
'document_project-link': '项目文档(外链)',
document_vue: 'Vue文档',
document_localhost:'本地测试',
document_vite: 'Vite文档',
document_unocss: 'UnoCSS文档',
document_naive: 'Naive UI文档',
document_antd: 'Ant Design Vue文档'
document: 'Document',
document_project: 'Project Document',
'document_project-link': 'Project Document(Link)',
document_vue: 'Vue Document',
document_localhost: 'Local Test',
document_vite: 'Vite Document',
document_unocss: 'UnoCSS Document',
document_naive: 'Naive UI Document',
document_antd: 'Ant Design Vue Document'
},
page: {
login: {
......
......@@ -155,6 +155,7 @@ const local: App.I18n.Schema = {
500: '服务器错误',
'iframe-page': '外链页面',
home: '首页',
chat: 'AI助手',
exception: '异常页',
exception_403: '403',
exception_404: '404',
......
......@@ -20,5 +20,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
chat: () => import("@/views/chat/index.vue"),
home: () => import("@/views/home/index.vue"),
};
......@@ -39,6 +39,17 @@ export const generatedRoutes: GeneratedRoute[] = [
}
},
{
name: 'chat',
path: '/chat',
component: 'layout.base$view.chat',
meta: {
title: 'chat',
i18nKey: 'route.chat',
icon: 'mdi:chat',
order: 1
}
},
{
name: 'home',
path: '/home',
component: 'layout.base$view.home',
......
......@@ -179,6 +179,7 @@ const routeMap: RouteMap = {
"403": "/403",
"404": "/404",
"500": "/500",
"chat": "/chat",
"home": "/home",
"iframe-page": "/iframe-page/:url",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
......
......@@ -13,49 +13,52 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/
const customRoutes: CustomRoute[] = [
// {
// name: 'exception',
// path: '/exception',
// component: 'layout.base',
// meta: {
// title: 'exception',
// i18nKey: 'route.exception',
// icon: 'ant-design:exception-outlined',
// order: 7
// },
// children: [
// {
// name: 'exception_403',
// path: '/exception/403',
// component: 'view.403',
// meta: {
// title: 'exception_403',
// i18nKey: 'route.exception_403',
// icon: 'ic:baseline-block'
// }
// },
// {
// name: 'exception_404',
// path: '/exception/404',
// component: 'view.404',
// meta: {
// title: 'exception_404',
// i18nKey: 'route.exception_404',
// icon: 'ic:baseline-web-asset-off'
// }
// },
// {
// name: 'exception_500',
// path: '/exception/500',
// component: 'view.500',
// meta: {
// title: 'exception_500',
// i18nKey: 'route.exception_500',
// icon: 'ic:baseline-wifi-off'
// }
// },
// ]
// }
{
name: 'exception',
path: '/exception',
component: 'layout.base',
meta: {
title: 'exception',
i18nKey: 'route.exception',
icon: 'ant-design:exception-outlined',
order: 7
},
children: [
{
name: 'exception_403',
path: '/exception/403',
component: 'view.403',
meta: {
title: 'exception_403',
i18nKey: 'route.exception_403',
icon: 'ic:baseline-block'
}
},
{
name: 'exception_404',
path: '/exception/404',
component: 'view.404',
meta: {
title: 'exception_404',
i18nKey: 'route.exception_404',
icon: 'ic:baseline-web-asset-off'
}
},
{
name: 'exception_500',
path: '/exception/500',
component: 'view.500',
meta: {
title: 'exception_500',
i18nKey: 'route.exception_500',
icon: 'ic:baseline-wifi-off'
}
}
]
},
// 以下是iframe-page的示例
// {
// name: 'document',
// path: '/document',
......@@ -72,7 +75,9 @@ const customRoutes: CustomRoute[] = [
// path: '/document/antd',
// component: 'view.iframe-page',
// props: {
// url: 'https://antdv.com/components/overview-cn'
// url: 'https://antdv.com/components/overview-cn',
// kvid: 'A0B25952-82B6-4A1E-B567-4BCDF9463Fs9A1',
// type: 'iframe'
// },
// meta: {
// title: 'document_antd',
......@@ -178,7 +183,7 @@ const customRoutes: CustomRoute[] = [
// icon: 'logos:vue'
// }
// }
// ]
// ]
// }
];
/**
......@@ -186,86 +191,85 @@ const customRoutes: CustomRoute[] = [
*
* @param MenuThree MenuThree
*/
// 以下是获取菜单的示例
// ${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=${window.uiGlobalConfig.InternalCode}`
);
// const rootMenu =getRootMenu('/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=Dashboard');
// // const { data: menus } = await getRootMenu(
// // `/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=${window.uiGlobalConfig.InternalCode}`
// // );
const MenuThree = await getMenuThree(menus?.MenusMain?.Results);
const MenuRoot = menus?.MenuRoot;
// console.log(MenuRoot);
// 存储 MenuRoot 到 store
// const MenuThree = await getMenuThree(menus?.MenusMain?.Results);
// const MenuRoot = menus?.MenuRoot;
// // console.log(MenuRoot);
// // 存储 MenuRoot 到 store
if (MenuRoot) {
setTimeout(() => {
const routeStore = useRouteStore();
routeStore.setMenuRoot(MenuRoot);
}, 1000);
}
// if (MenuRoot) {
// setTimeout(() => {
// const routeStore = useRouteStore();
// routeStore.setMenuRoot(MenuRoot);
// }, 1000);
// }
const MenuThree2 = generateRoutes(MenuThree);
if (MenuThree2.length > 0) {
for (let i = 0; i < MenuThree2.length; i++) {
customRoutes.push(MenuThree2[i]);
}
}
function getMenuThree(data: any) {
const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆
return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => {
// eslint-disable-next-line eqeqeq
const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid == child.ParentKvid);
// eslint-disable-next-line no-unused-expressions
branchArr.length > 0 ? (father.children = branchArr) : '';
// eslint-disable-next-line eqeqeq
return father.ParentKvid == undefined;
});
}
function generateRoutes(data: any[]) {
return data.map(item => {
const route: any = {
name: item.Type === 'System' ? `${item.Type}` : item.Kvid,
path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`,
component: 'layout.base',
meta: {
title: item.DisplayName,
icon: 'mdi:file-document-multiple-outline',
order: item.SortId,
keepAlive: true
},
children: []
};
// const MenuThree2 = generateRoutes(MenuThree);
// if (MenuThree2.length > 0) {
// for (let i = 0; i < MenuThree2.length; i++) {
// customRoutes.push(MenuThree2[i]);
// }
// }
// function getMenuThree(data: any) {
// const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆
// return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => {
// const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid === child.ParentKvid);
// // eslint-disable-next-line no-unused-expressions
// branchArr.length > 0 ? (father.children = branchArr) : '';
// // eslint-disable-next-line eqeqeq
// return father.ParentKvid == undefined;
// });
// }
// function generateRoutes(data: any[]) {
// return data.map(item => {
// const route: any = {
// name: item.Type === 'System' ? `${item.Type}` : item.Kvid,
// path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`,
// component: 'layout.base',
// meta: {
// title: item.DisplayName,
// icon: 'mdi:file-document-multiple-outline',
// order: item.SortId,
// keepAlive: true
// },
// children: []
// };
if (item.children && item.children.length > 0) {
route.children = item.children.map(
(child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => {
const sanitizedRemark =
child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark;
return {
name: 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',
props: {
url: child.Type === 'System' ? sanitizedRemark : '',
kvid: child.Kvid,
type: child.Type
},
meta: {
title: child.DisplayName,
icon: child.Icon,
order: child.SortId,
keepAlive: true,
type: 'iframe'
}
};
}
);
}
// if (item.children && item.children.length > 0) {
// route.children = item.children.map(
// (child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => {
// const sanitizedRemark =
// child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark;
// return {
// name: 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',
// props: {
// url: child.Type === 'System' ? sanitizedRemark : '',
// kvid: child.Kvid,
// type: child.Type
// },
// meta: {
// title: child.DisplayName,
// icon: child.Icon,
// order: child.SortId,
// keepAlive: true,
// type: 'iframe'
// }
// };
// }
// );
// }
return route;
});
}
// return route;
// });
// }
export function createStaticRoutes() {
const constantRoutes: ElegantRoute[] = [];
......
interface DeepSeekConfig {
apiKey: string;
baseURL: string;
model: string;
}
interface ModelOption {
id: string;
name: string;
description: string;
config: DeepSeekConfig;
}
interface DeepSeekMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface DeepSeekRequest {
model: string;
messages: DeepSeekMessage[];
temperature?: number;
max_tokens?: number;
stream?: boolean;
}
interface DeepSeekResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
class DeepSeekService {
private config: DeepSeekConfig;
private readonly modelOptions: ModelOption[] = [
{
id: 'deepseek',
name: 'DeepSeek Chat',
description: 'DeepSeek 深度思考模型,适合复杂问题分析',
config: {
apiKey: 'sk-71f583d8c182420d9d2001f01aa2b643', // 预配置的DeepSeek API Key
baseURL: 'https://api.deepseek.com',
model: 'deepseek-chat'
}
},
{
id: 'openai-gpt4o-mini',
name: 'GPT-4o Mini',
description: 'OpenAI GPT-4o Mini 模型,快速响应',
config: {
apiKey: 'sk-m5oOhoMo9l3QboachCkbIjRAetfc0c1DCcBPvHUs8U3cMzS2', // 预配置的OpenAI API Key
baseURL: 'https://aicvw.com',
model: 'gpt-4o-mini'
}
}
];
constructor() {
// 默认使用第一个模型
this.config = { ...this.modelOptions[0].config };
}
// 获取可用模型列表
getModelOptions(): ModelOption[] {
return this.modelOptions;
}
// 根据模型ID切换模型
switchModel(modelId: string) {
const selectedModel = this.modelOptions.find(model => model.id === modelId);
if (selectedModel) {
this.config = { ...selectedModel.config };
return true;
}
return false;
}
// 获取当前模型信息
getCurrentModel(): ModelOption | undefined {
return this.modelOptions.find(
model => model.config.model === this.config.model && model.config.baseURL === this.config.baseURL
);
}
// 设置 API Key (保留兼容性)
setApiKey(apiKey: string) {
this.config.apiKey = apiKey;
}
// 设置模型 (保留兼容性)
setModel(model: string) {
this.config.model = model;
}
// 检查配置是否完整
private validateConfig(): boolean {
if (!this.config.apiKey) {
console.error('API Key 未设置');
return false;
}
return true;
}
// 发送聊天请求
async chat(
messages: DeepSeekMessage[],
options?: {
temperature?: number;
maxTokens?: number;
stream?: boolean;
}
): Promise<DeepSeekResponse> {
if (!this.validateConfig()) {
throw new Error('API 配置不完整');
}
const requestBody: DeepSeekRequest = {
model: this.config.model,
messages,
temperature: options?.temperature || 0.7,
max_tokens: options?.maxTokens || 2000,
stream: options?.stream || false
};
try {
const response = await fetch(`${this.config.baseURL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`API 请求失败: ${response.status} ${response.statusText} - ${errorData.error?.message || ''}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('API 调用错误:', error);
throw error;
}
}
// 流式聊天请求
async streamChat(
messages: DeepSeekMessage[],
onChunk: (chunk: string) => void,
onComplete: () => void,
onError: (error: Error) => void,
options?: {
temperature?: number;
maxTokens?: number;
}
): Promise<void> {
if (!this.validateConfig()) {
onError(new Error('API 配置不完整'));
return;
}
const requestBody: DeepSeekRequest = {
model: this.config.model,
messages,
temperature: options?.temperature || 0.7,
max_tokens: options?.maxTokens || 2000,
stream: true
};
try {
const response = await fetch(`${this.config.baseURL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`API 请求失败: ${response.status} ${response.statusText} - ${errorData.error?.message || ''}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('无法读取响应流');
}
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
onComplete();
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onComplete();
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
onChunk(content);
}
} catch (parseError) {
// 忽略解析错误,继续处理下一行
}
}
}
}
} catch (error) {
console.error('流式请求错误:', error);
onError(error as Error);
}
}
// 生成最终回答
async generateAnswer(question: string, thinkingProcess: string): Promise<string> {
const answerPrompt: DeepSeekMessage[] = [
{
role: 'system',
content:
'你是一个专业的AI助手,善于给出详细、准确的回答。请基于之前的思考过程,给出最终的完整答案。如果涉及代码,请提供完整的、可运行的代码示例。使用Markdown格式回复。'
},
{
role: 'user',
content: `问题:${question}\n\n我的思考过程:\n${thinkingProcess}\n\n请基于以上思考,给出详细的最终答案。`
}
];
try {
const response = await this.chat(answerPrompt, { temperature: 0.7, maxTokens: 2000 });
return response.choices[0]?.message?.content || '抱歉,我无法生成回答。';
} catch (error) {
console.error('生成最终答案失败:', error);
throw error;
}
}
}
// 导出单例实例
export const deepSeekService = new DeepSeekService();
// 导出类型
export type { DeepSeekMessage, DeepSeekResponse, ModelOption };
......@@ -33,6 +33,7 @@ declare module "@elegant-router/types" {
"403": "/403";
"404": "/404";
"500": "/500";
"chat": "/chat";
"home": "/home";
"iframe-page": "/iframe-page/:url";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
......@@ -83,6 +84,7 @@ declare module "@elegant-router/types" {
| "403"
| "404"
| "500"
| "chat"
| "home"
| "iframe-page"
| "login"
......@@ -109,6 +111,7 @@ declare module "@elegant-router/types" {
| "500"
| "iframe-page"
| "login"
| "chat"
| "home"
>;
......
import katex from 'katex';
// 简单的数学公式渲染器
export const renderMathFormulas = (content: string): string => {
console.log('开始处理数学公式,内容长度:', content.length);
// 渲染块级数学公式 $$...$$ 和 \[...\](支持多行)
content = content.replace(/\$\$([\s\S]*?)\$\$/g, (match, formula) => {
console.log('发现$$块级公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: true,
throwOnError: false
});
console.log('$$块级公式渲染成功');
return `<div class="math-display">${rendered}</div>`;
} catch (error) {
console.error('KaTeX $$块级公式渲染错误:', error, '公式内容:', formula);
return `<div class="math-error">数学公式渲染错误: ${formula.trim()}</div>`;
}
});
// 渲染 \[...\] 块级数学公式
content = content.replace(/\\\[([\s\S]*?)\\\]/g, (match, formula) => {
console.log('发现\\[\\]块级公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: true,
throwOnError: false
});
console.log('\\[\\]块级公式渲染成功');
return `<div class="math-display">${rendered}</div>`;
} catch (error) {
console.error('KaTeX \\[\\]块级公式渲染错误:', error, '公式内容:', formula);
return `<div class="math-error">数学公式渲染错误: ${formula.trim()}</div>`;
}
});
// 渲染行内数学公式 $...$
content = content.replace(/\$([^$]+?)\$/g, (match, formula) => {
console.log('发现$行内公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: false,
throwOnError: false
});
console.log('$行内公式渲染成功');
return rendered;
} catch (error) {
console.error('KaTeX $行内公式渲染错误:', error, '公式内容:', formula);
return `<span class="math-error">公式错误: ${formula.trim()}</span>`;
}
});
// 渲染 \(...\) 行内数学公式
content = content.replace(/\\\((.*?)\\\)/g, (match, formula) => {
console.log('发现\\(\\)行内公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: false,
throwOnError: false
});
console.log('\\(\\)行内公式渲染成功');
return rendered;
} catch (error) {
console.error('KaTeX \\(\\)行内公式渲染错误:', error, '公式内容:', formula);
return `<span class="math-error">公式错误: ${formula.trim()}</span>`;
}
});
return content;
};
// 处理 markdown + 数学公式
export const renderMarkdownWithMath = (content: string): string => {
console.log('原始内容:', content);
// 首先处理数学公式
const withMath = renderMathFormulas(content);
console.log('处理数学公式后:', withMath);
// 简单的 markdown 处理,但保持数学公式完整
let processed = withMath;
// 处理标题(只在行首)
processed = processed.replace(/^### (.*)$/gm, '<h3>$1</h3>');
processed = processed.replace(/^## (.*)$/gm, '<h2>$1</h2>');
processed = processed.replace(/^# (.*)$/gm, '<h1>$1</h1>');
// 处理粗体
processed = processed.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 处理有序列表
processed = processed.replace(/^\d+\.\s+(.*)$/gm, '<li>$1</li>');
// 处理无序列表
processed = processed.replace(/^[-*]\s+(.*)$/gm, '<li>$1</li>');
// 将连续的 li 标签包装在 ol 或 ul 中
processed = processed.replace(/(<li>.*?<\/li>(?:\s*<li>.*?<\/li>)*)/gs, '<ul>$1</ul>');
// 处理段落(双换行表示段落分隔)
const paragraphs = processed.split(/\n\s*\n/);
processed = paragraphs.map(paragraph => {
const trimmed = paragraph.trim();
if (!trimmed) return '';
// 如果已经是HTML标签,不要包装
if (trimmed.startsWith('<')) {
return trimmed;
}
// 处理单行换行
const withBreaks = trimmed.replace(/\n/g, '<br>');
return `<p>${withBreaks}</p>`;
}).join('\n');
console.log('最终处理结果:', processed);
return processed;
};
import MarkdownIt from 'markdown-it';
// @ts-ignore
import markdownItKatex from 'markdown-it-katex';
// 创建 markdown-it 实例并配置 KaTeX 支持
export const createMarkdownRenderer = () => {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
// 使用 markdown-it-katex 插件
md.use(markdownItKatex, {
blockClass: 'math-display',
errorColor: '#cc0000',
macros: {
"\\RR": "\\mathbb{R}"
}
});
return md;
};
// 渲染 markdown 内容
export const renderMarkdown = (content: string): string => {
const md = createMarkdownRenderer();
return md.render(content);
};
<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">
.chat-container {
display: flex;
height: 85vh;
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;
}
.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 #ddd;
border-radius: 24px;
outline: none;
font-size: 14px;
&:focus {
border-color: #5e7ce0;
}
&:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
}
.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