111 files across 14 directories: common, tree-editor, kb-accelerator, copilot, assistant, analytics, library, procedural, procedural-editor, public, script-editor, ui, admin, step-library. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
5.7 KiB
TypeScript
187 lines
5.7 KiB
TypeScript
import { ChevronDown, AlertCircle, CheckCircle2, Loader2, Plus, HelpCircle } from 'lucide-react'
|
|
import { useState, useRef, useEffect } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
import type { TreeMarkdownValidationError } from '@/types'
|
|
|
|
interface CodeModeToolbarProps {
|
|
validationErrors: TreeMarkdownValidationError[]
|
|
isValidating: boolean
|
|
isValid: boolean
|
|
onInsertTemplate: (template: string) => void
|
|
onToggleSyntaxHelp: () => void
|
|
syntaxHelpOpen: boolean
|
|
}
|
|
|
|
const NODE_TEMPLATES = {
|
|
decision: [
|
|
'',
|
|
'---',
|
|
'id: new_decision',
|
|
'type: decision',
|
|
'parent: root',
|
|
'---',
|
|
'# What is the question?',
|
|
'',
|
|
'> Help text for the engineer',
|
|
'',
|
|
'- [A] Option A → @target_id',
|
|
'- [B] Option B → @target_id',
|
|
'',
|
|
].join('\n'),
|
|
action: [
|
|
'',
|
|
'---',
|
|
'id: new_action',
|
|
'type: action',
|
|
'parent: root',
|
|
'---',
|
|
'## Action Title',
|
|
'',
|
|
'Description of what to do.',
|
|
'',
|
|
'```commands',
|
|
'command here',
|
|
'```',
|
|
'',
|
|
'**Expected:** Expected outcome',
|
|
'',
|
|
'→ @next_node_id',
|
|
'',
|
|
].join('\n'),
|
|
solution: [
|
|
'',
|
|
'---',
|
|
'id: new_solution',
|
|
'type: solution',
|
|
'parent: root',
|
|
'---',
|
|
'## Solution Title',
|
|
'',
|
|
'Description of the resolution.',
|
|
'',
|
|
'1. Step 1',
|
|
'2. Step 2',
|
|
'',
|
|
].join('\n'),
|
|
}
|
|
|
|
export function CodeModeToolbar({
|
|
validationErrors,
|
|
isValidating,
|
|
isValid,
|
|
onInsertTemplate,
|
|
onToggleSyntaxHelp,
|
|
syntaxHelpOpen,
|
|
}: CodeModeToolbarProps) {
|
|
const [insertOpen, setInsertOpen] = useState(false)
|
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
|
|
const errorCount = validationErrors.filter(e => e.severity === 'error').length
|
|
const warningCount = validationErrors.filter(e => e.severity === 'warning').length
|
|
|
|
// Close dropdown on outside click
|
|
useEffect(() => {
|
|
const handler = (e: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
setInsertOpen(false)
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handler)
|
|
return () => document.removeEventListener('mousedown', handler)
|
|
}, [])
|
|
|
|
return (
|
|
<div className="flex items-center justify-between border-b border-[#1e2130] bg-[#14161d] px-3 py-1.5">
|
|
<div className="flex items-center gap-2">
|
|
{/* Insert Node dropdown */}
|
|
<div className="relative" ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setInsertOpen(!insertOpen)}
|
|
className={cn(
|
|
'flex items-center gap-1 rounded-md border border-[#1e2130] px-2.5 py-1 text-xs font-medium text-[#848b9b]',
|
|
'hover:bg-accent hover:text-[#e2e5eb]'
|
|
)}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
Insert Node
|
|
<ChevronDown className="h-3 w-3" />
|
|
</button>
|
|
{insertOpen && (
|
|
<div className="absolute left-0 top-full z-50 mt-1 w-44 rounded-lg border border-[#1e2130] bg-[#14161d] py-1 shadow-xl">
|
|
<button
|
|
onClick={() => { onInsertTemplate(NODE_TEMPLATES.decision); setInsertOpen(false) }}
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-[#848b9b] hover:bg-accent"
|
|
>
|
|
<span className="h-2 w-2 rounded-full bg-blue-400" />
|
|
Decision
|
|
</button>
|
|
<button
|
|
onClick={() => { onInsertTemplate(NODE_TEMPLATES.action); setInsertOpen(false) }}
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-[#848b9b] hover:bg-accent"
|
|
>
|
|
<span className="h-2 w-2 rounded-full bg-amber-400" />
|
|
Action
|
|
</button>
|
|
<button
|
|
onClick={() => { onInsertTemplate(NODE_TEMPLATES.solution); setInsertOpen(false) }}
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-[#848b9b] hover:bg-accent"
|
|
>
|
|
<span className="h-2 w-2 rounded-full bg-emerald-400" />
|
|
Solution
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Validation status */}
|
|
<div className="flex items-center gap-1.5 text-xs">
|
|
{isValidating ? (
|
|
<>
|
|
<Loader2 className="h-3 w-3 animate-spin text-[#848b9b]" />
|
|
<span className="text-[#848b9b]">Validating...</span>
|
|
</>
|
|
) : isValid ? (
|
|
<>
|
|
<CheckCircle2 className="h-3 w-3 text-emerald-400" />
|
|
<span className="text-emerald-400/70">Synced</span>
|
|
{warningCount > 0 && (
|
|
<span className="text-yellow-400/70">
|
|
({warningCount} warning{warningCount !== 1 ? 's' : ''})
|
|
</span>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<AlertCircle className="h-3 w-3 text-red-400" />
|
|
<span className="text-red-400/70">
|
|
{errorCount} error{errorCount !== 1 ? 's' : ''}
|
|
</span>
|
|
{warningCount > 0 && (
|
|
<span className="text-yellow-400/70">
|
|
, {warningCount} warning{warningCount !== 1 ? 's' : ''}
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Syntax Help toggle */}
|
|
<button
|
|
onClick={onToggleSyntaxHelp}
|
|
className={cn(
|
|
'flex items-center gap-1 rounded-md px-2 py-1 text-xs',
|
|
syntaxHelpOpen
|
|
? 'bg-accent text-[#e2e5eb]'
|
|
: 'text-[#848b9b] hover:bg-accent hover:text-[#848b9b]'
|
|
)}
|
|
>
|
|
<HelpCircle className="h-3 w-3" />
|
|
Syntax
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|