chore: update nanobot to 0.1.4.post6
This commit is contained in:
Generated
+132
@@ -15,6 +15,7 @@
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"axios": "^1.13.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -3751,6 +3752,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.27",
|
||||
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.27.tgz",
|
||||
@@ -3788,6 +3795,17 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz",
|
||||
@@ -4221,6 +4239,18 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
@@ -4834,6 +4864,15 @@
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
@@ -5038,6 +5077,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.45.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.45.1.tgz",
|
||||
@@ -5689,6 +5743,63 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/format/-/format-0.2.2.tgz",
|
||||
@@ -5979,6 +6090,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -8843,6 +8969,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"axios": "^1.13.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Login } from "./pages/Login";
|
||||
import { ModelConfigs } from "./pages/ModelConfigs";
|
||||
import { DataSources } from "./pages/DataSources";
|
||||
import { Modeling } from "./pages/Modeling";
|
||||
import { Subagents } from "./pages/Subagents";
|
||||
import { useAuthStore } from "./store/authStore";
|
||||
|
||||
// Protected Route Component
|
||||
@@ -95,6 +96,14 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/projects/:projectId/subagents" element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout>
|
||||
<Subagents />
|
||||
</MainLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/users" element={
|
||||
<ProtectedRoute requireAdmin={true}>
|
||||
<MainLayout>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = '/api/v1/projects';
|
||||
|
||||
// Add interceptor to include token
|
||||
const axiosInstance = axios.create();
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
export interface Subagent {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
model: string;
|
||||
instructions: string;
|
||||
status: string;
|
||||
projectId: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export const subagentApi = {
|
||||
list: async (projectId: string) => {
|
||||
const response = await axiosInstance.get<Subagent[]>(`${API_BASE_URL}/${projectId}/subagents`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (projectId: string, id: string) => {
|
||||
const response = await axiosInstance.get<Subagent>(`${API_BASE_URL}/${projectId}/subagents/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (projectId: string, data: Partial<Subagent>) => {
|
||||
const response = await axiosInstance.post<Subagent>(`${API_BASE_URL}/${projectId}/subagents`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (_projectId: string, id: string, data: Partial<Subagent>) => {
|
||||
const response = await axiosInstance.put<Subagent>(`/api/v1/subagents/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (_projectId: string, id: string) => {
|
||||
const response = await axiosInstance.delete(`/api/v1/subagents/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
@@ -100,6 +100,16 @@ interface Skill {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const dedupeSkillsById = (skills: Skill[]): Skill[] => {
|
||||
const map = new Map<string, Skill>();
|
||||
for (const skill of skills) {
|
||||
const id = (skill.id || "").trim();
|
||||
if (!id || map.has(id)) continue;
|
||||
map.set(id, skill);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
interface SessionData {
|
||||
key: string;
|
||||
metadata?: {
|
||||
@@ -537,7 +547,7 @@ export function ChatInterface() {
|
||||
url += `?project_id=${currentProject.id}`;
|
||||
}
|
||||
const skills = await api.get<Skill[]>(url);
|
||||
setAvailableSkills(skills);
|
||||
setAvailableSkills(dedupeSkillsById(skills || []));
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch skills:", err);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Settings, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw, Wand2, Folder, Globe } from "lucide-react";
|
||||
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Settings, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw, Wand2, Folder, Globe, Bot } from "lucide-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -821,6 +821,21 @@ function SidebarBody() {
|
||||
{t('projectManagement')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 transition-colors"
|
||||
onClick={() => {
|
||||
if (currentProject?.id) {
|
||||
navigate(`/projects/${currentProject.id}/subagents`);
|
||||
} else {
|
||||
navigate("/projects");
|
||||
}
|
||||
setShowUserMenu(false);
|
||||
}}
|
||||
>
|
||||
<Bot className="h-4 w-4 text-zinc-500" />
|
||||
{t('subagents', 'Subagents')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 transition-colors"
|
||||
onClick={() => {
|
||||
|
||||
@@ -251,5 +251,16 @@
|
||||
"mcpServerName": "MCP Server Name",
|
||||
"noMcpServers": "No MCP servers configured",
|
||||
"confirmDeleteMcpServer": "Are you sure you want to delete this MCP server?",
|
||||
"saveMcpServer": "Save MCP Server"
|
||||
"saveMcpServer": "Save MCP Server",
|
||||
"subagents": "Subagents",
|
||||
"subagentManagement": "Subagent Management",
|
||||
"manageSubagentsDesc": "Manage subagents for this project",
|
||||
"addSubagent": "Add Subagent",
|
||||
"editSubagent": "Edit Subagent",
|
||||
"subagentName": "Subagent Name",
|
||||
"systemInstructionsPlaceholder": "You are a helpful AI assistant...",
|
||||
"selectModel": "Select a model",
|
||||
"noSubagents": "No subagents configured",
|
||||
"confirmDeleteSubagent": "Are you sure you want to delete this subagent?",
|
||||
"selectProjectToManageSubagents": "Please select a project to manage subagents"
|
||||
}
|
||||
|
||||
@@ -251,5 +251,16 @@
|
||||
"mcpServerName": "MCP 服务器名称",
|
||||
"noMcpServers": "暂无 MCP 服务器",
|
||||
"confirmDeleteMcpServer": "确定要删除这个 MCP 服务器吗?",
|
||||
"saveMcpServer": "保存 MCP 服务器"
|
||||
"saveMcpServer": "保存 MCP 服务器",
|
||||
"subagents": "子代理",
|
||||
"subagentManagement": "子代理管理",
|
||||
"manageSubagentsDesc": "管理该项目的子代理",
|
||||
"addSubagent": "添加子代理",
|
||||
"editSubagent": "编辑子代理",
|
||||
"subagentName": "子代理名称",
|
||||
"selectModel": "请选择一个模型",
|
||||
"systemInstructionsPlaceholder": "你是一个有用的 AI 助手...",
|
||||
"noSubagents": "暂无配置的子代理",
|
||||
"confirmDeleteSubagent": "确定要删除这个子代理吗?",
|
||||
"selectProjectToManageSubagents": "请先选择一个项目以管理其子代理"
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@ interface MCPServer {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
const dedupeSkillsById = (skills: Skill[]): Skill[] => {
|
||||
const map = new Map<string, Skill>();
|
||||
for (const skill of skills) {
|
||||
const id = (skill.id || "").trim();
|
||||
if (!id || map.has(id)) continue;
|
||||
map.set(id, skill);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
export function Skills() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<'skills' | 'mcp'>('skills');
|
||||
@@ -69,7 +79,7 @@ export function Skills() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await api.get<Skill[]>(`/api/v1/skills?project_id=${currentProject.id}`);
|
||||
setSkills(data);
|
||||
setSkills(dedupeSkillsById(data || []));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch skills", error);
|
||||
} finally {
|
||||
@@ -104,7 +114,7 @@ export function Skills() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await api.get<Skill[]>(`/api/v1/skills?project_id=${currentProject.id}`);
|
||||
setSkills(data);
|
||||
setSkills(dedupeSkillsById(data || []));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch skills", error);
|
||||
} finally {
|
||||
@@ -153,7 +163,7 @@ export function Skills() {
|
||||
if (newSkill.name && newSkill.description && newSkill.content) {
|
||||
try {
|
||||
if (editingSkill) {
|
||||
await api.put<Skill>(`/api/v1/skills/${editingSkill.id}?project_id=${currentProject.id}`, {
|
||||
await api.put<Skill>(`/api/v1/skills/${encodeURIComponent(editingSkill.id)}?project_id=${currentProject.id}`, {
|
||||
...newSkill,
|
||||
project_id: currentProject.id
|
||||
});
|
||||
@@ -185,7 +195,7 @@ export function Skills() {
|
||||
if (!currentProject) return;
|
||||
if (!window.confirm(t('confirmDeleteSkill'))) return;
|
||||
try {
|
||||
await api.delete(`/api/v1/skills/${id}?project_id=${currentProject.id}`);
|
||||
await api.delete(`/api/v1/skills/${encodeURIComponent(id)}?project_id=${currentProject.id}`);
|
||||
setSkills(skills.filter(s => s.id !== id));
|
||||
} catch (error) {
|
||||
console.error("Failed to delete skill", error);
|
||||
@@ -350,8 +360,8 @@ export function Skills() {
|
||||
</TableRow>
|
||||
) : (
|
||||
<>
|
||||
{skills.map((skill) => (
|
||||
<TableRow key={skill.id} className="group hover:bg-zinc-50/50 transition-colors border-zinc-100">
|
||||
{skills.map((skill, index) => (
|
||||
<TableRow key={`${skill.id}_${index}`} className="group hover:bg-zinc-50/50 transition-colors border-zinc-100">
|
||||
<TableCell className="py-4 px-4 overflow-hidden">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<div className="p-2 bg-indigo-50 rounded-lg text-indigo-600 mt-0.5 shrink-0">
|
||||
@@ -729,4 +739,3 @@ export function Skills() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Trash2, Loader2, Bot, Plus, Pencil } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { subagentApi, type Subagent } from "@/api/subagents";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
interface ModelConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export function Subagents() {
|
||||
const { t } = useTranslation();
|
||||
const { projectId: routeProjectId } = useParams<{ projectId: string }>();
|
||||
const { currentProject } = useProjectStore();
|
||||
|
||||
// Use projectId from route, or fallback to currentProject
|
||||
const projectId = routeProjectId || currentProject?.id?.toString();
|
||||
|
||||
const [subagents, setSubagents] = useState<Subagent[]>([]);
|
||||
const [availableModels, setAvailableModels] = useState<ModelConfig[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingSubagent, setEditingSubagent] = useState<Subagent | null>(null);
|
||||
const [newSubagent, setNewSubagent] = useState<Partial<Subagent>>({
|
||||
name: '',
|
||||
description: '',
|
||||
model: '',
|
||||
instructions: '',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
if (!projectId) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [subagentsData, modelsData] = await Promise.all([
|
||||
subagentApi.list(projectId),
|
||||
api.get<ModelConfig[]>('/api/v1/llm')
|
||||
]);
|
||||
setSubagents(subagentsData || []);
|
||||
setAvailableModels(modelsData || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch initial data", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
fetchInitialData();
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const getModelDisplay = (value?: string) => {
|
||||
if (!value) return '-';
|
||||
const matched = availableModels.find((m) => m.id === value || m.model === value);
|
||||
if (!matched) return value;
|
||||
const label = matched.name || matched.model;
|
||||
return `${label} (${matched.provider})`;
|
||||
};
|
||||
|
||||
const handleSaveSubagent = async () => {
|
||||
if (!projectId) return;
|
||||
if (newSubagent.name && newSubagent.model) {
|
||||
try {
|
||||
if (editingSubagent && editingSubagent.id) {
|
||||
await subagentApi.update(projectId, editingSubagent.id, newSubagent);
|
||||
} else {
|
||||
const payload = {
|
||||
...newSubagent,
|
||||
instructions: newSubagent.instructions || ''
|
||||
};
|
||||
await subagentApi.create(projectId, payload);
|
||||
}
|
||||
await fetchInitialData();
|
||||
setNewSubagent({ name: '', description: '', model: '', instructions: '', status: 'active' });
|
||||
setEditingSubagent(null);
|
||||
setIsDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to save subagent", error);
|
||||
alert(t('saveFailed'));
|
||||
}
|
||||
} else {
|
||||
alert(t('fillRequiredFields'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSubagent = (subagent: Subagent) => {
|
||||
const matched = availableModels.find((m) => m.id === subagent.model || m.model === subagent.model);
|
||||
setEditingSubagent(subagent);
|
||||
setNewSubagent({
|
||||
...subagent,
|
||||
model: matched?.id || subagent.model
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteSubagent = async (id: string) => {
|
||||
if (!projectId) return;
|
||||
if (!window.confirm(t('confirmDeleteSubagent'))) return;
|
||||
try {
|
||||
await subagentApi.delete(projectId, id);
|
||||
setSubagents(subagents.filter(s => s.id !== id));
|
||||
} catch (error) {
|
||||
console.error("Failed to delete subagent", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!projectId) {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-zinc-500 gap-4">
|
||||
<Bot className="h-12 w-12 text-zinc-200" />
|
||||
<p>{t('selectProjectToManageSubagents', 'Please select a project to manage subagents')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-white overflow-hidden">
|
||||
<div className="border-b border-zinc-100 px-8 pt-5 pb-5 bg-white shrink-0 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-zinc-900 flex items-center gap-2">
|
||||
<Bot className="h-6 w-6 text-indigo-500" />{t('subagentManagement', 'Subagent Management')}
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">{t('manageSubagentsDesc', 'Manage subagents for this project')}</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white gap-2"
|
||||
onClick={() => {
|
||||
setEditingSubagent(null);
|
||||
setNewSubagent({ name: '', description: '', model: '', instructions: '', status: 'active' });
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4" />{t('addSubagent', 'Add Subagent')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4 md:p-8 bg-zinc-50/30">
|
||||
<div className="bg-white rounded-xl border border-zinc-200 shadow-sm overflow-hidden min-w-[800px] lg:min-w-0">
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader className="bg-zinc-50/50">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="w-[25%] font-semibold text-zinc-700 py-3 px-4 text-sm">{t('name')}</TableHead>
|
||||
<TableHead className="w-[25%] font-semibold text-zinc-700 py-3 px-4 text-sm">{t('modelName', 'Model')}</TableHead>
|
||||
<TableHead className="w-[35%] font-semibold text-zinc-700 py-3 px-4 text-sm">{t('description')}</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<>
|
||||
{subagents.map((subagent) => (
|
||||
<TableRow key={subagent.id} className="group hover:bg-zinc-50/50 transition-colors border-zinc-100">
|
||||
<TableCell className="py-4 px-4 overflow-hidden">
|
||||
<h3 className="font-bold text-zinc-900 text-sm md:text-base truncate flex-1" title={subagent.name}>
|
||||
{subagent.name}
|
||||
</h3>
|
||||
</TableCell>
|
||||
<TableCell className="py-4 px-4 text-zinc-600 text-sm truncate" title={getModelDisplay(subagent.model)}>
|
||||
{getModelDisplay(subagent.model)}
|
||||
</TableCell>
|
||||
<TableCell className="py-4 px-4 text-zinc-500 text-sm truncate" title={subagent.description}>
|
||||
{subagent.description || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="py-4 px-4 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-zinc-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-md transition-all shrink-0"
|
||||
onClick={() => handleEditSubagent(subagent)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-zinc-400 hover:text-rose-600 hover:bg-rose-50 rounded-md transition-all shrink-0"
|
||||
onClick={() => handleDeleteSubagent(subagent.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{subagents.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-24 text-center">
|
||||
<div className="flex flex-col items-center gap-3 text-zinc-400">
|
||||
<div className="p-4 bg-zinc-50 rounded-2xl">
|
||||
<Bot className="h-10 w-10 opacity-20" />
|
||||
</div>
|
||||
<p className="text-sm">{t('noSubagents', 'No subagents configured')}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={(open) => {
|
||||
setIsDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingSubagent(null);
|
||||
setNewSubagent({ name: '', description: '', model: '', instructions: '', status: 'active' });
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] flex flex-col rounded-2xl p-0 overflow-hidden">
|
||||
<DialogHeader className="p-6 pb-2">
|
||||
<DialogTitle className="text-xl font-bold text-zinc-900">
|
||||
{editingSubagent ? t('editSubagent', 'Edit Subagent') : t('addSubagent', 'Add Subagent')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-2">
|
||||
<div className="grid gap-5">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="name" className="text-zinc-600 font-medium text-sm">{t('name')} *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder={t('subagentName', 'Subagent Name')}
|
||||
value={newSubagent.name || ''}
|
||||
onChange={(e) => setNewSubagent({...newSubagent, name: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 h-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="model" className="text-zinc-600 font-medium text-sm">{t('modelName', 'Model')} *</Label>
|
||||
<Select
|
||||
value={newSubagent.model || ''}
|
||||
onValueChange={(v) => setNewSubagent({...newSubagent, model: v || undefined})}
|
||||
>
|
||||
<SelectTrigger className="w-full h-10 border-zinc-200 rounded-lg">
|
||||
<SelectValue placeholder={t('selectModel', 'Select a model')}>
|
||||
{newSubagent.model ? getModelDisplay(newSubagent.model) : undefined}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableModels.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id}>
|
||||
{m.name || m.model} <span className="text-xs text-zinc-400 ml-1">({m.provider})</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="description" className="text-zinc-600 font-medium text-sm">{t('description')}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder={t('descriptionOptional')}
|
||||
value={newSubagent.description || ''}
|
||||
onChange={(e) => setNewSubagent({...newSubagent, description: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 min-h-[80px] py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="instructions" className="text-zinc-600 font-medium text-sm">{t('instructions', 'System Instructions')}</Label>
|
||||
<Textarea
|
||||
id="instructions"
|
||||
value={newSubagent.instructions || ''}
|
||||
onChange={(e) => setNewSubagent({...newSubagent, instructions: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 font-mono text-xs min-h-[160px] py-3 bg-zinc-50"
|
||||
placeholder={t('systemInstructionsPlaceholder', 'You are a helpful AI assistant...')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="p-6 pt-2">
|
||||
<Button onClick={handleSaveSubagent} className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg px-6 h-10 w-full">
|
||||
{t('save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user