"use client" import { cn } from "@lib/utils" import { Check, ChevronDown, Loader2, Lock } from "lucide-react" import { useMemo, useState } from "react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@ui/components/select" import { type BrainModelConfig, type BrainModelRole, type BrainReasoningEffort, type BrainReasoningKey, useBrainModels, useUpdateBrainModels, } from "@/hooks/use-brain-models" import { useHasCompanyBrain } from "@/hooks/use-company-brain" import { useOrgMemberRole } from "@/hooks/use-org-member-role" import { dmSans125ClassName } from "@/lib/fonts" import { PillButton } from "../integrations/install-steps" const MODEL_LABELS: Record = { "claude-sonnet-5": "Sonnet 5", "claude-opus-4.8": "Opus 4.8", "claude-sonnet-4.6": "Sonnet 4.6", "claude-haiku-4.5": "Haiku 4.5", "grok-4.5": "Grok 4.5", "grok-4.3": "Grok 4.3", "grok-4-fast": "Grok 4 Fast", "gpt-5.6": "GPT-5.6", "gpt-5.5": "GPT-5.5", } const labelFor = (id: string) => MODEL_LABELS[id] ?? id // One-line personality tags so non-experts can tell models apart. const MODEL_TAGS: Record = { "claude-sonnet-5": "balanced", "claude-opus-4.8": "most capable, slower", "claude-sonnet-4.6": "balanced", "claude-haiku-4.5": "fast and light", "grok-4.5": "sharp all-rounder", "grok-4.3": "capable", "grok-4-fast": "fastest", "gpt-5.6": "capable", "gpt-5.5": "capable", } const EFFORT_LABELS: Record = { auto: "Auto", low: "Low", medium: "Medium", high: "High", xhigh: "Extra high", } const EFFORT_ORDER: BrainReasoningEffort[] = [ "auto", "low", "medium", "high", "xhigh", ] const AUTO_EFFORT_HELP = "Automatically adjusts thinking depth for each request." const ROWS: { role: BrainModelRole effortKey: BrainReasoningKey title: string help: string effortHelp: string }[] = [ { role: "main", effortKey: "mainEffort", title: "Answers", help: "Writes the replies your brain sends in Slack.", effortHelp: "Deeper thinking gives better answers but takes longer.", }, { role: "triage", effortKey: "triageEffort", title: "When to reply", help: "Decides whether and how the brain responds to a message.", effortHelp: "Deeper thinking routes messages more carefully but takes longer.", }, ] type FullConfig = Required type PresetDef = { id: string label: string description: string build: ( defaults: BrainModelConfig, choices: { main: string[] } & Partial< Record >, ) => FullConfig } const pickEffort = ( options: BrainReasoningEffort[] | undefined, wanted: BrainReasoningEffort, fallback: BrainReasoningEffort, ): BrainReasoningEffort => !options || options.length === 0 || options.includes(wanted) ? wanted : fallback const PRESETS: PresetDef[] = [ { id: "fast", label: "Fastest", description: "Snappy replies, lighter on credits. Best for quick lookups.", build: (defaults, choices) => ({ main: choices.main.includes("grok-4-fast") ? "grok-4-fast" : defaults.main, triage: defaults.triage, mainEffort: pickEffort(choices.mainEffort, "low", "low"), triageEffort: pickEffort(choices.triageEffort, "low", "low"), }), }, { id: "balanced", label: "Balanced", description: "Our recommended mix of speed and answer quality.", build: (defaults) => ({ main: defaults.main, triage: defaults.triage, mainEffort: defaults.mainEffort ?? "high", triageEffort: defaults.triageEffort ?? "low", }), }, { id: "thorough", label: "Most thorough", description: "Deepest answers. Slower, uses more credits.", build: (defaults, choices) => ({ main: defaults.main, triage: defaults.triage, mainEffort: pickEffort(choices.mainEffort, "xhigh", "high"), triageEffort: pickEffort(choices.triageEffort, "medium", "low"), }), }, ] const CONFIG_KEYS = ["main", "triage", "mainEffort", "triageEffort"] as const const extraHighIsBounded = (model: string): boolean => model.startsWith("grok-") || model.startsWith("gpt-") const orderedEfforts = ( options: BrainReasoningEffort[], ): BrainReasoningEffort[] => [...options].sort((a, b) => EFFORT_ORDER.indexOf(a) - EFFORT_ORDER.indexOf(b)) const controlClass = cn( dmSans125ClassName(), "h-9 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3.5 text-[13px] text-[#FAFAFA] outline-none disabled:opacity-50", ) const fieldLabel = cn( dmSans125ClassName(), "text-[11px] font-medium uppercase tracking-[0.06em] text-[#5B6675]", ) const selectContentClass = cn( dmSans125ClassName(), "rounded-[10px] border-white/[0.08] bg-[#1B1F24] text-[#FAFAFA] shadow-[0px_8px_24px_rgba(0,0,0,0.5)]", ) const selectItemClass = "cursor-pointer rounded-[8px] text-[13px] text-[#FAFAFA] hover:bg-white/10 hover:text-white data-[highlighted]:bg-white/10 data-[highlighted]:text-white focus:bg-white/10 focus:text-white" function SectionTitle({ children }: { children: React.ReactNode }) { return (

