Skip to content

Feat/modul ghiseu#22

Open
Cucuteanu-Tudor-912 wants to merge 45 commits into
mainfrom
feat/modul-ghiseu
Open

Feat/modul ghiseu#22
Cucuteanu-Tudor-912 wants to merge 45 commits into
mainfrom
feat/modul-ghiseu

Conversation

@Cucuteanu-Tudor-912

Copy link
Copy Markdown
Collaborator

No description provided.

Cucuteanu-Tudor-912 and others added 30 commits May 24, 2026 02:03
Standalone voice-first experience opt-in via a "Modul Ghișeu"
toggle on the login page. New /ghiseu route renders all 9 design
states (idle, listening, thinking, speaking, review, export, done,
error, mic-denied) with a scripted state machine — real voice
agent wiring is staged for a follow-up pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18-task TDD plan covering the localStorage pref helper, scripted
state machine, login-page toggle + OTP redirect, scoped CSS port,
all 9 design states (idle/listening/thinking/speaking/review/
export/done/error/mic-denied), accessibility profile menu, and
the /ghiseu route. Each task is bite-sized with concrete code
and explicit test+commit steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prefixes .vo-* → .gh-*. Color/radius/shadow tokens scoped to
.gh-root so they don't bleed app-wide. Font tokens come from
globals.css. Removed the html,body { overflow: hidden } reset
that would lock scrolling everywhere.
Four sub-phases, each independently shippable:
- 2.1 voice bridge integration (UI only)
- 2.2 real session + document data
- 2.3 export action wiring (one new backend endpoint)
- 2.4 polish + production readiness

Includes the GhiseuState → voice/session state mapping table that's
the core design problem for Phase 2.1, the new
POST /api/documents/{id}/submit endpoint spec for Phase 2.3, and
risks/sequencing/estimates per phase.
Design doc for the first sub-phase of the Modul Ghișeu Phase 2 roadmap.
Defines the store-shape split (attachVoiceBridge wiring vs enterVoiceMode
WS start), the kioskMode flag on sessionStore for navigation suppression,
the new interrupt() method on VoiceAgentHook, the voice-event mapping
into the new caption field, and the testing strategy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nine tasks, leaf-to-trunk: VoiceAgentHook.interrupt → sessionStore
kioskMode → MockVoiceAgentHook helper → ghiseuStore rewrite →
bridge integration tests → CaptionStrip → GhiseuShell wiring →
regression check → manual smoke test.

Every step shows the exact code, the exact test, and the exact
command to run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes the existing voiceWs.sendInterrupt() through the hook surface
so kiosk + chat surfaces can cut off agent audio mid-response. Throws
when called before start() (no WS open).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When /ghiseu is mounted, setKioskMode(true) prevents the agent's
start_procedure / redirect side-effects from navigating the user
off the kiosk. Cleared on unmount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typed mock of VoiceAgentHook with vi.fn() spies. start() captures the
opts so tests can fire opts.onUserDelta?.(...) to simulate bridge events
flowing back into the consumer.

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

Drops scriptedTalkingFlow + activeTimers. Adds:
- caption: { user, agent } field with Line { text, live } shape
- appendUserPartial / commitUserMessage / appendAgentPartial / commitAgentMessage
  handlers that the voice bridge will invoke
- 300ms thinking-debounce that gets canceled when agent starts replying
- _bridge reference + attachVoiceBridge / enterVoiceMode / exitVoiceMode /
  interrupt lifecycle actions
- toggleMute idle-branch re-enters voice mode; non-idle toggles mic

