返回 DeepSeek-Reasonix
ProviderModelDialog.tsx
根目录 / desktop / frontend / src / components / ProviderModelDialog.tsx
1 import { useId, useLayoutEffect, useRef, useState } from "react";
2 import { createPortal } from "react-dom";
3 import { LockKeyhole, RotateCcw } from "lucide-react";
4 import { useProviderT as useT } from "../lib/providerSettingsLocale";
5 import { imageInputHardBlocked, imageInputState } from "../lib/providerImageInput";
6 import { ModalCloseButton } from "./ModalCloseButton";
7 import type { ProviderModelCapabilityView } from "../lib/types";
8 import { modelDraftError } from "../lib/providerModelDraft";
9
10 export interface ModelDetailsDraft { model: string; contextWindow: string; maxOutputTokens: number; vision: boolean | null; supportedEfforts: string[]; defaultEffort: string; }
11 export default function ProviderModelDialog({ initial, candidates, contextDefault, capability, baseURL, busy, effortOptions, onClose, onApply, onDelete }: {
12 initial?: ModelDetailsDraft; candidates: string[]; contextDefault?: number;
13 effortOptions?: string[]; effortDefault?: string;
14 capability?: ProviderModelCapabilityView; baseURL?: string; busy: boolean; onClose: () => void; onApply: (draft: ModelDetailsDraft) => void;
15 onDelete?: () => void;
16 }) {
17 const t = useT(), titleId = useId();
18 const dialog = useRef<HTMLDialogElement>(null);
19 const [model, setModel] = useState(initial?.model ?? "");
20 const [context, setContext] = useState(initial?.contextWindow ?? "");
21 const [output, setOutput] = useState(initial?.maxOutputTokens ? String(Math.max(-1, initial.maxOutputTokens)) : "");
22 const [vision, setVision] = useState(initial?.vision == null ? "auto" : String(initial.vision));
23 const [effort, setEffort] = useState(initial?.defaultEffort ?? "");
24 const [explicitEfforts, setExplicitEfforts] = useState(Boolean(initial?.supportedEfforts?.length));
25 const [selectedEfforts, setSelectedEfforts] = useState<string[]>(initial?.supportedEfforts?.length ? initial.supportedEfforts : effortOptions ?? []);
26 const [error, setError] = useState(false);
27 useLayoutEffect(() => {
28 const previous = document.activeElement as HTMLElement | null;
29 dialog.current?.showModal();
30 dialog.current?.querySelector<HTMLInputElement>("input:not(:disabled)")?.focus();
31 return () => { if (previous?.isConnected) previous.focus(); };
32 }, []);
33 const validation = modelDraftError({model, context, output, vision: "auto"}, candidates, initial?.model);
34 const imageBlocked = imageInputHardBlocked(baseURL, model, capability);
35 const imageState = imageBlocked ? "unsupported" : imageInputState(vision === "auto" ? "auto" : vision === "true" ? "on" : "off", capability);
36 const toggleEffort = (option: string, checked: boolean) => {
37 setExplicitEfforts(true);
38 setSelectedEfforts(current => {
39 const next = checked ? [...current, option] : current.filter(item => item !== option);
40 if (!checked && effort === option) setEffort("");
41 return next;
42 });
43 };
44 return createPortal(<dialog ref={dialog} className="provider-model-dialog" data-app-overlay="" aria-labelledby={titleId}
45 onCancel={event => { event.preventDefault(); if (!busy) onClose(); }}>
46 <form onSubmit={event => { event.preventDefault(); if (validation) { setError(true); return; } const defaultEffort = effort && selectedEfforts.includes(effort) ? effort : ""; onApply({ model:model.trim(), contextWindow:context.trim(), maxOutputTokens:Number(output) || 0, vision:vision === "auto" ? null : !imageBlocked && vision === "true", supportedEfforts: explicitEfforts ? selectedEfforts : [], defaultEffort }); }}>
47 <header><h2 id={titleId}>{t(initial ? "settings.modelDialog.edit" : "settings.models.add")}</h2><ModalCloseButton label={t("common.close")} disabled={busy} onClick={onClose}/></header>
48 <label className="provider-model-dialog__id">{t("settings.modelDialog.id")}
49 {initial ? <span><LockKeyhole size={16}/>{model}</span> : <input autoFocus className="mem-input" value={model} disabled={busy} onChange={e=>setModel(e.target.value)} placeholder="deepseek-v4-flash"/>}
50 </label>
51 <div className="provider-model-dialog__columns">
52 <section><h3>{t("settings.modelDialog.parameters")}</h3>
53 <label htmlFor={`${titleId}-context`}>{t("settings.modelContextWindow")}<button type="button" className="btn provider-icon-action" title={t("settings.modelDialog.reset")} aria-label={t("settings.modelDialog.resetContext")} disabled={busy} onClick={()=>setContext("")}><RotateCcw size={16}/></button>
54 <input id={`${titleId}-context`} className="mem-input" type="number" min={1} value={context} disabled={busy} placeholder={t("settings.models.inherit")} onChange={e=>setContext(e.target.value)}/>
55 </label>
56 <p>{t("settings.models.inherit")}{contextDefault ? ` · ${contextDefault.toLocaleString()}` : ""}</p>
57 <label htmlFor={`${titleId}-output`}>{t("settings.modelDialog.outputLimit")}<button type="button" className="btn provider-icon-action" title={t("settings.modelDialog.reset")} aria-label={t("settings.modelDialog.resetOutput")} disabled={busy} onClick={()=>setOutput("")}><RotateCcw size={16}/></button>
58 <input id={`${titleId}-output`} className="mem-input" type="number" min={-1} value={output} disabled={busy} placeholder={t("settings.models.inherit")} onChange={e=>setOutput(e.target.value)}/>
59 </label>
60 <p>{t("settings.modelDialog.outputHint")}</p>
61 {effortOptions && effortOptions.length > 0 && <div className="provider-model-dialog__effort-card">
62 <label>{t("settings.modelDialog.reasoningEffortOptions")}
63 <button type="button" className="btn provider-icon-action" title={t("settings.modelDialog.reset")} aria-label={t("settings.modelDialog.resetReasoningEffort")} disabled={busy} onClick={()=>{setEffort("");setExplicitEfforts(false);setSelectedEfforts(effortOptions ?? []);}}><RotateCcw size={16}/></button>
64 </label>
65 <div className="provider-model-dialog__chips">
66 {effortOptions.map(option => <label key={option}><input type="checkbox" checked={selectedEfforts.includes(option)} disabled={busy} onChange={event=>toggleEffort(option, event.target.checked)}/>{option}</label>)}
67 </div>
68 <label htmlFor={`${titleId}-effort`}>{t("settings.modelDialog.reasoningEffortDefault")}</label>
69 <select id={`${titleId}-effort`} className="mem-select" value={effort} disabled={busy || selectedEfforts.length === 0} onChange={event=>setEffort(event.target.value)}>
70 <option value="">{t("settings.modelDialog.automatic")}</option>
71 {selectedEfforts.map(option => <option key={option} value={option}>{option}</option>)}
72 </select>
73 <p>{t("settings.modelDialog.reasoningEffortHint")}</p>
74 </div>}
75 {capability?.reasoning?.state === "unknown" && <p>{t("settings.modelDialog.reasoningUnknown")}</p>}
76 {capability?.reasoning?.state === "unsupported" && <p>{t("settings.modelDialog.reasoningUnsupported")}</p>}
77 {capability?.reasoning?.error && <p role="alert">{capability.reasoning.error}</p>}
78 </section>
79 <aside><h3>{t("settings.modelDialog.capabilities")}</h3>
80 <div className="provider-model-dialog__capability-title">{t("settings.modelDialog.input")}</div>
81 <div className="provider-model-dialog__chips">
82 <label><input type="checkbox" checked disabled/>{t("settings.textInput")}<LockKeyhole size={14}/></label>
83 <label><input type="checkbox" checked={imageState === "supported"} disabled={busy || imageBlocked} onChange={event=>setVision(String(event.target.checked))}/>{t("settings.modelDialog.image")}</label>
84 <label title={t("settings.modelDialog.unavailable")}><input type="checkbox" checked={false} disabled/>{t("settings.modelDialog.video")}</label>
85 <label title={t("settings.modelDialog.unavailable")}><input type="checkbox" checked={false} disabled/>PDF</label>
86 </div>
87 <p>{t("settings.modelDialog.unavailable")}</p>
88 <div className="provider-model-dialog__capability-title">{t("settings.modelDialog.output")}</div>
89 <div className="provider-model-dialog__chips"><label><input type="checkbox" checked disabled/>{t("settings.textInput")}<LockKeyhole size={14}/></label></div>
90 <dl><div><dt>{t("settings.modelDialog.source")}</dt><dd>{vision !== "auto" ? t("settings.modelDialog.manual") : capability ? t("settings.modelDialog.metadata") : t("settings.imageInputUnknown")}</dd></div></dl>
91 <button type="button" className="btn" disabled={busy || vision === "auto"} onClick={()=>setVision("auto")}>{t("settings.modelDialog.restore")}</button>
92 <p>{t("settings.modelDialog.overrideHint")}</p>
93 </aside>
94 </div>
95 {error && validation && <p role="alert">{t(`providerUI.validation.${validation}`)}</p>}
96 <footer>{initial && onDelete && <button type="button" className="btn btn--danger" disabled={busy} onClick={onDelete}>{t("providerUI.deleteModel")}</button>}<small>{t("settings.modelDialog.draftHint")}</small><button type="button" className="btn" disabled={busy} onClick={onClose}>{t("common.cancel")}</button><button className="btn btn--primary" disabled={busy}>{t(initial ? "settings.modelDialog.apply" : "settings.modelDialog.add")}</button></footer>
97 </form>
98 </dialog>, document.body);
99 }
100
100 lines Plain Text