Wworking on improving the tool
This commit is contained in:
+146
-24
@@ -1,6 +1,7 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useState, useEffect, useMemo } from 'preact/hooks';
|
||||
import LuaEditor from './LuaEditor';
|
||||
import type { InputRef, SignalDef } from './lib/types';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -25,14 +26,80 @@ const NODE_TYPES: Array<{ type: string; label: string; params: NodeParam[] }> =
|
||||
]},
|
||||
{ type: 'derivative', label: 'Derivative', params: [] },
|
||||
{ type: 'clamp', label: 'Clamp', params: [{ label: 'Min', key: 'min', type: 'number', default: '0' }, { label: 'Max', key: 'max', type: 'number', default: '100' }] },
|
||||
{ type: 'expr', label: 'Formula', params: [{ label: 'Expression (a=input)', key: 'expr', type: 'text', default: 'a * 1.0' }] },
|
||||
{ type: 'lua', label: 'Lua Script', params: [{ label: 'Script (a=input)', key: 'script', type: 'lua', default: 'return a' }] },
|
||||
{ type: 'expr', label: 'Formula', params: [{ label: 'Expression (a,b,c,d = inputs)', key: 'expr', type: 'text', default: 'a * 1.0' }] },
|
||||
{ type: 'lua', label: 'Lua Script', params: [{ label: 'Script (a,b,c,d = inputs)', key: 'script', type: 'lua', default: 'return a' }] },
|
||||
];
|
||||
|
||||
interface DataSource {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SignalInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// ── Searchable Selector ──────────────────────────────────────────────────────
|
||||
|
||||
function SearchableSelect({
|
||||
value,
|
||||
options,
|
||||
onSelect,
|
||||
placeholder = 'Select…'
|
||||
}: {
|
||||
value: string;
|
||||
options: string[];
|
||||
onSelect: (val: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(o => o.toLowerCase().includes(f));
|
||||
}, [options, filter]);
|
||||
|
||||
return (
|
||||
<div class="search-select">
|
||||
<div class="search-select-trigger" onClick={() => setOpen(!open)}>
|
||||
{value || <span class="hint">{placeholder}</span>}
|
||||
<span class="search-select-arrow">{open ? '▴' : '▾'}</span>
|
||||
</div>
|
||||
{open && (
|
||||
<div class="search-select-dropdown">
|
||||
<input
|
||||
class="prop-input"
|
||||
autoFocus
|
||||
placeholder="Search…"
|
||||
value={filter}
|
||||
onInput={(e) => setFilter((e.target as HTMLInputElement).value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div class="search-select-list">
|
||||
{filtered.length === 0 && <div class="search-select-item hint">No matches</div>}
|
||||
{filtered.map(opt => (
|
||||
<div
|
||||
key={opt}
|
||||
class={`search-select-item${opt === value ? ' search-select-item-selected' : ''}`}
|
||||
onClick={() => { onSelect(opt); setOpen(false); setFilter(''); }}
|
||||
>
|
||||
{opt}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{open && <div class="search-select-backdrop" onClick={() => setOpen(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Wizard ──────────────────────────────────────────────────────────────
|
||||
|
||||
export default function SyntheticWizard({ onClose, onCreated }: Props) {
|
||||
const [name, setName] = useState('');
|
||||
const [inputDs, setInputDs] = useState('stub');
|
||||
const [inputSig, setInputSig] = useState('sine_1hz');
|
||||
const [inputs, setInputs] = useState<InputRef[]>([{ ds: 'stub', signal: 'sine_1hz' }]);
|
||||
const [nodeType, setNodeType] = useState('gain');
|
||||
const [params, setParams] = useState<Record<string, string>>({});
|
||||
const [unit, setUnit] = useState('');
|
||||
@@ -42,6 +109,27 @@ export default function SyntheticWizard({ onClose, onCreated }: Props) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Loaded data
|
||||
const [dataSources, setDataSources] = useState<string[]>([]);
|
||||
const [dsSignals, setDsSignals] = useState<Record<string, string[]>>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/v1/datasources')
|
||||
.then(r => r.ok ? r.json() : [])
|
||||
.then((ds: DataSource[]) => setDataSources(ds.map(d => d.name)))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
async function loadSignals(ds: string) {
|
||||
if (dsSignals[ds]) return;
|
||||
try {
|
||||
const res = await fetch(`/api/v1/signals?ds=${encodeURIComponent(ds)}`);
|
||||
if (!res.ok) return;
|
||||
const sigs: SignalInfo[] = await res.json();
|
||||
setDsSignals(prev => ({ ...prev, [ds]: sigs.map(s => s.name) }));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const nodeDef = NODE_TYPES.find(n => n.type === nodeType)!;
|
||||
|
||||
function getParam(key: string, def: string): string {
|
||||
@@ -52,9 +140,29 @@ export default function SyntheticWizard({ onClose, onCreated }: Props) {
|
||||
setParams(p => ({ ...p, [key]: val }));
|
||||
}
|
||||
|
||||
function updateInput(idx: number, patch: Partial<InputRef>) {
|
||||
setInputs(prev => prev.map((inp, i) => {
|
||||
if (i !== idx) return inp;
|
||||
const next = { ...inp, ...patch };
|
||||
if (patch.ds) {
|
||||
next.signal = '';
|
||||
loadSignals(patch.ds);
|
||||
}
|
||||
return next;
|
||||
}));
|
||||
}
|
||||
|
||||
function addInput() {
|
||||
setInputs(prev => [...prev, { ds: dataSources[0] || '', signal: '' }]);
|
||||
}
|
||||
|
||||
function removeInput(idx: number) {
|
||||
setInputs(prev => prev.filter((_, i) => i !== idx));
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!name.trim()) { setError('Name is required'); return; }
|
||||
if (!inputSig.trim()) { setError('Input signal is required'); return; }
|
||||
if (inputs.some(i => !i.ds || !i.signal)) { setError('All inputs must have a data source and signal'); return; }
|
||||
|
||||
const nodeParams: Record<string, any> = {};
|
||||
for (const p of nodeDef.params) {
|
||||
@@ -62,10 +170,9 @@ export default function SyntheticWizard({ onClose, onCreated }: Props) {
|
||||
nodeParams[p.key] = p.type === 'number' ? parseFloat(raw) : raw;
|
||||
}
|
||||
|
||||
const def = {
|
||||
const def: SignalDef = {
|
||||
name: name.trim(),
|
||||
ds: inputDs,
|
||||
signal: inputSig.trim(),
|
||||
inputs,
|
||||
pipeline: [{ type: nodeType, params: nodeParams }],
|
||||
meta: {
|
||||
unit: unit || undefined,
|
||||
@@ -115,23 +222,38 @@ export default function SyntheticWizard({ onClose, onCreated }: Props) {
|
||||
onInput={(e) => setName((e.target as HTMLInputElement).value)} />
|
||||
</div>
|
||||
|
||||
<div class="wizard-section-title">Input signal</div>
|
||||
<div class="wizard-field wizard-field-row">
|
||||
<div class="wizard-field">
|
||||
<label>Data source</label>
|
||||
<input class="prop-input" value={inputDs}
|
||||
placeholder="stub"
|
||||
onInput={(e) => setInputDs((e.target as HTMLInputElement).value)} />
|
||||
<div class="wizard-section-title">Input signals</div>
|
||||
{inputs.map((inp, idx) => (
|
||||
<div key={idx} class="wizard-field wizard-field-row" style="align-items: flex-end; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<div class="wizard-field" style="flex:1;">
|
||||
<label>Data source</label>
|
||||
<SearchableSelect
|
||||
value={inp.ds}
|
||||
options={dataSources}
|
||||
onSelect={(ds) => updateInput(idx, { ds })}
|
||||
/>
|
||||
</div>
|
||||
<div class="wizard-field" style="flex:2;">
|
||||
<label>Signal</label>
|
||||
<SearchableSelect
|
||||
value={inp.signal}
|
||||
options={dsSignals[inp.ds] || []}
|
||||
onSelect={(signal) => updateInput(idx, { signal })}
|
||||
placeholder={inp.ds ? 'Search signals…' : 'Select data source first'}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="icon-btn"
|
||||
style="margin-bottom: 0.25rem;"
|
||||
title="Remove input"
|
||||
onClick={() => removeInput(idx)}
|
||||
disabled={inputs.length === 1}
|
||||
>✕</button>
|
||||
</div>
|
||||
<div class="wizard-field" style="flex:2;">
|
||||
<label>Signal name</label>
|
||||
<input class="prop-input" value={inputSig}
|
||||
placeholder="sine_1hz"
|
||||
onInput={(e) => setInputSig((e.target as HTMLInputElement).value)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button class="panel-btn" style="margin-top: 0.5rem;" onClick={addInput}>+ Add input</button>
|
||||
|
||||
<div class="wizard-section-title">Processing</div>
|
||||
<div class="wizard-section-title" style="margin-top: 1.5rem;">Processing</div>
|
||||
<div class="wizard-field">
|
||||
<label>Node type</label>
|
||||
<select class="prop-select" value={nodeType}
|
||||
|
||||
Reference in New Issue
Block a user