Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
56 changes: 55 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string> {
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
*/
Expand Down Expand Up @@ -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";
Expand Down
13 changes: 12 additions & 1 deletion src/components/assistant-ui/WorkspaceRuntimeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ interface WorkspaceRuntimeProviderProps {
children: React.ReactNode;
}

import { useShallow } from "zustand/react/shallow";

export function WorkspaceRuntimeProvider({
workspaceId,
children
}: WorkspaceRuntimeProviderProps) {

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
Expand Down Expand Up @@ -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,
});

Expand Down
13 changes: 0 additions & 13 deletions src/components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -629,19 +629,6 @@ const Composer: FC<ComposerProps> = ({ 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();
Expand Down