Skip to content

feat(ai): implement follow-up chat workflow with external AI continuation#55

Merged
imuniqueshiv merged 13 commits into
mainfrom
HyperAIWorkspace1
Jul 6, 2026
Merged

feat(ai): implement follow-up chat workflow with external AI continuation#55
imuniqueshiv merged 13 commits into
mainfrom
HyperAIWorkspace1

Conversation

@imuniqueshiv

@imuniqueshiv imuniqueshiv commented Jul 6, 2026

Copy link
Copy Markdown
Owner

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

  • Feature
  • Bug Fix
  • Documentation
  • Refactor
  • Performance Improvement
  • CI / Build
  • Other

What Changed?

  • Implemented live follow-up chat with a 3-question limit after the cached topic explanation.
  • Added external AI continuation (ChatGPT, Gemini, Claude, DeepSeek, and Grok) with automatic context copying.
  • Introduced followup-service, chat-prompt-builder, and external-ai-prompt-builder for a cleaner AI architecture.
  • Added loading skeletons and improved the Hyper AI chat experience.
  • Refactored the Workspace API to support cached topic explanations and live follow-up conversations.
  • Preserved canonical topic caching with regeneration support.

Screenshots (UI Changes Only)

N/A


Testing

  • Verified topic explanation generation using the canonical topic cache.
  • Tested cache hits, cache regeneration, and follow-up conversations.
  • Confirmed the 3-question follow-up limit.
  • Verified automatic context copying before opening external AI platforms.
  • Successfully tested the complete workflow locally.
  • Executed:
    • npm run format:check
    • npm run lint
    • npm run typecheck
    • npm run build

Checklist

  • My branch is up to date with the latest main.
  • My code follows the project's coding standards.
  • I have formatted the modified files (npx prettier --write <file>).
  • npm run format:check passes.
  • npm run lint passes.
  • npm run typecheck passes.
  • npm run build passes.
  • I have updated documentation if required.
  • I have tested my changes locally.
  • This Pull Request focuses on a single feature or fix.

Additional Notes

This PR completes Phase 2 of the Hyper AI Workspace roadmap.

Implemented workflow

Student selects topic
        ↓
Canonical topic lookup
        ↓
Redis topic cache
        ↓
Cache hit → Return cached explanation
Cache miss → Gemini → Save cache
        ↓
Student asks up to 3 follow-up questions
        ↓
Live Gemini follow-up service (no caching)
        ↓
Follow-up limit reached
        ↓
Copy full conversation context
        ↓
Continue in ChatGPT / Gemini / Claude / DeepSeek / Grok

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.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyper-learning-tech Ready Ready Preview, Comment Jul 6, 2026 1:56am

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

AI workspace topic/followup pipeline

Layer / File(s) Summary
Types and cache contract
types/topic-cache.ts, types/ai.ts, lib/ai/metrics.ts
Adds TopicCache interface, changes WorkspaceRequest to use topicId, adds follow-up chat types (ChatMessage, ChatPromptContext, FollowupRequest/Response, FOLLOWUP_QUESTION_LIMIT), and adds a CACHE_REFRESH metric event.
Redis topic cache
lib/ai/topic-cache.ts
Implements get/save/delete cache functions keyed by topic with version invalidation and error handling.
Topic and follow-up generation services
lib/ai/topic-service.ts, lib/ai/followup-service.ts, lib/ai/chat-prompt-builder.ts
Implements generateTopicAnswer (cache-aware, retrying Gemini calls) and generateFollowupAnswer (prompt-built, retrying Gemini calls) with metrics.
Workspace API route dispatch
app/api/ai/workspace/route.ts
POST branches into follow-up vs topic mode calling the new services; GET reports supported modes.
External AI provider utilities
lib/ai/external-ai.ts, lib/ai/external-ai-prompt-builder.ts, components/ai/external-ai-logos.tsx
Adds provider URLs/labels, clipboard/open helpers, logos, brand colors, and an external-AI prompt builder.
ContinueExternalAI component
components/ai/continue-external-ai.tsx
New component offering provider buttons that copy a generated prompt and open the external AI site.
Workspace chat follow-up mode
components/ai/workspace-chat.tsx, components/ai/workspace-chat-loader.tsx, components/ai/workspace-chat-skeleton.tsx, components/ai/workspace-message.tsx
Adds follow-up limit tracking, cached-explanation seeding, revised request payloads, demo fallback handling, a lazy-loaded chat loader/skeleton, and a client-safe timestamp component.
AI page prefetch and prompt wiring
app/rgpv/[branch]/[semester]/[subject]/ai/page.tsx
Prefetches a cached explanation via generateTopicAnswer, derives follow-up/general prompts, and wires the new loader and "Follow-up Ideas" card.

