feat: add config save and restart prompts

This commit is contained in:
SiYue-ZO
2026-04-25 02:03:00 +08:00
parent 39dec35408
commit afc600baed
10 changed files with 257 additions and 48 deletions
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { addModel, setDefaultModel } from "@/api/models"
import { ConfigChangeNotice } from "@/components/config-change-notice"
import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
import {
AdvancedSection,
@@ -21,6 +22,8 @@ import {
SheetTitle,
} from "@/components/ui/sheet"
import { Textarea } from "@/components/ui/textarea"
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
import { refreshGatewayState } from "@/store/gateway"
interface AddForm {
modelName: string
@@ -83,6 +86,8 @@ export function AddModelSheet({
form.apiKey,
t("models.field.apiKeyPlaceholder"),
)
const isDirty =
JSON.stringify(form) !== JSON.stringify(EMPTY_ADD_FORM) || setAsDefault
useEffect(() => {
if (open) {
@@ -149,6 +154,13 @@ export function AddModelSheet({
if (setAsDefault) {
await setDefaultModel(modelName)
}
const gateway = await refreshGatewayState({ force: true })
showSaveSuccessOrRestartToast(
t,
t("models.add.saveSuccess"),
modelName,
gateway?.restartRequired === true,
)
onSaved()
onClose()
} catch (e) {
@@ -367,10 +379,17 @@ export function AddModelSheet({
</div>
<SheetFooter className="border-t-muted border-t px-6 py-4">
{isDirty && (
<ConfigChangeNotice
kind="save"
title={t("common.saveChangesTitle")}
description={t("models.unsavedPrompt")}
/>
)}
<Button variant="ghost" onClick={onClose} disabled={saving}>
{t("common.cancel")}
</Button>
<Button onClick={handleSave} disabled={saving}>
<Button onClick={handleSave} disabled={!isDirty || saving}>
{saving && <IconLoader2 className="size-4 animate-spin" />}
{t("models.add.confirm")}
</Button>
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { type ModelInfo, setDefaultModel, updateModel } from "@/api/models"
import { ConfigChangeNotice } from "@/components/config-change-notice"
import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
import {
AdvancedSection,
@@ -21,6 +22,8 @@ import {
SheetTitle,
} from "@/components/ui/sheet"
import { Textarea } from "@/components/ui/textarea"
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
import { refreshGatewayState } from "@/store/gateway"
interface EditForm {
provider: string
@@ -46,6 +49,29 @@ interface EditModelSheetProps {
onSaved: () => void
}
function buildInitialEditForm(model: ModelInfo): EditForm {
return {
provider: model.provider ?? "",
modelId: model.model,
apiKey: "",
apiBase: model.api_base ?? "",
proxy: model.proxy ?? "",
authMethod: model.auth_method ?? "",
connectMode: model.connect_mode ?? "",
workspace: model.workspace ?? "",
rpm: model.rpm ? String(model.rpm) : "",
maxTokensField: model.max_tokens_field ?? "",
requestTimeout: model.request_timeout ? String(model.request_timeout) : "",
thinkingLevel: model.thinking_level ?? "",
extraBody: model.extra_body
? JSON.stringify(model.extra_body, null, 2)
: "",
customHeaders: model.custom_headers
? JSON.stringify(model.custom_headers, null, 2)
: "",
}
}
export function EditModelSheet({
model,
open,
@@ -72,31 +98,15 @@ export function EditModelSheet({
const [saving, setSaving] = useState(false)
const [setAsDefault, setSetAsDefault] = useState(false)
const [error, setError] = useState("")
const initialForm = model ? buildInitialEditForm(model) : null
const isDirty =
model != null &&
(JSON.stringify(form) !== JSON.stringify(initialForm) ||
setAsDefault !== model.is_default)
useEffect(() => {
if (model) {
setForm({
provider: model.provider ?? "",
modelId: model.model,
apiKey: "",
apiBase: model.api_base ?? "",
proxy: model.proxy ?? "",
authMethod: model.auth_method ?? "",
connectMode: model.connect_mode ?? "",
workspace: model.workspace ?? "",
rpm: model.rpm ? String(model.rpm) : "",
maxTokensField: model.max_tokens_field ?? "",
requestTimeout: model.request_timeout
? String(model.request_timeout)
: "",
thinkingLevel: model.thinking_level ?? "",
extraBody: model.extra_body
? JSON.stringify(model.extra_body, null, 2)
: "",
customHeaders: model.custom_headers
? JSON.stringify(model.custom_headers, null, 2)
: "",
})
setForm(buildInitialEditForm(model))
setSetAsDefault(model.is_default)
setError("")
}
@@ -142,6 +152,13 @@ export function EditModelSheet({
if (setAsDefault && !model.is_default) {
await setDefaultModel(model.model_name)
}
const gateway = await refreshGatewayState({ force: true })
showSaveSuccessOrRestartToast(
t,
t("models.edit.saveSuccess"),
model.model_name,
gateway?.restartRequired === true,
)
onSaved()
onClose()
} catch (e) {
@@ -359,10 +376,17 @@ export function EditModelSheet({
</div>
<SheetFooter className="border-t-muted border-t px-6 py-4">
{isDirty && (
<ConfigChangeNotice
kind="save"
title={t("common.saveChangesTitle")}
description={t("models.unsavedPrompt")}
/>
)}
<Button variant="ghost" onClick={onClose} disabled={saving}>
{t("common.cancel")}
</Button>
<Button onClick={handleSave} disabled={saving}>
<Button onClick={handleSave} disabled={!isDirty || saving}>
{saving && <IconLoader2 className="size-4 animate-spin" />}
{t("common.save")}
</Button>