feat(l1): real L1 dashboard with empty-state + resume widget

Replaces the T20 stub. L1 dashboard renders greeting, "Describe the
problem" intake card (autofocus textarea, optional customer fields,
primary "Start walk" CTA), open-tickets queue (Phase 1: display-only),
and a "Resume in progress" widget listing the L1's active sessions
ordered by last_step_at DESC. Empty-state card shows on accounts with
no queue + no active sessions (first-run nudge to upload KB or auth flows).

Adds /api/l1.ts (full L1 API client surface) and /types/l1.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 14:09:34 -04:00
parent d0561be6a1
commit 4e9610c252
5 changed files with 352 additions and 3 deletions

64
frontend/src/api/l1.ts Normal file
View File

@@ -0,0 +1,64 @@
import { apiClient } from './client'
import type {
IntakeRequest,
IntakeResponse,
QueueRow,
WalkSession,
AdhocNote,
} from '@/types/l1'
export const l1Api = {
intake: (body: IntakeRequest) =>
apiClient.post<IntakeResponse>('/l1/intake', body).then(r => r.data),
queue: (statusFilter?: string) =>
apiClient.get<QueueRow[]>('/l1/queue', {
params: statusFilter ? { status_filter: statusFilter } : {},
}).then(r => r.data),
listActiveSessions: () =>
apiClient.get<WalkSession[]>('/l1/sessions/active').then(r => r.data),
getSession: (sessionId: string) =>
apiClient.get<WalkSession>(`/l1/sessions/${sessionId}`).then(r => r.data),
step: (
sessionId: string,
step: { node_id: string; question: string; answer: string; note?: string | null },
) =>
apiClient
.post<WalkSession>(`/l1/sessions/${sessionId}/step`, step)
.then(r => r.data),
notes: (sessionId: string, notes: AdhocNote[]) =>
apiClient
.post<WalkSession>(`/l1/sessions/${sessionId}/notes`, { notes })
.then(r => r.data),
resolve: (
sessionId: string,
body: { helpful: boolean; resolution_notes: string },
) =>
apiClient
.post<WalkSession>(`/l1/sessions/${sessionId}/resolve`, body)
.then(r => r.data),
escalate: (
sessionId: string,
body: { reason: string; reason_category: string },
) =>
apiClient
.post<WalkSession>(`/l1/sessions/${sessionId}/escalate`, body)
.then(r => r.data),
escalateWithoutWalk: (body: {
problem_statement: string
customer_name?: string
customer_contact?: string
reason_category: string
reason?: string
}) =>
apiClient
.post<WalkSession>('/l1/escalate-without-walk', body)
.then(r => r.data),
}