fix(l1): resolve PR #193 frontend review findings (2a,2b,3,4,5,7)
Mounts L1EscalationsSection on EscalationQueuePage (Finding 2a — it was never rendered) and renders the correct fields: step.question ?? step.text, timeAgo, and the session problem_text (Finding 2b). ProposalDetail gates the /pilot link on source_session_id and shows an L1-source block for l1_session_id-sourced proposals (Finding 3 — was a broken /pilot/null link). Collapses the three near-identical intake handlers into one runIntake: "Use this flow" now passes near_miss.flow_id (Finding 4 — it previously re-suggested forever) and a navigate guard prevents /l1/walk/undefined; out_of_scope gains a "Walk it ad-hoc" button (Finding 5). Aligns L1-category permissions to owner+admin: usePermissions.canManageAccount includes account admins, User.account_role TS type gains 'admin', and a new ProtectedRoute requireAccountManager guard fronts the route (Finding 7). Drops the unused NextNodeRequest.acknowledged field. tsc -b + eslint + vite build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,18 +88,35 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-6 space-y-5">
|
<div className="flex-1 overflow-y-auto p-6 space-y-5">
|
||||||
{/* Source session link */}
|
{/* Source — exactly one of a FlowPilot session XOR an L1 walk is set
|
||||||
<div className="card-flat p-4">
|
(DB CHECK). Never link to /pilot for an L1-sourced proposal:
|
||||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">Source Session</h4>
|
source_session_id is NULL there, so the old unconditional link
|
||||||
<Link
|
rendered a broken /pilot/null. */}
|
||||||
to={`/pilot/${proposal.source_session_id}`}
|
{proposal.source_session_id ? (
|
||||||
target="_blank"
|
<div className="card-flat p-4">
|
||||||
className="flex items-center gap-2 text-sm text-primary hover:underline"
|
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">Source Session</h4>
|
||||||
>
|
<Link
|
||||||
<ExternalLink size={12} />
|
to={`/pilot/${proposal.source_session_id}`}
|
||||||
View session that generated this proposal
|
target="_blank"
|
||||||
</Link>
|
className="flex items-center gap-2 text-sm text-primary hover:underline"
|
||||||
</div>
|
>
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
View session that generated this proposal
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : proposal.l1_session_id ? (
|
||||||
|
<div className="card-flat p-4">
|
||||||
|
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">Source — L1 AI walkthrough</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Captured from an L1 technician's AI-guided walk and validated by a
|
||||||
|
successful resolution. The proposed flow is the path that resolved the ticket.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 flex items-center gap-1.5 text-xs text-text-muted">
|
||||||
|
<Hash size={11} />
|
||||||
|
<span className="font-mono">L1 session {proposal.l1_session_id.slice(0, 8)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Proposed diff (for enhancements) */}
|
{/* Proposed diff (for enhancements) */}
|
||||||
{proposal.proposed_diff && (() => {
|
{proposal.proposed_diff && (() => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { l1Api } from '@/api/l1'
|
import { l1Api } from '@/api/l1'
|
||||||
|
import { timeAgo } from '@/lib/timeAgo'
|
||||||
import type { WalkSession } from '@/types/l1'
|
import type { WalkSession } from '@/types/l1'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,11 +44,13 @@ export function L1EscalationsSection() {
|
|||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<span className="font-mono text-xs text-text-muted">#{s.id.slice(0, 8)}</span>
|
<span className="font-mono text-xs text-text-muted">#{s.id.slice(0, 8)}</span>
|
||||||
<span className="text-sm text-text-primary truncate">
|
<span className="text-sm text-text-primary truncate">
|
||||||
{s.walked_path.length} step{s.walked_path.length === 1 ? '' : 's'} walked
|
{s.problem_text
|
||||||
|
? s.problem_text
|
||||||
|
: `${s.walked_path.length} step${s.walked_path.length === 1 ? '' : 's'} walked`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-text-muted whitespace-nowrap">
|
<span className="text-xs text-text-muted whitespace-nowrap">
|
||||||
{new Date(s.last_step_at).toLocaleString()}
|
{timeAgo(s.last_step_at)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
@@ -58,7 +61,7 @@ export function L1EscalationsSection() {
|
|||||||
<ol className="space-y-1.5 text-sm">
|
<ol className="space-y-1.5 text-sm">
|
||||||
{s.walked_path.map((step, i) => (
|
{s.walked_path.map((step, i) => (
|
||||||
<li key={i} className="flex flex-col">
|
<li key={i} className="flex flex-col">
|
||||||
<span className="text-text-muted text-xs">{step.question}</span>
|
<span className="text-text-muted text-xs">{step.question ?? step.text}</span>
|
||||||
{step.answer && (
|
{step.answer && (
|
||||||
<span className="font-medium text-text-primary">→ {step.answer}</span>
|
<span className="font-medium text-text-primary">→ {step.answer}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function L1WalkTreeVariant({ session, onSessionUpdate, onDone }: Props) {
|
|||||||
}, [isAiBuild, session.id, session.status])
|
}, [isAiBuild, session.id, session.status])
|
||||||
|
|
||||||
const advanceNode = useCallback(
|
const advanceNode = useCallback(
|
||||||
async (body: { answer?: 'yes' | 'no'; acknowledged?: boolean }) => {
|
async (body: { answer?: 'yes' | 'no' }) => {
|
||||||
if (!node) return
|
if (!node) return
|
||||||
setNodeLoading(true)
|
setNodeLoading(true)
|
||||||
setNodeError(null)
|
setNodeError(null)
|
||||||
@@ -183,7 +183,7 @@ export function L1WalkTreeVariant({ session, onSessionUpdate, onDone }: Props) {
|
|||||||
<>
|
<>
|
||||||
<p className="text-lg">{node.text}</p>
|
<p className="text-lg">{node.text}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => advanceNode({ acknowledged: true })}
|
onClick={() => advanceNode({})}
|
||||||
className="rounded-md bg-accent text-white px-5 py-3 text-base font-medium hover:bg-accent/90 min-h-[44px] transition-colors"
|
className="rounded-md bg-accent text-white px-5 py-3 text-base font-medium hover:bg-accent/90 min-h-[44px] transition-colors"
|
||||||
>
|
>
|
||||||
Done — next step
|
Done — next step
|
||||||
@@ -251,8 +251,8 @@ export function L1WalkTreeVariant({ session, onSessionUpdate, onDone }: Props) {
|
|||||||
<ol className="space-y-3 text-sm">
|
<ol className="space-y-3 text-sm">
|
||||||
{session.walked_path.map((step, i) => (
|
{session.walked_path.map((step, i) => (
|
||||||
<li key={i} className="flex flex-col">
|
<li key={i} className="flex flex-col">
|
||||||
<span className="text-muted-foreground text-xs">{step.question}</span>
|
<span className="text-muted-foreground text-xs">{step.question ?? step.text}</span>
|
||||||
<span className="font-medium">→ {step.answer}</span>
|
{step.answer && <span className="font-medium">→ {step.answer}</span>}
|
||||||
{step.l1_note && <span className="text-muted-foreground text-xs italic mt-0.5">{step.l1_note}</span>}
|
{step.l1_note && <span className="text-muted-foreground text-xs italic mt-0.5">{step.l1_note}</span>}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ import { Spinner } from '@/components/common/Spinner'
|
|||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
requiredRole?: EffectiveRole
|
requiredRole?: EffectiveRole
|
||||||
|
// Gate on account-management capability (owner OR account-admin OR super_admin),
|
||||||
|
// mirroring backend require_account_owner_or_admin. Use instead of
|
||||||
|
// requiredRole="owner" when account admins must also pass — the role hierarchy
|
||||||
|
// has no 'admin' rung, so requiredRole alone wrongly bounces admins.
|
||||||
|
requireAccountManager?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) {
|
export function ProtectedRoute({ requiredRole, requireAccountManager, children }: ProtectedRouteProps) {
|
||||||
const { isAuthenticated, isLoading, user } = useAuthStore()
|
const { isAuthenticated, isLoading, user } = useAuthStore()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { effectiveRole } = usePermissions()
|
const { effectiveRole, canManageAccount } = usePermissions()
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -48,6 +53,10 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requireAccountManager && !canManageAccount) {
|
||||||
|
return <Navigate to="/trees" replace />
|
||||||
|
}
|
||||||
|
|
||||||
if (requiredRole) {
|
if (requiredRole) {
|
||||||
const ROLE_HIERARCHY: Record<EffectiveRole, number> = {
|
const ROLE_HIERARCHY: Record<EffectiveRole, number> = {
|
||||||
super_admin: 5,
|
super_admin: 5,
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ export function usePermissions() {
|
|||||||
// Management permissions
|
// Management permissions
|
||||||
canManageCategories: hasMinimumRole(user, 'owner'),
|
canManageCategories: hasMinimumRole(user, 'owner'),
|
||||||
canManageGlobalCategories: effectiveRole === 'super_admin',
|
canManageGlobalCategories: effectiveRole === 'super_admin',
|
||||||
canManageAccount: effectiveRole === 'super_admin' || effectiveRole === 'owner',
|
// Mirrors backend User.can_manage_account (super_admin OR owner OR admin).
|
||||||
|
// account_role 'admin' isn't in the effectiveRole hierarchy, so check it
|
||||||
|
// directly — otherwise account admins map to 'viewer' and are wrongly excluded.
|
||||||
|
canManageAccount:
|
||||||
|
effectiveRole === 'super_admin' ||
|
||||||
|
effectiveRole === 'owner' ||
|
||||||
|
user?.account_role === 'admin',
|
||||||
|
|
||||||
canManageScriptTemplate: (template: { created_by: string | null; team_id?: string | null }) => {
|
canManageScriptTemplate: (template: { created_by: string | null; team_id?: string | null }) => {
|
||||||
if (!user) return false
|
if (!user) return false
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { AlertTriangle } from 'lucide-react'
|
import { AlertTriangle } from 'lucide-react'
|
||||||
import { EscalationQueue, EscalationMetricCard } from '@/components/flowpilot'
|
import { EscalationQueue, EscalationMetricCard } from '@/components/flowpilot'
|
||||||
|
import { L1EscalationsSection } from '@/components/l1/L1EscalationsSection'
|
||||||
|
|
||||||
export default function EscalationQueuePage() {
|
export default function EscalationQueuePage() {
|
||||||
const [count, setCount] = useState<number | null>(null)
|
const [count, setCount] = useState<number | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl p-6">
|
<div className="mx-auto max-w-4xl p-6 space-y-6">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3">
|
||||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-warning-dim">
|
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-warning-dim">
|
||||||
<AlertTriangle size={16} className="text-warning" />
|
<AlertTriangle size={16} className="text-warning" />
|
||||||
</span>
|
</span>
|
||||||
@@ -24,6 +25,10 @@ export default function EscalationQueuePage() {
|
|||||||
<EscalationMetricCard period="30d" />
|
<EscalationMetricCard period="30d" />
|
||||||
|
|
||||||
<EscalationQueue onCountChange={setCount} />
|
<EscalationQueue onCountChange={setCount} />
|
||||||
|
|
||||||
|
{/* L1 AI-build handoffs (GET /l1/escalations). Renders nothing when empty,
|
||||||
|
so engineers without L1 escalations see no change. */}
|
||||||
|
<L1EscalationsSection />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { l1Api } from '@/api/l1'
|
|||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { EmptyStateCard } from '@/components/l1/EmptyStateCard'
|
import { EmptyStateCard } from '@/components/l1/EmptyStateCard'
|
||||||
import { ResumeInProgress } from '@/components/l1/ResumeInProgress'
|
import { ResumeInProgress } from '@/components/l1/ResumeInProgress'
|
||||||
import type { NearMiss, QueueRow } from '@/types/l1'
|
import type { IntakeRequest, NearMiss, QueueRow } from '@/types/l1'
|
||||||
|
|
||||||
export default function L1Dashboard() {
|
export default function L1Dashboard() {
|
||||||
const user = useAuthStore((s) => s.user)
|
const user = useAuthStore((s) => s.user)
|
||||||
@@ -44,23 +44,42 @@ export default function L1Dashboard() {
|
|||||||
setOutOfScope(null)
|
setOutOfScope(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStart = async () => {
|
// Single intake entry point — `opts` selects the variant:
|
||||||
|
// {} → normal match-or-build
|
||||||
|
// { flow_id } → "Use this flow" (bypass matcher, walk that flow)
|
||||||
|
// { force_build: true } → "Build new" (skip match, still category-gated)
|
||||||
|
// { adhoc: true } → out-of-scope "Walk it ad-hoc"
|
||||||
|
// Collapsing the old three near-identical handlers removes the drift that let
|
||||||
|
// "Use this flow" silently re-suggest forever (it never passed the flow_id).
|
||||||
|
const runIntake = async (opts: Partial<IntakeRequest> = {}) => {
|
||||||
if (!problem.trim()) return
|
if (!problem.trim()) return
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
resetPrompts()
|
resetPrompts()
|
||||||
try {
|
try {
|
||||||
// Phase 2A: intake dispatches via match_or_build and returns an `outcome`.
|
|
||||||
const response = await l1Api.intake({
|
const response = await l1Api.intake({
|
||||||
problem_statement: problem.trim(),
|
problem_statement: problem.trim(),
|
||||||
customer_name: customerName.trim() || undefined,
|
customer_name: customerName.trim() || undefined,
|
||||||
customer_contact: customerContact.trim() || undefined,
|
customer_contact: customerContact.trim() || undefined,
|
||||||
|
...opts,
|
||||||
})
|
})
|
||||||
if (response.outcome === 'matched' || response.outcome === 'build') {
|
switch (response.outcome) {
|
||||||
navigate(`/l1/walk/${response.session_id}`)
|
case 'matched':
|
||||||
} else if (response.outcome === 'suggest') {
|
case 'build':
|
||||||
setSuggestion(response.near_miss ?? null)
|
case 'adhoc':
|
||||||
} else if (response.outcome === 'out_of_scope') {
|
if (response.session_id) {
|
||||||
setOutOfScope(response.category ?? 'unknown')
|
navigate(`/l1/walk/${response.session_id}`)
|
||||||
|
} else {
|
||||||
|
// Backend guarantees session_id on these outcomes; guard so a
|
||||||
|
// regression never navigates to /l1/walk/undefined.
|
||||||
|
toast.error('Walk started but no session was returned. Try again.')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'suggest':
|
||||||
|
setSuggestion(response.near_miss ?? null)
|
||||||
|
break
|
||||||
|
case 'out_of_scope':
|
||||||
|
setOutOfScope(response.category ?? 'unknown')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = (err as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
const detail = (err as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||||
@@ -72,47 +91,14 @@ export default function L1Dashboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Use this flow" — re-run intake with the same text; it matches again and
|
const handleStart = () => runIntake()
|
||||||
// returns a `matched` outcome with a started flow session (acceptable Phase 2A).
|
// "Use this flow" — pass the near-miss flow_id so intake walks it directly
|
||||||
const useSuggestedFlow = async () => {
|
// (the matcher can't reliably re-derive the same flow from the same text).
|
||||||
setSubmitting(true)
|
const useSuggestedFlow = () => runIntake({ flow_id: suggestion?.flow_id })
|
||||||
try {
|
// "Build new" — skip the match pass (force_build); still gated by categories.
|
||||||
const response = await l1Api.intake({
|
const buildNew = () => runIntake({ force_build: true })
|
||||||
problem_statement: problem.trim(),
|
// "Walk it ad-hoc" — out-of-scope fallback: a free-form walk (no AI tree).
|
||||||
customer_name: customerName.trim() || undefined,
|
const walkAdhoc = () => runIntake({ adhoc: true })
|
||||||
customer_contact: customerContact.trim() || undefined,
|
|
||||||
})
|
|
||||||
if (response.session_id) navigate(`/l1/walk/${response.session_id}`)
|
|
||||||
else resetPrompts()
|
|
||||||
} catch {
|
|
||||||
toast.error('Could not start the matched flow. Try again.')
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// "Build new" — skip the match pass (force_build); still gated by enabled categories.
|
|
||||||
const buildNew = async () => {
|
|
||||||
setSubmitting(true)
|
|
||||||
resetPrompts()
|
|
||||||
try {
|
|
||||||
const response = await l1Api.intake({
|
|
||||||
problem_statement: problem.trim(),
|
|
||||||
customer_name: customerName.trim() || undefined,
|
|
||||||
customer_contact: customerContact.trim() || undefined,
|
|
||||||
force_build: true,
|
|
||||||
})
|
|
||||||
if (response.outcome === 'build' && response.session_id) {
|
|
||||||
navigate(`/l1/walk/${response.session_id}`)
|
|
||||||
} else if (response.outcome === 'out_of_scope') {
|
|
||||||
setOutOfScope(response.category ?? 'unknown')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
toast.error('Failed to start walk. Try again.')
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// out-of-scope fallback: escalate straight to engineering (no walk).
|
// out-of-scope fallback: escalate straight to engineering (no walk).
|
||||||
const escalateOutOfScope = async () => {
|
const escalateOutOfScope = async () => {
|
||||||
@@ -272,14 +258,23 @@ export default function L1Dashboard() {
|
|||||||
<p className="text-sm text-primary">
|
<p className="text-sm text-primary">
|
||||||
This problem isn’t in your account’s enabled L1 categories
|
This problem isn’t in your account’s enabled L1 categories
|
||||||
{outOfScope !== 'unknown' ? ` (${outOfScope.replace(/_/g, ' ')})` : ''}, so
|
{outOfScope !== 'unknown' ? ` (${outOfScope.replace(/_/g, ' ')})` : ''}, so
|
||||||
there’s no AI-built walk for it. You can escalate it to engineering.
|
there’s no AI-built walk for it. You can still walk it ad-hoc (free-form
|
||||||
|
notes, no AI tree), or escalate it to engineering.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={escalateOutOfScope}
|
onClick={walkAdhoc}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="rounded-md bg-accent text-white px-4 py-2 text-sm font-medium hover:bg-accent/90 transition-colors disabled:opacity-50"
|
className="rounded-md bg-accent text-white px-4 py-2 text-sm font-medium hover:bg-accent/90 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Walk it ad-hoc
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={escalateOutOfScope}
|
||||||
|
disabled={submitting}
|
||||||
|
className="rounded-md border border-default px-4 py-2 text-sm hover:bg-elevated transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Escalate to engineering
|
Escalate to engineering
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ export const router = sentryCreateBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: 'l1-categories',
|
path: 'l1-categories',
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute requiredRole="owner">
|
<ProtectedRoute requireAccountManager>
|
||||||
{page(L1CategoriesPage)}
|
{page(L1CategoriesPage)}
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ export type SessionStatus = 'active' | 'resolved' | 'escalated' | 'abandoned'
|
|||||||
export type TicketKind = 'psa' | 'internal'
|
export type TicketKind = 'psa' | 'internal'
|
||||||
|
|
||||||
export interface WalkStep {
|
export interface WalkStep {
|
||||||
node_id: string
|
// Two shapes coexist (segregated by session_kind): legacy flow/adhoc steps use
|
||||||
question: string
|
// node_id + question; ai_build steps use id + node_type + text. Render with
|
||||||
answer: string
|
// `step.question ?? step.text`.
|
||||||
|
node_id?: string
|
||||||
|
id?: string
|
||||||
|
node_type?: string
|
||||||
|
question?: string
|
||||||
|
text?: string
|
||||||
|
answer: string | null
|
||||||
l1_note: string | null
|
l1_note: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +23,8 @@ export interface AdhocNote {
|
|||||||
export interface WalkSession {
|
export interface WalkSession {
|
||||||
id: string
|
id: string
|
||||||
session_kind: SessionKind
|
session_kind: SessionKind
|
||||||
|
category: string | null
|
||||||
|
problem_text: string | null
|
||||||
flow_id: string | null
|
flow_id: string | null
|
||||||
flow_proposal_id: string | null
|
flow_proposal_id: string | null
|
||||||
current_node_id: string | null
|
current_node_id: string | null
|
||||||
@@ -42,10 +50,11 @@ export interface IntakeRequest {
|
|||||||
customer_name?: string
|
customer_name?: string
|
||||||
customer_contact?: string
|
customer_contact?: string
|
||||||
flow_id?: string
|
flow_id?: string
|
||||||
|
adhoc?: boolean
|
||||||
force_build?: boolean
|
force_build?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IntakeOutcome = 'matched' | 'suggest' | 'out_of_scope' | 'build'
|
export type IntakeOutcome = 'matched' | 'suggest' | 'out_of_scope' | 'build' | 'adhoc'
|
||||||
|
|
||||||
export interface NearMiss {
|
export interface NearMiss {
|
||||||
flow_id: string
|
flow_id: string
|
||||||
@@ -77,8 +86,7 @@ export type TreeNode =
|
|||||||
export interface NextNodeRequest {
|
export interface NextNodeRequest {
|
||||||
node_id?: string
|
node_id?: string
|
||||||
node_text?: string // rendered text of the node being answered
|
node_text?: string // rendered text of the node being answered
|
||||||
answer?: 'yes' | 'no'
|
answer?: 'yes' | 'no' // omit to acknowledge an instruction node
|
||||||
acknowledged?: boolean
|
|
||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface User {
|
|||||||
is_active: boolean
|
is_active: boolean
|
||||||
must_change_password: boolean
|
must_change_password: boolean
|
||||||
account_id: string | null
|
account_id: string | null
|
||||||
account_role: 'owner' | 'engineer' | 'l1_tech' | 'viewer' | null
|
account_role: 'owner' | 'admin' | 'engineer' | 'l1_tech' | 'viewer' | null
|
||||||
can_cover_l1: boolean
|
can_cover_l1: boolean
|
||||||
team_id: string | null
|
team_id: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
|
|||||||
Reference in New Issue
Block a user