fix: folder org

This commit is contained in:
2026-03-12 11:22:01 +01:00
parent 4184a60706
commit 7a6a4a43c5
10 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,240 @@
/**
* Registry of config node types. Each type defines syntax highlighting,
* insert-menu blocks, and how to render the content (after Nunjucks) for the renderer.
* Add a new entry here and wire its language in ConfigNode to add a new config type.
*/
export type ConfigTypeId = 'plantuml' | 'markdown' | 'wireframe'
/** A single insert option (label + snippet to insert at cursor). */
export type InsertBlock = { label: string; snippet: string }
/** A group of insert options (e.g. "Diagram" with startuml, actor, etc.). */
export type InsertBlockGroup = { label: string; items: InsertBlock[] }
export type InsertBlockOrGroup = InsertBlock | InsertBlockGroup
function isGroup(b: InsertBlockOrGroup): b is InsertBlockGroup {
return 'items' in b && Array.isArray((b as InsertBlockGroup).items)
}
/** Optional options passed to render (e.g. node size for wireframe SVG). */
export type RenderOptions = { width?: number; height?: number }
/** How the renderer should display this type: HTML in a div, or image (SVG/PNG) in a viewport. */
export type ConfigOutputType = 'html' | 'image'
export type ConfigType = {
id: ConfigTypeId
label: string
/** How the RenderingNode should display output: 'html' (div) or 'image' (viewport). */
outputType: ConfigOutputType
/** CodeMirror language key; used to pick the extension in ConfigNode. */
language: ConfigTypeId
/** Options under Insert → [type-specific]. Can be flat blocks or groups. */
insertBlocks: InsertBlockOrGroup[]
/** Render resolved content (after Nunjucks) to HTML/SVG string for the renderer. */
render: (content: string, options?: RenderOptions) => Promise<string>
}
const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg'
const KROKI_TIMEOUT_MS = 15000
const PLANTUML_INSERT_BLOCKS: InsertBlockOrGroup[] = [
{ label: 'Diagram start/end', snippet: '@startuml\n\n@enduml' },
{ label: 'Actor', snippet: 'actor ' },
{ label: 'Participant', snippet: 'participant "" as ' },
{ label: 'Arrow', snippet: ' -> ' },
{ label: 'Note', snippet: 'note right of ' },
{
label: 'Templating',
items: [
{ label: 'Variable {{ }}', snippet: '{{ }}' },
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
{ label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' },
{ label: 'set', snippet: '{% set = %}' },
{ label: 'extends', snippet: '{% extends "" %}' },
{ label: 'include', snippet: '{% include "" %}' },
{ label: 'import', snippet: '{% import "" as %}' },
],
},
]
const MARKDOWN_INSERT_BLOCKS: InsertBlockOrGroup[] = [
{ label: 'Heading 1', snippet: '# ' },
{ label: 'Heading 2', snippet: '## ' },
{ label: 'Heading 3', snippet: '### ' },
{ label: 'Bold', snippet: '****' },
{ label: 'Italic', snippet: '**' },
{ label: 'Code inline', snippet: '``' },
{ label: 'Code block', snippet: '```\n\n```' },
{ label: 'Link', snippet: '[]()' },
{ label: 'Image', snippet: '![]()' },
{ label: 'List item', snippet: '- ' },
{ label: 'Blockquote', snippet: '> ' },
{
label: 'Templating',
items: [
{ label: 'Variable {{ }}', snippet: '{{ }}' },
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
{ label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' },
{ label: 'set', snippet: '{% set = %}' },
],
},
]
/** Wireweave DSL: https://github.com/wireweave/core */
const WIREFRAME_INSERT_BLOCKS: InsertBlockOrGroup[] = [
{ label: 'Page', snippet: 'page "Title" {\n \n}' },
{ label: 'Card', snippet: 'card p=4 {\n \n}' },
{ label: 'Title', snippet: 'title "' },
{ label: 'Text', snippet: 'text "' },
{ label: 'Button', snippet: 'button "Label"' },
{ label: 'Primary button', snippet: 'button "Label" primary' },
{ label: 'Input', snippet: 'input placeholder=""' },
{ label: 'Row', snippet: 'row {\n col span=6 { }\n}' },
{ label: 'Col', snippet: 'col span=6 { }' },
{ label: 'Header / Main / Footer', snippet: 'header { }\nmain { }\nfooter { }' },
{ label: 'Image', snippet: 'image "" w=400 h=300' },
{
label: 'Templating',
items: [
{ label: 'Variable {{ }}', snippet: '{{ }}' },
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
{ label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' },
{ label: 'set', snippet: '{% set = %}' },
],
},
]
/** Markdown to HTML using dynamic import to avoid loading if only PlantUML is used. */
async function renderMarkdown(content: string): Promise<string> {
const { marked } = await import('marked')
const html = typeof marked.parse === 'function' ? await marked.parse(content) : (marked as (s: string) => string)(content)
return typeof html === 'string' ? html : String(html)
}
/** Wireweave DSL to SVG; theme from document dark mode. See https://www.wireweave.org/ */
async function renderWireframe(content: string, options?: RenderOptions): Promise<string> {
const { parse, renderToSvg } = await import('@wireweave/core')
const doc = parse(content)
const isDark =
typeof document !== 'undefined' &&
document.documentElement?.classList?.contains('dark')
const { svg } = renderToSvg(doc, {
theme: isDark ? 'dark' : 'light',
width: options?.width ?? 1200,
height: options?.height,
padding: 24,
})
return svg
}
export const CONFIG_TYPES: ConfigType[] = [
{
id: 'plantuml',
label: 'Diagram',
outputType: 'image',
language: 'plantuml',
insertBlocks: PLANTUML_INSERT_BLOCKS,
render: async (content: string) => {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), KROKI_TIMEOUT_MS)
try {
const res = await fetch(KROKI_PLANTUML_SVG, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: content,
signal: controller.signal,
})
clearTimeout(timeoutId)
if (!res.ok) {
const err = await res.text()
if (res.status >= 500) {
throw new Error('Diagram service unavailable. Try again later.')
}
throw new Error(res.status === 400 ? err || 'Invalid PlantUML' : `Kroki error: ${res.status}`)
}
return res.text()
} catch (err: unknown) {
clearTimeout(timeoutId)
if (err instanceof Error) {
if (err.name === 'AbortError') {
throw new Error('Diagram request timed out. The service may be slow or unavailable.')
}
if (err.message.includes('fetch') || err.message.includes('network') || err.message.includes('Failed to fetch')) {
throw new Error('Diagram service unavailable. Check your connection or try again later.')
}
}
throw err
}
},
},
{
id: 'markdown',
label: 'Markdown',
outputType: 'html',
language: 'markdown',
insertBlocks: MARKDOWN_INSERT_BLOCKS,
render: renderMarkdown,
},
{
id: 'wireframe',
label: 'Wireframe',
outputType: 'image',
language: 'markdown',
insertBlocks: WIREFRAME_INSERT_BLOCKS,
render: renderWireframe,
},
]
export const CONFIG_TYPE_IDS = CONFIG_TYPES.map((t) => t.id)
export const DEFAULT_CONFIG_TYPE_ID: ConfigTypeId = 'plantuml'
export function getConfigType(id: ConfigTypeId): ConfigType {
const t = CONFIG_TYPES.find((c) => c.id === id)
if (!t) throw new Error(`Unknown config type: ${id}`)
return t
}
export function getDefaultContentForConfigType(configTypeId: ConfigTypeId): string {
if (configTypeId === 'plantuml') return '@startuml\n\n@enduml\n'
if (configTypeId === 'markdown') return ''
if (configTypeId === 'wireframe') {
return `page "Hello" {
card p=4 {
title "Welcome"
text "Hello, wireweave!"
button "Get Started" primary
}
}`
}
return ''
}
/** Flatten insert blocks for iteration: either a single block or a group's items. */
export function* iterateInsertBlocks(blocks: InsertBlockOrGroup[]): Generator<InsertBlock> {
for (const b of blocks) {
if (isGroup(b)) {
for (const item of b.items) yield item
} else {
yield b
}
}
}
export { isGroup }
/** Content from config node data (backward compat: content ?? plantuml). */
export function getConfigContent(data: Record<string, unknown> | undefined): string {
if (!data) return ''
const content = data.content ?? data.plantuml
return typeof content === 'string' ? content : ''
}
/** Config type id from data (default plantuml). */
export function getConfigTypeId(data: Record<string, unknown> | undefined): ConfigTypeId {
if (!data || data.configType == null) return 'plantuml'
const id = data.configType
return CONFIG_TYPE_IDS.includes(id as ConfigTypeId) ? (id as ConfigTypeId) : 'plantuml'
}

