mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-25 08:22:20 +00:00
Co-authored-by: Vorflux AI <249966464+vorflux[bot]@users.noreply.github.com> Co-authored-by: Mahesh Sanikommu <maheshthedev@gmail.com>
567 lines
17 KiB
TypeScript
567 lines
17 KiB
TypeScript
"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<string, string> = {
|
|
"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<string, string> = {
|
|
"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<BrainReasoningEffort, string> = {
|
|
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.",
|
|
},
|
|
{
|
|
role: "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<BrainModelConfig>
|
|
|
|
type PresetDef = {
|
|
id: string
|
|
label: string
|
|
description: string
|
|
build: (
|
|
defaults: BrainModelConfig,
|
|
choices: { main: string[] } & Partial<
|
|
Record<BrainReasoningKey, BrainReasoningEffort[]>
|
|
>,
|
|
) => 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 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 (
|
|
<p
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"font-semibold text-[14px] tracking-[-0.14px] text-[#FAFAFA]",
|
|
)}
|
|
>
|
|
{children}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
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<Partial<BrainModelConfig>>({})
|
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
|
|
|
const updateDraft = (patch: Partial<BrainModelConfig>) => {
|
|
// 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<string, string | undefined> = {}
|
|
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 (
|
|
<section className="flex flex-col gap-4 px-1">
|
|
{showHeading ? (
|
|
<div className="flex flex-col gap-0.5">
|
|
<SectionTitle>Models</SectionTitle>
|
|
<span
|
|
className={cn(dmSans125ClassName(), "text-[12px] text-[#9A9A9A]")}
|
|
>
|
|
Choose which models Company Brain uses. Applies to this organization
|
|
only.
|
|
</span>
|
|
</div>
|
|
) : null}
|
|
|
|
{modelsQuery.isLoading ? (
|
|
<div className="flex items-center gap-2 text-[13px] text-[#9A9A9A]">
|
|
<Loader2 className="size-4 animate-spin" />
|
|
Loading models…
|
|
</div>
|
|
) : modelsQuery.isError ? (
|
|
<p className={cn(dmSans125ClassName(), "text-[13px] text-red-400")}>
|
|
Couldn't load models.
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
{presets.map((preset) => {
|
|
const isActive = preset.id === activePresetId
|
|
return (
|
|
<button
|
|
key={preset.id}
|
|
type="button"
|
|
disabled={disabled}
|
|
aria-pressed={isActive}
|
|
onClick={() => {
|
|
if (!resolved) return
|
|
setDraft({ ...preset.config })
|
|
}}
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"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]",
|
|
)}
|
|
>
|
|
<span className="flex items-center justify-between gap-2">
|
|
<span className="truncate font-semibold text-[14px] tracking-[-0.15px] text-[#FAFAFA]">
|
|
{preset.label}
|
|
{preset.id === "balanced" ? (
|
|
<span className="ml-1.5 text-[11px] font-medium text-[#737B87]">
|
|
Recommended
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
{isActive ? (
|
|
<Check className="size-4 shrink-0 text-[#6BB0FF]" />
|
|
) : null}
|
|
</span>
|
|
<span className="text-[12px] font-medium leading-[1.5] text-[#737373]">
|
|
{preset.description}
|
|
</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setAdvancedOpen((open) => !open)}
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"flex w-fit cursor-pointer items-center gap-1.5 rounded-full px-1 py-1 text-[12px] font-medium text-[#8B929E] transition-colors hover:text-[#FAFAFA]",
|
|
)}
|
|
>
|
|
<ChevronDown
|
|
className={cn(
|
|
"size-3.5 transition-transform",
|
|
(advancedOpen || activePresetId === null) && "rotate-180",
|
|
)}
|
|
/>
|
|
Advanced
|
|
{activePresetId === null ? (
|
|
<span className="text-[11px] text-[#5F6673]">
|
|
· custom settings in use
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
|
|
{advancedOpen || activePresetId === null ? (
|
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
{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<BrainReasoningEffort>([
|
|
"auto",
|
|
...availableEfforts,
|
|
]),
|
|
]
|
|
: availableEfforts.filter((effort) => effort !== "auto"),
|
|
)
|
|
const currentEffort = effortFor(effortKey)
|
|
const isDefault =
|
|
defaults?.[role] === current &&
|
|
(effortOptions.length === 0 ||
|
|
defaults?.[effortKey] === currentEffort)
|
|
return (
|
|
<div
|
|
key={role}
|
|
className="flex min-w-0 flex-col gap-5 rounded-xl bg-[#14161A] p-5 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"truncate font-semibold text-[15px] tracking-[-0.15px] text-[#FAFAFA]",
|
|
)}
|
|
>
|
|
{title}
|
|
</p>
|
|
<p
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"mt-1.5 min-h-[2lh] text-[12px] font-medium leading-[1.55] text-[#737373]",
|
|
)}
|
|
>
|
|
{help}
|
|
</p>
|
|
</div>
|
|
{isDefault ? (
|
|
<span
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"shrink-0 rounded-full border border-white/[0.08] px-2 py-0.5 text-[10px] font-medium text-[#737B87]",
|
|
)}
|
|
>
|
|
Default
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<span className={fieldLabel}>Model</span>
|
|
<Select
|
|
value={current}
|
|
disabled={disabled}
|
|
onValueChange={(v) => updateDraft({ [role]: v })}
|
|
>
|
|
<SelectTrigger className={controlClass}>
|
|
<SelectValue placeholder="Select a model…" />
|
|
</SelectTrigger>
|
|
<SelectContent className={selectContentClass}>
|
|
{options.map((id) => (
|
|
<SelectItem
|
|
key={id}
|
|
value={id}
|
|
className={selectItemClass}
|
|
>
|
|
{labelFor(id)}
|
|
{MODEL_TAGS[id] ? (
|
|
<span className="text-[#737B87]">
|
|
{" "}
|
|
· {MODEL_TAGS[id]}
|
|
</span>
|
|
) : null}
|
|
{defaults?.[role] === id ? " (default)" : ""}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{effortOptions.length > 0 ? (
|
|
<div className="flex flex-col gap-2">
|
|
<span className={fieldLabel}>Thinking depth</span>
|
|
<div className="flex w-full items-center gap-0.5 rounded-full bg-[#0D121A] p-0.5 shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.5)]">
|
|
{effortOptions.map((effort) => {
|
|
const isOn = currentEffort === effort
|
|
return (
|
|
<button
|
|
key={effort}
|
|
type="button"
|
|
aria-pressed={isOn}
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
updateDraft({ [effortKey]: effort })
|
|
}
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"h-7 min-w-0 cursor-pointer whitespace-nowrap rounded-full px-1.5 text-[11.5px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
effort === "xhigh" ? "flex-[1.35]" : "flex-1",
|
|
isOn
|
|
? "bg-white/[0.10] text-[#FAFAFA]"
|
|
: "text-[#8B929E] hover:text-[#FAFAFA]",
|
|
)}
|
|
>
|
|
{EFFORT_LABELS[effort]}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className="px-1.5 text-center text-[10px] font-medium text-[#4A5260]">
|
|
Manual: faster → smarter
|
|
</div>
|
|
<p
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"text-[11px] leading-[1.5] text-[#5F6673]",
|
|
)}
|
|
>
|
|
{role === "main" && currentEffort === "auto"
|
|
? AUTO_EFFORT_HELP
|
|
: effortHelp}
|
|
</p>
|
|
{currentEffort === "xhigh" &&
|
|
extraHighIsBounded(current) ? (
|
|
<p
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"text-[11px] text-amber-300/80",
|
|
)}
|
|
>
|
|
Extra high maps to High for this model provider.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
) : null}
|
|
|
|
{!isAdmin ? (
|
|
<div className="flex items-center gap-1.5 text-[12px] text-[#737373]">
|
|
<Lock className="size-3.5" />
|
|
Only organization admins can change these.
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-end gap-3">
|
|
{dirty ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setDraft({})}
|
|
disabled={update.isPending}
|
|
className={cn(
|
|
dmSans125ClassName(),
|
|
"h-9 rounded-full px-3 text-[13px] font-medium text-[#8B929E] transition-colors hover:text-[#FAFAFA] disabled:opacity-45",
|
|
)}
|
|
>
|
|
Reset
|
|
</button>
|
|
) : null}
|
|
<PillButton
|
|
disabled={disabled || !dirty}
|
|
onClick={() => {
|
|
const patch: Partial<BrainModelConfig> = {}
|
|
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 ? (
|
|
<Loader2 className="size-3.5 animate-spin" />
|
|
) : null}
|
|
Save
|
|
</PillButton>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|