Custom theme provider and layout toaster

Layer / File(s) Summary
Custom ThemeProvider implementation
components/theme-provider.tsx
Replaces next-themes wrapper with a custom localStorage/useSyncExternalStore-backed theme context and useTheme hook.
Theme consumer updates and layout script
components/mode-toggle.tsx, components/navbar.tsx, app/layout.tsx
Switches useTheme imports to the local provider and adds a theme-init script plus Sonner Toaster in the root layout.

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
Loading
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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: follow-up chat plus external AI continuation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch HyperAIWorkspace1

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@imuniqueshiv imuniqueshiv merged commit a8a7b23 into main Jul 6, 2026
4 of 6 checks passed
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🎉 Congratulations @imuniqueshiv!

Thank you for contributing to HyperLearningTech.

Your pull request has been successfully merged into main.

📦 Merge Summary

🚀 Keep Contributing

  • Follow the CONTRIBUTING.md guidelines.
  • Keep each Pull Request focused on a single feature or fix.
  • Run formatting, linting, type checking, and a production build before opening a PR.

Thank you for helping make HyperLearningTech better.

Happy Coding! 🚀

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 with Cmd + 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

continueWithExternalAI appears unused and diverges from the actual UI flow.

components/ai/continue-external-ai.tsx reimplements this copy+open+toast flow manually (with EXTERNAL_AI_OPEN_DELAY_MS and 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 value

Positional filtering of the cached explanation is fragile.

formatConversation drops the message at index === 0 only if it's an assistant message, relying on WorkspaceChat always seeding cachedExplanation as the very first message. This works today (per components/ai/workspace-chat.tsx's messages initializer), 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 win

No guard against rapid/duplicate clicks during the async copy.

handleContinue is async but the buttons stay enabled throughout, so multiple quick clicks (same or different providers) can queue several setTimeout-delayed window.open calls 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 win

No TTL on cached topic entries.