View File

@@ -0,0 +1,51 @@
/**
* Connection status: visual state of an edge (color/class).
* Priority when multiple apply: error > paused > updating > default.
*
* Status is derived from node lifecycle state: nodes report updating / paused / error
* via useSyncConnectionStatus() in nodeLifecycle.ts, which updates FlowContext sets.
* Edges read those sets here to pick the single status per edge.
*/
export type ConnectionStatus = 'default' | 'updating' | 'paused' | 'error'
export type ConnectionStatusInputs = {
source: string
target: string
pathNodeIds: Set<string>
pausedSegmentNodeIds: Set<string>
activeSegmentNodeIds: Set<string>
errorTargetNodeIds: Set<string>
}
/**
* Compute the single connection status for an edge (priority: error > paused > updating > default).
* Use in edge components; add new statuses by extending the type and adding a branch here.
*/
export function getConnectionStatus(inputs: ConnectionStatusInputs): ConnectionStatus {
const { target, pathNodeIds, pausedSegmentNodeIds, activeSegmentNodeIds, errorTargetNodeIds, source } = inputs
if (errorTargetNodeIds.has(target)) return 'error'
if (
pathNodeIds.has(source) &&
pathNodeIds.has(target) &&
pausedSegmentNodeIds.has(source) &&
pausedSegmentNodeIds.has(target)
)
return 'paused'
if (
pathNodeIds.has(source) &&
pathNodeIds.has(target) &&
activeSegmentNodeIds.has(source) &&
activeSegmentNodeIds.has(target)
)
return 'updating'
return 'default'
}
/** CSS class suffix for each status (animated-edge-path--{status}). */
export const CONNECTION_STATUS_CLASS: Record<ConnectionStatus, string> = {
default: '',
updating: 'animated-edge-path--updating',
paused: 'animated-edge-path--paused',
error: 'animated-edge-path--error',
}

