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
11 changes: 7 additions & 4 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ When a query requires both current data AND conceptual explanation, do both:
2. Use internal knowledge for the conceptual/explanatory component
3. Synthesize into a cohesive answer

YOUTUBE SEARCH GUIDANCE:
If the user asks to "add a youtube video" or "search for a video" but does not provide a specific topic (e.g., "add a video for this workspace"), you MUST inference a relevant search query based on the current workspace context, selected cards, or recent conversation history. Do NOT ask the user for a topic if meaningful context is available. Use the 'searchYoutube' tool directly with your inferred query.

CONFIDENCE THRESHOLD:
If you are uncertain about a fact's accuracy or currency, prefer to search rather than risk providing outdated information.`);

Expand Down Expand Up @@ -219,7 +222,7 @@ export async function POST(req: Request) {
const finalSystemPrompt = systemPromptParts.join('');

// Get model
const modelId = body.modelId || "gemini-3-flash-preview";
const modelId = body.modelId || "gemini-2.5-flash-lite";
const model = google(modelId);

// Create tools using the modular factory
Expand Down Expand Up @@ -256,14 +259,14 @@ export async function POST(req: Request) {
} : undefined,
finishReason,
};

logger.info("📊 [CHAT-API] Final Token Usage:", usageInfo);
},
onStepFinish: (result) => {
// stepType exists in runtime but may not be in type definitions
const stepResult = result as typeof result & { stepType?: "initial" | "continue" | "tool-result" };
const { stepType, usage, finishReason } = stepResult;

if (usage) {
const stepUsageInfo = {
stepType: stepType || 'unknown',
Expand All @@ -280,7 +283,7 @@ export async function POST(req: Request) {
noCacheTokens: (usage as any).inputTokenDetails?.noCacheTokens,
} : undefined,
};

logger.debug(`📊 [CHAT-API] Step Usage (${stepType || 'unknown'}):`, stepUsageInfo);
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/assistant-ui/AssistantPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function CreateFromPromptHandler({

setIsChatExpanded?.(true);

const wrapped = `Create a workspace about: ${createFrom}. Please create notes, flashcards, and a quiz on this topic.`;
const wrapped = `Create a workspace about: ${createFrom}. Please create notes, flashcards, a quiz, and search for YouTube videos on this topic if relevant.`;

let attempts = 0;
const maxAttempts = 12;
Expand Down
128 changes: 68 additions & 60 deletions src/components/assistant-ui/CreateQuizToolUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import type { QuizResult } from "@/lib/ai/tool-result-schemas";
import { parseQuizResult } from "@/lib/ai/tool-result-schemas";

type CreateQuizArgs = {
topic?: string;
difficulty?: "easy" | "medium" | "hard";
contextContent?: string;
sourceCardIds?: string[];
sourceCardNames?: string[];
topic?: string;
difficulty?: "easy" | "medium" | "hard";
contextContent?: string;
sourceCardIds?: string[];
sourceCardNames?: string[];
};

interface CreateQuizReceiptProps {
Expand Down Expand Up @@ -188,60 +188,68 @@ const CreateQuizReceipt = ({ args, result, status, moveItemToFolder, allItems =
};

export const CreateQuizToolUI = makeAssistantToolUI<CreateQuizArgs, QuizResult>({
toolName: "createQuiz",
render: function CreateQuizUI({ args, result, status }) {
const workspaceId = useWorkspaceStore((state) => state.currentWorkspaceId);
const { state: workspaceState } = useWorkspaceState(workspaceId);
const operations = useWorkspaceOperations(workspaceId, workspaceState || initialState);
const workspaceContext = useWorkspaceContext();
const currentWorkspace = workspaceContext.workspaces.find((w) => w.id === workspaceId);

useEffect(() => {
logger.debug("🎯 [CreateQuizTool] Render:", { args, result, status: status?.type });
}, [args, result, status]);

useOptimisticToolUpdate(status, result, workspaceId);

const parsed = result != null ? parseQuizResult(result) : null;

let content: ReactNode = null;

if (parsed?.success) {
content = (
<CreateQuizReceipt
args={args}
result={parsed}
status={status}
moveItemToFolder={operations.moveItemToFolder}
allItems={workspaceState?.items || []}
workspaceName={currentWorkspace?.name || workspaceState?.globalTitle || "Workspace"}
workspaceIcon={currentWorkspace?.icon}
workspaceColor={currentWorkspace?.color}
/>
);
} else if (status.type === "running") {
content = <ToolUILoadingShell label="Generating quiz..." />;
} else if (
(status.type === "incomplete" && status.reason === "error") ||
(status.type === "complete" && parsed && !parsed.success)
) {
content = (
<div className="my-2 flex w-full flex-col overflow-hidden rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950">
<div className="flex items-center gap-2">
<X className="size-4 text-red-600 dark:text-red-400" />
<p className="text-sm font-medium text-red-800 dark:text-red-200">Failed to create quiz</p>
</div>
{parsed && !parsed.success && parsed.message && (
<p className="mt-2 text-xs text-red-700 dark:text-red-300">{parsed.message}</p>
)}
</div>
);
}
toolName: "createQuiz",
render: function CreateQuizUI({ args, result, status }) {
const workspaceId = useWorkspaceStore((state) => state.currentWorkspaceId);
const { state: workspaceState } = useWorkspaceState(workspaceId);
const operations = useWorkspaceOperations(workspaceId, workspaceState || initialState);
const workspaceContext = useWorkspaceContext();
const currentWorkspace = workspaceContext.workspaces.find((w) => w.id === workspaceId);

useEffect(() => {
logger.debug("🎯 [CreateQuizTool] Render:", { args, result, status: status?.type });
}, [args, result, status]);

useOptimisticToolUpdate(status, result, workspaceId);

let parsed: QuizResult | null = null;
try {
parsed = result != null ? parseQuizResult(result) : null;
} catch (err) {
// If we're still running, ignore parsing errors (likely partial data)
if (status.type !== "running") {
throw err;
}
}

return (
<ToolUIErrorBoundary componentName="CreateQuiz">
{content}
</ToolUIErrorBoundary>
);
},
let content: ReactNode = null;

if (parsed?.success) {
content = (
<CreateQuizReceipt
args={args}
result={parsed}
status={status}
moveItemToFolder={operations.moveItemToFolder}
allItems={workspaceState?.items || []}
workspaceName={currentWorkspace?.name || workspaceState?.globalTitle || "Workspace"}
workspaceIcon={currentWorkspace?.icon}
workspaceColor={currentWorkspace?.color}
/>
);
} else if (status.type === "running") {
content = <ToolUILoadingShell label="Generating quiz..." />;
} else if (
(status.type === "incomplete" && status.reason === "error") ||
(status.type === "complete" && parsed && !parsed.success)
) {
content = (
<div className="my-2 flex w-full flex-col overflow-hidden rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950">
<div className="flex items-center gap-2">
<X className="size-4 text-red-600 dark:text-red-400" />
<p className="text-sm font-medium text-red-800 dark:text-red-200">Failed to create quiz</p>
</div>
{parsed && !parsed.success && parsed.message && (
<p className="mt-2 text-xs text-red-700 dark:text-red-300">{parsed.message}</p>
)}
</div>
);
}

return (
<ToolUIErrorBoundary componentName="CreateQuiz">
{content}
</ToolUIErrorBoundary>
);
},
});
Loading