diff --git a/apps/web/src/domains/settings/ai/ai-types.ts b/apps/web/src/domains/settings/ai/ai-types.ts index 4b614586dc3..0b34484e38c 100644 --- a/apps/web/src/domains/settings/ai/ai-types.ts +++ b/apps/web/src/domains/settings/ai/ai-types.ts @@ -304,4 +304,4 @@ export const LS_TTS_VOICE_ID_PREFIX = "vellum:voice:ttsVoiceId:"; export const LS_STT_PROVIDER = "vellum:voice:sttProvider"; export const LS_STT_API_KEY_PREFIX = "vellum:voice:sttApiKey:"; -export const LS_IMAGE_GEN_CREDENTIAL = "vellum:ai:geminiKey"; + diff --git a/apps/web/src/domains/settings/ai/call-site-helpers.test.ts b/apps/web/src/domains/settings/ai/call-site-helpers.test.ts index c54981262b9..41d849976d9 100644 --- a/apps/web/src/domains/settings/ai/call-site-helpers.test.ts +++ b/apps/web/src/domains/settings/ai/call-site-helpers.test.ts @@ -5,7 +5,7 @@ import { buildOrderedProfiles } from "@/domains/settings/ai/ai-utils"; import { isDraftActive, draftsEqual, -} from "@/domains/settings/ai/call-site-overrides-modal"; +} from "@/domains/settings/ai/call-site-helpers"; // --------------------------------------------------------------------------- // isDraftActive diff --git a/apps/web/src/domains/settings/ai/call-site-helpers.ts b/apps/web/src/domains/settings/ai/call-site-helpers.ts new file mode 100644 index 00000000000..530ea5df990 --- /dev/null +++ b/apps/web/src/domains/settings/ai/call-site-helpers.ts @@ -0,0 +1,31 @@ +import type { CallSiteOverrideDraft } from "@/domains/settings/ai/ai-types"; + +// --------------------------------------------------------------------------- +// Sentinel value for the "Custom" profile picker option +// --------------------------------------------------------------------------- + +export const CUSTOM_SENTINEL = "__custom__"; + +// --------------------------------------------------------------------------- +// Pure helpers +// --------------------------------------------------------------------------- + +export function isDraftActive(d: CallSiteOverrideDraft | null | undefined): boolean { + if (!d) return false; + return !!(d.profile || d.provider || d.model); +} + +export function draftsEqual( + a: CallSiteOverrideDraft | null | undefined, + b: CallSiteOverrideDraft | null | undefined, +): boolean { + const aActive = isDraftActive(a); + const bActive = isDraftActive(b); + if (aActive !== bActive) return false; + if (!aActive) return true; + return ( + (a?.profile ?? null) === (b?.profile ?? null) && + (a?.provider ?? null) === (b?.provider ?? null) && + (a?.model ?? null) === (b?.model ?? null) + ); +} diff --git a/apps/web/src/domains/settings/ai/call-site-overrides-modal.tsx b/apps/web/src/domains/settings/ai/call-site-overrides-modal.tsx index 5ef179d1e23..7b449bda452 100644 --- a/apps/web/src/domains/settings/ai/call-site-overrides-modal.tsx +++ b/apps/web/src/domains/settings/ai/call-site-overrides-modal.tsx @@ -5,9 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { Button } from "@vellum/design-library/components/button"; import { ConfirmDialog } from "@vellum/design-library/components/confirm-dialog"; -import { Dropdown } from "@vellum/design-library/components/dropdown"; import { Input } from "@vellum/design-library/components/input"; -import { Toggle } from "@vellum/design-library/components/toggle"; import { Modal } from "@vellum/design-library/components/modal"; import { toast } from "@vellum/design-library/components/toast"; import type { ConfigLlmCallsitesGetResponse } from "@/generated/daemon/types.gen"; @@ -16,12 +14,10 @@ import { captureError } from "@/lib/sentry/capture-error"; import { useAssistantFeatureFlagStore } from "@/stores/assistant-feature-flag-store"; import { getDefaultModelForProvider, - getModelsForProvider, } from "@/assistant/llm-model-catalog"; import { type CallSiteOverrideDraft, - INFERENCE_PROVIDER_DISPLAY_NAMES, INFERENCE_PROVIDERS, } from "@/domains/settings/ai/ai-types"; import { @@ -34,12 +30,8 @@ import { useDaemonConfigQuery, useDaemonConfigMutation, } from "@/domains/settings/ai/use-daemon-config"; - -// --------------------------------------------------------------------------- -// Sentinel value for the "Custom" profile picker option -// --------------------------------------------------------------------------- - -const CUSTOM_SENTINEL = "__custom__"; +import { CUSTOM_SENTINEL, isDraftActive, draftsEqual } from "@/domains/settings/ai/call-site-helpers"; +import { CallSiteOverrideRow } from "@/domains/settings/ai/call-site-overrides-row"; // --------------------------------------------------------------------------- // Types @@ -55,30 +47,6 @@ export interface CallSiteOverridesModalProps { assistantId: string; } -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -export function isDraftActive(d: CallSiteOverrideDraft | null | undefined): boolean { - if (!d) return false; - return !!(d.profile || d.provider || d.model); -} - -export function draftsEqual( - a: CallSiteOverrideDraft | null | undefined, - b: CallSiteOverrideDraft | null | undefined, -): boolean { - const aActive = isDraftActive(a); - const bActive = isDraftActive(b); - if (aActive !== bActive) return false; - if (!aActive) return true; - return ( - (a?.profile ?? null) === (b?.profile ?? null) && - (a?.provider ?? null) === (b?.provider ?? null) && - (a?.model ?? null) === (b?.model ?? null) - ); -} - // --------------------------------------------------------------------------- // CallSiteOverridesModal // --------------------------------------------------------------------------- @@ -192,8 +160,6 @@ function CallSiteOverridesModalInner({ // Derived state // --------------------------------------------------------------------------- - const availableProviders = INFERENCE_PROVIDERS; - const gatedCallSiteIdSet = useMemo( () => new Set(catalogCallSiteIds), [catalogCallSiteIds], @@ -281,75 +247,41 @@ function CallSiteOverridesModalInner({ }, [catalog, filteredCallSites]); // --------------------------------------------------------------------------- - // Row helpers + // Row callbacks // --------------------------------------------------------------------------- - function getDraft(id: string): CallSiteOverrideDraft | null { - return drafts[id] ?? null; - } - - function isOverrideOn(id: string): boolean { - return isDraftActive(getDraft(id)); - } - - function getProfilePickerValue(id: string): string { - const d = getDraft(id); - if (!d || !isDraftActive(d)) return ""; - if (d.provider || d.model) return CUSTOM_SENTINEL; - return d.profile ?? ""; - } - - function handleToggle(id: string, on: boolean, defaultProfile?: string) { - if (!on) { - setDraftEdits((prev) => ({ ...prev, [id]: null })); - return; - } - const seedProfile = selectSeedProfileForOverride( - orderedProfiles, - defaultProfile, - queryComplexityRoutingEnabled, - ); - if (seedProfile) { - setDraftEdits((prev) => ({ ...prev, [id]: { profile: seedProfile } })); - } else { - const defaultProvider = availableProviders[0]; - const defaultModel = getDefaultModelForProvider(defaultProvider) ?? ""; - setDraftEdits((prev) => ({ - ...prev, - [id]: { provider: defaultProvider, model: defaultModel }, - })); - } - } - - function handleProfilePickerChange(id: string, val: string) { - if (val === CUSTOM_SENTINEL) { - const defaultProvider = availableProviders[0]; - const defaultModel = getDefaultModelForProvider(defaultProvider) ?? ""; - setDraftEdits((prev) => ({ - ...prev, - [id]: { profile: null, provider: defaultProvider, model: defaultModel }, - })); - } else if (val === "") { - setDraftEdits((prev) => ({ ...prev, [id]: null })); - } else { - setDraftEdits((prev) => ({ - ...prev, - [id]: { profile: val, provider: null, model: null }, - })); - } - } - - function handleProviderChange(id: string, provider: string) { - const defaultModel = getDefaultModelForProvider(provider) ?? ""; - setDraftEdits((prev) => ({ - ...prev, - [id]: { ...(prev[id] ?? drafts[id]), profile: null, provider, model: defaultModel }, - })); - } + const handleDraftChange = useCallback( + (id: string, draft: CallSiteOverrideDraft | null) => { + setDraftEdits((prev) => ({ ...prev, [id]: draft })); + }, + [], + ); - function handleModelChange(id: string, model: string) { - setDraftEdits((prev) => ({ ...prev, [id]: { ...(prev[id] ?? drafts[id]), model } })); - } + const handleToggle = useCallback( + (id: string, on: boolean) => { + if (!on) { + setDraftEdits((prev) => ({ ...prev, [id]: null })); + return; + } + const cs = gatedCallSites.find((c) => c.id === id); + const seedProfile = selectSeedProfileForOverride( + orderedProfiles, + cs?.defaultProfile, + queryComplexityRoutingEnabled, + ); + if (seedProfile) { + setDraftEdits((prev) => ({ ...prev, [id]: { profile: seedProfile } })); + } else { + const defaultProvider = INFERENCE_PROVIDERS[0]; + const defaultModel = getDefaultModelForProvider(defaultProvider) ?? ""; + setDraftEdits((prev) => ({ + ...prev, + [id]: { provider: defaultProvider, model: defaultModel }, + })); + } + }, + [gatedCallSites, orderedProfiles, queryComplexityRoutingEnabled], + ); // --------------------------------------------------------------------------- // Save / Reset @@ -471,25 +403,12 @@ function CallSiteOverridesModalInner({

