* feat: Add TreeCanvasNode inline editor card component Replaces modal-based node editing with inline expand/collapse cards. Each card shows node type, title, and options in compact mode, then renders the full edit form inline on expand — no modal required. Local draft state with save/cancel prevents premature store writes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: Add TreeCanvas layout with visual branching and orchestration Replaces NodeList + TreePreviewPanel with a single full-width canvas. Decision nodes branch horizontally; action/solution nodes flow vertically. Inline type picker adds nodes without modal interruption. Handles pending link resolution, inbound reference cleanup on delete, and selection sync. CSS dot-grid background + connector lines for structure clarity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: Update forms for inline safety, add MetadataSidePanel, update layout - NodeFormDecision: option reorder via onUpdate (no premature store writes) - NodePicker: add allowCreate prop (default true) to hide Create New options during inline canvas editing, preventing side-effect node creation - MetadataSidePanel: 320px right slide-in overlay wrapping TreeMetadataForm, closes on backdrop click, close button, and Escape key - TreeEditorLayout: Flow mode now renders full-width TreeCanvas + MetadataSidePanel overlay; Code mode unchanged (Monaco + preview split) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: Wire toolbar metadata toggle and integrate canvas layout - Add isMetadataOpen state in TreeEditorPage - Add Metadata toolbar button (visible in Flow mode only) - Auto-close metadata panel when switching to Code mode - Pass isMetadataOpen/onCloseMetadata props through TreeEditorLayout - Update Flow mode toggle tooltip to reflect new canvas editing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add canvas UX fixes design doc (scroll, tooltips, answer stubs) Captures approved design for three post-implementation UX improvements to the tree canvas editor: card scroll fix, info tooltip replacement for hint text, and the new 'answer' node type for sketching decision branches before assigning types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add implementation plan for canvas UX fixes 12-task plan covering scroll fix, info tooltips, and answer stub node type. Each task has exact file paths, code, and build verification steps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: make canvas card expanded area scrollable with sticky header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add fullscreen toggle to Modal, enable in NodeEditorModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add reusable InfoTip component for field-level help Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormDecision Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormAction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormResolution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add 'answer' to NodeType union for branch placeholder stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add AnswerStubCard component for unresolved branch placeholders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: guard NODE_TYPE_CONFIG lookup against 'answer' type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: redesign NodeFormDecision to label-only options, remove NodePicker Users now type answer labels only. Stub nodes are created automatically by TreeCanvas when the decision node is saved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: auto-create answer stubs on decision save, render AnswerStubCard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add answer type to all Record<NodeType> icon and color maps Fixes NodeList, ContinuationModal, NodePicker, and TreePreviewNode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: allow 'answer' type in tree drafts, block on publish Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: block publish if unresolved answer stub nodes exist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: serialize 'answer' stub nodes in markdown output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add defensive guard for answer nodes in session navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add delete button with confirmation to AnswerStubCard Adds an inline delete flow to answer stub placeholder cards: - Trash icon button (top-right, subtle) visible in idle state - Click reveals "Delete this stub?" confirmation with Delete/Cancel - Confirmed delete calls onDelete(nodeId) wired to handleDelete in TreeCanvas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent category Cancel overflow and add Tab/Enter to create options - TreeMetadataForm: add min-w-0 + shrink-0 to keep Cancel button in-panel - NodeFormDecision: Tab or Enter on the last non-empty option input adds a new option and auto-focuses it; empty last input lets Tab pass through normally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: re-sync draft from store when canvas card is opened When a decision node is saved with new options, stub next_node_id values are written back to the store. But the local draft was initialized once at mount and never refreshed, so reopening the card gave a stale draft with empty next_node_ids — causing duplicate stubs on every subsequent save. Fix: reset draft from the live node whenever isExpanded transitions to true. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix+feat: blank options, stub card dismiss, collapsible subtrees - TreeCanvas: strip blank-label options on save so they don't generate stubs; also filter them from the unlinked-option add-button list - AnswerStubCard: collapse type-picker when clicking outside the card - TreeCanvasNode: add subtree collapse toggle button (ChevronsDownUp icon) visible in compact mode when the node has children - TreeCanvas: track collapsedNodeIds; hide subtree behind a clickable "N nodes hidden" pill when collapsed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: stop connector fork line from overlapping child cards Replace the two-element approach (separate fork line + child lanes div with mismatched maxWidth values) with a single relative-positioned container. The fork line is absolutely positioned and its left/right are calculated from the number of children so it spans exactly from the center of the first lane to the center of the last lane. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace Show Drafts checkbox with Drafts tab in Flow Library - Remove the out-of-place checkbox; add 'Drafts' as a tab alongside All | Troubleshooting | Projects | Maintenance - Drafts tab sets showDrafts=true + typeFilter='all' so the API filter still works correctly via include_drafts - Move SortDropdown to the right side next to ViewToggle, so both secondary controls are grouped together Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import type { TreeStructure } from '@/types'
|
|
|
|
export interface TreeMetadata {
|
|
name?: string
|
|
description?: string
|
|
category?: string
|
|
tags?: string[]
|
|
}
|
|
|
|
/**
|
|
* Lightweight frontend serializer for instant preview when switching Form→Code.
|
|
* The backend remains authoritative for actual saves.
|
|
*/
|
|
export function treeStructureToMarkdownPreview(
|
|
structure: TreeStructure,
|
|
metadata?: TreeMetadata,
|
|
): string {
|
|
const blocks: string[] = []
|
|
|
|
if (metadata) {
|
|
const fm = ['---']
|
|
if (metadata.name) fm.push(`name: ${metadata.name}`)
|
|
if (metadata.description) fm.push(`description: ${metadata.description}`)
|
|
if (metadata.category) fm.push(`category: ${metadata.category}`)
|
|
if (metadata.tags?.length) fm.push(`tags: [${metadata.tags.join(', ')}]`)
|
|
fm.push('---')
|
|
blocks.push(fm.join('\n'))
|
|
}
|
|
|
|
serializeNode(structure, null, blocks)
|
|
return blocks.join('\n\n') + '\n'
|
|
}
|
|
|
|
function serializeNode(
|
|
node: TreeStructure,
|
|
parentId: string | null,
|
|
blocks: string[],
|
|
): void {
|
|
const fm = ['---', `id: ${node.id}`, `type: ${node.type}`]
|
|
if (parentId !== null) fm.push(`parent: ${parentId}`)
|
|
fm.push('---')
|
|
|
|
const body: string[] = []
|
|
|
|
if (node.type === 'decision') {
|
|
if (node.question) {
|
|
body.push(`# ${node.question}`, '')
|
|
}
|
|
if (node.help_text) {
|
|
for (const line of node.help_text.split('\n')) {
|
|
body.push(`> ${line}`)
|
|
}
|
|
body.push('')
|
|
}
|
|
if (node.options) {
|
|
node.options.forEach((opt, i) => {
|
|
const letter = String.fromCharCode(65 + i)
|
|
if (opt.next_node_id) {
|
|
body.push(`- [${letter}] ${opt.label} → @${opt.next_node_id}`)
|
|
} else {
|
|
body.push(`- [${letter}] ${opt.label}`)
|
|
}
|
|
})
|
|
}
|
|
} else if (node.type === 'action') {
|
|
if (node.title) body.push(`## ${node.title}`, '')
|
|
if (node.description) body.push(node.description, '')
|
|
if (node.commands?.length) {
|
|
body.push('```commands')
|
|
node.commands.forEach(cmd => body.push(cmd))
|
|
body.push('```', '')
|
|
}
|
|
if (node.expected_outcome) body.push(`**Expected:** ${node.expected_outcome}`, '')
|
|
if (node.next_node_id) body.push(`→ @${node.next_node_id}`)
|
|
} else if (node.type === 'solution') {
|
|
if (node.title) body.push(`## ${node.title}`, '')
|
|
if (node.description) body.push(node.description, '')
|
|
if (node.resolution_steps?.length) {
|
|
node.resolution_steps.forEach((step, i) => body.push(`${i + 1}. ${step}`))
|
|
}
|
|
} else if (node.type === 'answer') {
|
|
// Answer placeholder — render as a clearly marked stub
|
|
body.push(`## [ANSWER PLACEHOLDER] ${node.title || 'Untitled'}`, '')
|
|
body.push('> This is an unresolved answer stub. Convert it to a Decision, Action, or Solution before publishing.')
|
|
}
|
|
|
|
blocks.push(fm.join('\n') + '\n' + body.join('\n'))
|
|
|
|
if (node.children) {
|
|
for (const child of node.children) {
|
|
serializeNode(child, node.id, blocks)
|
|
}
|
|
}
|
|
}
|