feat(ai): implement follow-up chat workflow with external AI continuation#55
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe AI workspace API is refactored into distinct "topic" and "followup" modes, backed by new Redis-cached topic service and follow-up chat service. New utilities support handing off conversations to external AI providers (ChatGPT, Gemini, Claude, DeepSeek, Grok). The workspace chat UI, AI page, and theming system (custom provider replacing next-themes) are updated accordingly. ChangesAI workspace topic/followup pipeline
Custom theme provider and layout toaster
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant WorkspaceChat
participant Route as api/ai/workspace
participant TopicService as generateTopicAnswer
participant Cache as topic-cache
participant Gemini as GoogleGenAI
User->>WorkspaceChat: open topic page
WorkspaceChat->>Route: request (topicId, action EXPLAIN)
Route->>TopicService: generateTopicAnswer(input)
TopicService->>Cache: getTopicCache(topicId)
alt cache hit
Cache-->>TopicService: cached answer
else cache miss
TopicService->>Gemini: generateWithRetry(prompt)
Gemini-->>TopicService: answer
TopicService->>Cache: saveTopicCache(answer)
end
TopicService-->>Route: { answer, cached }
Route-->>WorkspaceChat: mode topic, cachedExplanation
sequenceDiagram
participant User
participant WorkspaceChat
participant Route as api/ai/workspace
participant FollowupService as generateFollowupAnswer
participant ContinueExternalAI
User->>WorkspaceChat: submit follow-up question
WorkspaceChat->>Route: POST question, cachedExplanation, messages
Route->>FollowupService: generateFollowupAnswer(input)
FollowupService-->>Route: { answer }
Route-->>WorkspaceChat: mode followup, answer
WorkspaceChat-->>User: render answer, increment follow-up count
Note over WorkspaceChat: limit reached
WorkspaceChat->>ContinueExternalAI: render with conversation
User->>ContinueExternalAI: click provider
ContinueExternalAI-->>User: copy prompt, open external AI
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎉 Congratulations @imuniqueshiv!Thank you for contributing to HyperLearningTech. Your pull request has been successfully merged into main. 📦 Merge Summary
🚀 Keep Contributing
Thank you for helping make HyperLearningTech better. Happy Coding! 🚀 |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (7)
lib/ai/external-ai.ts (2)
34-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win"Ctrl + V" instruction doesn't apply to macOS users.
The success toast description hardcodes
Ctrl + V, but Mac users paste withCmd + V. Consider a platform-agnostic phrasing like "paste to continue."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai.ts` around lines 34 - 41, The success toast in getExternalAISuccessToast hardcodes a Windows-specific paste shortcut, which is not correct for macOS users. Update the description text to use platform-agnostic wording instead of “Ctrl + V,” or otherwise make it dynamic based on the user platform, while keeping the rest of the toast content in ExternalAIProvider and EXTERNAL_AI_LABELS unchanged.
68-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
continueWithExternalAIappears unused and diverges from the actual UI flow.
components/ai/continue-external-ai.tsxreimplements this copy+open+toast flow manually (withEXTERNAL_AI_OPEN_DELAY_MSand toasts) instead of calling this helper, and this helper itself doesn't apply the delay or surface toast info. This looks like dead/duplicated logic that will drift further from the real behavior over time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai.ts` around lines 68 - 75, `continueWithExternalAI` is duplicating the copy/open flow and is out of sync with the real UI behavior. Update the actual continuation flow to use this helper, or remove the helper if the component-level implementation is the source of truth; in either case make sure the shared path includes the existing delay and toast behavior currently handled around `EXTERNAL_AI_OPEN_DELAY_MS` in `continueWithExternalAI` and the continue-external-ai component logic. Align the copy-to-clipboard, open-external-ai, and toast sequencing so there is one canonical implementation instead of two drifting versions.lib/ai/external-ai-prompt-builder.ts (1)
12-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePositional filtering of the cached explanation is fragile.
formatConversationdrops the message atindex === 0only if it's an assistant message, relying onWorkspaceChatalways seedingcachedExplanationas the very first message. This works today (percomponents/ai/workspace-chat.tsx'smessagesinitializer), but is an implicit cross-file contract with no compile-time guarantee — if the seeding logic ever changes (e.g., a system message is prepended), this will silently start dropping/keeping the wrong message. Consider filtering by a stable identifier (e.g., excluding messages by id/marker) rather than position.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai-prompt-builder.ts` around lines 12 - 27, formatConversation currently removes the first assistant message by position, which is fragile because it depends on WorkspaceChat seeding cachedExplanation as index 0. Update formatConversation in external-ai-prompt-builder.ts to exclude the cached explanation using a stable identifier or marker on ChatMessage instead of relying on index === 0, and adjust the WorkspaceChat message creation path so the explanation is tagged in a way formatConversation can reliably filter it out.components/ai/continue-external-ai.tsx (1)
56-74: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo guard against rapid/duplicate clicks during the async copy.
handleContinueis async but the buttons stay enabled throughout, so multiple quick clicks (same or different providers) can queue severalsetTimeout-delayedwindow.opencalls and stacked toasts. Consider tracking an in-flight/loading state to disable the buttons during the copy operation.♻️ Proposed guard
+ const [pendingProvider, setPendingProvider] = useState<ExternalAIProvider | null>(null); + const handleContinue = async (provider: ExternalAIProvider) => { + if (pendingProvider) return; + setPendingProvider(provider); const copied = await copyToClipboard(prompt); + setPendingProvider(null); ...🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/ai/continue-external-ai.tsx` around lines 56 - 74, The async handleContinue flow in continue-external-ai.tsx has no in-flight guard, so rapid clicks can trigger duplicate copy attempts, stacked toasts, and multiple delayed opens via openExternalAI. Add a loading/in-progress state around handleContinue and use it to disable the ExternalAIProvider buttons while copyToClipboard(prompt) is running, then clear the state in a finally block so both success and error paths re-enable the UI consistently.lib/ai/topic-cache.ts (1)
55-73: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winNo TTL on cached topic entries.
saveTopicCachewrites to Redis with no expiration, so cache entries persist indefinitely (only removed via explicitforceRefresh/deleteTopicCache). Consider adding a TTL (e.g.redis.set(key, value, { ex: SECONDS })) to bound storage growth as more topics get cached over time.♻️ Proposed fix
try { - await redis.set(buildCacheKey(cache.topicId), { + await redis.set(buildCacheKey(cache.topicId), { ...cache, generatedAt: new Date().toISOString(), version: CACHE_VERSION, - }); + }, { ex: 60 * 60 * 24 * 30 }); // 30 days, tune as needed🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/topic-cache.ts` around lines 55 - 73, The saveTopicCache function writes topic entries to Redis without expiration, so cached data can accumulate indefinitely. Update saveTopicCache to set a TTL when calling redis.set for the key built by buildCacheKey(cache.topicId), using an appropriate expiry option so entries are automatically removed after a bounded period. Keep the existing generatedAt and version fields, and preserve the current error handling in the try/catch.app/api/ai/workspace/route.ts (2)
8-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
WorkspaceBodyduplicatesWorkspaceRequestfromtypes/ai.tsinstead of extending/reusing it.
types/ai.tsexportsWorkspaceRequest { topicId, subjectCode, action, forceRefresh? }, but this route defines its own localWorkspaceBodywith an overlapping-but-different field set (adds branch/semester/question/topic/module/cachedExplanation/messages). SinceWorkspaceRequestisn't referenced anywhere in this file, the shared contract and the actual wire format can silently drift.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/ai/workspace/route.ts` around lines 8 - 20, The local WorkspaceBody type is duplicating the shared WorkspaceRequest contract and can drift from the API shape. Update the route to reuse or extend the WorkspaceRequest type from types/ai.ts instead of keeping a separate overlapping interface, and keep any extra route-specific fields explicitly composed on top of that shared type. Use the WorkspaceBody definition in route.ts and the exported WorkspaceRequest symbol to align the request payload with the shared contract.
35-43: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winRequest body is only shallow-cast, not runtime-validated.
(await request.json()) as WorkspaceBodyprovides no runtime guarantee about field types/shapes (e.g.,messagesentries'roleisn't checked to actually be"user" | "assistant"), yetmessagesflows straight into the Gemini prompt viabuildChatPrompt/formatConversation. Consider validating the body with a schema library (e.g. zod) before use.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/ai/workspace/route.ts` around lines 35 - 43, The request body in the workspace route is only being type-cast via the WorkspaceBody annotation, so invalid payload shapes can still reach buildChatPrompt and formatConversation. Add runtime validation for the parsed request body in route.ts using a schema such as zod, and only proceed when fields like branch, semester, topicId, subjectCode, question, forceRefresh, and especially messages have the expected types and allowed message roles. Use the existing WorkspaceBody, buildChatPrompt, and formatConversation symbols to locate the flow and ensure invalid input is rejected before it is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/ai/workspace/route.ts`:
- Around line 45-94: The follow-up question path in POST /api/ai/workspace
currently accepts question requests without any server-side enforcement, so the
3-question cap can be bypassed. Update the workspace route’s follow-up branch
(the question handling block that calls generateFollowupAnswer) to validate a
tamper-resistant server-side counter or signed conversation token before
processing. Reuse the existing follow-up identifiers in the request body (such
as subjectCode, topic, module, and cachedExplanation) to tie the limit to the
conversation and reject requests that exceed the allowed count.
In `@app/layout.tsx`:
- Around line 113-117: The first-paint theme script in the layout currently
applies whatever value is stored in localStorage via the documentElement class
list, which can leave html without a valid theme class; update the inline script
so it only accepts the supported values used by the theme provider in the layout
logic, with a safe fallback to dark when the stored value is missing or invalid.
Keep the fix in the same dangerouslySetInnerHTML snippet and ensure the class
removal/addition and colorScheme assignment always use a validated light or dark
theme.
In `@app/rgpv/`[branch]/[semester]/[subject]/ai/page.tsx:
- Around line 41-69: Track a dedicated explanation-load failure state in
ai/page.tsx around the generateTopicAnswer flow so the UI does not imply success
when the fetch fails. If the try/catch in the topic-mode branch fails, suppress
cachedExplanation-dependent copy and do not build follow-up prompts from
getFollowupPrompts(topicTitle) until an explanation actually exists; update the
welcomeMessage and any downstream render paths to show an explicit
failure/loading state instead of the ready message. Also make sure the follow-up
chat and prompt lists are gated by the same success condition used in the
topic-mode branch, including the later render sections that consume
cachedExplanation and explanationCached.
In `@components/ai/workspace-chat.tsx`:
- Around line 147-152: The non-follow-up payload built in workspace-chat.tsx is
invalid because it includes question without the required follow-up fields
expected by the workspace API. Update the send path in the workspace chat
request builder so that this branch either does not dispatch when there is no
cached topic/explanation, or it uses the topic-action contract instead of
question; use the existing request assembly around conversationHistory and the
follow-up payload shape to keep the API contract consistent.
- Around line 57-68: Reset the seeded chat state when the selected topic
changes: the useState initializer in workspace-chat.tsx only runs on mount, so
Messages can carry over between different topicTitle/cachedExplanation props.
Update the WorkspaceChat component’s messages state handling to detect topic
changes and replace the existing history with the new cached explanation seed
(or an empty list) whenever the active topic changes, using the relevant props
and the existing setMessages state flow.
In `@components/theme-provider.tsx`:
- Around line 41-42: The readStoredTheme helper in theme-provider.tsx is
trusting localStorage too directly and may return an invalid theme value outside
the Theme union. Update readStoredTheme to validate the stored value against the
allowed theme options before returning it, and fall back to the provided
defaultTheme when the value is missing or invalid. Keep the fix localized around
readStoredTheme and the Theme typing so the document only ever receives a valid
theme class.
- Around line 49-67: The local override in the ThemeProvider keeps winning over
cross-tab storage updates, so another tab’s theme change never becomes active
after setTheme is called. Update the theme state logic in theme-provider.tsx so
themeOverride is cleared or synchronized when the storage-backed value changes,
and ensure the React.useEffect that calls applyTheme reflects the latest
storedTheme instead of permanently preferring the override; use the existing
ThemeProvider, setTheme, and subscribeToTheme flow to keep tabs in sync.
In `@lib/ai/external-ai.ts`:
- Around line 52-66: The delayed popup flow in openExternalAI can be blocked
when window.open is called from setTimeout, so add a fallback when the open call
returns null instead of failing silently. Update openExternalAI to check the
return value of window.open in both the immediate and delayed paths, and surface
an appropriate fallback/notification using the existing ContinueExternalAI click
flow so users know the external app could not be opened.
In `@lib/ai/followup-service.ts`:
- Around line 8-19: Replace the local FollowupRequest and FollowupResponse
declarations in followup-service.ts with the shared types from `@/types/ai` so
there is a single contract for the follow-up flow. Update the followup service
to use the imported FollowupRequest/FollowupResponse symbols instead of
redefining them, and make sure any return values align with the shared
FollowupResponse shape that includes success, answer, and error. This will keep
the service and app/api/ai/workspace/route.ts consistent and prevent the types
from diverging again.
In `@lib/ai/topic-service.ts`:
- Around line 1-4: The retry/backoff flow and hardcoded Gemini model name are
duplicated in topic-service.ts and followup-service.ts, so extract the shared
logic into a reusable helper such as gemini-client.ts. Move sleep,
generateWithRetry, and the model constant (for example, the existing
generateTopics/generateTopicSuggestions path in topic-service.ts) into a shared
function like generateWithRetry(ai, prompt, subjectCode, logLabel) plus a single
GEMINI_MODEL export, then update both topic-service.ts and followup-service.ts
to call the shared helper instead of maintaining separate copies.
- Around line 44-51: Add a per-call timeout to the Gemini request in
topic-service’s generateContent flow so a stalled call cannot consume all retry
attempts. Update the generateContent invocation in the topic-service method to
pass an abort signal from AbortSignal.timeout(ms) or an httpOptions timeout,
keeping the existing retry logic and prompt/model handling intact.
---
Nitpick comments:
In `@app/api/ai/workspace/route.ts`:
- Around line 8-20: The local WorkspaceBody type is duplicating the shared
WorkspaceRequest contract and can drift from the API shape. Update the route to
reuse or extend the WorkspaceRequest type from types/ai.ts instead of keeping a
separate overlapping interface, and keep any extra route-specific fields
explicitly composed on top of that shared type. Use the WorkspaceBody definition
in route.ts and the exported WorkspaceRequest symbol to align the request
payload with the shared contract.
- Around line 35-43: The request body in the workspace route is only being
type-cast via the WorkspaceBody annotation, so invalid payload shapes can still
reach buildChatPrompt and formatConversation. Add runtime validation for the
parsed request body in route.ts using a schema such as zod, and only proceed
when fields like branch, semester, topicId, subjectCode, question, forceRefresh,
and especially messages have the expected types and allowed message roles. Use
the existing WorkspaceBody, buildChatPrompt, and formatConversation symbols to
locate the flow and ensure invalid input is rejected before it is used.
In `@components/ai/continue-external-ai.tsx`:
- Around line 56-74: The async handleContinue flow in continue-external-ai.tsx
has no in-flight guard, so rapid clicks can trigger duplicate copy attempts,
stacked toasts, and multiple delayed opens via openExternalAI. Add a
loading/in-progress state around handleContinue and use it to disable the
ExternalAIProvider buttons while copyToClipboard(prompt) is running, then clear
the state in a finally block so both success and error paths re-enable the UI
consistently.
In `@lib/ai/external-ai-prompt-builder.ts`:
- Around line 12-27: formatConversation currently removes the first assistant
message by position, which is fragile because it depends on WorkspaceChat
seeding cachedExplanation as index 0. Update formatConversation in
external-ai-prompt-builder.ts to exclude the cached explanation using a stable
identifier or marker on ChatMessage instead of relying on index === 0, and
adjust the WorkspaceChat message creation path so the explanation is tagged in a
way formatConversation can reliably filter it out.
In `@lib/ai/external-ai.ts`:
- Around line 34-41: The success toast in getExternalAISuccessToast hardcodes a
Windows-specific paste shortcut, which is not correct for macOS users. Update
the description text to use platform-agnostic wording instead of “Ctrl + V,” or
otherwise make it dynamic based on the user platform, while keeping the rest of
the toast content in ExternalAIProvider and EXTERNAL_AI_LABELS unchanged.
- Around line 68-75: `continueWithExternalAI` is duplicating the copy/open flow
and is out of sync with the real UI behavior. Update the actual continuation
flow to use this helper, or remove the helper if the component-level
implementation is the source of truth; in either case make sure the shared path
includes the existing delay and toast behavior currently handled around
`EXTERNAL_AI_OPEN_DELAY_MS` in `continueWithExternalAI` and the
continue-external-ai component logic. Align the copy-to-clipboard,
open-external-ai, and toast sequencing so there is one canonical implementation
instead of two drifting versions.
In `@lib/ai/topic-cache.ts`:
- Around line 55-73: The saveTopicCache function writes topic entries to Redis
without expiration, so cached data can accumulate indefinitely. Update
saveTopicCache to set a TTL when calling redis.set for the key built by
buildCacheKey(cache.topicId), using an appropriate expiry option so entries are
automatically removed after a bounded period. Keep the existing generatedAt and
version fields, and preserve the current error handling in the try/catch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8df622d3-59dd-41fc-852a-4200ca69fd4a
📒 Files selected for processing (21)
app/api/ai/workspace/route.tsapp/layout.tsxapp/rgpv/[branch]/[semester]/[subject]/ai/page.tsxcomponents/ai/continue-external-ai.tsxcomponents/ai/external-ai-logos.tsxcomponents/ai/workspace-chat-loader.tsxcomponents/ai/workspace-chat-skeleton.tsxcomponents/ai/workspace-chat.tsxcomponents/ai/workspace-message.tsxcomponents/mode-toggle.tsxcomponents/navbar.tsxcomponents/theme-provider.tsxlib/ai/chat-prompt-builder.tslib/ai/external-ai-prompt-builder.tslib/ai/external-ai.tslib/ai/followup-service.tslib/ai/metrics.tslib/ai/topic-cache.tslib/ai/topic-service.tstypes/ai.tstypes/topic-cache.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 11
🧹 Nitpick comments (7)
lib/ai/external-ai.ts (2)
34-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win"Ctrl + V" instruction doesn't apply to macOS users.
The success toast description hardcodes
Ctrl + V, but Mac users paste withCmd + V. Consider a platform-agnostic phrasing like "paste to continue."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai.ts` around lines 34 - 41, The success toast in getExternalAISuccessToast hardcodes a Windows-specific paste shortcut, which is not correct for macOS users. Update the description text to use platform-agnostic wording instead of “Ctrl + V,” or otherwise make it dynamic based on the user platform, while keeping the rest of the toast content in ExternalAIProvider and EXTERNAL_AI_LABELS unchanged.
68-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
continueWithExternalAIappears unused and diverges from the actual UI flow.
components/ai/continue-external-ai.tsxreimplements this copy+open+toast flow manually (withEXTERNAL_AI_OPEN_DELAY_MSand toasts) instead of calling this helper, and this helper itself doesn't apply the delay or surface toast info. This looks like dead/duplicated logic that will drift further from the real behavior over time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai.ts` around lines 68 - 75, `continueWithExternalAI` is duplicating the copy/open flow and is out of sync with the real UI behavior. Update the actual continuation flow to use this helper, or remove the helper if the component-level implementation is the source of truth; in either case make sure the shared path includes the existing delay and toast behavior currently handled around `EXTERNAL_AI_OPEN_DELAY_MS` in `continueWithExternalAI` and the continue-external-ai component logic. Align the copy-to-clipboard, open-external-ai, and toast sequencing so there is one canonical implementation instead of two drifting versions.lib/ai/external-ai-prompt-builder.ts (1)
12-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePositional filtering of the cached explanation is fragile.
formatConversationdrops the message atindex === 0only if it's an assistant message, relying onWorkspaceChatalways seedingcachedExplanationas the very first message. This works today (percomponents/ai/workspace-chat.tsx'smessagesinitializer), but is an implicit cross-file contract with no compile-time guarantee — if the seeding logic ever changes (e.g., a system message is prepended), this will silently start dropping/keeping the wrong message. Consider filtering by a stable identifier (e.g., excluding messages by id/marker) rather than position.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai-prompt-builder.ts` around lines 12 - 27, formatConversation currently removes the first assistant message by position, which is fragile because it depends on WorkspaceChat seeding cachedExplanation as index 0. Update formatConversation in external-ai-prompt-builder.ts to exclude the cached explanation using a stable identifier or marker on ChatMessage instead of relying on index === 0, and adjust the WorkspaceChat message creation path so the explanation is tagged in a way formatConversation can reliably filter it out.components/ai/continue-external-ai.tsx (1)
56-74: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo guard against rapid/duplicate clicks during the async copy.
handleContinueis async but the buttons stay enabled throughout, so multiple quick clicks (same or different providers) can queue severalsetTimeout-delayedwindow.opencalls and stacked toasts. Consider tracking an in-flight/loading state to disable the buttons during the copy operation.♻️ Proposed guard
+ const [pendingProvider, setPendingProvider] = useState<ExternalAIProvider | null>(null); + const handleContinue = async (provider: ExternalAIProvider) => { + if (pendingProvider) return; + setPendingProvider(provider); const copied = await copyToClipboard(prompt); + setPendingProvider(null); ...🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/ai/continue-external-ai.tsx` around lines 56 - 74, The async handleContinue flow in continue-external-ai.tsx has no in-flight guard, so rapid clicks can trigger duplicate copy attempts, stacked toasts, and multiple delayed opens via openExternalAI. Add a loading/in-progress state around handleContinue and use it to disable the ExternalAIProvider buttons while copyToClipboard(prompt) is running, then clear the state in a finally block so both success and error paths re-enable the UI consistently.lib/ai/topic-cache.ts (1)
55-73: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winNo TTL on cached topic entries.
saveTopicCachewrites to Redis with no expiration, so cache entries persist indefinitely (only removed via explicitforceRefresh/deleteTopicCache). Consider adding a TTL (e.g.redis.set(key, value, { ex: SECONDS })) to bound storage growth as more topics get cached over time.♻️ Proposed fix
try { - await redis.set(buildCacheKey(cache.topicId), { + await redis.set(buildCacheKey(cache.topicId), { ...cache, generatedAt: new Date().toISOString(), version: CACHE_VERSION, - }); + }, { ex: 60 * 60 * 24 * 30 }); // 30 days, tune as needed🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/topic-cache.ts` around lines 55 - 73, The saveTopicCache function writes topic entries to Redis without expiration, so cached data can accumulate indefinitely. Update saveTopicCache to set a TTL when calling redis.set for the key built by buildCacheKey(cache.topicId), using an appropriate expiry option so entries are automatically removed after a bounded period. Keep the existing generatedAt and version fields, and preserve the current error handling in the try/catch.app/api/ai/workspace/route.ts (2)
8-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
WorkspaceBodyduplicatesWorkspaceRequestfromtypes/ai.tsinstead of extending/reusing it.
types/ai.tsexportsWorkspaceRequest { topicId, subjectCode, action, forceRefresh? }, but this route defines its own localWorkspaceBodywith an overlapping-but-different field set (adds branch/semester/question/topic/module/cachedExplanation/messages). SinceWorkspaceRequestisn't referenced anywhere in this file, the shared contract and the actual wire format can silently drift.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/ai/workspace/route.ts` around lines 8 - 20, The local WorkspaceBody type is duplicating the shared WorkspaceRequest contract and can drift from the API shape. Update the route to reuse or extend the WorkspaceRequest type from types/ai.ts instead of keeping a separate overlapping interface, and keep any extra route-specific fields explicitly composed on top of that shared type. Use the WorkspaceBody definition in route.ts and the exported WorkspaceRequest symbol to align the request payload with the shared contract.
35-43: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winRequest body is only shallow-cast, not runtime-validated.
(await request.json()) as WorkspaceBodyprovides no runtime guarantee about field types/shapes (e.g.,messagesentries'roleisn't checked to actually be"user" | "assistant"), yetmessagesflows straight into the Gemini prompt viabuildChatPrompt/formatConversation. Consider validating the body with a schema library (e.g. zod) before use.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/ai/workspace/route.ts` around lines 35 - 43, The request body in the workspace route is only being type-cast via the WorkspaceBody annotation, so invalid payload shapes can still reach buildChatPrompt and formatConversation. Add runtime validation for the parsed request body in route.ts using a schema such as zod, and only proceed when fields like branch, semester, topicId, subjectCode, question, forceRefresh, and especially messages have the expected types and allowed message roles. Use the existing WorkspaceBody, buildChatPrompt, and formatConversation symbols to locate the flow and ensure invalid input is rejected before it is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/ai/workspace/route.ts`:
- Around line 45-94: The follow-up question path in POST /api/ai/workspace
currently accepts question requests without any server-side enforcement, so the
3-question cap can be bypassed. Update the workspace route’s follow-up branch
(the question handling block that calls generateFollowupAnswer) to validate a
tamper-resistant server-side counter or signed conversation token before
processing. Reuse the existing follow-up identifiers in the request body (such
as subjectCode, topic, module, and cachedExplanation) to tie the limit to the
conversation and reject requests that exceed the allowed count.
In `@app/layout.tsx`:
- Around line 113-117: The first-paint theme script in the layout currently
applies whatever value is stored in localStorage via the documentElement class
list, which can leave html without a valid theme class; update the inline script
so it only accepts the supported values used by the theme provider in the layout
logic, with a safe fallback to dark when the stored value is missing or invalid.
Keep the fix in the same dangerouslySetInnerHTML snippet and ensure the class
removal/addition and colorScheme assignment always use a validated light or dark
theme.
In `@app/rgpv/`[branch]/[semester]/[subject]/ai/page.tsx:
- Around line 41-69: Track a dedicated explanation-load failure state in
ai/page.tsx around the generateTopicAnswer flow so the UI does not imply success
when the fetch fails. If the try/catch in the topic-mode branch fails, suppress
cachedExplanation-dependent copy and do not build follow-up prompts from
getFollowupPrompts(topicTitle) until an explanation actually exists; update the
welcomeMessage and any downstream render paths to show an explicit
failure/loading state instead of the ready message. Also make sure the follow-up
chat and prompt lists are gated by the same success condition used in the
topic-mode branch, including the later render sections that consume
cachedExplanation and explanationCached.
In `@components/ai/workspace-chat.tsx`:
- Around line 147-152: The non-follow-up payload built in workspace-chat.tsx is
invalid because it includes question without the required follow-up fields
expected by the workspace API. Update the send path in the workspace chat
request builder so that this branch either does not dispatch when there is no
cached topic/explanation, or it uses the topic-action contract instead of
question; use the existing request assembly around conversationHistory and the
follow-up payload shape to keep the API contract consistent.
- Around line 57-68: Reset the seeded chat state when the selected topic
changes: the useState initializer in workspace-chat.tsx only runs on mount, so
Messages can carry over between different topicTitle/cachedExplanation props.
Update the WorkspaceChat component’s messages state handling to detect topic
changes and replace the existing history with the new cached explanation seed
(or an empty list) whenever the active topic changes, using the relevant props
and the existing setMessages state flow.
In `@components/theme-provider.tsx`:
- Around line 41-42: The readStoredTheme helper in theme-provider.tsx is
trusting localStorage too directly and may return an invalid theme value outside
the Theme union. Update readStoredTheme to validate the stored value against the
allowed theme options before returning it, and fall back to the provided
defaultTheme when the value is missing or invalid. Keep the fix localized around
readStoredTheme and the Theme typing so the document only ever receives a valid
theme class.
- Around line 49-67: The local override in the ThemeProvider keeps winning over
cross-tab storage updates, so another tab’s theme change never becomes active
after setTheme is called. Update the theme state logic in theme-provider.tsx so
themeOverride is cleared or synchronized when the storage-backed value changes,
and ensure the React.useEffect that calls applyTheme reflects the latest
storedTheme instead of permanently preferring the override; use the existing
ThemeProvider, setTheme, and subscribeToTheme flow to keep tabs in sync.
In `@lib/ai/external-ai.ts`:
- Around line 52-66: The delayed popup flow in openExternalAI can be blocked
when window.open is called from setTimeout, so add a fallback when the open call
returns null instead of failing silently. Update openExternalAI to check the
return value of window.open in both the immediate and delayed paths, and surface
an appropriate fallback/notification using the existing ContinueExternalAI click
flow so users know the external app could not be opened.
In `@lib/ai/followup-service.ts`:
- Around line 8-19: Replace the local FollowupRequest and FollowupResponse
declarations in followup-service.ts with the shared types from `@/types/ai` so
there is a single contract for the follow-up flow. Update the followup service
to use the imported FollowupRequest/FollowupResponse symbols instead of
redefining them, and make sure any return values align with the shared
FollowupResponse shape that includes success, answer, and error. This will keep
the service and app/api/ai/workspace/route.ts consistent and prevent the types
from diverging again.
In `@lib/ai/topic-service.ts`:
- Around line 1-4: The retry/backoff flow and hardcoded Gemini model name are
duplicated in topic-service.ts and followup-service.ts, so extract the shared
logic into a reusable helper such as gemini-client.ts. Move sleep,
generateWithRetry, and the model constant (for example, the existing
generateTopics/generateTopicSuggestions path in topic-service.ts) into a shared
function like generateWithRetry(ai, prompt, subjectCode, logLabel) plus a single
GEMINI_MODEL export, then update both topic-service.ts and followup-service.ts
to call the shared helper instead of maintaining separate copies.
- Around line 44-51: Add a per-call timeout to the Gemini request in
topic-service’s generateContent flow so a stalled call cannot consume all retry
attempts. Update the generateContent invocation in the topic-service method to
pass an abort signal from AbortSignal.timeout(ms) or an httpOptions timeout,
keeping the existing retry logic and prompt/model handling intact.
---
Nitpick comments:
In `@app/api/ai/workspace/route.ts`:
- Around line 8-20: The local WorkspaceBody type is duplicating the shared
WorkspaceRequest contract and can drift from the API shape. Update the route to
reuse or extend the WorkspaceRequest type from types/ai.ts instead of keeping a
separate overlapping interface, and keep any extra route-specific fields
explicitly composed on top of that shared type. Use the WorkspaceBody definition
in route.ts and the exported WorkspaceRequest symbol to align the request
payload with the shared contract.
- Around line 35-43: The request body in the workspace route is only being
type-cast via the WorkspaceBody annotation, so invalid payload shapes can still
reach buildChatPrompt and formatConversation. Add runtime validation for the
parsed request body in route.ts using a schema such as zod, and only proceed
when fields like branch, semester, topicId, subjectCode, question, forceRefresh,
and especially messages have the expected types and allowed message roles. Use
the existing WorkspaceBody, buildChatPrompt, and formatConversation symbols to
locate the flow and ensure invalid input is rejected before it is used.
In `@components/ai/continue-external-ai.tsx`:
- Around line 56-74: The async handleContinue flow in continue-external-ai.tsx
has no in-flight guard, so rapid clicks can trigger duplicate copy attempts,
stacked toasts, and multiple delayed opens via openExternalAI. Add a
loading/in-progress state around handleContinue and use it to disable the
ExternalAIProvider buttons while copyToClipboard(prompt) is running, then clear
the state in a finally block so both success and error paths re-enable the UI
consistently.
In `@lib/ai/external-ai-prompt-builder.ts`:
- Around line 12-27: formatConversation currently removes the first assistant
message by position, which is fragile because it depends on WorkspaceChat
seeding cachedExplanation as index 0. Update formatConversation in
external-ai-prompt-builder.ts to exclude the cached explanation using a stable
identifier or marker on ChatMessage instead of relying on index === 0, and
adjust the WorkspaceChat message creation path so the explanation is tagged in a
way formatConversation can reliably filter it out.
In `@lib/ai/external-ai.ts`:
- Around line 34-41: The success toast in getExternalAISuccessToast hardcodes a
Windows-specific paste shortcut, which is not correct for macOS users. Update
the description text to use platform-agnostic wording instead of “Ctrl + V,” or
otherwise make it dynamic based on the user platform, while keeping the rest of
the toast content in ExternalAIProvider and EXTERNAL_AI_LABELS unchanged.
- Around line 68-75: `continueWithExternalAI` is duplicating the copy/open flow
and is out of sync with the real UI behavior. Update the actual continuation
flow to use this helper, or remove the helper if the component-level
implementation is the source of truth; in either case make sure the shared path
includes the existing delay and toast behavior currently handled around
`EXTERNAL_AI_OPEN_DELAY_MS` in `continueWithExternalAI` and the
continue-external-ai component logic. Align the copy-to-clipboard,
open-external-ai, and toast sequencing so there is one canonical implementation
instead of two drifting versions.
In `@lib/ai/topic-cache.ts`:
- Around line 55-73: The saveTopicCache function writes topic entries to Redis
without expiration, so cached data can accumulate indefinitely. Update
saveTopicCache to set a TTL when calling redis.set for the key built by
buildCacheKey(cache.topicId), using an appropriate expiry option so entries are
automatically removed after a bounded period. Keep the existing generatedAt and
version fields, and preserve the current error handling in the try/catch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8df622d3-59dd-41fc-852a-4200ca69fd4a
📒 Files selected for processing (21)
app/api/ai/workspace/route.tsapp/layout.tsxapp/rgpv/[branch]/[semester]/[subject]/ai/page.tsxcomponents/ai/continue-external-ai.tsxcomponents/ai/external-ai-logos.tsxcomponents/ai/workspace-chat-loader.tsxcomponents/ai/workspace-chat-skeleton.tsxcomponents/ai/workspace-chat.tsxcomponents/ai/workspace-message.tsxcomponents/mode-toggle.tsxcomponents/navbar.tsxcomponents/theme-provider.tsxlib/ai/chat-prompt-builder.tslib/ai/external-ai-prompt-builder.tslib/ai/external-ai.tslib/ai/followup-service.tslib/ai/metrics.tslib/ai/topic-cache.tslib/ai/topic-service.tstypes/ai.tstypes/topic-cache.ts
🛑 Comments failed to post (11)
app/api/ai/workspace/route.ts (1)
45-94: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n== File list ==\n' git ls-files 'app/api/ai/workspace/route.ts' 'types/ai.ts' 'app/**/workspace-chat.tsx' 'app/**/workspace*.tsx' 'app/**/workspace*.ts' | sed 's#^`#-` #' printf '\n== Outline: app/api/ai/workspace/route.ts ==\n' ast-grep outline app/api/ai/workspace/route.ts --view expanded || true printf '\n== Outline: types/ai.ts ==\n' ast-grep outline types/ai.ts --view expanded || true printf '\n== Search for FOLLOWUP_QUESTION_LIMIT and followup limit logic ==\n' rg -n "FOLLOWUP_QUESTION_LIMIT|followUpCount|limitReached|followupLimit|follow-up limit|followup" app types -g '!**/node_modules/**' || true printf '\n== Read relevant slices ==\n' for f in app/api/ai/workspace/route.ts types/ai.ts; do if [ -f "$f" ]; then lines=$(wc -l < "$f") echo "--- $f ($lines lines) ---" sed -n '1,220p' "$f" | cat -n fi doneRepository: imuniqueshiv/HyperLearningTech
Length of output: 12880
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n== Search for server-side follow-up limit enforcement ==\n' rg -n "FOLLOWUP_QUESTION_LIMIT|followup.*limit|limit.*followup|question limit|3-question|messages\.length|followUpCount|limitReached" app lib types -g '!**/node_modules/**' || true printf '\n== Inspect workspace UI and follow-up flow ==\n' for f in app/rgpv/[branch]/[semester]/[subject]/ai/page.tsx; do if [ -f "$f" ]; then lines=$(wc -l < "$f") echo "--- $f ($lines lines) ---" sed -n '1,260p' "$f" | cat -n fi done printf '\n== Inspect followup service for any limit checks ==\n' for f in $(git ls-files 'lib/**/followup*.ts' 'lib/**/followup*.tsx' 'app/**/followup*.ts' 'app/**/followup*.tsx'); do echo "--- $f ---" lines=$(wc -l < "$f") sed -n '1,240p' "$f" | cat -n doneRepository: imuniqueshiv/HyperLearningTech
Length of output: 11757
Enforce the follow-up cap on the server. The current limit is only enforced in the UI;
POST /api/ai/workspaceacceptsquestionindefinitely, so the 3-question cap can be bypassed by direct API calls. Add a tamper-resistant server-side counter or signed conversation token before this ships.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/ai/workspace/route.ts` around lines 45 - 94, The follow-up question path in POST /api/ai/workspace currently accepts question requests without any server-side enforcement, so the 3-question cap can be bypassed. Update the workspace route’s follow-up branch (the question handling block that calls generateFollowupAnswer) to validate a tamper-resistant server-side counter or signed conversation token before processing. Reuse the existing follow-up identifiers in the request body (such as subjectCode, topic, module, and cachedExplanation) to tie the limit to the conversation and reject requests that exceed the allowed count.app/layout.tsx (1)
113-117: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate the stored theme in the first-paint script.
The provider only supports
"light"and"dark", but Line 115 adds any stored value as a class. A stale value like"system"leaves<html>without the expected light/dark class until the user toggles.Proposed fix
<script dangerouslySetInnerHTML={{ - __html: `(function(){try{var t=localStorage.getItem("theme")||"dark";document.documentElement.classList.remove("light","dark");document.documentElement.classList.add(t);document.documentElement.style.colorScheme=t}catch(e){document.documentElement.classList.add("dark")}})();`, + __html: `(function(){try{var t=localStorage.getItem("theme");if(t!=="light"&&t!=="dark")t="dark";document.documentElement.classList.remove("light","dark");document.documentElement.classList.add(t);document.documentElement.style.colorScheme=t}catch(e){document.documentElement.classList.remove("light");document.documentElement.classList.add("dark");document.documentElement.style.colorScheme="dark"}})();`, }} />📝 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.<script dangerouslySetInnerHTML={{ __html: `(function(){try{var t=localStorage.getItem("theme");if(t!=="light"&&t!=="dark")t="dark";document.documentElement.classList.remove("light","dark");document.documentElement.classList.add(t);document.documentElement.style.colorScheme=t}catch(e){document.documentElement.classList.remove("light");document.documentElement.classList.add("dark");document.documentElement.style.colorScheme="dark"}})();`, }} />🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 113-113: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation(react-unsafe-html-injection)
🪛 React Doctor (0.5.8)
[error] 114-114:
dangerouslySetInnerHTMLis an XSS hole that runs attacker-controlled HTML in your users' browsers.Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.
(no-danger)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/layout.tsx` around lines 113 - 117, The first-paint theme script in the layout currently applies whatever value is stored in localStorage via the documentElement class list, which can leave html without a valid theme class; update the inline script so it only accepts the supported values used by the theme provider in the layout logic, with a safe fallback to dark when the stored value is missing or invalid. Keep the fix in the same dangerouslySetInnerHTML snippet and ensure the class removal/addition and colorScheme assignment always use a validated light or dark theme.app/rgpv/[branch]/[semester]/[subject]/ai/page.tsx (1)
41-69: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Render an explicit explanation-load failure state.
If
generateTopicAnswerthrows, the page still says the explanation is ready and passes follow-up prompts, butcachedExplanationis missing so follow-up chat cannot work. Track the failure and suppress follow-up prompts/cached-copy until the explanation actually exists.Proposed direction
let cachedExplanation: string | undefined; let explanationCached: boolean | undefined; + let explanationError = false; if (isTopicMode && topicId) { try { const result = await generateTopicAnswer({ branch, @@ cachedExplanation = result.answer; explanationCached = result.cached; } catch (error) { console.error("Failed to load topic explanation:", error); + explanationError = true; } } - const followupPrompts = isTopicMode + const followupPrompts = isTopicMode && cachedExplanation ? getFollowupPrompts(topicTitle).map((prompt) => ({ prompt })) : []; const welcomeMessage = isTopicMode - ? `Your **${topicTitle}** explanation is ready below. Ask up to 3 follow-up questions — examples, comparisons, exam tips, and clarifications.` + ? explanationError + ? `We couldn't load the cached explanation for **${topicTitle}**. Please try again later.` + : `Your **${topicTitle}** explanation is ready below. Ask up to 3 follow-up questions — examples, comparisons, exam tips, and clarifications.` : "Select a topic from the syllabus to get a cached explanation and follow-up chat.";Also applies to: 132-142, 147-156
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/rgpv/`[branch]/[semester]/[subject]/ai/page.tsx around lines 41 - 69, Track a dedicated explanation-load failure state in ai/page.tsx around the generateTopicAnswer flow so the UI does not imply success when the fetch fails. If the try/catch in the topic-mode branch fails, suppress cachedExplanation-dependent copy and do not build follow-up prompts from getFollowupPrompts(topicTitle) until an explanation actually exists; update the welcomeMessage and any downstream render paths to show an explicit failure/loading state instead of the ready message. Also make sure the follow-up chat and prompt lists are gated by the same success condition used in the topic-mode branch, including the later render sections that consume cachedExplanation and explanationCached.components/ai/workspace-chat.tsx (2)
57-68: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reset seeded messages when the selected topic changes.
The
useStateinitializer only runs on mount, so navigating between topics in the same client tree can keep the previous cached explanation and chat history whiletopicTitle/cachedExplanationprops now refer to another topic.Proposed fix
const [messages, setMessages] = useState<Message[]>(() => { if (!cachedExplanation) return []; return [ { id: "initial-cached-explanation", role: "assistant", content: cachedExplanation, timestamp: new Date(), }, ]; }); + + useEffect(() => { + setMessages( + cachedExplanation + ? [ + { + id: "initial-cached-explanation", + role: "assistant", + content: cachedExplanation, + timestamp: new Date(), + }, + ] + : [] + ); + setInput(""); + setError(null); + }, [cachedExplanation, topicId, topicTitle, moduleTitle]);📝 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.const [messages, setMessages] = useState<Message[]>(() => { if (!cachedExplanation) return []; return [ { id: "initial-cached-explanation", role: "assistant", content: cachedExplanation, timestamp: new Date(), }, ]; }); useEffect(() => { setMessages( cachedExplanation ? [ { id: "initial-cached-explanation", role: "assistant", content: cachedExplanation, timestamp: new Date(), }, ] : [] ); setInput(""); setError(null); }, [cachedExplanation, topicId, topicTitle, moduleTitle]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/ai/workspace-chat.tsx` around lines 57 - 68, Reset the seeded chat state when the selected topic changes: the useState initializer in workspace-chat.tsx only runs on mount, so Messages can carry over between different topicTitle/cachedExplanation props. Update the WorkspaceChat component’s messages state handling to detect topic changes and replace the existing history with the new cached explanation seed (or an empty list) whenever the active topic changes, using the relevant props and the existing setMessages state flow.
147-152: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Avoid sending an invalid non-follow-up payload.
The workspace API dispatches any request with
questioninto follow-up mode, wherecachedExplanation,topic, andmoduleare required. This non-follow-up branch sendsquestionwithout those fields, so suggested/general prompts can only fail with a 400. Either remove this send path when no cached topic exists, or send the topic-action contract instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/ai/workspace-chat.tsx` around lines 147 - 152, The non-follow-up payload built in workspace-chat.tsx is invalid because it includes question without the required follow-up fields expected by the workspace API. Update the send path in the workspace chat request builder so that this branch either does not dispatch when there is no cached topic/explanation, or it uses the topic-action contract instead of question; use the existing request assembly around conversationHistory and the follow-up payload shape to keep the API contract consistent.components/theme-provider.tsx (2)
41-42: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate stored theme values before trusting them.
localStoragecan contain stale or arbitrary values, so this cast can return a runtime value outside"light" | "dark"and leave the document without a valid theme class.Proposed fix
+function isTheme(value: string | null): value is Theme { + return value === "light" || value === "dark"; +} + function readStoredTheme(defaultTheme: Theme): Theme { - return (localStorage.getItem(STORAGE_KEY) as Theme | null) || defaultTheme; + try { + const storedTheme = localStorage.getItem(STORAGE_KEY); + return isTheme(storedTheme) ? storedTheme : defaultTheme; + } catch { + return defaultTheme; + } }📝 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.function isTheme(value: string | null): value is Theme { return value === "light" || value === "dark"; } function readStoredTheme(defaultTheme: Theme): Theme { try { const storedTheme = localStorage.getItem(STORAGE_KEY); return isTheme(storedTheme) ? storedTheme : defaultTheme; } catch { return defaultTheme; } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/theme-provider.tsx` around lines 41 - 42, The readStoredTheme helper in theme-provider.tsx is trusting localStorage too directly and may return an invalid theme value outside the Theme union. Update readStoredTheme to validate the stored value against the allowed theme options before returning it, and fall back to the provided defaultTheme when the value is missing or invalid. Keep the fix localized around readStoredTheme and the Theme typing so the document only ever receives a valid theme class.
49-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clear the local override when another tab changes the theme.
Line 57 keeps using
themeOverrideforever after this tab callssetTheme, so laterstorageevents updatestoredThemebut never affect the active theme.Proposed fix
const [themeOverride, setThemeOverride] = React.useState<Theme | null>(null); + + React.useEffect(() => { + const clearThemeOverride = () => setThemeOverride(null); + window.addEventListener("storage", clearThemeOverride); + return () => window.removeEventListener("storage", clearThemeOverride); + }, []); const storedTheme = React.useSyncExternalStore(📝 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.const [themeOverride, setThemeOverride] = React.useState<Theme | null>(null); React.useEffect(() => { const clearThemeOverride = () => setThemeOverride(null); window.addEventListener("storage", clearThemeOverride); return () => window.removeEventListener("storage", clearThemeOverride); }, []); const storedTheme = React.useSyncExternalStore( subscribeToTheme, () => readStoredTheme(defaultTheme), () => defaultTheme ); const theme = themeOverride ?? storedTheme; React.useEffect(() => { applyTheme(theme); }, [theme]); const setTheme = React.useCallback((newTheme: Theme) => { setThemeOverride(newTheme); localStorage.setItem(STORAGE_KEY, newTheme); applyTheme(newTheme); }, []);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/theme-provider.tsx` around lines 49 - 67, The local override in the ThemeProvider keeps winning over cross-tab storage updates, so another tab’s theme change never becomes active after setTheme is called. Update the theme state logic in theme-provider.tsx so themeOverride is cleared or synchronized when the storage-backed value changes, and ensure the React.useEffect that calls applyTheme reflects the latest storedTheme instead of permanently preferring the override; use the existing ThemeProvider, setTheme, and subscribeToTheme flow to keep tabs in sync.lib/ai/external-ai.ts (1)
52-66: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== file outline ==" ast-grep outline lib/ai/external-ai.ts --view expanded || true echo echo "== file lines ==" cat -n lib/ai/external-ai.ts | sed -n '1,220p' echo echo "== usages of openExternalAI ==" rg -n "openExternalAI\(" -S . echo echo "== usages of delayMs ==" rg -n "delayMs" lib . -S || trueRepository: imuniqueshiv/HyperLearningTech
Length of output: 3601
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== outline ==" ast-grep outline components/ai/continue-external-ai.tsx --view expanded || true echo echo "== relevant lines ==" cat -n components/ai/continue-external-ai.tsx | sed -n '1,220p' echo echo "== search for toast/opening text ==" rg -n "opening|Conversation copied|Continue|openExternalAI|EXTERNAL_AI_OPEN_DELAY_MS" components lib -SRepository: imuniqueshiv/HyperLearningTech
Length of output: 6615
Delayed opens can be blocked
window.openis deferred bysetTimeout, so the browser can lose the click’s transient activation and block the popup.ContinueExternalAIalready shows a success toast on click, so surface a fallback whenwindow.openreturnsnullinstead of failing silently.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/external-ai.ts` around lines 52 - 66, The delayed popup flow in openExternalAI can be blocked when window.open is called from setTimeout, so add a fallback when the open call returns null instead of failing silently. Update openExternalAI to check the return value of window.open in both the immediate and delayed paths, and surface an appropriate fallback/notification using the existing ContinueExternalAI click flow so users know the external app could not be opened.lib/ai/followup-service.ts (1)
8-19: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Local
FollowupRequest/FollowupResponseduplicate and diverge from@/types/ai.
types/ai.tsalready exportsFollowupRequest(identical shape) andFollowupResponse({ success: boolean; answer?: string; error?: string }), but this file defines its own local versions instead. The localFollowupResponse({ answer: string }) omitssuccess/error, so the shared contract and the actual service return type have already diverged —app/api/ai/workspace/route.tshas to manually reconstructsuccess/mode/cachedaround this return value instead of relying on a single typed contract.Import and reuse the shared types to keep the API surface consistent.
♻️ Proposed fix
+import type { FollowupRequest, FollowupResponse, ChatMessage } from "`@/types/ai`"; + import { GoogleGenAI } from "`@google/genai`"; import { buildChatPrompt } from "`@/lib/ai/chat-prompt-builder`"; import { getWorkspaceKey } from "`@/lib/ai/key-manager`"; import { getSubjectInfo } from "`@/lib/ai/subject-map`"; import { trackMetric } from "`@/lib/ai/metrics`"; -export interface FollowupRequest { - subjectCode: string; - module: string; - topic: string; - cachedExplanation: string; - question: string; - messages?: Array<{ role: "user" | "assistant"; content: string }>; -} - -export interface FollowupResponse { - answer: string; -}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/followup-service.ts` around lines 8 - 19, Replace the local FollowupRequest and FollowupResponse declarations in followup-service.ts with the shared types from `@/types/ai` so there is a single contract for the follow-up flow. Update the followup service to use the imported FollowupRequest/FollowupResponse symbols instead of redefining them, and make sure any return values align with the shared FollowupResponse shape that includes success, answer, and error. This will keep the service and app/api/ai/workspace/route.ts consistent and prevent the types from diverging again.lib/ai/topic-service.ts (2)
1-4: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicate retry/backoff/model-name logic vs.
followup-service.ts.
sleep,generateWithRetry, and the hardcoded"gemini-2.5-flash-lite"model string are duplicated almost verbatim inlib/ai/followup-service.ts(lines 21-70). The model string alone now appears in 3 places (here twice, once in followup-service.ts) — if the model or retry strategy changes, it's easy to update one copy and miss another.Consider extracting a shared
lib/ai/gemini-client.tshelper exportinggenerateWithRetry(ai, prompt, subjectCode, logLabel)and aGEMINI_MODELconstant, used by bothtopic-service.tsandfollowup-service.ts.Also applies to: 30-80, 143-162
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/topic-service.ts` around lines 1 - 4, The retry/backoff flow and hardcoded Gemini model name are duplicated in topic-service.ts and followup-service.ts, so extract the shared logic into a reusable helper such as gemini-client.ts. Move sleep, generateWithRetry, and the model constant (for example, the existing generateTopics/generateTopicSuggestions path in topic-service.ts) into a shared function like generateWithRetry(ai, prompt, subjectCode, logLabel) plus a single GEMINI_MODEL export, then update both topic-service.ts and followup-service.ts to call the shared helper instead of maintaining separate copies.
44-51: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does@google/genai2.10.0 generateContent support AbortSignal or a reliable per-call timeout option?💡 Result:
Yes, the
@google/genaiSDK (version 2.10.0) supports both AbortSignal and per-call timeout options for generateContent requests, though they function as client-side mechanisms [1][2]. AbortSignal You can pass an AbortSignal via the abortSignal field within the GenerateContentConfig object [1][3]. This allows you to cancel the request from your application code by calling abort on the associated AbortController [2][4]. Note that this is a client-side operation; while it stops the SDK from waiting for a response, it does not necessarily cancel the processing or billing for the operation on the Google service side [1]. Timeout Options You can configure a timeout using the httpOptions.timeout field (in milliseconds) within the GenerateContentConfig [2][3]. This is also a client-side mechanism handled by the SDK to automatically trigger an abort if the request exceeds the specified duration [2]. Important Technical Nuances: 1. Scope: These options can be applied either globally (via the GoogleGenAI constructor) or on a per-call basis within the generateContent config [2]. Per-call settings take precedence [2]. 2. Limitations: Because these are client-side operations, they primarily control how long your local environment waits for the API to respond [1][2]. They do not reliably cancel the server-side generation process [1]. 3. Known Issues: Some users have reported issues where the X-Server-Timeout header might not be correctly passed if the httpOptions object is not structured precisely (e.g., when providing timeout without an explicit headers object) [5]. While recent versions have addressed various timeout bugs, ensure your testing confirms the desired behavior in your specific environment [6].Citations:
- 1: https://googleapis.github.io/js-genai/release_docs/interfaces/types.GenerateContentConfig.html
- 2: https://deepwiki.com/googleapis/js-genai/12.3-abort-signals-and-timeouts
- 3: https://github.com/googleapis/js-genai/blob/89400098/api-report/genai.api.md
- 4: https://github.com/googleapis/js-genai/blob/89400098/sdk-samples/abort_signal.ts
- 5: googleapis/js-genai#1489
- 6: googleapis/js-genai#1277
🏁 Script executed:
#!/bin/bash set -euo pipefail git ls-files lib/ai/topic-service.ts wc -l lib/ai/topic-service.ts cat -n lib/ai/topic-service.ts | sed -n '1,220p' rg -n "generateContent|AbortSignal|timeout|httpOptions|retry" lib/ai/topic-service.tsRepository: imuniqueshiv/HyperLearningTech
Length of output: 4830
Add a timeout to
generateContent
generateContenthas no per-call timeout or abort signal here, so one stalled Gemini request can block each of the 3 retries. PassAbortSignal.timeout(ms)orhttpOptions.timeouton the call.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ai/topic-service.ts` around lines 44 - 51, Add a per-call timeout to the Gemini request in topic-service’s generateContent flow so a stalled call cannot consume all retry attempts. Update the generateContent invocation in the topic-service method to pass an abort signal from AbortSignal.timeout(ms) or an httpOptions timeout, keeping the existing retry logic and prompt/model handling intact.
Pull Request
Summary
Implement Phase 2 of the Hyper AI Workspace by introducing live follow-up conversations, external AI continuation, and an improved topic-based learning workflow while preserving the canonical topic cache architecture.
Related Issue
Closes #
Type of Change
What Changed?
followup-service,chat-prompt-builder, andexternal-ai-prompt-builderfor a cleaner AI architecture.Screenshots (UI Changes Only)
N/A
Testing
npm run format:checknpm run lintnpm run typechecknpm run buildChecklist
main.npx prettier --write <file>).npm run format:checkpasses.npm run lintpasses.npm run typecheckpasses.npm run buildpasses.Additional Notes
This PR completes Phase 2 of the Hyper AI Workspace roadmap.
Implemented workflow
The initial topic explanation is generated once and stored in the canonical topic cache, minimizing repeated AI requests. Follow-up conversations remain live for up to three questions, after which students can seamlessly continue the discussion in an external AI tool with the conversation context automatically copied to the clipboard.