{sites.map((cs) => { - const overrideOn = isOverrideOn(cs.id); - const profileVal = getProfilePickerValue(cs.id); - const isCustom = profileVal === CUSTOM_SENTINEL; - const draft = getDraft(cs.id); - const currentProvider = - draft?.provider ?? availableProviders[0]; - const availableModels = getModelsForProvider( - currentProvider ?? "anthropic", - ); - const modelOptions = availableModels.map((m) => ({ - value: m.id, - label: m.displayName, - })); - const hasModelError = !!draft?.provider && !draft?.model; - const profileOptions = buildProfileOptionsForRow( - profileVal === "" || profileVal === CUSTOM_SENTINEL - ? null - : profileVal, - ); + const profileVal = (() => { + const d = drafts[cs.id] ?? null; + if (!d || !isDraftActive(d)) return ""; + if (d.provider || d.model) return CUSTOM_SENTINEL; + return d.profile ?? ""; + })(); const defaultProfileLabel = cs.defaultProfile ? (orderedProfiles.find( (op) => op.name === cs.defaultProfile, @@ -497,90 +416,21 @@ function CallSiteOverridesModalInner({ : null; return ( -
-
-
- {/* typography: off-scale — call-site name uses medium weight for visual hierarchy within card */} -

- {cs.displayName} -

- {cs.description && ( -

- {cs.description} - {defaultProfileLabel && ( - - · Default: {defaultProfileLabel} - - )} -

- )} -
-
- {overrideOn && ( - - handleProfilePickerChange(cs.id, val) - } - options={profileOptions} - className="w-36" - /> - )} - - handleToggle(cs.id, on, cs.defaultProfile) - } - aria-label={`Override ${cs.displayName}`} - /> -
-
- - {/* Custom provider + model pickers */} - {overrideOn && isCustom && ( -
-
-
- - - handleProviderChange(cs.id, val) - } - options={availableProviders.map((p) => ({ - value: p, - label: - INFERENCE_PROVIDER_DISPLAY_NAMES[p] ?? - p, - }))} - /> -
-
- - - handleModelChange(cs.id, val) - } - options={modelOptions} - /> -
-
- {hasModelError && ( -

- Pick a model -

- )} -
+ id={cs.id} + displayName={cs.displayName} + description={cs.description} + defaultProfileLabel={defaultProfileLabel} + draft={drafts[cs.id] ?? null} + profileOptions={buildProfileOptionsForRow( + profileVal === "" || profileVal === CUSTOM_SENTINEL + ? null + : profileVal, )} -
+ onDraftChange={handleDraftChange} + onToggle={handleToggle} + /> ); })}
diff --git a/apps/web/src/domains/settings/ai/call-site-overrides-row.tsx b/apps/web/src/domains/settings/ai/call-site-overrides-row.tsx new file mode 100644 index 00000000000..37317b32a54 --- /dev/null +++ b/apps/web/src/domains/settings/ai/call-site-overrides-row.tsx @@ -0,0 +1,160 @@ +import { Dropdown } from "@vellum/design-library/components/dropdown"; +import { Toggle } from "@vellum/design-library/components/toggle"; + +import type { CallSiteOverrideDraft } from "@/domains/settings/ai/ai-types"; +import { + INFERENCE_PROVIDERS, + INFERENCE_PROVIDER_DISPLAY_NAMES, +} from "@/domains/settings/ai/ai-types"; +import { + getDefaultModelForProvider, + getModelsForProvider, +} from "@/assistant/llm-model-catalog"; +import { CUSTOM_SENTINEL, isDraftActive } from "@/domains/settings/ai/call-site-helpers"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface ProfileOption { + value: string; + label: string; +} + +export interface CallSiteOverrideRowProps { + id: string; + displayName: string; + description?: string; + defaultProfileLabel: string | null; + draft: CallSiteOverrideDraft | null; + profileOptions: ProfileOption[]; + onDraftChange: (id: string, draft: CallSiteOverrideDraft | null) => void; + onToggle: (id: string, on: boolean) => void; +} + +// --------------------------------------------------------------------------- +// CallSiteOverrideRow +// --------------------------------------------------------------------------- + +export function CallSiteOverrideRow({ + id, + displayName, + description, + defaultProfileLabel, + draft, + profileOptions, + onDraftChange, + onToggle, +}: CallSiteOverrideRowProps) { + const overrideOn = isDraftActive(draft); + + const profileVal = (() => { + if (!draft || !overrideOn) return ""; + if (draft.provider || draft.model) return CUSTOM_SENTINEL; + return draft.profile ?? ""; + })(); + + const isCustom = profileVal === CUSTOM_SENTINEL; + const currentProvider = draft?.provider ?? INFERENCE_PROVIDERS[0]; + const availableModels = getModelsForProvider(currentProvider ?? "anthropic"); + const modelOptions = availableModels.map((m) => ({ + value: m.id, + label: m.displayName, + })); + const hasModelError = !!draft?.provider && !draft?.model; + + function handleProfilePickerChange(val: string) { + if (val === CUSTOM_SENTINEL) { + const defaultProvider = INFERENCE_PROVIDERS[0]; + const defaultModel = getDefaultModelForProvider(defaultProvider) ?? ""; + onDraftChange(id, { profile: null, provider: defaultProvider, model: defaultModel }); + } else if (val === "") { + onDraftChange(id, null); + } else { + onDraftChange(id, { profile: val, provider: null, model: null }); + } + } + + function handleProviderChange(provider: string) { + const defaultModel = getDefaultModelForProvider(provider) ?? ""; + onDraftChange(id, { ...(draft ?? {}), profile: null, provider, model: defaultModel }); + } + + function handleModelChange(model: string) { + onDraftChange(id, { ...(draft ?? {}), model }); + } + + return ( +
+
+
+ {/* typography: off-scale — call-site name uses medium weight for visual hierarchy within card */} +

+ {displayName} +

+ {description && ( +

+ {description} + {defaultProfileLabel && ( + + · Default: {defaultProfileLabel} + + )} +

+ )} +
+
+ {overrideOn && ( + + )} + onToggle(id, on)} + aria-label={`Override ${displayName}`} + /> +
+
+ + {/* Custom provider + model pickers */} + {overrideOn && isCustom && ( +
+
+
+ + ({ + value: p, + label: INFERENCE_PROVIDER_DISPLAY_NAMES[p] ?? p, + }))} + /> +
+
+ + +
+
+ {hasModelError && ( +

+ Pick a model +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/domains/settings/ai/email-managed-content.tsx b/apps/web/src/domains/settings/ai/email-managed-content.tsx index 8ded7461d2a..695544ceb69 100644 --- a/apps/web/src/domains/settings/ai/email-managed-content.tsx +++ b/apps/web/src/domains/settings/ai/email-managed-content.tsx @@ -325,7 +325,7 @@ export function EmailManagedContent({ return (
{subscriptionWarning} -