{children}

) } export default function CompanyBrainModels({ showHeading = true, }: { showHeading?: boolean }) { const isCompanyBrain = useHasCompanyBrain() const { isAdmin } = useOrgMemberRole(isCompanyBrain) const modelsQuery = useBrainModels(isCompanyBrain) const update = useUpdateBrainModels() const [draft, setDraft] = useState>({}) const [advancedOpen, setAdvancedOpen] = useState(false) const updateDraft = (patch: Partial) => { // A saved custom config opens Advanced automatically. Keep it open if an // edit happens to match a preset again, instead of collapsing mid-click. setAdvancedOpen(true) setDraft((currentDraft) => ({ ...currentDraft, ...patch })) } const resolved = modelsQuery.data?.resolved const defaults = modelsQuery.data?.defaults const choices = modelsQuery.data?.choices const valueFor = (role: BrainModelRole): string => draft[role] ?? resolved?.[role] ?? "" const effortFor = (key: BrainReasoningKey): BrainReasoningEffort | "" => draft[key] ?? resolved?.[key] ?? defaults?.[key] ?? "" const dirty = useMemo(() => { if (!resolved) return false return ROWS.some(({ role, effortKey }) => { const modelChanged = draft[role] !== undefined && draft[role] !== resolved[role] const effortChanged = draft[effortKey] !== undefined && draft[effortKey] !== resolved[effortKey] return modelChanged || effortChanged }) }, [draft, resolved]) const presets = useMemo(() => { if (!defaults || !choices) return [] return PRESETS.map((p) => ({ ...p, config: p.build(defaults, choices) })) }, [defaults, choices]) // Preset whose full config matches what's currently on screen (draft over saved). const activePresetId = useMemo(() => { if (!resolved) return null const current: Record = {} for (const key of CONFIG_KEYS) { current[key] = draft[key] ?? resolved[key] ?? defaults?.[key] } return ( presets.find((p) => CONFIG_KEYS.every((key) => current[key] === p.config[key]), )?.id ?? null ) }, [presets, draft, resolved, defaults]) if (!isCompanyBrain) return null const disabled = !isAdmin || modelsQuery.isLoading || update.isPending return (
{showHeading ? (
Models Choose which models Company Brain uses. Applies to this organization only.
) : null} {modelsQuery.isLoading ? (
Loading models…
) : modelsQuery.isError ? (

Couldn't load models.

) : (
{presets.map((preset) => { const isActive = preset.id === activePresetId return ( ) })}
{advancedOpen || activePresetId === null ? (
{ROWS.map(({ role, effortKey, title, help, effortHelp }) => { const options = choices?.[role] ?? [] const current = valueFor(role) const availableEfforts = choices?.[effortKey] ?? [] const effortOptions = orderedEfforts( role === "main" ? [ ...new Set([ "auto", ...availableEfforts, ]), ] : availableEfforts.filter((effort) => effort !== "auto"), ) const currentEffort = effortFor(effortKey) const isDefault = defaults?.[role] === current && (effortOptions.length === 0 || defaults?.[effortKey] === currentEffort) return (

{title}

{help}

{isDefault ? ( Default ) : null}
Model
{effortOptions.length > 0 ? (
Thinking depth
{effortOptions.map((effort) => { const isOn = currentEffort === effort return ( ) })}
Manual: faster → smarter

{role === "main" && currentEffort === "auto" ? AUTO_EFFORT_HELP : effortHelp}

{currentEffort === "xhigh" && extraHighIsBounded(current) ? (

Extra high maps to High for this model provider.

) : null}
) : null}
) })}
) : null} {!isAdmin ? (
Only organization admins can change these.
) : (
{dirty ? ( ) : null} { const patch: Partial = {} for (const { role, effortKey } of ROWS) { if (draft[role] && draft[role] !== resolved?.[role]) { patch[role] = draft[role] } if ( draft[effortKey] && draft[effortKey] !== resolved?.[effortKey] ) { patch[effortKey] = draft[effortKey] } } update.mutate(patch, { onSuccess: () => setDraft({}) }) }} > {update.isPending ? ( ) : null} Save
)}
)}
) }