feat: add dual-mode tree editor with Code Mode, variables, and markdown sync
Implements the full dual-mode tree editor (Plan Phases 1-5): Backend: - JSONB↔Markdown bidirectional serializer/parser with mistune - Markdown validator with line/column error reporting - 3 API endpoints: export-markdown, import-markdown, validate-markdown - Variable extraction/resolution service ([USER_INPUT], [VAR], [SAVE_AS]) - Session variables JSONB column (migration 028) - 39 tree markdown tests + variable service tests (403 total passing) Frontend: - Monaco-based Code Mode with custom Monarch tokenizer and dark theme - Autocomplete for @node_id refs, type values, variable names - Debounced validation (800ms) with inline Monaco error markers - Syntax help panel (absolute overlay, toggleable) - Starter template for new trees with valid cross-references - Bidirectional metadata sync (name/description/category/tags frontmatter) - Synchronous tree→markdown serializer (fixes async race condition) - Pre-save validation blocks save on broken refs or missing tree name - Mode-aware undo/redo: Monaco native in Code Mode, throttled zundo in Flow Mode - Variable prompt modal and frontend resolver for session navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
73
frontend/src/components/session/VariablePromptModal.tsx
Normal file
73
frontend/src/components/session/VariablePromptModal.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface VariablePromptModalProps {
|
||||
/** The prompt text from [USER_INPUT:prompt] */
|
||||
prompt: string
|
||||
/** Called with the user's input value */
|
||||
onSubmit: (value: string) => void
|
||||
/** Called when user cancels */
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function VariablePromptModal({ prompt, onSubmit, onCancel }: VariablePromptModalProps) {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (value.trim()) {
|
||||
onSubmit(value.trim())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="glass-card w-full max-w-md rounded-2xl p-6 shadow-lg">
|
||||
<h2 className="mb-1 text-lg font-semibold text-white">Input Required</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
This step requires you to provide a value.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label className="mb-2 block text-sm font-medium text-white/70">
|
||||
{prompt}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Enter value..."
|
||||
autoFocus
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!value.trim()}
|
||||
className={cn(
|
||||
'flex-1 rounded-lg bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className={cn(
|
||||
'rounded-lg border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user