mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-17 04:56:52 -04:00
feat(react-ui): add multilingual (i18n) support (#9642)
Adds end-to-end internationalization to the React UI with five seed
languages (English, Italian, Spanish, German, Simplified Chinese) and
a sidebar-footer language switcher next to the existing theme toggle.
Library: react-i18next + i18next + i18next-http-backend +
i18next-browser-languagedetector. The detector caches the user's
choice in localStorage (key `localai-language`, mirroring the existing
`localai-theme` convention) and updates the `<html lang>` attribute on
change. fallbackLng is `en`, so any missing translation in another
locale falls back transparently.
Translation files live under `public/locales/<lng>/<ns>.json`. They
ride along with the existing `//go:embed react-ui/dist/*` directive,
but the previous SPA route in core/http/app.go only exposed
`/assets/*` from the embedded React build. This commit generalizes
the asset handler into a `serveReactSubdir(subdir)` helper and adds a
matching `/locales/*` route so i18next-http-backend can fetch the
JSONs at runtime. The http-backend `loadPath` is built via the
existing `apiUrl()` helper so instances served under a sub-path (e.g.
`<base href="/ui/">`) resolve correctly.
Namespaces (13): common, nav, errors, auth, home, models, importModel,
chat, agents, skills, collections, media, admin. Translated UI surfaces
include the sidebar/header/footer chrome, login + account flows, the
Home dashboard (incl. the manage-by-chat assistant CTA), the model
gallery + import flow, the chat experience (Chat.jsx + ChatsMenu),
agents/skills/collections list pages, the studio media tabs (Image,
Video, TTS), and the admin page-headers (Settings incl. its section
nav, Manage, Backends, Traces, Nodes, P2P, Users, Usage). Shared
components (ConfirmDialog, Toast) take their default labels from the
common namespace so callers don't need to pass strings explicitly.
Tooling for incremental adoption is included:
- `i18next-parser.config.js` + `npm run i18n:extract` to sweep `t()`
keys into the JSON skeletons.
- `scripts/translate-locales.mjs` (one-off helper) to bootstrap
non-English locales from English source via OpenAI or Anthropic
APIs, with --copy mode as a placeholder fallback. Idempotent;
preserves existing translations unless --overwrite is passed.
Larger config-driven pages (ModelEditor, Settings deep field forms,
AgentChat/AgentCreate, SkillEdit, CollectionDetails, Talk, Sound,
biometrics, FineTune/Quantize, Users modals, Nodes/P2P install
pickers, BackendLogs, Traces deep filters, Explorer) intentionally
keep their inner content untranslated for now — they fall back to
English via fallbackLng so functionality is unaffected, and the
extracted-strings pattern + the bootstrap script make follow-up
extraction straightforward.
The initial Suspense fallback at the root in main.jsx covers the
first JSON fetch on cold load. A simple `.app-boot-spinner` styled
in App.css provides a non-empty paint while the first namespace
loads.
Assisted-by: Claude:claude-opus-4-7 [Bash Read Edit Write Agent]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
1ad5b5907d
commit
87cf736068
@@ -447,24 +447,28 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
return prefixRedirect(c, "/app/"+p)
|
||||
})
|
||||
|
||||
// Serve React static assets (JS, CSS, etc.)
|
||||
serveReactAsset := func(c echo.Context) error {
|
||||
p := "assets/" + c.Param("*")
|
||||
f, err := reactFS.Open(p)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
stat, statErr := f.Stat()
|
||||
if statErr == nil && !stat.IsDir() {
|
||||
contentType := mime.TypeByExtension(filepath.Ext(p))
|
||||
if contentType == "" {
|
||||
contentType = echo.MIMEOctetStream
|
||||
// Serve React static assets (JS, CSS, etc.) and i18n locale JSONs
|
||||
// from the embedded React build.
|
||||
serveReactSubdir := func(subdir string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
p := subdir + "/" + c.Param("*")
|
||||
f, err := reactFS.Open(p)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
stat, statErr := f.Stat()
|
||||
if statErr == nil && !stat.IsDir() {
|
||||
contentType := mime.TypeByExtension(filepath.Ext(p))
|
||||
if contentType == "" {
|
||||
contentType = echo.MIMEOctetStream
|
||||
}
|
||||
return c.Stream(http.StatusOK, contentType, f)
|
||||
}
|
||||
return c.Stream(http.StatusOK, contentType, f)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
e.GET("/assets/*", serveReactAsset)
|
||||
e.GET("/assets/*", serveReactSubdir("assets"))
|
||||
e.GET("/locales/*", serveReactSubdir("locales"))
|
||||
}
|
||||
}
|
||||
routes.RegisterJINARoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
|
||||
|
||||
12
core/http/react-ui/i18next-parser.config.js
Normal file
12
core/http/react-ui/i18next-parser.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
locales: ['en', 'it', 'es', 'de', 'zh-CN'],
|
||||
defaultNamespace: 'common',
|
||||
output: 'public/locales/$LOCALE/$NAMESPACE.json',
|
||||
input: ['src/**/*.{js,jsx}'],
|
||||
keySeparator: '.',
|
||||
namespaceSeparator: ':',
|
||||
defaultValue: (locale, _ns, key) => (locale === 'en' ? key : ''),
|
||||
sort: true,
|
||||
createOldCatalogs: false,
|
||||
keepRemoved: false,
|
||||
}
|
||||
1906
core/http/react-ui/package-lock.json
generated
1906
core/http/react-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,20 +8,11 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"i18n:extract": "i18next-parser",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"marked": "^15.0.7",
|
||||
"dompurify": "^3.4.0",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@modelcontextprotocol/ext-apps": "^1.2.2",
|
||||
"yaml": "^2.8.3",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
@@ -30,16 +21,31 @@
|
||||
"@codemirror/search": "^6.5.10",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.8",
|
||||
"@lezer/highlight": "^1.2.1"
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@modelcontextprotocol/ext-apps": "^1.2.2",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"dompurify": "^3.4.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"i18next": "^26.0.8",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-http-backend": "^3.0.6",
|
||||
"marked": "^15.0.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^17.0.6",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"vite": "^6.4.2",
|
||||
"eslint": "^9.27.0",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"globals": "^16.1.0",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"@playwright/test": "1.58.2"
|
||||
"globals": "^16.1.0",
|
||||
"i18next-parser": "^9.4.0",
|
||||
"vite": "^6.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
62
core/http/react-ui/public/locales/de/admin.json
Normal file
62
core/http/react-ui/public/locales/de/admin.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"manage": {
|
||||
"title": "System",
|
||||
"subtitle": "Verwalten Sie installierte Modelle und Backends"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"subtitle": "Konfigurieren Sie die Laufzeiteinstellungen von LocalAI",
|
||||
"saved": "Einstellungen erfolgreich gespeichert",
|
||||
"saveFailed": "Speichern fehlgeschlagen: {{message}}",
|
||||
"loadFailed": "Laden der Einstellungen fehlgeschlagen: {{message}}",
|
||||
"sections": {
|
||||
"branding": "Branding",
|
||||
"watchdog": "Watchdog",
|
||||
"memory": "Speicher",
|
||||
"backends": "Backends",
|
||||
"performance": "Leistung",
|
||||
"tracing": "Tracing",
|
||||
"api": "API & CORS",
|
||||
"p2p": "P2P",
|
||||
"galleries": "Galerien",
|
||||
"apikeys": "API-Schlüssel",
|
||||
"agents": "Agentenaufgaben",
|
||||
"agentpool": "Agenten-Pool",
|
||||
"assistant": "LocalAI Assistant",
|
||||
"responses": "Antworten"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"title": "Backend-Verwaltung",
|
||||
"subtitle": "Entdecken und installieren Sie KI-Backends für Ihre Modelle"
|
||||
},
|
||||
"backendLogs": {
|
||||
"title": "Backend-Logs",
|
||||
"subtitle": "Logs laufender Backends anzeigen",
|
||||
"empty": "Keine Logs verfügbar"
|
||||
},
|
||||
"traces": {
|
||||
"title": "Traces",
|
||||
"subtitle": "Protokollierte API-Anfragen, Antworten und Backend-Operationen anzeigen"
|
||||
},
|
||||
"nodes": {
|
||||
"title": "Verteilte Knoten",
|
||||
"subtitle": "Backend- und Agenten-Worker-Knoten verwalten"
|
||||
},
|
||||
"p2p": {
|
||||
"title": "Verteilte KI-Berechnung",
|
||||
"subtitle": "Skalieren Sie Ihre KI-Workloads über mehrere Geräte mit Peer-to-Peer-Verteilung"
|
||||
},
|
||||
"users": {
|
||||
"title": "Benutzer",
|
||||
"subtitle": "Registrierte Benutzer, Rollen und Einladungen verwalten"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Nutzung",
|
||||
"subtitle": "API-Token-Nutzungsstatistiken"
|
||||
},
|
||||
"explorer": {
|
||||
"title": "Explorer",
|
||||
"subtitle": "Dateien und Konfiguration durchsuchen"
|
||||
}
|
||||
}
|
||||
55
core/http/react-ui/public/locales/de/agents.json
Normal file
55
core/http/react-ui/public/locales/de/agents.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"title": "Agenten",
|
||||
"subtitle": "Verwalten Sie autonome KI-Agenten",
|
||||
"actions": {
|
||||
"agentHub": "Agent Hub",
|
||||
"import": "Importieren",
|
||||
"createAgent": "Agent erstellen",
|
||||
"edit": "Bearbeiten",
|
||||
"chat": "Chat",
|
||||
"export": "Exportieren",
|
||||
"delete": "Löschen",
|
||||
"pause": "Pausieren",
|
||||
"resume": "Fortsetzen"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"events": "Ereignisse",
|
||||
"actions": "Aktionen",
|
||||
"eventsTooltip": "{{count}} Ereignisse - Zum Anzeigen klicken"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Agenten suchen...",
|
||||
"summary_one": "{{shown}} von {{total}} Agent",
|
||||
"summary_other": "{{shown}} von {{total}} Agenten"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigured": "Keine Agenten konfiguriert",
|
||||
"noConfiguredText": "Erstellen Sie einen Agenten, um mit autonomen KI-Workflows zu beginnen.",
|
||||
"browseHub": "Sie wissen nicht, wo Sie anfangen sollen? Durchsuchen Sie den <1>Agent Hub</1>, um vorgefertigte Agentenkonfigurationen zum Importieren zu finden.",
|
||||
"noMatching": "Keine passenden Agenten",
|
||||
"noMatchingText": "Kein Agent entspricht \"{{query}}\""
|
||||
},
|
||||
"sections": {
|
||||
"yourAgents": "Ihre Agenten",
|
||||
"otherUsersAgents": "Agenten anderer Benutzer"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Agent löschen",
|
||||
"message": "Agent \"{{name}}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Löschen"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Laden der Agenten fehlgeschlagen: {{message}}",
|
||||
"deleted": "Agent \"{{name}}\" gelöscht",
|
||||
"deleteFailed": "Löschen des Agenten fehlgeschlagen: {{message}}",
|
||||
"paused": "Agent \"{{name}}\" pausiert",
|
||||
"resumed": "Agent \"{{name}}\" fortgesetzt",
|
||||
"pauseFailed": "Pausieren des Agenten fehlgeschlagen: {{message}}",
|
||||
"resumeFailed": "Fortsetzen des Agenten fehlgeschlagen: {{message}}",
|
||||
"exported": "Agent \"{{name}}\" exportiert",
|
||||
"exportFailed": "Export des Agenten fehlgeschlagen: {{message}}",
|
||||
"parseFailed": "Parsen der Agentendatei fehlgeschlagen: {{message}}"
|
||||
}
|
||||
}
|
||||
112
core/http/react-ui/public/locales/de/auth.json
Normal file
112
core/http/react-ui/public/locales/de/auth.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"login": {
|
||||
"subtitle": "Anmelden, um fortzufahren",
|
||||
"registerSubtitle": "Konto erstellen",
|
||||
"createAdminSubtitle": "Erstellen Sie Ihr Administratorkonto",
|
||||
"tokenSubtitle": "Geben Sie Ihren API-Schlüssel ein, um fortzufahren",
|
||||
"email": "E-Mail",
|
||||
"emailPlaceholder": "du@beispiel.com",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Ihr Name (optional)",
|
||||
"password": "Passwort",
|
||||
"passwordPlaceholder": "Passwort eingeben...",
|
||||
"newPasswordPlaceholder": "Mindestens 8 Zeichen",
|
||||
"confirmPassword": "Passwort bestätigen",
|
||||
"confirmPasswordPlaceholder": "Passwort wiederholen",
|
||||
"inviteCodeLabel": "Einladungscode",
|
||||
"inviteCodeOptional": " (optional — überspringt die Genehmigung)",
|
||||
"inviteCodePlaceholder": "Einladungscode einfügen...",
|
||||
"tokenPlaceholder": "API-Schlüssel eingeben...",
|
||||
"tokenAltPlaceholder": "API-Token eingeben...",
|
||||
"signIn": "Anmelden",
|
||||
"signingIn": "Anmeldung läuft...",
|
||||
"register": "Registrieren",
|
||||
"creatingAccount": "Konto wird erstellt...",
|
||||
"createAdminAccount": "Administratorkonto erstellen",
|
||||
"signInWithGitHub": "Mit GitHub anmelden",
|
||||
"signInWithSSO": "Mit SSO anmelden",
|
||||
"loginWithToken": "Mit Token anmelden",
|
||||
"showTokenLogin": "Mit API-Token anmelden",
|
||||
"hideTokenLogin": "Token-Anmeldung ausblenden",
|
||||
"noAccount": "Noch kein Konto?",
|
||||
"hasAccount": "Bereits ein Konto?",
|
||||
"or": "oder",
|
||||
"errors": {
|
||||
"loginFailed": "Anmeldung fehlgeschlagen",
|
||||
"registrationFailed": "Registrierung fehlgeschlagen",
|
||||
"invalidToken": "Ungültiger Token",
|
||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||
"enterToken": "Bitte einen Token eingeben",
|
||||
"networkError": "Netzwerkfehler",
|
||||
"inviteRequired": "Ein gültiger Einladungscode ist zur Registrierung erforderlich"
|
||||
},
|
||||
"messages": {
|
||||
"registrationPending": "Registrierung erfolgreich, Genehmigung ausstehend."
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Konto",
|
||||
"subtitle": "Profil, Zugangsdaten und API-Schlüssel",
|
||||
"unavailable": "Konto nicht verfügbar",
|
||||
"unavailableText": "Authentifizierung muss aktiviert sein, um Ihr Konto zu verwalten.",
|
||||
"tabs": {
|
||||
"profile": "Profil",
|
||||
"security": "Sicherheit",
|
||||
"apiKeys": "API-Schlüssel"
|
||||
},
|
||||
"profile": {
|
||||
"displayName": "Anzeigename",
|
||||
"displayNameDescription": "Ihr öffentlich angezeigter Name",
|
||||
"avatarUrl": "Avatar-URL",
|
||||
"avatarUrlDescription": "URL Ihres Profilbildes",
|
||||
"avatarUrlPlaceholder": "https://beispiel.com/avatar.png",
|
||||
"save": "Speichern",
|
||||
"saving": "Speichern...",
|
||||
"updated": "Profil aktualisiert",
|
||||
"updateFailed": "Profilaktualisierung fehlgeschlagen: {{message}}"
|
||||
},
|
||||
"security": {
|
||||
"currentPassword": "Aktuelles Passwort",
|
||||
"currentPasswordDescription": "Geben Sie Ihr aktuelles Passwort zur Bestätigung Ihrer Identität ein",
|
||||
"currentPasswordPlaceholder": "Aktuelles Passwort",
|
||||
"newPassword": "Neues Passwort",
|
||||
"newPasswordDescription": "Muss mindestens 8 Zeichen haben",
|
||||
"newPasswordPlaceholder": "Neues Passwort",
|
||||
"confirmPassword": "Passwort bestätigen",
|
||||
"confirmPasswordDescription": "Geben Sie Ihr neues Passwort erneut ein",
|
||||
"confirmPasswordPlaceholder": "Neues Passwort bestätigen",
|
||||
"changePassword": "Passwort ändern",
|
||||
"changing": "Wird geändert...",
|
||||
"changed": "Passwort geändert",
|
||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||
"tooShort": "Das neue Passwort muss mindestens 8 Zeichen haben",
|
||||
"oauthOnly": "Passwortverwaltung ist für {{provider}}-Konten nicht verfügbar."
|
||||
},
|
||||
"apiKeys": {
|
||||
"create": "API-Schlüssel erstellen",
|
||||
"createDescription": "Erstellen Sie einen Schlüssel für den programmgesteuerten Zugriff",
|
||||
"namePlaceholder": "Schlüsselname (z. B. meine-app)",
|
||||
"createButton": "Erstellen",
|
||||
"creating": "Wird erstellt...",
|
||||
"createdToast": "API-Schlüssel erstellt",
|
||||
"createFailed": "API-Schlüssel-Erstellung fehlgeschlagen: {{message}}",
|
||||
"loadFailed": "Laden der API-Schlüssel fehlgeschlagen: {{message}}",
|
||||
"revoke": "Widerrufen",
|
||||
"revokeKey": "Schlüssel widerrufen",
|
||||
"revokeTitle": "API-Schlüssel widerrufen",
|
||||
"revokeMessage": "API-Schlüssel \"{{name}}\" widerrufen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"revoked": "API-Schlüssel widerrufen",
|
||||
"revokeFailed": "Widerruf des API-Schlüssels fehlgeschlagen: {{message}}",
|
||||
"copyNow": "Jetzt kopieren — dieser Schlüssel wird nicht erneut angezeigt",
|
||||
"copiedToast": "In die Zwischenablage kopiert",
|
||||
"copyFailed": "Kopieren fehlgeschlagen",
|
||||
"empty": "Noch keine API-Schlüssel. Erstellen Sie oben einen für programmgesteuerten Zugriff.",
|
||||
"lastUsed": "zuletzt verwendet {{date}}"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Seite nicht gefunden",
|
||||
"text": "Sieht aus, als wäre diese Seite verloren gegangen. Bringen wir Sie zurück auf den Weg.",
|
||||
"goHome": "Zur Startseite"
|
||||
}
|
||||
}
|
||||
116
core/http/react-ui/public/locales/de/chat.json
Normal file
116
core/http/react-ui/public/locales/de/chat.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"activity": {
|
||||
"thought": "Gedanke",
|
||||
"tool": "Werkzeug",
|
||||
"result": "Ergebnis",
|
||||
"toolResult": "Ergebnis von {{name}}",
|
||||
"thinking": "Denke nach..."
|
||||
},
|
||||
"header": {
|
||||
"manageModeTooltip": "Dieser Chat kann Modelle installieren, Konfigurationen bearbeiten und Backends verwalten, indem mit LocalAI gesprochen wird.",
|
||||
"modelInfo": "Modell-Info",
|
||||
"chatSettings": "Chat-Einstellungen",
|
||||
"modelInfoTitle": "Modell-Info: {{model}}",
|
||||
"editConfig": "Konfiguration bearbeiten",
|
||||
"close": "Schließen"
|
||||
},
|
||||
"modelInfo": {
|
||||
"backend": "Backend",
|
||||
"modelFile": "Modelldatei",
|
||||
"contextSize": "Kontextgröße",
|
||||
"threads": "Threads",
|
||||
"mcp": "MCP",
|
||||
"configured": "Konfiguriert",
|
||||
"chatTemplate": "Chat-Vorlage",
|
||||
"yes": "Ja",
|
||||
"gpuLayers": "GPU-Schichten"
|
||||
},
|
||||
"context": {
|
||||
"label": "Kontext: {{percent}}%",
|
||||
"labelWithTokens": "Kontext: {{percent}}% ({{tokens}} Tokens)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Chat-Einstellungen",
|
||||
"manageMode": "Verwaltungsmodus",
|
||||
"manageModeDesc": "Erlauben Sie diesem Chat, Modelle zu installieren, Backends zu wechseln und Konfigurationen zu bearbeiten — durch Gespräche mit LocalAI.",
|
||||
"systemPrompt": "System-Prompt",
|
||||
"systemPromptPlaceholder": "Sie sind ein hilfreicher Assistent...",
|
||||
"temperature": "Temperatur",
|
||||
"topP": "Top P",
|
||||
"topK": "Top K",
|
||||
"contextSize": "Kontextgröße",
|
||||
"contextSizePlaceholder": "2048",
|
||||
"clearHistory": "Chat-Verlauf löschen"
|
||||
},
|
||||
"empty": {
|
||||
"manageTitle": "LocalAI per Chat verwalten",
|
||||
"manageText": "Bitten Sie um die Installation von Modellen, das Wechseln von Backends, das Bearbeiten von Konfigurationen oder das Prüfen des Status. Der Assistent fasst die Aktionen zusammen und wartet auf Ihre Bestätigung, bevor etwas geändert wird.",
|
||||
"startTitle": "Beginnen Sie ein Gespräch",
|
||||
"readyText": "Bereit zum Chatten mit {{model}}",
|
||||
"selectModelText": "Wählen Sie oben ein Modell aus, um zu beginnen",
|
||||
"suggestionsManage": [
|
||||
"Was ist installiert?",
|
||||
"Installiere ein Chat-Modell",
|
||||
"Zeige den Systemstatus",
|
||||
"Aktualisiere ein Backend"
|
||||
],
|
||||
"suggestionsChat": [
|
||||
"Erkläre, wie das funktioniert",
|
||||
"Hilf mir, Code zu schreiben",
|
||||
"Fasse ein Dokument zusammen",
|
||||
"Brainstorme Ideen"
|
||||
],
|
||||
"recent": "Zuletzt",
|
||||
"noMessages": "Noch keine Nachrichten",
|
||||
"hintEnter": "Enter zum Senden",
|
||||
"hintShiftEnter": "Shift+Enter für neue Zeile",
|
||||
"hintAttach": "Dateien anhängen"
|
||||
},
|
||||
"errors": {
|
||||
"viewTraces": "Traces für Details anzeigen"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Kopieren",
|
||||
"regenerate": "Neu generieren"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Modell wird übertragen...",
|
||||
"transferringTo": "Modell wird zu {{node}} übertragen..."
|
||||
},
|
||||
"tokens": {
|
||||
"perSec": "{{count}} tok/s",
|
||||
"peak": "Spitze: {{count}} tok/s",
|
||||
"usage": "{{prompt}}p + {{completion}}c = {{total}}"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Nachricht...",
|
||||
"attachFile": "Datei anhängen",
|
||||
"stopGenerating": "Generierung stoppen",
|
||||
"canvasTitle": "Canvas — Codeblöcke und Medien in ein Seitenpanel zur Vorschau, zum Kopieren und Herunterladen extrahieren",
|
||||
"canvasLabel": "Canvas",
|
||||
"openCanvas": "Canvas-Panel öffnen"
|
||||
},
|
||||
"deleteAllDialog": {
|
||||
"title": "Alle Chats löschen",
|
||||
"message": "Alle Chats löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Alle löschen"
|
||||
},
|
||||
"toasts": {
|
||||
"selectModel": "Bitte wählen Sie ein Modell",
|
||||
"copied": "In die Zwischenablage kopiert"
|
||||
},
|
||||
"menu": {
|
||||
"trigger": "Chats",
|
||||
"triggerTitle": "Unterhaltungen (Strg/Cmd+K)",
|
||||
"search": "Unterhaltungen suchen...",
|
||||
"clearSearch": "Suche löschen",
|
||||
"noMatch": "Keine Unterhaltung entspricht Ihrer Suche",
|
||||
"noConversations": "Noch keine Unterhaltungen",
|
||||
"rename": "Umbenennen",
|
||||
"exportMarkdown": "Als Markdown exportieren",
|
||||
"deleteChat": "Chat löschen",
|
||||
"newChat": "Neuer Chat",
|
||||
"clearAll": "Alle löschen",
|
||||
"deleteAllTitle": "Alle Unterhaltungen löschen"
|
||||
}
|
||||
}
|
||||
43
core/http/react-ui/public/locales/de/collections.json
Normal file
43
core/http/react-ui/public/locales/de/collections.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"title": "Wissensdatenbank",
|
||||
"subtitle": "Verwalten Sie Dokumentensammlungen für RAG der Agenten",
|
||||
"newPlaceholder": "Name der neuen Sammlung...",
|
||||
"actions": {
|
||||
"create": "Erstellen",
|
||||
"creating": "Wird erstellt...",
|
||||
"details": "Details",
|
||||
"reset": "Zurücksetzen",
|
||||
"delete": "Löschen",
|
||||
"viewDetails": "Details anzeigen",
|
||||
"resetCollection": "Sammlung zurücksetzen",
|
||||
"deleteCollection": "Sammlung löschen"
|
||||
},
|
||||
"sections": {
|
||||
"yourCollections": "Ihre Sammlungen",
|
||||
"otherUsersCollections": "Sammlungen anderer Benutzer"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Noch keine Sammlungen",
|
||||
"text": "Mit Sammlungen organisieren Sie Dokumente in Wissensdatenbanken, die Agenten mit RAG (Retrieval-Augmented Generation) durchsuchen können. Erstellen Sie oben eine Sammlung, um zu beginnen.",
|
||||
"noPersonal": "Sie haben noch keine Sammlungen."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Sammlung löschen",
|
||||
"message": "Sammlung \"{{name}}\" löschen? Dies entfernt alle Einträge und kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Löschen"
|
||||
},
|
||||
"resetDialog": {
|
||||
"title": "Sammlung zurücksetzen",
|
||||
"message": "Sammlung \"{{name}}\" zurücksetzen? Dies entfernt alle Einträge, behält aber die Sammlung.",
|
||||
"confirm": "Zurücksetzen"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Laden der Sammlungen fehlgeschlagen: {{message}}",
|
||||
"created": "Sammlung \"{{name}}\" erstellt",
|
||||
"createFailed": "Erstellen der Sammlung fehlgeschlagen: {{message}}",
|
||||
"deleted": "Sammlung \"{{name}}\" gelöscht",
|
||||
"deleteFailed": "Löschen der Sammlung fehlgeschlagen: {{message}}",
|
||||
"reset": "Sammlung \"{{name}}\" zurückgesetzt",
|
||||
"resetFailed": "Zurücksetzen der Sammlung fehlgeschlagen: {{message}}"
|
||||
}
|
||||
}
|
||||
109
core/http/react-ui/public/locales/de/common.json
Normal file
109
core/http/react-ui/public/locales/de/common.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "Speichern",
|
||||
"saving": "Speichern...",
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen",
|
||||
"confirm": "Bestätigen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"add": "Hinzufügen",
|
||||
"remove": "Entfernen",
|
||||
"create": "Erstellen",
|
||||
"update": "Aktualisieren",
|
||||
"refresh": "Aktualisieren",
|
||||
"reload": "Neu laden",
|
||||
"retry": "Erneut versuchen",
|
||||
"search": "Suchen",
|
||||
"filter": "Filtern",
|
||||
"clear": "Leeren",
|
||||
"reset": "Zurücksetzen",
|
||||
"apply": "Übernehmen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"previous": "Vorherige",
|
||||
"open": "Öffnen",
|
||||
"submit": "Absenden",
|
||||
"select": "Auswählen",
|
||||
"selectAll": "Alle auswählen",
|
||||
"copy": "Kopieren",
|
||||
"copied": "Kopiert",
|
||||
"download": "Herunterladen",
|
||||
"upload": "Hochladen",
|
||||
"import": "Importieren",
|
||||
"export": "Exportieren",
|
||||
"view": "Ansehen",
|
||||
"details": "Details",
|
||||
"settings": "Einstellungen",
|
||||
"help": "Hilfe",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"loading": "Lädt..."
|
||||
},
|
||||
"status": {
|
||||
"loading": "Lädt...",
|
||||
"saving": "Speichern...",
|
||||
"saved": "Gespeichert",
|
||||
"ready": "Bereit",
|
||||
"running": "Läuft",
|
||||
"stopped": "Gestoppt",
|
||||
"starting": "Wird gestartet...",
|
||||
"stopping": "Wird gestoppt...",
|
||||
"pending": "Ausstehend",
|
||||
"active": "Aktiv",
|
||||
"inactive": "Inaktiv",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"error": "Fehler",
|
||||
"success": "Erfolg",
|
||||
"warning": "Warnung",
|
||||
"info": "Info",
|
||||
"empty": "Keine Einträge",
|
||||
"none": "Keine",
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
"dialogs": {
|
||||
"confirmDelete": {
|
||||
"title": "Löschen bestätigen",
|
||||
"message": "Möchten Sie dies wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Löschen",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"unsavedChanges": {
|
||||
"title": "Nicht gespeicherte Änderungen",
|
||||
"message": "Sie haben nicht gespeicherte Änderungen. Möchten Sie diese verwerfen?",
|
||||
"discard": "Verwerfen",
|
||||
"keepEditing": "Weiter bearbeiten"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"required": "Erforderlich",
|
||||
"optional": "Optional",
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"type": "Typ",
|
||||
"value": "Wert",
|
||||
"search": "Suchen...",
|
||||
"selectPlaceholder": "Option auswählen..."
|
||||
},
|
||||
"time": {
|
||||
"now": "jetzt",
|
||||
"secondsAgo_one": "vor {{count}} Sekunde",
|
||||
"secondsAgo_other": "vor {{count}} Sekunden",
|
||||
"minutesAgo_one": "vor {{count}} Minute",
|
||||
"minutesAgo_other": "vor {{count}} Minuten",
|
||||
"hoursAgo_one": "vor {{count}} Stunde",
|
||||
"hoursAgo_other": "vor {{count}} Stunden",
|
||||
"daysAgo_one": "vor {{count}} Tag",
|
||||
"daysAgo_other": "vor {{count}} Tagen"
|
||||
},
|
||||
"units": {
|
||||
"bytes": "B",
|
||||
"kilobytes": "KB",
|
||||
"megabytes": "MB",
|
||||
"gigabytes": "GB",
|
||||
"terabytes": "TB"
|
||||
}
|
||||
}
|
||||
17
core/http/react-ui/public/locales/de/errors.json
Normal file
17
core/http/react-ui/public/locales/de/errors.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"generic": "Etwas ist schiefgelaufen",
|
||||
"network": "Netzwerkfehler. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
|
||||
"unauthorized": "Sie sind nicht berechtigt, diese Aktion auszuführen.",
|
||||
"forbidden": "Zugriff verweigert.",
|
||||
"notFound": "Die angeforderte Ressource wurde nicht gefunden.",
|
||||
"serverError": "Serverfehler. Bitte versuchen Sie es später erneut.",
|
||||
"loadFailed": "Laden fehlgeschlagen: {{message}}",
|
||||
"saveFailed": "Speichern fehlgeschlagen: {{message}}",
|
||||
"deleteFailed": "Löschen fehlgeschlagen: {{message}}",
|
||||
"updateFailed": "Aktualisieren fehlgeschlagen: {{message}}",
|
||||
"createFailed": "Erstellen fehlgeschlagen: {{message}}",
|
||||
"operationFailed": "Vorgang fehlgeschlagen: {{message}}",
|
||||
"invalidInput": "Ungültige Eingabe. Bitte überprüfen Sie das Formular und versuchen Sie es erneut.",
|
||||
"tryAgain": "Bitte versuchen Sie es erneut.",
|
||||
"contactAdmin": "Wenn das Problem weiterhin besteht, wenden Sie sich an Ihren Administrator."
|
||||
}
|
||||
66
core/http/react-ui/public/locales/de/home.json
Normal file
66
core/http/react-ui/public/locales/de/home.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"cluster": {
|
||||
"vram": "Cluster-VRAM",
|
||||
"ram": "Cluster-RAM",
|
||||
"nodesOnline": "{{healthy}}/{{total}} Knoten online"
|
||||
},
|
||||
"resourceGpu": "GPU",
|
||||
"resourceRam": "RAM",
|
||||
"assistant": {
|
||||
"title": "LocalAI per Chat verwalten",
|
||||
"description": "Modelle installieren, Backends wechseln, Konfigurationen bearbeiten und Status prüfen — durch Gespräche mit LocalAI.",
|
||||
"open": "Assistent öffnen",
|
||||
"tooltip": "LocalAI per Chat verwalten"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Nachricht...",
|
||||
"attachImage": "Bild anhängen",
|
||||
"attachAudio": "Audio anhängen",
|
||||
"attachFile": "Datei anhängen",
|
||||
"enterToSend": "Enter zum Senden",
|
||||
"selectModelFirst": "Bitte zuerst ein Modell auswählen",
|
||||
"sendMessage": "Nachricht senden",
|
||||
"selectModelToast": "Bitte zuerst ein Modell auswählen"
|
||||
},
|
||||
"quickLinks": {
|
||||
"manageByChat": "Per Chat verwalten",
|
||||
"installedModels": "Installierte Modelle",
|
||||
"browseGallery": "Galerie durchsuchen",
|
||||
"importModel": "Modell importieren",
|
||||
"documentation": "Dokumentation"
|
||||
},
|
||||
"loadedModels": {
|
||||
"count_one": "{{count}} Modell geladen",
|
||||
"count_other": "{{count}} Modelle geladen",
|
||||
"stop": "Modell stoppen",
|
||||
"stopAll": "Alle stoppen"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Modell stoppen",
|
||||
"message": "Modell {{model}} stoppen?",
|
||||
"confirm": "{{model}} stoppen",
|
||||
"stopAllTitle": "Alle Modelle stoppen",
|
||||
"stopAllMessage": "Alle {{count}} geladenen Modelle stoppen?",
|
||||
"stopAllConfirm": "Alle stoppen",
|
||||
"stoppedToast": "{{model}} gestoppt",
|
||||
"allStoppedToast": "Alle Modelle gestoppt",
|
||||
"stopFailed": "Stopp fehlgeschlagen: {{message}}"
|
||||
},
|
||||
"wizard": {
|
||||
"getStarted": "Loslegen mit {{name}}",
|
||||
"intro": "Installieren Sie Ihr erstes Modell, um zu beginnen. Durchsuchen Sie die Galerie oder importieren Sie ein eigenes.",
|
||||
"steps": {
|
||||
"step1Title": "Modellgalerie durchsuchen",
|
||||
"step1Body": "Finden Sie das richtige Modell für Ihre Anforderungen aus unserer kuratierten Sammlung.",
|
||||
"step2Title": "Modell installieren",
|
||||
"step2Body": "Klicken Sie auf Installieren, um es automatisch herunterzuladen und zu konfigurieren.",
|
||||
"step3Title": "Chat starten",
|
||||
"step3Body": "Chatten Sie mit Ihrem Modell direkt im Browser oder nutzen Sie die API."
|
||||
},
|
||||
"browseGallery": "Modellgalerie durchsuchen",
|
||||
"importModel": "Modell importieren",
|
||||
"docs": "Dokumentation",
|
||||
"noModelsTitle": "Keine Modelle verfügbar",
|
||||
"noModelsBody": "Es sind noch keine Modelle installiert. Bitten Sie Ihren Administrator, Modelle einzurichten, damit Sie mit dem Chatten beginnen können."
|
||||
}
|
||||
}
|
||||
142
core/http/react-ui/public/locales/de/importModel.json
Normal file
142
core/http/react-ui/public/locales/de/importModel.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"title": "Neues Modell importieren",
|
||||
"subtitle": {
|
||||
"simple": "Modell von einer URI importieren — Auto-Erkennung wählt das Backend.",
|
||||
"powerYaml": "Vollständige Modell-YAML-Konfiguration schreiben.",
|
||||
"powerPrefs": "Detaillierte Importeinstellungen."
|
||||
},
|
||||
"actions": {
|
||||
"import": "Modell importieren",
|
||||
"importing": "Importieren...",
|
||||
"create": "Erstellen",
|
||||
"saving": "Speichern...",
|
||||
"browseHF": "Modelle auf HF durchsuchen",
|
||||
"addCustom": "Benutzerdefiniert hinzufügen",
|
||||
"copy": "Kopieren"
|
||||
},
|
||||
"form": {
|
||||
"modelUri": "Modell-URI",
|
||||
"uriPlaceholder": "huggingface://TheBloke/Llama-2-7B-Chat-GGUF oder https://example.com/model.gguf",
|
||||
"uriHint": "Geben Sie die URI oder den Pfad der zu importierenden Modelldatei ein",
|
||||
"supportedFormats": "Unterstützte URI-Formate",
|
||||
"options": "Optionen",
|
||||
"preferences": "Einstellungen (optional)",
|
||||
"commonPreferences": "Allgemeine Einstellungen",
|
||||
"customPreferences": "Benutzerdefinierte Einstellungen",
|
||||
"customKeyValueHint": "Fügen Sie benutzerdefinierte Schlüssel-Wert-Paare für erweiterte Konfiguration hinzu.",
|
||||
"preferenceKey": "Einstellungsschlüssel für Zeile {{index}}",
|
||||
"preferenceValue": "Einstellungswert für Zeile {{index}}",
|
||||
"removePref": "Diese Einstellung entfernen",
|
||||
"key": "Schlüssel",
|
||||
"value": "Wert",
|
||||
"backend": "Backend",
|
||||
"backendAuto": "Auto-Erkennung (basierend auf URI)",
|
||||
"backendLoading": "Backends werden geladen…",
|
||||
"backendSearch": "Backends suchen...",
|
||||
"backendHint": "Erzwingen Sie ein bestimmtes Backend. Leer lassen für Auto-Erkennung von der URI. Mit \"manuelle Auswahl\" markierte Einträge sind nicht auto-erkennbar — wählen Sie sie selbst, wenn Sie wissen, was das Modell benötigt.",
|
||||
"backendErrorHint": "Backend-Liste konnte nicht geladen werden — nur Auto-Erkennung.",
|
||||
"backendNotInstalled": "Dieses Backend ist noch nicht installiert. Beim Senden des Imports wird es zuerst heruntergeladen.",
|
||||
"modelName": "Modellname",
|
||||
"modelNamePlaceholder": "Leer lassen, um den Dateinamen zu verwenden",
|
||||
"modelNameHint": "Benutzerdefinierter Name für das Modell. Wenn leer, wird der Dateiname verwendet.",
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Leer lassen für Standardbeschreibung",
|
||||
"descriptionHint": "Benutzerdefinierte Beschreibung für das Modell.",
|
||||
"quantizations": "Quantisierungen",
|
||||
"quantizationsPlaceholder": "q4_k_m,q4_k_s,q3_k_m (kommasepariert)",
|
||||
"quantizationsHint": "Bevorzugte Quantisierungen (kommasepariert). Leer lassen für Standard (q4_k_m).",
|
||||
"mmprojQuantizations": "MMProj-Quantisierungen",
|
||||
"mmprojQuantizationsPlaceholder": "fp16,fp32 (kommasepariert)",
|
||||
"mmprojQuantizationsHint": "Bevorzugte MMProj-Quantisierungen. Leer lassen für Standard (fp16).",
|
||||
"embeddings": "Embeddings",
|
||||
"embeddingsHint": "Embeddings-Unterstützung für dieses Modell aktivieren.",
|
||||
"modelType": "Modelltyp",
|
||||
"modelTypePlaceholder": "AutoModelForCausalLM (für transformers-Backend)",
|
||||
"modelTypeHint": "Modelltyp für transformers-Backend. Beispiele: AutoModelForCausalLM, SentenceTransformer, Mamba.",
|
||||
"pipelineType": "Pipeline-Typ",
|
||||
"pipelineTypeHint": "Pipeline-Typ für diffusers-Backend.",
|
||||
"schedulerType": "Scheduler-Typ",
|
||||
"schedulerTypePlaceholder": "k_dpmpp_2m (optional)",
|
||||
"schedulerTypeHint": "Scheduler-Typ für diffusers-Backend. Beispiele: k_dpmpp_2m, euler_a, ddim.",
|
||||
"enableParameters": "Aktivierte Parameter",
|
||||
"enableParametersPlaceholder": "negative_prompt,num_inference_steps (kommasepariert)",
|
||||
"enableParametersHint": "Aktivierte Parameter für diffusers-Backend (kommasepariert).",
|
||||
"cuda": "CUDA",
|
||||
"cudaHint": "CUDA-Unterstützung für GPU-Beschleunigung aktivieren.",
|
||||
"yamlEditor": "YAML-Konfigurationseditor",
|
||||
"manualPick": "manuelle Auswahl",
|
||||
"manualPickTooltip": "Auto-Erkennung leitet nicht zu diesem Backend. Wählen Sie es hier, wenn Sie wissen, dass Sie das wollen."
|
||||
},
|
||||
"modality": {
|
||||
"text": "Text-LLM",
|
||||
"asr": "Spracherkennung",
|
||||
"tts": "Sprachsynthese",
|
||||
"image": "Bild / Video",
|
||||
"embeddings": "Embeddings",
|
||||
"reranker": "Reranker",
|
||||
"detection": "Objekterkennung",
|
||||
"vad": "Sprachaktivitätserkennung",
|
||||
"other": "Sonstige"
|
||||
},
|
||||
"powerTabs": {
|
||||
"ariaLabel": "Erweiterter Modus-Tab",
|
||||
"preferences": "Einstellungen",
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"switchDialog": {
|
||||
"title": "Benutzerdefinierte Einstellungen behalten?",
|
||||
"body": "Beim Wechsel in den Einfach-Modus werden Einstellungen außerhalb von Backend, Name und Beschreibung ausgeblendet. Sie werden beim Import dennoch gesendet.",
|
||||
"cancel": "Abbrechen",
|
||||
"discard": "Verwerfen & wechseln",
|
||||
"keep": "Behalten & wechseln"
|
||||
},
|
||||
"estimate": {
|
||||
"title": "Geschätzte Anforderungen",
|
||||
"download": "Download: {{size}}",
|
||||
"vram": "VRAM: {{vram}}"
|
||||
},
|
||||
"toasts": {
|
||||
"noUri": "Bitte eine Modell-URI eingeben",
|
||||
"noYaml": "Bitte YAML-Konfiguration eingeben",
|
||||
"started": "Import gestartet! Fortschritt wird verfolgt...",
|
||||
"startedWithMeta": "Import gestartet! Fortschritt wird verfolgt... ({{meta}})",
|
||||
"imported": "Modell erfolgreich importiert!",
|
||||
"importedYaml": "Modellkonfiguration erfolgreich importiert!",
|
||||
"importFailed": "Import fehlgeschlagen: {{message}}",
|
||||
"startImportFailed": "Start des Imports fehlgeschlagen: {{message}}",
|
||||
"backendsLoadFailed": "Backend-Liste konnte nicht geladen werden — nur Auto-Erkennung wird verwendet",
|
||||
"modalityClearedBackend": "Backend-Auswahl gelöscht — sie war nicht in der {{label}}-Gruppe.",
|
||||
"copied": "In die Zwischenablage kopiert"
|
||||
},
|
||||
"uriFormats": {
|
||||
"huggingface": {
|
||||
"title": "HuggingFace",
|
||||
"standard": "Standard HuggingFace-Format",
|
||||
"short": "Kurzes HuggingFace-Format",
|
||||
"fullUrl": "Vollständige HuggingFace-URL"
|
||||
},
|
||||
"http": {
|
||||
"title": "HTTP/HTTPS-URLs",
|
||||
"direct": "Direkter Download von einer beliebigen HTTPS-URL"
|
||||
},
|
||||
"local": {
|
||||
"title": "Lokale Dateien",
|
||||
"filePath": "Lokaler Dateipfad (absolut)",
|
||||
"directYaml": "Direkte lokale YAML-Konfigurationsdatei"
|
||||
},
|
||||
"oci": {
|
||||
"title": "OCI-Registry",
|
||||
"registry": "OCI-Container-Registry",
|
||||
"tarball": "Lokale OCI-Tarball-Datei"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"model": "Ollama-Modellformat"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "YAML-Konfigurationsdateien",
|
||||
"remote": "Remote-YAML-Konfigurationsdatei",
|
||||
"local": "Lokale YAML-Konfigurationsdatei"
|
||||
}
|
||||
}
|
||||
}
|
||||
154
core/http/react-ui/public/locales/de/media.json
Normal file
154
core/http/react-ui/public/locales/de/media.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Bilder",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Audio"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Bildgenerierung",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Beschreiben Sie das Bild, das Sie generieren möchten...",
|
||||
"negativePrompt": "Negativer Prompt",
|
||||
"negativePromptPlaceholder": "Was vermieden werden soll...",
|
||||
"size": "Größe",
|
||||
"count": "Anzahl (1-4)",
|
||||
"advanced": "Erweiterte Einstellungen",
|
||||
"imageInputs": "Bildeingaben",
|
||||
"steps": "Schritte",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Zufällig",
|
||||
"sourceImage": "Ausgangsbild (img2img)",
|
||||
"refImages": "Referenzbilder",
|
||||
"refImagesAdded_one": "{{count}} Bild hinzugefügt",
|
||||
"refImagesAdded_other": "{{count}} Bilder hinzugefügt"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generierte Bilder erscheinen hier",
|
||||
"toasts": {
|
||||
"noPrompt": "Bitte einen Prompt eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"noResults": "Keine Bilder generiert"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Videogenerierung",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Beschreiben Sie das Video, das Sie generieren möchten...",
|
||||
"duration": "Dauer (s)",
|
||||
"fps": "FPS",
|
||||
"size": "Größe",
|
||||
"advanced": "Erweiterte Einstellungen",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Zufällig"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Video generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generierte Videos erscheinen hier",
|
||||
"toasts": {
|
||||
"noPrompt": "Bitte einen Prompt eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"noResults": "Keine Videos generiert"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Text zu Sprache",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"voice": "Stimme",
|
||||
"voicePlaceholder": "Optionale Stimm-ID",
|
||||
"input": "Text",
|
||||
"inputPlaceholder": "Text zum Synthetisieren eingeben..."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Audio generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generiertes Audio erscheint hier",
|
||||
"toasts": {
|
||||
"noText": "Bitte Text eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"generateFailed": "Generierung fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Audiogenerierung",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Beschreiben Sie den Klang, den Sie generieren möchten...",
|
||||
"duration": "Dauer (s)",
|
||||
"language": "Sprache",
|
||||
"vocalLanguage": "Vokalsprache",
|
||||
"lyrics": "Liedtext (optional)",
|
||||
"lyricsPlaceholder": "Liedtext für Vokalgenerierung",
|
||||
"advanced": "Erweiterte Einstellungen",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Zufällig"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generiertes Audio erscheint hier",
|
||||
"toasts": {
|
||||
"noPrompt": "Bitte einen Prompt eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"generateFailed": "Generierung fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Sprechen",
|
||||
"subtitle": "Sprachunterhaltung in Echtzeit",
|
||||
"actions": {
|
||||
"start": "Sitzung starten",
|
||||
"stop": "Sitzung beenden",
|
||||
"connecting": "Verbinden...",
|
||||
"muted": "Stumm",
|
||||
"mute": "Stummschalten",
|
||||
"unmute": "Stumm aufheben"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"voice": "Stimme",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Sprache",
|
||||
"languagePlaceholder": "de",
|
||||
"instructions": "Anweisungen",
|
||||
"instructionsPlaceholder": "Persönlichkeit des Assistenten festlegen..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Inaktiv",
|
||||
"connecting": "Verbinden...",
|
||||
"listening": "Höre zu...",
|
||||
"speaking": "Spricht...",
|
||||
"ended": "Sitzung beendet"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Wählen Sie zuerst ein Modell",
|
||||
"connectFailed": "Verbindung fehlgeschlagen: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Verlauf",
|
||||
"empty": "Noch kein Verlauf",
|
||||
"deleteEntry": "Eintrag löschen",
|
||||
"clear": "Verlauf löschen",
|
||||
"clearTitle": "Gesamten Verlauf löschen",
|
||||
"clearMessage": "Alle Verlaufseinträge entfernen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"clearConfirm": "Löschen",
|
||||
"cleared": "Verlauf gelöscht"
|
||||
}
|
||||
}
|
||||
85
core/http/react-ui/public/locales/de/models.json
Normal file
85
core/http/react-ui/public/locales/de/models.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"title": "Modelle installieren",
|
||||
"subtitle": "Durchsuchen und installieren Sie KI-Modelle aus der Galerie",
|
||||
"stats": {
|
||||
"available": "Verfügbar",
|
||||
"installed": "Installiert"
|
||||
},
|
||||
"actions": {
|
||||
"addModel": "Modell hinzufügen",
|
||||
"importModel": "Modell importieren",
|
||||
"install": "Installieren",
|
||||
"reinstall": "Neu installieren",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Alle",
|
||||
"llm": "LLM",
|
||||
"image": "Bild",
|
||||
"multimodal": "Multimodal",
|
||||
"vision": "Vision",
|
||||
"tts": "TTS",
|
||||
"stt": "STT",
|
||||
"embedding": "Embedding",
|
||||
"rerank": "Rerank",
|
||||
"allBackends": "Alle Backends",
|
||||
"searchBackends": "Backends suchen..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Modelle suchen...",
|
||||
"clearFilters": "Filter zurücksetzen"
|
||||
},
|
||||
"table": {
|
||||
"modelName": "Modellname",
|
||||
"description": "Beschreibung",
|
||||
"backend": "Backend",
|
||||
"sizeVram": "Größe / VRAM",
|
||||
"status": "Status",
|
||||
"actions": "Aktionen",
|
||||
"size": "Größe: {{size}}",
|
||||
"vram": "VRAM: {{vram}}",
|
||||
"fits": "Passt",
|
||||
"mayNotFit": "Passt möglicherweise nicht",
|
||||
"trustRemoteCode": "Trust Remote Code",
|
||||
"installing": "Installation",
|
||||
"installingPct": "Installation · {{percent}}%",
|
||||
"installed": "Installiert",
|
||||
"notInstalled": "Nicht installiert"
|
||||
},
|
||||
"detail": {
|
||||
"description": "Beschreibung",
|
||||
"gallery": "Galerie",
|
||||
"backend": "Backend",
|
||||
"size": "Größe",
|
||||
"vram": "VRAM",
|
||||
"license": "Lizenz",
|
||||
"tags": "Tags",
|
||||
"links": "Links",
|
||||
"warning": "Warnung",
|
||||
"files": "Dateien",
|
||||
"fitsGpu": "Passt in die GPU",
|
||||
"mayNotFitGpu": "Passt möglicherweise nicht in die GPU",
|
||||
"requiresTrustRemoteCode": "Erfordert Trust Remote Code",
|
||||
"fileCount_one": "{{count}} Datei",
|
||||
"fileCount_other": "{{count}} Dateien",
|
||||
"filename": "Dateiname",
|
||||
"uri": "URI",
|
||||
"sha256": "SHA256"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Keine Modelle gefunden",
|
||||
"withFilters": "Keine Modelle entsprechen den aktuellen Such- oder Filterkriterien.",
|
||||
"noFilters": "Die Modellgalerie ist leer."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Modell löschen",
|
||||
"message": "Modell {{model}} löschen?",
|
||||
"confirm": "{{model}} löschen",
|
||||
"deletingToast": "{{model}} wird gelöscht..."
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Laden der Modelle fehlgeschlagen: {{message}}",
|
||||
"installFailed": "Installation fehlgeschlagen: {{message}}",
|
||||
"deleteFailed": "Löschen fehlgeschlagen: {{message}}"
|
||||
}
|
||||
}
|
||||
51
core/http/react-ui/public/locales/de/nav.json
Normal file
51
core/http/react-ui/public/locales/de/nav.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"appName": "LocalAI",
|
||||
"openMenu": "Menü öffnen",
|
||||
"closeMenu": "Menü schließen",
|
||||
"primaryNavigation": "Hauptnavigation",
|
||||
"switchToLightMode": "Zum hellen Modus wechseln",
|
||||
"switchToDarkMode": "Zum dunklen Modus wechseln",
|
||||
"expandSidebar": "Seitenleiste erweitern",
|
||||
"collapseSidebar": "Seitenleiste einklappen",
|
||||
"changeLanguage": "Sprache ändern",
|
||||
"logout": "Abmelden",
|
||||
"accountSettings": "Kontoeinstellungen",
|
||||
"account": "Konto",
|
||||
"accountFor": "Konto: {{name}}",
|
||||
"sections": {
|
||||
"tools": "Werkzeuge",
|
||||
"biometrics": "Biometrie",
|
||||
"agents": "Agenten",
|
||||
"system": "System"
|
||||
},
|
||||
"items": {
|
||||
"home": "Start",
|
||||
"installModels": "Modelle installieren",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Sprechen",
|
||||
"fineTune": "Fine-Tuning (Experimentell)",
|
||||
"quantize": "Quantisierung (Experimentell)",
|
||||
"faceRecognition": "Gesichtserkennung",
|
||||
"voiceRecognition": "Spracherkennung",
|
||||
"agents": "Agenten",
|
||||
"skills": "Fähigkeiten",
|
||||
"memory": "Speicher",
|
||||
"mcpJobs": "MCP CI-Aufgaben",
|
||||
"usage": "Nutzung",
|
||||
"users": "Benutzer",
|
||||
"backends": "Backends",
|
||||
"traces": "Traces",
|
||||
"nodes": "Knoten",
|
||||
"swarm": "Swarm",
|
||||
"system": "System",
|
||||
"settings": "Einstellungen",
|
||||
"api": "API"
|
||||
},
|
||||
"footer": {
|
||||
"github": "GitHub",
|
||||
"documentation": "Dokumentation",
|
||||
"author": "Autor",
|
||||
"copyright": "© 2023-{{year}} {{author}}"
|
||||
}
|
||||
}
|
||||
79
core/http/react-ui/public/locales/de/skills.json
Normal file
79
core/http/react-ui/public/locales/de/skills.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "Fähigkeiten",
|
||||
"subtitle": "Verwalten Sie Agentenfähigkeiten (wiederverwendbare Anweisungen und Ressourcen)",
|
||||
"unavailable": {
|
||||
"subtitle": "Der Fähigkeiten-Dienst ist nicht verfügbar oder der Index wird neu erstellt. Versuchen Sie es in einem Moment erneut.",
|
||||
"retry": "Erneut versuchen"
|
||||
},
|
||||
"actions": {
|
||||
"newSkill": "Neue Fähigkeit",
|
||||
"createSkill": "Fähigkeit erstellen",
|
||||
"import": "Importieren",
|
||||
"importing": "Importieren...",
|
||||
"gitRepos": "Git-Repos",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"export": "Exportieren",
|
||||
"sync": "Synchronisieren",
|
||||
"addRepo": "Repo hinzufügen",
|
||||
"adding": "Hinzufügen...",
|
||||
"remove": "Entfernen",
|
||||
"enable": "Aktivieren",
|
||||
"disable": "Deaktivieren"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Fähigkeiten suchen..."
|
||||
},
|
||||
"git": {
|
||||
"title": "Git-Repositories",
|
||||
"description": "Fügen Sie Git-Repositories hinzu, aus denen Fähigkeiten geladen werden. Fähigkeiten erscheinen nach der Synchronisierung in der Liste.",
|
||||
"urlPlaceholder": "https://github.com/user/repo oder git@github.com:user/repo.git",
|
||||
"noRepos": "Keine Git-Repos konfiguriert. Fügen Sie oben eines hinzu.",
|
||||
"disabled": "Deaktiviert",
|
||||
"removeRepo": "Repo entfernen"
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "Keine Beschreibung",
|
||||
"readOnly": "Nur lesen",
|
||||
"editTitle": "Fähigkeit bearbeiten",
|
||||
"deleteTitle": "Fähigkeit löschen",
|
||||
"exportTitle": "Als .tar.gz exportieren"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Keine Fähigkeiten gefunden",
|
||||
"text": "Erstellen oder importieren Sie eine Fähigkeit, um loszulegen.",
|
||||
"noPersonal": "Sie haben noch keine Fähigkeiten."
|
||||
},
|
||||
"sections": {
|
||||
"yourSkills": "Ihre Fähigkeiten",
|
||||
"otherUsersSkills": "Fähigkeiten anderer Benutzer"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Fähigkeit löschen",
|
||||
"message": "Fähigkeit \"{{name}}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Löschen"
|
||||
},
|
||||
"removeRepoDialog": {
|
||||
"title": "Git-Repository entfernen",
|
||||
"message": "Dieses Git-Repository entfernen? Fähigkeiten daraus sind nicht mehr verfügbar.",
|
||||
"confirm": "Entfernen"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Laden der Fähigkeiten fehlgeschlagen",
|
||||
"deleted": "Fähigkeit \"{{name}}\" gelöscht",
|
||||
"deleteFailed": "Löschen der Fähigkeit fehlgeschlagen",
|
||||
"exported": "Fähigkeit \"{{name}}\" exportiert",
|
||||
"exportFailed": "Export fehlgeschlagen",
|
||||
"imported": "Fähigkeit aus \"{{file}}\" importiert",
|
||||
"importFailed": "Import fehlgeschlagen",
|
||||
"loadReposFailed": "Laden der Git-Repos fehlgeschlagen",
|
||||
"repoAdded": "Git-Repo hinzugefügt und synchronisiert",
|
||||
"addRepoFailed": "Hinzufügen des Repos fehlgeschlagen",
|
||||
"synced": "Repo synchronisiert",
|
||||
"syncFailed": "Synchronisierung fehlgeschlagen",
|
||||
"toggled": "Repo umgeschaltet",
|
||||
"toggleFailed": "Umschalten fehlgeschlagen",
|
||||
"removed": "Repo entfernt",
|
||||
"removeFailed": "Entfernen fehlgeschlagen"
|
||||
}
|
||||
}
|
||||
62
core/http/react-ui/public/locales/en/admin.json
Normal file
62
core/http/react-ui/public/locales/en/admin.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"manage": {
|
||||
"title": "System",
|
||||
"subtitle": "Manage installed models and backends"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"subtitle": "Configure LocalAI runtime settings",
|
||||
"saved": "Settings saved successfully",
|
||||
"saveFailed": "Save failed: {{message}}",
|
||||
"loadFailed": "Failed to load settings: {{message}}",
|
||||
"sections": {
|
||||
"branding": "Branding",
|
||||
"watchdog": "Watchdog",
|
||||
"memory": "Memory",
|
||||
"backends": "Backends",
|
||||
"performance": "Performance",
|
||||
"tracing": "Tracing",
|
||||
"api": "API & CORS",
|
||||
"p2p": "P2P",
|
||||
"galleries": "Galleries",
|
||||
"apikeys": "API Keys",
|
||||
"agents": "Agent Jobs",
|
||||
"agentpool": "Agent Pool",
|
||||
"assistant": "LocalAI Assistant",
|
||||
"responses": "Responses"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"title": "Backend Management",
|
||||
"subtitle": "Discover and install AI backends to power your models"
|
||||
},
|
||||
"backendLogs": {
|
||||
"title": "Backend Logs",
|
||||
"subtitle": "View logs from running backends",
|
||||
"empty": "No logs available"
|
||||
},
|
||||
"traces": {
|
||||
"title": "Traces",
|
||||
"subtitle": "View logged API requests, responses, and backend operations"
|
||||
},
|
||||
"nodes": {
|
||||
"title": "Distributed Nodes",
|
||||
"subtitle": "Manage backend and agent worker nodes"
|
||||
},
|
||||
"p2p": {
|
||||
"title": "Distributed AI Computing",
|
||||
"subtitle": "Scale your AI workloads across multiple devices with peer-to-peer distribution"
|
||||
},
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"subtitle": "Manage registered users, roles, and invites"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage",
|
||||
"subtitle": "API token usage statistics"
|
||||
},
|
||||
"explorer": {
|
||||
"title": "Explorer",
|
||||
"subtitle": "Browse files and configuration"
|
||||
}
|
||||
}
|
||||
55
core/http/react-ui/public/locales/en/agents.json
Normal file
55
core/http/react-ui/public/locales/en/agents.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"title": "Agents",
|
||||
"subtitle": "Manage autonomous AI agents",
|
||||
"actions": {
|
||||
"agentHub": "Agent Hub",
|
||||
"import": "Import",
|
||||
"createAgent": "Create Agent",
|
||||
"edit": "Edit",
|
||||
"chat": "Chat",
|
||||
"export": "Export",
|
||||
"delete": "Delete",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"events": "Events",
|
||||
"actions": "Actions",
|
||||
"eventsTooltip": "{{count}} events - Click to view"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search agents...",
|
||||
"summary_one": "{{shown}} of {{total}} agent",
|
||||
"summary_other": "{{shown}} of {{total}} agents"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigured": "No agents configured",
|
||||
"noConfiguredText": "Create an agent to get started with autonomous AI workflows.",
|
||||
"browseHub": "Don't know where to start? Browse the <1>Agent Hub</1> to find ready-made agent configurations you can import.",
|
||||
"noMatching": "No matching agents",
|
||||
"noMatchingText": "No agents match \"{{query}}\""
|
||||
},
|
||||
"sections": {
|
||||
"yourAgents": "Your Agents",
|
||||
"otherUsersAgents": "Other Users' Agents"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete Agent",
|
||||
"message": "Delete agent \"{{name}}\"? This action cannot be undone.",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Failed to load agents: {{message}}",
|
||||
"deleted": "Agent \"{{name}}\" deleted",
|
||||
"deleteFailed": "Failed to delete agent: {{message}}",
|
||||
"paused": "Agent \"{{name}}\" paused",
|
||||
"resumed": "Agent \"{{name}}\" resumed",
|
||||
"pauseFailed": "Failed to pause agent: {{message}}",
|
||||
"resumeFailed": "Failed to resume agent: {{message}}",
|
||||
"exported": "Agent \"{{name}}\" exported",
|
||||
"exportFailed": "Failed to export agent: {{message}}",
|
||||
"parseFailed": "Failed to parse agent file: {{message}}"
|
||||
}
|
||||
}
|
||||
112
core/http/react-ui/public/locales/en/auth.json
Normal file
112
core/http/react-ui/public/locales/en/auth.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"login": {
|
||||
"subtitle": "Sign in to continue",
|
||||
"registerSubtitle": "Create an account",
|
||||
"createAdminSubtitle": "Create your admin account",
|
||||
"tokenSubtitle": "Enter your API key to continue",
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Your name (optional)",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password...",
|
||||
"newPasswordPlaceholder": "At least 8 characters",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"confirmPasswordPlaceholder": "Repeat password",
|
||||
"inviteCodeLabel": "Invite Code",
|
||||
"inviteCodeOptional": " (optional — skip the approval wait)",
|
||||
"inviteCodePlaceholder": "Paste your invite code...",
|
||||
"tokenPlaceholder": "Enter API key...",
|
||||
"tokenAltPlaceholder": "Enter API token...",
|
||||
"signIn": "Sign In",
|
||||
"signingIn": "Signing in...",
|
||||
"register": "Register",
|
||||
"creatingAccount": "Creating account...",
|
||||
"createAdminAccount": "Create Admin Account",
|
||||
"signInWithGitHub": "Sign in with GitHub",
|
||||
"signInWithSSO": "Sign in with SSO",
|
||||
"loginWithToken": "Login with Token",
|
||||
"showTokenLogin": "Login with API Token",
|
||||
"hideTokenLogin": "Hide token login",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"or": "or",
|
||||
"errors": {
|
||||
"loginFailed": "Login failed",
|
||||
"registrationFailed": "Registration failed",
|
||||
"invalidToken": "Invalid token",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"enterToken": "Please enter a token",
|
||||
"networkError": "Network error",
|
||||
"inviteRequired": "A valid invite code is required to register"
|
||||
},
|
||||
"messages": {
|
||||
"registrationPending": "Registration successful, awaiting approval."
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"subtitle": "Profile, credentials, and API keys",
|
||||
"unavailable": "Account unavailable",
|
||||
"unavailableText": "Authentication must be enabled to manage your account.",
|
||||
"tabs": {
|
||||
"profile": "Profile",
|
||||
"security": "Security",
|
||||
"apiKeys": "API Keys"
|
||||
},
|
||||
"profile": {
|
||||
"displayName": "Display name",
|
||||
"displayNameDescription": "Your public display name",
|
||||
"avatarUrl": "Avatar URL",
|
||||
"avatarUrlDescription": "URL to your profile picture",
|
||||
"avatarUrlPlaceholder": "https://example.com/avatar.png",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"updated": "Profile updated",
|
||||
"updateFailed": "Failed to update profile: {{message}}"
|
||||
},
|
||||
"security": {
|
||||
"currentPassword": "Current password",
|
||||
"currentPasswordDescription": "Enter your existing password to verify your identity",
|
||||
"currentPasswordPlaceholder": "Current password",
|
||||
"newPassword": "New password",
|
||||
"newPasswordDescription": "Must be at least 8 characters",
|
||||
"newPasswordPlaceholder": "New password",
|
||||
"confirmPassword": "Confirm password",
|
||||
"confirmPasswordDescription": "Re-enter your new password",
|
||||
"confirmPasswordPlaceholder": "Confirm new password",
|
||||
"changePassword": "Change password",
|
||||
"changing": "Changing...",
|
||||
"changed": "Password changed",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"tooShort": "New password must be at least 8 characters",
|
||||
"oauthOnly": "Password management is not available for {{provider}} accounts."
|
||||
},
|
||||
"apiKeys": {
|
||||
"create": "Create API key",
|
||||
"createDescription": "Generate a key for programmatic access",
|
||||
"namePlaceholder": "Key name (e.g. my-app)",
|
||||
"createButton": "Create",
|
||||
"creating": "Creating...",
|
||||
"createdToast": "API key created",
|
||||
"createFailed": "Failed to create API key: {{message}}",
|
||||
"loadFailed": "Failed to load API keys: {{message}}",
|
||||
"revoke": "Revoke",
|
||||
"revokeKey": "Revoke key",
|
||||
"revokeTitle": "Revoke API Key",
|
||||
"revokeMessage": "Revoke API key \"{{name}}\"? This cannot be undone.",
|
||||
"revoked": "API key revoked",
|
||||
"revokeFailed": "Failed to revoke API key: {{message}}",
|
||||
"copyNow": "Copy now — this key won't be shown again",
|
||||
"copiedToast": "Copied to clipboard",
|
||||
"copyFailed": "Failed to copy",
|
||||
"empty": "No API keys yet. Create one above to get programmatic access.",
|
||||
"lastUsed": "last used {{date}}"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page Not Found",
|
||||
"text": "Looks like this page wandered off. Let's get you back on track.",
|
||||
"goHome": "Go Home"
|
||||
}
|
||||
}
|
||||
116
core/http/react-ui/public/locales/en/chat.json
Normal file
116
core/http/react-ui/public/locales/en/chat.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"activity": {
|
||||
"thought": "Thought",
|
||||
"tool": "Tool",
|
||||
"result": "Result",
|
||||
"toolResult": "{{name}} result",
|
||||
"thinking": "Thinking..."
|
||||
},
|
||||
"header": {
|
||||
"manageModeTooltip": "This chat can install models, edit configs and manage backends by talking to LocalAI.",
|
||||
"modelInfo": "Model info",
|
||||
"chatSettings": "Chat settings",
|
||||
"modelInfoTitle": "Model Info: {{model}}",
|
||||
"editConfig": "Edit config",
|
||||
"close": "Close"
|
||||
},
|
||||
"modelInfo": {
|
||||
"backend": "Backend",
|
||||
"modelFile": "Model file",
|
||||
"contextSize": "Context size",
|
||||
"threads": "Threads",
|
||||
"mcp": "MCP",
|
||||
"configured": "Configured",
|
||||
"chatTemplate": "Chat template",
|
||||
"yes": "Yes",
|
||||
"gpuLayers": "GPU layers"
|
||||
},
|
||||
"context": {
|
||||
"label": "Context: {{percent}}%",
|
||||
"labelWithTokens": "Context: {{percent}}% ({{tokens}} tokens)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Chat Settings",
|
||||
"manageMode": "Manage mode",
|
||||
"manageModeDesc": "Let this chat install models, switch backends, and edit configs by talking to LocalAI.",
|
||||
"systemPrompt": "System Prompt",
|
||||
"systemPromptPlaceholder": "You are a helpful assistant...",
|
||||
"temperature": "Temperature",
|
||||
"topP": "Top P",
|
||||
"topK": "Top K",
|
||||
"contextSize": "Context Size",
|
||||
"contextSizePlaceholder": "2048",
|
||||
"clearHistory": "Clear chat history"
|
||||
},
|
||||
"empty": {
|
||||
"manageTitle": "Manage LocalAI by chatting",
|
||||
"manageText": "Ask to install models, switch backends, edit configs, or check status. The assistant will summarise actions and wait for your confirmation before changing anything.",
|
||||
"startTitle": "Start a conversation",
|
||||
"readyText": "Ready to chat with {{model}}",
|
||||
"selectModelText": "Select a model above to get started",
|
||||
"suggestionsManage": [
|
||||
"What is installed?",
|
||||
"Install a chat model",
|
||||
"Show system status",
|
||||
"Update a backend"
|
||||
],
|
||||
"suggestionsChat": [
|
||||
"Explain how this works",
|
||||
"Help me write code",
|
||||
"Summarize a document",
|
||||
"Brainstorm ideas"
|
||||
],
|
||||
"recent": "Recent",
|
||||
"noMessages": "No messages yet",
|
||||
"hintEnter": "Enter to send",
|
||||
"hintShiftEnter": "Shift+Enter for newline",
|
||||
"hintAttach": "Attach files"
|
||||
},
|
||||
"errors": {
|
||||
"viewTraces": "View traces for details"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copy",
|
||||
"regenerate": "Regenerate"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Transferring model...",
|
||||
"transferringTo": "Transferring model to {{node}}..."
|
||||
},
|
||||
"tokens": {
|
||||
"perSec": "{{count}} tok/s",
|
||||
"peak": "Peak: {{count}} tok/s",
|
||||
"usage": "{{prompt}}p + {{completion}}c = {{total}}"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Message...",
|
||||
"attachFile": "Attach file",
|
||||
"stopGenerating": "Stop generating",
|
||||
"canvasTitle": "Canvas — extract code blocks and media into a side panel for preview, copy, and download",
|
||||
"canvasLabel": "Canvas",
|
||||
"openCanvas": "Open canvas panel"
|
||||
},
|
||||
"deleteAllDialog": {
|
||||
"title": "Delete All Chats",
|
||||
"message": "Delete all chats? This cannot be undone.",
|
||||
"confirm": "Delete all"
|
||||
},
|
||||
"toasts": {
|
||||
"selectModel": "Please select a model",
|
||||
"copied": "Copied to clipboard"
|
||||
},
|
||||
"menu": {
|
||||
"trigger": "Chats",
|
||||
"triggerTitle": "Conversations (Ctrl/Cmd+K)",
|
||||
"search": "Search conversations...",
|
||||
"clearSearch": "Clear search",
|
||||
"noMatch": "No conversations match your search",
|
||||
"noConversations": "No conversations yet",
|
||||
"rename": "Rename",
|
||||
"exportMarkdown": "Export as Markdown",
|
||||
"deleteChat": "Delete chat",
|
||||
"newChat": "New chat",
|
||||
"clearAll": "Clear all",
|
||||
"deleteAllTitle": "Delete all conversations"
|
||||
}
|
||||
}
|
||||
43
core/http/react-ui/public/locales/en/collections.json
Normal file
43
core/http/react-ui/public/locales/en/collections.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"title": "Knowledge Base",
|
||||
"subtitle": "Manage document collections for agent RAG",
|
||||
"newPlaceholder": "New collection name...",
|
||||
"actions": {
|
||||
"create": "Create",
|
||||
"creating": "Creating...",
|
||||
"details": "Details",
|
||||
"reset": "Reset",
|
||||
"delete": "Delete",
|
||||
"viewDetails": "View details",
|
||||
"resetCollection": "Reset collection",
|
||||
"deleteCollection": "Delete collection"
|
||||
},
|
||||
"sections": {
|
||||
"yourCollections": "Your Collections",
|
||||
"otherUsersCollections": "Other Users' Collections"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No collections yet",
|
||||
"text": "Collections let you organize documents into knowledge bases that agents can search using RAG (Retrieval-Augmented Generation). Create a collection above to get started.",
|
||||
"noPersonal": "You have no collections yet."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete Collection",
|
||||
"message": "Delete collection \"{{name}}\"? This will remove all entries and cannot be undone.",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"resetDialog": {
|
||||
"title": "Reset Collection",
|
||||
"message": "Reset collection \"{{name}}\"? This will remove all entries but keep the collection.",
|
||||
"confirm": "Reset"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Failed to load collections: {{message}}",
|
||||
"created": "Collection \"{{name}}\" created",
|
||||
"createFailed": "Failed to create collection: {{message}}",
|
||||
"deleted": "Collection \"{{name}}\" deleted",
|
||||
"deleteFailed": "Failed to delete collection: {{message}}",
|
||||
"reset": "Collection \"{{name}}\" reset",
|
||||
"resetFailed": "Failed to reset collection: {{message}}"
|
||||
}
|
||||
}
|
||||
109
core/http/react-ui/public/locales/en/common.json
Normal file
109
core/http/react-ui/public/locales/en/common.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"refresh": "Refresh",
|
||||
"reload": "Reload",
|
||||
"retry": "Retry",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"clear": "Clear",
|
||||
"reset": "Reset",
|
||||
"apply": "Apply",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"open": "Open",
|
||||
"submit": "Submit",
|
||||
"select": "Select",
|
||||
"selectAll": "Select all",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
"view": "View",
|
||||
"details": "Details",
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"status": {
|
||||
"loading": "Loading...",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved",
|
||||
"ready": "Ready",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"pending": "Pending",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"empty": "No items",
|
||||
"none": "None",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"dialogs": {
|
||||
"confirmDelete": {
|
||||
"title": "Confirm deletion",
|
||||
"message": "Are you sure you want to delete this? This action cannot be undone.",
|
||||
"confirm": "Delete",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"unsavedChanges": {
|
||||
"title": "Unsaved changes",
|
||||
"message": "You have unsaved changes. Do you want to discard them?",
|
||||
"discard": "Discard",
|
||||
"keepEditing": "Keep editing"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"required": "Required",
|
||||
"optional": "Optional",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"type": "Type",
|
||||
"value": "Value",
|
||||
"search": "Search...",
|
||||
"selectPlaceholder": "Select an option..."
|
||||
},
|
||||
"time": {
|
||||
"now": "now",
|
||||
"secondsAgo_one": "{{count}} second ago",
|
||||
"secondsAgo_other": "{{count}} seconds ago",
|
||||
"minutesAgo_one": "{{count}} minute ago",
|
||||
"minutesAgo_other": "{{count}} minutes ago",
|
||||
"hoursAgo_one": "{{count}} hour ago",
|
||||
"hoursAgo_other": "{{count}} hours ago",
|
||||
"daysAgo_one": "{{count}} day ago",
|
||||
"daysAgo_other": "{{count}} days ago"
|
||||
},
|
||||
"units": {
|
||||
"bytes": "B",
|
||||
"kilobytes": "KB",
|
||||
"megabytes": "MB",
|
||||
"gigabytes": "GB",
|
||||
"terabytes": "TB"
|
||||
}
|
||||
}
|
||||
17
core/http/react-ui/public/locales/en/errors.json
Normal file
17
core/http/react-ui/public/locales/en/errors.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"generic": "Something went wrong",
|
||||
"network": "Network error. Check your connection and try again.",
|
||||
"unauthorized": "You are not authorized to perform this action.",
|
||||
"forbidden": "Access denied.",
|
||||
"notFound": "The requested resource was not found.",
|
||||
"serverError": "Server error. Please try again later.",
|
||||
"loadFailed": "Failed to load: {{message}}",
|
||||
"saveFailed": "Save failed: {{message}}",
|
||||
"deleteFailed": "Delete failed: {{message}}",
|
||||
"updateFailed": "Update failed: {{message}}",
|
||||
"createFailed": "Create failed: {{message}}",
|
||||
"operationFailed": "Operation failed: {{message}}",
|
||||
"invalidInput": "Invalid input. Please check the form and try again.",
|
||||
"tryAgain": "Please try again.",
|
||||
"contactAdmin": "If the problem persists, contact your administrator."
|
||||
}
|
||||
66
core/http/react-ui/public/locales/en/home.json
Normal file
66
core/http/react-ui/public/locales/en/home.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"cluster": {
|
||||
"vram": "Cluster VRAM",
|
||||
"ram": "Cluster RAM",
|
||||
"nodesOnline": "{{healthy}}/{{total}} nodes online"
|
||||
},
|
||||
"resourceGpu": "GPU",
|
||||
"resourceRam": "RAM",
|
||||
"assistant": {
|
||||
"title": "Manage LocalAI by chatting",
|
||||
"description": "Install models, switch backends, edit configs and check status by talking to LocalAI.",
|
||||
"open": "Open assistant",
|
||||
"tooltip": "Manage LocalAI by chatting"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Message...",
|
||||
"attachImage": "Attach image",
|
||||
"attachAudio": "Attach audio",
|
||||
"attachFile": "Attach file",
|
||||
"enterToSend": "Enter to send",
|
||||
"selectModelFirst": "Select a model first",
|
||||
"sendMessage": "Send message",
|
||||
"selectModelToast": "Please select a model first"
|
||||
},
|
||||
"quickLinks": {
|
||||
"manageByChat": "Manage by chat",
|
||||
"installedModels": "Installed Models",
|
||||
"browseGallery": "Browse Gallery",
|
||||
"importModel": "Import Model",
|
||||
"documentation": "Documentation"
|
||||
},
|
||||
"loadedModels": {
|
||||
"count_one": "{{count}} model loaded",
|
||||
"count_other": "{{count}} models loaded",
|
||||
"stop": "Stop model",
|
||||
"stopAll": "Stop all"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Stop Model",
|
||||
"message": "Stop model {{model}}?",
|
||||
"confirm": "Stop {{model}}",
|
||||
"stopAllTitle": "Stop All Models",
|
||||
"stopAllMessage": "Stop all {{count}} loaded models?",
|
||||
"stopAllConfirm": "Stop all",
|
||||
"stoppedToast": "Stopped {{model}}",
|
||||
"allStoppedToast": "All models stopped",
|
||||
"stopFailed": "Failed to stop: {{message}}"
|
||||
},
|
||||
"wizard": {
|
||||
"getStarted": "Get started with {{name}}",
|
||||
"intro": "Install your first model to begin. Browse the gallery or import your own.",
|
||||
"steps": {
|
||||
"step1Title": "Browse the Model Gallery",
|
||||
"step1Body": "Find the right model for your needs from our curated collection.",
|
||||
"step2Title": "Install a Model",
|
||||
"step2Body": "Click install to download and configure it automatically.",
|
||||
"step3Title": "Start Chatting",
|
||||
"step3Body": "Chat with your model right from the browser or use the API."
|
||||
},
|
||||
"browseGallery": "Browse Model Gallery",
|
||||
"importModel": "Import Model",
|
||||
"docs": "Docs",
|
||||
"noModelsTitle": "No Models Available",
|
||||
"noModelsBody": "There are no models installed yet. Ask your administrator to set up models so you can start chatting."
|
||||
}
|
||||
}
|
||||
142
core/http/react-ui/public/locales/en/importModel.json
Normal file
142
core/http/react-ui/public/locales/en/importModel.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"title": "Import New Model",
|
||||
"subtitle": {
|
||||
"simple": "Import a model from a URI — auto-detect picks the backend.",
|
||||
"powerYaml": "Write the full model YAML configuration.",
|
||||
"powerPrefs": "Fine-grained import preferences."
|
||||
},
|
||||
"actions": {
|
||||
"import": "Import Model",
|
||||
"importing": "Importing...",
|
||||
"create": "Create",
|
||||
"saving": "Saving...",
|
||||
"browseHF": "Browse models on HF",
|
||||
"addCustom": "Add Custom",
|
||||
"copy": "Copy"
|
||||
},
|
||||
"form": {
|
||||
"modelUri": "Model URI",
|
||||
"uriPlaceholder": "huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf",
|
||||
"uriHint": "Enter the URI or path to the model file you want to import",
|
||||
"supportedFormats": "Supported URI Formats",
|
||||
"options": "Options",
|
||||
"preferences": "Preferences (Optional)",
|
||||
"commonPreferences": "Common Preferences",
|
||||
"customPreferences": "Custom Preferences",
|
||||
"customKeyValueHint": "Add custom key-value pairs for advanced configuration.",
|
||||
"preferenceKey": "Preference key for row {{index}}",
|
||||
"preferenceValue": "Preference value for row {{index}}",
|
||||
"removePref": "Remove this preference",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"backend": "Backend",
|
||||
"backendAuto": "Auto-detect (based on URI)",
|
||||
"backendLoading": "Loading backends…",
|
||||
"backendSearch": "Search backends...",
|
||||
"backendHint": "Force a specific backend. Leave empty to auto-detect from the URI. Items marked \"manual pick\" aren't auto-detectable — pick them yourself if you know what the model needs.",
|
||||
"backendErrorHint": "Could not load backend list — auto-detect only.",
|
||||
"backendNotInstalled": "This backend isn't installed yet. Submitting import will download it first.",
|
||||
"modelName": "Model Name",
|
||||
"modelNamePlaceholder": "Leave empty to use filename",
|
||||
"modelNameHint": "Custom name for the model. If empty, the filename will be used.",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Leave empty to use default description",
|
||||
"descriptionHint": "Custom description for the model.",
|
||||
"quantizations": "Quantizations",
|
||||
"quantizationsPlaceholder": "q4_k_m,q4_k_s,q3_k_m (comma-separated)",
|
||||
"quantizationsHint": "Preferred quantizations (comma-separated). Leave empty for default (q4_k_m).",
|
||||
"mmprojQuantizations": "MMProj Quantizations",
|
||||
"mmprojQuantizationsPlaceholder": "fp16,fp32 (comma-separated)",
|
||||
"mmprojQuantizationsHint": "Preferred MMProj quantizations. Leave empty for default (fp16).",
|
||||
"embeddings": "Embeddings",
|
||||
"embeddingsHint": "Enable embeddings support for this model.",
|
||||
"modelType": "Model Type",
|
||||
"modelTypePlaceholder": "AutoModelForCausalLM (for transformers backend)",
|
||||
"modelTypeHint": "Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba.",
|
||||
"pipelineType": "Pipeline Type",
|
||||
"pipelineTypeHint": "Pipeline type for diffusers backend.",
|
||||
"schedulerType": "Scheduler Type",
|
||||
"schedulerTypePlaceholder": "k_dpmpp_2m (optional)",
|
||||
"schedulerTypeHint": "Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim.",
|
||||
"enableParameters": "Enable Parameters",
|
||||
"enableParametersPlaceholder": "negative_prompt,num_inference_steps (comma-separated)",
|
||||
"enableParametersHint": "Enabled parameters for diffusers backend (comma-separated).",
|
||||
"cuda": "CUDA",
|
||||
"cudaHint": "Enable CUDA support for GPU acceleration.",
|
||||
"yamlEditor": "YAML Configuration Editor",
|
||||
"manualPick": "manual pick",
|
||||
"manualPickTooltip": "Auto-detect won't route to this backend. Pick it here if you know that's what you want."
|
||||
},
|
||||
"modality": {
|
||||
"text": "Text LLM",
|
||||
"asr": "Speech recognition",
|
||||
"tts": "Text-to-speech",
|
||||
"image": "Image / Video",
|
||||
"embeddings": "Embeddings",
|
||||
"reranker": "Rerankers",
|
||||
"detection": "Object detection",
|
||||
"vad": "Voice activity detection",
|
||||
"other": "Other"
|
||||
},
|
||||
"powerTabs": {
|
||||
"ariaLabel": "Advanced mode tab",
|
||||
"preferences": "Preferences",
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"switchDialog": {
|
||||
"title": "Keep your custom preferences?",
|
||||
"body": "Switching to Simple mode hides preferences beyond backend, name, and description. They'll still be sent when you import.",
|
||||
"cancel": "Cancel",
|
||||
"discard": "Discard & switch",
|
||||
"keep": "Keep & switch"
|
||||
},
|
||||
"estimate": {
|
||||
"title": "Estimated requirements",
|
||||
"download": "Download: {{size}}",
|
||||
"vram": "VRAM: {{vram}}"
|
||||
},
|
||||
"toasts": {
|
||||
"noUri": "Please enter a model URI",
|
||||
"noYaml": "Please enter YAML configuration",
|
||||
"started": "Import started! Tracking progress...",
|
||||
"startedWithMeta": "Import started! Tracking progress... ({{meta}})",
|
||||
"imported": "Model imported successfully!",
|
||||
"importedYaml": "Model configuration imported successfully!",
|
||||
"importFailed": "Import failed: {{message}}",
|
||||
"startImportFailed": "Failed to start import: {{message}}",
|
||||
"backendsLoadFailed": "Could not load backend list — using auto-detect only",
|
||||
"modalityClearedBackend": "Cleared backend selection — it wasn't in the {{label}} group.",
|
||||
"copied": "Copied to clipboard"
|
||||
},
|
||||
"uriFormats": {
|
||||
"huggingface": {
|
||||
"title": "HuggingFace",
|
||||
"standard": "Standard HuggingFace format",
|
||||
"short": "Short HuggingFace format",
|
||||
"fullUrl": "Full HuggingFace URL"
|
||||
},
|
||||
"http": {
|
||||
"title": "HTTP/HTTPS URLs",
|
||||
"direct": "Direct download from any HTTPS URL"
|
||||
},
|
||||
"local": {
|
||||
"title": "Local Files",
|
||||
"filePath": "Local file path (absolute)",
|
||||
"directYaml": "Direct local YAML config file"
|
||||
},
|
||||
"oci": {
|
||||
"title": "OCI Registry",
|
||||
"registry": "OCI container registry",
|
||||
"tarball": "Local OCI tarball file"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"model": "Ollama model format"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "YAML Configuration Files",
|
||||
"remote": "Remote YAML config file",
|
||||
"local": "Local YAML config file"
|
||||
}
|
||||
}
|
||||
}
|
||||
154
core/http/react-ui/public/locales/en/media.json
Normal file
154
core/http/react-ui/public/locales/en/media.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Images",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Sound"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Image Generation",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the image you want to generate...",
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"negativePromptPlaceholder": "What to avoid...",
|
||||
"size": "Size",
|
||||
"count": "Count (1-4)",
|
||||
"advanced": "Advanced Settings",
|
||||
"imageInputs": "Image Inputs",
|
||||
"steps": "Steps",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random",
|
||||
"sourceImage": "Source Image (img2img)",
|
||||
"refImages": "Reference Images",
|
||||
"refImagesAdded_one": "{{count}} image added",
|
||||
"refImagesAdded_other": "{{count}} images added"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated images will appear here",
|
||||
"toasts": {
|
||||
"noPrompt": "Please enter a prompt",
|
||||
"noModel": "Please select a model",
|
||||
"noResults": "No images generated"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Video Generation",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the video you want to generate...",
|
||||
"duration": "Duration (seconds)",
|
||||
"fps": "FPS",
|
||||
"size": "Size",
|
||||
"advanced": "Advanced Settings",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated video will appear here",
|
||||
"toasts": {
|
||||
"noPrompt": "Please enter a prompt",
|
||||
"noModel": "Please select a model",
|
||||
"noResults": "No video generated"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Text to Speech",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"voice": "Voice",
|
||||
"voicePlaceholder": "Optional voice ID",
|
||||
"input": "Text",
|
||||
"inputPlaceholder": "Enter text to synthesize..."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated audio will appear here",
|
||||
"toasts": {
|
||||
"noText": "Please enter text",
|
||||
"noModel": "Please select a model",
|
||||
"generateFailed": "Generation failed"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Sound Generation",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the sound you want to generate...",
|
||||
"duration": "Duration (seconds)",
|
||||
"language": "Language",
|
||||
"vocalLanguage": "Vocal language",
|
||||
"lyrics": "Lyrics (optional)",
|
||||
"lyricsPlaceholder": "Lyrics for vocal generation",
|
||||
"advanced": "Advanced Settings",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated audio will appear here",
|
||||
"toasts": {
|
||||
"noPrompt": "Please enter a prompt",
|
||||
"noModel": "Please select a model",
|
||||
"generateFailed": "Generation failed"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Talk",
|
||||
"subtitle": "Realtime voice conversation",
|
||||
"actions": {
|
||||
"start": "Start session",
|
||||
"stop": "End session",
|
||||
"connecting": "Connecting...",
|
||||
"muted": "Muted",
|
||||
"mute": "Mute",
|
||||
"unmute": "Unmute"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"voice": "Voice",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Language",
|
||||
"languagePlaceholder": "en",
|
||||
"instructions": "Instructions",
|
||||
"instructionsPlaceholder": "Set the assistant's persona..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
"connecting": "Connecting...",
|
||||
"listening": "Listening...",
|
||||
"speaking": "Speaking...",
|
||||
"ended": "Session ended"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Select a model first",
|
||||
"connectFailed": "Failed to connect: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "History",
|
||||
"empty": "No history yet",
|
||||
"deleteEntry": "Delete entry",
|
||||
"clear": "Clear history",
|
||||
"clearTitle": "Clear all history",
|
||||
"clearMessage": "Remove all history entries? This cannot be undone.",
|
||||
"clearConfirm": "Clear",
|
||||
"cleared": "History cleared"
|
||||
}
|
||||
}
|
||||
85
core/http/react-ui/public/locales/en/models.json
Normal file
85
core/http/react-ui/public/locales/en/models.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"title": "Install Models",
|
||||
"subtitle": "Browse and install AI models from the gallery",
|
||||
"stats": {
|
||||
"available": "Available",
|
||||
"installed": "Installed"
|
||||
},
|
||||
"actions": {
|
||||
"addModel": "Add Model",
|
||||
"importModel": "Import Model",
|
||||
"install": "Install",
|
||||
"reinstall": "Reinstall",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"filters": {
|
||||
"all": "All",
|
||||
"llm": "LLM",
|
||||
"image": "Image",
|
||||
"multimodal": "Multimodal",
|
||||
"vision": "Vision",
|
||||
"tts": "TTS",
|
||||
"stt": "STT",
|
||||
"embedding": "Embedding",
|
||||
"rerank": "Rerank",
|
||||
"allBackends": "All Backends",
|
||||
"searchBackends": "Search backends..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search models...",
|
||||
"clearFilters": "Clear filters"
|
||||
},
|
||||
"table": {
|
||||
"modelName": "Model Name",
|
||||
"description": "Description",
|
||||
"backend": "Backend",
|
||||
"sizeVram": "Size / VRAM",
|
||||
"status": "Status",
|
||||
"actions": "Actions",
|
||||
"size": "Size: {{size}}",
|
||||
"vram": "VRAM: {{vram}}",
|
||||
"fits": "Fits",
|
||||
"mayNotFit": "May not fit",
|
||||
"trustRemoteCode": "Trust Remote Code",
|
||||
"installing": "Installing",
|
||||
"installingPct": "Installing · {{percent}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed"
|
||||
},
|
||||
"detail": {
|
||||
"description": "Description",
|
||||
"gallery": "Gallery",
|
||||
"backend": "Backend",
|
||||
"size": "Size",
|
||||
"vram": "VRAM",
|
||||
"license": "License",
|
||||
"tags": "Tags",
|
||||
"links": "Links",
|
||||
"warning": "Warning",
|
||||
"files": "Files",
|
||||
"fitsGpu": "Fits in GPU",
|
||||
"mayNotFitGpu": "May not fit in GPU",
|
||||
"requiresTrustRemoteCode": "Requires Trust Remote Code",
|
||||
"fileCount_one": "{{count}} file",
|
||||
"fileCount_other": "{{count}} files",
|
||||
"filename": "Filename",
|
||||
"uri": "URI",
|
||||
"sha256": "SHA256"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No models found",
|
||||
"withFilters": "No models match your current search or filters.",
|
||||
"noFilters": "The model gallery is empty."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete Model",
|
||||
"message": "Delete model {{model}}?",
|
||||
"confirm": "Delete {{model}}",
|
||||
"deletingToast": "Deleting {{model}}..."
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Failed to load models: {{message}}",
|
||||
"installFailed": "Failed to install: {{message}}",
|
||||
"deleteFailed": "Failed to delete: {{message}}"
|
||||
}
|
||||
}
|
||||
51
core/http/react-ui/public/locales/en/nav.json
Normal file
51
core/http/react-ui/public/locales/en/nav.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"appName": "LocalAI",
|
||||
"openMenu": "Open menu",
|
||||
"closeMenu": "Close menu",
|
||||
"primaryNavigation": "Primary navigation",
|
||||
"switchToLightMode": "Switch to light mode",
|
||||
"switchToDarkMode": "Switch to dark mode",
|
||||
"expandSidebar": "Expand sidebar",
|
||||
"collapseSidebar": "Collapse sidebar",
|
||||
"changeLanguage": "Change language",
|
||||
"logout": "Logout",
|
||||
"accountSettings": "Account settings",
|
||||
"account": "Account",
|
||||
"accountFor": "Account: {{name}}",
|
||||
"sections": {
|
||||
"tools": "Tools",
|
||||
"biometrics": "Biometrics",
|
||||
"agents": "Agents",
|
||||
"system": "System"
|
||||
},
|
||||
"items": {
|
||||
"home": "Home",
|
||||
"installModels": "Install Models",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Talk",
|
||||
"fineTune": "Fine-Tune (Experimental)",
|
||||
"quantize": "Quantize (Experimental)",
|
||||
"faceRecognition": "Face Recognition",
|
||||
"voiceRecognition": "Voice Recognition",
|
||||
"agents": "Agents",
|
||||
"skills": "Skills",
|
||||
"memory": "Memory",
|
||||
"mcpJobs": "MCP CI Jobs",
|
||||
"usage": "Usage",
|
||||
"users": "Users",
|
||||
"backends": "Backends",
|
||||
"traces": "Traces",
|
||||
"nodes": "Nodes",
|
||||
"swarm": "Swarm",
|
||||
"system": "System",
|
||||
"settings": "Settings",
|
||||
"api": "API"
|
||||
},
|
||||
"footer": {
|
||||
"github": "GitHub",
|
||||
"documentation": "Documentation",
|
||||
"author": "Author",
|
||||
"copyright": "© 2023-{{year}} {{author}}"
|
||||
}
|
||||
}
|
||||
79
core/http/react-ui/public/locales/en/skills.json
Normal file
79
core/http/react-ui/public/locales/en/skills.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "Skills",
|
||||
"subtitle": "Manage agent skills (reusable instructions and resources)",
|
||||
"unavailable": {
|
||||
"subtitle": "Skills service is not available or the index is rebuilding. Try again in a moment.",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"actions": {
|
||||
"newSkill": "New skill",
|
||||
"createSkill": "Create skill",
|
||||
"import": "Import",
|
||||
"importing": "Importing...",
|
||||
"gitRepos": "Git Repos",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"export": "Export",
|
||||
"sync": "Sync",
|
||||
"addRepo": "Add repo",
|
||||
"adding": "Adding...",
|
||||
"remove": "Remove",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search skills..."
|
||||
},
|
||||
"git": {
|
||||
"title": "Git repositories",
|
||||
"description": "Add Git repositories to pull skills from. Skills will appear in the list after sync.",
|
||||
"urlPlaceholder": "https://github.com/user/repo or git@github.com:user/repo.git",
|
||||
"noRepos": "No Git repos configured. Add one above.",
|
||||
"disabled": "Disabled",
|
||||
"removeRepo": "Remove repo"
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "No description",
|
||||
"readOnly": "Read-only",
|
||||
"editTitle": "Edit skill",
|
||||
"deleteTitle": "Delete skill",
|
||||
"exportTitle": "Export as .tar.gz"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No skills found",
|
||||
"text": "Create a skill or import one to get started.",
|
||||
"noPersonal": "You have no skills yet."
|
||||
},
|
||||
"sections": {
|
||||
"yourSkills": "Your Skills",
|
||||
"otherUsersSkills": "Other Users' Skills"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete Skill",
|
||||
"message": "Delete skill \"{{name}}\"? This action cannot be undone.",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"removeRepoDialog": {
|
||||
"title": "Remove Git Repository",
|
||||
"message": "Remove this Git repository? Skills from it will no longer be available.",
|
||||
"confirm": "Remove"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Failed to load skills",
|
||||
"deleted": "Skill \"{{name}}\" deleted",
|
||||
"deleteFailed": "Failed to delete skill",
|
||||
"exported": "Skill \"{{name}}\" exported",
|
||||
"exportFailed": "Export failed",
|
||||
"imported": "Skill imported from \"{{file}}\"",
|
||||
"importFailed": "Import failed",
|
||||
"loadReposFailed": "Failed to load Git repos",
|
||||
"repoAdded": "Git repo added and syncing",
|
||||
"addRepoFailed": "Failed to add repo",
|
||||
"synced": "Repo synced",
|
||||
"syncFailed": "Sync failed",
|
||||
"toggled": "Repo toggled",
|
||||
"toggleFailed": "Toggle failed",
|
||||
"removed": "Repo removed",
|
||||
"removeFailed": "Remove failed"
|
||||
}
|
||||
}
|
||||
62
core/http/react-ui/public/locales/es/admin.json
Normal file
62
core/http/react-ui/public/locales/es/admin.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"manage": {
|
||||
"title": "Sistema",
|
||||
"subtitle": "Administra modelos y backends instalados"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"subtitle": "Configura los ajustes de tiempo de ejecución de LocalAI",
|
||||
"saved": "Configuración guardada con éxito",
|
||||
"saveFailed": "Error al guardar: {{message}}",
|
||||
"loadFailed": "Error al cargar configuración: {{message}}",
|
||||
"sections": {
|
||||
"branding": "Branding",
|
||||
"watchdog": "Watchdog",
|
||||
"memory": "Memoria",
|
||||
"backends": "Backends",
|
||||
"performance": "Rendimiento",
|
||||
"tracing": "Tracing",
|
||||
"api": "API y CORS",
|
||||
"p2p": "P2P",
|
||||
"galleries": "Galerías",
|
||||
"apikeys": "Claves API",
|
||||
"agents": "Trabajos de agentes",
|
||||
"agentpool": "Pool de agentes",
|
||||
"assistant": "LocalAI Assistant",
|
||||
"responses": "Respuestas"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"title": "Administración de backends",
|
||||
"subtitle": "Descubre e instala backends de IA para tus modelos"
|
||||
},
|
||||
"backendLogs": {
|
||||
"title": "Logs de backends",
|
||||
"subtitle": "Visualiza logs de los backends en ejecución",
|
||||
"empty": "No hay logs disponibles"
|
||||
},
|
||||
"traces": {
|
||||
"title": "Trazas",
|
||||
"subtitle": "Visualiza peticiones API, respuestas y operaciones de backend registradas"
|
||||
},
|
||||
"nodes": {
|
||||
"title": "Nodos distribuidos",
|
||||
"subtitle": "Administra nodos worker de backends y agentes"
|
||||
},
|
||||
"p2p": {
|
||||
"title": "Computación de IA distribuida",
|
||||
"subtitle": "Escala tus cargas de trabajo de IA en múltiples dispositivos con distribución peer-to-peer"
|
||||
},
|
||||
"users": {
|
||||
"title": "Usuarios",
|
||||
"subtitle": "Administra usuarios registrados, roles e invitaciones"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Uso",
|
||||
"subtitle": "Estadísticas de uso de tokens API"
|
||||
},
|
||||
"explorer": {
|
||||
"title": "Explorador",
|
||||
"subtitle": "Explora archivos y configuración"
|
||||
}
|
||||
}
|
||||
55
core/http/react-ui/public/locales/es/agents.json
Normal file
55
core/http/react-ui/public/locales/es/agents.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"title": "Agentes",
|
||||
"subtitle": "Administra agentes de IA autónomos",
|
||||
"actions": {
|
||||
"agentHub": "Agent Hub",
|
||||
"import": "Importar",
|
||||
"createAgent": "Crear agente",
|
||||
"edit": "Editar",
|
||||
"chat": "Chat",
|
||||
"export": "Exportar",
|
||||
"delete": "Eliminar",
|
||||
"pause": "Pausar",
|
||||
"resume": "Reanudar"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nombre",
|
||||
"status": "Estado",
|
||||
"events": "Eventos",
|
||||
"actions": "Acciones",
|
||||
"eventsTooltip": "{{count}} eventos - Haz clic para ver"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar agentes...",
|
||||
"summary_one": "{{shown}} de {{total}} agente",
|
||||
"summary_other": "{{shown}} de {{total}} agentes"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigured": "No hay agentes configurados",
|
||||
"noConfiguredText": "Crea un agente para empezar con flujos de trabajo de IA autónomos.",
|
||||
"browseHub": "¿No sabes por dónde empezar? Explora el <1>Agent Hub</1> para encontrar configuraciones de agentes listas para importar.",
|
||||
"noMatching": "No hay agentes coincidentes",
|
||||
"noMatchingText": "Ningún agente coincide con \"{{query}}\""
|
||||
},
|
||||
"sections": {
|
||||
"yourAgents": "Tus agentes",
|
||||
"otherUsersAgents": "Agentes de otros usuarios"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Eliminar agente",
|
||||
"message": "¿Eliminar el agente \"{{name}}\"? Esta acción no se puede deshacer.",
|
||||
"confirm": "Eliminar"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Error al cargar agentes: {{message}}",
|
||||
"deleted": "Agente \"{{name}}\" eliminado",
|
||||
"deleteFailed": "Error al eliminar agente: {{message}}",
|
||||
"paused": "Agente \"{{name}}\" pausado",
|
||||
"resumed": "Agente \"{{name}}\" reanudado",
|
||||
"pauseFailed": "Error al pausar agente: {{message}}",
|
||||
"resumeFailed": "Error al reanudar agente: {{message}}",
|
||||
"exported": "Agente \"{{name}}\" exportado",
|
||||
"exportFailed": "Error al exportar agente: {{message}}",
|
||||
"parseFailed": "Error al analizar archivo de agente: {{message}}"
|
||||
}
|
||||
}
|
||||
112
core/http/react-ui/public/locales/es/auth.json
Normal file
112
core/http/react-ui/public/locales/es/auth.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"login": {
|
||||
"subtitle": "Inicia sesión para continuar",
|
||||
"registerSubtitle": "Crea una cuenta",
|
||||
"createAdminSubtitle": "Crea tu cuenta de administrador",
|
||||
"tokenSubtitle": "Introduce tu clave API para continuar",
|
||||
"email": "Correo electrónico",
|
||||
"emailPlaceholder": "tu@ejemplo.com",
|
||||
"name": "Nombre",
|
||||
"namePlaceholder": "Tu nombre (opcional)",
|
||||
"password": "Contraseña",
|
||||
"passwordPlaceholder": "Introduce la contraseña...",
|
||||
"newPasswordPlaceholder": "Al menos 8 caracteres",
|
||||
"confirmPassword": "Confirmar contraseña",
|
||||
"confirmPasswordPlaceholder": "Repite la contraseña",
|
||||
"inviteCodeLabel": "Código de invitación",
|
||||
"inviteCodeOptional": " (opcional — evita la espera de aprobación)",
|
||||
"inviteCodePlaceholder": "Pega tu código de invitación...",
|
||||
"tokenPlaceholder": "Introduce la clave API...",
|
||||
"tokenAltPlaceholder": "Introduce el token API...",
|
||||
"signIn": "Iniciar sesión",
|
||||
"signingIn": "Iniciando sesión...",
|
||||
"register": "Registrarse",
|
||||
"creatingAccount": "Creando cuenta...",
|
||||
"createAdminAccount": "Crear cuenta de administrador",
|
||||
"signInWithGitHub": "Iniciar sesión con GitHub",
|
||||
"signInWithSSO": "Iniciar sesión con SSO",
|
||||
"loginWithToken": "Iniciar sesión con token",
|
||||
"showTokenLogin": "Iniciar sesión con token API",
|
||||
"hideTokenLogin": "Ocultar inicio con token",
|
||||
"noAccount": "¿No tienes cuenta?",
|
||||
"hasAccount": "¿Ya tienes cuenta?",
|
||||
"or": "o",
|
||||
"errors": {
|
||||
"loginFailed": "Inicio de sesión fallido",
|
||||
"registrationFailed": "Registro fallido",
|
||||
"invalidToken": "Token no válido",
|
||||
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
||||
"enterToken": "Introduce un token",
|
||||
"networkError": "Error de red",
|
||||
"inviteRequired": "Se requiere un código de invitación válido para registrarse"
|
||||
},
|
||||
"messages": {
|
||||
"registrationPending": "Registro exitoso, esperando aprobación."
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Cuenta",
|
||||
"subtitle": "Perfil, credenciales y claves API",
|
||||
"unavailable": "Cuenta no disponible",
|
||||
"unavailableText": "La autenticación debe estar habilitada para administrar tu cuenta.",
|
||||
"tabs": {
|
||||
"profile": "Perfil",
|
||||
"security": "Seguridad",
|
||||
"apiKeys": "Claves API"
|
||||
},
|
||||
"profile": {
|
||||
"displayName": "Nombre para mostrar",
|
||||
"displayNameDescription": "Tu nombre público",
|
||||
"avatarUrl": "URL del avatar",
|
||||
"avatarUrlDescription": "URL de tu imagen de perfil",
|
||||
"avatarUrlPlaceholder": "https://ejemplo.com/avatar.png",
|
||||
"save": "Guardar",
|
||||
"saving": "Guardando...",
|
||||
"updated": "Perfil actualizado",
|
||||
"updateFailed": "Error al actualizar el perfil: {{message}}"
|
||||
},
|
||||
"security": {
|
||||
"currentPassword": "Contraseña actual",
|
||||
"currentPasswordDescription": "Introduce tu contraseña actual para verificar tu identidad",
|
||||
"currentPasswordPlaceholder": "Contraseña actual",
|
||||
"newPassword": "Nueva contraseña",
|
||||
"newPasswordDescription": "Debe tener al menos 8 caracteres",
|
||||
"newPasswordPlaceholder": "Nueva contraseña",
|
||||
"confirmPassword": "Confirmar contraseña",
|
||||
"confirmPasswordDescription": "Vuelve a introducir tu nueva contraseña",
|
||||
"confirmPasswordPlaceholder": "Confirmar nueva contraseña",
|
||||
"changePassword": "Cambiar contraseña",
|
||||
"changing": "Cambiando...",
|
||||
"changed": "Contraseña cambiada",
|
||||
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
||||
"tooShort": "La nueva contraseña debe tener al menos 8 caracteres",
|
||||
"oauthOnly": "La gestión de contraseña no está disponible para cuentas {{provider}}."
|
||||
},
|
||||
"apiKeys": {
|
||||
"create": "Crear clave API",
|
||||
"createDescription": "Genera una clave para acceso programático",
|
||||
"namePlaceholder": "Nombre de la clave (ej. mi-app)",
|
||||
"createButton": "Crear",
|
||||
"creating": "Creando...",
|
||||
"createdToast": "Clave API creada",
|
||||
"createFailed": "Error al crear clave API: {{message}}",
|
||||
"loadFailed": "Error al cargar claves API: {{message}}",
|
||||
"revoke": "Revocar",
|
||||
"revokeKey": "Revocar clave",
|
||||
"revokeTitle": "Revocar clave API",
|
||||
"revokeMessage": "¿Revocar la clave API \"{{name}}\"? Esta acción no se puede deshacer.",
|
||||
"revoked": "Clave API revocada",
|
||||
"revokeFailed": "Error al revocar clave API: {{message}}",
|
||||
"copyNow": "Cópiala ahora — esta clave no se mostrará de nuevo",
|
||||
"copiedToast": "Copiado al portapapeles",
|
||||
"copyFailed": "Error al copiar",
|
||||
"empty": "Aún no hay claves API. Crea una arriba para obtener acceso programático.",
|
||||
"lastUsed": "último uso {{date}}"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Página no encontrada",
|
||||
"text": "Parece que esta página se perdió. Volvamos al buen camino.",
|
||||
"goHome": "Ir al inicio"
|
||||
}
|
||||
}
|
||||
116
core/http/react-ui/public/locales/es/chat.json
Normal file
116
core/http/react-ui/public/locales/es/chat.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"activity": {
|
||||
"thought": "Pensamiento",
|
||||
"tool": "Herramienta",
|
||||
"result": "Resultado",
|
||||
"toolResult": "Resultado de {{name}}",
|
||||
"thinking": "Pensando..."
|
||||
},
|
||||
"header": {
|
||||
"manageModeTooltip": "Este chat puede instalar modelos, editar configuraciones y administrar backends hablando con LocalAI.",
|
||||
"modelInfo": "Información del modelo",
|
||||
"chatSettings": "Ajustes del chat",
|
||||
"modelInfoTitle": "Información del modelo: {{model}}",
|
||||
"editConfig": "Editar configuración",
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"modelInfo": {
|
||||
"backend": "Backend",
|
||||
"modelFile": "Archivo del modelo",
|
||||
"contextSize": "Tamaño del contexto",
|
||||
"threads": "Hilos",
|
||||
"mcp": "MCP",
|
||||
"configured": "Configurado",
|
||||
"chatTemplate": "Plantilla de chat",
|
||||
"yes": "Sí",
|
||||
"gpuLayers": "Capas GPU"
|
||||
},
|
||||
"context": {
|
||||
"label": "Contexto: {{percent}}%",
|
||||
"labelWithTokens": "Contexto: {{percent}}% ({{tokens}} tokens)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ajustes del chat",
|
||||
"manageMode": "Modo administración",
|
||||
"manageModeDesc": "Permite que este chat instale modelos, cambie backends y edite configuraciones hablando con LocalAI.",
|
||||
"systemPrompt": "Prompt del sistema",
|
||||
"systemPromptPlaceholder": "Eres un asistente útil...",
|
||||
"temperature": "Temperatura",
|
||||
"topP": "Top P",
|
||||
"topK": "Top K",
|
||||
"contextSize": "Tamaño del contexto",
|
||||
"contextSizePlaceholder": "2048",
|
||||
"clearHistory": "Borrar historial del chat"
|
||||
},
|
||||
"empty": {
|
||||
"manageTitle": "Administra LocalAI chateando",
|
||||
"manageText": "Pide instalar modelos, cambiar backends, editar configuraciones o consultar el estado. El asistente resumirá las acciones y esperará tu confirmación antes de cambiar nada.",
|
||||
"startTitle": "Inicia una conversación",
|
||||
"readyText": "Listo para chatear con {{model}}",
|
||||
"selectModelText": "Selecciona un modelo arriba para comenzar",
|
||||
"suggestionsManage": [
|
||||
"¿Qué está instalado?",
|
||||
"Instala un modelo de chat",
|
||||
"Muestra el estado del sistema",
|
||||
"Actualiza un backend"
|
||||
],
|
||||
"suggestionsChat": [
|
||||
"Explica cómo funciona esto",
|
||||
"Ayúdame a escribir código",
|
||||
"Resume un documento",
|
||||
"Lluvia de ideas"
|
||||
],
|
||||
"recent": "Recientes",
|
||||
"noMessages": "Aún no hay mensajes",
|
||||
"hintEnter": "Enter para enviar",
|
||||
"hintShiftEnter": "Shift+Enter para nueva línea",
|
||||
"hintAttach": "Adjuntar archivos"
|
||||
},
|
||||
"errors": {
|
||||
"viewTraces": "Ver trazas para más detalles"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"regenerate": "Regenerar"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Transfiriendo modelo...",
|
||||
"transferringTo": "Transfiriendo modelo a {{node}}..."
|
||||
},
|
||||
"tokens": {
|
||||
"perSec": "{{count}} tok/s",
|
||||
"peak": "Pico: {{count}} tok/s",
|
||||
"usage": "{{prompt}}p + {{completion}}c = {{total}}"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Mensaje...",
|
||||
"attachFile": "Adjuntar archivo",
|
||||
"stopGenerating": "Detener generación",
|
||||
"canvasTitle": "Canvas — extrae bloques de código y multimedia en un panel lateral para vista previa, copia y descarga",
|
||||
"canvasLabel": "Canvas",
|
||||
"openCanvas": "Abrir panel canvas"
|
||||
},
|
||||
"deleteAllDialog": {
|
||||
"title": "Eliminar todos los chats",
|
||||
"message": "¿Eliminar todos los chats? Esta acción no se puede deshacer.",
|
||||
"confirm": "Eliminar todo"
|
||||
},
|
||||
"toasts": {
|
||||
"selectModel": "Por favor selecciona un modelo",
|
||||
"copied": "Copiado al portapapeles"
|
||||
},
|
||||
"menu": {
|
||||
"trigger": "Chats",
|
||||
"triggerTitle": "Conversaciones (Ctrl/Cmd+K)",
|
||||
"search": "Buscar conversaciones...",
|
||||
"clearSearch": "Limpiar búsqueda",
|
||||
"noMatch": "Ninguna conversación coincide con tu búsqueda",
|
||||
"noConversations": "Aún no hay conversaciones",
|
||||
"rename": "Renombrar",
|
||||
"exportMarkdown": "Exportar como Markdown",
|
||||
"deleteChat": "Eliminar chat",
|
||||
"newChat": "Nuevo chat",
|
||||
"clearAll": "Borrar todo",
|
||||
"deleteAllTitle": "Eliminar todas las conversaciones"
|
||||
}
|
||||
}
|
||||
43
core/http/react-ui/public/locales/es/collections.json
Normal file
43
core/http/react-ui/public/locales/es/collections.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"title": "Base de conocimiento",
|
||||
"subtitle": "Administra colecciones de documentos para el RAG de los agentes",
|
||||
"newPlaceholder": "Nombre de la nueva colección...",
|
||||
"actions": {
|
||||
"create": "Crear",
|
||||
"creating": "Creando...",
|
||||
"details": "Detalles",
|
||||
"reset": "Restablecer",
|
||||
"delete": "Eliminar",
|
||||
"viewDetails": "Ver detalles",
|
||||
"resetCollection": "Restablecer colección",
|
||||
"deleteCollection": "Eliminar colección"
|
||||
},
|
||||
"sections": {
|
||||
"yourCollections": "Tus colecciones",
|
||||
"otherUsersCollections": "Colecciones de otros usuarios"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Aún no hay colecciones",
|
||||
"text": "Las colecciones te permiten organizar documentos en bases de conocimiento que los agentes pueden buscar usando RAG (Retrieval-Augmented Generation). Crea una colección arriba para empezar.",
|
||||
"noPersonal": "Aún no tienes colecciones."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Eliminar colección",
|
||||
"message": "¿Eliminar la colección \"{{name}}\"? Esto eliminará todas las entradas y no se puede deshacer.",
|
||||
"confirm": "Eliminar"
|
||||
},
|
||||
"resetDialog": {
|
||||
"title": "Restablecer colección",
|
||||
"message": "¿Restablecer la colección \"{{name}}\"? Esto eliminará todas las entradas pero mantendrá la colección.",
|
||||
"confirm": "Restablecer"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Error al cargar colecciones: {{message}}",
|
||||
"created": "Colección \"{{name}}\" creada",
|
||||
"createFailed": "Error al crear colección: {{message}}",
|
||||
"deleted": "Colección \"{{name}}\" eliminada",
|
||||
"deleteFailed": "Error al eliminar colección: {{message}}",
|
||||
"reset": "Colección \"{{name}}\" restablecida",
|
||||
"resetFailed": "Error al restablecer colección: {{message}}"
|
||||
}
|
||||
}
|
||||
109
core/http/react-ui/public/locales/es/common.json
Normal file
109
core/http/react-ui/public/locales/es/common.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "Guardar",
|
||||
"saving": "Guardando...",
|
||||
"cancel": "Cancelar",
|
||||
"close": "Cerrar",
|
||||
"confirm": "Confirmar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"add": "Añadir",
|
||||
"remove": "Quitar",
|
||||
"create": "Crear",
|
||||
"update": "Actualizar",
|
||||
"refresh": "Refrescar",
|
||||
"reload": "Recargar",
|
||||
"retry": "Reintentar",
|
||||
"search": "Buscar",
|
||||
"filter": "Filtrar",
|
||||
"clear": "Limpiar",
|
||||
"reset": "Restablecer",
|
||||
"apply": "Aplicar",
|
||||
"back": "Atrás",
|
||||
"next": "Siguiente",
|
||||
"previous": "Anterior",
|
||||
"open": "Abrir",
|
||||
"submit": "Enviar",
|
||||
"select": "Seleccionar",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"download": "Descargar",
|
||||
"upload": "Subir",
|
||||
"import": "Importar",
|
||||
"export": "Exportar",
|
||||
"view": "Ver",
|
||||
"details": "Detalles",
|
||||
"settings": "Configuración",
|
||||
"help": "Ayuda",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"loading": "Cargando..."
|
||||
},
|
||||
"status": {
|
||||
"loading": "Cargando...",
|
||||
"saving": "Guardando...",
|
||||
"saved": "Guardado",
|
||||
"ready": "Listo",
|
||||
"running": "En ejecución",
|
||||
"stopped": "Detenido",
|
||||
"starting": "Iniciando...",
|
||||
"stopping": "Deteniendo...",
|
||||
"pending": "Pendiente",
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Deshabilitado",
|
||||
"online": "En línea",
|
||||
"offline": "Desconectado",
|
||||
"error": "Error",
|
||||
"success": "Éxito",
|
||||
"warning": "Advertencia",
|
||||
"info": "Info",
|
||||
"empty": "Sin elementos",
|
||||
"none": "Ninguno",
|
||||
"unknown": "Desconocido"
|
||||
},
|
||||
"dialogs": {
|
||||
"confirmDelete": {
|
||||
"title": "Confirmar eliminación",
|
||||
"message": "¿Seguro que quieres eliminarlo? Esta acción no se puede deshacer.",
|
||||
"confirm": "Eliminar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"unsavedChanges": {
|
||||
"title": "Cambios sin guardar",
|
||||
"message": "Tienes cambios sin guardar. ¿Quieres descartarlos?",
|
||||
"discard": "Descartar",
|
||||
"keepEditing": "Seguir editando"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"required": "Obligatorio",
|
||||
"optional": "Opcional",
|
||||
"name": "Nombre",
|
||||
"description": "Descripción",
|
||||
"type": "Tipo",
|
||||
"value": "Valor",
|
||||
"search": "Buscar...",
|
||||
"selectPlaceholder": "Selecciona una opción..."
|
||||
},
|
||||
"time": {
|
||||
"now": "ahora",
|
||||
"secondsAgo_one": "hace {{count}} segundo",
|
||||
"secondsAgo_other": "hace {{count}} segundos",
|
||||
"minutesAgo_one": "hace {{count}} minuto",
|
||||
"minutesAgo_other": "hace {{count}} minutos",
|
||||
"hoursAgo_one": "hace {{count}} hora",
|
||||
"hoursAgo_other": "hace {{count}} horas",
|
||||
"daysAgo_one": "hace {{count}} día",
|
||||
"daysAgo_other": "hace {{count}} días"
|
||||
},
|
||||
"units": {
|
||||
"bytes": "B",
|
||||
"kilobytes": "KB",
|
||||
"megabytes": "MB",
|
||||
"gigabytes": "GB",
|
||||
"terabytes": "TB"
|
||||
}
|
||||
}
|
||||
17
core/http/react-ui/public/locales/es/errors.json
Normal file
17
core/http/react-ui/public/locales/es/errors.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"generic": "Algo salió mal",
|
||||
"network": "Error de red. Comprueba tu conexión e inténtalo de nuevo.",
|
||||
"unauthorized": "No estás autorizado para realizar esta acción.",
|
||||
"forbidden": "Acceso denegado.",
|
||||
"notFound": "El recurso solicitado no se encontró.",
|
||||
"serverError": "Error del servidor. Inténtalo de nuevo más tarde.",
|
||||
"loadFailed": "Error al cargar: {{message}}",
|
||||
"saveFailed": "Error al guardar: {{message}}",
|
||||
"deleteFailed": "Error al eliminar: {{message}}",
|
||||
"updateFailed": "Error al actualizar: {{message}}",
|
||||
"createFailed": "Error al crear: {{message}}",
|
||||
"operationFailed": "Operación fallida: {{message}}",
|
||||
"invalidInput": "Entrada no válida. Revisa el formulario e inténtalo de nuevo.",
|
||||
"tryAgain": "Por favor, inténtalo de nuevo.",
|
||||
"contactAdmin": "Si el problema persiste, contacta con tu administrador."
|
||||
}
|
||||
66
core/http/react-ui/public/locales/es/home.json
Normal file
66
core/http/react-ui/public/locales/es/home.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"cluster": {
|
||||
"vram": "VRAM del clúster",
|
||||
"ram": "RAM del clúster",
|
||||
"nodesOnline": "{{healthy}}/{{total}} nodos en línea"
|
||||
},
|
||||
"resourceGpu": "GPU",
|
||||
"resourceRam": "RAM",
|
||||
"assistant": {
|
||||
"title": "Administra LocalAI chateando",
|
||||
"description": "Instala modelos, cambia backends, edita configuraciones y consulta el estado hablando con LocalAI.",
|
||||
"open": "Abrir asistente",
|
||||
"tooltip": "Administra LocalAI chateando"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Mensaje...",
|
||||
"attachImage": "Adjuntar imagen",
|
||||
"attachAudio": "Adjuntar audio",
|
||||
"attachFile": "Adjuntar archivo",
|
||||
"enterToSend": "Enter para enviar",
|
||||
"selectModelFirst": "Selecciona un modelo primero",
|
||||
"sendMessage": "Enviar mensaje",
|
||||
"selectModelToast": "Por favor selecciona un modelo primero"
|
||||
},
|
||||
"quickLinks": {
|
||||
"manageByChat": "Administrar por chat",
|
||||
"installedModels": "Modelos instalados",
|
||||
"browseGallery": "Explorar galería",
|
||||
"importModel": "Importar modelo",
|
||||
"documentation": "Documentación"
|
||||
},
|
||||
"loadedModels": {
|
||||
"count_one": "{{count}} modelo cargado",
|
||||
"count_other": "{{count}} modelos cargados",
|
||||
"stop": "Detener modelo",
|
||||
"stopAll": "Detener todos"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Detener modelo",
|
||||
"message": "¿Detener el modelo {{model}}?",
|
||||
"confirm": "Detener {{model}}",
|
||||
"stopAllTitle": "Detener todos los modelos",
|
||||
"stopAllMessage": "¿Detener todos los {{count}} modelos cargados?",
|
||||
"stopAllConfirm": "Detener todos",
|
||||
"stoppedToast": "{{model}} detenido",
|
||||
"allStoppedToast": "Todos los modelos detenidos",
|
||||
"stopFailed": "Error al detener: {{message}}"
|
||||
},
|
||||
"wizard": {
|
||||
"getStarted": "Empieza con {{name}}",
|
||||
"intro": "Instala tu primer modelo para comenzar. Explora la galería o importa el tuyo.",
|
||||
"steps": {
|
||||
"step1Title": "Explora la galería de modelos",
|
||||
"step1Body": "Encuentra el modelo adecuado para tus necesidades en nuestra colección.",
|
||||
"step2Title": "Instala un modelo",
|
||||
"step2Body": "Haz clic en instalar para descargarlo y configurarlo automáticamente.",
|
||||
"step3Title": "Empieza a chatear",
|
||||
"step3Body": "Chatea con tu modelo desde el navegador o usa la API."
|
||||
},
|
||||
"browseGallery": "Explorar galería de modelos",
|
||||
"importModel": "Importar modelo",
|
||||
"docs": "Documentación",
|
||||
"noModelsTitle": "No hay modelos disponibles",
|
||||
"noModelsBody": "No hay modelos instalados aún. Pídele a tu administrador que configure modelos para que puedas empezar a chatear."
|
||||
}
|
||||
}
|
||||
142
core/http/react-ui/public/locales/es/importModel.json
Normal file
142
core/http/react-ui/public/locales/es/importModel.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"title": "Importar nuevo modelo",
|
||||
"subtitle": {
|
||||
"simple": "Importa un modelo desde una URI — la auto-detección elige el backend.",
|
||||
"powerYaml": "Escribe la configuración YAML completa del modelo.",
|
||||
"powerPrefs": "Preferencias de importación detalladas."
|
||||
},
|
||||
"actions": {
|
||||
"import": "Importar modelo",
|
||||
"importing": "Importando...",
|
||||
"create": "Crear",
|
||||
"saving": "Guardando...",
|
||||
"browseHF": "Explorar modelos en HF",
|
||||
"addCustom": "Añadir personalizado",
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"form": {
|
||||
"modelUri": "URI del modelo",
|
||||
"uriPlaceholder": "huggingface://TheBloke/Llama-2-7B-Chat-GGUF o https://example.com/model.gguf",
|
||||
"uriHint": "Introduce la URI o ruta del archivo de modelo a importar",
|
||||
"supportedFormats": "Formatos URI soportados",
|
||||
"options": "Opciones",
|
||||
"preferences": "Preferencias (opcionales)",
|
||||
"commonPreferences": "Preferencias comunes",
|
||||
"customPreferences": "Preferencias personalizadas",
|
||||
"customKeyValueHint": "Añade pares clave-valor personalizados para configuración avanzada.",
|
||||
"preferenceKey": "Clave de preferencia para la fila {{index}}",
|
||||
"preferenceValue": "Valor de preferencia para la fila {{index}}",
|
||||
"removePref": "Eliminar esta preferencia",
|
||||
"key": "Clave",
|
||||
"value": "Valor",
|
||||
"backend": "Backend",
|
||||
"backendAuto": "Auto-detectar (basado en la URI)",
|
||||
"backendLoading": "Cargando backends…",
|
||||
"backendSearch": "Buscar backends...",
|
||||
"backendHint": "Forzar un backend específico. Déjalo vacío para auto-detectar desde la URI. Los marcados con \"selección manual\" no son auto-detectables — selecciónalos tú si sabes lo que necesita el modelo.",
|
||||
"backendErrorHint": "No se pudo cargar la lista de backends — solo auto-detect.",
|
||||
"backendNotInstalled": "Este backend aún no está instalado. Enviar la importación lo descargará primero.",
|
||||
"modelName": "Nombre del modelo",
|
||||
"modelNamePlaceholder": "Déjalo vacío para usar el nombre del archivo",
|
||||
"modelNameHint": "Nombre personalizado para el modelo. Si está vacío, se usará el nombre del archivo.",
|
||||
"description": "Descripción",
|
||||
"descriptionPlaceholder": "Déjalo vacío para usar la descripción predeterminada",
|
||||
"descriptionHint": "Descripción personalizada para el modelo.",
|
||||
"quantizations": "Cuantizaciones",
|
||||
"quantizationsPlaceholder": "q4_k_m,q4_k_s,q3_k_m (separadas por comas)",
|
||||
"quantizationsHint": "Cuantizaciones preferidas (separadas por comas). Déjalo vacío para el predeterminado (q4_k_m).",
|
||||
"mmprojQuantizations": "Cuantizaciones MMProj",
|
||||
"mmprojQuantizationsPlaceholder": "fp16,fp32 (separadas por comas)",
|
||||
"mmprojQuantizationsHint": "Cuantizaciones MMProj preferidas. Déjalo vacío para el predeterminado (fp16).",
|
||||
"embeddings": "Embeddings",
|
||||
"embeddingsHint": "Habilita el soporte de embeddings para este modelo.",
|
||||
"modelType": "Tipo de modelo",
|
||||
"modelTypePlaceholder": "AutoModelForCausalLM (para el backend transformers)",
|
||||
"modelTypeHint": "Tipo de modelo para el backend transformers. Ejemplos: AutoModelForCausalLM, SentenceTransformer, Mamba.",
|
||||
"pipelineType": "Tipo de pipeline",
|
||||
"pipelineTypeHint": "Tipo de pipeline para el backend diffusers.",
|
||||
"schedulerType": "Tipo de scheduler",
|
||||
"schedulerTypePlaceholder": "k_dpmpp_2m (opcional)",
|
||||
"schedulerTypeHint": "Tipo de scheduler para el backend diffusers. Ejemplos: k_dpmpp_2m, euler_a, ddim.",
|
||||
"enableParameters": "Parámetros habilitados",
|
||||
"enableParametersPlaceholder": "negative_prompt,num_inference_steps (separados por comas)",
|
||||
"enableParametersHint": "Parámetros habilitados para el backend diffusers (separados por comas).",
|
||||
"cuda": "CUDA",
|
||||
"cudaHint": "Habilita el soporte CUDA para aceleración GPU.",
|
||||
"yamlEditor": "Editor de configuración YAML",
|
||||
"manualPick": "selección manual",
|
||||
"manualPickTooltip": "El auto-detect no enrutará a este backend. Selecciónalo aquí si sabes que es lo que quieres."
|
||||
},
|
||||
"modality": {
|
||||
"text": "LLM de texto",
|
||||
"asr": "Reconocimiento de voz",
|
||||
"tts": "Texto a voz",
|
||||
"image": "Imagen / Video",
|
||||
"embeddings": "Embeddings",
|
||||
"reranker": "Rerankers",
|
||||
"detection": "Detección de objetos",
|
||||
"vad": "Detección de actividad de voz",
|
||||
"other": "Otro"
|
||||
},
|
||||
"powerTabs": {
|
||||
"ariaLabel": "Pestaña de modo avanzado",
|
||||
"preferences": "Preferencias",
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"switchDialog": {
|
||||
"title": "¿Mantener tus preferencias personalizadas?",
|
||||
"body": "Cambiar al modo Simple oculta las preferencias más allá del backend, nombre y descripción. Se enviarán de todos modos al importar.",
|
||||
"cancel": "Cancelar",
|
||||
"discard": "Descartar y cambiar",
|
||||
"keep": "Mantener y cambiar"
|
||||
},
|
||||
"estimate": {
|
||||
"title": "Requisitos estimados",
|
||||
"download": "Descarga: {{size}}",
|
||||
"vram": "VRAM: {{vram}}"
|
||||
},
|
||||
"toasts": {
|
||||
"noUri": "Por favor introduce una URI de modelo",
|
||||
"noYaml": "Por favor introduce la configuración YAML",
|
||||
"started": "¡Importación iniciada! Siguiendo el progreso...",
|
||||
"startedWithMeta": "¡Importación iniciada! Siguiendo el progreso... ({{meta}})",
|
||||
"imported": "¡Modelo importado con éxito!",
|
||||
"importedYaml": "¡Configuración del modelo importada con éxito!",
|
||||
"importFailed": "Importación fallida: {{message}}",
|
||||
"startImportFailed": "Error al iniciar importación: {{message}}",
|
||||
"backendsLoadFailed": "No se pudo cargar la lista de backends — usando solo auto-detect",
|
||||
"modalityClearedBackend": "Selección de backend limpiada — no estaba en el grupo {{label}}.",
|
||||
"copied": "Copiado al portapapeles"
|
||||
},
|
||||
"uriFormats": {
|
||||
"huggingface": {
|
||||
"title": "HuggingFace",
|
||||
"standard": "Formato HuggingFace estándar",
|
||||
"short": "Formato HuggingFace corto",
|
||||
"fullUrl": "URL HuggingFace completa"
|
||||
},
|
||||
"http": {
|
||||
"title": "URLs HTTP/HTTPS",
|
||||
"direct": "Descarga directa desde cualquier URL HTTPS"
|
||||
},
|
||||
"local": {
|
||||
"title": "Archivos locales",
|
||||
"filePath": "Ruta de archivo local (absoluta)",
|
||||
"directYaml": "Archivo de configuración YAML local directo"
|
||||
},
|
||||
"oci": {
|
||||
"title": "Registro OCI",
|
||||
"registry": "Registro de contenedores OCI",
|
||||
"tarball": "Archivo tarball OCI local"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"model": "Formato de modelo Ollama"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "Archivos de configuración YAML",
|
||||
"remote": "Archivo de configuración YAML remoto",
|
||||
"local": "Archivo de configuración YAML local"
|
||||
}
|
||||
}
|
||||
}
|
||||
154
core/http/react-ui/public/locales/es/media.json
Normal file
154
core/http/react-ui/public/locales/es/media.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Imágenes",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Sonido"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Generación de imágenes",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe la imagen que quieres generar...",
|
||||
"negativePrompt": "Prompt negativo",
|
||||
"negativePromptPlaceholder": "Qué evitar...",
|
||||
"size": "Tamaño",
|
||||
"count": "Cantidad (1-4)",
|
||||
"advanced": "Configuración avanzada",
|
||||
"imageInputs": "Entradas de imagen",
|
||||
"steps": "Pasos",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Aleatorio",
|
||||
"sourceImage": "Imagen fuente (img2img)",
|
||||
"refImages": "Imágenes de referencia",
|
||||
"refImagesAdded_one": "{{count}} imagen añadida",
|
||||
"refImagesAdded_other": "{{count}} imágenes añadidas"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "Las imágenes generadas aparecerán aquí",
|
||||
"toasts": {
|
||||
"noPrompt": "Por favor introduce un prompt",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"noResults": "No se generaron imágenes"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Generación de video",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe el video que quieres generar...",
|
||||
"duration": "Duración (s)",
|
||||
"fps": "FPS",
|
||||
"size": "Tamaño",
|
||||
"advanced": "Configuración avanzada",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Aleatorio"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar video",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "Los videos generados aparecerán aquí",
|
||||
"toasts": {
|
||||
"noPrompt": "Por favor introduce un prompt",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"noResults": "No se generaron videos"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Texto a voz",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "ID de voz opcional",
|
||||
"input": "Texto",
|
||||
"inputPlaceholder": "Introduce el texto a sintetizar..."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar audio",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "El audio generado aparecerá aquí",
|
||||
"toasts": {
|
||||
"noText": "Por favor introduce texto",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"generateFailed": "Generación fallida"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Generación de sonido",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe el sonido que quieres generar...",
|
||||
"duration": "Duración (s)",
|
||||
"language": "Idioma",
|
||||
"vocalLanguage": "Idioma vocal",
|
||||
"lyrics": "Letras (opcional)",
|
||||
"lyricsPlaceholder": "Letras para generación vocal",
|
||||
"advanced": "Configuración avanzada",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Aleatorio"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "El audio generado aparecerá aquí",
|
||||
"toasts": {
|
||||
"noPrompt": "Por favor introduce un prompt",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"generateFailed": "Generación fallida"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Hablar",
|
||||
"subtitle": "Conversación de voz en tiempo real",
|
||||
"actions": {
|
||||
"start": "Iniciar sesión",
|
||||
"stop": "Terminar sesión",
|
||||
"connecting": "Conectando...",
|
||||
"muted": "Silenciado",
|
||||
"mute": "Silenciar",
|
||||
"unmute": "Activar sonido"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Idioma",
|
||||
"languagePlaceholder": "es",
|
||||
"instructions": "Instrucciones",
|
||||
"instructionsPlaceholder": "Define la personalidad del asistente..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Inactivo",
|
||||
"connecting": "Conectando...",
|
||||
"listening": "Escuchando...",
|
||||
"speaking": "Hablando...",
|
||||
"ended": "Sesión finalizada"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Selecciona un modelo primero",
|
||||
"connectFailed": "Error al conectar: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Historial",
|
||||
"empty": "Sin historial aún",
|
||||
"deleteEntry": "Eliminar entrada",
|
||||
"clear": "Borrar historial",
|
||||
"clearTitle": "Borrar todo el historial",
|
||||
"clearMessage": "¿Eliminar todas las entradas del historial? Esto no se puede deshacer.",
|
||||
"clearConfirm": "Borrar",
|
||||
"cleared": "Historial borrado"
|
||||
}
|
||||
}
|
||||
85
core/http/react-ui/public/locales/es/models.json
Normal file
85
core/http/react-ui/public/locales/es/models.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"title": "Instalar modelos",
|
||||
"subtitle": "Explora e instala modelos de IA desde la galería",
|
||||
"stats": {
|
||||
"available": "Disponibles",
|
||||
"installed": "Instalados"
|
||||
},
|
||||
"actions": {
|
||||
"addModel": "Añadir modelo",
|
||||
"importModel": "Importar modelo",
|
||||
"install": "Instalar",
|
||||
"reinstall": "Reinstalar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Todos",
|
||||
"llm": "LLM",
|
||||
"image": "Imagen",
|
||||
"multimodal": "Multimodal",
|
||||
"vision": "Visión",
|
||||
"tts": "TTS",
|
||||
"stt": "STT",
|
||||
"embedding": "Embedding",
|
||||
"rerank": "Rerank",
|
||||
"allBackends": "Todos los backends",
|
||||
"searchBackends": "Buscar backends..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar modelos...",
|
||||
"clearFilters": "Limpiar filtros"
|
||||
},
|
||||
"table": {
|
||||
"modelName": "Nombre del modelo",
|
||||
"description": "Descripción",
|
||||
"backend": "Backend",
|
||||
"sizeVram": "Tamaño / VRAM",
|
||||
"status": "Estado",
|
||||
"actions": "Acciones",
|
||||
"size": "Tamaño: {{size}}",
|
||||
"vram": "VRAM: {{vram}}",
|
||||
"fits": "Cabe",
|
||||
"mayNotFit": "Puede no caber",
|
||||
"trustRemoteCode": "Trust Remote Code",
|
||||
"installing": "Instalando",
|
||||
"installingPct": "Instalando · {{percent}}%",
|
||||
"installed": "Instalado",
|
||||
"notInstalled": "No instalado"
|
||||
},
|
||||
"detail": {
|
||||
"description": "Descripción",
|
||||
"gallery": "Galería",
|
||||
"backend": "Backend",
|
||||
"size": "Tamaño",
|
||||
"vram": "VRAM",
|
||||
"license": "Licencia",
|
||||
"tags": "Etiquetas",
|
||||
"links": "Enlaces",
|
||||
"warning": "Advertencia",
|
||||
"files": "Archivos",
|
||||
"fitsGpu": "Cabe en la GPU",
|
||||
"mayNotFitGpu": "Puede no caber en la GPU",
|
||||
"requiresTrustRemoteCode": "Requiere Trust Remote Code",
|
||||
"fileCount_one": "{{count}} archivo",
|
||||
"fileCount_other": "{{count}} archivos",
|
||||
"filename": "Nombre del archivo",
|
||||
"uri": "URI",
|
||||
"sha256": "SHA256"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No se encontraron modelos",
|
||||
"withFilters": "Ningún modelo coincide con la búsqueda o filtros actuales.",
|
||||
"noFilters": "La galería de modelos está vacía."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Eliminar modelo",
|
||||
"message": "¿Eliminar el modelo {{model}}?",
|
||||
"confirm": "Eliminar {{model}}",
|
||||
"deletingToast": "Eliminando {{model}}..."
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Error al cargar modelos: {{message}}",
|
||||
"installFailed": "Error al instalar: {{message}}",
|
||||
"deleteFailed": "Error al eliminar: {{message}}"
|
||||
}
|
||||
}
|
||||
51
core/http/react-ui/public/locales/es/nav.json
Normal file
51
core/http/react-ui/public/locales/es/nav.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"appName": "LocalAI",
|
||||
"openMenu": "Abrir menú",
|
||||
"closeMenu": "Cerrar menú",
|
||||
"primaryNavigation": "Navegación principal",
|
||||
"switchToLightMode": "Cambiar a tema claro",
|
||||
"switchToDarkMode": "Cambiar a tema oscuro",
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"collapseSidebar": "Contraer barra lateral",
|
||||
"changeLanguage": "Cambiar idioma",
|
||||
"logout": "Cerrar sesión",
|
||||
"accountSettings": "Configuración de la cuenta",
|
||||
"account": "Cuenta",
|
||||
"accountFor": "Cuenta: {{name}}",
|
||||
"sections": {
|
||||
"tools": "Herramientas",
|
||||
"biometrics": "Biometría",
|
||||
"agents": "Agentes",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"items": {
|
||||
"home": "Inicio",
|
||||
"installModels": "Instalar modelos",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Hablar",
|
||||
"fineTune": "Ajuste fino (Experimental)",
|
||||
"quantize": "Cuantización (Experimental)",
|
||||
"faceRecognition": "Reconocimiento facial",
|
||||
"voiceRecognition": "Reconocimiento de voz",
|
||||
"agents": "Agentes",
|
||||
"skills": "Habilidades",
|
||||
"memory": "Memoria",
|
||||
"mcpJobs": "Trabajos MCP CI",
|
||||
"usage": "Uso",
|
||||
"users": "Usuarios",
|
||||
"backends": "Backends",
|
||||
"traces": "Trazas",
|
||||
"nodes": "Nodos",
|
||||
"swarm": "Swarm",
|
||||
"system": "Sistema",
|
||||
"settings": "Configuración",
|
||||
"api": "API"
|
||||
},
|
||||
"footer": {
|
||||
"github": "GitHub",
|
||||
"documentation": "Documentación",
|
||||
"author": "Autor",
|
||||
"copyright": "© 2023-{{year}} {{author}}"
|
||||
}
|
||||
}
|
||||
79
core/http/react-ui/public/locales/es/skills.json
Normal file
79
core/http/react-ui/public/locales/es/skills.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "Habilidades",
|
||||
"subtitle": "Administra las habilidades de los agentes (instrucciones y recursos reutilizables)",
|
||||
"unavailable": {
|
||||
"subtitle": "El servicio de habilidades no está disponible o el índice se está reconstruyendo. Inténtalo de nuevo en un momento.",
|
||||
"retry": "Reintentar"
|
||||
},
|
||||
"actions": {
|
||||
"newSkill": "Nueva habilidad",
|
||||
"createSkill": "Crear habilidad",
|
||||
"import": "Importar",
|
||||
"importing": "Importando...",
|
||||
"gitRepos": "Repos Git",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"export": "Exportar",
|
||||
"sync": "Sincronizar",
|
||||
"addRepo": "Añadir repo",
|
||||
"adding": "Añadiendo...",
|
||||
"remove": "Quitar",
|
||||
"enable": "Habilitar",
|
||||
"disable": "Deshabilitar"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar habilidades..."
|
||||
},
|
||||
"git": {
|
||||
"title": "Repositorios Git",
|
||||
"description": "Añade repositorios Git de los que obtener habilidades. Las habilidades aparecerán en la lista después de sincronizar.",
|
||||
"urlPlaceholder": "https://github.com/user/repo o git@github.com:user/repo.git",
|
||||
"noRepos": "No hay repos Git configurados. Añade uno arriba.",
|
||||
"disabled": "Deshabilitado",
|
||||
"removeRepo": "Quitar repo"
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "Sin descripción",
|
||||
"readOnly": "Solo lectura",
|
||||
"editTitle": "Editar habilidad",
|
||||
"deleteTitle": "Eliminar habilidad",
|
||||
"exportTitle": "Exportar como .tar.gz"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No se encontraron habilidades",
|
||||
"text": "Crea una habilidad o importa una para empezar.",
|
||||
"noPersonal": "Aún no tienes habilidades."
|
||||
},
|
||||
"sections": {
|
||||
"yourSkills": "Tus habilidades",
|
||||
"otherUsersSkills": "Habilidades de otros usuarios"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Eliminar habilidad",
|
||||
"message": "¿Eliminar la habilidad \"{{name}}\"? Esta acción no se puede deshacer.",
|
||||
"confirm": "Eliminar"
|
||||
},
|
||||
"removeRepoDialog": {
|
||||
"title": "Quitar repositorio Git",
|
||||
"message": "¿Quitar este repositorio Git? Las habilidades de él ya no estarán disponibles.",
|
||||
"confirm": "Quitar"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Error al cargar habilidades",
|
||||
"deleted": "Habilidad \"{{name}}\" eliminada",
|
||||
"deleteFailed": "Error al eliminar habilidad",
|
||||
"exported": "Habilidad \"{{name}}\" exportada",
|
||||
"exportFailed": "Error al exportar",
|
||||
"imported": "Habilidad importada desde \"{{file}}\"",
|
||||
"importFailed": "Error al importar",
|
||||
"loadReposFailed": "Error al cargar repos Git",
|
||||
"repoAdded": "Repo Git añadido y sincronizando",
|
||||
"addRepoFailed": "Error al añadir repo",
|
||||
"synced": "Repo sincronizado",
|
||||
"syncFailed": "Error al sincronizar",
|
||||
"toggled": "Repo actualizado",
|
||||
"toggleFailed": "Error al actualizar",
|
||||
"removed": "Repo eliminado",
|
||||
"removeFailed": "Error al eliminar"
|
||||
}
|
||||
}
|
||||
62
core/http/react-ui/public/locales/it/admin.json
Normal file
62
core/http/react-ui/public/locales/it/admin.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"manage": {
|
||||
"title": "Sistema",
|
||||
"subtitle": "Gestisci modelli e backend installati"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"subtitle": "Configura le impostazioni runtime di LocalAI",
|
||||
"saved": "Impostazioni salvate con successo",
|
||||
"saveFailed": "Salvataggio fallito: {{message}}",
|
||||
"loadFailed": "Caricamento impostazioni fallito: {{message}}",
|
||||
"sections": {
|
||||
"branding": "Branding",
|
||||
"watchdog": "Watchdog",
|
||||
"memory": "Memoria",
|
||||
"backends": "Backend",
|
||||
"performance": "Prestazioni",
|
||||
"tracing": "Tracing",
|
||||
"api": "API e CORS",
|
||||
"p2p": "P2P",
|
||||
"galleries": "Gallerie",
|
||||
"apikeys": "Chiavi API",
|
||||
"agents": "Job degli agenti",
|
||||
"agentpool": "Pool agenti",
|
||||
"assistant": "LocalAI Assistant",
|
||||
"responses": "Risposte"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"title": "Gestione backend",
|
||||
"subtitle": "Scopri e installa backend AI per i tuoi modelli"
|
||||
},
|
||||
"backendLogs": {
|
||||
"title": "Log dei backend",
|
||||
"subtitle": "Visualizza i log dei backend in esecuzione",
|
||||
"empty": "Nessun log disponibile"
|
||||
},
|
||||
"traces": {
|
||||
"title": "Tracce",
|
||||
"subtitle": "Visualizza richieste API, risposte e operazioni dei backend registrate"
|
||||
},
|
||||
"nodes": {
|
||||
"title": "Nodi distribuiti",
|
||||
"subtitle": "Gestisci i nodi worker dei backend e degli agenti"
|
||||
},
|
||||
"p2p": {
|
||||
"title": "Calcolo AI distribuito",
|
||||
"subtitle": "Scala i tuoi carichi di lavoro AI su più dispositivi con la distribuzione peer-to-peer"
|
||||
},
|
||||
"users": {
|
||||
"title": "Utenti",
|
||||
"subtitle": "Gestisci utenti registrati, ruoli e inviti"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Utilizzo",
|
||||
"subtitle": "Statistiche di utilizzo dei token API"
|
||||
},
|
||||
"explorer": {
|
||||
"title": "Esplora risorse",
|
||||
"subtitle": "Sfoglia file e configurazioni"
|
||||
}
|
||||
}
|
||||
55
core/http/react-ui/public/locales/it/agents.json
Normal file
55
core/http/react-ui/public/locales/it/agents.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"title": "Agenti",
|
||||
"subtitle": "Gestisci agenti AI autonomi",
|
||||
"actions": {
|
||||
"agentHub": "Agent Hub",
|
||||
"import": "Importa",
|
||||
"createAgent": "Crea agente",
|
||||
"edit": "Modifica",
|
||||
"chat": "Chat",
|
||||
"export": "Esporta",
|
||||
"delete": "Elimina",
|
||||
"pause": "Pausa",
|
||||
"resume": "Riprendi"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"status": "Stato",
|
||||
"events": "Eventi",
|
||||
"actions": "Azioni",
|
||||
"eventsTooltip": "{{count}} eventi - Clicca per visualizzare"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Cerca agenti...",
|
||||
"summary_one": "{{shown}} di {{total}} agente",
|
||||
"summary_other": "{{shown}} di {{total}} agenti"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigured": "Nessun agente configurato",
|
||||
"noConfiguredText": "Crea un agente per iniziare con i flussi di lavoro AI autonomi.",
|
||||
"browseHub": "Non sai da dove iniziare? Sfoglia l'<1>Agent Hub</1> per trovare configurazioni di agenti pronte da importare.",
|
||||
"noMatching": "Nessun agente corrispondente",
|
||||
"noMatchingText": "Nessun agente corrisponde a \"{{query}}\""
|
||||
},
|
||||
"sections": {
|
||||
"yourAgents": "I tuoi agenti",
|
||||
"otherUsersAgents": "Agenti di altri utenti"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Elimina agente",
|
||||
"message": "Eliminare l'agente \"{{name}}\"? Questa azione non può essere annullata.",
|
||||
"confirm": "Elimina"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Caricamento agenti fallito: {{message}}",
|
||||
"deleted": "Agente \"{{name}}\" eliminato",
|
||||
"deleteFailed": "Eliminazione agente fallita: {{message}}",
|
||||
"paused": "Agente \"{{name}}\" in pausa",
|
||||
"resumed": "Agente \"{{name}}\" ripreso",
|
||||
"pauseFailed": "Pausa agente fallita: {{message}}",
|
||||
"resumeFailed": "Ripresa agente fallita: {{message}}",
|
||||
"exported": "Agente \"{{name}}\" esportato",
|
||||
"exportFailed": "Esportazione agente fallita: {{message}}",
|
||||
"parseFailed": "Analisi del file agente fallita: {{message}}"
|
||||
}
|
||||
}
|
||||
112
core/http/react-ui/public/locales/it/auth.json
Normal file
112
core/http/react-ui/public/locales/it/auth.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"login": {
|
||||
"subtitle": "Accedi per continuare",
|
||||
"registerSubtitle": "Crea un account",
|
||||
"createAdminSubtitle": "Crea il tuo account amministratore",
|
||||
"tokenSubtitle": "Inserisci la tua chiave API per continuare",
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "tu@esempio.com",
|
||||
"name": "Nome",
|
||||
"namePlaceholder": "Il tuo nome (opzionale)",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Inserisci la password...",
|
||||
"newPasswordPlaceholder": "Almeno 8 caratteri",
|
||||
"confirmPassword": "Conferma password",
|
||||
"confirmPasswordPlaceholder": "Ripeti la password",
|
||||
"inviteCodeLabel": "Codice invito",
|
||||
"inviteCodeOptional": " (opzionale — salta l'attesa di approvazione)",
|
||||
"inviteCodePlaceholder": "Incolla il codice invito...",
|
||||
"tokenPlaceholder": "Inserisci la chiave API...",
|
||||
"tokenAltPlaceholder": "Inserisci il token API...",
|
||||
"signIn": "Accedi",
|
||||
"signingIn": "Accesso in corso...",
|
||||
"register": "Registrati",
|
||||
"creatingAccount": "Creazione account...",
|
||||
"createAdminAccount": "Crea account amministratore",
|
||||
"signInWithGitHub": "Accedi con GitHub",
|
||||
"signInWithSSO": "Accedi con SSO",
|
||||
"loginWithToken": "Accedi con token",
|
||||
"showTokenLogin": "Accedi con token API",
|
||||
"hideTokenLogin": "Nascondi accesso con token",
|
||||
"noAccount": "Non hai un account?",
|
||||
"hasAccount": "Hai già un account?",
|
||||
"or": "oppure",
|
||||
"errors": {
|
||||
"loginFailed": "Accesso non riuscito",
|
||||
"registrationFailed": "Registrazione non riuscita",
|
||||
"invalidToken": "Token non valido",
|
||||
"passwordsDoNotMatch": "Le password non coincidono",
|
||||
"enterToken": "Inserisci un token",
|
||||
"networkError": "Errore di rete",
|
||||
"inviteRequired": "È richiesto un codice invito valido per registrarsi"
|
||||
},
|
||||
"messages": {
|
||||
"registrationPending": "Registrazione completata, in attesa di approvazione."
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"subtitle": "Profilo, credenziali e chiavi API",
|
||||
"unavailable": "Account non disponibile",
|
||||
"unavailableText": "L'autenticazione deve essere abilitata per gestire l'account.",
|
||||
"tabs": {
|
||||
"profile": "Profilo",
|
||||
"security": "Sicurezza",
|
||||
"apiKeys": "Chiavi API"
|
||||
},
|
||||
"profile": {
|
||||
"displayName": "Nome visualizzato",
|
||||
"displayNameDescription": "Il tuo nome visualizzato pubblicamente",
|
||||
"avatarUrl": "URL avatar",
|
||||
"avatarUrlDescription": "URL della tua immagine del profilo",
|
||||
"avatarUrlPlaceholder": "https://esempio.com/avatar.png",
|
||||
"save": "Salva",
|
||||
"saving": "Salvataggio...",
|
||||
"updated": "Profilo aggiornato",
|
||||
"updateFailed": "Aggiornamento profilo non riuscito: {{message}}"
|
||||
},
|
||||
"security": {
|
||||
"currentPassword": "Password attuale",
|
||||
"currentPasswordDescription": "Inserisci la tua password attuale per verificare la tua identità",
|
||||
"currentPasswordPlaceholder": "Password attuale",
|
||||
"newPassword": "Nuova password",
|
||||
"newPasswordDescription": "Deve avere almeno 8 caratteri",
|
||||
"newPasswordPlaceholder": "Nuova password",
|
||||
"confirmPassword": "Conferma password",
|
||||
"confirmPasswordDescription": "Reinserisci la nuova password",
|
||||
"confirmPasswordPlaceholder": "Conferma la nuova password",
|
||||
"changePassword": "Cambia password",
|
||||
"changing": "Modifica in corso...",
|
||||
"changed": "Password modificata",
|
||||
"passwordsDoNotMatch": "Le password non coincidono",
|
||||
"tooShort": "La nuova password deve avere almeno 8 caratteri",
|
||||
"oauthOnly": "La gestione della password non è disponibile per gli account {{provider}}."
|
||||
},
|
||||
"apiKeys": {
|
||||
"create": "Crea chiave API",
|
||||
"createDescription": "Genera una chiave per l'accesso programmatico",
|
||||
"namePlaceholder": "Nome chiave (es. mia-app)",
|
||||
"createButton": "Crea",
|
||||
"creating": "Creazione...",
|
||||
"createdToast": "Chiave API creata",
|
||||
"createFailed": "Creazione chiave API non riuscita: {{message}}",
|
||||
"loadFailed": "Caricamento chiavi API non riuscito: {{message}}",
|
||||
"revoke": "Revoca",
|
||||
"revokeKey": "Revoca chiave",
|
||||
"revokeTitle": "Revoca chiave API",
|
||||
"revokeMessage": "Revocare la chiave API \"{{name}}\"? Questa azione non può essere annullata.",
|
||||
"revoked": "Chiave API revocata",
|
||||
"revokeFailed": "Revoca chiave API non riuscita: {{message}}",
|
||||
"copyNow": "Copia ora — questa chiave non sarà più mostrata",
|
||||
"copiedToast": "Copiato negli appunti",
|
||||
"copyFailed": "Copia non riuscita",
|
||||
"empty": "Nessuna chiave API. Creane una sopra per ottenere l'accesso programmatico.",
|
||||
"lastUsed": "ultimo utilizzo {{date}}"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Pagina non trovata",
|
||||
"text": "Sembra che questa pagina si sia persa. Torniamo sulla strada giusta.",
|
||||
"goHome": "Torna alla home"
|
||||
}
|
||||
}
|
||||
116
core/http/react-ui/public/locales/it/chat.json
Normal file
116
core/http/react-ui/public/locales/it/chat.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"activity": {
|
||||
"thought": "Pensiero",
|
||||
"tool": "Strumento",
|
||||
"result": "Risultato",
|
||||
"toolResult": "Risultato di {{name}}",
|
||||
"thinking": "Sto pensando..."
|
||||
},
|
||||
"header": {
|
||||
"manageModeTooltip": "Questa chat può installare modelli, modificare configurazioni e gestire i backend parlando con LocalAI.",
|
||||
"modelInfo": "Info modello",
|
||||
"chatSettings": "Impostazioni chat",
|
||||
"modelInfoTitle": "Info modello: {{model}}",
|
||||
"editConfig": "Modifica configurazione",
|
||||
"close": "Chiudi"
|
||||
},
|
||||
"modelInfo": {
|
||||
"backend": "Backend",
|
||||
"modelFile": "File modello",
|
||||
"contextSize": "Dimensione contesto",
|
||||
"threads": "Thread",
|
||||
"mcp": "MCP",
|
||||
"configured": "Configurato",
|
||||
"chatTemplate": "Template chat",
|
||||
"yes": "Sì",
|
||||
"gpuLayers": "Layer GPU"
|
||||
},
|
||||
"context": {
|
||||
"label": "Contesto: {{percent}}%",
|
||||
"labelWithTokens": "Contesto: {{percent}}% ({{tokens}} token)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni chat",
|
||||
"manageMode": "Modalità gestione",
|
||||
"manageModeDesc": "Permetti a questa chat di installare modelli, cambiare backend e modificare configurazioni parlando con LocalAI.",
|
||||
"systemPrompt": "Prompt di sistema",
|
||||
"systemPromptPlaceholder": "Sei un assistente utile...",
|
||||
"temperature": "Temperatura",
|
||||
"topP": "Top P",
|
||||
"topK": "Top K",
|
||||
"contextSize": "Dimensione contesto",
|
||||
"contextSizePlaceholder": "2048",
|
||||
"clearHistory": "Cancella cronologia chat"
|
||||
},
|
||||
"empty": {
|
||||
"manageTitle": "Gestisci LocalAI chattando",
|
||||
"manageText": "Chiedi di installare modelli, cambiare backend, modificare configurazioni o controllare lo stato. L'assistente riassumerà le azioni e attenderà la tua conferma prima di apportare qualsiasi modifica.",
|
||||
"startTitle": "Inizia una conversazione",
|
||||
"readyText": "Pronto a chattare con {{model}}",
|
||||
"selectModelText": "Seleziona un modello sopra per iniziare",
|
||||
"suggestionsManage": [
|
||||
"Cosa è installato?",
|
||||
"Installa un modello chat",
|
||||
"Mostra lo stato del sistema",
|
||||
"Aggiorna un backend"
|
||||
],
|
||||
"suggestionsChat": [
|
||||
"Spiega come funziona",
|
||||
"Aiutami a scrivere codice",
|
||||
"Riassumi un documento",
|
||||
"Brainstorming di idee"
|
||||
],
|
||||
"recent": "Recenti",
|
||||
"noMessages": "Nessun messaggio ancora",
|
||||
"hintEnter": "Invio per inviare",
|
||||
"hintShiftEnter": "Shift+Invio per andare a capo",
|
||||
"hintAttach": "Allega file"
|
||||
},
|
||||
"errors": {
|
||||
"viewTraces": "Visualizza tracce per i dettagli"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copia",
|
||||
"regenerate": "Rigenera"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Trasferimento del modello...",
|
||||
"transferringTo": "Trasferimento del modello a {{node}}..."
|
||||
},
|
||||
"tokens": {
|
||||
"perSec": "{{count}} tok/s",
|
||||
"peak": "Picco: {{count}} tok/s",
|
||||
"usage": "{{prompt}}p + {{completion}}c = {{total}}"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Messaggio...",
|
||||
"attachFile": "Allega file",
|
||||
"stopGenerating": "Ferma generazione",
|
||||
"canvasTitle": "Canvas — estrae blocchi di codice e media in un pannello laterale per anteprima, copia e download",
|
||||
"canvasLabel": "Canvas",
|
||||
"openCanvas": "Apri pannello canvas"
|
||||
},
|
||||
"deleteAllDialog": {
|
||||
"title": "Elimina tutte le chat",
|
||||
"message": "Eliminare tutte le chat? Questa azione non può essere annullata.",
|
||||
"confirm": "Elimina tutto"
|
||||
},
|
||||
"toasts": {
|
||||
"selectModel": "Seleziona un modello",
|
||||
"copied": "Copiato negli appunti"
|
||||
},
|
||||
"menu": {
|
||||
"trigger": "Chat",
|
||||
"triggerTitle": "Conversazioni (Ctrl/Cmd+K)",
|
||||
"search": "Cerca conversazioni...",
|
||||
"clearSearch": "Cancella ricerca",
|
||||
"noMatch": "Nessuna conversazione corrisponde alla ricerca",
|
||||
"noConversations": "Nessuna conversazione",
|
||||
"rename": "Rinomina",
|
||||
"exportMarkdown": "Esporta come Markdown",
|
||||
"deleteChat": "Elimina chat",
|
||||
"newChat": "Nuova chat",
|
||||
"clearAll": "Cancella tutto",
|
||||
"deleteAllTitle": "Elimina tutte le conversazioni"
|
||||
}
|
||||
}
|
||||
43
core/http/react-ui/public/locales/it/collections.json
Normal file
43
core/http/react-ui/public/locales/it/collections.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"title": "Base di conoscenza",
|
||||
"subtitle": "Gestisci raccolte di documenti per il RAG degli agenti",
|
||||
"newPlaceholder": "Nome della nuova raccolta...",
|
||||
"actions": {
|
||||
"create": "Crea",
|
||||
"creating": "Creazione...",
|
||||
"details": "Dettagli",
|
||||
"reset": "Reimposta",
|
||||
"delete": "Elimina",
|
||||
"viewDetails": "Visualizza dettagli",
|
||||
"resetCollection": "Reimposta raccolta",
|
||||
"deleteCollection": "Elimina raccolta"
|
||||
},
|
||||
"sections": {
|
||||
"yourCollections": "Le tue raccolte",
|
||||
"otherUsersCollections": "Raccolte di altri utenti"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nessuna raccolta ancora",
|
||||
"text": "Le raccolte ti permettono di organizzare i documenti in basi di conoscenza che gli agenti possono consultare con RAG (Retrieval-Augmented Generation). Crea una raccolta sopra per iniziare.",
|
||||
"noPersonal": "Non hai ancora raccolte."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Elimina raccolta",
|
||||
"message": "Eliminare la raccolta \"{{name}}\"? Verranno rimosse tutte le voci e l'azione non può essere annullata.",
|
||||
"confirm": "Elimina"
|
||||
},
|
||||
"resetDialog": {
|
||||
"title": "Reimposta raccolta",
|
||||
"message": "Reimpostare la raccolta \"{{name}}\"? Verranno rimosse tutte le voci ma la raccolta verrà mantenuta.",
|
||||
"confirm": "Reimposta"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Caricamento raccolte fallito: {{message}}",
|
||||
"created": "Raccolta \"{{name}}\" creata",
|
||||
"createFailed": "Creazione raccolta fallita: {{message}}",
|
||||
"deleted": "Raccolta \"{{name}}\" eliminata",
|
||||
"deleteFailed": "Eliminazione raccolta fallita: {{message}}",
|
||||
"reset": "Raccolta \"{{name}}\" reimpostata",
|
||||
"resetFailed": "Reimpostazione raccolta fallita: {{message}}"
|
||||
}
|
||||
}
|
||||
109
core/http/react-ui/public/locales/it/common.json
Normal file
109
core/http/react-ui/public/locales/it/common.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "Salva",
|
||||
"saving": "Salvataggio...",
|
||||
"cancel": "Annulla",
|
||||
"close": "Chiudi",
|
||||
"confirm": "Conferma",
|
||||
"delete": "Elimina",
|
||||
"edit": "Modifica",
|
||||
"add": "Aggiungi",
|
||||
"remove": "Rimuovi",
|
||||
"create": "Crea",
|
||||
"update": "Aggiorna",
|
||||
"refresh": "Aggiorna",
|
||||
"reload": "Ricarica",
|
||||
"retry": "Riprova",
|
||||
"search": "Cerca",
|
||||
"filter": "Filtra",
|
||||
"clear": "Pulisci",
|
||||
"reset": "Reimposta",
|
||||
"apply": "Applica",
|
||||
"back": "Indietro",
|
||||
"next": "Avanti",
|
||||
"previous": "Precedente",
|
||||
"open": "Apri",
|
||||
"submit": "Invia",
|
||||
"select": "Seleziona",
|
||||
"selectAll": "Seleziona tutto",
|
||||
"copy": "Copia",
|
||||
"copied": "Copiato",
|
||||
"download": "Scarica",
|
||||
"upload": "Carica",
|
||||
"import": "Importa",
|
||||
"export": "Esporta",
|
||||
"view": "Visualizza",
|
||||
"details": "Dettagli",
|
||||
"settings": "Impostazioni",
|
||||
"help": "Aiuto",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
"loading": "Caricamento..."
|
||||
},
|
||||
"status": {
|
||||
"loading": "Caricamento...",
|
||||
"saving": "Salvataggio...",
|
||||
"saved": "Salvato",
|
||||
"ready": "Pronto",
|
||||
"running": "In esecuzione",
|
||||
"stopped": "Fermo",
|
||||
"starting": "Avvio in corso...",
|
||||
"stopping": "Arresto in corso...",
|
||||
"pending": "In attesa",
|
||||
"active": "Attivo",
|
||||
"inactive": "Inattivo",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"error": "Errore",
|
||||
"success": "Successo",
|
||||
"warning": "Attenzione",
|
||||
"info": "Info",
|
||||
"empty": "Nessun elemento",
|
||||
"none": "Nessuno",
|
||||
"unknown": "Sconosciuto"
|
||||
},
|
||||
"dialogs": {
|
||||
"confirmDelete": {
|
||||
"title": "Conferma eliminazione",
|
||||
"message": "Sei sicuro di volerlo eliminare? Questa azione non può essere annullata.",
|
||||
"confirm": "Elimina",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"unsavedChanges": {
|
||||
"title": "Modifiche non salvate",
|
||||
"message": "Hai modifiche non salvate. Vuoi scartarle?",
|
||||
"discard": "Scarta",
|
||||
"keepEditing": "Continua a modificare"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"required": "Obbligatorio",
|
||||
"optional": "Opzionale",
|
||||
"name": "Nome",
|
||||
"description": "Descrizione",
|
||||
"type": "Tipo",
|
||||
"value": "Valore",
|
||||
"search": "Cerca...",
|
||||
"selectPlaceholder": "Seleziona un'opzione..."
|
||||
},
|
||||
"time": {
|
||||
"now": "ora",
|
||||
"secondsAgo_one": "{{count}} secondo fa",
|
||||
"secondsAgo_other": "{{count}} secondi fa",
|
||||
"minutesAgo_one": "{{count}} minuto fa",
|
||||
"minutesAgo_other": "{{count}} minuti fa",
|
||||
"hoursAgo_one": "{{count}} ora fa",
|
||||
"hoursAgo_other": "{{count}} ore fa",
|
||||
"daysAgo_one": "{{count}} giorno fa",
|
||||
"daysAgo_other": "{{count}} giorni fa"
|
||||
},
|
||||
"units": {
|
||||
"bytes": "B",
|
||||
"kilobytes": "KB",
|
||||
"megabytes": "MB",
|
||||
"gigabytes": "GB",
|
||||
"terabytes": "TB"
|
||||
}
|
||||
}
|
||||
17
core/http/react-ui/public/locales/it/errors.json
Normal file
17
core/http/react-ui/public/locales/it/errors.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"generic": "Si è verificato un errore",
|
||||
"network": "Errore di rete. Controlla la connessione e riprova.",
|
||||
"unauthorized": "Non sei autorizzato a eseguire questa azione.",
|
||||
"forbidden": "Accesso negato.",
|
||||
"notFound": "La risorsa richiesta non è stata trovata.",
|
||||
"serverError": "Errore del server. Riprova più tardi.",
|
||||
"loadFailed": "Caricamento fallito: {{message}}",
|
||||
"saveFailed": "Salvataggio fallito: {{message}}",
|
||||
"deleteFailed": "Eliminazione fallita: {{message}}",
|
||||
"updateFailed": "Aggiornamento fallito: {{message}}",
|
||||
"createFailed": "Creazione fallita: {{message}}",
|
||||
"operationFailed": "Operazione fallita: {{message}}",
|
||||
"invalidInput": "Input non valido. Controlla il modulo e riprova.",
|
||||
"tryAgain": "Per favore riprova.",
|
||||
"contactAdmin": "Se il problema persiste, contatta l'amministratore."
|
||||
}
|
||||
66
core/http/react-ui/public/locales/it/home.json
Normal file
66
core/http/react-ui/public/locales/it/home.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"cluster": {
|
||||
"vram": "VRAM cluster",
|
||||
"ram": "RAM cluster",
|
||||
"nodesOnline": "{{healthy}}/{{total}} nodi online"
|
||||
},
|
||||
"resourceGpu": "GPU",
|
||||
"resourceRam": "RAM",
|
||||
"assistant": {
|
||||
"title": "Gestisci LocalAI chattando",
|
||||
"description": "Installa modelli, cambia backend, modifica configurazioni e controlla lo stato parlando con LocalAI.",
|
||||
"open": "Apri assistente",
|
||||
"tooltip": "Gestisci LocalAI chattando"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Messaggio...",
|
||||
"attachImage": "Allega immagine",
|
||||
"attachAudio": "Allega audio",
|
||||
"attachFile": "Allega file",
|
||||
"enterToSend": "Invio per inviare",
|
||||
"selectModelFirst": "Seleziona prima un modello",
|
||||
"sendMessage": "Invia messaggio",
|
||||
"selectModelToast": "Seleziona prima un modello"
|
||||
},
|
||||
"quickLinks": {
|
||||
"manageByChat": "Gestisci via chat",
|
||||
"installedModels": "Modelli installati",
|
||||
"browseGallery": "Sfoglia galleria",
|
||||
"importModel": "Importa modello",
|
||||
"documentation": "Documentazione"
|
||||
},
|
||||
"loadedModels": {
|
||||
"count_one": "{{count}} modello caricato",
|
||||
"count_other": "{{count}} modelli caricati",
|
||||
"stop": "Ferma modello",
|
||||
"stopAll": "Ferma tutti"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Ferma modello",
|
||||
"message": "Fermare il modello {{model}}?",
|
||||
"confirm": "Ferma {{model}}",
|
||||
"stopAllTitle": "Ferma tutti i modelli",
|
||||
"stopAllMessage": "Fermare tutti i {{count}} modelli caricati?",
|
||||
"stopAllConfirm": "Ferma tutti",
|
||||
"stoppedToast": "{{model}} fermato",
|
||||
"allStoppedToast": "Tutti i modelli fermati",
|
||||
"stopFailed": "Arresto non riuscito: {{message}}"
|
||||
},
|
||||
"wizard": {
|
||||
"getStarted": "Inizia con {{name}}",
|
||||
"intro": "Installa il tuo primo modello per iniziare. Sfoglia la galleria o importa il tuo.",
|
||||
"steps": {
|
||||
"step1Title": "Sfoglia la galleria modelli",
|
||||
"step1Body": "Trova il modello giusto per le tue esigenze dalla nostra collezione curata.",
|
||||
"step2Title": "Installa un modello",
|
||||
"step2Body": "Clicca installa per scaricarlo e configurarlo automaticamente.",
|
||||
"step3Title": "Inizia a chattare",
|
||||
"step3Body": "Chatta con il tuo modello dal browser o usa l'API."
|
||||
},
|
||||
"browseGallery": "Sfoglia galleria modelli",
|
||||
"importModel": "Importa modello",
|
||||
"docs": "Documentazione",
|
||||
"noModelsTitle": "Nessun modello disponibile",
|
||||
"noModelsBody": "Non ci sono ancora modelli installati. Chiedi al tuo amministratore di configurare i modelli per iniziare a chattare."
|
||||
}
|
||||
}
|
||||
142
core/http/react-ui/public/locales/it/importModel.json
Normal file
142
core/http/react-ui/public/locales/it/importModel.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"title": "Importa nuovo modello",
|
||||
"subtitle": {
|
||||
"simple": "Importa un modello da un URI — l'auto-detect sceglie il backend.",
|
||||
"powerYaml": "Scrivi la configurazione YAML completa del modello.",
|
||||
"powerPrefs": "Preferenze di importazione dettagliate."
|
||||
},
|
||||
"actions": {
|
||||
"import": "Importa modello",
|
||||
"importing": "Importazione...",
|
||||
"create": "Crea",
|
||||
"saving": "Salvataggio...",
|
||||
"browseHF": "Sfoglia modelli su HF",
|
||||
"addCustom": "Aggiungi personalizzato",
|
||||
"copy": "Copia"
|
||||
},
|
||||
"form": {
|
||||
"modelUri": "URI modello",
|
||||
"uriPlaceholder": "huggingface://TheBloke/Llama-2-7B-Chat-GGUF o https://example.com/model.gguf",
|
||||
"uriHint": "Inserisci l'URI o il percorso del file modello da importare",
|
||||
"supportedFormats": "Formati URI supportati",
|
||||
"options": "Opzioni",
|
||||
"preferences": "Preferenze (opzionali)",
|
||||
"commonPreferences": "Preferenze comuni",
|
||||
"customPreferences": "Preferenze personalizzate",
|
||||
"customKeyValueHint": "Aggiungi coppie chiave-valore personalizzate per configurazioni avanzate.",
|
||||
"preferenceKey": "Chiave preferenza per la riga {{index}}",
|
||||
"preferenceValue": "Valore preferenza per la riga {{index}}",
|
||||
"removePref": "Rimuovi questa preferenza",
|
||||
"key": "Chiave",
|
||||
"value": "Valore",
|
||||
"backend": "Backend",
|
||||
"backendAuto": "Auto-detect (basato sull'URI)",
|
||||
"backendLoading": "Caricamento backend…",
|
||||
"backendSearch": "Cerca backend...",
|
||||
"backendHint": "Forza un backend specifico. Lascia vuoto per auto-rilevare dall'URI. Le voci segnate \"manual pick\" non sono auto-rilevabili — selezionale tu se sai cosa serve al modello.",
|
||||
"backendErrorHint": "Impossibile caricare la lista dei backend — solo auto-detect.",
|
||||
"backendNotInstalled": "Questo backend non è ancora installato. L'invio dell'importazione lo scaricherà prima.",
|
||||
"modelName": "Nome modello",
|
||||
"modelNamePlaceholder": "Lascia vuoto per usare il nome file",
|
||||
"modelNameHint": "Nome personalizzato per il modello. Se vuoto, verrà usato il nome del file.",
|
||||
"description": "Descrizione",
|
||||
"descriptionPlaceholder": "Lascia vuoto per usare la descrizione predefinita",
|
||||
"descriptionHint": "Descrizione personalizzata per il modello.",
|
||||
"quantizations": "Quantizzazioni",
|
||||
"quantizationsPlaceholder": "q4_k_m,q4_k_s,q3_k_m (separate da virgola)",
|
||||
"quantizationsHint": "Quantizzazioni preferite (separate da virgola). Lascia vuoto per il default (q4_k_m).",
|
||||
"mmprojQuantizations": "Quantizzazioni MMProj",
|
||||
"mmprojQuantizationsPlaceholder": "fp16,fp32 (separate da virgola)",
|
||||
"mmprojQuantizationsHint": "Quantizzazioni MMProj preferite. Lascia vuoto per il default (fp16).",
|
||||
"embeddings": "Embedding",
|
||||
"embeddingsHint": "Abilita il supporto degli embedding per questo modello.",
|
||||
"modelType": "Tipo di modello",
|
||||
"modelTypePlaceholder": "AutoModelForCausalLM (per il backend transformers)",
|
||||
"modelTypeHint": "Tipo di modello per il backend transformers. Esempi: AutoModelForCausalLM, SentenceTransformer, Mamba.",
|
||||
"pipelineType": "Tipo di pipeline",
|
||||
"pipelineTypeHint": "Tipo di pipeline per il backend diffusers.",
|
||||
"schedulerType": "Tipo di scheduler",
|
||||
"schedulerTypePlaceholder": "k_dpmpp_2m (opzionale)",
|
||||
"schedulerTypeHint": "Tipo di scheduler per il backend diffusers. Esempi: k_dpmpp_2m, euler_a, ddim.",
|
||||
"enableParameters": "Parametri abilitati",
|
||||
"enableParametersPlaceholder": "negative_prompt,num_inference_steps (separati da virgola)",
|
||||
"enableParametersHint": "Parametri abilitati per il backend diffusers (separati da virgola).",
|
||||
"cuda": "CUDA",
|
||||
"cudaHint": "Abilita il supporto CUDA per l'accelerazione GPU.",
|
||||
"yamlEditor": "Editor configurazione YAML",
|
||||
"manualPick": "selezione manuale",
|
||||
"manualPickTooltip": "L'auto-detect non instraderà a questo backend. Selezionalo qui se sai che è quello che vuoi."
|
||||
},
|
||||
"modality": {
|
||||
"text": "LLM testuale",
|
||||
"asr": "Riconoscimento vocale",
|
||||
"tts": "Sintesi vocale",
|
||||
"image": "Immagine / Video",
|
||||
"embeddings": "Embedding",
|
||||
"reranker": "Reranker",
|
||||
"detection": "Rilevamento oggetti",
|
||||
"vad": "Rilevamento attività vocale",
|
||||
"other": "Altro"
|
||||
},
|
||||
"powerTabs": {
|
||||
"ariaLabel": "Tab modalità avanzata",
|
||||
"preferences": "Preferenze",
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"switchDialog": {
|
||||
"title": "Mantenere le preferenze personalizzate?",
|
||||
"body": "Passando alla modalità Semplice si nascondono le preferenze oltre a backend, nome e descrizione. Saranno comunque inviate all'importazione.",
|
||||
"cancel": "Annulla",
|
||||
"discard": "Scarta e cambia",
|
||||
"keep": "Mantieni e cambia"
|
||||
},
|
||||
"estimate": {
|
||||
"title": "Requisiti stimati",
|
||||
"download": "Download: {{size}}",
|
||||
"vram": "VRAM: {{vram}}"
|
||||
},
|
||||
"toasts": {
|
||||
"noUri": "Inserisci un URI del modello",
|
||||
"noYaml": "Inserisci la configurazione YAML",
|
||||
"started": "Importazione avviata! Tracciamento dei progressi...",
|
||||
"startedWithMeta": "Importazione avviata! Tracciamento dei progressi... ({{meta}})",
|
||||
"imported": "Modello importato con successo!",
|
||||
"importedYaml": "Configurazione del modello importata con successo!",
|
||||
"importFailed": "Importazione fallita: {{message}}",
|
||||
"startImportFailed": "Avvio importazione fallito: {{message}}",
|
||||
"backendsLoadFailed": "Impossibile caricare la lista dei backend — uso solo auto-detect",
|
||||
"modalityClearedBackend": "Selezione backend cancellata — non era nel gruppo {{label}}.",
|
||||
"copied": "Copiato negli appunti"
|
||||
},
|
||||
"uriFormats": {
|
||||
"huggingface": {
|
||||
"title": "HuggingFace",
|
||||
"standard": "Formato HuggingFace standard",
|
||||
"short": "Formato HuggingFace breve",
|
||||
"fullUrl": "URL HuggingFace completo"
|
||||
},
|
||||
"http": {
|
||||
"title": "URL HTTP/HTTPS",
|
||||
"direct": "Download diretto da qualsiasi URL HTTPS"
|
||||
},
|
||||
"local": {
|
||||
"title": "File locali",
|
||||
"filePath": "Percorso file locale (assoluto)",
|
||||
"directYaml": "File di configurazione YAML locale diretto"
|
||||
},
|
||||
"oci": {
|
||||
"title": "Registry OCI",
|
||||
"registry": "Registry container OCI",
|
||||
"tarball": "File tarball OCI locale"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"model": "Formato modello Ollama"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "File di configurazione YAML",
|
||||
"remote": "File di configurazione YAML remoto",
|
||||
"local": "File di configurazione YAML locale"
|
||||
}
|
||||
}
|
||||
}
|
||||
154
core/http/react-ui/public/locales/it/media.json
Normal file
154
core/http/react-ui/public/locales/it/media.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Immagini",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Audio"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Generazione immagini",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descrivi l'immagine che vuoi generare...",
|
||||
"negativePrompt": "Prompt negativo",
|
||||
"negativePromptPlaceholder": "Cosa evitare...",
|
||||
"size": "Dimensione",
|
||||
"count": "Quantità (1-4)",
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"imageInputs": "Input immagini",
|
||||
"steps": "Passi",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Casuale",
|
||||
"sourceImage": "Immagine sorgente (img2img)",
|
||||
"refImages": "Immagini di riferimento",
|
||||
"refImagesAdded_one": "{{count}} immagine aggiunta",
|
||||
"refImagesAdded_other": "{{count}} immagini aggiunte"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "Le immagini generate appariranno qui",
|
||||
"toasts": {
|
||||
"noPrompt": "Inserisci un prompt",
|
||||
"noModel": "Seleziona un modello",
|
||||
"noResults": "Nessuna immagine generata"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Generazione video",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descrivi il video che vuoi generare...",
|
||||
"duration": "Durata (s)",
|
||||
"fps": "FPS",
|
||||
"size": "Dimensione",
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Casuale"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera video",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "I video generati appariranno qui",
|
||||
"toasts": {
|
||||
"noPrompt": "Inserisci un prompt",
|
||||
"noModel": "Seleziona un modello",
|
||||
"noResults": "Nessun video generato"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Text to Speech",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"voice": "Voce",
|
||||
"voicePlaceholder": "ID voce opzionale",
|
||||
"input": "Testo",
|
||||
"inputPlaceholder": "Inserisci il testo da convertire in voce..."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera audio",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "L'audio generato apparirà qui",
|
||||
"toasts": {
|
||||
"noText": "Inserisci del testo",
|
||||
"noModel": "Seleziona un modello",
|
||||
"generateFailed": "Generazione fallita"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Generazione audio",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descrivi il suono che vuoi generare...",
|
||||
"duration": "Durata (s)",
|
||||
"language": "Lingua",
|
||||
"vocalLanguage": "Lingua vocale",
|
||||
"lyrics": "Testi (opzionale)",
|
||||
"lyricsPlaceholder": "Testi per la generazione vocale",
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Casuale"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "L'audio generato apparirà qui",
|
||||
"toasts": {
|
||||
"noPrompt": "Inserisci un prompt",
|
||||
"noModel": "Seleziona un modello",
|
||||
"generateFailed": "Generazione fallita"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Conversazione",
|
||||
"subtitle": "Conversazione vocale in tempo reale",
|
||||
"actions": {
|
||||
"start": "Avvia sessione",
|
||||
"stop": "Termina sessione",
|
||||
"connecting": "Connessione...",
|
||||
"muted": "Muto",
|
||||
"mute": "Disattiva microfono",
|
||||
"unmute": "Riattiva microfono"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"voice": "Voce",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Lingua",
|
||||
"languagePlaceholder": "it",
|
||||
"instructions": "Istruzioni",
|
||||
"instructionsPlaceholder": "Imposta la personalità dell'assistente..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Inattivo",
|
||||
"connecting": "Connessione...",
|
||||
"listening": "In ascolto...",
|
||||
"speaking": "Sta parlando...",
|
||||
"ended": "Sessione terminata"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Seleziona prima un modello",
|
||||
"connectFailed": "Connessione fallita: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Cronologia",
|
||||
"empty": "Nessuna cronologia",
|
||||
"deleteEntry": "Elimina voce",
|
||||
"clear": "Cancella cronologia",
|
||||
"clearTitle": "Cancella tutta la cronologia",
|
||||
"clearMessage": "Rimuovere tutte le voci della cronologia? Questa azione non può essere annullata.",
|
||||
"clearConfirm": "Cancella",
|
||||
"cleared": "Cronologia cancellata"
|
||||
}
|
||||
}
|
||||
85
core/http/react-ui/public/locales/it/models.json
Normal file
85
core/http/react-ui/public/locales/it/models.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"title": "Installa modelli",
|
||||
"subtitle": "Sfoglia e installa modelli AI dalla galleria",
|
||||
"stats": {
|
||||
"available": "Disponibili",
|
||||
"installed": "Installati"
|
||||
},
|
||||
"actions": {
|
||||
"addModel": "Aggiungi modello",
|
||||
"importModel": "Importa modello",
|
||||
"install": "Installa",
|
||||
"reinstall": "Reinstalla",
|
||||
"delete": "Elimina"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Tutti",
|
||||
"llm": "LLM",
|
||||
"image": "Immagine",
|
||||
"multimodal": "Multimodale",
|
||||
"vision": "Visione",
|
||||
"tts": "TTS",
|
||||
"stt": "STT",
|
||||
"embedding": "Embedding",
|
||||
"rerank": "Rerank",
|
||||
"allBackends": "Tutti i backend",
|
||||
"searchBackends": "Cerca backend..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Cerca modelli...",
|
||||
"clearFilters": "Rimuovi filtri"
|
||||
},
|
||||
"table": {
|
||||
"modelName": "Nome modello",
|
||||
"description": "Descrizione",
|
||||
"backend": "Backend",
|
||||
"sizeVram": "Dimensione / VRAM",
|
||||
"status": "Stato",
|
||||
"actions": "Azioni",
|
||||
"size": "Dimensione: {{size}}",
|
||||
"vram": "VRAM: {{vram}}",
|
||||
"fits": "Compatibile",
|
||||
"mayNotFit": "Potrebbe non entrare",
|
||||
"trustRemoteCode": "Trust Remote Code",
|
||||
"installing": "Installazione",
|
||||
"installingPct": "Installazione · {{percent}}%",
|
||||
"installed": "Installato",
|
||||
"notInstalled": "Non installato"
|
||||
},
|
||||
"detail": {
|
||||
"description": "Descrizione",
|
||||
"gallery": "Galleria",
|
||||
"backend": "Backend",
|
||||
"size": "Dimensione",
|
||||
"vram": "VRAM",
|
||||
"license": "Licenza",
|
||||
"tags": "Tag",
|
||||
"links": "Link",
|
||||
"warning": "Attenzione",
|
||||
"files": "File",
|
||||
"fitsGpu": "Entra nella GPU",
|
||||
"mayNotFitGpu": "Potrebbe non entrare nella GPU",
|
||||
"requiresTrustRemoteCode": "Richiede Trust Remote Code",
|
||||
"fileCount_one": "{{count}} file",
|
||||
"fileCount_other": "{{count}} file",
|
||||
"filename": "Nome file",
|
||||
"uri": "URI",
|
||||
"sha256": "SHA256"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nessun modello trovato",
|
||||
"withFilters": "Nessun modello corrisponde ai filtri attuali.",
|
||||
"noFilters": "La galleria modelli è vuota."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Elimina modello",
|
||||
"message": "Eliminare il modello {{model}}?",
|
||||
"confirm": "Elimina {{model}}",
|
||||
"deletingToast": "Eliminazione di {{model}} in corso..."
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Caricamento modelli fallito: {{message}}",
|
||||
"installFailed": "Installazione fallita: {{message}}",
|
||||
"deleteFailed": "Eliminazione fallita: {{message}}"
|
||||
}
|
||||
}
|
||||
51
core/http/react-ui/public/locales/it/nav.json
Normal file
51
core/http/react-ui/public/locales/it/nav.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"appName": "LocalAI",
|
||||
"openMenu": "Apri menu",
|
||||
"closeMenu": "Chiudi menu",
|
||||
"primaryNavigation": "Navigazione principale",
|
||||
"switchToLightMode": "Passa al tema chiaro",
|
||||
"switchToDarkMode": "Passa al tema scuro",
|
||||
"expandSidebar": "Espandi barra laterale",
|
||||
"collapseSidebar": "Comprimi barra laterale",
|
||||
"changeLanguage": "Cambia lingua",
|
||||
"logout": "Esci",
|
||||
"accountSettings": "Impostazioni account",
|
||||
"account": "Account",
|
||||
"accountFor": "Account: {{name}}",
|
||||
"sections": {
|
||||
"tools": "Strumenti",
|
||||
"biometrics": "Biometria",
|
||||
"agents": "Agenti",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"items": {
|
||||
"home": "Home",
|
||||
"installModels": "Installa modelli",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Conversazione",
|
||||
"fineTune": "Fine-Tuning (Sperimentale)",
|
||||
"quantize": "Quantizzazione (Sperimentale)",
|
||||
"faceRecognition": "Riconoscimento volti",
|
||||
"voiceRecognition": "Riconoscimento vocale",
|
||||
"agents": "Agenti",
|
||||
"skills": "Competenze",
|
||||
"memory": "Memoria",
|
||||
"mcpJobs": "Job MCP CI",
|
||||
"usage": "Utilizzo",
|
||||
"users": "Utenti",
|
||||
"backends": "Backend",
|
||||
"traces": "Tracce",
|
||||
"nodes": "Nodi",
|
||||
"swarm": "Swarm",
|
||||
"system": "Sistema",
|
||||
"settings": "Impostazioni",
|
||||
"api": "API"
|
||||
},
|
||||
"footer": {
|
||||
"github": "GitHub",
|
||||
"documentation": "Documentazione",
|
||||
"author": "Autore",
|
||||
"copyright": "© 2023-{{year}} {{author}}"
|
||||
}
|
||||
}
|
||||
79
core/http/react-ui/public/locales/it/skills.json
Normal file
79
core/http/react-ui/public/locales/it/skills.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "Competenze",
|
||||
"subtitle": "Gestisci le competenze degli agenti (istruzioni e risorse riutilizzabili)",
|
||||
"unavailable": {
|
||||
"subtitle": "Il servizio competenze non è disponibile o l'indice è in ricostruzione. Riprova tra un momento.",
|
||||
"retry": "Riprova"
|
||||
},
|
||||
"actions": {
|
||||
"newSkill": "Nuova competenza",
|
||||
"createSkill": "Crea competenza",
|
||||
"import": "Importa",
|
||||
"importing": "Importazione...",
|
||||
"gitRepos": "Repo Git",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
"export": "Esporta",
|
||||
"sync": "Sincronizza",
|
||||
"addRepo": "Aggiungi repo",
|
||||
"adding": "Aggiunta...",
|
||||
"remove": "Rimuovi",
|
||||
"enable": "Abilita",
|
||||
"disable": "Disabilita"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Cerca competenze..."
|
||||
},
|
||||
"git": {
|
||||
"title": "Repository Git",
|
||||
"description": "Aggiungi repository Git da cui ottenere le competenze. Le competenze appariranno nella lista dopo la sincronizzazione.",
|
||||
"urlPlaceholder": "https://github.com/user/repo o git@github.com:user/repo.git",
|
||||
"noRepos": "Nessun repo Git configurato. Aggiungine uno sopra.",
|
||||
"disabled": "Disabilitato",
|
||||
"removeRepo": "Rimuovi repo"
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "Nessuna descrizione",
|
||||
"readOnly": "Sola lettura",
|
||||
"editTitle": "Modifica competenza",
|
||||
"deleteTitle": "Elimina competenza",
|
||||
"exportTitle": "Esporta come .tar.gz"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nessuna competenza trovata",
|
||||
"text": "Crea una competenza o importane una per iniziare.",
|
||||
"noPersonal": "Non hai ancora competenze."
|
||||
},
|
||||
"sections": {
|
||||
"yourSkills": "Le tue competenze",
|
||||
"otherUsersSkills": "Competenze di altri utenti"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Elimina competenza",
|
||||
"message": "Eliminare la competenza \"{{name}}\"? Questa azione non può essere annullata.",
|
||||
"confirm": "Elimina"
|
||||
},
|
||||
"removeRepoDialog": {
|
||||
"title": "Rimuovi repository Git",
|
||||
"message": "Rimuovere questo repository Git? Le competenze al suo interno non saranno più disponibili.",
|
||||
"confirm": "Rimuovi"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Caricamento competenze fallito",
|
||||
"deleted": "Competenza \"{{name}}\" eliminata",
|
||||
"deleteFailed": "Eliminazione competenza fallita",
|
||||
"exported": "Competenza \"{{name}}\" esportata",
|
||||
"exportFailed": "Esportazione fallita",
|
||||
"imported": "Competenza importata da \"{{file}}\"",
|
||||
"importFailed": "Importazione fallita",
|
||||
"loadReposFailed": "Caricamento repo Git fallito",
|
||||
"repoAdded": "Repo Git aggiunto e in sincronizzazione",
|
||||
"addRepoFailed": "Aggiunta repo fallita",
|
||||
"synced": "Repo sincronizzato",
|
||||
"syncFailed": "Sincronizzazione fallita",
|
||||
"toggled": "Repo modificato",
|
||||
"toggleFailed": "Modifica fallita",
|
||||
"removed": "Repo rimosso",
|
||||
"removeFailed": "Rimozione fallita"
|
||||
}
|
||||
}
|
||||
62
core/http/react-ui/public/locales/zh-CN/admin.json
Normal file
62
core/http/react-ui/public/locales/zh-CN/admin.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"manage": {
|
||||
"title": "系统",
|
||||
"subtitle": "管理已安装的模型和后端"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"subtitle": "配置 LocalAI 运行时设置",
|
||||
"saved": "设置保存成功",
|
||||
"saveFailed": "保存失败:{{message}}",
|
||||
"loadFailed": "加载设置失败:{{message}}",
|
||||
"sections": {
|
||||
"branding": "品牌",
|
||||
"watchdog": "看门狗",
|
||||
"memory": "内存",
|
||||
"backends": "后端",
|
||||
"performance": "性能",
|
||||
"tracing": "追踪",
|
||||
"api": "API 和 CORS",
|
||||
"p2p": "P2P",
|
||||
"galleries": "模型库",
|
||||
"apikeys": "API 密钥",
|
||||
"agents": "智能体任务",
|
||||
"agentpool": "智能体池",
|
||||
"assistant": "LocalAI Assistant",
|
||||
"responses": "响应"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"title": "后端管理",
|
||||
"subtitle": "发现并安装为模型提供支持的 AI 后端"
|
||||
},
|
||||
"backendLogs": {
|
||||
"title": "后端日志",
|
||||
"subtitle": "查看运行中后端的日志",
|
||||
"empty": "暂无日志"
|
||||
},
|
||||
"traces": {
|
||||
"title": "追踪",
|
||||
"subtitle": "查看记录的 API 请求、响应和后端操作"
|
||||
},
|
||||
"nodes": {
|
||||
"title": "分布式节点",
|
||||
"subtitle": "管理后端和智能体工作节点"
|
||||
},
|
||||
"p2p": {
|
||||
"title": "分布式 AI 计算",
|
||||
"subtitle": "通过点对点分发将您的 AI 工作负载扩展到多个设备"
|
||||
},
|
||||
"users": {
|
||||
"title": "用户",
|
||||
"subtitle": "管理注册用户、角色和邀请"
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用情况",
|
||||
"subtitle": "API 令牌使用统计"
|
||||
},
|
||||
"explorer": {
|
||||
"title": "资源浏览器",
|
||||
"subtitle": "浏览文件和配置"
|
||||
}
|
||||
}
|
||||
55
core/http/react-ui/public/locales/zh-CN/agents.json
Normal file
55
core/http/react-ui/public/locales/zh-CN/agents.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"title": "智能体",
|
||||
"subtitle": "管理自主 AI 智能体",
|
||||
"actions": {
|
||||
"agentHub": "Agent Hub",
|
||||
"import": "导入",
|
||||
"createAgent": "创建智能体",
|
||||
"edit": "编辑",
|
||||
"chat": "聊天",
|
||||
"export": "导出",
|
||||
"delete": "删除",
|
||||
"pause": "暂停",
|
||||
"resume": "恢复"
|
||||
},
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"status": "状态",
|
||||
"events": "事件",
|
||||
"actions": "操作",
|
||||
"eventsTooltip": "{{count}} 个事件 - 点击查看"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索智能体...",
|
||||
"summary_one": "{{total}} 个智能体中的 {{shown}} 个",
|
||||
"summary_other": "{{total}} 个智能体中的 {{shown}} 个"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigured": "未配置智能体",
|
||||
"noConfiguredText": "创建一个智能体以开始自主 AI 工作流。",
|
||||
"browseHub": "不知从何开始?浏览 <1>Agent Hub</1> 寻找现成的智能体配置以便导入。",
|
||||
"noMatching": "没有匹配的智能体",
|
||||
"noMatchingText": "没有智能体匹配 \"{{query}}\""
|
||||
},
|
||||
"sections": {
|
||||
"yourAgents": "您的智能体",
|
||||
"otherUsersAgents": "其他用户的智能体"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "删除智能体",
|
||||
"message": "删除智能体 \"{{name}}\"?此操作无法撤销。",
|
||||
"confirm": "删除"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "加载智能体失败:{{message}}",
|
||||
"deleted": "智能体 \"{{name}}\" 已删除",
|
||||
"deleteFailed": "删除智能体失败:{{message}}",
|
||||
"paused": "智能体 \"{{name}}\" 已暂停",
|
||||
"resumed": "智能体 \"{{name}}\" 已恢复",
|
||||
"pauseFailed": "暂停智能体失败:{{message}}",
|
||||
"resumeFailed": "恢复智能体失败:{{message}}",
|
||||
"exported": "智能体 \"{{name}}\" 已导出",
|
||||
"exportFailed": "导出智能体失败:{{message}}",
|
||||
"parseFailed": "解析智能体文件失败:{{message}}"
|
||||
}
|
||||
}
|
||||
112
core/http/react-ui/public/locales/zh-CN/auth.json
Normal file
112
core/http/react-ui/public/locales/zh-CN/auth.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"login": {
|
||||
"subtitle": "登录以继续",
|
||||
"registerSubtitle": "创建账户",
|
||||
"createAdminSubtitle": "创建您的管理员账户",
|
||||
"tokenSubtitle": "输入您的 API 密钥以继续",
|
||||
"email": "邮箱",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"name": "姓名",
|
||||
"namePlaceholder": "您的姓名(可选)",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "输入密码...",
|
||||
"newPasswordPlaceholder": "至少 8 个字符",
|
||||
"confirmPassword": "确认密码",
|
||||
"confirmPasswordPlaceholder": "再次输入密码",
|
||||
"inviteCodeLabel": "邀请码",
|
||||
"inviteCodeOptional": "(可选 — 跳过审核等待)",
|
||||
"inviteCodePlaceholder": "粘贴您的邀请码...",
|
||||
"tokenPlaceholder": "输入 API 密钥...",
|
||||
"tokenAltPlaceholder": "输入 API 令牌...",
|
||||
"signIn": "登录",
|
||||
"signingIn": "登录中...",
|
||||
"register": "注册",
|
||||
"creatingAccount": "创建账户中...",
|
||||
"createAdminAccount": "创建管理员账户",
|
||||
"signInWithGitHub": "使用 GitHub 登录",
|
||||
"signInWithSSO": "使用 SSO 登录",
|
||||
"loginWithToken": "使用令牌登录",
|
||||
"showTokenLogin": "使用 API 令牌登录",
|
||||
"hideTokenLogin": "隐藏令牌登录",
|
||||
"noAccount": "还没有账户?",
|
||||
"hasAccount": "已有账户?",
|
||||
"or": "或",
|
||||
"errors": {
|
||||
"loginFailed": "登录失败",
|
||||
"registrationFailed": "注册失败",
|
||||
"invalidToken": "无效的令牌",
|
||||
"passwordsDoNotMatch": "密码不匹配",
|
||||
"enterToken": "请输入令牌",
|
||||
"networkError": "网络错误",
|
||||
"inviteRequired": "注册需要有效的邀请码"
|
||||
},
|
||||
"messages": {
|
||||
"registrationPending": "注册成功,等待审核。"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "账户",
|
||||
"subtitle": "个人资料、凭证和 API 密钥",
|
||||
"unavailable": "账户不可用",
|
||||
"unavailableText": "必须启用身份验证才能管理您的账户。",
|
||||
"tabs": {
|
||||
"profile": "个人资料",
|
||||
"security": "安全",
|
||||
"apiKeys": "API 密钥"
|
||||
},
|
||||
"profile": {
|
||||
"displayName": "显示名称",
|
||||
"displayNameDescription": "您的公开显示名称",
|
||||
"avatarUrl": "头像 URL",
|
||||
"avatarUrlDescription": "您的头像图片 URL",
|
||||
"avatarUrlPlaceholder": "https://example.com/avatar.png",
|
||||
"save": "保存",
|
||||
"saving": "保存中...",
|
||||
"updated": "个人资料已更新",
|
||||
"updateFailed": "更新个人资料失败:{{message}}"
|
||||
},
|
||||
"security": {
|
||||
"currentPassword": "当前密码",
|
||||
"currentPasswordDescription": "输入您的现有密码以验证身份",
|
||||
"currentPasswordPlaceholder": "当前密码",
|
||||
"newPassword": "新密码",
|
||||
"newPasswordDescription": "至少需要 8 个字符",
|
||||
"newPasswordPlaceholder": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"confirmPasswordDescription": "再次输入新密码",
|
||||
"confirmPasswordPlaceholder": "确认新密码",
|
||||
"changePassword": "更改密码",
|
||||
"changing": "正在更改...",
|
||||
"changed": "密码已更改",
|
||||
"passwordsDoNotMatch": "密码不匹配",
|
||||
"tooShort": "新密码至少需要 8 个字符",
|
||||
"oauthOnly": "{{provider}} 账户不支持密码管理。"
|
||||
},
|
||||
"apiKeys": {
|
||||
"create": "创建 API 密钥",
|
||||
"createDescription": "为程序化访问生成密钥",
|
||||
"namePlaceholder": "密钥名称(例如 my-app)",
|
||||
"createButton": "创建",
|
||||
"creating": "创建中...",
|
||||
"createdToast": "API 密钥已创建",
|
||||
"createFailed": "创建 API 密钥失败:{{message}}",
|
||||
"loadFailed": "加载 API 密钥失败:{{message}}",
|
||||
"revoke": "撤销",
|
||||
"revokeKey": "撤销密钥",
|
||||
"revokeTitle": "撤销 API 密钥",
|
||||
"revokeMessage": "撤销 API 密钥 \"{{name}}\"?此操作无法撤销。",
|
||||
"revoked": "API 密钥已撤销",
|
||||
"revokeFailed": "撤销 API 密钥失败:{{message}}",
|
||||
"copyNow": "立即复制 — 此密钥不会再次显示",
|
||||
"copiedToast": "已复制到剪贴板",
|
||||
"copyFailed": "复制失败",
|
||||
"empty": "尚无 API 密钥。在上方创建一个以获得程序化访问。",
|
||||
"lastUsed": "上次使用 {{date}}"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "页面未找到",
|
||||
"text": "看起来这个页面走丢了。让我们回到正轨。",
|
||||
"goHome": "返回首页"
|
||||
}
|
||||
}
|
||||
116
core/http/react-ui/public/locales/zh-CN/chat.json
Normal file
116
core/http/react-ui/public/locales/zh-CN/chat.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"activity": {
|
||||
"thought": "思考",
|
||||
"tool": "工具",
|
||||
"result": "结果",
|
||||
"toolResult": "{{name}} 的结果",
|
||||
"thinking": "思考中..."
|
||||
},
|
||||
"header": {
|
||||
"manageModeTooltip": "此聊天可以通过与 LocalAI 对话来安装模型、编辑配置和管理后端。",
|
||||
"modelInfo": "模型信息",
|
||||
"chatSettings": "聊天设置",
|
||||
"modelInfoTitle": "模型信息:{{model}}",
|
||||
"editConfig": "编辑配置",
|
||||
"close": "关闭"
|
||||
},
|
||||
"modelInfo": {
|
||||
"backend": "后端",
|
||||
"modelFile": "模型文件",
|
||||
"contextSize": "上下文大小",
|
||||
"threads": "线程数",
|
||||
"mcp": "MCP",
|
||||
"configured": "已配置",
|
||||
"chatTemplate": "聊天模板",
|
||||
"yes": "是",
|
||||
"gpuLayers": "GPU 层数"
|
||||
},
|
||||
"context": {
|
||||
"label": "上下文:{{percent}}%",
|
||||
"labelWithTokens": "上下文:{{percent}}% ({{tokens}} 个 token)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "聊天设置",
|
||||
"manageMode": "管理模式",
|
||||
"manageModeDesc": "允许此聊天通过与 LocalAI 对话来安装模型、切换后端和编辑配置。",
|
||||
"systemPrompt": "系统提示",
|
||||
"systemPromptPlaceholder": "你是一个有用的助手...",
|
||||
"temperature": "温度",
|
||||
"topP": "Top P",
|
||||
"topK": "Top K",
|
||||
"contextSize": "上下文大小",
|
||||
"contextSizePlaceholder": "2048",
|
||||
"clearHistory": "清除聊天记录"
|
||||
},
|
||||
"empty": {
|
||||
"manageTitle": "通过聊天管理 LocalAI",
|
||||
"manageText": "可以请求安装模型、切换后端、编辑配置或检查状态。助手会总结操作并在更改任何内容前等待您确认。",
|
||||
"startTitle": "开始对话",
|
||||
"readyText": "准备与 {{model}} 聊天",
|
||||
"selectModelText": "在上方选择一个模型以开始",
|
||||
"suggestionsManage": [
|
||||
"已经安装了什么?",
|
||||
"安装一个聊天模型",
|
||||
"显示系统状态",
|
||||
"更新一个后端"
|
||||
],
|
||||
"suggestionsChat": [
|
||||
"解释这是如何工作的",
|
||||
"帮我写代码",
|
||||
"总结一份文档",
|
||||
"头脑风暴"
|
||||
],
|
||||
"recent": "最近",
|
||||
"noMessages": "暂无消息",
|
||||
"hintEnter": "回车发送",
|
||||
"hintShiftEnter": "Shift+回车换行",
|
||||
"hintAttach": "附加文件"
|
||||
},
|
||||
"errors": {
|
||||
"viewTraces": "查看追踪以了解详情"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "复制",
|
||||
"regenerate": "重新生成"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "正在传输模型...",
|
||||
"transferringTo": "正在传输模型到 {{node}}..."
|
||||
},
|
||||
"tokens": {
|
||||
"perSec": "{{count}} tok/s",
|
||||
"peak": "峰值:{{count}} tok/s",
|
||||
"usage": "{{prompt}}p + {{completion}}c = {{total}}"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "消息...",
|
||||
"attachFile": "附加文件",
|
||||
"stopGenerating": "停止生成",
|
||||
"canvasTitle": "Canvas — 将代码块和媒体提取到侧边栏以便预览、复制和下载",
|
||||
"canvasLabel": "Canvas",
|
||||
"openCanvas": "打开 Canvas 面板"
|
||||
},
|
||||
"deleteAllDialog": {
|
||||
"title": "删除所有聊天",
|
||||
"message": "删除所有聊天?此操作无法撤销。",
|
||||
"confirm": "全部删除"
|
||||
},
|
||||
"toasts": {
|
||||
"selectModel": "请选择一个模型",
|
||||
"copied": "已复制到剪贴板"
|
||||
},
|
||||
"menu": {
|
||||
"trigger": "聊天",
|
||||
"triggerTitle": "对话 (Ctrl/Cmd+K)",
|
||||
"search": "搜索对话...",
|
||||
"clearSearch": "清除搜索",
|
||||
"noMatch": "没有对话匹配您的搜索",
|
||||
"noConversations": "暂无对话",
|
||||
"rename": "重命名",
|
||||
"exportMarkdown": "导出为 Markdown",
|
||||
"deleteChat": "删除聊天",
|
||||
"newChat": "新对话",
|
||||
"clearAll": "清除全部",
|
||||
"deleteAllTitle": "删除所有对话"
|
||||
}
|
||||
}
|
||||
43
core/http/react-ui/public/locales/zh-CN/collections.json
Normal file
43
core/http/react-ui/public/locales/zh-CN/collections.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"title": "知识库",
|
||||
"subtitle": "为智能体的 RAG 管理文档集合",
|
||||
"newPlaceholder": "新集合名称...",
|
||||
"actions": {
|
||||
"create": "创建",
|
||||
"creating": "创建中...",
|
||||
"details": "详情",
|
||||
"reset": "重置",
|
||||
"delete": "删除",
|
||||
"viewDetails": "查看详情",
|
||||
"resetCollection": "重置集合",
|
||||
"deleteCollection": "删除集合"
|
||||
},
|
||||
"sections": {
|
||||
"yourCollections": "您的集合",
|
||||
"otherUsersCollections": "其他用户的集合"
|
||||
},
|
||||
"empty": {
|
||||
"title": "暂无集合",
|
||||
"text": "集合让您可以将文档组织成知识库,智能体可以通过 RAG(检索增强生成)进行搜索。在上方创建一个集合以开始。",
|
||||
"noPersonal": "您还没有任何集合。"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "删除集合",
|
||||
"message": "删除集合 \"{{name}}\"?这将删除所有条目,且无法撤销。",
|
||||
"confirm": "删除"
|
||||
},
|
||||
"resetDialog": {
|
||||
"title": "重置集合",
|
||||
"message": "重置集合 \"{{name}}\"?这将删除所有条目但保留集合。",
|
||||
"confirm": "重置"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "加载集合失败:{{message}}",
|
||||
"created": "集合 \"{{name}}\" 已创建",
|
||||
"createFailed": "创建集合失败:{{message}}",
|
||||
"deleted": "集合 \"{{name}}\" 已删除",
|
||||
"deleteFailed": "删除集合失败:{{message}}",
|
||||
"reset": "集合 \"{{name}}\" 已重置",
|
||||
"resetFailed": "重置集合失败:{{message}}"
|
||||
}
|
||||
}
|
||||
109
core/http/react-ui/public/locales/zh-CN/common.json
Normal file
109
core/http/react-ui/public/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": {
|
||||
"save": "保存",
|
||||
"saving": "保存中...",
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
"remove": "移除",
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"refresh": "刷新",
|
||||
"reload": "重新加载",
|
||||
"retry": "重试",
|
||||
"search": "搜索",
|
||||
"filter": "筛选",
|
||||
"clear": "清除",
|
||||
"reset": "重置",
|
||||
"apply": "应用",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"open": "打开",
|
||||
"submit": "提交",
|
||||
"select": "选择",
|
||||
"selectAll": "全选",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"import": "导入",
|
||||
"export": "导出",
|
||||
"view": "查看",
|
||||
"details": "详情",
|
||||
"settings": "设置",
|
||||
"help": "帮助",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"status": {
|
||||
"loading": "加载中...",
|
||||
"saving": "保存中...",
|
||||
"saved": "已保存",
|
||||
"ready": "就绪",
|
||||
"running": "运行中",
|
||||
"stopped": "已停止",
|
||||
"starting": "启动中...",
|
||||
"stopping": "停止中...",
|
||||
"pending": "等待中",
|
||||
"active": "活跃",
|
||||
"inactive": "未活跃",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"empty": "无内容",
|
||||
"none": "无",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"dialogs": {
|
||||
"confirmDelete": {
|
||||
"title": "确认删除",
|
||||
"message": "您确定要删除吗?此操作无法撤销。",
|
||||
"confirm": "删除",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"unsavedChanges": {
|
||||
"title": "未保存的更改",
|
||||
"message": "您有未保存的更改。是否要放弃?",
|
||||
"discard": "放弃",
|
||||
"keepEditing": "继续编辑"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"required": "必填",
|
||||
"optional": "可选",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"type": "类型",
|
||||
"value": "值",
|
||||
"search": "搜索...",
|
||||
"selectPlaceholder": "选择一个选项..."
|
||||
},
|
||||
"time": {
|
||||
"now": "刚才",
|
||||
"secondsAgo_one": "{{count}} 秒前",
|
||||
"secondsAgo_other": "{{count}} 秒前",
|
||||
"minutesAgo_one": "{{count}} 分钟前",
|
||||
"minutesAgo_other": "{{count}} 分钟前",
|
||||
"hoursAgo_one": "{{count}} 小时前",
|
||||
"hoursAgo_other": "{{count}} 小时前",
|
||||
"daysAgo_one": "{{count}} 天前",
|
||||
"daysAgo_other": "{{count}} 天前"
|
||||
},
|
||||
"units": {
|
||||
"bytes": "B",
|
||||
"kilobytes": "KB",
|
||||
"megabytes": "MB",
|
||||
"gigabytes": "GB",
|
||||
"terabytes": "TB"
|
||||
}
|
||||
}
|
||||
17
core/http/react-ui/public/locales/zh-CN/errors.json
Normal file
17
core/http/react-ui/public/locales/zh-CN/errors.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"generic": "出错了",
|
||||
"network": "网络错误。请检查您的连接并重试。",
|
||||
"unauthorized": "您无权执行此操作。",
|
||||
"forbidden": "访问被拒绝。",
|
||||
"notFound": "未找到请求的资源。",
|
||||
"serverError": "服务器错误。请稍后再试。",
|
||||
"loadFailed": "加载失败:{{message}}",
|
||||
"saveFailed": "保存失败:{{message}}",
|
||||
"deleteFailed": "删除失败:{{message}}",
|
||||
"updateFailed": "更新失败:{{message}}",
|
||||
"createFailed": "创建失败:{{message}}",
|
||||
"operationFailed": "操作失败:{{message}}",
|
||||
"invalidInput": "输入无效。请检查表单后重试。",
|
||||
"tryAgain": "请重试。",
|
||||
"contactAdmin": "如果问题仍然存在,请联系您的管理员。"
|
||||
}
|
||||
66
core/http/react-ui/public/locales/zh-CN/home.json
Normal file
66
core/http/react-ui/public/locales/zh-CN/home.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"cluster": {
|
||||
"vram": "集群 VRAM",
|
||||
"ram": "集群 RAM",
|
||||
"nodesOnline": "{{healthy}}/{{total}} 节点在线"
|
||||
},
|
||||
"resourceGpu": "GPU",
|
||||
"resourceRam": "RAM",
|
||||
"assistant": {
|
||||
"title": "通过聊天管理 LocalAI",
|
||||
"description": "通过与 LocalAI 对话来安装模型、切换后端、编辑配置和查看状态。",
|
||||
"open": "打开助手",
|
||||
"tooltip": "通过聊天管理 LocalAI"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "消息...",
|
||||
"attachImage": "附加图片",
|
||||
"attachAudio": "附加音频",
|
||||
"attachFile": "附加文件",
|
||||
"enterToSend": "按回车发送",
|
||||
"selectModelFirst": "请先选择一个模型",
|
||||
"sendMessage": "发送消息",
|
||||
"selectModelToast": "请先选择一个模型"
|
||||
},
|
||||
"quickLinks": {
|
||||
"manageByChat": "通过聊天管理",
|
||||
"installedModels": "已安装的模型",
|
||||
"browseGallery": "浏览模型库",
|
||||
"importModel": "导入模型",
|
||||
"documentation": "文档"
|
||||
},
|
||||
"loadedModels": {
|
||||
"count_one": "已加载 {{count}} 个模型",
|
||||
"count_other": "已加载 {{count}} 个模型",
|
||||
"stop": "停止模型",
|
||||
"stopAll": "全部停止"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "停止模型",
|
||||
"message": "停止模型 {{model}}?",
|
||||
"confirm": "停止 {{model}}",
|
||||
"stopAllTitle": "停止所有模型",
|
||||
"stopAllMessage": "停止所有 {{count}} 个已加载的模型?",
|
||||
"stopAllConfirm": "全部停止",
|
||||
"stoppedToast": "已停止 {{model}}",
|
||||
"allStoppedToast": "所有模型已停止",
|
||||
"stopFailed": "停止失败:{{message}}"
|
||||
},
|
||||
"wizard": {
|
||||
"getStarted": "开始使用 {{name}}",
|
||||
"intro": "安装您的第一个模型即可开始。浏览模型库或导入您自己的模型。",
|
||||
"steps": {
|
||||
"step1Title": "浏览模型库",
|
||||
"step1Body": "从我们精选的集合中找到适合您需求的模型。",
|
||||
"step2Title": "安装模型",
|
||||
"step2Body": "点击安装即可自动下载并配置。",
|
||||
"step3Title": "开始聊天",
|
||||
"step3Body": "直接在浏览器中与模型聊天,或使用 API。"
|
||||
},
|
||||
"browseGallery": "浏览模型库",
|
||||
"importModel": "导入模型",
|
||||
"docs": "文档",
|
||||
"noModelsTitle": "暂无可用模型",
|
||||
"noModelsBody": "尚未安装模型。请联系您的管理员设置模型,以便开始聊天。"
|
||||
}
|
||||
}
|
||||
142
core/http/react-ui/public/locales/zh-CN/importModel.json
Normal file
142
core/http/react-ui/public/locales/zh-CN/importModel.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"title": "导入新模型",
|
||||
"subtitle": {
|
||||
"simple": "从 URI 导入模型 — 自动检测会选择后端。",
|
||||
"powerYaml": "编写完整的模型 YAML 配置。",
|
||||
"powerPrefs": "细粒度的导入偏好设置。"
|
||||
},
|
||||
"actions": {
|
||||
"import": "导入模型",
|
||||
"importing": "导入中...",
|
||||
"create": "创建",
|
||||
"saving": "保存中...",
|
||||
"browseHF": "在 HF 上浏览模型",
|
||||
"addCustom": "添加自定义",
|
||||
"copy": "复制"
|
||||
},
|
||||
"form": {
|
||||
"modelUri": "模型 URI",
|
||||
"uriPlaceholder": "huggingface://TheBloke/Llama-2-7B-Chat-GGUF 或 https://example.com/model.gguf",
|
||||
"uriHint": "输入要导入的模型文件的 URI 或路径",
|
||||
"supportedFormats": "支持的 URI 格式",
|
||||
"options": "选项",
|
||||
"preferences": "偏好设置(可选)",
|
||||
"commonPreferences": "常用偏好",
|
||||
"customPreferences": "自定义偏好",
|
||||
"customKeyValueHint": "添加自定义键值对以进行高级配置。",
|
||||
"preferenceKey": "第 {{index}} 行的偏好键",
|
||||
"preferenceValue": "第 {{index}} 行的偏好值",
|
||||
"removePref": "移除此偏好",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"backend": "后端",
|
||||
"backendAuto": "自动检测(基于 URI)",
|
||||
"backendLoading": "正在加载后端…",
|
||||
"backendSearch": "搜索后端...",
|
||||
"backendHint": "强制指定一个后端。留空可从 URI 自动检测。标记为 \"manual pick\" 的项目无法自动检测 — 如果您知道模型需要什么,请自行选择。",
|
||||
"backendErrorHint": "无法加载后端列表 — 仅使用自动检测。",
|
||||
"backendNotInstalled": "此后端尚未安装。提交导入将先下载它。",
|
||||
"modelName": "模型名称",
|
||||
"modelNamePlaceholder": "留空以使用文件名",
|
||||
"modelNameHint": "模型的自定义名称。若为空,将使用文件名。",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "留空以使用默认描述",
|
||||
"descriptionHint": "模型的自定义描述。",
|
||||
"quantizations": "量化",
|
||||
"quantizationsPlaceholder": "q4_k_m,q4_k_s,q3_k_m(逗号分隔)",
|
||||
"quantizationsHint": "首选量化(逗号分隔)。留空使用默认值(q4_k_m)。",
|
||||
"mmprojQuantizations": "MMProj 量化",
|
||||
"mmprojQuantizationsPlaceholder": "fp16,fp32(逗号分隔)",
|
||||
"mmprojQuantizationsHint": "首选 MMProj 量化。留空使用默认值(fp16)。",
|
||||
"embeddings": "嵌入",
|
||||
"embeddingsHint": "为此模型启用嵌入支持。",
|
||||
"modelType": "模型类型",
|
||||
"modelTypePlaceholder": "AutoModelForCausalLM(用于 transformers 后端)",
|
||||
"modelTypeHint": "transformers 后端的模型类型。例如:AutoModelForCausalLM、SentenceTransformer、Mamba。",
|
||||
"pipelineType": "管道类型",
|
||||
"pipelineTypeHint": "diffusers 后端的管道类型。",
|
||||
"schedulerType": "调度器类型",
|
||||
"schedulerTypePlaceholder": "k_dpmpp_2m(可选)",
|
||||
"schedulerTypeHint": "diffusers 后端的调度器类型。例如:k_dpmpp_2m、euler_a、ddim。",
|
||||
"enableParameters": "启用的参数",
|
||||
"enableParametersPlaceholder": "negative_prompt,num_inference_steps(逗号分隔)",
|
||||
"enableParametersHint": "diffusers 后端启用的参数(逗号分隔)。",
|
||||
"cuda": "CUDA",
|
||||
"cudaHint": "启用 CUDA 支持以进行 GPU 加速。",
|
||||
"yamlEditor": "YAML 配置编辑器",
|
||||
"manualPick": "手动选择",
|
||||
"manualPickTooltip": "自动检测不会路由到此后端。如果您知道这就是您想要的,请在此处选择。"
|
||||
},
|
||||
"modality": {
|
||||
"text": "文本 LLM",
|
||||
"asr": "语音识别",
|
||||
"tts": "文字转语音",
|
||||
"image": "图像 / 视频",
|
||||
"embeddings": "嵌入",
|
||||
"reranker": "重排器",
|
||||
"detection": "对象检测",
|
||||
"vad": "语音活动检测",
|
||||
"other": "其他"
|
||||
},
|
||||
"powerTabs": {
|
||||
"ariaLabel": "高级模式标签",
|
||||
"preferences": "偏好",
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"switchDialog": {
|
||||
"title": "保留您的自定义偏好?",
|
||||
"body": "切换到简单模式会隐藏除后端、名称和描述之外的偏好设置。它们仍会在您导入时被发送。",
|
||||
"cancel": "取消",
|
||||
"discard": "放弃并切换",
|
||||
"keep": "保留并切换"
|
||||
},
|
||||
"estimate": {
|
||||
"title": "估计需求",
|
||||
"download": "下载:{{size}}",
|
||||
"vram": "VRAM:{{vram}}"
|
||||
},
|
||||
"toasts": {
|
||||
"noUri": "请输入模型 URI",
|
||||
"noYaml": "请输入 YAML 配置",
|
||||
"started": "导入已开始!正在跟踪进度...",
|
||||
"startedWithMeta": "导入已开始!正在跟踪进度...({{meta}})",
|
||||
"imported": "模型导入成功!",
|
||||
"importedYaml": "模型配置导入成功!",
|
||||
"importFailed": "导入失败:{{message}}",
|
||||
"startImportFailed": "开始导入失败:{{message}}",
|
||||
"backendsLoadFailed": "无法加载后端列表 — 仅使用自动检测",
|
||||
"modalityClearedBackend": "已清除后端选择 — 它不在 {{label}} 组中。",
|
||||
"copied": "已复制到剪贴板"
|
||||
},
|
||||
"uriFormats": {
|
||||
"huggingface": {
|
||||
"title": "HuggingFace",
|
||||
"standard": "标准 HuggingFace 格式",
|
||||
"short": "简短的 HuggingFace 格式",
|
||||
"fullUrl": "完整的 HuggingFace URL"
|
||||
},
|
||||
"http": {
|
||||
"title": "HTTP/HTTPS URL",
|
||||
"direct": "从任何 HTTPS URL 直接下载"
|
||||
},
|
||||
"local": {
|
||||
"title": "本地文件",
|
||||
"filePath": "本地文件路径(绝对路径)",
|
||||
"directYaml": "直接的本地 YAML 配置文件"
|
||||
},
|
||||
"oci": {
|
||||
"title": "OCI 注册表",
|
||||
"registry": "OCI 容器注册表",
|
||||
"tarball": "本地 OCI tarball 文件"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"model": "Ollama 模型格式"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "YAML 配置文件",
|
||||
"remote": "远程 YAML 配置文件",
|
||||
"local": "本地 YAML 配置文件"
|
||||
}
|
||||
}
|
||||
}
|
||||
154
core/http/react-ui/public/locales/zh-CN/media.json
Normal file
154
core/http/react-ui/public/locales/zh-CN/media.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "图像",
|
||||
"video": "视频",
|
||||
"tts": "TTS",
|
||||
"sound": "声音"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "图像生成",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"prompt": "提示词",
|
||||
"promptPlaceholder": "描述您想要生成的图像...",
|
||||
"negativePrompt": "负面提示词",
|
||||
"negativePromptPlaceholder": "想要避免的内容...",
|
||||
"size": "尺寸",
|
||||
"count": "数量 (1-4)",
|
||||
"advanced": "高级设置",
|
||||
"imageInputs": "图像输入",
|
||||
"steps": "步数",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "随机种子",
|
||||
"seedPlaceholder": "随机",
|
||||
"sourceImage": "源图像 (img2img)",
|
||||
"refImages": "参考图像",
|
||||
"refImagesAdded_one": "已添加 {{count}} 张图像",
|
||||
"refImagesAdded_other": "已添加 {{count}} 张图像"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的图像将显示在此处",
|
||||
"toasts": {
|
||||
"noPrompt": "请输入提示词",
|
||||
"noModel": "请选择一个模型",
|
||||
"noResults": "未生成图像"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "视频生成",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"prompt": "提示词",
|
||||
"promptPlaceholder": "描述您想要生成的视频...",
|
||||
"duration": "时长(秒)",
|
||||
"fps": "FPS",
|
||||
"size": "尺寸",
|
||||
"advanced": "高级设置",
|
||||
"seed": "随机种子",
|
||||
"seedPlaceholder": "随机"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成视频",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的视频将显示在此处",
|
||||
"toasts": {
|
||||
"noPrompt": "请输入提示词",
|
||||
"noModel": "请选择一个模型",
|
||||
"noResults": "未生成视频"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "文字转语音",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"voice": "声音",
|
||||
"voicePlaceholder": "可选的声音 ID",
|
||||
"input": "文本",
|
||||
"inputPlaceholder": "输入要合成的文本..."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成音频",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的音频将显示在此处",
|
||||
"toasts": {
|
||||
"noText": "请输入文本",
|
||||
"noModel": "请选择一个模型",
|
||||
"generateFailed": "生成失败"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "声音生成",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"prompt": "提示词",
|
||||
"promptPlaceholder": "描述您想要生成的声音...",
|
||||
"duration": "时长(秒)",
|
||||
"language": "语言",
|
||||
"vocalLanguage": "声乐语言",
|
||||
"lyrics": "歌词(可选)",
|
||||
"lyricsPlaceholder": "用于声乐生成的歌词",
|
||||
"advanced": "高级设置",
|
||||
"seed": "随机种子",
|
||||
"seedPlaceholder": "随机"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的音频将显示在此处",
|
||||
"toasts": {
|
||||
"noPrompt": "请输入提示词",
|
||||
"noModel": "请选择一个模型",
|
||||
"generateFailed": "生成失败"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "通话",
|
||||
"subtitle": "实时语音对话",
|
||||
"actions": {
|
||||
"start": "开始会话",
|
||||
"stop": "结束会话",
|
||||
"connecting": "连接中...",
|
||||
"muted": "已静音",
|
||||
"mute": "静音",
|
||||
"unmute": "取消静音"
|
||||
},
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"voice": "声音",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "语言",
|
||||
"languagePlaceholder": "zh",
|
||||
"instructions": "指令",
|
||||
"instructionsPlaceholder": "设置助手的角色..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "空闲",
|
||||
"connecting": "连接中...",
|
||||
"listening": "正在聆听...",
|
||||
"speaking": "正在说话...",
|
||||
"ended": "会话已结束"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "请先选择一个模型",
|
||||
"connectFailed": "连接失败:{{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "历史",
|
||||
"empty": "暂无历史",
|
||||
"deleteEntry": "删除条目",
|
||||
"clear": "清除历史",
|
||||
"clearTitle": "清除全部历史",
|
||||
"clearMessage": "删除所有历史条目?此操作无法撤销。",
|
||||
"clearConfirm": "清除",
|
||||
"cleared": "历史已清除"
|
||||
}
|
||||
}
|
||||
85
core/http/react-ui/public/locales/zh-CN/models.json
Normal file
85
core/http/react-ui/public/locales/zh-CN/models.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"title": "安装模型",
|
||||
"subtitle": "从模型库浏览和安装 AI 模型",
|
||||
"stats": {
|
||||
"available": "可用",
|
||||
"installed": "已安装"
|
||||
},
|
||||
"actions": {
|
||||
"addModel": "添加模型",
|
||||
"importModel": "导入模型",
|
||||
"install": "安装",
|
||||
"reinstall": "重新安装",
|
||||
"delete": "删除"
|
||||
},
|
||||
"filters": {
|
||||
"all": "全部",
|
||||
"llm": "LLM",
|
||||
"image": "图像",
|
||||
"multimodal": "多模态",
|
||||
"vision": "视觉",
|
||||
"tts": "TTS",
|
||||
"stt": "STT",
|
||||
"embedding": "嵌入",
|
||||
"rerank": "重排",
|
||||
"allBackends": "所有后端",
|
||||
"searchBackends": "搜索后端..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索模型...",
|
||||
"clearFilters": "清除筛选"
|
||||
},
|
||||
"table": {
|
||||
"modelName": "模型名称",
|
||||
"description": "描述",
|
||||
"backend": "后端",
|
||||
"sizeVram": "大小 / VRAM",
|
||||
"status": "状态",
|
||||
"actions": "操作",
|
||||
"size": "大小:{{size}}",
|
||||
"vram": "VRAM:{{vram}}",
|
||||
"fits": "适合",
|
||||
"mayNotFit": "可能不适合",
|
||||
"trustRemoteCode": "Trust Remote Code",
|
||||
"installing": "正在安装",
|
||||
"installingPct": "正在安装 · {{percent}}%",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装"
|
||||
},
|
||||
"detail": {
|
||||
"description": "描述",
|
||||
"gallery": "模型库",
|
||||
"backend": "后端",
|
||||
"size": "大小",
|
||||
"vram": "VRAM",
|
||||
"license": "许可证",
|
||||
"tags": "标签",
|
||||
"links": "链接",
|
||||
"warning": "警告",
|
||||
"files": "文件",
|
||||
"fitsGpu": "适合 GPU",
|
||||
"mayNotFitGpu": "可能不适合 GPU",
|
||||
"requiresTrustRemoteCode": "需要 Trust Remote Code",
|
||||
"fileCount_one": "{{count}} 个文件",
|
||||
"fileCount_other": "{{count}} 个文件",
|
||||
"filename": "文件名",
|
||||
"uri": "URI",
|
||||
"sha256": "SHA256"
|
||||
},
|
||||
"empty": {
|
||||
"title": "未找到模型",
|
||||
"withFilters": "没有模型与当前搜索或筛选条件匹配。",
|
||||
"noFilters": "模型库为空。"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "删除模型",
|
||||
"message": "删除模型 {{model}}?",
|
||||
"confirm": "删除 {{model}}",
|
||||
"deletingToast": "正在删除 {{model}}..."
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "加载模型失败:{{message}}",
|
||||
"installFailed": "安装失败:{{message}}",
|
||||
"deleteFailed": "删除失败:{{message}}"
|
||||
}
|
||||
}
|
||||
51
core/http/react-ui/public/locales/zh-CN/nav.json
Normal file
51
core/http/react-ui/public/locales/zh-CN/nav.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"appName": "LocalAI",
|
||||
"openMenu": "打开菜单",
|
||||
"closeMenu": "关闭菜单",
|
||||
"primaryNavigation": "主导航",
|
||||
"switchToLightMode": "切换到浅色模式",
|
||||
"switchToDarkMode": "切换到深色模式",
|
||||
"expandSidebar": "展开侧边栏",
|
||||
"collapseSidebar": "收起侧边栏",
|
||||
"changeLanguage": "更改语言",
|
||||
"logout": "退出登录",
|
||||
"accountSettings": "账户设置",
|
||||
"account": "账户",
|
||||
"accountFor": "账户:{{name}}",
|
||||
"sections": {
|
||||
"tools": "工具",
|
||||
"biometrics": "生物识别",
|
||||
"agents": "智能体",
|
||||
"system": "系统"
|
||||
},
|
||||
"items": {
|
||||
"home": "首页",
|
||||
"installModels": "安装模型",
|
||||
"chat": "聊天",
|
||||
"studio": "工作室",
|
||||
"talk": "通话",
|
||||
"fineTune": "微调(实验性)",
|
||||
"quantize": "量化(实验性)",
|
||||
"faceRecognition": "人脸识别",
|
||||
"voiceRecognition": "语音识别",
|
||||
"agents": "智能体",
|
||||
"skills": "技能",
|
||||
"memory": "记忆库",
|
||||
"mcpJobs": "MCP CI 任务",
|
||||
"usage": "用量",
|
||||
"users": "用户",
|
||||
"backends": "后端",
|
||||
"traces": "追踪",
|
||||
"nodes": "节点",
|
||||
"swarm": "Swarm",
|
||||
"system": "系统",
|
||||
"settings": "设置",
|
||||
"api": "API"
|
||||
},
|
||||
"footer": {
|
||||
"github": "GitHub",
|
||||
"documentation": "文档",
|
||||
"author": "作者",
|
||||
"copyright": "© 2023-{{year}} {{author}}"
|
||||
}
|
||||
}
|
||||
79
core/http/react-ui/public/locales/zh-CN/skills.json
Normal file
79
core/http/react-ui/public/locales/zh-CN/skills.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "技能",
|
||||
"subtitle": "管理智能体技能(可重用的指令和资源)",
|
||||
"unavailable": {
|
||||
"subtitle": "技能服务不可用或索引正在重建。请稍后再试。",
|
||||
"retry": "重试"
|
||||
},
|
||||
"actions": {
|
||||
"newSkill": "新技能",
|
||||
"createSkill": "创建技能",
|
||||
"import": "导入",
|
||||
"importing": "正在导入...",
|
||||
"gitRepos": "Git 仓库",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"export": "导出",
|
||||
"sync": "同步",
|
||||
"addRepo": "添加仓库",
|
||||
"adding": "添加中...",
|
||||
"remove": "移除",
|
||||
"enable": "启用",
|
||||
"disable": "禁用"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索技能..."
|
||||
},
|
||||
"git": {
|
||||
"title": "Git 仓库",
|
||||
"description": "添加 Git 仓库以拉取技能。同步后技能将出现在列表中。",
|
||||
"urlPlaceholder": "https://github.com/user/repo 或 git@github.com:user/repo.git",
|
||||
"noRepos": "未配置 Git 仓库。请在上方添加。",
|
||||
"disabled": "已禁用",
|
||||
"removeRepo": "移除仓库"
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "无描述",
|
||||
"readOnly": "只读",
|
||||
"editTitle": "编辑技能",
|
||||
"deleteTitle": "删除技能",
|
||||
"exportTitle": "导出为 .tar.gz"
|
||||
},
|
||||
"empty": {
|
||||
"title": "未找到技能",
|
||||
"text": "创建或导入一个技能以开始。",
|
||||
"noPersonal": "您还没有任何技能。"
|
||||
},
|
||||
"sections": {
|
||||
"yourSkills": "您的技能",
|
||||
"otherUsersSkills": "其他用户的技能"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "删除技能",
|
||||
"message": "删除技能 \"{{name}}\"?此操作无法撤销。",
|
||||
"confirm": "删除"
|
||||
},
|
||||
"removeRepoDialog": {
|
||||
"title": "移除 Git 仓库",
|
||||
"message": "移除此 Git 仓库?其中的技能将不再可用。",
|
||||
"confirm": "移除"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "加载技能失败",
|
||||
"deleted": "技能 \"{{name}}\" 已删除",
|
||||
"deleteFailed": "删除技能失败",
|
||||
"exported": "技能 \"{{name}}\" 已导出",
|
||||
"exportFailed": "导出失败",
|
||||
"imported": "已从 \"{{file}}\" 导入技能",
|
||||
"importFailed": "导入失败",
|
||||
"loadReposFailed": "加载 Git 仓库失败",
|
||||
"repoAdded": "Git 仓库已添加并正在同步",
|
||||
"addRepoFailed": "添加仓库失败",
|
||||
"synced": "仓库已同步",
|
||||
"syncFailed": "同步失败",
|
||||
"toggled": "仓库已切换",
|
||||
"toggleFailed": "切换失败",
|
||||
"removed": "仓库已移除",
|
||||
"removeFailed": "移除失败"
|
||||
}
|
||||
}
|
||||
180
core/http/react-ui/scripts/translate-locales.mjs
Normal file
180
core/http/react-ui/scripts/translate-locales.mjs
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
// Bootstrap non-English locale JSON files from the English source.
|
||||
//
|
||||
// Usage:
|
||||
// # Fill missing keys in non-English locales by copying the English value
|
||||
// # (placeholder — translators / community can refine afterwards).
|
||||
// node scripts/translate-locales.mjs --copy
|
||||
//
|
||||
// # Translate via OpenAI (default provider).
|
||||
// OPENAI_API_KEY=sk-... node scripts/translate-locales.mjs --translate
|
||||
//
|
||||
// # Translate via Anthropic.
|
||||
// ANTHROPIC_API_KEY=sk-ant-... node scripts/translate-locales.mjs --translate --provider=anthropic
|
||||
//
|
||||
// # Dry-run to see what would change.
|
||||
// node scripts/translate-locales.mjs --translate --dry-run
|
||||
//
|
||||
// Behavior:
|
||||
// - Reads public/locales/en/*.json as source of truth.
|
||||
// - For each other locale (it, es, de, zh-CN), opens the matching file
|
||||
// (or creates it). Walks the source object; for each leaf string:
|
||||
// * If the target already has a non-empty translation, leave it.
|
||||
// * If --copy mode, fill with the English value.
|
||||
// * If --translate mode, send the value to the LLM with the key path
|
||||
// and locale name as context.
|
||||
// - Writes the updated file with sorted keys, 2-space indent.
|
||||
//
|
||||
// The script is idempotent: existing translations are preserved unless
|
||||
// --overwrite is passed. Run it whenever new keys are added in en/.
|
||||
|
||||
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = join(__dirname, '..')
|
||||
const LOCALES_DIR = join(ROOT, 'public', 'locales')
|
||||
const SOURCE_LOCALE = 'en'
|
||||
const TARGET_LOCALES = ['it', 'es', 'de', 'zh-CN']
|
||||
|
||||
const LANGUAGE_NAMES = {
|
||||
it: 'Italian',
|
||||
es: 'Spanish',
|
||||
de: 'German',
|
||||
'zh-CN': 'Simplified Chinese',
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
const args = new Set(argv)
|
||||
const MODE = args.has('--translate') ? 'translate' : 'copy'
|
||||
const DRY_RUN = args.has('--dry-run')
|
||||
const OVERWRITE = args.has('--overwrite')
|
||||
const providerArg = argv.find(a => a.startsWith('--provider='))?.split('=')[1]
|
||||
const PROVIDER = providerArg || (process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY ? 'anthropic' : 'openai')
|
||||
|
||||
function readJson(path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'))
|
||||
}
|
||||
|
||||
function writeJson(path, data) {
|
||||
if (DRY_RUN) {
|
||||
console.log(`[dry-run] would write ${path}`)
|
||||
return
|
||||
}
|
||||
const dir = dirname(path)
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
||||
}
|
||||
|
||||
function listNamespaces() {
|
||||
return readdirSync(join(LOCALES_DIR, SOURCE_LOCALE))
|
||||
.filter((f) => f.endsWith('.json'))
|
||||
.map((f) => f.replace(/\.json$/, ''))
|
||||
}
|
||||
|
||||
async function translateString(value, locale, keyPath) {
|
||||
if (MODE === 'copy') return value
|
||||
const language = LANGUAGE_NAMES[locale] || locale
|
||||
const prompt = `Translate the following UI string from English to ${language}.
|
||||
Preserve {{interpolation}} placeholders exactly. Preserve trailing punctuation
|
||||
and ellipses. Do not add quotes around the result. Reply with the translation only.
|
||||
|
||||
Key: ${keyPath}
|
||||
String: ${value}`
|
||||
|
||||
if (PROVIDER === 'anthropic') {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY
|
||||
if (!apiKey) throw new Error('ANTHROPIC_API_KEY required for --provider=anthropic')
|
||||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.TRANSLATE_MODEL || 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 1024,
|
||||
system: 'You are a professional UI string translator. Reply with the translation only, no preamble.',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.text()
|
||||
throw new Error(`Anthropic API ${res.status}: ${body}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
return (data.content?.[0]?.text || '').trim()
|
||||
}
|
||||
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
if (!apiKey) throw new Error('OPENAI_API_KEY required for --provider=openai')
|
||||
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.TRANSLATE_MODEL || 'gpt-4o-mini',
|
||||
temperature: 0.2,
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a professional UI string translator.' },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.text()
|
||||
throw new Error(`OpenAI API ${res.status}: ${body}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
return data.choices[0].message.content.trim()
|
||||
}
|
||||
|
||||
async function walk(source, target, locale, prefix = '') {
|
||||
for (const key of Object.keys(source)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key
|
||||
const sv = source[key]
|
||||
if (sv && typeof sv === 'object' && !Array.isArray(sv)) {
|
||||
target[key] = target[key] && typeof target[key] === 'object' ? target[key] : {}
|
||||
await walk(sv, target[key], locale, path)
|
||||
} else if (typeof sv === 'string') {
|
||||
const existing = target[key]
|
||||
const hasTranslation = typeof existing === 'string' && existing.trim().length > 0
|
||||
if (hasTranslation && !OVERWRITE) continue
|
||||
try {
|
||||
target[key] = await translateString(sv, locale, path)
|
||||
process.stdout.write('.')
|
||||
} catch (err) {
|
||||
process.stdout.write('!')
|
||||
console.error(`\n failed at ${locale}/${path}: ${err.message}`)
|
||||
target[key] = existing ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const namespaces = listNamespaces()
|
||||
console.log(`mode=${MODE} provider=${PROVIDER} locales=${TARGET_LOCALES.join(',')} namespaces=${namespaces.join(',')}`)
|
||||
for (const ns of namespaces) {
|
||||
const sourcePath = join(LOCALES_DIR, SOURCE_LOCALE, `${ns}.json`)
|
||||
const source = readJson(sourcePath)
|
||||
for (const locale of TARGET_LOCALES) {
|
||||
const targetPath = join(LOCALES_DIR, locale, `${ns}.json`)
|
||||
const target = existsSync(targetPath) ? readJson(targetPath) : {}
|
||||
process.stdout.write(`${locale}/${ns} `)
|
||||
await walk(source, target, locale)
|
||||
process.stdout.write('\n')
|
||||
writeJson(targetPath, target)
|
||||
}
|
||||
}
|
||||
console.log('done.')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -536,6 +536,100 @@
|
||||
border-color: var(--color-primary-border);
|
||||
}
|
||||
|
||||
/* Language switcher */
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.language-switcher-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.language-switcher-code {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.sidebar.collapsed .language-switcher-code {
|
||||
display: none;
|
||||
}
|
||||
.language-switcher-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 0;
|
||||
min-width: 160px;
|
||||
background: var(--color-bg-elevated, var(--color-bg-secondary));
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.18));
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.sidebar.collapsed .language-switcher-menu {
|
||||
left: calc(100% + 8px);
|
||||
bottom: 0;
|
||||
}
|
||||
.language-switcher-menu li {
|
||||
margin: 0;
|
||||
}
|
||||
.language-switcher-option {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
}
|
||||
.language-switcher-option:hover {
|
||||
background: var(--color-bg-tertiary, rgba(255, 255, 255, 0.04));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.language-switcher-option.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.language-switcher-flag {
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
width: 22px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.language-switcher-name {
|
||||
flex: 1;
|
||||
}
|
||||
.language-switcher-check {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* App boot fallback (rendered while initial i18n namespaces load) */
|
||||
.app-boot-spinner {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-bg-primary, #111);
|
||||
}
|
||||
.app-boot-spinner-dot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--color-border-subtle, rgba(255, 255, 255, 0.15));
|
||||
border-top-color: var(--color-primary, #4f8cff);
|
||||
animation: app-boot-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes app-boot-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Operations bar */
|
||||
.operations-bar {
|
||||
background: var(--color-bg-secondary);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import OperationsBar from './components/OperationsBar'
|
||||
import { ToastContainer, useToast } from './components/Toast'
|
||||
@@ -22,6 +23,7 @@ export default function App() {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { authEnabled, user } = useAuth()
|
||||
const branding = useBranding()
|
||||
const { t } = useTranslation('nav')
|
||||
const hamburgerRef = useRef(null)
|
||||
const isChatRoute = location.pathname.match(/\/chat(\/|$)/) || location.pathname.match(/\/agents\/[^/]+\/chat/)
|
||||
|
||||
@@ -67,7 +69,8 @@ export default function App() {
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
const showAvatar = authEnabled && user
|
||||
const accountLabel = user?.name || user?.email || 'Account'
|
||||
const accountLabel = user?.name || user?.email || t('account')
|
||||
const themeToggleLabel = theme === 'dark' ? t('switchToLightMode') : t('switchToDarkMode')
|
||||
|
||||
return (
|
||||
<div className={layoutClasses}>
|
||||
@@ -83,7 +86,7 @@ export default function App() {
|
||||
ref={hamburgerRef}
|
||||
className="hamburger-btn"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="Open menu"
|
||||
aria-label={t('openMenu')}
|
||||
aria-expanded={sidebarOpen}
|
||||
aria-controls="app-sidebar"
|
||||
>
|
||||
@@ -95,8 +98,8 @@ export default function App() {
|
||||
type="button"
|
||||
className="mobile-header-btn"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
aria-label={themeToggleLabel}
|
||||
title={themeToggleLabel}
|
||||
>
|
||||
<i className={`fas ${theme === 'dark' ? 'fa-sun' : 'fa-moon'}`} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -105,7 +108,7 @@ export default function App() {
|
||||
type="button"
|
||||
className="mobile-header-btn mobile-header-avatar"
|
||||
onClick={() => navigate('/app/account')}
|
||||
aria-label={`Account: ${accountLabel}`}
|
||||
aria-label={t('accountFor', { name: accountLabel })}
|
||||
title={accountLabel}
|
||||
>
|
||||
{user.avatarUrl ? (
|
||||
@@ -132,13 +135,13 @@ export default function App() {
|
||||
)}
|
||||
<div className="app-footer-links">
|
||||
<a href="https://github.com/mudler/LocalAI" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fab fa-github" /> GitHub
|
||||
<i className="fab fa-github" /> {t('footer.github')}
|
||||
</a>
|
||||
<a href="https://localai.io" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-book" /> Documentation
|
||||
<i className="fas fa-book" /> {t('footer.documentation')}
|
||||
</a>
|
||||
<a href="https://mudler.pm" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-user" /> Author
|
||||
<i className="fas fa-user" /> {t('footer.author')}
|
||||
</a>
|
||||
</div>
|
||||
<span className="app-footer-copyright">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useRef, useCallback, useImperativeHandle, forwardRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { relativeTime } from '../utils/format'
|
||||
|
||||
function getLastMessagePreview(chat) {
|
||||
@@ -24,6 +25,7 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
onRename,
|
||||
onExport,
|
||||
}, ref) {
|
||||
const { t } = useTranslation('chat')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const [editingId, setEditingId] = useState(null)
|
||||
@@ -139,11 +141,11 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
className={`btn btn-secondary btn-sm chats-menu-trigger${open ? ' active' : ''}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
title="Conversations (Ctrl/Cmd+K)"
|
||||
title={t('menu.triggerTitle')}
|
||||
onClick={() => setOpen(prev => !prev)}
|
||||
>
|
||||
<i className="fas fa-comments" />
|
||||
<span className="chats-menu-trigger-label">Chats</span>
|
||||
<span className="chats-menu-trigger-label">{t('menu.trigger')}</span>
|
||||
<kbd className="chats-menu-trigger-kbd">⌘K</kbd>
|
||||
</button>
|
||||
|
||||
@@ -157,14 +159,14 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setActiveIdx(0) }}
|
||||
placeholder="Search conversations..."
|
||||
placeholder={t('menu.search')}
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
className="chats-menu-search-clear"
|
||||
onClick={() => setSearch('')}
|
||||
aria-label="Clear search"
|
||||
aria-label={t('menu.clearSearch')}
|
||||
>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
@@ -174,7 +176,7 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
<div className="chats-menu-list" ref={listRef}>
|
||||
{filtered.length === 0 && (
|
||||
<div className="chats-menu-empty">
|
||||
{search ? 'No conversations match your search' : 'No conversations yet'}
|
||||
{search ? t('menu.noMatch') : t('menu.noConversations')}
|
||||
</div>
|
||||
)}
|
||||
{filtered.map((chat, idx) => (
|
||||
@@ -216,7 +218,7 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
<span className="chats-menu-item-time">{relativeTime(chat.updatedAt)}</span>
|
||||
</div>
|
||||
<span className="chats-menu-item-preview">
|
||||
{getLastMessagePreview(chat) || 'No messages yet'}
|
||||
{getLastMessagePreview(chat) || t('empty.noMessages')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -224,7 +226,7 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); startRename(chat.id, chat.name) }}
|
||||
title="Rename"
|
||||
title={t('menu.rename')}
|
||||
>
|
||||
<i className="fas fa-pen" />
|
||||
</button>
|
||||
@@ -232,7 +234,7 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onExport(chat) }}
|
||||
title="Export as Markdown"
|
||||
title={t('menu.exportMarkdown')}
|
||||
>
|
||||
<i className="fas fa-download" />
|
||||
</button>
|
||||
@@ -242,7 +244,7 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
type="button"
|
||||
className="chats-menu-item-delete"
|
||||
onClick={(e) => { e.stopPropagation(); onDelete?.(chat.id) }}
|
||||
title="Delete chat"
|
||||
title={t('menu.deleteChat')}
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
@@ -254,16 +256,16 @@ const ChatsMenu = forwardRef(function ChatsMenu({
|
||||
|
||||
<div className="chats-menu-footer">
|
||||
<button type="button" className="btn btn-primary btn-sm chats-menu-new" onClick={handleNew}>
|
||||
<i className="fas fa-plus" /> New chat
|
||||
<i className="fas fa-plus" /> {t('menu.newChat')}
|
||||
</button>
|
||||
{chats.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm chats-menu-clear-all"
|
||||
onClick={() => { onDeleteAll?.(); setOpen(false) }}
|
||||
title="Delete all conversations"
|
||||
title={t('menu.deleteAllTitle')}
|
||||
>
|
||||
<i className="fas fa-trash" /> Clear all
|
||||
<i className="fas fa-trash" /> {t('menu.clearAll')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ConfirmDialog({
|
||||
open,
|
||||
title = 'Confirm',
|
||||
title,
|
||||
message,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
confirmLabel,
|
||||
cancelLabel,
|
||||
danger = false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const titleText = title ?? t('actions.confirm')
|
||||
const confirmText = confirmLabel ?? t('actions.confirm')
|
||||
const cancelText = cancelLabel ?? t('actions.cancel')
|
||||
const dialogRef = useRef(null)
|
||||
const confirmRef = useRef(null)
|
||||
|
||||
@@ -69,19 +74,19 @@ export default function ConfirmDialog({
|
||||
>
|
||||
<div className="confirm-dialog-header">
|
||||
{danger && <i className="fas fa-exclamation-triangle confirm-dialog-danger-icon" />}
|
||||
<span id={titleId} className="confirm-dialog-title">{title}</span>
|
||||
<span id={titleId} className="confirm-dialog-title">{titleText}</span>
|
||||
</div>
|
||||
{message && <div id={bodyId} className="confirm-dialog-body">{message}</div>}
|
||||
<div className="confirm-dialog-actions">
|
||||
<button className="btn btn-secondary btn-sm" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
ref={confirmRef}
|
||||
className={`btn btn-sm ${danger ? 'btn-danger' : 'btn-primary'}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
74
core/http/react-ui/src/components/LanguageSwitcher.jsx
Normal file
74
core/http/react-ui/src/components/LanguageSwitcher.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SUPPORTED_LANGUAGES } from '../i18n'
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n, t } = useTranslation('nav')
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef(null)
|
||||
const current =
|
||||
SUPPORTED_LANGUAGES.find((l) => l.code === i18n.resolvedLanguage) ||
|
||||
SUPPORTED_LANGUAGES.find((l) => l.code === i18n.language) ||
|
||||
SUPPORTED_LANGUAGES[0]
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onDoc = (e) => {
|
||||
if (ref.current && !ref.current.contains(e.target)) setOpen(false)
|
||||
}
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDoc)
|
||||
document.removeEventListener('keydown', onKey)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const select = (code) => {
|
||||
i18n.changeLanguage(code)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const label = t('changeLanguage', { defaultValue: 'Change language' })
|
||||
|
||||
return (
|
||||
<div className="language-switcher" ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
className="theme-toggle language-switcher-trigger"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<i className="fas fa-globe" aria-hidden="true" />
|
||||
<span className="language-switcher-code">{current.flag}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<ul className="language-switcher-menu" role="listbox" aria-label={label}>
|
||||
{SUPPORTED_LANGUAGES.map((l) => (
|
||||
<li key={l.code}>
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={l.code === current.code}
|
||||
className={`language-switcher-option ${l.code === current.code ? 'active' : ''}`}
|
||||
onClick={() => select(l.code)}
|
||||
>
|
||||
<span className="language-switcher-flag">{l.flag}</span>
|
||||
<span className="language-switcher-name">{l.name}</span>
|
||||
{l.code === current.code && (
|
||||
<i className="fas fa-check language-switcher-check" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
import LanguageSwitcher from './LanguageSwitcher'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useBranding } from '../contexts/BrandingContext'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
@@ -9,37 +11,37 @@ const COLLAPSED_KEY = 'localai_sidebar_collapsed'
|
||||
const SECTIONS_KEY = 'localai_sidebar_sections'
|
||||
|
||||
const topItems = [
|
||||
{ path: '/app', icon: 'fas fa-home', label: 'Home' },
|
||||
{ path: '/app/models', icon: 'fas fa-download', label: 'Install Models', adminOnly: true },
|
||||
{ path: '/app/chat', icon: 'fas fa-comments', label: 'Chat' },
|
||||
{ path: '/app/studio', icon: 'fas fa-palette', label: 'Studio' },
|
||||
{ path: '/app/talk', icon: 'fas fa-phone', label: 'Talk' },
|
||||
{ path: '/app', icon: 'fas fa-home', labelKey: 'items.home' },
|
||||
{ path: '/app/models', icon: 'fas fa-download', labelKey: 'items.installModels', adminOnly: true },
|
||||
{ path: '/app/chat', icon: 'fas fa-comments', labelKey: 'items.chat' },
|
||||
{ path: '/app/studio', icon: 'fas fa-palette', labelKey: 'items.studio' },
|
||||
{ path: '/app/talk', icon: 'fas fa-phone', labelKey: 'items.talk' },
|
||||
]
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'tools',
|
||||
title: 'Tools',
|
||||
titleKey: 'sections.tools',
|
||||
items: [
|
||||
{ path: '/app/fine-tune', icon: 'fas fa-graduation-cap', label: 'Fine-Tune (Experimental)', feature: 'fine_tuning' },
|
||||
{ path: '/app/quantize', icon: 'fas fa-compress', label: 'Quantize (Experimental)', feature: 'quantization' },
|
||||
{ path: '/app/fine-tune', icon: 'fas fa-graduation-cap', labelKey: 'items.fineTune', feature: 'fine_tuning' },
|
||||
{ path: '/app/quantize', icon: 'fas fa-compress', labelKey: 'items.quantize', feature: 'quantization' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'biometrics',
|
||||
title: 'Biometrics',
|
||||
titleKey: 'sections.biometrics',
|
||||
featureMap: {
|
||||
'/app/face': 'face_recognition',
|
||||
'/app/voice': 'voice_recognition',
|
||||
},
|
||||
items: [
|
||||
{ path: '/app/face', icon: 'fas fa-face-smile', label: 'Face Recognition', feature: 'face_recognition' },
|
||||
{ path: '/app/voice', icon: 'fas fa-microphone-lines', label: 'Voice Recognition', feature: 'voice_recognition' },
|
||||
{ path: '/app/face', icon: 'fas fa-face-smile', labelKey: 'items.faceRecognition', feature: 'face_recognition' },
|
||||
{ path: '/app/voice', icon: 'fas fa-microphone-lines', labelKey: 'items.voiceRecognition', feature: 'voice_recognition' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Agents',
|
||||
titleKey: 'sections.agents',
|
||||
featureMap: {
|
||||
'/app/agents': 'agents',
|
||||
'/app/skills': 'skills',
|
||||
@@ -47,29 +49,31 @@ const sections = [
|
||||
'/app/agent-jobs': 'mcp_jobs',
|
||||
},
|
||||
items: [
|
||||
{ path: '/app/agents', icon: 'fas fa-robot', label: 'Agents' },
|
||||
{ path: '/app/skills', icon: 'fas fa-wand-magic-sparkles', label: 'Skills' },
|
||||
{ path: '/app/collections', icon: 'fas fa-database', label: 'Memory' },
|
||||
{ path: '/app/agent-jobs', icon: 'fas fa-tasks', label: 'MCP CI Jobs', feature: 'mcp' },
|
||||
{ path: '/app/agents', icon: 'fas fa-robot', labelKey: 'items.agents' },
|
||||
{ path: '/app/skills', icon: 'fas fa-wand-magic-sparkles', labelKey: 'items.skills' },
|
||||
{ path: '/app/collections', icon: 'fas fa-database', labelKey: 'items.memory' },
|
||||
{ path: '/app/agent-jobs', icon: 'fas fa-tasks', labelKey: 'items.mcpJobs', feature: 'mcp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: 'System',
|
||||
titleKey: 'sections.system',
|
||||
items: [
|
||||
{ path: '/app/usage', icon: 'fas fa-chart-bar', label: 'Usage', authOnly: true },
|
||||
{ path: '/app/users', icon: 'fas fa-users', label: 'Users', adminOnly: true, authOnly: true },
|
||||
{ path: '/app/backends', icon: 'fas fa-server', label: 'Backends', adminOnly: true },
|
||||
{ path: '/app/traces', icon: 'fas fa-chart-line', label: 'Traces', adminOnly: true },
|
||||
{ path: '/app/nodes', icon: 'fas fa-network-wired', label: 'Nodes', adminOnly: true, feature: 'distributed' },
|
||||
{ path: '/app/p2p', icon: 'fas fa-circle-nodes', label: 'Swarm', adminOnly: true },
|
||||
{ path: '/app/manage', icon: 'fas fa-desktop', label: 'System', adminOnly: true },
|
||||
{ path: '/app/settings', icon: 'fas fa-cog', label: 'Settings', adminOnly: true },
|
||||
{ path: '/app/usage', icon: 'fas fa-chart-bar', labelKey: 'items.usage', authOnly: true },
|
||||
{ path: '/app/users', icon: 'fas fa-users', labelKey: 'items.users', adminOnly: true, authOnly: true },
|
||||
{ path: '/app/backends', icon: 'fas fa-server', labelKey: 'items.backends', adminOnly: true },
|
||||
{ path: '/app/traces', icon: 'fas fa-chart-line', labelKey: 'items.traces', adminOnly: true },
|
||||
{ path: '/app/nodes', icon: 'fas fa-network-wired', labelKey: 'items.nodes', adminOnly: true, feature: 'distributed' },
|
||||
{ path: '/app/p2p', icon: 'fas fa-circle-nodes', labelKey: 'items.swarm', adminOnly: true },
|
||||
{ path: '/app/manage', icon: 'fas fa-desktop', labelKey: 'items.system', adminOnly: true },
|
||||
{ path: '/app/settings', icon: 'fas fa-cog', labelKey: 'items.settings', adminOnly: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function NavItem({ item, onClose, collapsed }) {
|
||||
const { t } = useTranslation('nav')
|
||||
const label = t(item.labelKey)
|
||||
return (
|
||||
<NavLink
|
||||
to={item.path}
|
||||
@@ -78,10 +82,10 @@ function NavItem({ item, onClose, collapsed }) {
|
||||
`nav-item ${isActive ? 'active' : ''}`
|
||||
}
|
||||
onClick={onClose}
|
||||
title={collapsed ? item.label : undefined}
|
||||
title={collapsed ? label : undefined}
|
||||
>
|
||||
<i className={`${item.icon} nav-icon`} />
|
||||
<span className="nav-label">{item.label}</span>
|
||||
<span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
@@ -100,6 +104,7 @@ function saveSectionState(state) {
|
||||
}
|
||||
|
||||
export default function Sidebar({ isOpen, onClose }) {
|
||||
const { t } = useTranslation('nav')
|
||||
const [features, setFeatures] = useState({})
|
||||
const [collapsed, setCollapsed] = useState(() => {
|
||||
try { return localStorage.getItem(COLLAPSED_KEY) === 'true' } catch (_) { return false }
|
||||
@@ -197,7 +202,7 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
<aside
|
||||
id="app-sidebar"
|
||||
className={`sidebar ${isOpen ? 'open' : ''} ${collapsed ? 'collapsed' : ''}`}
|
||||
aria-label="Primary navigation"
|
||||
aria-label={t('primaryNavigation')}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="sidebar-header">
|
||||
@@ -211,7 +216,7 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
ref={closeBtnRef}
|
||||
className="sidebar-close-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Close menu"
|
||||
aria-label={t('closeMenu')}
|
||||
>
|
||||
<i className="fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -236,15 +241,16 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
|
||||
const isSectionOpen = openSections[section.id]
|
||||
const showItems = isSectionOpen || collapsed
|
||||
const sectionTitle = t(section.titleKey)
|
||||
|
||||
return (
|
||||
<div key={section.id} className="sidebar-section">
|
||||
<button
|
||||
className={`sidebar-section-title sidebar-section-toggle ${isSectionOpen ? 'open' : ''}`}
|
||||
onClick={() => toggleSection(section.id)}
|
||||
title={collapsed ? section.title : undefined}
|
||||
title={collapsed ? sectionTitle : undefined}
|
||||
>
|
||||
<span>{section.title}</span>
|
||||
<span>{sectionTitle}</span>
|
||||
<i className="fas fa-chevron-right sidebar-section-chevron" />
|
||||
</button>
|
||||
{showItems && (
|
||||
@@ -255,10 +261,10 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="nav-item"
|
||||
title={collapsed ? 'API' : undefined}
|
||||
title={collapsed ? t('items.api') : undefined}
|
||||
>
|
||||
<i className="fas fa-code nav-icon" />
|
||||
<span className="nav-label">API</span>
|
||||
<span className="nav-label">{t('items.api')}</span>
|
||||
<i className="fas fa-external-link-alt nav-external" />
|
||||
</a>
|
||||
)}
|
||||
@@ -279,7 +285,7 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
<button
|
||||
className="sidebar-user-link"
|
||||
onClick={() => { navigate('/app/account'); onClose?.() }}
|
||||
title="Account settings"
|
||||
title={t('accountSettings')}
|
||||
>
|
||||
{user.avatarUrl ? (
|
||||
<img src={user.avatarUrl} alt="" className="sidebar-user-avatar" />
|
||||
@@ -288,16 +294,17 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
)}
|
||||
<span className="nav-label sidebar-user-name">{user.name || user.email}</span>
|
||||
</button>
|
||||
<button className="sidebar-logout-btn" onClick={logout} title="Logout">
|
||||
<button className="sidebar-logout-btn" onClick={logout} title={t('logout')}>
|
||||
<i className="fas fa-sign-out-alt" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<LanguageSwitcher />
|
||||
<ThemeToggle />
|
||||
<button
|
||||
className="sidebar-collapse-btn"
|
||||
onClick={toggleCollapse}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
title={collapsed ? t('expandSidebar') : t('collapseSidebar')}
|
||||
>
|
||||
<i className={`fas fa-chevron-${collapsed ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
let toastId = 0
|
||||
|
||||
@@ -54,6 +55,7 @@ export function ToastContainer({ toasts, removeToast }) {
|
||||
}
|
||||
|
||||
function ToastItem({ toast, onRemove }) {
|
||||
const { t } = useTranslation('common')
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,7 +74,11 @@ function ToastItem({ toast, onRemove }) {
|
||||
{toast.link && (
|
||||
<a href={toast.link.href} className="toast-link">{toast.link.text}</a>
|
||||
)}
|
||||
<button onClick={() => onRemove(toast.id)} className="toast-close" aria-label="Dismiss notification">
|
||||
<button
|
||||
onClick={() => onRemove(toast.id)}
|
||||
className="toast-close"
|
||||
aria-label={t('actions.close')}
|
||||
>
|
||||
<i className="fas fa-xmark" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
63
core/http/react-ui/src/i18n/index.js
vendored
Normal file
63
core/http/react-ui/src/i18n/index.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import i18n from 'i18next'
|
||||
import HttpBackend from 'i18next-http-backend'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
{ code: 'en', name: 'English', flag: 'EN' },
|
||||
{ code: 'it', name: 'Italiano', flag: 'IT' },
|
||||
{ code: 'es', name: 'Español', flag: 'ES' },
|
||||
{ code: 'de', name: 'Deutsch', flag: 'DE' },
|
||||
{ code: 'zh-CN', name: '简体中文', flag: 'ZH' },
|
||||
]
|
||||
|
||||
export const NAMESPACES = [
|
||||
'common',
|
||||
'nav',
|
||||
'errors',
|
||||
'auth',
|
||||
'home',
|
||||
'chat',
|
||||
'studio',
|
||||
'models',
|
||||
'agents',
|
||||
'skills',
|
||||
'collections',
|
||||
'biometrics',
|
||||
'media',
|
||||
'tools',
|
||||
'admin',
|
||||
'usage',
|
||||
'explorer',
|
||||
]
|
||||
|
||||
i18n
|
||||
.use(HttpBackend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: SUPPORTED_LANGUAGES.map((l) => l.code),
|
||||
ns: ['common', 'nav', 'errors'],
|
||||
defaultNS: 'common',
|
||||
debug: import.meta.env.DEV,
|
||||
interpolation: { escapeValue: false },
|
||||
backend: {
|
||||
loadPath: apiUrl('/locales/{{lng}}/{{ns}}.json'),
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||
lookupLocalStorage: 'localai-language',
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
react: {
|
||||
useSuspense: true,
|
||||
},
|
||||
})
|
||||
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
document.documentElement.setAttribute('lang', lng)
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,26 +1,37 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { StrictMode, Suspense } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import { BrandingProvider } from './contexts/BrandingContext'
|
||||
import { AuthProvider } from './context/AuthContext'
|
||||
import { router } from './router'
|
||||
import './i18n'
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import './index.css'
|
||||
import './theme.css'
|
||||
import './App.css'
|
||||
|
||||
function BootFallback() {
|
||||
return (
|
||||
<div className="app-boot-spinner" role="status" aria-label="Loading">
|
||||
<div className="app-boot-spinner-dot" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// BrandingProvider sits outside AuthProvider so the login screen — which
|
||||
// renders before authentication completes — can pick up the configured
|
||||
// instance name and logo from the public /api/branding endpoint.
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<BrandingProvider>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</BrandingProvider>
|
||||
</ThemeProvider>
|
||||
<Suspense fallback={<BootFallback />}>
|
||||
<ThemeProvider>
|
||||
<BrandingProvider>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</BrandingProvider>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { apiKeysApi, profileApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -13,12 +14,13 @@ function formatDate(d) {
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 'profile', icon: 'fa-user', label: 'Profile' },
|
||||
{ id: 'security', icon: 'fa-lock', label: 'Security' },
|
||||
{ id: 'apikeys', icon: 'fa-key', label: 'API Keys' },
|
||||
{ id: 'profile', icon: 'fa-user', labelKey: 'account.tabs.profile' },
|
||||
{ id: 'security', icon: 'fa-lock', labelKey: 'account.tabs.security' },
|
||||
{ id: 'apikeys', icon: 'fa-key', labelKey: 'account.tabs.apiKeys' },
|
||||
]
|
||||
|
||||
function ProfileTab({ addToast }) {
|
||||
const { t } = useTranslation('auth')
|
||||
const { user, refresh } = useAuth()
|
||||
const [name, setName] = useState(user?.name || '')
|
||||
const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || '')
|
||||
@@ -35,10 +37,10 @@ function ProfileTab({ addToast }) {
|
||||
setSaving(true)
|
||||
try {
|
||||
await profileApi.updateProfile(name.trim(), avatarUrl.trim())
|
||||
addToast('Profile updated', 'success')
|
||||
addToast(t('account.profile.updated'), 'success')
|
||||
refresh()
|
||||
} catch (err) {
|
||||
addToast(`Failed to update profile: ${err.message}`, 'error')
|
||||
addToast(t('account.profile.updateFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -71,7 +73,7 @@ function ProfileTab({ addToast }) {
|
||||
{/* Profile form */}
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="card">
|
||||
<SettingRow label="Display name" description="Your public display name">
|
||||
<SettingRow label={t('account.profile.displayName')} description={t('account.profile.displayNameDescription')}>
|
||||
<input
|
||||
type="text"
|
||||
className="input account-input-sm"
|
||||
@@ -81,7 +83,7 @@ function ProfileTab({ addToast }) {
|
||||
maxLength={100}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="Avatar URL" description="URL to your profile picture">
|
||||
<SettingRow label={t('account.profile.avatarUrl')} description={t('account.profile.avatarUrlDescription')}>
|
||||
<div className="account-input-row">
|
||||
<input
|
||||
type="url"
|
||||
@@ -90,7 +92,7 @@ function ProfileTab({ addToast }) {
|
||||
onChange={(e) => setAvatarUrl(e.target.value)}
|
||||
disabled={saving}
|
||||
maxLength={512}
|
||||
placeholder="https://example.com/avatar.png"
|
||||
placeholder={t('account.profile.avatarUrlPlaceholder')}
|
||||
/>
|
||||
{avatarUrl.trim() && (
|
||||
<img
|
||||
@@ -110,7 +112,9 @@ function ProfileTab({ addToast }) {
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={saving || !name.trim() || !hasChanges}
|
||||
>
|
||||
{saving ? <><LoadingSpinner size="sm" /> Saving...</> : <><i className="fas fa-save" /> Save</>}
|
||||
{saving
|
||||
? <><LoadingSpinner size="sm" /> {t('account.profile.saving')}</>
|
||||
: <><i className="fas fa-save" /> {t('account.profile.save')}</>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -119,6 +123,7 @@ function ProfileTab({ addToast }) {
|
||||
}
|
||||
|
||||
function SecurityTab({ addToast }) {
|
||||
const { t } = useTranslation('auth')
|
||||
const { user } = useAuth()
|
||||
const isLocal = user?.provider === 'local'
|
||||
|
||||
@@ -130,17 +135,17 @@ function SecurityTab({ addToast }) {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (newPw !== confirmPw) {
|
||||
addToast('Passwords do not match', 'error')
|
||||
addToast(t('account.security.passwordsDoNotMatch'), 'error')
|
||||
return
|
||||
}
|
||||
if (newPw.length < 8) {
|
||||
addToast('New password must be at least 8 characters', 'error')
|
||||
addToast(t('account.security.tooShort'), 'error')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
await profileApi.changePassword(currentPw, newPw)
|
||||
addToast('Password changed', 'success')
|
||||
addToast(t('account.security.changed'), 'success')
|
||||
setCurrentPw('')
|
||||
setNewPw('')
|
||||
setConfirmPw('')
|
||||
@@ -156,7 +161,7 @@ function SecurityTab({ addToast }) {
|
||||
<div className="card empty-icon-block">
|
||||
<i className="fas fa-shield-halved" />
|
||||
<div className="empty-icon-block-text">
|
||||
Password management is not available for {user?.provider || 'OAuth'} accounts.
|
||||
{t('account.security.oauthOnly', { provider: user?.provider || 'OAuth' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -165,36 +170,36 @@ function SecurityTab({ addToast }) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="card">
|
||||
<SettingRow label="Current password" description="Enter your existing password to verify your identity">
|
||||
<SettingRow label={t('account.security.currentPassword')} description={t('account.security.currentPasswordDescription')}>
|
||||
<input
|
||||
type="password"
|
||||
className="input account-input-sm"
|
||||
value={currentPw}
|
||||
onChange={(e) => setCurrentPw(e.target.value)}
|
||||
placeholder="Current password"
|
||||
placeholder={t('account.security.currentPasswordPlaceholder')}
|
||||
disabled={saving}
|
||||
required
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="New password" description="Must be at least 8 characters">
|
||||
<SettingRow label={t('account.security.newPassword')} description={t('account.security.newPasswordDescription')}>
|
||||
<input
|
||||
type="password"
|
||||
className="input account-input-sm"
|
||||
value={newPw}
|
||||
onChange={(e) => setNewPw(e.target.value)}
|
||||
placeholder="New password"
|
||||
placeholder={t('account.security.newPasswordPlaceholder')}
|
||||
minLength={8}
|
||||
disabled={saving}
|
||||
required
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="Confirm password" description="Re-enter your new password">
|
||||
<SettingRow label={t('account.security.confirmPassword')} description={t('account.security.confirmPasswordDescription')}>
|
||||
<input
|
||||
type="password"
|
||||
className="input account-input-sm"
|
||||
value={confirmPw}
|
||||
onChange={(e) => setConfirmPw(e.target.value)}
|
||||
placeholder="Confirm new password"
|
||||
placeholder={t('account.security.confirmPasswordPlaceholder')}
|
||||
disabled={saving}
|
||||
required
|
||||
/>
|
||||
@@ -206,7 +211,9 @@ function SecurityTab({ addToast }) {
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={saving || !currentPw || !newPw || !confirmPw}
|
||||
>
|
||||
{saving ? <><LoadingSpinner size="sm" /> Changing...</> : 'Change password'}
|
||||
{saving
|
||||
? <><LoadingSpinner size="sm" /> {t('account.security.changing')}</>
|
||||
: t('account.security.changePassword')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -214,6 +221,7 @@ function SecurityTab({ addToast }) {
|
||||
}
|
||||
|
||||
function ApiKeysTab({ addToast }) {
|
||||
const { t } = useTranslation('auth')
|
||||
const [keys, setKeys] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [creating, setCreating] = useState(false)
|
||||
@@ -228,11 +236,11 @@ function ApiKeysTab({ addToast }) {
|
||||
const data = await apiKeysApi.list()
|
||||
setKeys(data.keys || [])
|
||||
} catch (err) {
|
||||
addToast(`Failed to load API keys: ${err.message}`, 'error')
|
||||
addToast(t('account.apiKeys.loadFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [addToast])
|
||||
}, [addToast, t])
|
||||
|
||||
useEffect(() => { fetchKeys() }, [fetchKeys])
|
||||
|
||||
@@ -245,9 +253,9 @@ function ApiKeysTab({ addToast }) {
|
||||
setNewKeyPlaintext(data.key)
|
||||
setNewKeyName('')
|
||||
await fetchKeys()
|
||||
addToast('API key created', 'success')
|
||||
addToast(t('account.apiKeys.createdToast'), 'success')
|
||||
} catch (err) {
|
||||
addToast(`Failed to create API key: ${err.message}`, 'error')
|
||||
addToast(t('account.apiKeys.createFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
@@ -255,9 +263,9 @@ function ApiKeysTab({ addToast }) {
|
||||
|
||||
const handleRevoke = async (id, name) => {
|
||||
setConfirmDialog({
|
||||
title: 'Revoke API Key',
|
||||
message: `Revoke API key "${name}"? This cannot be undone.`,
|
||||
confirmLabel: 'Revoke',
|
||||
title: t('account.apiKeys.revokeTitle'),
|
||||
message: t('account.apiKeys.revokeMessage', { name }),
|
||||
confirmLabel: t('account.apiKeys.revoke'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
@@ -265,9 +273,9 @@ function ApiKeysTab({ addToast }) {
|
||||
try {
|
||||
await apiKeysApi.revoke(id)
|
||||
setKeys(prev => prev.filter(k => k.id !== id))
|
||||
addToast('API key revoked', 'success')
|
||||
addToast(t('account.apiKeys.revoked'), 'success')
|
||||
} catch (err) {
|
||||
addToast(`Failed to revoke API key: ${err.message}`, 'error')
|
||||
addToast(t('account.apiKeys.revokeFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setRevokingId(null)
|
||||
}
|
||||
@@ -278,7 +286,7 @@ function ApiKeysTab({ addToast }) {
|
||||
const copyToClipboard = (text) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => addToast('Copied to clipboard', 'success'),
|
||||
() => addToast(t('account.apiKeys.copiedToast'), 'success'),
|
||||
() => fallbackCopy(text),
|
||||
)
|
||||
} else {
|
||||
@@ -295,9 +303,9 @@ function ApiKeysTab({ addToast }) {
|
||||
ta.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
addToast('Copied to clipboard', 'success')
|
||||
addToast(t('account.apiKeys.copiedToast'), 'success')
|
||||
} catch (_) {
|
||||
addToast('Failed to copy', 'error')
|
||||
addToast(t('account.apiKeys.copyFailed'), 'error')
|
||||
}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
@@ -307,19 +315,19 @@ function ApiKeysTab({ addToast }) {
|
||||
{/* Create key form */}
|
||||
<div className="card" style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<form onSubmit={handleCreate}>
|
||||
<SettingRow label="Create API key" description="Generate a key for programmatic access">
|
||||
<SettingRow label={t('account.apiKeys.create')} description={t('account.apiKeys.createDescription')}>
|
||||
<div className="account-input-row">
|
||||
<input
|
||||
type="text"
|
||||
className="input account-input-xs"
|
||||
placeholder="Key name (e.g. my-app)"
|
||||
placeholder={t('account.apiKeys.namePlaceholder')}
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
disabled={creating}
|
||||
maxLength={64}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary btn-sm" disabled={creating || !newKeyName.trim()}>
|
||||
{creating ? <LoadingSpinner size="sm" /> : <><i className="fas fa-plus" /> Create</>}
|
||||
{creating ? <LoadingSpinner size="sm" /> : <><i className="fas fa-plus" /> {t('account.apiKeys.createButton')}</>}
|
||||
</button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
@@ -331,7 +339,7 @@ function ApiKeysTab({ addToast }) {
|
||||
<div className="new-key-banner">
|
||||
<div className="new-key-banner-header">
|
||||
<i className="fas fa-triangle-exclamation" />
|
||||
Copy now — this key won't be shown again
|
||||
{t('account.apiKeys.copyNow')}
|
||||
</div>
|
||||
<div className="new-key-banner-body">
|
||||
<code className="new-key-value">
|
||||
@@ -356,7 +364,7 @@ function ApiKeysTab({ addToast }) {
|
||||
<div className="card empty-icon-block">
|
||||
<i className="fas fa-key" />
|
||||
<div className="empty-icon-block-text">
|
||||
No API keys yet. Create one above to get programmatic access.
|
||||
{t('account.apiKeys.empty')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -368,14 +376,14 @@ function ApiKeysTab({ addToast }) {
|
||||
<div className="apikey-name">{k.name}</div>
|
||||
<div className="apikey-details">
|
||||
{k.keyPrefix}... · {formatDate(k.createdAt)}
|
||||
{k.lastUsed && <> · last used {formatDate(k.lastUsed)}</>}
|
||||
{k.lastUsed && <> · {t('account.apiKeys.lastUsed', { date: formatDate(k.lastUsed) })}</>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm apikey-revoke-btn"
|
||||
onClick={() => handleRevoke(k.id, k.name)}
|
||||
disabled={revokingId === k.id}
|
||||
title="Revoke key"
|
||||
title={t('account.apiKeys.revokeKey')}
|
||||
>
|
||||
{revokingId === k.id ? <LoadingSpinner size="sm" /> : <i className="fas fa-trash" />}
|
||||
</button>
|
||||
@@ -398,6 +406,7 @@ function ApiKeysTab({ addToast }) {
|
||||
|
||||
export default function Account() {
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('auth')
|
||||
const { authEnabled, user } = useAuth()
|
||||
const [activeTab, setActiveTab] = useState('profile')
|
||||
|
||||
@@ -406,8 +415,8 @@ export default function Account() {
|
||||
<div className="page page--narrow">
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-user-gear" /></div>
|
||||
<h2 className="empty-state-title">Account unavailable</h2>
|
||||
<p className="empty-state-text">Authentication must be enabled to manage your account.</p>
|
||||
<h2 className="empty-state-title">{t('account.unavailable')}</h2>
|
||||
<p className="empty-state-text">{t('account.unavailableText')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -415,14 +424,14 @@ export default function Account() {
|
||||
|
||||
// Filter tabs: hide security tab for OAuth-only users
|
||||
const isLocal = user?.provider === 'local'
|
||||
const visibleTabs = isLocal ? TABS : TABS.filter(t => t.id !== 'security')
|
||||
const visibleTabs = isLocal ? TABS : TABS.filter(tab => tab.id !== 'security')
|
||||
|
||||
return (
|
||||
<div className="page page--narrow account-page">
|
||||
{/* Header */}
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Account</h1>
|
||||
<p className="page-subtitle">Profile, credentials, and API keys</p>
|
||||
<h1 className="page-title">{t('account.title')}</h1>
|
||||
<p className="page-subtitle">{t('account.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
@@ -434,7 +443,7 @@ export default function Account() {
|
||||
className={`auth-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
>
|
||||
<i className={`fas ${tab.icon} auth-tab-icon`} />
|
||||
{tab.label}
|
||||
{t(tab.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { agentsApi } from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useUserMap } from '../hooks/useUserMap'
|
||||
@@ -9,6 +10,7 @@ import ConfirmDialog from '../components/ConfirmDialog'
|
||||
export default function Agents() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('agents')
|
||||
const { isAdmin, authEnabled, user } = useAuth()
|
||||
const userMap = useUserMap()
|
||||
const [agents, setAgents] = useState([])
|
||||
@@ -25,7 +27,7 @@ export default function Agents() {
|
||||
const statuses = data.statuses || {}
|
||||
if (data.agent_hub_url) setAgentHubURL(data.agent_hub_url)
|
||||
setUserGroups(data.user_groups || null)
|
||||
|
||||
|
||||
// Fetch observable counts for each agent
|
||||
const agentsWithCounts = await Promise.all(
|
||||
names.map(async (name) => {
|
||||
@@ -45,11 +47,11 @@ export default function Agents() {
|
||||
)
|
||||
setAgents(agentsWithCounts)
|
||||
} catch (err) {
|
||||
addToast(`Failed to load agents: ${err.message}`, 'error')
|
||||
addToast(t('toasts.loadFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [addToast, isAdmin, authEnabled])
|
||||
}, [addToast, isAdmin, authEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents()
|
||||
@@ -65,18 +67,18 @@ export default function Agents() {
|
||||
|
||||
const handleDelete = (name, userId) => {
|
||||
setConfirmDialog({
|
||||
title: 'Delete Agent',
|
||||
message: `Delete agent "${name}"? This action cannot be undone.`,
|
||||
confirmLabel: 'Delete',
|
||||
title: t('deleteDialog.title'),
|
||||
message: t('deleteDialog.message', { name }),
|
||||
confirmLabel: t('deleteDialog.confirm'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await agentsApi.delete(name, userId)
|
||||
addToast(`Agent "${name}" deleted`, 'success')
|
||||
addToast(t('toasts.deleted', { name }), 'success')
|
||||
fetchAgents()
|
||||
} catch (err) {
|
||||
addToast(`Failed to delete agent: ${err.message}`, 'error')
|
||||
addToast(t('toasts.deleteFailed', { message: err.message }), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -88,14 +90,14 @@ export default function Agents() {
|
||||
try {
|
||||
if (isActive) {
|
||||
await agentsApi.pause(name, userId)
|
||||
addToast(`Agent "${name}" paused`, 'success')
|
||||
addToast(t('toasts.paused', { name }), 'success')
|
||||
} else {
|
||||
await agentsApi.resume(name, userId)
|
||||
addToast(`Agent "${name}" resumed`, 'success')
|
||||
addToast(t('toasts.resumed', { name }), 'success')
|
||||
}
|
||||
fetchAgents()
|
||||
} catch (err) {
|
||||
addToast(`Failed to ${isActive ? 'pause' : 'resume'} agent: ${err.message}`, 'error')
|
||||
addToast(t(isActive ? 'toasts.pauseFailed' : 'toasts.resumeFailed', { message: err.message }), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +113,9 @@ export default function Agents() {
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
addToast(`Agent "${name}" exported`, 'success')
|
||||
addToast(t('toasts.exported', { name }), 'success')
|
||||
} catch (err) {
|
||||
addToast(`Failed to export agent: ${err.message}`, 'error')
|
||||
addToast(t('toasts.exportFailed', { message: err.message }), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ export default function Agents() {
|
||||
const config = JSON.parse(text)
|
||||
navigate('/app/agents/new', { state: { importedConfig: config } })
|
||||
} catch (err) {
|
||||
addToast(`Failed to parse agent file: ${err.message}`, 'error')
|
||||
addToast(t('toasts.parseFailed', { message: err.message }), 'error')
|
||||
}
|
||||
e.target.value = ''
|
||||
}
|
||||
@@ -181,21 +183,21 @@ export default function Agents() {
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Agents</h1>
|
||||
<p className="page-subtitle">Manage autonomous AI agents</p>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('subtitle')}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', alignItems: 'center' }}>
|
||||
{agentHubURL && (
|
||||
<a className="btn btn-secondary" href={agentHubURL} target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-store" /> Agent Hub
|
||||
<i className="fas fa-store" /> {t('actions.agentHub')}
|
||||
</a>
|
||||
)}
|
||||
<label className="btn btn-secondary">
|
||||
<i className="fas fa-file-import" /> Import
|
||||
<i className="fas fa-file-import" /> {t('actions.import')}
|
||||
<input type="file" accept=".json" className="agents-import-input" onChange={handleImport} />
|
||||
</label>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/agents/new')}>
|
||||
<i className="fas fa-plus" /> Create Agent
|
||||
<i className="fas fa-plus" /> {t('actions.createAgent')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,62 +209,68 @@ export default function Agents() {
|
||||
) : agents.length === 0 && !userGroups ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-robot" /></div>
|
||||
<h2 className="empty-state-title">No agents configured</h2>
|
||||
<p className="empty-state-text">Create an agent to get started with autonomous AI workflows.</p>
|
||||
<h2 className="empty-state-title">{t('empty.noConfigured')}</h2>
|
||||
<p className="empty-state-text">{t('empty.noConfiguredText')}</p>
|
||||
{agentHubURL && (
|
||||
<p className="empty-state-text">
|
||||
Don't know where to start? Browse the <a href={agentHubURL} target="_blank" rel="noopener noreferrer">Agent Hub</a> to find ready-made agent configurations you can import.
|
||||
<Trans
|
||||
i18nKey="agents:empty.browseHub"
|
||||
values={{}}
|
||||
components={{
|
||||
1: <a href={agentHubURL} target="_blank" rel="noopener noreferrer" />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/agents/new')}>
|
||||
<i className="fas fa-plus" /> Create Agent
|
||||
<i className="fas fa-plus" /> {t('actions.createAgent')}
|
||||
</button>
|
||||
<label className="btn btn-secondary">
|
||||
<i className="fas fa-file-import" /> Import
|
||||
<i className="fas fa-file-import" /> {t('actions.import')}
|
||||
<input type="file" accept=".json" className="agents-import-input" onChange={handleImport} />
|
||||
</label>
|
||||
{agentHubURL && (
|
||||
<a className="btn btn-secondary" href={agentHubURL} target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-store" /> Agent Hub
|
||||
<i className="fas fa-store" /> {t('actions.agentHub')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{userGroups && <h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>Your Agents</h2>}
|
||||
{userGroups && <h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>{t('sections.yourAgents')}</h2>}
|
||||
<div className="agents-toolbar">
|
||||
<div className="agents-search">
|
||||
<i className="fas fa-search" />
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Search agents..."
|
||||
placeholder={t('search.placeholder')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
|
||||
{filtered.length} of {agents.length} agent{agents.length !== 1 ? 's' : ''}
|
||||
{t('search.summary', { shown: filtered.length, total: agents.length, count: agents.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-search" /></div>
|
||||
<h2 className="empty-state-title">No matching agents</h2>
|
||||
<p className="empty-state-text">No agents match "{search}"</p>
|
||||
<h2 className="empty-state-title">{t('empty.noMatching')}</h2>
|
||||
<p className="empty-state-text">{t('empty.noMatchingText', { query: search })}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-container">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Events</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
<th>{t('table.name')}</th>
|
||||
<th>{t('table.status')}</th>
|
||||
<th>{t('table.events')}</th>
|
||||
<th style={{ textAlign: 'right' }}>{t('table.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -281,7 +289,7 @@ export default function Agents() {
|
||||
<a
|
||||
className="agents-name"
|
||||
onClick={() => navigate(`/app/agents/${encodeURIComponent(name)}/status`)}
|
||||
title={`${agent.eventsCount} events - Click to view`}
|
||||
title={t('table.eventsTooltip', { count: agent.eventsCount })}
|
||||
>
|
||||
{agent.eventsCount}
|
||||
</a>
|
||||
@@ -291,35 +299,35 @@ export default function Agents() {
|
||||
<button
|
||||
className={`btn btn-sm ${isActive ? 'btn-warning' : 'btn-success'}`}
|
||||
onClick={() => handlePauseResume(agent)}
|
||||
title={isActive ? 'Pause' : 'Resume'}
|
||||
title={isActive ? t('actions.pause') : t('actions.resume')}
|
||||
>
|
||||
<i className={`fas ${isActive ? 'fa-pause' : 'fa-play'}`} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/agents/${encodeURIComponent(name)}/edit`)}
|
||||
title="Edit"
|
||||
title={t('actions.edit')}
|
||||
>
|
||||
<i className="fas fa-edit" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/agents/${encodeURIComponent(name)}/chat`)}
|
||||
title="Chat"
|
||||
title={t('actions.chat')}
|
||||
>
|
||||
<i className="fas fa-comment" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleExport(name)}
|
||||
title="Export"
|
||||
title={t('actions.export')}
|
||||
>
|
||||
<i className="fas fa-download" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleDelete(name)}
|
||||
title="Delete"
|
||||
title={t('actions.delete')}
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
@@ -338,7 +346,7 @@ export default function Agents() {
|
||||
|
||||
{userGroups && (
|
||||
<UserGroupSection
|
||||
title="Other Users' Agents"
|
||||
title={t('sections.otherUsersAgents')}
|
||||
userGroups={userGroups}
|
||||
userMap={userMap}
|
||||
currentUserId={user?.id}
|
||||
@@ -348,9 +356,9 @@ export default function Agents() {
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
<th>{t('table.name')}</th>
|
||||
<th>{t('table.status')}</th>
|
||||
<th style={{ textAlign: 'right' }}>{t('table.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -369,35 +377,35 @@ export default function Agents() {
|
||||
<button
|
||||
className={`btn btn-sm ${isActive ? 'btn-warning' : 'btn-success'}`}
|
||||
onClick={() => handlePauseResume(a, userId)}
|
||||
title={isActive ? 'Pause' : 'Resume'}
|
||||
title={isActive ? t('actions.pause') : t('actions.resume')}
|
||||
>
|
||||
<i className={`fas ${isActive ? 'fa-pause' : 'fa-play'}`} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/agents/${encodeURIComponent(a.name)}/edit?user_id=${encodeURIComponent(userId)}`)}
|
||||
title="Edit"
|
||||
title={t('actions.edit')}
|
||||
>
|
||||
<i className="fas fa-edit" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/agents/${encodeURIComponent(a.name)}/chat?user_id=${encodeURIComponent(userId)}`)}
|
||||
title="Chat"
|
||||
title={t('actions.chat')}
|
||||
>
|
||||
<i className="fas fa-comment" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleExport(a.name, userId)}
|
||||
title="Export"
|
||||
title={t('actions.export')}
|
||||
>
|
||||
<i className="fas fa-download" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleDelete(a.name, userId)}
|
||||
title="Delete"
|
||||
title={t('actions.delete')}
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { backendsApi, nodesApi } from '../utils/api'
|
||||
import { useDebouncedCallback } from '../hooks/useDebounce'
|
||||
import React from 'react'
|
||||
@@ -16,6 +17,7 @@ import Popover from '../components/Popover'
|
||||
export default function Backends() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('admin')
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const { operations } = useOperations()
|
||||
const { enabled: distributedEnabled, nodes: clusterNodes, refetch: refetchNodes } = useDistributedMode()
|
||||
@@ -344,8 +346,8 @@ export default function Backends() {
|
||||
{/* Header */}
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Backend Management</h1>
|
||||
<p className="page-subtitle">Discover and install AI backends to power your models</p>
|
||||
<h1 className="page-title">{t('backends.title')}</h1>
|
||||
<p className="page-subtitle">{t('backends.subtitle')}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', fontSize: '0.8125rem' }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||
import { useParams, useOutletContext, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChat } from '../hooks/useChat'
|
||||
import ModelSelector from '../components/ModelSelector'
|
||||
import { renderMarkdown, highlightAll } from '../utils/markdown'
|
||||
@@ -89,6 +90,7 @@ function ToolParams({ entries, fallback }) {
|
||||
}
|
||||
|
||||
function ActivityGroup({ items, updateChatSettings, activeChat, getClientForTool }) {
|
||||
const { t } = useTranslation('chat')
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const contentRef = useRef(null)
|
||||
|
||||
@@ -103,12 +105,12 @@ function ActivityGroup({ items, updateChatSettings, activeChat, getClientForTool
|
||||
const regularItems = items.filter(item => !(item.role === 'tool_result' && item.appUI))
|
||||
|
||||
const labels = regularItems.map(item => {
|
||||
if (item.role === 'thinking' || item.role === 'reasoning') return 'Thought'
|
||||
if (item.role === 'thinking' || item.role === 'reasoning') return t('activity.thought')
|
||||
if (item.role === 'tool_call') {
|
||||
try { return JSON.parse(item.content)?.name || 'Tool' } catch (_e) { return 'Tool' }
|
||||
try { return JSON.parse(item.content)?.name || t('activity.tool') } catch (_e) { return t('activity.tool') }
|
||||
}
|
||||
if (item.role === 'tool_result') {
|
||||
try { return `${JSON.parse(item.content)?.name || 'Tool'} result` } catch (_e) { return 'Result' }
|
||||
try { return t('activity.toolResult', { name: JSON.parse(item.content)?.name || t('activity.tool') }) } catch (_e) { return t('activity.result') }
|
||||
}
|
||||
return item.role
|
||||
})
|
||||
@@ -132,7 +134,7 @@ function ActivityGroup({ items, updateChatSettings, activeChat, getClientForTool
|
||||
if (item.role === 'thinking' || item.role === 'reasoning') {
|
||||
return (
|
||||
<div key={idx} className="chat-activity-item chat-activity-thinking">
|
||||
<span className="chat-activity-item-label">Thought</span>
|
||||
<span className="chat-activity-item-label">{t('activity.thought')}</span>
|
||||
<div className="chat-activity-item-content"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(item.content || '') }} />
|
||||
</div>
|
||||
@@ -176,6 +178,7 @@ function ActivityGroup({ items, updateChatSettings, activeChat, getClientForTool
|
||||
}
|
||||
|
||||
function StreamingActivity({ reasoning, toolCalls, hasResponse }) {
|
||||
const { t } = useTranslation('chat')
|
||||
const hasContent = reasoning || (toolCalls && toolCalls.length > 0)
|
||||
if (!hasContent) return null
|
||||
|
||||
@@ -200,9 +203,9 @@ function StreamingActivity({ reasoning, toolCalls, hasResponse }) {
|
||||
|
||||
const lastTool = toolCalls && toolCalls.length > 0 ? toolCalls[toolCalls.length - 1] : null
|
||||
const label = reasoning
|
||||
? 'Thinking...'
|
||||
? t('activity.thinking')
|
||||
: lastTool
|
||||
? (lastTool.type === 'tool_call' ? lastTool.name : `${lastTool.name} result`)
|
||||
? (lastTool.type === 'tool_call' ? lastTool.name : t('activity.toolResult', { name: lastTool.name }))
|
||||
: ''
|
||||
|
||||
return (
|
||||
@@ -231,7 +234,7 @@ function StreamingActivity({ reasoning, toolCalls, hasResponse }) {
|
||||
if (tc.type === 'tool_result') {
|
||||
return (
|
||||
<div key={idx} className="chat-activity-item chat-activity-tool-result">
|
||||
<span className="chat-activity-item-label">{tc.name} result</span>
|
||||
<span className="chat-activity-item-label">{t('activity.toolResult', { name: tc.name })}</span>
|
||||
<div className="chat-activity-item-content"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(tc.result || '') }} />
|
||||
</div>
|
||||
@@ -278,6 +281,7 @@ export default function Chat() {
|
||||
const { model: urlModel } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('chat')
|
||||
const { isAdmin } = useAuth()
|
||||
const { operations } = useOperations()
|
||||
const {
|
||||
@@ -719,7 +723,7 @@ export default function Chat() {
|
||||
const msg = input.trim()
|
||||
if (!msg && files.length === 0) return
|
||||
if (!activeChat?.model) {
|
||||
addToast('Please select a model', 'warning')
|
||||
addToast(t('toasts.selectModel'), 'warning')
|
||||
return
|
||||
}
|
||||
setInput('')
|
||||
@@ -797,7 +801,7 @@ export default function Chat() {
|
||||
const copyMessage = (content) => {
|
||||
const text = typeof content === 'string' ? content : content?.[0]?.text || ''
|
||||
navigator.clipboard.writeText(text)
|
||||
addToast('Copied to clipboard', 'success', 2000)
|
||||
addToast(t('toasts.copied'), 'success', 2000)
|
||||
}
|
||||
|
||||
const contextPercent = getContextUsagePercent()
|
||||
@@ -809,9 +813,9 @@ export default function Chat() {
|
||||
.slice(0, 4)
|
||||
|
||||
const promptDeleteAll = () => setConfirmDialog({
|
||||
title: 'Delete All Chats',
|
||||
message: 'Delete all chats? This cannot be undone.',
|
||||
confirmLabel: 'Delete all',
|
||||
title: t('deleteAllDialog.title'),
|
||||
message: t('deleteAllDialog.message'),
|
||||
confirmLabel: t('deleteAllDialog.confirm'),
|
||||
danger: true,
|
||||
onConfirm: () => { setConfirmDialog(null); deleteAllChats() },
|
||||
})
|
||||
@@ -845,7 +849,7 @@ export default function Chat() {
|
||||
{activeChat.localaiAssistant && (
|
||||
<span
|
||||
className="chat-header-shield"
|
||||
title="This chat can install models, edit configs and manage backends by talking to LocalAI."
|
||||
title={t('header.manageModeTooltip')}
|
||||
>
|
||||
<i className="fas fa-user-shield" />
|
||||
</span>
|
||||
@@ -863,7 +867,7 @@ export default function Chat() {
|
||||
type="button"
|
||||
className={`btn btn-secondary btn-sm${showModelInfo ? ' active' : ''}`}
|
||||
onClick={() => setShowModelInfo(prev => !prev)}
|
||||
title="Model info"
|
||||
title={t('header.modelInfo')}
|
||||
aria-pressed={showModelInfo}
|
||||
aria-controls="chat-model-info-panel"
|
||||
>
|
||||
@@ -874,7 +878,7 @@ export default function Chat() {
|
||||
type="button"
|
||||
className={`btn btn-secondary btn-sm${showSettings ? ' active' : ''}`}
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
title="Chat settings"
|
||||
title={t('header.chatSettings')}
|
||||
aria-pressed={showSettings}
|
||||
>
|
||||
<i className="fas fa-sliders-h" />
|
||||
@@ -886,31 +890,31 @@ export default function Chat() {
|
||||
{showModelInfo && modelInfo && (
|
||||
<div id="chat-model-info-panel" className="chat-model-info-panel">
|
||||
<div className="chat-model-info-header">
|
||||
<span>Model Info: {activeChat.model}</span>
|
||||
<span>{t('header.modelInfoTitle', { model: activeChat.model })}</span>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-xs)' }}>
|
||||
{isAdmin && activeChat.model && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/model-editor/${encodeURIComponent(activeChat.model)}`)}
|
||||
title="Edit model config"
|
||||
title={t('header.editConfig')}
|
||||
>
|
||||
<i className="fas fa-pen-to-square" /> Edit config
|
||||
<i className="fas fa-pen-to-square" /> {t('header.editConfig')}
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setShowModelInfo(false)} title="Close">
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setShowModelInfo(false)} title={t('header.close')}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-model-info-body">
|
||||
{modelInfo.backend && <div className="chat-model-info-row"><span>Backend</span><span>{modelInfo.backend}</span></div>}
|
||||
{modelInfo.parameters?.model && <div className="chat-model-info-row"><span>Model file</span><span>{modelInfo.parameters.model}</span></div>}
|
||||
{modelInfo.context_size > 0 && <div className="chat-model-info-row"><span>Context size</span><span>{modelInfo.context_size}</span></div>}
|
||||
{modelInfo.threads > 0 && <div className="chat-model-info-row"><span>Threads</span><span>{modelInfo.threads}</span></div>}
|
||||
{(modelInfo.mcp?.remote || modelInfo.mcp?.stdio) && <div className="chat-model-info-row"><span>MCP</span><span className="badge badge-success">Configured</span></div>}
|
||||
{modelInfo.template?.chat_message && <div className="chat-model-info-row"><span>Chat template</span><span>Yes</span></div>}
|
||||
{modelInfo.gpu_layers > 0 && <div className="chat-model-info-row"><span>GPU layers</span><span>{modelInfo.gpu_layers}</span></div>}
|
||||
{modelInfo.backend && <div className="chat-model-info-row"><span>{t('modelInfo.backend')}</span><span>{modelInfo.backend}</span></div>}
|
||||
{modelInfo.parameters?.model && <div className="chat-model-info-row"><span>{t('modelInfo.modelFile')}</span><span>{modelInfo.parameters.model}</span></div>}
|
||||
{modelInfo.context_size > 0 && <div className="chat-model-info-row"><span>{t('modelInfo.contextSize')}</span><span>{modelInfo.context_size}</span></div>}
|
||||
{modelInfo.threads > 0 && <div className="chat-model-info-row"><span>{t('modelInfo.threads')}</span><span>{modelInfo.threads}</span></div>}
|
||||
{(modelInfo.mcp?.remote || modelInfo.mcp?.stdio) && <div className="chat-model-info-row"><span>{t('modelInfo.mcp')}</span><span className="badge badge-success">{t('modelInfo.configured')}</span></div>}
|
||||
{modelInfo.template?.chat_message && <div className="chat-model-info-row"><span>{t('modelInfo.chatTemplate')}</span><span>{t('modelInfo.yes')}</span></div>}
|
||||
{modelInfo.gpu_layers > 0 && <div className="chat-model-info-row"><span>{t('modelInfo.gpuLayers')}</span><span>{modelInfo.gpu_layers}</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -925,8 +929,9 @@ export default function Chat() {
|
||||
}}
|
||||
/>
|
||||
<span className="chat-context-label">
|
||||
Context: {Math.round(contextPercent)}%
|
||||
{activeChat.tokenUsage.total > 0 && ` (${activeChat.tokenUsage.total} tokens)`}
|
||||
{activeChat.tokenUsage.total > 0
|
||||
? t('context.labelWithTokens', { percent: Math.round(contextPercent), tokens: activeChat.tokenUsage.total })
|
||||
: t('context.label', { percent: Math.round(contextPercent) })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -935,7 +940,7 @@ export default function Chat() {
|
||||
<div className={`chat-settings-overlay${showSettings ? ' open' : ''}`} onClick={() => setShowSettings(false)} />
|
||||
<div className={`chat-settings-drawer${showSettings ? ' open' : ''}`}>
|
||||
<div className="chat-settings-drawer-header">
|
||||
<span>Chat Settings</span>
|
||||
<span>{t('settings.title')}</span>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setShowSettings(false)}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
@@ -945,10 +950,10 @@ export default function Chat() {
|
||||
<div className="form-group chat-settings-toggle-row">
|
||||
<div className="chat-settings-toggle-text">
|
||||
<span className="chat-settings-toggle-title">
|
||||
<i className="fas fa-user-shield" /> Manage mode
|
||||
<i className="fas fa-user-shield" /> {t('settings.manageMode')}
|
||||
</span>
|
||||
<span className="chat-settings-toggle-desc">
|
||||
Let this chat install models, switch backends, and edit configs by talking to LocalAI.
|
||||
{t('settings.manageModeDesc')}
|
||||
</span>
|
||||
</div>
|
||||
<label className="toggle">
|
||||
@@ -962,18 +967,18 @@ export default function Chat() {
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label className="form-label">System Prompt</label>
|
||||
<label className="form-label">{t('settings.systemPrompt')}</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={activeChat.systemPrompt || ''}
|
||||
onChange={(e) => updateChatSettings(activeChat.id, { systemPrompt: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="You are a helpful assistant..."
|
||||
placeholder={t('settings.systemPromptPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
Temperature {activeChat.temperature !== null ? `(${activeChat.temperature})` : ''}
|
||||
{t('settings.temperature')} {activeChat.temperature !== null ? `(${activeChat.temperature})` : ''}
|
||||
</label>
|
||||
<input
|
||||
type="range" min="0" max="2" step="0.1"
|
||||
@@ -985,7 +990,7 @@ export default function Chat() {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
Top P {activeChat.topP !== null ? `(${activeChat.topP})` : ''}
|
||||
{t('settings.topP')} {activeChat.topP !== null ? `(${activeChat.topP})` : ''}
|
||||
</label>
|
||||
<input
|
||||
type="range" min="0" max="1" step="0.05"
|
||||
@@ -997,7 +1002,7 @@ export default function Chat() {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
Top K {activeChat.topK !== null ? `(${activeChat.topK})` : ''}
|
||||
{t('settings.topK')} {activeChat.topK !== null ? `(${activeChat.topK})` : ''}
|
||||
</label>
|
||||
<input
|
||||
type="range" min="1" max="100" step="1"
|
||||
@@ -1008,13 +1013,13 @@ export default function Chat() {
|
||||
<div className="chat-slider-labels"><span>1</span><span>100</span></div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Context Size</label>
|
||||
<label className="form-label">{t('settings.contextSize')}</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
value={activeChat.contextSize || ''}
|
||||
onChange={(e) => updateChatSettings(activeChat.id, { contextSize: parseInt(e.target.value) || null })}
|
||||
placeholder="2048"
|
||||
placeholder={t('settings.contextSizePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="chat-settings-danger-zone">
|
||||
@@ -1022,9 +1027,9 @@ export default function Chat() {
|
||||
type="button"
|
||||
className="chat-settings-danger-btn"
|
||||
onClick={() => clearHistory(activeChat.id)}
|
||||
title="Clear chat history"
|
||||
title={t('settings.clearHistory')}
|
||||
>
|
||||
<i className="fas fa-eraser" /> Clear chat history
|
||||
<i className="fas fa-eraser" /> {t('settings.clearHistory')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1034,16 +1039,16 @@ export default function Chat() {
|
||||
<div className="chat-messages" ref={messagesRef}>
|
||||
{activeChat.history.length === 0 && !isStreaming && (
|
||||
<div className="chat-empty-state">
|
||||
<h2 className="chat-empty-title">{activeChat.localaiAssistant ? 'Manage LocalAI by chatting' : 'Start a conversation'}</h2>
|
||||
<h2 className="chat-empty-title">{activeChat.localaiAssistant ? t('empty.manageTitle') : t('empty.startTitle')}</h2>
|
||||
<p className="chat-empty-text">
|
||||
{activeChat.localaiAssistant
|
||||
? 'Ask to install models, switch backends, edit configs, or check status. The assistant will summarise actions and wait for your confirmation before changing anything.'
|
||||
: (activeChat.model ? `Ready to chat with ${activeChat.model}` : 'Select a model above to get started')}
|
||||
? t('empty.manageText')
|
||||
: (activeChat.model ? t('empty.readyText', { model: activeChat.model }) : t('empty.selectModelText'))}
|
||||
</p>
|
||||
<div className="chat-empty-suggestions">
|
||||
{(activeChat.localaiAssistant
|
||||
? ['What is installed?', 'Install a chat model', 'Show system status', 'Update a backend']
|
||||
: ['Explain how this works', 'Help me write code', 'Summarize a document', 'Brainstorm ideas']
|
||||
? t('empty.suggestionsManage', { returnObjects: true })
|
||||
: t('empty.suggestionsChat', { returnObjects: true })
|
||||
).map((prompt) => (
|
||||
<button
|
||||
key={prompt}
|
||||
@@ -1057,7 +1062,7 @@ export default function Chat() {
|
||||
{recentChats.length > 0 && (
|
||||
<div className="chat-recent-strip">
|
||||
<div className="chat-recent-strip-label">
|
||||
Recent <kbd className="chat-recent-strip-kbd">⌘K</kbd>
|
||||
{t('empty.recent')} <kbd className="chat-recent-strip-kbd">⌘K</kbd>
|
||||
</div>
|
||||
<div className="chat-recent-strip-list">
|
||||
{recentChats.map(chat => (
|
||||
@@ -1070,7 +1075,7 @@ export default function Chat() {
|
||||
>
|
||||
<span className="chat-recent-strip-item-name">{chat.name}</span>
|
||||
<span className="chat-recent-strip-item-preview">
|
||||
{getLastMessagePreview(chat) || 'No messages yet'}
|
||||
{getLastMessagePreview(chat) || t('empty.noMessages')}
|
||||
</span>
|
||||
<span className="chat-recent-strip-item-time">{relativeTime(chat.updatedAt)}</span>
|
||||
</button>
|
||||
@@ -1079,9 +1084,9 @@ export default function Chat() {
|
||||
</div>
|
||||
)}
|
||||
<div className="chat-empty-hints">
|
||||
<span><i className="fas fa-keyboard" /> Enter to send</span>
|
||||
<span><i className="fas fa-level-down-alt" /> Shift+Enter for newline</span>
|
||||
<span><i className="fas fa-paperclip" /> Attach files</span>
|
||||
<span><i className="fas fa-keyboard" /> {t('empty.hintEnter')}</span>
|
||||
<span><i className="fas fa-level-down-alt" /> {t('empty.hintShiftEnter')}</span>
|
||||
<span><i className="fas fa-paperclip" /> {t('empty.hintAttach')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1128,15 +1133,15 @@ export default function Chat() {
|
||||
</div>
|
||||
{msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.includes('Error:') && (
|
||||
<a href="/app/traces?tab=backend" className="chat-error-trace-link">
|
||||
<i className="fas fa-wave-square" /> View traces for details
|
||||
<i className="fas fa-wave-square" /> {t('errors.viewTraces')}
|
||||
</a>
|
||||
)}
|
||||
<div className="chat-message-actions">
|
||||
<button onClick={() => copyMessage(msg.content)} title="Copy">
|
||||
<button onClick={() => copyMessage(msg.content)} title={t('actions.copy')}>
|
||||
<i className="fas fa-copy" />
|
||||
</button>
|
||||
{msg.role === 'assistant' && i === activeChat.history.length - 1 && !isStreaming && (
|
||||
<button onClick={handleRegenerate} title="Regenerate">
|
||||
<button onClick={handleRegenerate} title={t('actions.regenerate')}>
|
||||
<i className="fas fa-rotate" />
|
||||
</button>
|
||||
)}
|
||||
@@ -1170,7 +1175,7 @@ export default function Chat() {
|
||||
</div>
|
||||
{tokensPerSecond !== null && (
|
||||
<div className="chat-streaming-speed">
|
||||
<i className="fas fa-tachometer-alt" /> {tokensPerSecond} tok/s
|
||||
<i className="fas fa-tachometer-alt" /> {t('tokens.perSec', { count: tokensPerSecond })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1186,7 +1191,7 @@ export default function Chat() {
|
||||
{stagingOp ? (
|
||||
<div className="chat-staging-progress">
|
||||
<div className="chat-staging-label">
|
||||
<i className="fas fa-cloud-arrow-up" /> Transferring model{stagingOp.nodeName ? ` to ${stagingOp.nodeName}` : ''}...
|
||||
<i className="fas fa-cloud-arrow-up" /> {stagingOp.nodeName ? t('streaming.transferringTo', { node: stagingOp.nodeName }) : t('streaming.transferring')}
|
||||
</div>
|
||||
{stagingOp.progress > 0 && (
|
||||
<div className="chat-staging-detail">
|
||||
@@ -1215,15 +1220,15 @@ export default function Chat() {
|
||||
{/* Token info bar */}
|
||||
{(tokensPerSecond || maxTokensPerSecond || activeChat.tokenUsage?.total > 0) && (
|
||||
<div className="chat-token-info">
|
||||
{tokensPerSecond !== null && <span><i className="fas fa-tachometer-alt" /> {tokensPerSecond} tok/s</span>}
|
||||
{tokensPerSecond !== null && <span><i className="fas fa-tachometer-alt" /> {t('tokens.perSec', { count: tokensPerSecond })}</span>}
|
||||
{maxTokensPerSecond !== null && !isStreaming && (
|
||||
<span className="chat-max-tps-badge">
|
||||
<i className="fas fa-bolt" /> Peak: {maxTokensPerSecond} tok/s
|
||||
<i className="fas fa-bolt" /> {t('tokens.peak', { count: maxTokensPerSecond })}
|
||||
</span>
|
||||
)}
|
||||
{activeChat.tokenUsage?.total > 0 && (
|
||||
<span>
|
||||
<i className="fas fa-coins" /> {activeChat.tokenUsage.prompt}p + {activeChat.tokenUsage.completion}c = {activeChat.tokenUsage.total}
|
||||
<i className="fas fa-coins" /> {t('tokens.usage', { prompt: activeChat.tokenUsage.prompt, completion: activeChat.tokenUsage.completion, total: activeChat.tokenUsage.total })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1257,16 +1262,16 @@ export default function Chat() {
|
||||
if (!next) setCanvasOpen(false)
|
||||
}}
|
||||
aria-pressed={canvasMode}
|
||||
title="Canvas — extract code blocks and media into a side panel for preview, copy, and download"
|
||||
title={t('input.canvasTitle')}
|
||||
>
|
||||
<i className="fas fa-columns" />
|
||||
<span className="chat-mode-chip-label">Canvas</span>
|
||||
<span className="chat-mode-chip-label">{t('input.canvasLabel')}</span>
|
||||
{canvasMode && artifacts.length > 0 && !canvasOpen && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="chat-mode-chip-count"
|
||||
title="Open canvas panel"
|
||||
title={t('input.openCanvas')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedArtifactId(artifacts[0]?.id)
|
||||
@@ -1325,7 +1330,7 @@ export default function Chat() {
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm chat-attach-btn"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
title="Attach file"
|
||||
title={t('input.attachFile')}
|
||||
>
|
||||
<i className="fas fa-paperclip" />
|
||||
</button>
|
||||
@@ -1343,12 +1348,12 @@ export default function Chat() {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Message..."
|
||||
placeholder={t('input.placeholder')}
|
||||
rows={1}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
{isStreaming ? (
|
||||
<button className="chat-stop-btn" onClick={stopGeneration} title="Stop generating">
|
||||
<button className="chat-stop-btn" onClick={stopGeneration} title={t('input.stopGenerating')}>
|
||||
<i className="fas fa-stop" />
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { agentCollectionsApi } from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useUserMap } from '../hooks/useUserMap'
|
||||
@@ -9,6 +10,7 @@ import ConfirmDialog from '../components/ConfirmDialog'
|
||||
export default function Collections() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('collections')
|
||||
const { isAdmin, authEnabled, user } = useAuth()
|
||||
const userMap = useUserMap()
|
||||
const [collections, setCollections] = useState([])
|
||||
@@ -24,11 +26,11 @@ export default function Collections() {
|
||||
setCollections(Array.isArray(data.collections) ? data.collections : [])
|
||||
setUserGroups(data.user_groups || null)
|
||||
} catch (err) {
|
||||
addToast(`Failed to load collections: ${err.message}`, 'error')
|
||||
addToast(t('toasts.loadFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [addToast, isAdmin, authEnabled])
|
||||
}, [addToast, isAdmin, authEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
fetchCollections()
|
||||
@@ -40,11 +42,11 @@ export default function Collections() {
|
||||
setCreating(true)
|
||||
try {
|
||||
await agentCollectionsApi.create(name)
|
||||
addToast(`Collection "${name}" created`, 'success')
|
||||
addToast(t('toasts.created', { name }), 'success')
|
||||
setNewName('')
|
||||
fetchCollections()
|
||||
} catch (err) {
|
||||
addToast(`Failed to create collection: ${err.message}`, 'error')
|
||||
addToast(t('toasts.createFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
@@ -52,18 +54,18 @@ export default function Collections() {
|
||||
|
||||
const handleDelete = (name, userId) => {
|
||||
setConfirmDialog({
|
||||
title: 'Delete Collection',
|
||||
message: `Delete collection "${name}"? This will remove all entries and cannot be undone.`,
|
||||
confirmLabel: 'Delete',
|
||||
title: t('deleteDialog.title'),
|
||||
message: t('deleteDialog.message', { name }),
|
||||
confirmLabel: t('deleteDialog.confirm'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await agentCollectionsApi.reset(name, userId)
|
||||
addToast(`Collection "${name}" deleted`, 'success')
|
||||
addToast(t('toasts.deleted', { name }), 'success')
|
||||
fetchCollections()
|
||||
} catch (err) {
|
||||
addToast(`Failed to delete collection: ${err.message}`, 'error')
|
||||
addToast(t('toasts.deleteFailed', { message: err.message }), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -71,18 +73,18 @@ export default function Collections() {
|
||||
|
||||
const handleReset = (name, userId) => {
|
||||
setConfirmDialog({
|
||||
title: 'Reset Collection',
|
||||
message: `Reset collection "${name}"? This will remove all entries but keep the collection.`,
|
||||
confirmLabel: 'Reset',
|
||||
title: t('resetDialog.title'),
|
||||
message: t('resetDialog.message', { name }),
|
||||
confirmLabel: t('resetDialog.confirm'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await agentCollectionsApi.reset(name, userId)
|
||||
addToast(`Collection "${name}" reset`, 'success')
|
||||
addToast(t('toasts.reset', { name }), 'success')
|
||||
fetchCollections()
|
||||
} catch (err) {
|
||||
addToast(`Failed to reset collection: ${err.message}`, 'error')
|
||||
addToast(t('toasts.resetFailed', { message: err.message }), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -118,21 +120,21 @@ export default function Collections() {
|
||||
`}</style>
|
||||
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Knowledge Base</h1>
|
||||
<p className="page-subtitle">Manage document collections for agent RAG</p>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="collections-create-bar">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="New collection name..."
|
||||
placeholder={t('newPlaceholder')}
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleCreate() }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={handleCreate} disabled={creating || !newName.trim()}>
|
||||
{creating ? <><i className="fas fa-spinner fa-spin" /> Creating...</> : <><i className="fas fa-plus" /> Create</>}
|
||||
{creating ? <><i className="fas fa-spinner fa-spin" /> {t('actions.creating')}</> : <><i className="fas fa-plus" /> {t('actions.create')}</>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -143,17 +145,16 @@ export default function Collections() {
|
||||
) : collections.length === 0 && !userGroups ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-database" /></div>
|
||||
<h2 className="empty-state-title">No collections yet</h2>
|
||||
<h2 className="empty-state-title">{t('empty.title')}</h2>
|
||||
<p className="empty-state-text">
|
||||
Collections let you organize documents into knowledge bases that agents can search using RAG (Retrieval-Augmented Generation).
|
||||
Create a collection above to get started.
|
||||
{t('empty.text')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{userGroups && <h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>Your Collections</h2>}
|
||||
{userGroups && <h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>{t('sections.yourCollections')}</h2>}
|
||||
{collections.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-md)' }}>You have no collections yet.</p>
|
||||
<p style={{ color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-md)' }}>{t('empty.noPersonal')}</p>
|
||||
) : (
|
||||
<div className="collections-grid">
|
||||
{collections.map((collection) => {
|
||||
@@ -165,13 +166,13 @@ export default function Collections() {
|
||||
{name}
|
||||
</div>
|
||||
<div className="collections-card-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/app/collections/${encodeURIComponent(name)}`)} title="View details">
|
||||
<i className="fas fa-eye" /> Details
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/app/collections/${encodeURIComponent(name)}`)} title={t('actions.viewDetails')}>
|
||||
<i className="fas fa-eye" /> {t('actions.details')}
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleReset(name)} title="Reset collection">
|
||||
<i className="fas fa-rotate" /> Reset
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleReset(name)} title={t('actions.resetCollection')}>
|
||||
<i className="fas fa-rotate" /> {t('actions.reset')}
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name)} title="Delete collection">
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name)} title={t('actions.deleteCollection')}>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -185,7 +186,7 @@ export default function Collections() {
|
||||
|
||||
{userGroups && (
|
||||
<UserGroupSection
|
||||
title="Other Users' Collections"
|
||||
title={t('sections.otherUsersCollections')}
|
||||
userGroups={userGroups}
|
||||
userMap={userMap}
|
||||
currentUserId={user?.id}
|
||||
@@ -201,13 +202,13 @@ export default function Collections() {
|
||||
{name}
|
||||
</div>
|
||||
<div className="collections-card-actions">
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/app/collections/${encodeURIComponent(name)}?user_id=${encodeURIComponent(userId)}`)} title="View details">
|
||||
<i className="fas fa-eye" /> Details
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/app/collections/${encodeURIComponent(name)}?user_id=${encodeURIComponent(userId)}`)} title={t('actions.viewDetails')}>
|
||||
<i className="fas fa-eye" /> {t('actions.details')}
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleReset(name, userId)} title="Reset collection">
|
||||
<i className="fas fa-rotate" /> Reset
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleReset(name, userId)} title={t('actions.resetCollection')}>
|
||||
<i className="fas fa-rotate" /> {t('actions.reset')}
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name, userId)} title="Delete collection">
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name, userId)} title={t('actions.deleteCollection')}>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useBranding } from '../contexts/BrandingContext'
|
||||
@@ -20,6 +21,7 @@ function formatBytes(bytes) {
|
||||
export default function Home() {
|
||||
const navigate = useNavigate()
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('home')
|
||||
const { isAdmin } = useAuth()
|
||||
const branding = useBranding()
|
||||
const { resources } = useResources()
|
||||
@@ -200,7 +202,7 @@ export default function Home() {
|
||||
const text = message.trim()
|
||||
if (!text && allFiles.length === 0) return
|
||||
if (!selectedModel) {
|
||||
addToast('Please select a model first', 'warning')
|
||||
addToast(t('input.selectModelToast'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -240,18 +242,18 @@ export default function Home() {
|
||||
|
||||
const handleStopModel = async (modelName) => {
|
||||
setConfirmDialog({
|
||||
title: 'Stop Model',
|
||||
message: `Stop model ${modelName}?`,
|
||||
confirmLabel: `Stop ${modelName}`,
|
||||
title: t('stopDialog.title'),
|
||||
message: t('stopDialog.message', { model: modelName }),
|
||||
confirmLabel: t('stopDialog.confirm', { model: modelName }),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await backendControlApi.shutdown({ model: modelName })
|
||||
addToast(`Stopped ${modelName}`, 'success')
|
||||
addToast(t('stopDialog.stoppedToast', { model: modelName }), 'success')
|
||||
setTimeout(fetchSystemInfo, 500)
|
||||
} catch (err) {
|
||||
addToast(`Failed to stop: ${err.message}`, 'error')
|
||||
addToast(t('stopDialog.stopFailed', { message: err.message }), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -259,18 +261,18 @@ export default function Home() {
|
||||
|
||||
const handleStopAll = async () => {
|
||||
setConfirmDialog({
|
||||
title: 'Stop All Models',
|
||||
message: `Stop all ${loadedModels.length} loaded models?`,
|
||||
confirmLabel: 'Stop all',
|
||||
title: t('stopDialog.stopAllTitle'),
|
||||
message: t('stopDialog.stopAllMessage', { count: loadedModels.length }),
|
||||
confirmLabel: t('stopDialog.stopAllConfirm'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await Promise.all(loadedModels.map(m => backendControlApi.shutdown({ model: m.id })))
|
||||
addToast('All models stopped', 'success')
|
||||
addToast(t('stopDialog.allStoppedToast'), 'success')
|
||||
setTimeout(fetchSystemInfo, 1000)
|
||||
} catch (err) {
|
||||
addToast(`Failed to stop: ${err.message}`, 'error')
|
||||
addToast(t('stopDialog.stopFailed', { message: err.message }), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -303,7 +305,7 @@ export default function Home() {
|
||||
<div className="home-resource-bar">
|
||||
<div className="home-resource-bar-header">
|
||||
<i className={`fas ${clusterData.isGPU ? 'fa-microchip' : 'fa-memory'}`} />
|
||||
<span className="home-resource-label">Cluster {clusterData.isGPU ? 'VRAM' : 'RAM'}</span>
|
||||
<span className="home-resource-label">{clusterData.isGPU ? t('cluster.vram') : t('cluster.ram')}</span>
|
||||
<span className="home-resource-pct" style={{ color: clusterPctColor }}>
|
||||
{formatBytes(clusterData.usedMem)} / {formatBytes(clusterData.totalMem)}
|
||||
</span>
|
||||
@@ -316,14 +318,14 @@ export default function Home() {
|
||||
</div>
|
||||
<div className="home-cluster-status">
|
||||
<span className="home-cluster-dot" style={clusterData.healthyCount === 0 ? { background: 'var(--color-error)' } : undefined} />
|
||||
<span>{clusterData.healthyCount}/{clusterData.totalCount} nodes online</span>
|
||||
<span>{t('cluster.nodesOnline', { healthy: clusterData.healthyCount, total: clusterData.totalCount })}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : !distributedMode && resources ? (
|
||||
<div className="home-resource-bar">
|
||||
<div className="home-resource-bar-header">
|
||||
<i className={`fas ${resType === 'gpu' ? 'fa-microchip' : 'fa-memory'}`} />
|
||||
<span className="home-resource-label">{resType === 'gpu' ? 'GPU' : 'RAM'}</span>
|
||||
<span className="home-resource-label">{resType === 'gpu' ? t('resourceGpu') : t('resourceRam')}</span>
|
||||
<span className="home-resource-pct" style={{ color: pctColor }}>
|
||||
{usagePct.toFixed(0)}%
|
||||
</span>
|
||||
@@ -348,13 +350,11 @@ export default function Home() {
|
||||
>
|
||||
<span className="home-assistant-icon"><i className="fas fa-user-shield" /></span>
|
||||
<span className="home-assistant-text">
|
||||
<span className="home-assistant-title">Manage LocalAI by chatting</span>
|
||||
<span className="home-assistant-desc">
|
||||
Install models, switch backends, edit configs and check status by talking to LocalAI.
|
||||
</span>
|
||||
<span className="home-assistant-title">{t('assistant.title')}</span>
|
||||
<span className="home-assistant-desc">{t('assistant.description')}</span>
|
||||
</span>
|
||||
<span className="home-assistant-cta">
|
||||
Open assistant <i className="fas fa-arrow-right" />
|
||||
{t('assistant.open')} <i className="fas fa-arrow-right" />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -407,7 +407,7 @@ export default function Home() {
|
||||
className="home-textarea"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Message..."
|
||||
placeholder={t('input.placeholder')}
|
||||
rows={3}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
@@ -418,22 +418,22 @@ export default function Home() {
|
||||
/>
|
||||
<div className="home-input-footer">
|
||||
<div className="home-attach-buttons">
|
||||
<button type="button" className="home-attach-btn" onClick={() => imageInputRef.current?.click()} title="Attach image">
|
||||
<button type="button" className="home-attach-btn" onClick={() => imageInputRef.current?.click()} title={t('input.attachImage')}>
|
||||
<i className="fas fa-image" />
|
||||
</button>
|
||||
<button type="button" className="home-attach-btn" onClick={() => audioInputRef.current?.click()} title="Attach audio">
|
||||
<button type="button" className="home-attach-btn" onClick={() => audioInputRef.current?.click()} title={t('input.attachAudio')}>
|
||||
<i className="fas fa-microphone" />
|
||||
</button>
|
||||
<button type="button" className="home-attach-btn" onClick={() => fileInputRef.current?.click()} title="Attach file">
|
||||
<button type="button" className="home-attach-btn" onClick={() => fileInputRef.current?.click()} title={t('input.attachFile')}>
|
||||
<i className="fas fa-file" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="home-input-hint">Enter to send</span>
|
||||
<span className="home-input-hint">{t('input.enterToSend')}</span>
|
||||
<button
|
||||
type="submit"
|
||||
className="home-send-btn"
|
||||
disabled={!selectedModel}
|
||||
title={!selectedModel ? 'Select a model first' : 'Send message'}
|
||||
title={!selectedModel ? t('input.selectModelFirst') : t('input.sendMessage')}
|
||||
>
|
||||
<i className="fas fa-arrow-up" />
|
||||
</button>
|
||||
@@ -453,24 +453,24 @@ export default function Home() {
|
||||
<button
|
||||
className="home-link-btn"
|
||||
onClick={openAssistantChat}
|
||||
title="Manage LocalAI by chatting"
|
||||
title={t('assistant.tooltip')}
|
||||
>
|
||||
<i className="fas fa-user-shield" /> Manage by chat
|
||||
<i className="fas fa-user-shield" /> {t('quickLinks.manageByChat')}
|
||||
</button>
|
||||
)}
|
||||
<button className="home-link-btn" onClick={() => navigate('/app/manage')}>
|
||||
<i className="fas fa-desktop" /> Installed Models
|
||||
<i className="fas fa-desktop" /> {t('quickLinks.installedModels')}
|
||||
</button>
|
||||
<button className="home-link-btn" onClick={() => navigate('/app/models')}>
|
||||
<i className="fas fa-download" /> Browse Gallery
|
||||
<i className="fas fa-download" /> {t('quickLinks.browseGallery')}
|
||||
</button>
|
||||
<button className="home-link-btn" onClick={() => navigate('/app/import-model')}>
|
||||
<i className="fas fa-upload" /> Import Model
|
||||
<i className="fas fa-upload" /> {t('quickLinks.importModel')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<a className="home-link-btn" href="https://localai.io" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-book" /> Documentation
|
||||
<i className="fas fa-book" /> {t('quickLinks.documentation')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -478,12 +478,12 @@ export default function Home() {
|
||||
{loadedCount > 0 && (
|
||||
<div className="home-loaded-models">
|
||||
<span className="home-loaded-dot" />
|
||||
<span className="home-loaded-text">{loadedCount} model{loadedCount !== 1 ? 's' : ''} loaded</span>
|
||||
<span className="home-loaded-text">{t('loadedModels.count', { count: loadedCount })}</span>
|
||||
<div className="home-loaded-list">
|
||||
{[...loadedModels].sort((a, b) => a.id.localeCompare(b.id)).map(m => (
|
||||
<span key={m.id} className="home-loaded-item">
|
||||
{m.id}
|
||||
<button onClick={() => handleStopModel(m.id)} title="Stop model">
|
||||
<button onClick={() => handleStopModel(m.id)} title={t('loadedModels.stop')}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</span>
|
||||
@@ -491,7 +491,7 @@ export default function Home() {
|
||||
</div>
|
||||
{loadedCount > 1 && (
|
||||
<button className="home-stop-all" onClick={handleStopAll}>
|
||||
Stop all
|
||||
{t('loadedModels.stopAll')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -502,43 +502,43 @@ export default function Home() {
|
||||
<div className="home-wizard">
|
||||
<div className="home-wizard-hero">
|
||||
<img src={apiUrl(branding.logoUrl)} alt={branding.instanceName} className="home-logo" />
|
||||
<h1>Get started with {branding.instanceName}</h1>
|
||||
<p>Install your first model to begin. Browse the gallery or import your own.</p>
|
||||
<h1>{t('wizard.getStarted', { name: branding.instanceName })}</h1>
|
||||
<p>{t('wizard.intro')}</p>
|
||||
</div>
|
||||
|
||||
<div className="home-wizard-steps card">
|
||||
<div className="home-wizard-step">
|
||||
<div className="home-wizard-step-num">1</div>
|
||||
<div>
|
||||
<strong>Browse the Model Gallery</strong>
|
||||
<p>Find the right model for your needs from our curated collection.</p>
|
||||
<strong>{t('wizard.steps.step1Title')}</strong>
|
||||
<p>{t('wizard.steps.step1Body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="home-wizard-step">
|
||||
<div className="home-wizard-step-num">2</div>
|
||||
<div>
|
||||
<strong>Install a Model</strong>
|
||||
<p>Click install to download and configure it automatically.</p>
|
||||
<strong>{t('wizard.steps.step2Title')}</strong>
|
||||
<p>{t('wizard.steps.step2Body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="home-wizard-step">
|
||||
<div className="home-wizard-step-num">3</div>
|
||||
<div>
|
||||
<strong>Start Chatting</strong>
|
||||
<p>Chat with your model right from the browser or use the API.</p>
|
||||
<strong>{t('wizard.steps.step3Title')}</strong>
|
||||
<p>{t('wizard.steps.step3Body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="home-wizard-actions">
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/models')}>
|
||||
<i className="fas fa-store" /> Browse Model Gallery
|
||||
<i className="fas fa-store" /> {t('wizard.browseGallery')}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => navigate('/app/import-model')}>
|
||||
<i className="fas fa-upload" /> Import Model
|
||||
<i className="fas fa-upload" /> {t('wizard.importModel')}
|
||||
</button>
|
||||
<a className="btn btn-secondary" href="https://localai.io/docs/getting-started" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-book" /> Docs
|
||||
<i className="fas fa-book" /> {t('wizard.docs')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -547,12 +547,12 @@ export default function Home() {
|
||||
<div className="home-wizard">
|
||||
<div className="home-wizard-hero">
|
||||
<img src={apiUrl(branding.logoUrl)} alt={branding.instanceName} className="home-logo" />
|
||||
<h1>No Models Available</h1>
|
||||
<p>There are no models installed yet. Ask your administrator to set up models so you can start chatting.</p>
|
||||
<h1>{t('wizard.noModelsTitle')}</h1>
|
||||
<p>{t('wizard.noModelsBody')}</p>
|
||||
</div>
|
||||
<div className="home-wizard-actions">
|
||||
<a className="btn btn-secondary" href="https://localai.io" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-book" /> Documentation
|
||||
<i className="fas fa-book" /> {t('quickLinks.documentation')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelSelector from '../components/ModelSelector'
|
||||
import { CAP_IMAGE } from '../utils/capabilities'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -13,6 +14,7 @@ const SIZES = ['256x256', '512x512', '768x768', '1024x1024']
|
||||
export default function ImageGen() {
|
||||
const { model: urlModel } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('media')
|
||||
const [model, setModel] = useState(urlModel || '')
|
||||
const [prompt, setPrompt] = useState('')
|
||||
const [negativePrompt, setNegativePrompt] = useState('')
|
||||
@@ -33,8 +35,8 @@ export default function ImageGen() {
|
||||
|
||||
const handleGenerate = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!prompt.trim()) { addToast('Please enter a prompt', 'warning'); return }
|
||||
if (!model) { addToast('Please select a model', 'warning'); return }
|
||||
if (!prompt.trim()) { addToast(t('image.toasts.noPrompt'), 'warning'); return }
|
||||
if (!model) { addToast(t('image.toasts.noModel'), 'warning'); return }
|
||||
|
||||
setLoading(true)
|
||||
setImages([])
|
||||
@@ -54,7 +56,7 @@ export default function ImageGen() {
|
||||
const results = data?.data || []
|
||||
setImages(results)
|
||||
if (!results.length) {
|
||||
addToast('No images generated', 'warning')
|
||||
addToast(t('image.toasts.noResults'), 'warning')
|
||||
} else {
|
||||
const urlResults = results.filter(r => r.url && !r.url.startsWith('data:')).map(r => ({ url: r.url }))
|
||||
if (urlResults.length) {
|
||||
@@ -83,62 +85,62 @@ export default function ImageGen() {
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-image" /> Image Generation</h1>
|
||||
<h1 className="page-title"><i className="fas fa-image" /> {t('image.title')}</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Model</label>
|
||||
<label className="form-label">{t('image.labels.model')}</label>
|
||||
<ModelSelector value={model} onChange={setModel} capability={CAP_IMAGE} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Prompt</label>
|
||||
<textarea className="textarea" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Describe the image you want to generate..." rows={3} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleGenerate(e) } }} />
|
||||
<label className="form-label">{t('image.labels.prompt')}</label>
|
||||
<textarea className="textarea" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder={t('image.labels.promptPlaceholder')} rows={3} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleGenerate(e) } }} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Negative Prompt</label>
|
||||
<textarea className="textarea" value={negativePrompt} onChange={(e) => setNegativePrompt(e.target.value)} placeholder="What to avoid..." rows={2} />
|
||||
<label className="form-label">{t('image.labels.negativePrompt')}</label>
|
||||
<textarea className="textarea" value={negativePrompt} onChange={(e) => setNegativePrompt(e.target.value)} placeholder={t('image.labels.negativePromptPlaceholder')} rows={2} />
|
||||
</div>
|
||||
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Size</label>
|
||||
<label className="form-label">{t('image.labels.size')}</label>
|
||||
<select className="input btn-full" value={size} onChange={(e) => setSize(e.target.value)}>
|
||||
{SIZES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Count (1-4)</label>
|
||||
<label className="form-label">{t('image.labels.count')}</label>
|
||||
<input className="input" type="number" min="1" max="4" value={count} onChange={(e) => setCount(parseInt(e.target.value) || 1)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`collapsible-header ${showAdvanced ? 'open' : ''}`} onClick={() => setShowAdvanced(!showAdvanced)}>
|
||||
<i className="fas fa-chevron-right" /> Advanced Settings
|
||||
<i className="fas fa-chevron-right" /> {t('image.labels.advanced')}
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group"><label className="form-label">Steps</label><input className="input" type="number" value={steps} onChange={(e) => setSteps(e.target.value)} placeholder="20" /></div>
|
||||
<div className="form-group"><label className="form-label">Seed</label><input className="input" type="number" value={seed} onChange={(e) => setSeed(e.target.value)} placeholder="Random" /></div>
|
||||
<div className="form-group"><label className="form-label">{t('image.labels.steps')}</label><input className="input" type="number" value={steps} onChange={(e) => setSteps(e.target.value)} placeholder={t('image.labels.stepsPlaceholder')} /></div>
|
||||
<div className="form-group"><label className="form-label">{t('image.labels.seed')}</label><input className="input" type="number" value={seed} onChange={(e) => setSeed(e.target.value)} placeholder={t('image.labels.seedPlaceholder')} /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`collapsible-header ${showImageInputs ? 'open' : ''}`} onClick={() => setShowImageInputs(!showImageInputs)}>
|
||||
<i className="fas fa-chevron-right" /> Image Inputs
|
||||
<i className="fas fa-chevron-right" /> {t('image.labels.imageInputs')}
|
||||
</div>
|
||||
{showImageInputs && (
|
||||
<>
|
||||
<div className="form-group"><label className="form-label">Source Image (img2img)</label><input ref={sourceRef} type="file" accept="image/*" onChange={handleSourceImage} className="input" /></div>
|
||||
<div className="form-group"><label className="form-label">{t('image.labels.sourceImage')}</label><input ref={sourceRef} type="file" accept="image/*" onChange={handleSourceImage} className="input" /></div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Reference Images</label>
|
||||
<label className="form-label">{t('image.labels.refImages')}</label>
|
||||
<input ref={refRef} type="file" accept="image/*" multiple onChange={handleRefImages} className="input" />
|
||||
{refImages.length > 0 && <span className="form-field__hint">{refImages.length} image(s) added</span>}
|
||||
{refImages.length > 0 && <span className="form-field__hint">{t('image.labels.refImagesAdded', { count: refImages.length })}</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-wand-magic-sparkles" /> Generate</>}
|
||||
{loading ? <><LoadingSpinner size="sm" /> {t('image.actions.generating')}</> : <><i className="fas fa-wand-magic-sparkles" /> {t('image.actions.generate')}</>}
|
||||
</button>
|
||||
</form>
|
||||
<MediaHistory {...historyProps} />
|
||||
@@ -169,7 +171,7 @@ export default function ImageGen() {
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: 'var(--color-text-muted)' }}>
|
||||
<i className="fas fa-image" style={{ fontSize: '3rem', marginBottom: 'var(--spacing-md)', opacity: 0.4 }} />
|
||||
<p>Generated images will appear here</p>
|
||||
<p>{t('image.empty')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { modelsApi, backendsApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import CodeEditor from '../components/CodeEditor'
|
||||
@@ -12,21 +13,9 @@ import ModalityChips from '../components/ModalityChips'
|
||||
// with auto-detect only rather than showing an empty dropdown.
|
||||
const BACKENDS_FALLBACK_EMPTY = []
|
||||
|
||||
const MODALITY_LABELS = {
|
||||
text: 'Text LLM',
|
||||
asr: 'Speech recognition',
|
||||
tts: 'Text-to-speech',
|
||||
image: 'Image / Video',
|
||||
embeddings: 'Embeddings',
|
||||
reranker: 'Rerankers',
|
||||
detection: 'Object detection',
|
||||
vad: 'Voice activity detection',
|
||||
}
|
||||
|
||||
// Tooltip shown on the "manual pick" badge so screen reader + hover users
|
||||
// understand what opting into a non-autodetectable backend means. Kept at
|
||||
// module scope so the Playwright locator can assert the exact copy.
|
||||
const MANUAL_PICK_TOOLTIP = "Auto-detect won't route to this backend. Pick it here if you know that's what you want."
|
||||
// Modality keys used as i18n keys under "modality.*" namespace; resolved
|
||||
// at render time inside `buildBackendOptions`.
|
||||
const MODALITY_KEYS = ['text', 'asr', 'tts', 'image', 'embeddings', 'reranker', 'detection', 'vad']
|
||||
|
||||
// buildBackendOptions groups known backends by modality and tags
|
||||
// auto_detect=false entries with a muted "manual pick" badge so users
|
||||
@@ -34,7 +23,7 @@ const MANUAL_PICK_TOOLTIP = "Auto-detect won't route to this backend. Pick it he
|
||||
// the list is narrowed before grouping so the dropdown shows only
|
||||
// backends the user asked about — grouping is preserved even if the
|
||||
// result ends up being a single section.
|
||||
function buildBackendOptions(list, modalityFilter = '') {
|
||||
function buildBackendOptions(list, modalityFilter, t) {
|
||||
if (!Array.isArray(list) || list.length === 0) return BACKENDS_FALLBACK_EMPTY
|
||||
const filtered = modalityFilter
|
||||
? list.filter(b => b && b.modality === modalityFilter)
|
||||
@@ -49,14 +38,14 @@ function buildBackendOptions(list, modalityFilter = '') {
|
||||
const keys = Array.from(groups.keys()).sort()
|
||||
const out = []
|
||||
for (const key of keys) {
|
||||
const label = MODALITY_LABELS[key] || (key ? key : 'Other')
|
||||
const label = MODALITY_KEYS.includes(key) ? t(`modality.${key}`) : (key ? t('modality.other') : t('modality.other'))
|
||||
out.push({ value: `__header_${key}`, label, isHeader: true })
|
||||
const sorted = groups.get(key).slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
for (const b of sorted) {
|
||||
const opt = { value: b.name, label: b.name }
|
||||
if (b.auto_detect === false) {
|
||||
opt.badge = 'manual pick'
|
||||
opt.badgeTooltip = MANUAL_PICK_TOOLTIP
|
||||
opt.badge = t('form.manualPick')
|
||||
opt.badgeTooltip = t('form.manualPickTooltip')
|
||||
}
|
||||
out.push(opt)
|
||||
}
|
||||
@@ -64,46 +53,48 @@ function buildBackendOptions(list, modalityFilter = '') {
|
||||
return out
|
||||
}
|
||||
|
||||
// URI_FORMATS describes the example list rendered in the format guide.
|
||||
// Title + description strings are i18n keys, resolved at render time.
|
||||
const URI_FORMATS = [
|
||||
{
|
||||
icon: 'fab fa-hubspot', color: 'var(--color-accent)', title: 'HuggingFace',
|
||||
icon: 'fab fa-hubspot', color: 'var(--color-accent)', titleKey: 'uriFormats.huggingface.title',
|
||||
examples: [
|
||||
{ prefix: 'huggingface://', suffix: 'TheBloke/Llama-2-7B-Chat-GGUF', desc: 'Standard HuggingFace format' },
|
||||
{ prefix: 'hf://', suffix: 'TheBloke/Llama-2-7B-Chat-GGUF', desc: 'Short HuggingFace format' },
|
||||
{ prefix: 'https://huggingface.co/', suffix: 'TheBloke/Llama-2-7B-Chat-GGUF', desc: 'Full HuggingFace URL' },
|
||||
{ prefix: 'huggingface://', suffix: 'TheBloke/Llama-2-7B-Chat-GGUF', descKey: 'uriFormats.huggingface.standard' },
|
||||
{ prefix: 'hf://', suffix: 'TheBloke/Llama-2-7B-Chat-GGUF', descKey: 'uriFormats.huggingface.short' },
|
||||
{ prefix: 'https://huggingface.co/', suffix: 'TheBloke/Llama-2-7B-Chat-GGUF', descKey: 'uriFormats.huggingface.fullUrl' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-globe', color: 'var(--color-primary)', title: 'HTTP/HTTPS URLs',
|
||||
icon: 'fas fa-globe', color: 'var(--color-primary)', titleKey: 'uriFormats.http.title',
|
||||
examples: [
|
||||
{ prefix: 'https://', suffix: 'example.com/model.gguf', desc: 'Direct download from any HTTPS URL' },
|
||||
{ prefix: 'https://', suffix: 'example.com/model.gguf', descKey: 'uriFormats.http.direct' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-file', color: 'var(--color-warning)', title: 'Local Files',
|
||||
icon: 'fas fa-file', color: 'var(--color-warning)', titleKey: 'uriFormats.local.title',
|
||||
examples: [
|
||||
{ prefix: 'file://', suffix: '/path/to/model.gguf', desc: 'Local file path (absolute)' },
|
||||
{ prefix: '', suffix: '/path/to/model.yaml', desc: 'Direct local YAML config file' },
|
||||
{ prefix: 'file://', suffix: '/path/to/model.gguf', descKey: 'uriFormats.local.filePath' },
|
||||
{ prefix: '', suffix: '/path/to/model.yaml', descKey: 'uriFormats.local.directYaml' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-box', color: 'var(--color-data-8)', title: 'OCI Registry',
|
||||
icon: 'fas fa-box', color: 'var(--color-data-8)', titleKey: 'uriFormats.oci.title',
|
||||
examples: [
|
||||
{ prefix: 'oci://', suffix: 'registry.example.com/model:tag', desc: 'OCI container registry' },
|
||||
{ prefix: 'ocifile://', suffix: '/path/to/image.tar', desc: 'Local OCI tarball file' },
|
||||
{ prefix: 'oci://', suffix: 'registry.example.com/model:tag', descKey: 'uriFormats.oci.registry' },
|
||||
{ prefix: 'ocifile://', suffix: '/path/to/image.tar', descKey: 'uriFormats.oci.tarball' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-cube', color: 'var(--color-data-1)', title: 'Ollama',
|
||||
icon: 'fas fa-cube', color: 'var(--color-data-1)', titleKey: 'uriFormats.ollama.title',
|
||||
examples: [
|
||||
{ prefix: 'ollama://', suffix: 'llama2:7b', desc: 'Ollama model format' },
|
||||
{ prefix: 'ollama://', suffix: 'llama2:7b', descKey: 'uriFormats.ollama.model' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-code', color: 'var(--color-data-7)', title: 'YAML Configuration Files',
|
||||
icon: 'fas fa-code', color: 'var(--color-data-7)', titleKey: 'uriFormats.yaml.title',
|
||||
examples: [
|
||||
{ prefix: '', suffix: 'https://example.com/model.yaml', desc: 'Remote YAML config file' },
|
||||
{ prefix: 'file://', suffix: '/path/to/config.yaml', desc: 'Local YAML config file' },
|
||||
{ prefix: '', suffix: 'https://example.com/model.yaml', descKey: 'uriFormats.yaml.remote' },
|
||||
{ prefix: 'file://', suffix: '/path/to/config.yaml', descKey: 'uriFormats.yaml.local' },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -154,11 +145,12 @@ function hasCustomPrefs(prefs, customPrefs, yamlContent) {
|
||||
// (not a separate component) — the strip is tiny and lives inside the
|
||||
// Power-mode card so extracting it would just add indirection.
|
||||
function PowerTabs({ value, onChange }) {
|
||||
const { t } = useTranslation('importModel')
|
||||
return (
|
||||
<div
|
||||
className="segmented"
|
||||
role="tablist"
|
||||
aria-label="Advanced mode tab"
|
||||
aria-label={t('powerTabs.ariaLabel')}
|
||||
data-testid="power-tabs"
|
||||
style={{ marginBottom: 'var(--spacing-md)' }}
|
||||
>
|
||||
@@ -171,7 +163,7 @@ function PowerTabs({ value, onChange }) {
|
||||
data-testid="power-tab-preferences"
|
||||
>
|
||||
<i className="fas fa-sliders" aria-hidden="true" />
|
||||
Preferences
|
||||
{t('powerTabs.preferences')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -182,7 +174,7 @@ function PowerTabs({ value, onChange }) {
|
||||
data-testid="power-tab-yaml"
|
||||
>
|
||||
<i className="fas fa-code" aria-hidden="true" />
|
||||
YAML
|
||||
{t('powerTabs.yaml')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -193,6 +185,7 @@ function PowerTabs({ value, onChange }) {
|
||||
// component is 2-button (confirm/cancel); the UX here needs Keep / Discard
|
||||
// / Cancel with distinct semantics.
|
||||
function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
|
||||
const { t } = useTranslation('importModel')
|
||||
const keepRef = useRef(null)
|
||||
useEffect(() => {
|
||||
keepRef.current?.focus()
|
||||
@@ -216,10 +209,10 @@ function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="confirm-dialog-header">
|
||||
<span id="switch-mode-title" className="confirm-dialog-title">Keep your custom preferences?</span>
|
||||
<span id="switch-mode-title" className="confirm-dialog-title">{t('switchDialog.title')}</span>
|
||||
</div>
|
||||
<div id="switch-mode-body" className="confirm-dialog-body">
|
||||
Switching to Simple mode hides preferences beyond backend, name, and description. They’ll still be sent when you import.
|
||||
{t('switchDialog.body')}
|
||||
</div>
|
||||
<div className="confirm-dialog-actions">
|
||||
<button
|
||||
@@ -228,7 +221,7 @@ function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
|
||||
onClick={onCancel}
|
||||
data-testid="switch-mode-cancel"
|
||||
>
|
||||
Cancel
|
||||
{t('switchDialog.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -236,7 +229,7 @@ function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
|
||||
onClick={onDiscard}
|
||||
data-testid="switch-mode-discard"
|
||||
>
|
||||
Discard & switch
|
||||
{t('switchDialog.discard')}
|
||||
</button>
|
||||
<button
|
||||
ref={keepRef}
|
||||
@@ -245,7 +238,7 @@ function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
|
||||
onClick={onKeep}
|
||||
data-testid="switch-mode-keep"
|
||||
>
|
||||
Keep & switch
|
||||
{t('switchDialog.keep')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,6 +249,7 @@ function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
|
||||
export default function ImportModel() {
|
||||
const navigate = useNavigate()
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('importModel')
|
||||
|
||||
// Mode + tab state. Persisted to localStorage so reloads keep the user
|
||||
// on the same surface they last picked. `showOptions` is Simple-mode
|
||||
@@ -321,17 +315,17 @@ export default function ImportModel() {
|
||||
console.error('Failed to load /backends/known:', err)
|
||||
setBackendsError(true)
|
||||
setBackends([])
|
||||
addToast('Could not load backend list — using auto-detect only', 'warning')
|
||||
addToast(t('toasts.backendsLoadFailed'), 'warning')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setBackendsLoading(false)
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [addToast])
|
||||
}, [addToast, t])
|
||||
|
||||
const backendOptions = useMemo(
|
||||
() => buildBackendOptions(backends, modalityFilter),
|
||||
[backends, modalityFilter]
|
||||
() => buildBackendOptions(backends, modalityFilter, t),
|
||||
[backends, modalityFilter, t]
|
||||
)
|
||||
|
||||
// Progressive disclosure — hide preference fields that don't apply to the
|
||||
@@ -394,7 +388,7 @@ export default function ImportModel() {
|
||||
pollRef.current = null
|
||||
setIsSubmitting(false)
|
||||
setJobProgress(null)
|
||||
addToast('Model imported successfully!', 'success')
|
||||
addToast(t('toasts.imported'), 'success')
|
||||
navigate('/app/manage')
|
||||
} else if (data.error || (data.message && data.message.startsWith('error:'))) {
|
||||
clearInterval(pollRef.current)
|
||||
@@ -406,16 +400,16 @@ export default function ImportModel() {
|
||||
else if (data.error?.message) msg = data.error.message
|
||||
else if (data.message) msg = data.message
|
||||
if (msg.startsWith('error: ')) msg = msg.substring(7)
|
||||
addToast(`Import failed: ${msg}`, 'error')
|
||||
addToast(t('toasts.importFailed', { message: msg }), 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error polling job status:', err)
|
||||
}
|
||||
}, 1000)
|
||||
}, [addToast, navigate])
|
||||
}, [addToast, navigate, t])
|
||||
|
||||
const handleSimpleImport = useCallback(async (overrideBackend) => {
|
||||
if (!importUri.trim()) { addToast('Please enter a model URI', 'error'); return }
|
||||
if (!importUri.trim()) { addToast(t('toasts.noUri'), 'error'); return }
|
||||
setIsSubmitting(true)
|
||||
setEstimate(null)
|
||||
try {
|
||||
@@ -450,11 +444,12 @@ export default function ImportModel() {
|
||||
const jobId = result.uuid || result.ID
|
||||
if (!jobId) throw new Error('No job ID returned from server')
|
||||
|
||||
let msg = 'Import started! Tracking progress...'
|
||||
const parts = []
|
||||
if (hasSize) parts.push(`Size: ${result.estimated_size_display}`)
|
||||
if (hasVram) parts.push(`VRAM: ${result.estimated_vram_display}`)
|
||||
if (parts.length) msg += ` (${parts.join(' \u00b7 ')})`
|
||||
if (hasSize) parts.push(`${t('estimate.download', { size: result.estimated_size_display })}`)
|
||||
if (hasVram) parts.push(`${t('estimate.vram', { vram: result.estimated_vram_display })}`)
|
||||
const msg = parts.length
|
||||
? t('toasts.startedWithMeta', { meta: parts.join(' \u00b7 ') })
|
||||
: t('toasts.started')
|
||||
addToast(msg, 'success')
|
||||
// Clear any prior ambiguity alert once the server accepts the import.
|
||||
setAmbiguity(null)
|
||||
@@ -471,10 +466,10 @@ export default function ImportModel() {
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
addToast(`Failed to start import: ${err.message}`, 'error')
|
||||
addToast(t('toasts.startImportFailed', { message: err.message }), 'error')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}, [importUri, prefs, customPrefs, addToast, startJobPolling])
|
||||
}, [importUri, prefs, customPrefs, addToast, startJobPolling, t])
|
||||
|
||||
const pickAmbiguityCandidate = useCallback((backend) => {
|
||||
setPrefs(p => ({ ...p, backend }))
|
||||
@@ -514,20 +509,20 @@ export default function ImportModel() {
|
||||
const selected = backends.find(b => b.name === prefs.backend)
|
||||
if (selected && selected.modality !== next) {
|
||||
setPrefs(p => ({ ...p, backend: '' }))
|
||||
const label = (MODALITY_LABELS[next] || next)
|
||||
addToast(`Cleared backend selection — it wasn't in the ${label} group.`, 'info')
|
||||
const label = MODALITY_KEYS.includes(next) ? t(`modality.${next}`) : next
|
||||
addToast(t('toasts.modalityClearedBackend', { label }), 'info')
|
||||
}
|
||||
}, [backends, prefs.backend, addToast])
|
||||
}, [backends, prefs.backend, addToast, t])
|
||||
|
||||
const handleAdvancedImport = async () => {
|
||||
if (!yamlContent.trim()) { addToast('Please enter YAML configuration', 'error'); return }
|
||||
if (!yamlContent.trim()) { addToast(t('toasts.noYaml'), 'error'); return }
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await modelsApi.importConfig(yamlContent, 'application/x-yaml')
|
||||
addToast('Model configuration imported successfully!', 'success')
|
||||
addToast(t('toasts.importedYaml'), 'success')
|
||||
navigate('/app/manage')
|
||||
} catch (err) {
|
||||
addToast(`Import failed: ${err.message}`, 'error')
|
||||
addToast(t('toasts.importFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -537,10 +532,8 @@ export default function ImportModel() {
|
||||
const isPowerYaml = mode === 'power' && powerTab === 'yaml'
|
||||
|
||||
const subtitle = isSimple
|
||||
? 'Import a model from a URI — auto-detect picks the backend.'
|
||||
: (powerTab === 'yaml'
|
||||
? 'Write the full model YAML configuration.'
|
||||
: 'Fine-grained import preferences.')
|
||||
? t('subtitle.simple')
|
||||
: (powerTab === 'yaml' ? t('subtitle.powerYaml') : t('subtitle.powerPrefs'))
|
||||
|
||||
// The Ambiguity alert + URI input live at the top of both Simple and
|
||||
// Power/Preferences modes. Extracted so both branches stay readable.
|
||||
@@ -559,11 +552,11 @@ export default function ImportModel() {
|
||||
<div className="form-group">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-xs)' }}>
|
||||
<label className="form-label" style={{ marginBottom: 0 }}>
|
||||
Model URI
|
||||
{t('form.modelUri')}
|
||||
</label>
|
||||
<a href="https://huggingface.co/models?sort=trending" target="_blank" rel="noreferrer"
|
||||
className="btn btn-secondary" style={{ fontSize: '0.7rem', padding: '3px 8px' }}>
|
||||
Browse models on HF <i className="fas fa-external-link-alt" aria-hidden="true" style={{ marginLeft: 'var(--spacing-xs)' }} />
|
||||
{t('actions.browseHF')} <i className="fas fa-external-link-alt" aria-hidden="true" style={{ marginLeft: 'var(--spacing-xs)' }} />
|
||||
</a>
|
||||
</div>
|
||||
<input
|
||||
@@ -571,10 +564,10 @@ export default function ImportModel() {
|
||||
type="text"
|
||||
value={importUri}
|
||||
onChange={(e) => setImportUri(e.target.value)}
|
||||
placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
|
||||
placeholder={t('form.uriPlaceholder')}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<p style={hintStyle}>Enter the URI or path to the model file you want to import</p>
|
||||
<p style={hintStyle}>{t('form.uriHint')}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -583,7 +576,7 @@ export default function ImportModel() {
|
||||
>
|
||||
<i className={`fas ${showGuide ? 'fa-chevron-down' : 'fa-chevron-right'}`} aria-hidden="true" />
|
||||
<i className="fas fa-info-circle" aria-hidden="true" />
|
||||
Supported URI Formats
|
||||
{t('form.supportedFormats')}
|
||||
</button>
|
||||
{showGuide && (
|
||||
<div style={{ marginTop: 'var(--spacing-sm)', padding: 'var(--spacing-md)', background: 'var(--color-bg-primary)', border: '1px solid var(--color-border-default)', borderRadius: 'var(--radius-md)' }}>
|
||||
@@ -591,14 +584,14 @@ export default function ImportModel() {
|
||||
<div key={i} style={{ marginBottom: i < URI_FORMATS.length - 1 ? 'var(--spacing-md)' : 0 }}>
|
||||
<h4 style={{ fontSize: '0.8125rem', fontWeight: 600, marginBottom: '6px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<i className={fmt.icon} aria-hidden="true" style={{ color: fmt.color }} />
|
||||
{fmt.title}
|
||||
{t(fmt.titleKey)}
|
||||
</h4>
|
||||
<div style={{ paddingLeft: '20px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)' }}>
|
||||
{fmt.examples.map((ex, j) => (
|
||||
<div key={j} style={{ marginBottom: 'var(--spacing-xs)' }}>
|
||||
<code style={{ color: 'var(--color-success)' }}>{ex.prefix}</code>
|
||||
<span style={{ color: 'var(--color-text-secondary)' }}>{ex.suffix}</span>
|
||||
<p style={{ color: 'var(--color-text-muted)', marginTop: '1px', fontFamily: 'inherit' }}>{ex.desc}</p>
|
||||
<p style={{ color: 'var(--color-text-muted)', marginTop: '1px', fontFamily: 'inherit' }}>{t(ex.descKey)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -614,21 +607,21 @@ export default function ImportModel() {
|
||||
// and Power/Preferences.
|
||||
const renderBackendField = () => (
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Backend</label>
|
||||
<label className="form-label">{t('form.backend')}</label>
|
||||
<SearchableSelect
|
||||
value={prefs.backend}
|
||||
onChange={(v) => updatePref('backend', v)}
|
||||
options={backendOptions}
|
||||
allOption="Auto-detect (based on URI)"
|
||||
placeholder={backendsLoading ? 'Loading backends…' : 'Auto-detect (based on URI)'}
|
||||
searchPlaceholder="Search backends..."
|
||||
allOption={t('form.backendAuto')}
|
||||
placeholder={backendsLoading ? t('form.backendLoading') : t('form.backendAuto')}
|
||||
searchPlaceholder={t('form.backendSearch')}
|
||||
disabled={isSubmitting || backendsLoading}
|
||||
/>
|
||||
<p style={hintStyle}>
|
||||
Force a specific backend. Leave empty to auto-detect from the URI. Items marked “manual pick” aren’t auto-detectable — pick them yourself if you know what the model needs.
|
||||
{t('form.backendHint')}
|
||||
{backendsError && (
|
||||
<span style={{ color: 'var(--color-warning)', marginLeft: '6px' }}>
|
||||
Could not load backend list — auto-detect only.
|
||||
{t('form.backendErrorHint')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
@@ -642,7 +635,7 @@ export default function ImportModel() {
|
||||
style={{ ...hintStyle, display: 'flex', alignItems: 'center', gap: '6px', marginTop: '6px' }}
|
||||
>
|
||||
<i className="fas fa-download" aria-hidden="true" />
|
||||
This backend isn’t installed yet. Submitting import will download it first.
|
||||
{t('form.backendNotInstalled')}
|
||||
</p>
|
||||
)
|
||||
})()}
|
||||
@@ -651,17 +644,17 @@ export default function ImportModel() {
|
||||
|
||||
const renderNameField = () => (
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Model Name</label>
|
||||
<input className="input" type="text" value={prefs.name} onChange={e => updatePref('name', e.target.value)} placeholder="Leave empty to use filename" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Custom name for the model. If empty, the filename will be used.</p>
|
||||
<label className="form-label">{t('form.modelName')}</label>
|
||||
<input className="input" type="text" value={prefs.name} onChange={e => updatePref('name', e.target.value)} placeholder={t('form.modelNamePlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.modelNameHint')}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderDescriptionField = () => (
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Description</label>
|
||||
<textarea className="textarea" rows={2} value={prefs.description} onChange={e => updatePref('description', e.target.value)} placeholder="Leave empty to use default description" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Custom description for the model.</p>
|
||||
<label className="form-label">{t('form.description')}</label>
|
||||
<textarea className="textarea" rows={2} value={prefs.description} onChange={e => updatePref('description', e.target.value)} placeholder={t('form.descriptionPlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.descriptionHint')}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -669,7 +662,7 @@ export default function ImportModel() {
|
||||
const renderFullPreferences = () => (
|
||||
<div style={{ marginTop: 'var(--spacing-lg)' }}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-cog" aria-hidden="true" style={{ marginRight: '6px' }} />Preferences (Optional)
|
||||
<i className="fas fa-cog" aria-hidden="true" style={{ marginRight: '6px' }} />{t('form.preferences')}
|
||||
</div>
|
||||
|
||||
<ModalityChips
|
||||
@@ -681,7 +674,7 @@ export default function ImportModel() {
|
||||
<div style={{ padding: 'var(--spacing-md)', background: 'var(--color-bg-primary)', border: '1px solid var(--color-border-default)', borderRadius: 'var(--radius-md)' }}>
|
||||
<h3 style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-md)', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<i className="fas fa-sliders" style={{ color: 'var(--color-primary)' }} aria-hidden="true" />
|
||||
Common Preferences
|
||||
{t('form.commonPreferences')}
|
||||
</h3>
|
||||
|
||||
<div style={{ display: 'grid', gap: 'var(--spacing-md)' }}>
|
||||
@@ -691,17 +684,17 @@ export default function ImportModel() {
|
||||
|
||||
{showQuantizations && (
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Quantizations</label>
|
||||
<input className="input" type="text" value={prefs.quantizations} onChange={e => updatePref('quantizations', e.target.value)} placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Preferred quantizations (comma-separated). Leave empty for default (q4_k_m).</p>
|
||||
<label className="form-label">{t('form.quantizations')}</label>
|
||||
<input className="input" type="text" value={prefs.quantizations} onChange={e => updatePref('quantizations', e.target.value)} placeholder={t('form.quantizationsPlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.quantizationsHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showMmprojQuantizations && (
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">MMProj Quantizations</label>
|
||||
<input className="input" type="text" value={prefs.mmproj_quantizations} onChange={e => updatePref('mmproj_quantizations', e.target.value)} placeholder="fp16,fp32 (comma-separated)" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Preferred MMProj quantizations. Leave empty for default (fp16).</p>
|
||||
<label className="form-label">{t('form.mmprojQuantizations')}</label>
|
||||
<input className="input" type="text" value={prefs.mmproj_quantizations} onChange={e => updatePref('mmproj_quantizations', e.target.value)} placeholder={t('form.mmprojQuantizationsPlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.mmprojQuantizationsHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -709,45 +702,45 @@ export default function ImportModel() {
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={prefs.embeddings} onChange={e => updatePref('embeddings', e.target.checked)} disabled={isSubmitting} />
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-text-secondary)' }}>
|
||||
Embeddings
|
||||
{t('form.embeddings')}
|
||||
</span>
|
||||
</label>
|
||||
<p style={{ ...hintStyle, marginLeft: '28px' }}>Enable embeddings support for this model.</p>
|
||||
<p style={{ ...hintStyle, marginLeft: '28px' }}>{t('form.embeddingsHint')}</p>
|
||||
</div>
|
||||
|
||||
{showModelType && (
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Model Type</label>
|
||||
<input className="input" type="text" value={prefs.type} onChange={e => updatePref('type', e.target.value)} placeholder="AutoModelForCausalLM (for transformers backend)" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba.</p>
|
||||
<label className="form-label">{t('form.modelType')}</label>
|
||||
<input className="input" type="text" value={prefs.type} onChange={e => updatePref('type', e.target.value)} placeholder={t('form.modelTypePlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.modelTypeHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{prefs.backend === 'diffusers' && (
|
||||
<>
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Pipeline Type</label>
|
||||
<label className="form-label">{t('form.pipelineType')}</label>
|
||||
<input className="input" type="text" value={prefs.pipeline_type} onChange={e => updatePref('pipeline_type', e.target.value)} placeholder="StableDiffusionPipeline" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Pipeline type for diffusers backend.</p>
|
||||
<p style={hintStyle}>{t('form.pipelineTypeHint')}</p>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Scheduler Type</label>
|
||||
<input className="input" type="text" value={prefs.scheduler_type} onChange={e => updatePref('scheduler_type', e.target.value)} placeholder="k_dpmpp_2m (optional)" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim.</p>
|
||||
<label className="form-label">{t('form.schedulerType')}</label>
|
||||
<input className="input" type="text" value={prefs.scheduler_type} onChange={e => updatePref('scheduler_type', e.target.value)} placeholder={t('form.schedulerTypePlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.schedulerTypeHint')}</p>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Enable Parameters</label>
|
||||
<input className="input" type="text" value={prefs.enable_parameters} onChange={e => updatePref('enable_parameters', e.target.value)} placeholder="negative_prompt,num_inference_steps (comma-separated)" disabled={isSubmitting} />
|
||||
<p style={hintStyle}>Enabled parameters for diffusers backend (comma-separated).</p>
|
||||
<label className="form-label">{t('form.enableParameters')}</label>
|
||||
<input className="input" type="text" value={prefs.enable_parameters} onChange={e => updatePref('enable_parameters', e.target.value)} placeholder={t('form.enableParametersPlaceholder')} disabled={isSubmitting} />
|
||||
<p style={hintStyle}>{t('form.enableParametersHint')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={prefs.cuda} onChange={e => updatePref('cuda', e.target.checked)} disabled={isSubmitting} />
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-text-secondary)' }}>
|
||||
CUDA
|
||||
{t('form.cuda')}
|
||||
</span>
|
||||
</label>
|
||||
<p style={{ ...hintStyle, marginLeft: '28px' }}>Enable CUDA support for GPU acceleration.</p>
|
||||
<p style={{ ...hintStyle, marginLeft: '28px' }}>{t('form.cudaHint')}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -758,10 +751,10 @@ export default function ImportModel() {
|
||||
<div style={{ marginTop: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-sm)' }}>
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-text-secondary)' }}>
|
||||
<i className="fas fa-plus-circle" style={{ marginRight: '6px' }} aria-hidden="true" />Custom Preferences
|
||||
<i className="fas fa-plus-circle" style={{ marginRight: '6px' }} aria-hidden="true" />{t('form.customPreferences')}
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={addCustomPref} disabled={isSubmitting} style={{ fontSize: '0.75rem' }}>
|
||||
<i className="fas fa-plus" aria-hidden="true" /> Add Custom
|
||||
<i className="fas fa-plus" aria-hidden="true" /> {t('actions.addCustom')}
|
||||
</button>
|
||||
</div>
|
||||
{customPrefs.map((cp, i) => (
|
||||
@@ -771,8 +764,8 @@ export default function ImportModel() {
|
||||
type="text"
|
||||
value={cp.key}
|
||||
onChange={e => updateCustomPref(i, 'key', e.target.value)}
|
||||
placeholder="Key"
|
||||
aria-label={`Preference key for row ${i + 1}`}
|
||||
placeholder={t('form.key')}
|
||||
aria-label={t('form.preferenceKey', { index: i + 1 })}
|
||||
disabled={isSubmitting}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
@@ -782,8 +775,8 @@ export default function ImportModel() {
|
||||
type="text"
|
||||
value={cp.value}
|
||||
onChange={e => updateCustomPref(i, 'value', e.target.value)}
|
||||
placeholder="Value"
|
||||
aria-label={`Preference value for row ${i + 1}`}
|
||||
placeholder={t('form.value')}
|
||||
aria-label={t('form.preferenceValue', { index: i + 1 })}
|
||||
disabled={isSubmitting}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
@@ -791,14 +784,14 @@ export default function ImportModel() {
|
||||
className="btn btn-secondary"
|
||||
onClick={() => removeCustomPref(i)}
|
||||
disabled={isSubmitting}
|
||||
aria-label="Remove this preference"
|
||||
aria-label={t('form.removePref')}
|
||||
style={{ color: 'var(--color-error)' }}
|
||||
>
|
||||
<i className="fas fa-trash" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<p style={hintStyle}>Add custom key-value pairs for advanced configuration.</p>
|
||||
<p style={hintStyle}>{t('form.customKeyValueHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -807,18 +800,18 @@ export default function ImportModel() {
|
||||
<div className="page page--narrow">
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 'var(--spacing-sm)' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Import New Model</h1>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{subtitle}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<SimplePowerSwitch value={mode} onChange={requestModeSwitch} disabled={isSubmitting} />
|
||||
{isPowerYaml ? (
|
||||
<button className="btn btn-primary" onClick={handleAdvancedImport} disabled={isSubmitting}>
|
||||
{isSubmitting ? <><LoadingSpinner size="sm" /> Saving...</> : <><i className="fas fa-save" aria-hidden="true" /> Create</>}
|
||||
{isSubmitting ? <><LoadingSpinner size="sm" /> {t('actions.saving')}</> : <><i className="fas fa-save" aria-hidden="true" /> {t('actions.create')}</>}
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn btn-primary" onClick={() => handleSimpleImport()} disabled={isSubmitting || !importUri.trim()}>
|
||||
{isSubmitting ? <><LoadingSpinner size="sm" /> Importing...</> : <><i className="fas fa-upload" aria-hidden="true" /> Import Model</>}
|
||||
{isSubmitting ? <><LoadingSpinner size="sm" /> {t('actions.importing')}</> : <><i className="fas fa-upload" aria-hidden="true" /> {t('actions.import')}</>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -829,12 +822,12 @@ export default function ImportModel() {
|
||||
<div className="card" style={{ marginBottom: 'var(--spacing-md)', padding: 'var(--spacing-md)', borderColor: 'var(--color-primary)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)', fontSize: '0.875rem', flexWrap: 'wrap' }}>
|
||||
<i className="fas fa-memory" aria-hidden="true" style={{ color: 'var(--color-primary)' }} />
|
||||
<strong>Estimated requirements</strong>
|
||||
<strong>{t('estimate.title')}</strong>
|
||||
{estimate.sizeDisplay && estimate.sizeDisplay !== '0 B' && (
|
||||
<span><i className="fas fa-download" aria-hidden="true" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} />Download: {estimate.sizeDisplay}</span>
|
||||
<span><i className="fas fa-download" aria-hidden="true" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} />{t('estimate.download', { size: estimate.sizeDisplay })}</span>
|
||||
)}
|
||||
{estimate.vramDisplay && estimate.vramDisplay !== '0 B' && (
|
||||
<span><i className="fas fa-microchip" aria-hidden="true" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} />VRAM: {estimate.vramDisplay}</span>
|
||||
<span><i className="fas fa-microchip" aria-hidden="true" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} />{t('estimate.vram', { vram: estimate.vramDisplay })}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -885,7 +878,7 @@ export default function ImportModel() {
|
||||
>
|
||||
<i className={`fas ${showOptions ? 'fa-chevron-down' : 'fa-chevron-right'}`} aria-hidden="true" />
|
||||
<i className="fas fa-sliders" aria-hidden="true" />
|
||||
Options
|
||||
{t('form.options')}
|
||||
</button>
|
||||
|
||||
{showOptions && (
|
||||
@@ -941,10 +934,10 @@ export default function ImportModel() {
|
||||
<div style={{ padding: 'var(--spacing-md)', borderTop: '1px solid var(--color-border-default)', borderBottom: '1px solid var(--color-border-default)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-code" aria-hidden="true" style={{ color: 'var(--color-data-3)' }} />
|
||||
YAML Configuration Editor
|
||||
{t('form.yamlEditor')}
|
||||
</h2>
|
||||
<button className="btn btn-secondary" style={{ fontSize: '0.75rem' }} onClick={() => { navigator.clipboard.writeText(yamlContent); addToast('Copied to clipboard', 'success') }}>
|
||||
<i className="fas fa-copy" aria-hidden="true" /> Copy
|
||||
<button className="btn btn-secondary" style={{ fontSize: '0.75rem' }} onClick={() => { navigator.clipboard.writeText(yamlContent); addToast(t('toasts.copied'), 'success') }}>
|
||||
<i className="fas fa-copy" aria-hidden="true" /> {t('actions.copy')}
|
||||
</button>
|
||||
</div>
|
||||
<CodeEditor value={yamlContent} onChange={setYamlContent} disabled={isSubmitting} minHeight="calc(100vh - 400px)" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useBranding } from '../contexts/BrandingContext'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
@@ -7,6 +8,7 @@ import './auth.css'
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('auth')
|
||||
const { code: urlInviteCode } = useParams()
|
||||
const [searchParams] = useSearchParams()
|
||||
const { authEnabled, staticApiKeyRequired, user, loading: authLoading, refresh } = useAuth()
|
||||
@@ -49,9 +51,9 @@ export default function Login() {
|
||||
useEffect(() => {
|
||||
const errorParam = searchParams.get('error')
|
||||
if (errorParam === 'invite_required') {
|
||||
setError('A valid invite code is required to register')
|
||||
setError(t('login.errors.inviteRequired'))
|
||||
}
|
||||
}, [searchParams])
|
||||
}, [searchParams, t])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(apiUrl('/api/auth/status'))
|
||||
@@ -88,14 +90,14 @@ export default function Login() {
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
setError(extractError(data, 'Login failed'))
|
||||
setError(extractError(data, t('login.errors.loginFailed')))
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
await refresh()
|
||||
} catch {
|
||||
setError('Network error')
|
||||
setError(t('login.errors.networkError'))
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
@@ -106,7 +108,7 @@ export default function Login() {
|
||||
setMessage('')
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match')
|
||||
setError(t('login.errors.passwordsDoNotMatch'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,13 +128,13 @@ export default function Login() {
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
setError(extractError(data, 'Registration failed'))
|
||||
setError(extractError(data, t('login.errors.registrationFailed')))
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.pending) {
|
||||
setMessage(data.message || 'Registration successful, awaiting approval.')
|
||||
setMessage(data.message || t('login.messages.registrationPending'))
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
@@ -141,7 +143,7 @@ export default function Login() {
|
||||
window.location.href = '/app'
|
||||
return
|
||||
} catch {
|
||||
setError('Network error')
|
||||
setError(t('login.errors.networkError'))
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
@@ -149,7 +151,7 @@ export default function Login() {
|
||||
const handleTokenLogin = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!token.trim()) {
|
||||
setError('Please enter a token')
|
||||
setError(t('login.errors.enterToken'))
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
@@ -164,14 +166,14 @@ export default function Login() {
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
setError(extractError(data, 'Invalid token'))
|
||||
setError(extractError(data, t('login.errors.invalidToken')))
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
await refresh()
|
||||
} catch {
|
||||
setError('Network error')
|
||||
setError(t('login.errors.networkError'))
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +189,7 @@ export default function Login() {
|
||||
<img src={apiUrl(branding.logoUrl)} alt={branding.instanceName} className="login-logo" />
|
||||
<h1 className="login-title">{branding.instanceName}</h1>
|
||||
{branding.instanceTagline && <p className="login-tagline">{branding.instanceTagline}</p>}
|
||||
<p className="login-subtitle">Enter your API key to continue</p>
|
||||
<p className="login-subtitle">{t('login.tokenSubtitle')}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -201,12 +203,12 @@ export default function Login() {
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => { setToken(e.target.value); setError('') }}
|
||||
placeholder="Enter API key..."
|
||||
placeholder={t('login.tokenPlaceholder')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary login-btn-full" disabled={submitting}>
|
||||
{submitting ? 'Signing in...' : 'Sign In'}
|
||||
{submitting ? t('login.signingIn') : t('login.signIn')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -238,7 +240,11 @@ export default function Login() {
|
||||
<h1 className="login-title">{branding.instanceName}</h1>
|
||||
{branding.instanceTagline && <p className="login-tagline">{branding.instanceTagline}</p>}
|
||||
<p className="login-subtitle">
|
||||
{!hasUsers ? 'Create your admin account' : mode === 'register' ? 'Create an account' : 'Sign in to continue'}
|
||||
{!hasUsers
|
||||
? t('login.createAdminSubtitle')
|
||||
: mode === 'register'
|
||||
? t('login.registerSubtitle')
|
||||
: t('login.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -256,7 +262,7 @@ export default function Login() {
|
||||
className="btn btn-primary login-btn-full"
|
||||
style={{ marginBottom: hasOIDC ? '0.5rem' : undefined }}
|
||||
>
|
||||
<i className="fab fa-github" /> Sign in with GitHub
|
||||
<i className="fab fa-github" /> {t('login.signInWithGitHub')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -265,47 +271,47 @@ export default function Login() {
|
||||
href={oidcLoginUrl}
|
||||
className="btn btn-primary login-btn-full"
|
||||
>
|
||||
<i className="fas fa-sign-in-alt" /> Sign in with SSO
|
||||
<i className="fas fa-sign-in-alt" /> {t('login.signInWithSSO')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{hasOAuth && hasLocal && (
|
||||
<div className="login-divider">or</div>
|
||||
<div className="login-divider">{t('login.or')}</div>
|
||||
)}
|
||||
|
||||
{hasLocal && mode === 'login' && (
|
||||
<form onSubmit={handleEmailLogin}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Email</label>
|
||||
<label className="form-label">{t('login.email')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => { setEmail(e.target.value); setError('') }}
|
||||
placeholder="you@example.com"
|
||||
placeholder={t('login.emailPlaceholder')}
|
||||
autoFocus={!hasGitHub}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Password</label>
|
||||
<label className="form-label">{t('login.password')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => { setPassword(e.target.value); setError('') }}
|
||||
placeholder="Enter password..."
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary login-btn-full" disabled={submitting}>
|
||||
{submitting ? 'Signing in...' : 'Sign In'}
|
||||
{submitting ? t('login.signingIn') : t('login.signIn')}
|
||||
</button>
|
||||
{!(registrationMode === 'invite' && hasUsers && !urlInviteCode) && (
|
||||
<p className="login-footer">
|
||||
Don't have an account?{' '}
|
||||
{t('login.noAccount')}{' '}
|
||||
<button type="button" className="login-link" onClick={() => { setMode('register'); setError(''); setMessage('') }}>
|
||||
Register
|
||||
{t('login.register')}
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
@@ -317,72 +323,76 @@ export default function Login() {
|
||||
{showInviteField && (
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
Invite Code{inviteRequired ? '' : ' (optional — skip the approval wait)'}
|
||||
{t('login.inviteCodeLabel')}{inviteRequired ? '' : t('login.inviteCodeOptional')}
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={inviteCode}
|
||||
onChange={(e) => { setInviteCode(e.target.value); setError('') }}
|
||||
placeholder="Paste your invite code..."
|
||||
placeholder={t('login.inviteCodePlaceholder')}
|
||||
required={inviteRequired}
|
||||
readOnly={!!urlInviteCode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label className="form-label">Email</label>
|
||||
<label className="form-label">{t('login.email')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => { setEmail(e.target.value); setError('') }}
|
||||
placeholder="you@example.com"
|
||||
placeholder={t('login.emailPlaceholder')}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Name</label>
|
||||
<label className="form-label">{t('login.name')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your name (optional)"
|
||||
placeholder={t('login.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Password</label>
|
||||
<label className="form-label">{t('login.password')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => { setPassword(e.target.value); setError('') }}
|
||||
placeholder="At least 8 characters"
|
||||
placeholder={t('login.newPasswordPlaceholder')}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Confirm Password</label>
|
||||
<label className="form-label">{t('login.confirmPassword')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setError('') }}
|
||||
placeholder="Repeat password"
|
||||
placeholder={t('login.confirmPasswordPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary login-btn-full" disabled={submitting}>
|
||||
{submitting ? 'Creating account...' : !hasUsers ? 'Create Admin Account' : 'Register'}
|
||||
{submitting
|
||||
? t('login.creatingAccount')
|
||||
: !hasUsers
|
||||
? t('login.createAdminAccount')
|
||||
: t('login.register')}
|
||||
</button>
|
||||
{hasUsers && (
|
||||
<p className="login-footer">
|
||||
Already have an account?{' '}
|
||||
{t('login.hasAccount')}{' '}
|
||||
<button type="button" className="login-link" onClick={() => { setMode('login'); setError(''); setMessage('') }}>
|
||||
Sign in
|
||||
{t('login.signIn')}
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
@@ -395,7 +405,7 @@ export default function Login() {
|
||||
type="button"
|
||||
onClick={() => setShowTokenLogin(!showTokenLogin)}
|
||||
>
|
||||
{showTokenLogin ? 'Hide token login' : 'Login with API Token'}
|
||||
{showTokenLogin ? t('login.hideTokenLogin') : t('login.showTokenLogin')}
|
||||
</button>
|
||||
{showTokenLogin && (
|
||||
<form onSubmit={handleTokenLogin} className="login-token-form">
|
||||
@@ -405,11 +415,11 @@ export default function Login() {
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => { setToken(e.target.value); setError('') }}
|
||||
placeholder="Enter API token..."
|
||||
placeholder={t('login.tokenAltPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-secondary login-btn-full" disabled={submitting}>
|
||||
<i className="fas fa-key" /> Login with Token
|
||||
<i className="fas fa-key" /> {t('login.loginWithToken')}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ResourceMonitor from '../components/ResourceMonitor'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import NodeDistributionChip from '../components/NodeDistributionChip'
|
||||
@@ -109,9 +110,10 @@ function formatBackendVersion(metadata) {
|
||||
export default function Manage() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('admin')
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const initialTab = searchParams.get('tab') || localStorage.getItem('manage-tab') || 'models'
|
||||
const [activeTab, setActiveTab] = useState(TABS.some(t => t.key === initialTab) ? initialTab : 'models')
|
||||
const [activeTab, setActiveTab] = useState(TABS.some(tab => tab.key === initialTab) ? initialTab : 'models')
|
||||
const { models, loading: modelsLoading, refetch: refetchModels } = useModels()
|
||||
const { enrichModel, enrichBackend } = useGalleryEnrichment()
|
||||
const [loadedModelIds, setLoadedModelIds] = useState(new Set())
|
||||
@@ -428,8 +430,8 @@ export default function Manage() {
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">System</h1>
|
||||
<p className="page-subtitle">Manage installed models and backends</p>
|
||||
<h1 className="page-title">{t('manage.title')}</h1>
|
||||
<p className="page-subtitle">{t('manage.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Resource Monitor */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { modelsApi } from '../utils/api'
|
||||
import { useDebouncedCallback } from '../hooks/useDebounce'
|
||||
import { useOperations } from '../hooks/useOperations'
|
||||
@@ -11,20 +12,21 @@ import React from 'react'
|
||||
|
||||
|
||||
const FILTERS = [
|
||||
{ key: '', label: 'All', icon: 'fa-layer-group' },
|
||||
{ key: 'llm', label: 'LLM', icon: 'fa-brain' },
|
||||
{ key: 'sd', label: 'Image', icon: 'fa-image' },
|
||||
{ key: 'multimodal', label: 'Multimodal', icon: 'fa-shapes' },
|
||||
{ key: 'vision', label: 'Vision', icon: 'fa-eye' },
|
||||
{ key: 'tts', label: 'TTS', icon: 'fa-microphone' },
|
||||
{ key: 'stt', label: 'STT', icon: 'fa-headphones' },
|
||||
{ key: 'embedding', label: 'Embedding', icon: 'fa-vector-square' },
|
||||
{ key: 'reranker', label: 'Rerank', icon: 'fa-sort' },
|
||||
{ key: '', labelKey: 'filters.all', icon: 'fa-layer-group' },
|
||||
{ key: 'llm', labelKey: 'filters.llm', icon: 'fa-brain' },
|
||||
{ key: 'sd', labelKey: 'filters.image', icon: 'fa-image' },
|
||||
{ key: 'multimodal', labelKey: 'filters.multimodal', icon: 'fa-shapes' },
|
||||
{ key: 'vision', labelKey: 'filters.vision', icon: 'fa-eye' },
|
||||
{ key: 'tts', labelKey: 'filters.tts', icon: 'fa-microphone' },
|
||||
{ key: 'stt', labelKey: 'filters.stt', icon: 'fa-headphones' },
|
||||
{ key: 'embedding', labelKey: 'filters.embedding', icon: 'fa-vector-square' },
|
||||
{ key: 'reranker', labelKey: 'filters.rerank', icon: 'fa-sort' },
|
||||
]
|
||||
|
||||
export default function Models() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('models')
|
||||
const { operations } = useOperations()
|
||||
const { resources } = useResources()
|
||||
const [models, setModels] = useState([])
|
||||
@@ -73,11 +75,11 @@ export default function Models() {
|
||||
})
|
||||
setAllBackends(data?.allBackends || [])
|
||||
} catch (err) {
|
||||
addToast(`Failed to load models: ${err.message}`, 'error')
|
||||
addToast(t('errors.loadFailed', { message: err.message }), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, search, filter, sort, order, backendFilter, addToast])
|
||||
}, [page, search, filter, sort, order, backendFilter, addToast, t])
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels()
|
||||
@@ -112,24 +114,24 @@ export default function Models() {
|
||||
setInstalling(prev => new Map(prev).set(modelId, Date.now()))
|
||||
await modelsApi.install(modelId)
|
||||
} catch (err) {
|
||||
addToast(`Failed to install: ${err.message}`, 'error')
|
||||
addToast(t('errors.installFailed', { message: err.message }), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (modelId) => {
|
||||
setConfirmDialog({
|
||||
title: 'Delete Model',
|
||||
message: `Delete model ${modelId}?`,
|
||||
confirmLabel: `Delete ${modelId}`,
|
||||
title: t('deleteDialog.title'),
|
||||
message: t('deleteDialog.message', { model: modelId }),
|
||||
confirmLabel: t('deleteDialog.confirm', { model: modelId }),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await modelsApi.delete(modelId)
|
||||
addToast(`Deleting ${modelId}...`, 'info')
|
||||
addToast(t('deleteDialog.deletingToast', { model: modelId }), 'info')
|
||||
fetchModels()
|
||||
} catch (err) {
|
||||
addToast(`Failed to delete: ${err.message}`, 'error')
|
||||
addToast(t('errors.deleteFailed', { message: err.message }), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -180,27 +182,27 @@ export default function Models() {
|
||||
<div className="page page--wide">
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Install Models</h1>
|
||||
<p className="page-subtitle">Browse and install AI models from the gallery</p>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('subtitle')}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', fontSize: '0.8125rem' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-primary)' }}>{stats.total}</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>Available</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>{t('stats.available')}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<a onClick={() => navigate('/app/manage')} style={{ cursor: 'pointer' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-success)' }}>{stats.installed}</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>Installed</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>{t('stats.installed')}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => navigate('/app/model-editor')}>
|
||||
<i className="fas fa-plus" /> Add Model
|
||||
<i className="fas fa-plus" /> {t('actions.addModel')}
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate('/app/import-model')}>
|
||||
<i className="fas fa-upload" /> Import Model
|
||||
<i className="fas fa-upload" /> {t('actions.importModel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,7 +213,7 @@ export default function Models() {
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Search models..."
|
||||
placeholder={t('search.placeholder')}
|
||||
value={search}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
@@ -226,7 +228,7 @@ export default function Models() {
|
||||
onClick={() => { setFilter(f.key); setPage(1) }}
|
||||
>
|
||||
<i className={`fas ${f.icon}`} style={{ marginRight: 4 }} />
|
||||
{f.label}
|
||||
{t(f.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
{allBackends.length > 0 && (
|
||||
@@ -234,9 +236,9 @@ export default function Models() {
|
||||
value={backendFilter}
|
||||
onChange={(v) => { setBackendFilter(v); setPage(1) }}
|
||||
options={allBackends}
|
||||
placeholder="All Backends"
|
||||
allOption="All Backends"
|
||||
searchPlaceholder="Search backends..."
|
||||
placeholder={t('filters.allBackends')}
|
||||
allOption={t('filters.allBackends')}
|
||||
searchPlaceholder={t('filters.searchBackends')}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
@@ -248,18 +250,16 @@ export default function Models() {
|
||||
) : models.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-search" /></div>
|
||||
<h2 className="empty-state-title">No models found</h2>
|
||||
<h2 className="empty-state-title">{t('empty.title')}</h2>
|
||||
<p className="empty-state-text">
|
||||
{search || filter || backendFilter
|
||||
? 'No models match your current search or filters.'
|
||||
: 'The model gallery is empty.'}
|
||||
{search || filter || backendFilter ? t('empty.withFilters') : t('empty.noFilters')}
|
||||
</p>
|
||||
{(search || filter || backendFilter) && (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => { handleSearch(''); setFilter(''); setBackendFilter(''); setPage(1) }}
|
||||
>
|
||||
<i className="fas fa-times" /> Clear filters
|
||||
<i className="fas fa-times" /> {t('search.clearFilters')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -272,15 +272,15 @@ export default function Models() {
|
||||
<th style={{ width: '30px' }}></th>
|
||||
<th style={{ width: '60px' }}></th>
|
||||
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('name')}>
|
||||
Model Name {sort === 'name' && <i className={`fas fa-arrow-${order === 'asc' ? 'up' : 'down'}`} style={{ fontSize: '0.625rem' }} />}
|
||||
{t('table.modelName')} {sort === 'name' && <i className={`fas fa-arrow-${order === 'asc' ? 'up' : 'down'}`} style={{ fontSize: '0.625rem' }} />}
|
||||
</th>
|
||||
<th>Description</th>
|
||||
<th>Backend</th>
|
||||
<th>Size / VRAM</th>
|
||||
<th>{t('table.description')}</th>
|
||||
<th>{t('table.backend')}</th>
|
||||
<th>{t('table.sizeVram')}</th>
|
||||
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('status')}>
|
||||
Status {sort === 'status' && <i className={`fas fa-arrow-${order === 'asc' ? 'up' : 'down'}`} style={{ fontSize: '0.625rem' }} />}
|
||||
{t('table.status')} {sort === 'status' && <i className={`fas fa-arrow-${order === 'asc' ? 'up' : 'down'}`} style={{ fontSize: '0.625rem' }} />}
|
||||
</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
<th style={{ textAlign: 'right' }}>{t('table.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -324,7 +324,7 @@ export default function Models() {
|
||||
{model.trustRemoteCode && (
|
||||
<div style={{ marginTop: '2px' }}>
|
||||
<span className="badge badge-error" style={{ fontSize: '0.625rem' }}>
|
||||
<i className="fas fa-circle-exclamation" /> Trust Remote Code
|
||||
<i className="fas fa-circle-exclamation" /> {t('table.trustRemoteCode')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -359,18 +359,18 @@ export default function Models() {
|
||||
<>
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-secondary)' }}>
|
||||
{model.estimated_size_display && model.estimated_size_display !== '0 B' && (
|
||||
<span>Size: {model.estimated_size_display}</span>
|
||||
<span>{t('table.size', { size: model.estimated_size_display })}</span>
|
||||
)}
|
||||
{model.estimated_size_display && model.estimated_size_display !== '0 B' && model.estimated_vram_display && model.estimated_vram_display !== '0 B' && ' · '}
|
||||
{model.estimated_vram_display && model.estimated_vram_display !== '0 B' && (
|
||||
<span>VRAM: {model.estimated_vram_display}</span>
|
||||
<span>{t('table.vram', { vram: model.estimated_vram_display })}</span>
|
||||
)}
|
||||
</span>
|
||||
{fit !== null && (
|
||||
<span style={{ fontSize: '0.6875rem', display: 'flex', alignItems: 'center', gap: 'var(--spacing-xs)' }}>
|
||||
<i className="fas fa-microchip" style={{ color: fit ? 'var(--color-success)' : 'var(--color-error)' }} />
|
||||
<span style={{ color: fit ? 'var(--color-success)' : 'var(--color-error)' }}>
|
||||
{fit ? 'Fits' : 'May not fit'}
|
||||
{fit ? t('table.fits') : t('table.mayNotFit')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
@@ -387,7 +387,9 @@ export default function Models() {
|
||||
<div className="inline-install">
|
||||
<div className="inline-install__row">
|
||||
<div className="operation-spinner" />
|
||||
<span className="inline-install__label">Installing{progress > 0 ? ` · ${Math.round(progress)}%` : '...'}</span>
|
||||
<span className="inline-install__label">
|
||||
{progress > 0 ? t('table.installingPct', { percent: Math.round(progress) }) : `${t('table.installing')}...`}
|
||||
</span>
|
||||
</div>
|
||||
{progress > 0 && (
|
||||
<div className="operation-bar-container" style={{ flex: 'none', width: '120px', marginTop: 4 }}>
|
||||
@@ -397,11 +399,11 @@ export default function Models() {
|
||||
</div>
|
||||
) : model.installed ? (
|
||||
<span className="badge badge-success">
|
||||
<i className="fas fa-check-circle" /> Installed
|
||||
<i className="fas fa-check-circle" /> {t('table.installed')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge" style={{ background: 'var(--color-surface-sunken)', color: 'var(--color-text-muted)', border: '1px solid var(--color-border-default)' }}>
|
||||
<i className="fas fa-circle" /> Not Installed
|
||||
<i className="fas fa-circle" /> {t('table.notInstalled')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -411,10 +413,10 @@ export default function Models() {
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-xs)', justifyContent: 'flex-end' }} onClick={e => e.stopPropagation()}>
|
||||
{model.installed ? (
|
||||
<>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleInstall(name)} title="Reinstall">
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleInstall(name)} title={t('actions.reinstall')}>
|
||||
<i className="fas fa-rotate" />
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name)} title="Delete">
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name)} title={t('actions.delete')}>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</>
|
||||
@@ -423,7 +425,7 @@ export default function Models() {
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => handleInstall(name)}
|
||||
disabled={installing}
|
||||
title="Install"
|
||||
title={t('actions.install')}
|
||||
>
|
||||
<i className="fas fa-download" />
|
||||
</button>
|
||||
@@ -435,7 +437,7 @@ export default function Models() {
|
||||
{isExpanded && (
|
||||
<tr>
|
||||
<td colSpan="8" style={{ padding: 0 }}>
|
||||
<ModelDetail model={model} fit={fit} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} />
|
||||
<ModelDetail model={model} fit={fit} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} t={t} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -488,50 +490,50 @@ function DetailRow({ label, children }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelDetail({ model, fit, expandedFiles, setExpandedFiles }) {
|
||||
function ModelDetail({ model, fit, expandedFiles, setExpandedFiles, t }) {
|
||||
const files = model.additionalFiles || model.files || []
|
||||
return (
|
||||
<div style={{ padding: 'var(--spacing-md) var(--spacing-lg)', background: 'var(--color-bg-primary)', borderTop: '1px solid var(--color-border-subtle)' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
<DetailRow label="Description">
|
||||
<DetailRow label={t('detail.description')}>
|
||||
{model.description && (
|
||||
<span style={{ color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>{model.description}</span>
|
||||
)}
|
||||
</DetailRow>
|
||||
<DetailRow label="Gallery">
|
||||
<DetailRow label={t('detail.gallery')}>
|
||||
{model.gallery && (
|
||||
<span className="badge badge-info" style={{ fontSize: '0.6875rem' }}>
|
||||
{typeof model.gallery === 'string' ? model.gallery : model.gallery.name || '—'}
|
||||
</span>
|
||||
)}
|
||||
</DetailRow>
|
||||
<DetailRow label="Backend">
|
||||
<DetailRow label={t('detail.backend')}>
|
||||
{model.backend && (
|
||||
<span className="badge badge-info" style={{ fontSize: '0.6875rem' }}>
|
||||
{model.backend}
|
||||
</span>
|
||||
)}
|
||||
</DetailRow>
|
||||
<DetailRow label="Size">
|
||||
<DetailRow label={t('detail.size')}>
|
||||
{model.estimated_size_display && model.estimated_size_display !== '0 B' ? model.estimated_size_display : null}
|
||||
</DetailRow>
|
||||
<DetailRow label="VRAM">
|
||||
<DetailRow label={t('detail.vram')}>
|
||||
{model.estimated_vram_display && model.estimated_vram_display !== '0 B' ? (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)' }}>
|
||||
{model.estimated_vram_display}
|
||||
{fit !== null && (
|
||||
<span style={{ fontSize: '0.75rem', color: fit ? 'var(--color-success)' : 'var(--color-error)' }}>
|
||||
<i className="fas fa-microchip" /> {fit ? 'Fits in GPU' : 'May not fit in GPU'}
|
||||
<i className="fas fa-microchip" /> {fit ? t('detail.fitsGpu') : t('detail.mayNotFitGpu')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</DetailRow>
|
||||
<DetailRow label="License">
|
||||
<DetailRow label={t('detail.license')}>
|
||||
{model.license && <span>{model.license}</span>}
|
||||
</DetailRow>
|
||||
<DetailRow label="Tags">
|
||||
<DetailRow label={t('detail.tags')}>
|
||||
{model.tags?.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-xs)', flexWrap: 'wrap' }}>
|
||||
{model.tags.map(tag => (
|
||||
@@ -540,7 +542,7 @@ function ModelDetail({ model, fit, expandedFiles, setExpandedFiles }) {
|
||||
</div>
|
||||
)}
|
||||
</DetailRow>
|
||||
<DetailRow label="Links">
|
||||
<DetailRow label={t('detail.links')}>
|
||||
{model.urls?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||
{model.urls.map((url, i) => (
|
||||
@@ -552,14 +554,14 @@ function ModelDetail({ model, fit, expandedFiles, setExpandedFiles }) {
|
||||
)}
|
||||
</DetailRow>
|
||||
{model.trustRemoteCode && (
|
||||
<DetailRow label="Warning">
|
||||
<DetailRow label={t('detail.warning')}>
|
||||
<span className="badge badge-error" style={{ fontSize: '0.6875rem' }}>
|
||||
<i className="fas fa-circle-exclamation" /> Requires Trust Remote Code
|
||||
<i className="fas fa-circle-exclamation" /> {t('detail.requiresTrustRemoteCode')}
|
||||
</span>
|
||||
</DetailRow>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<DetailRow label="Files">
|
||||
<DetailRow label={t('detail.files')}>
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
@@ -567,16 +569,16 @@ function ModelDetail({ model, fit, expandedFiles, setExpandedFiles }) {
|
||||
style={{ marginBottom: expandedFiles ? 'var(--spacing-sm)' : 0 }}
|
||||
>
|
||||
<i className={`fas fa-chevron-${expandedFiles ? 'down' : 'right'}`} style={{ fontSize: '0.5rem', marginRight: 4 }} />
|
||||
{files.length} file{files.length !== 1 ? 's' : ''}
|
||||
{t('detail.fileCount', { count: files.length })}
|
||||
</button>
|
||||
{expandedFiles && (
|
||||
<div style={{ border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--color-bg-tertiary)' }}>
|
||||
<th style={{ padding: 'var(--spacing-xs) var(--spacing-sm)', textAlign: 'left', fontWeight: 500 }}>Filename</th>
|
||||
<th style={{ padding: 'var(--spacing-xs) var(--spacing-sm)', textAlign: 'left', fontWeight: 500 }}>URI</th>
|
||||
<th style={{ padding: 'var(--spacing-xs) var(--spacing-sm)', textAlign: 'left', fontWeight: 500 }}>SHA256</th>
|
||||
<th style={{ padding: 'var(--spacing-xs) var(--spacing-sm)', textAlign: 'left', fontWeight: 500 }}>{t('detail.filename')}</th>
|
||||
<th style={{ padding: 'var(--spacing-xs) var(--spacing-sm)', textAlign: 'left', fontWeight: 500 }}>{t('detail.uri')}</th>
|
||||
<th style={{ padding: 'var(--spacing-xs) var(--spacing-sm)', textAlign: 'left', fontWeight: 500 }}>{t('detail.sha256')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, Fragment } from 'react'
|
||||
import { useOutletContext, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { nodesApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
@@ -610,6 +611,7 @@ function SchedulingForm({ onSave, onCancel }) {
|
||||
export default function Nodes() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('admin')
|
||||
const [nodesList, setNodesList] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [enabled, setEnabled] = useState(true)
|
||||
@@ -904,10 +906,10 @@ export default function Nodes() {
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-network-wired" style={{ marginRight: 'var(--spacing-sm)' }} />
|
||||
Distributed Nodes
|
||||
{t('nodes.title')}
|
||||
</h1>
|
||||
<p className="page-subtitle">
|
||||
Manage backend and agent worker nodes
|
||||
{t('nodes.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function NotFound() {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('auth')
|
||||
|
||||
return (
|
||||
<div className="page page--narrow">
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-compass" /></div>
|
||||
<h1 className="empty-state-title" style={{ fontSize: '3rem' }}>404</h1>
|
||||
<h2 className="empty-state-title">Page Not Found</h2>
|
||||
<p className="empty-state-text">Looks like this page wandered off. Let's get you back on track.</p>
|
||||
<h2 className="empty-state-title">{t('notFound.title')}</h2>
|
||||
<p className="empty-state-text">{t('notFound.text')}</p>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app')}>
|
||||
<i className="fas fa-home" /> Go Home
|
||||
<i className="fas fa-home" /> {t('notFound.goHome')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { p2pApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import ImageSelector, { useImageSelector, dockerImage, dockerFlags } from '../components/ImageSelector'
|
||||
@@ -103,6 +104,7 @@ function StepNumber({ n, bg, color }) {
|
||||
|
||||
export default function P2P() {
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('admin')
|
||||
const [workers, setWorkers] = useState([])
|
||||
const [mlxWorkers, setMlxWorkers] = useState([])
|
||||
const [federation, setFederation] = useState([])
|
||||
@@ -296,10 +298,10 @@ export default function P2P() {
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-circle-nodes" style={{ marginRight: 'var(--spacing-sm)' }} />
|
||||
Distributed AI Computing
|
||||
{t('p2p.title')}
|
||||
</h1>
|
||||
<p className="page-subtitle">
|
||||
Scale your AI workloads across multiple devices with peer-to-peer distribution
|
||||
{t('p2p.subtitle')}
|
||||
{' '}
|
||||
<a href="https://localai.io/features/distribute/" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: 'var(--color-primary)' }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { settingsApi, resourcesApi, brandingApi } from '../utils/api'
|
||||
import { useBranding } from '../contexts/BrandingContext'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -10,20 +11,20 @@ import SettingRow from '../components/SettingRow'
|
||||
import { formatBytes, percentColor } from '../utils/format'
|
||||
|
||||
const SECTIONS = [
|
||||
{ id: 'branding', icon: 'fa-palette', color: 'var(--color-primary)', label: 'Branding' },
|
||||
{ id: 'watchdog', icon: 'fa-shield-halved', color: 'var(--color-primary)', label: 'Watchdog' },
|
||||
{ id: 'memory', icon: 'fa-memory', color: 'var(--color-accent)', label: 'Memory' },
|
||||
{ id: 'backends', icon: 'fa-cogs', color: 'var(--color-accent)', label: 'Backends' },
|
||||
{ id: 'performance', icon: 'fa-gauge-high', color: 'var(--color-success)', label: 'Performance' },
|
||||
{ id: 'tracing', icon: 'fa-bug', color: 'var(--color-warning)', label: 'Tracing' },
|
||||
{ id: 'api', icon: 'fa-globe', color: 'var(--color-warning)', label: 'API & CORS' },
|
||||
{ id: 'p2p', icon: 'fa-network-wired', color: 'var(--color-accent)', label: 'P2P' },
|
||||
{ id: 'galleries', icon: 'fa-images', color: 'var(--color-accent)', label: 'Galleries' },
|
||||
{ id: 'apikeys', icon: 'fa-key', color: 'var(--color-error)', label: 'API Keys' },
|
||||
{ id: 'agents', icon: 'fa-tasks', color: 'var(--color-primary)', label: 'Agent Jobs' },
|
||||
{ id: 'agentpool', icon: 'fa-robot', color: 'var(--color-primary)', label: 'Agent Pool' },
|
||||
{ id: 'assistant', icon: 'fa-user-shield', color: 'var(--color-accent)', label: 'LocalAI Assistant' },
|
||||
{ id: 'responses', icon: 'fa-database', color: 'var(--color-accent)', label: 'Responses' },
|
||||
{ id: 'branding', icon: 'fa-palette', color: 'var(--color-primary)' },
|
||||
{ id: 'watchdog', icon: 'fa-shield-halved', color: 'var(--color-primary)' },
|
||||
{ id: 'memory', icon: 'fa-memory', color: 'var(--color-accent)' },
|
||||
{ id: 'backends', icon: 'fa-cogs', color: 'var(--color-accent)' },
|
||||
{ id: 'performance', icon: 'fa-gauge-high', color: 'var(--color-success)' },
|
||||
{ id: 'tracing', icon: 'fa-bug', color: 'var(--color-warning)' },
|
||||
{ id: 'api', icon: 'fa-globe', color: 'var(--color-warning)' },
|
||||
{ id: 'p2p', icon: 'fa-network-wired', color: 'var(--color-accent)' },
|
||||
{ id: 'galleries', icon: 'fa-images', color: 'var(--color-accent)' },
|
||||
{ id: 'apikeys', icon: 'fa-key', color: 'var(--color-error)' },
|
||||
{ id: 'agents', icon: 'fa-tasks', color: 'var(--color-primary)' },
|
||||
{ id: 'agentpool', icon: 'fa-robot', color: 'var(--color-primary)' },
|
||||
{ id: 'assistant', icon: 'fa-user-shield', color: 'var(--color-accent)' },
|
||||
{ id: 'responses', icon: 'fa-database', color: 'var(--color-accent)' },
|
||||
]
|
||||
|
||||
const BRANDING_ASSETS = [
|
||||
@@ -34,6 +35,7 @@ const BRANDING_ASSETS = [
|
||||
|
||||
export default function Settings() {
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('admin')
|
||||
const [settings, setSettings] = useState(null)
|
||||
const [initialSettings, setInitialSettings] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -162,8 +164,8 @@ export default function Settings() {
|
||||
padding: 'var(--spacing-lg) var(--spacing-lg) var(--spacing-md)',
|
||||
}}>
|
||||
<div>
|
||||
<h1 className="page-title">Settings</h1>
|
||||
<p className="page-subtitle">Configure LocalAI runtime settings</p>
|
||||
<h1 className="page-title">{t('settings.title')}</h1>
|
||||
<p className="page-subtitle">{t('settings.subtitle')}</p>
|
||||
</div>
|
||||
<button className={`btn ${isDirty ? 'btn-primary' : 'btn-secondary'}`} onClick={handleSave} disabled={saving || !isDirty}>
|
||||
{saving ? <><LoadingSpinner size="sm" /> Saving...</> : <><i className="fas fa-save" /> {isDirty ? 'Save Changes' : 'Saved'}</>}
|
||||
@@ -197,7 +199,7 @@ export default function Settings() {
|
||||
width: 16, textAlign: 'center', fontSize: '0.75rem',
|
||||
color: activeSection === s.id ? s.color : 'var(--color-text-muted)',
|
||||
}} />
|
||||
{s.label}
|
||||
{t(`settings.sections.${s.id}`)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { skillsApi } from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useUserMap } from '../hooks/useUserMap'
|
||||
@@ -9,6 +10,7 @@ import ConfirmDialog from '../components/ConfirmDialog'
|
||||
export default function Skills() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('skills')
|
||||
const { isAdmin, authEnabled, user } = useAuth()
|
||||
const userMap = useUserMap()
|
||||
const [skills, setSkills] = useState([])
|
||||
@@ -56,13 +58,13 @@ export default function Skills() {
|
||||
setUnavailable(true)
|
||||
setSkills([])
|
||||
} else {
|
||||
addToast(err.message || 'Failed to load skills', 'error')
|
||||
addToast(err.message || t('toasts.loadFailed'), 'error')
|
||||
setSkills([])
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [searchQuery, addToast, isAdmin, authEnabled])
|
||||
}, [searchQuery, addToast, isAdmin, authEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills()
|
||||
@@ -70,18 +72,18 @@ export default function Skills() {
|
||||
|
||||
const deleteSkill = async (name, userId) => {
|
||||
setConfirmDialog({
|
||||
title: 'Delete Skill',
|
||||
message: `Delete skill "${name}"? This action cannot be undone.`,
|
||||
confirmLabel: 'Delete',
|
||||
title: t('deleteDialog.title'),
|
||||
message: t('deleteDialog.message', { name }),
|
||||
confirmLabel: t('deleteDialog.confirm'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
try {
|
||||
await skillsApi.delete(name, userId)
|
||||
addToast(`Skill "${name}" deleted`, 'success')
|
||||
addToast(t('toasts.deleted', { name }), 'success')
|
||||
fetchSkills()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to delete skill', 'error')
|
||||
addToast(err.message || t('toasts.deleteFailed'), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -100,9 +102,9 @@ export default function Skills() {
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(a.href)
|
||||
addToast(`Skill "${name}" exported`, 'success')
|
||||
addToast(t('toasts.exported', { name }), 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Export failed', 'error')
|
||||
addToast(err.message || t('toasts.exportFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +114,10 @@ export default function Skills() {
|
||||
setImporting(true)
|
||||
try {
|
||||
await skillsApi.import(file)
|
||||
addToast(`Skill imported from "${file.name}"`, 'success')
|
||||
addToast(t('toasts.imported', { file: file.name }), 'success')
|
||||
fetchSkills()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Import failed', 'error')
|
||||
addToast(err.message || t('toasts.importFailed'), 'error')
|
||||
} finally {
|
||||
setImporting(false)
|
||||
e.target.value = ''
|
||||
@@ -128,7 +130,7 @@ export default function Skills() {
|
||||
const list = await skillsApi.listGitRepos()
|
||||
setGitRepos(Array.isArray(list) ? list : [])
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to load Git repos', 'error')
|
||||
addToast(err.message || t('toasts.loadReposFailed'), 'error')
|
||||
setGitRepos([])
|
||||
} finally {
|
||||
setGitReposLoading(false)
|
||||
@@ -149,9 +151,9 @@ export default function Skills() {
|
||||
setGitRepoUrl('')
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Git repo added and syncing', 'success')
|
||||
addToast(t('toasts.repoAdded'), 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to add repo', 'error')
|
||||
addToast(err.message || t('toasts.addRepoFailed'), 'error')
|
||||
} finally {
|
||||
setGitReposAction(null)
|
||||
}
|
||||
@@ -163,9 +165,9 @@ export default function Skills() {
|
||||
await skillsApi.syncGitRepo(id)
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Repo synced', 'success')
|
||||
addToast(t('toasts.synced'), 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Sync failed', 'error')
|
||||
addToast(err.message || t('toasts.syncFailed'), 'error')
|
||||
} finally {
|
||||
setGitReposAction(null)
|
||||
}
|
||||
@@ -176,17 +178,17 @@ export default function Skills() {
|
||||
await skillsApi.toggleGitRepo(id)
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Repo toggled', 'success')
|
||||
addToast(t('toasts.toggled'), 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Toggle failed', 'error')
|
||||
addToast(err.message || t('toasts.toggleFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteGitRepo = async (id) => {
|
||||
setConfirmDialog({
|
||||
title: 'Remove Git Repository',
|
||||
message: 'Remove this Git repository? Skills from it will no longer be available.',
|
||||
confirmLabel: 'Remove',
|
||||
title: t('removeRepoDialog.title'),
|
||||
message: t('removeRepoDialog.message'),
|
||||
confirmLabel: t('removeRepoDialog.confirm'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog(null)
|
||||
@@ -194,9 +196,9 @@ export default function Skills() {
|
||||
await skillsApi.deleteGitRepo(id)
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Repo removed', 'success')
|
||||
addToast(t('toasts.removed'), 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Remove failed', 'error')
|
||||
addToast(err.message || t('toasts.removeFailed'), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -206,12 +208,12 @@ export default function Skills() {
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Skills</h1>
|
||||
<p className="page-subtitle">Skills service is not available or the index is rebuilding. Try again in a moment.</p>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('unavailable.subtitle')}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<button className="btn btn-primary" onClick={() => { setUnavailable(false); fetchSkills() }}>
|
||||
<i className="fas fa-redo" /> Retry
|
||||
<i className="fas fa-redo" /> {t('unavailable.retry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,23 +314,23 @@ export default function Skills() {
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Skills</h1>
|
||||
<p className="page-subtitle">Manage agent skills (reusable instructions and resources)</p>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('subtitle')}</p>
|
||||
</div>
|
||||
<div className="skills-header-actions">
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="Search skills..."
|
||||
placeholder={t('search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/skills/new')}>
|
||||
<i className="fas fa-plus" /> New skill
|
||||
<i className="fas fa-plus" /> {t('actions.newSkill')}
|
||||
</button>
|
||||
<label className="btn btn-secondary" style={{ cursor: 'pointer' }}>
|
||||
<i className="fas fa-file-import" /> {importing ? 'Importing...' : 'Import'}
|
||||
<i className="fas fa-file-import" /> {importing ? t('actions.importing') : t('actions.import')}
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
@@ -341,7 +343,7 @@ export default function Skills() {
|
||||
className={`btn ${showGitRepos ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setShowGitRepos((v) => !v)}
|
||||
>
|
||||
<i className="fas fa-code-branch" /> Git Repos
|
||||
<i className="fas fa-code-branch" /> {t('actions.gitRepos')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,21 +351,21 @@ export default function Skills() {
|
||||
{showGitRepos && (
|
||||
<div className="skills-git-section">
|
||||
<h2 className="skills-git-title">
|
||||
<i className="fas fa-code-branch" style={{ marginRight: 'var(--spacing-xs)', color: 'var(--color-primary)' }} /> Git repositories
|
||||
<i className="fas fa-code-branch" style={{ marginRight: 'var(--spacing-xs)', color: 'var(--color-primary)' }} /> {t('git.title')}
|
||||
</h2>
|
||||
<p className="skills-git-desc">
|
||||
Add Git repositories to pull skills from. Skills will appear in the list after sync.
|
||||
{t('git.description')}
|
||||
</p>
|
||||
<form onSubmit={addGitRepo} className="skills-git-form">
|
||||
<input
|
||||
type="url"
|
||||
className="input"
|
||||
placeholder="https://github.com/user/repo or git@github.com:user/repo.git"
|
||||
placeholder={t('git.urlPlaceholder')}
|
||||
value={gitRepoUrl}
|
||||
onChange={(e) => setGitRepoUrl(e.target.value)}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary" disabled={gitReposAction === 'add'}>
|
||||
{gitReposAction === 'add' ? <><i className="fas fa-spinner fa-spin" /> Adding...</> : 'Add repo'}
|
||||
{gitReposAction === 'add' ? <><i className="fas fa-spinner fa-spin" /> {t('actions.adding')}</> : t('actions.addRepo')}
|
||||
</button>
|
||||
</form>
|
||||
{gitReposLoading ? (
|
||||
@@ -371,7 +373,7 @@ export default function Skills() {
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '1.5rem', color: 'var(--color-text-muted)' }} />
|
||||
</div>
|
||||
) : gitRepos.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>No Git repos configured. Add one above.</p>
|
||||
<p style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>{t('git.noRepos')}</p>
|
||||
) : (
|
||||
<div>
|
||||
{gitRepos.map((r) => (
|
||||
@@ -379,28 +381,28 @@ export default function Skills() {
|
||||
<div>
|
||||
<span className="skills-git-repo-name">{r.name || r.url}</span>
|
||||
<span className="skills-git-repo-url">{r.url}</span>
|
||||
{!r.enabled && <span className="badge" style={{ marginLeft: 'var(--spacing-sm)' }}>Disabled</span>}
|
||||
{!r.enabled && <span className="badge" style={{ marginLeft: 'var(--spacing-sm)' }}>{t('git.disabled')}</span>}
|
||||
</div>
|
||||
<div className="skills-git-repo-actions">
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => syncGitRepo(r.id)}
|
||||
disabled={gitReposAction === r.id}
|
||||
title="Sync"
|
||||
title={t('actions.sync')}
|
||||
>
|
||||
{gitReposAction === r.id ? <i className="fas fa-spinner fa-spin" /> : <><i className="fas fa-sync-alt" /> Sync</>}
|
||||
{gitReposAction === r.id ? <i className="fas fa-spinner fa-spin" /> : <><i className="fas fa-sync-alt" /> {t('actions.sync')}</>}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => toggleGitRepo(r.id)}
|
||||
title={r.enabled ? 'Disable' : 'Enable'}
|
||||
title={r.enabled ? t('actions.disable') : t('actions.enable')}
|
||||
>
|
||||
<i className={`fas fa-toggle-${r.enabled ? 'on' : 'off'}`} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => deleteGitRepo(r.id)}
|
||||
title="Remove repo"
|
||||
title={t('git.removeRepo')}
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
@@ -419,14 +421,14 @@ export default function Skills() {
|
||||
) : skills.length === 0 && !userGroups ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-book" /></div>
|
||||
<h2 className="empty-state-title">No skills found</h2>
|
||||
<p className="empty-state-text">Create a skill or import one to get started.</p>
|
||||
<h2 className="empty-state-title">{t('empty.title')}</h2>
|
||||
<p className="empty-state-text">{t('empty.text')}</p>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/skills/new')}>
|
||||
<i className="fas fa-plus" /> Create skill
|
||||
<i className="fas fa-plus" /> {t('actions.createSkill')}
|
||||
</button>
|
||||
<label className="btn btn-secondary" style={{ cursor: 'pointer' }}>
|
||||
<i className="fas fa-file-import" /> Import
|
||||
<i className="fas fa-file-import" /> {t('actions.import')}
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
@@ -439,45 +441,45 @@ export default function Skills() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{userGroups && <h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>Your Skills</h2>}
|
||||
{userGroups && <h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>{t('sections.yourSkills')}</h2>}
|
||||
{skills.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-md)' }}>You have no skills yet.</p>
|
||||
<p style={{ color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-md)' }}>{t('empty.noPersonal')}</p>
|
||||
) : (
|
||||
<div className="skills-grid">
|
||||
{skills.map((s) => (
|
||||
<div key={s.name} className="card">
|
||||
<div className="skills-card-header">
|
||||
<h3 className="skills-card-name">{s.name}</h3>
|
||||
{s.readOnly && <span className="badge">Read-only</span>}
|
||||
{s.readOnly && <span className="badge">{t('card.readOnly')}</span>}
|
||||
</div>
|
||||
<p className="skills-card-desc">
|
||||
{s.description || 'No description'}
|
||||
{s.description || t('card.noDescription')}
|
||||
</p>
|
||||
<div className="skills-card-actions">
|
||||
{!s.readOnly && (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/skills/edit/${encodeURIComponent(s.name)}`)}
|
||||
title="Edit skill"
|
||||
title={t('card.editTitle')}
|
||||
>
|
||||
<i className="fas fa-edit" /> Edit
|
||||
<i className="fas fa-edit" /> {t('actions.edit')}
|
||||
</button>
|
||||
)}
|
||||
{!s.readOnly && (
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => deleteSkill(s.name)}
|
||||
title="Delete skill"
|
||||
title={t('card.deleteTitle')}
|
||||
>
|
||||
<i className="fas fa-trash" /> Delete
|
||||
<i className="fas fa-trash" /> {t('actions.delete')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => exportSkill(s.name)}
|
||||
title="Export as .tar.gz"
|
||||
title={t('card.exportTitle')}
|
||||
>
|
||||
<i className="fas fa-download" /> Export
|
||||
<i className="fas fa-download" /> {t('actions.export')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,7 +501,7 @@ export default function Skills() {
|
||||
|
||||
{userGroups && (
|
||||
<UserGroupSection
|
||||
title="Other Users' Skills"
|
||||
title={t('sections.otherUsersSkills')}
|
||||
userGroups={userGroups}
|
||||
userMap={userMap}
|
||||
currentUserId={user?.id}
|
||||
@@ -510,34 +512,34 @@ export default function Skills() {
|
||||
<div key={s.name} className="card">
|
||||
<div className="skills-card-header">
|
||||
<h3 className="skills-card-name">{s.name}</h3>
|
||||
{s.readOnly && <span className="badge">Read-only</span>}
|
||||
{s.readOnly && <span className="badge">{t('card.readOnly')}</span>}
|
||||
</div>
|
||||
<p className="skills-card-desc">{s.description || 'No description'}</p>
|
||||
<p className="skills-card-desc">{s.description || t('card.noDescription')}</p>
|
||||
<div className="skills-card-actions">
|
||||
{!s.readOnly && (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/app/skills/edit/${encodeURIComponent(s.name)}?user_id=${encodeURIComponent(userId)}`)}
|
||||
title="Edit skill"
|
||||
title={t('card.editTitle')}
|
||||
>
|
||||
<i className="fas fa-edit" /> Edit
|
||||
<i className="fas fa-edit" /> {t('actions.edit')}
|
||||
</button>
|
||||
)}
|
||||
{!s.readOnly && (
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => deleteSkill(s.name, userId)}
|
||||
title="Delete skill"
|
||||
title={t('card.deleteTitle')}
|
||||
>
|
||||
<i className="fas fa-trash" /> Delete
|
||||
<i className="fas fa-trash" /> {t('actions.delete')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => exportSkill(s.name, userId)}
|
||||
title="Export as .tar.gz"
|
||||
title={t('card.exportTitle')}
|
||||
>
|
||||
<i className="fas fa-download" /> Export
|
||||
<i className="fas fa-download" /> {t('actions.export')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ImageGen from './ImageGen'
|
||||
import VideoGen from './VideoGen'
|
||||
import TTS from './TTS'
|
||||
import Sound from './Sound'
|
||||
|
||||
const TABS = [
|
||||
{ key: 'images', label: 'Images', icon: 'fas fa-image' },
|
||||
{ key: 'video', label: 'Video', icon: 'fas fa-video' },
|
||||
{ key: 'tts', label: 'TTS', icon: 'fas fa-headphones' },
|
||||
{ key: 'sound', label: 'Sound', icon: 'fas fa-music' },
|
||||
{ key: 'images', labelKey: 'studio.tabs.images', icon: 'fas fa-image' },
|
||||
{ key: 'video', labelKey: 'studio.tabs.video', icon: 'fas fa-video' },
|
||||
{ key: 'tts', labelKey: 'studio.tabs.tts', icon: 'fas fa-headphones' },
|
||||
{ key: 'sound', labelKey: 'studio.tabs.sound', icon: 'fas fa-music' },
|
||||
]
|
||||
|
||||
const TAB_COMPONENTS = {
|
||||
@@ -19,6 +20,7 @@ const TAB_COMPONENTS = {
|
||||
}
|
||||
|
||||
export default function Studio() {
|
||||
const { t } = useTranslation('media')
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const activeTab = searchParams.get('tab') || 'images'
|
||||
|
||||
@@ -38,7 +40,7 @@ export default function Studio() {
|
||||
onClick={() => setTab(tab.key)}
|
||||
>
|
||||
<i className={tab.icon} />
|
||||
<span>{tab.label}</span>
|
||||
<span>{t(tab.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelSelector from '../components/ModelSelector'
|
||||
import { CAP_TTS } from '../utils/capabilities'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -11,6 +12,7 @@ import { useMediaHistory } from '../hooks/useMediaHistory'
|
||||
export default function TTS() {
|
||||
const { model: urlModel } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('media')
|
||||
const [model, setModel] = useState(urlModel || '')
|
||||
const [text, setText] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -21,8 +23,8 @@ export default function TTS() {
|
||||
|
||||
const handleGenerate = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!text.trim()) { addToast('Please enter text', 'warning'); return }
|
||||
if (!model) { addToast('Please select a model', 'warning'); return }
|
||||
if (!text.trim()) { addToast(t('tts.toasts.noText'), 'warning'); return }
|
||||
if (!model) { addToast(t('tts.toasts.noModel'), 'warning'); return }
|
||||
|
||||
setLoading(true)
|
||||
setAudioUrl(null)
|
||||
@@ -32,7 +34,7 @@ export default function TTS() {
|
||||
const { blob, serverUrl } = await ttsApi.generate({ model, input: text.trim() })
|
||||
const url = URL.createObjectURL(blob)
|
||||
setAudioUrl(url)
|
||||
addToast('Audio generated', 'success')
|
||||
addToast(t('tts.actions.generate'), 'success')
|
||||
if (serverUrl) {
|
||||
addEntry({ prompt: text.trim(), model, params: {}, results: [{ url: serverUrl }] })
|
||||
}
|
||||
@@ -49,26 +51,26 @@ export default function TTS() {
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-headphones" /> Text to Speech</h1>
|
||||
<h1 className="page-title"><i className="fas fa-headphones" /> {t('tts.title')}</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Model</label>
|
||||
<label className="form-label">{t('tts.labels.model')}</label>
|
||||
<ModelSelector value={model} onChange={setModel} capability={CAP_TTS} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Text</label>
|
||||
<label className="form-label">{t('tts.labels.input')}</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Enter text to convert to speech..."
|
||||
placeholder={t('tts.labels.inputPlaceholder')}
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-headphones" /> Generate Audio</>}
|
||||
{loading ? <><LoadingSpinner size="sm" /> {t('tts.actions.generating')}</> : <><i className="fas fa-headphones" /> {t('tts.actions.generate')}</>}
|
||||
</button>
|
||||
</form>
|
||||
<MediaHistory {...historyProps} />
|
||||
@@ -101,7 +103,7 @@ export default function TTS() {
|
||||
) : (
|
||||
<div className="media-empty">
|
||||
<i className="fas fa-headphones media-empty__icon" />
|
||||
<p>Generated audio will appear here</p>
|
||||
<p>{t('tts.empty')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useOutletContext, useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { tracesApi, settingsApi } from '../utils/api'
|
||||
import { formatTimestamp } from '../utils/format'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -283,6 +284,7 @@ function ApiTraceDetail({ trace }) {
|
||||
|
||||
export default function Traces() {
|
||||
const { addToast } = useOutletContext()
|
||||
const { t } = useTranslation('admin')
|
||||
const [searchParams] = useSearchParams()
|
||||
const [activeTab, setActiveTab] = useState(() => searchParams.get('tab') === 'backend' ? 'backend' : 'api')
|
||||
const [traces, setTraces] = useState([])
|
||||
@@ -382,8 +384,8 @@ export default function Traces() {
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Traces</h1>
|
||||
<p className="page-subtitle">View logged API requests, responses, and backend operations</p>
|
||||
<h1 className="page-title">{t('traces.title')}</h1>
|
||||
<p className="page-subtitle">{t('traces.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="tabs">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef, Fragment } from 'react'
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -629,6 +630,7 @@ function ModelDistChart({ rows }) {
|
||||
export default function Usage() {
|
||||
const { addToast } = useOutletContext()
|
||||
const { isAdmin, authEnabled } = useAuth()
|
||||
const { t } = useTranslation('admin')
|
||||
const [period, setPeriod] = useState('month')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [usage, setUsage] = useState([])
|
||||
@@ -707,8 +709,8 @@ export default function Usage() {
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header" style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<h1 className="page-title">Usage</h1>
|
||||
<p className="page-subtitle">API token usage statistics</p>
|
||||
<h1 className="page-title">{t('usage.title')}</h1>
|
||||
<p className="page-subtitle">{t('usage.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Period selector + tabs */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { adminUsersApi, adminInvitesApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
@@ -658,6 +659,7 @@ function InvitesTab({ addToast }) {
|
||||
export default function Users() {
|
||||
const { addToast } = useOutletContext()
|
||||
const { user: currentUser } = useAuth()
|
||||
const { t } = useTranslation('admin')
|
||||
const [users, setUsers] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -796,8 +798,8 @@ export default function Users() {
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Users</h1>
|
||||
<p className="page-subtitle">Manage registered users, roles, and invites</p>
|
||||
<h1 className="page-title">{t('users.title')}</h1>
|
||||
<p className="page-subtitle">{t('users.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user