feat(system):优化后端

1.新增后端测试
2.增加了后端的加密
3.增加了i18n(国际化)
This commit is contained in:
2026-05-31 15:39:34 +00:00
parent affe460180
commit 99520c69d7
118 changed files with 8174 additions and 1491 deletions
+9
View File
@@ -1,4 +1,5 @@
import { useEffect } from 'react';
import i18n from './i18n';
import { TopBar } from './components/Layout/TopBar';
import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar';
import { SettingsLayout } from './components/Settings/SettingsLayout';
@@ -22,6 +23,7 @@ function App() {
workTab,
agentTab,
applyTheme,
locale,
} = useAppStore();
const { loadSessions } = useChatStore();
@@ -35,6 +37,13 @@ function App() {
return () => mediaQuery.removeEventListener('change', handler);
}, [applyTheme]);
// Sync persisted locale to i18next on mount
useEffect(() => {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale]);
// Check auth and load sessions
useEffect(() => {
const token = localStorage.getItem('token');
+4 -1
View File
@@ -10,12 +10,15 @@ export const apiClient = axios.create({
},
});
// Interceptor to attach token to requests if we have one
// Interceptor to attach token and locale to requests
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 把用户语言偏好透传给后端,让 Agent prompt 和错误消息都能本地化
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh';
config.headers['Accept-Language'] = lang;
return config;
});
@@ -1,9 +1,11 @@
import { useState, useEffect } from 'react';
import { Box, Plus, X, Server } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Box, Plus, X, Server, Loader2, Boxes } from 'lucide-react';
import type { Provider } from '../../types';
import apiClient from '../../api/client';
export function ProvidersSettings() {
const { t } = useTranslation();
const [providers, setProviders] = useState<Provider[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -29,7 +31,7 @@ export function ProvidersSettings() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.provider_title || !formData.provider_url || !formData.provider_apikey) {
setError('Please fill in all fields.');
setError(t('agent.providerFillAll'));
return;
}
setSubmitLoading(true);
@@ -39,7 +41,7 @@ export function ProvidersSettings() {
await fetchProviders();
setIsModalOpen(false);
} catch (err) {
setError('Failed to add provider. Please check your inputs and try again.');
setError(t('agent.providerAddFailed'));
} finally {
setSubmitLoading(false);
}
@@ -49,20 +51,24 @@ export function ProvidersSettings() {
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex justify-between items-end">
<div>
<h3 className="text-lg font-bold text-text-primary">Provider Management</h3>
<p className="text-sm text-text-muted mt-0.5">Configure external AI model providers</p>
<h3 className="text-lg font-bold text-text-primary">{t('agent.providerManagement')}</h3>
<p className="text-sm text-text-muted mt-0.5">{t('agent.providerDesc')}</p>
</div>
<button onClick={() => { setFormData({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); setError(''); setIsModalOpen(true); }}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
<Plus size={14} /> Add Provider
<Plus size={14} /> {t('agent.addProvider')}
</button>
</div>
{loading ? (
<div className="text-center text-text-muted py-12 text-sm">Loading providers...</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Loader2 size={24} className="animate-spin mb-3" />
<span className="text-sm">{t('common.loading')}</span>
</div>
) : providers.length === 0 ? (
<div className="text-center text-text-muted py-12 bg-bg-card rounded-2xl border border-border-primary border-dashed text-sm">
No providers configured yet
<div className="flex flex-col items-center justify-center py-12 bg-bg-card rounded-2xl border border-border-primary border-dashed text-text-muted">
<Boxes size={32} className="mb-3 opacity-40" />
<span className="text-sm">{t('agent.noProviders')}</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -80,19 +86,19 @@ export function ProvidersSettings() {
</div>
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
{provider.status === 'Connected' && <span className="w-1 h-1 rounded-full bg-success" />}
{provider.status || 'Unknown'}
{provider.status || t('common.unknown')}
</span>
</div>
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-4">
<p className="text-[10px] text-text-muted mb-0.5">Endpoint</p>
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || 'Default'}</p>
<p className="text-[10px] text-text-muted mb-0.5">{t('agent.endpoint')}</p>
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || t('common.default')}</p>
</div>
<div className="flex justify-end gap-2">
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">Edit</button>
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button>
<button onClick={async () => {
if (!confirm('Delete this provider?')) return;
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert('Failed'); }
}} className="px-3 py-1.5 text-xs font-medium text-danger bg-danger-bg hover:bg-danger-bg/80 rounded-lg transition-colors border border-danger/20">Delete</button>
if (!confirm(t('agent.deleteProviderConfirm'))) return;
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert(t('common.deleteFailed')); }
}} className="px-3 py-1.5 text-xs font-medium text-danger bg-danger-bg hover:bg-danger-bg/80 rounded-lg transition-colors border border-danger/20">{t('common.delete')}</button>
</div>
</div>
))}
@@ -106,14 +112,14 @@ export function ProvidersSettings() {
<div className="flex justify-between items-center p-5 border-b border-border-primary">
<div className="flex items-center gap-2">
<Server size={16} className="text-accent" />
<h3 className="text-base font-bold text-text-primary">Add New Provider</h3>
<h3 className="text-base font-bold text-text-primary">{t('agent.addNewProvider')}</h3>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Type</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.providerType')}</label>
<select name="provider_type" value={formData.provider_type} onChange={(e) => 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">
<option value="openai">OpenAI</option>
@@ -125,18 +131,18 @@ export function ProvidersSettings() {
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => (
<div key={field}>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{field === 'provider_title' ? 'Title' : field === 'provider_url' ? 'Base URL' : 'API Key'}
{field === 'provider_title' ? t('agent.providerTitle') : field === 'provider_url' ? t('agent.baseUrl') : t('agent.apiKey')}
</label>
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'}
name={field} value={(formData as any)[field]}
onChange={(e) => setFormData({...formData, [field]: e.target.value})}
placeholder={field === 'provider_title' ? 'My OpenAI' : field === 'provider_url' ? 'https://api.openai.com/v1' : 'sk-...'}
placeholder={field === 'provider_title' ? t('agent.providerTitlePlaceholder') : field === 'provider_url' ? t('agent.baseUrlPlaceholder') : t('agent.apiKeyPlaceholder')}
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" />
</div>
))}
<div className="pt-2 flex justify-end gap-2">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">Cancel</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? 'Saving...' : 'Add Provider'}</button>
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? t('common.saving') : t('agent.addProvider')}</button>
</div>
</form>
</div>
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import apiClient from '../../api/client';
import { Save, Plus, Edit2, Trash2, X, Bot } from 'lucide-react';
import { Save, Plus, Edit2, Trash2, X, Bot, Loader2, Users } from 'lucide-react';
import type { Provider } from '../../types';
interface WorkerIndividual {
@@ -18,6 +19,7 @@ interface WorkerIndividual {
}
export function WorkerIndividualSettings() {
const { t } = useTranslation();
const [providers, setProviders] = useState<Provider[]>([]);
const [workers, setWorkers] = useState<WorkerIndividual[]>([]);
const [systemNodes, setSystemNodes] = useState<any[]>([]);
@@ -62,7 +64,7 @@ export function WorkerIndividualSettings() {
};
}));
} catch (err: any) {
setError('Failed to load data');
setError(t('agent.loadFailed'));
} finally {
setLoading(false);
}
@@ -93,8 +95,8 @@ export function WorkerIndividualSettings() {
};
const handleDelete = async (agent_id: string) => {
if (!confirm('Delete this agent?')) return;
try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert('Failed'); }
if (!confirm(t('agent.deleteWorkerConfirm'))) return;
try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert(t('common.deleteFailed')); }
};
const handleModalSave = async (e: React.FormEvent) => {
@@ -123,31 +125,31 @@ export function WorkerIndividualSettings() {
setIsEditing(false);
fetchData();
} catch (err: any) {
setModalMessage(err.response?.data?.detail || err.message || 'Failed to save');
setModalMessage(err.response?.data?.detail || err.message || t('common.saveFailed'));
} finally {
setSubmitLoading(false);
}
};
const getTypeBadge = (type: string, isSystem?: boolean) => {
if (isSystem) return <span className="px-2 py-0.5 rounded-md text-[10px] font-bold bg-accent-light text-accent uppercase tracking-wider">System</span>;
if (isSystem) return <span className="px-2 py-0.5 rounded-md text-[10px] font-bold bg-accent-light text-accent uppercase tracking-wider">{t('agent.system')}</span>;
const colors: Record<string, string> = {
ordinary_individual: 'bg-bg-secondary text-text-muted',
skill_individual: 'bg-success-bg text-success',
special_individual: 'bg-warning-bg text-warning',
};
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{type.replace('_', ' ')}</span>;
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{t(`agent.type.${type}`, type.replace('_', ' '))}</span>;
};
return (
<div className="max-w-5xl space-y-6">
<div className="flex justify-between items-end">
<div>
<h1 className="text-lg font-bold text-text-primary">Individual</h1>
<p className="text-sm text-text-muted mt-0.5">Manage system nodes and custom workers</p>
<h1 className="text-lg font-bold text-text-primary">{t('agent.individual')}</h1>
<p className="text-sm text-text-muted mt-0.5">{t('agent.individualDesc')}</p>
</div>
<button onClick={handleAddNew} className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
<Plus size={14} /> Add Worker
<Plus size={14} /> {t('agent.addWorker')}
</button>
</div>
@@ -155,17 +157,23 @@ export function WorkerIndividualSettings() {
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
{loading ? (
<div className="p-6 text-text-muted text-sm">Loading...</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Loader2 size={24} className="animate-spin mb-3" />
<span className="text-sm">{t('common.loading')}</span>
</div>
) : (workers.length === 0 && systemNodes.length === 0) ? (
<div className="p-6 text-text-muted text-sm">No individuals found.</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Users size={32} className="mb-3 opacity-40" />
<span className="text-sm">{t('agent.noIndividuals')}</span>
</div>
) : (
<table className="w-full text-left text-sm">
<thead>
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
<th className="px-5 py-3 font-semibold">Name</th>
<th className="px-5 py-3 font-semibold">Type</th>
<th className="px-5 py-3 font-semibold">Provider / Model</th>
<th className="px-5 py-3 font-semibold text-right">Actions</th>
<th className="px-5 py-3 font-semibold">{t('agent.name')}</th>
<th className="px-5 py-3 font-semibold">{t('agent.type')}</th>
<th className="px-5 py-3 font-semibold">{t('agent.providerModel')}</th>
<th className="px-5 py-3 font-semibold text-right">{t('common.actions')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border-secondary">
@@ -212,47 +220,47 @@ export function WorkerIndividualSettings() {
{/* Modal */}
{isEditing && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary animate-fade-in-scale">
<div className="flex justify-between items-center p-5 border-b border-border-primary sticky top-0 bg-bg-card z-10">
<h2 className="text-base font-bold text-text-primary">{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}</h2>
<h2 className="text-base font-bold text-text-primary">{(editData as any).is_system ? t('agent.editSystemNode') : (isNew ? t('agent.createWorker') : t('agent.editWorker'))}</h2>
<button onClick={() => setIsEditing(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={20} /></button>
</div>
<form onSubmit={handleModalSave} className="p-5 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Name</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.name')}</label>
<input type="text" required value={editData.agent_name || ''} onChange={(e) => 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} />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Type</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
<select value={editData.agent_type || 'ordinary_individual'} onChange={(e) => 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}>
<option value="ordinary_individual">Ordinary</option>
<option value="skill_individual">Skill</option>
<option value="special_individual">Special</option>
{(editData as any).is_system && <option value="System Node">System Node</option>}
<option value="ordinary_individual">{t('agent.type.ordinary_individual')}</option>
<option value="skill_individual">{t('agent.type.skill_individual')}</option>
<option value="special_individual">{t('agent.type.special_individual')}</option>
{(editData as any).is_system && <option value="System Node">{t('agent.system')}</option>}
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Provider</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.provider')}</label>
<select value={editData.provider_title || ''} onChange={(e) => 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">
<option value="" disabled>Select</option>
<option value="" disabled>{t('common.select')}</option>
{providers.map((p) => (<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Model</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.model')}</label>
{(() => {
const sp = providers.find(p => p.provider_title === editData.provider_title);
const models = sp?.provider_models || [];
return (
<select value={editData.model_id || ''} onChange={(e) => 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">
<option value="" disabled>Select</option>
<option value="" disabled>{t('common.select')}</option>
{models.map(m => <option key={m} value={m}>{m}</option>)}
</select>
);
@@ -262,40 +270,40 @@ export function WorkerIndividualSettings() {
{!(editData as any).is_system && (
<>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Description</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.description')}</label>
<textarea value={editData.description || ''} onChange={(e) => setEditData({...editData, description: e.target.value})} rows={2}
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" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">System Prompt</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.systemPrompt')}</label>
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={3}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Output Template (JSON)</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.outputTemplate')}</label>
<textarea value={editData.output_template || '{}'} onChange={(e) => setEditData({...editData, output_template: e.target.value})} rows={3}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Bound Skill</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.boundSkill')}</label>
<select value={(() => { try { return Object.keys(JSON.parse(editData.bound_skill || '{}'))[0] || ''; } catch { return ''; } })()}
onChange={(e) => setEditData({...editData, bound_skill: JSON.stringify(e.target.value ? { [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.agent_type !== 'skill_individual'}>
<option value="">None</option>
<option value="">{t('common.none')}</option>
{availableSkills.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Workspace (JSON)</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.workspace')}</label>
<textarea value={editData.workspace || '[]'} onChange={(e) => setEditData({...editData, workspace: e.target.value})} rows={2}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
</>
)}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Tools</label>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tools')}</label>
<div className="flex flex-wrap gap-1.5 p-3 bg-bg-input border border-border-primary rounded-xl max-h-40 overflow-y-auto">
{availableTools.map(tool => {
let currentTools: string[] = [];
@@ -311,14 +319,14 @@ export function WorkerIndividualSettings() {
</button>
);
})}
{availableTools.length === 0 && <span className="text-xs text-text-muted">No tools</span>}
{availableTools.length === 0 && <span className="text-xs text-text-muted">{t('agent.noTools')}</span>}
</div>
</div>
{modalMessage && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{modalMessage}</div>}
<div className="pt-3 flex justify-end gap-2 border-t border-border-primary">
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">Cancel</button>
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
<button type="submit" disabled={submitLoading} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
<Save size={14} /> {submitLoading ? 'Saving...' : 'Save'}
<Save size={14} /> {submitLoading ? t('common.saving') : t('common.save')}
</button>
</div>
</form>
+4 -4
View File
@@ -42,7 +42,7 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
}
} catch (err: any) {
console.error(err);
setError(err.response?.data?.detail || err.response?.data?.message || 'Authentication failed');
setError(err.response?.data?.detail || err.response?.data?.message || t('auth.authFailed'));
} finally {
setLoading(false);
}
@@ -89,7 +89,7 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
value={userName}
onChange={(e) => setUserName(e.target.value)}
className="w-full px-4 py-2.5 bg-bg-input border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-text-primary placeholder:text-text-muted/60 text-sm"
placeholder="Enter your username"
placeholder={t('auth.usernamePlaceholder')}
required
/>
</div>
@@ -101,7 +101,7 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2.5 bg-bg-input border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-text-primary placeholder:text-text-muted/60 text-sm"
placeholder="Enter your password"
placeholder={t('auth.passwordPlaceholder')}
required
/>
</div>
@@ -137,7 +137,7 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
</div>
<p className="text-center text-xs text-text-muted/60 mt-6">
KiloStar Distributed Multi-Agent System
{t('app.tagline')}
</p>
</div>
</div>
+11 -11
View File
@@ -3,15 +3,15 @@ import { useTranslation } from 'react-i18next';
import { MessageSquare, Activity, ArrowUp, Plus, Sparkles, Code, FileText, Search } from 'lucide-react';
import { useChatStore } from '../../store/useChatStore';
const QUICK_ACTIONS = [
{ icon: Sparkles, label: ' brainstorm', prompt: '帮我头脑风暴一些创意' },
{ icon: Code, label: '写代码', prompt: '帮我写一段 Python 代码' },
{ icon: FileText, label: '总结文档', prompt: '帮我总结这篇文档' },
{ icon: Search, label: '查找资料', prompt: '帮我搜索相关资料' },
];
export function ChatPanel() {
const { t } = useTranslation();
const quickActions = [
{ icon: Sparkles, label: t('chat.quickActions.brainstorm'), prompt: '帮我头脑风暴一些创意' },
{ icon: Code, label: t('chat.quickActions.writeCode'), prompt: '帮我写一段 Python 代码' },
{ icon: FileText, label: t('chat.quickActions.summarize'), prompt: '帮我总结这篇文档' },
{ icon: Search, label: t('chat.quickActions.search'), prompt: '帮我搜索相关资料' },
];
const [input, setInput] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -79,7 +79,7 @@ export function ChatPanel() {
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-3 mb-8 w-full max-w-md">
{QUICK_ACTIONS.map((action) => (
{quickActions.map((action) => (
<button
key={action.label}
onClick={() => handleQuickAction(action.prompt)}
@@ -111,7 +111,7 @@ export function ChatPanel() {
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
<MessageSquare size={14} className="text-accent" />
</div>
<h1 className="font-semibold text-sm text-text-primary">{activeSession?.title || 'Chat'}</h1>
<h1 className="font-semibold text-sm text-text-primary">{activeSession?.title || t('chat.defaultTitle')}</h1>
</div>
</div>
@@ -177,7 +177,7 @@ export function ChatPanel() {
<button
onClick={() => fileInputRef.current?.click()}
className="p-2.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-xl transition-all flex-shrink-0 mb-0.5"
title="Add attachment"
title={t('chat.addAttachment')}
>
<Plus size={18} />
</button>
@@ -206,7 +206,7 @@ export function ChatPanel() {
</button>
</div>
<p className="text-center text-[10px] text-text-muted/50 mt-2">
KiloStar can make mistakes. Please verify important information.
{t('chat.mistakeWarning')}
</p>
</div>
</div>
+1 -1
View File
@@ -142,7 +142,7 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
<WorkflowIcon size={14} className={`flex-shrink-0 ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-muted'}`} />
<div className="flex-1 min-w-0">
<h3 className={`text-xs font-medium truncate ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-secondary'}`}>
{wf.title || 'Unnamed'}
{wf.title || t('common.unnamed')}
</h3>
<div className="flex items-center gap-1.5 mt-0.5">
<span className={`w-1.5 h-1.5 rounded-full ${
@@ -17,8 +17,8 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
const handleSubmit = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!title.trim()) { setError('请输入工作流标题'); return; }
if (!command.trim()) { setError('请输入具体需求描述'); return; }
if (!title.trim()) { setError(t('workflow.titleRequired')); return; }
if (!command.trim()) { setError(t('workflow.commandRequired')); return; }
setLoading(true);
setError('');
@@ -26,7 +26,7 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
const response = await apiClient.post('/api/v1/workflow', { title: title.trim(), command: command.trim() });
if (response.data?.trace_id) onSuccess(response.data.trace_id);
} catch (err: any) {
setError(err.response?.data?.message || '创建工作流失败,请重试');
setError(err.response?.data?.message || t('workflow.createFailed'));
} finally {
setLoading(false);
}
@@ -77,7 +77,7 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
rows={8}
className="w-full px-4 py-3 text-sm text-text-primary bg-bg-input border border-border-primary rounded-xl resize-none placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 transition-all" />
<div className="flex items-center justify-between mt-3">
<span className="text-[10px] text-text-muted">{command.length > 0 ? `${command.length} chars` : 'Ctrl + Enter to submit'}</span>
<span className="text-[10px] text-text-muted">{command.length > 0 ? `${command.length} ${t('workflow.chars')}` : t('workflow.submitHint')}</span>
<div className="flex items-center gap-2">
<button type="button" onClick={onClose} className="px-4 py-2 text-xs font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
{t('workflow.cancel')}
@@ -105,8 +105,8 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
<ArrowRight size={16} className="text-text-muted opacity-0 group-hover:opacity-100 transition-all group-hover:translate-x-0.5" />
</div>
<h3 className="text-base font-semibold text-text-primary mb-1 line-clamp-1" title={wf.title || 'Unnamed'}>
{wf.title || 'Unnamed Workflow'}
<h3 className="text-base font-semibold text-text-primary mb-1 line-clamp-1" title={wf.title || t('common.unnamed')}>
{wf.title || t('common.unnamed')}
</h3>
{wf.command && (
+3 -3
View File
@@ -80,7 +80,7 @@ export function TopBar() {
<button
onClick={toggleLanguage}
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all"
title={i18n.language.startsWith('zh') ? 'Switch to English' : '切换到中文'}
title={i18n.language.startsWith('zh') ? t('topbar.switchToEn') : t('topbar.switchToZh')}
>
<Globe size={16} />
</button>
@@ -88,7 +88,7 @@ export function TopBar() {
<button
onClick={toggleTheme}
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all"
title={resolvedTheme === 'dark' ? 'Light mode' : 'Dark mode'}
title={resolvedTheme === 'dark' ? t('topbar.lightMode') : t('topbar.darkMode')}
>
{resolvedTheme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
</button>
@@ -110,7 +110,7 @@ export function TopBar() {
<button
onClick={handleLogout}
className="p-2 rounded-lg text-text-muted hover:text-danger hover:bg-danger-bg transition-all ml-0.5"
title="Logout"
title={t('topbar.logout')}
>
<LogOut size={16} />
</button>
@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react';
import { Download, Trash2, Plus, Box, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Download, Trash2, Plus, Box, Sparkles, Loader2, Wand2 } from 'lucide-react';
import apiClient from '../../api/client';
export function SkillSettings() {
const { t } = useTranslation();
const [skills, setSkills] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [repoUrl, setRepoUrl] = useState('');
@@ -34,24 +36,24 @@ export function SkillSettings() {
setError('');
try {
await apiClient.post('/api/v1/resource/skill', { repo_url: repoUrl, path: path || null });
setMessage('Skill installed successfully');
setMessage(t('plugin.skillInstallSuccess'));
setRepoUrl('');
setPath('');
fetchSkills();
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to install skill');
setError(err.response?.data?.message || t('plugin.skillInstallFailed'));
} finally {
setInstalling(false);
}
};
const handleDelete = async (skillName: string) => {
if (!confirm(`Delete ${skillName}?`)) return;
if (!confirm(t('plugin.deleteSkillConfirm', { name: skillName }))) return;
try {
await apiClient.delete(`/api/v1/resource/skill/${skillName}`);
fetchSkills();
} catch {
alert('Failed to delete skill');
alert(t('plugin.skillDeleteFailed'));
}
};
@@ -60,9 +62,9 @@ export function SkillSettings() {
<div>
<div className="flex items-center gap-2 mb-1">
<Sparkles size={16} className="text-accent" />
<h1 className="text-lg font-bold text-text-primary">Skill Management</h1>
<h1 className="text-lg font-bold text-text-primary">{t('plugin.skillManagement')}</h1>
</div>
<p className="text-sm text-text-muted">Manage agent skills and functions</p>
<p className="text-sm text-text-muted">{t('plugin.skillDesc')}</p>
</div>
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
@@ -71,21 +73,21 @@ export function SkillSettings() {
<Download size={16} className="text-accent" />
</div>
<div>
<h2 className="text-sm font-bold text-text-primary">Install Skill</h2>
<p className="text-[11px] text-text-muted">Install from a repository</p>
<h2 className="text-sm font-bold text-text-primary">{t('plugin.installSkill')}</h2>
<p className="text-[11px] text-text-muted">{t('plugin.installSkillDesc')}</p>
</div>
</div>
<div className="p-6">
<form onSubmit={handleInstall} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Repository URL</label>
<input type="text" required value={repoUrl} onChange={(e) => setRepoUrl(e.target.value)} placeholder="https://github.com/user/repo"
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('plugin.repoUrl')}</label>
<input type="text" required value={repoUrl} onChange={(e) => setRepoUrl(e.target.value)} placeholder={t('plugin.repoUrlPlaceholder')}
className="w-full px-3.5 py-2.5 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 placeholder:text-text-muted/50" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Path (Optional)</label>
<input type="text" value={path} onChange={(e) => setPath(e.target.value)} placeholder="subfolder/path"
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('plugin.pathOptional')}</label>
<input type="text" value={path} onChange={(e) => setPath(e.target.value)} placeholder={t('plugin.pathPlaceholder')}
className="w-full px-3.5 py-2.5 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 placeholder:text-text-muted/50" />
</div>
</div>
@@ -95,7 +97,7 @@ export function SkillSettings() {
<button type="submit" disabled={installing}
className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium disabled:opacity-50">
<Plus size={14} />
{installing ? 'Installing...' : 'Install'}
{installing ? t('common.installing') : t('plugin.install')}
</button>
</div>
</form>
@@ -104,13 +106,19 @@ export function SkillSettings() {
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-border-primary">
<h2 className="text-sm font-bold text-text-primary">Installed Skills ({skills.length})</h2>
<h2 className="text-sm font-bold text-text-primary">{t('plugin.installedSkills', { count: skills.length })}</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-text-muted text-sm">Loading skills...</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Loader2 size={24} className="animate-spin mb-3" />
<span className="text-sm">{t('common.loading')}</span>
</div>
) : skills.length === 0 ? (
<div className="text-text-muted text-sm">No skills installed yet.</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Wand2 size={32} className="mb-3 opacity-40" />
<span className="text-sm">{t('plugin.noSkills')}</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{skills.map((skill) => (
@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react';
import { Package, Wrench } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Package, Wrench, Loader2, Box } from 'lucide-react';
import apiClient from '../../api/client';
export function ToolSettings() {
const { t } = useTranslation();
const [tools, setTools] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
@@ -25,23 +27,29 @@ export function ToolSettings() {
<div>
<div className="flex items-center gap-2 mb-1">
<Wrench size={16} className="text-accent" />
<h3 className="text-lg font-bold text-text-primary">Installed Tools</h3>
<h3 className="text-lg font-bold text-text-primary">{t('plugin.toolManagement')}</h3>
</div>
<p className="text-sm text-text-muted">Manage agent tools and functions</p>
<p className="text-sm text-text-muted">{t('plugin.toolDesc')}</p>
</div>
<div className="bg-bg-card border border-border-primary rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-border-primary flex justify-between items-center bg-bg-secondary">
<div>
<h4 className="text-sm font-bold text-text-primary">Available Tools</h4>
<p className="text-[11px] text-text-muted">Installed tools for agents</p>
<h4 className="text-sm font-bold text-text-primary">{t('plugin.availableTools')}</h4>
<p className="text-[11px] text-text-muted">{t('plugin.toolSubDesc')}</p>
</div>
</div>
<div className="p-6">
{loading ? (
<div className="text-text-muted text-sm">Loading tools...</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Loader2 size={24} className="animate-spin mb-3" />
<span className="text-sm">{t('common.loading')}</span>
</div>
) : tools.length === 0 ? (
<div className="text-text-muted text-sm">No tools installed yet.</div>
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Box size={32} className="mb-3 opacity-40" />
<span className="text-sm">{t('plugin.noTools')}</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{tools.map((tool) => (
@@ -5,14 +5,15 @@ import { useAppStore } from '../../store/useAppStore';
export function SystemSettings() {
const { t, i18n } = useTranslation();
const { theme, setTheme } = useAppStore();
const [localLang, setLocalLang] = useState(i18n.language.startsWith('zh') ? 'zh' : 'en');
const { theme, setTheme, locale, setLocale } = useAppStore();
const [localLang, setLocalLang] = useState(locale);
const [localTheme, setLocalTheme] = useState(theme);
const [debugMode, setDebugMode] = useState(true);
const [saved, setSaved] = useState(false);
const handleSave = () => {
i18n.changeLanguage(localLang);
setLocale(localLang);
setTheme(localTheme);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
@@ -22,7 +23,7 @@ export function SystemSettings() {
<div className="space-y-6">
<div>
<h3 className="text-lg font-bold text-text-primary">{t('settings.systemSettings')}</h3>
<p className="text-sm text-text-muted mt-0.5">Global platform configurations</p>
<p className="text-sm text-text-muted mt-0.5">{t('settings.systemSettingsDesc')}</p>
</div>
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
@@ -35,8 +36,8 @@ export function SystemSettings() {
<label className="block text-xs font-semibold text-text-secondary mb-2 uppercase tracking-wider">{t('settings.language')}</label>
<div className="flex gap-2">
{[
{ value: 'en', label: 'English' },
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: t('settings.langEnglish') },
{ value: 'zh', label: t('settings.langChinese') },
].map((opt) => (
<button
key={opt.value}
@@ -43,7 +43,7 @@ export function UsersSettings() {
setIsModalOpen(false);
setFormData({ username: '', password: '' });
} catch (err) {
setError('Registration failed. Please try again.');
setError(t('settings.registerFailed'));
} finally {
setSubmitLoading(false);
}
@@ -88,7 +88,7 @@ export function UsersSettings() {
<div className="flex justify-between items-end">
<div>
<h3 className="text-lg font-bold text-text-primary">{t('settings.userManagement')}</h3>
<p className="text-sm text-text-muted mt-0.5">Manage system users and their roles</p>
<p className="text-sm text-text-muted mt-0.5">{t('settings.userManagementDesc')}</p>
</div>
<button
onClick={() => { setFormData({ username: '', password: '' }); setError(''); setIsModalOpen(true); }}
@@ -123,7 +123,7 @@ export function UsersSettings() {
<td className="px-5 py-3.5">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-bg-secondary text-text-secondary border border-border-primary">
<Shield size={10} />
{user.role || 'User'}
{user.role || t('settings.roleUser')}
</span>
</td>
<td className="px-5 py-3.5">
@@ -143,7 +143,7 @@ export function UsersSettings() {
{users.length === 0 && (
<tr>
<td colSpan={4} className="px-5 py-8 text-center text-text-muted text-sm">
No users found
{t('settings.noUsers')}
</td>
</tr>
)}
@@ -178,7 +178,7 @@ export function UsersSettings() {
{t('settings.cancel')}
</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
{submitLoading ? 'Creating...' : t('settings.create')}
{submitLoading ? t('common.creating') : t('settings.create')}
</button>
</div>
</form>
@@ -215,7 +215,7 @@ export function UsersSettings() {
{t('settings.cancel')}
</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
{submitLoading ? 'Saving...' : t('settings.save')}
{submitLoading ? t('common.saving') : t('settings.save')}
</button>
</div>
</form>
+110 -6
View File
@@ -19,12 +19,15 @@
"signUpToStart": "Sign up to start using the platform",
"username": "Username",
"password": "Password",
"usernamePlaceholder": "Enter your username",
"passwordPlaceholder": "Enter your password",
"signIn": "Sign In",
"signUp": "Sign Up",
"noAccount": "Don't have an account?",
"hasAccount": "Already have an account?",
"processing": "Processing...",
"registerSuccess": "Registration successful. Please log in."
"registerSuccess": "Registration successful. Please log in.",
"authFailed": "Authentication failed, please check your credentials"
},
"chat": {
"newChat": "New Chat",
@@ -35,7 +38,16 @@
"send": "Send",
"selectChat": "Select a chat history or create a new one to start.",
"assistantName": "kilostar Assistant",
"errorCommunication": "Sorry, an error occurred while communicating with the server."
"errorCommunication": "Sorry, an error occurred while communicating with the server.",
"mistakeWarning": "KiloStar can make mistakes. Please verify important information.",
"addAttachment": "Add attachment",
"defaultTitle": "Chat",
"quickActions": {
"brainstorm": "Brainstorm",
"writeCode": "Write code",
"summarize": "Summarize doc",
"search": "Search info"
}
},
"workflow": {
"workflows": "Workflows",
@@ -66,6 +78,11 @@
"refresh": "Refresh Data",
"workflowDetails": "Workflow Details",
"loading": "Loading Workflows...",
"titleRequired": "Please enter a workflow title",
"commandRequired": "Please enter a detailed command",
"createFailed": "Failed to create workflow, please try again",
"chars": "chars",
"submitHint": "Ctrl + Enter to submit",
"status": {
"waiting": "Waiting",
"running": "Running",
@@ -104,13 +121,86 @@
"editUserRole": "Edit User Role",
"enterUsername": "e.g. jsmith",
"enterPassword": "Enter secure password",
"fillBoth": "Please fill in both username and password."
"fillBoth": "Please fill in both username and password.",
"registerFailed": "Registration failed. Please try again.",
"roleUser": "User",
"userManagementDesc": "Manage system users and their roles",
"noUsers": "No users found",
"systemSettingsDesc": "Global platform configurations",
"langEnglish": "English",
"langChinese": "简体中文"
},
"agent": {
"individual": "Individual",
"individualDesc": "Manage system nodes and custom workers",
"providerManagement": "Provider Management",
"skills": "Skills",
"tools": "Tools"
"providerDesc": "Configure external AI model providers",
"addProvider": "Add Provider",
"noProviders": "No providers configured yet",
"providerFillAll": "Please fill in all fields.",
"providerAddFailed": "Failed to add provider. Please check your inputs and try again.",
"deleteProviderConfirm": "Delete this provider?",
"providerType": "Type",
"providerTitle": "Title",
"baseUrl": "Base URL",
"apiKey": "API Key",
"providerTitlePlaceholder": "My OpenAI",
"baseUrlPlaceholder": "https://api.openai.com/v1",
"apiKeyPlaceholder": "sk-...",
"addNewProvider": "Add New Provider",
"endpoint": "Endpoint",
"addWorker": "Add Worker",
"loadFailed": "Failed to load data",
"deleteWorkerConfirm": "Delete this agent?",
"noIndividuals": "No individuals found.",
"name": "Name",
"type": "Type",
"providerModel": "Provider / Model",
"editSystemNode": "Edit System Node",
"createWorker": "Create Worker",
"editWorker": "Edit Worker",
"provider": "Provider",
"model": "Model",
"description": "Description",
"systemPrompt": "System Prompt",
"outputTemplate": "Output Template (JSON)",
"boundSkill": "Bound Skill",
"workspace": "Workspace (JSON)",
"tools": "Tools",
"noTools": "No tools",
"system": "System",
"type.ordinary_individual": "Ordinary",
"type.skill_individual": "Skill",
"type.special_individual": "Special"
},
"plugin": {
"toolManagement": "Tool Management",
"toolDesc": "Manage agent tools and functions",
"availableTools": "Available Tools",
"toolSubDesc": "Installed tools for agents",
"noTools": "No tools installed yet.",
"skillManagement": "Skill Management",
"skillDesc": "Manage agent skills and functions",
"installSkill": "Install Skill",
"installSkillDesc": "Install from a repository",
"repoUrl": "Repository URL",
"repoUrlPlaceholder": "https://github.com/user/repo",
"pathOptional": "Path (Optional)",
"pathPlaceholder": "subfolder/path",
"skillInstallSuccess": "Skill installed successfully",
"skillInstallFailed": "Failed to install skill",
"deleteSkillConfirm": "Delete {{name}}?",
"skillDeleteFailed": "Failed to delete skill",
"installedSkills": "Installed Skills ({{count}})",
"noSkills": "No skills installed yet.",
"install": "Install"
},
"topbar": {
"switchToEn": "Switch to English",
"switchToZh": "切换到中文",
"lightMode": "Light mode",
"darkMode": "Dark mode",
"logout": "Logout"
},
"common": {
"close": "Close",
@@ -118,6 +208,20 @@
"loading": "Loading...",
"error": "Error",
"success": "Success",
"back": "Back"
"back": "Back",
"unnamed": "Unnamed",
"unknown": "Unknown",
"default": "Default",
"edit": "Edit",
"delete": "Delete",
"deleteFailed": "Failed",
"saving": "Saving...",
"saveFailed": "Failed to save",
"installing": "Installing...",
"select": "Select",
"none": "None",
"creating": "Creating...",
"actions": "Actions",
"cancel": "Cancel"
}
}
+110 -6
View File
@@ -19,12 +19,15 @@
"signUpToStart": "注册以开始使用平台",
"username": "用户名",
"password": "密码",
"usernamePlaceholder": "请输入用户名",
"passwordPlaceholder": "请输入密码",
"signIn": "登录",
"signUp": "注册",
"noAccount": "还没有账号?",
"hasAccount": "已有账号?",
"processing": "处理中...",
"registerSuccess": "注册成功,请登录。"
"registerSuccess": "注册成功,请登录。",
"authFailed": "登录失败,请检查用户名和密码"
},
"chat": {
"newChat": "新对话",
@@ -35,7 +38,16 @@
"send": "发送",
"selectChat": "选择对话记录或创建新对话以开始",
"assistantName": "kilostar 助手",
"errorCommunication": "抱歉,与服务器通信时出错。"
"errorCommunication": "抱歉,与服务器通信时出错。",
"mistakeWarning": "KiloStar 可能会犯错,重要信息请自行核实。",
"addAttachment": "添加附件",
"defaultTitle": "新对话",
"quickActions": {
"brainstorm": "头脑风暴",
"writeCode": "写代码",
"summarize": "总结文档",
"search": "查找资料"
}
},
"workflow": {
"workflows": "工作流",
@@ -66,6 +78,11 @@
"refresh": "刷新数据",
"workflowDetails": "工作流详情",
"loading": "正在加载工作流...",
"titleRequired": "请输入工作流标题",
"commandRequired": "请输入具体需求描述",
"createFailed": "创建工作流失败,请重试",
"chars": "字符",
"submitHint": "Ctrl + Enter 发送",
"status": {
"waiting": "等待中",
"running": "运行中",
@@ -104,13 +121,86 @@
"editUserRole": "编辑用户角色",
"enterUsername": "例如:zhangsan",
"enterPassword": "输入安全密码",
"fillBoth": "请填写用户名和密码。"
"fillBoth": "请填写用户名和密码。",
"registerFailed": "注册失败,请重试。",
"roleUser": "用户",
"userManagementDesc": "管理系统用户及其角色",
"noUsers": "暂无用户",
"systemSettingsDesc": "全局平台配置",
"langEnglish": "English",
"langChinese": "简体中文"
},
"agent": {
"individual": "个体",
"individualDesc": "管理系统节点和自定义工作者",
"providerManagement": "供应商管理",
"skills": "技能",
"tools": "工具"
"providerDesc": "配置外部 AI 模型供应商",
"addProvider": "添加供应商",
"noProviders": "暂无已配置的供应商",
"providerFillAll": "请填写所有字段。",
"providerAddFailed": "添加供应商失败,请检查输入后重试。",
"deleteProviderConfirm": "确定要删除此供应商吗?",
"providerType": "类型",
"providerTitle": "名称",
"baseUrl": "基础 URL",
"apiKey": "API 密钥",
"providerTitlePlaceholder": "我的 OpenAI",
"baseUrlPlaceholder": "https://api.openai.com/v1",
"apiKeyPlaceholder": "sk-...",
"addNewProvider": "添加新供应商",
"endpoint": "接口",
"addWorker": "添加工作者",
"loadFailed": "加载数据失败",
"deleteWorkerConfirm": "确定要删除此代理吗?",
"noIndividuals": "暂无个体",
"name": "名称",
"type": "类型",
"providerModel": "供应商 / 模型",
"editSystemNode": "编辑系统节点",
"createWorker": "创建工作节点",
"editWorker": "编辑工作节点",
"provider": "供应商",
"model": "模型",
"description": "描述",
"systemPrompt": "系统提示词",
"outputTemplate": "输出模板 (JSON)",
"boundSkill": "绑定技能",
"workspace": "工作空间 (JSON)",
"tools": "工具",
"noTools": "暂无工具",
"system": "系统",
"type.ordinary_individual": "普通",
"type.skill_individual": "技能",
"type.special_individual": "特殊"
},
"plugin": {
"toolManagement": "工具管理",
"toolDesc": "管理代理工具和函数",
"availableTools": "可用工具",
"toolSubDesc": "已安装的工具",
"noTools": "暂无已安装的工具",
"skillManagement": "技能管理",
"skillDesc": "管理代理技能和函数",
"installSkill": "安装技能",
"installSkillDesc": "从代码仓库安装",
"repoUrl": "仓库地址",
"repoUrlPlaceholder": "https://github.com/user/repo",
"pathOptional": "路径(可选)",
"pathPlaceholder": "子文件夹/路径",
"skillInstallSuccess": "技能安装成功",
"skillInstallFailed": "技能安装失败",
"deleteSkillConfirm": "确定要删除技能 {{name}} 吗?",
"skillDeleteFailed": "删除技能失败",
"installedSkills": "已安装技能 ({{count}})",
"noSkills": "暂无已安装的技能",
"install": "安装"
},
"topbar": {
"switchToEn": "Switch to English",
"switchToZh": "切换到中文",
"lightMode": "浅色模式",
"darkMode": "深色模式",
"logout": "退出登录"
},
"common": {
"close": "关闭",
@@ -118,6 +208,20 @@
"loading": "加载中...",
"error": "错误",
"success": "成功",
"back": "返回"
"back": "返回",
"unnamed": "未命名",
"unknown": "未知",
"default": "默认",
"edit": "编辑",
"delete": "删除",
"deleteFailed": "删除失败",
"saving": "保存中...",
"saveFailed": "保存失败",
"installing": "安装中...",
"select": "请选择",
"none": "无",
"creating": "创建中...",
"actions": "操作",
"cancel": "取消"
}
}
+12
View File
@@ -40,6 +40,10 @@ interface AppState {
setTheme: (theme: ThemeMode) => void;
resolvedTheme: 'light' | 'dark';
applyTheme: () => void;
// Locale
locale: string;
setLocale: (locale: string) => void;
}
function resolveTheme(theme: ThemeMode): 'light' | 'dark' {
@@ -96,12 +100,20 @@ export const useAppStore = create<AppState>()(
document.documentElement.classList.remove('dark');
}
},
locale: 'zh',
setLocale: (locale) => {
set({ locale });
// 同步到 i18next,确保刷新后语言一致
import('i18next').then((i18n) => i18n.default.changeLanguage(locale));
},
}),
{
name: 'kilostar-app-storage',
partialize: (state) => ({
theme: state.theme,
isSidebarOpen: state.isSidebarOpen,
locale: state.locale,
}),
}
)