refactor(frontend): remove i18n fallback strings and drop providers route

- Replace `t(key, defaultValue)` calls with key-only translations across UI pages
- Clean up locale files by pruning unused keys and adding missing shared keys
- Remove the obsolete `/providers` page and update generated route tree
This commit is contained in:
wenjie 2026-03-09 19:12:41 +08:00
parent b06e9e3707
commit a418554441
23 changed files with 149 additions and 495 deletions

View file

@ -87,10 +87,7 @@ export function AppHeader() {
<span className="bg-destructive/50 relative flex size-2 shrink-0 items-center justify-center rounded-full"> <span className="bg-destructive/50 relative flex size-2 shrink-0 items-center justify-center rounded-full">
<span className="bg-destructive absolute inline-flex size-full animate-ping rounded-full opacity-75"></span> <span className="bg-destructive absolute inline-flex size-full animate-ping rounded-full opacity-75"></span>
</span> </span>
{t( {t("chat.notConnected")}
"chat.notConnected",
"Gateway is not running. Start it to chat.",
)}
</div> </div>
)} )}
</div> </div>
@ -99,24 +96,19 @@ export function AppHeader() {
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{t("header.gateway.stopDialog.title", "Stop Gateway Service?")} {t("header.gateway.stopDialog.title")}
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t( {t("header.gateway.stopDialog.description")}
"header.gateway.stopDialog.description",
"Are you sure you want to stop the gateway? This will disconnect your active chat sessions and halt inference.",
)}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel> <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
{t("common.cancel", "Cancel")}
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={confirmStop} onClick={confirmStop}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
> >
{t("header.gateway.stopDialog.confirm", "Stop Gateway")} {t("header.gateway.stopDialog.confirm")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -146,10 +138,10 @@ export function AppHeader() {
)} )}
<span className="text-xs font-semibold"> <span className="text-xs font-semibold">
{isRunning {isRunning
? t("header.gateway.action.stop", "Stop Gateway") ? t("header.gateway.action.stop")
: isStarting : isStarting
? t("header.gateway.status.starting", "Starting Gateway...") ? t("header.gateway.status.starting")
: t("header.gateway.action.start", "Start Gateway")} : t("header.gateway.action.start")}
</span> </span>
</Button> </Button>

View file

