Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dda5f28
dr and blocknote
1shCha Jan 18, 2026
a90333b
first iter of quizzes
1shCha Jan 18, 2026
18259f6
quiz merge
1shCha Jan 19, 2026
9d1ab1c
Merge old_thinkex_merge: quiz tools (modular), PDF support, quiz scro…
1shCha Jan 19, 2026
0c867f2
old-thinkex merge
1shCha Jan 19, 2026
4679f8d
style: cleanup minimized quiz card appearance
1shCha Jan 19, 2026
1da9224
style: adjust vertical resize sensitivity (less sensitive expanded/co…
1shCha Jan 19, 2026
916ccf8
Merge branch 'main' of https://github.com/ThinkEx-OSS/thinkex
1shCha Jan 19, 2026
81380a6
changes
1shCha Jan 19, 2026
ee44f46
fix: replace console logs with debug logger in deep research status r…
urjitc Jan 19, 2026
daca01a
fix(quiz-tool): resolve UI mismatch, add error handling and context flag
urjitc Jan 19, 2026
ea3d9a4
Merge branch 'main' into ot_merge_main_pr
urjitc Jan 19, 2026
c3b14d3
revert: assistant runtime provider to match main
urjitc Jan 19, 2026
97a38ba
fix(update-quiz-ui): add client directive and fix refetch key
urjitc Jan 19, 2026
ae2b112
fix: correct parameter nesting in updateQuiz tool
urjitc Jan 19, 2026
e336a72
refactor: remove emojis and internal markers from context
urjitc Jan 19, 2026
c1a9ef5
fix: improve quiz completion state handling
urjitc Jan 19, 2026
5133435
fix: resolve math parsing edge cases
urjitc Jan 19, 2026
66aab19
fix: allow dollar signs in math expressions
urjitc Jan 19, 2026
c5983c9
fix: resolve duplicate character in regex character class
urjitc Jan 19, 2026
6066574
refactor: split workers.ts into modular files; fix updateQuiz paramet…
urjitc Jan 19, 2026
5002feb
fix: replace loadCurrentState with loadWorkspaceState
urjitc Jan 19, 2026
af9aaff
refactor: remove unused PDF marker detection from quiz worker
urjitc Jan 19, 2026
329b863
chore: merge origin/main; resolve workers.ts conflict by keeping modu…
urjitc Jan 19, 2026
cd86bcf
refactor: replace google-vertex with google generative AI imports
urjitc Jan 19, 2026
9368f9a
chore: remove unused QuizData import
urjitc Jan 19, 2026
31e1248
fix: include sourceCardNames in quizData for downstream consumers
urjitc Jan 19, 2026
bf02699
chore: remove unused imports from common.ts
urjitc Jan 19, 2026
6ece723
fix: skip invalid cards and filter nulls to prevent runtime errors
urjitc Jan 19, 2026
c2cda36
fix: replace ${null}::text with NULL::text to avoid implicit conversion
urjitc Jan 19, 2026
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"ai": "^6.0.5",
"assistant-cloud": "^0.1.12",
"better-auth": "^1.4.10",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.1.1",
Expand Down Expand Up @@ -135,6 +136,7 @@
"@eslint/eslintrc": "^3.3.3",
"@tailwindcss/postcss": "^4.1.18",
"@types/bun": "^1.3.5",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^24.10.4",
"@types/pg": "^8.16.0",
"@types/react": "19.2.2",
Expand Down
1 change: 1 addition & 0 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export async function POST(req: Request) {
userId,
activeFolderId,
clientTools: body.tools,
messages: cleanedMessages, // For quiz context extraction
});

// Stream the response
Expand Down
54 changes: 29 additions & 25 deletions src/app/api/deep-research/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { GoogleGenAI } from "@google/genai";
import { NextRequest, NextResponse } from "next/server";

import { logger } from "@/lib/utils/logger";
// Initialize client
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
if (!apiKey) {
console.error("GOOGLE_GENERATIVE_AI_API_KEY is not set");
}

const client = new GoogleGenAI({
apiKey: apiKey,
});
Comment on lines 5 to 11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Client initialized even when API key is missing.

If GOOGLE_GENERATIVE_AI_API_KEY is not set, the code logs an error but continues to initialize the client with undefined. This will likely cause cryptic errors later when client.interactions.get() is called.

