feat: add initial data node
This commit is contained in:
54
frontend/src/lib/csvParse.ts
Normal file
54
frontend/src/lib/csvParse.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Parse a CSV string into an array of row objects (first row = headers).
|
||||
* Handles quoted fields with commas.
|
||||
*/
|
||||
export function parseCsvToRows(csvText: string): { columns: string[]; rows: Record<string, string>[] } {
|
||||
const lines = csvText.trim().split(/\r?\n/)
|
||||
if (lines.length === 0) return { columns: [], rows: [] }
|
||||
|
||||
const parseRow = (line: string): string[] => {
|
||||
const out: string[] = []
|
||||
let i = 0
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"') {
|
||||
let cell = ''
|
||||
i += 1
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"') {
|
||||
i += 1
|
||||
if (line[i] === '"') {
|
||||
cell += '"'
|
||||
i += 1
|
||||
} else break
|
||||
} else {
|
||||
cell += line[i]
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
out.push(cell)
|
||||
if (line[i] === ',') i += 1
|
||||
} else {
|
||||
const comma = line.indexOf(',', i)
|
||||
const value = comma === -1 ? line.slice(i) : line.slice(i, comma)
|
||||
out.push(value.trim())
|
||||
i = comma === -1 ? line.length : comma + 1
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const headers = parseRow(lines[0])
|
||||
const columns = headers.map((h) => h.trim() || `Column${headers.indexOf(h) + 1}`)
|
||||
const rows: Record<string, string>[] = []
|
||||
|
||||
for (let r = 1; r < lines.length; r++) {
|
||||
const cells = parseRow(lines[r])
|
||||
const row: Record<string, string> = {}
|
||||
for (let c = 0; c < columns.length; c++) {
|
||||
row[columns[c]] = cells[c] ?? ''
|
||||
}
|
||||
rows.push(row)
|
||||
}
|
||||
|
||||
return { columns, rows }
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export const DEFAULT_NODE_STYLE: Record<string, { width: number; height: number
|
||||
render: { width: 384, height: 320 },
|
||||
variable: { width: 224, height: 180 },
|
||||
function: { width: 288, height: 260 },
|
||||
data: { width: 360, height: 280 },
|
||||
}
|
||||
|
||||
/** Default data for a new node. Uses nodeRegistry when type is registered. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from 'react'
|
||||
|
||||
export type NodeType = 'config' | 'render' | 'variable' | 'function'
|
||||
export type NodeType = 'config' | 'render' | 'variable' | 'function' | 'data'
|
||||
|
||||
export type NodeHelpEntry = {
|
||||
title: string
|
||||
@@ -77,6 +77,19 @@ export const NODE_HELP: Record<NodeType, NodeHelpEntry> = {
|
||||
</>
|
||||
),
|
||||
},
|
||||
data: {
|
||||
title: 'Data node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Data nodes hold CSV data. Drop a .csv file onto the node to load it; the table is shown in the node. Connect this node to a Config node to use the data in Nunjucks templates.</p>
|
||||
</Section>
|
||||
<Section title="Using in templates">
|
||||
<p>In a Config template connected to this data node, the variable <Code>{'{{ '}<em>nodeId</em>{' }}'}</Code> is an array of row objects (one per CSV row). Each row has keys from the CSV header. Example: <Code>{'{% for row in data_001 %}{{ row.name }}, {{ row.value }}{% endfor %}'}</Code></p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export function getNodeHelp(nodeType: NodeType): NodeHelpEntry {
|
||||
|
||||
@@ -7,7 +7,8 @@ import type { ConfigNodeData } from '@/components/nodes/ConfigNode'
|
||||
import type { RenderingNodeData } from '@/components/nodes/RenderingNode'
|
||||
import type { VariableNodeData } from '@/components/nodes/VariableNode'
|
||||
import type { FunctionNodeData } from '@/components/nodes/FunctionNode'
|
||||
import type { DataNodeData } from '@/components/nodes/DataNode'
|
||||
|
||||
export type AppNodeData = ConfigNodeData | RenderingNodeData | VariableNodeData | FunctionNodeData
|
||||
export type AppNodeData = ConfigNodeData | RenderingNodeData | VariableNodeData | FunctionNodeData | DataNodeData
|
||||
export type AppNode = Node<AppNodeData>
|
||||
export type AppEdge = Edge
|
||||
|
||||
@@ -43,6 +43,7 @@ export function nunjucksCompletionSource(
|
||||
variableIds: string[],
|
||||
configTitles?: string[],
|
||||
functionIds?: string[],
|
||||
dataIds?: string[],
|
||||
): (context: CompletionContext) => CompletionResult | null {
|
||||
return (context: CompletionContext) => {
|
||||
const { state, pos } = context
|
||||
@@ -81,6 +82,11 @@ export function nunjucksCompletionSource(
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const dataId of dataIds ?? []) {
|
||||
if (!word || dataId.toLowerCase().startsWith(word.toLowerCase())) {
|
||||
options.push({ label: dataId, type: 'variable', info: 'Data (array of rows)' })
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length === 0) return null
|
||||
return {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { ScrollText, Sparkles, Variable, Code2 } from 'lucide-react'
|
||||
import { ScrollText, Sparkles, Variable, Code2, Database } from 'lucide-react'
|
||||
import { registerNodeType } from './nodeRegistry'
|
||||
import { NODE_HELP } from './nodeHelp'
|
||||
import ConfigNode from '../components/nodes/ConfigNode'
|
||||
import RenderingNode from '../components/nodes/RenderingNode'
|
||||
import VariableNode from '../components/nodes/VariableNode'
|
||||
import FunctionNode from '../components/nodes/FunctionNode'
|
||||
import DataNode from '../components/nodes/DataNode'
|
||||
|
||||
const ICON_CLASS = 'mr-2 h-4 w-4'
|
||||
|
||||
@@ -24,7 +25,7 @@ export function registerBuiltinNodes(): void {
|
||||
hasInput: true,
|
||||
hasOutput: true,
|
||||
classification: 'psyche',
|
||||
allowedSourceTypes: ['config', 'variable', 'function'],
|
||||
allowedSourceTypes: ['config', 'variable', 'function', 'data'],
|
||||
allowedTargetTypes: ['config', 'render'],
|
||||
help: NODE_HELP.config,
|
||||
menuLabel: 'Config',
|
||||
@@ -73,6 +74,22 @@ export function registerBuiltinNodes(): void {
|
||||
menuIcon: <Variable className={ICON_CLASS} />,
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'data',
|
||||
component: DataNode,
|
||||
defaultStyle: { width: 360, height: 280 },
|
||||
defaultData: { rows: [], columns: [], fileName: '' },
|
||||
idPrefix: 'data_',
|
||||
hasInput: false,
|
||||
hasOutput: true,
|
||||
classification: 'physis',
|
||||
allowedTargetTypes: ['config'],
|
||||
help: NODE_HELP.data,
|
||||
menuLabel: 'Data',
|
||||
menuIcon: <Database className={ICON_CLASS} />,
|
||||
connectionLabel: 'data source',
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'function',
|
||||
component: FunctionNode,
|
||||
|
||||
Reference in New Issue
Block a user