Bridge-lifecycle actions are exercised by the integration test in the
next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers attach + enterVoiceMode (parallel start/enableMic), captured
callbacks flowing back into caption state, stop-before-start eviction
when wsReady, mic-denied/error transitions, interrupt + reset cleanup,
and toggleMute's three branches (idle re-enter, enable, disable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads caption.user / caption.agent from useGhiseuStore. Falls back to
the design-copy TRANSCRIPT[state] map when either is null (mainly the
agent greeting in idle). Live captions show the partial wrapper while
live: true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three effects:
- attach the voice bridge + set kioskMode=true on mount; reverse on unmount
- auto-call enterVoiceMode once citizen is hydrated (guarded to fire once)
- mirror voice.micOn into ghiseuStore.muted so UI ripple stays in sync

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cucuteanu-Tudor-912 and others added 15 commits May 24, 2026 05:07
GhiseuShell gated enterVoiceMode() on citizen being non-null, but only
ChatSurface (/) called hydrateCitizen(). On /ghiseu the citizen stayed
null forever, Effect B short-circuited, the voice WS never opened, and
the user only saw the static TRANSCRIPT fallback ("Bună! Spune-mi cu
ce te pot ajuta").

Mirror ChatSurface.tsx:72-75: call hydrateCitizen() on mount when
citizen is null. Once it lands, the existing enter-voice effect fires
naturally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three frontend bugs surfaced by manual smoke:

1. toggleMute read bridge.micOn from the stored bridge ref, which is a
   stale snapshot from the render when attachVoiceBridge ran. Replaced
   with ghiseuStore.muted (already mirrored from voice.micOn).

2. Effect A had voice as a dep but the hook returns a new object literal
   every render — every state change re-fired the effect, cleanup called
   bridge.stop(), more state changes, infinite loop → Maximum update
   depth → GlobalError. Empty deps so it's a true mount/unmount effect.

3. Întrerupe button (canInterrupt = state === "speaking") never activated
   because some backend configs emit audio without transcript deltas, so
   onAgentDelta never fires and appendAgentPartial never runs. Added a
   voice.state mirror effect that sets ghiseu state to "speaking" /
   "listening" based on the bridge, skipping when in UI-flow states
   (review/export/done/error/mic-denied).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the 'agent asks for CNP twice' bug:

The procedure schema marks fields like cnp/nume_complet/email as
source: 'profile' — intent: auto-fill from the citizen's profile. But
nothing actually did that. start_procedure created an empty doc and
the system prompt told the agent 'don't repeat info you already have
(name, CNP, address)'. The agent then acknowledged user-provided values
verbally without firing set_field, because rule 3 implied CNP was
already in profile. On the next turn, the state recap showed cnp still
missing → agent asked again.

Two compounding issues fixed:

1. citizen_attributes only included the attributes jsonb, which doesn't
   contain cnp (cnp is a top-level column). Added enrich_citizen_attrs()
   that merges top-level columns into the attrs dict, plus a derived
   nume_complet. Wired into both voice (agent_voice) and text
   (session_engine) paths.

2. No prefill mechanism existed. Added compute_profile_prefill() that
   walks procedure fields, returns {field: value} for source-profile
   fields with non-empty citizen attrs. start_procedure now applies it
   right after insert_document so the agent's state recap reflects the
   real auto-filled fields.

Net effect: agent sees cnp/nume_complet/email/etc. as already satisfied
when the citizen profile has them, never asks for them, document is
filled correctly from the start.

Tests: 4 new cases for compute_profile_prefill, 4 new for
enrich_citizen_attrs. All 30 affected tests pass. Pre-existing
test_institutions failure unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read-only wiring: DocumentReview + DoneScreen consume sessionStore
instead of static arrays. GhiseuShell mirrors session.state to drive
the review-screen transition. amendDoc stays on the review screen so
field values update live as the agent corrects them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
null/undefined/empty → em dash. boolean → Da/Nu. ISO date → DD.MM.YYYY.
Strings pass through. 7 unit tests cover each branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the legacy 'switch to listening' jump with a no-op for state.
User stays on the review screen; as the agent corrects fields via
set_field, the field grid updates in place. Cleaner UX than switching
to the voice ripple and back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Effect E in GhiseuShell: when backend session.state transitions to
'reviewing', set ghiseu state to 'review' (swaps the kiosk to the
review screen). When 'delivered', set to 'done'. Skips when ghiseu is
already in 'error' or 'mic-denied' (don't override user-facing failure
states).

Also: enterVoiceMode no longer clobbers UI-flow states (review/export/
done) when it resolves. The state value is only updated to 'listening'
when the kiosk is in a voice-driven state. This matters for both the
new test ordering AND a real edge case: user reloading /ghiseu mid-
'reviewing' session shouldn't get bounced back to the voice ripple
while their WS reconnects.

5 new tests cover the four state transitions + the override-skip rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the hard-coded FIELDS array. Reads document.fields and
procedure.fields from useSessionStore, renders one row per procedure
field with the real label and value (formatted via formatValue).

Auto badge = field.source.includes('profile') && value present.
Doc-paper title uses procedure.title. Removed the fake 'Cerere nr.
2026-AV-08412' line — pre-delivery the doc has no registration
number, post-delivery it's shown on the done screen.

Loading fallback when document or procedure is null (brief race
between session.state mirror and doc load).

10 unit tests cover the loading fallback, full render, ISO date
formatting, auto badge presence/absence, em-dash for empty fields,
and button click handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads document.ref_number from useSessionStore with REG-PENDING
fallback. Until Phase 2.3 wires the submit endpoint, the kiosk will
show REG-PENDING (ref_number stays null). Once 2.3 lands, the same
component picks up the real value automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- POST /documents/{id}/submit accepting {method: 'city'|'email',
  email_address?: string}. Returns SubmitDocumentResponse with the
  generated ref_number (REG-YYYY-XXXXXXXX format, deterministic).
- 409 already_delivered on a finalized doc — frontend treats as success.
- delivery field now accepts 'city' and 'email' in addition to legacy
  save/send/print.
- 4 new unit tests for the ref-number generator.

Frontend:
- api.submitDocument(id, body) hits the new endpoint.
- ghiseuStore.pickExport becomes async: export → submitting → done.
  Refreshes sessionStore.document after success. 409 path is idempotent.
- New 'submitting' state in GhiseuState with VoiceStage copy, ControlsDock
  no-op branch, ExportOptions disabled+pulsing chosen card.
- 1 new + 1 updated test (14 ghiseuStore tests passing total).

Out of scope (Phase 2.4 polish): real SMTP/SendGrid email delivery,
explicit retry button, idempotency-key dedup of double-clicks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w states

Surfaced during smoke: the done screen would close (transition back to
voice ripple) if the user spoke and the agent responded. Root cause:
appendAgentPartial / commitAgentMessage / commitUserMessage unguarded-
set state to speaking/listening/thinking, overriding the terminal
'done' state.

Fix: a new isVoiceDrivenState() helper. The three event handlers now
only transition state when the kiosk is in a voice-driven state
(idle/listening/thinking/speaking). When in review/export/submitting/
done/error/mic-denied, captions still update (in case any surface
wants to render them) but state stays put.

4 new tests cover the done + review guard cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops a fully-filled certificat-fiscal document into sessionStore
(Maria Ionescu, real address, real scop) and jumps the kiosk to the
review screen. Lets demo judges see what the completed-form UI looks
like without driving the full voice flow.

Resets cleanly via the existing 'Ia-o de la capăt' button next to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kiosk on a tablet sits at arm's length from the user, not keyboard
distance. Desktop font sizes look tiny at that range. New media query
bumps every text element up while keeping the layout intact:

- Header brand 20 → 26px, reset/demo buttons 14 → 16px
- Status text clamp(44, 6vw, 78) → clamp(56, 7.2vw, 92)
- Caption lines clamp(20, 1.9vw, 26) → clamp(22, 2.4vw, 30)
- Control buttons 16 → 20px with bigger padding + larger icons
- CTA button 16 → 22px
- Doc-paper title 17 → 26px, labels 10 → 14, values 13.5 → 20
- Review action cards: action title 22, hint 16
- Export cards: title 24, hint 16
- Done-check icon 64 → 96

No layout changes — only typography + paddings + icon sizes scale.
Desktop (>1280px) and phone (<720px) breakpoints untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the auto-enter-voice-mode-on-mount effect. The kiosk now
opens silent with the mic visibly off. User clicks the mic button
('Pornește microfonul') when they're ready, which routes through
toggleMute → enterVoiceMode → WS handshake + mic enable.

Better UX for a kiosk in a public space — no surprise audio capture
on page load. Matches the design's idle state copy 'Bună. Apasă
microfonul pentru ajutor.'

Test updated: the previous 'auto-calls enterVoiceMode' assertion is
inverted to verify NOTHING is called on mount.

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

vercel Bot commented May 24, 2026

Copy link
Copy Markdown

@Cucuteanu-Tudor-912 is attempting to deploy a commit to the Bogdan's projects team on Vercel, but is not a member of this team. To resolve this issue, you can:

  • Make your repository public. Collaboration is free for open source and public repositories.
  • Upgrade to pro and add @Cucuteanu-Tudor-912 as a member. A Pro subscription is required to access Vercel's collaborative features.
    • If you're the owner of the team, click here to upgrade and add @Cucuteanu-Tudor-912 as a member.
    • If you're the user who initiated this build request, click here to request access.
    • If you're already a member of the Bogdan's projects team, make sure that your Vercel account is connected to your GitHub account.

To read more about collaboration on Vercel, click here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant