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