diff --git a/gui/src/combo-public-model.ts b/gui/src/combo-public-model.ts new file mode 100644 index 000000000..9292cbf71 --- /dev/null +++ b/gui/src/combo-public-model.ts @@ -0,0 +1,7 @@ +/** Placeholder shown before the draft id/alias can form a public model id. */ +export const PUBLIC_MODEL_PREVIEW_PLACEHOLDER = "…"; + +/** True when the preview value is a real public model id clients can request. */ +export function canCopyPublicModelId(model: string): boolean { + return model.trim().length > 0 && model !== PUBLIC_MODEL_PREVIEW_PLACEHOLDER; +} diff --git a/gui/src/components/combo-workspace-add-modal.tsx b/gui/src/components/combo-workspace-add-modal.tsx index cff828c5c..8dcfb5869 100644 --- a/gui/src/components/combo-workspace-add-modal.tsx +++ b/gui/src/components/combo-workspace-add-modal.tsx @@ -6,11 +6,12 @@ import { intersectComboEfforts, validateComboDraft, } from "../combo-workspace-data"; +import { PUBLIC_MODEL_PREVIEW_PLACEHOLDER } from "../combo-public-model"; import { IconX } from "../icons"; import { useT } from "../i18n/shared"; import { Notice } from "../ui"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; -import { EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { EffortSelect, PublicModelPreview, StrategySeg, TargetEditor } from "./combo-workspace-controls"; import { clampedNumberInput } from "./combo-workspace-utils"; export function AddComboModal({ @@ -139,11 +140,9 @@ export function AddComboModal({

{t("cws.field.aliasHint")}

-

- {t("cws.field.idHint", { - model: draft.id.trim() ? comboPublicModelId(draft.id, draft.alias) : "…", - })} -

+
{t("cws.strategy")} diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx index 8a7277fd4..c5e619e0e 100644 --- a/gui/src/components/combo-workspace-controls.tsx +++ b/gui/src/components/combo-workspace-controls.tsx @@ -1,11 +1,13 @@ import { useState } from "react"; import type { ComboEffort, ComboStrategy, ComboTarget } from "../combo-workspace-data"; +import { canCopyPublicModelId } from "../combo-public-model"; import { COMBO_EFFORTS, newComboTarget } from "../combo-workspace-data"; import { IconArrowDown, IconArrowUp, IconGrip, IconPlus, IconTrash } from "../icons"; import { useT } from "../i18n/shared"; import { formatProviderDisplayName } from "../provider-icons"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; import { clampedNumberInput, enabledProviders, modelsForProvider } from "./combo-workspace-utils"; +import { useCopyFeedback } from "./use-copy-feedback"; export function StrategySeg({ value, @@ -262,3 +264,40 @@ export function TargetEditor({
); } + +/** Effective public model id clients will request — mono value + copy. */ +export function PublicModelPreview({ model }: { model: string }) { + const t = useT(); + const { outcomeFor, copy } = useCopyFeedback(); + const canCopy = canCopyPublicModelId(model); + const outcome = outcomeFor(model); + const copyLabel = outcome === "copied" + ? t("cws.copiedPublicModel") + : outcome === "unavailable" + ? t("cws.copyUnavailable") + : t("cws.copyPublicModel"); + // Split around a sentinel so the model token stays mono in any locale word order. + const sentinel = "\u0001"; + const [before, after = ""] = t("cws.field.publicModelPreview", { model: sentinel }).split(sentinel); + + return ( +
+

+ {before} + {model} + {after} +

+ +
+ ); +} diff --git a/gui/src/components/combo-workspace-detail-panel.tsx b/gui/src/components/combo-workspace-detail-panel.tsx index 1c6f9e059..47070eeb1 100644 --- a/gui/src/components/combo-workspace-detail-panel.tsx +++ b/gui/src/components/combo-workspace-detail-panel.tsx @@ -8,12 +8,14 @@ import { updateComboAliasDraft, validateComboDraft, } from "../combo-workspace-data"; +import { PUBLIC_MODEL_PREVIEW_PLACEHOLDER } from "../combo-public-model"; import { IconChevron, IconTrash } from "../icons"; import { useT } from "../i18n/shared"; import { Notice } from "../ui"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; -import { EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { EffortSelect, PublicModelPreview, StrategySeg, TargetEditor } from "./combo-workspace-controls"; import { clampedNumberInput } from "./combo-workspace-utils"; +import { useCopyFeedback } from "./use-copy-feedback"; type DetailTab = "config" | "about"; @@ -57,6 +59,7 @@ export function DetailPanel({ onDirtyChange: (dirty: boolean) => void; }) { const t = useT(); + const { outcomeFor, copy } = useCopyFeedback(); const [tab, setTab] = useState("config"); /* @@ -79,7 +82,6 @@ export function DetailPanel({ const [draft, setDraft] = useState(baseline); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); - const [copied, setCopied] = useState(false); const dirty = !draftEquals(draft, baseline); const baselineSyncKey = `${baseline.id}:${baseline.alias ?? ""}:${baseline.nativeAlias}:${baseline.displayName ?? ""}:${baseline.strategy}:${baseline.stickyLimit}:${baseline.defaultEffort}:${baseline.targets.map((t) => `${t.provider}/${t.model}:${t.weight ?? 1}`).join(",")}`; const effortMap = useMemo(() => { @@ -111,15 +113,6 @@ export function DetailPanel({ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: key captures baseline payload }, [baselineSyncKey]); - const copyModel = async () => { - try { - await navigator.clipboard.writeText(baseline.model); - setCopied(true); - window.setTimeout(() => setCopied(false), 1200); - } catch { - /* ignore */ - } - }; const save = async () => { const code = validateComboDraft(draft, { @@ -163,6 +156,14 @@ export function DetailPanel({ const headerModel = isCreate ? (draft.id.trim() ? comboPublicModelId(draft.id, draft.alias) : t("cws.addTitle")) : baseline.model; + // Public model id clients request — same string PublicModelPreview copies. + const copyModelId = baseline.model; + const copyOutcome = outcomeFor(copyModelId); + const copyLabel = copyOutcome === "copied" + ? t("cws.copied") + : copyOutcome === "unavailable" + ? t("cws.copyUnavailable") + : t("cws.copyModel"); return (
@@ -175,8 +176,13 @@ export function DetailPanel({ )}

{headerModel}

{!isCreate && ( - )}
@@ -251,9 +257,7 @@ export function DetailPanel({ }))} />

- {isCreate - ? t("cws.field.idInternalHint") - : t("cws.field.idHintEdit", { model: comboPublicModelId(draft.id, draft.alias) })} + {isCreate ? t("cws.field.idInternalHint") : t("cws.field.idHintEdit")}

@@ -269,6 +273,9 @@ export function DetailPanel({

{t("cws.field.aliasHint")}

+