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:
parent
b06e9e3707
commit
a418554441
23 changed files with 149 additions and 495 deletions
|
|
@ -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 absolute inline-flex size-full animate-ping rounded-full opacity-75"></span>
|
||||
</span>
|
||||
{t(
|
||||
"chat.notConnected",
|
||||
"Gateway is not running. Start it to chat.",
|
||||
)}
|
||||
{t("chat.notConnected")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -99,24 +96,19 @@ export function AppHeader() {
|
|||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("header.gateway.stopDialog.title", "Stop Gateway Service?")}
|
||||
{t("header.gateway.stopDialog.title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
"header.gateway.stopDialog.description",
|
||||
"Are you sure you want to stop the gateway? This will disconnect your active chat sessions and halt inference.",
|
||||
)}
|
||||
{t("header.gateway.stopDialog.description")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("common.cancel", "Cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmStop}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{t("header.gateway.stopDialog.confirm", "Stop Gateway")}
|
||||
{t("header.gateway.stopDialog.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
@ -146,10 +138,10 @@ export function AppHeader() {
|
|||
)}
|
||||
<span className="text-xs font-semibold">
|
||||
{isRunning
|
||||
? t("header.gateway.action.stop", "Stop Gateway")
|
||||
? t("header.gateway.action.stop")
|
||||
: isStarting
|
||||
? t("header.gateway.status.starting", "Starting Gateway...")
|
||||
: t("header.gateway.action.start", "Start Gateway")}
|
||||
? t("header.gateway.status.starting")
|
||||
: t("header.gateway.action.start")}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ export function ChannelConfigPage({ channelName }: ChannelConfigPageProps) {
|
|||
<div className="w-full max-w-250 space-y-5 pt-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<p className="font-medium">
|
||||
{t("channels.edit.title", {
|
||||
{t("channels.edit", {
|
||||
name: channelDisplayName,
|
||||
})}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export function ChatPage() {
|
|||
return (
|
||||
<div className="bg-background/95 flex h-full flex-col">
|
||||
<PageHeader
|
||||
title={t("navigation.chat", "Chat")}
|
||||
title={t("navigation.chat")}
|
||||
titleExtra={
|
||||
hasConfiguredModels && (
|
||||
<ModelSelector
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function ModelSelector({
|
|||
<SelectContent>
|
||||
{apiKeyModels.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("chat.modelGroup.apikey", "API Key")}</SelectLabel>
|
||||
<SelectLabel>{t("chat.modelGroup.apikey")}</SelectLabel>
|
||||
{apiKeyModels.map((model) => (
|
||||
<SelectItem key={model.index} value={model.model_name}>
|
||||
{model.model_name}
|
||||
|
|
@ -55,7 +55,7 @@ export function ModelSelector({
|
|||
|
||||
{oauthModels.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("chat.modelGroup.oauth", "OAuth")}</SelectLabel>
|
||||
<SelectLabel>{t("chat.modelGroup.oauth")}</SelectLabel>
|
||||
{oauthModels.map((model) => (
|
||||
<SelectItem key={model.index} value={model.model_name}>
|
||||
{model.model_name}
|
||||
|
|
@ -70,7 +70,7 @@ export function ModelSelector({
|
|||
|
||||
{localModels.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("chat.modelGroup.local", "Local")}</SelectLabel>
|
||||
<SelectLabel>{t("chat.modelGroup.local")}</SelectLabel>
|
||||
{localModels.map((model) => (
|
||||
<SelectItem key={model.index} value={model.model_name}>
|
||||
{model.model_name}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function SessionHistoryMenu({
|
|||
<Button
|
||||
variant="ghost"
|
||||
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"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -87,7 +87,7 @@ export function SessionHistoryMenu({
|
|||
{hasMore && sessions.length > 0 && (
|
||||
<div ref={observerRef} className="py-2 text-center">
|
||||
<span className="text-muted-foreground animate-pulse text-xs">
|
||||
{t("chat.loadingMore", "Loading more...")}
|
||||
{t("chat.loadingMore")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -106,29 +106,14 @@ export function ConfigPage() {
|
|||
|
||||
const autoStartSupported = autoStartStatus?.supported !== false
|
||||
const autoStartHint = autoStartError
|
||||
? t(
|
||||
"pages.config.autostart_load_error",
|
||||
"Failed to load launch-at-login status.",
|
||||
)
|
||||
? t("pages.config.autostart_load_error")
|
||||
: !autoStartSupported
|
||||
? t(
|
||||
"pages.config.autostart_unsupported",
|
||||
"Launch at login is not supported on this platform.",
|
||||
)
|
||||
: t(
|
||||
"pages.config.autostart_hint",
|
||||
"Start PicoClaw Web automatically when you log in.",
|
||||
)
|
||||
? t("pages.config.autostart_unsupported")
|
||||
: t("pages.config.autostart_hint")
|
||||
|
||||
const launcherHint = launcherError
|
||||
? t(
|
||||
"pages.config.launcher_load_error",
|
||||
"Failed to load service parameters.",
|
||||
)
|
||||
: t(
|
||||
"pages.config.launcher_restart_hint",
|
||||
"Service parameter changes apply after restarting PicoClaw Web.",
|
||||
)
|
||||
? t("pages.config.launcher_load_error")
|
||||
: t("pages.config.launcher_restart_hint")
|
||||
|
||||
const updateField = <K extends keyof CoreConfigForm>(
|
||||
key: K,
|
||||
|
|
@ -148,12 +133,7 @@ export function ConfigPage() {
|
|||
setForm(baseline)
|
||||
setLauncherForm(launcherBaseline)
|
||||
setAutoStartEnabled(autoStartBaseline)
|
||||
toast.info(
|
||||
t(
|
||||
"pages.config.reset_success",
|
||||
"Changes have been reset to the last saved state.",
|
||||
),
|
||||
)
|
||||
toast.info(t("pages.config.reset_success"))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
|
|
@ -251,12 +231,7 @@ export function ConfigPage() {
|
|||
|
||||
if (autoStartDirty) {
|
||||
if (!autoStartSupported) {
|
||||
throw new Error(
|
||||
t(
|
||||
"pages.config.autostart_unsupported",
|
||||
"Launch at login is not supported on this platform.",
|
||||
),
|
||||
)
|
||||
throw new Error(t("pages.config.autostart_unsupported"))
|
||||
}
|
||||
const status = await updateAutoStartEnabled(autoStartEnabled)
|
||||
setAutoStartEnabled(status.enabled)
|
||||
|
|
@ -264,14 +239,10 @@ export function ConfigPage() {
|
|||
queryClient.setQueryData(["system", "autostart"], status)
|
||||
}
|
||||
|
||||
toast.success(
|
||||
t("pages.config.save_success", "Configuration saved successfully."),
|
||||
)
|
||||
toast.success(t("pages.config.save_success"))
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("pages.config.save_error", "Failed to save configuration."),
|
||||
err instanceof Error ? err.message : t("pages.config.save_error"),
|
||||
)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
|
|
@ -281,12 +252,12 @@ export function ConfigPage() {
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<PageHeader
|
||||
title={t("navigation.config", "Config")}
|
||||
title={t("navigation.config")}
|
||||
children={
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/config/raw">
|
||||
<IconCode className="size-4" />
|
||||
{t("pages.config.open_raw", "Raw Config")}
|
||||
{t("pages.config.open_raw")}
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
|
|
@ -295,23 +266,17 @@ export function ConfigPage() {
|
|||
<div className="mx-auto w-full max-w-[1000px] space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground py-6 text-sm">
|
||||
{t("labels.loading", "Loading...")}
|
||||
{t("labels.loading")}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-destructive py-6 text-sm">
|
||||
{t(
|
||||
"pages.config.load_error",
|
||||
"Failed to load configuration. Please refresh and try again.",
|
||||
)}
|
||||
{t("pages.config.load_error")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{isDirty && (
|
||||
<div className="bg-yellow-50 px-3 py-2 text-sm text-yellow-700">
|
||||
{t(
|
||||
"pages.config.unsaved_changes",
|
||||
"You have unsaved changes.",
|
||||
)}
|
||||
{t("pages.config.unsaved_changes")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -356,13 +321,11 @@ export function ConfigPage() {
|
|||
onClick={handleReset}
|
||||
disabled={!isDirty || saving}
|
||||
>
|
||||
{t("common.reset", "Reset")}
|
||||
{t("common.reset")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!isDirty || saving}>
|
||||
<IconDeviceFloppy className="size-4" />
|
||||
{saving
|
||||
? t("common.saving", "Saving...")
|
||||
: t("common.save", "Save")}
|
||||
{saving ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,11 +44,8 @@ export function AgentDefaultsSection({
|
|||
<section className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
label={t("pages.config.workspace", "Workspace Directory")}
|
||||
hint={t(
|
||||
"pages.config.workspace_hint",
|
||||
"Base directory for agent file operations.",
|
||||
)}
|
||||
label={t("pages.config.workspace")}
|
||||
hint={t("pages.config.workspace_hint")}
|
||||
>
|
||||
<Input
|
||||
value={form.workspace}
|
||||
|
|
@ -58,11 +55,8 @@ export function AgentDefaultsSection({
|
|||
</Field>
|
||||
|
||||
<SwitchCardField
|
||||
label={t("pages.config.restrict_workspace", "Restrict to Workspace")}
|
||||
hint={t(
|
||||
"pages.config.restrict_workspace_hint",
|
||||
"Only allow file operations inside workspace.",
|
||||
)}
|
||||
label={t("pages.config.restrict_workspace")}
|
||||
hint={t("pages.config.restrict_workspace_hint")}
|
||||
checked={form.restrictToWorkspace}
|
||||
onCheckedChange={(checked) =>
|
||||
onFieldChange("restrictToWorkspace", checked)
|
||||
|
|
@ -70,11 +64,8 @@ export function AgentDefaultsSection({
|
|||
/>
|
||||
|
||||
<Field
|
||||
label={t("pages.config.max_tokens", "Max Tokens")}
|
||||
hint={t(
|
||||
"pages.config.max_tokens_hint",
|
||||
"Upper token limit per model response.",
|
||||
)}
|
||||
label={t("pages.config.max_tokens")}
|
||||
hint={t("pages.config.max_tokens_hint")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -85,11 +76,8 @@ export function AgentDefaultsSection({
|
|||
</Field>
|
||||
|
||||
<Field
|
||||
label={t("pages.config.max_tool_iterations", "Max Tool Iterations")}
|
||||
hint={t(
|
||||
"pages.config.max_tool_iterations_hint",
|
||||
"Maximum tool-call loops in a single task.",
|
||||
)}
|
||||
label={t("pages.config.max_tool_iterations")}
|
||||
hint={t("pages.config.max_tool_iterations_hint")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -100,14 +88,8 @@ export function AgentDefaultsSection({
|
|||
</Field>
|
||||
|
||||
<Field
|
||||
label={t(
|
||||
"pages.config.summarize_threshold",
|
||||
"Summarize Message Threshold",
|
||||
)}
|
||||
hint={t(
|
||||
"pages.config.summarize_threshold_hint",
|
||||
"Start summarization after this many messages.",
|
||||
)}
|
||||
label={t("pages.config.summarize_threshold")}
|
||||
hint={t("pages.config.summarize_threshold_hint")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -120,14 +102,8 @@ export function AgentDefaultsSection({
|
|||
</Field>
|
||||
|
||||
<Field
|
||||
label={t(
|
||||
"pages.config.summarize_token_percent",
|
||||
"Summarize Token Percent",
|
||||
)}
|
||||
hint={t(
|
||||
"pages.config.summarize_token_percent_hint",
|
||||
"Used when conversation summary is triggered.",
|
||||
)}
|
||||
label={t("pages.config.summarize_token_percent")}
|
||||
hint={t("pages.config.summarize_token_percent_hint")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -159,11 +135,8 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
|
|||
<section className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
label={t("pages.config.session_scope", "Session Scope")}
|
||||
hint={t(
|
||||
"pages.config.session_scope_hint",
|
||||
"How chat context is isolated across peers/channels.",
|
||||
)}
|
||||
label={t("pages.config.session_scope")}
|
||||
hint={t("pages.config.session_scope_hint")}
|
||||
>
|
||||
<Select
|
||||
value={form.dmScope}
|
||||
|
|
@ -183,11 +156,9 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
|
|||
{DM_SCOPE_OPTIONS.map((scope) => (
|
||||
<SelectItem key={scope.value} value={scope.value}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-medium">
|
||||
{t(scope.labelKey, scope.labelDefault)}
|
||||
</span>
|
||||
<span className="font-medium">{t(scope.labelKey)}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t(scope.descKey, scope.descDefault)}
|
||||
{t(scope.descKey)}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
|
@ -197,11 +168,8 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
|
|||
</Field>
|
||||
|
||||
<SwitchCardField
|
||||
label={t("pages.config.heartbeat_enabled", "Heartbeat")}
|
||||
hint={t(
|
||||
"pages.config.heartbeat_enabled_hint",
|
||||
"Send periodic heartbeat messages.",
|
||||
)}
|
||||
label={t("pages.config.heartbeat_enabled")}
|
||||
hint={t("pages.config.heartbeat_enabled_hint")}
|
||||
checked={form.heartbeatEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onFieldChange("heartbeatEnabled", checked)
|
||||
|
|
@ -210,14 +178,8 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
|
|||
|
||||
{form.heartbeatEnabled && (
|
||||
<Field
|
||||
label={t(
|
||||
"pages.config.heartbeat_interval",
|
||||
"Heartbeat Interval (minutes)",
|
||||
)}
|
||||
hint={t(
|
||||
"pages.config.heartbeat_interval_hint",
|
||||
"Interval in minutes between heartbeat signals.",
|
||||
)}
|
||||
label={t("pages.config.heartbeat_interval")}
|
||||
hint={t("pages.config.heartbeat_interval_hint")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -253,11 +215,8 @@ export function LauncherSection({
|
|||
<section className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
label={t("pages.config.server_port", "Service Port")}
|
||||
hint={t(
|
||||
"pages.config.server_port_hint",
|
||||
"HTTP port used by PicoClaw Web.",
|
||||
)}
|
||||
label={t("pages.config.server_port")}
|
||||
hint={t("pages.config.server_port_hint")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -270,30 +229,21 @@ export function LauncherSection({
|
|||
</Field>
|
||||
|
||||
<SwitchCardField
|
||||
label={t("pages.config.lan_access", "Enable LAN Access")}
|
||||
hint={t(
|
||||
"pages.config.lan_access_hint",
|
||||
"Allow access from other devices on your local network.",
|
||||
)}
|
||||
label={t("pages.config.lan_access")}
|
||||
hint={t("pages.config.lan_access_hint")}
|
||||
checked={launcherForm.publicAccess}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(checked) => onFieldChange("publicAccess", checked)}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={t("pages.config.allowed_cidrs", "Allowed Network CIDRs")}
|
||||
hint={t(
|
||||
"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.",
|
||||
)}
|
||||
label={t("pages.config.allowed_cidrs")}
|
||||
hint={t("pages.config.allowed_cidrs_hint")}
|
||||
>
|
||||
<Textarea
|
||||
value={launcherForm.allowedCIDRsText}
|
||||
disabled={disabled}
|
||||
placeholder={t(
|
||||
"pages.config.allowed_cidrs_placeholder",
|
||||
"192.168.1.0/24\n10.0.0.0/8",
|
||||
)}
|
||||
placeholder={t("pages.config.allowed_cidrs_placeholder")}
|
||||
className="min-h-[88px]"
|
||||
onChange={(e) => onFieldChange("allowedCIDRsText", e.target.value)}
|
||||
/>
|
||||
|
|
@ -328,11 +278,8 @@ export function DevicesSection({
|
|||
<section className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<SwitchCardField
|
||||
label={t("pages.config.devices_enabled", "Enable Devices")}
|
||||
hint={t(
|
||||
"pages.config.devices_enabled_hint",
|
||||
"Enable hardware-device integrations.",
|
||||
)}
|
||||
label={t("pages.config.devices_enabled")}
|
||||
hint={t("pages.config.devices_enabled_hint")}
|
||||
checked={form.devicesEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onFieldChange("devicesEnabled", checked)
|
||||
|
|
@ -340,17 +287,14 @@ export function DevicesSection({
|
|||
/>
|
||||
|
||||
<SwitchCardField
|
||||
label={t("pages.config.monitor_usb", "Monitor USB")}
|
||||
hint={t(
|
||||
"pages.config.monitor_usb_hint",
|
||||
"Watch USB plug/unplug events when devices are enabled.",
|
||||
)}
|
||||
label={t("pages.config.monitor_usb")}
|
||||
hint={t("pages.config.monitor_usb_hint")}
|
||||
checked={form.monitorUSB}
|
||||
onCheckedChange={(checked) => onFieldChange("monitorUSB", checked)}
|
||||
/>
|
||||
|
||||
<SwitchCardField
|
||||
label={t("pages.config.autostart_label", "Launch at Login")}
|
||||
label={t("pages.config.autostart_label")}
|
||||
hint={autoStartHint}
|
||||
checked={autoStartEnabled}
|
||||
disabled={autoStartDisabled}
|
||||
|
|
@ -367,16 +311,13 @@ export function AdvancedSection() {
|
|||
return (
|
||||
<section className="space-y-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(
|
||||
"pages.config.advanced_desc",
|
||||
"Open the raw JSON page to edit every field directly.",
|
||||
)}
|
||||
{t("pages.config.advanced_desc")}
|
||||
</p>
|
||||
<div>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/config/raw">
|
||||
<IconCode className="size-4" />
|
||||
{t("pages.config.open_raw", "Raw Config")}
|
||||
{t("pages.config.open_raw")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -52,9 +52,7 @@ export function RawJsonPanel() {
|
|||
}
|
||||
},
|
||||
onSuccess: (_, submittedConfig) => {
|
||||
toast.success(
|
||||
t("pages.config.save_success", "Configuration saved successfully."),
|
||||
)
|
||||
toast.success(t("pages.config.save_success"))
|
||||
try {
|
||||
const savedConfig = JSON.parse(submittedConfig)
|
||||
setLastSavedConfig(savedConfig)
|
||||
|
|
@ -65,7 +63,7 @@ export function RawJsonPanel() {
|
|||
}
|
||||
},
|
||||
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,
|
||||
)
|
||||
setEditorValue(formatted)
|
||||
toast.success(
|
||||
t("pages.config.format_success", "JSON formatted successfully."),
|
||||
)
|
||||
toast.success(t("pages.config.format_success"))
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t(
|
||||
|
|
@ -123,38 +119,26 @@ export function RawJsonPanel() {
|
|||
setEditorValue(JSON.stringify(config, null, 2))
|
||||
}
|
||||
setIsDirty(false)
|
||||
toast.info(
|
||||
t(
|
||||
"pages.config.reset_success",
|
||||
"Changes have been reset to the last saved state.",
|
||||
),
|
||||
)
|
||||
toast.info(t("pages.config.reset_success"))
|
||||
setShowResetDialog(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{t("pages.config.raw_json_title", "Raw JSON Configuration")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"pages.config.raw_json_desc",
|
||||
"Advanced users can directly edit the raw JSON configuration below.",
|
||||
)}
|
||||
</CardDescription>
|
||||
<CardTitle>{t("pages.config.raw_json_title")}</CardTitle>
|
||||
<CardDescription>{t("pages.config.raw_json_desc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p>{t("labels.loading", "Loading...")}</p>
|
||||
<p>{t("labels.loading")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{isDirty && (
|
||||
<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 className="bg-muted/30 relative rounded-lg border">
|
||||
|
|
@ -166,10 +150,7 @@ export function RawJsonPanel() {
|
|||
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"
|
||||
placeholder={t(
|
||||
"pages.config.json_placeholder",
|
||||
"Enter valid JSON configuration...",
|
||||
)}
|
||||
placeholder={t("pages.config.json_placeholder")}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
|
@ -179,7 +160,7 @@ export function RawJsonPanel() {
|
|||
onClick={handleFormat}
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
{t("pages.config.format", "Format")}
|
||||
{t("pages.config.format")}
|
||||
</Button>
|
||||
<AlertDialog
|
||||
open={showResetDialog}
|
||||
|
|
@ -191,35 +172,28 @@ export function RawJsonPanel() {
|
|||
disabled={!isDirty}
|
||||
onClick={() => setShowResetDialog(true)}
|
||||
>
|
||||
{t("common.reset", "Reset")}
|
||||
{t("common.reset")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("pages.config.reset_confirm_title", "Reset Changes")}
|
||||
{t("pages.config.reset_confirm_title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
"pages.config.reset_confirm_desc",
|
||||
"Are you sure you want to reset your unsaved changes back to the last saved state?",
|
||||
)}
|
||||
{t("pages.config.reset_confirm_desc")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("common.cancel", "Cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmReset}>
|
||||
{t("common.confirm", "Confirm")}
|
||||
{t("common.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button onClick={handleSave} disabled={mutation.isPending}>
|
||||
{mutation.isPending
|
||||
? t("common.saving", "Saving...")
|
||||
: t("common.save", "Save")}
|
||||
{mutation.isPending ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function AnthropicCredentialCard({
|
|||
const { t } = useTranslation()
|
||||
const actionBusy = activeAction !== ""
|
||||
const tokenLoading = activeAction === "anthropic:token"
|
||||
const stopLabel = t("credentials.actions.stopLoading", "Stop Loading")
|
||||
const stopLabel = t("credentials.actions.stopLoading")
|
||||
|
||||
return (
|
||||
<CredentialCard
|
||||
|
|
@ -46,10 +46,7 @@ export function AnthropicCredentialCard({
|
|||
<span>Anthropic</span>
|
||||
</span>
|
||||
}
|
||||
description={t(
|
||||
"credentials.providers.anthropic.description",
|
||||
"Uses token login for Claude access.",
|
||||
)}
|
||||
description={t("credentials.providers.anthropic.description")}
|
||||
status={status?.status ?? "not_logged_in"}
|
||||
authMethod={status?.auth_method}
|
||||
actions={
|
||||
|
|
@ -60,10 +57,7 @@ export function AnthropicCredentialCard({
|
|||
value={token}
|
||||
onChange={(e) => onTokenChange(e.target.value)}
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"credentials.fields.anthropicToken",
|
||||
"Anthropic token",
|
||||
)}
|
||||
placeholder={t("credentials.fields.anthropicToken")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -75,7 +69,7 @@ export function AnthropicCredentialCard({
|
|||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
<IconKey className="size-4" />
|
||||
{t("credentials.actions.saveToken", "Save")}
|
||||
{t("credentials.actions.saveToken")}
|
||||
</Button>
|
||||
{tokenLoading && (
|
||||
<Button
|
||||
|
|
@ -105,7 +99,7 @@ export function AnthropicCredentialCard({
|
|||
{activeAction === "anthropic:logout" && (
|
||||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
{t("credentials.actions.logout", "Logout")}
|
||||
{t("credentials.actions.logout")}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,22 +40,19 @@ export function AntigravityCredentialCard({
|
|||
<span>Google Antigravity</span>
|
||||
</span>
|
||||
}
|
||||
description={t(
|
||||
"credentials.providers.antigravity.description",
|
||||
"Uses browser OAuth for Google Cloud Code Assist.",
|
||||
)}
|
||||
description={t("credentials.providers.antigravity.description")}
|
||||
status={status?.status ?? "not_logged_in"}
|
||||
authMethod={status?.auth_method}
|
||||
details={
|
||||
<div className="space-y-1">
|
||||
{status?.email && (
|
||||
<p>
|
||||
{t("credentials.labels.email", "Email")}: {status.email}
|
||||
{t("credentials.labels.email")}: {status.email}
|
||||
</p>
|
||||
)}
|
||||
{status?.project_id && (
|
||||
<p>
|
||||
{t("credentials.labels.project", "Project")}: {status.project_id}
|
||||
{t("credentials.labels.project")}: {status.project_id}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -73,7 +70,7 @@ export function AntigravityCredentialCard({
|
|||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
<IconLockOpen className="size-4" />
|
||||
{t("credentials.actions.browser", "Browser OAuth")}
|
||||
{t("credentials.actions.browser")}
|
||||
</Button>
|
||||
{browserLoading && (
|
||||
<Button
|
||||
|
|
@ -100,7 +97,7 @@ export function AntigravityCredentialCard({
|
|||
{activeAction === "google-antigravity:logout" && (
|
||||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
{t("credentials.actions.logout", "Logout")}
|
||||
{t("credentials.actions.logout")}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,15 +42,12 @@ export function CredentialsPage() {
|
|||
|
||||
return (
|
||||
<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="pt-2">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(
|
||||
"credentials.description",
|
||||
"Manage OAuth and token-based credentials for supported providers.",
|
||||
)}
|
||||
{t("credentials.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -62,9 +59,7 @@ export function CredentialsPage() {
|
|||
|
||||
{activeFlow && (
|
||||
<div className="bg-muted mt-4 rounded-lg border px-4 py-3 text-sm">
|
||||
<p className="font-medium">
|
||||
{t("credentials.flow.current", "Current authentication status")}
|
||||
</p>
|
||||
<p className="font-medium">{t("credentials.flow.current")}</p>
|
||||
<p className="text-muted-foreground mt-1">{flowHint}</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -72,7 +67,7 @@ export function CredentialsPage() {
|
|||
{loading ? (
|
||||
<div className="text-muted-foreground flex items-center gap-2 py-10 text-sm">
|
||||
<IconLoader2 className="size-4 animate-spin" />
|
||||
{t("credentials.loading", "Loading credentials...")}
|
||||
{t("credentials.loading")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 py-5 lg:auto-rows-fr lg:grid-cols-3">
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
>
|
||||
<SheetHeader className="border-b-muted border-b px-6 py-5">
|
||||
<SheetTitle>
|
||||
{t("credentials.device.title", "OpenAI Device Login")}
|
||||
</SheetTitle>
|
||||
<SheetTitle>{t("credentials.device.title")}</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t(
|
||||
"credentials.device.description",
|
||||
"Open the verification page and enter the code below. This page will refresh automatically.",
|
||||
)}
|
||||
{t("credentials.device.description")}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-4 px-6 py-5">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs uppercase">
|
||||
{t("credentials.device.code", "User Code")}
|
||||
{t("credentials.device.code")}
|
||||
</p>
|
||||
<p className="mt-1 rounded-md border px-3 py-2 font-mono text-lg font-semibold tracking-wide">
|
||||
{flow?.user_code || "-"}
|
||||
|
|
@ -57,7 +52,7 @@ export function DeviceCodeSheet({
|
|||
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs uppercase">
|
||||
{t("credentials.device.url", "Verification URL")}
|
||||
{t("credentials.device.url")}
|
||||
</p>
|
||||
<a
|
||||
href={flow?.verify_url || "#"}
|
||||
|
|
@ -71,7 +66,7 @@ export function DeviceCodeSheet({
|
|||
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<IconRefresh className="size-4" />
|
||||
{t("credentials.device.polling", "Polling login status...")}
|
||||
{t("credentials.device.polling")}
|
||||
</div>
|
||||
|
||||
{flow && (
|
||||
|
|
@ -83,11 +78,11 @@ export function DeviceCodeSheet({
|
|||
|
||||
<SheetFooter className="border-t-muted border-t px-6 py-4">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
{t("common.cancel", "Close")}
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button asChild disabled={!flow?.verify_url}>
|
||||
<a href={flow?.verify_url || "#"} target="_blank" rel="noreferrer">
|
||||
{t("credentials.device.open", "Open Verification Page")}
|
||||
{t("credentials.device.open")}
|
||||
</a>
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function LogoutConfirmDialog({
|
|||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("credentials.logoutDialog.title", "Logout provider?")}
|
||||
{t("credentials.logoutDialog.title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
|
|
@ -45,10 +45,10 @@ export function LogoutConfirmDialog({
|
|||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("common.cancel", "Cancel")}</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm} variant="destructive">
|
||||
{isSubmitting && <IconLoader2 className="size-4 animate-spin" />}
|
||||
{t("credentials.actions.logout", "Logout")}
|
||||
{t("credentials.actions.logout")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
|
|||
|
|
@ -53,16 +53,13 @@ export function OpenAICredentialCard({
|
|||
<span>OpenAI</span>
|
||||
</span>
|
||||
}
|
||||
description={t(
|
||||
"credentials.providers.openai.description",
|
||||
"Supports browser OAuth, device code, and token login.",
|
||||
)}
|
||||
description={t("credentials.providers.openai.description")}
|
||||
status={status?.status ?? "not_logged_in"}
|
||||
authMethod={status?.auth_method}
|
||||
details={
|
||||
status?.account_id ? (
|
||||
<p>
|
||||
{t("credentials.labels.account", "Account")}: {status.account_id}
|
||||
{t("credentials.labels.account")}: {status.account_id}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
|
|
@ -81,7 +78,7 @@ export function OpenAICredentialCard({
|
|||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
<IconBrandOpenai className="size-4" />
|
||||
{t("credentials.actions.browser", "Browser OAuth")}
|
||||
{t("credentials.actions.browser")}
|
||||
</Button>
|
||||
|
||||
{oauthLoading && !deviceLoading && (
|
||||
|
|
@ -105,7 +102,7 @@ export function OpenAICredentialCard({
|
|||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
<IconClockHour4 className="size-4" />
|
||||
{t("credentials.actions.deviceCode", "Device Code")}
|
||||
{t("credentials.actions.deviceCode")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -116,10 +113,7 @@ export function OpenAICredentialCard({
|
|||
value={token}
|
||||
onChange={(e) => onTokenChange(e.target.value)}
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"credentials.fields.openaiToken",
|
||||
"OpenAI token",
|
||||
)}
|
||||
placeholder={t("credentials.fields.openaiToken")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -130,7 +124,7 @@ export function OpenAICredentialCard({
|
|||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
<IconKey className="size-4" />
|
||||
{t("credentials.actions.saveToken", "Save")}
|
||||
{t("credentials.actions.saveToken")}
|
||||
</Button>
|
||||
{tokenLoading && (
|
||||
<Button
|
||||
|
|
@ -159,7 +153,7 @@ export function OpenAICredentialCard({
|
|||
{activeAction === "openai:logout" && (
|
||||
<IconLoader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
{t("credentials.actions.logout", "Logout")}
|
||||
{t("credentials.actions.logout")}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ export function ProviderStatusLine({
|
|||
<div className="flex items-center justify-between gap-2">
|
||||
<span className={`rounded px-2 py-1 text-xs font-medium ${style}`}>
|
||||
{status === "connected"
|
||||
? t("credentials.status.connected", "Connected")
|
||||
? t("credentials.status.connected")
|
||||
: status === "needs_refresh"
|
||||
? t("credentials.status.needsRefresh", "Needs refresh")
|
||||
? t("credentials.status.needsRefresh")
|
||||
: status === "expired"
|
||||
? t("credentials.status.expired", "Expired")
|
||||
: t("credentials.status.notLoggedIn", "Not logged in")}
|
||||
? t("credentials.status.expired")
|
||||
: t("credentials.status.notLoggedIn")}
|
||||
</span>
|
||||
{authMethod && (
|
||||
<span className="text-muted-foreground text-xs uppercase">
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export function ModelsPage() {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<Button size="sm" variant="outline" onClick={() => setAddOpen(true)}>
|
||||
<IconPlus className="size-4" />
|
||||
|
|
|
|||
|
|
@ -53,9 +53,7 @@ export function useCredentialsPage() {
|
|||
setError("")
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("credentials.errors.loadFailed", "Failed to load credentials"),
|
||||
err instanceof Error ? err.message : t("credentials.errors.loadFailed"),
|
||||
)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
|
@ -113,10 +111,7 @@ export function useCredentialsPage() {
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t(
|
||||
"credentials.errors.flowFailed",
|
||||
"Failed to check authentication flow",
|
||||
),
|
||||
: t("credentials.errors.flowFailed"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -196,12 +191,7 @@ export function useCredentialsPage() {
|
|||
return
|
||||
}
|
||||
setActiveAction("")
|
||||
setError(
|
||||
t(
|
||||
"credentials.errors.popupBlocked",
|
||||
"Unable to open a new tab. Please allow popups and try again.",
|
||||
),
|
||||
)
|
||||
setError(t("credentials.errors.popupBlocked"))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -212,12 +202,7 @@ export function useCredentialsPage() {
|
|||
return
|
||||
}
|
||||
if (!resp.auth_url || !resp.flow_id) {
|
||||
throw new Error(
|
||||
t(
|
||||
"credentials.errors.invalidBrowserResponse",
|
||||
"Invalid browser login response",
|
||||
),
|
||||
)
|
||||
throw new Error(t("credentials.errors.invalidBrowserResponse"))
|
||||
}
|
||||
|
||||
authTab.location.href = resp.auth_url
|
||||
|
|
@ -242,7 +227,7 @@ export function useCredentialsPage() {
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("credentials.errors.loginFailed", "Login failed"),
|
||||
: t("credentials.errors.loginFailed"),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -263,12 +248,7 @@ export function useCredentialsPage() {
|
|||
return
|
||||
}
|
||||
if (!resp.flow_id || !resp.user_code || !resp.verify_url) {
|
||||
throw new Error(
|
||||
t(
|
||||
"credentials.errors.invalidDeviceResponse",
|
||||
"Invalid device code response",
|
||||
),
|
||||
)
|
||||
throw new Error(t("credentials.errors.invalidDeviceResponse"))
|
||||
}
|
||||
|
||||
const flow: OAuthFlowState = {
|
||||
|
|
@ -296,7 +276,7 @@ export function useCredentialsPage() {
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("credentials.errors.loginFailed", "Login failed"),
|
||||
: t("credentials.errors.loginFailed"),
|
||||
)
|
||||
}
|
||||
}, [bumpActionToken, isActionTokenCurrent, t])
|
||||
|
|
@ -320,7 +300,7 @@ export function useCredentialsPage() {
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("credentials.errors.loginFailed", "Login failed"),
|
||||
: t("credentials.errors.loginFailed"),
|
||||
)
|
||||
} finally {
|
||||
setActiveAction("")
|
||||
|
|
@ -342,7 +322,7 @@ export function useCredentialsPage() {
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("credentials.errors.logoutFailed", "Logout failed"),
|
||||
: t("credentials.errors.logoutFailed"),
|
||||
)
|
||||
} finally {
|
||||
setActiveAction("")
|
||||
|
|
@ -415,17 +395,15 @@ export function useCredentialsPage() {
|
|||
return ""
|
||||
}
|
||||
if (activeFlow.status === "pending") {
|
||||
return t("credentials.flow.pending", "Waiting for authorization...")
|
||||
return t("credentials.flow.pending")
|
||||
}
|
||||
if (activeFlow.status === "success") {
|
||||
return t("credentials.flow.success", "Authentication successful")
|
||||
return t("credentials.flow.success")
|
||||
}
|
||||
if (activeFlow.status === "expired") {
|
||||
return t("credentials.flow.expired", "Authentication session expired")
|
||||
return t("credentials.flow.expired")
|
||||
}
|
||||
return (
|
||||
activeFlow.error || t("credentials.flow.error", "Authentication failed")
|
||||
)
|
||||
return activeFlow.error || t("credentials.flow.error")
|
||||
}, [activeFlow, t])
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
"credentials": "Credentials",
|
||||
"services": "Services",
|
||||
"channels_group": "Channels",
|
||||
"channels": "Channels",
|
||||
"show_more_channels": "More",
|
||||
"show_less_channels": "Less",
|
||||
"config": "Config",
|
||||
|
|
@ -15,13 +14,8 @@
|
|||
"chat": {
|
||||
"welcome": "How can I help you today?",
|
||||
"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...",
|
||||
"attach": "Attach file",
|
||||
"voice": "Voice input",
|
||||
"newChat": "New Chat",
|
||||
"connecting": "Connecting...",
|
||||
"notConnected": "Gateway is not running. Start it to chat.",
|
||||
"thinking": {
|
||||
"step1": "Thinking...",
|
||||
|
|
@ -29,12 +23,6 @@
|
|||
"step3": "Preparing response...",
|
||||
"step4": "Almost there..."
|
||||
},
|
||||
"time": {
|
||||
"justNow": "just now",
|
||||
"minsAgo": "{{count}}m ago",
|
||||
"hoursAgo": "{{count}}h ago",
|
||||
"daysAgo": "{{count}}d ago"
|
||||
},
|
||||
"history": "History",
|
||||
"noHistory": "No chat history yet",
|
||||
"loadingMore": "Loading more...",
|
||||
|
|
@ -79,6 +67,9 @@
|
|||
"reset": "Reset",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"labels": {
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Manage OAuth and token-based credentials for supported providers.",
|
||||
"loading": "Loading credentials...",
|
||||
|
|
@ -102,6 +93,7 @@
|
|||
"actions": {
|
||||
"browser": "Browser OAuth",
|
||||
"deviceCode": "Device Code",
|
||||
"stopLoading": "Stop Loading",
|
||||
"saveToken": "Save",
|
||||
"logout": "Logout"
|
||||
},
|
||||
|
|
@ -146,9 +138,6 @@
|
|||
"models": {
|
||||
"description": "Configure API keys for AI providers. Only configured models are available for chat.",
|
||||
"loadError": "Failed to load models",
|
||||
"header": {
|
||||
"configured": "configured"
|
||||
},
|
||||
"noDefaultHintPrefix": "No default model set yet. Click",
|
||||
"noDefaultHintSuffix": "to set one.",
|
||||
"status": {
|
||||
|
|
@ -185,8 +174,7 @@
|
|||
"delete": {
|
||||
"title": "Delete Model?",
|
||||
"description": "\"{{name}}\" will be permanently removed from your model list. This cannot be undone.",
|
||||
"confirm": "Delete",
|
||||
"errorDefault": "Cannot delete the default model. Please set another model as default first."
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"advanced": {
|
||||
"toggle": "Advanced options"
|
||||
|
|
@ -221,19 +209,10 @@
|
|||
}
|
||||
},
|
||||
"channels": {
|
||||
"description": "Configure messaging channels to connect your AI agent to chat platforms.",
|
||||
"loadError": "Failed to load channels",
|
||||
"search": "Search channels...",
|
||||
"noResults": "No channels match your search.",
|
||||
"header": {
|
||||
"enabled": "enabled"
|
||||
},
|
||||
"edit": "Configure {{name}}",
|
||||
"status": {
|
||||
"configured": "Configured",
|
||||
"unconfigured": "Not configured"
|
||||
},
|
||||
"action": {
|
||||
"configure": "Configure"
|
||||
"configured": "Configured"
|
||||
},
|
||||
"name": {
|
||||
"telegram": "Telegram",
|
||||
|
|
@ -265,7 +244,6 @@
|
|||
"encryptKey": "Encrypt Key",
|
||||
"baseUrl": "API Base URL",
|
||||
"proxy": "HTTP Proxy",
|
||||
"proxyHint": "Optional. e.g. http://127.0.0.1:7890",
|
||||
"mentionOnly": "Mention Only",
|
||||
"typingEnabled": "Typing Indicator",
|
||||
"placeholderEnabled": "Placeholder Message",
|
||||
|
|
@ -273,32 +251,17 @@
|
|||
"groupTriggerMentionOnly": "Group Mention Only",
|
||||
"groupTriggerPrefixes": "Group Trigger Prefixes",
|
||||
"allowFrom": "Allow From",
|
||||
"allowFromHint": "Comma-separated list of allowed user/group IDs. Leave empty to allow all.",
|
||||
"allowFromPlaceholder": "e.g. 123456, 789012",
|
||||
"allowOrigins": "Allow Origins",
|
||||
"allowOriginsHint": "Comma-separated list of allowed origins. Leave empty to allow all.",
|
||||
"allowOriginsPlaceholder": "e.g. https://example.com, http://localhost:5173",
|
||||
"secretPlaceholder": "Enter secret",
|
||||
"secretPlaceholderSet": "Leave blank to keep existing",
|
||||
"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": {
|
||||
"notFound": "Channel \"{{name}}\" is not supported.",
|
||||
"saveSuccess": "Channel configuration saved.",
|
||||
"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",
|
||||
"disabled": "disabled",
|
||||
"docLink": "Documentation",
|
||||
"enableLabel": "Enable channel"
|
||||
},
|
||||
|
|
@ -316,7 +279,6 @@
|
|||
"mentionOnly": "Only respond when the bot is explicitly mentioned in group chats.",
|
||||
"typingEnabled": "Display typing status while the assistant is generating a response.",
|
||||
"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.",
|
||||
"groupTriggerPrefixes": "Custom group-chat trigger prefixes, separated by commas.",
|
||||
"allowFrom": "Allowed user or group IDs, separated by commas.",
|
||||
|
|
@ -362,23 +324,8 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"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.",
|
||||
"section_agents": "Agent Defaults",
|
||||
"section_runtime": "Runtime",
|
||||
"section_devices": "Devices",
|
||||
"workspace": "Workspace Directory",
|
||||
"workspace_hint": "Base directory for agent file operations.",
|
||||
"restrict_workspace": "Restrict to Workspace",
|
||||
|
|
@ -422,10 +369,6 @@
|
|||
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
||||
"launcher_load_error": "Failed to load service parameters.",
|
||||
"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.",
|
||||
"open_raw": "Raw Config",
|
||||
"back_to_visual": "Visual Config",
|
||||
|
|
@ -441,7 +384,6 @@
|
|||
"format_success": "JSON formatted successfully.",
|
||||
"format_error": "Invalid JSON 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."
|
||||
},
|
||||
"logs": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
"credentials": "凭据",
|
||||
"services": "服务",
|
||||
"channels_group": "频道",
|
||||
"channels": "频道",
|
||||
"show_more_channels": "更多",
|
||||
"show_less_channels": "收起",
|
||||
"config": "配置",
|
||||
|
|
@ -15,13 +14,8 @@
|
|||
"chat": {
|
||||
"welcome": "今天我能为您做些什么?",
|
||||
"welcomeDesc": "您可以询问我天气、设置或其他任何任务,我随时为您效劳。",
|
||||
"model": "模型",
|
||||
"user": "用户",
|
||||
"placeholder": "输入新消息...",
|
||||
"attach": "附加文件",
|
||||
"voice": "语音输入",
|
||||
"newChat": "新建对话",
|
||||
"connecting": "连接中...",
|
||||
"notConnected": "服务未运行,请先启动以进行对话。",
|
||||
"thinking": {
|
||||
"step1": "思考中...",
|
||||
|
|
@ -29,12 +23,6 @@
|
|||
"step3": "准备回复...",
|
||||
"step4": "马上就好..."
|
||||
},
|
||||
"time": {
|
||||
"justNow": "刚刚",
|
||||
"minsAgo": "{{count}}分钟前",
|
||||
"hoursAgo": "{{count}}小时前",
|
||||
"daysAgo": "{{count}}天前"
|
||||
},
|
||||
"history": "历史记录",
|
||||
"noHistory": "暂无对话历史",
|
||||
"loadingMore": "加载更多...",
|
||||
|
|
@ -79,6 +67,9 @@
|
|||
"reset": "重置",
|
||||
"confirm": "确认"
|
||||
},
|
||||
"labels": {
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"credentials": {
|
||||
"description": "管理已支持服务商的 OAuth 与 Token 凭据。",
|
||||
"loading": "正在加载凭据...",
|
||||
|
|
@ -102,6 +93,7 @@
|
|||
"actions": {
|
||||
"browser": "浏览器 OAuth",
|
||||
"deviceCode": "设备码",
|
||||
"stopLoading": "停止加载",
|
||||
"saveToken": "保存",
|
||||
"logout": "退出登录"
|
||||
},
|
||||
|
|
@ -146,9 +138,6 @@
|
|||
"models": {
|
||||
"description": "为 AI 服务商配置 API Key。只有已配置的模型可用于对话。",
|
||||
"loadError": "加载模型列表失败",
|
||||
"header": {
|
||||
"configured": "已配置"
|
||||
},
|
||||
"noDefaultHintPrefix": "尚未设置默认模型,点击",
|
||||
"noDefaultHintSuffix": "设为默认。",
|
||||
"status": {
|
||||
|
|
@ -185,8 +174,7 @@
|
|||
"delete": {
|
||||
"title": "确认删除模型?",
|
||||
"description": "「{{name}}」将从模型列表中永久移除,此操作不可撤销。",
|
||||
"confirm": "删除",
|
||||
"errorDefault": "无法删除默认模型。请先将其他模型设为默认。"
|
||||
"confirm": "删除"
|
||||
},
|
||||
"advanced": {
|
||||
"toggle": "高级选项"
|
||||
|
|
@ -221,19 +209,10 @@
|
|||
}
|
||||
},
|
||||
"channels": {
|
||||
"description": "配置消息频道,将 AI 助手连接到各聊天平台。",
|
||||
"loadError": "加载频道列表失败",
|
||||
"search": "搜索频道...",
|
||||
"noResults": "没有匹配的频道。",
|
||||
"header": {
|
||||
"enabled": "已启用"
|
||||
},
|
||||
"edit": "配置 {{name}}",
|
||||
"status": {
|
||||
"configured": "已配置",
|
||||
"unconfigured": "未配置"
|
||||
},
|
||||
"action": {
|
||||
"configure": "配置"
|
||||
"configured": "已配置"
|
||||
},
|
||||
"name": {
|
||||
"telegram": "Telegram",
|
||||
|
|
@ -265,7 +244,6 @@
|
|||
"encryptKey": "Encrypt Key",
|
||||
"baseUrl": "API Base URL",
|
||||
"proxy": "HTTP 代理",
|
||||
"proxyHint": "可选。例如 http://127.0.0.1:7890",
|
||||
"mentionOnly": "仅提及时响应",
|
||||
"typingEnabled": "输入中提示",
|
||||
"placeholderEnabled": "占位消息",
|
||||
|
|
@ -273,32 +251,17 @@
|
|||
"groupTriggerMentionOnly": "群聊仅提及时响应",
|
||||
"groupTriggerPrefixes": "群聊触发前缀",
|
||||
"allowFrom": "允许来源",
|
||||
"allowFromHint": "用逗号分隔的用户/群组 ID 列表,留空表示允许所有。",
|
||||
"allowFromPlaceholder": "例如 123456, 789012",
|
||||
"allowOrigins": "允许来源域名",
|
||||
"allowOriginsHint": "用逗号分隔允许的 Origin,留空表示允许所有。",
|
||||
"allowOriginsPlaceholder": "例如 https://example.com, http://localhost:5173",
|
||||
"secretPlaceholder": "输入密钥",
|
||||
"secretPlaceholderSet": "留空保持原有值不变",
|
||||
"secretHintSet": "已设置密钥,留空表示不修改。"
|
||||
},
|
||||
"edit": {
|
||||
"title": "配置 {{name}}",
|
||||
"description": "设置此频道的凭据和选项。",
|
||||
"saveError": "保存频道配置失败",
|
||||
"saving": "保存中..."
|
||||
},
|
||||
"page": {
|
||||
"notFound": "不支持频道“{{name}}”。",
|
||||
"saveSuccess": "频道配置已保存。",
|
||||
"saveError": "保存频道配置失败",
|
||||
"flowTitle": "配置流程",
|
||||
"flowDescription": "按以下步骤完成频道接入并投入使用。",
|
||||
"step1": "填写凭据",
|
||||
"step2": "启用频道",
|
||||
"step3": "保存并重启网关",
|
||||
"enabled": "已启用",
|
||||
"disabled": "未启用",
|
||||
"docLink": "配置文档",
|
||||
"enableLabel": "启用频道"
|
||||
},
|
||||
|
|
@ -316,7 +279,6 @@
|
|||
"mentionOnly": "在群聊中仅当明确提及时才响应。",
|
||||
"typingEnabled": "在生成回复时显示“正在输入”状态。",
|
||||
"placeholderEnabled": "在最终回复发送前,先发送临时占位消息。",
|
||||
"placeholderText": "等待最终回复期间显示的占位文案。",
|
||||
"groupTriggerMentionOnly": "在群聊中仅当提及机器人时才响应。",
|
||||
"groupTriggerPrefixes": "群聊触发前缀,多个值用逗号分隔。",
|
||||
"allowFrom": "允许访问的用户或群组 ID,多个值用逗号分隔。",
|
||||
|
|
@ -362,23 +324,8 @@
|
|||
}
|
||||
},
|
||||
"pages": {
|
||||
"providers": {
|
||||
"description": "管理各个 AI 模型服务商的接入配置。"
|
||||
},
|
||||
"models": {
|
||||
"description": "在此管理您下载或借用的 AI 模型。"
|
||||
},
|
||||
"credentials": {
|
||||
"description": "安全管理您的 API 密钥与访问凭据。"
|
||||
},
|
||||
"config": {
|
||||
"description": "系统配置和偏好设置。",
|
||||
"visual_title": "核心配置",
|
||||
"visual_desc": "这里可编辑关键运行配置;若需完整字段请使用原始配置页。",
|
||||
"load_error": "加载配置失败,请刷新后重试。",
|
||||
"section_agents": "智能体默认设置",
|
||||
"section_runtime": "运行时",
|
||||
"section_devices": "设备功能",
|
||||
"workspace": "工作目录",
|
||||
"workspace_hint": "智能体执行文件读写操作时使用的基础目录。",
|
||||
"restrict_workspace": "限制工作目录访问",
|
||||
|
|
@ -422,10 +369,6 @@
|
|||
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
||||
"launcher_load_error": "加载服务参数失败。",
|
||||
"launcher_restart_hint": "服务参数变更需重启 PicoClaw Web 后生效。",
|
||||
"autostart_enable_success": "已开启开机自启。",
|
||||
"autostart_disable_success": "已关闭开机自启。",
|
||||
"autostart_update_error": "更新开机自启设置失败。",
|
||||
"advanced_title": "需要完整配置能力?",
|
||||
"advanced_desc": "可打开原始 JSON 页面直接编辑全部字段。",
|
||||
"open_raw": "原始配置",
|
||||
"back_to_visual": "可视化配置",
|
||||
|
|
@ -441,7 +384,6 @@
|
|||
"format_success": "JSON 格式化成功。",
|
||||
"format_error": "JSON 格式无效。",
|
||||
"format": "格式化",
|
||||
"lose_unsaved_changes": "您有未保存的更改。确定要重置并丢失这些更改吗?",
|
||||
"unsaved_changes": "您有未保存的更改。"
|
||||
},
|
||||
"logs": {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ProvidersRouteImport } from './routes/providers'
|
||||
import { Route as ModelsRouteImport } from './routes/models'
|
||||
import { Route as LogsRouteImport } from './routes/logs'
|
||||
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 ChannelsNameRouteImport } from './routes/channels/$name'
|
||||
|
||||
const ProvidersRoute = ProvidersRouteImport.update({
|
||||
id: '/providers',
|
||||
path: '/providers',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ModelsRoute = ModelsRouteImport.update({
|
||||
id: '/models',
|
||||
path: '/models',
|
||||
|
|
@ -72,7 +66,6 @@ export interface FileRoutesByFullPath {
|
|||
'/credentials': typeof CredentialsRoute
|
||||
'/logs': typeof LogsRoute
|
||||
'/models': typeof ModelsRoute
|
||||
'/providers': typeof ProvidersRoute
|
||||
'/channels/$name': typeof ChannelsNameRoute
|
||||
'/config/raw': typeof ConfigRawRoute
|
||||
}
|
||||
|
|
@ -83,7 +76,6 @@ export interface FileRoutesByTo {
|
|||
'/credentials': typeof CredentialsRoute
|
||||
'/logs': typeof LogsRoute
|
||||
'/models': typeof ModelsRoute
|
||||
'/providers': typeof ProvidersRoute
|
||||
'/channels/$name': typeof ChannelsNameRoute
|
||||
'/config/raw': typeof ConfigRawRoute
|
||||
}
|
||||
|
|
@ -95,7 +87,6 @@ export interface FileRoutesById {
|
|||
'/credentials': typeof CredentialsRoute
|
||||
'/logs': typeof LogsRoute
|
||||
'/models': typeof ModelsRoute
|
||||
'/providers': typeof ProvidersRoute
|
||||
'/channels/$name': typeof ChannelsNameRoute
|
||||
'/config/raw': typeof ConfigRawRoute
|
||||
}
|
||||
|
|
@ -108,7 +99,6 @@ export interface FileRouteTypes {
|
|||
| '/credentials'
|
||||
| '/logs'
|
||||
| '/models'
|
||||
| '/providers'
|
||||
| '/channels/$name'
|
||||
| '/config/raw'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
|
|
@ -119,7 +109,6 @@ export interface FileRouteTypes {
|
|||
| '/credentials'
|
||||
| '/logs'
|
||||
| '/models'
|
||||
| '/providers'
|
||||
| '/channels/$name'
|
||||
| '/config/raw'
|
||||
id:
|
||||
|
|
@ -130,7 +119,6 @@ export interface FileRouteTypes {
|
|||
| '/credentials'
|
||||
| '/logs'
|
||||
| '/models'
|
||||
| '/providers'
|
||||
| '/channels/$name'
|
||||
| '/config/raw'
|
||||
fileRoutesById: FileRoutesById
|
||||
|
|
@ -142,18 +130,10 @@ export interface RootRouteChildren {
|
|||
CredentialsRoute: typeof CredentialsRoute
|
||||
LogsRoute: typeof LogsRoute
|
||||
ModelsRoute: typeof ModelsRoute
|
||||
ProvidersRoute: typeof ProvidersRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/providers': {
|
||||
id: '/providers'
|
||||
path: '/providers'
|
||||
fullPath: '/providers'
|
||||
preLoaderRoute: typeof ProvidersRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/models': {
|
||||
id: '/models'
|
||||
path: '/models'
|
||||
|
|
@ -243,7 +223,6 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
CredentialsRoute: CredentialsRoute,
|
||||
LogsRoute: LogsRoute,
|
||||
ModelsRoute: ModelsRoute,
|
||||
ProvidersRoute: ProvidersRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
|
|
|||
|
|
@ -15,13 +15,11 @@ function RawConfigPage() {
|
|||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<PageHeader
|
||||
title={t("pages.config.raw_json_title", "Raw JSON Configuration")}
|
||||
>
|
||||
<PageHeader title={t("pages.config.raw_json_title")}>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/config">
|
||||
<IconAdjustments className="size-4" />
|
||||
{t("pages.config.back_to_visual", "Visual Config")}
|
||||
{t("pages.config.back_to_visual")}
|
||||
</Link>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
|
|
|||
|
|
@ -87,15 +87,15 @@ function LogsPage() {
|
|||
|
||||
return (
|
||||
<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="mb-4">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{t("navigation.logs", "Logs")}
|
||||
{t("navigation.logs")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{t("pages.logs.description", "System logs and monitoring.")}
|
||||
{t("pages.logs.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue