Skip to content
71 changes: 48 additions & 23 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,15 @@ function getSelectedCardsContext(body: any): string {
return body.selectedCardsContext || "";
}

// Regex to detect createFrom auto-generated prompts
const CREATE_FROM_REGEX = /^Update the preexisting contents of this workspace to be about (.+)\. Only add one quality YouTube video\.$/;
// Regex to detect createFrom auto-generated prompts (YouTube suffix is optional for custom type selections)
const CREATE_FROM_REGEX = /^Update the preexisting contents of this workspace to be about (.+)\.(?:\s*Only add one quality YouTube video\.)?$/;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The greedy capture in the create-from regex causes the topic to include the optional YouTube sentence, so generated prompts will treat that suffix as part of the topic. Use a non-greedy capture to stop at the first period.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/api/chat/route.ts, line 99:

<comment>The greedy capture in the create-from regex causes the topic to include the optional YouTube sentence, so generated prompts will treat that suffix as part of the topic. Use a non-greedy capture to stop at the first period.</comment>

<file context>
@@ -96,7 +96,7 @@ function getSelectedCardsContext(body: any): string {
 
 // Regex to detect createFrom auto-generated prompts (YouTube suffix is optional for custom type selections)
-const CREATE_FROM_REGEX = /^Update the preexisting contents of this workspace to be about (.+?)\.(?:\s*Only add one quality YouTube video\.)?$/;
+const CREATE_FROM_REGEX = /^Update the preexisting contents of this workspace to be about (.+)\.(?:\s*Only add one quality YouTube video\.)?$/;
 
 /**
</file context>
Suggested change
const CREATE_FROM_REGEX = /^Update the preexisting contents of this workspace to be about (.+)\.(?:\s*Only add one quality YouTube video\.)?$/;
const CREATE_FROM_REGEX = /^Update the preexisting contents of this workspace to be about (.+?)\.(?:\s*Only add one quality YouTube video\.)?$/;
Fix with Cubic


/**
* Detect if the first user message is a createFrom auto-generated prompt
* and return additional system instructions for better workspace curation
* and return additional system instructions for better workspace curation.
* When genTypes is provided, only instructions for those types are included.
*/
function getCreateFromSystemPrompt(messages: any[]): string | null {
function getCreateFromSystemPrompt(messages: any[], genTypes: string[] | null): string | null {
// Find the first user message
const firstUserMessage = messages.find((m) => m.role === "user");
if (!firstUserMessage) return null;
Expand All @@ -121,29 +122,52 @@ function getCreateFromSystemPrompt(messages: any[]): string | null {
if (!match) return null;

const topic = match[1];
const allTypes = ['note', 'quiz', 'flashcard', 'youtube'] as const;
const validTypes = genTypes ? genTypes.filter(t => allTypes.includes(t as any)) : null;
const types = new Set(validTypes && validTypes.length > 0 ? validTypes : allTypes);

// Build type-specific update instructions — always include title param
const updateInstructions: string[] = [];
Comment on lines 124 to 130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid genTypes yields no-op

getCreateFromSystemPrompt() treats any non-null genTypes as authoritative (const types = genTypes ? new Set(genTypes) : ...). If body.genTypes is present but contains no recognized values (e.g. genTypes=foo,bar or an empty/filtered client value), updateInstructions becomes empty and the prompt still adds the “Do NOT create or update any other item types” restriction, which can leave the model with no valid update actions to take during create-from initialization. Consider validating/filtering genTypes to the allowed set and falling back to default types when the recognized set is empty.

if (types.has('note')) updateInstructions.push(` - Use \`updateNote\` with noteName "Update me" and a descriptive \`title\` (e.g. "${topic} Notes")`);
if (types.has('flashcard')) updateInstructions.push(` - Use \`updateFlashcards\` with deckName "Update me" and a descriptive \`title\` (e.g. "${topic} Flashcards")`);
if (types.has('quiz')) updateInstructions.push(` - Use \`updateQuiz\` with quizName "Update me" and a descriptive \`title\` (e.g. "${topic} Quiz")`);

// Build type-specific quality guidelines
const qualityGuidelines: string[] = [];
if (types.has('note')) qualityGuidelines.push('- For notes: add a comprehensive summary of the topic');
if (types.has('flashcard')) qualityGuidelines.push('- For flashcards: create exactly 5 meaningful question/answer pairs covering key concepts');
if (types.has('quiz')) qualityGuidelines.push('- For quizzes: create challenging but fair questions that test understanding');

// YouTube section only if selected
let youtubeSection = '';
if (types.has('youtube')) {
youtubeSection = `
YOUTUBE VIDEO:
- Search with specific, relevant terms for the topic
- Prefer videos that are educational/explanatory
- Look for high view counts and reputable channels as quality signals`;
}

// Build numbered instructions dynamically to avoid numbering gaps
const instructions: string[] = [];
let instrNum = 1;
if (updateInstructions.length > 0) {
instructions.push(`${instrNum++}. **Update ONLY these workspace items** about the topic:\n${updateInstructions.join('\n')}`);
}
instructions.push(`${instrNum++}. **Be thorough but focused** - Provide a solid foundation for understanding the topic without being overwhelming.`);
if (validTypes && validTypes.length > 0) {
instructions.push(`${instrNum++}. **Do NOT create or update any other item types** beyond what is listed above.`);
}
instructions.push(`${instrNum++}. **Do NOT ask the user questions** - This is an automated initialization, proceed directly with updating the workspace.`);

return `
CREATE-FROM WORKSPACE INITIALIZATION MODE:
This is an automatic workspace initialization request. The user wants to transform this workspace into a curated learning/research space
ace about: "${topic}"
This is an automatic workspace initialization request. The user wants to transform this workspace into a curated learning/research space about: "${topic}"

CRITICAL INSTRUCTIONS FOR WORKSPACE CURATION:
1. **For each of the existing workspace items** update the title and content to be about the topic:
- Use \`updateNote\` tool for notes
- Use \`updateFlashcards\` tool for flashcard sets
- Use \`updateQuiz\` tool for quizzes
2. **Be thorough but focused** - Provide a solid foundation for understanding the topic without being overwhelming.
3. **Do NOT ask the user questions** - This is an automated initialization, proceed directly with updating the workspace.

QUALITY GUIDELINES FOR CONTENT:
- For notes: add a comprehensive summary of the topic
- For flashcards: create exactly 5 meaningful question/answer pairs covering key concepts
- For quizzes: create challenging but fair questions that test understanding

QUALITY GUIDELINES FOR THE YOUTUBE VIDEO:
- Search with specific, relevant terms for the topic
- Prefer videos that are educational/explanatory
- Look for high view counts and reputable channels as quality signals
${instructions.join('\n')}

${qualityGuidelines.length > 0 ? `QUALITY GUIDELINES FOR CONTENT:\n${qualityGuidelines.join('\n')}` : ''}${youtubeSection}
`;
}

Expand Down Expand Up @@ -224,7 +248,8 @@ export async function POST(req: Request) {
];

// Inject createFrom workspace initialization prompt if detected
const createFromPrompt = getCreateFromSystemPrompt(cleanedMessages);
const genTypes: string[] | null = body.genTypes ? body.genTypes.split(',').map((t: string) => t.trim()).filter(Boolean) : null;
const createFromPrompt = getCreateFromSystemPrompt(cleanedMessages, genTypes);
Comment on lines +251 to +252
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

genTypes values are not validated against the known set of types.

body.genTypes comes from the client and is split/trimmed but never validated. Arbitrary strings pass through to getCreateFromSystemPrompt, where they silently miss every types.has(...) check, producing a prompt with empty instructions and guidelines sections. Consider filtering against an allowlist server-side:

Proposed fix
-    const genTypes: string[] | null = body.genTypes ? body.genTypes.split(',').map((t: string) => t.trim()).filter(Boolean) : null;
+    const ALLOWED_GEN_TYPES = new Set(['note', 'quiz', 'flashcard', 'youtube']);
+    const genTypes: string[] | null = body.genTypes
+      ? body.genTypes.split(',').map((t: string) => t.trim()).filter((t: string) => ALLOWED_GEN_TYPES.has(t))
+      : null;
+    // Fall back to null if filtering removed all entries
+    const validGenTypes = genTypes && genTypes.length > 0 ? genTypes : null;
🤖 Prompt for AI Agents
In `@src/app/api/chat/route.ts` around lines 251 - 252, Validate and sanitize
body.genTypes before passing to getCreateFromSystemPrompt by applying an
allowlist of known generation types: split and trim as you already do for
genTypes, then filter each token against the server-side allowed set (e.g.,
allowedGenTypes or the same set used inside getCreateFromSystemPrompt, using a
consistent normalization like toLowerCase()), discard unknown values, and set
genTypes to null if the resulting array is empty; update the use-site where
genTypes is declared and where getCreateFromSystemPrompt(cleanedMessages,
genTypes) is called so only validated/allowed types reach
getCreateFromSystemPrompt.

if (createFromPrompt) {
systemPromptParts.push(`\n\n${createFromPrompt}`);
}
Expand Down
40 changes: 30 additions & 10 deletions src/components/assistant-ui/AssistantPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,19 @@ function CreateFromPromptHandler({
const timeoutIdsRef = useRef<ReturnType<typeof setTimeout>[]>([]);

const createFrom = searchParams.get("createFrom");
const genTypes = searchParams.get("genTypes");

useEffect(() => {
if (!createFrom || !workspaceId || isLoading || hasAutoSentRef.current) return;

setIsChatExpanded?.(true);

const wrapped = `Update the preexisting contents of this workspace to be about ${createFrom}. Only add one quality YouTube video.`;
// Build type-aware message: only mention YouTube if selected (or auto mode)
const types = genTypes ? genTypes.split(',').map(s => s.trim()).filter(Boolean) : null;
const includeYoutube = !types || types.includes('youtube');
const wrapped = includeYoutube
? `Update the preexisting contents of this workspace to be about ${createFrom}. Only add one quality YouTube video.`
: `Update the preexisting contents of this workspace to be about ${createFrom}.`;

let attempts = 0;
const maxAttempts = 12;
Expand All @@ -152,6 +158,7 @@ function CreateFromPromptHandler({
clearAll();
const url = new URL(window.location.href);
url.searchParams.delete("createFrom");
url.searchParams.delete("genTypes");
router.replace(url.pathname + url.search);
return;
} catch {
Expand All @@ -168,7 +175,7 @@ function CreateFromPromptHandler({
ids.push(id);

return () => clearAll();
}, [createFrom, workspaceId, isLoading, aui, router, setIsChatExpanded]);
}, [createFrom, genTypes, workspaceId, isLoading, aui, router, setIsChatExpanded]);

return null;
}
Expand All @@ -189,19 +196,31 @@ function GenerateStudyMaterialsHandler({
const timeoutIdsRef = useRef<ReturnType<typeof setTimeout>[]>([]);

const action = searchParams.get("action");
const genTypes = searchParams.get("genTypes");

useEffect(() => {
if (action !== "generate_study_materials" || !workspaceId || isLoading || hasAutoSentRef.current) return;

setIsChatExpanded?.(true);

const prompt = `First, process any PDF files in this workspace.

Then, using the content:
1. Update the note with a comprehensive summary
2. Update the quiz with 5-10 relevant questions
3. Update the flashcards with key terms and concepts
4. Search and add one relevant YouTube video if possible`;
// Build type-aware prompt: only include steps for selected types
const types = genTypes ? new Set(genTypes.split(',').map(s => s.trim()).filter(Boolean)) : null;
const steps: string[] = [];
let n = 1;
if (!types || types.has('note')) steps.push(`${n++}. Update the note with a comprehensive summary`);
if (!types || types.has('quiz')) steps.push(`${n++}. Update the quiz with 5-10 relevant questions`);
if (!types || types.has('flashcard')) steps.push(`${n++}. Update the flashcards with key terms and concepts`);
if (!types || types.has('youtube')) steps.push(`${n++}. Search and add one relevant YouTube video if possible`);

// If genTypes was set but contained no recognized values, fall back to all types
if (steps.length === 0) {
steps.push('1. Update the note with a comprehensive summary');
steps.push('2. Update the quiz with 5-10 relevant questions');
steps.push('3. Update the flashcards with key terms and concepts');
steps.push('4. Search and add one relevant YouTube video if possible');
}
Comment on lines +206 to +221
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid genTypes not detected

GenerateStudyMaterialsHandler treats any non-empty genTypes entries as recognized and builds steps off types.has(...). For ?genTypes=foo,bar (or whitespace variants), types is non-null and steps becomes empty, which triggers the fallback-to-all path, but CreateFromPromptHandler does not have an equivalent fallback: it will omit the YouTube suffix because types is non-null and includes('youtube') is false. That makes the first user message not match CREATE_FROM_REGEX (server still expects a trailing period) and getCreateFromSystemPrompt won’t inject the initialization instructions. Net effect: createFrom auto-init can silently lose the server-side system prompt when genTypes contains only invalid values.


const prompt = `First, process any PDF files in this workspace.\n\nThen, using the content:\n${steps.join('\n')}`;
Comment on lines 206 to 223
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

genTypes parsing misses whitespace

genTypes.split(',') isn’t trimmed before being put into the Set. A query like ?genTypes=note, quiz will produce ' quiz' which won’t match types.has('quiz'), dropping steps unexpectedly and potentially triggering the “fallback to all” path. Trimming each entry (and filtering empties) when parsing would keep the handler consistent with the server-side parsing in route.ts.


let attempts = 0;
const maxAttempts = 12;
Expand All @@ -224,6 +243,7 @@ Then, using the content:
clearAll();
const url = new URL(window.location.href);
url.searchParams.delete("action");
url.searchParams.delete("genTypes");
router.replace(url.pathname + url.search);
return;
} catch {
Expand All @@ -240,7 +260,7 @@ Then, using the content:
ids.push(id);

return () => clearAll();
}, [action, workspaceId, isLoading, aui, router, setIsChatExpanded]);
}, [action, genTypes, workspaceId, isLoading, aui, router, setIsChatExpanded]);

return null;
}
Expand Down
9 changes: 8 additions & 1 deletion src/components/assistant-ui/WorkspaceRuntimeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { AssistantCloud, AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/react-ai-sdk";
import { useMemo, useCallback } from "react";
import { useSearchParams } from "next/navigation";
import { AssistantAvailableProvider } from "@/contexts/AssistantAvailabilityContext";
import { useUIStore } from "@/lib/stores/ui-store";
import { useSession } from "@/lib/auth-client";
Expand Down Expand Up @@ -38,6 +39,11 @@ export function WorkspaceRuntimeProvider({
const selectedCardIdsSet = useUIStore((state) => state.selectedCardIds);
const selectedActions = useUIStore((state) => state.selectedActions);
const replySelections = useUIStore(useShallow((state) => state.replySelections));
const searchParams = useSearchParams();
// Only pass genTypes during auto-init flows (createFrom / generate_study_materials)
// to avoid leaking it into every subsequent chat request
const hasAutoInit = searchParams.has("createFrom") || searchParams.get("action") === "generate_study_materials";
const genTypes = hasAutoInit ? searchParams.get("genTypes") : null;
const { data: session } = useSession();
Comment on lines 40 to 47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaking URL genTypes

WorkspaceRuntimeProvider always reads genTypes from the URL and sends it in every /api/chat request body (body.genTypes), but AssistantPanel deletes genTypes from the URL only when it auto-sends createFrom/action prompts. If the user opens a workspace URL containing genTypes without those params (or after manual navigation), genTypes will persist and unintentionally constrain all future chats (server-side route.ts will restrict create-from instructions based on it). Consider scoping genTypes to only the auto-init flows (e.g., only attach it when createFrom/action=generate_study_materials is present, or remove it from the URL on load even when no auto-send triggers).


// Get workspace state to format selected cards context on client
Expand Down Expand Up @@ -147,13 +153,14 @@ export function WorkspaceRuntimeProvider({
selectedCardsContext, // Pre-formatted context (client-side) instead of IDs
replySelections,
selectedActions,
genTypes,
},
headers: {
// Headers for static context if needed
},
});
return transport;
}, [workspaceId, selectedModelId, activeFolderId, selectedCardsContext, replySelections, selectedActions]),
}, [workspaceId, selectedModelId, activeFolderId, selectedCardsContext, replySelections, selectedActions, genTypes]),
adapters: {
attachments: new SupabaseAttachmentAdapter(),
},
Expand Down
Loading