From 9824f0c9cd80408719215648610fbe276574abc6 Mon Sep 17 00:00:00 2001 From: sreedharsreeram <141047751+sreedharsreeram@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:12:45 +0000 Subject: [PATCH] feat(web): configure Company Brain reasoning effort (#1307) ## Stack context Stacks on #1306, which moves Company Brain settings into the revamped Configure page. Backend contract and runtime support: supermemoryai/mono#2581. The UI safely hides any effort controls omitted by an older backend response. ## What changed - Adds independent Low, Medium, High, and Extra high reasoning controls for Main, Triage, and Research. - Saves model and reasoning edits together through the existing partial PATCH. - Keeps controls visible but disabled for non-admin members. - Explains that Extra high maps to High for Grok and GPT providers. ## Validation - `bunx biome check apps/web/hooks/use-brain-models.ts apps/web/components/settings/company-brain-models.tsx` - `bun run build` in `apps/web` - `git diff --check` The standalone web TypeScript command still reports pre-existing unrelated errors outside these files. --- **Session Details** - Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/1c4f3ec7-a536-4105-bbe7-8b19e61f245f) - Requested by: Sreeram Sreedhar (sreeram@supermemory.com) - Address comments on this PR. Add `(aside)` to your comment to have me ignore it. --- apps/web/components/configure-view.tsx | 3 +- .../settings/company-brain-models.tsx | 462 +++++++++++++++--- apps/web/hooks/use-brain-models.ts | 8 +- 3 files changed, 393 insertions(+), 80 deletions(-) diff --git a/apps/web/components/configure-view.tsx b/apps/web/components/configure-view.tsx index 19d9928f6..0b75762e9 100644 --- a/apps/web/components/configure-view.tsx +++ b/apps/web/components/configure-view.tsx @@ -27,7 +27,8 @@ const SECTIONS: { { id: "models", label: "Models", - description: "Choose the models used for reasoning, triage, and research.", + description: + "Pick how fast or thorough your brain should be. Fine-tune each task under Advanced.", icon: Cpu, }, { diff --git a/apps/web/components/settings/company-brain-models.tsx b/apps/web/components/settings/company-brain-models.tsx index 3bded837d..37815ef2c 100644 --- a/apps/web/components/settings/company-brain-models.tsx +++ b/apps/web/components/settings/company-brain-models.tsx @@ -1,7 +1,7 @@ "use client" import { cn } from "@lib/utils" -import { Loader2, Lock } from "lucide-react" +import { Check, ChevronDown, Loader2, Lock } from "lucide-react" import { useMemo, useState } from "react" import { Select, @@ -11,13 +11,17 @@ import { 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", @@ -33,27 +37,143 @@ const MODEL_LABELS: Record = { const labelFor = (id: string) => MODEL_LABELS[id] ?? id -const ROWS: { role: BrainModelRole; title: string; help: string }[] = [ +// 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 = { + low: "Low", + medium: "Medium", + high: "High", + xhigh: "Extra high", +} + +const ROWS: { + role: BrainModelRole + effortKey: BrainReasoningKey + title: string + help: string + effortHelp: string +}[] = [ { role: "main", - title: "Main model", - help: "Reasoning, tool use, and the final Slack answer.", + effortKey: "mainEffort", + title: "Answers", + help: "Writes the replies your brain sends in Slack.", + effortHelp: "Deeper thinking gives better answers but takes longer.", }, { role: "triage", - title: "Triage model", - help: "Decides whether and how the brain replies to a message.", + 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.", }, { role: "research", - title: "Research model", - help: "Grounded web research during company research.", + effortKey: "researchEffort", + title: "Web research", + help: "Looks things up on the web when researching your company.", + effortHelp: "Deeper research per web search, at the cost of speed.", }, ] +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, + research: defaults.research, + mainEffort: pickEffort(choices.mainEffort, "low", "low"), + triageEffort: pickEffort(choices.triageEffort, "low", "low"), + researchEffort: pickEffort(choices.researchEffort, "low", "low"), + }), + }, + { + id: "balanced", + label: "Balanced", + description: "Our recommended mix of speed and answer quality.", + build: (defaults) => ({ + main: defaults.main, + triage: defaults.triage, + research: defaults.research, + mainEffort: defaults.mainEffort ?? "high", + triageEffort: defaults.triageEffort ?? "low", + researchEffort: defaults.researchEffort ?? "high", + }), + }, + { + id: "thorough", + label: "Most thorough", + description: "Deepest answers and research. Slower, uses more credits.", + build: (defaults, choices) => ({ + main: defaults.main, + triage: defaults.triage, + research: defaults.research, + mainEffort: pickEffort(choices.mainEffort, "xhigh", "high"), + triageEffort: pickEffort(choices.triageEffort, "medium", "low"), + researchEffort: pickEffort(choices.researchEffort, "xhigh", "high"), + }), + }, +] + +const CONFIG_KEYS = [ + "main", + "triage", + "research", + "mainEffort", + "triageEffort", + "researchEffort", +] as const + +const extraHighIsBounded = (model: string): boolean => + model.startsWith("grok-") || model.startsWith("gpt-") + const controlClass = cn( dmSans125ClassName(), - "h-9 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] px-3 text-[13px] text-[#FAFAFA] outline-none disabled:opacity-50", + "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(), @@ -86,9 +206,8 @@ export default function CompanyBrainModels({ const modelsQuery = useBrainModels(isCompanyBrain) const update = useUpdateBrainModels() - const [draft, setDraft] = useState>>( - {}, - ) + const [draft, setDraft] = useState>({}) + const [advancedOpen, setAdvancedOpen] = useState(false) const resolved = modelsQuery.data?.resolved const defaults = modelsQuery.data?.defaults @@ -96,14 +215,40 @@ export default function CompanyBrainModels({ 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 }) => draft[role] && draft[role] !== resolved[role], - ) + 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 @@ -133,63 +278,212 @@ export default function CompanyBrainModels({

) : (
- {ROWS.map(({ role, title, help }) => { - const options = choices?.[role] ?? [] - const current = valueFor(role) - return ( -
-
- - {title} - - {defaults?.[role] === current ? ( - - Default - - ) : null} -
- - { + if (!resolved) return + setDraft({ ...preset.config }) + }} className={cn( dmSans125ClassName(), - "text-[12px] text-[#9A9A9A]", + "flex min-w-0 cursor-pointer flex-col gap-1.5 rounded-xl p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50", + "bg-[#14161A] shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", + isActive + ? "bg-[#10161f] ring-1 ring-[#2261CA]/45" + : "hover:bg-[#171A1F]", )} > - {help} - -
- ) - })} + + + {preset.label} + {preset.id === "balanced" ? ( + + Recommended + + ) : null} + + {isActive ? ( + + ) : null} + + + {preset.description} + + + ) + })} +
+ + + + {advancedOpen || activePresetId === null ? ( +
+ {ROWS.map(({ role, effortKey, title, help, effortHelp }) => { + const options = choices?.[role] ?? [] + const current = valueFor(role) + const effortOptions = choices?.[effortKey] ?? [] + 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 ( + + ) + })} +
+
+ Faster + Smarter +
+

+ {effortHelp} +

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

+ Extra high maps to High for this model provider. +

+ ) : null} +
+ ) : null} +
+ ) + })} +
+ ) : null} {!isAdmin ? (
@@ -197,29 +491,43 @@ export default function CompanyBrainModels({ Only organization admins can change these.
) : ( -
- + ) : null} + { - const patch: Partial> = {} - for (const { role } of ROWS) { + 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({}) }) }} - className={cn( - dmSans125ClassName(), - "inline-flex h-8 items-center gap-1.5 rounded-full bg-white px-4 text-[12px] font-medium text-black transition-opacity hover:opacity-90 disabled:opacity-40", - )} > {update.isPending ? ( ) : null} Save - +
)} diff --git a/apps/web/hooks/use-brain-models.ts b/apps/web/hooks/use-brain-models.ts index e8985eee6..95651d52f 100644 --- a/apps/web/hooks/use-brain-models.ts +++ b/apps/web/hooks/use-brain-models.ts @@ -7,13 +7,17 @@ const BACKEND = const BASE = `${BACKEND}/brain/models` export type BrainModelRole = "main" | "triage" | "research" +export type BrainReasoningEffort = "low" | "medium" | "high" | "xhigh" +export type BrainReasoningKey = "mainEffort" | "triageEffort" | "researchEffort" -export type BrainModelConfig = Record +export type BrainModelConfig = Record & + Partial> export type BrainModelsResponse = { resolved: BrainModelConfig defaults: BrainModelConfig - choices: Record + choices: Record & + Partial> } export function useBrainModels(enabled: boolean) {