返回 DeepSeek-Reasonix
ProviderConnections.tsx
根目录 / desktop / frontend / src / components / ProviderConnections.tsx
1 import { useEffect, useState, type ReactNode } from "react";
2 import type { ProviderAccessGroup } from "./SettingsPanel";
3 import type { ProviderPresetView } from "../lib/types";
4 import { catalogForPreset } from "../lib/providerCatalog";
5 import { providerBrandIcons } from "../lib/providerBrandIcons";
6 import { useT } from "../lib/i18n";
7 import { apiFormatLabel } from "./ProviderCatalogPicker";
8
9 export function connectionBrand(group: ProviderAccessGroup, presets: ProviderPresetView[]) {
10 const preset = presets.find((p) => p.providerNames.some((name) => group.providers.some((v) => v.name === name)))
11 ?? presets.find((p) => {
12 const base = catalogForPreset(p).baseUrl;
13 try { return Boolean(base) && new URL(base).origin === new URL(group.baseUrl).origin; } catch { return false; }
14 });
15 if (preset) { const c = catalogForPreset(preset); return { id: c.brandId, label: c.brandLabel }; }
16 return { id: group.id, label: group.label };
17 }
18
19 export function ProviderConnections({ groups, presets, revealedProvider, hidden, busy, onAdd, renderDetail }: {
20 groups: ProviderAccessGroup[]; presets: ProviderPresetView[]; revealedProvider: string | null;
21 hidden: boolean; busy: boolean; onAdd: () => void; renderDetail: (group: ProviderAccessGroup) => ReactNode;
22 }) {
23 const t = useT();
24 const [selected, setSelected] = useState<string | null>(null);
25 const [query, setQuery] = useState("");
26 const [mobileDetail, setMobileDetail] = useState(false);
27 const active = groups.find((g) => g.id === selected)?.id ?? groups[0]?.id;
28 useEffect(() => {
29 const target = groups.find((g) => g.providers.some((p) => p.name === revealedProvider));
30 if (target) { setSelected(target.id); setQuery(""); setMobileDetail(true); }
31 }, [revealedProvider, groups]);
32 const connections = groups.filter(g => `${connectionBrand(g,presets).label} ${g.label} ${g.models.join(" ")} ${g.baseUrl}`.toLowerCase().includes(query.trim().toLowerCase()));
33 if (!groups.length) return null;
34 return <div hidden={hidden} className={`provider-connections${mobileDetail ? " provider-connections--detail" : ""}`}>
35 <nav className="provider-connections__nav" aria-label={t("settings.providerAccess")}>
36 <input className="mem-input" value={query} onChange={(e) => setQuery(e.target.value)} placeholder={t("settings.connections.search")} aria-label={t("settings.connections.search")} />
37 <div className="provider-connections__list">
38 {connections.map(g => { const brand=connectionBrand(g,presets); return <button type="button" key={g.id} className="provider-connections__item provider-connections__item--flat" title={`${g.label} · ${apiFormatLabel(g.kind)}`} aria-current={active===g.id ? "true" : undefined} onClick={()=>{setSelected(g.id);setMobileDetail(true);}}>
39 {providerBrandIcons.has(brand.id) ? <span className="provider-catalog__icon" style={{maskImage:`url(/provider-icons/${brand.id}.svg)`}} aria-hidden="true"/> : <span className="provider-catalog__monogram" aria-hidden="true">{brand.label.slice(0,1)}</span>}
40 <strong>{g.label}</strong><span className={`provider-connections__status${g.configured ? " provider-connections__status--ready" : ""}`} aria-label={t(g.configured ? "settings.connections.configured" : "settings.modelsRequireKey")}/>
41 </button>;})}
42 {!connections.length && <p className="muted">{t("settings.connections.noResults")}</p>}
43 </div>
44 <button className="btn" disabled={busy} onClick={onAdd}>{t("settings.addProvider")}</button>
45 </nav>
46 <div className="provider-connections__detail">
47 <button className="btn btn--small provider-connections__back" onClick={() => setMobileDetail(false)}>{t("settings.connections.back")}</button>
48 {/* Retain editors and fetched-model drafts across navigation and add flow. */}
49 {groups.map((g) => <div key={g.id} hidden={g.id !== active}>{renderDetail(g)}</div>)}
50 </div>
51 </div>;
52 }
53
53 lines Plain Text