diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 20438e84..05b6758b 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -4,6 +4,8 @@ import { logger } from "@/lib/utils/logger"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { createChatTools } from "@/lib/ai/tools"; +import { loadWorkspaceState } from "@/lib/workspace/state-loader"; +import { formatSelectedCardsContext } from "@/lib/utils/format-workspace-context"; /** * Extract workspaceId from system context or request body @@ -101,6 +103,39 @@ function cleanMessages(messages: any[]): any[] { }); } +/** + * Build selected cards context from card IDs + * Fetches workspace state and formats the selected cards for the system prompt + */ +async function buildSelectedCardsContext( + workspaceId: string | null, + selectedCardIds: string[] +): Promise { + if (!workspaceId || !selectedCardIds || selectedCardIds.length === 0) { + return ""; + } + + try { + const state = await loadWorkspaceState(workspaceId); + const selectedItems = state.items.filter((item) => + selectedCardIds.includes(item.id) + ); + + if (selectedItems.length === 0) { + return ""; + } + + return formatSelectedCardsContext(selectedItems, state.items); + } catch (error) { + logger.error("❌ [CHAT-API] Failed to load selected cards context:", { + error: error instanceof Error ? error.message : String(error), + workspaceId, + selectedCardIds, + }); + return ""; + } +} + /** * Build the enhanced system prompt with guidelines and detection hints */ @@ -188,8 +223,27 @@ export async function POST(req: Request) { // Clean messages const cleanedMessages = cleanMessages(convertedMessages); + // Build selected cards context + const selectedCardIds = body.selectedCardIds || []; + const selectedCardsContext = await buildSelectedCardsContext(workspaceId, selectedCardIds); + // Build system prompt - const finalSystemPrompt = buildSystemPrompt(system, fileUrls, urlContextUrls); + let finalSystemPrompt = buildSystemPrompt(system, fileUrls, urlContextUrls); + + // Inject selected cards context if available + if (selectedCardsContext) { + finalSystemPrompt = `${finalSystemPrompt}\n\n${selectedCardsContext}`; + } + + // Inject reply context if available + const replySelections = body.replySelections || []; + if (replySelections.length > 0) { + const replyContext = replySelections + .map((sel: { text: string }) => `> ${sel.text}`) + .join("\n"); + + finalSystemPrompt = `${finalSystemPrompt}\n\nREPLY CONTEXT:\nThe user is replying specifically to these parts of the previous message:\n${replyContext}`; + } // Get model const modelId = body.modelId || "gemini-2.5-pro"; diff --git a/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx b/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx index ff7bf515..4f35b28a 100644 --- a/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx +++ b/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx @@ -14,6 +14,8 @@ interface WorkspaceRuntimeProviderProps { children: React.ReactNode; } +import { useShallow } from "zustand/react/shallow"; + export function WorkspaceRuntimeProvider({ workspaceId, children @@ -21,6 +23,13 @@ export function WorkspaceRuntimeProvider({ const selectedModelId = useUIStore((state) => state.selectedModelId); const activeFolderId = useUIStore((state) => state.activeFolderId); + /* + FIX for "Maximum update depth exceeded": + We select the Set directly because Array.from() inside the selector creates a new reference on every render, + triggering an infinite update loop. The Set reference is stable until changed. + */ + const selectedCardIdsSet = useUIStore((state) => state.selectedCardIds); + const replySelections = useUIStore(useShallow((state) => state.replySelections)); const { data: session } = useSession(); // Create AssistantCloud instance - use anonymous mode for anonymous users @@ -100,13 +109,15 @@ export function WorkspaceRuntimeProvider({ workspaceId, modelId: selectedModelId, activeFolderId, + selectedCardIds: Array.from(selectedCardIdsSet), + replySelections, }, headers: { // Headers for static context if needed }, }); return transport; - }, [workspaceId, selectedModelId, activeFolderId]), + }, [workspaceId, selectedModelId, activeFolderId, selectedCardIdsSet, replySelections]), onError: handleChatError, }); diff --git a/src/components/assistant-ui/thread.tsx b/src/components/assistant-ui/thread.tsx index 4251e666..b9a4c0e0 100644 --- a/src/components/assistant-ui/thread.tsx +++ b/src/components/assistant-ui/thread.tsx @@ -629,19 +629,6 @@ const Composer: FC = ({ items }) => { // Combine all context: selected cards, reply texts, and user message let modifiedText = currentText; - // Add selected cards context with markers (no UI representation, but sent to LLM) - // Always append this so LLM knows the selection state (even when nothing is selected) - const cardsContext = formatSelectedCardsContext(selectedItems, items); - const cardsMarker = "[[SELECTED_CARDS_MARKER]]"; - modifiedText = modifiedText + `\n\n${cardsMarker}${cardsContext}${cardsMarker}`; - - // Add reply texts with pipe separator if there are replies - if (replySelections.length > 0) { - const replyTexts = replySelections.map(sel => sel.text).join('|'); - const specialMarker = "[[REPLY_MARKER]]"; - modifiedText = modifiedText + `\n\n${specialMarker}${replyTexts}${specialMarker}`; - } - // Set the modified text and send api.composer().setText(modifiedText); api.composer().send();