Merge branch 'dev' into issue-3153-codex-mcp-config
This commit is contained in:
commit
e17afefdfa
6
.github/workflows/pub-release.yml
vendored
6
.github/workflows/pub-release.yml
vendored
@ -202,7 +202,7 @@ jobs:
|
||||
include:
|
||||
# Keep GNU Linux release artifacts on Ubuntu 22.04 to preserve
|
||||
# a broadly compatible GLIBC baseline for user distributions.
|
||||
- os: ubuntu-22.04
|
||||
- os: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2204]
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact: zeroclaw
|
||||
archive_ext: tar.gz
|
||||
@ -217,7 +217,7 @@ jobs:
|
||||
linker_env: ""
|
||||
linker: ""
|
||||
use_cross: true
|
||||
- os: ubuntu-22.04
|
||||
- os: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2204]
|
||||
target: aarch64-unknown-linux-gnu
|
||||
artifact: zeroclaw
|
||||
archive_ext: tar.gz
|
||||
@ -232,7 +232,7 @@ jobs:
|
||||
linker_env: ""
|
||||
linker: ""
|
||||
use_cross: true
|
||||
- os: ubuntu-22.04
|
||||
- os: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2204]
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
artifact: zeroclaw
|
||||
archive_ext: tar.gz
|
||||
|
||||
@ -12,7 +12,7 @@ import Cost from './pages/Cost';
|
||||
import Logs from './pages/Logs';
|
||||
import Doctor from './pages/Doctor';
|
||||
import { AuthProvider, useAuth } from './hooks/useAuth';
|
||||
import { setLocale, type Locale } from './lib/i18n';
|
||||
import { setLocale, t, useLocale, type Locale } from './lib/i18n';
|
||||
|
||||
// Locale context
|
||||
interface LocaleContextType {
|
||||
@ -21,12 +21,25 @@ interface LocaleContextType {
|
||||
}
|
||||
|
||||
export const LocaleContext = createContext<LocaleContextType>({
|
||||
locale: 'tr',
|
||||
locale: 'en',
|
||||
setAppLocale: (_locale: Locale) => {},
|
||||
});
|
||||
|
||||
export const useLocaleContext = () => useContext(LocaleContext);
|
||||
|
||||
const LOCALE_STORAGE_KEY = 'zeroclaw.web.locale';
|
||||
|
||||
function readStoredLocale(): Locale | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
|
||||
return stored === 'en' || stored === 'tr' || stored === 'zh-CN'
|
||||
? stored
|
||||
: null;
|
||||
}
|
||||
|
||||
// Pairing dialog component
|
||||
function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> }) {
|
||||
const [code, setCode] = useState('');
|
||||
@ -40,7 +53,7 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
|
||||
try {
|
||||
await onPair(code);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Pairing failed');
|
||||
setError(err instanceof Error ? err.message : t('auth.pairing_failed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -51,14 +64,14 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
|
||||
<div className="bg-gray-900 rounded-xl p-8 w-full max-w-md border border-gray-800">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">ZeroClaw</h1>
|
||||
<p className="text-gray-400">Enter the pairing code from your terminal</p>
|
||||
<p className="text-gray-400">{t('auth.enter_code_terminal')}</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="6-digit code"
|
||||
placeholder={t('auth.code_placeholder')}
|
||||
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-center text-2xl tracking-widest focus:outline-none focus:border-blue-500 mb-4"
|
||||
maxLength={6}
|
||||
autoFocus
|
||||
@ -71,7 +84,7 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
|
||||
disabled={loading || code.length < 6}
|
||||
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
{loading ? 'Pairing...' : 'Pair'}
|
||||
{loading ? t('auth.pairing_progress') : t('auth.pair_button')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -81,9 +94,29 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
|
||||
|
||||
function AppContent() {
|
||||
const { isAuthenticated, loading, pair, logout } = useAuth();
|
||||
const [locale, setLocaleState] = useState<Locale>('tr');
|
||||
const { locale: detectedLocale } = useLocale();
|
||||
const [locale, setLocaleState] = useState<Locale>(() => readStoredLocale() ?? detectedLocale);
|
||||
const [hasStoredLocale, setHasStoredLocale] = useState(() => readStoredLocale() !== null);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasStoredLocale) {
|
||||
const storedLocale = readStoredLocale();
|
||||
if (storedLocale) {
|
||||
setLocaleState(storedLocale);
|
||||
setLocale(storedLocale);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setLocaleState(detectedLocale);
|
||||
setLocale(detectedLocale);
|
||||
}, [detectedLocale, hasStoredLocale]);
|
||||
|
||||
const setAppLocale = (newLocale: Locale) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
|
||||
}
|
||||
setHasStoredLocale(true);
|
||||
setLocaleState(newLocale);
|
||||
setLocale(newLocale);
|
||||
};
|
||||
@ -100,7 +133,7 @@ function AppContent() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||
<p className="text-gray-400">Connecting...</p>
|
||||
<p className="text-gray-400">{t('agent.connecting')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,6 +37,15 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'dashboard.overview': 'Overview',
|
||||
'dashboard.system_info': 'System Information',
|
||||
'dashboard.quick_actions': 'Quick Actions',
|
||||
'dashboard.provider_model': 'Provider / Model',
|
||||
'dashboard.since_restart': 'Since last restart',
|
||||
'dashboard.cost_overview': 'Cost Overview',
|
||||
'dashboard.active_channels': 'Active Channels',
|
||||
'dashboard.no_channels': 'No channels configured',
|
||||
'dashboard.component_health': 'Component Health',
|
||||
'dashboard.no_components': 'No components reporting',
|
||||
'dashboard.restarts': 'Restarts: {count}',
|
||||
'dashboard.load_error': 'Failed to load dashboard: {error}',
|
||||
|
||||
// Agent / Chat
|
||||
'agent.title': 'Agent Chat',
|
||||
@ -49,6 +58,17 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'agent.thinking': 'Thinking...',
|
||||
'agent.tool_call': 'Tool Call',
|
||||
'agent.tool_result': 'Tool Result',
|
||||
'agent.empty_title': 'ZeroClaw Agent',
|
||||
'agent.empty_subtitle': 'Send a message to start the conversation',
|
||||
'agent.connection_error': 'Connection error. Attempting to reconnect...',
|
||||
'agent.send_failed': 'Failed to send message. Please try again.',
|
||||
'agent.done_without_text': 'Tool execution completed, but no final response text was returned.',
|
||||
'agent.tool_call_message': '[Tool Call] {name}({args})',
|
||||
'agent.tool_result_message': '[Tool Result] {output}',
|
||||
'agent.error_message': '[Error] {message}',
|
||||
'agent.typing': 'Typing...',
|
||||
'agent.unknown_tool': 'unknown',
|
||||
'agent.unknown_error': 'Unknown error',
|
||||
|
||||
// Tools
|
||||
'tools.title': 'Available Tools',
|
||||
@ -58,6 +78,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'tools.search': 'Search tools...',
|
||||
'tools.empty': 'No tools available.',
|
||||
'tools.count': 'Total tools',
|
||||
'tools.agent_tools': 'Agent Tools',
|
||||
'tools.cli_tools': 'CLI Tools',
|
||||
'tools.parameter_schema': 'Parameter Schema',
|
||||
'tools.no_match': 'No tools match your search.',
|
||||
'tools.load_error': 'Failed to load tools: {error}',
|
||||
|
||||
// Cron
|
||||
'cron.title': 'Scheduled Jobs',
|
||||
@ -74,6 +99,18 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'cron.enabled': 'Enabled',
|
||||
'cron.empty': 'No scheduled jobs.',
|
||||
'cron.confirm_delete': 'Are you sure you want to delete this job?',
|
||||
'cron.load_error': 'Failed to load cron jobs: {error}',
|
||||
'cron.required_fields': 'Schedule and command are required.',
|
||||
'cron.add_error': 'Failed to add job',
|
||||
'cron.delete_error': 'Failed to delete job',
|
||||
'cron.modal_title': 'Add Cron Job',
|
||||
'cron.name_optional': 'Name (optional)',
|
||||
'cron.name_placeholder': 'e.g. Daily cleanup',
|
||||
'cron.schedule_placeholder': 'e.g. 0 0 * * * (cron expression)',
|
||||
'cron.command_placeholder': 'e.g. cleanup --older-than 7d',
|
||||
'cron.adding': 'Adding...',
|
||||
'cron.empty_configured': 'No scheduled tasks configured.',
|
||||
'cron.delete_prompt': 'Delete?',
|
||||
|
||||
// Integrations
|
||||
'integrations.title': 'Integrations',
|
||||
@ -86,6 +123,58 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'integrations.empty': 'No integrations found.',
|
||||
'integrations.activate': 'Activate',
|
||||
'integrations.deactivate': 'Deactivate',
|
||||
'integrations.load_error': 'Failed to load integrations: {error}',
|
||||
'integrations.current_provider': 'current provider',
|
||||
'integrations.custom_model_hint_scoped': 'Format: anthropic/claude-sonnet-4-6',
|
||||
'integrations.custom_model_hint_generic': 'Format: claude-sonnet-4-6 (or provider/model when required)',
|
||||
'integrations.field_required': '{field} is required.',
|
||||
'integrations.custom_value_required':
|
||||
'Enter a custom value for {field} or choose a recommended model.',
|
||||
'integrations.no_changes': 'No changes to save.',
|
||||
'integrations.confirm_switch_provider':
|
||||
'Switch default AI provider from {current} to {target}?',
|
||||
'integrations.confirm_switch_provider_with_model':
|
||||
'Switch default AI provider from {current} to {target} and set model to {model}?',
|
||||
'integrations.credentials_saved': '{name} credentials saved.',
|
||||
'integrations.save_error': 'Failed to save credentials',
|
||||
'integrations.stale_save':
|
||||
'Configuration changed elsewhere. Refreshed latest settings; re-enter values and save again.',
|
||||
'integrations.model_updated': 'Model updated to {model} for {name}.',
|
||||
'integrations.update_model_error': 'Failed to update model',
|
||||
'integrations.stale_model':
|
||||
'Configuration changed elsewhere. Refreshed latest settings; choose the model again.',
|
||||
'integrations.default_summary': 'default: {model}',
|
||||
'integrations.default_only': 'default',
|
||||
'integrations.default_badge': 'Default',
|
||||
'integrations.configured_badge': 'Configured',
|
||||
'integrations.current_model': 'Current model',
|
||||
'integrations.quick_model_help': 'For custom model IDs, use Edit Keys.',
|
||||
'integrations.default_provider_configured': 'Default provider configured',
|
||||
'integrations.provider_configured': 'Provider configured',
|
||||
'integrations.credentials_configured': 'Credentials configured',
|
||||
'integrations.credentials_not_configured': 'Credentials not configured',
|
||||
'integrations.edit_keys': 'Edit Keys',
|
||||
'integrations.configure': 'Configure',
|
||||
'integrations.configure_title': 'Configure {name}',
|
||||
'integrations.configure_intro_update': 'Enter only fields you want to update.',
|
||||
'integrations.configure_intro_new': 'Enter required fields to configure this integration.',
|
||||
'integrations.default_provider_notice_prefix':
|
||||
'Saving here updates credentials and switches your default AI provider to',
|
||||
'integrations.default_provider_notice_suffix':
|
||||
'For advanced provider settings, use',
|
||||
'integrations.keep_current_model': 'Keep current model',
|
||||
'integrations.keep_current_model_with_value': 'Keep current model ({model})',
|
||||
'integrations.select_recommended_model': 'Select a recommended model',
|
||||
'integrations.custom_model': 'Custom model...',
|
||||
'integrations.clear_current_model': 'Clear current model',
|
||||
'integrations.pick_model_help':
|
||||
'Pick a recommended model or choose Custom model. {hint}.',
|
||||
'integrations.current_value': 'Current value:',
|
||||
'integrations.replace_current_placeholder': 'Enter a new value to replace current',
|
||||
'integrations.enter_value_placeholder': 'Enter value',
|
||||
'integrations.keep_current_placeholder': 'Type new value, or leave empty to keep current',
|
||||
'integrations.save_activate': 'Save & Activate',
|
||||
'integrations.save_keys': 'Save Keys',
|
||||
|
||||
// Memory
|
||||
'memory.title': 'Memory Store',
|
||||
@ -101,6 +190,16 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'memory.empty': 'No memory entries found.',
|
||||
'memory.confirm_delete': 'Are you sure you want to delete this memory entry?',
|
||||
'memory.all_categories': 'All Categories',
|
||||
'memory.load_error': 'Failed to load memory: {error}',
|
||||
'memory.required_fields': 'Key and content are required.',
|
||||
'memory.store_error': 'Failed to store memory',
|
||||
'memory.delete_error': 'Failed to delete memory',
|
||||
'memory.add_button': 'Add Memory',
|
||||
'memory.key_placeholder': 'e.g. user_preferences',
|
||||
'memory.content_placeholder': 'Memory content...',
|
||||
'memory.category_optional': 'Category (optional)',
|
||||
'memory.category_placeholder': 'e.g. preferences, context, facts',
|
||||
'memory.delete_prompt': 'Delete?',
|
||||
|
||||
// Config
|
||||
'config.title': 'Configuration',
|
||||
@ -110,6 +209,10 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'config.error': 'Failed to save configuration.',
|
||||
'config.loading': 'Loading configuration...',
|
||||
'config.editor_placeholder': 'TOML configuration...',
|
||||
'config.sensitive_title': 'Sensitive fields are masked',
|
||||
'config.sensitive_body':
|
||||
'API keys, tokens, and passwords are hidden for security. To update a masked field, replace the entire masked value with your new value.',
|
||||
'config.editor_title': 'TOML Configuration',
|
||||
|
||||
// Cost
|
||||
'cost.title': 'Cost Tracker',
|
||||
@ -123,6 +226,14 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'cost.tokens': 'Tokens',
|
||||
'cost.requests': 'Requests',
|
||||
'cost.usd': 'Cost (USD)',
|
||||
'cost.load_error': 'Failed to load cost data: {error}',
|
||||
'cost.total_requests': 'Total Requests',
|
||||
'cost.token_statistics': 'Token Statistics',
|
||||
'cost.average_tokens_per_request': 'Avg Tokens / Request',
|
||||
'cost.cost_per_1k_tokens': 'Cost per 1K Tokens',
|
||||
'cost.model_breakdown': 'Model Breakdown',
|
||||
'cost.no_models': 'No model data available.',
|
||||
'cost.share': 'Share',
|
||||
|
||||
// Logs
|
||||
'logs.title': 'Live Logs',
|
||||
@ -133,6 +244,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'logs.empty': 'No log entries.',
|
||||
'logs.connected': 'Connected to event stream.',
|
||||
'logs.disconnected': 'Disconnected from event stream.',
|
||||
'logs.jump_to_bottom': 'Jump to bottom',
|
||||
'logs.events': 'events',
|
||||
'logs.filter_label': 'Filter:',
|
||||
'logs.paused_empty': 'Log streaming is paused.',
|
||||
'logs.waiting_empty': 'Waiting for events...',
|
||||
|
||||
// Doctor
|
||||
'doctor.title': 'System Diagnostics',
|
||||
@ -146,6 +262,13 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'doctor.message': 'Message',
|
||||
'doctor.empty': 'No diagnostics have been run yet.',
|
||||
'doctor.summary': 'Diagnostic Summary',
|
||||
'doctor.run_error': 'Failed to run diagnostics',
|
||||
'doctor.running_short': 'Running...',
|
||||
'doctor.may_take_seconds': 'This may take a few seconds.',
|
||||
'doctor.issues_found': 'Issues Found',
|
||||
'doctor.warnings': 'Warnings',
|
||||
'doctor.all_clear': 'All Clear',
|
||||
'doctor.empty_help': 'Click "Run Diagnostics" to check your ZeroClaw installation.',
|
||||
|
||||
// Auth / Pairing
|
||||
'auth.pair': 'Pair Device',
|
||||
@ -155,6 +278,9 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'auth.pairing_success': 'Pairing successful!',
|
||||
'auth.pairing_failed': 'Pairing failed. Please try again.',
|
||||
'auth.enter_code': 'Enter your pairing code to connect to the agent.',
|
||||
'auth.enter_code_terminal': 'Enter the pairing code from your terminal',
|
||||
'auth.code_placeholder': '6-digit code',
|
||||
'auth.pairing_progress': 'Pairing...',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Loading...',
|
||||
@ -178,6 +304,27 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'common.status': 'Status',
|
||||
'common.created': 'Created',
|
||||
'common.updated': 'Updated',
|
||||
'common.saving': 'Saving...',
|
||||
'common.adding': 'Adding...',
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
'common.active': 'Active',
|
||||
'common.inactive': 'Inactive',
|
||||
'common.optional': 'optional',
|
||||
'common.id': 'ID',
|
||||
'common.path': 'Path',
|
||||
'common.version': 'Version',
|
||||
'common.all': 'All',
|
||||
'common.unknown': 'Unknown',
|
||||
'common.current': 'Current',
|
||||
'common.current_value': 'Current value',
|
||||
'common.apply': 'Apply',
|
||||
'common.configured': 'Configured',
|
||||
'common.configure': 'Configure',
|
||||
'common.default': 'Default',
|
||||
'common.events': 'events',
|
||||
'common.lines': 'lines',
|
||||
'common.select': 'Select',
|
||||
|
||||
// Health
|
||||
'health.title': 'System Health',
|
||||
@ -220,6 +367,15 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'dashboard.overview': 'Genel Bakis',
|
||||
'dashboard.system_info': 'Sistem Bilgisi',
|
||||
'dashboard.quick_actions': 'Hizli Islemler',
|
||||
'dashboard.provider_model': 'Saglayici / Model',
|
||||
'dashboard.since_restart': 'Son yeniden baslatmadan beri',
|
||||
'dashboard.cost_overview': 'Maliyet Ozeti',
|
||||
'dashboard.active_channels': 'Aktif Kanallar',
|
||||
'dashboard.no_channels': 'Yapilandirilmis kanal yok',
|
||||
'dashboard.component_health': 'Bilesen Sagligi',
|
||||
'dashboard.no_components': 'Raporlayan bilesen yok',
|
||||
'dashboard.restarts': 'Yeniden baslatmalar: {count}',
|
||||
'dashboard.load_error': 'Kontrol paneli yuklenemedi: {error}',
|
||||
|
||||
// Agent / Chat
|
||||
'agent.title': 'Ajan Sohbet',
|
||||
@ -232,6 +388,17 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'agent.thinking': 'Dusunuyor...',
|
||||
'agent.tool_call': 'Arac Cagrisi',
|
||||
'agent.tool_result': 'Arac Sonucu',
|
||||
'agent.empty_title': 'ZeroClaw Ajan',
|
||||
'agent.empty_subtitle': 'Sohbeti baslatmak icin bir mesaj gonderin',
|
||||
'agent.connection_error': 'Baglanti hatasi. Yeniden baglanmayi deniyor...',
|
||||
'agent.send_failed': 'Mesaj gonderilemedi. Lutfen tekrar deneyin.',
|
||||
'agent.done_without_text': 'Arac calismasi tamamlandi ancak son yanit metni dondurulmedi.',
|
||||
'agent.tool_call_message': '[Arac Cagrisi] {name}({args})',
|
||||
'agent.tool_result_message': '[Arac Sonucu] {output}',
|
||||
'agent.error_message': '[Hata] {message}',
|
||||
'agent.typing': 'Yaziyor...',
|
||||
'agent.unknown_tool': 'bilinmeyen',
|
||||
'agent.unknown_error': 'Bilinmeyen hata',
|
||||
|
||||
// Tools
|
||||
'tools.title': 'Mevcut Araclar',
|
||||
@ -241,6 +408,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'tools.search': 'Arac ara...',
|
||||
'tools.empty': 'Mevcut arac yok.',
|
||||
'tools.count': 'Toplam arac',
|
||||
'tools.agent_tools': 'Ajan Araclari',
|
||||
'tools.cli_tools': 'CLI Araclari',
|
||||
'tools.parameter_schema': 'Parametre Semasi',
|
||||
'tools.no_match': 'Aramanizla eslesen arac yok.',
|
||||
'tools.load_error': 'Araclar yuklenemedi: {error}',
|
||||
|
||||
// Cron
|
||||
'cron.title': 'Zamanlanmis Gorevler',
|
||||
@ -257,6 +429,18 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'cron.enabled': 'Etkin',
|
||||
'cron.empty': 'Zamanlanmis gorev yok.',
|
||||
'cron.confirm_delete': 'Bu gorevi silmek istediginizden emin misiniz?',
|
||||
'cron.load_error': 'Zamanlanmis gorevler yuklenemedi: {error}',
|
||||
'cron.required_fields': 'Zamanlama ve komut gereklidir.',
|
||||
'cron.add_error': 'Gorev eklenemedi',
|
||||
'cron.delete_error': 'Gorev silinemedi',
|
||||
'cron.modal_title': 'Cron Gorevi Ekle',
|
||||
'cron.name_optional': 'Ad (istege bagli)',
|
||||
'cron.name_placeholder': 'ornegin Gunluk temizleme',
|
||||
'cron.schedule_placeholder': 'ornegin 0 0 * * * (cron ifadesi)',
|
||||
'cron.command_placeholder': 'ornegin cleanup --older-than 7d',
|
||||
'cron.adding': 'Ekleniyor...',
|
||||
'cron.empty_configured': 'Yapilandirilmis zamanlanmis gorev yok.',
|
||||
'cron.delete_prompt': 'Silinsin mi?',
|
||||
|
||||
// Integrations
|
||||
'integrations.title': 'Entegrasyonlar',
|
||||
@ -269,6 +453,60 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'integrations.empty': 'Entegrasyon bulunamadi.',
|
||||
'integrations.activate': 'Etkinlestir',
|
||||
'integrations.deactivate': 'Devre Disi Birak',
|
||||
'integrations.load_error': 'Entegrasyonlar yuklenemedi: {error}',
|
||||
'integrations.current_provider': 'mevcut saglayici',
|
||||
'integrations.custom_model_hint_scoped': 'Format: anthropic/claude-sonnet-4-6',
|
||||
'integrations.custom_model_hint_generic':
|
||||
'Format: claude-sonnet-4-6 (gerektiginde provider/model)',
|
||||
'integrations.field_required': '{field} gereklidir.',
|
||||
'integrations.custom_value_required':
|
||||
'{field} icin ozel bir deger girin veya onerilen bir model secin.',
|
||||
'integrations.no_changes': 'Kaydedilecek degisiklik yok.',
|
||||
'integrations.confirm_switch_provider':
|
||||
'Varsayilan AI saglayicisini {current} konumundan {target} konumuna gecirmek istiyor musunuz?',
|
||||
'integrations.confirm_switch_provider_with_model':
|
||||
'Varsayilan AI saglayicisini {current} konumundan {target} konumuna gecirip modeli {model} olarak ayarlamak istiyor musunuz?',
|
||||
'integrations.credentials_saved': '{name} kimlik bilgileri kaydedildi.',
|
||||
'integrations.save_error': 'Kimlik bilgileri kaydedilemedi',
|
||||
'integrations.stale_save':
|
||||
'Yapilandirma baska yerde degisti. En guncel ayarlar yenilendi; degerleri yeniden girip tekrar kaydedin.',
|
||||
'integrations.model_updated': '{name} icin model {model} olarak guncellendi.',
|
||||
'integrations.update_model_error': 'Model guncellenemedi',
|
||||
'integrations.stale_model':
|
||||
'Yapilandirma baska yerde degisti. En guncel ayarlar yenilendi; modeli yeniden secin.',
|
||||
'integrations.default_summary': 'varsayilan: {model}',
|
||||
'integrations.default_only': 'varsayilan',
|
||||
'integrations.default_badge': 'Varsayilan',
|
||||
'integrations.configured_badge': 'Yapilandirildi',
|
||||
'integrations.current_model': 'Guncel model',
|
||||
'integrations.quick_model_help': 'Ozel model kimlikleri icin Anahtarlari Duzenle secenegini kullanin.',
|
||||
'integrations.default_provider_configured': 'Varsayilan saglayici yapilandirildi',
|
||||
'integrations.provider_configured': 'Saglayici yapilandirildi',
|
||||
'integrations.credentials_configured': 'Kimlik bilgileri yapilandirildi',
|
||||
'integrations.credentials_not_configured': 'Kimlik bilgileri yapilandirilmadi',
|
||||
'integrations.edit_keys': 'Anahtarlari Duzenle',
|
||||
'integrations.configure': 'Yapilandir',
|
||||
'integrations.configure_title': '{name} Yapilandir',
|
||||
'integrations.configure_intro_update': 'Yalnizca guncellemek istediginiz alanlari girin.',
|
||||
'integrations.configure_intro_new': 'Bu entegrasyonu yapilandirmak icin gerekli alanlari girin.',
|
||||
'integrations.default_provider_notice_prefix':
|
||||
'Burada kaydetmek kimlik bilgilerini gunceller ve varsayilan AI saglayicinizi su olarak degistirir:',
|
||||
'integrations.default_provider_notice_suffix':
|
||||
'Gelismis saglayici ayarlari icin sunu kullanin:',
|
||||
'integrations.keep_current_model': 'Guncel modeli koru',
|
||||
'integrations.keep_current_model_with_value': 'Guncel modeli koru ({model})',
|
||||
'integrations.select_recommended_model': 'Onerilen bir model secin',
|
||||
'integrations.custom_model': 'Ozel model...',
|
||||
'integrations.clear_current_model': 'Guncel modeli temizle',
|
||||
'integrations.pick_model_help':
|
||||
'Onerilen bir model secin veya Ozel model secenegini kullanin. {hint}.',
|
||||
'integrations.current_value': 'Guncel deger:',
|
||||
'integrations.replace_current_placeholder': 'Gunceli degistirmek icin yeni bir deger girin',
|
||||
'integrations.enter_value_placeholder': 'Deger girin',
|
||||
'integrations.keep_current_placeholder':
|
||||
'Yeni deger yazin veya gunceli korumak icin bos birakin',
|
||||
'integrations.save_activate': 'Kaydet ve Etkinlestir',
|
||||
'integrations.save_keys': 'Anahtarlari Kaydet',
|
||||
|
||||
// Memory
|
||||
'memory.title': 'Hafiza Deposu',
|
||||
@ -284,6 +522,16 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'memory.empty': 'Hafiza kaydi bulunamadi.',
|
||||
'memory.confirm_delete': 'Bu hafiza kaydini silmek istediginizden emin misiniz?',
|
||||
'memory.all_categories': 'Tum Kategoriler',
|
||||
'memory.load_error': 'Hafiza yuklenemedi: {error}',
|
||||
'memory.required_fields': 'Anahtar ve icerik gereklidir.',
|
||||
'memory.store_error': 'Hafiza kaydedilemedi',
|
||||
'memory.delete_error': 'Hafiza silinemedi',
|
||||
'memory.add_button': 'Hafiza Ekle',
|
||||
'memory.key_placeholder': 'ornegin user_preferences',
|
||||
'memory.content_placeholder': 'Hafiza icerigi...',
|
||||
'memory.category_optional': 'Kategori (istege bagli)',
|
||||
'memory.category_placeholder': 'ornegin preferences, context, facts',
|
||||
'memory.delete_prompt': 'Silinsin mi?',
|
||||
|
||||
// Config
|
||||
'config.title': 'Yapilandirma',
|
||||
@ -293,6 +541,10 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'config.error': 'Yapilandirma kaydedilemedi.',
|
||||
'config.loading': 'Yapilandirma yukleniyor...',
|
||||
'config.editor_placeholder': 'TOML yapilandirmasi...',
|
||||
'config.sensitive_title': 'Hassas alanlar maskelenir',
|
||||
'config.sensitive_body':
|
||||
'Guvenlik icin API anahtarlari, tokenlar ve parolalar gizlenir. Maskeli bir alani guncellemek icin tum maskeli degeri yeni degerinizle degistirin.',
|
||||
'config.editor_title': 'TOML Yapilandirmasi',
|
||||
|
||||
// Cost
|
||||
'cost.title': 'Maliyet Takibi',
|
||||
@ -306,6 +558,14 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'cost.tokens': 'Token',
|
||||
'cost.requests': 'Istekler',
|
||||
'cost.usd': 'Maliyet (USD)',
|
||||
'cost.load_error': 'Maliyet verileri yuklenemedi: {error}',
|
||||
'cost.total_requests': 'Toplam Istek',
|
||||
'cost.token_statistics': 'Token Istatistikleri',
|
||||
'cost.average_tokens_per_request': 'Istek Basina Ort. Token',
|
||||
'cost.cost_per_1k_tokens': '1K Token Basina Maliyet',
|
||||
'cost.model_breakdown': 'Model Dagilimi',
|
||||
'cost.no_models': 'Model verisi yok.',
|
||||
'cost.share': 'Pay',
|
||||
|
||||
// Logs
|
||||
'logs.title': 'Canli Kayitlar',
|
||||
@ -316,6 +576,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'logs.empty': 'Kayit girisi yok.',
|
||||
'logs.connected': 'Olay akisina baglandi.',
|
||||
'logs.disconnected': 'Olay akisi baglantisi kesildi.',
|
||||
'logs.jump_to_bottom': 'En alta git',
|
||||
'logs.events': 'olay',
|
||||
'logs.filter_label': 'Filtre:',
|
||||
'logs.paused_empty': 'Kayit akisi duraklatildi.',
|
||||
'logs.waiting_empty': 'Olaylar bekleniyor...',
|
||||
|
||||
// Doctor
|
||||
'doctor.title': 'Sistem Teshisleri',
|
||||
@ -329,6 +594,13 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'doctor.message': 'Mesaj',
|
||||
'doctor.empty': 'Henuz teshis calistirilmadi.',
|
||||
'doctor.summary': 'Teshis Ozeti',
|
||||
'doctor.run_error': 'Teshisler calistirilamadi',
|
||||
'doctor.running_short': 'Calisiyor...',
|
||||
'doctor.may_take_seconds': 'Bu islem birkac saniye surebilir.',
|
||||
'doctor.issues_found': 'Sorunlar Bulundu',
|
||||
'doctor.warnings': 'Uyarilar',
|
||||
'doctor.all_clear': 'Her Sey Yolunda',
|
||||
'doctor.empty_help': 'ZeroClaw kurulumunuzu kontrol etmek icin "Teshis Calistir" dugmesine tiklayin.',
|
||||
|
||||
// Auth / Pairing
|
||||
'auth.pair': 'Cihaz Esle',
|
||||
@ -338,6 +610,9 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'auth.pairing_success': 'Eslestirme basarili!',
|
||||
'auth.pairing_failed': 'Eslestirme basarisiz. Lutfen tekrar deneyin.',
|
||||
'auth.enter_code': 'Ajana baglanmak icin eslestirme kodunuzu girin.',
|
||||
'auth.enter_code_terminal': 'Terminalinizdeki eslestirme kodunu girin',
|
||||
'auth.code_placeholder': '6 haneli kod',
|
||||
'auth.pairing_progress': 'Eslestiriliyor...',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Yukleniyor...',
|
||||
@ -361,6 +636,27 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'common.status': 'Durum',
|
||||
'common.created': 'Olusturulma',
|
||||
'common.updated': 'Guncellenme',
|
||||
'common.saving': 'Kaydediliyor...',
|
||||
'common.adding': 'Ekleniyor...',
|
||||
'common.enabled': 'Etkin',
|
||||
'common.disabled': 'Devre disi',
|
||||
'common.active': 'Aktif',
|
||||
'common.inactive': 'Pasif',
|
||||
'common.optional': 'istege bagli',
|
||||
'common.id': 'Kimlik',
|
||||
'common.path': 'Yol',
|
||||
'common.version': 'Surum',
|
||||
'common.all': 'Tum',
|
||||
'common.unknown': 'Bilinmeyen',
|
||||
'common.current': 'Guncel',
|
||||
'common.current_value': 'Guncel deger',
|
||||
'common.apply': 'Uygula',
|
||||
'common.configured': 'Yapilandirildi',
|
||||
'common.configure': 'Yapilandir',
|
||||
'common.default': 'Varsayilan',
|
||||
'common.events': 'olay',
|
||||
'common.lines': 'satir',
|
||||
'common.select': 'Sec',
|
||||
|
||||
// Health
|
||||
'health.title': 'Sistem Sagligi',
|
||||
@ -403,6 +699,15 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'dashboard.overview': '总览',
|
||||
'dashboard.system_info': '系统信息',
|
||||
'dashboard.quick_actions': '快捷操作',
|
||||
'dashboard.provider_model': '提供商 / 模型',
|
||||
'dashboard.since_restart': '自上次重启以来',
|
||||
'dashboard.cost_overview': '成本总览',
|
||||
'dashboard.active_channels': '活跃渠道',
|
||||
'dashboard.no_channels': '未配置任何渠道',
|
||||
'dashboard.component_health': '组件健康状态',
|
||||
'dashboard.no_components': '没有组件上报状态',
|
||||
'dashboard.restarts': '重启次数:{count}',
|
||||
'dashboard.load_error': '加载仪表盘失败:{error}',
|
||||
|
||||
// Agent / Chat
|
||||
'agent.title': '智能体聊天',
|
||||
@ -415,6 +720,17 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'agent.thinking': '思考中...',
|
||||
'agent.tool_call': '工具调用',
|
||||
'agent.tool_result': '工具结果',
|
||||
'agent.empty_title': 'ZeroClaw 智能体',
|
||||
'agent.empty_subtitle': '发送一条消息以开始对话',
|
||||
'agent.connection_error': '连接出错,正在尝试重新连接...',
|
||||
'agent.send_failed': '发送消息失败,请重试。',
|
||||
'agent.done_without_text': '工具执行已完成,但未返回最终响应文本。',
|
||||
'agent.tool_call_message': '[工具调用] {name}({args})',
|
||||
'agent.tool_result_message': '[工具结果] {output}',
|
||||
'agent.error_message': '[错误] {message}',
|
||||
'agent.typing': '输入中...',
|
||||
'agent.unknown_tool': '未知',
|
||||
'agent.unknown_error': '未知错误',
|
||||
|
||||
// Tools
|
||||
'tools.title': '可用工具',
|
||||
@ -424,6 +740,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'tools.search': '搜索工具...',
|
||||
'tools.empty': '暂无可用工具。',
|
||||
'tools.count': '工具总数',
|
||||
'tools.agent_tools': '智能体工具',
|
||||
'tools.cli_tools': 'CLI 工具',
|
||||
'tools.parameter_schema': '参数结构',
|
||||
'tools.no_match': '没有工具符合你的搜索。',
|
||||
'tools.load_error': '加载工具失败:{error}',
|
||||
|
||||
// Cron
|
||||
'cron.title': '定时任务',
|
||||
@ -440,6 +761,18 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'cron.enabled': '已启用',
|
||||
'cron.empty': '暂无定时任务。',
|
||||
'cron.confirm_delete': '确定要删除此任务吗?',
|
||||
'cron.load_error': '加载定时任务失败:{error}',
|
||||
'cron.required_fields': '计划和命令为必填项。',
|
||||
'cron.add_error': '添加任务失败',
|
||||
'cron.delete_error': '删除任务失败',
|
||||
'cron.modal_title': '添加 Cron 任务',
|
||||
'cron.name_optional': '名称(可选)',
|
||||
'cron.name_placeholder': '例如:每日清理',
|
||||
'cron.schedule_placeholder': '例如:0 0 * * *(cron 表达式)',
|
||||
'cron.command_placeholder': '例如:cleanup --older-than 7d',
|
||||
'cron.adding': '添加中...',
|
||||
'cron.empty_configured': '尚未配置定时任务。',
|
||||
'cron.delete_prompt': '删除?',
|
||||
|
||||
// Integrations
|
||||
'integrations.title': '集成',
|
||||
@ -452,6 +785,55 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'integrations.empty': '未找到集成。',
|
||||
'integrations.activate': '激活',
|
||||
'integrations.deactivate': '停用',
|
||||
'integrations.load_error': '加载集成失败:{error}',
|
||||
'integrations.current_provider': '当前提供商',
|
||||
'integrations.custom_model_hint_scoped': '格式:anthropic/claude-sonnet-4-6',
|
||||
'integrations.custom_model_hint_generic':
|
||||
'格式:claude-sonnet-4-6(需要时使用 provider/model)',
|
||||
'integrations.field_required': '{field} 为必填项。',
|
||||
'integrations.custom_value_required': '请为 {field} 输入自定义值,或选择推荐模型。',
|
||||
'integrations.no_changes': '没有可保存的更改。',
|
||||
'integrations.confirm_switch_provider': '要将默认 AI 提供商从 {current} 切换到 {target} 吗?',
|
||||
'integrations.confirm_switch_provider_with_model':
|
||||
'要将默认 AI 提供商从 {current} 切换到 {target},并将模型设置为 {model} 吗?',
|
||||
'integrations.credentials_saved': '{name} 凭据已保存。',
|
||||
'integrations.save_error': '保存凭据失败',
|
||||
'integrations.stale_save':
|
||||
'配置已在其他地方发生变化。已刷新最新设置;请重新输入值后再次保存。',
|
||||
'integrations.model_updated': '{name} 的模型已更新为 {model}。',
|
||||
'integrations.update_model_error': '更新模型失败',
|
||||
'integrations.stale_model':
|
||||
'配置已在其他地方发生变化。已刷新最新设置;请重新选择模型。',
|
||||
'integrations.default_summary': '默认:{model}',
|
||||
'integrations.default_only': '默认',
|
||||
'integrations.default_badge': '默认',
|
||||
'integrations.configured_badge': '已配置',
|
||||
'integrations.current_model': '当前模型',
|
||||
'integrations.quick_model_help': '如需自定义模型 ID,请使用“编辑密钥”。',
|
||||
'integrations.default_provider_configured': '默认提供商已配置',
|
||||
'integrations.provider_configured': '提供商已配置',
|
||||
'integrations.credentials_configured': '凭据已配置',
|
||||
'integrations.credentials_not_configured': '凭据未配置',
|
||||
'integrations.edit_keys': '编辑密钥',
|
||||
'integrations.configure': '配置',
|
||||
'integrations.configure_title': '配置 {name}',
|
||||
'integrations.configure_intro_update': '只输入你想更新的字段。',
|
||||
'integrations.configure_intro_new': '输入必填字段以配置此集成。',
|
||||
'integrations.default_provider_notice_prefix':
|
||||
'在此保存会更新凭据,并将你的默认 AI 提供商切换为',
|
||||
'integrations.default_provider_notice_suffix': '如需高级提供商设置,请使用',
|
||||
'integrations.keep_current_model': '保留当前模型',
|
||||
'integrations.keep_current_model_with_value': '保留当前模型({model})',
|
||||
'integrations.select_recommended_model': '选择推荐模型',
|
||||
'integrations.custom_model': '自定义模型...',
|
||||
'integrations.clear_current_model': '清除当前模型',
|
||||
'integrations.pick_model_help': '请选择推荐模型或使用自定义模型。{hint}。',
|
||||
'integrations.current_value': '当前值:',
|
||||
'integrations.replace_current_placeholder': '输入新值以替换当前值',
|
||||
'integrations.enter_value_placeholder': '输入值',
|
||||
'integrations.keep_current_placeholder': '输入新值,或留空以保留当前值',
|
||||
'integrations.save_activate': '保存并激活',
|
||||
'integrations.save_keys': '保存密钥',
|
||||
|
||||
// Memory
|
||||
'memory.title': '记忆存储',
|
||||
@ -467,6 +849,16 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'memory.empty': '未找到记忆条目。',
|
||||
'memory.confirm_delete': '确定要删除此记忆条目吗?',
|
||||
'memory.all_categories': '全部分类',
|
||||
'memory.load_error': '加载记忆失败:{error}',
|
||||
'memory.required_fields': '键和内容为必填项。',
|
||||
'memory.store_error': '存储记忆失败',
|
||||
'memory.delete_error': '删除记忆失败',
|
||||
'memory.add_button': '添加记忆',
|
||||
'memory.key_placeholder': '例如:user_preferences',
|
||||
'memory.content_placeholder': '记忆内容...',
|
||||
'memory.category_optional': '分类(可选)',
|
||||
'memory.category_placeholder': '例如:preferences、context、facts',
|
||||
'memory.delete_prompt': '删除?',
|
||||
|
||||
// Config
|
||||
'config.title': '配置',
|
||||
@ -476,6 +868,10 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'config.error': '配置保存失败。',
|
||||
'config.loading': '配置加载中...',
|
||||
'config.editor_placeholder': 'TOML 配置...',
|
||||
'config.sensitive_title': '敏感字段已隐藏',
|
||||
'config.sensitive_body':
|
||||
'为了安全,API 密钥、令牌和密码会被隐藏。要更新已隐藏的字段,请用你的新值替换整个隐藏值。',
|
||||
'config.editor_title': 'TOML 配置',
|
||||
|
||||
// Cost
|
||||
'cost.title': '成本追踪',
|
||||
@ -489,6 +885,14 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'cost.tokens': 'Token',
|
||||
'cost.requests': '请求',
|
||||
'cost.usd': '成本(USD)',
|
||||
'cost.load_error': '加载成本数据失败:{error}',
|
||||
'cost.total_requests': '总请求数',
|
||||
'cost.token_statistics': 'Token 统计',
|
||||
'cost.average_tokens_per_request': '每次请求平均 Token',
|
||||
'cost.cost_per_1k_tokens': '每 1K Token 成本',
|
||||
'cost.model_breakdown': '模型拆分',
|
||||
'cost.no_models': '暂无模型数据。',
|
||||
'cost.share': '占比',
|
||||
|
||||
// Logs
|
||||
'logs.title': '实时日志',
|
||||
@ -499,6 +903,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'logs.empty': '暂无日志条目。',
|
||||
'logs.connected': '已连接到事件流。',
|
||||
'logs.disconnected': '与事件流断开连接。',
|
||||
'logs.jump_to_bottom': '跳到末尾',
|
||||
'logs.events': '事件',
|
||||
'logs.filter_label': '筛选:',
|
||||
'logs.paused_empty': '日志流已暂停。',
|
||||
'logs.waiting_empty': '正在等待事件...',
|
||||
|
||||
// Doctor
|
||||
'doctor.title': '系统诊断',
|
||||
@ -512,6 +921,13 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'doctor.message': '消息',
|
||||
'doctor.empty': '尚未运行诊断。',
|
||||
'doctor.summary': '诊断摘要',
|
||||
'doctor.run_error': '运行诊断失败',
|
||||
'doctor.running_short': '运行中...',
|
||||
'doctor.may_take_seconds': '这可能需要几秒钟。',
|
||||
'doctor.issues_found': '发现问题',
|
||||
'doctor.warnings': '警告',
|
||||
'doctor.all_clear': '一切正常',
|
||||
'doctor.empty_help': '点击“运行诊断”以检查你的 ZeroClaw 安装。',
|
||||
|
||||
// Auth / Pairing
|
||||
'auth.pair': '设备配对',
|
||||
@ -521,6 +937,9 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'auth.pairing_success': '配对成功!',
|
||||
'auth.pairing_failed': '配对失败,请重试。',
|
||||
'auth.enter_code': '输入配对码以连接到智能体。',
|
||||
'auth.enter_code_terminal': '输入终端中的配对码',
|
||||
'auth.code_placeholder': '6 位代码',
|
||||
'auth.pairing_progress': '配对中...',
|
||||
|
||||
// Common
|
||||
'common.loading': '加载中...',
|
||||
@ -544,6 +963,27 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
'common.status': '状态',
|
||||
'common.created': '创建时间',
|
||||
'common.updated': '更新时间',
|
||||
'common.saving': '保存中...',
|
||||
'common.adding': '添加中...',
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
'common.active': '激活',
|
||||
'common.inactive': '未激活',
|
||||
'common.optional': '可选',
|
||||
'common.id': 'ID',
|
||||
'common.path': '路径',
|
||||
'common.version': '版本',
|
||||
'common.all': '全部',
|
||||
'common.unknown': '未知',
|
||||
'common.current': '当前',
|
||||
'common.current_value': '当前值',
|
||||
'common.apply': '应用',
|
||||
'common.configured': '已配置',
|
||||
'common.configure': '配置',
|
||||
'common.default': '默认',
|
||||
'common.events': '事件',
|
||||
'common.lines': '行',
|
||||
'common.select': '选择',
|
||||
|
||||
// Health
|
||||
'health.title': '系统健康',
|
||||
@ -584,6 +1024,16 @@ export function t(key: string): string {
|
||||
return translations[currentLocale]?.[key] ?? translations.en[key] ?? key;
|
||||
}
|
||||
|
||||
export function tf(
|
||||
key: string,
|
||||
values: Record<string, string | number>,
|
||||
): string {
|
||||
return Object.entries(values).reduce((result, [name, value]) => {
|
||||
const token = `{${name}}`;
|
||||
return result.split(token).join(String(value));
|
||||
}, t(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translation for a specific locale. Falls back to English, then to the
|
||||
* raw key.
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { Send, Bot, User, AlertCircle } from 'lucide-react';
|
||||
import type { WsMessage } from '@/types/api';
|
||||
import { WebSocketClient } from '@/lib/ws';
|
||||
import { getLocale, t, tf } from '@/lib/i18n';
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
@ -18,8 +19,6 @@ interface PersistedChatMessage {
|
||||
}
|
||||
|
||||
let fallbackMessageIdCounter = 0;
|
||||
const EMPTY_DONE_FALLBACK =
|
||||
'Tool execution completed, but no final response text was returned.';
|
||||
const CHAT_HISTORY_STORAGE_KEY = 'zeroclaw.agent_chat.messages.v1';
|
||||
const MAX_PERSISTED_MESSAGES = 500;
|
||||
|
||||
@ -124,7 +123,7 @@ export default function AgentChat() {
|
||||
};
|
||||
|
||||
ws.onError = () => {
|
||||
setError('Connection error. Attempting to reconnect...');
|
||||
setError(t('agent.connection_error'));
|
||||
};
|
||||
|
||||
ws.onMessage = (msg: WsMessage) => {
|
||||
@ -137,7 +136,7 @@ export default function AgentChat() {
|
||||
case 'message':
|
||||
case 'done': {
|
||||
const content = (msg.full_response ?? msg.content ?? pendingContentRef.current ?? '').trim();
|
||||
const finalContent = content || EMPTY_DONE_FALLBACK;
|
||||
const finalContent = content || t('agent.done_without_text');
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
@ -160,7 +159,10 @@ export default function AgentChat() {
|
||||
{
|
||||
id: makeMessageId(),
|
||||
role: 'agent',
|
||||
content: `[Tool Call] ${msg.name ?? 'unknown'}(${JSON.stringify(msg.args ?? {})})`,
|
||||
content: tf('agent.tool_call_message', {
|
||||
name: msg.name ?? t('agent.unknown_tool'),
|
||||
args: JSON.stringify(msg.args ?? {}),
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
@ -172,7 +174,9 @@ export default function AgentChat() {
|
||||
{
|
||||
id: makeMessageId(),
|
||||
role: 'agent',
|
||||
content: `[Tool Result] ${msg.output ?? ''}`,
|
||||
content: tf('agent.tool_result_message', {
|
||||
output: msg.output ?? '',
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
@ -184,7 +188,9 @@ export default function AgentChat() {
|
||||
{
|
||||
id: makeMessageId(),
|
||||
role: 'agent',
|
||||
content: `[Error] ${msg.message ?? 'Unknown error'}`,
|
||||
content: tf('agent.error_message', {
|
||||
message: msg.message ?? t('agent.unknown_error'),
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
@ -229,7 +235,7 @@ export default function AgentChat() {
|
||||
setTyping(true);
|
||||
pendingContentRef.current = '';
|
||||
} catch {
|
||||
setError('Failed to send message. Please try again.');
|
||||
setError(t('agent.send_failed'));
|
||||
}
|
||||
|
||||
setInput('');
|
||||
@ -258,8 +264,8 @@ export default function AgentChat() {
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<Bot className="h-12 w-12 mb-3 text-gray-600" />
|
||||
<p className="text-lg font-medium">ZeroClaw Agent</p>
|
||||
<p className="text-sm mt-1">Send a message to start the conversation</p>
|
||||
<p className="text-lg font-medium">{t('agent.empty_title')}</p>
|
||||
<p className="text-sm mt-1">{t('agent.empty_subtitle')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -296,7 +302,7 @@ export default function AgentChat() {
|
||||
msg.role === 'user' ? 'text-blue-200' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{msg.timestamp.toLocaleTimeString()}
|
||||
{msg.timestamp.toLocaleTimeString(getLocale())}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -313,7 +319,7 @@ export default function AgentChat() {
|
||||
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">Typing...</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('agent.typing')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -331,7 +337,7 @@ export default function AgentChat() {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={connected ? 'Type a message...' : 'Connecting...'}
|
||||
placeholder={connected ? t('agent.placeholder') : t('agent.connecting')}
|
||||
disabled={!connected}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
|
||||
/>
|
||||
@ -351,7 +357,7 @@ export default function AgentChat() {
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
{connected ? t('agent.connected') : t('agent.disconnected')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
ShieldAlert,
|
||||
} from 'lucide-react';
|
||||
import { getConfig, putConfig } from '@/lib/api';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
export default function Config() {
|
||||
const [config, setConfig] = useState('');
|
||||
@ -31,9 +32,9 @@ export default function Config() {
|
||||
setSuccess(null);
|
||||
try {
|
||||
await putConfig(config);
|
||||
setSuccess('Configuration saved successfully.');
|
||||
setSuccess(t('config.saved'));
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save configuration');
|
||||
setError(err instanceof Error ? err.message : t('config.error'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@ -60,7 +61,7 @@ export default function Config() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">Configuration</h2>
|
||||
<h2 className="text-base font-semibold text-white">{t('config.title')}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
@ -68,7 +69,7 @@ export default function Config() {
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -77,11 +78,10 @@ export default function Config() {
|
||||
<ShieldAlert className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-yellow-300 font-medium">
|
||||
Sensitive fields are masked
|
||||
{t('config.sensitive_title')}
|
||||
</p>
|
||||
<p className="text-sm text-yellow-400/70 mt-0.5">
|
||||
API keys, tokens, and passwords are hidden for security. To update a
|
||||
masked field, replace the entire masked value with your new value.
|
||||
{t('config.sensitive_body')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -106,10 +106,10 @@ export default function Config() {
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-800/50">
|
||||
<span className="text-xs text-gray-400 font-medium uppercase tracking-wider">
|
||||
TOML Configuration
|
||||
{t('config.editor_title')}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{config.split('\n').length} lines
|
||||
{config.split('\n').length} {t('common.lines')}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { CostSummary } from '@/types/api';
|
||||
import { getCost } from '@/lib/api';
|
||||
import { getLocale, t, tf } from '@/lib/i18n';
|
||||
|
||||
function formatUSD(value: number): string {
|
||||
return `$${value.toFixed(4)}`;
|
||||
@ -28,7 +29,7 @@ export default function Cost() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
||||
Failed to load cost data: {error}
|
||||
{tf('cost.load_error', { error })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -53,7 +54,7 @@ export default function Cost() {
|
||||
<div className="p-2 bg-blue-600/20 rounded-lg">
|
||||
<DollarSign className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Session Cost</span>
|
||||
<span className="text-sm text-gray-400">{t('cost.session')}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{formatUSD(cost.session_cost_usd)}
|
||||
@ -65,7 +66,7 @@ export default function Cost() {
|
||||
<div className="p-2 bg-green-600/20 rounded-lg">
|
||||
<TrendingUp className="h-5 w-5 text-green-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Daily Cost</span>
|
||||
<span className="text-sm text-gray-400">{t('cost.daily')}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{formatUSD(cost.daily_cost_usd)}
|
||||
@ -77,7 +78,7 @@ export default function Cost() {
|
||||
<div className="p-2 bg-purple-600/20 rounded-lg">
|
||||
<Layers className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Monthly Cost</span>
|
||||
<span className="text-sm text-gray-400">{t('cost.monthly')}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{formatUSD(cost.monthly_cost_usd)}
|
||||
@ -89,10 +90,10 @@ export default function Cost() {
|
||||
<div className="p-2 bg-orange-600/20 rounded-lg">
|
||||
<Hash className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Total Requests</span>
|
||||
<span className="text-sm text-gray-400">{t('cost.total_requests')}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{cost.request_count.toLocaleString()}
|
||||
{cost.request_count.toLocaleString(getLocale())}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,25 +101,25 @@ export default function Cost() {
|
||||
{/* Token Statistics */}
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-5">
|
||||
<h3 className="text-base font-semibold text-white mb-4">
|
||||
Token Statistics
|
||||
{t('cost.token_statistics')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-400">Total Tokens</p>
|
||||
<p className="text-sm text-gray-400">{t('cost.total_tokens')}</p>
|
||||
<p className="text-xl font-bold text-white mt-1">
|
||||
{cost.total_tokens.toLocaleString()}
|
||||
{cost.total_tokens.toLocaleString(getLocale())}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-400">Avg Tokens / Request</p>
|
||||
<p className="text-sm text-gray-400">{t('cost.average_tokens_per_request')}</p>
|
||||
<p className="text-xl font-bold text-white mt-1">
|
||||
{cost.request_count > 0
|
||||
? Math.round(cost.total_tokens / cost.request_count).toLocaleString()
|
||||
? Math.round(cost.total_tokens / cost.request_count).toLocaleString(getLocale())
|
||||
: '0'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-400">Cost per 1K Tokens</p>
|
||||
<p className="text-sm text-gray-400">{t('cost.cost_per_1k_tokens')}</p>
|
||||
<p className="text-xl font-bold text-white mt-1">
|
||||
{cost.total_tokens > 0
|
||||
? formatUSD((cost.monthly_cost_usd / cost.total_tokens) * 1000)
|
||||
@ -132,12 +133,12 @@ export default function Cost() {
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-gray-800">
|
||||
<h3 className="text-base font-semibold text-white">
|
||||
Model Breakdown
|
||||
{t('cost.model_breakdown')}
|
||||
</h3>
|
||||
</div>
|
||||
{models.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No model data available.
|
||||
{t('cost.no_models')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@ -145,19 +146,19 @@ export default function Cost() {
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left px-5 py-3 text-gray-400 font-medium">
|
||||
Model
|
||||
{t('cost.model')}
|
||||
</th>
|
||||
<th className="text-right px-5 py-3 text-gray-400 font-medium">
|
||||
Cost
|
||||
{t('cost.usd')}
|
||||
</th>
|
||||
<th className="text-right px-5 py-3 text-gray-400 font-medium">
|
||||
Tokens
|
||||
{t('cost.tokens')}
|
||||
</th>
|
||||
<th className="text-right px-5 py-3 text-gray-400 font-medium">
|
||||
Requests
|
||||
{t('cost.requests')}
|
||||
</th>
|
||||
<th className="text-left px-5 py-3 text-gray-400 font-medium">
|
||||
Share
|
||||
{t('cost.share')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -181,10 +182,10 @@ export default function Cost() {
|
||||
{formatUSD(m.cost_usd)}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-gray-300 text-right">
|
||||
{m.total_tokens.toLocaleString()}
|
||||
{m.total_tokens.toLocaleString(getLocale())}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-gray-300 text-right">
|
||||
{m.request_count.toLocaleString()}
|
||||
{m.request_count.toLocaleString(getLocale())}
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@ -10,11 +10,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { CronJob } from '@/types/api';
|
||||
import { getCronJobs, addCronJob, deleteCronJob } from '@/lib/api';
|
||||
import { getLocale, t, tf } from '@/lib/i18n';
|
||||
|
||||
function formatDate(iso: string | null): string {
|
||||
if (!iso) return '-';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString();
|
||||
return d.toLocaleString(getLocale());
|
||||
}
|
||||
|
||||
export default function Cron() {
|
||||
@ -45,7 +46,7 @@ export default function Cron() {
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!formSchedule.trim() || !formCommand.trim()) {
|
||||
setFormError('Schedule and command are required.');
|
||||
setFormError(t('cron.required_fields'));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
@ -62,7 +63,7 @@ export default function Cron() {
|
||||
setFormSchedule('');
|
||||
setFormCommand('');
|
||||
} catch (err: unknown) {
|
||||
setFormError(err instanceof Error ? err.message : 'Failed to add job');
|
||||
setFormError(err instanceof Error ? err.message : t('cron.add_error'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@ -73,7 +74,7 @@ export default function Cron() {
|
||||
await deleteCronJob(id);
|
||||
setJobs((prev) => prev.filter((j) => j.id !== id));
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete job');
|
||||
setError(err instanceof Error ? err.message : t('cron.delete_error'));
|
||||
} finally {
|
||||
setConfirmDelete(null);
|
||||
}
|
||||
@ -97,7 +98,7 @@ export default function Cron() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
||||
Failed to load cron jobs: {error}
|
||||
{tf('cron.load_error', { error })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -118,7 +119,7 @@ export default function Cron() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
Scheduled Tasks ({jobs.length})
|
||||
{t('cron.title')} ({jobs.length})
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
@ -126,7 +127,7 @@ export default function Cron() {
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Job
|
||||
{t('cron.add')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -135,7 +136,7 @@ export default function Cron() {
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white">Add Cron Job</h3>
|
||||
<h3 className="text-lg font-semibold text-white">{t('cron.modal_title')}</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForm(false);
|
||||
@ -156,37 +157,37 @@ export default function Cron() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Name (optional)
|
||||
{t('cron.name_optional')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formName}
|
||||
onChange={(e) => setFormName(e.target.value)}
|
||||
placeholder="e.g. Daily cleanup"
|
||||
placeholder={t('cron.name_placeholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Schedule <span className="text-red-400">*</span>
|
||||
{t('cron.schedule')} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formSchedule}
|
||||
onChange={(e) => setFormSchedule(e.target.value)}
|
||||
placeholder="e.g. 0 0 * * * (cron expression)"
|
||||
placeholder={t('cron.schedule_placeholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Command <span className="text-red-400">*</span>
|
||||
{t('cron.command')} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formCommand}
|
||||
onChange={(e) => setFormCommand(e.target.value)}
|
||||
placeholder="e.g. cleanup --older-than 7d"
|
||||
placeholder={t('cron.command_placeholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
@ -200,14 +201,14 @@ export default function Cron() {
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Adding...' : 'Add Job'}
|
||||
{submitting ? t('cron.adding') : t('cron.add')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,35 +219,35 @@ export default function Cron() {
|
||||
{jobs.length === 0 ? (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
|
||||
<Clock className="h-10 w-10 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-400">No scheduled tasks configured.</p>
|
||||
<p className="text-gray-400">{t('cron.empty_configured')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
ID
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Name
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Command
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Next Run
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Last Status
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Enabled
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-gray-400 font-medium">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('common.id')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('cron.name')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('cron.command')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('cron.next_run')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('cron.last_status')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('cron.enabled')}
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-gray-400 font-medium">
|
||||
{t('common.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobs.map((job) => (
|
||||
@ -282,24 +283,24 @@ export default function Cron() {
|
||||
: 'bg-gray-800 text-gray-500 border border-gray-700'
|
||||
}`}
|
||||
>
|
||||
{job.enabled ? 'Enabled' : 'Disabled'}
|
||||
{job.enabled ? t('common.enabled') : t('common.disabled')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{confirmDelete === job.id ? (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-xs text-red-400">Delete?</span>
|
||||
<span className="text-xs text-red-400">{t('cron.delete_prompt')}</span>
|
||||
<button
|
||||
onClick={() => handleDelete(job.id)}
|
||||
className="text-red-400 hover:text-red-300 text-xs font-medium"
|
||||
>
|
||||
Yes
|
||||
{t('common.yes')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
className="text-gray-400 hover:text-white text-xs font-medium"
|
||||
>
|
||||
No
|
||||
{t('common.no')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { StatusResponse, CostSummary } from '@/types/api';
|
||||
import { getStatus, getCost } from '@/lib/api';
|
||||
import { getLocale, t, tf } from '@/lib/i18n';
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
const d = Math.floor(seconds / 86400);
|
||||
@ -70,7 +71,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
||||
Failed to load dashboard: {error}
|
||||
{tf('dashboard.load_error', { error })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -95,10 +96,10 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-blue-600/20 rounded-lg">
|
||||
<Cpu className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Provider / Model</span>
|
||||
<span className="text-sm text-gray-400">{t('dashboard.provider_model')}</span>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-white truncate">
|
||||
{status.provider ?? 'Unknown'}
|
||||
{status.provider ?? t('common.unknown')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 truncate">{status.model}</p>
|
||||
</div>
|
||||
@ -108,12 +109,12 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-green-600/20 rounded-lg">
|
||||
<Clock className="h-5 w-5 text-green-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Uptime</span>
|
||||
<span className="text-sm text-gray-400">{t('dashboard.uptime')}</span>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{formatUptime(status.uptime_seconds)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">Since last restart</p>
|
||||
<p className="text-sm text-gray-400">{t('dashboard.since_restart')}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
||||
@ -121,12 +122,14 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-purple-600/20 rounded-lg">
|
||||
<Globe className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Gateway Port</span>
|
||||
<span className="text-sm text-gray-400">{t('dashboard.gateway_port')}</span>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
:{status.gateway_port}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">Locale: {status.locale}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t('dashboard.locale')}: {status.locale}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
||||
@ -134,13 +137,13 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-orange-600/20 rounded-lg">
|
||||
<Database className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Memory Backend</span>
|
||||
<span className="text-sm text-gray-400">{t('dashboard.memory_backend')}</span>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-white capitalize">
|
||||
{status.memory_backend}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Paired: {status.paired ? 'Yes' : 'No'}
|
||||
{t('dashboard.paired')}: {status.paired ? t('common.yes') : t('common.no')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -150,13 +153,13 @@ export default function Dashboard() {
|
||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<DollarSign className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">Cost Overview</h2>
|
||||
<h2 className="text-base font-semibold text-white">{t('dashboard.cost_overview')}</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ label: 'Session', value: cost.session_cost_usd, color: 'bg-blue-500' },
|
||||
{ label: 'Daily', value: cost.daily_cost_usd, color: 'bg-green-500' },
|
||||
{ label: 'Monthly', value: cost.monthly_cost_usd, color: 'bg-purple-500' },
|
||||
{ label: t('cost.session'), value: cost.session_cost_usd, color: 'bg-blue-500' },
|
||||
{ label: t('cost.daily'), value: cost.daily_cost_usd, color: 'bg-green-500' },
|
||||
{ label: t('cost.monthly'), value: cost.monthly_cost_usd, color: 'bg-purple-500' },
|
||||
].map(({ label, value, color }) => (
|
||||
<div key={label}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
@ -173,12 +176,12 @@ export default function Dashboard() {
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 pt-3 border-t border-gray-800 flex justify-between text-sm">
|
||||
<span className="text-gray-400">Total Tokens</span>
|
||||
<span className="text-white">{cost.total_tokens.toLocaleString()}</span>
|
||||
<span className="text-gray-400">{t('cost.total_tokens')}</span>
|
||||
<span className="text-white">{cost.total_tokens.toLocaleString(getLocale())}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm mt-1">
|
||||
<span className="text-gray-400">Requests</span>
|
||||
<span className="text-white">{cost.request_count.toLocaleString()}</span>
|
||||
<span className="text-gray-400">{t('cost.requests')}</span>
|
||||
<span className="text-white">{cost.request_count.toLocaleString(getLocale())}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -186,11 +189,11 @@ export default function Dashboard() {
|
||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Radio className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">Active Channels</h2>
|
||||
<h2 className="text-base font-semibold text-white">{t('dashboard.active_channels')}</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(status.channels).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">No channels configured</p>
|
||||
<p className="text-sm text-gray-500">{t('dashboard.no_channels')}</p>
|
||||
) : (
|
||||
Object.entries(status.channels).map(([name, active]) => (
|
||||
<div
|
||||
@ -205,7 +208,7 @@ export default function Dashboard() {
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-gray-400">
|
||||
{active ? 'Active' : 'Inactive'}
|
||||
{active ? t('common.active') : t('common.inactive')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,11 +221,11 @@ export default function Dashboard() {
|
||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Activity className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">Component Health</h2>
|
||||
<h2 className="text-base font-semibold text-white">{t('dashboard.component_health')}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{Object.entries(status.health.components).length === 0 ? (
|
||||
<p className="text-sm text-gray-500 col-span-2">No components reporting</p>
|
||||
<p className="text-sm text-gray-500 col-span-2">{t('dashboard.no_components')}</p>
|
||||
) : (
|
||||
Object.entries(status.health.components).map(([name, comp]) => (
|
||||
<div
|
||||
@ -238,7 +241,7 @@ export default function Dashboard() {
|
||||
<p className="text-xs text-gray-400 capitalize">{comp.status}</p>
|
||||
{comp.restart_count > 0 && (
|
||||
<p className="text-xs text-yellow-400 mt-1">
|
||||
Restarts: {comp.restart_count}
|
||||
{tf('dashboard.restarts', { count: comp.restart_count })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { DiagResult } from '@/types/api';
|
||||
import { runDoctor } from '@/lib/api';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
function severityIcon(severity: DiagResult['severity']) {
|
||||
switch (severity) {
|
||||
@ -43,6 +44,17 @@ function severityBg(severity: DiagResult['severity']): string {
|
||||
}
|
||||
}
|
||||
|
||||
function severityLabel(severity: DiagResult['severity']): string {
|
||||
switch (severity) {
|
||||
case 'ok':
|
||||
return t('doctor.ok');
|
||||
case 'warn':
|
||||
return t('doctor.warn');
|
||||
case 'error':
|
||||
return t('doctor.error');
|
||||
}
|
||||
}
|
||||
|
||||
export default function Doctor() {
|
||||
const [results, setResults] = useState<DiagResult[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -56,7 +68,7 @@ export default function Doctor() {
|
||||
const data = await runDoctor();
|
||||
setResults(data);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to run diagnostics');
|
||||
setError(err instanceof Error ? err.message : t('doctor.run_error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -82,7 +94,7 @@ export default function Doctor() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Stethoscope className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">Diagnostics</h2>
|
||||
<h2 className="text-base font-semibold text-white">{t('doctor.title')}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRun}
|
||||
@ -92,12 +104,12 @@ export default function Doctor() {
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Running...
|
||||
{t('doctor.running_short')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
Run Diagnostics
|
||||
{t('doctor.run')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@ -114,9 +126,9 @@ export default function Doctor() {
|
||||
{loading && (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<Loader2 className="h-10 w-10 text-blue-500 animate-spin mb-4" />
|
||||
<p className="text-gray-400">Running diagnostics...</p>
|
||||
<p className="text-gray-400">{t('doctor.running')}</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
This may take a few seconds.
|
||||
{t('doctor.may_take_seconds')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -129,27 +141,21 @@ export default function Doctor() {
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
||||
<span className="text-sm text-white font-medium">
|
||||
{okCount} <span className="text-gray-400 font-normal">ok</span>
|
||||
{okCount} <span className="text-gray-400 font-normal">{t('doctor.ok')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px h-5 bg-gray-700" />
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-400" />
|
||||
<span className="text-sm text-white font-medium">
|
||||
{warnCount}{' '}
|
||||
<span className="text-gray-400 font-normal">
|
||||
warning{warnCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
{warnCount} <span className="text-gray-400 font-normal">{t('doctor.warn')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px h-5 bg-gray-700" />
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-5 w-5 text-red-400" />
|
||||
<span className="text-sm text-white font-medium">
|
||||
{errorCount}{' '}
|
||||
<span className="text-gray-400 font-normal">
|
||||
error{errorCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
{errorCount} <span className="text-gray-400 font-normal">{t('doctor.error')}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -157,15 +163,15 @@ export default function Doctor() {
|
||||
<div className="ml-auto">
|
||||
{errorCount > 0 ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-red-900/40 text-red-400 border border-red-700/50">
|
||||
Issues Found
|
||||
{t('doctor.issues_found')}
|
||||
</span>
|
||||
) : warnCount > 0 ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-yellow-900/40 text-yellow-400 border border-yellow-700/50">
|
||||
Warnings
|
||||
{t('doctor.warnings')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-green-900/40 text-green-400 border border-green-700/50">
|
||||
All Clear
|
||||
{t('doctor.all_clear')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -191,7 +197,7 @@ export default function Doctor() {
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-white">{result.message}</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5 capitalize">
|
||||
{result.severity}
|
||||
{severityLabel(result.severity)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -206,9 +212,9 @@ export default function Doctor() {
|
||||
{!results && !loading && !error && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-gray-500">
|
||||
<Stethoscope className="h-12 w-12 text-gray-600 mb-4" />
|
||||
<p className="text-lg font-medium">System Diagnostics</p>
|
||||
<p className="text-lg font-medium">{t('doctor.title')}</p>
|
||||
<p className="text-sm mt-1">
|
||||
Click "Run Diagnostics" to check your ZeroClaw installation.
|
||||
{t('doctor.empty_help')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -13,25 +13,26 @@ import {
|
||||
getStatus,
|
||||
putIntegrationCredentials,
|
||||
} from '@/lib/api';
|
||||
import { t, tf } from '@/lib/i18n';
|
||||
|
||||
function statusBadge(status: Integration['status']) {
|
||||
switch (status) {
|
||||
case 'Active':
|
||||
return {
|
||||
icon: Check,
|
||||
label: 'Active',
|
||||
label: t('integrations.active'),
|
||||
classes: 'bg-green-900/40 text-green-400 border-green-700/50',
|
||||
};
|
||||
case 'Available':
|
||||
return {
|
||||
icon: Zap,
|
||||
label: 'Available',
|
||||
label: t('integrations.available'),
|
||||
classes: 'bg-blue-900/40 text-blue-400 border-blue-700/50',
|
||||
};
|
||||
case 'ComingSoon':
|
||||
return {
|
||||
icon: Clock,
|
||||
label: 'Coming Soon',
|
||||
label: t('integrations.coming_soon'),
|
||||
classes: 'bg-gray-800 text-gray-400 border-gray-700',
|
||||
};
|
||||
}
|
||||
@ -70,9 +71,9 @@ const FALLBACK_MODEL_OPTIONS: Record<string, string[]> = {
|
||||
|
||||
function customModelFormatHint(integrationId: string): string {
|
||||
if (integrationId === 'openrouter' || integrationId === 'vercel') {
|
||||
return 'Format: anthropic/claude-sonnet-4-6';
|
||||
return t('integrations.custom_model_hint_scoped');
|
||||
}
|
||||
return 'Format: claude-sonnet-4-6 (or provider/model when required)';
|
||||
return t('integrations.custom_model_hint_generic');
|
||||
}
|
||||
|
||||
function modelOptionsForField(
|
||||
@ -175,7 +176,7 @@ export default function Integrations() {
|
||||
setRuntimeStatus(status ? { model: status.model } : null);
|
||||
return nextSettingsByName;
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load integrations');
|
||||
setError(err instanceof Error ? err.message : t('common.error'));
|
||||
setActiveAiIntegrationId(null);
|
||||
setRuntimeStatus(null);
|
||||
return null;
|
||||
@ -252,7 +253,7 @@ export default function Integrations() {
|
||||
if (isSelectField) {
|
||||
if (value === SELECT_KEEP) {
|
||||
if (field.required && !field.has_value) {
|
||||
setSaveError(`${field.label} is required.`);
|
||||
setSaveError(tf('integrations.field_required', { field: field.label }));
|
||||
return;
|
||||
}
|
||||
if (isDirty) {
|
||||
@ -268,12 +269,14 @@ export default function Integrations() {
|
||||
const trimmed = resolvedValue.trim();
|
||||
|
||||
if (isSelectField && value === SELECT_CUSTOM && !trimmed) {
|
||||
setSaveError(`Enter a custom value for ${field.label} or choose a recommended model.`);
|
||||
setSaveError(
|
||||
tf('integrations.custom_value_required', { field: field.label }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.required && !trimmed && !field.has_value) {
|
||||
setSaveError(`${field.label} is required.`);
|
||||
setSaveError(tf('integrations.field_required', { field: field.label }));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -289,7 +292,7 @@ export default function Integrations() {
|
||||
Object.keys(payload).length === 0 &&
|
||||
!activeEditor.activates_default_provider
|
||||
) {
|
||||
setSaveError('No changes to save.');
|
||||
setSaveError(t('integrations.no_changes'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -298,9 +301,13 @@ export default function Integrations() {
|
||||
activeAiIntegrationId &&
|
||||
activeEditor.id !== activeAiIntegrationId
|
||||
) {
|
||||
const currentProvider = activeAiIntegration?.name ?? 'current provider';
|
||||
const currentProvider =
|
||||
activeAiIntegration?.name ?? t('integrations.current_provider');
|
||||
const confirmed = window.confirm(
|
||||
`Switch default AI provider from ${currentProvider} to ${activeEditor.name}?`,
|
||||
tf('integrations.confirm_switch_provider', {
|
||||
current: currentProvider,
|
||||
target: activeEditor.name,
|
||||
}),
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@ -315,10 +322,10 @@ export default function Integrations() {
|
||||
});
|
||||
|
||||
await loadData(false);
|
||||
setSaveSuccess(`${activeEditor.name} credentials saved.`);
|
||||
setSaveSuccess(tf('integrations.credentials_saved', { name: activeEditor.name }));
|
||||
closeEditor();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to save credentials';
|
||||
const message = err instanceof Error ? err.message : t('integrations.save_error');
|
||||
if (message.includes('API 409')) {
|
||||
const refreshed = await loadData(false);
|
||||
if (refreshed) {
|
||||
@ -331,7 +338,7 @@ export default function Integrations() {
|
||||
}
|
||||
}
|
||||
setSaveError(
|
||||
'Configuration changed elsewhere. Refreshed latest settings; re-enter values and save again.',
|
||||
t('integrations.stale_save'),
|
||||
);
|
||||
} else {
|
||||
setSaveError(message);
|
||||
@ -357,9 +364,14 @@ export default function Integrations() {
|
||||
!isActiveDefaultProvider &&
|
||||
integration.id !== activeAiIntegrationId
|
||||
) {
|
||||
const currentProvider = activeAiIntegration?.name ?? 'current provider';
|
||||
const currentProvider =
|
||||
activeAiIntegration?.name ?? t('integrations.current_provider');
|
||||
const confirmed = window.confirm(
|
||||
`Switch default AI provider from ${currentProvider} to ${integration.name} and set model to ${trimmedTarget}?`,
|
||||
tf('integrations.confirm_switch_provider_with_model', {
|
||||
current: currentProvider,
|
||||
target: integration.name,
|
||||
model: trimmedTarget,
|
||||
}),
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@ -378,19 +390,22 @@ export default function Integrations() {
|
||||
});
|
||||
|
||||
await loadData(false);
|
||||
setSaveSuccess(`Model updated to ${trimmedTarget} for ${integration.name}.`);
|
||||
setSaveSuccess(
|
||||
tf('integrations.model_updated', {
|
||||
model: trimmedTarget,
|
||||
name: integration.name,
|
||||
}),
|
||||
);
|
||||
setQuickModelDrafts((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[integration.id];
|
||||
return next;
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to update model';
|
||||
const message = err instanceof Error ? err.message : t('integrations.update_model_error');
|
||||
if (message.includes('API 409')) {
|
||||
await loadData(false);
|
||||
setQuickModelError(
|
||||
'Configuration changed elsewhere. Refreshed latest settings; choose the model again.',
|
||||
);
|
||||
setQuickModelError(t('integrations.stale_model'));
|
||||
} else {
|
||||
setQuickModelError(message);
|
||||
}
|
||||
@ -421,7 +436,7 @@ export default function Integrations() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
||||
Failed to load integrations: {error}
|
||||
{tf('integrations.load_error', { error })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -441,7 +456,7 @@ export default function Integrations() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Puzzle className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
Integrations ({integrations.length})
|
||||
{t('integrations.title')} ({integrations.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -469,7 +484,7 @@ export default function Integrations() {
|
||||
: 'bg-gray-900 text-gray-400 border border-gray-700 hover:bg-gray-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{cat === 'all' ? 'All' : formatCategory(cat)}
|
||||
{cat === 'all' ? t('common.all') : formatCategory(cat)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -478,7 +493,7 @@ export default function Integrations() {
|
||||
{Object.keys(grouped).length === 0 ? (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
|
||||
<Puzzle className="h-10 w-10 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-400">No integrations found.</p>
|
||||
<p className="text-gray-400">{t('integrations.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(grouped)
|
||||
@ -510,8 +525,8 @@ export default function Integrations() {
|
||||
const modelSummary = currentModel
|
||||
? currentModel
|
||||
: fallbackModel
|
||||
? `default: ${fallbackModel}`
|
||||
: 'default';
|
||||
? tf('integrations.default_summary', { model: fallbackModel })
|
||||
: t('integrations.default_only');
|
||||
const modelBaseline = currentModel ?? fallbackModel ?? '';
|
||||
const quickDraft = editable
|
||||
? quickModelDrafts[editable.id] ?? modelBaseline
|
||||
@ -555,7 +570,9 @@ export default function Integrations() {
|
||||
: 'bg-gray-800 text-gray-300 border-gray-700'
|
||||
}`}
|
||||
>
|
||||
{isActiveDefaultProvider ? 'Default' : 'Configured'}
|
||||
{isActiveDefaultProvider
|
||||
? t('integrations.default_badge')
|
||||
: t('integrations.configured_badge')}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
@ -571,7 +588,7 @@ export default function Integrations() {
|
||||
<div className="mt-3 rounded-lg border border-gray-800 bg-gray-950/50 p-3 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] uppercase tracking-wider text-gray-500">
|
||||
Current model
|
||||
{t('integrations.current_model')}
|
||||
</span>
|
||||
<span className="text-xs text-gray-200 truncate" title={modelSummary}>
|
||||
{modelSummary}
|
||||
@ -615,11 +632,13 @@ export default function Integrations() {
|
||||
}
|
||||
className="px-2.5 py-1.5 rounded-lg text-xs font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{quickModelSavingId === editable.id ? 'Saving...' : 'Apply'}
|
||||
{quickModelSavingId === editable.id
|
||||
? t('common.saving')
|
||||
: t('common.apply')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
For custom model IDs, use Edit Keys.
|
||||
{t('integrations.quick_model_help')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -632,17 +651,19 @@ export default function Integrations() {
|
||||
{editable.configured
|
||||
? editable.activates_default_provider
|
||||
? isActiveDefaultProvider
|
||||
? 'Default provider configured'
|
||||
: 'Provider configured'
|
||||
: 'Credentials configured'
|
||||
: 'Credentials not configured'}
|
||||
? t('integrations.default_provider_configured')
|
||||
: t('integrations.provider_configured')
|
||||
: t('integrations.credentials_configured')
|
||||
: t('integrations.credentials_not_configured')}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openEditor(editable)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-blue-700/70 bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs font-medium transition-colors"
|
||||
>
|
||||
<KeyRound className="h-3.5 w-3.5" />
|
||||
{editable.configured ? 'Edit Keys' : 'Configure'}
|
||||
{editable.configured
|
||||
? t('integrations.edit_keys')
|
||||
: t('integrations.configure')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -667,19 +688,19 @@ export default function Integrations() {
|
||||
<div className="px-5 py-4 border-b border-gray-800 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
Configure {activeEditor.name}
|
||||
{tf('integrations.configure_title', { name: activeEditor.name })}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
{activeEditor.configured
|
||||
? 'Enter only fields you want to update.'
|
||||
: 'Enter required fields to configure this integration.'}
|
||||
? t('integrations.configure_intro_update')
|
||||
: t('integrations.configure_intro_new')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeEditor}
|
||||
disabled={saving}
|
||||
className="text-gray-400 hover:text-white transition-colors disabled:opacity-50"
|
||||
aria-label="Close"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@ -688,10 +709,11 @@ export default function Integrations() {
|
||||
<div className="p-5 space-y-4">
|
||||
{activeEditor.activates_default_provider && (
|
||||
<div className="rounded-lg border border-blue-800 bg-blue-950/30 p-3 text-xs text-blue-200">
|
||||
Saving here updates credentials and switches your default AI provider to{' '}
|
||||
<strong>{activeEditor.name}</strong>. For advanced provider settings, use{' '}
|
||||
{t('integrations.default_provider_notice_prefix')}{' '}
|
||||
<strong>{activeEditor.name}</strong>.{' '}
|
||||
{t('integrations.default_provider_notice_suffix')}{' '}
|
||||
<Link to="/config" className="underline underline-offset-2 hover:text-blue-100">
|
||||
Configuration
|
||||
{t('config.title')}
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
@ -712,8 +734,10 @@ export default function Integrations() {
|
||||
field.current_value?.trim() ||
|
||||
(activeEditorIsDefaultProvider ? runtimeStatus?.model?.trim() || '' : '');
|
||||
const keepCurrentLabel = currentModelValue
|
||||
? `Keep current model (${currentModelValue})`
|
||||
: 'Keep current model';
|
||||
? tf('integrations.keep_current_model_with_value', {
|
||||
model: currentModelValue,
|
||||
})
|
||||
: t('integrations.keep_current_model');
|
||||
|
||||
return (
|
||||
<div key={field.key}>
|
||||
@ -722,7 +746,7 @@ export default function Integrations() {
|
||||
{field.required && <span className="text-red-400">*</span>}
|
||||
{field.has_value && (
|
||||
<span className="text-[11px] text-green-400 bg-green-900/30 border border-green-800 px-1.5 py-0.5 rounded">
|
||||
Configured
|
||||
{t('common.configured')}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
@ -737,7 +761,7 @@ export default function Integrations() {
|
||||
<option value={SELECT_KEEP}>{keepCurrentLabel}</option>
|
||||
) : (
|
||||
<option value="" disabled>
|
||||
Select a recommended model
|
||||
{t('integrations.select_recommended_model')}
|
||||
</option>
|
||||
)}
|
||||
{selectOptions.map((option) => (
|
||||
@ -745,8 +769,12 @@ export default function Integrations() {
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
<option value={SELECT_CUSTOM}>Custom model...</option>
|
||||
{field.has_value && <option value={SELECT_CLEAR}>Clear current model</option>}
|
||||
<option value={SELECT_CUSTOM}>{t('integrations.custom_model')}</option>
|
||||
{field.has_value && (
|
||||
<option value={SELECT_CLEAR}>
|
||||
{t('integrations.clear_current_model')}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
|
||||
{fieldValues[field.key] === SELECT_CUSTOM && (
|
||||
@ -760,14 +788,17 @@ export default function Integrations() {
|
||||
)}
|
||||
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Pick a recommended model or choose Custom model. {customModelFormatHint(activeEditor.id)}.
|
||||
{tf('integrations.pick_model_help', {
|
||||
hint: customModelFormatHint(activeEditor.id),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{maskedSecretValue && (
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Current value: <span className="font-mono text-gray-300">{maskedSecretValue}</span>
|
||||
{t('integrations.current_value')}{' '}
|
||||
<span className="font-mono text-gray-300">{maskedSecretValue}</span>
|
||||
</p>
|
||||
)}
|
||||
<input
|
||||
@ -777,11 +808,11 @@ export default function Integrations() {
|
||||
placeholder={
|
||||
field.required
|
||||
? field.has_value
|
||||
? 'Enter a new value to replace current'
|
||||
: 'Enter value'
|
||||
? t('integrations.replace_current_placeholder')
|
||||
: t('integrations.enter_value_placeholder')
|
||||
: field.has_value
|
||||
? 'Type new value, or leave empty to keep current'
|
||||
: 'Optional'
|
||||
? t('integrations.keep_current_placeholder')
|
||||
: t('common.optional')
|
||||
}
|
||||
className="w-full px-3 py-2 rounded-lg bg-gray-950 border border-gray-700 text-sm text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
@ -805,7 +836,7 @@ export default function Integrations() {
|
||||
disabled={saving}
|
||||
className="px-4 py-2 rounded-lg text-sm border border-gray-700 text-gray-300 hover:bg-gray-800 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={saveCredentials}
|
||||
@ -813,10 +844,10 @@ export default function Integrations() {
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{saving
|
||||
? 'Saving...'
|
||||
? t('common.saving')
|
||||
: activeEditor.activates_default_provider
|
||||
? 'Save & Activate'
|
||||
: 'Save Keys'}
|
||||
? t('integrations.save_activate')
|
||||
: t('integrations.save_keys')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,10 +8,11 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { SSEEvent } from '@/types/api';
|
||||
import { SSEClient } from '@/lib/sse';
|
||||
import { getLocale, t } from '@/lib/i18n';
|
||||
|
||||
function formatTimestamp(ts?: string): string {
|
||||
if (!ts) return new Date().toLocaleTimeString();
|
||||
return new Date(ts).toLocaleTimeString();
|
||||
if (!ts) return new Date().toLocaleTimeString(getLocale());
|
||||
return new Date(ts).toLocaleTimeString(getLocale());
|
||||
}
|
||||
|
||||
function eventTypeBadgeColor(type: string): string {
|
||||
@ -138,7 +139,7 @@ export default function Logs() {
|
||||
<div className="flex items-center justify-between px-6 py-3 border-b border-gray-800 bg-gray-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">Live Logs</h2>
|
||||
<h2 className="text-base font-semibold text-white">{t('logs.title')}</h2>
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${
|
||||
@ -146,11 +147,11 @@ export default function Logs() {
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
{connected ? t('agent.connected') : t('agent.disconnected')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{filteredEntries.length} events
|
||||
{filteredEntries.length} {t('logs.events')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -166,11 +167,11 @@ export default function Logs() {
|
||||
>
|
||||
{paused ? (
|
||||
<>
|
||||
<Play className="h-3.5 w-3.5" /> Resume
|
||||
<Play className="h-3.5 w-3.5" /> {t('logs.resume')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="h-3.5 w-3.5" /> Pause
|
||||
<Pause className="h-3.5 w-3.5" /> {t('logs.pause')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@ -182,7 +183,7 @@ export default function Logs() {
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
||||
>
|
||||
<ArrowDown className="h-3.5 w-3.5" />
|
||||
Jump to bottom
|
||||
{t('logs.jump_to_bottom')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -192,7 +193,7 @@ export default function Logs() {
|
||||
{allTypes.length > 0 && (
|
||||
<div className="flex items-center gap-2 px-6 py-2 border-b border-gray-800 bg-gray-900/80 overflow-x-auto">
|
||||
<Filter className="h-4 w-4 text-gray-500 flex-shrink-0" />
|
||||
<span className="text-xs text-gray-500 flex-shrink-0">Filter:</span>
|
||||
<span className="text-xs text-gray-500 flex-shrink-0">{t('logs.filter_label')}</span>
|
||||
{allTypes.map((type) => (
|
||||
<label
|
||||
key={type}
|
||||
@ -212,7 +213,7 @@ export default function Logs() {
|
||||
onClick={() => setTypeFilters(new Set())}
|
||||
className="text-xs text-blue-400 hover:text-blue-300 flex-shrink-0 ml-1"
|
||||
>
|
||||
Clear
|
||||
{t('logs.clear')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -228,9 +229,7 @@ export default function Logs() {
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<Activity className="h-10 w-10 text-gray-600 mb-3" />
|
||||
<p className="text-sm">
|
||||
{paused
|
||||
? 'Log streaming is paused.'
|
||||
: 'Waiting for events...'}
|
||||
{paused ? t('logs.paused_empty') : t('logs.waiting_empty')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { MemoryEntry } from '@/types/api';
|
||||
import { getMemory, storeMemory, deleteMemory } from '@/lib/api';
|
||||
import { getLocale, t, tf } from '@/lib/i18n';
|
||||
|
||||
function truncate(text: string, max: number): string {
|
||||
if (text.length <= max) return text;
|
||||
@ -17,7 +18,7 @@ function truncate(text: string, max: number): string {
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString();
|
||||
return d.toLocaleString(getLocale());
|
||||
}
|
||||
|
||||
export default function Memory() {
|
||||
@ -60,7 +61,7 @@ export default function Memory() {
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!formKey.trim() || !formContent.trim()) {
|
||||
setFormError('Key and content are required.');
|
||||
setFormError(t('memory.required_fields'));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
@ -77,7 +78,7 @@ export default function Memory() {
|
||||
setFormContent('');
|
||||
setFormCategory('');
|
||||
} catch (err: unknown) {
|
||||
setFormError(err instanceof Error ? err.message : 'Failed to store memory');
|
||||
setFormError(err instanceof Error ? err.message : t('memory.store_error'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@ -88,7 +89,7 @@ export default function Memory() {
|
||||
await deleteMemory(key);
|
||||
setEntries((prev) => prev.filter((e) => e.key !== key));
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete memory');
|
||||
setError(err instanceof Error ? err.message : t('memory.delete_error'));
|
||||
} finally {
|
||||
setConfirmDelete(null);
|
||||
}
|
||||
@ -98,7 +99,7 @@ export default function Memory() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
||||
Failed to load memory: {error}
|
||||
{tf('memory.load_error', { error })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -111,7 +112,7 @@ export default function Memory() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
Memory ({entries.length})
|
||||
{t('memory.title')} ({entries.length})
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
@ -119,7 +120,7 @@ export default function Memory() {
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Memory
|
||||
{t('memory.add_button')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -132,7 +133,7 @@ export default function Memory() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search memory entries..."
|
||||
placeholder={t('memory.search')}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
@ -143,7 +144,7 @@ export default function Memory() {
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-8 py-2.5 text-sm text-white appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
<option value="">{t('memory.all_categories')}</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
@ -155,7 +156,7 @@ export default function Memory() {
|
||||
onClick={handleSearch}
|
||||
className="px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Search
|
||||
{t('common.search')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -171,7 +172,7 @@ export default function Memory() {
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white">Add Memory</h3>
|
||||
<h3 className="text-lg font-semibold text-white">{t('memory.add_button')}</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForm(false);
|
||||
@ -192,37 +193,37 @@ export default function Memory() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Key <span className="text-red-400">*</span>
|
||||
{t('memory.key')} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formKey}
|
||||
onChange={(e) => setFormKey(e.target.value)}
|
||||
placeholder="e.g. user_preferences"
|
||||
placeholder={t('memory.key_placeholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Content <span className="text-red-400">*</span>
|
||||
{t('memory.content')} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={formContent}
|
||||
onChange={(e) => setFormContent(e.target.value)}
|
||||
placeholder="Memory content..."
|
||||
placeholder={t('memory.content_placeholder')}
|
||||
rows={4}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Category (optional)
|
||||
{t('memory.category_optional')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formCategory}
|
||||
onChange={(e) => setFormCategory(e.target.value)}
|
||||
placeholder="e.g. preferences, context, facts"
|
||||
placeholder={t('memory.category_placeholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
@ -236,14 +237,14 @@ export default function Memory() {
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Saving...' : 'Save'}
|
||||
{submitting ? t('common.saving') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -258,29 +259,29 @@ export default function Memory() {
|
||||
) : entries.length === 0 ? (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
|
||||
<Brain className="h-10 w-10 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-400">No memory entries found.</p>
|
||||
<p className="text-gray-400">{t('memory.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Key
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Content
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Category
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Timestamp
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-gray-400 font-medium">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('memory.key')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('memory.content')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('memory.category')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
{t('memory.timestamp')}
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-gray-400 font-medium">
|
||||
{t('common.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
@ -307,18 +308,18 @@ export default function Memory() {
|
||||
<td className="px-4 py-3 text-right">
|
||||
{confirmDelete === entry.key ? (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-xs text-red-400">Delete?</span>
|
||||
<span className="text-xs text-red-400">{t('memory.delete_prompt')}</span>
|
||||
<button
|
||||
onClick={() => handleDelete(entry.key)}
|
||||
className="text-red-400 hover:text-red-300 text-xs font-medium"
|
||||
>
|
||||
Yes
|
||||
{t('common.yes')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
className="text-gray-400 hover:text-white text-xs font-medium"
|
||||
>
|
||||
No
|
||||
{t('common.no')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { ToolSpec, CliTool } from '@/types/api';
|
||||
import { getTools, getCliTools } from '@/lib/api';
|
||||
import { t, tf } from '@/lib/i18n';
|
||||
|
||||
export default function Tools() {
|
||||
const [tools, setTools] = useState<ToolSpec[]>([]);
|
||||
@ -44,7 +45,7 @@ export default function Tools() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
||||
Failed to load tools: {error}
|
||||
{tf('tools.load_error', { error })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -67,7 +68,7 @@ export default function Tools() {
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search tools..."
|
||||
placeholder={t('tools.search')}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
@ -77,12 +78,12 @@ export default function Tools() {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Wrench className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
Agent Tools ({filtered.length})
|
||||
{t('tools.agent_tools')} ({filtered.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">No tools match your search.</p>
|
||||
<p className="text-sm text-gray-500">{t('tools.no_match')}</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{filtered.map((tool) => {
|
||||
@ -119,7 +120,7 @@ export default function Tools() {
|
||||
{isExpanded && tool.parameters && (
|
||||
<div className="border-t border-gray-800 p-4">
|
||||
<p className="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wider">
|
||||
Parameter Schema
|
||||
{t('tools.parameter_schema')}
|
||||
</p>
|
||||
<pre className="text-xs text-gray-300 bg-gray-950 rounded-lg p-3 overflow-x-auto max-h-64 overflow-y-auto">
|
||||
{JSON.stringify(tool.parameters, null, 2)}
|
||||
@ -139,7 +140,7 @@ export default function Tools() {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Terminal className="h-5 w-5 text-green-400" />
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
CLI Tools ({filteredCli.length})
|
||||
{t('tools.cli_tools')} ({filteredCli.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -148,16 +149,16 @@ export default function Tools() {
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Name
|
||||
{t('common.name')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Path
|
||||
{t('common.path')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Version
|
||||
{t('common.version')}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
||||
Category
|
||||
{t('integrations.category')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user