| 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 |