feat(system):优化后端
1.新增后端测试 2.增加了后端的加密 3.增加了i18n(国际化)
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "取消"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user