feat(temple): add Temple modal with Tools browser and Skills management

This commit is contained in:
2026-04-08 16:46:02 +08:00
parent 51e38e039b
commit aa12c92a5a
12 changed files with 2571 additions and 41 deletions

View File

@@ -0,0 +1,348 @@
"""Tools API Router
聚合两套工具体系的元数据:
1. 注册层 (app/tools/) - YAML manifest 定义
2. Agent 层 (app/agents/tools/) - @tool 装饰器定义
"""
import re
import importlib
from fastapi import APIRouter, Depends
from app.routers.auth import get_current_user
from app.models.user import User
from app.schemas.tools import (
ToolsResponse,
ToolCategory,
ToolSubgroup,
ToolInfo,
ToolCommand,
ToolStats,
ToolSummary,
)
router = APIRouter(prefix="/api/tools", tags=["Tools"])
# ============================================================
# 辅助函数
# ============================================================
def _parse_command_from_docstring(docstring: str) -> dict:
"""从函数的 docstring 解析参数信息"""
params = {"type": "object", "properties": {}, "required": []}
if not docstring:
return params
# 简单解析 Args: 段落
args_match = re.search(
r"Args:\s*(.*?)(?=\n\s*(?:Returns?|Raises?)|$", docstring, re.DOTALL | re.IGNORECASE
)
if args_match:
args_section = args_match.group(1)
# 匹配形如 "arg_name (type): description" 的行
for line in args_section.strip().split("\n"):
line = line.strip()
if not line:
continue
# 匹配: "name (type): description" 或 "name: description"
m = re.match(r"(\w+)\s*(?:\(\s*(\w+)\s*\))?\s*:", line)
if m:
param_name = m.group(1)
params["properties"][param_name] = {"type": "string", "description": line}
params["required"].append(param_name)
return params
def _build_agent_tools() -> list[ToolInfo]:
"""扫描 app/agents/tools/ 目录,内省 @tool 装饰器"""
tools: list[ToolInfo] = []
# 分类映射:文件名 -> (分类名, 子分类名)
category_map = {
"search": ("Agent层", "知识检索"),
"schedule": ("Agent层", "日程管理"),
"task": ("Agent层", "任务管理"),
"forum": ("Agent层", "论坛功能"),
"time_reasoning": ("Agent层", "时间推理"),
"builtins/file_tools": ("Agent层", "文件工具"),
"builtins/system_tools": ("Agent层", "系统命令"),
"builtins/dev_tools": ("Agent层", "开发工具"),
"builtins/collaboration_tools": ("Agent层", "协作工具"),
}
# 工具名称 -> 中文显示名
display_names = {
"search_knowledge": "知识库搜索",
"get_knowledge_graph_context": "知识图谱查询",
"build_knowledge_graph": "构建知识图谱",
"hybrid_search": "混合搜索",
"web_search": "联网搜索",
"get_schedule_day": "获取日程",
"create_todo": "创建待办",
"create_schedule_task": "创建日程任务",
"create_reminder": "创建提醒",
"create_goal": "创建目标",
"get_tasks": "获取任务列表",
"create_task": "创建任务",
"update_task_status": "更新任务状态",
"get_forum_posts": "获取论坛帖子",
"create_forum_post": "发布论坛帖子",
"scan_forum_for_instructions": "扫描论坛指令",
"resolve_time_expression": "解析时间表达式",
"glob": "文件路径匹配",
"grep": "文件内容搜索",
"read_file": "读取文件",
"write_file": "写入文件",
"bash": "Bash命令",
"powershell": "PowerShell命令",
"git": "Git操作",
"lsp_tools": "LSP代码导航",
"team_agent": "团队Agent通信",
"task_broadcast": "任务广播",
}
# 工具描述
descriptions = {
"search_knowledge": "搜索用户的私人知识库,返回最相关的文档片段",
"get_knowledge_graph_context": "获取用户知识图谱的上下文信息",
"build_knowledge_graph": "从文档构建/更新知识图谱",
"hybrid_search": "混合搜索,结合向量语义检索和关键词匹配",
"web_search": "通过 SearxNG 搜索外部网页信息",
"get_schedule_day": "获取指定日期的 todo/task/reminder/goal 聚合信息",
"create_todo": "创建指定日期的待办",
"create_schedule_task": "创建任务,支持优先级和截止日期",
"create_reminder": "创建提醒,支持自然语言时间",
"create_goal": "创建指定日期的目标",
"get_tasks": "获取用户当前的任务列表",
"create_task": "创建新任务",
"update_task_status": "更新任务状态",
"get_forum_posts": "获取论坛帖子列表",
"create_forum_post": "在论坛发布新帖子",
"scan_forum_for_instructions": "扫描论坛中的指令类帖子",
"resolve_time_expression": "解析中文自然语言时间表达",
"glob": "使用 glob 模式查找文件路径",
"grep": "在文件中搜索匹配的文本行",
"read_file": "读取文件内容",
"write_file": "写入文件内容",
"bash": "执行 Bash 命令",
"powershell": "执行 PowerShell 命令",
"git": "执行 Git 命令",
"lsp_tools": "LSP 代码导航和查找引用",
"team_agent": "向团队 Agent 发送消息或请求协作",
"task_broadcast": "向多个 Agent 广播任务",
}
# 需要扫描的模块
modules_to_scan = [
("app.agents.tools.search", "search"),
("app.agents.tools.schedule", "schedule"),
("app.agents.tools.task", "task"),
("app.agents.tools.forum", "forum"),
("app.agents.tools.time_reasoning", "time_reasoning"),
("app.agents.tools.builtins.file_tools", "builtins/file_tools"),
("app.agents.tools.builtins.system_tools", "builtins/system_tools"),
("app.agents.tools.builtins.dev_tools", "builtins/dev_tools"),
("app.agents.tools.builtins.collaboration_tools", "builtins/collaboration_tools"),
]
for module_name, category_key in modules_to_scan:
try:
mod = importlib.import_module(module_name)
except ImportError:
continue
# 扫描模块中所有 @tool 装饰的函数
for attr_name in dir(mod):
if attr_name.startswith("_"):
continue
attr = getattr(mod, attr_name)
# 检查是否是 langchain @tool 装饰的对象
if hasattr(attr, "name") and hasattr(attr, "description"):
tool_name = attr.name
tool_desc = attr.description or ""
# 清理 docstring 中的参数说明用于显示
display_desc = re.sub(r"\s*Args:\s*.*", "", tool_desc, flags=re.DOTALL).strip()
display_desc = re.sub(
r"\s*Returns?:\s*.*", "", display_desc, flags=re.DOTALL
).strip()
# 获取 category 和 subcategory
cat_info = category_map.get(category_key, ("Agent层", category_key))
category, subcategory = cat_info[0], cat_info[1]
# 获取参数 schema
params_schema = getattr(attr, "args_schema", None)
parameters = {}
if params_schema:
try:
if hasattr(params_schema, "model_json_schema"):
parameters = params_schema.model_json_schema()
elif hasattr(params_schema, "schema"):
parameters = params_schema.schema()
except Exception:
pass
tool_info = ToolInfo(
name=tool_name,
display_name=display_names.get(tool_name, tool_name),
description=descriptions.get(tool_name, display_desc or tool_desc),
category=category,
subcategory=subcategory,
source="agent",
source_file=module_name,
tags=[],
enabled=True,
commands=[
ToolCommand(
name=tool_name,
description=tool_desc or display_desc,
parameters=parameters,
)
],
stats=ToolStats(),
)
tools.append(tool_info)
return tools
def _build_manifest_tools() -> list[ToolInfo]:
"""从 YAML manifest 构建工具信息"""
tools: list[ToolInfo] = []
# manifest 文件 -> 分类映射
manifest_map = {
"file_operator": (
"注册层",
"文件操作",
[
ToolCommand(name="read_file", description="读取指定路径的文件内容"),
ToolCommand(name="write_file", description="将内容写入文件"),
ToolCommand(name="list_directory", description="列出目录内容"),
ToolCommand(name="search_files", description="递归搜索匹配模式的文件"),
],
),
"task_manager": (
"注册层",
"任务管理",
[
ToolCommand(name="create_task", description="创建新任务"),
ToolCommand(name="list_tasks", description="列出任务"),
ToolCommand(name="get_task", description="获取任务详情"),
ToolCommand(name="complete_task", description="标记任务完成"),
ToolCommand(name="fail_task", description="标记任务失败"),
],
),
"web_fetch": (
"注册层",
"网页抓取",
[
ToolCommand(name="fetch", description="抓取网页内容"),
ToolCommand(name="screenshot", description="截取网页截图"),
],
),
"web_search": (
"注册层",
"联网搜索",
[
ToolCommand(name="search", description="执行语义级搜索"),
ToolCommand(name="deep_search", description="深度搜索,带摘要生成"),
],
),
}
manifest_descriptions = {
"file_operator": "强大的文件系统操作工具,支持读写、搜索、下载等功能",
"task_manager": "任务创建、查询、更新和状态管理",
"web_fetch": "网页内容抓取工具,支持 HTML 解析、截图等功能",
"web_search": "语义级并发搜索引擎,支持多源搜索和结果聚合",
}
for tool_name, (category, subcategory, commands) in manifest_map.items():
tool_info = ToolInfo(
name=tool_name,
display_name=subcategory,
description=manifest_descriptions.get(tool_name, ""),
category=category,
subcategory=subcategory,
source="manifest",
source_file=f"app/tools/manifests/{tool_name}.yaml",
tags=[],
enabled=True,
commands=commands,
stats=ToolStats(),
)
tools.append(tool_info)
return tools
# ============================================================
# 路由
# ============================================================
@router.get("", response_model=ToolsResponse)
async def list_tools(
current_user: User = Depends(get_current_user),
):
"""获取所有内置工具列表(只读)"""
# 构建工具列表
manifest_tools = _build_manifest_tools()
agent_tools = _build_agent_tools()
all_tools = manifest_tools + agent_tools
# 按 category 和 subcategory 分组
category_map: dict[str, dict[str, list[ToolInfo]]] = {
"注册层": {},
"Agent层": {},
}
for tool in all_tools:
cat = tool.category
subcat = tool.subcategory
if cat not in category_map:
category_map[cat] = {}
if subcat not in category_map[cat]:
category_map[cat][subcat] = []
category_map[cat][subcat].append(tool)
# 构建响应
categories = []
for cat_name, subgroups_dict in category_map.items():
if not subgroups_dict:
continue
subgroups = []
for subcat_name, tools_list in subgroups_dict.items():
subgroups.append(
ToolSubgroup(
name=subcat_name,
display_name=subcat_name,
tools=tools_list,
)
)
categories.append(
ToolCategory(
name=cat_name,
display_name=cat_name,
subgroups=subgroups,
)
)
# 计算摘要
total_commands = sum(len(t.commands) for t in all_tools)
active_commands = sum(len(t.commands) for t in all_tools if t.enabled)
summary = ToolSummary(
total_commands=total_commands,
active_commands=active_commands,
total_tools=len(all_tools),
manifest_tools=len(manifest_tools),
agent_tools=len(agent_tools),
)
return ToolsResponse(categories=categories, summary=summary)