@ -494,7 +494,7 @@ export function ChannelConfigPage({ channelName }: ChannelConfigPageProps) {
<div className="w-full max-w-250 space-y-5 pt-2"> <div className="w-full max-w-250 space-y-5 pt-2">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<p className="font-medium"> <p className="font-medium">
{t("channels.edit.title", { {t("channels.edit", {
name: channelDisplayName, name: channelDisplayName,
})} })}
</p> </p>

View file

@ -69,7 +69,7 @@ export function ChatPage() {
return ( return (
<div className="bg-background/95 flex h-full flex-col"> <div className="bg-background/95 flex h-full flex-col">
<PageHeader <PageHeader
title={t("navigation.chat", "Chat")} title={t("navigation.chat")}
titleExtra={ titleExtra={
hasConfiguredModels && ( hasConfiguredModels && (
<ModelSelector <ModelSelector

View file

@ -40,7 +40,7 @@ export function ModelSelector({
<SelectContent> <SelectContent>
{apiKeyModels.length > 0 && ( {apiKeyModels.length > 0 && (
<SelectGroup> <SelectGroup>
<SelectLabel>{t("chat.modelGroup.apikey", "API Key")}</SelectLabel> <SelectLabel>{t("chat.modelGroup.apikey")}</SelectLabel>
{apiKeyModels.map((model) => ( {apiKeyModels.map((model) => (
<SelectItem key={model.index} value={model.model_name}> <SelectItem key={model.index} value={model.model_name}>
{model.model_name} {model.model_name}
@ -55,7 +55,7 @@ export function ModelSelector({
{oauthModels.length > 0 && ( {oauthModels.length > 0 && (
<SelectGroup> <SelectGroup>
<SelectLabel>{t("chat.modelGroup.oauth", "OAuth")}</SelectLabel> <SelectLabel>{t("chat.modelGroup.oauth")}</SelectLabel>
{oauthModels.map((model) => ( {oauthModels.map((model) => (
<SelectItem key={model.index} value={model.model_name}> <SelectItem key={model.index} value={model.model_name}>
{model.model_name} {model.model_name}
@ -70,7 +70,7 @@ export function ModelSelector({
{localModels.length > 0 && ( {localModels.length > 0 && (
<SelectGroup> <SelectGroup>
<SelectLabel>{t("chat.modelGroup.local", "Local")}</SelectLabel> <SelectLabel>{t("chat.modelGroup.local")}</SelectLabel>
{localModels.map((model) => ( {localModels.map((model) => (
<SelectItem key={model.index} value={model.model_name}> <SelectItem key={model.index} value={model.model_name}>
{model.model_name} {model.model_name}

View file

@ -71,7 +71,7 @@ export function SessionHistoryMenu({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
aria-label={t("chat.deleteSession", "Delete session")} aria-label={t("chat.deleteSession")}
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive absolute top-1/2 right-2 h-6 w-6 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100" className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive absolute top-1/2 right-2 h-6 w-6 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
@ -87,7 +87,7 @@ export function SessionHistoryMenu({
{hasMore && sessions.length > 0 && ( {hasMore && sessions.length > 0 && (
<div ref={observerRef} className="py-2 text-center"> <div ref={observerRef} className="py-2 text-center">
<span className="text-muted-foreground animate-pulse text-xs"> <span className="text-muted-foreground animate-pulse text-xs">
{t("chat.loadingMore", "Loading more...")} {t("chat.loadingMore")}
</span> </span>
</div> </div>
)} )}

View file

@ -106,29 +106,14 @@ export function ConfigPage() {
const autoStartSupported = autoStartStatus?.supported !== false const autoStartSupported = autoStartStatus?.supported !== false
const autoStartHint = autoStartError const autoStartHint = autoStartError
? t( ? t("pages.config.autostart_load_error")
"pages.config.autostart_load_error",
"Failed to load launch-at-login status.",
)
: !autoStartSupported : !autoStartSupported
? t( ? t("pages.config.autostart_unsupported")
"pages.config.autostart_unsupported", : t("pages.config.autostart_hint")
"Launch at login is not supported on this platform.",
)
: t(
"pages.config.autostart_hint",
"Start PicoClaw Web automatically when you log in.",
)
const launcherHint = launcherError const launcherHint = launcherError
? t( ? t("pages.config.launcher_load_error")
"pages.config.launcher_load_error", : t("pages.config.launcher_restart_hint")
"Failed to load service parameters.",
)
: t(
"pages.config.launcher_restart_hint",
"Service parameter changes apply after restarting PicoClaw Web.",
)
const updateField = <K extends keyof CoreConfigForm>( const updateField = <K extends keyof CoreConfigForm>(
key: K, key: K,
@ -148,12 +133,7 @@ export function ConfigPage() {
setForm(baseline) setForm(baseline)
setLauncherForm(launcherBaseline) setLauncherForm(launcherBaseline)
setAutoStartEnabled(autoStartBaseline) setAutoStartEnabled(autoStartBaseline)
toast.info( toast.info(t("pages.config.reset_success"))
t(
"pages.config.reset_success",
"Changes have been reset to the last saved state.",
),
)
} }
const handleSave = async () => { const handleSave = async () => {
@ -251,12 +231,7 @@ export function ConfigPage() {
if (autoStartDirty) { if (autoStartDirty) {
if (!autoStartSupported) { if (!autoStartSupported) {
throw new Error( throw new Error(t("pages.config.autostart_unsupported"))
t(
"pages.config.autostart_unsupported",
"Launch at login is not supported on this platform.",
),
)
} }
const status = await updateAutoStartEnabled(autoStartEnabled) const status = await updateAutoStartEnabled(autoStartEnabled)
setAutoStartEnabled(status.enabled) setAutoStartEnabled(status.enabled)
@ -264,14 +239,10 @@ export function ConfigPage() {
queryClient.setQueryData(["system", "autostart"], status) queryClient.setQueryData(["system", "autostart"], status)
} }
toast.success( toast.success(t("pages.config.save_success"))
t("pages.config.save_success", "Configuration saved successfully."),
)
} catch (err) { } catch (err) {
toast.error( toast.error(
err instanceof Error err instanceof Error ? err.message : t("pages.config.save_error"),
? err.message
: t("pages.config.save_error", "Failed to save configuration."),
) )
} finally { } finally {
setSaving(false) setSaving(false)
@ -281,12 +252,12 @@ export function ConfigPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader <PageHeader
title={t("navigation.config", "Config")} title={t("navigation.config")}
children={ children={
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link to="/config/raw"> <Link to="/config/raw">
<IconCode className="size-4" /> <IconCode className="size-4" />
{t("pages.config.open_raw", "Raw Config")} {t("pages.config.open_raw")}
</Link> </Link>
</Button> </Button>
} }
@ -295,23 +266,17 @@ export function ConfigPage() {
<div className="mx-auto w-full max-w-[1000px] space-y-6"> <div className="mx-auto w-full max-w-[1000px] space-y-6">
{isLoading ? ( {isLoading ? (
<div className="text-muted-foreground py-6 text-sm"> <div className="text-muted-foreground py-6 text-sm">
{t("labels.loading", "Loading...")} {t("labels.loading")}
</div> </div>
) : error ? ( ) : error ? (
<div className="text-destructive py-6 text-sm"> <div className="text-destructive py-6 text-sm">
{t( {t("pages.config.load_error")}
"pages.config.load_error",
"Failed to load configuration. Please refresh and try again.",
)}
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{isDirty && ( {isDirty && (
<div className="bg-yellow-50 px-3 py-2 text-sm text-yellow-700"> <div className="bg-yellow-50 px-3 py-2 text-sm text-yellow-700">
{t( {t("pages.config.unsaved_changes")}
"pages.config.unsaved_changes",
"You have unsaved changes.",
)}
</div> </div>
)} )}
@ -356,13 +321,11 @@ export function ConfigPage() {
onClick={handleReset} onClick={handleReset}
disabled={!isDirty || saving} disabled={!isDirty || saving}
> >
{t("common.reset", "Reset")} {t("common.reset")}
</Button> </Button>
<Button onClick={handleSave} disabled={!isDirty || saving}> <Button onClick={handleSave} disabled={!isDirty || saving}>
<IconDeviceFloppy className="size-4" /> <IconDeviceFloppy className="size-4" />
{saving {saving ? t("common.saving") : t("common.save")}
? t("common.saving", "Saving...")
: t("common.save", "Save")}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -44,11 +44,8 @@ export function AgentDefaultsSection({
<section className="space-y-3"> <section className="space-y-3">
<div className="space-y-4"> <div className="space-y-4">
<Field <Field
label={t("pages.config.workspace", "Workspace Directory")} label={t("pages.config.workspace")}
hint={t( hint={t("pages.config.workspace_hint")}
"pages.config.workspace_hint",
"Base directory for agent file operations.",
)}
> >
<Input <Input
value={form.workspace} value={form.workspace}
@ -58,11 +55,8 @@ export function AgentDefaultsSection({
</Field> </Field>
<SwitchCardField <SwitchCardField
label={t("pages.config.restrict_workspace", "Restrict to Workspace")} label={t("pages.config.restrict_workspace")}
hint={t( hint={t("pages.config.restrict_workspace_hint")}
"pages.config.restrict_workspace_hint",
"Only allow file operations inside workspace.",
)}
checked={form.restrictToWorkspace} checked={form.restrictToWorkspace}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onFieldChange("restrictToWorkspace", checked) onFieldChange("restrictToWorkspace", checked)
@ -70,11 +64,8 @@ export function AgentDefaultsSection({
/> />
<Field <Field
label={t("pages.config.max_tokens", "Max Tokens")} label={t("pages.config.max_tokens")}
hint={t( hint={t("pages.config.max_tokens_hint")}
"pages.config.max_tokens_hint",
"Upper token limit per model response.",
)}
> >
<Input <Input
type="number" type="number"
@ -85,11 +76,8 @@ export function AgentDefaultsSection({
</Field> </Field>
<Field <Field
label={t("pages.config.max_tool_iterations", "Max Tool Iterations")} label={t("pages.config.max_tool_iterations")}
hint={t( hint={t("pages.config.max_tool_iterations_hint")}
"pages.config.max_tool_iterations_hint",
"Maximum tool-call loops in a single task.",
)}
> >
<Input <Input
type="number" type="number"
@ -100,14 +88,8 @@ export function AgentDefaultsSection({
</Field> </Field>
<Field <Field
label={t( label={t("pages.config.summarize_threshold")}
"pages.config.summarize_threshold", hint={t("pages.config.summarize_threshold_hint")}
"Summarize Message Threshold",
)}
hint={t(
"pages.config.summarize_threshold_hint",
"Start summarization after this many messages.",
)}
> >
<Input <Input
type="number" type="number"
@ -120,14 +102,8 @@ export function AgentDefaultsSection({
</Field> </Field>
<Field <Field
label={t( label={t("pages.config.summarize_token_percent")}
"pages.config.summarize_token_percent", hint={t("pages.config.summarize_token_percent_hint")}
"Summarize Token Percent",
)}
hint={t(
"pages.config.summarize_token_percent_hint",
"Used when conversation summary is triggered.",
)}
> >
<Input <Input
type="number" type="number"
@ -159,11 +135,8 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
<section className="space-y-3"> <section className="space-y-3">
<div className="space-y-4"> <div className="space-y-4">
<Field <Field
label={t("pages.config.session_scope", "Session Scope")} label={t("pages.config.session_scope")}
hint={t( hint={t("pages.config.session_scope_hint")}
"pages.config.session_scope_hint",
"How chat context is isolated across peers/channels.",
)}
> >
<Select <Select
value={form.dmScope} value={form.dmScope}
@ -183,11 +156,9 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
{DM_SCOPE_OPTIONS.map((scope) => ( {DM_SCOPE_OPTIONS.map((scope) => (
<SelectItem key={scope.value} value={scope.value}> <SelectItem key={scope.value} value={scope.value}>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="font-medium"> <span className="font-medium">{t(scope.labelKey)}</span>
{t(scope.labelKey, scope.labelDefault)}
</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{t(scope.descKey, scope.descDefault)} {t(scope.descKey)}
</span> </span>
</div> </div>
</SelectItem> </SelectItem>
@ -197,11 +168,8 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
</Field> </Field>
<SwitchCardField <SwitchCardField
label={t("pages.config.heartbeat_enabled", "Heartbeat")} label={t("pages.config.heartbeat_enabled")}
hint={t( hint={t("pages.config.heartbeat_enabled_hint")}
"pages.config.heartbeat_enabled_hint",
"Send periodic heartbeat messages.",
)}
checked={form.heartbeatEnabled} checked={form.heartbeatEnabled}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onFieldChange("heartbeatEnabled", checked) onFieldChange("heartbeatEnabled", checked)
@ -210,14 +178,8 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
{form.heartbeatEnabled && ( {form.heartbeatEnabled && (
<Field <Field
label={t( label={t("pages.config.heartbeat_interval")}
"pages.config.heartbeat_interval", hint={t("pages.config.heartbeat_interval_hint")}
"Heartbeat Interval (minutes)",
)}
hint={t(
"pages.config.heartbeat_interval_hint",
"Interval in minutes between heartbeat signals.",
)}
> >
<Input <Input
type="number" type="number"
@ -253,11 +215,8 @@ export function LauncherSection({
<section className="space-y-3"> <section className="space-y-3">
<div className="space-y-4"> <div className="space-y-4">
<Field <Field
label={t("pages.config.server_port", "Service Port")} label={t("pages.config.server_port")}
hint={t( hint={t("pages.config.server_port_hint")}
"pages.config.server_port_hint",
"HTTP port used by PicoClaw Web.",
)}
> >
<Input <Input
type="number" type="number"
@ -270,30 +229,21 @@ export function LauncherSection({
</Field> </Field>
<SwitchCardField <SwitchCardField
label={t("pages.config.lan_access", "Enable LAN Access")} label={t("pages.config.lan_access")}
hint={t( hint={t("pages.config.lan_access_hint")}
"pages.config.lan_access_hint",
"Allow access from other devices on your local network.",
)}
checked={launcherForm.publicAccess} checked={launcherForm.publicAccess}
disabled={disabled} disabled={disabled}
onCheckedChange={(checked) => onFieldChange("publicAccess", checked)} onCheckedChange={(checked) => onFieldChange("publicAccess", checked)}
/> />
<Field <Field
label={t("pages.config.allowed_cidrs", "Allowed Network CIDRs")} label={t("pages.config.allowed_cidrs")}
hint={t( hint={t("pages.config.allowed_cidrs_hint")}
"pages.config.allowed_cidrs_hint",
"Only clients from these CIDR ranges can access the service. One per line or comma-separated. Leave empty to allow all.",
)}
> >
<Textarea <Textarea
value={launcherForm.allowedCIDRsText} value={launcherForm.allowedCIDRsText}
disabled={disabled} disabled={disabled}
placeholder={t( placeholder={t("pages.config.allowed_cidrs_placeholder")}
"pages.config.allowed_cidrs_placeholder",
"192.168.1.0/24\n10.0.0.0/8",
)}
className="min-h-[88px]" className="min-h-[88px]"
onChange={(e) => onFieldChange("allowedCIDRsText", e.target.value)} onChange={(e) => onFieldChange("allowedCIDRsText", e.target.value)}
/> />
@ -328,11 +278,8 @@ export function DevicesSection({
<section className="space-y-3"> <section className="space-y-3">
<div className="space-y-4"> <div className="space-y-4">
<SwitchCardField <SwitchCardField
label={t("pages.config.devices_enabled", "Enable Devices")} label={t("pages.config.devices_enabled")}
hint={t( hint={t("pages.config.devices_enabled_hint")}
"pages.config.devices_enabled_hint",
"Enable hardware-device integrations.",
)}
checked={form.devicesEnabled} checked={form.devicesEnabled}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onFieldChange("devicesEnabled", checked) onFieldChange("devicesEnabled", checked)
@ -340,17 +287,14 @@ export function DevicesSection({
/> />
<SwitchCardField <SwitchCardField
label={t("pages.config.monitor_usb", "Monitor USB")} label={t("pages.config.monitor_usb")}
hint={t( hint={t("pages.config.monitor_usb_hint")}
"pages.config.monitor_usb_hint",
"Watch USB plug/unplug events when devices are enabled.",
)}
checked={form.monitorUSB} checked={form.monitorUSB}
onCheckedChange={(checked) => onFieldChange("monitorUSB", checked)} onCheckedChange={(checked) => onFieldChange("monitorUSB", checked)}
/> />
<SwitchCardField <SwitchCardField
label={t("pages.config.autostart_label", "Launch at Login")} label={t("pages.config.autostart_label")}
hint={autoStartHint} hint={autoStartHint}
checked={autoStartEnabled} checked={autoStartEnabled}
disabled={autoStartDisabled} disabled={autoStartDisabled}
@ -367,16 +311,13 @@ export function AdvancedSection() {
return ( return (
<section className="space-y-3"> <section className="space-y-3">
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{t( {t("pages.config.advanced_desc")}
"pages.config.advanced_desc",
"Open the raw JSON page to edit every field directly.",
)}
</p> </p>
<div> <div>
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link to="/config/raw"> <Link to="/config/raw">
<IconCode className="size-4" /> <IconCode className="size-4" />
{t("pages.config.open_raw", "Raw Config")} {t("pages.config.open_raw")}
</Link> </Link>
</Button> </Button>
</div> </div>

View file

@ -52,9 +52,7 @@ export function RawJsonPanel() {
} }
}, },
onSuccess: (_, submittedConfig) => { onSuccess: (_, submittedConfig) => {
toast.success( toast.success(t("pages.config.save_success"))
t("pages.config.save_success", "Configuration saved successfully."),
)
try { try {
const savedConfig = JSON.parse(submittedConfig) const savedConfig = JSON.parse(submittedConfig)
setLastSavedConfig(savedConfig) setLastSavedConfig(savedConfig)
@ -65,7 +63,7 @@ export function RawJsonPanel() {
} }
}, },
onError: () => { onError: () => {
toast.error(t("pages.config.save_error", "Failed to save configuration.")) toast.error(t("pages.config.save_error"))
}, },
}) })
@ -101,9 +99,7 @@ export function RawJsonPanel() {
2, 2,
) )
setEditorValue(formatted) setEditorValue(formatted)
toast.success( toast.success(t("pages.config.format_success"))
t("pages.config.format_success", "JSON formatted successfully."),
)
} catch (error) { } catch (error) {
toast.error( toast.error(
t( t(
@ -123,38 +119,26 @@ export function RawJsonPanel() {
setEditorValue(JSON.stringify(config, null, 2)) setEditorValue(JSON.stringify(config, null, 2))
} }
setIsDirty(false) setIsDirty(false)
toast.info( toast.info(t("pages.config.reset_success"))
t(
"pages.config.reset_success",
"Changes have been reset to the last saved state.",
),
)
setShowResetDialog(false) setShowResetDialog(false)
} }
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>{t("pages.config.raw_json_title")}</CardTitle>
{t("pages.config.raw_json_title", "Raw JSON Configuration")} <CardDescription>{t("pages.config.raw_json_desc")}</CardDescription>
</CardTitle>
<CardDescription>
{t(
"pages.config.raw_json_desc",
"Advanced users can directly edit the raw JSON configuration below.",
)}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
<p>{t("labels.loading", "Loading...")}</p> <p>{t("labels.loading")}</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{isDirty && ( {isDirty && (
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-2 text-sm text-yellow-700"> <div className="rounded-lg border border-yellow-200 bg-yellow-50 p-2 text-sm text-yellow-700">
{t("pages.config.unsaved_changes", "You have unsaved changes.")} {t("pages.config.unsaved_changes")}
</div> </div>
)} )}
<div className="bg-muted/30 relative rounded-lg border"> <div className="bg-muted/30 relative rounded-lg border">
@ -166,10 +150,7 @@ export function RawJsonPanel() {
setIsDirty(true) setIsDirty(true)
}} }}
className="min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 font-mono text-sm shadow-none focus-visible:ring-0" className="min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 font-mono text-sm shadow-none focus-visible:ring-0"
placeholder={t( placeholder={t("pages.config.json_placeholder")}
"pages.config.json_placeholder",
"Enter valid JSON configuration...",
)}
/> />
</ScrollArea> </ScrollArea>
</div> </div>
@ -179,7 +160,7 @@ export function RawJsonPanel() {
onClick={handleFormat} onClick={handleFormat}
disabled={mutation.isPending} disabled={mutation.isPending}
> >
{t("pages.config.format", "Format")} {t("pages.config.format")}
</Button> </Button>
<AlertDialog <AlertDialog
open={showResetDialog} open={showResetDialog}
@ -191,35 +172,28 @@ export function RawJsonPanel() {
disabled={!isDirty} disabled={!isDirty}
onClick={() => setShowResetDialog(true)} onClick={() => setShowResetDialog(true)}
> >
{t("common.reset", "Reset")} {t("common.reset")}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{t("pages.config.reset_confirm_title", "Reset Changes")} {t("pages.config.reset_confirm_title")}
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t( {t("pages.config.reset_confirm_desc")}
"pages.config.reset_confirm_desc",
"Are you sure you want to reset your unsaved changes back to the last saved state?",
)}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel> <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
{t("common.cancel", "Cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={confirmReset}> <AlertDialogAction onClick={confirmReset}>
{t("common.confirm", "Confirm")} {t("common.confirm")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<Button onClick={handleSave} disabled={mutation.isPending}> <Button onClick={handleSave} disabled={mutation.isPending}>
{mutation.isPending {mutation.isPending ? t("common.saving") : t("common.save")}
? t("common.saving", "Saving...")
: t("common.save", "Save")}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -34,7 +34,7 @@ export function AnthropicCredentialCard({
const { t } = useTranslation() const { t } = useTranslation()
const actionBusy = activeAction !== "" const actionBusy = activeAction !== ""
const tokenLoading = activeAction === "anthropic:token" const tokenLoading = activeAction === "anthropic:token"
const stopLabel = t("credentials.actions.stopLoading", "Stop Loading") const stopLabel = t("credentials.actions.stopLoading")
return ( return (
<CredentialCard <CredentialCard
@ -46,10 +46,7 @@ export function AnthropicCredentialCard({
<span>Anthropic</span> <span>Anthropic</span>
</span> </span>
} }
description={t( description={t("credentials.providers.anthropic.description")}
"credentials.providers.anthropic.description",
"Uses token login for Claude access.",
)}
status={status?.status ?? "not_logged_in"} status={status?.status ?? "not_logged_in"}
authMethod={status?.auth_method} authMethod={status?.auth_method}
actions={ actions={
@ -60,10 +57,7 @@ export function AnthropicCredentialCard({
value={token} value={token}
onChange={(e) => onTokenChange(e.target.value)} onChange={(e) => onTokenChange(e.target.value)}
type="password" type="password"
placeholder={t( placeholder={t("credentials.fields.anthropicToken")}
"credentials.fields.anthropicToken",
"Anthropic token",
)}
/> />
<Button <Button
size="sm" size="sm"
@ -75,7 +69,7 @@ export function AnthropicCredentialCard({
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
<IconKey className="size-4" /> <IconKey className="size-4" />
{t("credentials.actions.saveToken", "Save")} {t("credentials.actions.saveToken")}
</Button> </Button>
{tokenLoading && ( {tokenLoading && (
<Button <Button
@ -105,7 +99,7 @@ export function AnthropicCredentialCard({
{activeAction === "anthropic:logout" && ( {activeAction === "anthropic:logout" && (
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
{t("credentials.actions.logout", "Logout")} {t("credentials.actions.logout")}
</Button> </Button>
) : null ) : null
} }

View file

@ -40,22 +40,19 @@ export function AntigravityCredentialCard({
<span>Google Antigravity</span> <span>Google Antigravity</span>
</span> </span>
} }
description={t( description={t("credentials.providers.antigravity.description")}
"credentials.providers.antigravity.description",
"Uses browser OAuth for Google Cloud Code Assist.",
)}
status={status?.status ?? "not_logged_in"} status={status?.status ?? "not_logged_in"}
authMethod={status?.auth_method} authMethod={status?.auth_method}
details={ details={
<div className="space-y-1"> <div className="space-y-1">
{status?.email && ( {status?.email && (
<p> <p>
{t("credentials.labels.email", "Email")}: {status.email} {t("credentials.labels.email")}: {status.email}
</p> </p>
)} )}
{status?.project_id && ( {status?.project_id && (
<p> <p>
{t("credentials.labels.project", "Project")}: {status.project_id} {t("credentials.labels.project")}: {status.project_id}
</p> </p>
)} )}
</div> </div>
@ -73,7 +70,7 @@ export function AntigravityCredentialCard({
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
<IconLockOpen className="size-4" /> <IconLockOpen className="size-4" />
{t("credentials.actions.browser", "Browser OAuth")} {t("credentials.actions.browser")}
</Button> </Button>
{browserLoading && ( {browserLoading && (
<Button <Button
@ -100,7 +97,7 @@ export function AntigravityCredentialCard({
{activeAction === "google-antigravity:logout" && ( {activeAction === "google-antigravity:logout" && (
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
{t("credentials.actions.logout", "Logout")} {t("credentials.actions.logout")}
</Button> </Button>
) : null ) : null
} }

View file

@ -42,15 +42,12 @@ export function CredentialsPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader title={t("navigation.credentials", "Credentials")} /> <PageHeader title={t("navigation.credentials")} />
<div className="min-h-0 flex-1 overflow-y-auto px-4 sm:px-6"> <div className="min-h-0 flex-1 overflow-y-auto px-4 sm:px-6">
<div className="pt-2"> <div className="pt-2">
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{t( {t("credentials.description")}
"credentials.description",
"Manage OAuth and token-based credentials for supported providers.",
)}
</p> </p>
</div> </div>
@ -62,9 +59,7 @@ export function CredentialsPage() {
{activeFlow && ( {activeFlow && (
<div className="bg-muted mt-4 rounded-lg border px-4 py-3 text-sm"> <div className="bg-muted mt-4 rounded-lg border px-4 py-3 text-sm">
<p className="font-medium"> <p className="font-medium">{t("credentials.flow.current")}</p>
{t("credentials.flow.current", "Current authentication status")}
</p>
<p className="text-muted-foreground mt-1">{flowHint}</p> <p className="text-muted-foreground mt-1">{flowHint}</p>
</div> </div>
)} )}
@ -72,7 +67,7 @@ export function CredentialsPage() {
{loading ? ( {loading ? (
<div className="text-muted-foreground flex items-center gap-2 py-10 text-sm"> <div className="text-muted-foreground flex items-center gap-2 py-10 text-sm">
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
{t("credentials.loading", "Loading credentials...")} {t("credentials.loading")}
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-4 py-5 lg:auto-rows-fr lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 py-5 lg:auto-rows-fr lg:grid-cols-3">

View file

@ -34,21 +34,16 @@ export function DeviceCodeSheet({
className="data-[side=right]:!w-full data-[side=right]:sm:!w-[480px] data-[side=right]:sm:!max-w-[480px]" className="data-[side=right]:!w-full data-[side=right]:sm:!w-[480px] data-[side=right]:sm:!max-w-[480px]"
> >
<SheetHeader className="border-b-muted border-b px-6 py-5"> <SheetHeader className="border-b-muted border-b px-6 py-5">
<SheetTitle> <SheetTitle>{t("credentials.device.title")}</SheetTitle>
{t("credentials.device.title", "OpenAI Device Login")}
</SheetTitle>
<SheetDescription> <SheetDescription>
{t( {t("credentials.device.description")}
"credentials.device.description",
"Open the verification page and enter the code below. This page will refresh automatically.",
)}
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<div className="space-y-4 px-6 py-5"> <div className="space-y-4 px-6 py-5">
<div> <div>
<p className="text-muted-foreground text-xs uppercase"> <p className="text-muted-foreground text-xs uppercase">
{t("credentials.device.code", "User Code")} {t("credentials.device.code")}
</p> </p>
<p className="mt-1 rounded-md border px-3 py-2 font-mono text-lg font-semibold tracking-wide"> <p className="mt-1 rounded-md border px-3 py-2 font-mono text-lg font-semibold tracking-wide">
{flow?.user_code || "-"} {flow?.user_code || "-"}
@ -57,7 +52,7 @@ export function DeviceCodeSheet({
<div> <div>
<p className="text-muted-foreground text-xs uppercase"> <p className="text-muted-foreground text-xs uppercase">
{t("credentials.device.url", "Verification URL")} {t("credentials.device.url")}
</p> </p>
<a <a
href={flow?.verify_url || "#"} href={flow?.verify_url || "#"}
@ -71,7 +66,7 @@ export function DeviceCodeSheet({
<div className="text-muted-foreground flex items-center gap-2 text-sm"> <div className="text-muted-foreground flex items-center gap-2 text-sm">
<IconRefresh className="size-4" /> <IconRefresh className="size-4" />
{t("credentials.device.polling", "Polling login status...")} {t("credentials.device.polling")}
</div> </div>
{flow && ( {flow && (
@ -83,11 +78,11 @@ export function DeviceCodeSheet({
<SheetFooter className="border-t-muted border-t px-6 py-4"> <SheetFooter className="border-t-muted border-t px-6 py-4">
<Button variant="ghost" onClick={() => onOpenChange(false)}> <Button variant="ghost" onClick={() => onOpenChange(false)}>
{t("common.cancel", "Close")} {t("common.cancel")}
</Button> </Button>
<Button asChild disabled={!flow?.verify_url}> <Button asChild disabled={!flow?.verify_url}>
<a href={flow?.verify_url || "#"} target="_blank" rel="noreferrer"> <a href={flow?.verify_url || "#"} target="_blank" rel="noreferrer">
{t("credentials.device.open", "Open Verification Page")} {t("credentials.device.open")}
</a> </a>
</Button> </Button>
</SheetFooter> </SheetFooter>

View file

@ -34,7 +34,7 @@ export function LogoutConfirmDialog({
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{t("credentials.logoutDialog.title", "Logout provider?")} {t("credentials.logoutDialog.title")}
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t( {t(
@ -45,10 +45,10 @@ export function LogoutConfirmDialog({
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel", "Cancel")}</AlertDialogCancel> <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} variant="destructive"> <AlertDialogAction onClick={onConfirm} variant="destructive">
{isSubmitting && <IconLoader2 className="size-4 animate-spin" />} {isSubmitting && <IconLoader2 className="size-4 animate-spin" />}
{t("credentials.actions.logout", "Logout")} {t("credentials.actions.logout")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View file

@ -53,16 +53,13 @@ export function OpenAICredentialCard({
<span>OpenAI</span> <span>OpenAI</span>
</span> </span>
} }
description={t( description={t("credentials.providers.openai.description")}
"credentials.providers.openai.description",
"Supports browser OAuth, device code, and token login.",
)}
status={status?.status ?? "not_logged_in"} status={status?.status ?? "not_logged_in"}
authMethod={status?.auth_method} authMethod={status?.auth_method}
details={ details={
status?.account_id ? ( status?.account_id ? (
<p> <p>
{t("credentials.labels.account", "Account")}: {status.account_id} {t("credentials.labels.account")}: {status.account_id}
</p> </p>
) : null ) : null
} }
@ -81,7 +78,7 @@ export function OpenAICredentialCard({
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
<IconBrandOpenai className="size-4" /> <IconBrandOpenai className="size-4" />
{t("credentials.actions.browser", "Browser OAuth")} {t("credentials.actions.browser")}
</Button> </Button>
{oauthLoading && !deviceLoading && ( {oauthLoading && !deviceLoading && (
@ -105,7 +102,7 @@ export function OpenAICredentialCard({
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
<IconClockHour4 className="size-4" /> <IconClockHour4 className="size-4" />
{t("credentials.actions.deviceCode", "Device Code")} {t("credentials.actions.deviceCode")}
</Button> </Button>
</div> </div>
</div> </div>
@ -116,10 +113,7 @@ export function OpenAICredentialCard({
value={token} value={token}
onChange={(e) => onTokenChange(e.target.value)} onChange={(e) => onTokenChange(e.target.value)}
type="password" type="password"
placeholder={t( placeholder={t("credentials.fields.openaiToken")}
"credentials.fields.openaiToken",
"OpenAI token",
)}
/> />
<Button <Button
size="sm" size="sm"
@ -130,7 +124,7 @@ export function OpenAICredentialCard({
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
<IconKey className="size-4" /> <IconKey className="size-4" />
{t("credentials.actions.saveToken", "Save")} {t("credentials.actions.saveToken")}
</Button> </Button>
{tokenLoading && ( {tokenLoading && (
<Button <Button
@ -159,7 +153,7 @@ export function OpenAICredentialCard({
{activeAction === "openai:logout" && ( {activeAction === "openai:logout" && (
<IconLoader2 className="size-4 animate-spin" /> <IconLoader2 className="size-4 animate-spin" />
)} )}
{t("credentials.actions.logout", "Logout")} {t("credentials.actions.logout")}
</Button> </Button>
) : null ) : null
} }

View file

@ -26,12 +26,12 @@ export function ProviderStatusLine({
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className={`rounded px-2 py-1 text-xs font-medium ${style}`}> <span className={`rounded px-2 py-1 text-xs font-medium ${style}`}>
{status === "connected" {status === "connected"
? t("credentials.status.connected", "Connected") ? t("credentials.status.connected")
: status === "needs_refresh" : status === "needs_refresh"
? t("credentials.status.needsRefresh", "Needs refresh") ? t("credentials.status.needsRefresh")
: status === "expired" : status === "expired"
? t("credentials.status.expired", "Expired") ? t("credentials.status.expired")
: t("credentials.status.notLoggedIn", "Not logged in")} : t("credentials.status.notLoggedIn")}
</span> </span>
{authMethod && ( {authMethod && (
<span className="text-muted-foreground text-xs uppercase"> <span className="text-muted-foreground text-xs uppercase">

View file

@ -136,7 +136,7 @@ export function ModelsPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader title={t("navigation.models", "Models")}> <PageHeader title={t("navigation.models")}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button size="sm" variant="outline" onClick={() => setAddOpen(true)}> <Button size="sm" variant="outline" onClick={() => setAddOpen(true)}>
<IconPlus className="size-4" /> <IconPlus className="size-4" />

View file

@ -53,9 +53,7 @@ export function useCredentialsPage() {
setError("") setError("")
} catch (err) { } catch (err) {
setError( setError(
err instanceof Error err instanceof Error ? err.message : t("credentials.errors.loadFailed"),
? err.message
: t("credentials.errors.loadFailed", "Failed to load credentials"),
) )
} finally { } finally {
setLoading(false) setLoading(false)
@ -113,10 +111,7 @@ export function useCredentialsPage() {
setError( setError(
err instanceof Error err instanceof Error
? err.message ? err.message
: t( : t("credentials.errors.flowFailed"),
"credentials.errors.flowFailed",
"Failed to check authentication flow",
),
) )
} }
} }
@ -196,12 +191,7 @@ export function useCredentialsPage() {
return return
} }
setActiveAction("") setActiveAction("")
setError( setError(t("credentials.errors.popupBlocked"))
t(
"credentials.errors.popupBlocked",
"Unable to open a new tab. Please allow popups and try again.",
),
)
return return
} }
@ -212,12 +202,7 @@ export function useCredentialsPage() {
return return
} }
if (!resp.auth_url || !resp.flow_id) { if (!resp.auth_url || !resp.flow_id) {
throw new Error( throw new Error(t("credentials.errors.invalidBrowserResponse"))
t(
"credentials.errors.invalidBrowserResponse",
"Invalid browser login response",
),
)
} }
authTab.location.href = resp.auth_url authTab.location.href = resp.auth_url
@ -242,7 +227,7 @@ export function useCredentialsPage() {
setError( setError(
err instanceof Error err instanceof Error
? err.message ? err.message
: t("credentials.errors.loginFailed", "Login failed"), : t("credentials.errors.loginFailed"),
) )
} }
}, },
@ -263,12 +248,7 @@ export function useCredentialsPage() {
return return
} }
if (!resp.flow_id || !resp.user_code || !resp.verify_url) { if (!resp.flow_id || !resp.user_code || !resp.verify_url) {
throw new Error( throw new Error(t("credentials.errors.invalidDeviceResponse"))
t(
"credentials.errors.invalidDeviceResponse",
"Invalid device code response",
),
)
} }
const flow: OAuthFlowState = { const flow: OAuthFlowState = {
@ -296,7 +276,7 @@ export function useCredentialsPage() {
setError( setError(
err instanceof Error err instanceof Error
? err.message ? err.message
: t("credentials.errors.loginFailed", "Login failed"), : t("credentials.errors.loginFailed"),
) )
} }
}, [bumpActionToken, isActionTokenCurrent, t]) }, [bumpActionToken, isActionTokenCurrent, t])
@ -320,7 +300,7 @@ export function useCredentialsPage() {
setError( setError(
err instanceof Error err instanceof Error
? err.message ? err.message
: t("credentials.errors.loginFailed", "Login failed"), : t("credentials.errors.loginFailed"),
) )
} finally { } finally {
setActiveAction("") setActiveAction("")
@ -342,7 +322,7 @@ export function useCredentialsPage() {
setError( setError(
err instanceof Error err instanceof Error
? err.message ? err.message
: t("credentials.errors.logoutFailed", "Logout failed"), : t("credentials.errors.logoutFailed"),
) )
} finally { } finally {
setActiveAction("") setActiveAction("")
@ -415,17 +395,15 @@ export function useCredentialsPage() {
return "" return ""
} }
if (activeFlow.status === "pending") { if (activeFlow.status === "pending") {
return t("credentials.flow.pending", "Waiting for authorization...") return t("credentials.flow.pending")
} }
if (activeFlow.status === "success") { if (activeFlow.status === "success") {
return t("credentials.flow.success", "Authentication successful") return t("credentials.flow.success")
} }
if (activeFlow.status === "expired") { if (activeFlow.status === "expired") {
return t("credentials.flow.expired", "Authentication session expired") return t("credentials.flow.expired")
} }
return ( return activeFlow.error || t("credentials.flow.error")
activeFlow.error || t("credentials.flow.error", "Authentication failed")
)
}, [activeFlow, t]) }, [activeFlow, t])
return { return {

View file

@ -6,7 +6,6 @@
"credentials": "Credentials", "credentials": "Credentials",
"services": "Services", "services": "Services",
"channels_group": "Channels", "channels_group": "Channels",
"channels": "Channels",
"show_more_channels": "More", "show_more_channels": "More",
"show_less_channels": "Less", "show_less_channels": "Less",
"config": "Config", "config": "Config",
@ -15,13 +14,8 @@
"chat": { "chat": {
"welcome": "How can I help you today?", "welcome": "How can I help you today?",
"welcomeDesc": "Ask me about weather, settings, or any other tasks. I'm here to assist you.", "welcomeDesc": "Ask me about weather, settings, or any other tasks. I'm here to assist you.",
"model": "Model",
"user": "User",
"placeholder": "Start a new message...", "placeholder": "Start a new message...",
"attach": "Attach file",
"voice": "Voice input",
"newChat": "New Chat", "newChat": "New Chat",
"connecting": "Connecting...",
"notConnected": "Gateway is not running. Start it to chat.", "notConnected": "Gateway is not running. Start it to chat.",
"thinking": { "thinking": {
"step1": "Thinking...", "step1": "Thinking...",
@ -29,12 +23,6 @@
"step3": "Preparing response...", "step3": "Preparing response...",
"step4": "Almost there..." "step4": "Almost there..."
}, },
"time": {
"justNow": "just now",
"minsAgo": "{{count}}m ago",
"hoursAgo": "{{count}}h ago",
"daysAgo": "{{count}}d ago"
},
"history": "History", "history": "History",
"noHistory": "No chat history yet", "noHistory": "No chat history yet",
"loadingMore": "Loading more...", "loadingMore": "Loading more...",
@ -79,6 +67,9 @@
"reset": "Reset", "reset": "Reset",
"confirm": "Confirm" "confirm": "Confirm"
}, },
"labels": {
"loading": "Loading..."
},
"credentials": { "credentials": {
"description": "Manage OAuth and token-based credentials for supported providers.", "description": "Manage OAuth and token-based credentials for supported providers.",
"loading": "Loading credentials...", "loading": "Loading credentials...",
@ -102,6 +93,7 @@
"actions": { "actions": {
"browser": "Browser OAuth", "browser": "Browser OAuth",
"deviceCode": "Device Code", "deviceCode": "Device Code",
"stopLoading": "Stop Loading",
"saveToken": "Save", "saveToken": "Save",
"logout": "Logout" "logout": "Logout"
}, },
@ -146,9 +138,6 @@
"models": { "models": {
"description": "Configure API keys for AI providers. Only configured models are available for chat.", "description": "Configure API keys for AI providers. Only configured models are available for chat.",
"loadError": "Failed to load models", "loadError": "Failed to load models",
"header": {
"configured": "configured"
},
"noDefaultHintPrefix": "No default model set yet. Click", "noDefaultHintPrefix": "No default model set yet. Click",
"noDefaultHintSuffix": "to set one.", "noDefaultHintSuffix": "to set one.",
"status": { "status": {
@ -185,8 +174,7 @@
"delete": { "delete": {
"title": "Delete Model?", "title": "Delete Model?",
"description": "\"{{name}}\" will be permanently removed from your model list. This cannot be undone.", "description": "\"{{name}}\" will be permanently removed from your model list. This cannot be undone.",
"confirm": "Delete", "confirm": "Delete"
"errorDefault": "Cannot delete the default model. Please set another model as default first."
}, },
"advanced": { "advanced": {
"toggle": "Advanced options" "toggle": "Advanced options"
@ -221,19 +209,10 @@
} }
}, },
"channels": { "channels": {
"description": "Configure messaging channels to connect your AI agent to chat platforms.",
"loadError": "Failed to load channels", "loadError": "Failed to load channels",
"search": "Search channels...", "edit": "Configure {{name}}",
"noResults": "No channels match your search.",
"header": {
"enabled": "enabled"
},
"status": { "status": {
"configured": "Configured", "configured": "Configured"
"unconfigured": "Not configured"
},
"action": {
"configure": "Configure"
}, },
"name": { "name": {
"telegram": "Telegram", "telegram": "Telegram",
@ -265,7 +244,6 @@
"encryptKey": "Encrypt Key", "encryptKey": "Encrypt Key",
"baseUrl": "API Base URL", "baseUrl": "API Base URL",
"proxy": "HTTP Proxy", "proxy": "HTTP Proxy",
"proxyHint": "Optional. e.g. http://127.0.0.1:7890",
"mentionOnly": "Mention Only", "mentionOnly": "Mention Only",
"typingEnabled": "Typing Indicator", "typingEnabled": "Typing Indicator",
"placeholderEnabled": "Placeholder Message", "placeholderEnabled": "Placeholder Message",
@ -273,32 +251,17 @@
"groupTriggerMentionOnly": "Group Mention Only", "groupTriggerMentionOnly": "Group Mention Only",
"groupTriggerPrefixes": "Group Trigger Prefixes", "groupTriggerPrefixes": "Group Trigger Prefixes",
"allowFrom": "Allow From", "allowFrom": "Allow From",
"allowFromHint": "Comma-separated list of allowed user/group IDs. Leave empty to allow all.",
"allowFromPlaceholder": "e.g. 123456, 789012", "allowFromPlaceholder": "e.g. 123456, 789012",
"allowOrigins": "Allow Origins", "allowOrigins": "Allow Origins",
"allowOriginsHint": "Comma-separated list of allowed origins. Leave empty to allow all.",
"allowOriginsPlaceholder": "e.g. https://example.com, http://localhost:5173", "allowOriginsPlaceholder": "e.g. https://example.com, http://localhost:5173",
"secretPlaceholder": "Enter secret", "secretPlaceholder": "Enter secret",
"secretPlaceholderSet": "Leave blank to keep existing",
"secretHintSet": "A value is already set. Leave blank to keep it unchanged." "secretHintSet": "A value is already set. Leave blank to keep it unchanged."
}, },
"edit": {
"title": "Configure {{name}}",
"description": "Set up credentials and options for this channel.",
"saveError": "Failed to save channel configuration",
"saving": "Saving..."
},
"page": { "page": {
"notFound": "Channel \"{{name}}\" is not supported.", "notFound": "Channel \"{{name}}\" is not supported.",
"saveSuccess": "Channel configuration saved.", "saveSuccess": "Channel configuration saved.",
"saveError": "Failed to save channel configuration", "saveError": "Failed to save channel configuration",
"flowTitle": "Setup Flow",
"flowDescription": "Complete the following steps to put this channel into service.",
"step1": "Set credentials",
"step2": "Enable channel",
"step3": "Save and restart gateway",
"enabled": "enabled", "enabled": "enabled",
"disabled": "disabled",
"docLink": "Documentation", "docLink": "Documentation",
"enableLabel": "Enable channel" "enableLabel": "Enable channel"
}, },
@ -316,7 +279,6 @@
"mentionOnly": "Only respond when the bot is explicitly mentioned in group chats.", "mentionOnly": "Only respond when the bot is explicitly mentioned in group chats.",
"typingEnabled": "Display typing status while the assistant is generating a response.", "typingEnabled": "Display typing status while the assistant is generating a response.",
"placeholderEnabled": "Enable temporary placeholder messages before the final reply is sent.", "placeholderEnabled": "Enable temporary placeholder messages before the final reply is sent.",
"placeholderText": "Placeholder text shown while waiting for the final response.",
"groupTriggerMentionOnly": "In group chats, respond only when the bot is mentioned.", "groupTriggerMentionOnly": "In group chats, respond only when the bot is mentioned.",
"groupTriggerPrefixes": "Custom group-chat trigger prefixes, separated by commas.", "groupTriggerPrefixes": "Custom group-chat trigger prefixes, separated by commas.",
"allowFrom": "Allowed user or group IDs, separated by commas.", "allowFrom": "Allowed user or group IDs, separated by commas.",
@ -362,23 +324,8 @@
} }
}, },
"pages": { "pages": {
"providers": {
"description": "Manage AI model providers and configurations."
},
"models": {
"description": "Manage AI models here."
},
"credentials": {
"description": "Securely manage your API keys and credentials."
},
"config": { "config": {
"description": "System configuration and preferences.",
"visual_title": "Core Configuration",
"visual_desc": "Edit key runtime options here. Use Raw Config for full JSON editing.",
"load_error": "Failed to load configuration. Please refresh and try again.", "load_error": "Failed to load configuration. Please refresh and try again.",
"section_agents": "Agent Defaults",
"section_runtime": "Runtime",
"section_devices": "Devices",
"workspace": "Workspace Directory", "workspace": "Workspace Directory",
"workspace_hint": "Base directory for agent file operations.", "workspace_hint": "Base directory for agent file operations.",
"restrict_workspace": "Restrict to Workspace", "restrict_workspace": "Restrict to Workspace",
@ -422,10 +369,6 @@
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8", "allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
"launcher_load_error": "Failed to load service parameters.", "launcher_load_error": "Failed to load service parameters.",
"launcher_restart_hint": "Service parameter changes apply after restarting PicoClaw Web.", "launcher_restart_hint": "Service parameter changes apply after restarting PicoClaw Web.",
"autostart_enable_success": "Launch at login has been enabled.",
"autostart_disable_success": "Launch at login has been disabled.",
"autostart_update_error": "Failed to update launch-at-login setting.",
"advanced_title": "Need full configuration access?",
"advanced_desc": "Open the raw JSON page to edit every field directly.", "advanced_desc": "Open the raw JSON page to edit every field directly.",
"open_raw": "Raw Config", "open_raw": "Raw Config",
"back_to_visual": "Visual Config", "back_to_visual": "Visual Config",
@ -441,7 +384,6 @@
"format_success": "JSON formatted successfully.", "format_success": "JSON formatted successfully.",
"format_error": "Invalid JSON format.", "format_error": "Invalid JSON format.",
"format": "Format", "format": "Format",
"lose_unsaved_changes": "You have unsaved changes. Are you sure you want to reset and lose these changes?",
"unsaved_changes": "You have unsaved changes." "unsaved_changes": "You have unsaved changes."
}, },
"logs": { "logs": {

View file

@ -6,7 +6,6 @@
"credentials": "凭据", "credentials": "凭据",
"services": "服务", "services": "服务",
"channels_group": "频道", "channels_group": "频道",
"channels": "频道",
"show_more_channels": "更多", "show_more_channels": "更多",
"show_less_channels": "收起", "show_less_channels": "收起",
"config": "配置", "config": "配置",
@ -15,13 +14,8 @@
"chat": { "chat": {
"welcome": "今天我能为您做些什么?", "welcome": "今天我能为您做些什么?",
"welcomeDesc": "您可以询问我天气、设置或其他任何任务,我随时为您效劳。", "welcomeDesc": "您可以询问我天气、设置或其他任何任务,我随时为您效劳。",
"model": "模型",
"user": "用户",
"placeholder": "输入新消息...", "placeholder": "输入新消息...",
"attach": "附加文件",
"voice": "语音输入",
"newChat": "新建对话", "newChat": "新建对话",
"connecting": "连接中...",
"notConnected": "服务未运行,请先启动以进行对话。", "notConnected": "服务未运行,请先启动以进行对话。",
"thinking": { "thinking": {
"step1": "思考中...", "step1": "思考中...",
@ -29,12 +23,6 @@
"step3": "准备回复...", "step3": "准备回复...",
"step4": "马上就好..." "step4": "马上就好..."
}, },
"time": {
"justNow": "刚刚",
"minsAgo": "{{count}}分钟前",
"hoursAgo": "{{count}}小时前",
"daysAgo": "{{count}}天前"
},
"history": "历史记录", "history": "历史记录",
"noHistory": "暂无对话历史", "noHistory": "暂无对话历史",
"loadingMore": "加载更多...", "loadingMore": "加载更多...",
@ -79,6 +67,9 @@
"reset": "重置", "reset": "重置",
"confirm": "确认" "confirm": "确认"
}, },
"labels": {
"loading": "加载中..."
},
"credentials": { "credentials": {
"description": "管理已支持服务商的 OAuth 与 Token 凭据。", "description": "管理已支持服务商的 OAuth 与 Token 凭据。",
"loading": "正在加载凭据...", "loading": "正在加载凭据...",
@ -102,6 +93,7 @@
"actions": { "actions": {
"browser": "浏览器 OAuth", "browser": "浏览器 OAuth",
"deviceCode": "设备码", "deviceCode": "设备码",
"stopLoading": "停止加载",
"saveToken": "保存", "saveToken": "保存",
"logout": "退出登录" "logout": "退出登录"
}, },
@ -146,9 +138,6 @@
"models": { "models": {
"description": "为 AI 服务商配置 API Key。只有已配置的模型可用于对话。", "description": "为 AI 服务商配置 API Key。只有已配置的模型可用于对话。",
"loadError": "加载模型列表失败", "loadError": "加载模型列表失败",
"header": {
"configured": "已配置"
},
"noDefaultHintPrefix": "尚未设置默认模型,点击", "noDefaultHintPrefix": "尚未设置默认模型,点击",
"noDefaultHintSuffix": "设为默认。", "noDefaultHintSuffix": "设为默认。",
"status": { "status": {
@ -185,8 +174,7 @@
"delete": { "delete": {
"title": "确认删除模型?", "title": "确认删除模型?",
"description": "「{{name}}」将从模型列表中永久移除,此操作不可撤销。", "description": "「{{name}}」将从模型列表中永久移除,此操作不可撤销。",
"confirm": "删除", "confirm": "删除"
"errorDefault": "无法删除默认模型。请先将其他模型设为默认。"
}, },
"advanced": { "advanced": {
"toggle": "高级选项" "toggle": "高级选项"
@ -221,19 +209,10 @@
} }
}, },
"channels": { "channels": {
"description": "配置消息频道,将 AI 助手连接到各聊天平台。",
"loadError": "加载频道列表失败", "loadError": "加载频道列表失败",
"search": "搜索频道...", "edit": "配置 {{name}}",
"noResults": "没有匹配的频道。",
"header": {
"enabled": "已启用"
},
"status": { "status": {
"configured": "已配置", "configured": "已配置"
"unconfigured": "未配置"
},
"action": {
"configure": "配置"
}, },
"name": { "name": {
"telegram": "Telegram", "telegram": "Telegram",
@ -265,7 +244,6 @@
"encryptKey": "Encrypt Key", "encryptKey": "Encrypt Key",
"baseUrl": "API Base URL", "baseUrl": "API Base URL",
"proxy": "HTTP 代理", "proxy": "HTTP 代理",
"proxyHint": "可选。例如 http://127.0.0.1:7890",
"mentionOnly": "仅提及时响应", "mentionOnly": "仅提及时响应",
"typingEnabled": "输入中提示", "typingEnabled": "输入中提示",
"placeholderEnabled": "占位消息", "placeholderEnabled": "占位消息",
@ -273,32 +251,17 @@
"groupTriggerMentionOnly": "群聊仅提及时响应", "groupTriggerMentionOnly": "群聊仅提及时响应",
"groupTriggerPrefixes": "群聊触发前缀", "groupTriggerPrefixes": "群聊触发前缀",
"allowFrom": "允许来源", "allowFrom": "允许来源",
"allowFromHint": "用逗号分隔的用户/群组 ID 列表,留空表示允许所有。",
"allowFromPlaceholder": "例如 123456, 789012", "allowFromPlaceholder": "例如 123456, 789012",
"allowOrigins": "允许来源域名", "allowOrigins": "允许来源域名",
"allowOriginsHint": "用逗号分隔允许的 Origin留空表示允许所有。",
"allowOriginsPlaceholder": "例如 https://example.com, http://localhost:5173", "allowOriginsPlaceholder": "例如 https://example.com, http://localhost:5173",
"secretPlaceholder": "输入密钥", "secretPlaceholder": "输入密钥",
"secretPlaceholderSet": "留空保持原有值不变",
"secretHintSet": "已设置密钥,留空表示不修改。" "secretHintSet": "已设置密钥,留空表示不修改。"
}, },
"edit": {
"title": "配置 {{name}}",
"description": "设置此频道的凭据和选项。",
"saveError": "保存频道配置失败",
"saving": "保存中..."
},
"page": { "page": {
"notFound": "不支持频道“{{name}}”。", "notFound": "不支持频道“{{name}}”。",
"saveSuccess": "频道配置已保存。", "saveSuccess": "频道配置已保存。",
"saveError": "保存频道配置失败", "saveError": "保存频道配置失败",
"flowTitle": "配置流程",
"flowDescription": "按以下步骤完成频道接入并投入使用。",
"step1": "填写凭据",
"step2": "启用频道",
"step3": "保存并重启网关",
"enabled": "已启用", "enabled": "已启用",
"disabled": "未启用",
"docLink": "配置文档", "docLink": "配置文档",
"enableLabel": "启用频道" "enableLabel": "启用频道"
}, },
@ -316,7 +279,6 @@
"mentionOnly": "在群聊中仅当明确提及时才响应。", "mentionOnly": "在群聊中仅当明确提及时才响应。",
"typingEnabled": "在生成回复时显示“正在输入”状态。", "typingEnabled": "在生成回复时显示“正在输入”状态。",
"placeholderEnabled": "在最终回复发送前,先发送临时占位消息。", "placeholderEnabled": "在最终回复发送前,先发送临时占位消息。",
"placeholderText": "等待最终回复期间显示的占位文案。",
"groupTriggerMentionOnly": "在群聊中仅当提及机器人时才响应。", "groupTriggerMentionOnly": "在群聊中仅当提及机器人时才响应。",
"groupTriggerPrefixes": "群聊触发前缀,多个值用逗号分隔。", "groupTriggerPrefixes": "群聊触发前缀,多个值用逗号分隔。",
"allowFrom": "允许访问的用户或群组 ID多个值用逗号分隔。", "allowFrom": "允许访问的用户或群组 ID多个值用逗号分隔。",
@ -362,23 +324,8 @@
} }
}, },
"pages": { "pages": {
"providers": {
"description": "管理各个 AI 模型服务商的接入配置。"
},
"models": {
"description": "在此管理您下载或借用的 AI 模型。"
},
"credentials": {
"description": "安全管理您的 API 密钥与访问凭据。"
},
"config": { "config": {
"description": "系统配置和偏好设置。",
"visual_title": "核心配置",
"visual_desc": "这里可编辑关键运行配置;若需完整字段请使用原始配置页。",
"load_error": "加载配置失败,请刷新后重试。", "load_error": "加载配置失败,请刷新后重试。",
"section_agents": "智能体默认设置",
"section_runtime": "运行时",
"section_devices": "设备功能",
"workspace": "工作目录", "workspace": "工作目录",
"workspace_hint": "智能体执行文件读写操作时使用的基础目录。", "workspace_hint": "智能体执行文件读写操作时使用的基础目录。",
"restrict_workspace": "限制工作目录访问", "restrict_workspace": "限制工作目录访问",
@ -422,10 +369,6 @@
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8", "allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
"launcher_load_error": "加载服务参数失败。", "launcher_load_error": "加载服务参数失败。",
"launcher_restart_hint": "服务参数变更需重启 PicoClaw Web 后生效。", "launcher_restart_hint": "服务参数变更需重启 PicoClaw Web 后生效。",
"autostart_enable_success": "已开启开机自启。",
"autostart_disable_success": "已关闭开机自启。",
"autostart_update_error": "更新开机自启设置失败。",
"advanced_title": "需要完整配置能力?",
"advanced_desc": "可打开原始 JSON 页面直接编辑全部字段。", "advanced_desc": "可打开原始 JSON 页面直接编辑全部字段。",
"open_raw": "原始配置", "open_raw": "原始配置",
"back_to_visual": "可视化配置", "back_to_visual": "可视化配置",
@ -441,7 +384,6 @@
"format_success": "JSON 格式化成功。", "format_success": "JSON 格式化成功。",
"format_error": "JSON 格式无效。", "format_error": "JSON 格式无效。",
"format": "格式化", "format": "格式化",
"lose_unsaved_changes": "您有未保存的更改。确定要重置并丢失这些更改吗?",
"unsaved_changes": "您有未保存的更改。" "unsaved_changes": "您有未保存的更改。"
}, },
"logs": { "logs": {

View file

@ -9,7 +9,6 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as ProvidersRouteImport } from './routes/providers'
import { Route as ModelsRouteImport } from './routes/models' import { Route as ModelsRouteImport } from './routes/models'
import { Route as LogsRouteImport } from './routes/logs' import { Route as LogsRouteImport } from './routes/logs'
import { Route as CredentialsRouteImport } from './routes/credentials' import { Route as CredentialsRouteImport } from './routes/credentials'
@ -19,11 +18,6 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as ConfigRawRouteImport } from './routes/config.raw' import { Route as ConfigRawRouteImport } from './routes/config.raw'
import { Route as ChannelsNameRouteImport } from './routes/channels/$name' import { Route as ChannelsNameRouteImport } from './routes/channels/$name'
const ProvidersRoute = ProvidersRouteImport.update({
id: '/providers',
path: '/providers',
getParentRoute: () => rootRouteImport,
} as any)
const ModelsRoute = ModelsRouteImport.update({ const ModelsRoute = ModelsRouteImport.update({
id: '/models', id: '/models',
path: '/models', path: '/models',
@ -72,7 +66,6 @@ export interface FileRoutesByFullPath {
'/credentials': typeof CredentialsRoute '/credentials': typeof CredentialsRoute
'/logs': typeof LogsRoute '/logs': typeof LogsRoute
'/models': typeof ModelsRoute '/models': typeof ModelsRoute
'/providers': typeof ProvidersRoute
'/channels/$name': typeof ChannelsNameRoute '/channels/$name': typeof ChannelsNameRoute
'/config/raw': typeof ConfigRawRoute '/config/raw': typeof ConfigRawRoute
} }
@ -83,7 +76,6 @@ export interface FileRoutesByTo {
'/credentials': typeof CredentialsRoute '/credentials': typeof CredentialsRoute
'/logs': typeof LogsRoute '/logs': typeof LogsRoute
'/models': typeof ModelsRoute '/models': typeof ModelsRoute
'/providers': typeof ProvidersRoute
'/channels/$name': typeof ChannelsNameRoute '/channels/$name': typeof ChannelsNameRoute
'/config/raw': typeof ConfigRawRoute '/config/raw': typeof ConfigRawRoute
} }
@ -95,7 +87,6 @@ export interface FileRoutesById {
'/credentials': typeof CredentialsRoute '/credentials': typeof CredentialsRoute
'/logs': typeof LogsRoute '/logs': typeof LogsRoute
'/models': typeof ModelsRoute '/models': typeof ModelsRoute
'/providers': typeof ProvidersRoute
'/channels/$name': typeof ChannelsNameRoute '/channels/$name': typeof ChannelsNameRoute
'/config/raw': typeof ConfigRawRoute '/config/raw': typeof ConfigRawRoute
} }
@ -108,7 +99,6 @@ export interface FileRouteTypes {
| '/credentials' | '/credentials'
| '/logs' | '/logs'
| '/models' | '/models'
| '/providers'
| '/channels/$name' | '/channels/$name'
| '/config/raw' | '/config/raw'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
@ -119,7 +109,6 @@ export interface FileRouteTypes {
| '/credentials' | '/credentials'
| '/logs' | '/logs'
| '/models' | '/models'
| '/providers'
| '/channels/$name' | '/channels/$name'
| '/config/raw' | '/config/raw'
id: id:
@ -130,7 +119,6 @@ export interface FileRouteTypes {
| '/credentials' | '/credentials'
| '/logs' | '/logs'
| '/models' | '/models'
| '/providers'
| '/channels/$name' | '/channels/$name'
| '/config/raw' | '/config/raw'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
@ -142,18 +130,10 @@ export interface RootRouteChildren {
CredentialsRoute: typeof CredentialsRoute CredentialsRoute: typeof CredentialsRoute
LogsRoute: typeof LogsRoute LogsRoute: typeof LogsRoute
ModelsRoute: typeof ModelsRoute ModelsRoute: typeof ModelsRoute
ProvidersRoute: typeof ProvidersRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/providers': {
id: '/providers'
path: '/providers'
fullPath: '/providers'
preLoaderRoute: typeof ProvidersRouteImport
parentRoute: typeof rootRouteImport
}
'/models': { '/models': {
id: '/models' id: '/models'
path: '/models' path: '/models'
@ -243,7 +223,6 @@ const rootRouteChildren: RootRouteChildren = {
CredentialsRoute: CredentialsRoute, CredentialsRoute: CredentialsRoute,
LogsRoute: LogsRoute, LogsRoute: LogsRoute,
ModelsRoute: ModelsRoute, ModelsRoute: ModelsRoute,
ProvidersRoute: ProvidersRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View file

@ -15,13 +15,11 @@ function RawConfigPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader <PageHeader title={t("pages.config.raw_json_title")}>
title={t("pages.config.raw_json_title", "Raw JSON Configuration")}
>
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link to="/config"> <Link to="/config">
<IconAdjustments className="size-4" /> <IconAdjustments className="size-4" />
{t("pages.config.back_to_visual", "Visual Config")} {t("pages.config.back_to_visual")}
</Link> </Link>
</Button> </Button>
</PageHeader> </PageHeader>

View file

@ -87,15 +87,15 @@ function LogsPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader title={t("navigation.logs", "Logs")} /> <PageHeader title={t("navigation.logs")} />
<div className="flex flex-1 flex-col overflow-hidden p-4 sm:p-8"> <div className="flex flex-1 flex-col overflow-hidden p-4 sm:p-8">
<div className="mb-4"> <div className="mb-4">
<h1 className="text-2xl font-semibold tracking-tight"> <h1 className="text-2xl font-semibold tracking-tight">
{t("navigation.logs", "Logs")} {t("navigation.logs")}
</h1> </h1>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
{t("pages.logs.description", "System logs and monitoring.")} {t("pages.logs.description")}
</p> </p>
</div> </div>

View file

@ -1,30 +0,0 @@
import { createFileRoute } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { PageHeader } from "@/components/page-header"
export const Route = createFileRoute("/providers")({
component: ProvidersPage,
})
function ProvidersPage() {
const { t } = useTranslation()
return (
<div className="flex h-full flex-col">
<PageHeader title={t("navigation.providers", "Providers")} />
<div className="flex flex-1 items-center justify-center p-8">
<div className="text-center">
<h1 className="text-2xl font-semibold tracking-tight">
{t("navigation.providers", "Providers")}
</h1>
<p className="text-muted-foreground mt-2 text-sm">
{t(
"pages.providers.description",
"Manage AI model providers and configurations.",
)}
</p>
</div>
</div>
</div>
)
}