Files
resolutionflow/frontend/src/components/ai-builder/AIFlowBuilderModal.tsx
chihlasm 2196886cd2 feat: rename AI builder to "Flow Assist" and add sidebar nav item
- Rename all "Build with AI" labels to "Flow Assist"
- Update CreateFlowDropdown description to "AI-powered flow builder"
- Add Flow Assist nav item with Sparkles icon to sidebar (both
  expanded and collapsed states)
- Update AIFlowBuilderModal titles for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:55:51 -05:00

169 lines
4.6 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { Modal } from '@/components/common/Modal'
import { useAIFlowBuilderStore } from '@/store/aiFlowBuilderStore'
import { treesApi } from '@/api/trees'
import { toast } from '@/lib/toast'
import { WizardStepIndicator } from './WizardStepIndicator'
import { FoundationForm } from './FoundationForm'
import { BranchSelector } from './BranchSelector'
import { BranchDetailView } from './BranchDetailView'
import { TreePreviewCard } from './TreePreviewCard'
import { GeneratingAnimation } from './GeneratingAnimation'
interface AIFlowBuilderModalProps {
isOpen: boolean
onClose: () => void
}
export function AIFlowBuilderModal({ isOpen, onClose }: AIFlowBuilderModalProps) {
const navigate = useNavigate()
const {
phase,
metadata,
assembledTree,
loadQuota,
scaffold,
reset,
} = useAIFlowBuilderStore()
// Load quota when modal opens
useEffect(() => {
if (isOpen) {
loadQuota()
}
}, [isOpen, loadQuota])
// Auto-trigger scaffold after conversation starts (ref prevents double-fire)
const hasTriggeredScaffold = useRef(false)
useEffect(() => {
// Reset guard when wizard resets to foundation (Start Over or close)
if (phase === 'foundation') {
hasTriggeredScaffold.current = false
return
}
if (phase === 'scaffolding' && !hasTriggeredScaffold.current && !useAIFlowBuilderStore.getState().suggestedBranches.length) {
hasTriggeredScaffold.current = true
scaffold()
}
}, [phase, scaffold])
const handleClose = () => {
reset()
onClose()
}
const createTree = async () => {
if (!assembledTree) return null
try {
return await treesApi.create({
name: assembledTree.suggested_name,
description: assembledTree.suggested_description,
tree_structure: assembledTree.tree_structure,
tree_type: metadata.flow_type,
status: 'draft',
})
} catch {
toast.error('Failed to create flow. Please try again.')
return null
}
}
const handleOpenInEditor = async () => {
const tree = await createTree()
if (!tree) return
handleClose()
const editorPath =
metadata.flow_type === 'procedural'
? `/flows/${tree.id}/edit`
: `/trees/${tree.id}/edit`
navigate(editorPath)
}
const handleStartFlow = async () => {
const tree = await createTree()
if (!tree) return
handleClose()
const navigatePath =
metadata.flow_type === 'procedural'
? `/flows/${tree.id}/navigate`
: `/trees/${tree.id}/navigate`
navigate(navigatePath)
}
const handleBuildAnother = () => {
reset()
}
const getTitle = () => {
switch (phase) {
case 'foundation':
return 'Flow Assist'
case 'scaffolding':
case 'generating':
return 'AI Scaffold'
case 'detailing':
return 'Branch Detail'
case 'reviewing':
return 'Review & Assemble'
case 'error':
return 'Flow Assist'
default:
return 'Flow Assist'
}
}
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title={getTitle()}
size="lg"
footer={
<WizardStepIndicator phase={phase} />
}
>
{phase === 'foundation' && <FoundationForm />}
{phase === 'scaffolding' && <BranchSelector />}
{phase === 'generating' && <GeneratingAnimation />}
{phase === 'detailing' && <BranchDetailView />}
{phase === 'reviewing' && (
<TreePreviewCard
onOpenInEditor={handleOpenInEditor}
onStartFlow={handleStartFlow}
onBuildAnother={handleBuildAnother}
/>
)}
{phase === 'error' && <ErrorView />}
</Modal>
)
}
function ErrorView() {
const { error, reset, setPhase } = useAIFlowBuilderStore()
return (
<div className="flex flex-col items-center gap-4 py-8">
<div className="rounded-lg border border-red-400/20 bg-red-400/5 px-4 py-3 text-sm text-red-400">
{error || 'An unexpected error occurred.'}
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => setPhase('foundation')}
className="rounded-lg border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
Go Back
</button>
<button
type="button"
onClick={reset}
className="rounded-lg bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
>
Start Over
</button>
</div>
</div>
)
}