diff --git a/web/frontend/src/components/chat/chat-page.tsx b/web/frontend/src/components/chat/chat-page.tsx
index ebcde8981..ae705ff1b 100644
--- a/web/frontend/src/components/chat/chat-page.tsx
+++ b/web/frontend/src/components/chat/chat-page.tsx
@@ -39,7 +39,7 @@ export function ChatPage() {
const {
defaultModelName,
- hasConfiguredModels,
+ hasAvailableModels,
apiKeyModels,
oauthModels,
localModels,
@@ -94,7 +94,7 @@ export function ChatPage() {
hasScrolled ? "shadow-sm" : "shadow-none"
}`}
titleExtra={
- hasConfiguredModels && (
+ hasAvailableModels && (
{messages.length === 0 && !isTyping && (
diff --git a/web/frontend/src/components/logs/log-level-select.tsx b/web/frontend/src/components/logs/log-level-select.tsx
new file mode 100644
index 000000000..a8a273b32
--- /dev/null
+++ b/web/frontend/src/components/logs/log-level-select.tsx
@@ -0,0 +1,102 @@
+import { useQuery, useQueryClient } from "@tanstack/react-query"
+import { useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import { type AppConfig, getAppConfig, patchAppConfig } from "@/api/channels"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { refreshGatewayState } from "@/store/gateway"
+
+const LOG_LEVEL_OPTIONS = ["debug", "info", "warn", "error", "fatal"] as const
+type GatewayLogLevel = (typeof LOG_LEVEL_OPTIONS)[number]
+
+const LOG_LEVEL_LABELS: Record = {
+ debug: "Debug",
+ info: "Info",
+ warn: "Warn",
+ error: "Error",
+ fatal: "Fatal",
+}
+
+function getGatewayLogLevel(config: AppConfig | undefined): GatewayLogLevel {
+ const gateway = config?.gateway
+ if (typeof gateway === "object" && gateway !== null) {
+ const logLevel = (gateway as Record).log_level
+ if (
+ typeof logLevel === "string" &&
+ LOG_LEVEL_OPTIONS.includes(logLevel as GatewayLogLevel)
+ ) {
+ return logLevel as GatewayLogLevel
+ }
+ }
+ return "warn"
+}
+
+export function LogLevelSelect() {
+ const { t } = useTranslation()
+ const queryClient = useQueryClient()
+ const [logLevel, setLogLevel] = useState("warn")
+ const [savingLogLevel, setSavingLogLevel] = useState(false)
+
+ const { data: configData } = useQuery({
+ queryKey: ["config"],
+ queryFn: getAppConfig,
+ })
+
+ useEffect(() => {
+ setLogLevel(getGatewayLogLevel(configData))
+ }, [configData])
+
+ const handleLogLevelChange = async (nextValue: string) => {
+ const nextLevel = nextValue as GatewayLogLevel
+ const previousLevel = logLevel
+ setLogLevel(nextLevel)
+ setSavingLogLevel(true)
+
+ try {
+ await patchAppConfig({
+ gateway: {
+ log_level: nextLevel,
+ },
+ })
+ await queryClient.invalidateQueries({ queryKey: ["config"] })
+ await refreshGatewayState({ force: true })
+ } catch (error) {
+ setLogLevel(previousLevel)
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : t("pages.logs.log_level_error"),
+ )
+ } finally {
+ setSavingLogLevel(false)
+ }
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/logs/logs-page.tsx b/web/frontend/src/components/logs/logs-page.tsx
index a4c458fa2..853da223a 100644
--- a/web/frontend/src/components/logs/logs-page.tsx
+++ b/web/frontend/src/components/logs/logs-page.tsx
@@ -1,6 +1,7 @@
import { IconTrash } from "@tabler/icons-react"
import { useTranslation } from "react-i18next"
+import { LogLevelSelect } from "@/components/logs/log-level-select"
import { LogsPanel } from "@/components/logs/logs-panel"
import { PageHeader } from "@/components/page-header"
import { Button } from "@/components/ui/button"
@@ -17,15 +18,19 @@ export function LogsPage() {
-
- {t("pages.logs.clear")}
-
+ <>
+
+
+
+ >
}
/>
diff --git a/web/frontend/src/components/models/edit-model-sheet.tsx b/web/frontend/src/components/models/edit-model-sheet.tsx
index d1cba6719..52e2d8d9d 100644
--- a/web/frontend/src/components/models/edit-model-sheet.tsx
+++ b/web/frontend/src/components/models/edit-model-sheet.tsx
@@ -133,9 +133,10 @@ export function EditModelSheet({
}
const isOAuth = model?.auth_method === "oauth"
- const apiKeyPlaceholder = model?.configured
+ const hasSavedAPIKey = Boolean(model?.api_key)
+ const apiKeyPlaceholder = hasSavedAPIKey
? maskedSecretPlaceholder(
- model.api_key,
+ model?.api_key ?? "",
t("models.field.apiKeyPlaceholderSet"),
)
: t("models.field.apiKeyPlaceholder")
@@ -161,7 +162,7 @@ export function EditModelSheet({
{model.model_name}
@@ -127,14 +127,14 @@ export function ModelCard({
OAuth
- ) : model.configured && model.api_key ? (
+ ) : status === "available" && model.api_key ? (
{model.api_key}
) : (
- {t("models.status.unconfigured")}
+ {statusLabel}
)}
diff --git a/web/frontend/src/components/models/models-page.tsx b/web/frontend/src/components/models/models-page.tsx
index a6747c5e0..c08b3bdd6 100644
--- a/web/frontend/src/components/models/models-page.tsx
+++ b/web/frontend/src/components/models/models-page.tsx
@@ -40,7 +40,7 @@ interface ProviderGroup {
label: string
models: ModelInfo[]
hasDefault: boolean
- configuredCount: number
+ availableCount: number
}
export function ModelsPage() {
@@ -62,8 +62,8 @@ export function ModelsPage() {
const sorted = [...data.models].sort((a, b) => {
if (a.is_default && !b.is_default) return -1
if (!a.is_default && b.is_default) return 1
- if (a.configured && !b.configured) return -1
- if (!a.configured && b.configured) return 1
+ if (a.available && !b.available) return -1
+ if (!a.available && b.available) return 1
return a.model_name.localeCompare(b.model_name)
})
setModels(sorted)
@@ -107,23 +107,23 @@ export function ModelsPage() {
const providerGroups: ProviderGroup[] = Object.entries(grouped)
.map(([key, group]) => {
- const configuredCount = group.models.filter(
- (model) => model.configured,
+ const availableCount = group.models.filter(
+ (model) => model.available,
).length
return {
key,
label: group.label,
models: group.models,
hasDefault: group.models.some((model) => model.is_default),
- configuredCount,
+ availableCount,
}
})
.sort((a, b) => {
if (a.hasDefault && !b.hasDefault) return -1
if (!a.hasDefault && b.hasDefault) return 1
- if (a.configuredCount !== b.configuredCount) {
- return b.configuredCount - a.configuredCount
+ if (a.availableCount !== b.availableCount) {
+ return b.availableCount - a.availableCount
}
const aPriority = PROVIDER_PRIORITY[a.key] ?? Number.MAX_SAFE_INTEGER
diff --git a/web/frontend/src/hooks/use-chat-models.ts b/web/frontend/src/hooks/use-chat-models.ts
index 9afa882db..17cfba00f 100644
--- a/web/frontend/src/hooks/use-chat-models.ts
+++ b/web/frontend/src/hooks/use-chat-models.ts
@@ -65,32 +65,32 @@ export function useChatModels({ isConnected }: UseChatModelsOptions) {
[defaultModelName],
)
- const hasConfiguredModels = useMemo(
- () => modelList.some((m) => m.configured),
+ const hasAvailableModels = useMemo(
+ () => modelList.some((m) => m.available),
[modelList],
)
const oauthModels = useMemo(
- () => modelList.filter((m) => m.configured && m.auth_method === "oauth"),
+ () => modelList.filter((m) => m.available && m.auth_method === "oauth"),
[modelList],
)
const localModels = useMemo(
- () => modelList.filter((m) => m.configured && isLocalModel(m)),
+ () => modelList.filter((m) => m.available && isLocalModel(m)),
[modelList],
)
const apiKeyModels = useMemo(
() =>
modelList.filter(
- (m) => m.configured && m.auth_method !== "oauth" && !isLocalModel(m),
+ (m) => m.available && m.auth_method !== "oauth" && !isLocalModel(m),
),
[modelList],
)
return {
defaultModelName,
- hasConfiguredModels,
+ hasAvailableModels,
apiKeyModels,
oauthModels,
localModels,
diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json
index 69e256758..d79e90ef7 100644
--- a/web/frontend/src/i18n/locales/en.json
+++ b/web/frontend/src/i18n/locales/en.json
@@ -170,8 +170,9 @@
"noDefaultHintPrefix": "No default model set yet. Click",
"noDefaultHintSuffix": "to set one.",
"status": {
- "configured": "Configured",
- "unconfigured": "Not configured"
+ "available": "Available",
+ "unconfigured": "Not configured",
+ "unreachable": "Service unreachable"
},
"badge": {
"default": "Default",
@@ -547,6 +548,7 @@
"unsaved_changes": "You have unsaved changes."
},
"logs": {
+ "log_level_error": "Failed to update log level.",
"clear": "Clear logs",
"empty": "Waiting for logs..."
}
diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json
index e7aca1918..e5aa71a44 100644
--- a/web/frontend/src/i18n/locales/zh.json
+++ b/web/frontend/src/i18n/locales/zh.json
@@ -170,8 +170,9 @@
"noDefaultHintPrefix": "尚未设置默认模型,点击",
"noDefaultHintSuffix": "设为默认。",
"status": {
- "configured": "已配置",
- "unconfigured": "未配置"
+ "available": "可用",
+ "unconfigured": "未配置",
+ "unreachable": "服务不可达"
},
"badge": {
"default": "默认",
@@ -547,6 +548,7 @@
"unsaved_changes": "您有未保存的更改。"
},
"logs": {
+ "log_level_error": "更新日志等级失败。",
"clear": "清空日志",
"empty": "等待日志中..."
}