Commit Graph
28 Commits
Author SHA1 Message Date
LING DUANandwyuc 9a28ca39d5 feat(chat): add single PPT element references to Pi (#1224)
* feat(chat): add PPT element references to Pi

* fix(chat): include chart values in reference routing

* fix(chat): harden slide element reference evidence

* fix(playback): clear element reference on scene navigation

* fix(pi): preserve element grounding across child retries

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-08-29 14:38:15 +08:00
LING DUAN 2cbe4ae293 feat(pi): add Native Child whiteboard tools (#1152)
* feat(pi): add native whiteboard tools

* style(pi): format native whiteboard tests

* fix(pi): clarify whiteboard CAS handoff

* fix(whiteboard): reconcile runtime projection on open
2026-08-21 18:07:49 +08:00
LING DUANandwyuc 0d308ed78d feat(pi): add Native Child Web Search (#1133)
* feat(pi): add native child web search

* fix(pi): align native web search with provider system

* fix(pi): harden native web search configuration and cancellation

* fix(pi): guide Native Child web search selection

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-08-19 10:35:44 +08:00
LING DUAN 61a7154f07 feat(pi): add evidence-aware Director context runtime (#971)
* feat(pi): add evidence-aware Director context runtime

* fix(web-search): require completed native search

* fix(orchestration): preserve whiteboard across agent handoffs

* fix(pi): remove wrap-up turn bypass
2026-07-27 11:38:41 -04:00
3eea9dc542 feat(runtime): complete the #869 learner-data cutover — quiz + playback onto RuntimeStore (#955)
* feat(runtime): persist quiz attempts in RuntimeStore

* fix(runtime): coalesce quiz draft snapshots

* fix(runtime): recover concurrent quiz attempts

* fix(runtime): handle quiz completion races

* fix(runtime): commit quiz review atomically

* fix(runtime): dedupe concurrent quiz writes

* fix(runtime): drop stale quiz drafts

* fix: harden quiz runtime review recovery

* fix: serialize quiz attempt identity

* feat: read quiz state from runtime store

* fix: persist quiz retries before resetting

* fix: preserve authoritative quiz outcomes

* fix: preserve legacy quiz retries during cutover

* fix: reconcile legacy quiz snapshots safely

* fix: drain rollover quiz write queues

* fix: drain completed quiz retry queues

* fix: reuse concurrent quiz retries

* fix(quiz): preserve drafts across abrupt reloads

* fix(quiz): recover empty retry sessions

* fix(chat): abort stalled runtime state reads

* fix(quiz): expose queued phases to readers

* fix(quiz): retain concurrent writer tails

* fix(quiz): canonicalize retry branches

* fix(quiz): keep retry rollovers monotonic

* fix(quiz): validate skipped retry siblings

* fix(quiz): close read cutover races

* test(classroom): cover legacy quiz summaries

* fix(quiz): reset async consumers on scene changes

* fix(quiz): close scene transition windows

* test(pbl): cover launch freshness guards

* fix(quiz): close cutover concurrency gaps

* fix(quiz): harden retry and context freshness

* fix(quiz): reject malformed legacy answers

* fix(quiz): validate legacy answer values

* test(quiz): cover legacy multi-answer migration

* fix(merge): retire dead ChatRequestTemplate.storeState after quiz read cutover

Main's three call sites built static storeState blocks that runAgentLoopFn
never consumed (it always rebuilds fresh state via getStoreState); the quiz
read cutover replaced that callback with the async two-phase RuntimeStore
read, leaving the template field with zero consumers. Drop it.

* feat(storage): conform HTTP/PG backends and reference server to RuntimeAppendOptions

The quiz write path's expectedLastSeq / sessionTransition / RuntimeAppendConflictError
semantics existed only in the browser backend; server-backed deployments would
silently accept conflicting appends and leave completed sessions active. Forward
the options over the wire, detect conflicts atomically under the PG transaction,
map them to HTTP 409 RUNTIME_APPEND_CONFLICT, and rematerialize the typed error
client-side so quiz retry logic works across every backend.

Co-authored-by: Codex <codex@openai.com>

* fix(chat): Pi single requests build storeState via the async runtime quiz read

Pi bypasses runAgentLoop's per-iteration getStoreState and serializes the
request template straight to /api/chat/pi, which rejects bodies without
storeState. Extract the fresh-snapshot builder (async RuntimeStore quiz read
with the scene-transition guard) and call it on the Pi path too.

* ci: whitelist the runtime-data-cutover integration trunk for PR checks

* feat(runtime): playback cutover — cursor in KV, discussion facts in RuntimeStore (#956)

* feat(runtime): cut playback over to the runtime layer — cursor in KV, facts in RuntimeStore (#869)

The fourth and last runtime family. Consumed-discussion facts become
append-only 'playback' records folded into a set at read (at-least-once
appends, no conflict machinery); the resume cursor is device-scoped
last-write-wins KV per the amended #779/#869 split. sessionStorage keeps
same-tab priority; KV takes over on fresh tabs/reloads. The dead Dexie
playbackState machinery is retired, with a one-time lazy migration of any
legacy row (cursor half + facts half) before deletion, and stage deletion
now clears both the KV cursor and any unmigrated legacy row.

Co-authored-by: Codex <codex@openai.com>

* test(runtime): include playbackState in the stage-delete db mock

---------

Co-authored-by: Codex <codex@openai.com>

* fix(playback): persist discussion facts on every consumption path (#957)

* fix(playback): persist discussion facts on every consumption path (final-review P0+P1s)

- The engine now publishes a progress snapshot the moment a discussion is
  consumed (join / skip / unselected-agent auto-skip). onProgress otherwise
  fires before the discussion action executes and a discussion is the scene's
  last action, so the fact never reached persistence.
- Reads fold records across ALL playback sessions in the learner partition
  (mergeLearner deliberately preserves same-kind sessions from both keys).
- Legacy migration appends only not-yet-durable facts, so an interrupted
  migration resumes instead of dropping the tail.
- recordConsumedDiscussion reports durability; the component drops failed ids
  from its observed set so a later progress tick retries (at-least-once).

* test(e2e): live verification of the playback persistence chain

Seeds a deterministic stage straight into the Dexie DB, starts the lecture
via the canvas overlay, and asserts the full chain: discussion auto-skip
appends a discussionConsumed record to maic-runtime, the device cursor lands
in KV, and both survive a fresh browsing context (empty sessionStorage).

* refactor(playback): consumed-discussion state is volatile by decision — cursor-only persistence (#959)

Product ruling on #869's fourth family: playback learner state is front-end
ephemeral UX, not learner data. A re-shown proactive card auto-skips, joined
discussions' content already lives in chat runtime records, and no replay
export / analytics consumer exists — so durable facts bought nothing over
in-memory + same-tab sessionStorage. Drop lib/playback/runtime.ts and the
RuntimeStore facts wiring; keep the device-scoped KV resume cursor (the half
with real UX value), the engine's consumption-time progress snapshot (cursor
freshness), and the legacy Dexie retirement (cursor half migrates, row
deletes, consumed ids are dropped).

* fix(review): P3 pair from cross-review — scene-id boundary + sessionTransition 400 (#966)

* fix(review): scene-id boundary for quiz context + 4xx for malformed sessionTransition (P3 pair)

Review findings on #955: didActiveSceneRemainUnchanged compared the active
scene by object identity, so a store update reallocating the scene during
the async quiz read dropped the learner's graded answers from that turn's
request — the scene id is the real boundary. The records route now
classifies a malformed sessionTransition as a validation failure instead of
letting the store's throw surface as a 500.

* fix(playback): superseded-engine cursor guard + migration write-window recheck

Second-vendor review of the #959 shrink (requested after the cross-review
noted it had single-vendor coverage) found: an engine orphaned by a scene
switch during async lecture resume could pass the idle-only recheck, be
resurrected, and publish its old scene's progress over the new scene's
debounced cursor — the resume continuation now requires identity with the
installed engine, and onProgress drops snapshots from superseded engines.
The legacy cursor migration also rechecks KV immediately before its write
so a concurrent tab's newer cursor cannot be overwritten and orphaned by
the legacy-row delete.

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: Codex <codex@openai.com>

* fix(review): approval follow-up P3 nits (#967)

* release: v0.3.1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): approval P3 nits — ISO gate on sessionTransition, dead mocks, corrupt-timestamp guard

- The records route's sessionTransition guard now requires an ISO
  updatedAt (isIsoTimestamp), matching the sibling PATCH /status route
- Dead vi.mock factories for the deleted playback-storage module dropped
- A corrupt legacy playback timestamp falls back to 'now' instead of
  wedging migration into a permanent re-throw that disabled resume

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:28:04 +08:00
bb8d4f30e7 feat(chat): add experimental Pi classroom runtime behind a flag (#914)
* feat(chat): add experimental Pi classroom runtime

* fix(chat): harden Pi whiteboard action sequencing

* style(chat): format session retirement flow

* style(playback): format async scene transition

* fix(chat): retire actions on session errors

* fix(chat): close Pi runtime lifecycle gaps

* fix(chat): avoid replaying historical whiteboard actions

* fix(chat): prevent Pi structured-output residue from leaking and inflating turns

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(chat): expose whiteboard element/line ids and bound code-line context

Surface each element's id (and code line ids) in the Pi whiteboard context so
later child agents can target wb_delete / wb_edit_code, and cap the rendered
code lines with a shared character budget so a code-heavy board cannot grow the
prompt without bound. Return only the current turn's whiteboard ledger from the
Pi director loop instead of carrying history forward across requests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(chat): bound Pi whiteboard code context

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(chat): prioritize newer persisted whiteboard code in budget

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(chat): enforce whiteboard boundaries across Pi sessions

* feat(chat): refine soft-closing discussion controls

* fix(chat): make whiteboard clearing model-directed

---------

Co-authored-by: apple <apple@GigideMacBook-Air.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: LING-6150 <duan.lin@northeastern.edu>
2026-07-17 11:22:02 +08:00
wyuc 65bf20a84b feat(runtime): cut chat sessions over to RuntimeStore (#926)
* feat(runtime): persist chat sessions in RuntimeStore

* fix(runtime): harden chat persistence ordering

* fix(runtime): protect cross-tab chat snapshots

* fix(runtime): compare structured chat values safely

* fix(runtime): preserve chat backup and reset semantics

* fix(runtime): restore imported chat snapshots

* fix(runtime): isolate chat load failures

* fix(runtime): make cache clearing lossless

* fix(runtime): normalize empty chat saves

* fix(runtime): serialize legacy chat migration

* fix(chat): serialize backup restoration

* test(chat): cover multi-stage restore locking

* fix(chat): coordinate fallback migrations across tabs

* fix(chat): harden cross-tab migration locking

* fix(database): retire abandoned chat lease schema

* fix(chat): preserve monotonic local edits

* fix(chat): protect unseen runtime sessions

* fix(chat): order lifecycle transitions monotonically

* fix(chat): serialize runtime maintenance

* fix(chat): order queued writes before maintenance

* fix(chat): preserve cross-version lock compatibility

* fix(chat): preserve streamed and backup data

* fix(chat): isolate empty no-lock saves

* fix(chat): isolate partial runtime snapshots

* fix(runtime): close cross-store cutover races

* fix(runtime): close remaining cutover races

* fix(chat): retain caller-visible conflict baseline

* fix(chat): preserve read-only legacy autosaves

* fix(chat): avoid restore lock inversion

* fix(runtime): enroll writes before maintenance

* fix(runtime): retain maintenance ordering

* fix(runtime): close restore and deletion races

* fix(chat): order partition queues within lock epochs

* fix(chat): preserve recovery and deletion intent

* fix(runtime): recover interrupted chat restores

* fix(backup): scope chat deduplication by stage

* fix(backup): stage chats by runtime partition

* fix(backup): clear staged chats with stages
2026-07-16 18:30:40 +08:00
Yizuki_Ame 50d3abc32e fix(chat): preserve message line breaks (#908) 2026-07-13 14:27:50 +08:00
7fffec78f6 feat(choreography): shared orchestration spec in lib/choreography (#863) (#890)
* feat(choreography): shared orchestration spec in lib/choreography (#863)

Introduce lib/choreography/ as the single source of truth for the
orchestration semantics a faithful classroom-video exporter needs from
playback, so the app runtime and the exporter interpret one spec instead
of each re-implementing (and silently drifting from) the other.

Kept in lib/ rather than a package: these semantics co-evolve with the
playback engine, and the exporter will also live in the app, so both
consumers share them via ordinary imports. Purity is machine-enforced by
an eslint boundary on lib/choreography/** (blocks @/ host-app paths +
react/react-dom/gsap/framer-motion/motion), so the exporter can interpret
the spec in a pure Node environment.

- timing.ts   — timing constants + the deterministic no-audio speech
                estimate, moved verbatim from the engines.
- cursor.ts   — resolvePlaybackCursor + EMPTY_SCENE_DWELL, moved from
                lib/playback/engine-cursor.ts (typed on dsl SceneCore).
- timeline.ts — new pure resolveActionTimeline: index-domain -> time-domain
                expansion (blocking cursor-advance vs fire-and-forget visual
                duration), keyed off the DSL fire-and-forget partition.
- descriptors/— versioned, zod-schema-validated animation descriptors
                spotlight.v1 + laser.v1 (declarative: property/from/to/
                duration/easing; no implementation), pinned to the current
                overlay components.

Behavior-neutral engine refactor: lib/action/engine.ts and
lib/playback/engine.ts import from lib/choreography and the local literals
are deleted, so the timing dimension now has exactly one copy.

The spotlight/laser overlay components still hardcode their animation
values (they do not yet READ the descriptors) — tracked in #889.

Closes #863

* fix(choreography): address cross-review findings on resolveActionTimeline + descriptors

- P1: model implicit whiteboard auto-open — a wb_* mutation on a closed board
  now prepends a synthetic IMPLICIT_WB_OPEN (WB_OPEN_MS) beat, mirroring the
  engine's ensureWhiteboardOpen; open state carries across scenes and toggles
  on wb_open/wb_close (new `whiteboardOpen` option to seed it).
- P2: scale real speech audio duration by playbackSpeed too (live path sets
  AudioPlayer.setPlaybackRate), keeping it in lockstep with the estimate path.
- P2: express the spotlight mask relationship in the descriptor model —
  LayerSchema gains `role` ('content'|'mask') + `maskedBy` (subtract|intersect);
  spotlight.v1's cutout is now a mask layer the dim layer subtracts, so a
  non-React consumer reconstructs the cutout instead of a black rect.
- P3: wb_clear on an empty board is 0ms (engine early-returns), not wbClearMs(0).

Tests: tests/lib/choreography 45 pass (+6); engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): address final-audit findings on effect lifetime + spotlight descriptor

Second cross-review round (fresh-session codex final audit) surfaced three
deeper mismatches with live playback:

- P1: fire-and-forget effect lifetime is not a flat EFFECT_AUTO_CLEAR_MS. The
  engine's processNext clears effects at every scene boundary and on completion,
  and scheduleEffectClear uses one shared timer each new effect resets. Added
  clampFireAndForgetLifetimes: an effect's visual durationMs is now min(next
  scene boundary / completion, shared-timer deadline chained through later
  effects in the same scene). advancesCursorMs (0) is untouched.
- P2: spotlight dimness default is 0.5 (executeSpotlight: dimOpacity ?? 0.5; DSL
  documents 0.5), not the component's unreachable ?? 0.7 fallback. Fixed the
  descriptor param + test.
- P2: model the spotlight wrapper's enter/exit opacity fade (motion.div, no
  explicit duration → engine default). TrackSchema.durationMs is now optional to
  express "use the consumer's engine default"; dim layer carries the fade tracks.

Tests: tests/lib/choreography 50 pass (+5, incl. boundary-cut / completion-cut /
full-lifetime / shared-timer-extension / wrapper-fade); engine regression 28 pass;
tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): pin spotlight fade duration + laser dot geometry in descriptors

Third cross-review round (codex) flagged two descriptor-completeness gaps that
would make a non-Motion consumer (the exporter) diverge from the app:

- Spotlight wrapper fade: the enter/exit opacity tracks left durationMs implicit
  (Motion default). A literal consumer treats a missing duration as instant, so
  the spotlight would pop on/off. Pinned to Motion's default 300ms tween.
- Laser dot geometry: the descriptor captured only tracks, not the dot group's
  center anchor (translate -50%,-50%) or the rounded-full ring/core. A literal
  renderer would draw an offset 10px square. Added the static geometry (anchor,
  borderRadius 9999, ring inset/position) so the shape/position match the app.

Also refined the effect-lifetime docstring to cite the app's per-scene engine
teardown/completion (the actual clearEffects path) rather than an intra-engine
boundary gate that is dead in the single-scene-per-engine configuration.

The empty-scene "speech dwell → blank chat bubble" observation is pre-existing
behavior: EMPTY_SCENE_DWELL is a verbatim move from lib/playback/engine-cursor.ts
(unchanged from origin/main), out of scope for this move-only PR; tracked separately.

Tests: tests/lib/choreography 54 pass; engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): zero-duration for engine-skipped/no-op actions in timeline

Fourth cross-review round (codex) flagged two remaining timeline divergences:

- Skipped discussions: the engine skips a discussion outright (no timer) when
  it's already consumed or its agent isn't selected, but the timeline always
  charged DISCUSSION_TRIGGER_DELAY_MS. Added `isDiscussionSkipped` resolver
  (runtime-state-dependent, like getVideoDurationMs) → 0ms when skipped.
- No-op whiteboard draws: executeWbDrawText (empty content) and
  executeWbDrawTable (no rows/cols) return before any delay. The timeline now
  charges 0ms for these determinable-from-the-action no-ops instead of
  WB_DRAW_MS. (KaTeX-failure / missing-edit-target no-ops depend on runtime
  state and remain out of scope, consistent with the resolver pattern.)

Tests: tests/lib/choreography 56 pass; engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): exact-5s effect chain break + spotlight dim full-screen geometry

Fifth cross-review round (codex), both P2:

- Effect chain break at an EXACT 5s boundary: the earlier effect's clear timer
  is queued before the reading timer that triggers the later effect (same 5000ms
  delay), so it fires first — the predecessor is cleared at exactly deadlineMs,
  not extended. Changed the chain guard from `> deadlineMs` to `>= deadlineMs`.
- Spotlight dim layer full-screen geometry: the descriptor recorded only fill +
  mask relation, leaving a literal consumer no way to know the dim rect spans the
  0..100 viewport. Added explicit x/y/width/height (100×100 at origin) so the
  descriptor is self-contained.

Tests: tests/lib/choreography 57 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): no-op wb_edit_code resolver in timeline

Sixth cross-review round (codex), one P2: executeWbEditCode returns before its
delay when the edit can't apply (missing/non-code target, stale line refs). The
timeline always charged WB_EDIT_MS. Added `isEditCodeNoop` resolver (runtime-
state-dependent, same pattern as getClearElementCount / isDiscussionSkipped) →
0ms when the caller flags a no-op.

Tests: tests/lib/choreography 58 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): descriptor layer inheritance for nested effect layers

Seventh cross-review round (codex), two P2 with one root cause: the flat
layers[] model couldn't express a child layer riding a parent's animation
(the source nests some layers inside an animated wrapper). Added an
`inheritsFrom: {parentId, props}` relation to LayerSchema:

- Laser ring + core inheritsFrom the animated `dot` (left/top/opacity), so a
  literal consumer flies them in/out with the dot instead of leaving them at a
  static origin while only the dot moves.
- Spotlight border inheritsFrom `dim` (opacity), so the outline fades out with
  the wrapper instead of lingering after the dimming layer disappears.

Tests: tests/lib/choreography 60 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): address review — discussion auto-skip, explicit video policy, import allowlist

Human review (wyuc) raised two blocking timing issues + one non-blocking guard:

- Discussion dwell (blocking): a non-skipped discussion in unattended
  playback/export blocks for the trigger delay AND the ProactiveCard's own
  auto-skip countdown, not just DISCUSSION_TRIGGER_DELAY_MS. Added
  DISCUSSION_AUTO_SKIP_MS (5000) to the timing spec and charge
  DISCUSSION_TRIGGER_DELAY_MS + DISCUSSION_AUTO_SKIP_MS. ProactiveCard now reads
  the same constant (was a hardcoded 5000), so card countdown and timeline can't
  drift. A `spotlight -> discussion -> speech` timeline now extends the spotlight
  across the full discussion interval.
- play_video (blocking): an unresolved duration no longer silently becomes a
  0ms segment (which shifted every later action early). New
  `onUnresolvedVideoDuration` policy defaults to 'throw' (fail loudly); 'cap'
  assumes MAX_VIDEO_WAIT_MS, 'zero' opts back into no-dwell explicitly.
- Purity guard (non-blocking): turned the lib/choreography boundary into a true
  import allowlist. Beyond the existing @/… + render-package blocks, it now
  rejects parent-escape (../…) imports/re-exports, any bare package other than
  @openmaic/dsl / zod, and dynamic import()/require(). Negative-tested: ../store,
  a stray bare package, export * from ../playback, and import('react') all fail.

Tests: tests/lib/choreography 60 pass; engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-07-11 13:57:06 +08:00
danishsshaikh d00795094c feat(playback): add action-level navigation (#843)
Squashed from #843. Adds Notes click-to-jump action navigation and action-level playback resume/restore, with visible toolbar sentence controls removed after review.
2026-07-08 00:02:39 -04:00
Yanpeng Wangandwyuc b6fe2814d2 fix(quiz): stop leaking questions on entry and pass results to chat agent (#823)
* fix(quiz): stop leaking questions on entry and pass results to chat agent

Fixes #822.

Two related defects on quiz scenes:

1. The `quiz-actions` opening monologue could enumerate question stems,
   compare options, and trigger a `discussion` action that walked agents
   through the quiz BEFORE the learner had answered anything.

2. After the learner submitted, the chat/discussion agent never saw
   their answers, correctness, the canonical `analysis`, or the
   `aiComment` from `/api/quiz-grade` — so post-quiz feedback was
   generic and often re-explained the whole quiz from scratch.

Changes:

- `lib/prompts/templates/quiz-actions/{system,user}.md`: collapse the
  generator down to a brief 1-2 segment opening, forbid `discussion`
  actions in scene.actions, and add explicit "no preview, no answer,
  no detailed concept re-teach" safety rules.
- `lib/types/chat.ts` + `lib/chat/agent-loop.ts`: add optional
  `quizResults` to `StatelessChatRequest.storeState` /
  `AgentLoopStoreState` (sceneId + answers + per-question
  status/earned/aiComment).
- `components/chat/use-chat-sessions.ts`: hydrate `quizResults` from
  `readSubmittedState(sceneId)` once per agent-loop iteration (and on
  initial dispatch) so retries are picked up.
- `lib/orchestration/summarizers/state-context.ts`: when results are
  present, surface per-question student answer / correct answer /
  verdict / analysis / aiComment with a "address THIS student's
  mistakes" instruction; when unsubmitted, keep question visibility
  for clarifying questions but pin strict no-leak rules so a learner
  saying "I'm done" cannot bait the agent into reciting the quiz.

Discussion / multi-agent round-robin and the canonical session flow
are untouched.

* style: apply prettier formatting to state-context

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-07-01 14:50:15 +08:00
wyuc 0bcab10b83 fix(lecture-notes): render interactive-webpage widget actions in notes area (#810)
Interactive-webpage scenes (SceneType 'interactive') emit four widget_*
action types — widget_highlight, widget_setState, widget_annotation,
widget_reveal — each typically followed by a speech action. These execute
correctly on stage during playback but were dropped from the Lecture Notes
timeline due to two compounding gaps:

1. chat-area.tsx built lectureNotes with an allowlist that only admitted
   speech/spotlight/laser/play_video/discussion, filtering out all widget_*
   types so they never entered the notes data.
2. lecture-notes-view.tsx ACTION_ICON_ONLY had no entries for widget_*, and
   unknown types render as null, so even with data they would be invisible.

Add the four widget_* types to the allowlist and extend ACTION_ICON_ONLY with
icon/style entries (Highlighter/SlidersHorizontal/StickyNote/Eye), reusing the
existing inline icon-only badge pattern already used by spotlight/laser.
2026-06-29 04:33:22 -04:00
wyuc f064590a5e test+fix(orchestration): rip maxTurns; restore answer-the-user's-question behavior (#598 follow-up) (#599)
* test(orchestration): add premature-END regression eval (#598 follow-up)

Adds eval/orchestration/ following the outline-language pattern: a runner
that A/B-tests the director against the same scenario with two prompt
variants — current main (post-#554) and a synthesised pre-#554 baseline
that strips the role-aware summary labels AND the new system.md rules
10/11/12 together.

Pass criterion is now framed as a regression guard rather than a fixture
discrimination test: every scenario's post-fix END rate must stay below
EVAL_END_THRESHOLD (default 20%). The pre-vs-post Δ is reported as
informational data, since #554's reviewer feedback was that earlier
fixtures didn't discriminate.

Empirical finding across 7 shipped model configs (gpt-4.1-mini,
gpt-4o-mini, gpt-5.4-nano, qwen-plus, qwen3.5-flash, deepseek-chat,
deepseek-v4-flash, gemini-2.5-flash, claude-haiku-4-5) with 5 scenarios
modelled on #511 (incl. the exact Tiananmen-3D objection trace, soft
pushback after a long resolved-looking discussion, topic pivot, brief
acknowledgement, and explicit teacher-signals-end-then-user-objects):
no scenario produced a non-zero END rate in either variant. Useful
data for the #554/#598 discussion — the prompt-layer rules don't
measurably change behavior on these shipped models with these prompts,
but the eval is now wired so future regressions show up.

* test(orchestration): add director question-answering eval (#598 follow-up)

Adds an A/B eval probing whether the director routes correctly when the
conversation contains an unanswered user question — both in the first-turn
drift case (agents started answering but drifted onto adjacent topics,
no frustration yet) and in the escalated frustration case (user has
already complained multiple times).

The prod symptom this targets ("答非所问"): agents reply on adjacent
topics rather than answering the literal question, and the director
keeps picking peer agents for "variety" instead of routing back to
the teacher to actually answer.

Scenarios: 5 synthesized cases across math, biology, English grammar,
physics, and calculus. 3 are first-turn drift (no frustration yet),
2 include frustration signals. Each captures: user asks specific
question, agents reply on adjacent topics, director is asked to pick
the next move.

Decision rule-judged into USER | TEACHER | OTHER_AGENT | END:
  - USER       → ✓ valid (cue user to clarify)
  - TEACHER    → ✓ valid (re-route to teacher to answer)
  - OTHER_AGENT → ✗ wrong (peer-agent "variety" routing — the bug)
  - END        → ✗ wrong

A/B:
  - baseline  : current director template with rule 13 stripped
  - with_rule : current director template as-shipped

Pass = with_rule.correctRate ≥ EVAL_PASS_THRESHOLD (default 0.7).
Pre-vs-post Δ is reported as informational only.

Note this commit only adds the eval; the rule it strips/restores in
the A/B is injected in the next commit.

* fix(orchestration): inject director rule for answering user questions

Adds rule 13 to lib/prompts/templates/director/system.md so the director
recognizes when the conversation contains an unanswered user question
and routes appropriately — to the teacher (literal role match) by
default, or to USER cue when the question is ambiguous or the user
has already complained multiple times.

This addresses the "答非所问" symptom: when agents drift onto adjacent
topics, the director keeps picking peer agents for "variety" rather
than routing back to the teacher to actually answer. The rule is
preventive (applies whenever the question is unanswered, including
the first turn after drift) rather than reactive-only (waiting for
the user to express frustration).

Rule wording uses constrained-output framing: literal `role: teacher`
match (not "best fit" / "highest priority" / "most knowledgeable"),
and an explicit list of forbidden choices (peer agents, END). Earlier
softer wordings let the model pick the assistant when it seemed more
topically relevant; the literal-match wording prevents that.

Validated by the question-answering eval added in the previous commit.
On google:gemini-3-flash-preview, 5 samples per variant across 5
synthesized scenarios:

| Scenario                                           | baseline | with_rule |
|----------------------------------------------------|----------|-----------|
| math_quadratic_axis_drift_no_frustration           | 100%     | 100%      |
| bio_dark_reaction_drift_no_frustration             | 80%      | 100%      |
| english_team_isare_drift_no_frustration            | 20%      | 100%      |
| physics_inertial_mass_escalated_frustration        | 0%       | 100%      |
| calculus_product_rule_drift_no_frustration         | 60%      | 100%      |
| **mean**                                           | **52%**  | **100%**  |

3 of 5 scenarios reproduce the bug at baseline (correct rate ≤ 60%);
the rule lifts every scenario to 100%. The remaining 2 baselines are
already high — the rule doesn't regress them.

* fix(orchestration): teach agents to answer the user's literal question

Adds an "Answering the User's Question" section to the agent system
prompt. Before this, the agent SP had no rule for direct-answering — the
persona, role guideline, current-state context, and length guidelines all
pushed the agent to advance the lesson and inspire thought, with no
explicit directive to first answer the user's literal question.

The result observed in prod ("答非所问"): when the user asks a specific
question (a formula, a yes/no, a term), agents reply with adjacent
concepts, examples, or follow-up questions — but never the literal
answer.

The new section directs the agent, on every response where the user
message contains a question, to:
- lead with the concrete answer to the literal question,
- not pivot to an adjacent topic even when it seems pedagogically
  richer,
- treat "inspire thought" and peer-differentiation guidance as
  applying only AFTER the literal question is answered,
- say "I don't know" directly when uncertain rather than answering
  a different question,
- on frustration signals from prior turns, look back at the original
  question (before the frustration) and answer THAT specifically.

This is the agent-side counterpart to the director rule landed in the
previous commit. Director routes to the teacher when the question is
unanswered; the teacher now has explicit guidance for what to do when
dispatched.

* refactor+fix(orchestration): rip maxTurns end-to-end; distinguish errors from completion

Two coupled cleanups in one commit since the outcome-handler rewrite
naturally falls out of removing the client-side max-turns loop.

The director graph used `maxTurns: turnCount + 1` to force "one
director→agent cycle per request" — a hack expressing the single-round
contract as a turn limit. This commit removes the hack and the
surrounding user-facing setting, replacing implicit intent with explicit
graph topology.

Server:
- `director-graph.ts`: drop `maxTurns` from state, drop the hard-cap
  check at the top of `directorNode`, change `agent_generate → director`
  edge to `agent_generate → END`. Single-round contract is now expressed
  by topology.
- `lib/types/chat.ts`: drop `maxTurns` and `currentTurn` from
  `SessionConfig`; drop `maxTurns?` from `CreateSessionRequest.trigger`.

Client:
- `lib/store/settings.ts`: drop `maxTurns` state, `setMaxTurns` action,
  the `'10'` default, and the localStorage migration path.
- `components/agent/agent-bar.tsx`: drop the entire compact-stepper UI
  block plus the unused MessageSquare/Minus/Plus lucide imports.
- `components/settings/agent-settings.tsx`: drop the AgentSettings prop
  and the multi-agent-only Input block.
- `lib/chat/agent-loop.ts`: drop `maxTurns` parameter, change
  `while (turnCount < maxTurns)` to `while (true)` controlled by inner
  exits, drop `'max_turns'` reason from `AgentLoopOutcome`.
- `components/chat/use-chat-sessions.ts`: drop the maxTurns lookup, drop
  the trailing arg to runAgentLoop, drop vestigial `maxTurns: 0` /
  `currentTurn: 0` from session creation.
- `eval/whiteboard-layout/runner.ts`: drop unused MAX_AGENT_TURNS plus
  the trailing arg.

i18n: drop `settings.maxTurns` and `settings.maxTurnsDesc` from six
locales (en-US, zh-CN, zh-TW, ja-JP, ar-SA, ru-RU).

Motivation for removing the user-facing setting: the LLM director
controls round length via cue_user / END decisions, so a numeric
"max discussion turns" knob is redundant with rule-driven routing.
When the director prompt was buggy, the cap masked the bug by force-
ending; once director routing is fixed, the cap just removes user
agency.

Before, every non-`cue_user` outcome of the agent loop mapped to
`status: 'completed'` in `use-chat-sessions.ts`. That conflated four
distinct paths:
- `end`: director chose END (legitimate completion)
- `empty_turns`: two consecutive agents returned no text or actions
  (parser failure, model timeout, etc.)
- `no_done`: SSE stream ended without a `done` event (infrastructure
  failure)
- `aborted`: user pressed stop (handled elsewhere)

The new switch routes `end` to completed, `empty_turns` and `no_done`
through `clearLiveSessionAfterError` so the user sees an inline System
message describing what went wrong instead of a quiet "completed".

- `lib/types/chat.ts`: add `'error'` to `SessionStatus`.
- `components/chat/use-chat-sessions.ts`: rewrite the outcome handler
  as a switch; have `clearLiveSessionAfterError` set `status: 'error'`
  so all entry points (the runAgentLoop outcome AND existing throw
  paths from fetchChat / SSE error events) converge on the same
  visible state.
- `components/chat/session-list.tsx`: add `AlertCircle` red icon for
  the new error status.
- i18n: add `chat.error.emptyAgentResponses` and
  `chat.error.streamInterrupted` strings to all six locales.

This is the in-product fix that the prod investigation surfaced as
an independent root cause of the #511 "discussion ended unexpectedly"
symptom — separate from the director and agent prompt fixes in the
earlier commits of this PR.

* fix(chat): drop dead settingsState binding; wire eval:orchestration:answering script

Two minor CR follow-ups:

- components/chat/use-chat-sessions.ts: the `settingsState` lookup at the
  top of runAgentLoopFn was the entry point for the maxTurns lookup
  ripped in the previous commit; with maxTurns gone, the binding is
  unused and ESLint flags it. Drop the line. (Other uses of
  useSettingsStore.getState() elsewhere in this file are unaffected.)

- package.json: the answering eval runner had no convenience script,
  while the premature-end runner did. Add eval:orchestration:answering
  for symmetry and discoverability.

* fix(orchestration): route ambiguous/unaddressed user turns to teacher, never USER cue

Addresses reviewer issues 2 & 3 (ambiguous "帮我看一下这个" / "Can you take a
look at this?"): the director was cueing USER for vague input, which makes no
agent speak — the user faces dead air with nothing to react to ("nobody
responds"); and when an agent did speak it guessed a topic instead of asking
what the user meant.

Root cause was the previous rule 13, which offered `{"next_agent":"USER"}` as
a valid response to an ambiguous user turn. A USER cue is the wrong tool here:
it returns control to the user without any agent speaking. The right behavior
is to dispatch the teacher, who either answers or — if the message is too
vague — asks a clarifying question out loud.

Director (`director/system.md` rule 13):
- When the most recent user turn is unaddressed (clear question, vague
  request, OR frustration signal), the only acceptable output is the literal
  `role: teacher` agent id.
- USER cue is now explicitly forbidden as a response to a user turn (reserved
  for rule 7, where an agent has just asked the user something).
- Tightened the END prohibition: a high turn count or a thoroughly-discussed
  topic does NOT mean the specific question was answered. Added rule 5
  (don't-drag-on) to the override list, since that pressure was driving the
  director to END on well-discussed-but-unanswered turns.

Agent (`agent-system/system.md`):
- Added a clause to "Answering the User's Question": if the user's message is
  too vague to answer (no clear referent), ask ONE short clarifying question
  with a concrete option or two — do not guess a topic and lecture, do not
  stay silent.

Eval (`answering-runner.ts` + `scenarios/answering.json`):
- Reclassified USER as wrong (dead air); only TEACHER counts as correct now
  (teacher answers, or asks a clarifying question for vague input).
- Added two ambiguity scenarios: a bare "帮我看一下这个" opener, and a vague
  "再帮我看看这个吧" follow-up after a real Q&A.
- Made the rule-13 strip regex match by rule number rather than heading text,
  so it survives heading rewrites.

On google:gemini-3-flash-preview, 10 samples per variant, all 7 scenarios
PASS (with_rule mean ~97%, baseline ~57%). The two ambiguity scenarios:
ambiguous_look_at_this 100%/100%, ambiguous_vague_followup 20%→100%.
Manually verified the agent side: for "帮我看一下这个" the teacher now replies
"…你想讨论的是具体的代码逻辑,还是 Agent 的决策流程图呢?" (clarifying question +
concrete options) instead of guessing.

Note: reviewer issue 1 (follow-up not answered) could not be reproduced on
this branch across 7 configurations; it is consistent with the deployed
environment running older code and/or provider API flakiness, which the C7
error-vs-completion distinction in this PR already surfaces instead of a
silent 已结束.

* docs(eval): fix stale physics scenario description after USER→wrong reclassification

The physics_inertial_mass scenario description still said "USER-cue signal —
but TEACHER re-answer is also valid", which contradicts the new classifier
(USER is dead air = wrong; only TEACHER is correct). Cosmetic only — the
description field is printed into the report, never used in classification.
2026-05-29 10:52:30 +08:00
wyucand杨慎 e613b7578f [codex] add per-model thinking config (#494)
* add per-model thinking config

* Refine thinking model controls

---------

Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-04-26 19:41:10 +08:00
ea0e8126a5 feat: whiteboard layout quality eval harness (#425)
* feat(eval): add state manager bridging ActionEngine for eval

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): add shared types for whiteboard layout eval harness

* feat(eval): add SSE chat client for whiteboard eval

* feat(eval): add Playwright capture module for whiteboard screenshots

* feat(eval): add VLM scorer for whiteboard layout evaluation

* feat(eval): add report generator for whiteboard eval results

* feat(eval): add 8 constructed scenarios for whiteboard layout eval

* feat(eval): add minimal whiteboard render page for Playwright screenshots

Creates app/eval/whiteboard/page.tsx — a headless client page that
seeds the stageStore with a synthetic slide scene, exposes
window.__setElements() for Playwright to inject PPTElement[], and
renders them via ScreenElement inside a 1000×562.5px white canvas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): add main runner for whiteboard layout eval

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): add eval:whiteboard script, install tsx, gitignore results

* fix(eval): fix TS errors, lint, and prettier formatting

* fix(eval): address code review — add cue_user/empty turn guards, validate VLM output, fix empty array crash

* refactor(eval): replace synthetic scenarios with realistic ones

- Replace 8 generic scenarios with 6 that match real usage patterns
- Multi-agent discussion with short user replies (嗯, 明白了, 继续)
- Include real slide scene data as initialStoreState
- Generated agent configs with Chinese names and proper roles
- Cover: physics, math, finance, primary school, economics, medical

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract shared agent loop from use-chat-sessions

Extract the core agent loop logic into lib/chat/agent-loop.ts as a pure
async function with callback injection. Both the frontend React hook and
the eval harness now share the same loop — SSE parsing, exit conditions
(END/cue_user/empty turns/max turns), and director state accumulation.

The frontend wires StreamBuffer callbacks for UI pacing; the eval wires
ActionEngine + message accumulation for headless execution. If loop logic
changes in the shared module, both consumers automatically stay in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(eval): use project LLM infrastructure, fix eval page and model config

- Rewrite scorer to use resolveModel() + generateText() from AI SDK
  instead of raw fetch — supports all providers (OpenAI, Google, Anthropic)
- Model config via env vars (EVAL_CHAT_MODEL, EVAL_SCORER_MODEL),
  matching the pattern from outline-language eval
- Fix eval page: bootstrap store before SceneProvider mounts
- Fix __dirname for tsx CJS mode
- Remove --api-key/--scorer-model CLI args (use env vars instead)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): organize results by model/timestamp

* fix: remove double turnCount increment in shared agent loop

The extracted agent-loop.ts had both `turnCount = directorState?.turnCount ?? turnCount + 1`
(line 190) and a redundant `turnCount++` (line 215), causing multi-agent scenarios to hit
maxTurns at half the expected number of iterations.

Also removes unused processSSEStream import from use-chat-sessions.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): organize screenshots by scenario subdirectory

Results structure: results/<model>/<timestamp>/<scenario>/run0_turn1.png
Report files stay at the timestamp level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): revise scorer rubric and add rescore mode

- Replace space_utilization with rendering_correctness and content_completeness
- Rubric now evaluates from a teacher's perspective (empty space is normal)
- readability emphasizes font size consistency
- Add --rescore flag to re-score existing screenshots without re-running chat
- Increase maxOutputTokens to 2000, add JSON parse error recovery
- Score errors no longer abort the entire scenario

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(eval): sharpen scorer rubric with teacher-perspective examples

The rubric now catches specific classroom whiteboard failure modes:
- overlap now explicitly penalizes writing over existing content when empty space is available (spatial planning failure)
- rendering_correctness calls out diagram accuracy (e.g., parabola drawn as V-shape), raw subscripts (G_x), Chinese inside LaTeX math mode
- content_completeness emphasizes canvas edge clipping and bare unlabeled diagrams
- readability penalizes text styled as UI components (gray card backgrounds)
- overall instructed to weight overlap and rendering_correctness more heavily
- explicit note to ignore the "N" page UI element

Also increases maxOutputTokens to 3000 since longer rubric produces longer justifications.
Reporter now guards against null scores (scorer failures no longer crash report generation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review findings

Critical fixes:
- use-chat-sessions: restore agent_end handling, currentMessageId fallback
  for text_delta/action events with missing messageId, and re-throw on
  SSE error events (previously silently pushed to buffer only).
- eval runner: serialize ActionEngine executions via promise chain.
  void-fire-and-forget raced with ensureWhiteboardOpen's 2s delay and
  could insert elements out of order or before the first element was
  committed to the store.

Important fixes:
- CHAT_MODEL default: 'openai/gpt-4o-mini' -> 'openai:gpt-4o-mini'
  (parseModelString splits on ':', not '/').
- CheckpointResult.score is now VlmScore | null; removed the
  'as unknown as' cast that hid the null contract from consumers.
- Delete dead code: eval/whiteboard-layout/chat-client.ts and
  components/chat/process-sse-stream.ts (both unused after the
  shared agent loop refactor).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-04-18 15:29:54 +08:00
杨慎andClaude Opus 4.6 c75cf6e602 feat: add code element support for whiteboard (#385)
* feat: add CODE element type and wb_draw_code/wb_edit_code action types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: implement wb_draw_code and wb_edit_code in ActionEngine

* feat: add BaseCodeElement with Shiki syntax highlighting and animations

* feat: register CodeElement in renderer and add inline action tags

* feat: add wb_draw_code and wb_edit_code to agent action schemas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add code element context to director prompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add code element guidelines and context to prompt builder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add i18n keys for code element

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent whiteboard zoom from intercepting code element scroll

* chore: remove unused code element i18n keys

The code.untitled and code.lines keys were added but never referenced
by the BaseCodeElement component. Remove to reduce i18n maintenance
burden and merge conflict surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:57:51 +08:00
2c26e3a461 feat: Ollama local LLM support (#94)
* feat: Ollama local LLM support

- Ollama as a built-in provider (OpenAI-compatible, no API key needed)
- 8 default models: llama3.3, llama3.2, qwen2.5, qwen2.5:32b, mistral, gemma3, deepseek-r1, phi4
- Keyless provider activation: providers with only a baseUrl (no API key) now activate correctly
- isProviderKeyRequired() helper for consistent key validation across routes
- i18n strings for Ollama (zh-CN + en-US) and missing Doubao strings
- SSRF guard documentation clarifying server-configured URLs bypass safely

Previously, clients could send `x-requires-api-key: false` (header) or
`requiresApiKey: false` (body) to skip API key validation for ANY provider,
including ones like OpenAI that require keys. This meant a client could
consume server-configured credentials without authorization.

Fix: the server now derives requiresApiKey exclusively from its own provider
registry. The client header and body field are removed from all server-side
parsing. For Ollama, the registry says `requiresApiKey: false` so it works
keyless. For OpenAI, the registry says `true` so the check is enforced
regardless of what a client sends.

* fix: address reviewer feedback on Ollama integration

- Fix env-var activation so OLLAMA_BASE_URL alone activates the provider
- Move SSRF bypass comment from ssrf-guard.ts to resolve-model.ts
- Add ollama and doubao provider names to i18n locale files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fixed issues from reviewer notes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: format provider-config.ts with Prettier

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-04-09 21:07:35 +08:00
杨慎andClaude Opus 4.6 ad9e0ee7c6 refactor(i18n): migrate to i18next framework (#331)
* refactor(i18n): migrate to i18next framework

Replace hand-rolled i18n with i18next + react-i18next so that adding a
new language only requires dropping a JSON file in lib/i18n/locales/.

- Add i18next, react-i18next, i18next-browser-languagedetector deps
- Generate zh-CN.json / en-US.json from existing TS translation modules
- Rewrite lib/i18n/index.ts as a thin wrapper around i18n.t()
- Rewrite use-i18n hook to delegate to useTranslation(); external API
  (locale, setLocale, t) is unchanged so consumers need no changes
- SSR-safe: LanguageDetector only loaded on client side

Closes #327

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(i18n): use interpolation for greeting to support natural phrasing

Replace string concatenation (greeting + displayName) with two
i18next keys: greetingWithName (with {{name}} interpolation) and
greetingDefault (standalone, no name).

This lets each locale choose natural phrasing independently:
- zh-CN: "嗨,同学" / "嗨,Alice"
- en-US: "Hi there" / "Hi, Alice"
- Future locales can avoid gender issues by choosing genderless defaults

Also widen the t() type signature to accept interpolation options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(i18n): auto-discover locale files via dynamic import

Replace hardcoded zh-CN/en-US imports with i18next-resources-to-backend
and dynamic import(`./locales/${language}.json`). Bundler scans the
locales/ directory at build time, so adding a new language now requires
only dropping a JSON file — zero changes to existing code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(i18n): resolve hydration mismatch by deferring language detection

LanguageDetector ran during i18next init(), detecting browser language
before React hydrated — server rendered zh-CN while client switched to
en-US immediately, causing a hydration mismatch.

Fix: remove i18next-browser-languagedetector; init with a fixed lng
(zh-CN) so server and client agree on the first render. Language
detection is now done in I18nProvider's useEffect after hydration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(i18n): remove hardcoded locale list from language detection

Replace manual locale validation and startsWith('zh') prefix matching
with i18next's built-in fallback mechanism. Now changeLanguage() is
called with navigator.language directly — if the exact locale has no
JSON file, i18next automatically falls back to fallbackLng.

Also widen Locale type from union to string so adding new languages
doesn't require modifying types.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(i18n): add course language config with 11 languages and UI linkage

- Add lib/i18n/course-languages.ts with curated language list
  (zh-CN, zh-TW, en-US, ja, ko, fr, de, es, pt, ru, ar) including
  native labels and English prompt names
- Course language defaults to UI locale on first visit; once user
  explicitly picks a language, that choice persists across sessions
- Replace toggle button with dropdown selector showing native labels
- Widen language types from 'zh-CN'|'en-US' to string throughout
- Fix hardcoded language ternaries in LLM prompt injection:
  - prompt-builder.ts: use getCourseLanguagePromptName()
  - classroom-generation.ts: remove normalizeLanguage() that forced
    all non-English to zh-CN
  - PBL system prompt, agent templates, generate-pbl: append language
    instruction for non-zh/en languages
  - quiz-grade API: add language suffix for grading feedback

Closes #327

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(quiz): use course language instead of UI locale for grading

quiz-view was passing the UI locale to the grading API, causing AI
feedback to follow the student's answer language instead of the course
language. Now reads stage.language from the store.

Also strengthen the grading prompt: explicitly instruct the LLM to
write comments in the course language regardless of the student's
input language.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert: remove course language config and quiz fix from i18next branch

Reverts f7bd6bd and 5bb5133 — these are independent features that
should go into a separate branch/PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(i18n): regenerate locale JSON after merging latest main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(i18n): remove obsolete TS translation files

These files are replaced by lib/i18n/locales/*.json and are no longer
imported anywhere in the codebase.

Removed: chat.ts, common.ts, generation.ts, settings.ts, stage.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(i18n): fix prettier formatting in locale JSON files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(i18n): replace hardcoded language options with central locale registry

Add lib/i18n/locales.ts as a single source of truth for supported languages.
Language selectors in header and homepage now render dynamically from this
registry. Also adds supportedLngs to i18next config so unsupported languages
fall back correctly. Adding a new language now only requires a JSON file and
one line in the registry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(i18n): add missing rename keys to JSON locale files

Add classroom.rename, classroom.renamePlaceholder, and
classroom.renameFailed that were added to generation.ts on main
but missing from the JSON locale files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(i18n): extract LanguageSwitcher, restore type safety, fix locale resolution

- Extract shared <LanguageSwitcher /> component from page.tsx and header.tsx
- Derive Locale type from supportedLocales registry (as const satisfies)
- Add resolveLocale() to match browser language prefixes (e.g. 'en' → 'en-US')
- Remove config.ts changes (nonExplicitSupportedLngs + inline resources) that
  broke translation loading when combined with resourcesToBackend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(i18n): use i18next double-brace interpolation instead of manual .replace()

- Convert all {var} placeholders in locale JSONs to {{var}} (i18next default)
- Replace .replace('{var}', value) calls with t(key, {var: value}) in all
  consuming components: lecture-notes-view, pbl-renderer, use-pbl-chat,
  generation-preview, whiteboard-history, agent-settings
- Widen t() type signature in handleIssueComplete to accept interpolation options

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(i18n): unify greeting with default nickname, add translation guide

- Remove greetingDefault key; greeting always uses greetingWithName
  with displayName (falls back to profile.defaultNickname)
- Change en-US defaultNickname from "Student" to "Learner"
- Add TRANSLATION_GUIDE.md documenting how to add languages and
  explaining keys with non-obvious UX design intent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:04:42 +08:00
杨慎andClaude Opus 4.6 59e6d2f734 fix(roundtable): pause TTS and bubble text instead of interrupting when input box opens (#295)
* fix(roundtable): pause TTS and bubble text instead of interrupting when input box opens (#294)

Replace doSoftPause() with Level-1 pause (buffer tick + TTS audio freeze)
when the user opens the input box during a live discussion, so the SSE
stream continues buffering. When the user actually sends a message,
upgrade to a full soft-pause and flush the TTS queue to prevent stale
audio from leaking into the next round.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(roundtable): ensure pause UI stays in sync with buffer state (#294)

- pauseActiveLiveBuffer() now returns boolean; UI state
  (isDiscussionPaused) is only set when the buffer is actually paused
- Remove !isDiscussionPaused guard from onInputActivate to avoid
  stale closure preventing re-pause after message send
- Unconditionally clear isDiscussionPaused in onMessageSend to handle
  voice input's stale onTranscription closure
- Move livePausedRef assignment after buffer validity check to prevent
  phantom pause inheritance on subsequent buffers
- Remove unused doSoftPause (no longer called from any path)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:16:34 +08:00
wyucandClaude Opus 4.6 815cdc22fc fix: presentation mode bubble polish and TTS timing improvements (#244)
* fix: use fixed width for normal mode speech bubble to prevent streaming jitter

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add click-to-pause, AudioIndicator, and play/pause icon to presentation bubbles

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: anchor ProactiveCard to agent avatar in presentation mode dock

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bubble click events, ProactiveCard anchor timing, and dock sidebar overlap

- Add pointer-events-auto to presentation bubbles so clicks pass through
- Use agent avatar ref directly for ProactiveCard anchor (removes fallback)
- Shift dock left when chat sidebar is open to prevent overlap

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: align presentation bubble icon with roundtable breathing bars and hover behavior

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent play/pause icon from overlapping bubble text

Add pr-10 padding to text area and align icon position with roundtable style.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace AudioIndicator bars with volume icon and loading spinner in presentation bubbles

Avoids visual conflict with the breathing bars play/pause icon at bottom-right.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace AudioIndicator bars with volume icon and loading spinner in roundtable bubbles

Syncs with presentation bubble style for consistency.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: add breathing opacity animation to volume icon during TTS playback

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: use ripple wave animation for volume icon during TTS playback

Three concentric rings expand outward from the volume icon to indicate
active audio playback. Replaces the opacity breathing animation.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent blank flash between same-agent speech segments

Only clear bubble text on agent_start when switching to a different agent.
Same-agent continuation keeps the previous text visible until the next
segment starts revealing, avoiding a lingering first bubble and blank gap.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent TTS queue from blocking text reveal across segment boundaries

The shouldHold() callback checked both isPlaying and queue.length, which
meant the buffer stayed stuck on segment 1's text while segment 2's TTS
was generating/playing. Now shouldHold() only checks isPlaying.

Also changed queueMicrotask to setTimeout(0) in audio.ended callbacks
so the StreamBuffer's 30ms tick has a chance to observe isPlaying=false
and release its hold before processQueue sets it back to true.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: release StreamBuffer hold per-segment instead of waiting for entire TTS queue

Previously, shouldHold() returned a single boolean based on global TTS
state (isPlaying || queue.length > 0). When speech was split into
multiple segments by an action, the buffer would hold on Segment 1's
text even after its audio finished, because Segment 2's audio started
immediately.

Now shouldHold() returns a segmentDone counter that increments each
time a segment finishes. The buffer snapshots this counter when hold
begins and releases as soon as it changes, allowing text to advance
to the next segment even if its audio is already queued.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: replace ripple circles with sound wave arcs on volume icon

Three SVG arcs emanate from the right side of the volume icon with
staggered opacity animation, resembling speaker sound waves.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: simplify volume icon to static display without animation

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: enable scrolling in presentation bubble when text overflows

Replace overflow-hidden with overflow-y-auto and scrollbar-hide to
maintain proper bottom padding when content exceeds max height.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase bottom padding and max height for presentation bubble text area

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: pause and resume TTS audio when discussion is paused/resumed

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: defer TTS playback when paused during generation

When discussion is paused while TTS is still fetching, the audio
element is created but not played. Resume will start it.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: check holding flag before segmentDone counter in TTS hold logic

The object returned by shouldHold is always truthy, so the falsy
release path was dead code. Now checks result.holding first: if
false (queue empty), release immediately. Only use segmentDone
counter comparison when holding is true.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore ProactiveCard anchor fallback to dock container

When agent avatar hasn't mounted yet, fall back to dock container
so the card can still position and render.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: stop TTS playback and abort active SSE stream on stage unmount

Previously, leaving a classroom while a discussion was active would
leave TTS audio playing and the API call streaming in the background.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: abort active SSE stream and shutdown buffers on chat sessions unmount

When navigating away from a classroom (e.g. clicking the back button),
the chat sessions hook now aborts any active API request and shuts
down all stream buffers. This prevents orphaned SSE connections and
runaway agent loops after leaving the stage.

Refs #236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review feedback

- Reset pausedRef and segmentDoneCounterRef in cleanup() to prevent
  stale state on hook reuse
- Remove unused audioAgentColor prop from PresentationBubbleCard
- Capture chatAreaRef.current to local variable in Stage unmount effect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:32:14 +08:00
Yizuki_AmeandYizukiAme 0533adbe82 feat: refine presentation mode speech bubbles, input flow, and accessibility (#195)
* feat: add presentation mode with fullscreen, idle-hide, and keyboard navigation

- Fullscreen via toolbar button or F11; exit via ESC/F11/button
- Header auto-hides, sidebars collapse, slide fills viewport
- Idle auto-hide (3s): toolbar/avatars fade out, speech bubble stays visible
- Smart suspension: idle-hide pauses during typing/recording/voice input
- Keyboard navigation: Arrow keys (prev/next), Space (play/pause), ESC (exit)
feat: add presentation mode with fullscreen, idle-hide, and keyboard navigation

- Fullscreen via toolbar button or F11; exit via ESC/F11/button
- Header auto-hides, sidebars collapse, slide fills viewport
- Idle auto-hide (3s): toolbar/avatars fade out, speech bubble stays visible
- Smart suspension: idle-hide pauses during typing/recording/voice input
- Keyboard navigation: Arrow keys (prev/next), Space (play/pause), ESC (exit)
- F11 intercepted to use Fullscreen API (ESC-friendly) instead of browser native
- Whiteboard hints reposition from bottom to top corners in fullscreen
- i18n: fullscreen/exitFullscreen keys (zh-CN + en-US)

Closes #102

.git/COMMIT_EDITMSG [unix] (14:47 22/03/2026)                                                                    1,1 Top
feat: add presentation mode with fullscreen, idle-hide, and keyboard navigation

- Fullscreen via toolbar button or F11; exit via ESC/F11/button
- Header auto-hides, sidebars collapse, slide fills viewport
- Idle auto-hide (3s): toolbar/avatars fade out, speech bubble stays visible
- Smart suspension: idle-hide pauses during typing/recording/voice input
- Keyboard navigation: Arrow keys (prev/next), Space (play/pause), ESC (exit)
- F11 intercepted to use Fullscreen API (ESC-friendly) instead of browser native
- Whiteboard hints reposition from bottom to top corners in fullscreen
- i18n: fullscreen/exitFullscreen keys (zh-CN + en-US)

Closes #102

.git/COMMIT_EDITMSG [unix] (14:47 22/03/2026)                                                                    1,1 Top
- F11 intercepted to use Fullscreen API (ESC-friendly) instead of browser native
- Whiteboard hints reposition from bottom to top corners in fullscreen
- i18n: fullscreen/exitFullscreen keys (zh-CN + en-US)

Closes #102

* feat: refine presentation mode speech bubbles, input flow, and accessibility

- User messages now display as right-side speech bubbles for send confirmation
- 'Your turn' cue is a clickable glass button respecting ASR preference
- Cue properly excludes bubbleRole/thinkingState to avoid UI overlap
- Right-side dock + bubble uses flexbox for dynamic stacking
- Voice-to-text switch now stops recording to prevent mic leaks
- Backdrop no longer blocks toolbar (bottom-14 cutoff)
- User message clears immediately when agent starts responding
- Dock buttons and avatar have proper aria-labels and button semantics
- Voice panel mic trigger converted from div to button with aria-label
- Added 5 i18n keys (zh-CN + en-US) for new accessible labels
- matchesSide typed as boolean, removed unused userAvatar from left overlay
- Avatar alt text localized via i18n

Refs #102

* fix: address code review audit  cancel ghost sends, consolidate getAgentConfig, fix a11y

* fix: guard concurrent voice recording and suppress cancel error toast

* style: vertically center placeholder text in presentation input bar

* style: full light-mode adaptation for presentation UI, toolbar dividers, bubble opacity 60%

* chore: address #129 follow-up  remove orphan i18n keys, clarify waitUntilDrained pause behavior

* fix: resolve spacebar conflict and address review feedback

- Stage: skip Space during active QA/discussion (let Roundtable own
  buffer-level pause); deterministic, no listener-order dependency
- Stage: wrap handlePlayPause in useCallback([playbackCompleted,
  currentScene]) to avoid keyboard listener re-registration
- Stage: clear presentationIdleTimerRef on unmount
- PresentationBubbleCard: add aria-live='polite' for screen readers
- Extract shared avatar constants to roundtable/constants.ts

* style: format with prettier

* fix: render ProactiveCard inside fullscreen container via portalContainer prop

* feat: show requesting agent avatar in dock on discussion request

---------

Co-authored-by: YizukiAme <yizukiame@github.com>
2026-03-24 13:04:55 +08:00
Yizuki_Ame 2841ce6ae4 fix: clean up Roundtable UI state after QA/Discussion request failure (#128)
* fix: clean up Roundtable UI state after QA/Discussion request failure (#127)

When /api/chat fails mid-stream during QA or Discussion, the catch blocks
only appended an error message to the chat panel. The Roundtable live UI
(bubble, thinking animation, engine mode) was never cleaned up, leaving
the interface stuck until page refresh.

Changes:
- Add clearLiveSessionAfterError() in use-chat-sessions.ts that notifies
  Stage via onLiveSessionError callback on request failure
- Add handleLiveSessionError() in stage.tsx to reset all live UI state
  and transition PlaybackEngine back to idle
- Add handleDiscussionError() in engine.ts to exit live mode without
  triggering the normal discussion-end sequence
- Add engine.resume() in doResumeTopic() to sync engine mode after
  soft-pause resume

Closes #127

* style: fix prettier formatting
2026-03-24 12:13:50 +08:00
ddb52241fb feat: add Discussion TTS with per-agent voice assignment (#211)
* feat(tts): add resolveVoice() and getServerVoiceList() utilities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tts): add AudioIndicator equalizer bars component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tts): add onSegmentSealed callback to StreamBuffer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tts): add voiceOverrides field to AgentConfig and AgentTemplate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tts): add useDiscussionTTS hook with audio queue and cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): add audio state indicator to Roundtable bubble

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tts): wire onSegmentSealed callback through chat sessions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): add per-agent voice dropdown to AgentBar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): integrate useDiscussionTTS in Stage and pass state to Roundtable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(tts): refine voice dropdown to pill-style selector

Replace native select styling with a compact rounded-full pill
that blends into the agent row. Remove border, use muted bg,
smaller text, and a custom chevron icon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(tts): use shadcn Select for voice dropdown, link with TTS toggle

- Replace native <select> with shadcn Select component for consistent UI
- Hide voice dropdown when TTS is muted (ttsMuted)
- Compact pill-style trigger with rounded-full, no border, muted bg

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(tts): add voice label prefix and always show dropdown

Show "音色: Alloy" instead of plain "Alloy" in the voice pill.
Always show dropdown regardless of mute state (voice config is
independent of playback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(tts): add volume icon hint in collapsed AgentBar

Show a small Volume2 icon in the collapsed pill when voice config
is available, hinting that voice settings are inside.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): fix voice dropdown layout and click handling

- Move voice pill below agent name (second line) to prevent
  horizontal overflow in English
- Wrap Select in div with onPointerDown stopPropagation to fix
  Radix click-through to parent row
- Add line-clamp-1 to descriptions for consistent row height
- Use items-start instead of items-center for better multi-line alignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(tts): redesign AgentBar voice layout for compactness

- Single-line layout: checkbox · avatar · name · role · voice pill
- Remove descriptions from agent rows (saves vertical space)
- Extract AgentVoicePill component to isolate Select event handling
- Smaller avatars (size-7), tighter row padding (py-1.5)
- Voice pill uses Volume2 icon + voice name (no prefix text)
- Works in both Chinese and English without overflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): cross-provider voice selection per agent

- Change voiceConfig from per-provider lookup to explicit
  { providerId, voiceId } per agent
- Each agent can use a different TTS provider's voice
- Voice picker dropdown groups voices by provider
- useDiscussionTTS routes TTS requests per agent's provider
- resolveAgentVoice falls back to global provider if no config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): align role badge and voice pill across agent rows

Give role badge fixed width (w-14 text-right) so role text
and voice pills align vertically across all rows regardless
of agent name length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): fix role badge and voice pill alignment

Wrap role badge + voice pill in a fixed-width container
(w-[9.5rem] justify-end) so both align vertically across
all agent rows regardless of name or role text length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(tts): align role badge and voice pill across agent rows

Add min-w-[52px] text-right to role badge so it starts at a
consistent position regardless of agent name length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): use fixed w-[60px] for role badge alignment

* fix(tts): use fixed w-[88px] for voice pill alignment

* fix(tts): prevent click-outside from closing AgentBar when Select portal is open

* fix(tts): comprehensive voice picker rewrite

- Replace Radix Select with Popover + button list (fixes click issue)
- Fix getAvailableProvidersWithVoices to always include global provider
- Widen panel from w-80 to w-96 (prevents name truncation)
- Voice pill uses primary color instead of gray (more visible)
- Extract renderAgentRow helper to reduce duplication
- Popover shows voices grouped by provider with active state
- Add findVoiceDisplayName utility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): align voice provider availability with toolbar logic

* fix(tts): fallback to first available provider when global provider has no voices

* refactor(tts): remove global provider fallback from voice resolution

Voice resolution now only depends on available providers (those with
API keys or server-configured). No more globalProviderId parameter.
Fallback is first available provider, then browser-native-tts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): add browser native TTS voices to agent voice picker

Load speechSynthesis.getVoices() in AgentBar and include as a
"Browser Native" provider group in the voice popover. No API key
needed - always available if browser supports it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): simplify toolbar TTS to on/off toggle, add disabled state

Toolbar:
- Replace volume slider with simple TTS on/off toggle button
- Remove ttsMuted/ttsVolume/onVolumeChange props from CanvasToolbar
- Toggle now controls ttsEnabled (not ttsMuted)

AgentBar:
- Collapsed: show VolumeX icon when TTS disabled
- Voice pills show disabled state (gray, cursor-not-allowed, no popover)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(tts): simplify Settings TTS tab to toggle + provider config

Remove voice selection, speed slider, preview/test, Azure locale
filter from Settings TTS tab. Voice is now per-agent in AgentBar.
Keep: on/off toggle, provider selector, API key + base URL config.
Add hint text pointing to AgentBar for voice configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(tts): simplify media popover TTS tab to toggle only

* fix(tts): add voice config hint to media popover TTS tab

* feat(tts): add per-voice preview button in voice picker

Each voice row in the popover has a small speaker icon button.
Click to preview the voice with "欢迎来到AI课堂" / "Welcome to
AI Classroom" (follows i18n). Browser native uses Web Speech API,
server TTS calls /api/generate/tts. Click again or close popover
to stop. Shows spinner while generating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): preview text follows course language instead of UI language

* refactor(tts): redesign AgentBar expanded panel layout

- Teacher always at top with voice pill (works in both modes)
- Mode tabs moved below teacher
- Auto mode: single compact row with shuffle icon + description
- Max turns: compact inline row with smaller input
- Preset mode: only student agents listed (teacher already above)
- Remove large shuffle animation from auto mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(tts): merge max turns into teacher row

* refactor(tts): separate teacher row and max turns, use stepper UI

- Teacher row: avatar + name + voice pill only
- Max turns: bottom row with MessageSquare icon + compact stepper
  (minus/number/plus in a rounded pill)
- Remove Input component dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): increase voice pill contrast in dark mode

* fix(tts): make max turns input editable, tighten panel padding

* fix(tts): restore shuffle animation in auto mode (compact version)

* fix(tts): adjust auto mode text spacing and add voice auto-assign hint

* fix(tts): auto-close voice popover after selecting a voice

* fix(tts): increase auto mode vertical padding for better balance

* fix(tts): push auto mode text toward bottom with flex spacer

* fix(tts): reduce auto mode bottom padding

* feat(tts): wait for TTS audio to finish before next agent turn

Add waitForDrain() to useDiscussionTTS that returns a promise
resolving when the audio queue is empty. The agent loop in
useChatSessions now awaits this after buffer drain, so the next
agent's turn doesn't start until the current agent's TTS audio
finishes playing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): keep bubble visible while TTS audio is still playing

When buffer drains (text=null) but audio indicator is still active,
don't clear liveSpeech. Clear it only when audio state goes idle.
This keeps the speech bubble visible until TTS finishes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): hold discussion bubble until TTS audio finishes

When StreamBuffer fires the done signal (onLiveSpeech null), Stage
now checks if TTS is still playing. If so, it defers clearing the
bubble state. The bubble stays visible until onAllAudioEnd fires
from the TTS hook (queue empty + nothing playing), then clears.

This prevents the jarring UX where the bubble disappears while
the agent's voice is still audible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): fix bubble hold - guard onStopSession instead of onLiveSpeech

Root cause: bubble disappears because doSessionCleanup fires via
onStopSession when the agent loop ends naturally, NOT because of
onLiveSpeech(null, null).

Fix: when onStopSession fires and TTS is still playing, defer
doSessionCleanup to onAllAudioEnd callback. Manual stop (user
presses button) still cleans up immediately via handleStopDiscussion.

Use doSessionCleanupRef to avoid circular dependency between
discussionTTS hook and doSessionCleanup useCallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): guard BOTH onLiveSpeech and onStopSession for bubble hold

Two paths clear the bubble:
1. onLiveSpeech(null, null) from StreamBuffer done → clears liveSpeech
2. onStopSession → doSessionCleanup → clears all state

Both fire when agent loop ends. Path 1 fires first (tick loop),
path 2 fires after (waitUntilDrained resolves). Both must be
guarded when TTS is still playing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): hold bubble during TTS playback and respect playback speed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tts): LLM picks voice matching agent persona during generation

- Client sends available voices (providerId + voiceId + name) to
  /api/generate/agent-profiles
- LLM prompt asks to pick a voice matching each agent's personality
- Parse "providerId::voiceId" from response, save as voiceConfig
- Fallback to index-based assignment if LLM doesn't pick
- Browser native voices hidden when server providers are available
- saveGeneratedAgents accepts and persists voiceConfig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): restore volume slider in classroom toolbar

Revert the toolbar simplification from 36e3997 that replaced the
volume slider with a TTS on/off toggle. The volume control with
hover slider is a core classroom UX. TTS on/off is controlled via
Settings and Media popover instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): teacher uses global lecture voice in discussion when no voiceConfig override

* fix(tts): teacher always uses global lecture voice, no overrides

* fix(tts): sync playback speed to currently playing audio in real-time

* fix(tts): address code review issues

- Issue 2: enabled flag now checks ttsEnabled && !ttsMuted in stage.tsx
- Issue 4: remove unused browserAvailableVoices from useDiscussionTTS
- Issue 5: remove dead code in audio-settings.tsx (Slider, Loader2, handleTTSVoiceChange, handleTTSSpeedChange, handleTestTTS, testingTTS, ttsTestStatus, ttsTestMessage, testText, ttsSpeed, setTTSSpeed, and unused browser-tts-preview imports)
- Issue 6: shouldHold now checks queue length in addition to isPlayingRef
- Issue 8: hide AgentVoicePill for teacher row in agent-bar.tsx (teacher voice is controlled in Settings)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tts): address PR review — abort preview fetch, defer error recovery

1. Add AbortController to voice preview server TTS fetch, abort on
   stopPreview to prevent stale responses on rapid switching
2. Use queueMicrotask for processQueue calls in error/ended handlers
   to prevent synchronous recursion if multiple items fail consecutively
3. Add ordering invariant comment on sealLastText's onSegmentSealed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): restore teacher voice pill, respect voiceConfig override

* fix(tts): sync volume and mute to discussion TTS audio in real-time

* fix(tts): allow browser-native TTS alongside server providers

* fix(tts): remove top padding from voice popover content

* fix(tts): make selectedAgents reactive to voiceConfig changes

* fix(tts): use agents record instead of listAgents() to avoid infinite loop

* fix(tts): single source of truth for teacher voice

Teacher voice pill now reads/writes global ttsProviderId + ttsVoice
(same settings used by lecture TTS). This ensures lecture and
discussion always use the same teacher voice. Student agents still
use per-agent voiceConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add avatar descriptions for smarter LLM avatar selection

Each avatar now has a one-line description (appearance, vibe) sent
to the agent-profiles generation API. LLM picks avatars matching
agent personality instead of guessing from file paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-03-23 17:35:09 +08:00
Mohit Neerukonda d0c324cbe9 fix: forward providerType to chat API for custom provider support (#114) 2026-03-22 23:35:51 +08:00
Yizuki_Ame 920d298b8e feat: discussion buffer-level pause (freeze text reveal without aborting SSE) (#129)
* feat: discussion buffer-level pause (freeze text reveal without aborting SSE)

- Add livePausedRef for session-scoped paused intent that survives buffer recreation
- New buffers auto-inherit pause state so new turns stay paused
- pauseActiveLiveBuffer / resumeActiveLiveBuffer convenience functions
- Intent cleared on endSession / softPauseSession / startDiscussion
- Bubble click during discussion now toggles buffer pause (not soft-pause)
- Canvas toolbar shows pause/resume toggle alongside stop button
- Disable pause during thinking phase and before text arrives
- i18n keys: pauseDiscussion / resumeDiscussion (zh-CN + en-US)

Note: soft-pause (abort SSE) is still available via opening the input box (onInputActivate).

* fix: address reviewer nits  remove unused onSoftPause, explicit livePausedRef clear, rename isThinking to isPauseDisabled

1. Remove onSoftPause from RoundtableProps interface and stage.tsx
   prop  no longer used after buffer-pause replaced soft-pause
2. Add explicit livePausedRef.current = false in startDiscussion
   as defensive guard against future refactors
3. Rename isThinking  isPauseDisabled in CanvasToolbar to better
   match the prop's actual semantics (controls pause button state)

* fix(ui): remove toolbar pause button, freeze bubble bars, add spacebar shortcut
2026-03-22 14:29:14 +08:00
wyucandClaude Opus 4.6 843d33e5cc chore: enforce Prettier formatting and fix lint issues
- Add .prettierignore to exclude vendor packages, lock files, markdown, and YAML
- Update .prettierrc: printWidth 100, singleQuote, trailingComma "all"
- Run Prettier across all source files for consistent formatting
- Fix unused imports (UserRequirements, setTTSProvider)
- Fix eslint-disable comment placement after Prettier reformat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:09:45 +08:00
wyucandClaude Opus 4.6 cc44f32ca8 Refactor: directory structure cleanup + Google Fonts localization
- Replace next/font/google with local fonts (geist + @fontsource-variable/inter)
  to eliminate build-time network dependency
- Resolve lib/utils.ts vs lib/utils/ naming conflict (cn() → lib/utils/cn.ts)
- Move React Context Providers from lib/store/ to lib/contexts/
- Consolidate hooks into lib/hooks/ (use-theme, use-i18n)
- Organize loose components into subdirectories:
  agent-* → components/agent/
  canvas-* → components/canvas/
  generation-toolbar, media-popover → components/generation/
  speech-button, tts-config-popover → components/audio/
  proactive-card → components/chat/

65 files changed, lint 0 errors, build 28 pages passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:35:06 +08:00
wyucandClaude Opus 4.6 0d20abfed9 Initial commit: OpenMAIC AI classroom platform
Next.js 16 App Router application with:
- AI-powered interactive classroom generation from PDF/text requirements
- Multi-agent discussion system (teacher, students, assistant roles)
- Provider abstraction for LLM, TTS, ASR, image, video, web search
- Local-first data architecture (IndexedDB/Dexie)
- i18n support (zh-CN/en-US)
- Structured logger, SSRF guard, lint/build passing (0 errors)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:41:07 +08:00