Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
b597ffc
quiz changes
1shCha Jan 21, 2026
a7b133d
Merge branch 'ot_merge_main_pr' with quiz update logic fixes
1shCha Jan 21, 2026
1c3b14c
Merge branch 'main' into main_pr_jan20
urjitc Jan 21, 2026
4b2f51e
Merge branch 'main' into main_pr_jan20
urjitc Jan 21, 2026
52dbb27
fix: remove regex that double-escapes LaTeX backslashes in quiz worker
urjitc Jan 21, 2026
4cb7b7e
remove canvas-confetti package and types
urjitc Jan 21, 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
1 change: 1 addition & 0 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,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";

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 | 🟠 Major

Guard debug logs to avoid leaking interaction data in production.

The interaction state can contain sensitive content; logging it (and IDs) is risky if debug logging is enabled in prod. Gate these logs or redact fields.

🔒 Suggested guard + redaction
-        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);
+        if (process.env.NODE_ENV !== "production") {
+            logger.debug('[DEEP-RESEARCH-STATUS] Interaction ID:', interactionId);
+            logger.debug('[DEEP-RESEARCH-STATUS] Status field:', (interaction as any).status);
+        }

Also applies to: 27-31

🤖 Prompt for AI Agents
In `@src/app/api/deep-research/status/route.ts` at line 3, Guard and/or redact
sensitive interaction data before logging: wrap any logger.debug/logger.info
calls that print interaction, interactionState, or IDs in a runtime check that
ensures debug logs only emit outside production (e.g., check
process.env.NODE_ENV !== 'production' or logger.isLevelEnabled('debug')), and
before logging replace or omit sensitive fields (message text, user IDs, request
IDs) with redacted placeholders; update the route handler's logging sites that
reference logger and variables named interaction or interactionState (and the ID
fields logged around lines 27-31) to perform this guard+redaction prior to
calling 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,
});

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";
Comment on lines +33 to +35

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

🧩 Analysis chain

🏁 Script executed:

cat -n src/app/api/deep-research/status/route.ts

Repository: ThinkEx-OSS/thinkex

Length of output: 6444


🌐 Web query:

@google/genai v1.34.0 interactions.get outputs schema output.summary type

💡 Result:

In @google/genai v1.34.0 the interaction output's summary (output.summary) is an array of summary parts — each part is an object with type "summary_text" and a text string (i.e., [{ type: "summary_text", text: "" }, ...]). [1][2]

Sources: SDK/docs for Interactions & output schema. [1] Google GenAI Interactions docs. [2] Community/SDK reference describing output.reasoning/summary structure.


Fix text output accumulation and add defensive type checks for outputs array and summary field.

Line 45 uses assignment (=) instead of concatenation (+=), causing multiple text outputs to overwrite each other and lose content. Additionally, outputs and output.summary lack Array.isArray checks before iteration, making the code fragile to API schema variations.

✅ Safer parsing
-        const outputs = (interaction as any).outputs || [];
+        const outputs = Array.isArray((interaction as any).outputs)
+            ? (interaction as any).outputs
+            : [];
         for (const output of outputs) {
             if (output.type === "text" && output.text) {
                 // This is the final report
-                report = output.text;
+                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);
-                    }
-                }
+                const summaries = Array.isArray(output.summary)
+                    ? output.summary
+                    : [output.summary];
+                for (const thought of summaries) {
+                    const thoughtText =
+                        typeof thought === "string" ? thought : thought?.text || thought?.content;
+                    if (thoughtText && typeof thoughtText === "string" && !thoughts.includes(thoughtText)) {
+                        thoughts.push(thoughtText);
+                    }
+                }
             }
         }
🤖 Prompt for AI Agents
In `@src/app/api/deep-research/status/route.ts` around lines 33 - 35, The code is
overwriting accumulated text outputs and not defensively handling unexpected
shapes for outputs/summary; update the logic that builds the text (where you
currently assign to the accumulator) to concatenate instead (use += style
behavior on the text accumulator) so multiple outputs are appended rather than
replaced, and add guards using Array.isArray on outputs (e.g., the variable
outputs derived from interaction) and check that each output has a string
summary (e.g., output.summary) before reading or iterating it; ensure you also
handle missing or non-array interaction.state/status shapes when reading
rawStatus to avoid runtime errors.

// 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;

@cubic-dev-ai cubic-dev-ai Bot Jan 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The report = output.text assignment overwrites on each iteration. If multiple text outputs exist in the outputs array, only the last one is kept. Consider using report += output.text for consistency with other extraction paths, or add a break after assignment if only one text output is expected.

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

<comment>The `report = output.text` assignment overwrites on each iteration. If multiple text outputs exist in the `outputs` array, only the last one is kept. Consider using `report += output.text` for consistency with other extraction paths, or add a `break` after assignment if only one text output is expected.</comment>

<file context>
@@ -21,45 +18,57 @@ export const dynamic = 'force-dynamic';
+        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
</file context>
Fix with Cubic

} 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);
}
}
}
}
// 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 @@ -1239,4 +1239,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;
}
Comment on lines +1243 to +1263

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

Remove duplicate gradient border definitions.

The @keyframes gradient-rotate and .gradient-border-animated are already defined earlier in this file (lines 586–616). These identical declarations should be removed to avoid maintenance confusion.

🧹 Suggested fix: remove duplicate definitions
-/* 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;
-}
📝 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
/* 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;
}
🤖 Prompt for AI Agents
In `@src/app/globals.css` around lines 1243 - 1263, The file contains duplicate
declarations for the `@keyframes` gradient-rotate and the
.gradient-border-animated class; remove this later duplicate block so only the
original definitions remain (keep the earlier definitions already declared
around gradient-rotate and .gradient-border-animated and delete the repeated
declarations in this later section), and verify no other styles rely on a
different variant name so references to gradient-rotate and
.gradient-border-animated remain unchanged.

/* 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