View File

@@ -5,8 +5,8 @@
*/
import type React from 'react'
import { registerSourceRenderingLogic } from '../sourceRenderingLogic'
import type { SourceRenderingLogic } from '../sourceRenderingLogic'
import { registerSourceRenderingLogic } from './sourceRenderingLogic'
import type { SourceRenderingLogic } from './sourceRenderingLogic'
/** Node classification for UI: Psyche, Pneuma, Physis, Archon (AI Agent). */
export type NodeClassification = 'psyche' | 'pneuma' | 'physis' | 'archon'

View File

@@ -19,7 +19,7 @@
import type React from 'react'
import type { NodeTypeDescriptor, NodeClassification, NodeHelpEntry } from './nodeRegistry'
import type { SourceRenderingLogic } from '../sourceRenderingLogic'
import type { SourceRenderingLogic } from './sourceRenderingLogic'
type OptionalDescriptor = Partial<
Omit<

View File

@@ -0,0 +1,47 @@
/**
* Source rendering logic: each node type that can feed the Rendering node
* registers how to get "resolved" content and default update behavior.
* The Rendering node uses this to run the right logic and respect auto vs manual updates.
*
* Register in registerBuiltinNodes (or at app init) via registerSourceRenderingLogic(nodeType, logic).
* Node types that are allowed sources for the Renderer should register here (see nodeRegistry NodeTypeDescriptor).
*/
import type { ConfigTypeId } from './configTypes'
export type SourceRenderingLogicContext = {
nodes: { id: string; type?: string; data?: unknown }[]
edges: { id: string; source: string; target: string }[]
sourceNodeId: string
renderNodeId: string
viewportWidth?: number
viewportHeight?: number
}
/**
* Result of getResolvedContent: resolved string plus the config type to use for final render.
*/
export type ResolvedContentResult = {
resolved: string
outputTypeId: ConfigTypeId
}
/**
* Rendering logic provided by a source node type (e.g. config, agent).
* - defaultUpdateMode: 'auto' = re-render on upstream changes; 'manual' = only on Run
* - getResolvedContent: async resolve step; returns resolved string and which renderer (ConfigTypeId) to use
*/
export type SourceRenderingLogic = {
defaultUpdateMode: 'auto' | 'manual'
getResolvedContent: (context: SourceRenderingLogicContext) => Promise<ResolvedContentResult>
}
const registry = new Map<string, SourceRenderingLogic>()
export function registerSourceRenderingLogic(nodeType: string, logic: SourceRenderingLogic): void {
registry.set(nodeType, logic)
}
export function getSourceRenderingLogic(nodeType: string): SourceRenderingLogic | null {
return registry.get(nodeType) ?? null
}