Skip to content

feat(web,api): Async initial question generation with generating state UI#66

Merged
shettydev merged 19 commits intomainfrom
refactor/thinking-map
Mar 31, 2026
Merged

feat(web,api): Async initial question generation with generating state UI#66
shettydev merged 19 commits intomainfrom
refactor/thinking-map

Conversation

@shettydev
Copy link
Copy Markdown
Owner

@shettydev shettydev commented Mar 30, 2026

Summary

  • Async initial question flow: Dialogue start now enqueues AI generation via BullMQ instead of returning a static question synchronously. The endpoint returns { jobId, position } (202 Accepted) for new dialogues, or { initialQuestion } for existing ones.
  • Generating state UI: ThoughtMapDialoguePanel shows a "Generating initial question..." loader while the AI produces the opening Socratic question via SSE.
  • QuestionNode animations: Reworked ghost node acceptance animation (ink stroke → fade → accept), entrance spring animation, and countdown sweep. Respects prefers-reduced-motion.
  • Ghost node layout improvements: Wider offset for ghost nodes, centered Y positioning, persisted positions across re-renders.
  • Runtime safety & race condition fixes: Replaced non-null assertions in API client with explicit guards, fixed Zustand race conditions using functional setState, conditional ghost removal on success only.

Changes

API (@mukti/api)

  • thought-map-dialogue.controller.ts — Start endpoint enqueues AI job for new dialogues, returns union response (async/sync)
  • thought-map-dialogue-queue.service.ts — Added processInitialQuestion worker path with dedicated prompt, SSE emissions, and usage event logging
  • prompt-builder.ts — Added buildThoughtMapInitialQuestionPrompt with sibling label context
  • dialogue-ai.service.ts — Handle empty user message for initial question generation
  • thought-map-dialogue.swagger.ts — Updated Swagger to document union response with 202 status for both paths
  • convert-canvas.dto.ts — Made title optional in ConvertCanvasDto
  • auth.controller.ts — Minor lint fixes

Web (@mukti/web)

  • ThoughtMapDialoguePanel.tsx — Added generating state detection (hasDialogue && !hasHistory)
  • use-thought-map-dialogue.ts — Seed React Query cache for async start, functional setState for isExplored, invalidate map detail on SSE complete
  • thought-map-dialogue.ts — Replaced ! assertions with explicit runtime guards for backend response validation
  • thought-map-store.ts — Conditional ghost removal on addNode success, functional setState for deleteNode rollback
  • ThoughtMapCanvas.tsx — Ghost node layout: wider offsets, centered Y positions, persisted positions ref
  • QuestionNode.tsx — Reworked animations: acceptance ink stroke, entrance spring, countdown sweep with reduced-motion support
  • thought-map-layout.ts — Updated layout spacing for ghost node offsets
  • thought-map.ts — Split StartDialogueResponse into async/sync union type

Tests

  • thought-map-dialogue.controller.spec.ts — Updated for async start behaviour, BYOK paths, error cases
  • thought-map-dialogue-queue.service.spec.tsNew: 9 tests for processInitialQuestion (SSE events, prompt building, message persistence, usage logging, error path)
  • thought-map-canvas.test.ts — Updated for new layout spacing
  • thought-map.dto.spec.ts — Accept omitted title in ConvertCanvasDto

Testing

  • bun nx run @mukti/api:test --testPathPattern="thought-map" — 11 suites, 160 tests pass
  • bun nx run @mukti/api:lint — 0 errors
  • bun nx run @mukti/web:lint — 0 errors
  • Manual: verify generating state shows in dialogue panel for new nodes
  • Manual: verify ghost node accept/dismiss animations

Screenshots (if applicable)

Checklist

  • Code follows project style guidelines
  • Tests added/updated
  • Documentation updated (if needed)
  • No breaking changes (or documented if any)

Additional Notes

Addresses all 4 Copilot review comments:

  1. Runtime validation replacing ! assertions in API client
  2. Functional setState to prevent Zustand race conditions
  3. Swagger docs updated for union response + 202 semantics
  4. Tests added for processInitialQuestion worker path

- Add mocks for getDefaultModel and thought node updateOne, set node
  depth/fromSuggestion in fixtures, and update assertions to ensure no
  synchronous assistant message is created.
- Verify enqueueMapNodeRequest is called with defaultModel and
  isInitialQuestion, and that the async response includes jobId and
  position.
- Enqueue server-side AI jobs to generate opening Socratic questions for
  empty node dialogues.
- Add buildThoughtMapInitialQuestionPrompt, processInitialQuestion
  handler and resolveSiblingLabels helper.
- Update controller to return 202 Accepted and mark the legacy
  generateThoughtMapInitialQuestion as deprecated.
Support async start responses when the AI initial question is generated
via the queue. Key changes:
- API/types: start response can be async (jobId, position) or sync
  (initialQuestion)
- Seed cache for async case with an empty messages page so SSE can
  stream the AI message when ready
- Optimistically mark node isExplored in the thought-map store on start
  to reflect backend persistence immediately
- Invalidate thought map detail on stream completion so worker-set node
  changes are reflected in the UI
