diff --git a/.env.template b/.env.template index b940b43..bb5c737 100644 --- a/.env.template +++ b/.env.template @@ -3,4 +3,7 @@ POSTGRES_PASSWORD=postgrespassword POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=5432 POSTGRES_DB=kilostar -SECRET_KEY=114514 \ No newline at end of file +# 必须填写一个高熵随机字符串,建议生成命令: +# python -c "import secrets; print(secrets.token_urlsafe(32))" +# 留空或填 "secret" / "114514" / "changethiskey12345" 等弱值会被拒绝。 +SECRET_KEY= diff --git a/README.md b/README.md index 049f89a..87409a2 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,7 @@ | 项目名称 | 代号 | 功能定位 | 当前状态 | |:-----------------------------------------------------------|:--------| :--- | :--- | | **[kilostar-viceroy](https://github.com/zhaoxi826/viceroy)** | **总督** | **资源管理**:负责系统 Skill 的动态安装、元数据解析与全集群分发。 | ✅ 已发布 | -| **kilostar-stardomain** | **星域** | **安全沙箱**:为 Agent 自动生成的代码提供轻量化的隔离运行环境,防止逃逸。 | 📅 规划中 | -| **kilostar-explorer** | **探索者** | **网页感知**:自动化爬虫引擎,赋予智能体实时互联网信息搜索与内容抓取能力。 | 📅 规划中 | -| **kilostar-pioneer** | **先驱者** | **知识增强**:RAG 检索增强引擎,管理私有知识库的向量化、索引与精准检索。 | 📅 规划中 | - +| **[kilostar-thought](https://github.com/zhaoxi826/thought)** | **思绪** | **记忆系统**:增强agent的记忆系统。 | 开发中 | --- ## 🚀 快速开始 (Quick Start) diff --git a/docker-compose.yml b/docker-compose.yml index 1e3b0c1..4f47305 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,10 @@ -version: '3.8' - services: db: image: postgres:16-alpine container_name: kilostar_db environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgrespassword + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgrespassword} POSTGRES_DB: kilostar ports: - "5432:5432" @@ -27,9 +25,8 @@ services: condition: service_healthy environment: - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgrespassword + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgrespassword} - POSTGRES_HOST=db - POSTGRES_PORT=5432 - POSTGRES_DB=kilostar - - SECRET_KEY=changethiskey12345 - + - SECRET_KEY=${SECRET_KEY:?SECRET_KEY must be set; generate one via: python -c \"import secrets;print(secrets.token_urlsafe(32))\"} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5031c55..8a2878a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,17 @@ "name": "kilostar-dashboard", "version": "0.0.0", "dependencies": { + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", "@xyflow/react": "^12.10.2", "axios": "^1.15.1", + "i18next": "^26.3.0", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.8.0", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "react-i18next": "^17.0.8", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -223,6 +229,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -462,6 +477,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1583,6 +1616,34 @@ "react-dom": ">=17" } }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@xyflow/system": { "version": "0.0.76", "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", @@ -2624,6 +2685,52 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.0.tgz", + "integrity": "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3370,6 +3477,33 @@ "react": "^19.2.5" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3572,7 +3706,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3741,6 +3875,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3811,20 +3954,18 @@ } }, "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3835,6 +3976,9 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } } diff --git a/frontend/package.json b/frontend/package.json index c82d908..38d2d3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", "@xyflow/react": "^12.10.2", "axios": "^1.15.1", + "i18next": "^26.3.0", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.8.0", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "react-i18next": "^17.0.8", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 905f8ec..5228b15 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useEffect } from 'react'; import { TopBar } from './components/Layout/TopBar'; import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar'; import { SettingsLayout } from './components/Settings/SettingsLayout'; @@ -10,162 +10,75 @@ import { RightPanel } from './components/Chat/RightPanel'; import { WorkflowListView } from './components/Chat/WorkflowListView'; import { NewWorkflowDialog } from './components/Chat/NewWorkflowDialog'; import { AuthPage } from './components/Auth/AuthPage'; -import apiClient from './api/client'; -import type { ChatSessionDB } from './types'; - -export interface Message { - id: string; - role: 'user' | 'assistant' | 'system'; - content: string; - timestamp: number; -} - -export interface ChatSession { - id: string; - title: string; - messages: Message[]; - updatedAt: number; -} - -function mapSessionFromDB(s: ChatSessionDB): ChatSession { - return { - id: s.chat_id, - title: s.title, - messages: [], - updatedAt: new Date(s.updated_at).getTime(), - }; -} +import { useAppStore } from './store/useAppStore'; +import { useChatStore } from './store/useChatStore'; function App() { - const [isAuthenticated, setIsAuthenticated] = useState(false); + const { + isAuthenticated, + setIsAuthenticated, + mode, + showSettings, + workTab, + agentTab, + applyTheme, + } = useAppStore(); - const [mode, setMode] = useState<'work' | 'agent'>('work'); - const [showSettings, setShowSettings] = useState(false); - const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const { loadSessions } = useChatStore(); - const [workTab, setWorkTab] = useState<'chat' | 'workflow'>('chat'); - const [selectedWorkflow, setSelectedWorkflow] = useState(null); - - const [agentTab, setAgentTab] = useState<'plugin' | 'agents'>('plugin'); - const [settingsTab, setSettingsTab] = useState('users'); - const [innerAgentTab, setInnerAgentTab] = useState('worker'); - const [resourceTab, setResourceTab] = useState('skill'); - - const [chatSessions, setChatSessions] = useState([]); - const [activeSessionId, setActiveSessionId] = useState(null); - - const loadChatSessions = useCallback(async () => { - try { - const response = await apiClient.get('/api/v1/chat'); - const sessions: ChatSessionDB[] = response.data?.sessions || []; - setChatSessions(sessions.map(mapSessionFromDB)); - } catch (error) { - console.error('Failed to load chat sessions', error); - } - }, []); + // Initialize theme on mount + useEffect(() => { + applyTheme(); + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handler = () => applyTheme(); + mediaQuery.addEventListener('change', handler); + return () => mediaQuery.removeEventListener('change', handler); + }, [applyTheme]); + // Check auth and load sessions useEffect(() => { const token = localStorage.getItem('token'); if (token) { setIsAuthenticated(true); } - }, []); + }, [setIsAuthenticated]); useEffect(() => { if (isAuthenticated) { - loadChatSessions(); + loadSessions(); } - }, [isAuthenticated, loadChatSessions]); + }, [isAuthenticated, loadSessions]); if (!isAuthenticated) { return setIsAuthenticated(true)} />; } return ( -
- +
+
{showSettings ? ( - + ) : ( <> - +
{mode === 'work' && workTab === 'chat' && ( -
-
- {}} - chatSessions={chatSessions} - setChatSessions={setChatSessions} - activeSessionId={activeSessionId} - setActiveSessionId={setActiveSessionId} - onSessionsChanged={loadChatSessions} - /> - +
+
+ +
)} - {mode === 'work' && workTab === 'workflow' && ( - <> - {selectedWorkflow === 'new' ? ( - <> - - setSelectedWorkflow(null)} - onSuccess={(traceId: string) => setSelectedWorkflow(traceId)} - /> - - ) : selectedWorkflow ? ( - <> - - - - ) : ( - - )} - - )} + {mode === 'work' && workTab === 'workflow' && } - {mode === 'agent' && agentTab === 'agents' && ( - - )} + {mode === 'agent' && agentTab === 'agents' && } - {mode === 'agent' && agentTab === 'plugin' && ( - - )} + {mode === 'agent' && agentTab === 'plugin' && }
)} @@ -174,4 +87,31 @@ function App() { ); } +function WorkflowShell() { + const { selectedWorkflow, setSelectedWorkflow } = useChatStore(); + + if (selectedWorkflow === 'new') { + return ( + <> + + setSelectedWorkflow(null)} + onSuccess={(traceId: string) => setSelectedWorkflow(traceId)} + /> + + ); + } + + if (selectedWorkflow) { + return ( + <> + + + + ); + } + + return ; +} + export default App; diff --git a/frontend/src/components/Agent/AgentLayout.tsx b/frontend/src/components/Agent/AgentLayout.tsx index 5c3f842..65d59e0 100644 --- a/frontend/src/components/Agent/AgentLayout.tsx +++ b/frontend/src/components/Agent/AgentLayout.tsx @@ -1,38 +1,37 @@ +import { useTranslation } from 'react-i18next'; +import { useAppStore } from '../../store/useAppStore'; import { ProvidersSettings } from './ProvidersSettings'; import { WorkerIndividualSettings } from './WorkerIndividualSettings'; -interface AgentLayoutProps { - agentTab: string; - setAgentTab: (tab: string) => void; -} +export function AgentLayout() { + const { t } = useTranslation(); + const { innerAgentTab, setInnerAgentTab } = useAppStore(); + + const tabs = [ + { key: 'worker', label: t('agent.individual') }, + { key: 'providers', label: t('agent.providerManagement') }, + ]; -export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) { return ( -
- {/* Top Tabs for Agent Module */} -
- - +
+
+ {tabs.map((tab) => ( + + ))}
- - {/* Main Content */}
- {agentTab === 'worker' && } - {agentTab === 'providers' && } + {innerAgentTab === 'worker' && } + {innerAgentTab === 'providers' && }
); diff --git a/frontend/src/components/Agent/ProvidersSettings.tsx b/frontend/src/components/Agent/ProvidersSettings.tsx index d3bd6e0..3d5e8df 100644 --- a/frontend/src/components/Agent/ProvidersSettings.tsx +++ b/frontend/src/components/Agent/ProvidersSettings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Box, Plus, X } from 'lucide-react'; +import { Box, Plus, X, Server } from 'lucide-react'; import type { Provider } from '../../types'; import apiClient from '../../api/client'; @@ -7,12 +7,7 @@ export function ProvidersSettings() { const [providers, setProviders] = useState([]); const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); - const [formData, setFormData] = useState({ - provider_type: 'openai', - provider_title: '', - provider_url: '', - provider_apikey: '' - }); + const [formData, setFormData] = useState({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); const [submitLoading, setSubmitLoading] = useState(false); const [error, setError] = useState(''); @@ -20,9 +15,7 @@ export function ProvidersSettings() { setLoading(true); try { const response = await apiClient.get('/api/v1/provider/list'); - const data = response.data.provider_list || {}; - const providerArray: Provider[] = Object.values(data); - setProviders(providerArray); + setProviders(Object.values(response.data.provider_list || {})); } catch (error) { console.error("Failed to fetch providers", error); setProviders([]); @@ -31,32 +24,7 @@ export function ProvidersSettings() { } }; - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - fetchProviders(); - }, []); - - const handleOpenModal = () => { - setFormData({ - provider_type: 'openai', - provider_title: '', - provider_url: '', - provider_apikey: '' - }); - setError(''); - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setIsModalOpen(false); - }; - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value - }); - }; + useEffect(() => { fetchProviders(); }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -64,15 +32,13 @@ export function ProvidersSettings() { setError('Please fill in all fields.'); return; } - setSubmitLoading(true); setError(''); try { await apiClient.post('/api/v1/provider', formData); await fetchProviders(); - handleCloseModal(); + setIsModalOpen(false); } catch (err) { - console.error("Error adding provider", err); setError('Failed to add provider. Please check your inputs and try again.'); } finally { setSubmitLoading(false); @@ -80,174 +46,97 @@ export function ProvidersSettings() { }; return ( -
-
+
+
-

Provider Management

-

Configure external AI model providers and API keys.

+

Provider Management

+

Configure external AI model providers

-
{loading ? ( -
Loading providers...
+
Loading providers...
) : providers.length === 0 ? ( -
- No providers configured yet. Click "Add Provider" to get started. +
+ No providers configured yet
) : ( -
+
{providers.map((provider, i) => ( -
-
-
-
-
- -
-
-

{provider.provider_title}

- {provider.provider_type} -
+
+
+
+
+
- - {provider.status === 'Connected' && } - {provider.status || 'Unknown'} - -
-
-

URL / Endpoint:

-
- {provider.provider_url || 'Default'} +
+

{provider.provider_title}

+ {provider.provider_type}
+ + {provider.status === 'Connected' && } + {provider.status || 'Unknown'} +
-
- - +
+

Endpoint

+

{provider.provider_url || 'Default'}

+
+
+ +
))}
)} - {/* Add Provider Modal */} + {/* Modal */} {isModalOpen && ( -
-
-
-

Add New Provider

- +
+
+
+
+ +

Add New Provider

+
+
- -
- {error && ( -
- {error} -
- )} - + + {error &&
{error}
}
- - setFormData({...formData, provider_type: e.target.value})} + className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary">
- -
- - -
- -
- - -
- -
- - -
- -
- - + {['provider_title', 'provider_url', 'provider_apikey'].map((field) => ( +
+ + setFormData({...formData, [field]: e.target.value})} + placeholder={field === 'provider_title' ? 'My OpenAI' : field === 'provider_url' ? 'https://api.openai.com/v1' : 'sk-...'} + className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono" /> +
+ ))} +
+ +
diff --git a/frontend/src/components/Agent/WorkerIndividualSettings.tsx b/frontend/src/components/Agent/WorkerIndividualSettings.tsx index ce74a69..f670bb7 100644 --- a/frontend/src/components/Agent/WorkerIndividualSettings.tsx +++ b/frontend/src/components/Agent/WorkerIndividualSettings.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import apiClient from '../../api/client'; -import { Save, Plus, Edit2, Trash2, X } from 'lucide-react'; +import { Save, Plus, Edit2, Trash2, X, Bot } from 'lucide-react'; import type { Provider } from '../../types'; interface WorkerIndividual { @@ -11,10 +11,10 @@ interface WorkerIndividual { provider_title: string; model_id: string; system_prompt?: string; - output_template?: string; // Change to string for the form state - bound_skill?: string; // Change to string for the form state - workspace?: string; // Change to string for the form state - tools?: string; // Form state for tools JSON array + output_template?: string; + bound_skill?: string; + workspace?: string; + tools?: string; } export function WorkerIndividualSettings() { @@ -25,12 +25,11 @@ export function WorkerIndividualSettings() { const [availableTools, setAvailableTools] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const [isEditing, setIsEditing] = useState(false); const [editData, setEditData] = useState>({}); const [isNew, setIsNew] = useState(false); - const [modalMessage, setModalMessage] = useState(''); + const [submitLoading, setSubmitLoading] = useState(false); const fetchData = async () => { setLoading(true); @@ -44,43 +43,34 @@ export function WorkerIndividualSettings() { ]); setProviders(Object.values(provRes.data.provider_list || {})); setWorkers(workRes.data.workers || []); - - const allTools = toolsRes.data.tools || []; - setAvailableTools(allTools); + setAvailableTools(toolsRes.data.tools || []); setAvailableSkills(Object.keys(skillsRes.data.skills || {})); - const sysNodesData = sysRes.data.system_nodes || []; - const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node']; - const providersList = Object.values(provRes.data.provider_list || {}) as Provider[]; const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : ''; + const sysNodesData = sysRes.data.system_nodes || []; + const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node']; - const formattedSysNodes = defaultSysNodes.map(nodeName => { + setSystemNodes(defaultSysNodes.map(nodeName => { const found = sysNodesData.find((n: any) => n.node_name === nodeName); return { - agent_id: nodeName, - agent_name: nodeName, - agent_type: 'System Node', - provider_title: found && found.provider_title ? found.provider_title : defaultProvider, - model_id: found && found.model_id ? found.model_id : '', - tools: found && found.tools ? JSON.stringify(found.tools) : '[]', + agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node', + provider_title: found?.provider_title || defaultProvider, + model_id: found?.model_id || '', + tools: found?.tools ? JSON.stringify(found.tools) : '[]', is_system: true }; - }); - setSystemNodes(formattedSysNodes); + })); } catch (err: any) { - console.error(err); setError('Failed to load data'); } finally { setLoading(false); } }; - useEffect(() => { - fetchData(); - }, []); + useEffect(() => { fetchData(); }, []); - const handleEdit = (worker: any) => { // Accept the backend object which might have objects instead of strings + const handleEdit = (worker: any) => { setEditData({ ...worker, output_template: typeof worker.output_template === 'string' ? worker.output_template : JSON.stringify(worker.output_template || {}), @@ -94,46 +84,31 @@ export function WorkerIndividualSettings() { }; const handleAddNew = () => { - setEditData({ - agent_name: '', - agent_type: 'ordinary_individual', - description: '', - provider_title: providers.length > 0 ? providers[0].provider_title : '', - model_id: '', - system_prompt: '', - output_template: '{}', - bound_skill: '{}', - workspace: '[]', - tools: '[]' - }); + setEditData({ agent_name: '', agent_type: 'ordinary_individual', description: '', + provider_title: providers.length > 0 ? providers[0].provider_title : '', model_id: '', + system_prompt: '', output_template: '{}', bound_skill: '{}', workspace: '[]', tools: '[]' }); setIsNew(true); setIsEditing(true); setModalMessage(''); }; const handleDelete = async (agent_id: string) => { - if (!confirm('Are you sure you want to delete this agent?')) return; - try { - await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); - fetchData(); - } catch (err: any) { - console.error(err); - alert('Failed to delete agent'); - } + if (!confirm('Delete this agent?')) return; + try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert('Failed'); } }; const handleModalSave = async (e: React.FormEvent) => { e.preventDefault(); setModalMessage(''); + setSubmitLoading(true); try { if ((editData as any).is_system) { - const payload = { + await apiClient.post('/api/v1/agent', { individual_name: editData.agent_name, provider_title: editData.provider_title, model_id: editData.model_id, tools: JSON.parse(editData.tools || '[]') - }; - await apiClient.post('/api/v1/agent', payload); + }); } else { const payload = { ...editData, @@ -142,305 +117,208 @@ export function WorkerIndividualSettings() { workspace: JSON.parse(editData.workspace || '[]'), tools: JSON.parse(editData.tools || '[]') }; - - if (isNew) { - await apiClient.post('/api/v1/agent/worker', payload); - } else { - await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload); - } + if (isNew) await apiClient.post('/api/v1/agent/worker', payload); + else await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload); } - setIsEditing(false); fetchData(); } catch (err: any) { - console.error(err); setModalMessage(err.response?.data?.detail || err.message || 'Failed to save'); + } finally { + setSubmitLoading(false); } }; + const getTypeBadge = (type: string, isSystem?: boolean) => { + if (isSystem) return System; + const colors: Record = { + ordinary_individual: 'bg-bg-secondary text-text-muted', + skill_individual: 'bg-success-bg text-success', + special_individual: 'bg-warning-bg text-warning', + }; + return {type.replace('_', ' ')}; + }; + return ( -
-
+
+
-

Individual

-

Manage all system nodes and custom workers.

+

Individual

+

Manage system nodes and custom workers

-
- {error &&
{error}
} + {error &&
{error}
} -
-
- {loading ? ( -
Loading...
- ) : (workers.length === 0 && systemNodes.length === 0) ? ( -
No individuals found.
- ) : ( - - - - - - - +
+ {loading ? ( +
Loading...
+ ) : (workers.length === 0 && systemNodes.length === 0) ? ( +
No individuals found.
+ ) : ( +
NameTypeProvider / Model IDActions
+ + + + + + + + + + {systemNodes.map((w) => ( + + + + + - - - {systemNodes.map((w) => ( - - - - - - - ))} - {workers.map((w) => ( - - - - - - - ))} - -
NameTypeProvider / ModelActions
+
+
+ +
+ {w.agent_name} +
+
{getTypeBadge(w.agent_type, true)}{w.provider_title} / {w.model_id} + +
{w.agent_name} - {w.agent_type} - - {w.provider_title} / {w.model_id} - - -
{w.agent_name} - {w.agent_type} - - {w.provider_title} / {w.model_id} - - - -
- )} -
+ ))} + {workers.map((w) => ( + + +
+
+ +
+ {w.agent_name} +
+ + {getTypeBadge(w.agent_type)} + {w.provider_title} / {w.model_id} + + + + + + ))} + + + )}
- {/* Edit/Create Modal */} + {/* Modal */} {isEditing && (
-
-
-

- {(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')} -

- +
+
+

{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}

+
- -
+
- - setEditData({...editData, agent_name: e.target.value})} - className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500" - disabled={(editData as any).is_system} - /> + + setEditData({...editData, agent_name: e.target.value})} + className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system} />
- - setEditData({...editData, agent_type: e.target.value})} + className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system}> + + + + {(editData as any).is_system && }
-
- - setEditData({...editData, provider_title: e.target.value, model_id: ''})} required + className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"> + + {providers.map((p) => ())}
- + {(() => { - const selectedProvider = providers.find(p => p.provider_title === editData.provider_title); - const models = selectedProvider?.provider_models || []; + const sp = providers.find(p => p.provider_title === editData.provider_title); + const models = sp?.provider_models || []; return ( - setEditData({...editData, model_id: e.target.value})} required + className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"> + {models.map(m => )} ); })()}
- {!(editData as any).is_system && ( <>
- -