feat: integrate AI panel, context menu, and ghost steps in procedural editor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,11 @@ function SortableStepWrapper({
|
||||
)
|
||||
}
|
||||
|
||||
export function StepList() {
|
||||
interface StepListProps {
|
||||
onStepContextMenu?: (e: React.MouseEvent, stepId: string) => void
|
||||
}
|
||||
|
||||
export function StepList({ onStepContextMenu }: StepListProps) {
|
||||
const {
|
||||
steps,
|
||||
intakeForm,
|
||||
@@ -208,6 +212,7 @@ export function StepList() {
|
||||
const contentType = step.content_type || 'action'
|
||||
const config = contentTypeConfig[contentType]
|
||||
const Icon = config.icon
|
||||
const isGhost = !!(step as unknown as Record<string, unknown>)._suggestion
|
||||
|
||||
if (isExpanded) {
|
||||
return (
|
||||
@@ -232,53 +237,80 @@ export function StepList() {
|
||||
{({ dragHandleProps }) => (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex items-center gap-2 rounded-xl border border-border px-3 py-2.5 transition-colors',
|
||||
'hover:border-primary/30 hover:bg-accent/50'
|
||||
'group flex flex-col rounded-xl border border-border px-3 py-2.5 transition-colors',
|
||||
'hover:border-primary/30 hover:bg-accent/50',
|
||||
isGhost && 'border-l-2 border-dashed !border-l-primary/40 opacity-60'
|
||||
)}
|
||||
onContextMenu={(e) => onStepContextMenu?.(e, step.id)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 cursor-grab touch-none text-muted-foreground active:cursor-grabbing"
|
||||
aria-label={`Drag to reorder step ${stepNumber}: ${step.title || 'Untitled step'}`}
|
||||
{...dragHandleProps}
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 cursor-grab touch-none text-muted-foreground active:cursor-grabbing"
|
||||
aria-label={`Drag to reorder step ${stepNumber}: ${step.title || 'Untitled step'}`}
|
||||
{...dragHandleProps}
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-accent text-xs font-medium text-muted-foreground">
|
||||
{stepNumber}
|
||||
</span>
|
||||
|
||||
<span className={cn('shrink-0', config.color)}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="min-w-0 flex-1 cursor-pointer truncate text-sm text-foreground"
|
||||
onClick={() => setExpandedStepId(step.id)}
|
||||
>
|
||||
{step.title || 'Untitled step'}
|
||||
</span>
|
||||
|
||||
{step.estimated_minutes && (
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||
~{step.estimated_minutes}m
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-accent text-xs font-medium text-muted-foreground">
|
||||
{stepNumber}
|
||||
</span>
|
||||
|
||||
<span className={cn('shrink-0', config.color)}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="min-w-0 flex-1 cursor-pointer truncate text-sm text-foreground"
|
||||
onClick={() => setExpandedStepId(step.id)}
|
||||
>
|
||||
{step.title || 'Untitled step'}
|
||||
</span>
|
||||
|
||||
{step.estimated_minutes && (
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||
~{step.estimated_minutes}m
|
||||
</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setExpandedStepId(step.id)}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => removeStep(step.id)}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 hover:bg-red-500/20 hover:text-red-400 group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isGhost && (
|
||||
<div className="mt-2 flex gap-2 border-t border-border/50 pt-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// TODO: accept suggestion
|
||||
}}
|
||||
className="flex-1 rounded-md bg-emerald-500/20 px-2 py-1 text-xs text-emerald-400 hover:bg-emerald-500/30 transition-colors"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// TODO: dismiss suggestion
|
||||
}}
|
||||
className="flex-1 rounded-md bg-rose-500/20 px-2 py-1 text-xs text-rose-400 hover:bg-rose-500/30 transition-colors"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setExpandedStepId(step.id)}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => removeStep(step.id)}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 hover:bg-red-500/20 hover:text-red-400 group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</SortableStepWrapper>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Save, ArrowLeft, ListOrdered, Wrench, Settings, FileText, Calendar } from 'lucide-react'
|
||||
import { Save, ArrowLeft, ListOrdered, Wrench, Settings, FileText, Calendar, Sparkles, Layers } from 'lucide-react'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { useProceduralEditorStore } from '@/store/proceduralEditorStore'
|
||||
import { CollapsibleEditorSection } from '@/components/procedural-editor/CollapsibleEditorSection'
|
||||
@@ -10,6 +10,10 @@ import { getScheduleSummary } from '@/components/procedural-editor/scheduleUtils
|
||||
import { StepList } from '@/components/procedural-editor/StepList'
|
||||
import { TagInput } from '@/components/common/TagInput'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
|
||||
import { ContextMenu } from '@/components/common/ContextMenu'
|
||||
import { useEditorAI } from '@/hooks/useEditorAI'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { TreeType, MaintenanceSchedule, TargetList } from '@/types'
|
||||
|
||||
@@ -44,6 +48,13 @@ export function ProceduralEditorPage() {
|
||||
getTreeForSave,
|
||||
} = useProceduralEditorStore()
|
||||
|
||||
const steps = useProceduralEditorStore(s => s.steps)
|
||||
|
||||
const editorAI = useEditorAI({
|
||||
flowType: 'procedural',
|
||||
treeId: id,
|
||||
})
|
||||
|
||||
const isMaintenance = treeType === 'maintenance'
|
||||
const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure'
|
||||
|
||||
@@ -175,6 +186,19 @@ export function ProceduralEditorPage() {
|
||||
{isDirty && (
|
||||
<span className="text-xs text-muted-foreground">Unsaved changes</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => editorAI.isOpen ? editorAI.closePanel() : editorAI.openPanel()}
|
||||
title="Toggle AI Assist panel"
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors',
|
||||
editorAI.isOpen
|
||||
? 'bg-primary/10 text-primary border-primary/30'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
AI Assist
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSave('draft')}
|
||||
disabled={isSaving}
|
||||
@@ -272,10 +296,56 @@ export function ProceduralEditorPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step List — flex-1, scrolls independently */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||
<StepList />
|
||||
{/* Step List + AI Panel */}
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<StepList onStepContextMenu={editorAI.openContextMenu} />
|
||||
</div>
|
||||
|
||||
<EditorAIPanel
|
||||
isOpen={editorAI.isOpen}
|
||||
onClose={editorAI.closePanel}
|
||||
focalNode={null}
|
||||
flowName={name}
|
||||
flowType={isMaintenance ? 'maintenance' : 'procedural'}
|
||||
nodeCount={steps.length}
|
||||
messages={editorAI.messages}
|
||||
input={editorAI.input}
|
||||
onInputChange={editorAI.setInput}
|
||||
onSend={editorAI.sendMessage}
|
||||
isLoading={editorAI.isLoading}
|
||||
suggestions={editorAI.suggestions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editorAI.contextMenu && (
|
||||
<ContextMenu
|
||||
position={editorAI.contextMenu.position}
|
||||
items={[
|
||||
{
|
||||
id: 'generate-steps',
|
||||
label: 'Generate Steps After',
|
||||
icon: <Sparkles className="h-4 w-4" />,
|
||||
onClick: () => editorAI.triggerAction(
|
||||
editorAI.contextMenu!.nodeId,
|
||||
'add_steps',
|
||||
`Generate steps after this step`
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'expand-step',
|
||||
label: 'Expand Step',
|
||||
icon: <Layers className="h-4 w-4" />,
|
||||
onClick: () => editorAI.triggerAction(
|
||||
editorAI.contextMenu!.nodeId,
|
||||
'quick_action',
|
||||
`Expand this step into detailed substeps`
|
||||
),
|
||||
},
|
||||
]}
|
||||
onClose={editorAI.closeContextMenu}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user