mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-04-15 13:46:36 +08:00
959 lines
24 KiB
Vue
959 lines
24 KiB
Vue
<script setup lang="ts">
|
||
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
||
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
||
import { Loading, Tools, Check, Plus, Delete } from '@element-plus/icons-vue';
|
||
import { ElIcon, ElMessage, ElMessageBox } from 'element-plus';
|
||
import { computed, onMounted, ref, watch } from 'vue';
|
||
import { Sender } from 'vue-element-plus-x';
|
||
import { agentSend, getAgentTools, getAgentContext } from '@/api/agent';
|
||
import type { AgentToolOutput, AgentResultOutput, AgentUsage } from '@/api/agent/types';
|
||
import { getSelectableTokenInfo } from '@/api';
|
||
import { useUserStore } from '@/stores/modules/user';
|
||
import { useAgentSessionStore } from '@/stores/modules/agentSession';
|
||
import { getUserProfilePicture } from '@/utils/user.ts';
|
||
import MarkedMarkdown from '@/components/MarkedMarkdown/index.vue';
|
||
import agentAvatar from '@/assets/images/czld.png';
|
||
import '@/styles/github-markdown.css';
|
||
import '@/styles/yixin-markdown.scss';
|
||
|
||
// 消息类型定义
|
||
type MessageItem = BubbleProps & {
|
||
key: number;
|
||
role: 'ai' | 'user' | 'assistant';
|
||
avatar: string;
|
||
thinkingStatus?: ThinkingStatus;
|
||
thinlCollapse?: boolean;
|
||
reasoning_content?: string;
|
||
toolCalls?: { name: string; status: 'calling' | 'called'; result?: any; usage?: { prompt: number; completion: number; total: number } }[];
|
||
tokenUsage?: { prompt: number; completion: number; total: number };
|
||
};
|
||
|
||
const userStore = useUserStore();
|
||
const agentSessionStore = useAgentSessionStore();
|
||
|
||
// 响应式数据
|
||
const inputValue = ref('');
|
||
const bubbleItems = ref<MessageItem[]>([]);
|
||
const bubbleListRef = ref<BubbleListInstance | null>(null);
|
||
const isSending = ref(false);
|
||
const sessionId = ref('');
|
||
|
||
// 会话列表相关
|
||
const showSessionList = ref(true);
|
||
|
||
// 工具相关
|
||
const availableTools = ref<AgentToolOutput[]>([]);
|
||
const selectedTools = ref<string[]>([]);
|
||
const showToolsPanel = ref(false);
|
||
|
||
// 配置相关
|
||
const tokenId = ref('');
|
||
const tokenOptions = ref<any[]>([]);
|
||
const tokenLoading = ref(false);
|
||
const modelId = ref('gpt-5.2-chat');
|
||
|
||
// 加载Token列表
|
||
async function loadTokens() {
|
||
tokenLoading.value = true;
|
||
try {
|
||
const res = await getSelectableTokenInfo();
|
||
const data = Array.isArray(res) ? res : (res as any).data || [];
|
||
tokenOptions.value = data;
|
||
// 默认选中第一个可用的token
|
||
if (tokenOptions.value.length > 0 && !tokenId.value) {
|
||
const firstAvailable = tokenOptions.value.find(t => !t.isDisabled);
|
||
if (firstAvailable) {
|
||
tokenId.value = firstAvailable.tokenId;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载Token列表失败:', error);
|
||
} finally {
|
||
tokenLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 加载工具列表
|
||
async function loadTools() {
|
||
try {
|
||
const res = await getAgentTools();
|
||
availableTools.value = res.data || [];
|
||
// 默认选中所有工具
|
||
selectedTools.value = availableTools.value.map(t => t.code);
|
||
} catch (error) {
|
||
console.error('加载工具列表失败:', error);
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadTokens();
|
||
loadTools();
|
||
// 加载会话列表
|
||
if (userStore.token) {
|
||
agentSessionStore.requestSessionList(1);
|
||
}
|
||
});
|
||
|
||
// 创建新会话
|
||
async function createNewSession() {
|
||
sessionId.value = '';
|
||
bubbleItems.value = [];
|
||
agentSessionStore.setCurrentSession(null);
|
||
}
|
||
|
||
// 选择会话
|
||
async function selectSession(session: any) {
|
||
sessionId.value = session.id;
|
||
agentSessionStore.setCurrentSession(session);
|
||
bubbleItems.value = [];
|
||
|
||
// 加载历史上下文
|
||
await loadSessionContext(session.id);
|
||
}
|
||
|
||
// 加载会话历史上下文
|
||
async function loadSessionContext(sid: string) {
|
||
try {
|
||
const res = await getAgentContext(sid);
|
||
|
||
// 获取实际数据,可能是 res.data 或直接是 res
|
||
let rawData = res?.data ?? res;
|
||
if (!rawData) return;
|
||
|
||
// 如果是字符串则解析JSON
|
||
let contextData = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
|
||
|
||
// 获取消息列表
|
||
const messages = contextData?.storeState?.messages || [];
|
||
|
||
if (messages.length === 0) return;
|
||
|
||
// 转换为消息列表
|
||
messages.forEach((msg: any, index: number) => {
|
||
const isUser = msg.role === 'user';
|
||
const content = msg.contents
|
||
?.filter((c: any) => c.$type === 'text')
|
||
?.map((c: any) => c.text)
|
||
?.join('') || '';
|
||
|
||
if (content) {
|
||
const obj: MessageItem = {
|
||
key: index,
|
||
avatar: isUser ? getUserProfilePicture() : agentAvatar,
|
||
avatarSize: '32px',
|
||
role: isUser ? 'user' : 'assistant',
|
||
placement: isUser ? 'end' : 'start',
|
||
isMarkdown: !isUser,
|
||
loading: false,
|
||
content,
|
||
reasoning_content: '',
|
||
thinkingStatus: 'end',
|
||
thinlCollapse: false,
|
||
noStyle: !isUser,
|
||
toolCalls: [],
|
||
};
|
||
bubbleItems.value.push(obj);
|
||
}
|
||
});
|
||
|
||
// 滚动到底部
|
||
setTimeout(() => {
|
||
bubbleListRef.value?.scrollToBottom();
|
||
}, 100);
|
||
} catch (error) {
|
||
console.error('加载会话上下文失败:', error);
|
||
}
|
||
}
|
||
|
||
// 删除会话
|
||
async function handleDeleteSession(session: any, event: Event) {
|
||
event.stopPropagation();
|
||
try {
|
||
await ElMessageBox.confirm('确定要删除该会话吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
});
|
||
await agentSessionStore.deleteSession([session.id]);
|
||
if (sessionId.value === session.id) {
|
||
createNewSession();
|
||
}
|
||
ElMessage.success('删除成功');
|
||
} catch {
|
||
// 取消删除
|
||
}
|
||
}
|
||
|
||
// 切换工具选择
|
||
function toggleTool(code: string) {
|
||
const index = selectedTools.value.indexOf(code);
|
||
if (index > -1) {
|
||
selectedTools.value.splice(index, 1);
|
||
} else {
|
||
selectedTools.value.push(code);
|
||
}
|
||
}
|
||
|
||
// 添加消息
|
||
function addMessage(message: string, isUser: boolean) {
|
||
const i = bubbleItems.value.length;
|
||
const obj: MessageItem = {
|
||
key: i,
|
||
avatar: isUser ? getUserProfilePicture() : agentAvatar,
|
||
avatarSize: '32px',
|
||
role: isUser ? 'user' : 'assistant',
|
||
placement: isUser ? 'end' : 'start',
|
||
isMarkdown: !isUser,
|
||
loading: !isUser,
|
||
content: message || '',
|
||
reasoning_content: '',
|
||
thinkingStatus: 'start',
|
||
thinlCollapse: false,
|
||
noStyle: !isUser,
|
||
toolCalls: [],
|
||
};
|
||
bubbleItems.value.push(obj);
|
||
}
|
||
|
||
// AbortController 用于取消请求
|
||
let abortController: AbortController | null = null;
|
||
|
||
// 临时存储工具调用用量
|
||
let pendingToolUsage: { prompt: number; completion: number; total: number } | null = null;
|
||
|
||
// 处理Agent流式数据
|
||
function handleAgentChunk(data: AgentResultOutput) {
|
||
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||
if (!latest) return;
|
||
|
||
switch (data.type) {
|
||
case 'text':
|
||
latest.content += data.content || '';
|
||
latest.loading = false;
|
||
break;
|
||
case 'toolCalling':
|
||
// 工具调用中
|
||
if (!latest.toolCalls) latest.toolCalls = [];
|
||
const newToolCall: { name: string; status: 'calling' | 'called'; result?: any; usage?: { prompt: number; completion: number; total: number } } = {
|
||
name: data.content as string,
|
||
status: 'calling',
|
||
};
|
||
// 如果有待处理的用量(toolCallUsage 先于 toolCalling 到达),设置到这个工具调用
|
||
if (pendingToolUsage) {
|
||
newToolCall.usage = pendingToolUsage;
|
||
pendingToolUsage = null;
|
||
}
|
||
latest.toolCalls.push(newToolCall);
|
||
break;
|
||
case 'toolCallUsage':
|
||
// 工具调用用量统计 - 先保存,等 toolCalled 时再设置
|
||
const toolUsage = data.content as AgentUsage;
|
||
pendingToolUsage = {
|
||
prompt: toolUsage.input_tokens || toolUsage.prompt_tokens || 0,
|
||
completion: toolUsage.output_tokens || toolUsage.completion_tokens || 0,
|
||
total: toolUsage.total_tokens || 0,
|
||
};
|
||
// 同时尝试设置到最后一个工具调用
|
||
if (latest.toolCalls && latest.toolCalls.length > 0) {
|
||
const lastTool = latest.toolCalls[latest.toolCalls.length - 1];
|
||
if (lastTool) {
|
||
lastTool.usage = pendingToolUsage;
|
||
}
|
||
}
|
||
break;
|
||
case 'toolCalled':
|
||
// 工具调用完成
|
||
if (latest.toolCalls && latest.toolCalls.length > 0) {
|
||
const lastTool = latest.toolCalls[latest.toolCalls.length - 1];
|
||
if (lastTool) {
|
||
lastTool.status = 'called';
|
||
lastTool.result = data.content;
|
||
// 如果有待处理的用量,设置到这个工具调用
|
||
if (pendingToolUsage && !lastTool.usage) {
|
||
lastTool.usage = pendingToolUsage;
|
||
}
|
||
}
|
||
}
|
||
pendingToolUsage = null;
|
||
break;
|
||
case 'usage':
|
||
// 对话用量统计
|
||
const chatUsage = data.content as AgentUsage;
|
||
latest.tokenUsage = {
|
||
prompt: chatUsage.input_tokens || chatUsage.prompt_tokens || 0,
|
||
completion: chatUsage.output_tokens || chatUsage.completion_tokens || 0,
|
||
total: chatUsage.total_tokens || 0,
|
||
};
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 发送消息
|
||
async function startSSE(chatContent: string) {
|
||
if (isSending.value) return;
|
||
if (!chatContent.trim()) {
|
||
ElMessage.warning('请输入消息内容');
|
||
return;
|
||
}
|
||
if (!tokenId.value) {
|
||
ElMessage.warning('请先选择 API 密钥');
|
||
showToolsPanel.value = true;
|
||
return;
|
||
}
|
||
|
||
isSending.value = true;
|
||
inputValue.value = '';
|
||
|
||
// 如果没有会话ID,先创建会话
|
||
let currentSessionId = sessionId.value;
|
||
if (!currentSessionId && userStore.token) {
|
||
const newSession = await agentSessionStore.createSession({
|
||
sessionTitle: chatContent.slice(0, 20),
|
||
sessionContent: chatContent,
|
||
userId: userStore.userInfo?.userId as number,
|
||
});
|
||
if (newSession) {
|
||
currentSessionId = newSession.id!;
|
||
sessionId.value = currentSessionId;
|
||
} else {
|
||
currentSessionId = crypto.randomUUID();
|
||
sessionId.value = currentSessionId;
|
||
}
|
||
} else if (!currentSessionId) {
|
||
currentSessionId = crypto.randomUUID();
|
||
sessionId.value = currentSessionId;
|
||
}
|
||
|
||
// 添加用户消息和AI消息占位
|
||
addMessage(chatContent, true);
|
||
addMessage('', false);
|
||
|
||
// 滚动到底部
|
||
setTimeout(() => {
|
||
bubbleListRef.value?.scrollToBottom();
|
||
}, 100);
|
||
|
||
abortController = new AbortController();
|
||
|
||
try {
|
||
const response = await fetch(`${import.meta.env.VITE_WEB_BASE_API}/ai-chat/agent/send`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${userStore.token}`,
|
||
},
|
||
body: JSON.stringify({
|
||
sessionId: currentSessionId,
|
||
content: chatContent,
|
||
tokenId: tokenId.value,
|
||
modelId: modelId.value,
|
||
tools: selectedTools.value,
|
||
}),
|
||
signal: abortController.signal,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const reader = response.body?.getReader();
|
||
if (!reader) throw new Error('No reader available');
|
||
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
buffer = lines.pop() || '';
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = line.slice(6).trim();
|
||
if (data === '[DONE]') {
|
||
break;
|
||
}
|
||
try {
|
||
const parsed = JSON.parse(data) as AgentResultOutput;
|
||
handleAgentChunk(parsed);
|
||
} catch (e) {
|
||
console.error('解析数据失败:', e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (err: any) {
|
||
if (err.name === 'AbortError') {
|
||
console.log('请求已取消');
|
||
} else {
|
||
console.error('请求失败:', err);
|
||
ElMessage.error(err.message || '请求失败');
|
||
}
|
||
} finally {
|
||
isSending.value = false;
|
||
abortController = null;
|
||
// 停止加载状态
|
||
if (bubbleItems.value.length) {
|
||
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||
latest.loading = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 取消请求
|
||
function cancelSSE() {
|
||
if (abortController) {
|
||
abortController.abort();
|
||
isSending.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="agent-page">
|
||
<!-- 左侧会话列表 -->
|
||
<div class="session-sidebar" :class="{ collapsed: !showSessionList }">
|
||
<div class="sidebar-header">
|
||
<span class="sidebar-title">会话列表</span>
|
||
<el-button type="primary" :icon="Plus" circle size="small" @click="createNewSession" />
|
||
</div>
|
||
<div class="session-list">
|
||
<div
|
||
v-for="session in agentSessionStore.sessionList"
|
||
:key="session.id"
|
||
class="session-item"
|
||
:class="{ active: sessionId === session.id }"
|
||
@click="selectSession(session)"
|
||
>
|
||
<span class="session-title">{{ session.sessionTitle }}</span>
|
||
<el-icon class="delete-icon" @click="handleDeleteSession(session, $event)">
|
||
<Delete />
|
||
</el-icon>
|
||
</div>
|
||
<div v-if="agentSessionStore.sessionList.length === 0" class="empty-tip">
|
||
暂无会话记录
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧主内容区 -->
|
||
<div class="main-content">
|
||
<!-- 头部 -->
|
||
<div class="agent-header">
|
||
<div class="header-left">
|
||
<el-icon :size="24" color="var(--el-color-primary)">
|
||
<Tools />
|
||
</el-icon>
|
||
<span class="header-title">AI 智能体</span>
|
||
</div>
|
||
<div class="header-right">
|
||
<el-button type="primary" plain size="small" @click="showToolsPanel = !showToolsPanel">
|
||
<el-icon><Tools /></el-icon>
|
||
<span>配置</span>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工具配置面板 -->
|
||
<el-collapse-transition>
|
||
<div v-show="showToolsPanel" class="tools-panel">
|
||
<div class="tools-header">
|
||
<span>可用工具</span>
|
||
<el-button link type="primary" size="small" @click="selectedTools = availableTools.map(t => t.code)">
|
||
全选
|
||
</el-button>
|
||
<el-button link type="info" size="small" @click="selectedTools = []">
|
||
清空
|
||
</el-button>
|
||
</div>
|
||
<div class="tools-list">
|
||
<div
|
||
v-for="tool in availableTools"
|
||
:key="tool.code"
|
||
class="tool-item"
|
||
:class="{ active: selectedTools.includes(tool.code) }"
|
||
@click="toggleTool(tool.code)"
|
||
>
|
||
<el-icon v-if="selectedTools.includes(tool.code)" class="check-icon">
|
||
<Check />
|
||
</el-icon>
|
||
<span>{{ tool.name }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="tools-config">
|
||
<el-select
|
||
v-model="tokenId"
|
||
placeholder="请选择"
|
||
size="small"
|
||
style="width: 300px;"
|
||
:loading="tokenLoading"
|
||
clearable
|
||
>
|
||
<template #prefix>
|
||
<span style="color: var(--el-text-color-regular);">API密钥(可选)</span>
|
||
</template>
|
||
<el-option
|
||
v-for="token in tokenOptions"
|
||
:key="token.tokenId"
|
||
:label="token.name"
|
||
:value="token.tokenId"
|
||
:disabled="token.isDisabled"
|
||
/>
|
||
</el-select>
|
||
<!-- 模型ID固定为 gpt-5.2-chat,不允许用户修改 -->
|
||
<!-- <el-input v-model="modelId" placeholder="请输入模型ID" size="small" style="width: 300px;">
|
||
<template #prepend>模型</template>
|
||
</el-input> -->
|
||
</div>
|
||
</div>
|
||
</el-collapse-transition>
|
||
|
||
<!-- 聊天区域 -->
|
||
<div class="chat-area">
|
||
<!-- 消息为空时的欢迎界面 -->
|
||
<div v-if="bubbleItems.length === 0" class="welcome-container">
|
||
<div class="welcome-icon">
|
||
<img :src="agentAvatar" alt="Agent" class="welcome-avatar" />
|
||
</div>
|
||
<h2 class="welcome-title">意心Ai 智能体</h2>
|
||
<p class="welcome-desc">我叫橙子老弟,我,啥都会</p>
|
||
<div class="welcome-tips">
|
||
<div class="tip-item">可选择 API 密钥后开始对话</div>
|
||
<div class="tip-item">选择需要的工具来增强我的能力</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 消息列表 -->
|
||
<BubbleList v-else ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 320px)">
|
||
<template #header="{ item }">
|
||
<!-- 工具调用状态 -->
|
||
<div v-if="item.toolCalls && item.toolCalls.length > 0" class="tool-calls-container">
|
||
<div v-for="(tc, idx) in item.toolCalls" :key="idx" class="tool-call-item">
|
||
<el-icon v-if="tc.status === 'calling'" class="is-loading">
|
||
<Loading />
|
||
</el-icon>
|
||
<el-icon v-else color="var(--el-color-success)">
|
||
<Check />
|
||
</el-icon>
|
||
<span class="tool-name">{{ tc.name }}</span>
|
||
<span class="tool-status">{{ tc.status === 'calling' ? '调用中...' : '已完成' }}</span>
|
||
<span v-if="tc.usage?.total" class="tool-usage">token: {{ tc.usage.total }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #content="{ item }">
|
||
<MarkedMarkdown
|
||
v-if="item.content && (item.role === 'assistant' || item.role === 'system')"
|
||
class="markdown-body"
|
||
:content="item.content"
|
||
/>
|
||
<div v-if="item.role === 'user'" class="user-content">
|
||
{{ item.content }}
|
||
</div>
|
||
</template>
|
||
|
||
<template #footer="{ item }">
|
||
<div v-if="item.tokenUsage?.total" class="footer-wrapper">
|
||
<span class="footer-token">token: {{ item.tokenUsage.total }}</span>
|
||
</div>
|
||
</template>
|
||
</BubbleList>
|
||
|
||
<!-- 输入区域 -->
|
||
<Sender
|
||
v-model="inputValue"
|
||
class="agent-sender"
|
||
:auto-size="{ maxRows: 6, minRows: 2 }"
|
||
variant="updown"
|
||
clearable
|
||
allow-speech
|
||
:loading="isSending"
|
||
:disabled="!tokenId"
|
||
@submit="startSSE"
|
||
@cancel="cancelSSE"
|
||
>
|
||
<template #suffix>
|
||
<ElIcon v-if="isSending" class="is-loading">
|
||
<Loading />
|
||
</ElIcon>
|
||
</template>
|
||
</Sender>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.agent-page {
|
||
display: flex;
|
||
flex-direction: row;
|
||
width: 100%;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.session-sidebar {
|
||
width: 240px;
|
||
height: 100%;
|
||
border-right: 1px solid var(--el-border-color-lighter);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
background: var(--el-bg-color);
|
||
|
||
&.collapsed {
|
||
width: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sidebar-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
|
||
.sidebar-title {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.session-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: var(--el-border-color-lighter);
|
||
border-radius: 2px;
|
||
|
||
&:hover {
|
||
background: var(--el-border-color);
|
||
}
|
||
}
|
||
}
|
||
|
||
.session-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
margin-bottom: 4px;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
background: var(--el-fill-color-light);
|
||
|
||
.delete-icon {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background: var(--el-color-primary-light-9);
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.session-title {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.delete-icon {
|
||
opacity: 0;
|
||
color: var(--el-text-color-secondary);
|
||
transition: opacity 0.2s;
|
||
|
||
&:hover {
|
||
color: var(--el-color-danger);
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-tip {
|
||
text-align: center;
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 13px;
|
||
padding: 20px;
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 0 20px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.agent-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 60px;
|
||
flex-shrink: 0;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
|
||
.tools-panel {
|
||
background: var(--el-bg-color-page);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
|
||
.tools-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tools-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.tool-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 6px 12px;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--el-border-color);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
border-color: var(--el-color-primary);
|
||
}
|
||
|
||
&.active {
|
||
background: var(--el-color-primary-light-9);
|
||
border-color: var(--el-color-primary);
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.check-icon {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.tools-config {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
}
|
||
|
||
.chat-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-width: 1000px;
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.welcome-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
|
||
.welcome-icon {
|
||
margin-bottom: 20px;
|
||
|
||
.welcome-avatar {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
|
||
.welcome-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin: 0 0 12px;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.welcome-desc {
|
||
font-size: 14px;
|
||
color: var(--el-text-color-secondary);
|
||
margin: 0 0 24px;
|
||
}
|
||
|
||
.welcome-tips {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.tip-item {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-regular);
|
||
padding: 8px 16px;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tool-calls-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin-bottom: 12px;
|
||
padding: 8px 12px;
|
||
background: var(--el-fill-color-lighter);
|
||
border-radius: 8px;
|
||
border-left: 3px solid var(--el-color-primary);
|
||
}
|
||
|
||
.tool-call-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
|
||
.tool-name {
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.tool-status {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.tool-usage {
|
||
font-size: 11px;
|
||
background: rgba(255, 153, 0, 0.6);
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
color: #ffffff;
|
||
margin-left: auto;
|
||
}
|
||
}
|
||
|
||
.user-content {
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.footer-wrapper {
|
||
margin-top: 4px;
|
||
|
||
.footer-token {
|
||
font-size: 12px;
|
||
background: rgba(1, 183, 86, 0.53);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
.agent-sender {
|
||
margin-top: auto;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.is-loading {
|
||
animation: rotating 2s linear infinite;
|
||
}
|
||
|
||
@keyframes rotating {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
:deep(.el-bubble-list) {
|
||
padding-top: 24px;
|
||
}
|
||
|
||
:deep(.el-bubble) {
|
||
padding: 0 12px;
|
||
padding-bottom: 24px;
|
||
}
|
||
|
||
:deep(.markdown-body) {
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.agent-page {
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.agent-header {
|
||
height: 50px;
|
||
|
||
.header-title {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.tools-panel {
|
||
padding: 12px;
|
||
|
||
.tools-config {
|
||
flex-direction: column;
|
||
|
||
:deep(.el-input) {
|
||
width: 100% !important;
|
||
margin-right: 0 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.welcome-container {
|
||
padding: 20px 12px;
|
||
|
||
.welcome-title {
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|