feat: add procedural flow support to AI chat builder (Flow Assist)
- Add procedural-specific system prompts (schema, interview protocol, response format) - Dispatch prompts by flow_type: procedural/maintenance use flat steps schema, troubleshooting uses decision tree schema - Parse [STEPS_UPDATE] and [INTAKE_FORM] markers in AI responses - Add validate_generated_procedural_steps() validator - Handle intake form extraction in AI chat import endpoint - Add StaticStepsPreview component for procedural flow preview - Update store and page to render correct preview by flow type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
112
frontend/src/components/ai-chat/StaticStepsPreview.tsx
Normal file
112
frontend/src/components/ai-chat/StaticStepsPreview.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { ProceduralStep } from '@/types'
|
||||
import { Terminal, Info, CheckSquare, AlertTriangle, LayoutList } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface StaticStepsPreviewProps {
|
||||
steps: ProceduralStep[]
|
||||
name?: string
|
||||
}
|
||||
|
||||
const CONTENT_TYPE_ICONS: Record<string, typeof Terminal> = {
|
||||
action: Terminal,
|
||||
informational: Info,
|
||||
verification: CheckSquare,
|
||||
warning: AlertTriangle,
|
||||
}
|
||||
|
||||
export function StaticStepsPreview({ steps, name }: StaticStepsPreviewProps) {
|
||||
let stepNumber = 0
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b border-border px-4 py-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Preview: {name || 'Untitled Flow'}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{steps.filter((s) => s.type === 'procedure_step').length} steps
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="space-y-1.5">
|
||||
{steps.map((step) => {
|
||||
if (step.type === 'section_header') {
|
||||
return (
|
||||
<div key={step.id} className="pt-3 pb-1 first:pt-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutList className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-primary">
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (step.type === 'procedure_end') {
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className="mt-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2"
|
||||
>
|
||||
<span className="text-xs font-medium text-emerald-400">
|
||||
{step.title || 'Procedure Complete'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
stepNumber++
|
||||
const contentType = step.content_type || 'action'
|
||||
const Icon = CONTENT_TYPE_ICONS[contentType] || Terminal
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
'rounded-lg border px-3 py-2 text-xs',
|
||||
contentType === 'warning'
|
||||
? 'border-amber-500/20 bg-amber-500/5'
|
||||
: 'border-border bg-card'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded bg-primary/10 font-label text-[0.5rem] text-primary">
|
||||
{stepNumber}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon className={cn(
|
||||
'h-3 w-3 shrink-0',
|
||||
contentType === 'warning' ? 'text-amber-400' : 'text-muted-foreground'
|
||||
)} />
|
||||
<span className={cn(
|
||||
'font-medium truncate',
|
||||
contentType === 'warning' ? 'text-amber-400' : 'text-foreground'
|
||||
)}>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
{step.commands && (
|
||||
<div className="mt-1 flex items-center gap-1 text-muted-foreground">
|
||||
<Terminal className="h-2.5 w-2.5" />
|
||||
<span className="font-label text-[0.5rem]">
|
||||
{Array.isArray(step.commands) ? step.commands.length : 1} command{(Array.isArray(step.commands) ? step.commands.length : 1) !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{step.estimated_minutes && (
|
||||
<span className="shrink-0 font-label text-[0.5rem] text-muted-foreground">
|
||||
~{step.estimated_minutes}m
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import { ChatPanel } from '@/components/ai-chat/ChatPanel'
|
||||
import { ChatToolbar } from '@/components/ai-chat/ChatToolbar'
|
||||
import { EmptyPreview } from '@/components/ai-chat/EmptyPreview'
|
||||
import { StaticTreePreview } from '@/components/ai-chat/StaticTreePreview'
|
||||
import { StaticStepsPreview } from '@/components/ai-chat/StaticStepsPreview'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { getTreeEditorPath } from '@/lib/routing'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { TreeStructure } from '@/types'
|
||||
import type { TreeStructure, ProceduralStep } from '@/types'
|
||||
|
||||
export function AIChatBuilderPage() {
|
||||
const navigate = useNavigate()
|
||||
@@ -113,7 +114,8 @@ export function AIChatBuilderPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const previewTree = (generatedTree || workingTree) as TreeStructure | null
|
||||
const previewData = generatedTree || workingTree
|
||||
const isProceduralPreview = previewData && 'steps' in previewData
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
@@ -139,13 +141,20 @@ export function AIChatBuilderPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right panel: Tree preview (40%) — hidden below 1024px */}
|
||||
{/* Right panel: Preview (40%) — hidden below 1024px */}
|
||||
<div className="w-2/5 overflow-hidden bg-background max-lg:hidden">
|
||||
{previewTree ? (
|
||||
<StaticTreePreview
|
||||
tree={previewTree}
|
||||
name={treeMetadata?.name}
|
||||
/>
|
||||
{previewData ? (
|
||||
isProceduralPreview ? (
|
||||
<StaticStepsPreview
|
||||
steps={(previewData as { steps: ProceduralStep[] }).steps}
|
||||
name={treeMetadata?.name}
|
||||
/>
|
||||
) : (
|
||||
<StaticTreePreview
|
||||
tree={previewData as TreeStructure}
|
||||
name={treeMetadata?.name}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<EmptyPreview />
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ChatMessage,
|
||||
InterviewPhase,
|
||||
TreeStructure,
|
||||
ProceduralStep,
|
||||
} from '@/types'
|
||||
|
||||
interface TreeMetadata {
|
||||
@@ -25,12 +26,12 @@ interface AIChatState {
|
||||
messages: ChatMessage[]
|
||||
isResponding: boolean
|
||||
|
||||
// Progressive tree
|
||||
workingTree: TreeStructure | null
|
||||
// Progressive tree (troubleshooting = TreeStructure, procedural = {steps})
|
||||
workingTree: TreeStructure | { steps: ProceduralStep[] } | null
|
||||
treeMetadata: TreeMetadata | null
|
||||
|
||||
// Final generation
|
||||
generatedTree: TreeStructure | null
|
||||
generatedTree: TreeStructure | { steps: ProceduralStep[] } | null
|
||||
isGenerating: boolean
|
||||
importedTreeId: string | null
|
||||
|
||||
@@ -121,7 +122,7 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
set((state) => ({
|
||||
messages: [...state.messages, aiMessage],
|
||||
currentPhase: response.current_phase,
|
||||
workingTree: (response.working_tree as TreeStructure | null) ?? state.workingTree,
|
||||
workingTree: (response.working_tree as TreeStructure | { steps: ProceduralStep[] } | null) ?? state.workingTree,
|
||||
treeMetadata: (response.tree_metadata as TreeMetadata | null) ?? state.treeMetadata,
|
||||
isResponding: false,
|
||||
}))
|
||||
@@ -139,8 +140,8 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
try {
|
||||
const response = await aiChatApi.generateTree(sessionId)
|
||||
set({
|
||||
generatedTree: response.tree_structure as unknown as TreeStructure,
|
||||
workingTree: response.tree_structure as unknown as TreeStructure,
|
||||
generatedTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] },
|
||||
workingTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] },
|
||||
treeMetadata: response.tree_metadata as TreeMetadata,
|
||||
status: 'completed',
|
||||
isGenerating: false,
|
||||
@@ -182,9 +183,9 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
currentPhase: session.current_phase,
|
||||
flowType: session.flow_type,
|
||||
messages: session.conversation_history as ChatMessage[],
|
||||
workingTree: session.working_tree as TreeStructure | null,
|
||||
workingTree: session.working_tree as TreeStructure | { steps: ProceduralStep[] } | null,
|
||||
treeMetadata: session.tree_metadata as TreeMetadata | null,
|
||||
generatedTree: session.generated_tree as TreeStructure | null,
|
||||
generatedTree: session.generated_tree as TreeStructure | { steps: ProceduralStep[] } | null,
|
||||
isResponding: false,
|
||||
})
|
||||
} catch (e: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user