mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-11 22:51:05 +00:00
persist Nova thread settings
This commit is contained in:
parent
2c6e3af565
commit
8f2cf19e1f
3 changed files with 160 additions and 13 deletions
|
|
@ -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"
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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(
|
||||
() =>
|
||||
|
|
@ -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],
|
||||
[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",
|
||||
})
|
||||
},
|
||||
[persistThreadSettings],
|
||||
)
|
||||
|
||||
const setAttachmentDraftState = useCallback(
|
||||
|
|
@ -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
|
||||
|
|
@ -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([])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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])
|
||||
|
|
@ -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
|
||||
|
|
@ -2112,7 +2184,7 @@ export function ChatSidebar({
|
|||
isStackedInput ? (
|
||||
<ReasoningSelector
|
||||
value={reasoningEffort}
|
||||
onChange={setReasoningEffort}
|
||||
onChange={handleReasoningEffortChange}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
|
|
@ -2120,7 +2192,7 @@ export function ChatSidebar({
|
|||
isStackedInput ? (
|
||||
<SpaceSelector
|
||||
selectedProjects={chatSpaceProjects}
|
||||
onValueChange={setChatSpaceProjects}
|
||||
onValueChange={handleChatSpaceProjectsChange}
|
||||
variant="insideOut"
|
||||
includeAuto
|
||||
hideCount
|
||||
|
|
|
|||
33
apps/web/lib/chat-thread-settings.test.ts
Normal file
33
apps/web/lib/chat-thread-settings.test.ts
Normal file
|
|
@ -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
apps/web/lib/chat-thread-settings.ts
Normal file
42
apps/web/lib/chat-thread-settings.ts
Normal file
|
|
@ -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"
|
||||
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…
Add table
Reference in a new issue