Compare commits

...

4 Commits

Author SHA1 Message Date
ac384ce10b fix: 优化各页面代码格式和样式
- 微调多个页面的样式和布局
- 保持功能不变

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:39:10 +08:00
950635d9a9 docs: 添加设计和规划文档
- 新增 memory.html 记忆设计文档
- 新增 plan.html 规划文档
- 添加新的项目截图

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:39:04 +08:00
d3100e8219 feat: 优化多个前端页面
- 扩展 Account 账户页面功能
- 优化 Script 脚本页面
- 完善 Settings 设置页面
- 增强 Chat 聊天页面

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:38:57 +08:00
c590aa21d0 feat: 新增 Logs、Memory、Plan 页面
- 添加 Logs.vue 日志页面
- 添加 Memory.vue 记忆页面
- 添加 Plan.vue 计划页面
- 更新路由和侧边栏导航

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:38:23 +08:00
32 changed files with 2554 additions and 167 deletions

304
docs/memory.html Normal file
View File

@@ -0,0 +1,304 @@
<template>
<div class="min-h-screen bg-[#121212] text-gray-200 p-6">
<!-- 顶部开关 -->
<div class="flex justify-end items-center mb-6">
<span class="text-gray-400 mr-3">记忆管理 已启用</span>
<button class="w-12 h-6 bg-green-500 rounded-full relative">
<span class="absolute right-1 top-1 w-4 h-4 bg-white rounded-full"></span>
</button>
</div>
<!-- 统计卡片区域 -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-white mb-1">15</div>
<div class="text-sm text-gray-400">总记忆数</div>
</div>
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-white mb-1">0.78</div>
<div class="text-sm text-gray-400">平均分数</div>
</div>
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-cyan-400 mb-1">6</div>
<div class="text-sm text-gray-400">经验</div>
</div>
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-red-400 mb-1">9</div>
<div class="text-sm text-gray-400">经验教训</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="mb-4">
<div class="relative">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
type="text"
placeholder="搜索记忆内容..."
class="w-full bg-[#0a0a0a] border border-gray-700 rounded-lg py-3 pl-10 pr-4 text-gray-200 focus:outline-none focus:border-gray-600"
>
</div>
</div>
<!-- 筛选与操作栏 -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-3">
<select class="bg-[#0a0a0a] border border-gray-700 rounded-lg py-2 px-4 text-gray-200 focus:outline-none">
<option>全部类型</option>
<option>经验</option>
<option>经验教训</option>
</select>
<button class="flex items-center space-x-2 bg-[#1e1e1e] border border-gray-700 rounded-lg py-2 px-4 text-gray-200 hover:bg-[#2a2a2a]">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span>刷新</span>
</button>
<button class="flex items-center space-x-2 bg-indigo-600 rounded-lg py-2 px-4 text-white hover:bg-indigo-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
<span>LLM 智能审查</span>
</button>
</div>
</div>
<!-- 表格区域 -->
<div class="bg-[#1e1e1e] rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-[#0a0a0a]">
<tr>
<th class="w-10 px-4 py-3">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">类型</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">内容</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">分数</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">创建时间</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
<!-- 表格行 1 -->
<tr class="hover:bg-[#252525]">
<td class="px-4 py-4">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</td>
<td class="px-4 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
</td>
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
当任务明确要求获取外部信息搜索最新数据、新闻、财报、行业报告等模型必须主动调用搜索工具而非仅依赖内部知识。应在系统提示中明确要求「必须使用搜索工具获取XX最新数据」首次迭代即调用搜索工具采用「搜索→分析→输出」的递进式流程避免模型陷入纯文字生成的空转循环。
<div class="text-xs text-gray-500 mt-1">主体: 外部信息获取规范 · 属性: 工具调用原则</div>
</td>
<td class="px-4 py-4">
<span class="text-emerald-400 font-medium">0.95</span>
</td>
<td class="px-4 py-4 text-sm text-gray-400">3/10 15:35</td>
<td class="px-4 py-4">
<div class="flex space-x-2">
<button class="text-gray-400 hover:text-white">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button class="text-red-400 hover:text-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
<!-- 表格行 2 -->
<tr class="hover:bg-[#252525]">
<td class="px-4 py-4">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</td>
<td class="px-4 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
</td>
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
当任务明确要求获取外部信息搜索最新数据、新闻、财报、行业报告等模型必须调用搜索工具而非仅依赖内部知识。应在系统提示中明确要求「必须使用搜索工具获取XX最新数据」并建立工具调用检测机制确保任务执行路径正确。
<div class="text-xs text-gray-500 mt-1">主体: 工具调用 · 属性: 错误教训</div>
</td>
<td class="px-4 py-4">
<span class="text-emerald-400 font-medium">0.95</span>
</td>
<td class="px-4 py-4 text-sm text-gray-400">3/10 12:34</td>
<td class="px-4 py-4">
<div class="flex space-x-2">
<button class="text-gray-400 hover:text-white">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button class="text-red-400 hover:text-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
<!-- 表格行 3 -->
<tr class="hover:bg-[#252525]">
<td class="px-4 py-4">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</td>
<td class="px-4 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
</td>
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
任务执行应采用「一次性完整输出」策略避免分批次小幅输出导致的迭代空转。对于代码生成、文件写入等任务应要求模型一次性完成方案设计与工具执行的完整流程将确认性回复并入上一次响应将冗余迭代压缩合并复杂任务控制在2-3次迭代内完成。
<div class="text-xs text-gray-500 mt-1">主体: 任务执行效率优化 · 属性: 迭代策略原则</div>
</td>
<td class="px-4 py-4">
<span class="text-emerald-400 font-medium">0.92</span>
</td>
<td class="px-4 py-4 text-sm text-gray-400">3/10 15:35</td>
<td class="px-4 py-4">
<div class="flex space-x-2">
<button class="text-gray-400 hover:text-white">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button class="text-red-400 hover:text-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
<!-- 表格行 4 -->
<tr class="hover:bg-[#252525]">
<td class="px-4 py-4">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</td>
<td class="px-4 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
</td>
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
任务执行过程中应建立工具调用检测机制监控模型是否按要求调用了必要的工具。若迭代中工具调用数为0说明执行路径不正确应触发异常处理或提示而非继续无效迭代。
<div class="text-xs text-gray-500 mt-1">主体: 执行路径监控 · 属性: 质量控制机制</div>
</td>
<td class="px-4 py-4">
<span class="text-emerald-400 font-medium">0.88</span>
</td>
<td class="px-4 py-4 text-sm text-gray-400">3/10 15:35</td>
<td class="px-4 py-4">
<div class="flex space-x-2">
<button class="text-gray-400 hover:text-white">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button class="text-red-400 hover:text-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
<!-- 表格行 5 -->
<tr class="hover:bg-[#252525]">
<td class="px-4 py-4">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</td>
<td class="px-4 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
</td>
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
任务执行应采用「搜索→分析→输出」的递进式流程避免无效迭代和空转。纯文字生成的重复迭代应压缩或合并确认性回复应并入上一次响应。简单任务应在1-2次迭代内完成避免模型陷入重复思考或无意义的自我确认。
<div class="text-xs text-gray-500 mt-1">主体: 迭代效率 · 属性: 错误教训</div>
</td>
<td class="px-4 py-4">
<span class="text-emerald-400 font-medium">0.85</span>
</td>
<td class="px-4 py-4 text-sm text-gray-400">3/10 12:34</td>
<td class="px-4 py-4">
<div class="flex space-x-2">
<button class="text-gray-400 hover:text-white">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button class="text-red-400 hover:text-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
<!-- 表格行 6 -->
<tr class="hover:bg-[#252525]">
<td class="px-4 py-4">
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
</td>
<td class="px-4 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
</td>
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
对于代码生成或文件写入类任务,应要求模型一次性完整输出或按模块批量输出,避免分批次的小幅输出导致迭代次数过多、执行效率低下。
<div class="text-xs text-gray-500 mt-1">主体: 代码生成策略 · 属性: 错误教训</div>
</td>
<td class="px-4 py-4">
<span class="text-amber-400 font-medium">0.80</span>
</td>
<td class="px-4 py-4 text-sm text-gray-400">3/10 12:34</td>
<td class="px-4 py-4">
<div class="flex space-x-2">
<button class="text-gray-400 hover:text-white">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button class="text-red-400 hover:text-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 底部文本区域 -->
<div class="mt-6 px-4 text-gray-400 text-sm">
<div class="mb-2">任务执行复盘发现问题:## 任务执行分析</div>
<div class="mb-2">### 问题诊断</div>
<div class="mb-2">**核心问题模型在9次迭代中未执行任何工具调用**</div>
<div>从迭代记录看前9次迭代工具调用数均为0说明模型一直在"空转"生成文字未实际搜索最新数据。直到第10次才产出完整报告包含2025年9月销量等最新数据说明最后一次才真正调用了搜索工具。</div>
</div>
</div>
</template>
<script setup>
// 这里可以添加交互逻辑
</script>
<style scoped>
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #444;
}
</style>

212
docs/plan.html Normal file
View File

@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>计划任务管理</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #444;
}
</style>
</head>
<body class="bg-[#0f0f0f] text-gray-200 min-h-screen font-sans">
<!-- 顶部导航栏 -->
<header class="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-[#0a0a0a]">
<!-- 左侧状态区 -->
<div class="flex items-center space-x-3">
<span class="text-gray-300 font-medium">default</span>
<span class="flex items-center text-green-400 text-sm">
<span class="w-2 h-2 rounded-full bg-green-400 mr-1.5"></span>
运行中
</span>
<span class="bg-blue-600/20 text-blue-400 text-xs px-2 py-0.5 rounded">网页访问</span>
<span class="text-gray-400 text-sm">远程地址</span>
<span class="text-gray-400 text-sm">1 端点</span>
</div>
<!-- 右侧操作区 -->
<div class="flex items-center space-x-4">
<button class="flex items-center space-x-1 border border-blue-500 bg-[#0a0a0a] text-white px-3 py-1 rounded text-sm hover:bg-blue-900/20 transition">
<span>×</span>
<span>断开</span>
</button>
<button class="text-gray-400 hover:text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
<button class="text-gray-400 hover:text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
</svg>
</button>
<button class="text-gray-400 hover:text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
</svg>
</button>
<div class="flex items-center ml-4">
<span class="text-gray-400 mr-2 text-sm">计划任务 已启用</span>
<button class="w-10 h-5 bg-green-500 rounded-full relative">
<span class="absolute right-0.5 top-0.5 w-4 h-4 bg-white rounded-full"></span>
</button>
</div>
</div>
</header>
<!-- 主体内容区 -->
<main class="px-6 py-5">
<!-- 标题与操作栏 -->
<div class="flex justify-between items-start mb-5">
<!-- 标题+标签页 -->
<div>
<h1 class="text-xl font-bold text-white mb-3">计划任务</h1>
<div class="flex items-center space-x-6">
<button class="text-blue-400 border-b-2 border-blue-500 px-1 py-2 font-medium text-sm -mb-[1px]">进行中 3</button>
<button class="text-gray-400 hover:text-white px-1 py-2 text-sm transition">已完成 0</button>
<button class="text-gray-400 hover:text-white px-1 py-2 text-sm transition">全部 3</button>
</div>
</div>
<!-- 右侧操作+搜索区 -->
<div class="flex flex-col items-end space-y-2">
<div class="flex items-center space-x-3">
<button class="flex items-center space-x-1 text-gray-400 hover:text-white text-sm transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span>刷新</span>
</button>
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded text-sm flex items-center space-x-1 transition">
<span>+</span>
<span>新建任务</span>
</button>
</div>
<div class="relative">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
type="text"
placeholder="搜索任务..."
class="w-full bg-[#0a0a0a] border border-gray-700 rounded-lg py-2 pl-10 pr-4 text-sm text-gray-200 focus:outline-none focus:border-gray-600"
>
</div>
</div>
</div>
<!-- 任务列表 -->
<div class="space-y-3">
<!-- 任务1活人感心跳 -->
<div class="bg-[#1a1a1a] rounded-lg p-4 hover:bg-[#1e1e1e] transition">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<span class="text-white font-medium">活人感心跳</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">系统任务</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">Agent 任务</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">间隔重复</span>
</div>
<div class="flex items-center space-x-1">
<button class="bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-3 py-1 rounded transition">暂停</button>
<button class="text-gray-400 hover:text-white p-1 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
</div>
<div class="flex items-center flex-wrap gap-x-8 gap-y-1 text-xs text-gray-400 mb-2">
<span>状态: 已调度</span>
<span>触发方式: 间隔重复 30 分钟</span>
<span>下次执行: 2026/03/10 16:26</span>
<span>上次执行: 2026/03/10 15:56</span>
<span>通知通道: -</span>
<span>执行次数: 13</span>
</div>
<div class="text-sm text-gray-300">
检查是否需要发送主动消息 (问候/提醒/跟进)
</div>
</div>
<!-- 任务2记忆整理 -->
<div class="bg-[#1a1a1a] rounded-lg p-4 hover:bg-[#1e1e1e] transition">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<span class="text-white font-medium">记忆整理</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">系统任务</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">Agent 任务</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">间隔重复</span>
</div>
<div class="flex items-center space-x-1">
<button class="bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-3 py-1 rounded transition">暂停</button>
<button class="text-gray-400 hover:text-white p-1 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
</div>
<div class="flex items-center flex-wrap gap-x-8 gap-y-1 text-xs text-gray-400 mb-2">
<span>状态: 已调度</span>
<span>触发方式: 间隔重复 3 小时</span>
<span>下次执行: 2026/03/10 18:35</span>
<span>上次执行: 2026/03/10 15:35</span>
<span>通知通道: -</span>
<span>执行次数: 2</span>
</div>
<div class="text-sm text-gray-300">
执行记忆整理: 整理对话历史, 提取精华记忆, 刷新 MEMORY.md
</div>
</div>
<!-- 任务3系统自检 -->
<div class="bg-[#1a1a1a] rounded-lg p-4 hover:bg-[#1e1e1e] transition">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<span class="text-white font-medium">系统自检</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">系统任务</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">Agent 任务</span>
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">每天</span>
</div>
<div class="flex items-center space-x-1">
<button class="bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-3 py-1 rounded transition">暂停</button>
<button class="text-gray-400 hover:text-white p-1 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
</div>
<div class="flex items-center flex-wrap gap-x-8 gap-y-1 text-xs text-gray-400 mb-2">
<span>状态: 已调度</span>
<span>触发方式: 每天 04:00</span>
<span>下次执行: 2026/03/11 04:00</span>
<span>上次执行: 从未</span>
<span>通知通道: -</span>
<span>执行次数: 0</span>
</div>
<div class="text-sm text-gray-300">
执行系统自检: 分析 ERROR 日志, 尝试修复工具问题, 生成报告
</div>
</div>
</div>
</main>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -51,11 +51,13 @@ const group2 = computed(() => [
{ name: 'Knowledge', icon: 'fa-brain', path: '/knowledge', badge: knowledgeCount.value },
])
// 第3组: Skills, Tools, Script
// 第3组: Skills, Tools, Script, Plan, Memory
const group3 = computed(() => [
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
{ name: 'Script', icon: 'fa-code', path: '/script' },
{ name: 'Plan', icon: 'fa-clock', path: '/plan' },
{ name: 'Memory', icon: 'fa-brain', path: '/memory' },
])
// 第4组: Dashboard, Account, Settings

View File

@@ -8,9 +8,12 @@ import Skill from '@/views/Skill.vue'
import ModelAPIs from '@/views/ModelAPIs.vue'
import Database from '@/views/Database.vue'
import Script from '@/views/Script.vue'
import Plan from '@/views/Plan.vue'
import Memory from '@/views/Memory.vue'
import Knowledge from '@/views/Knowledge.vue'
import Settings from '@/views/Settings.vue'
import Account from '@/views/Account.vue'
import Logs from '@/views/Logs.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -60,6 +63,16 @@ const router = createRouter({
name: 'script',
component: Script
},
{
path: '/plan',
name: 'plan',
component: Plan
},
{
path: '/memory',
name: 'memory',
component: Memory
},
{
path: '/knowledge',
name: 'knowledge',
@@ -74,6 +87,11 @@ const router = createRouter({
path: '/account',
name: 'account',
component: Account
},
{
path: '/logs',
name: 'logs',
component: Logs
}
]
})

View File