Suggested improvement
 const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
-if (!apiKey) {
-    console.error("GOOGLE_GENERATIVE_AI_API_KEY is not set");
-}
-const client = new GoogleGenAI({
-    apiKey: apiKey,
-});
+if (!apiKey) {
+    throw new Error("GOOGLE_GENERATIVE_AI_API_KEY is not set");
+}
+const client = new GoogleGenAI({ apiKey });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
if (!apiKey) {
console.error("GOOGLE_GENERATIVE_AI_API_KEY is not set");
}
const client = new GoogleGenAI({
apiKey: apiKey,
});
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
if (!apiKey) {
throw new Error("GOOGLE_GENERATIVE_AI_API_KEY is not set");
}
const client = new GoogleGenAI({ apiKey });
🤖 Prompt for AI Agents
In `@src/app/api/deep-research/status/route.ts` around lines 5 - 11, The code logs
when process.env.GOOGLE_GENERATIVE_AI_API_KEY is missing but still constructs a
GoogleGenAI client with apiKey possibly undefined; update the logic around
apiKey and client so you do not initialize the GoogleGenAI instance when apiKey
is falsy — e.g., throw an Error or return a 500/unauthorized response as
appropriate, ensure any downstream use of client (like client.interactions.get)
is guarded, and reference the apiKey variable and GoogleGenAI/client
initialization in route.ts to implement this early-fail behavior.


export const dynamic = 'force-dynamic';