Use dialogue presence (hasDialogue) to control the Start button and
display a 'Generating initial question…' loader while the AI produces
the first message
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates Thought Map node dialogue startup to support an async “initial question generation” flow (queue + SSE), and surfaces a “generating” UI state in the web dialogue panel while the AI produces the opening message.

Changes:

  • Introduces a sync/async union response for “start dialogue” (existing message returned immediately vs. {jobId, position} when queued).
  • Updates web hooks/components to seed caches appropriately and show a “Generating initial question…” state after dialogue creation.
  • Adds a dedicated queue worker path + prompt builder for AI-generated initial questions.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/mukti-web/src/types/thought-map.ts Splits start-dialogue response into async/sync interfaces and a union type.
packages/mukti-web/src/lib/hooks/use-thought-map-dialogue.ts Seeds React Query cache for async start; optimistically marks nodes explored; invalidates map detail on SSE complete.
packages/mukti-web/src/lib/api/thought-map-dialogue.ts Expands backend response parsing to handle optional jobId/position vs initialQuestion.
packages/mukti-web/src/components/thought-map/ThoughtMapDialoguePanel.tsx Shows generating state when dialogue exists but no messages have arrived yet.
packages/mukti-api/src/modules/thought-map/thought-map-dialogue.controller.ts Switches start dialogue to enqueue initial question generation and return 202 + job metadata.
packages/mukti-api/src/modules/thought-map/services/thought-map-dialogue-queue.service.ts Adds isInitialQuestion job routing and an initial-question processing pipeline (prompt + SSE message emission).
packages/mukti-api/src/modules/thought-map/tests/thought-map-dialogue.controller.spec.ts Updates controller tests for the new async start-dialogue behavior.
packages/mukti-api/src/modules/dialogue/utils/prompt-builder.ts Adds buildThoughtMapInitialQuestionPrompt and deprecates the old static initial-question generator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Group ghost suggestions by parent and compute centred Y positions
  using a new GHOST_VERTICAL_SPACING. - Prefer existing
  persisted/dragged positions when available to avoid jumps.
- Persist computed/dragged ghost positions in a ghostPositionsRef and
  update drag-stop handling to save ghost positions to the ref instead
  of calling updateNode.
- Introduce GHOST_HORIZONTAL_OFFSET = 360 and update the thought-map
  layout to clear the rendered width of wide TopicNode (max-w-[320px]).
- The 360px offset provides roughly 40px horizontal clearance given
  React Flow's top-left anchoring.
- Add tests for BYOK and server key model resolution
- Also cover errors when server key or user record is missing
- Fetch the user's encrypted OpenRouter key and preferences, decrypt and
  resolve the effective model via aiPolicyService.
- Fall back to the server OPENROUTER_API_KEY only if BYOK is absent.
  Throw 404 when user not found and 500 when no API key is available.
- Show "Generating initial question" loader when a dialogue exists but
  no messages yet.
- Only render the processing "Mukti is thinking..." loader when there
  are existing messages and processing is active.
- Add buildThoughtMapInitialQuestionPrompt and remove duplicate
  instance.
- Relocate and document centredYPositions helper for thought-map layout.

- Tidy auth imports, fix DTO decorator order, and cleanup web components
  (import order, strict equality check, braces)
- Add optional position parameter to acceptGhostNode and pass the
  ghost's canvas coordinates when accepting AI-suggested nodes.
- The backend create endpoint ignores position, so the client strips x/y
  before POSTing, overrides the returned node's position in the store,
  and then fire-and-forget updates the node to persist the intended
  position.
- Failures to persist are logged but do not block the UI.
- Split QuestionNode into GhostNode and AcceptedQuestionNode.
- Add framer-motion entrance and "ink solidifies" acceptance animations;
  introduce ENTRANCE_ANIMATION_WINDOW_MS and respect
  prefers-reduced-motion.
- Delay onAccept (500ms) to allow animations to complete.
Implement comprehensive unit tests for `processInitialQuestion` within
the `ThoughtMapDialogueQueueService`. These tests verify:
- Correct SSE event sequences (processing, progress, message, complete)
- Proper AI prompt construction with node context and sibling labels
- Scoped data persistence via `dialogueService.addMessage`
- Usage event tracking for initial question generation
- Error handling and SSE error event emission
- Correct technique selection logic based on node depth/type
Update the `ApiStartThoughtMapNodeDialogue` decorator to accurately
reflect the async (202 Accepted) and sync response paths for initial
Socratic question generation.
- Add validation for jobId and initialQuestion in thoughtMapDialogueApi
- Refactor useStartThoughtMapDialogue to use functional state updates
- Ensure ghost nodes are only removed on successful node creation
- Improve error rollback logic in thought-map-store delete method
@shettydev shettydev changed the title feat(web, components): Show generating state when creating dialogue feat(web,api): Async initial question generation with generating state UI Mar 31, 2026
@shettydev shettydev merged commit 486a637 into main Mar 31, 2026
2 checks passed
@shettydev shettydev deleted the refactor/thinking-map branch March 31, 2026 19:49
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.

2 participants