View File

@@ -0,0 +1,76 @@
"""Tools API Schemas"""
from pydantic import BaseModel
from typing import Optional
class ToolCommand(BaseModel):
"""单个工具命令"""
name: str
description: str
parameters: dict = {}
class ToolStats(BaseModel):
"""工具调用统计"""
call_count: int = 0
error_count: int = 0
total_duration_ms: int = 0
avg_duration_ms: int = 0
error_rate: float = 0.0
class ToolInfo(BaseModel):
"""工具完整信息"""
name: str
display_name: str
description: str
category: str # 中文分类名
subcategory: str = "" # 子分类
source: str # "manifest" | "agent"
source_file: str = "" # 来源文件路径
tags: list[str] = []
enabled: bool = True
commands: list[ToolCommand] = []
stats: Optional[ToolStats] = None
config: dict = {} # 配置参数(只读)
class ToolCategory(BaseModel):
"""工具分类"""
name: str # 大分类:注册层 / Agent层
display_name: str # 中文显示名
subgroups: list["ToolSubgroup"] = []
class ToolSubgroup(BaseModel):
"""工具子分类"""
name: str # 子分类名
display_name: str # 中文显示名
tools: list[ToolInfo] = []
class ToolSummary(BaseModel):
"""工具统计摘要"""
total_commands: int = 0
active_commands: int = 0
total_tools: int = 0
manifest_tools: int = 0
agent_tools: int = 0
class ToolsResponse(BaseModel):
"""GET /api/tools 响应"""
categories: list[ToolCategory]
summary: ToolSummary
# 更新前向引用
ToolCategory.model_rebuild()

View File

@@ -0,0 +1,165 @@
# 智慧神殿Temple升级计划索引
本目录用于存放智慧神殿Temple页面的升级规划文档。
## 文档说明
| 文件 | 说明 |
|------|------|
| `README.md` | 总览、阶段关系、实施顺序、当前状态 |
| `phase-0-current-state.md` | 当前现状、问题、目标架构 |
| `phase-1-tools-api.md` | 后端 Tools API 开发 |
| `phase-2-tools-frontend.md` | Tools Tab 前端实现 |
| `phase-3-skills-integration.md` | Skills Tab 复用集成 |
| `checklist.md` | 执行清单 |
## 推荐阅读顺序
1. 先读 `README.md`(本文)
2. 再读 `phase-0-current-state.md`
3. 再按顺序阅读 phase 1 ~ 3
4. 参考 `checklist.md` 进行任务追踪
---
## 当前总体状态2026-04-08
| Phase | 当前状态 | 说明 |
|------|------|------|
| Phase 0 | 已完成 | 现状梳理完毕,本文档 |
| Phase 1 | 待开始 | 后端 Tools API 开发 |
| Phase 2 | 待开始 | 前端 Tools Tab 实现 |
| Phase 3 | 待开始 | Skills Tab 复用集成 |
---
## 总体升级原则
1. **Tools 只读不做编辑** - 系统内置工具不允许手动修改,防止配置破坏
2. **Skills 以 DB 为 source of truth** - UI 操作 DB后端自动生成 `.md` 文件,用户不直接碰代码
3. **复用现有 Skills 页面** - 已有完整 CRUD改动成本最低
4. **MCP 暂不纳入** - 当前仅为概念性能力包,后期独立需求
5. **样式沿用现有体系** - 复用 `chatPage.css` 的深色终端风格 + `jarvis-*` CSS 变量
---
## 阶段关系图
```
Phase 0 ──────────────────────────────────────────────────────────────┐
│ 现状与目标 │
│ - Temple 页面现状分析 │
│ - Tools 系统梳理 │
│ - Skills 系统梳理 │
│ - 设计决策 │
│ 状态:已完成 │
└────────────────────────────────────────────────────────────────────┘
Phase 1 ──────────────────────────────────────────────────────────────┐
│ 后端 Tools API │
│ - GET /api/tools 接口开发 │
│ - ToolRegistry 聚合所有工具 │
│ - 聚合两套工具体系元数据 │
│ │
│ 核心文件: app/routers/tools.py │
│ 依赖: 无 │
│ 工作量: 1 天 │
└────────────────────────────────────────────────────────────────────┘
Phase 2 ──────────────────────────────────────────────────────────────┐
│ 前端 Tools Tab │
│ - useTemple.ts composable │
│ - Tools 分类树实现 │
│ - 工具详情面板 │
│ - Metrics Strip 统计行 │
│ │
│ 核心文件: frontend/src/pages/temple/ │
│ 依赖: Phase 1 │
│ 工作量: 2 天 │
└────────────────────────────────────────────────────────────────────┘
Phase 3 ──────────────────────────────────────────────────────────────┐
│ Skills Tab 复用集成 │
│ - 确认现有 Skills 页面功能完整 │
│ - 与 Temple 页面 Tab 切换联动 │
│ - 样式一致性检查 │
│ │
│ 核心文件: frontend/src/pages/temple/, frontend/src/pages/skills/ │
│ 依赖: Phase 2 │
│ 工作量: 0.5 天 │
└────────────────────────────────────────────────────────────────────┘
```
---
## 两套 Tools 体系梳理
### 注册层工具(`app/tools/`
| 工具 | Manifest | 命令数 |
|------|---------|--------|
| `file_operator` | `manifests/file_operator.yaml` | 4 |
| `task_manager` | `manifests/task_manager.yaml` | 5 |
| `web_fetch` | `manifests/web_fetch.yaml` | 2 |
| `web_search` | `manifests/web_search.yaml` | 2 |
### Agent 内置层工具(`app/agents/tools/`
| 类别 | 工具数 | 来源文件 |
|------|--------|---------|
| 文件操作 | 4 | `builtins/file_tools.py` |
| 系统命令 | 2 | `builtins/system_tools.py` |
| 开发工具 | 2 | `builtins/dev_tools.py` |
| 协作工具 | 2 | `builtins/collaboration_tools.py` |
| 知识检索 | 5 | `search.py` |
| 日程管理 | 5 | `schedule.py` |
| 任务管理 | 3 | `task.py` |
| 论坛功能 | 3 | `forum.py` |
| 时间推理 | 1 | `time_reasoning.py` |
**合计约 34 个工具命令**
---
## 设计决策记录
| 决策 | 原因 |
|------|------|
| Tools 只读不做编辑 | 系统内置工具不允许用户手动修改,防止配置破坏 |
| 不引入 MCP 管理 | 当前 MCP 仅为概念性能力包,无实际 server 连接需求,后期独立需求 |
| Skills 以 DB 为 source of truth | UI 操作 DB后端同步生成 .md 文件,用户不直接碰代码 |
| 复用现有 Skills 页面 | 已有完整 CRUD改动成本最低 |
| 按工具来源分类 | 与代码结构对应,用户可追溯工具定义位置 |
---
## 文件变更追踪
| Phase | 新增文件 | 修改文件 |
|-------|---------|---------|
| Phase 1 | `app/routers/tools.py`, `app/schemas/tools.py` | `app/main.py`(注册路由) |
| Phase 2 | `frontend/src/pages/temple/index.vue`, `templePage.css`, `composables/useTemple.ts`, `frontend/src/api/tools.ts` | `frontend/src/pages/temple/index.vue`(重写占位页) |
| Phase 3 | 无 | `frontend/src/pages/temple/index.vue`Tab 切换逻辑) |
---
## 与其他 Phase 的关系
| 相关模块 | 协作内容 |
|---------|---------|
| Skills Registry (agent-update Phase 9) | Skills 的 DB 层由 `/api/skills` 提供,文件层由 SkillRegistry 管理 |
| Tool System (tool-update T.1-T.4) | Temple 展示的 Tools 元数据来自 tool-update 建立的 manifest 系统 |
---
## 总工作量
| Phase | 工作量 |
|-------|--------|
| Phase 1 | 1 天 |
| Phase 2 | 2 天 |
| Phase 3 | 0.5 天 |
| **总计** | **3.5 天** |

View File

@@ -0,0 +1,60 @@
# 智慧神殿Temple执行清单
> 更新日期2026-04-08
> 总工作量3.5 天
---
## Phase 1后端 Tools API
| 序号 | 任务 | 状态 | 备注 |
|------|------|------|------|
| 1.1 | 创建 `app/schemas/tools.py`,定义 Pydantic Schema | 待开始 | |
| 1.2 | 创建 `app/routers/tools.py`,实现 `GET /api/tools` | 待开始 | |
| 1.3 | 实现 ToolRegistry 工具元数据聚合 | 待开始 | 复用 `list_all()` |
| 1.4 | 实现 Agent 层工具扫描(内省 `@tool` 装饰器) | 待开始 | 扫描 `app/agents/tools/` |
| 1.5 | 实现分类分组逻辑(注册层 / Agent 层) | 待开始 | |
| 1.6 | 在 `app/main.py` 注册路由 | 待开始 | |
| 1.7 | 本地测试 `GET /api/tools` 返回正确数据 | 待开始 | |
---
## Phase 2前端 Tools Tab
| 序号 | 任务 | 状态 | 备注 |
|------|------|------|------|
| 2.1 | 创建 `frontend/src/api/tools.ts` API 客户端 | 待开始 | |
| 2.2 | 创建 `frontend/src/pages/temple/composables/useTemple.ts` | 待开始 | |
| 2.3 | 实现 Tab 切换器组件 | 待开始 | Tools / Skills 切换 |
| 2.4 | 实现 Metrics Strip 统计行 | 待开始 | |
| 2.5 | 实现分类树组件(两极结构) | 待开始 | |
| 2.6 | 实现工具列表(无选中时) | 待开始 | 卡片形式 |
| 2.7 | 实现工具详情面板 | 待开始 | 含 Commands 列表 |
| 2.8 | 创建 `templePage.css` 样式 | 待开始 | 复用 jarvis-* 变量 |
| 2.9 | 重写 `frontend/src/pages/temple/index.vue` | 待开始 | 替换占位符 |
| 2.10 | 联调后端 API数据正确渲染 | 待开始 | |
---
## Phase 3Skills Tab 复用集成
| 序号 | 任务 | 状态 | 备注 |
|------|------|------|------|
| 3.1 | 将 Skills 页面集成到 Temple Skills Tab | 待开始 | 推荐方案 A条件渲染 |
| 3.2 | Tab 切换逻辑实现 | 待开始 | |
| 3.3 | Skills CRUD 功能验证 | 待开始 | 创建/编辑/删除/启用/禁用 |
| 3.4 | Skills Modal 和 Drawer 交互验证 | 待开始 | |
| 3.5 | Skills Tab 下 Metrics Strip 切换指标 | 待开始 | 显示 Skills 指标 |
| 3.6 | Tab 切换状态保持验证 | 待开始 | 不丢失选中状态 |
---
## 验收标准
- [ ] `GET /api/tools` 返回 200响应结构正确
- [ ] Temple 页面加载无报错
- [ ] Tools Tab 显示所有工具分类
- [ ] 点击工具有详情Commands 列表完整)
- [ ] Skills Tab 下 Skills CRUD 全部正常
- [ ] 样式与 Jarvis 整体风格一致
- [ ] 无前端 console.error

View File

@@ -0,0 +1,171 @@
# Phase 0智慧神殿现状与目标
日期2026-04-08
状态:已完成
---
## 1. 本阶段目的
本文件用于统一背景认知,明确:
- Temple 页面当前处于什么状态
- 主要短板是什么
- 为什么要升级
- 升级后的目标形态是什么
---
## 2. 当前 Temple 页面状态
### 2.1 现有实现
`frontend/src/pages/temple/index.vue` 是一个**空白占位页**
```vue
<script setup lang="ts">
// 智慧神殿 - Temple of Wisdom
</script>
<template>
<div class="temple-page">
<div class="page-header">
<h1> 智慧神殿</h1>
<p class="subtitle">深邃智慧永恒传承</p>
</div>
<div class="page-content">
<div class="placeholder-content">
<div class="temple-icon">🏛</div>
<p>智慧神殿 - 敬请期待</p>
</div>
</div>
</div>
</template>
```
### 2.2 触发入口
聊天输入框上方三个按钮之一(`◈`),跳转到 `/temple`
```html
<!-- frontend/src/pages/chat/index.vue -->
<div class="top-buttons-row">
<button class="top-action-btn" @click="$router.push('/temple')" title="Temple">
<span class="btn-icon temple-icon"></span>
</button>
<button class="top-action-btn" @click="$router.push('/knowledge')" title="Knowledge">
<span class="btn-icon knowledge-icon"></span>
</button>
<button class="top-action-btn" @click="$router.push('/war-room')" title="War Room">
<span class="btn-icon war-icon"></span>
</button>
</div>
```
---
## 3. 当前系统现状
### 3.1 Tools 系统(两套并存)
#### A. 工具注册层(`app/tools/`
已建立 manifest 驱动的工具注册体系:
```
app/tools/
├── manifests/ # YAML manifest 定义
│ ├── file_operator.yaml # 4 commands: read_file, write_file, list_directory, search_files
│ ├── task_manager.yaml # 5 commands: create_task, list_tasks, get_task, complete_task, fail_task
│ ├── web_fetch.yaml # 2 commands: fetch, screenshot
│ └── web_search.yaml # 2 commands: search, deep_search
├── registry.py # ToolRegistry 动态注册中心
├── implementations/ # 工具 Python 实现
├── permissions.py # 权限控制
├── hooks/ # Hook 系统(审计日志、安全扫描、危险确认)
└── schemas/ # Pydantic Schema
```
#### B. Agent 工具层(`app/agents/tools/`
LangChain `@tool` 装饰器定义的 Agent 可用工具:
| 类别 | 工具 | 源文件 |
|------|------|--------|
| 文件操作 | `glob`, `grep`, `read_file`, `write_file` | `builtins/file_tools.py` |
| 系统命令 | `bash`, `powershell` | `builtins/system_tools.py` |
| 开发工具 | `git`, `lsp_tools` | `builtins/dev_tools.py` |
| 协作工具 | `team_agent`, `task_broadcast` | `builtins/collaboration_tools.py` |
| 知识检索 | `search_knowledge`, `get_knowledge_graph_context`, `build_knowledge_graph`, `hybrid_search`, `web_search` | `search.py` |
| 日程管理 | `get_schedule_day`, `create_todo`, `create_schedule_task`, `create_reminder`, `create_goal` | `schedule.py` |
| 任务管理 | `get_tasks`, `create_task`, `update_task_status` | `task.py` |
| 论坛功能 | `get_forum_posts`, `create_forum_post`, `scan_forum_for_instructions` | `forum.py` |
| 时间推理 | `resolve_time_expression` | `time_reasoning.py` |
### 3.2 Skills 系统
#### A. DB 层
已有完整 CRUD
- 路由:`/api/skills`
- 字段:`name`, `description`, `instructions`, `agent_type`, `tools`, `visibility`, `is_builtin`, `is_active`
- Agent types`general`, `schedule_planner`, `executor`, `librarian`, `analyst`
- Visibility`private`, `team`, `market`
#### B. 文件层
`SkillRegistry` 加载 `.md` 文件供 Agent 运行时使用。
加载器:
- `MCPSkillLoader` - MCP 能力包加载
- `LocalSkillLoader` - 本地 `.md` 文件加载
- `PluginLoader` - 插件式加载
### 3.3 当前问题
| 问题 | 影响 |
|------|------|
| Temple 页面是空白占位页 | 三个按钮入口之一完全无功能 |
| Tools 无统一展示入口 | 用户无法看到系统有哪些可用工具 |
| Tools 散落在两套体系 | manifest 层 + agent 层,用户无感知 |
| Skills 页面独立在 `/skills` | 工具和技能没有统一管理入口 |
---
## 4. 目标架构
```
┌─────────────────────────────────────────────────────────────┐
│ /temple │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [◈ 智慧神殿] [Tools] [Skills] │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ TOTAL: 30 ACTIVE: 28 AGENTS: 5 (Metrics) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────┐ ┌─────────────────────────────────┐ │
│ │ [分类树] │ │ [工具详情] │ │
│ │ │ │ │ │
│ │ ▼ 注册层 │ │ file_operator │ │
│ │ 文件操作 │ │ 描述: 强大的文件系统操作工具 │ │
│ │ 任务管理 │ │ 命令: 4 个 │ │
│ │ ▼ Agent层 │ │ 调用: 1,234 次 错误率: 0.2% │ │
│ │ 知识检索 │ │ │ │
│ │ 日程管理 │ │ [Commands] │ │
│ │ 任务管理 │ │ • read_file │ │
│ │ 论坛功能 │ │ • write_file │ │
│ │ 时间推理 │ │ • list_directory │ │
│ └────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 5. 本阶段产出要求
- [x] 团队对 Temple 当前状态和目标方向达成一致
- [x] Tools 系统两套并存的现状已梳理清楚
- [x] Skills 系统现有架构已梳理清楚
- [x] 后续 phase 文档能够在这个认知基础上展开

View File

@@ -0,0 +1,135 @@
# Phase 1后端 Tools API 开发
日期2026-04-08
状态:待开始
---
## 1. 本阶段目的
开发 `GET /api/tools` 接口,聚合两套工具体系的元数据,为前端 Tools Tab 提供数据源。
---
## 2. 核心文件
| 文件 | 作用 |
|------|------|
| `app/routers/tools.py` | 新建Tools 路由 |
| `app/schemas/tools.py` | 新建Tools API Pydantic Schema |
---
## 3. API 设计
### 3.1 接口
```
GET /api/tools
```
### 3.2 响应结构
```python
class ToolCommand(BaseModel):
name: str
description: str
parameters: dict # JSON Schema
class ToolStats(BaseModel):
call_count: int
error_count: int
total_duration_ms: int
avg_duration_ms: int
error_rate: float
class ToolCategory(BaseModel):
name: str # 显示用中文分类名
source: str # "manifest" | "agent"
tools: list[ToolInfo]
class ToolInfo(BaseModel):
name: str
display_name: str
description: str
category: str
tags: list[str]
enabled: bool
source: str # "manifest" | "agent"
commands: list[ToolCommand]
stats: ToolStats | None
class ToolsResponse(BaseModel):
categories: list[ToolCategory]
summary: dict:
total: int
active: int
by_source: dict
```
### 3.3 分类结构
按工具来源分为两大类:
**注册层source: "manifest"**
| Category Name | 来源 |
|--------------|------|
| `文件操作` | `manifests/file_operator.yaml` |
| `任务管理` | `manifests/task_manager.yaml` |
| `网页抓取` | `manifests/web_fetch.yaml` |
| `联网搜索` | `manifests/web_search.yaml` |
**Agent 层source: "agent"**
| Category Name | 来源 |
|--------------|------|
| `文件工具` | `builtins/file_tools.py` |
| `系统命令` | `builtins/system_tools.py` |
| `开发工具` | `builtins/dev_tools.py` |
| `协作工具` | `builtins/collaboration_tools.py` |
| `知识检索` | `search.py` |
| `日程管理` | `schedule.py` |
| `任务管理` | `task.py` |
| `论坛功能` | `forum.py` |
| `时间推理` | `time_reasoning.py` |
---
## 4. 实现逻辑
### 4.1 数据聚合流程
```
1. 从 ToolRegistry.list_all() 获取注册层工具元数据
2. 扫描 app/agents/tools/ 下所有 @tool 装饰器,获取 Agent 层工具
3. 合并两套数据,按 category 分组
4. 调用 ToolRegistry.get_stats() 获取统计数据
5. 返回聚合后的 categories + summary
```
### 4.2 Agent 层工具扫描
通过内省 `app/agents/tools/` 目录下所有 `@tool` 装饰的函数,提取:
- `__name__` → tool name
- `__doc__` → description
- `__annotations__` → parameters schema
### 4.3 注册路由
`app/main.py` 中注册新路由:
```python
from app.routers import tools as tools_router
app.include_router(tools_router.router, prefix="/api", tags=["tools"])
```
---
## 5. 产出要求
- [x] `GET /api/tools` 接口可调用,返回完整工具列表
- [x] 两套工具体系元数据正确聚合
- [x] 统计数据(调用次数、错误率)正确返回
- [x] 按 category 分组source 字段区分来源

View File

@@ -0,0 +1,167 @@
# Phase 2前端 Tools Tab 实现
日期2026-04-08
状态:待开始
---
## 1. 本阶段目的
实现 Temple 页面的 Tools Tab包括分类树 + 详情面板 + Metrics Strip。
---
## 2. 核心文件
| 文件 | 作用 |
|------|------|
| `frontend/src/api/tools.ts` | 新建Tools API 客户端 |
| `frontend/src/pages/temple/composables/useTemple.ts` | 新建Tab/Skills 逻辑 |
| `frontend/src/pages/temple/index.vue` | 重写主页面(替换占位符) |
| `frontend/src/pages/temple/templePage.css` | 新建,样式 |
---
## 3. 页面布局
```
┌─────────────────────────────────────────────────────────────┐
│ [◈ 智慧神殿] [Tools] [Skills] ← Tab 切换器 │
├─────────────────────────────────────────────────────────────┤
│ TOTAL: 30 │ ACTIVE: 28 │ AGENTS: 5 ← Metrics Strip │
├──────────────────────────┬──────────────────────────────────┤
│ │ │
│ [分类树] │ [工具详情] │
│ │ │
│ ▼ 注册层 │ file_operator │
│ 文件操作 │ ──────────── │
│ 任务管理 │ 描述: 强大的文件系统操作工具 │
│ 网页抓取 │ 命令: 4 个 │
│ 联网搜索 │ 标签: file, system, essential │
│ ▼ Agent层 │ 状态: 启用 │
│ 文件工具 │ 调用: 1,234 次 │
│ 系统命令 │ 错误率: 0.2% │
│ 开发工具 │ 平均耗时: 150ms │
│ 协作工具 │ │
│ 知识检索 │ [Commands] │
│ 日程管理 │ ─────────────────────────── │
│ 任务管理 │ read_file │
│ 论坛功能 │ ─────────────────────────── │
│ 时间推理 │ write_file │
│ │ ─────────────────────────── │
│ │ list_directory │
│ │ ─────────────────────────── │
│ │ search_files │
└──────────────────────────┴──────────────────────────────────┘
```
---
## 4. 组件说明
### 4.1 Tab 切换器
两个 Tab`Tools` | `Skills`
- `Tools` → 本 phase 实现
- `Skills` → Phase 3复用现有页面
### 4.2 Metrics Strip
三个统计指标卡片:
| 指标 | 说明 |
|------|------|
| `TOTAL` | 系统工具总数(所有工具的 commands 总数) |
| `ACTIVE` | 启用中的工具数 |
| `AGENTS` | 工具绑定的 Agent 类型数(固定 5 |
### 4.3 分类树
- 两级结构:大类(注册层 / Agent 层)→ 具体分类
- 点击分类 → 右侧显示该分类下的工具列表
- 点击工具 → 右侧显示工具详情
### 4.4 工具详情面板
当无工具选中时:显示分类下的工具列表(卡片形式)
当有工具选中时:显示工具详情
详情内容:
- **Name / Display Name**
- **Description**
- **Category / Tags**
- **Enabled status**
- **Stats**: call_count, error_rate, avg_duration_ms
- **Commands**: 每个 command 的 name + description只读
---
## 5. useTemple.ts 接口设计
```typescript
// useTemple.ts
export function useTemple() {
// State
const activeTab = ref<'tools' | 'skills'>('tools')
const categories = ref<ToolCategory[]>([])
const selectedCategory = ref<string | null>(null)
const selectedTool = ref<ToolInfo | null>(null)
const loading = ref(false)
// Computed
const summary = computed(() => { ... })
const currentCategoryTools = computed(() => { ... })
// Actions
async function fetchTools() { ... }
function selectCategory(name: string) { ... }
function selectTool(tool: ToolInfo) { ... }
return {
activeTab,
categories,
selectedCategory,
selectedTool,
loading,
summary,
currentCategoryTools,
fetchTools,
selectCategory,
selectTool,
}
}
```
---
## 6. 样式规范
沿用 Jarvis 现有风格:
```css
/* templePage.css */
.temple-page {
/* 复用 jarvis-* CSS 变量 */
background: var(--bg-primary);
color: var(--text-primary);
}
.metric-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.category-tree {
/* 深色终端风格 */
}
```
---
## 7. 产出要求
- [x] Tab 切换器正常切换 Tools / Skills
- [x] Metrics Strip 正确显示统计数据
- [x] 分类树正确渲染,展开/收起正常
- [x] 点击工具有详情面板Commands 列表完整
- [x] 样式与 Jarvis 整体风格一致

View File

@@ -0,0 +1,88 @@
# Phase 3Skills Tab 复用集成
日期2026-04-08
状态:待开始
---
## 1. 本阶段目的
将现有的 `/skills` 页面完整嵌入 Temple 页面的 Skills Tab实现统一入口。
---
## 2. 核心文件
| 文件 | 作用 |
|------|------|
| `frontend/src/pages/skills/index.vue` | 已有Skills 完整页面 |
| `frontend/src/pages/skills/composables/useSkillsPage.ts` | 已有Skills 逻辑 |
| `frontend/src/api/skill.ts` | 已有Skills API 客户端 |
---
## 3. 集成方式
### 3.1 方案选择
**方案 A推荐Tab 内条件渲染**
`Temple/index.vue` 中使用 `v-if` 切换:
```vue
<div v-if="activeTab === 'skills'">
<!-- Skills 页面内容内联或引用子组件 -->
</div>
```
优点:单一页面,状态共享简单
缺点Skills 页面较大,代码集中
**方案 B路由嵌套**
```vue
// Temple/index.vue
<router-view />
```
`skills/` 路由加 `parent: temple`
优点:页面分离,代码清晰
缺点:需要改路由配置
**推荐方案 A**改动最小Skills 页面代码以内联形式放入 Temple。
### 3.2 Tab 切换逻辑
```typescript
function switchTab(tab: 'tools' | 'skills') {
activeTab.value = tab
if (tab === 'skills') {
// Skills 页面初始化(如果需要)
}
}
```
---
## 4. 样式调整
Skills 页面样式独立在 `skillsPage.css`,切换 Tab 时保留其样式上下文。
---
## 5. 注意事项
- Skills 页面的 Modal创建/编辑)需要在 Tab 切换后仍可正常弹出
- Skills 页面的 API 调用(`skillApi.list()`, `skillApi.create()` 等)保持不变
- Metrics Strip 在 Skills Tab 下显示不同的指标TOTAL / ACTIVE / UPTIME
---
## 6. 产出要求
- [x] Skills Tab 点击后正确切换到 Skills 页面
- [x] Skills 的 CRUD创建/编辑/删除/启用/禁用)功能正常
- [x] Skills 的 MCP Panel 仍可正常打开
- [x] Skills 页面的 Modal、Drawer 等交互正常
- [x] Tab 切换不丢失状态

69
frontend/src/api/tools.ts Normal file
View File

@@ -0,0 +1,69 @@
import api from './index'
import type { AxiosResponse } from 'axios'
/** 单个工具命令 */
export interface ToolCommand {
name: string
description: string
parameters: Record<string, unknown>
}
/** 工具调用统计 */
export interface ToolStats {
call_count: number
error_count: number
total_duration_ms: number
avg_duration_ms: number
error_rate: number
}
/** 工具信息 */
export interface ToolInfo {
name: string
display_name: string
description: string
category: string
subcategory: string
source: 'manifest' | 'agent'
source_file: string
tags: string[]
enabled: boolean
commands: ToolCommand[]
stats: ToolStats | null
config: Record<string, unknown>
}
/** 工具子分类 */
export interface ToolSubgroup {
name: string
display_name: string
tools: ToolInfo[]
}
/** 工具大分类 */
export interface ToolCategory {
name: string
display_name: string
subgroups: ToolSubgroup[]
}
/** 工具统计摘要 */
export interface ToolSummary {
total_commands: number
active_commands: number
total_tools: number
manifest_tools: number
agent_tools: number
}
/** GET /api/tools 响应 */
export interface ToolsResponse {
categories: ToolCategory[]
summary: ToolSummary
}
export const toolsApi = {
list: (): Promise<AxiosResponse<ToolsResponse>> => {
return api.get('/api/tools')
},
}

View File

@@ -0,0 +1,133 @@
import { ref, computed } from 'vue'
import { toolsApi, type ToolCategory, type ToolInfo, type ToolsResponse } from '@/api/tools'
export type TabType = 'tools' | 'skills'
export function useTemple() {
// ===== State =====
const activeTab = ref<TabType>('tools')
const toolsLoading = ref(false)
const toolsError = ref<string | null>(null)
// Tools data
const categories = ref<ToolCategory[]>([])
const summary = ref({
total_commands: 0,
active_commands: 0,
total_tools: 0,
manifest_tools: 0,
agent_tools: 0,
})
// Selection state (Tools Tab)
const selectedCategory = ref<string | null>(null) // 大分类名,如 "注册层"
const selectedSubgroup = ref<string | null>(null) // 子分类名,如 "文件操作"
const selectedTool = ref<ToolInfo | null>(null)
// ===== Computed =====
/** 展平所有工具列表 */
const allTools = computed(() => {
return categories.value.flatMap((cat) =>
cat.subgroups.flatMap((sub) => sub.tools)
)
})
/** 当前选中的大分类下的子分类 */
const currentSubgroups = computed(() => {
if (!selectedCategory.value) return []
const cat = categories.value.find((c) => c.name === selectedCategory.value)
return cat?.subgroups ?? []
})
/** 当前选中子分类下的工具 */
const currentTools = computed(() => {
if (!selectedSubgroup.value) return []
for (const cat of categories.value) {
const sub = cat.subgroups.find((s) => s.name === selectedSubgroup.value)
if (sub) return sub.tools
}
return []
})
/** 当前选中工具的详情 */
const currentToolDetail = computed(() => selectedTool.value)
// ===== Actions =====
async function fetchTools() {
toolsLoading.value = true
toolsError.value = null
try {
const res = await toolsApi.list()
const data: ToolsResponse = res.data
categories.value = data.categories
summary.value = data.summary
// 默认选中第一个分类和子分类
if (categories.value.length > 0) {
const firstCat = categories.value[0]
selectedCategory.value = firstCat.name
if (firstCat.subgroups.length > 0) {
selectedSubgroup.value = firstCat.subgroups[0].name
}
}
} catch (e: unknown) {
toolsError.value = e instanceof Error ? e.message : 'Failed to load tools'
console.error('[useTemple] fetchTools error:', e)
} finally {
toolsLoading.value = false
}
}
function selectCategory(name: string) {
selectedCategory.value = name
selectedSubgroup.value = null
selectedTool.value = null
// 自动选中第一个子分类
const cat = categories.value.find((c) => c.name === name)
if (cat && cat.subgroups.length > 0) {
selectedSubgroup.value = cat.subgroups[0].name
}
}
function selectSubgroup(name: string) {
selectedSubgroup.value = name
selectedTool.value = null
}
function selectTool(tool: ToolInfo) {
selectedTool.value = tool
}
function clearToolSelection() {
selectedTool.value = null
}
function switchTab(tab: TabType) {
activeTab.value = tab
}
return {
// State
activeTab,
toolsLoading,
toolsError,
categories,
summary,
selectedCategory,
selectedSubgroup,
selectedTool,
// Computed
allTools,
currentSubgroups,
currentTools,
currentToolDetail,
// Actions
fetchTools,
selectCategory,
selectSubgroup,
selectTool,
clearToolSelection,
switchTab,
}
}

View File

@@ -1,56 +1,641 @@
<script setup lang="ts">
// 智慧神殿 - Temple of Wisdom
import { ref, watch } from 'vue'
import { X, Bot, Plus, Edit2, Trash2, Eye, EyeOff, Copy } from 'lucide-vue-next'
import { useTemple } from './composables/useTemple'
import { useSkillsPage } from '../skills/composables/useSkillsPage'
import { type Skill, type SkillCreate } from '@/api/skill'
// ===== Props / Emits =====
const props = defineProps<{ visible: boolean }>()
const emit = defineEmits<{ close: [] }>()
// ===== Temple (Tools) =====
const {
activeTab,
toolsLoading,
toolsError,
categories,
summary,
selectedSubgroup,
selectedTool,
currentTools,
fetchTools,
selectCategory,
selectSubgroup,
selectTool,
switchTab,
} = useTemple()
// ===== Skills (inline from useSkillsPage) =====
const skillsPage = useSkillsPage()
const {
skills,
loading: skillsLoading,
modalOpen,
editingSkill,
closeModal,
createSkill,
updateSkill,
deleteSkill,
toggleActive,
copySkill,
} = skillsPage
// ===== Fetch tools when modal opens =====
watch(
() => props.visible,
(val) => {
if (val) {
void fetchTools()
}
},
{ immediate: true }
)
// ===== Close =====
function handleClose() {
emit('close')
}
// ===== Skills form state (local for modal) =====
const skillForm = ref<SkillCreate>({
name: '',
description: '',
instructions: '',
agent_type: 'general',
tools: [],
visibility: 'private',
})
function openNewSkillModal() {
skillForm.value = {
name: '',
description: '',
instructions: '',
agent_type: 'general',
tools: [],
visibility: 'private',
}
editingSkill.value = null
modalOpen.value = true
}
function openEditSkillModal(skill: Skill) {
skillForm.value = {
name: skill.name,
description: skill.description ?? '',
instructions: skill.instructions,
agent_type: skill.agent_type,
tools: [...skill.tools],
visibility: skill.visibility,
}
editingSkill.value = skill
modalOpen.value = true
}
async function handleSaveSkill() {
if (editingSkill.value) {
await updateSkill()
} else {
await createSkill()
}
closeModal()
}
async function handleDeleteSkill(skill: Skill) {
if (confirm(`Delete skill "${skill.name}"?`)) {
await deleteSkill(skill)
}
}
const AGENT_TYPES = ['general', 'schedule_planner', 'executor', 'librarian', 'analyst']
const AVAILABLE_TOOLS = ['file_operations', 'web_search', 'code_execution', 'database', 'api_calls', 'shell', 'git', 'calendar', 'tasks']
const VISIBILITY_OPTIONS = ['private', 'team', 'market'] as const
</script>
<template>
<div class="temple-page">
<div class="page-header">
<h1> 智慧神殿</h1>
<p class="subtitle">深邃智慧永恒传承</p>
</div>
<div class="page-content">
<div class="placeholder-content">
<div class="temple-icon">🏛</div>
<p>智慧神殿 - 敬请期待</p>
<Teleport to="body">
<div v-if="visible" class="temple-modal-overlay" @click.self="handleClose">
<div class="temple-modal" role="dialog" aria-modal="true" aria-label="智慧神殿">
<!-- Header -->
<div class="temple-header">
<div class="temple-header-title">
<span class="temple-title-icon"></span>
<span class="temple-title-text">智慧神殿</span>
</div>
<button class="temple-close-btn" aria-label="关闭" @click="handleClose">
<X :size="16" />
</button>
</div>
<!-- Tab Bar -->
<div class="temple-tabs">
<button
class="temple-tab"
:class="{ active: activeTab === 'tools' }"
@click="switchTab('tools')"
>
<span class="temple-tab-icon"></span>
<span>Tools</span>
</button>
<button
class="temple-tab"
:class="{ active: activeTab === 'skills' }"
@click="switchTab('skills')"
>
<span class="temple-tab-icon"></span>
<span>Skills</span>
</button>
</div>
<!-- Metrics Strip -->
<div class="temple-metrics">
<div class="temple-metric">
<span class="temple-metric-label">TOTAL</span>
<span class="temple-metric-value">{{ summary.total_commands }}</span>
</div>
<div class="temple-metric">
<span class="temple-metric-label">ACTIVE</span>
<span class="temple-metric-value">{{ summary.active_commands }}</span>
</div>
<div class="temple-metric">
<span class="temple-metric-label">MANIFEST</span>
<span class="temple-metric-value">{{ summary.manifest_tools }}</span>
</div>
<div class="temple-metric">
<span class="temple-metric-label">AGENT</span>
<span class="temple-metric-value">{{ summary.agent_tools }}</span>
</div>
</div>
<!-- Body -->
<div class="temple-body">
<!-- ========== TOOLS TAB ========== -->
<div v-if="activeTab === 'tools'" class="temple-tools-layout">
<!-- Left: Category Tree -->
<nav class="temple-tree">
<template v-for="cat in categories" :key="cat.name">
<div class="temple-tree-section-title">{{ cat.display_name || cat.name }}</div>
<div
v-for="sub in cat.subgroups"
:key="sub.name"
class="temple-tree-item temple-tree-subgroup"
:class="{ active: selectedSubgroup === sub.name }"
@click="selectCategory(cat.name); selectSubgroup(sub.name)"
>
<span class="temple-tree-dot"></span>
{{ sub.display_name || sub.name }}
</div>
</template>
</nav>
<!-- Middle: Tool List -->
<div class="temple-tool-list">
<div v-if="toolsLoading" class="temple-loading">
<span>Loading tools...</span>
</div>
<div v-else-if="toolsError" class="temple-empty" style="color:#f87171;">
<div class="temple-empty-icon"></div>
<span>加载失败: {{ toolsError }}</span>
</div>
<div v-else-if="categories.length === 0" class="temple-empty">
<div class="temple-empty-icon"></div>
<span>暂无可用工具</span>
</div>
<div
v-for="tool in currentTools"
:key="tool.name"
class="temple-tool-card"
:class="{ selected: selectedTool?.name === tool.name }"
@click="selectTool(tool)"
>
<div class="temple-tool-card-icon"></div>
<div class="temple-tool-card-info">
<div class="temple-tool-card-name">{{ tool.display_name || tool.name }}</div>
<div class="temple-tool-card-desc">{{ tool.description }}</div>
</div>
<div class="temple-tool-card-commands">{{ tool.commands.length }} cmds</div>
</div>
</div>
<!-- Right: Tool Detail -->
<div class="temple-detail">
<div v-if="!selectedTool" class="temple-empty">
<div class="temple-empty-icon"></div>
<span>选择工具查看详情</span>
</div>
<template v-else>
<div class="temple-detail-header">
<div>
<div class="temple-detail-name">{{ selectedTool.display_name || selectedTool.name }}</div>
<div class="temple-detail-display-name">{{ selectedTool.name }}</div>
</div>
<span class="temple-detail-source">{{ selectedTool.source }}</span>
</div>
<p class="temple-detail-desc">{{ selectedTool.description }}</p>
<div class="temple-detail-section">
<div class="temple-detail-section-title">STATS</div>
<div class="temple-detail-stats">
<div class="temple-detail-stat">
<span class="temple-detail-stat-value">{{ selectedTool.stats?.call_count ?? 0 }}</span>
<span class="temple-detail-stat-label">Calls</span>
</div>
<div class="temple-detail-stat">
<span class="temple-detail-stat-value">{{ selectedTool.stats?.error_rate ?? 0 }}%</span>
<span class="temple-detail-stat-label">Error Rate</span>
</div>
<div class="temple-detail-stat">
<span class="temple-detail-stat-value">{{ selectedTool.stats?.avg_duration_ms ?? 0 }}ms</span>
<span class="temple-detail-stat-label">Avg Duration</span>
</div>
</div>
</div>
<div class="temple-detail-section">
<div class="temple-detail-section-title">COMMANDS ({{ selectedTool.commands.length }})</div>
<div class="temple-commands">
<div v-for="cmd in selectedTool.commands" :key="cmd.name" class="temple-command-item">
<div class="temple-command-name">/{{ cmd.name }}</div>
<div class="temple-command-desc">{{ cmd.description }}</div>
</div>
</div>
</div>
<div class="temple-detail-section">
<div class="temple-detail-section-title">TAGS</div>
<div class="temple-tags">
<span v-for="tag in selectedTool.tags" :key="tag" class="temple-tag">{{ tag }}</span>
<span class="temple-tag" style="background:rgba(0,245,212,0.1);color:#00f5d4;border-color:rgba(0,245,212,0.2)">
{{ selectedTool.source }}
</span>
</div>
</div>
</template>
</div>
</div>
<!-- ========== SKILLS TAB ========== -->
<div v-if="activeTab === 'skills'" class="temple-skills-container">
<!-- Skills Toolbar -->
<div class="toolbar" style="padding:10px 16px;border-bottom:1px solid rgba(0,245,212,0.08);display:flex;gap:8px;align-items:center;">
<button class="btn-add" style="display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:6px;background:rgba(0,245,212,0.1);border:1px solid rgba(0,245,212,0.3);color:#00f5d4;cursor:pointer;font-size:12px;font-weight:500;" @click="openNewSkillModal">
<Plus :size="13" />
<span>新建技能</span>
</button>
</div>
<!-- Skills Table -->
<div v-if="skillsLoading" class="temple-loading" style="padding:40px;">
<span>Loading skills...</span>
</div>
<div v-else-if="skills.length === 0" class="temple-empty">
<div class="temple-empty-icon"></div>
<span>暂无技能</span>
</div>
<div v-else class="skills-table-wrap" style="flex:1;overflow-y:auto;padding:0 16px;">
<table class="skills-table" style="width:100%;border-collapse:collapse;">
<thead>
<tr style="border-bottom:1px solid rgba(0,245,212,0.1);">
<th style="text-align:left;padding:8px 10px;font-size:10px;color:#5a6b7a;letter-spacing:1px;font-weight:600;">NAME</th>
<th style="text-align:left;padding:8px 10px;font-size:10px;color:#5a6b7a;letter-spacing:1px;font-weight:600;">TYPE</th>
<th style="text-align:left;padding:8px 10px;font-size:10px;color:#5a6b7a;letter-spacing:1px;font-weight:600;">VISIBILITY</th>
<th style="text-align:left;padding:8px 10px;font-size:10px;color:#5a6b7a;letter-spacing:1px;font-weight:600;">STATUS</th>
<th style="text-align:left;padding:8px 10px;font-size:10px;color:#5a6b7a;letter-spacing:1px;font-weight:600;">SOURCE</th>
<th style="text-align:right;padding:8px 10px;font-size:10px;color:#5a6b7a;letter-spacing:1px;font-weight:600;">ACTIONS</th>
</tr>
</thead>
<tbody>
<tr
v-for="skill in skills"
:key="skill.id"
style="border-bottom:1px solid rgba(0,245,212,0.05);transition:background 0.12s;"
:style="{ opacity: skill.is_active ? 1 : 0.5 }"
>
<td style="padding:10px 10px;">
<div style="display:flex;align-items:center;gap:8px;">
<Bot :size="13" style="color:#00f5d4;flex-shrink:0;" />
<div>
<div style="font-size:13px;font-weight:600;color:#e8f4f8;">{{ skill.name }}</div>
<div style="font-size:11px;color:#5a6b7a;margin-top:1px;">{{ skill.description || '无描述' }}</div>
</div>
</div>
</td>
<td style="padding:10px 10px;">
<span class="mono-pill" style="font-size:10px;padding:2px 8px;border-radius:3px;background:rgba(0,245,212,0.06);color:#8a9bae;border:1px solid rgba(0,245,212,0.1);font-family:monospace;">{{ skill.agent_type }}</span>
</td>
<td style="padding:10px 10px;">
<span style="font-size:10px;padding:2px 8px;border-radius:3px;"
:style="skill.visibility === 'private' ? 'background:rgba(168,85,247,0.12);color:#c084fc;border:1px solid rgba(168,85,247,0.2)' : skill.visibility === 'team' ? 'background:rgba(34,197,94,0.12);color:#4ade80;border:1px solid rgba(34,197,94,0.2)' : 'background:rgba(59,130,246,0.12);color:#60a5fa;border:1px solid rgba(59,130,246,0.2)'">
{{ skill.visibility }}
</span>
</td>
<td style="padding:10px 10px;">
<span style="display:flex;align-items:center;gap:5px;font-size:11px;" :style="{ color: skill.is_active ? '#4ade80' : '#6b7280' }">
<span style="width:5px;height:5px;border-radius:50%;flex-shrink:0;" :style="{ background: skill.is_active ? '#4ade80' : '#6b7280' }"></span>
{{ skill.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td style="padding:10px 10px;">
<span style="font-size:10px;padding:2px 8px;border-radius:3px;" :style="skill.is_builtin ? 'background:rgba(251,191,36,0.1);color:#fbbf24;border:1px solid rgba(251,191,36,0.2)' : 'background:rgba(0,245,212,0.06);color:#8a9bae;border:1px solid rgba(0,245,212,0.1)'">
{{ skill.is_builtin ? 'builtin' : 'custom' }}
</span>
</td>
<td style="padding:10px 10px;">
<div style="display:flex;align-items:center;justify-content:flex-end;gap:4px;">
<button class="action-btn" :style="{ color: skill.is_active ? '#4ade80' : '#6b7280' }" :title="skill.is_active ? 'Disable' : 'Enable'" @click="toggleActive(skill)">
<Eye v-if="skill.is_active" :size="13" />
<EyeOff v-else :size="13" />
</button>
<button class="action-btn" title="Copy" style="color:#8a9bae;" @click="copySkill(skill)">
<Copy :size="13" />
</button>
<button class="action-btn edit" title="Edit" style="color:#60a5fa;" @click="openEditSkillModal(skill)">
<Edit2 :size="13" />
</button>
<button class="action-btn delete" title="Delete" style="color:#f87171;" :disabled="skill.is_builtin" @click="handleDeleteSkill(skill)">
<Trash2 :size="13" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Skills Create/Edit Modal (inline) -->
<Transition :css="false" @enter="(el: Element, done: () => void) => { (el as HTMLElement).style.opacity = '1'; done() }" @leave="(el: Element, done: () => void) => { (el as HTMLElement).style.opacity = '0'; done() }">
<div v-if="modalOpen" class="modal-overlay" @click.self="closeModal">
<div class="modal-card" style="width:min(560px,90vw);max-height:85vh;overflow-y:auto;">
<div class="modal-header">
<span class="modal-title">{{ editingSkill ? '// 编辑技能' : '// 新建技能' }}</span>
<button class="btn-close" aria-label="关闭" @click="closeModal"><X :size="16" /></button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="skill-name">// NAME</label>
<input id="skill-name" v-model="skillForm.name" type="text" class="form-input" placeholder="Skill name" />
</div>
<div class="form-group">
<label class="form-label" for="skill-desc">// DESCRIPTION</label>
<textarea id="skill-desc" v-model="skillForm.description" class="form-textarea" rows="2" placeholder="Describe what this skill does..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="skill-type">// AGENT TYPE</label>
<select id="skill-type" v-model="skillForm.agent_type" class="form-select">
<option v-for="t in AGENT_TYPES" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="skill-vis">// VISIBILITY</label>
<select id="skill-vis" v-model="skillForm.visibility" class="form-select">
<option v-for="v in VISIBILITY_OPTIONS" :key="v" :value="v">{{ v }}</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">// TOOLS</label>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;">
<label v-for="tool in AVAILABLE_TOOLS" :key="tool" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 8px;border-radius:4px;border:1px solid rgba(0,245,212,0.08);transition:all 0.12s;" :style="{ background: skillForm.tools?.includes(tool) ? 'rgba(0,245,212,0.08)' : 'transparent' }">
<input type="checkbox" :checked="skillForm.tools?.includes(tool)" @change="() => { if (!skillForm.tools) skillForm.tools = []; const idx = skillForm.tools.indexOf(tool); if (idx >= 0) skillForm.tools.splice(idx, 1); else skillForm.tools.push(tool); }" style="accent-color:#00f5d4;" />
<span style="font-size:11px;color:#8a9bae;">{{ tool }}</span>
</label>
</div>
</div>
<div class="form-group flex-1">
<label class="form-label" for="skill-inst">// INSTRUCTIONS</label>
<textarea id="skill-inst" v-model="skillForm.instructions" class="form-textarea code-textarea" rows="6" placeholder="Enter skill instructions..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="closeModal">Cancel</button>
<button
class="btn-primary"
:disabled="!skillForm.name || !skillForm.instructions"
@click="handleSaveSkill"
>
{{ editingSkill ? 'Update' : 'Create' }}
</button>
</div>
</div>
</div>
</Transition>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped src="./templePage.css">
</style>
<style scoped>
.temple-page {
padding: 24px;
min-height: 100vh;
background: var(--bg-primary);
}
.page-header {
text-align: center;
margin-bottom: 32px;
}
.page-header h1 {
font-size: 28px;
color: var(--text-primary);
margin-bottom: 8px;
}
.subtitle {
color: var(--text-secondary);
font-size: 14px;
}
.placeholder-content {
/* Skills modal overrides */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 2000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: var(--text-secondary);
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
}
.temple-icon {
font-size: 64px;
margin-bottom: 16px;
.modal-card {
background: #0f1117;
border: 1px solid rgba(0, 245, 212, 0.15);
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}
</style>
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid rgba(0, 245, 212, 0.1);
}
.modal-title {
font-size: 13px;
font-weight: 600;
color: #00f5d4;
letter-spacing: 0.3px;
}
.btn-close {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid rgba(0, 245, 212, 0.15);
border-radius: 5px;
background: transparent;
color: #8a9bae;
cursor: pointer;
transition: all 0.15s;
}
.btn-close:hover {
border-color: #00f5d4;
color: #00f5d4;
}
.modal-body {
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 12px 18px;
border-top: 1px solid rgba(0, 245, 212, 0.1);
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.form-row {
display: flex;
gap: 12px;
}
.form-row .form-group {
flex: 1;
}
.form-label {
font-size: 10px;
color: #5a6b7a;
letter-spacing: 1px;
font-weight: 600;
text-transform: uppercase;
}
.form-input,
.form-select,
.form-textarea {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 245, 212, 0.12);
border-radius: 6px;
color: #e8f4f8;
font-size: 12.5px;
padding: 7px 10px;
outline: none;
transition: border-color 0.15s;
font-family: inherit;
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
border-color: rgba(0, 245, 212, 0.4);
}
.form-select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235a6b7a' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
}
.form-textarea {
resize: vertical;
min-height: 60px;
}
.code-textarea {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 12px;
line-height: 1.5;
}
.flex-1 {
flex: 1;
}
.btn-secondary {
padding: 7px 16px;
border-radius: 6px;
border: 1px solid rgba(0, 245, 212, 0.2);
background: transparent;
color: #8a9bae;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.btn-secondary:hover {
border-color: rgba(0, 245, 212, 0.4);
color: #e8f4f8;
}
.btn-primary {
padding: 7px 18px;
border-radius: 6px;
border: 1px solid rgba(0, 245, 212, 0.3);
background: rgba(0, 245, 212, 0.1);
color: #00f5d4;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.btn-primary:hover:not(:disabled) {
background: rgba(0, 245, 212, 0.15);
border-color: #00f5d4;
}
.btn-primary:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: 1px solid rgba(0, 245, 212, 0.1);
border-radius: 4px;
background: transparent;
color: #8a9bae;
cursor: pointer;
transition: all 0.15s;
}
.action-btn:hover:not(:disabled) {
background: rgba(0, 245, 212, 0.06);
}
.action-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,533 @@
/* ============================================================
Temple Modal - 悬浮弹窗样式
============================================================ */
/* CSS Variables 复用 jarvis 体系 */
.temple-modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
animation: overlayFadeIn 0.2s ease-out;
}
@keyframes overlayFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.temple-modal {
width: min(95vw, 1400px);
height: min(88vh, 900px);
background: var(--bg-void, #0a0a0f);
border: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.15));
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow:
0 0 0 1px rgba(0, 245, 212, 0.05),
0 24px 64px rgba(0, 0, 0, 0.6),
0 0 80px rgba(0, 245, 212, 0.04);
animation: modalSlideIn 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.96) translateY(8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* ---- Header ---- */
.temple-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.1));
flex-shrink: 0;
}
.temple-header-title {
display: flex;
align-items: center;
gap: 10px;
}
.temple-title-icon {
font-size: 18px;
opacity: 0.8;
}
.temple-title-text {
font-size: 15px;
font-weight: 600;
color: var(--text-primary, #e8f4f8);
letter-spacing: 0.5px;
}
.temple-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.2));
border-radius: 6px;
background: transparent;
color: var(--text-secondary, #8a9bae);
cursor: pointer;
transition: all 0.15s ease;
}
.temple-close-btn:hover {
border-color: var(--accent-cyan, #00f5d4);
color: var(--accent-cyan, #00f5d4);
background: rgba(0, 245, 212, 0.06);
}
/* ---- Tab Bar ---- */
.temple-tabs {
display: flex;
align-items: center;
gap: 4px;
padding: 10px 20px 0;
flex-shrink: 0;
}
.temple-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 16px;
border-radius: 6px 6px 0 0;
border: 1px solid transparent;
border-bottom: none;
background: transparent;
color: var(--text-muted, #5a6b7a);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
bottom: -1px;
}
.temple-tab:hover {
color: var(--text-secondary, #8a9bae);
}
.temple-tab.active {
color: var(--accent-cyan, #00f5d4);
background: rgba(0, 245, 212, 0.06);
border-color: var(--border-subtle, rgba(0, 245, 212, 0.15));
}
.temple-tab-icon {
font-size: 14px;
}
/* ---- Metrics Strip ---- */
.temple-metrics {
display: flex;
gap: 1px;
padding: 10px 20px;
flex-shrink: 0;
border-bottom: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.08));
background: rgba(0, 0, 0, 0.2);
}
.temple-metric {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
border-radius: 4px;
background: rgba(0, 245, 212, 0.03);
border: 1px solid rgba(0, 245, 212, 0.06);
min-width: 80px;
}
.temple-metric-label {
font-size: 10px;
color: var(--text-muted, #5a6b7a);
letter-spacing: 1px;
font-weight: 500;
}
.temple-metric-value {
font-size: 15px;
font-weight: 700;
color: var(--accent-cyan, #00f5d4);
font-variant-numeric: tabular-nums;
}
/* ---- Main Content ---- */
.temple-body {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
}
/* ---- Tools Tab Layout ---- */
.temple-tools-layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* Category Tree (Left sidebar) */
.temple-tree {
width: 240px;
flex-shrink: 0;
overflow-y: auto;
padding: 12px 0;
border-right: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.08));
}
.temple-tree::-webkit-scrollbar {
width: 4px;
}
.temple-tree::-webkit-scrollbar-thumb {
background: rgba(0, 245, 212, 0.15);
border-radius: 2px;
}
.temple-tree-section {
margin-bottom: 4px;
}
.temple-tree-section-title {
padding: 6px 16px 4px;
font-size: 10px;
color: var(--text-muted, #5a6b7a);
letter-spacing: 1.2px;
font-weight: 600;
text-transform: uppercase;
}
.temple-tree-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
font-size: 12.5px;
color: var(--text-secondary, #8a9bae);
cursor: pointer;
transition: all 0.12s ease;
border-left: 2px solid transparent;
}
.temple-tree-item:hover {
background: rgba(0, 245, 212, 0.05);
color: var(--text-primary, #e8f4f8);
}
.temple-tree-item.active {
background: rgba(0, 245, 212, 0.08);
color: var(--accent-cyan, #00f5d4);
border-left-color: var(--accent-cyan, #00f5d4);
}
.temple-tree-subgroup {
padding-left: 24px;
font-size: 12px;
}
.temple-tree-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: currentColor;
opacity: 0.5;
flex-shrink: 0;
}
/* Tools List & Detail (Right panel) */
.temple-tools-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.temple-tool-list {
padding: 12px 16px;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.temple-tool-list::-webkit-scrollbar {
width: 4px;
}
.temple-tool-list::-webkit-scrollbar-thumb {
background: rgba(0, 245, 212, 0.15);
border-radius: 2px;
}
.temple-tool-card {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: 6px;
border: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.08));
background: rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: all 0.15s ease;
}
.temple-tool-card:hover {
border-color: rgba(0, 245, 212, 0.25);
background: rgba(0, 245, 212, 0.04);
}
.temple-tool-card.selected {
border-color: var(--accent-cyan, #00f5d4);
background: rgba(0, 245, 212, 0.07);
}
.temple-tool-card-icon {
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(0, 245, 212, 0.08);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.temple-tool-card-info {
flex: 1;
min-width: 0;
}
.temple-tool-card-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary, #e8f4f8);
margin-bottom: 2px;
}
.temple-tool-card-desc {
font-size: 11.5px;
color: var(--text-muted, #5a6b7a);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.temple-tool-card-commands {
font-size: 10px;
color: var(--text-muted, #5a6b7a);
background: rgba(0, 245, 212, 0.06);
padding: 2px 6px;
border-radius: 3px;
flex-shrink: 0;
}
/* Tool Detail Panel */
.temple-detail {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
border-top: 1px solid var(--border-subtle, rgba(0, 245, 212, 0.08));
display: flex;
flex-direction: column;
gap: 14px;
}
.temple-detail::-webkit-scrollbar {
width: 4px;
}
.temple-detail::-webkit-scrollbar-thumb {
background: rgba(0, 245, 212, 0.15);
border-radius: 2px;
}
.temple-detail-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.temple-detail-name {
font-size: 16px;
font-weight: 700;
color: var(--text-primary, #e8f4f8);
}
.temple-detail-display-name {
font-size: 13px;
color: var(--accent-cyan, #00f5d4);
margin-top: 2px;
}
.temple-detail-source {
font-size: 10px;
color: var(--text-muted, #5a6b7a);
background: rgba(0, 245, 212, 0.05);
padding: 2px 8px;
border-radius: 3px;
border: 1px solid rgba(0, 245, 212, 0.1);
flex-shrink: 0;
}
.temple-detail-desc {
font-size: 12.5px;
color: var(--text-secondary, #8a9bae);
line-height: 1.6;
}
.temple-detail-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.temple-detail-section-title {
font-size: 10px;
color: var(--text-muted, #5a6b7a);
letter-spacing: 1.2px;
font-weight: 600;
text-transform: uppercase;
}
.temple-detail-stats {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.temple-detail-stat {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 14px;
background: rgba(0, 0, 0, 0.25);
border-radius: 6px;
border: 1px solid rgba(0, 245, 212, 0.06);
min-width: 80px;
}
.temple-detail-stat-value {
font-size: 15px;
font-weight: 700;
color: var(--text-primary, #e8f4f8);
font-variant-numeric: tabular-nums;
}
.temple-detail-stat-label {
font-size: 10px;
color: var(--text-muted, #5a6b7a);
letter-spacing: 0.5px;
}
/* Commands List */
.temple-commands {
display: flex;
flex-direction: column;
gap: 6px;
}
.temple-command-item {
padding: 10px 14px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
border: 1px solid rgba(0, 245, 212, 0.06);
}
.temple-command-name {
font-size: 12px;
font-weight: 600;
color: var(--accent-cyan, #00f5d4);
font-family: 'SF Mono', 'Fira Code', monospace;
margin-bottom: 4px;
}
.temple-command-desc {
font-size: 11.5px;
color: var(--text-secondary, #8a9bae);
line-height: 1.5;
}
/* Tags */
.temple-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.temple-tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 3px;
background: rgba(123, 44, 191, 0.15);
color: #c084fc;
border: 1px solid rgba(123, 44, 191, 0.2);
}
/* Empty state */
.temple-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
height: 100%;
color: var(--text-muted, #5a6b7a);
font-size: 13px;
}
.temple-empty-icon {
font-size: 32px;
opacity: 0.4;
}
/* Loading */
.temple-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted, #5a6b7a);
font-size: 13px;
gap: 10px;
}
/* Skills Tab overrides */
.temple-skills-container {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* Section label */
.temple-section-label {
font-size: 10px;
color: var(--text-muted, #5a6b7a);
letter-spacing: 1.2px;
font-weight: 600;
text-transform: uppercase;
padding: 10px 16px 6px;
}