@@ -1,15 +1,484 @@
<script setup lang="ts">
// Account 页面 - 占位
import { ref, computed } from 'vue'
import './database/database.css'
// 菜单类型
type MenuKey = 'users' | 'roles' | 'permissions'
// 当前选中的菜单
const activeMenu = ref<MenuKey>('users')
// 菜单列表
const menuItems = [
{ key: 'users', label: 'Users', icon: 'fa-users' },
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
]
// 用户数据
interface User {
id: number
name: string
email: string
role: string
status: 'active' | 'inactive'
createdAt: string
}
const users = ref<User[]>([
{ id: 1, name: 'Alex Smith', email: 'alex@example.com', role: 'Admin', status: 'active', createdAt: '2025-01-15' },
{ id: 2, name: 'John Doe', email: 'john@example.com', role: 'Developer', status: 'active', createdAt: '2025-02-20' },
{ id: 3, name: 'Jane Wilson', email: 'jane@example.com', role: 'Viewer', status: 'active', createdAt: '2025-03-05' },
{ id: 4, name: 'Mike Brown', email: 'mike@example.com', role: 'Developer', status: 'inactive', createdAt: '2025-03-10' },
{ id: 5, name: 'Sarah Davis', email: 'sarah@example.com', role: 'Admin', status: 'active', createdAt: '2025-03-15' },
])
// 角色数据
interface Role {
id: number
name: string
description: string
userCount: number
permissions: string[]
}
const roles = ref<Role[]>([
{ id: 1, name: 'Admin', description: 'Full access to all features', userCount: 2, permissions: ['all'] },
{ id: 2, name: 'Developer', description: 'Access to development tools', userCount: 2, permissions: ['read', 'write', 'execute'] },
{ id: 3, name: 'Viewer', description: 'Read-only access', userCount: 1, permissions: ['read'] },
{ id: 4, name: 'Manager', description: 'Manage users and resources', userCount: 0, permissions: ['read', 'write', 'manage'] },
])
// 权限数据
interface Permission {
id: string
name: string
description: string
category: string
}
const permissions = ref<Permission[]>([
{ id: 'read', name: 'Read', description: 'View resources', category: 'General' },
{ id: 'write', name: 'Write', description: 'Create and update resources', category: 'General' },
{ id: 'delete', name: 'Delete', description: 'Remove resources', category: 'General' },
{ id: 'execute', name: 'Execute', description: 'Run scripts and tools', category: 'Tools' },
{ id: 'manage', name: 'Manage', description: 'Manage users and settings', category: 'Admin' },
{ id: 'all', name: 'All Access', description: 'Full system access', category: 'Admin' },
])
// 搜索和筛选
const searchQuery = ref('')
const filterRole = ref('')
// 编辑状态
const isEditingUser = ref(false)
const editingUser = ref<User | null>(null)
const isEditingRole = ref(false)
const editingRole = ref<Role | null>(null)
const isCreatingRole = ref(false)
const newRole = ref({ name: '', description: '' })
// 过滤后的用户
const filteredUsers = computed(() => {
return users.value.filter(user => {
const matchSearch = user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchRole = filterRole.value === '' || user.role === filterRole.value
return matchSearch && matchRole
})
})
// 获取用户角色选项
const roleOptions = computed(() => {
return [...new Set(users.value.map(u => u.role))]
})
// 编辑用户
const openEditUser = (user: User) => {
editingUser.value = { ...user }
isEditingUser.value = true
}
const saveUser = () => {
if (editingUser.value) {
const index = users.value.findIndex(u => u.id === editingUser.value!.id)
if (index !== -1) {
users.value[index] = { ...editingUser.value }
}
}
isEditingUser.value = false
}
// 删除用户
const deleteUser = (id: number) => {
users.value = users.value.filter(u => u.id !== id)
}
// 切换用户状态
const toggleUserStatus = (user: User) => {
user.status = user.status === 'active' ? 'inactive' : 'active'
}
// 编辑角色
const openEditRole = (role: Role) => {
editingRole.value = { ...role }
isEditingRole.value = true
}
const saveRole = () => {
if (editingRole.value) {
const index = roles.value.findIndex(r => r.id === editingRole.value!.id)
if (index !== -1) {
roles.value[index] = { ...editingRole.value }
}
}
isEditingRole.value = false
}
// 删除角色
const deleteRole = (id: number) => {
roles.value = roles.value.filter(r => r.id !== id)
}
// 创建角色
const openCreateRole = () => {
newRole.value = { name: '', description: '' }
isCreatingRole.value = true
}
const saveNewRole = () => {
const newId = Math.max(...roles.value.map(r => r.id)) + 1
roles.value.push({
id: newId,
name: newRole.value.name || 'New Role',
description: newRole.value.description,
userCount: 0,
permissions: [],
})
isCreatingRole.value = false
}
// 状态样式
const statusClass = (status: string) => {
return status === 'active' ? 'bg-primary-success' : 'bg-gray-500'
}
</script>
<template>
<div class="p-6 min-h-screen">
<!-- 页面标题 -->
<div class="flex items-center gap-2 mb-6">
<i class="fa-solid fa-user text-gray-400"></i>
<i class="fa-solid fa-user-shield text-orange-500"></i>
<span class="font-medium">Account</span>
</div>
<div class="text-gray-400">
Account management coming soon...
<div class="flex gap-6">
<!-- 左侧菜单 -->
<nav class="w-48 flex-shrink-0">
<ul class="space-y-1">
<li
v-for="item in menuItems"
:key="item.key"
@click="activeMenu = item.key as MenuKey"
class="px-4 py-3 rounded-lg cursor-pointer transition-colors flex items-center gap-3"
:class="activeMenu === item.key
? 'bg-orange-500/10 text-orange-400'
: 'text-gray-400 hover:bg-dark-600 hover:text-white'"
>
<i :class="['fa-solid', item.icon]"></i>
<span>{{ item.label }}</span>
</li>
</ul>
</nav>
<!-- 右侧内容 -->
<div class="flex-1">
<!-- Users -->
<div v-if="activeMenu === 'users'" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold">Users</h2>
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
<i class="fa-solid fa-plus"></i>
Add User
</button>
</div>
<!-- 搜索和筛选 -->
<div class="flex gap-4">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search users..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<el-select v-model="filterRole" placeholder="All Roles" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option label="All Roles" value="" />
<el-option v-for="role in roleOptions" :key="role" :label="role" :value="role" />
</el-select>
</div>
<!-- 用户列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Role</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="user in filteredUsers" :key="user.id" class="border-t border-dark-600 hover:bg-dark-600/50">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-orange-500 to-red-500 flex items-center justify-center text-white text-sm font-medium">
{{ user.name.charAt(0) }}
</div>
<div>
<div class="font-medium">{{ user.name }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-5 py-4">
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ user.role }}</span>
</td>
<td class="px-5 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="statusClass(user.status)"></span>
<span class="capitalize text-sm">{{ user.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ user.createdAt }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button
@click="toggleUserStatus(user)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
:title="user.status === 'active' ? 'Deactivate' : 'Activate'"
>
<i :class="['fa-solid', user.status === 'active' ? 'fa-ban' : 'fa-check', 'text-gray-400 hover:text-white']"></i>
</button>
<button
@click="openEditUser(user)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Edit"
>
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button
@click="deleteUser(user.id)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="filteredUsers.length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-users text-4xl mb-3"></i>
<p>No users found</p>
</div>
</div>
</div>
<!-- Roles -->
<div v-if="activeMenu === 'roles'" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold">Roles</h2>
<button @click="openCreateRole" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
<i class="fa-solid fa-plus"></i>
Add Role
</button>
</div>
<!-- 角色列表 -->
<div class="grid grid-cols-2 gap-4">
<div v-for="role in roles" :key="role.id" class="bg-dark-700 rounded-xl p-5 border border-dark-600">
<div class="flex items-start justify-between mb-3">
<div>
<h3 class="font-semibold text-lg">{{ role.name }}</h3>
<p class="text-sm text-gray-400">{{ role.description }}</p>
</div>
<div class="flex gap-1">
<button @click="openEditRole(role)" class="p-2 rounded-lg hover:bg-dark-600 transition-colors">
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button @click="deleteRole(role.id)" class="p-2 rounded-lg hover:bg-dark-600 transition-colors">
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
</button>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex flex-wrap gap-1">
<span v-for="perm in role.permissions.slice(0, 3)" :key="perm" class="px-2 py-0.5 bg-dark-600 rounded text-xs text-gray-400">
{{ perm }}
</span>
<span v-if="role.permissions.length > 3" class="px-2 py-0.5 bg-dark-600 rounded text-xs text-gray-400">
+{{ role.permissions.length - 3 }}
</span>
</div>
<span class="text-sm text-gray-400">{{ role.userCount }} users</span>
</div>
</div>
</div>
</div>
<!-- Permissions -->
<div v-if="activeMenu === 'permissions'" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold">Permissions</h2>
</div>
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Permission</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Description</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Category</th>
</tr>
</thead>
<tbody>
<tr v-for="perm in permissions" :key="perm.id" class="border-t border-dark-600 hover:bg-dark-600/50">
<td class="px-5 py-4">
<span class="font-medium">{{ perm.name }}</span>
<span class="text-gray-500 ml-2">({{ perm.id }})</span>
</td>
<td class="px-5 py-4 text-gray-400">{{ perm.description }}</td>
<td class="px-5 py-4">
<span class="px-2 py-1 bg-dark-500 rounded text-sm">{{ perm.category }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 编辑用户弹窗 -->
<Teleport to="body">
<div v-if="isEditingUser" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit User</h3>
<button @click="isEditingUser = false" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Name</label>
<input
v-model="editingUser!.name"
type="text"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Email</label>
<input
v-model="editingUser!.email"
type="email"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Role</label>
<el-select v-model="editingUser!.role" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option v-for="role in roleOptions" :key="role" :label="role" :value="role" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Status</label>
<el-select v-model="editingUser!.status" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
</el-select>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="isEditingUser = false"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveUser"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
>
Save Changes
</button>
</div>
</div>
</div>
</Teleport>
<!-- 创建角色弹窗 -->
<Teleport to="body">
<div v-if="isCreatingRole" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Create Role</h3>
<button @click="isCreatingRole = false" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Role Name</label>
<input
v-model="newRole.name"
type="text"
placeholder="Enter role name..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="newRole.description"
rows="3"
placeholder="Describe this role..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="isCreatingRole = false"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveNewRole"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
>
Create Role
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>

View File

@@ -70,7 +70,7 @@ const deleteAgent = (id: number) => {
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-robot text-gray-400"></i>
<i class="fa-solid fa-robot text-orange-500"></i>
<span class="font-medium">Agents</span>
</div>
<button class="btn-primary">

View File

@@ -19,6 +19,14 @@ interface Agent {
status: 'online' | 'offline'
}
interface ChatSession {
id: number
title: string
agentId: number
lastMessage: string
timestamp: Date
}
// AI 助手配置
const chatAgents = ref<Agent[]>([
{ id: 1, name: 'Claude', avatar: '🧠', description: 'Anthropic AI', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'online' },
@@ -32,13 +40,22 @@ const chatAgents = ref<Agent[]>([
// 当前选中的助手
const selectedAgent = ref<Agent | null>(chatAgents.value[0])
const sidebarCollapsed = ref(false)
// 聊天消息
const messages = ref<ChatMessage[]>([
{ id: 1, role: 'assistant', content: '你好!我是 Claude你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() },
])
// 模拟历史对话列表
const chatSessions = ref<ChatSession[]>([
{ id: 1, title: '关于 Python 学习的讨论', agentId: 1, lastMessage: '谢谢你!', timestamp: new Date(Date.now() - 3600000) },
{ id: 2, title: '代码调试帮助', agentId: 1, lastMessage: '让我看看这个问题...', timestamp: new Date(Date.now() - 7200000) },
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) },
])
// 侧边栏展开/收起状态
const sidebarCollapsed = ref(false)
// 输入内容
const inputMessage = ref('')
const isLoading = ref(false)
@@ -118,6 +135,17 @@ const selectAgent = (agent: Agent) => {
]
}
// 选择历史对话
const selectSession = (session: ChatSession) => {
const agent = chatAgents.value.find(a => a.id === session.agentId)
if (agent) {
selectedAgent.value = agent
}
messages.value = [
{ id: 1, role: 'assistant', content: `已加载会话:${session.title}`, timestamp: new Date() }
]
}
// 新建聊天
const newChat = () => {
messages.value = [
@@ -130,6 +158,19 @@ const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// 格式化相对时间
const formatRelativeTime = (date: Date) => {
const now = new Date()
const diff = now.getTime() - date.getTime()
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (hours < 1) return '刚刚'
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
return date.toLocaleDateString('zh-CN')
}
// 回车发送
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -145,18 +186,15 @@ const autoResize = (e: Event) => {
target.style.height = Math.min(target.scrollHeight, 160) + 'px'
}
// 切换侧边栏
// 折叠侧边栏
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
setTimeout(() => {
scrollToBottom()
}, 350)
}
</script>
<style scoped>
::-webkit-scrollbar {
width: 4px;
width: 6px;
}
::-webkit-scrollbar-track {
@@ -164,27 +202,27 @@ const toggleSidebar = () => {
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.15);
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(12px);
transform: translateY(16px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0);
transform: translateY(0) scale(1);
}
}
.message-enter {
animation: messageSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
animation: messageSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes blink {
@@ -195,53 +233,53 @@ const toggleSidebar = () => {
.cursor-blink {
animation: blink 1s step-end infinite;
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
50% { box-shadow: 0 0 20px 4px rgba(249, 115, 22, 0.2); }
}
.agent-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
</style>
<template>
<div class="h-screen flex bg-[#0a0a0f]">
<div class="h-screen flex bg-[#09090b]">
<!-- 主聊天区域 -->
<div class="flex-1 flex flex-col bg-[#0a0a0f]">
<div class="flex-1 flex flex-col bg-[#09090b]">
<!-- 顶部栏 -->
<div class="h-14 px-6 flex items-center justify-between border-b border-white/5 bg-[#0d0d12]/50 backdrop-blur-sm">
<div class="h-16 px-4 flex items-center justify-between border-b border-white/[0.06] bg-[#0c0c0f]/80 backdrop-blur-xl">
<!-- 左侧当前AI信息 -->
<div class="flex items-center gap-3">
<div v-if="selectedAgent" class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg shadow-lg"
:style="{ backgroundColor: selectedAgent.accentColor + '20', color: selectedAgent.accentColor }"
class="w-9 h-9 rounded-xl flex items-center justify-center text-lg shadow-lg"
:style="{ backgroundColor: selectedAgent.accentColor + '15', color: selectedAgent.accentColor }"
>
{{ selectedAgent.avatar }}
</div>
<div>
<div class="text-sm font-medium text-white">{{ selectedAgent?.name || 'Chat' }}</div>
<div class="text-sm font-semibold text-white tracking-wide">{{ selectedAgent?.name || 'Chat' }}</div>
<div class="text-[11px] flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
<span class="text-white/40">Online</span>
</div>
</div>
</div>
</div>
<!-- 右上角操作 -->
<div class="flex items-center gap-2">
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
</button>
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
</svg>
</button>
<!-- 展开侧边栏按钮 -->
<!-- 中间空白 -->
<div class="flex-1"></div>
<!-- 右侧折叠按钮 -->
<div class="flex items-center">
<button
v-if="sidebarCollapsed"
@click="toggleSidebar"
class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors"
title="Show AI assistants"
class="p-2.5 rounded-xl hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200"
:title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-[18px] h-[18px] transition-transform duration-300" :class="sidebarCollapsed ? '' : 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
@@ -249,20 +287,20 @@ const toggleSidebar = () => {
</div>
<!-- 消息区域 -->
<div ref="messagesContainer" class="flex-1 overflow-y-auto px-6 py-6">
<div class="max-w-3xl mx-auto space-y-6">
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
<div class="px-6">
<div
v-for="message in messages"
:key="message.id"
class="message-enter flex gap-4"
class="message-enter flex items-start mb-4"
:class="message.role === 'user' ? 'flex-row-reverse' : ''"
>
<!-- 头像 -->
<div
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 shadow-lg"
:class="message.role === 'user' ? 'bg-gradient-to-br from-emerald-500 to-teal-600' : ''"
class="w-9 h-9 rounded-full flex-shrink-0 flex items-center justify-center mx-3 mt-1"
:class="message.role === 'user' ? 'bg-gradient-to-br from-orange-500 to-amber-600' : ''"
:style="message.role === 'assistant' && selectedAgent ? {
backgroundColor: selectedAgent.accentColor + '20',
backgroundColor: selectedAgent.accentColor + '25',
color: selectedAgent.accentColor
} : {}"
>
@@ -270,28 +308,24 @@ const toggleSidebar = () => {
<span v-else class="text-lg">{{ selectedAgent?.avatar || '🧠' }}</span>
</div>
<!-- 消息内容 -->
<div
class="max-w-[75%] rounded-2xl px-4 py-3"
:class="message.role === 'user' ? 'bg-[#1e1e28] text-white' : 'bg-transparent'"
>
<div class="text-sm leading-relaxed whitespace-pre-wrap text-white/90">{{ message.content }}
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-violet-400 cursor-blink align-middle"></span>
<!-- 气泡和时间戳容器 -->
<div :class="message.role === 'user' ? 'mr-3 ml-auto' : 'ml-3'">
<!-- 消息气泡 -->
<div
class="px-4 py-2.5 rounded-xl text-[14px] leading-6"
:class="message.role === 'user'
? 'bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-tr-sm'
: 'bg-[#2a2a35] text-white/90 rounded-tl-sm max-w-[80%]'"
>
{{ message.content }}
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-orange-300 cursor-blink align-middle"></span>
</div>
<!-- 消息底部 -->
<div class="flex items-center justify-end mt-2 gap-3">
<span class="text-[10px] text-white/25">{{ formatTime(message.timestamp) }}</span>
<button
v-if="message.role === 'assistant' && !message.isStreaming"
@click="copyMessage(message.content)"
class="text-white/25 hover:text-violet-400 transition-colors"
title="Copy"
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
<!-- 时间戳 -->
<div
class="text-[11px] text-white/30 mt-1"
:class="message.role === 'user' ? 'text-right' : ''"
>
{{ formatTime(message.timestamp) }}
</div>
</div>
</div>
@@ -299,11 +333,11 @@ const toggleSidebar = () => {
</div>
<!-- 输入区域 -->
<div class="p-4 border-t border-white/5 bg-[#0d0d12]/50">
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
<div class="max-w-3xl mx-auto">
<div class="relative bg-[#12121a] rounded-2xl border border-white/8 focus-within:border-violet-500/40 focus-within:shadow-lg focus-within:shadow-violet-500/10 transition-all duration-300">
<div class="relative bg-[#12121a] rounded-2xl border border-white/[0.08] focus-within:border-orange-500/40 focus-within:shadow-[0_0_30px_rgba(249,115,22,0.08)] transition-all duration-300">
<!-- 附件按钮 -->
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors p-1">
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/25 hover:text-orange-400 transition-colors p-1">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
</svg>
@@ -314,117 +348,111 @@ const toggleSidebar = () => {
v-model="inputMessage"
@keydown="handleKeydown"
@input="autoResize"
placeholder="Send a message..."
placeholder="发送消息..."
rows="1"
class="w-full bg-transparent text-white placeholder-white/30 py-3.5 pl-12 pr-24 resize-none focus:outline-none text-sm"
class="w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
></textarea>
<!-- 发送按钮 -->
<button
@click="sendMessage"
:disabled="!inputMessage.trim() || isLoading"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-violet-500 hover:bg-violet-400 disabled:bg-white/8 disabled:text-white/20 text-white transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/25"
class="absolute right-2 top-1/2 -translate-y-1/2 w-9 h-9 rounded-lg flex items-center justify-center transition-all duration-200 disabled:opacity-30 disabled:cursor-not-allowed"
:class="inputMessage.trim() && !isLoading
? 'bg-orange-500 hover:bg-orange-400 shadow-lg shadow-orange-500/30 active:scale-90'
: 'bg-white/10'"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
<svg v-if="!isLoading" class="w-4 h-4 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
<svg v-else class="w-4 h-4 text-white animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
<!-- 提示 -->
<div class="text-center mt-2.5">
<span class="text-[10px] text-white/20">AI can make mistakes. Please verify important information.</span>
<div class="text-center mt-3">
<span class="text-[10px] text-white/20 tracking-wide">AI 可能会产生错误信息请核实重要内容</span>
</div>
</div>
</div>
</div>
<!-- 右侧边栏 - 可折叠 -->
<transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 translate-x-4"
enter-to-class="opacity-100 translate-x-0"
leave-active-class="transition-all duration-250 ease-in"
leave-from-class="opacity-100 translate-x-0"
leave-to-class="opacity-0 translate-x-4"
<!-- 右侧边栏AI Hub -->
<div
class="flex-shrink-0 border-l border-white/[0.06] bg-[#0c0c0f] transition-all duration-300 ease-in-out overflow-hidden"
:class="sidebarCollapsed ? 'w-0 opacity-0' : 'w-72 opacity-100'"
>
<div v-show="!sidebarCollapsed" class="w-72 bg-[#0d0d12] border-l border-white/5 flex flex-col">
<!-- Logo -->
<div class="p-4 border-b border-white/5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-500/25">
<span class="text-white text-lg">🤖</span>
</div>
<span class="text-lg font-semibold text-white tracking-tight">AI Hub</span>
</div>
<div class="w-72 h-full flex flex-col">
<!-- 侧边栏头部 -->
<div class="p-4 border-b border-white/[0.06]">
<div class="flex items-center gap-2 text-white font-semibold">
<svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
<span>AI Hub</span>
</div>
</div>
<!-- 新建对话按钮 -->
<div class="p-3">
<button
@click="newChat"
class="w-full flex items-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium transition-all duration-200"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
<span>新建对话</span>
</button>
</div>
<!-- AI 助手选择 -->
<div class="px-3 pb-3">
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">选择 AI 助手</div>
<div class="space-y-1">
<button
@click="toggleSidebar"
class="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white/60 transition-colors"
title="Hide sidebar"
v-for="agent in chatAgents"
:key="agent.id"
@click="selectAgent(agent)"
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-200"
:class="selectedAgent?.id === agent.id
? 'bg-orange-500/15 text-orange-400'
: 'text-white/60 hover:bg-white/5 hover:text-white'"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"></path>
</svg>
<span class="text-base">{{ agent.avatar }}</span>
<span class="text-sm truncate">{{ agent.name }}</span>
<span
v-if="agent.status === 'online'"
class="w-1.5 h-1.5 rounded-full bg-emerald-400 ml-auto"
></span>
</button>
</div>
</div>
<!-- 新建聊天按钮 -->
<div class="p-4">
<button
@click="newChat"
class="w-full py-2.5 px-4 bg-[#1a1a24] hover:bg-[#22222e] border border-white/8 hover:border-violet-500/30 rounded-xl text-white/90 text-sm flex items-center justify-center gap-2 transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/10 group"
>
<svg class="w-4 h-4 text-violet-400 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
<span class="font-medium">New Chat</span>
</button>
</div>
<!-- AI 助手列表 -->
<div class="flex-1 overflow-y-auto px-3 py-2">
<div class="text-[11px] font-medium text-white/30 uppercase tracking-wider px-3 mb-3">AI Assistants</div>
<!-- 历史对话列表 -->
<div class="flex-1 overflow-y-auto px-3 pb-3">
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">最近对话</div>
<div class="space-y-1">
<div
v-for="agent in chatAgents"
:key="agent.id"
@click="selectAgent(agent)"
class="group px-3 py-2.5 rounded-xl cursor-pointer transition-all duration-200"
:class="selectedAgent?.id === agent.id
? 'bg-gradient-to-r ' + agent.gradient + ' border-l-2'
: 'hover:bg-white/[0.03] border-l-2 border-transparent'"
:style="selectedAgent?.id === agent.id ? `border-left-color: ${agent.accentColor}` : ''"
<button
v-for="session in chatSessions"
:key="session.id"
@click="selectSession(session)"
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
>
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg transition-transform duration-200 group-hover:scale-110"
:class="selectedAgent?.id === agent.id ? 'shadow-lg' : ''"
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
>
{{ agent.avatar }}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-white/90 truncate">{{ agent.name }}</div>
<div class="text-[11px] text-white/40 truncate">{{ agent.description }}</div>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-white/30 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
</svg>
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ session.title }}</span>
</div>
</div>
<div class="text-xs text-white/30 mt-1 pl-6">{{ formatRelativeTime(session.timestamp) }}</div>
</button>
</div>
</div>
<!-- 底部设置 -->
<div class="p-4 border-t border-white/5">
<button class="w-full py-2.5 rounded-xl bg-white/[0.02] hover:bg-white/[0.05] text-white/50 hover:text-white/80 text-sm flex items-center justify-center gap-2 transition-all duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Settings</span>
</button>
</div>
</div>
</transition>
</div>
</div>
</template>

View File

@@ -118,7 +118,7 @@ const topRequestsTab = ref<'general' | 'errors'>('general')
<!-- 顶部导航与日期选择区 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-gauge text-gray-400"></i>
<i class="fa-solid fa-gauge text-orange-500"></i>
<span class="font-medium">Dashboard</span>
</div>
<div class="flex items-center gap-4">

View File

@@ -57,7 +57,7 @@ const {
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-database text-gray-400"></i>
<i class="fa-solid fa-database text-orange-500"></i>
<span class="font-medium">Database</span>
</div>
<button @click="openCreate" class="btn-primary">

View File

@@ -716,7 +716,7 @@ const deleteDocument = async (docId: string) => {
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-brain text-gray-400"></i>
<i class="fa-solid fa-brain text-orange-500"></i>
<span class="font-medium">Knowledge Base</span>
</div>
<button @click="openCreateDialog" class="btn-primary">

226
web/src/views/Logs.vue Normal file
View File

@@ -0,0 +1,226 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import './database/database.css'
// 日志数据
interface Log {
id: number
level: 'info' | 'warning' | 'error' | 'debug'
source: string
message: string
timestamp: string
user?: string
}
const logs = ref<Log[]>([
{ id: 1, level: 'info', source: 'System', message: 'User logged in successfully', timestamp: '2025-03-10 14:35:22', user: 'alex@example.com' },
{ id: 2, level: 'warning', source: 'API', message: 'Rate limit approaching for API key', timestamp: '2025-03-10 14:32:15', user: 'john@example.com' },
{ id: 3, level: 'error', source: 'Database', message: 'Connection timeout to primary database', timestamp: '2025-03-10 14:30:45' },
{ id: 4, level: 'info', source: 'Skill', message: 'MCP Server started successfully', timestamp: '2025-03-10 14:28:10' },
{ id: 5, level: 'debug', source: 'Auth', message: 'Token refresh initiated', timestamp: '2025-03-10 14:25:33', user: 'jane@example.com' },
{ id: 6, level: 'error', source: 'Script', message: 'Failed to execute backup script', timestamp: '2025-03-10 14:20:18' },
{ id: 7, level: 'info', source: 'Account', message: 'User role updated', timestamp: '2025-03-10 14:15:42', user: 'admin@example.com' },
{ id: 8, level: 'warning', source: 'Memory', message: 'Memory usage exceeds 80% threshold', timestamp: '2025-03-10 14:10:55' },
{ id: 9, level: 'info', source: 'Knowledge', message: 'Document indexed successfully', timestamp: '2025-03-10 14:05:30' },
{ id: 10, level: 'error', source: 'API', message: 'Invalid API key provided', timestamp: '2025-03-10 14:00:12' },
])
// 搜索和筛选
const searchQuery = ref('')
const filterLevel = ref('')
const filterSource = ref('')
// 日志级别选项
const levelOptions = [
{ value: '', label: 'All Levels' },
{ value: 'info', label: 'Info' },
{ value: 'warning', label: 'Warning' },
{ value: 'error', label: 'Error' },
{ value: 'debug', label: 'Debug' },
]
// 来源选项
const sourceOptions = computed(() => {
const sources = [...new Set(logs.value.map(l => l.source))]
return [{ value: '', label: 'All Sources' }, ...sources.map(s => ({ value: s, label: s }))]
})
// 过滤后的日志
const filteredLogs = computed(() => {
return logs.value.filter(log => {
const matchSearch = searchQuery.value === '' ||
log.message.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
log.source.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchLevel = filterLevel.value === '' || log.level === filterLevel.value
const matchSource = filterSource.value === '' || log.source === filterSource.value
return matchSearch && matchLevel && matchSource
})
})
// 日志级别样式
const levelClass = (level: string) => {
switch (level) {
case 'info': return 'bg-blue-500/20 text-blue-400'
case 'warning': return 'bg-yellow-500/20 text-yellow-400'
case 'error': return 'bg-red-500/20 text-red-400'
case 'debug': return 'bg-gray-500/20 text-gray-400'
default: return 'bg-gray-500/20 text-gray-400'
}
}
// 日志详情
const selectedLog = ref<Log | null>(null)
const showLogDetail = ref(false)
const viewDetail = (log: Log) => {
selectedLog.value = log
showLogDetail.value = true
}
const closeDetail = () => {
showLogDetail.value = false
selectedLog.value = null
}
// 清空日志
const clearLogs = () => {
logs.value = []
}
</script>
<template>
<div class="p-6 min-h-screen">
<!-- 顶部标题 -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-file-lines text-orange-500"></i>
<span class="font-medium">Logs</span>
</div>
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
<i class="fa-solid fa-trash"></i>
Clear Logs
</button>
</div>
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search logs..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<el-select v-model="filterLevel" placeholder="All Levels" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option v-for="opt in levelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-select v-model="filterSource" placeholder="All Sources" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option v-for="opt in sourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</div>
<!-- 日志列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Level</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Source</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Message</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Timestamp</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="log in filteredLogs" :key="log.id" class="border-t border-dark-600 hover:bg-dark-600/50">
<td class="px-5 py-4">
<span :class="['px-2 py-1 rounded text-xs font-medium', levelClass(log.level)]">
{{ log.level.toUpperCase() }}
</span>
</td>
<td class="px-5 py-4 text-gray-300">{{ log.source }}</td>
<td class="px-5 py-4 text-gray-300 max-w-md">
<div class="truncate">{{ log.message }}</div>
</td>
<td class="px-5 py-4 text-gray-400">{{ log.user || '-' }}</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ log.timestamp }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end">
<button
@click="viewDetail(log)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="View Details"
>
<i class="fa-solid fa-eye text-gray-400 hover:text-white"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-if="filteredLogs.length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-file-lines text-4xl mb-3"></i>
<p>No logs found</p>
</div>
</div>
<!-- 日志详情弹窗 -->
<Teleport to="body">
<div v-if="showLogDetail" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="closeDetail">
<div class="bg-dark-700 rounded-2xl w-full max-w-2xl border border-dark-500 shadow-2xl" @click.stop>
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Log Details</h3>
<button @click="closeDetail" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div v-if="selectedLog" class="p-5 space-y-4">
<div class="flex items-center gap-4">
<span :class="['px-3 py-1 rounded text-sm font-medium', levelClass(selectedLog.level)]">
{{ selectedLog.level.toUpperCase() }}
</span>
<span class="text-gray-400">{{ selectedLog.timestamp }}</span>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Source</label>
<div class="text-white">{{ selectedLog.source }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">User</label>
<div class="text-white">{{ selectedLog.user || 'System' }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
<div class="bg-dark-800 rounded-lg p-4 text-gray-300 font-mono text-sm whitespace-pre-wrap">
{{ selectedLog.message }}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Log ID</label>
<div class="text-gray-500">#{{ selectedLog.id }}</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="closeDetail"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Close
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>

309
web/src/views/Memory.vue Normal file
View File

@@ -0,0 +1,309 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import './database/database.css'
interface MemoryItem {
id: number
type: 'Experience' | 'Lessons'
content: string
subject: string
attribute: string
score: number
createdAt: string
selected: boolean
}
// 记忆数据
const memories = ref<MemoryItem[]>([
{
id: 1,
type: 'Experience',
content: '当任务明确要求获取外部信息搜索最新数据、新闻、财报、行业报告等模型必须主动调用搜索工具而非仅依赖内部知识。应在系统提示中明确要求「必须使用搜索工具获取XX最新数据」首次迭代即调用搜索工具采用「搜索→分析→输出」的递进式流程避免模型陷入纯文字生成的空转循环。',
subject: '外部信息获取规范',
attribute: '工具调用原则',
score: 0.95,
createdAt: '3/10 15:35',
selected: false
},
{
id: 2,
type: 'Experience',
content: '当任务明确要求获取外部信息搜索最新数据、新闻、财报、行业报告等模型必须调用搜索工具而非仅依赖内部知识。应在系统提示中明确要求「必须使用搜索工具获取XX最新数据」并建立工具调用检测机制确保任务执行路径正确。',
subject: '工具调用',
attribute: '错误教训',
score: 0.95,
createdAt: '3/10 12:34',
selected: false
},
{
id: 3,
type: 'Experience',
content: '任务执行应采用「一次性完整输出」策略避免分批次小幅输出导致的迭代空转。对于代码生成、文件写入等任务应要求模型一次性完成方案设计与工具执行的完整流程将确认性回复并入上一次响应将冗余迭代压缩合并复杂任务控制在2-3次迭代内完成。',
subject: '任务执行效率优化',
attribute: '迭代策略原则',
score: 0.92,
createdAt: '3/10 15:35',
selected: false
},
{
id: 4,
type: 'Experience',
content: '任务执行过程中应建立工具调用检测机制监控模型是否按要求调用了必要的工具。若迭代中工具调用数为0说明执行路径不正确应触发异常处理或提示而非继续无效迭代。',
subject: '执行路径监控',
attribute: '质量控制机制',
score: 0.88,
createdAt: '3/10 15:35',
selected: false
},
{
id: 5,
type: 'Experience',
content: '任务执行应采用「搜索→分析→输出」的递进式流程避免无效迭代和空转。纯文字生成的重复迭代应压缩或合并确认性回复应并入上一次响应。简单任务应在1-2次迭代内完成避免模型陷入重复思考或无意义的自我确认。',
subject: '迭代效率',
attribute: '错误教训',
score: 0.85,
createdAt: '3/10 12:34',
selected: false
},
{
id: 6,
type: 'Experience',
content: '对于代码生成或文件写入类任务,应要求模型一次性完整输出或按模块批量输出,避免分批次的小幅输出导致迭代次数过多、执行效率低下。',
subject: '代码生成策略',
attribute: '错误教训',
score: 0.80,
createdAt: '3/10 12:34',
selected: false
},
])
// 搜索和筛选
const searchQuery = ref('')
const filterType = ref('All Types')
// 统计数据
const stats = computed(() => ({
total: memories.value.length,
avgScore: (memories.value.reduce((sum, m) => sum + m.score, 0) / memories.value.length).toFixed(2),
experience: memories.value.filter(m => m.type === 'Experience').length,
lessons: memories.value.filter(m => m.type === 'Lessons').length,
}))
// 过滤后的记忆
const filteredMemories = computed(() => {
return memories.value.filter(m => {
const matchSearch = searchQuery.value === '' ||
m.content.includes(searchQuery.value) ||
m.subject.includes(searchQuery.value) ||
m.attribute.includes(searchQuery.value)
const matchType = filterType.value === 'All Types' || m.type === filterType.value
return matchSearch && matchType
})
})
// 切换选中状态
const toggleSelect = (id: number) => {
const memory = memories.value.find(m => m.id === id)
if (memory) {
memory.selected = !memory.selected
}
}
// 全选
const selectAll = ref(false)
const toggleSelectAll = () => {
selectAll.value = !selectAll.value
memories.value.forEach(m => m.selected = selectAll.value)
}
// 删除记忆
const deleteMemory = (id: number) => {
memories.value = memories.value.filter(m => m.id !== id)
}
// 编辑记忆
const editMemory = (id: number) => {
console.log('Edit memory:', id)
}
// 刷新
const refresh = () => {
console.log('Refresh')
}
// LLM 智能审查
const llmReview = () => {
console.log('LLM Review')
}
// 获取分数颜色
const getScoreColor = (score: number) => {
if (score >= 0.9) return 'text-emerald-400'
if (score >= 0.7) return 'text-amber-400'
return 'text-red-400'
}
</script>
<template>
<div class="min-h-screen bg-[#121212] text-gray-200 p-6">
<!-- 顶部标题 -->
<div class="flex items-center gap-2 mb-6">
<i class="fa-solid fa-brain text-orange-500"></i>
<span class="font-medium">Memory</span>
</div>
<!-- 统计卡片区域 -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-white mb-1">{{ stats.total }}</div>
<div class="text-sm text-gray-400">Total Memories</div>
</div>
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-white mb-1">{{ stats.avgScore }}</div>
<div class="text-sm text-gray-400">Avg Score</div>
</div>
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-cyan-400 mb-1">{{ stats.experience }}</div>
<div class="text-sm text-gray-400">Experience</div>
</div>
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
<div class="text-3xl font-bold text-red-400 mb-1">{{ stats.lessons }}</div>
<div class="text-sm text-gray-400">Lessons</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="mb-4">
<div class="relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search memories..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
</div>
<!-- 筛选与操作栏 -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-3">
<el-select v-model="filterType" placeholder="Select" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option label="All Types" value="All Types" />
<el-option label="Experience" value="Experience" />
<el-option label="Lessons" value="Lessons" />
</el-select>
<button class="flex items-center space-x-2 bg-[#1e1e1e] border border-gray-700 rounded-lg py-2 px-4 text-gray-200 hover:bg-[#2a2a2a]">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span>Refresh</span>
</button>
<button class="flex items-center space-x-2 bg-orange-500 rounded-lg py-2 px-4 text-white hover:bg-orange-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
<span>LLM Review</span>
</button>
</div>
</div>
<!-- 表格区域 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="w-10 px-5 py-3 text-left text-sm font-medium text-gray-400">
<input
type="checkbox"
v-model="selectAll"
@change="toggleSelectAll"
class="rounded bg-dark-600 border-dark-500"
>
</th>
<th class="px-5 py-3 text-left text-sm font-medium text-gray-400">Type</th>
<th class="px-5 py-3 text-left text-sm font-medium text-gray-400">Content</th>
<th class="px-5 py-3 text-left text-sm font-medium text-gray-400">Score</th>
<th class="px-5 py-3 text-left text-sm font-medium text-gray-400">Created</th>
<th class="px-5 py-3 text-center text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr
v-for="memory in filteredMemories"
:key="memory.id"
class="table-row"
>
<td class="px-5 py-4">
<input
type="checkbox"
:checked="memory.selected"
@change="toggleSelect(memory.id)"
class="rounded bg-dark-600 border-dark-500"
>
</td>
<td class="px-5 py-4">
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">{{ memory.type }}</span>
</td>
<td class="px-5 py-4 text-sm text-gray-300 max-w-xl">
<div class="line-clamp-2">{{ memory.content }}</div>
<div class="text-xs text-gray-500 mt-1">Subject: {{ memory.subject }} · Attribute: {{ memory.attribute }}</div>
</td>
<td class="px-5 py-4">
<span :class="['font-medium', getScoreColor(memory.score)]">{{ memory.score }}</span>
</td>
<td class="px-5 py-4 text-sm text-gray-400">{{ memory.createdAt }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
<button
@click="editMemory(memory.id)"
class="btn-icon"
>
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button
@click="deleteMemory(memory.id)"
class="btn-icon"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-if="filteredMemories.length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-brain text-3xl mb-2"></i>
<p>No memories found</p>
</div>
</div>
</div>
</template>
<style scoped>
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #444;
}
/* 行高限制 */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -110,7 +110,7 @@ const providerIcon = (provider: string) => {
<div class="p-6 min-h-screen">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-cube text-gray-400"></i>
<i class="fa-solid fa-cube text-orange-500"></i>
<span class="font-medium">Model APIs</span>
</div>
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">

292
web/src/views/Plan.vue Normal file
View File

@@ -0,0 +1,292 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
// Mock scheduled tasks data
const tasks = ref([
{
id: 1,
name: 'Human-like Heartbeat',
status: 'running',
triggerType: 'Interval 30 minutes',
nextRun: '2026/03/10 16:26',
lastRun: '2026/03/10 15:56',
notifyChannel: '-',
executionCount: 13,
description: 'Check if proactive messages need to be sent (greetings/reminders/follow-ups)',
tags: ['System Task', 'Agent Task', 'Interval']
},
{
id: 2,
name: 'Memory Organization',
status: 'running',
triggerType: 'Interval 3 hours',
nextRun: '2026/03/10 18:35',
lastRun: '2026/03/10 15:35',
notifyChannel: '-',
executionCount: 2,
description: 'Execute memory organization: organize chat history, extract key memories, refresh MEMORY.md',
tags: ['System Task', 'Agent Task', 'Interval']
},
{
id: 3,
name: 'System Self-Check',
status: 'running',
triggerType: 'Daily 04:00',
nextRun: '2026/03/11 04:00',
lastRun: 'Never',
notifyChannel: '-',
executionCount: 0,
description: 'Execute system self-check: analyze ERROR logs, try to fix tool issues, generate report',
tags: ['System Task', 'Agent Task', 'Daily']
},
])
const activeTab = ref('running') // running, completed, all
const searchQuery = ref('')
const filteredTasks = computed(() => {
let result = tasks.value
if (activeTab.value === 'running') {
result = result.filter(t => t.status === 'running')
} else if (activeTab.value === 'completed') {
result = result.filter(t => t.status === 'stopped')
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(t =>
t.name.toLowerCase().includes(query) ||
t.description.toLowerCase().includes(query)
)
}
return result
})
const getTaskCount = (status: string) => {
if (status === 'running') return tasks.value.filter(t => t.status === 'running').length
if (status === 'completed') return tasks.value.filter(t => t.status === 'stopped').length
return tasks.value.length
}
const getStatusClass = (status: string) => {
switch (status) {
case 'running': return 'bg-green-500'
case 'stopped': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
</script>
<template>
<div class="p-6 min-h-screen plan-page">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-clock text-orange-500"></i>
<span class="font-medium text-white">Scheduled Tasks</span>
</div>
<button class="btn-primary">
<i class="fa-solid fa-plus"></i>
New Task
</button>
</div>
<!-- Search -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search tasks..."
class="search-input w-full"
>
</div>
</div>
<!-- Tab Navigation -->
<div class="flex gap-6 mb-4">
<button
:class="['tab-item', { active: activeTab === 'running' }]"
@click="activeTab = 'running'"
>
Running {{ getTaskCount('running') }}
</button>
<button
:class="['tab-item', { active: activeTab === 'completed' }]"
@click="activeTab = 'completed'"
>
Completed {{ getTaskCount('completed') }}
</button>
<button
:class="['tab-item', { active: activeTab === 'all' }]"
@click="activeTab = 'all'"
>
All {{ getTaskCount('all') }}
</button>
</div>
<!-- Task List Table -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<div v-if="filteredTasks.length === 0" class="py-12 text-center text-gray-400">
<i class="fa-solid fa-clock text-2xl mb-2"></i>
<p>No tasks found</p>
</div>
<table v-else class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Task Name</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Trigger</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Next Run</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Executions</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="task in filteredTasks" :key="task.id" class="table-row">
<td class="px-5 py-4">
<div class="flex items-center gap-2">
<span :class="['w-2 h-2 rounded-full', getStatusClass(task.status)]"></span>
<div>
<div class="font-medium text-white">{{ task.name }}</div>
<div class="text-sm text-gray-500">{{ task.description }}</div>
</div>
</div>
<div class="flex gap-2 mt-2">
<span v-for="tag in task.tags" :key="tag" class="task-tag">{{ tag }}</span>
</div>
</td>
<td class="px-5 py-4 text-center">
<span :class="['status-badge', getStatusClass(task.status)]">
{{ task.status === 'running' ? 'Running' : 'Stopped' }}
</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">
{{ task.triggerType }}
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">
{{ task.nextRun }}
</td>
<td class="px-5 py-4 text-center">
<span class="text-primary-cyan">{{ task.executionCount }}</span>
</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
<button class="btn-icon" title="Pause">
<i class="fa-solid fa-pause text-gray-400 hover:text-white"></i>
</button>
<button class="btn-icon" title="Edit">
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button class="btn-icon" title="Delete">
<i class="fa-solid fa-trash text-gray-400 hover:text-red-500"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.plan-page {
background-color: #0f1419;
}
.btn-primary {
background: linear-gradient(135deg, #f97316, #ef4444);
color: white;
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3);
}
.search-input {
background-color: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
padding: 10px 12px 10px 36px;
color: white;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #f97316;
}
.search-input::placeholder {
color: #6b7280;
}
.tab-item {
background: none;
border: none;
color: #9ca3af;
font-size: 14px;
padding: 8px 4px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab-item:hover {
color: white;
}
.tab-item.active {
color: #f97316;
border-bottom-color: #f97316;
}
.table-row {
border-top: 1px solid #2a2a3a;
transition: background-color 0.2s;
}
.table-row:hover {
background-color: rgba(255, 255, 255, 0.02);
}
.task-tag {
background-color: #374151;
color: #d1d5db;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
color: white;
}
.btn-icon {
background: none;
border: none;
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: background-color 0.2s;
}
.btn-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

View File

@@ -1,15 +1,327 @@
<script setup lang="ts">
// Script 页面 - 占位
import { ref, computed } from 'vue'
interface Script {
id: number
name: string
type: string
description: string
status: 'running' | 'stopped'
createdAt: string
}
// 模拟脚本数据
const scripts = ref<Script[]>([
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', status: 'running', createdAt: '2025-04-10' },
{ id: 2, name: 'Report Generator', type: 'Python', description: 'Generate weekly reports', status: 'stopped', createdAt: '2025-04-08' },
{ id: 3, name: 'Backup Script', type: 'Shell', description: 'Database backup automation', status: 'running', createdAt: '2025-04-05' },
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', status: 'stopped', createdAt: '2025-04-12' },
])
const searchQuery = ref('')
const filterStatus = ref('all')
const isCreating = ref(false)
const isEditing = ref(false)
const editingScript = ref<Script | null>(null)
const newScriptForm = ref({
name: '',
type: 'Python',
description: '',
})
// 过滤后的脚本
const filteredScripts = computed(() => {
return scripts.value.filter(script => {
const matchSearch = script.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
script.description.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || script.status === filterStatus.value
return matchSearch && matchStatus
})
})
// 状态样式
const statusClass = (status: string) => {
switch (status) {
case 'running': return 'bg-primary-success'
case 'stopped': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
// 切换状态
const toggleStatus = (script: Script) => {
script.status = script.status === 'running' ? 'stopped' : 'running'
}
// 删除脚本
const deleteScript = (id: number) => {
scripts.value = scripts.value.filter(s => s.id !== id)
}
// 编辑脚本
const openEdit = (script: Script) => {
editingScript.value = { ...script }
isEditing.value = true
}
const saveEdit = () => {
if (editingScript.value) {
const index = scripts.value.findIndex(s => s.id === editingScript.value!.id)
if (index !== -1) {
scripts.value[index] = { ...editingScript.value }
}
}
isEditing.value = false
}
const cancelEdit = () => {
isEditing.value = false
editingScript.value = null
}
// 新建脚本
const openCreate = () => {
newScriptForm.value = {
name: '',
type: 'Python',
description: '',
}
isCreating.value = true
}
const closeCreate = () => {
isCreating.value = false
}
const saveNewScript = () => {
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
scripts.value.push({
id: newId,
name: newScriptForm.value.name || 'Untitled Script',
type: newScriptForm.value.type,
description: newScriptForm.value.description,
status: 'stopped',
createdAt: new Date().toISOString().split('T')[0],
})
isCreating.value = false
}
</script>
<template>
<div class="p-6 min-h-screen">
<div class="flex items-center gap-2 mb-6">
<i class="fa-solid fa-code text-gray-400"></i>
<span class="font-medium">Script</span>
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-code text-orange-500"></i>
<span class="font-medium">Scripts</span>
</div>
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
<i class="fa-solid fa-plus"></i>
New Script
</button>
</div>
<div class="text-gray-400">
Script management coming soon...
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search scripts..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option label="All Status" value="all" />
<el-option label="Running" value="running" />
<el-option label="Stopped" value="stopped" />
</el-select>
</div>
<!-- 脚本列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Script Name</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Type</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="script in filteredScripts" :key="script.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
<td class="px-5 py-4">
<div class="font-medium">{{ script.name }}</div>
<div class="text-sm text-gray-500">{{ script.description }}</div>
</td>
<td class="px-5 py-4">
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ script.type }}</span>
</td>
<td class="px-5 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="statusClass(script.status)"></span>
<span class="capitalize text-sm">{{ script.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ script.createdAt }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button
@click="toggleStatus(script)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
:title="script.status === 'running' ? 'Stop' : 'Start'"
>
<i :class="['fa-solid', script.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
</button>
<button
@click="openEdit(script)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Edit"
>
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
</button>
<button
@click="deleteScript(script.id)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-if="filteredScripts.length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-code text-4xl mb-3"></i>
<p>No scripts found</p>
</div>
</div>
<!-- 新建脚本弹窗 -->
<Teleport to="body">
<div v-if="isCreating" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">New Script</h3>
<button @click="closeCreate" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Script Name</label>
<input
v-model="newScriptForm.name"
type="text"
placeholder="Enter script name..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
<el-select v-model="newScriptForm.type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option label="Python" value="Python" />
<el-option label="Shell" value="Shell" />
<el-option label="JavaScript" value="JavaScript" />
<el-option label="Go" value="Go" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="newScriptForm.description"
rows="3"
placeholder="Describe your script..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="closeCreate"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveNewScript"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
>
Create Script
</button>
</div>
</div>
</div>
</Teleport>
<!-- 编辑脚本弹窗 -->
<Teleport to="body">
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit Script</h3>
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Script Name</label>
<input
v-model="editingScript!.name"
type="text"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
<el-select v-model="editingScript!.type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option label="Python" value="Python" />
<el-option label="Shell" value="Shell" />
<el-option label="JavaScript" value="JavaScript" />
<el-option label="Go" value="Go" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="editingScript!.description"
rows="3"
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="cancelEdit"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveEdit"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
>
Save Changes
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import FormDialog from '@/components/FormDialog.vue'
@@ -47,6 +47,7 @@ const menuItems = [
{ key: 'members', label: 'Members', icon: 'fa-users' },
{ key: 'notifications', label: 'Notifications', icon: 'fa-bell' },
{ key: 'modelSettings', label: 'Model Settings', icon: 'fa-brain' },
{ key: 'logs', label: 'Logs', icon: 'fa-file-lines' },
]
// General 设置表单
@@ -82,13 +83,92 @@ const saveChanges = () => {
const showChangePassword = () => {
ElMessage.info('Password change dialog would open here')
}
// ========== Logs 功能 ==========
interface Log {
id: number
level: 'info' | 'warning' | 'error' | 'debug'
source: string
message: string
timestamp: string
user?: string
}
const logs = ref<Log[]>([
{ id: 1, level: 'info', source: 'System', message: 'User logged in successfully', timestamp: '2025-03-10 14:35:22', user: 'alex@example.com' },
{ id: 2, level: 'warning', source: 'API', message: 'Rate limit approaching for API key', timestamp: '2025-03-10 14:32:15', user: 'john@example.com' },
{ id: 3, level: 'error', source: 'Database', message: 'Connection timeout to primary database', timestamp: '2025-03-10 14:30:45' },
{ id: 4, level: 'info', source: 'Skill', message: 'MCP Server started successfully', timestamp: '2025-03-10 14:28:10' },
{ id: 5, level: 'debug', source: 'Auth', message: 'Token refresh initiated', timestamp: '2025-03-10 14:25:33', user: 'jane@example.com' },
{ id: 6, level: 'error', source: 'Script', message: 'Failed to execute backup script', timestamp: '2025-03-10 14:20:18' },
{ id: 7, level: 'info', source: 'Account', message: 'User role updated', timestamp: '2025-03-10 14:15:42', user: 'admin@example.com' },
{ id: 8, level: 'warning', source: 'Memory', message: 'Memory usage exceeds 80% threshold', timestamp: '2025-03-10 14:10:55' },
{ id: 9, level: 'info', source: 'Knowledge', message: 'Document indexed successfully', timestamp: '2025-03-10 14:05:30' },
{ id: 10, level: 'error', source: 'API', message: 'Invalid API key provided', timestamp: '2025-03-10 14:00:12' },
])
const logSearchQuery = ref('')
const logFilterLevel = ref('')
const logFilterSource = ref('')
const logLevelOptions = [
{ value: '', label: 'All Levels' },
{ value: 'info', label: 'Info' },
{ value: 'warning', label: 'Warning' },
{ value: 'error', label: 'Error' },
{ value: 'debug', label: 'Debug' },
]
const logSourceOptions = computed(() => {
const sources = [...new Set(logs.value.map(l => l.source))]
return [{ value: '', label: 'All Sources' }, ...sources.map(s => ({ value: s, label: s }))]
})
const filteredLogs = computed(() => {
return logs.value.filter(log => {
const matchSearch = logSearchQuery.value === '' ||
log.message.toLowerCase().includes(logSearchQuery.value.toLowerCase()) ||
log.source.toLowerCase().includes(logSearchQuery.value.toLowerCase())
const matchLevel = logFilterLevel.value === '' || log.level === logFilterLevel.value
const matchSource = logFilterSource.value === '' || log.source === logFilterSource.value
return matchSearch && matchLevel && matchSource
})
})
const logLevelClass = (level: string) => {
switch (level) {
case 'info': return 'bg-blue-500/20 text-blue-400'
case 'warning': return 'bg-yellow-500/20 text-yellow-400'
case 'error': return 'bg-red-500/20 text-red-400'
case 'debug': return 'bg-gray-500/20 text-gray-400'
default: return 'bg-gray-500/20 text-gray-400'
}
}
const selectedLog = ref<Log | null>(null)
const showLogDetail = ref(false)
const viewLogDetail = (log: Log) => {
selectedLog.value = log
showLogDetail.value = true
}
const closeLogDetail = () => {
showLogDetail.value = false
selectedLog.value = null
}
const clearLogs = () => {
logs.value = []
}
</script>
<template>
<div class="settings-page">
<!-- 页面标题 -->
<div class="page-header">
<h1 class="page-title">Settings</h1>
<div class="flex items-center gap-2 mb-6">
<i class="fa-solid fa-gear text-orange-500"></i>
<span class="font-medium">Settings</span>
</div>
<div class="settings-container">
@@ -432,6 +512,141 @@ const showChangePassword = () => {
</template>
</FormDialog>
</div>
<!-- Logs 设置 -->
<div v-if="activeMenu === 'logs'" class="settings-section">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="section-title">Logs</h2>
<p class="section-desc">View system logs</p>
</div>
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
<i class="fa-solid fa-trash"></i>
Clear Logs
</button>
</div>
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="logSearchQuery"
type="text"
placeholder="Search logs..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<el-select v-model="logFilterLevel" placeholder="All Levels" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option v-for="opt in logLevelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-select v-model="logFilterSource" placeholder="All Sources" class="w-40" size="large" popper-class="dark-select-dropdown">
<el-option v-for="opt in logSourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</div>
<!-- 日志列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Level</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Source</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Message</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Timestamp</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="log in filteredLogs" :key="log.id" class="border-t border-dark-600 hover:bg-dark-600/50">
<td class="px-5 py-4">
<span :class="['px-2 py-1 rounded text-xs font-medium', logLevelClass(log.level)]">
{{ log.level.toUpperCase() }}
</span>
</td>
<td class="px-5 py-4 text-gray-300">{{ log.source }}</td>
<td class="px-5 py-4 text-gray-300 max-w-md">
<div class="truncate">{{ log.message }}</div>
</td>
<td class="px-5 py-4 text-gray-400">{{ log.user || '-' }}</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ log.timestamp }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end">
<button
@click="viewLogDetail(log)"
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
title="View Details"
>
<i class="fa-solid fa-eye text-gray-400 hover:text-white"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-if="filteredLogs.length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-file-lines text-4xl mb-3"></i>
<p>No logs found</p>
</div>
</div>
<!-- 日志详情弹窗 -->
<Teleport to="body">
<div v-if="showLogDetail" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="closeLogDetail">
<div class="bg-dark-700 rounded-2xl w-full max-w-2xl border border-dark-500 shadow-2xl" @click.stop>
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Log Details</h3>
<button @click="closeLogDetail" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div v-if="selectedLog" class="p-5 space-y-4">
<div class="flex items-center gap-4">
<span :class="['px-3 py-1 rounded text-sm font-medium', logLevelClass(selectedLog.level)]">
{{ selectedLog.level.toUpperCase() }}
</span>
<span class="text-gray-400">{{ selectedLog.timestamp }}</span>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Source</label>
<div class="text-white">{{ selectedLog.source }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">User</label>
<div class="text-white">{{ selectedLog.user || 'System' }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
<div class="bg-dark-800 rounded-lg p-4 text-gray-300 font-mono text-sm whitespace-pre-wrap">
{{ selectedLog.message }}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Log ID</label>
<div class="text-gray-500">#{{ selectedLog.id }}</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="closeLogDetail"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Close
</button>
</div>
</div>
</div>
</Teleport>
</div>
</div>
</div>
</div>

View File

@@ -308,7 +308,7 @@ const saveNewSkill = () => {
<div class="p-6 min-h-screen">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-wand-magic-sparkles text-gray-400"></i>
<i class="fa-solid fa-wand-magic-sparkles text-orange-500"></i>
<span class="font-medium">Skills</span>
</div>
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">