From df7150fc296d794c11b8642a8e5420b76a430be8 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 30 May 2026 20:08:09 -0400 Subject: [PATCH] feat(l1): dashboard intake dispatch on match_or_build outcome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleStart dispatches on outcome: matched/build → walker; suggest → inline 'use this flow / build new' prompt; out_of_scope → escalate-to-engineering prompt (via escalate-without-walk, since intake no longer yields adhoc directly). buildNew re-runs intake with force_build. tsc -b + eslint clean. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/l1/L1Dashboard.tsx | 269 +++++++++++++------------- 1 file changed, 135 insertions(+), 134 deletions(-) diff --git a/frontend/src/pages/l1/L1Dashboard.tsx b/frontend/src/pages/l1/L1Dashboard.tsx index 250fc148..cc099ca5 100644 --- a/frontend/src/pages/l1/L1Dashboard.tsx +++ b/frontend/src/pages/l1/L1Dashboard.tsx @@ -1,168 +1,169 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { PageMeta } from '@/components/common/PageMeta' -import { useAuthStore } from '@/store/authStore' import { l1Api } from '@/api/l1' import { toast } from '@/lib/toast' -import { EmptyStateCard } from '@/components/l1/EmptyStateCard' +import { StartWalkPanel } from '@/components/l1/EmptyStateCard' import { ResumeInProgress } from '@/components/l1/ResumeInProgress' -import type { QueueRow } from '@/types/l1' +import { L1CoverageBanner } from '@/components/l1/L1CoverageBanner' +import type { NearMiss } from '@/types/l1' export default function L1Dashboard() { - const user = useAuthStore((s) => s.user) const navigate = useNavigate() const [problem, setProblem] = useState('') const [customerName, setCustomerName] = useState('') - const [customerContact, setCustomerContact] = useState('') const [submitting, setSubmitting] = useState(false) - const [queue, setQueue] = useState([]) - const [isEmpty, setIsEmpty] = useState(false) + const [suggestion, setSuggestion] = useState(null) + const [outOfScope, setOutOfScope] = useState(null) - useEffect(() => { - l1Api.queue('open').then(setQueue).catch(() => setQueue([])) - // Phase 1: emptiness detection is just "is the queue empty AND no resumable sessions" — - // we conservatively show the empty-state card on accounts with literally no L1 activity yet. - // (A stricter KB-empty detection arrives in Phase 2 when the kb_documents table exists.) - }, []) - - useEffect(() => { - // Show empty-state ONLY for first-run state — no queue items and no active sessions - if (queue.length === 0) { - l1Api - .listActiveSessions() - .then((active) => setIsEmpty(active.length === 0)) - .catch(() => setIsEmpty(false)) - } else { - setIsEmpty(false) - } - }, [queue]) + const reset = () => { + setSuggestion(null) + setOutOfScope(null) + } const handleStart = async () => { if (!problem.trim()) return setSubmitting(true) + reset() try { - const response = await l1Api.intake({ + const res = await l1Api.intake({ problem_statement: problem.trim(), customer_name: customerName.trim() || undefined, - customer_contact: customerContact.trim() || undefined, }) - navigate(`/l1/walk/${response.session_id}`) - } catch (err) { - const detail = (err as { response?: { data?: { detail?: string } } }).response?.data?.detail - const msg = - typeof detail === 'string' ? detail : 'Failed to start walk. Try again.' - toast.error(msg) + if (res.outcome === 'matched' || res.outcome === 'build') { + navigate(`/l1/walk/${res.session_id}`) + } else if (res.outcome === 'suggest') { + setSuggestion(res.near_miss ?? null) + } else if (res.outcome === 'out_of_scope') { + setOutOfScope(res.category ?? 'unknown') + } + } catch { + toast.error('Failed to start session. Please try again.') } finally { setSubmitting(false) } } - const now = new Date() - const greeting = - now.getHours() < 12 ? 'morning' : now.getHours() < 18 ? 'afternoon' : 'evening' - const firstName = user?.name?.split(' ')[0] || 'there' + // "Use this flow" — re-run intake with the same text; it matches again and + // returns a `matched` outcome with a started flow session (Phase 2A approach). + const useSuggestedFlow = async () => { + setSubmitting(true) + try { + const res = await l1Api.intake({ problem_statement: problem.trim() }) + if (res.session_id) navigate(`/l1/walk/${res.session_id}`) + else reset() + } catch { + toast.error('Could not start the matched flow. Please try again.') + } finally { + setSubmitting(false) + } + } + + // "Build new" — skip the match pass; still gated by enabled categories. + const buildNew = async () => { + setSubmitting(true) + reset() + try { + const res = await l1Api.intake({ + problem_statement: problem.trim(), + customer_name: customerName.trim() || undefined, + force_build: true, + }) + if (res.outcome === 'build' && res.session_id) { + navigate(`/l1/walk/${res.session_id}`) + } else if (res.outcome === 'out_of_scope') { + setOutOfScope(res.category ?? 'unknown') + } + } catch { + toast.error('Failed to start session. Please try again.') + } finally { + setSubmitting(false) + } + } + + // out-of-scope fallback: escalate straight to engineering (no walk). + const escalateOutOfScope = async () => { + if (!problem.trim()) return + setSubmitting(true) + try { + const session = await l1Api.escalateWithoutWalk({ + problem_statement: problem.trim(), + customer_name: customerName.trim() || undefined, + reason_category: 'out_of_scope', + reason: 'Problem is outside the enabled L1 AI-build categories.', + }) + toast.success('Escalated to engineering.') + navigate(`/l1/walk/${session.id}`) + } catch { + toast.error('Could not escalate. Please try again.') + } finally { + setSubmitting(false) + } + } return ( -
- -
- {/* Greeting */} -
-

- {now.toLocaleDateString('en-US', { - weekday: 'long', - month: 'long', - day: 'numeric', - })} +

+ + + + {suggestion && ( +
+

+ Found a similar flow: {suggestion.flow_name}. Use it, or + build a new troubleshooting tree for this problem?

-

- Good {greeting}, {firstName}. -

+
+ + +
+ )} - {/* Empty state (first-run) */} - {isEmpty && } - - {/* Describe the problem */} -
-
- - - Describe the problem - + {outOfScope && ( +
+

+ This problem isn’t in your account’s enabled L1 categories + {outOfScope !== 'unknown' ? ` (${outOfScope.replace(/_/g, ' ')})` : ''}, so + there’s no AI-built walk for it. You can escalate it to engineering. +

+
+ +
-
-