/**
* GET /api/deep-research/status
* Poll endpoint to check the status of a deep research interaction
Expand All @@ -21,45 +18,57 @@ export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
const interactionId = searchParams.get("interactionId");

if (!interactionId) {
return NextResponse.json({ error: "interactionId is required" }, { status: 400 });
}

try {
// Get the interaction without streaming - this returns the current state
const interaction = await client.interactions.get(interactionId);

// DEBUG: Log the entire raw interaction object (optional, kept minimal)
logger.debug('[DEEP-RESEARCH-STATUS] ========== RAW INTERACTION PAYLOAD ==========');
logger.debug('[DEEP-RESEARCH-STATUS] Interaction ID:', interactionId);
logger.debug('[DEEP-RESEARCH-STATUS] State object:', (interaction as any).state);
logger.debug('[DEEP-RESEARCH-STATUS] Status field:', (interaction as any).status);
// Extract status from interaction - check multiple possible properties
const rawStatus = (interaction as any).state?.status ||
(interaction as any).status ||
"unknown";

const rawStatus = (interaction as any).state?.status ||
(interaction as any).status ||
"unknown";
// Collect thoughts and report from events or content
const thoughts: string[] = [];
let report = "";
let error: string | undefined = undefined;

// Try to get content directly first (if API returns full content)
if ((interaction as any).content) {
// PRIMARY: Extract from outputs array (this is where the API actually returns data)
const outputs = (interaction as any).outputs || [];
for (const output of outputs) {
if (output.type === "text" && output.text) {
// This is the final report
report = output.text;
} else if (output.type === "thought" && output.summary) {
// This contains the thought summaries
for (const thought of output.summary) {
if (thought.text && typeof thought.text === "string" && !thoughts.includes(thought.text)) {
thoughts.push(thought.text);
}
}
}
Comment on lines +46 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing array check before iterating output.summary.

Line 48 iterates output.summary assuming it's an array, but no check ensures this. If the API returns a non-array value, this will throw a runtime error.

Additionally, thoughts.includes() is O(n) per call. Consider using a Set for O(1) deduplication if the thoughts list can grow large.

Suggested fix
         } else if (output.type === "thought" && output.summary) {
-            // This contains the thought summaries
-            for (const thought of output.summary) {
-                if (thought.text && typeof thought.text === "string" && !thoughts.includes(thought.text)) {
-                    thoughts.push(thought.text);
+            // This contains the thought summaries
+            if (Array.isArray(output.summary)) {
+                for (const thought of output.summary) {
+                    if (thought.text && typeof thought.text === "string" && !thoughts.includes(thought.text)) {
+                        thoughts.push(thought.text);
+                    }
                 }
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (output.type === "thought" && output.summary) {
// This contains the thought summaries
for (const thought of output.summary) {
if (thought.text && typeof thought.text === "string" && !thoughts.includes(thought.text)) {
thoughts.push(thought.text);
}
}
}
} else if (output.type === "thought" && output.summary) {
// This contains the thought summaries
if (Array.isArray(output.summary)) {
for (const thought of output.summary) {
if (thought.text && typeof thought.text === "string" && !thoughts.includes(thought.text)) {
thoughts.push(thought.text);
}
}
}
}
🤖 Prompt for AI Agents
In `@src/app/api/deep-research/status/route.ts` around lines 46 - 53, The code
assumes output.summary is an array and pushes unique thought.text into the
thoughts array; add a guard using Array.isArray(output.summary) before iterating
and skip if not an array, and replace the O(n) deduplication with an O(1)
Set-based approach (e.g., create a seenThoughts Set or initialize it from
existing thoughts) so the loop in the block handling output.type === "thought"
uses seenThoughts.has()/add() instead of thoughts.includes() to avoid runtime
errors and improve performance.

}
// FALLBACK: Try to get content directly (legacy format)
if (!report && (interaction as any).content) {
const content = (interaction as any).content;
if (typeof content === "string") {
report = content;
} else if (Array.isArray(content)) {
// Content might be an array of content blocks
for (const block of content) {
if (block.type === "text" && block.text) {
report += block.text;
}
}
}
}

// Process events to extract thoughts and report content
// FALLBACK: Process events to extract thoughts and report content (legacy format)
const events = (interaction as any).events || [];
for (const event of events) {
const eventType = event.event_type || event.type;

if (eventType === "content.delta") {
const delta = event.delta || {};
if (delta.type === "text" && delta.text) {
Expand All @@ -72,12 +81,9 @@ export async function GET(req: NextRequest) {
}
} else if (eventType === "interaction.failed" || eventType === "interaction_failed") {
error = event.error?.message || event.error || "Research failed";
} else if (eventType === "interaction.complete" || eventType === "interaction_complete") {
// Research completed
}
}

// Also check for thoughts in other possible locations
// FALLBACK: Also check for thoughts in other possible locations
if ((interaction as any).thoughts && Array.isArray((interaction as any).thoughts)) {
for (const thought of (interaction as any).thoughts) {
const thoughtText = typeof thought === "string" ? thought : thought?.text || thought?.content;
Expand All @@ -86,7 +92,6 @@ export async function GET(req: NextRequest) {
}
}
}

// Map Google's status to our status format
let mappedStatus: "researching" | "complete" | "failed" = "researching";
const statusLower = String(rawStatus).toLowerCase();
Expand All @@ -95,7 +100,6 @@ export async function GET(req: NextRequest) {
} else if (statusLower === "failed" || statusLower === "error" || error) {
mappedStatus = "failed";
}

return NextResponse.json({
status: mappedStatus,
thoughts,
Expand All @@ -105,7 +109,7 @@ export async function GET(req: NextRequest) {
} catch (error: any) {
console.error("[DEEP_RESEARCH_STATUS] Error:", error);
return NextResponse.json(
{
{
error: error?.message || "Failed to retrieve interaction status",
status: "failed" as const,
},
Expand Down
35 changes: 34 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1224,4 +1224,37 @@ body.marquee-selecting iframe {
body:has(.card-detail-modal) .workspace-grid-container {
opacity: 0;
pointer-events: none;
}
}
/* Animated gradient border - rotates colors around the border in a circle */
@keyframes gradient-rotate {
0% { background-position: 0% 0%; }
25% { background-position: 100% 0%; }
50% { background-position: 100% 100%; }
75% { background-position: 0% 100%; }
100% { background-position: 0% 0%; }
}
.gradient-border-animated {
background: linear-gradient(
90deg,
#ef444480,
#eab30880,
#22c55e80,
#3b82f680,
#a855f780,
#ef444480
);
background-size: 200% 200%;
animation: gradient-rotate 10s ease-in-out infinite;
}
/* Ensure markdown content in cards looks good */
.prose {
max-width: none;
}
.prose p {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.prose h1, .prose h2, .prose h3 {
margin-top: 1em;
margin-bottom: 0.5em;
}
Loading