saveTopicCache writes to Redis with no expiration, so cache entries persist indefinitely (only removed via explicit forceRefresh/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

WorkspaceBody duplicates WorkspaceRequest from types/ai.ts instead of extending/reusing it.

types/ai.ts exports WorkspaceRequest { topicId, subjectCode, action, forceRefresh? }, but this route defines its own local WorkspaceBody with an overlapping-but-different field set (adds branch/semester/question/topic/module/cachedExplanation/messages). Since WorkspaceRequest isn'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 win

Request body is only shallow-cast, not runtime-validated.

(await request.json()) as WorkspaceBody provides no runtime guarantee about field types/shapes (e.g., messages entries' role isn't checked to actually be "user" | "assistant"), yet messages flows straight into the Gemini prompt via buildChatPrompt/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

📥 Commits

Reviewing files that changed from the base of the PR and between 6815998 and c3bb3ef.

📒 Files selected for processing (21)
  • app/api/ai/workspace/route.ts
  • app/layout.tsx
  • app/rgpv/[branch]/[semester]/[subject]/ai/page.tsx
  • components/ai/continue-external-ai.tsx
  • components/ai/external-ai-logos.tsx
  • components/ai/workspace-chat-loader.tsx
  • components/ai/workspace-chat-skeleton.tsx
  • components/ai/workspace-chat.tsx
  • components/ai/workspace-message.tsx
  • components/mode-toggle.tsx
  • components/navbar.tsx
  • components/theme-provider.tsx
  • lib/ai/chat-prompt-builder.ts
  • lib/ai/external-ai-prompt-builder.ts
  • lib/ai/external-ai.ts
  • lib/ai/followup-service.ts
  • lib/ai/metrics.ts
  • lib/ai/topic-cache.ts
  • lib/ai/topic-service.ts
  • types/ai.ts
  • types/topic-cache.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 with Cmd + 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

continueWithExternalAI appears unused and diverges from the actual UI flow.

components/ai/continue-external-ai.tsx reimplements this copy+open+toast flow manually (with EXTERNAL_AI_OPEN_DELAY_MS and 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 value

Positional filtering of the cached explanation is fragile.

formatConversation drops the message at index === 0 only if it's an assistant message, relying on WorkspaceChat always seeding cachedExplanation as the very first message. This works today (per components/ai/workspace-chat.tsx's messages initializer), 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 win

No guard against rapid/duplicate clicks during the async copy.

handleContinue is async but the buttons stay enabled throughout, so multiple quick clicks (same or different providers) can queue several setTimeout-delayed window.open calls 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 win

No TTL on cached topic entries.

saveTopicCache writes to Redis with no expiration, so cache entries persist indefinitely (only removed via explicit forceRefresh/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

WorkspaceBody duplicates WorkspaceRequest from types/ai.ts instead of extending/reusing it.

types/ai.ts exports WorkspaceRequest { topicId, subjectCode, action, forceRefresh? }, but this route defines its own local WorkspaceBody with an overlapping-but-different field set (adds branch/semester/question/topic/module/cachedExplanation/messages). Since WorkspaceRequest isn'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 win

Request body is only shallow-cast, not runtime-validated.

(await request.json()) as WorkspaceBody provides no runtime guarantee about field types/shapes (e.g., messages entries' role isn't checked to actually be "user" | "assistant"), yet messages flows straight into the Gemini prompt via buildChatPrompt/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

📥 Commits

Reviewing files that changed from the base of the PR and between 6815998 and c3bb3ef.

📒 Files selected for processing (21)
  • app/api/ai/workspace/route.ts
  • app/layout.tsx
  • app/rgpv/[branch]/[semester]/[subject]/ai/page.tsx
  • components/ai/continue-external-ai.tsx
  • components/ai/external-ai-logos.tsx
  • components/ai/workspace-chat-loader.tsx
  • components/ai/workspace-chat-skeleton.tsx
  • components/ai/workspace-chat.tsx
  • components/ai/workspace-message.tsx
  • components/mode-toggle.tsx
  • components/navbar.tsx
  • components/theme-provider.tsx
  • lib/ai/chat-prompt-builder.ts
  • lib/ai/external-ai-prompt-builder.ts
  • lib/ai/external-ai.ts
  • lib/ai/followup-service.ts
  • lib/ai/metrics.ts
  • lib/ai/topic-cache.ts
  • lib/ai/topic-service.ts
  • types/ai.ts
  • types/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
done

Repository: 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
done

Repository: 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/workspace accepts question indefinitely, 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: dangerouslySetInnerHTML is 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 generateTopicAnswer throws, the page still says the explanation is ready and passes follow-up prompts, but cachedExplanation is 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 useState initializer only runs on mount, so navigating between topics in the same client tree can keep the previous cached explanation and chat history while topicTitle/cachedExplanation props 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 question into follow-up mode, where cachedExplanation, topic, and module are required. This non-follow-up branch sends question without 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.

localStorage can 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 themeOverride forever after this tab calls setTheme, so later storage events update storedTheme but 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 || true

Repository: 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 -S

Repository: imuniqueshiv/HyperLearningTech

Length of output: 6615


Delayed opens can be blocked window.open is deferred by setTimeout, so the browser can lose the click’s transient activation and block the popup. ContinueExternalAI already shows a success toast on click, so surface a fallback when window.open returns null instead 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/FollowupResponse duplicate and diverge from @/types/ai.

types/ai.ts already exports FollowupRequest (identical shape) and FollowupResponse ({ success: boolean; answer?: string; error?: string }), but this file defines its own local versions instead. The local FollowupResponse ({ answer: string }) omits success/error, so the shared contract and the actual service return type have already diverged — app/api/ai/workspace/route.ts has to manually reconstruct success/mode/cached around 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 in lib/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.ts helper exporting generateWithRetry(ai, prompt, subjectCode, logLabel) and a GEMINI_MODEL constant, used by both topic-service.ts and followup-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/genai 2.10.0 generateContent support AbortSignal or a reliable per-call timeout option?

💡 Result:

Yes, the @google/genai SDK (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:


🏁 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.ts

Repository: imuniqueshiv/HyperLearningTech

Length of output: 4830


Add a timeout to generateContent
generateContent has no per-call timeout or abort signal here, so one stalled Gemini request can block each of the 3 retries. Pass AbortSignal.timeout(ms) or httpOptions.timeout on 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant