Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 85 additions & 13 deletions apps/web/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ import {
} from "./attachments"
import { cacheFileBlob, removeCachedFile } from "@/lib/file-cache"
import { ReasoningSelector } from "./reasoning-selector"
import {
type ChatThreadSettings,
readChatThreadSettings,
} from "@/lib/chat-thread-settings"

type ChatMessageSendSource = "typed" | "suggested" | "highlight" | "home"

Expand Down Expand Up @@ -284,7 +288,8 @@ export function ChatSidebar({
const [chatSpaceProjects, setChatSpaceProjects] = useState<string[]>([
initialChatProject ?? AUTO_CHAT_SPACE_ID,
])
const chatProject = chatSpaceProjects[0] ?? selectedProject
const chatProject =
chatSpaceProjects[0] ?? selectedProject ?? AUTO_CHAT_SPACE_ID
const { allProjects } = useContainerTags()
const selectedProjectRef = useRef(chatProject)
selectedProjectRef.current = chatProject
Expand Down Expand Up @@ -348,6 +353,28 @@ export function ChatSidebar({
)
const chatApiBase =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const persistThreadSettings = useCallback(
async (settings: ChatThreadSettings) => {
if (!threadId) return
try {
const response = await fetch(
`${chatApiBase}/chat/threads/${threadId}`,
{
method: "PATCH",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings }),
},
)
if (!response.ok) {
console.error("Failed to persist chat settings", response.status)
}
} catch (error) {
console.error("Failed to persist chat settings", error)
}
},
[chatApiBase, threadId],
)

const chatTransport = useMemo(
() =>
Expand Down Expand Up @@ -471,11 +498,47 @@ export function ChatSidebar({

const handleModelChange = useCallback(
(modelId: ModelId) => {
const nextReasoningEffort = getDefaultReasoningEffort(modelId)
setSelectedModel(modelId)
setReasoningEffort(getDefaultReasoningEffort(modelId))
setReasoningEffort(nextReasoningEffort)
clearError()
void persistThreadSettings({
model: modelId,
projectId: selectedProjectRef.current,
reasoningEffort: nextReasoningEffort,
spaceMode:
selectedProjectRef.current === AUTO_CHAT_SPACE_ID ? "auto" : "manual",
})
},
[clearError, persistThreadSettings],
)

const handleReasoningEffortChange = useCallback(
(nextReasoningEffort: ReasoningEffort) => {
setReasoningEffort(nextReasoningEffort)
void persistThreadSettings({
model: selectedModelRef.current,
projectId: selectedProjectRef.current,
reasoningEffort: nextReasoningEffort,
spaceMode:
selectedProjectRef.current === AUTO_CHAT_SPACE_ID ? "auto" : "manual",
})
},
[persistThreadSettings],
)

const handleChatSpaceProjectsChange = useCallback(
(nextProjects: string[]) => {
const nextProject = nextProjects[0] ?? AUTO_CHAT_SPACE_ID
setChatSpaceProjects([nextProject])
void persistThreadSettings({
model: selectedModelRef.current,
projectId: nextProject,
reasoningEffort: reasoningEffortRef.current,
spaceMode: nextProject === AUTO_CHAT_SPACE_ID ? "auto" : "manual",
})
},
[clearError],
[persistThreadSettings],
)

const setAttachmentDraftState = useCallback(
Expand Down Expand Up @@ -1139,6 +1202,12 @@ export function ChatSidebar({
})
if (response.ok) {
const data = await response.json()
const restoredSettings = readChatThreadSettings(
data.thread?.settings,
data.thread?.space?.containerTag ??
selectedProject ??
AUTO_CHAT_SPACE_ID,
)
const uiMessages = data.messages.map(
(m: {
id: string
Expand All @@ -1165,6 +1234,9 @@ export function ChatSidebar({
pendingResponseModelsRef.current = []
seenAssistantMessageIdsRef.current = new Set()
setResponseModelByMessageId({})
setSelectedModel(restoredSettings.model)
setReasoningEffort(restoredSettings.reasoningEffort)
setChatSpaceProjects([restoredSettings.projectId])
setThreadId(id)
setPendingThreadLoad({ id, messages: uiMessages })
setMessageQueue([])
Expand All @@ -1178,7 +1250,7 @@ export function ChatSidebar({
console.error("Failed to load thread:", error)
}
},
[chatApiBase, setThreadId],
[chatApiBase, selectedProject, setThreadId],
)

// Auto-restore thread from URL on mount (e.g. reload or direct link)
Expand Down Expand Up @@ -1612,11 +1684,11 @@ export function ChatSidebar({
]
for (const t of filtered) {
const ts = new Date(t.updatedAt).getTime()
if (ts >= startOfToday) buckets[0].items.push(t)
else if (ts >= startOfToday - day) buckets[1].items.push(t)
else if (ts >= startOfToday - 7 * day) buckets[2].items.push(t)
else if (ts >= startOfToday - 30 * day) buckets[3].items.push(t)
else buckets[4].items.push(t)
if (ts >= startOfToday) buckets[0]?.items.push(t)
else if (ts >= startOfToday - day) buckets[1]?.items.push(t)
else if (ts >= startOfToday - 7 * day) buckets[2]?.items.push(t)
else if (ts >= startOfToday - 30 * day) buckets[3]?.items.push(t)
else buckets[4]?.items.push(t)
}
return buckets.filter((b) => b.items.length > 0)
}, [threads, historySearch])
Expand Down Expand Up @@ -1894,11 +1966,11 @@ export function ChatSidebar({
/>
<ReasoningSelector
value={reasoningEffort}
onChange={setReasoningEffort}
onChange={handleReasoningEffortChange}
/>
<SpaceSelector
selectedProjects={chatSpaceProjects}
onValueChange={setChatSpaceProjects}
onValueChange={handleChatSpaceProjectsChange}
variant="insideOut"
includeAuto
hideCount
Expand Down Expand Up @@ -2112,15 +2184,15 @@ export function ChatSidebar({
isStackedInput ? (
<ReasoningSelector
value={reasoningEffort}
onChange={setReasoningEffort}
onChange={handleReasoningEffortChange}
/>
) : undefined
}
toolbarEnd={
isStackedInput ? (
<SpaceSelector
selectedProjects={chatSpaceProjects}
onValueChange={setChatSpaceProjects}
onValueChange={handleChatSpaceProjectsChange}
variant="insideOut"
includeAuto
hideCount
Expand Down
33 changes: 33 additions & 0 deletions apps/web/lib/chat-thread-settings.test.ts
Comment thread
ishaanxgupta marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest"
import { AUTO_CHAT_SPACE_ID } from "./chat-auto-space"
import { readChatThreadSettings } from "./chat-thread-settings"

describe("chat thread settings", () => {
it("restores Auto mode independently of the physical default space", () => {
expect(
readChatThreadSettings(
{
model: "gemini-2.5-pro",
projectId: AUTO_CHAT_SPACE_ID,
reasoningEffort: "thinking",
spaceMode: "auto",
},
"sm_project_default",
),
).toEqual({
model: "gemini-2.5-pro",
projectId: AUTO_CHAT_SPACE_ID,
reasoningEffort: "thinking",
spaceMode: "auto",
})
})

it("falls back safely for a legacy manual-space thread", () => {
expect(readChatThreadSettings(undefined, "project_legacy")).toEqual({
model: "grok-4.3",
projectId: "project_legacy",
reasoningEffort: "instant",
spaceMode: "manual",
})
})
})
42 changes: 42 additions & 0 deletions apps/web/lib/chat-thread-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { AUTO_CHAT_SPACE_ID } from "./chat-auto-space"
import {
getDefaultReasoningEffort,
modelNames,
type ModelId,
type ReasoningEffort,
} from "./models"

export type ChatThreadSettings = {
model: ModelId
reasoningEffort: ReasoningEffort
spaceMode: "auto" | "manual"
projectId: string
}

function isModelId(value: unknown): value is ModelId {
return typeof value === "string" && value in modelNames
}

export function readChatThreadSettings(
value: unknown,
fallbackProjectId: string,
): ChatThreadSettings {
const settings =
typeof value === "object" && value !== null
? (value as Record<string, unknown>)
: {}
const model = isModelId(settings.model) ? settings.model : "grok-4.3"
Comment thread
ishaanxgupta marked this conversation as resolved.
const reasoningEffort =
settings.reasoningEffort === "instant" ||
settings.reasoningEffort === "thinking"
? settings.reasoningEffort
: getDefaultReasoningEffort(model)
const spaceMode = settings.spaceMode === "auto" ? "auto" : "manual"
const storedProjectId =
typeof settings.projectId === "string" && settings.projectId.length > 0
? settings.projectId
: fallbackProjectId
const projectId = spaceMode === "auto" ? AUTO_CHAT_SPACE_ID : storedProjectId

return { model, reasoningEffort, spaceMode, projectId }
}
Loading