diff --git a/apps/ai-studio/.env.example b/apps/ai-studio/.env.example
new file mode 100644
index 000000000..751b07155
--- /dev/null
+++ b/apps/ai-studio/.env.example
@@ -0,0 +1,6 @@
+# Base URL of the reference backend the demo talks to.
+VITE_BACKEND_URL=http://127.0.0.1:3001
+# Cloudflare Turnstile site key (public). Leave empty to run the demo without
+# bot protection (local dev). When set, the Run button attaches a Turnstile
+# token that the backend verifies before executing a workflow.
+VITE_TURNSTILE_SITE_KEY=
diff --git a/apps/ai-studio/package.json b/apps/ai-studio/package.json
index 9f3e7b85c..243eec574 100644
--- a/apps/ai-studio/package.json
+++ b/apps/ai-studio/package.json
@@ -20,9 +20,15 @@
"@workflow-builder/types": "workspace:*",
"@xyflow/react": "catalog:",
"clsx": "^2.1.1",
+ "html-to-image": "1.11.11",
"immer": "^10.1.1",
+ "mermaid": "^11.15.0",
"react": "^19.1.0",
"react-dom": "catalog:",
+ "react-i18next": "^15.4.1",
+ "react-markdown": "^10.1.0",
+ "recharts": "^3.9.0",
+ "remark-gfm": "^4.0.1",
"zustand": "^5.0.1"
},
"devDependencies": {
diff --git a/apps/ai-studio/src/app/app.tsx b/apps/ai-studio/src/app/app.tsx
index 8d96eb295..7b21ee11f 100644
--- a/apps/ai-studio/src/app/app.tsx
+++ b/apps/ai-studio/src/app/app.tsx
@@ -1,28 +1,40 @@
import { WorkflowBuilder } from '@workflowbuilder/sdk';
+import './node-overrides.css';
import '@workflowbuilder/sdk/style.css';
+import logoDark from '../assets/workflow-builder-logo-white.svg';
+import logoLight from '../assets/workflow-builder-logo.svg';
import { AiStudioControls } from '../components/controls/ai-studio-controls';
+import { DisclaimerModal } from '../components/disclaimer/disclaimer-modal';
import { ExecutionHighlighting } from '../components/execution/highlighting';
import { ExecutionLogPanel } from '../components/execution/log-panel';
-import { ExecutionNodeDetail } from '../components/execution/node-detail';
import { aiStudioTemplates } from '../data/ai-studio-templates';
import { aiStudioNodeTypes } from '../data/node-types';
+import { supportTriageFlow } from '../data/support-triage-flow';
import { plugin as aiStudioFeaturesPlugin } from '../plugin';
+import { plugin as undoRedoPlugin } from '../plugins/undo-redo/plugin-exports';
+
+const flagship = supportTriageFlow.value;
export function App() {
return (
+ This is a live demo of Workflow Builder — a toolkit for building visual, AI-powered + workflow editors. +
++ The workflows here run for real: every AI step calls a live model through OpenRouter. +
++ It is not a place to test or benchmark AI models. The model is just the engine — the point + is to show what you can build with Workflow Builder. To keep the demo open to everyone, runs are + rate-limited. +
+{outputText}
- {text};
+ }
+
+ const { type, rows } = payload;
+ const keys = Object.keys(rows[0]);
+ const valueKeys = keys.filter((key) => rows.every((row) => typeof row[key] === 'number'));
+ const categoryKey = keys.find((key) => !valueKeys.includes(key)) ?? keys[0];
+
+ if (valueKeys.length === 0) {
+ return {text};
+ }
+
+ let chart: ReactElement;
+ switch (type) {
+ case 'pie':
+ case 'donut': {
+ chart = (
+ Not a valid Mermaid diagram — showing the raw text.
+{text}
+ Rendering diagram…
; + } + // mermaid output, sanitized via securityLevel 'strict' + return ; +} diff --git a/apps/ai-studio/src/components/visualize/renderers.module.css b/apps/ai-studio/src/components/visualize/renderers.module.css new file mode 100644 index 000000000..6d64314e0 --- /dev/null +++ b/apps/ai-studio/src/components/visualize/renderers.module.css @@ -0,0 +1,234 @@ +.markdown { + font-size: 0.85rem; + line-height: 1.6; + color: var(--ax-txt-primary-default, #151516); + overflow-wrap: anywhere; +} + +.markdown h1, +.markdown h2, +.markdown h3 { + margin: 0.9em 0 0.4em; + font-weight: 600; + line-height: 1.3; +} + +.markdown h1 { + font-size: 1.15rem; +} + +.markdown h2 { + font-size: 1.05rem; +} + +.markdown h3 { + font-size: 0.95rem; +} + +.markdown p { + margin: 0.5em 0; +} + +.markdown ul, +.markdown ol { + margin: 0.5em 0; + padding-left: 1.3em; +} + +.markdown li { + margin: 0.2em 0; +} + +.markdown a { + color: var(--ax-colors-acc1-500, #1096e7); + text-decoration: underline; +} + +.markdown strong { + font-weight: 600; +} + +.markdown code { + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 0.82em; + background: var(--ax-ui-bg-secondary-default, #f5f5f7); + padding: 0.1em 0.35em; + border-radius: 0.25rem; +} + +.markdown pre { + margin: 0.6em 0; + padding: 0.7em 0.85em; + background: var(--ax-ui-bg-secondary-default, #f5f5f7); + border-radius: 0.5rem; + overflow-x: auto; +} + +.markdown pre code { + background: none; + padding: 0; +} + +.markdown blockquote { + margin: 0.6em 0; + padding-left: 0.85em; + border-left: 0.1875rem solid var(--ax-ui-stroke-primary-default, #edeff3); + color: var(--ax-txt-secondary-default, #4d5059); +} + +.rich p { + margin: 0; +} + +.rich ul, +.rich ol { + margin: 0.2em 0; + padding-left: 1.1em; +} + +.rich code { + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 0.85em; + background: var(--ax-ui-bg-secondary-default, #f5f5f7); + padding: 0.05em 0.3em; + border-radius: 0.2rem; +} + +.rich a { + color: var(--ax-colors-acc1-500, #1096e7); + text-decoration: underline; +} + +.rich strong { + font-weight: 600; +} + +.text { + margin: 0; + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 0.8rem; + line-height: 1.55; + color: var(--ax-txt-primary-default, #151516); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.json { + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 0.78rem; + line-height: 1.5; + color: var(--ax-txt-primary-default, #151516); +} + +.json-row { + display: flex; + gap: 0.3rem; + align-items: baseline; +} + +.json-row--expandable { + cursor: pointer; +} + +.json-children { + padding-left: 0.9rem; + border-left: 0.0625rem solid var(--ax-ui-stroke-primary-default, #edeff3); + margin-left: 0.25rem; +} + +.json-toggle { + width: 0.7rem; + flex-shrink: 0; + color: var(--ax-txt-tertiary-default, #6f7480); +} + +.json-key { + color: var(--ax-colors-acc1-600, #0477c5); +} + +.json-string { + color: #15803d; +} + +.json-number { + color: #b45309; +} + +.json-boolean, +.json-null { + color: #7c3aed; +} + +.json-bracket { + color: var(--ax-txt-tertiary-default, #6f7480); +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.78rem; +} + +.table th, +.table td { + border: 0.0625rem solid var(--ax-ui-stroke-primary-default, #edeff3); + padding: 0.3em 0.5em; + text-align: left; + vertical-align: top; +} + +.table th { + background: var(--ax-ui-bg-secondary-default, #f5f5f7); + font-weight: 600; + position: sticky; + top: 0; +} + +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); + gap: 0.5rem; +} + +.stat-card { + padding: 0.6rem 0.7rem; + border: 0.0625rem solid var(--ax-ui-stroke-primary-default, #edeff3); + border-radius: 0.5rem; + background: var(--ax-ui-bg-secondary-default, #f5f5f7); +} + +.stat-value { + font-size: 1.05rem; + font-weight: 600; + color: var(--ax-txt-primary-default, #151516); + overflow-wrap: anywhere; +} + +.stat-label { + margin-top: 0.15rem; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--ax-txt-tertiary-default, #6f7480); +} + +.fallback-note { + margin-bottom: 0.4rem; + font-size: 0.72rem; + color: var(--ax-txt-tertiary-default, #6f7480); +} + +.chart { + width: 100%; + height: 14rem; +} + +.diagram { + display: flex; + justify-content: center; +} + +.diagram svg { + max-width: 100%; + height: auto; +} diff --git a/apps/ai-studio/src/components/visualize/renderers.tsx b/apps/ai-studio/src/components/visualize/renderers.tsx new file mode 100644 index 000000000..db03c4f05 --- /dev/null +++ b/apps/ai-studio/src/components/visualize/renderers.tsx @@ -0,0 +1,264 @@ +import { type ComponentType, lazy, useState } from 'react'; +import Markdown, { type Components } from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +import styles from './renderers.module.css'; + +import { type VisualizeRenderer, detectFormat } from '../../utils/detect-format'; + +const ChartRenderer = lazy(() => import('./chart-renderer').then((module) => ({ default: module.ChartRenderer }))); +const DiagramRenderer = lazy(() => + import('./diagram-renderer').then((module) => ({ default: module.DiagramRenderer })), +); + +export type RendererProps = { + text: string; + data?: unknown; +}; + +export const RENDERER_LABELS: Record so embedded ```mermaid / ```json blocks render as real components.
+const markdownComponents: Components = {
+ pre: ({ children }) => <>{children}>,
+ code({ className, children }) {
+ const language = /language-(\w+)/.exec(className ?? '')?.[1];
+ const value = String(children).replace(/\n$/, '');
+ if (language === 'mermaid') {
+ return ;
+ }
+ if (language === 'json') {
+ const detected = detectFormat(value);
+ const Renderer = getRenderer(detected.renderer);
+ return ;
+ }
+ if (language) {
+ return (
+
+ {children}
+
+ );
+ }
+ return {children};
+ },
+};
+
+function MarkdownRenderer({ text }: RendererProps) {
+ return (
+
+
+ {text}
+
+
+ );
+}
+
+function TextRenderer({ text }: RendererProps) {
+ return {text};
+}
+
+function JsonValue({ name, value, depth }: { name?: string; value: unknown; depth: number }) {
+ const [open, setOpen] = useState(depth < 2);
+ const isExpandable = typeof value === 'object' && value !== null;
+
+ if (!isExpandable) {
+ const scalarClass =
+ value === null ? 'json-null' : (`json-${typeof value}` as 'json-string' | 'json-number' | 'json-boolean');
+ const display = typeof value === 'string' ? `"${value}"` : String(value);
+ return (
+
+
+ {name !== undefined && {name}:}
+ {display}
+
+ );
+ }
+
+ const entries: [string, unknown][] = Array.isArray(value)
+ ? value.map((item, index) => [String(index), item])
+ : Object.entries(value as Record);
+ const bracket = Array.isArray(value) ? `[${entries.length}]` : `{${entries.length}}`;
+
+ return (
+
+ setOpen((o) => !o)}>
+ {open ? '▾' : '▸'}
+ {name !== undefined && {name}:}
+ {bracket}
+
+ {open && (
+
+ {entries.map(([key, child]) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+function JsonRenderer({ text, data }: RendererProps) {
+ const value = parseOr(text, data);
+ if (value === undefined) {
+ return {text};
+ }
+ return (
+
+
+
+ );
+}
+
+function TableRenderer({ text, data }: RendererProps) {
+ const rows = parseOr(text, data);
+ if (!Array.isArray(rows) || rows.length === 0) {
+ return {text};
+ }
+
+ const objectRows = rows.every((row) => row !== null && typeof row === 'object' && !Array.isArray(row));
+ const headers = objectRows
+ ? [...new Set(rows.flatMap((row) => Object.keys(row as Record)))]
+ : ['value'];
+
+ return (
+
+
+
+ {headers.map((header) => (
+ {header}
+ ))}
+
+
+
+ {rows.map((row, rowIndex) => (
+
+ {objectRows ? (
+ headers.map((header) => (
+
+ )[header])} />
+
+ ))
+ ) : (
+
+
+
+ )}
+
+ ))}
+
+
+ );
+}
+
+function StatCardsRenderer({ text, data }: RendererProps) {
+ const value = parseOr(text, data);
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
+ return {text};
+ }
+ const entries = Object.entries(value as Record);
+ return (
+
+ {entries.map(([key, entryValue]) => (
+
+
+
+
+ {humanize(key)}
+
+ ))}
+
+ );
+}
+
+export function getRenderer(renderer: VisualizeRenderer): ComponentType {
+ switch (renderer) {
+ case 'text': {
+ return TextRenderer;
+ }
+ case 'json': {
+ return JsonRenderer;
+ }
+ case 'table': {
+ return TableRenderer;
+ }
+ case 'stat-cards': {
+ return StatCardsRenderer;
+ }
+ case 'chart': {
+ return ChartRenderer;
+ }
+ case 'diagram': {
+ return DiagramRenderer;
+ }
+ default: {
+ return MarkdownRenderer;
+ }
+ }
+}
diff --git a/apps/ai-studio/src/components/visualize/visualize-card.module.css b/apps/ai-studio/src/components/visualize/visualize-card.module.css
new file mode 100644
index 000000000..a89c57d1c
--- /dev/null
+++ b/apps/ai-studio/src/components/visualize/visualize-card.module.css
@@ -0,0 +1,157 @@
+.integrated {
+ width: 100%;
+ margin-top: 0.4rem;
+ padding-top: 0.5rem;
+ border-top: 0.0625rem solid var(--ax-ui-stroke-primary-default, #edeff3);
+ font-family: var(--wb-font-family);
+ text-align: left;
+ color: var(--ax-txt-primary-default, #151516);
+
+ &,
+ * {
+ box-sizing: border-box;
+ }
+}
+
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ margin-bottom: 0.4rem;
+}
+
+.badge {
+ flex-shrink: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ font-size: 0.64rem;
+ padding: 0.15rem 0.4rem;
+ border-radius: 0.3rem;
+ background: var(--ax-ui-bg-secondary-default, #f5f5f7);
+ color: var(--ax-txt-tertiary-default, #6f7480);
+}
+
+.actions {
+ display: flex;
+ gap: 0.05rem;
+ margin-left: auto;
+ flex-shrink: 0;
+}
+
+.action {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.45rem;
+ height: 1.45rem;
+ border: none;
+ border-radius: 0.3rem;
+ background: transparent;
+ color: var(--ax-txt-tertiary-default, #6f7480);
+ cursor: pointer;
+}
+
+.action svg {
+ width: 0.9rem;
+ height: 0.9rem;
+}
+
+.action:hover {
+ background: var(--ax-ui-bg-secondary-default, #f5f5f7);
+ color: var(--ax-txt-primary-default, #151516);
+}
+
+.body {
+ max-height: 20rem;
+ overflow-y: auto;
+}
+
+.revealed {
+ animation: md-reveal 420ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
+}
+
+.empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ min-height: 5rem;
+ text-align: center;
+ padding: 0.5rem 0.25rem;
+}
+
+.empty-icon {
+ width: 1.6rem;
+ height: 1.6rem;
+ color: var(--ax-txt-tertiary-default, #6f7480);
+ opacity: 0.55;
+}
+
+.empty-text {
+ margin: 0;
+ font-size: 0.74rem;
+ line-height: 1.45;
+ color: var(--ax-txt-tertiary-default, #6f7480);
+}
+
+.dots {
+ display: flex;
+ gap: 0.4rem;
+ justify-content: center;
+}
+
+.dot {
+ width: 0.45rem;
+ height: 0.45rem;
+ border-radius: 50%;
+ background: var(--ax-colors-acc1-500, #1096e7);
+ animation: md-pulse 1s ease-in-out infinite;
+}
+
+.dot:nth-child(2) {
+ animation-delay: 0.15s;
+}
+
+.dot:nth-child(3) {
+ animation-delay: 0.3s;
+}
+
+@keyframes md-reveal {
+ from {
+ opacity: 0;
+ transform: translateY(0.5rem) scale(0.98);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+@keyframes md-pulse {
+ 0%,
+ 100% {
+ opacity: 0.3;
+ transform: scale(0.8);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .revealed {
+ animation: none;
+ }
+
+ .dot {
+ animation: none;
+ opacity: 0.7;
+ }
+}
diff --git a/apps/ai-studio/src/components/visualize/visualize-card.tsx b/apps/ai-studio/src/components/visualize/visualize-card.tsx
new file mode 100644
index 000000000..13944b2f0
--- /dev/null
+++ b/apps/ai-studio/src/components/visualize/visualize-card.tsx
@@ -0,0 +1,174 @@
+import { ArrowsOut, ClipboardText, Copy, DownloadSimple, Eye } from '@phosphor-icons/react';
+import { getStoreEdges, getStoreNodes } from '@workflowbuilder/sdk';
+import { Suspense, useEffect, useRef, useState } from 'react';
+
+import styles from './visualize-card.module.css';
+
+import { VISUALIZE_MODES } from '../../nodes/visualize/schema';
+import { useExecutionStore } from '../../stores/use-execution-store';
+import { adaptVisualization } from '../../utils/adapt-visualization';
+import { type VisualizeRenderer, detectFormat } from '../../utils/detect-format';
+import { copyImage, copySource, downloadPng } from '../../utils/export-visualization';
+import { extractOutputText } from '../../utils/extract-output-text';
+import { RENDERER_LABELS, getRenderer } from './renderers';
+import { VisualizeModal } from './visualize-modal';
+
+type Props = {
+ props?: {
+ nodeId: string;
+ };
+};
+
+type VisualizeMode = VisualizeRenderer | 'auto';
+const VALID_MODES = new Set(VISUALIZE_MODES);
+const ADAPTABLE = new Set(['diagram', 'chart', 'table', 'json', 'stat-cards']);
+
+function EmptyState({ running }: { running: boolean }) {
+ if (running) {
+ return (
+
+
+
+
+
+
+ Generating visualization…
+
+ );
+ }
+ return (
+
+
+ The visualization appears here after you run the workflow.
+
+ );
+}
+
+export function VisualizeCard({ props }: Props) {
+ const nodeId = props?.nodeId ?? '';
+ const [expanded, setExpanded] = useState(false);
+ const [adaptedText, setAdaptedText] = useState(null);
+ const [adapting, setAdapting] = useState(false);
+ const contentRef = useRef(null);
+
+ // Nodes/edges are static during a run, so snapshot reads are fine.
+ const node = getStoreNodes().find((entry) => entry.id === nodeId);
+ const isVisualizeNode = node?.data.type === 'ai-studio/visualize';
+ const sourceId = getStoreEdges().find((edge) => edge.target === nodeId)?.source;
+
+ const selfStatus = useExecutionStore((state) => state.nodeStates[nodeId]?.status);
+ const sourceOutput = useExecutionStore((state) => (sourceId ? state.nodeStates[sourceId]?.output : undefined));
+
+ const text = extractOutputText(sourceOutput);
+ const hasOutput = selfStatus === 'completed' && text.length > 0;
+
+ const properties = node?.data.properties as { mode?: string } | undefined;
+ const mode: VisualizeMode =
+ properties?.mode && VALID_MODES.has(properties.mode) ? (properties.mode as VisualizeMode) : 'auto';
+ const detection = detectFormat(text);
+ const activeRenderer: VisualizeRenderer = mode === 'auto' ? detection.renderer : mode;
+
+ useEffect(() => {
+ setAdaptedText(null);
+ }, [text]);
+
+ useEffect(() => {
+ if (!hasOutput || !ADAPTABLE.has(activeRenderer) || adaptedText !== null) return;
+ let cancelled = false;
+ setAdapting(true);
+ adaptVisualization(text, activeRenderer)
+ .then((output) => {
+ if (!cancelled) setAdaptedText(output);
+ })
+ .catch(() => {
+ // keep original content
+ })
+ .finally(() => {
+ if (!cancelled) setAdapting(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [hasOutput, activeRenderer, text, adaptedText]);
+
+ if (!isVisualizeNode) {
+ return null;
+ }
+
+ const renderText = adaptedText ?? text;
+ const data = adaptedText === null && mode === 'auto' ? detection.data : undefined;
+ const Renderer = hasOutput ? getRenderer(activeRenderer) : null;
+ const badge = mode === 'auto' ? `Auto › ${RENDERER_LABELS[activeRenderer]}` : RENDERER_LABELS[activeRenderer];
+ const isVector = activeRenderer === 'chart' || activeRenderer === 'diagram';
+
+ return (
+
+ {hasOutput && Renderer ? (
+ <>
+
+ {badge}
+
+
+
+
+
+
+
+
+ {adapting ? (
+
+
+
+
+
+
+ Adapting with AI…
+
+ ) : (
+ Loading…}>
+
+
+
+
+ )}
+
+ >
+ ) : (
+
+ )}
+ {expanded && (
+ setExpanded(false)}
+ />
+ )}
+
+ );
+}
diff --git a/apps/ai-studio/src/components/visualize/visualize-modal.module.css b/apps/ai-studio/src/components/visualize/visualize-modal.module.css
new file mode 100644
index 000000000..7850c1acc
--- /dev/null
+++ b/apps/ai-studio/src/components/visualize/visualize-modal.module.css
@@ -0,0 +1,92 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ background: rgba(8, 10, 14, 0.55);
+ backdrop-filter: blur(2px);
+ font-family: var(--wb-font-family);
+}
+
+.modal {
+ display: flex;
+ flex-direction: column;
+ width: min(56rem, 92vw);
+ max-height: 86vh;
+ background: var(--ax-ui-bg-primary-default, #ffffff);
+ border: 0.0625rem solid var(--ax-ui-stroke-primary-default, #edeff3);
+ border-radius: 0.9rem;
+ box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.4);
+ color: var(--ax-txt-primary-default, #151516);
+ overflow: hidden;
+
+ &,
+ * {
+ box-sizing: border-box;
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border-bottom: 0.0625rem solid var(--ax-ui-stroke-primary-default, #edeff3);
+}
+
+.title {
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.badge {
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ font-size: 0.7rem;
+ padding: 0.15rem 0.5rem;
+ border-radius: 0.3rem;
+ background: var(--ax-ui-bg-secondary-default, #f5f5f7);
+ color: var(--ax-txt-tertiary-default, #6f7480);
+}
+
+.actions {
+ display: flex;
+ gap: 0.15rem;
+ margin-left: auto;
+}
+
+.action {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border: none;
+ border-radius: 0.4rem;
+ background: transparent;
+ color: var(--ax-txt-tertiary-default, #6f7480);
+ cursor: pointer;
+}
+
+.action svg {
+ width: 1.15rem;
+ height: 1.15rem;
+}
+
+.action:hover {
+ background: var(--ax-ui-bg-secondary-default, #f5f5f7);
+ color: var(--ax-txt-primary-default, #151516);
+}
+
+.body {
+ flex: 1;
+ overflow: auto;
+ padding: 1.25rem 1.5rem;
+}
+
+.loading {
+ color: var(--ax-txt-tertiary-default, #6f7480);
+ font-size: 0.85rem;
+}
diff --git a/apps/ai-studio/src/components/visualize/visualize-modal.tsx b/apps/ai-studio/src/components/visualize/visualize-modal.tsx
new file mode 100644
index 000000000..775dc8717
--- /dev/null
+++ b/apps/ai-studio/src/components/visualize/visualize-modal.tsx
@@ -0,0 +1,80 @@
+import { ClipboardText, Copy, DownloadSimple, FileSvg, X } from '@phosphor-icons/react';
+import { Suspense, useRef } from 'react';
+import { createPortal } from 'react-dom';
+
+import styles from './visualize-modal.module.css';
+
+import type { VisualizeRenderer } from '../../utils/detect-format';
+import { copyImage, copySource, downloadPng, downloadSvg } from '../../utils/export-visualization';
+import { getRenderer } from './renderers';
+
+type Props = {
+ renderer: VisualizeRenderer;
+ text: string;
+ data?: unknown;
+ badge: string;
+ isVector: boolean;
+ onClose: () => void;
+};
+
+// Portaled to document.body so the fixed overlay escapes the transformed React Flow viewport.
+export function VisualizeModal({ renderer, text, data, badge, isVector, onClose }: Props) {
+ const contentRef = useRef(null);
+ const Renderer = getRenderer(renderer);
+
+ return createPortal(
+
+ event.stopPropagation()}>
+
+ Visualize
+ {badge}
+
+
+
+ {isVector && (
+
+ )}
+
+
+
+
+
+ Loading…}>
+
+
+
+
+ ,
+ document.body,
+ );
+}
diff --git a/apps/ai-studio/src/config.ts b/apps/ai-studio/src/config.ts
index 35dfa8743..03d6fff01 100644
--- a/apps/ai-studio/src/config.ts
+++ b/apps/ai-studio/src/config.ts
@@ -1 +1,4 @@
export const BACKEND_URL = import.meta.env['VITE_BACKEND_URL'] ?? 'http://127.0.0.1:3001';
+
+// Public site key. Undefined = bot protection disabled (local dev).
+export const TURNSTILE_SITE_KEY = import.meta.env['VITE_TURNSTILE_SITE_KEY'] as string | undefined;
diff --git a/apps/ai-studio/src/data/ai-debate-flow.ts b/apps/ai-studio/src/data/ai-debate-flow.ts
new file mode 100644
index 000000000..d469b66e3
--- /dev/null
+++ b/apps/ai-studio/src/data/ai-debate-flow.ts
@@ -0,0 +1,172 @@
+import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';
+
+const diagram: DiagramModel = {
+ name: 'AI Debate',
+ diagram: {
+ nodes: [
+ {
+ id: 'trigger-1',
+ type: 'start-node',
+ position: { x: 0, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'The Question',
+ description: 'A decision to stress-test from both sides.',
+ inputPrompt:
+ 'Should our 12-person startup build a native mobile app now, or keep doubling down on the web app first?',
+ },
+ type: 'ai-studio/trigger',
+ icon: 'Lightning',
+ },
+ selected: false,
+ measured: { width: 258, height: 63 },
+ dragging: false,
+ },
+ {
+ id: 'optimist-1',
+ type: 'node',
+ position: { x: 400, y: 120 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Optimist',
+ description: 'Argues the strongest case in favour.',
+ systemPrompt: `You are an optimistic strategist. Argue the strongest possible case FOR the
+proposal in the question. Give 3-4 crisp bullet points - upside, opportunity,
+why now. Be persuasive but honest, no hype.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'skeptic-1',
+ type: 'node',
+ position: { x: 400, y: 480 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Skeptic',
+ description: "Argues the strongest case against - devil's advocate.",
+ systemPrompt: `You are a rigorous skeptic and devil's advocate. Argue the strongest possible
+case AGAINST the proposal in the question. Give 3-4 crisp bullet points - risks,
+hidden costs, what could go wrong. Surface the objections others gloss over.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'verdict-1',
+ type: 'node',
+ position: { x: 820, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Balanced Verdict',
+ description: 'Weighs both sides and recommends.',
+ systemPrompt: `You moderate a debate. You receive an optimist's case and a skeptic's case for
+the same proposal. Weigh both and deliver a balanced recommendation as markdown,
+in exactly this shape:
+
+**Verdict:** [one decisive line - pick a direction]
+
+| For | Against |
+| --- | --- |
+| [strongest point in favour] | [strongest point against] |
+| [second point in favour] | [second point against] |
+
+**Reasoning:** [2-3 sentences: why this verdict, and the first concrete step.]
+
+Be decisive.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'visualize-1',
+ type: 'node',
+ position: { x: 1240, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Visualize',
+ description: 'Renders the verdict (auto-detects the format).',
+ },
+ type: 'ai-studio/visualize',
+ icon: 'Eye',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ ],
+ edges: [
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'optimist-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-optimist',
+ data: {},
+ },
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'skeptic-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-skeptic',
+ data: {},
+ },
+ {
+ source: 'optimist-1',
+ sourceHandle: 'source',
+ target: 'verdict-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-optimist-verdict',
+ data: {},
+ },
+ {
+ source: 'skeptic-1',
+ sourceHandle: 'source',
+ target: 'verdict-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-skeptic-verdict',
+ data: {},
+ },
+ {
+ source: 'verdict-1',
+ sourceHandle: 'source',
+ target: 'visualize-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-verdict-visualize',
+ data: {},
+ },
+ ],
+ viewport: { x: 80, y: 80, zoom: 0.55 },
+ },
+ layoutDirection: 'RIGHT',
+};
+
+export const aiDebateFlow: TemplateModel = {
+ id: 302,
+ name: 'AI Debate',
+ value: diagram,
+ icon: 'ChatCircleDots',
+};
diff --git a/apps/ai-studio/src/data/ai-studio-templates.ts b/apps/ai-studio/src/data/ai-studio-templates.ts
index 6f3010121..a603b4fe7 100644
--- a/apps/ai-studio/src/data/ai-studio-templates.ts
+++ b/apps/ai-studio/src/data/ai-studio-templates.ts
@@ -1,5 +1,15 @@
import type { TemplateModel } from '@workflowbuilder/sdk';
-import { salesInquiryFlow } from './sales-inquiry-flow';
+import { aiDebateFlow } from './ai-debate-flow';
+import { contentRepurposerFlow } from './content-repurposer-flow';
+import { meetingNotesFlow } from './meeting-notes-flow';
+import { researchFlow } from './research-flow';
+import { supportTriageFlow } from './support-triage-flow';
-export const aiStudioTemplates: TemplateModel[] = [salesInquiryFlow];
+export const aiStudioTemplates: TemplateModel[] = [
+ supportTriageFlow,
+ aiDebateFlow,
+ contentRepurposerFlow,
+ meetingNotesFlow,
+ researchFlow,
+];
diff --git a/apps/ai-studio/src/data/content-repurposer-flow.ts b/apps/ai-studio/src/data/content-repurposer-flow.ts
new file mode 100644
index 000000000..df469fc47
--- /dev/null
+++ b/apps/ai-studio/src/data/content-repurposer-flow.ts
@@ -0,0 +1,223 @@
+import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';
+
+const diagram: DiagramModel = {
+ name: 'Content Repurposer',
+ diagram: {
+ nodes: [
+ {
+ id: 'trigger-1',
+ type: 'start-node',
+ position: { x: 0, y: 320 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Source Content',
+ description: 'One piece of long-form content to repurpose.',
+ inputPrompt: `We just shipped Scheduled Reports in Lumen. You can now build any dashboard view
+and have it delivered as a PDF to your inbox or a Slack channel on a daily,
+weekly, or monthly cadence - no more manual exports before the Monday standup.
+
+It works with every chart type, respects your team's access permissions, and
+takes about 30 seconds to set up. Early users tell us it has quietly removed one
+of the most tedious parts of their week.`,
+ },
+ type: 'ai-studio/trigger',
+ icon: 'Lightning',
+ },
+ selected: false,
+ measured: { width: 258, height: 63 },
+ dragging: false,
+ },
+ {
+ id: 'twitter-1',
+ type: 'node',
+ position: { x: 420, y: 100 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'X / Twitter Thread',
+ description: 'Rewrites the content as a thread.',
+ systemPrompt: `Turn the source content into an engaging X/Twitter thread of 4-6 posts.
+- Open with a scroll-stopping hook
+- One idea per post, punchy and concrete
+- End with a clear call to action
+Number each post (1/, 2/, ...).`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'linkedin-1',
+ type: 'node',
+ position: { x: 420, y: 320 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'LinkedIn Post',
+ description: 'Rewrites the content for LinkedIn.',
+ systemPrompt: `Turn the source content into a LinkedIn post:
+- A strong first line that earns the click-to-expand
+- Short, skimmable paragraphs
+- 2-3 concrete takeaways
+- A closing question to drive comments
+Keep it under 200 words. Professional but human.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'instagram-1',
+ type: 'node',
+ position: { x: 420, y: 540 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Instagram Caption',
+ description: 'Rewrites the content as a caption.',
+ systemPrompt: `Turn the source content into an Instagram caption:
+- Punchy and friendly, conversational tone
+- A few relevant emojis (not too many)
+- A short call to action
+- 5 relevant hashtags on the last line`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'pack-1',
+ type: 'node',
+ position: { x: 840, y: 320 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Content Pack',
+ description: 'Collects every channel into one document.',
+ systemPrompt: `You receive a source brief and three repurposed drafts from previous steps: an
+X/Twitter thread, a LinkedIn post, and an Instagram caption. Identify each draft
+by its content and assemble them into a single content pack as markdown, one
+section per channel, in exactly this shape:
+
+## X / Twitter
+[the thread]
+
+## LinkedIn
+[the post]
+
+## Instagram
+[the caption]
+
+Keep each draft's wording as-is - do not rewrite it. Just organize and label.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'visualize-1',
+ type: 'node',
+ position: { x: 1260, y: 320 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Visualize',
+ description: 'Renders the content pack (auto-detects the format).',
+ },
+ type: 'ai-studio/visualize',
+ icon: 'Eye',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ ],
+ edges: [
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'twitter-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-twitter',
+ data: {},
+ },
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'linkedin-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-linkedin',
+ data: {},
+ },
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'instagram-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-instagram',
+ data: {},
+ },
+ {
+ source: 'twitter-1',
+ sourceHandle: 'source',
+ target: 'pack-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-twitter-pack',
+ data: {},
+ },
+ {
+ source: 'linkedin-1',
+ sourceHandle: 'source',
+ target: 'pack-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-linkedin-pack',
+ data: {},
+ },
+ {
+ source: 'instagram-1',
+ sourceHandle: 'source',
+ target: 'pack-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-instagram-pack',
+ data: {},
+ },
+ {
+ source: 'pack-1',
+ sourceHandle: 'source',
+ target: 'visualize-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-pack-visualize',
+ data: {},
+ },
+ ],
+ viewport: { x: 120, y: 80, zoom: 0.5 },
+ },
+ layoutDirection: 'RIGHT',
+};
+
+export const contentRepurposerFlow: TemplateModel = {
+ id: 303,
+ name: 'Content Repurposer',
+ value: diagram,
+ icon: 'Broadcast',
+};
diff --git a/apps/ai-studio/src/data/meeting-notes-flow.ts b/apps/ai-studio/src/data/meeting-notes-flow.ts
new file mode 100644
index 000000000..7189a5498
--- /dev/null
+++ b/apps/ai-studio/src/data/meeting-notes-flow.ts
@@ -0,0 +1,165 @@
+import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';
+
+const diagram: DiagramModel = {
+ name: 'Meeting Notes to Action Items',
+ diagram: {
+ nodes: [
+ {
+ id: 'trigger-1',
+ type: 'start-node',
+ position: { x: 0, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Meeting Transcript',
+ description: 'A raw transcript pasted in.',
+ inputPrompt: `[10:02] Priya: Okay, the launch date. Marketing wants the 14th, but the export bug is still open.
+[10:03] Sam: Engineering can have the export fix in by the 11th if QA starts Monday.
+[10:04] Priya: Works. Let's lock the 14th then. Sam, you own the fix.
+[10:05] Dana: I'll prep the announcement email and the changelog - ready for review by the 12th.
+[10:06] Priya: Great. And we still need pricing sign-off from finance before we announce.
+[10:07] Sam: I'll ping finance today.`,
+ },
+ type: 'ai-studio/trigger',
+ icon: 'Lightning',
+ },
+ selected: false,
+ measured: { width: 258, height: 63 },
+ dragging: false,
+ },
+ {
+ id: 'summary-1',
+ type: 'node',
+ position: { x: 360, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Summarize',
+ description: 'Condenses the discussion and decisions.',
+ systemPrompt: `Summarize the meeting transcript in 3-4 sentences: what was discussed and what
+was decided. Neutral, factual tone.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'actions-1',
+ type: 'node',
+ position: { x: 720, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Extract Action Items',
+ description: 'Pulls out owners, tasks and due dates.',
+ systemPrompt: `From the meeting transcript, extract every action item as a list. For each item
+give: owner, task, and due date if one was mentioned. If the owner is unclear,
+mark it "unassigned". Do not invent items that were not discussed.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'recap-1',
+ type: 'node',
+ position: { x: 1080, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Format Recap',
+ description: 'Produces a clean recap with an action-items table.',
+ systemPrompt: `Produce a clean meeting recap as markdown, in exactly this shape:
+
+## Recap
+[one short paragraph summarizing the meeting and its decisions]
+
+## Action Items
+
+| Owner | Task | Due |
+| --- | --- | --- |
+| [owner] | [task] | [due date or "-"] |
+
+One row per action item. Keep it tight and skimmable. Sign off with a final
+line "_Meeting Bot_".`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'visualize-1',
+ type: 'node',
+ position: { x: 1440, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Visualize',
+ description: 'Renders the recap (auto-detects the format).',
+ },
+ type: 'ai-studio/visualize',
+ icon: 'Eye',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ ],
+ edges: [
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'summary-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-summary',
+ data: {},
+ },
+ {
+ source: 'summary-1',
+ sourceHandle: 'source',
+ target: 'actions-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-summary-actions',
+ data: {},
+ },
+ {
+ source: 'actions-1',
+ sourceHandle: 'source',
+ target: 'recap-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-actions-recap',
+ data: {},
+ },
+ {
+ source: 'recap-1',
+ sourceHandle: 'source',
+ target: 'visualize-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-recap-visualize',
+ data: {},
+ },
+ ],
+ viewport: { x: 80, y: 90, zoom: 0.55 },
+ },
+ layoutDirection: 'RIGHT',
+};
+
+export const meetingNotesFlow: TemplateModel = {
+ id: 304,
+ name: 'Meeting Notes to Action Items',
+ value: diagram,
+ icon: 'CalendarCheck',
+};
diff --git a/apps/ai-studio/src/data/node-types.ts b/apps/ai-studio/src/data/node-types.ts
index 8390126bc..48ad1faef 100644
--- a/apps/ai-studio/src/data/node-types.ts
+++ b/apps/ai-studio/src/data/node-types.ts
@@ -3,11 +3,12 @@ import type { PaletteItemOrGroup } from '@workflowbuilder/sdk';
import { aiAgentPaletteItem } from '../nodes/ai-agent';
import { decisionPaletteItem } from '../nodes/decision';
import { triggerPaletteItem } from '../nodes/trigger';
+import { visualizePaletteItem } from '../nodes/visualize';
export const aiStudioNodeTypes: PaletteItemOrGroup[] = [
{
label: 'AI Studio',
isOpen: true,
- groupItems: [triggerPaletteItem, aiAgentPaletteItem, decisionPaletteItem],
+ groupItems: [triggerPaletteItem, aiAgentPaletteItem, decisionPaletteItem, visualizePaletteItem],
},
];
diff --git a/apps/ai-studio/src/data/research-flow.ts b/apps/ai-studio/src/data/research-flow.ts
new file mode 100644
index 000000000..444006d97
--- /dev/null
+++ b/apps/ai-studio/src/data/research-flow.ts
@@ -0,0 +1,105 @@
+import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';
+
+const diagram: DiagramModel = {
+ name: 'Market Research Brief',
+ diagram: {
+ nodes: [
+ {
+ id: 'trigger-1',
+ type: 'start-node',
+ position: { x: 0, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Research Topic',
+ description: 'The question to research on the web.',
+ inputPrompt: `Give me a briefing on the current market for AI-powered customer support tools: the main products, any notable recent launches or updates, and what users commonly praise or complain about.`,
+ },
+ type: 'ai-studio/trigger',
+ icon: 'Lightning',
+ },
+ selected: false,
+ measured: { width: 258, height: 63 },
+ dragging: false,
+ },
+ {
+ id: 'research-1',
+ type: 'node',
+ position: { x: 380, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Research Agent',
+ description: 'Searches the web and writes a sourced brief.',
+ systemPrompt: `You are a research analyst. Use the web_search tool to gather current, factual
+information about the topic in the request - search more than once if it helps.
+Then write a concise brief in Markdown, in exactly this shape:
+
+## Summary
+2-3 sentences answering the request.
+
+## Key findings
+- 3-5 bullets, each a concrete fact you found.
+
+## Sources
+- A short list of the page titles and URLs you actually used.
+
+Only state things you found via search. If a claim isn't supported by a result, leave it out.`,
+ webSearch: true,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'visualize-1',
+ type: 'node',
+ position: { x: 760, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Visualize',
+ description: 'Renders the research brief (auto-detects the format).',
+ },
+ type: 'ai-studio/visualize',
+ icon: 'Eye',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ ],
+ edges: [
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'research-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-research',
+ data: {},
+ },
+ {
+ source: 'research-1',
+ sourceHandle: 'source',
+ target: 'visualize-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-research-visualize',
+ data: {},
+ },
+ ],
+ viewport: { x: 180, y: 150, zoom: 0.7 },
+ },
+ layoutDirection: 'RIGHT',
+};
+
+export const researchFlow: TemplateModel = {
+ id: 305,
+ name: 'Market Research Brief',
+ value: diagram,
+ icon: 'MagnifyingGlass',
+};
diff --git a/apps/ai-studio/src/data/sales-inquiry-flow.ts b/apps/ai-studio/src/data/sales-inquiry-flow.ts
deleted file mode 100644
index b23995024..000000000
--- a/apps/ai-studio/src/data/sales-inquiry-flow.ts
+++ /dev/null
@@ -1,339 +0,0 @@
-import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';
-
-const PRODUCT_KNOWLEDGE = `You work for Synergy Codes, the company behind Workflow Builder.
-
-Key product facts:
-- Workflow Builder is a frontend-only SDK for building visual workflow editors, not a SaaS platform
-- It's built on React Flow, extends it with node library, schema-driven properties panel, design system, plugin architecture
-- White-label: can be themed and branded to match the customer's product
-- Community Edition: Apache 2.0 (free, commercial use allowed)
-- Enterprise Edition: one-time license EUR 6,990 (no subscription, no revenue sharing)
-- Source code ownership: customer gets full source code, can modify and extend
-- Use cases: embed workflow editors into B2B SaaS, build AI agent platforms, visual rule engines, automation tools
-- NOT an iPaaS like n8n/Make/Zapier — those are hosted platforms, WB is an embeddable SDK
-- Execution-agnostic: outputs JSON, customer builds their own execution backend
-- Handles up to ~500 nodes, supports undo/redo, keyboard shortcuts, WCAG accessibility
-- No telemetry, no external data — runs entirely in customer's infrastructure
-- Tech stack: React, Zustand, React Flow, JSONForms, Overflow UI`;
-
-const diagram: DiagramModel = {
- name: 'Sales Inquiry Pipeline',
- diagram: {
- nodes: [
- {
- id: 'trigger-1',
- type: 'start-node',
- position: { x: 0, y: 300 },
- data: {
- segments: [],
- properties: {
- label: 'Incoming Email',
- description: 'Customer inquiry arrives via email.',
- inputPrompt: `Hi there,
-
-I'm a product manager at DataFlow Inc. We're building an internal automation platform and need a visual workflow editor for our users to design data pipelines.
-
-We've looked at building something custom with React Flow but realized it would take months to get to production quality. A colleague mentioned Workflow Builder.
-
-A few questions:
-1. Can we embed it into our existing React app?
-2. How does pricing work — is it per-seat or per-deployment?
-3. Do you support custom node types? Our nodes would need to represent database connections, API calls, and ML model steps.
-4. How does it compare to just using React Flow directly?
-
-We'd need this for about 200 internal users. Timeline is Q3 this year.
-
-Best,
-Sarah Chen
-Product Manager, DataFlow Inc.`,
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/trigger',
- icon: 'Lightning',
- },
- selected: false,
- measured: { width: 258, height: 63 },
- dragging: false,
- },
- {
- id: 'classify-1',
- type: 'node',
- position: { x: 350, y: 300 },
- data: {
- segments: [],
- properties: {
- label: 'Classify & Extract',
- description: 'Classifies the inquiry type and extracts key details.',
- systemPrompt: `${PRODUCT_KNOWLEDGE}
-
-Analyze the incoming customer email. Return a structured classification:
-
-**Type:** [pricing / technical / feature-request / partnership / general]
-**Urgency:** [high / medium / low]
-**Company:** [extract company name if mentioned]
-**Key Questions:** [bullet list of specific questions asked]
-**Product Interest:** [which aspects of Workflow Builder they're asking about]
-
-Be concise. Use the exact format above.`,
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/ai-agent',
- icon: 'AiAgent',
- },
- selected: false,
- measured: { width: 258, height: 123 },
- dragging: false,
- },
- {
- id: 'decision-1',
- type: 'decision-node',
- position: { x: 700, y: 300 },
- data: {
- segments: [],
- properties: {
- label: 'Route by Type',
- description: 'Routes the inquiry to the right specialist.',
- decisionBranches: [
- {
- id: 'branch-pricing',
- sourceHandle: 'source:inner:pricing',
- label: 'Pricing',
- conditions: [
- {
- x: '{{nodes.classify-1.response}}',
- y: 'pricing',
- comparisonOperator: 'isContaining',
- logicalOperator: 'AND',
- },
- ],
- },
- {
- id: 'branch-technical',
- sourceHandle: 'source:inner:technical',
- label: 'Technical',
- conditions: [
- {
- x: '{{nodes.classify-1.response}}',
- y: 'technical',
- comparisonOperator: 'isContaining',
- logicalOperator: 'AND',
- },
- ],
- },
- {
- id: 'branch-general',
- sourceHandle: 'source:inner:general',
- label: 'General',
- conditions: [],
- },
- ],
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/decision',
- icon: 'ArrowsSplit',
- },
- selected: false,
- measured: { width: 258, height: 236 },
- dragging: false,
- },
- {
- id: 'pricing-1',
- type: 'node',
- position: { x: 1100, y: 50 },
- data: {
- segments: [],
- properties: {
- label: 'Pricing Specialist',
- description: 'Drafts a pricing-focused reply.',
- systemPrompt: `${PRODUCT_KNOWLEDGE}
-
-You are a pricing specialist at Synergy Codes. Write a pricing-focused reply to the customer:
-- Lead with clear pricing info: Enterprise EUR 6,990 one-time, Community free (Apache 2.0)
-- Explain what's included in each tier
-- Address any specific pricing question they asked
-- Offer a call to walk through licensing details
-- Keep under 180 words. Sign as "Synergy Codes Sales".`,
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/ai-agent',
- icon: 'AiAgent',
- },
- selected: false,
- measured: { width: 258, height: 123 },
- dragging: false,
- },
- {
- id: 'technical-1',
- type: 'node',
- position: { x: 1100, y: 300 },
- data: {
- segments: [],
- properties: {
- label: 'Technical Specialist',
- description: 'Drafts a technical reply.',
- systemPrompt: `${PRODUCT_KNOWLEDGE}
-
-You are a senior engineer at Synergy Codes. Write a technical reply focused on integration and extension:
-- Answer technical questions concretely (embedding, custom nodes, React Flow comparison)
-- Reference how the plugin architecture and schema-driven properties panel work
-- Offer a demo call to show custom node patterns
-- Keep under 180 words. Sign as "Synergy Codes Engineering".`,
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/ai-agent',
- icon: 'AiAgent',
- },
- selected: false,
- measured: { width: 258, height: 123 },
- dragging: false,
- },
- {
- id: 'general-1',
- type: 'node',
- position: { x: 1100, y: 550 },
- data: {
- segments: [],
- properties: {
- label: 'General Response',
- description: 'Drafts a general reply when no specialist branch matches.',
- systemPrompt: `${PRODUCT_KNOWLEDGE}
-
-You are a sales engineer at Synergy Codes. Write a friendly general reply:
-- Acknowledge the inquiry
-- Summarize what Workflow Builder is in 2-3 sentences
-- Ask a clarifying question to direct the conversation
-- Offer a discovery call
-- Keep under 150 words. Sign as "Synergy Codes Team".`,
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/ai-agent',
- icon: 'AiAgent',
- },
- selected: false,
- measured: { width: 258, height: 123 },
- dragging: false,
- },
- {
- id: 'review-1',
- type: 'node',
- position: { x: 1500, y: 300 },
- data: {
- segments: [],
- properties: {
- label: 'Final QA Check',
- description: 'Reviews the draft for accuracy and tone before sending.',
- systemPrompt: `${PRODUCT_KNOWLEDGE}
-
-Review the draft email reply from the previous step. Check for:
-1. **Factual accuracy** — does it match the product knowledge? Any wrong claims?
-2. **Tone** — professional but approachable? Not too salesy?
-3. **Completeness** — did it address all the customer's questions?
-4. **Call to action** — is there a clear next step?
-
-If everything is good, output: "✅ APPROVED" followed by the final email text.
-If there are issues, output: "⚠️ NEEDS REVISION" followed by specific corrections.`,
- errors: [],
- errorPolicy: 'fail',
- },
- type: 'ai-studio/ai-agent',
- icon: 'AiAgent',
- },
- selected: false,
- measured: { width: 258, height: 123 },
- dragging: false,
- },
- ],
- edges: [
- {
- source: 'trigger-1',
- sourceHandle: 'source',
- target: 'classify-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-trigger-classify',
- data: {},
- },
- {
- source: 'classify-1',
- sourceHandle: 'source',
- target: 'decision-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-classify-decision',
- data: {},
- },
- {
- source: 'decision-1',
- sourceHandle: 'source:inner:pricing',
- zIndex: 1001,
- target: 'pricing-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-decision-pricing',
- data: {},
- },
- {
- source: 'decision-1',
- sourceHandle: 'source:inner:technical',
- zIndex: 1001,
- target: 'technical-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-decision-technical',
- data: {},
- },
- {
- source: 'decision-1',
- sourceHandle: 'source:inner:general',
- zIndex: 1001,
- target: 'general-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-decision-general',
- data: {},
- },
- {
- source: 'pricing-1',
- sourceHandle: 'source',
- target: 'review-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-pricing-review',
- data: {},
- },
- {
- source: 'technical-1',
- sourceHandle: 'source',
- target: 'review-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-technical-review',
- data: {},
- },
- {
- source: 'general-1',
- sourceHandle: 'source',
- target: 'review-1',
- targetHandle: 'target',
- type: 'labelEdge',
- id: 'edge-general-review',
- data: {},
- },
- ],
- viewport: { x: 100, y: 100, zoom: 0.6 },
- },
- layoutDirection: 'RIGHT',
-};
-
-export const salesInquiryFlow: TemplateModel = {
- id: 202,
- name: 'Sales Inquiry Pipeline',
- value: diagram,
- icon: 'Envelope',
-};
diff --git a/apps/ai-studio/src/data/support-triage-flow.ts b/apps/ai-studio/src/data/support-triage-flow.ts
new file mode 100644
index 000000000..7fa6b06a6
--- /dev/null
+++ b/apps/ai-studio/src/data/support-triage-flow.ts
@@ -0,0 +1,336 @@
+import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';
+
+const SUPPORT_CONTEXT = `You are part of the customer support team for Lumen, a SaaS analytics product.
+
+Plans: Free, Pro ($49 / month), and Team (custom pricing).
+Support style: empathetic, concise, solution-first. Always acknowledge how the
+customer feels, give concrete next steps, and never promise a timeline you cannot keep.`;
+
+const diagram: DiagramModel = {
+ name: 'Customer Support Triage',
+ diagram: {
+ nodes: [
+ {
+ id: 'trigger-1',
+ type: 'start-node',
+ position: { x: 0, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'New Support Ticket',
+ description: 'A support ticket arrives in the shared inbox.',
+ inputPrompt: `Subject: Charged twice AND export is broken
+
+Hi, I've been on the Pro plan for 8 months and I just noticed TWO $49 charges on my card this month instead of one. On top of that, the CSV export on the Reports page has been stuck on a spinner for two days.
+
+I have a board meeting on Thursday and I genuinely need that export working. This is really frustrating - can someone please sort out the refund and tell me how to get my data out?
+
+Thanks,
+Marcus
+Head of Ops, Brightwave`,
+ },
+ type: 'ai-studio/trigger',
+ icon: 'Lightning',
+ },
+ selected: false,
+ measured: { width: 258, height: 63 },
+ dragging: false,
+ },
+ {
+ id: 'classify-1',
+ type: 'node',
+ position: { x: 350, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Classify Ticket',
+ description: 'Detects the primary issue type, urgency and sentiment.',
+ systemPrompt: `${SUPPORT_CONTEXT}
+
+Read the incoming support ticket and classify it. A ticket may mention several
+problems - pick the SINGLE most important one as the primary type.
+
+Return exactly this format:
+
+**Type:** [one of: billing / bug / how-to / other]
+**Urgency:** [high / medium / low]
+**Sentiment:** [happy / neutral / frustrated]
+**Summary:** [one sentence describing the core request]
+**Also mentioned:** [any secondary issues, or "none"]
+
+Use the exact lowercase keyword on the Type line - it drives downstream routing.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'decision-1',
+ type: 'decision-node',
+ position: { x: 700, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Route by Type',
+ description: 'Sends the ticket to the right responder.',
+ decisionBranches: [
+ {
+ id: 'branch-billing',
+ sourceHandle: 'source:inner:billing',
+ label: 'Billing',
+ conditions: [
+ {
+ x: '{{nodes.classify-1.response}}',
+ y: 'billing',
+ comparisonOperator: 'isContaining',
+ logicalOperator: 'AND',
+ },
+ ],
+ },
+ {
+ id: 'branch-bug',
+ sourceHandle: 'source:inner:bug',
+ label: 'Bug',
+ conditions: [
+ {
+ x: '{{nodes.classify-1.response}}',
+ y: 'bug',
+ comparisonOperator: 'isContaining',
+ logicalOperator: 'AND',
+ },
+ ],
+ },
+ {
+ id: 'branch-general',
+ sourceHandle: 'source:inner:general',
+ label: 'How-to / Other',
+ conditions: [],
+ },
+ ],
+ },
+ type: 'ai-studio/decision',
+ icon: 'ArrowsSplit',
+ },
+ selected: false,
+ measured: { width: 258, height: 236 },
+ dragging: false,
+ },
+ {
+ id: 'billing-1',
+ type: 'node',
+ position: { x: 1100, y: 50 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Billing Reply',
+ description: 'Drafts a reply for billing and payment issues.',
+ systemPrompt: `${SUPPORT_CONTEXT}
+
+You handle billing issues. Draft a reply to the customer:
+- Open by acknowledging the problem and apologising for the duplicate charge
+- Explain the refund will be issued to the original card and how long it usually takes
+- If they also reported a non-billing problem, tell them you've looped in the right team and they'll hear back separately
+- End with a clear next step
+- Keep it under 160 words. Sign as "Lumen Support".`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'bug-1',
+ type: 'node',
+ position: { x: 1100, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Bug Triage Reply',
+ description: 'Drafts a reply for product bugs and breakage.',
+ systemPrompt: `${SUPPORT_CONTEXT}
+
+You triage product bugs. Draft a reply to the customer:
+- Acknowledge the broken behaviour and that it is not expected
+- Offer a workaround if a plausible one exists (e.g. a different export path)
+- Ask for the details engineering will need: browser, time it last worked, a screenshot
+- Set honest expectations - it has been escalated, not "fixed by Thursday"
+- Keep it under 160 words. Sign as "Lumen Support".`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'general-1',
+ type: 'node',
+ position: { x: 1100, y: 550 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'How-to Reply',
+ description: 'Drafts a reply for how-to questions and everything else.',
+ systemPrompt: `${SUPPORT_CONTEXT}
+
+You answer how-to and general questions. Draft a friendly reply:
+- Acknowledge the question
+- Give a concrete, step-by-step answer if you can, or ask one clarifying question if you can't
+- Point to the relevant Help Center section
+- Keep it under 140 words. Sign as "Lumen Support".`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'qa-1',
+ type: 'node',
+ position: { x: 1500, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Tone & Accuracy QA',
+ description: 'Reviews the drafted reply before it goes out.',
+ systemPrompt: `${SUPPORT_CONTEXT}
+
+Review the drafted reply from the previous step before it is sent. Check:
+1. **Tone** - empathetic and professional, not defensive or robotic?
+2. **Accuracy** - does it match the plan and policy facts above? No invented promises?
+3. **Completeness** - did it address the customer's main request?
+4. **Next step** - is there a clear call to action?
+
+If it is good, output "✅ APPROVED" followed by the final reply text.
+If not, output "⚠️ NEEDS REVISION" followed by specific, actionable fixes.`,
+ },
+ type: 'ai-studio/ai-agent',
+ icon: 'AiAgent',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ {
+ id: 'visualize-1',
+ type: 'node',
+ position: { x: 1850, y: 300 },
+ data: {
+ segments: [],
+ properties: {
+ label: 'Visualize',
+ description: 'Visualizes the approved reply (auto-detects the format).',
+ },
+ type: 'ai-studio/visualize',
+ icon: 'Eye',
+ },
+ selected: false,
+ measured: { width: 258, height: 123 },
+ dragging: false,
+ },
+ ],
+ edges: [
+ {
+ source: 'trigger-1',
+ sourceHandle: 'source',
+ target: 'classify-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-trigger-classify',
+ data: {},
+ },
+ {
+ source: 'classify-1',
+ sourceHandle: 'source',
+ target: 'decision-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-classify-decision',
+ data: {},
+ },
+ {
+ source: 'decision-1',
+ sourceHandle: 'source:inner:billing',
+ zIndex: 1001,
+ target: 'billing-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-decision-billing',
+ data: {},
+ },
+ {
+ source: 'decision-1',
+ sourceHandle: 'source:inner:bug',
+ zIndex: 1001,
+ target: 'bug-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-decision-bug',
+ data: {},
+ },
+ {
+ source: 'decision-1',
+ sourceHandle: 'source:inner:general',
+ zIndex: 1001,
+ target: 'general-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-decision-general',
+ data: {},
+ },
+ {
+ source: 'billing-1',
+ sourceHandle: 'source',
+ target: 'qa-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-billing-qa',
+ data: {},
+ },
+ {
+ source: 'bug-1',
+ sourceHandle: 'source',
+ target: 'qa-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-bug-qa',
+ data: {},
+ },
+ {
+ source: 'general-1',
+ sourceHandle: 'source',
+ target: 'qa-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-general-qa',
+ data: {},
+ },
+ {
+ source: 'qa-1',
+ sourceHandle: 'source',
+ target: 'visualize-1',
+ targetHandle: 'target',
+ type: 'labelEdge',
+ id: 'edge-qa-visualize',
+ data: {},
+ },
+ ],
+ viewport: { x: 100, y: 100, zoom: 0.6 },
+ },
+ layoutDirection: 'RIGHT',
+};
+
+export const supportTriageFlow: TemplateModel = {
+ id: 301,
+ name: 'Customer Support Triage',
+ value: diagram,
+ icon: 'Envelope',
+};
diff --git a/apps/ai-studio/src/hooks/use-backend-execution.ts b/apps/ai-studio/src/hooks/use-backend-execution.ts
index 2ac606541..8c5fa6b16 100644
--- a/apps/ai-studio/src/hooks/use-backend-execution.ts
+++ b/apps/ai-studio/src/hooks/use-backend-execution.ts
@@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react';
import { connectExecutionStream } from '../adapters/execution-stream-adapter';
import { BACKEND_URL } from '../config';
+import { getTurnstileToken } from '../security/turnstile';
import { resetExecution, setExecutionStarted, useExecutionStore } from '../stores/use-execution-store';
export function useBackendExecution() {
@@ -26,9 +27,14 @@ export function useBackendExecution() {
const { id: workflowId } = (await wfResponse.json()) as { id: string };
+ const turnstileToken = await getTurnstileToken();
+
const execResponse = await fetch(`${BACKEND_URL}/api/workflows/${workflowId}/execute`, {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(turnstileToken ? { 'cf-turnstile-token': turnstileToken } : {}),
+ },
body: JSON.stringify({ sourceVersion: 'draft', triggerPayload }),
});
diff --git a/apps/ai-studio/src/nodes/ai-agent/default-properties-data.ts b/apps/ai-studio/src/nodes/ai-agent/default-properties-data.ts
index 5fe48ee57..b4483d606 100644
--- a/apps/ai-studio/src/nodes/ai-agent/default-properties-data.ts
+++ b/apps/ai-studio/src/nodes/ai-agent/default-properties-data.ts
@@ -6,5 +6,5 @@ export const defaultPropertiesData: NodeDataProperties = {
label: 'AI Agent',
description: '',
systemPrompt: '',
- errorPolicy: 'fail',
+ webSearch: false,
};
diff --git a/apps/ai-studio/src/nodes/ai-agent/index.ts b/apps/ai-studio/src/nodes/ai-agent/index.ts
index 0dffa4dfe..d74e375c6 100644
--- a/apps/ai-studio/src/nodes/ai-agent/index.ts
+++ b/apps/ai-studio/src/nodes/ai-agent/index.ts
@@ -13,4 +13,11 @@ export const aiAgentPaletteItem: PaletteItem = {
defaultPropertiesData,
schema,
uischema,
+ // Lets `{{ nodes..response }}` references resolve to a real mention instead of a "missing mention" pill.
+ outputSchema: {
+ type: 'default',
+ properties: {
+ response: { type: 'string', label: 'Response', description: 'The text generated by the AI model' },
+ },
+ },
};
diff --git a/apps/ai-studio/src/nodes/ai-agent/schema.ts b/apps/ai-studio/src/nodes/ai-agent/schema.ts
index 37aabb088..969af44b5 100644
--- a/apps/ai-studio/src/nodes/ai-agent/schema.ts
+++ b/apps/ai-studio/src/nodes/ai-agent/schema.ts
@@ -1,14 +1,16 @@
-import { errorPolicyProperty, sharedProperties } from '@workflowbuilder/sdk';
+import { sharedProperties } from '@workflowbuilder/sdk';
import type { NodeSchema } from '@workflowbuilder/sdk';
export const schema = {
type: 'object',
properties: {
...sharedProperties,
- ...errorPolicyProperty,
systemPrompt: {
type: 'string',
},
+ webSearch: {
+ type: 'boolean',
+ },
},
} satisfies NodeSchema;
diff --git a/apps/ai-studio/src/nodes/ai-agent/uischema.ts b/apps/ai-studio/src/nodes/ai-agent/uischema.ts
index 676d50c7c..06abc4aa4 100644
--- a/apps/ai-studio/src/nodes/ai-agent/uischema.ts
+++ b/apps/ai-studio/src/nodes/ai-agent/uischema.ts
@@ -20,11 +20,12 @@ export const uischema: UISchema = {
label: 'System Prompt',
placeholder: 'Describe what the AI should do...',
minRows: 5,
+ maxRows: 14,
},
{
- type: 'Select',
- scope: scope('properties.errorPolicy'),
- label: 'Error Policy',
+ type: 'Switch',
+ scope: scope('properties.webSearch'),
+ label: 'Web search (let the agent look things up)',
},
],
};
diff --git a/apps/ai-studio/src/nodes/decision/default-properties-data.ts b/apps/ai-studio/src/nodes/decision/default-properties-data.ts
index 325a08e82..4a0ca0969 100644
--- a/apps/ai-studio/src/nodes/decision/default-properties-data.ts
+++ b/apps/ai-studio/src/nodes/decision/default-properties-data.ts
@@ -6,5 +6,4 @@ export const defaultPropertiesData: NodeDataProperties = {
label: 'Decision',
description: '',
decisionBranches: [],
- errorPolicy: 'fail',
};
diff --git a/apps/ai-studio/src/nodes/decision/schema.ts b/apps/ai-studio/src/nodes/decision/schema.ts
index 3818559c0..6186d362f 100644
--- a/apps/ai-studio/src/nodes/decision/schema.ts
+++ b/apps/ai-studio/src/nodes/decision/schema.ts
@@ -1,4 +1,4 @@
-import { errorPolicyProperty, sharedProperties } from '@workflowbuilder/sdk';
+import { sharedProperties } from '@workflowbuilder/sdk';
import type { NodeSchema } from '@workflowbuilder/sdk';
const conditions = {
@@ -31,7 +31,6 @@ export const schema = {
type: 'object',
properties: {
...sharedProperties,
- ...errorPolicyProperty,
decisionBranches,
},
} satisfies NodeSchema;
diff --git a/apps/ai-studio/src/nodes/decision/uischema.ts b/apps/ai-studio/src/nodes/decision/uischema.ts
index fa11cd924..1f9d607db 100644
--- a/apps/ai-studio/src/nodes/decision/uischema.ts
+++ b/apps/ai-studio/src/nodes/decision/uischema.ts
@@ -18,10 +18,5 @@ export const uischema: UISchema = {
type: 'DecisionBranches',
scope: scope('properties.decisionBranches'),
},
- {
- type: 'Select',
- scope: scope('properties.errorPolicy'),
- label: 'Error Policy',
- },
],
};
diff --git a/apps/ai-studio/src/nodes/trigger/default-properties-data.ts b/apps/ai-studio/src/nodes/trigger/default-properties-data.ts
index 0ae4f01ac..1a5f91f60 100644
--- a/apps/ai-studio/src/nodes/trigger/default-properties-data.ts
+++ b/apps/ai-studio/src/nodes/trigger/default-properties-data.ts
@@ -6,5 +6,4 @@ export const defaultPropertiesData: NodeDataProperties = {
label: 'AI Studio Trigger',
description: '',
inputPrompt: '',
- errorPolicy: 'fail',
};
diff --git a/apps/ai-studio/src/nodes/trigger/schema.ts b/apps/ai-studio/src/nodes/trigger/schema.ts
index 097333b79..417f43319 100644
--- a/apps/ai-studio/src/nodes/trigger/schema.ts
+++ b/apps/ai-studio/src/nodes/trigger/schema.ts
@@ -1,11 +1,10 @@
-import { errorPolicyProperty, sharedProperties } from '@workflowbuilder/sdk';
+import { sharedProperties } from '@workflowbuilder/sdk';
import type { NodeSchema } from '@workflowbuilder/sdk';
export const schema = {
type: 'object',
properties: {
...sharedProperties,
- ...errorPolicyProperty,
inputPrompt: {
type: 'string',
},
diff --git a/apps/ai-studio/src/nodes/trigger/uischema.ts b/apps/ai-studio/src/nodes/trigger/uischema.ts
index 41352f58c..09f353511 100644
--- a/apps/ai-studio/src/nodes/trigger/uischema.ts
+++ b/apps/ai-studio/src/nodes/trigger/uischema.ts
@@ -20,11 +20,7 @@ export const uischema: UISchema = {
label: 'Input',
placeholder: 'Paste the input data here (e.g. email content)...',
minRows: 5,
- },
- {
- type: 'Select',
- scope: scope('properties.errorPolicy'),
- label: 'Error Policy',
+ maxRows: 14,
},
],
};
diff --git a/apps/ai-studio/src/nodes/visualize/default-properties-data.ts b/apps/ai-studio/src/nodes/visualize/default-properties-data.ts
new file mode 100644
index 000000000..ddb6ac5a9
--- /dev/null
+++ b/apps/ai-studio/src/nodes/visualize/default-properties-data.ts
@@ -0,0 +1,9 @@
+import type { NodeDataProperties } from '@workflowbuilder/sdk';
+
+import type { VisualizeSchema } from './schema';
+
+export const defaultPropertiesData: NodeDataProperties = {
+ label: 'Visualize',
+ description: '',
+ mode: 'auto',
+};
diff --git a/apps/ai-studio/src/nodes/visualize/index.ts b/apps/ai-studio/src/nodes/visualize/index.ts
new file mode 100644
index 000000000..98ae3a2ee
--- /dev/null
+++ b/apps/ai-studio/src/nodes/visualize/index.ts
@@ -0,0 +1,16 @@
+import { NodeType, type PaletteItem } from '@workflowbuilder/sdk';
+
+import { defaultPropertiesData } from './default-properties-data';
+import { type VisualizeSchema, schema } from './schema';
+import { uischema } from './uischema';
+
+export const visualizePaletteItem: PaletteItem = {
+ label: 'Visualize',
+ description: 'Render the result visually',
+ type: 'ai-studio/visualize',
+ icon: 'Eye',
+ templateType: NodeType.Node,
+ defaultPropertiesData,
+ schema,
+ uischema,
+};
diff --git a/apps/ai-studio/src/nodes/visualize/schema.ts b/apps/ai-studio/src/nodes/visualize/schema.ts
new file mode 100644
index 000000000..04c96fcb4
--- /dev/null
+++ b/apps/ai-studio/src/nodes/visualize/schema.ts
@@ -0,0 +1,29 @@
+import { sharedProperties } from '@workflowbuilder/sdk';
+import type { NodeSchema } from '@workflowbuilder/sdk';
+
+export const VISUALIZE_MODES = ['auto', 'markdown', 'text', 'json', 'table', 'stat-cards', 'chart', 'diagram'] as const;
+type VisualizeMode = (typeof VISUALIZE_MODES)[number];
+
+const MODE_LABELS: Record = {
+ auto: 'Auto (detect format)',
+ markdown: 'Markdown',
+ text: 'Plain text',
+ json: 'JSON tree',
+ table: 'Table',
+ 'stat-cards': 'Stat cards',
+ chart: 'Chart',
+ diagram: 'Diagram',
+};
+
+export const schema = {
+ type: 'object',
+ properties: {
+ ...sharedProperties,
+ mode: {
+ type: 'string',
+ options: VISUALIZE_MODES.map((value) => ({ label: MODE_LABELS[value], value })),
+ },
+ },
+} satisfies NodeSchema;
+
+export type VisualizeSchema = typeof schema;
diff --git a/apps/ai-studio/src/nodes/visualize/uischema.ts b/apps/ai-studio/src/nodes/visualize/uischema.ts
new file mode 100644
index 000000000..b4dc491c3
--- /dev/null
+++ b/apps/ai-studio/src/nodes/visualize/uischema.ts
@@ -0,0 +1,23 @@
+import { getScope } from '@workflowbuilder/sdk';
+import type { UISchema } from '@workflowbuilder/sdk';
+
+import type { VisualizeSchema } from './schema';
+
+const scope = getScope;
+
+export const uischema: UISchema = {
+ type: 'VerticalLayout',
+ elements: [
+ {
+ type: 'Text',
+ scope: scope('properties.label'),
+ label: 'Title',
+ placeholder: 'Node Title...',
+ },
+ {
+ type: 'Select',
+ scope: scope('properties.mode'),
+ label: 'Render as',
+ },
+ ],
+};
diff --git a/apps/ai-studio/src/plugin.ts b/apps/ai-studio/src/plugin.ts
index d4fb8698e..1fc5c8c6c 100644
--- a/apps/ai-studio/src/plugin.ts
+++ b/apps/ai-studio/src/plugin.ts
@@ -1,7 +1,7 @@
import { type OptionalNodeContent, registerComponentDecorator } from '@workflowbuilder/sdk';
-import { ErrorHandle } from './components/error-handle/error-handle';
import { ExecutionNodeMarkers } from './components/execution/node-markers';
+import { VisualizeCard } from './components/visualize/visualize-card';
type OptionalNodeContentProps = React.ComponentProps;
@@ -10,7 +10,7 @@ export function plugin(): void {
content: ExecutionNodeMarkers,
});
registerComponentDecorator('OptionalNodeContent', {
- content: ErrorHandle,
+ content: VisualizeCard,
place: 'after',
});
}
diff --git a/apps/ai-studio/src/plugins/undo-redo/README.md b/apps/ai-studio/src/plugins/undo-redo/README.md
new file mode 100644
index 000000000..458a03907
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/README.md
@@ -0,0 +1,9 @@
+# Undo redo from Overflow Component Library
+
+Enable local session history for your **React Flow** graph.
+
+https://www.overflow.dev/premium?path=/docs/interaction-undo-redo-documentation--docs
+
+## About overflow.dev
+
+Designed for developers, [overflow.dev](https://www.overflow.dev/) provides a React Flow UI library to build visual workflows, automations, and tools with production-grade components.
diff --git a/apps/ai-studio/src/plugins/undo-redo/components/buttons-undo-redo/buttons-undo-redo.tsx b/apps/ai-studio/src/plugins/undo-redo/components/buttons-undo-redo/buttons-undo-redo.tsx
new file mode 100644
index 000000000..c12f452fc
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/components/buttons-undo-redo/buttons-undo-redo.tsx
@@ -0,0 +1,23 @@
+import { NavButton } from '@synergycodes/overflow-ui';
+import { Icon, useStore } from '@workflowbuilder/sdk';
+import { useTranslation } from 'react-i18next';
+
+import { redo, undo, useUndoRedoStore } from '../../stores/use-undo-redo-store';
+
+export function ButtonsUndoRedo() {
+ const { t } = useTranslation();
+ const canUndo = useUndoRedoStore((store) => store.past.length > 0);
+ const canRedo = useUndoRedoStore((store) => store.future.length > 0);
+ const isReadOnlyMode = useStore((store) => store.isReadOnlyMode);
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/ai-studio/src/plugins/undo-redo/functions/decorators.ts b/apps/ai-studio/src/plugins/undo-redo/functions/decorators.ts
new file mode 100644
index 000000000..5b8b9304d
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/functions/decorators.ts
@@ -0,0 +1,36 @@
+import {
+ processSnapshotWatching,
+ startSnapshotWatching,
+ stopSnapshotWatching,
+ takeSnapshot,
+} from '../stores/use-undo-redo-store';
+
+type TrackFutureChangeDecoratorParams = {
+ params: unknown[];
+};
+
+const dragCallbackByCodename: {
+ [codename: string]: (name: string) => void;
+} = {
+ nodeDragStart: startSnapshotWatching,
+ nodeDragChange: processSnapshotWatching,
+ nodeDragStop: stopSnapshotWatching,
+};
+
+export function trackFutureChangeDecorator({ params }: TrackFutureChangeDecoratorParams) {
+ const codename = typeof params[0] === 'string' ? params[0] : '';
+
+ if (['undo', 'redo'].includes(codename)) {
+ // Triggered by history skipping takeSnapshot()
+ return;
+ }
+
+ const specialDragCallback = dragCallbackByCodename[codename];
+ if (specialDragCallback) {
+ specialDragCallback('nodeDrag');
+
+ return;
+ }
+
+ takeSnapshot();
+}
diff --git a/apps/ai-studio/src/plugins/undo-redo/hooks/use-undo-redo-keyboard-handler.tsx b/apps/ai-studio/src/plugins/undo-redo/hooks/use-undo-redo-keyboard-handler.tsx
new file mode 100644
index 000000000..77f872f9b
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/hooks/use-undo-redo-keyboard-handler.tsx
@@ -0,0 +1,37 @@
+import { useEffect } from 'react';
+
+import { redo, undo } from '../stores/use-undo-redo-store';
+
+function isTextTarget(target: EventTarget | null): boolean {
+ return (
+ target instanceof HTMLInputElement ||
+ target instanceof HTMLTextAreaElement ||
+ (target instanceof HTMLElement && target.isContentEditable)
+ );
+}
+
+export const useUndoRedoKeyboardHandler = () => {
+ useEffect(() => {
+ function onKeyDown(event: KeyboardEvent) {
+ // `event.repeat` guards held-key OS auto-repeat; text fields keep their native undo.
+ if (!(event.ctrlKey || event.metaKey) || event.repeat || isTextTarget(event.target)) {
+ return;
+ }
+ const key = event.key.toLowerCase();
+ if (key === 'z') {
+ event.preventDefault();
+ if (event.shiftKey) {
+ redo();
+ } else {
+ undo();
+ }
+ } else if (key === 'y') {
+ event.preventDefault();
+ redo();
+ }
+ }
+
+ document.addEventListener('keydown', onKeyDown);
+ return () => document.removeEventListener('keydown', onKeyDown);
+ }, []);
+};
diff --git a/apps/ai-studio/src/plugins/undo-redo/locales/en/translation.json b/apps/ai-studio/src/plugins/undo-redo/locales/en/translation.json
new file mode 100644
index 000000000..d9800588f
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/locales/en/translation.json
@@ -0,0 +1,8 @@
+{
+ "plugins": {
+ "undoRedo": {
+ "undo": "Undo",
+ "redo": "Redo"
+ }
+ }
+}
diff --git a/apps/ai-studio/src/plugins/undo-redo/locales/pl/translation.json b/apps/ai-studio/src/plugins/undo-redo/locales/pl/translation.json
new file mode 100644
index 000000000..d02a682f7
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/locales/pl/translation.json
@@ -0,0 +1,8 @@
+{
+ "plugins": {
+ "undoRedo": {
+ "undo": "Cofnij",
+ "redo": "Ponów"
+ }
+ }
+}
diff --git a/apps/ai-studio/src/plugins/undo-redo/plugin-exports.ts b/apps/ai-studio/src/plugins/undo-redo/plugin-exports.ts
new file mode 100644
index 000000000..9e4e45ec7
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/plugin-exports.ts
@@ -0,0 +1,32 @@
+import { registerComponentDecorator, registerFunctionDecorator, registerPluginTranslation } from '@workflowbuilder/sdk';
+
+import { ButtonsUndoRedo } from './components/buttons-undo-redo/buttons-undo-redo';
+import { trackFutureChangeDecorator } from './functions/decorators';
+import * as translationEN from './locales/en/translation.json';
+import * as translationPL from './locales/pl/translation.json';
+import { UndoRedoProvider } from './providers/undo-redo-provider';
+
+export function plugin(): void {
+ registerComponentDecorator('OptionalHooks', {
+ content: UndoRedoProvider,
+ });
+
+ registerComponentDecorator('OptionalAppBarTools', {
+ content: ButtonsUndoRedo,
+ place: 'after',
+ name: 'UndoRedo',
+ });
+
+ registerFunctionDecorator('trackFutureChange', {
+ callback: trackFutureChangeDecorator,
+ });
+
+ registerPluginTranslation({
+ en: {
+ translation: translationEN,
+ },
+ pl: {
+ translation: translationPL,
+ },
+ });
+}
diff --git a/apps/ai-studio/src/plugins/undo-redo/providers/undo-redo-provider.tsx b/apps/ai-studio/src/plugins/undo-redo/providers/undo-redo-provider.tsx
new file mode 100644
index 000000000..dc21afffa
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/providers/undo-redo-provider.tsx
@@ -0,0 +1,15 @@
+import { type ReactNode, memo } from 'react';
+
+import { useUndoRedoKeyboardHandler } from '../hooks/use-undo-redo-keyboard-handler';
+
+type UndoRedoProviderProps = {
+ children: ReactNode;
+};
+
+function UndoRedoProviderComponent({ children }: UndoRedoProviderProps) {
+ useUndoRedoKeyboardHandler();
+
+ return children;
+}
+
+export const UndoRedoProvider = memo(UndoRedoProviderComponent);
diff --git a/apps/ai-studio/src/plugins/undo-redo/stores/use-undo-redo-store.ts b/apps/ai-studio/src/plugins/undo-redo/stores/use-undo-redo-store.ts
new file mode 100644
index 000000000..e365f7d91
--- /dev/null
+++ b/apps/ai-studio/src/plugins/undo-redo/stores/use-undo-redo-store.ts
@@ -0,0 +1,161 @@
+import {
+ getStoreEdges,
+ getStoreLayoutDirection,
+ getStoreNodes,
+ setStoreEdges,
+ setStoreLayoutDirection,
+ setStoreNodes,
+ trackFutureChange,
+} from '@workflowbuilder/sdk';
+import type { LayoutDirection, WorkflowBuilderEdge, WorkflowBuilderNode } from '@workflowbuilder/sdk';
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+const DEFAULT_MAX_HISTORY_SIZE = 100;
+
+type HistoryItem = {
+ nodes: WorkflowBuilderNode[];
+ edges: WorkflowBuilderEdge[];
+ layoutDirection: LayoutDirection;
+};
+
+type SnapshotWatchers = {
+ [name: string]: {
+ snapshot?: HistoryItem;
+ };
+};
+
+type UndoRedoStore = {
+ snapshotsWatchers: SnapshotWatchers;
+ past: HistoryItem[];
+ future: HistoryItem[];
+};
+
+const emptyStore: UndoRedoStore = {
+ snapshotsWatchers: {},
+ past: [],
+ future: [],
+};
+
+export const useUndoRedoStore = create()(
+ devtools(
+ () =>
+ ({
+ ...emptyStore,
+ }) satisfies UndoRedoStore,
+ { name: 'undoRedoStore' },
+ ),
+);
+
+function areSnapshotsEqual(previous: HistoryItem, current: HistoryItem): boolean {
+ if (
+ previous.nodes === current.nodes &&
+ previous.edges === current.edges &&
+ previous.layoutDirection === current.layoutDirection
+ ) {
+ return true;
+ }
+
+ return JSON.stringify(previous) === JSON.stringify(current);
+}
+
+export function takeSnapshot(snapshot?: HistoryItem) {
+ const nodes = snapshot?.nodes || getStoreNodes();
+ const edges = snapshot?.edges || getStoreEdges();
+ const layoutDirection = snapshot?.layoutDirection || getStoreLayoutDirection();
+
+ const newSnapshot: HistoryItem = { nodes, edges, layoutDirection };
+
+ const lastSnapshot = useUndoRedoStore.getState().past.at(-1);
+ if (lastSnapshot && areSnapshotsEqual(lastSnapshot, newSnapshot)) {
+ return;
+ }
+
+ useUndoRedoStore.setState((state) => ({
+ past: [...state.past.slice(state.past.length - DEFAULT_MAX_HISTORY_SIZE + 1), newSnapshot],
+ future: [],
+ }));
+}
+
+export function startSnapshotWatching(name: string) {
+ useUndoRedoStore.setState((state) => ({
+ snapshotsWatchers: {
+ ...state.snapshotsWatchers,
+ [name]: {
+ snapshot: {
+ nodes: getStoreNodes(),
+ edges: getStoreEdges(),
+ layoutDirection: getStoreLayoutDirection(),
+ },
+ },
+ },
+ }));
+}
+
+export function stopSnapshotWatching(name: string) {
+ useUndoRedoStore.setState((state) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { [name]: snapshotRemoved, ...newSnapshotsWatchers } = state.snapshotsWatchers;
+
+ return {
+ snapshotsWatchers: newSnapshotsWatchers,
+ };
+ });
+}
+
+export function processSnapshotWatching(name: string, shouldSkip = false) {
+ const item = useUndoRedoStore.getState().snapshotsWatchers[name];
+
+ if (!item?.snapshot) {
+ return;
+ }
+
+ if (!shouldSkip) {
+ takeSnapshot(item.snapshot);
+ stopSnapshotWatching(name);
+ }
+}
+
+export function undo() {
+ trackFutureChange('undo');
+
+ const state = useUndoRedoStore.getState();
+ const pastState = state.past.at(-1);
+
+ if (pastState) {
+ const nodes = getStoreNodes();
+ const edges = getStoreEdges();
+ const layoutDirection = getStoreLayoutDirection();
+
+ useUndoRedoStore.setState((state) => ({
+ past: state.past.slice(0, -1),
+ future: [...state.future, { nodes, edges, layoutDirection }],
+ }));
+
+ setStoreNodes(pastState.nodes);
+ setStoreEdges(pastState.edges);
+ setStoreLayoutDirection(pastState.layoutDirection);
+ }
+}
+
+export function redo() {
+ trackFutureChange('redo');
+
+ const state = useUndoRedoStore.getState();
+ const futureState = state.future.at(-1);
+
+ if (futureState) {
+ const nodes = getStoreNodes();
+ const edges = getStoreEdges();
+ const layoutDirection = getStoreLayoutDirection();
+
+ useUndoRedoStore.setState((state) => ({
+ past: [...state.past, { nodes, edges, layoutDirection }],
+ future: state.future.slice(0, -1),
+ }));
+
+ setStoreNodes(futureState.nodes);
+ setStoreEdges(futureState.edges);
+ setStoreLayoutDirection(futureState.layoutDirection);
+ }
+}
diff --git a/apps/ai-studio/src/security/turnstile.ts b/apps/ai-studio/src/security/turnstile.ts
new file mode 100644
index 000000000..9b37fec71
--- /dev/null
+++ b/apps/ai-studio/src/security/turnstile.ts
@@ -0,0 +1,94 @@
+import { TURNSTILE_SITE_KEY } from '../config';
+
+interface TurnstileApi {
+ render: (element: HTMLElement, options: Record) => string;
+ execute: (widgetId: string, options?: Record) => void;
+ reset: (widgetId: string) => void;
+}
+
+const SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
+
+let scriptPromise: Promise | null = null;
+let widgetId: string | null = null;
+let container: HTMLElement | null = null;
+let resolveToken: ((token: string) => void) | null = null;
+let rejectToken: ((error: Error) => void) | null = null;
+
+function turnstileApi(): TurnstileApi | undefined {
+ return (globalThis as typeof globalThis & { turnstile?: TurnstileApi }).turnstile;
+}
+
+function loadScript(): Promise {
+ if (scriptPromise) {
+ return scriptPromise;
+ }
+ scriptPromise = new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = SCRIPT_URL;
+ script.async = true;
+ script.defer = true;
+ script.addEventListener('load', () => resolve());
+ script.addEventListener('error', () => reject(new Error('Failed to load Turnstile')));
+ document.head.append(script);
+ });
+ return scriptPromise;
+}
+
+async function waitForApi(): Promise {
+ await loadScript();
+ for (let attempt = 0; attempt < 50 && !turnstileApi(); attempt++) {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ }
+ const api = turnstileApi();
+ if (!api) {
+ throw new Error('Turnstile is unavailable');
+ }
+ return api;
+}
+
+// Returns undefined when no site key is configured (local dev). One invisible widget re-executed per run, since tokens are single-use.
+export function getTurnstileToken(): Promise {
+ // Serialized: the single widget and resolve/reject slots can't handle overlapping requests.
+ const next = queue
+ .catch(() => {
+ // a failed predecessor must not poison the queue
+ })
+ .then(requestToken);
+ queue = next;
+ return next;
+}
+
+let queue: Promise = Promise.resolve();
+
+async function requestToken(): Promise {
+ const siteKey = TURNSTILE_SITE_KEY;
+ if (!siteKey) {
+ return undefined;
+ }
+
+ const turnstile = await waitForApi();
+
+ return new Promise((resolve, reject) => {
+ resolveToken = resolve;
+ rejectToken = reject;
+
+ let id = widgetId;
+ if (id === null) {
+ container = document.createElement('div');
+ container.style.display = 'none';
+ document.body.append(container);
+ id = turnstile.render(container, {
+ sitekey: siteKey,
+ size: 'invisible',
+ callback: (token: string) => resolveToken?.(token),
+ 'error-callback': () => rejectToken?.(new Error('Turnstile error')),
+ 'timeout-callback': () => rejectToken?.(new Error('Turnstile timeout')),
+ });
+ widgetId = id;
+ } else {
+ turnstile.reset(id);
+ }
+
+ turnstile.execute(id, { sitekey: siteKey });
+ });
+}
diff --git a/apps/ai-studio/src/stores/use-execution-store.ts b/apps/ai-studio/src/stores/use-execution-store.ts
index ddf74f000..563c58373 100644
--- a/apps/ai-studio/src/stores/use-execution-store.ts
+++ b/apps/ai-studio/src/stores/use-execution-store.ts
@@ -21,7 +21,7 @@ type ExecutionStore = {
streamUrl: string | undefined;
nodeStates: Record;
events: ExecutionEvent[];
- selectedNodeId: string | null;
+ logCollapsed: boolean;
};
const emptyStore: ExecutionStore = {
@@ -30,7 +30,7 @@ const emptyStore: ExecutionStore = {
streamUrl: undefined,
nodeStates: {},
events: [],
- selectedNodeId: null,
+ logCollapsed: false,
};
export const useExecutionStore = create()(
@@ -102,8 +102,12 @@ function applyEventToNodeStates(event: ExecutionEvent, states: Record ({ logCollapsed: !state.logCollapsed }));
}
function eventToExecutionStatus(event: ExecutionEvent): ExecutionStatus | undefined {
diff --git a/apps/ai-studio/src/utils/adapt-visualization.ts b/apps/ai-studio/src/utils/adapt-visualization.ts
new file mode 100644
index 000000000..1f7a94026
--- /dev/null
+++ b/apps/ai-studio/src/utils/adapt-visualization.ts
@@ -0,0 +1,20 @@
+import { BACKEND_URL } from '../config';
+import { getTurnstileToken } from '../security/turnstile';
+
+export async function adaptVisualization(content: string, format: string): Promise {
+ const token = await getTurnstileToken();
+ const response = await fetch(`${BACKEND_URL}/api/visualize/adapt`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { 'cf-turnstile-token': token } : {}),
+ },
+ body: JSON.stringify({ content, format }),
+ });
+ if (!response.ok) {
+ const error = (await response.json().catch(() => ({}))) as { message?: string };
+ throw new Error(error.message ?? 'Adapt failed');
+ }
+ const data = (await response.json()) as { output: string };
+ return data.output;
+}
diff --git a/apps/ai-studio/src/utils/detect-format.test.ts b/apps/ai-studio/src/utils/detect-format.test.ts
new file mode 100644
index 000000000..7e89896fa
--- /dev/null
+++ b/apps/ai-studio/src/utils/detect-format.test.ts
@@ -0,0 +1,72 @@
+import { describe, expect, it } from 'vitest';
+
+import { detectFormat } from './detect-format';
+
+describe('detectFormat', () => {
+ it('returns text for empty input', () => {
+ expect(detectFormat('').renderer).toBe('text');
+ expect(detectFormat(' ').renderer).toBe('text');
+ });
+
+ it('falls back to markdown for prose and markdown', () => {
+ expect(detectFormat('Hi Marcus, sorry about the double charge.').renderer).toBe('markdown');
+ expect(detectFormat('# Title\n\nSome **bold** text.').renderer).toBe('markdown');
+ });
+
+ it('detects a mermaid diagram from a clear declaration', () => {
+ expect(detectFormat('flowchart TD\n A --> B').renderer).toBe('diagram');
+ expect(detectFormat('sequenceDiagram\n A->>B: hi').renderer).toBe('diagram');
+ });
+
+ it('detects a fenced mermaid block and strips the fence', () => {
+ const result = detectFormat('```mermaid\nflowchart TD\n A --> B\n```');
+ expect(result.renderer).toBe('diagram');
+ expect(result.data).toBe('flowchart TD\n A --> B');
+ });
+
+ it('does not mis-detect prose starting with graph/pie/timeline as a diagram', () => {
+ expect(detectFormat('graph shows the revenue went up this quarter.').renderer).toBe('markdown');
+ expect(detectFormat('pie of the market share by region, roughly even.').renderer).toBe('markdown');
+ expect(detectFormat('timeline of the rollout is aggressive but doable.').renderer).toBe('markdown');
+ });
+
+ it('detects a flat scalar object as stat-cards', () => {
+ const result = detectFormat('{"users": 1200, "churn": 0.03, "plan": "Pro"}');
+ expect(result.renderer).toBe('stat-cards');
+ expect(result.data).toEqual({ users: 1200, churn: 0.03, plan: 'Pro' });
+ });
+
+ it('detects a nested object as json tree', () => {
+ expect(detectFormat('{"a": {"b": 1}}').renderer).toBe('json');
+ });
+
+ it('detects an array of objects as a table', () => {
+ const result = detectFormat('[{"id": 1, "city": "NY"}, {"id": 2, "city": "LA"}]');
+ expect(result.renderer).toBe('table');
+ });
+
+ it('detects a {label,value} array as a chart', () => {
+ expect(detectFormat('[{"name": "A", "value": 3}, {"name": "B", "value": 5}]').renderer).toBe('chart');
+ expect(detectFormat('[{"x": "Jan", "y": 10}, {"x": "Feb", "y": 20}]').renderer).toBe('chart');
+ });
+
+ it('detects an explicit chart-spec envelope', () => {
+ const result = detectFormat('{"type": "bar", "data": [{"k": 1}]}');
+ expect(result.renderer).toBe('chart');
+ });
+
+ it('detects CSV as a table', () => {
+ const result = detectFormat('name,age\nAlice,30\nBob,25');
+ expect(result.renderer).toBe('table');
+ expect((result.data as Record[])[0]).toEqual({ name: 'Alice', age: '30' });
+ });
+
+ it('does not mistake comma-prose for CSV', () => {
+ expect(detectFormat('Hi there,\nthanks a lot, really, for everything you did.').renderer).toBe('markdown');
+ });
+
+ it('does not treat a bare scalar as JSON', () => {
+ expect(detectFormat('42').renderer).toBe('markdown');
+ expect(detectFormat('"hello"').renderer).toBe('markdown');
+ });
+});
diff --git a/apps/ai-studio/src/utils/detect-format.ts b/apps/ai-studio/src/utils/detect-format.ts
new file mode 100644
index 000000000..330b7023f
--- /dev/null
+++ b/apps/ai-studio/src/utils/detect-format.ts
@@ -0,0 +1,127 @@
+export type VisualizeRenderer = 'markdown' | 'text' | 'json' | 'table' | 'stat-cards' | 'chart' | 'diagram';
+
+type DetectResult = {
+ renderer: VisualizeRenderer;
+ data?: unknown;
+};
+
+// Strict on purpose: flowchart/graph require a direction so prose isn't mis-detected as a diagram.
+const MERMAID_FENCE = /^```mermaid\s*\n?([\s\S]*?)```$/;
+const MERMAID_FIRST_LINE =
+ /^(?:sequenceDiagram|classDiagram|stateDiagram(?:-v2)?|erDiagram|gantt|gitGraph|mindmap|quadrantChart|requirementDiagram)\b|^(?:flowchart|graph)\s+(?:TB|TD|BT|RL|LR)\b/;
+
+const LABEL_KEYS = new Set(['label', 'name', 'category', 'x', 'key', 'date', 'month', 'day']);
+const VALUE_KEYS = new Set(['value', 'count', 'y', 'amount', 'total', 'qty', 'quantity', 'score']);
+const CHART_TYPES = new Set(['bar', 'line', 'pie', 'area', 'donut']);
+
+function isScalar(value: unknown): boolean {
+ return value === null || ['string', 'number', 'boolean'].includes(typeof value);
+}
+
+function isPlainObject(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function looksLikeChartArray(array: unknown[]): boolean {
+ if (array.length === 0) {
+ return false;
+ }
+ return array.every((item) => {
+ if (!isPlainObject(item)) {
+ return false;
+ }
+ const keys = Object.keys(item).map((k) => k.toLowerCase());
+ const hasLabel = keys.some((k) => LABEL_KEYS.has(k));
+ const hasNumericValue = Object.entries(item).some(
+ ([k, v]) => VALUE_KEYS.has(k.toLowerCase()) && typeof v === 'number',
+ );
+ return hasLabel && hasNumericValue;
+ });
+}
+
+function detectJson(parsed: unknown): DetectResult | null {
+ if (
+ isPlainObject(parsed) &&
+ typeof parsed['type'] === 'string' &&
+ Array.isArray(parsed['data']) &&
+ CHART_TYPES.has(parsed['type'].toLowerCase())
+ ) {
+ return { renderer: 'chart', data: parsed };
+ }
+
+ if (Array.isArray(parsed)) {
+ if (looksLikeChartArray(parsed)) {
+ return { renderer: 'chart', data: parsed };
+ }
+ if (parsed.length > 0 && parsed.every(isPlainObject)) {
+ return { renderer: 'table', data: parsed };
+ }
+ return { renderer: 'table', data: parsed };
+ }
+
+ if (isPlainObject(parsed)) {
+ return Object.values(parsed).every(isScalar)
+ ? { renderer: 'stat-cards', data: parsed }
+ : { renderer: 'json', data: parsed };
+ }
+
+ return null;
+}
+
+function parseCsv(text: string): Record[] | null {
+ const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
+ if (lines.length < 2) {
+ return null;
+ }
+ const delimiter = lines[0].includes('\t') ? '\t' : ',';
+ const counts = lines.map((line) => line.split(delimiter).length);
+ if (counts[0] < 2 || !counts.every((c) => c === counts[0])) {
+ return null;
+ }
+ const headers = lines[0].split(delimiter).map((h) => h.trim());
+ if (headers.some((h) => h.length === 0 || h.length > 30)) {
+ return null;
+ }
+ return lines.slice(1).map((line) => {
+ const cells = line.split(delimiter);
+ const row: Record = {};
+ for (const [index, header] of headers.entries()) {
+ row[header] = (cells[index] ?? '').trim();
+ }
+ return row;
+ });
+}
+
+export function detectFormat(input: string): DetectResult {
+ const text = (input ?? '').trim();
+ if (!text) {
+ return { renderer: 'text' };
+ }
+
+ const fence = MERMAID_FENCE.exec(text);
+ if (fence) {
+ return { renderer: 'diagram', data: fence[1].trim() };
+ }
+ const firstLine = text.split(/\r?\n/)[0].trim();
+ if (MERMAID_FIRST_LINE.test(firstLine)) {
+ return { renderer: 'diagram', data: text };
+ }
+
+ if (text.startsWith('{') || text.startsWith('[')) {
+ try {
+ const result = detectJson(JSON.parse(text));
+ if (result) {
+ return result;
+ }
+ } catch {
+ // not JSON
+ }
+ }
+
+ const csv = parseCsv(text);
+ if (csv) {
+ return { renderer: 'table', data: csv };
+ }
+
+ return { renderer: 'markdown' };
+}
diff --git a/apps/ai-studio/src/utils/export-visualization.ts b/apps/ai-studio/src/utils/export-visualization.ts
new file mode 100644
index 000000000..cdfc8e139
--- /dev/null
+++ b/apps/ai-studio/src/utils/export-visualization.ts
@@ -0,0 +1,55 @@
+import { toBlob, toPng, toSvg } from 'html-to-image';
+
+const PNG_OPTIONS = { pixelRatio: 2, backgroundColor: '#ffffff' };
+
+function triggerDownload(href: string, filename: string): void {
+ const link = document.createElement('a');
+ link.href = href;
+ link.download = filename;
+ link.click();
+}
+
+export async function downloadPng(element: HTMLElement, filename = 'visualization.png'): Promise {
+ const dataUrl = await toPng(element, PNG_OPTIONS);
+ triggerDownload(dataUrl, filename);
+}
+
+// Returns false when it falls back to a download (e.g. Firefox can't write image blobs to the clipboard).
+export async function copyImage(element: HTMLElement): Promise {
+ const blob = await toBlob(element, PNG_OPTIONS);
+ if (!blob) {
+ return false;
+ }
+ const canCopyImage = typeof ClipboardItem !== 'undefined' && Boolean(navigator.clipboard?.write);
+ if (canCopyImage) {
+ try {
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
+ return true;
+ } catch {
+ // fall back to download
+ }
+ }
+ triggerDownload(URL.createObjectURL(blob), 'visualization.png');
+ return false;
+}
+
+export async function downloadSvg(element: HTMLElement, filename = 'visualization.svg'): Promise {
+ const svg = element.querySelector('svg');
+ if (svg) {
+ const serialized = new XMLSerializer().serializeToString(svg);
+ const blob = new Blob([serialized], { type: 'image/svg+xml' });
+ triggerDownload(URL.createObjectURL(blob), filename);
+ return;
+ }
+ const dataUrl = await toSvg(element);
+ triggerDownload(dataUrl, filename);
+}
+
+export async function copySource(text: string): Promise {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/apps/backend/.env.example b/apps/backend/.env.example
index 5e0ded9e8..8c7df3951 100644
--- a/apps/backend/.env.example
+++ b/apps/backend/.env.example
@@ -11,3 +11,12 @@ HOST=127.0.0.1
# exposing the API. Remove this line when wiring a real AuthPort. See:
# apps/backend/auth-port.decision-log.md
WB_AUTH_PORT=allow-all
+# Cloudflare Turnstile secret key (server-side). Leave empty to disable bot
+# verification (local dev). When set, POST /api/workflows/:id/execute requires a
+# valid Turnstile token sent by the frontend as the cf-turnstile-token header.
+TURNSTILE_SECRET_KEY=
+# OpenRouter key for the Visualize "AI adapt" endpoint (POST /api/visualize/adapt).
+# Optional: leave empty to disable AI adapt (the endpoint returns 501). The
+# execution worker keeps its own key for running workflows.
+OPENROUTER_API_KEY=
+AI_MODEL=google/gemini-2.5-flash-lite
diff --git a/apps/backend/package.json b/apps/backend/package.json
index 3f1e1fb1a..045a7d4db 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -18,9 +18,11 @@
},
"dependencies": {
"@hono/node-server": "^1.14.0",
+ "@openrouter/ai-sdk-provider": "^2.8.0",
"@temporalio/client": "^1.11.0",
"@workflow-builder/execution-core": "workspace:*",
"@workflow-builder/types": "workspace:*",
+ "ai": "^6.0.168",
"dotenv": "^17.4.2",
"drizzle-orm": "^0.44.0",
"hono": "^4.7.0",
diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts
index a813c14f6..cc2e7543e 100644
--- a/apps/backend/src/env.ts
+++ b/apps/backend/src/env.ts
@@ -16,4 +16,9 @@ export const env = {
RATE_LIMIT_EXECUTE_PER_MINUTE: Number(envOr('RATE_LIMIT_EXECUTE_PER_MINUTE', '0')),
RATE_LIMIT_EXECUTE_PER_DAY: Number(envOr('RATE_LIMIT_EXECUTE_PER_DAY', '0')),
TRUST_PROXY: envOr('TRUST_PROXY', 'false') === 'true',
+ // Null = Turnstile verification disabled (local dev runs unprotected).
+ TURNSTILE_SECRET_KEY: process.env['TURNSTILE_SECRET_KEY'] ?? null,
+ // Null = the "AI adapt" endpoint is disabled (returns 501). The worker keeps its own key.
+ OPENROUTER_API_KEY: process.env['OPENROUTER_API_KEY'] ?? null,
+ AI_MODEL: envOr('AI_MODEL', 'google/gemini-2.5-flash-lite'),
};
diff --git a/apps/backend/src/routes/visualize.ts b/apps/backend/src/routes/visualize.ts
new file mode 100644
index 000000000..ea1039894
--- /dev/null
+++ b/apps/backend/src/routes/visualize.ts
@@ -0,0 +1,80 @@
+import { createOpenRouter } from '@openrouter/ai-sdk-provider';
+import { generateText } from 'ai';
+import { Hono } from 'hono';
+import { z } from 'zod';
+
+import type { AssertAuthorized, AuthVariables } from '../auth';
+import { env } from '../env';
+import { logger as backendLogger } from '../logger';
+import { guardExecution } from '../security/execution-guard';
+import type { TenantVariables } from '../tenant';
+
+const logger = backendLogger.child({ component: 'visualize-route' });
+
+const adaptSchema = z.object({
+ content: z.string().min(1).max(20_000),
+ format: z.enum(['diagram', 'chart', 'table', 'json', 'stat-cards', 'markdown', 'text']),
+});
+
+const FORMAT_PROMPTS: Record['format'], string> = {
+ diagram: `You reshape content into a Mermaid diagram so it can be rendered as one. Choose the diagram type that best represents the content: a flowchart (\`flowchart TD\`) for processes/steps/dependencies, a \`sequenceDiagram\` for interactions over time.
+Rules:
+- Output ONLY the raw Mermaid source. No code fences, no commentary.
+- Begin with a valid declaration and direction, e.g. \`flowchart TD\`.
+- Keep node labels short. Wrap every label that contains a space or punctuation in double quotes, e.g. \`A["Fix export bug"]\`. Never put parentheses, semicolons, colons, or unescaped quotes inside a label.
+- Aim for 4-12 nodes; connect them to show the real relationships.
+- Use only facts from the content. Do not invent steps.`,
+ chart: `You reshape content into chart data so it can be rendered as a chart. Find a categorical dimension and a numeric measure in the content.
+Rules:
+- Output ONLY JSON. No code fences, no commentary.
+- Shape: a JSON array like [{"label":"Q1","value":42}], OR {"type":"bar"|"line"|"pie"|"area","data":[{"label":...,"value":...}]}.
+- "value" must be a number. Aggregate or count where the content implies it (e.g. number of items per category).
+- Produce 2-12 data points. Use real numbers from the content; if there are none, count occurrences. If the content has nothing quantifiable, output [].`,
+ table: `You reshape content into a table. Output ONLY a JSON array of flat row objects that ALL share the same keys (the columns). Use concise column names, flatten nested values to short strings, and include one object per row. No code fences, no commentary. Use only facts from the content.`,
+ json: `Output ONLY a single JSON value (object or array) that faithfully captures the structure of the content. No code fences, no commentary.`,
+ 'stat-cards': `Extract the key metrics / KPIs from the content as a flat JSON object mapping a short human label to a scalar value (string, number, or boolean), e.g. {"Open tickets":14,"Owner":"Sam"}. Use 2-8 entries, the most important first. Output ONLY JSON, no code fences, no commentary.`,
+ markdown: `Reformat the content as clean, well-structured Markdown (headings, lists, bold where it helps). Keep all the information. Output ONLY the Markdown.`,
+ text: `Return the content as clean, readable plain text. Output ONLY the text.`,
+};
+
+export function createVisualizeRoutes(
+ assertAuthorized: AssertAuthorized,
+): Hono<{ Variables: AuthVariables & TenantVariables }> {
+ const routes = new Hono<{ Variables: AuthVariables & TenantVariables }>();
+
+ routes.post('/adapt', async (c) => {
+ await assertAuthorized(c, 'workflows:execute', { kind: 'workflows' });
+
+ const blocked = await guardExecution(c);
+ if (blocked) {
+ return blocked;
+ }
+
+ if (!env.OPENROUTER_API_KEY) {
+ return c.json({ code: 'adapt_disabled', message: 'AI adapt is not configured on this server.' }, 501);
+ }
+
+ const parsed = z.safeParse(adaptSchema, await c.req.json());
+ if (!parsed.success) {
+ return c.json({ code: 'validation_error', message: 'Request body failed validation' }, 400);
+ }
+ const { content, format } = parsed.data;
+
+ try {
+ const openrouter = createOpenRouter({ apiKey: env.OPENROUTER_API_KEY });
+ const result = await generateText({
+ model: openrouter.chat(env.AI_MODEL),
+ system: FORMAT_PROMPTS[format],
+ // Low temperature for stable structured output.
+ temperature: 0.2,
+ prompt: `Content to convert:\n\n${content}`,
+ });
+ return c.json({ output: result.text.trim() });
+ } catch (error) {
+ logger.error('adapt failed', { error: error instanceof Error ? error.message : String(error) });
+ return c.json({ code: 'adapt_failed', message: 'Could not adapt the content.' }, 502);
+ }
+ });
+
+ return routes;
+}
diff --git a/apps/backend/src/routes/workflows.ts b/apps/backend/src/routes/workflows.ts
index 20d632f81..cb7906c85 100644
--- a/apps/backend/src/routes/workflows.ts
+++ b/apps/backend/src/routes/workflows.ts
@@ -9,6 +9,7 @@ import { mapToExecutionModel } from '../domain/mapper/from-integration-data';
import { workflowSnapshotSchema } from '../domain/mapper/snapshot-schema';
import { getWorkflowEngine } from '../engine';
import { logger as backendLogger } from '../logger';
+import { guardExecution } from '../security/execution-guard';
import type { TenantVariables } from '../tenant';
const logger = backendLogger.child({ component: 'workflows-route' });
@@ -170,6 +171,12 @@ export function createWorkflowsRoutes(
await assertAuthorized(c, 'workflows:execute', { kind: 'workflow', workflowId });
+ // The one endpoint that spends real LLM budget, so it carries the abuse gate.
+ const blocked = await guardExecution(c);
+ if (blocked) {
+ return blocked;
+ }
+
const parsed = z.safeParse(executeSchema, await c.req.json());
if (!parsed.success) {
return c.json(
diff --git a/apps/backend/src/security/execution-guard.ts b/apps/backend/src/security/execution-guard.ts
new file mode 100644
index 000000000..0c5672590
--- /dev/null
+++ b/apps/backend/src/security/execution-guard.ts
@@ -0,0 +1,38 @@
+import type { Context } from 'hono';
+
+import { isTurnstileEnabled, verifyTurnstileToken } from './turnstile';
+
+function clientIp(c: Context): string {
+ const forwarded = c.req.header('x-forwarded-for');
+ return (
+ c.req.header('cf-connecting-ip') ??
+ (forwarded ? forwarded.split(',')[0].trim() : undefined) ??
+ c.req.header('x-real-ip') ??
+ 'unknown'
+ );
+}
+
+// Optional Turnstile bot verification; no-op when unconfigured. Returns a
+// Response to short-circuit the request, or null to proceed. Rate limiting is
+// handled by the execute rate-limit middleware in server.ts.
+export async function guardExecution(c: Context): Promise {
+ if (!isTurnstileEnabled()) {
+ return null;
+ }
+
+ const token = c.req.header('cf-turnstile-token');
+ if (!token) {
+ return c.json({ code: 'verification_required', message: 'Bot verification is required to run a workflow.' }, 403);
+ }
+
+ const ip = clientIp(c);
+ const ok = await verifyTurnstileToken(token, ip === 'unknown' ? undefined : ip);
+ if (!ok) {
+ return c.json(
+ { code: 'verification_failed', message: 'Bot verification failed. Please reload the page and try again.' },
+ 403,
+ );
+ }
+
+ return null;
+}
diff --git a/apps/backend/src/security/turnstile.ts b/apps/backend/src/security/turnstile.ts
new file mode 100644
index 000000000..3472d0146
--- /dev/null
+++ b/apps/backend/src/security/turnstile.ts
@@ -0,0 +1,43 @@
+import { env } from '../env';
+import { logger as backendLogger } from '../logger';
+
+const logger = backendLogger.child({ component: 'turnstile' });
+
+const SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
+
+export function isTurnstileEnabled(): boolean {
+ return Boolean(env.TURNSTILE_SECRET_KEY);
+}
+
+// Returns true when disabled (no secret); fails closed on a verifier/network error, since this gates a paid LLM run.
+export async function verifyTurnstileToken(token: string, remoteIp?: string): Promise {
+ const secret = env.TURNSTILE_SECRET_KEY;
+ if (!secret) {
+ return true;
+ }
+
+ const form = new URLSearchParams();
+ form.set('secret', secret);
+ form.set('response', token);
+ if (remoteIp) {
+ form.set('remoteip', remoteIp);
+ }
+
+ try {
+ const response = await fetch(SITEVERIFY_URL, {
+ method: 'POST',
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
+ body: form,
+ });
+ const result = (await response.json()) as { success?: boolean; 'error-codes'?: string[] };
+ if (!result.success) {
+ logger.warn('turnstile verification rejected', { errorCodes: result['error-codes'] ?? [] });
+ }
+ return result.success === true;
+ } catch (error) {
+ logger.error('turnstile verification error', {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ return false;
+ }
+}
diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts
index d29dd2853..9a2d7773e 100644
--- a/apps/backend/src/server.ts
+++ b/apps/backend/src/server.ts
@@ -17,6 +17,7 @@ import { env } from './env';
import { logger } from './logger';
import { createRateLimitMiddleware } from './middleware/rate-limit';
import { createExecutionsRoutes } from './routes/executions';
+import { createVisualizeRoutes } from './routes/visualize';
import { createWorkflowsRoutes } from './routes/workflows';
import { NoopTenantContextPort, type TenantContextPort, type TenantVariables, createTenantMiddleware } from './tenant';
@@ -63,14 +64,14 @@ app.use('/api/*', createAuthMiddleware(authPort));
app.use('/api/*', createTenantMiddleware(tenantPort));
if (env.RATE_LIMIT_EXECUTE_PER_MINUTE > 0 || env.RATE_LIMIT_EXECUTE_PER_DAY > 0) {
- app.use(
- '/api/workflows/:id/execute',
- createRateLimitMiddleware({
- perMinute: env.RATE_LIMIT_EXECUTE_PER_MINUTE,
- perDay: env.RATE_LIMIT_EXECUTE_PER_DAY,
- trustProxy: env.TRUST_PROXY,
- }),
- );
+ // Shared instance: workflow runs and Visualize "AI adapt" draw from one LLM-call budget.
+ const executeRateLimit = createRateLimitMiddleware({
+ perMinute: env.RATE_LIMIT_EXECUTE_PER_MINUTE,
+ perDay: env.RATE_LIMIT_EXECUTE_PER_DAY,
+ trustProxy: env.TRUST_PROXY,
+ });
+ app.use('/api/workflows/:id/execute', executeRateLimit);
+ app.use('/api/visualize/adapt', executeRateLimit);
logger.info('execute rate limit enabled', {
perMinute: env.RATE_LIMIT_EXECUTE_PER_MINUTE,
perDay: env.RATE_LIMIT_EXECUTE_PER_DAY,
@@ -80,6 +81,7 @@ if (env.RATE_LIMIT_EXECUTE_PER_MINUTE > 0 || env.RATE_LIMIT_EXECUTE_PER_DAY > 0)
app.route('/api/workflows', createWorkflowsRoutes(assertAuthorized));
app.route('/api/executions', createExecutionsRoutes(assertAuthorized));
+app.route('/api/visualize', createVisualizeRoutes(assertAuthorized));
// a failure (DB still starting) exits the process; the container restart policy retries
await runMigrations();
diff --git a/apps/execution-worker/.env.example b/apps/execution-worker/.env.example
index 9703d99c5..4a2aaf531 100644
--- a/apps/execution-worker/.env.example
+++ b/apps/execution-worker/.env.example
@@ -3,4 +3,9 @@ TEMPORAL_ADDRESS=127.0.0.1:7233
# OpenRouter — any model
OPENROUTER_API_KEY=sk-or-...
-AI_MODEL=anthropic/claude-3.5-haiku
+AI_MODEL=google/gemini-2.5-flash-lite
+
+# Tavily web search (optional). Enables the AI Agent's "Web search" tool. Get a
+# free key at https://tavily.com (free tier ~1000 searches/month). Leave empty
+# to disable: agents with web search toggled on still run, just without the tool.
+TAVILY_API_KEY=
diff --git a/apps/execution-worker/src/activities/ai-agent.ts b/apps/execution-worker/src/activities/ai-agent.ts
index 582be8056..cbc4e201a 100644
--- a/apps/execution-worker/src/activities/ai-agent.ts
+++ b/apps/execution-worker/src/activities/ai-agent.ts
@@ -1,12 +1,17 @@
-import { generateText } from 'ai';
+import { generateText, stepCountIs } from 'ai';
import { type ExecutionContext, type LoggerPort, resolveTemplate } from '@workflow-builder/execution-core';
import type { AiAgentNode } from '../domain/ai-studio-nodes';
+import { createWebSearchTool } from '../tools/web-search';
+
+// Bounds the agentic tool loop so a misbehaving model can't run up cost.
+const MAX_TOOL_STEPS = 4;
type AiAgentDeps = {
model: Parameters[0]['model'];
logger?: LoggerPort;
+ tavilyApiKey?: string;
};
export async function executeAiAgent(node: AiAgentNode, context: ExecutionContext, deps: AiAgentDeps) {
@@ -36,18 +41,21 @@ export async function executeAiAgent(node: AiAgentNode, context: ExecutionContex
userPrompt = `Here is the context from previous steps:\n\n${parts.join('\n\n')}`;
}
+ const webSearchEnabled = node.config.webSearch === true && Boolean(deps.tavilyApiKey);
+ const tools = webSearchEnabled ? { webSearch: createWebSearchTool(deps.tavilyApiKey!) } : undefined;
+
try {
const result = await generateText({
model: deps.model,
system: resolvedPrompt,
prompt: userPrompt,
+ // The AI SDK runs the tool call/execute/continue loop internally up to this many steps.
+ ...(tools ? { tools, stopWhen: stepCountIs(MAX_TOOL_STEPS) } : {}),
});
return { output: { response: result.text } };
} catch (error) {
- // Mirror the `node_failed` event payload shape (`{ error: { message, code? } }`)
- // so operators correlating a log line with the SSE event by executionId see
- // the same structure on both sides.
+ // Mirror the `node_failed` SSE payload shape so a log line and the event line up by executionId.
const message = error instanceof Error ? error.message : String(error);
deps.logger?.error('llm call failed', {
workflowId: context.workflowId,
diff --git a/apps/execution-worker/src/domain/ai-studio-nodes.ts b/apps/execution-worker/src/domain/ai-studio-nodes.ts
index 131fb0a73..2e331abfd 100644
--- a/apps/execution-worker/src/domain/ai-studio-nodes.ts
+++ b/apps/execution-worker/src/domain/ai-studio-nodes.ts
@@ -7,6 +7,7 @@ type TriggerNodeConfig = Record;
type AiAgentNodeConfig = {
systemPrompt: string; // supports {{namespace.path}} template references
+ webSearch?: boolean; // needs TAVILY_API_KEY to take effect
};
export type DecisionBranchCondition = {
@@ -27,6 +28,9 @@ type DecisionNodeConfig = {
decisionBranches: DecisionBranch[];
};
+// Display-only node; the UI reads the upstream output directly, so no runtime config.
+type VisualizeNodeConfig = Record;
+
export type TriggerNode = {
id: string;
type: 'ai-studio/trigger';
@@ -45,4 +49,10 @@ export type DecisionNode = {
config: DecisionNodeConfig;
};
-export type AiStudioNode = TriggerNode | AiAgentNode | DecisionNode;
+type VisualizeNode = {
+ id: string;
+ type: 'ai-studio/visualize';
+ config: VisualizeNodeConfig;
+};
+
+export type AiStudioNode = TriggerNode | AiAgentNode | DecisionNode | VisualizeNode;
diff --git a/apps/execution-worker/src/engines/temporal/worker.ts b/apps/execution-worker/src/engines/temporal/worker.ts
index 401c60e93..c941b41bc 100644
--- a/apps/execution-worker/src/engines/temporal/worker.ts
+++ b/apps/execution-worker/src/engines/temporal/worker.ts
@@ -10,6 +10,7 @@ import type { AiStudioNode } from '../../domain/ai-studio-nodes';
import { env } from '../../env';
import { executeDecision } from '../../executors/decision';
import { executeTrigger } from '../../executors/trigger';
+import { executeVisualize } from '../../executors/visualize';
import { logger } from '../../logger';
const { createOpenRouter } = await import('@openrouter/ai-sdk-provider');
@@ -24,7 +25,9 @@ const aiAgentLogger = logger.child({ component: 'ai-agent' });
const nodeExecutors: NodeExecutorRegistry = {
'ai-studio/trigger': executeTrigger,
'ai-studio/decision': executeDecision,
- 'ai-studio/ai-agent': (node, context) => executeAiAgent(node, context, { model, logger: aiAgentLogger }),
+ 'ai-studio/ai-agent': (node, context) =>
+ executeAiAgent(node, context, { model, logger: aiAgentLogger, tavilyApiKey: env.TAVILY_API_KEY }),
+ 'ai-studio/visualize': executeVisualize,
};
const activities = {
diff --git a/apps/execution-worker/src/env.ts b/apps/execution-worker/src/env.ts
index 019e35447..c86338aa9 100644
--- a/apps/execution-worker/src/env.ts
+++ b/apps/execution-worker/src/env.ts
@@ -17,5 +17,8 @@ export const env = {
DATABASE_URL: envOr('DATABASE_URL', 'postgresql://wb:wb@127.0.0.1:5432/workflow_builder'),
TEMPORAL_ADDRESS: envOr('TEMPORAL_ADDRESS', '127.0.0.1:7233'),
OPENROUTER_API_KEY: requireEnv('OPENROUTER_API_KEY'),
- AI_MODEL: envOr('AI_MODEL', 'anthropic/claude-3.5-haiku'),
+ // Cheap, fast default for the public demo; quality-per-cost over frontier capability.
+ AI_MODEL: envOr('AI_MODEL', 'google/gemini-2.5-flash-lite'),
+ // Optional. Enables the AI Agent's web-search tool; agents run without it when unset.
+ TAVILY_API_KEY: process.env['TAVILY_API_KEY'],
};
diff --git a/apps/execution-worker/src/executors/visualize.ts b/apps/execution-worker/src/executors/visualize.ts
new file mode 100644
index 000000000..1ce3587d2
--- /dev/null
+++ b/apps/execution-worker/src/executors/visualize.ts
@@ -0,0 +1,4 @@
+// Display-only node: the executor just has to complete so the on-canvas card reveals.
+export function executeVisualize() {
+ return { output: { visualized: true } };
+}
diff --git a/apps/execution-worker/src/tools/web-search.ts b/apps/execution-worker/src/tools/web-search.ts
new file mode 100644
index 000000000..085977447
--- /dev/null
+++ b/apps/execution-worker/src/tools/web-search.ts
@@ -0,0 +1,55 @@
+import { jsonSchema, tool } from 'ai';
+
+const TAVILY_ENDPOINT = 'https://api.tavily.com/search';
+const MAX_RESULTS = 5;
+
+type TavilyResult = { title: string; url: string; content: string };
+type TavilyResponse = { answer?: string; results?: TavilyResult[] };
+
+export function createWebSearchTool(apiKey: string) {
+ return tool({
+ description:
+ 'Search the web for current or external information. Use it when the answer needs up-to-date facts, recent events, or sources that were not provided. Returns a short answer plus the top result snippets with URLs.',
+ inputSchema: jsonSchema<{ query: string }>({
+ type: 'object',
+ properties: {
+ query: { type: 'string', description: 'The search query' },
+ },
+ required: ['query'],
+ additionalProperties: false,
+ }),
+ execute: async ({ query }) => {
+ try {
+ const response = await fetch(TAVILY_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: JSON.stringify({
+ query,
+ search_depth: 'basic',
+ max_results: MAX_RESULTS,
+ include_answer: true,
+ }),
+ });
+
+ if (!response.ok) {
+ return { error: `Web search failed (HTTP ${response.status}).` };
+ }
+
+ const data = (await response.json()) as TavilyResponse;
+ return {
+ answer: data.answer ?? '',
+ results: (data.results ?? []).slice(0, MAX_RESULTS).map((result) => ({
+ title: result.title,
+ url: result.url,
+ snippet: result.content,
+ })),
+ };
+ } catch (error) {
+ return { error: error instanceof Error ? error.message : 'Web search failed.' };
+ }
+ },
+ });
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f2ec0e8c9..f18eda6ba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -106,7 +106,7 @@ importers:
version: 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@synergycodes/overflow-ui':
specifier: 1.0.0-beta.27
- version: 1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ version: 1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@workflow-builder/types':
specifier: workspace:*
version: link:../../packages/types
@@ -116,15 +116,33 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ html-to-image:
+ specifier: 1.11.11
+ version: 1.11.11
immer:
specifier: ^10.1.1
version: 10.1.1
+ mermaid:
+ specifier: ^11.15.0
+ version: 11.15.0
react:
specifier: ^19.1.0
version: 19.1.0
react-dom:
specifier: 'catalog:'
version: 19.1.0(react@19.1.0)
+ react-i18next:
+ specifier: ^15.4.1
+ version: 15.4.1(i18next@24.2.3(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react-markdown:
+ specifier: ^10.1.0
+ version: 10.1.0(@types/react@19.1.8)(react@19.1.0)
+ recharts:
+ specifier: ^3.9.0
+ version: 3.9.0(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@19.0.0)(react@19.1.0)(redux@5.0.1)
+ remark-gfm:
+ specifier: ^4.0.1
+ version: 4.0.1
zustand:
specifier: ^5.0.1
version: 5.0.3(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0))
@@ -143,7 +161,7 @@ importers:
version: 6.4.1(@types/node@22.12.0)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.4)
vite-plugin-svgr:
specifier: ^4.3.0
- version: 4.3.0(rollup@4.57.1)(typescript@5.6.3)(vite@6.4.1(@types/node@22.12.0)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.4))
+ version: 4.3.0(rollup@4.57.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.12.0)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.4))
vitest:
specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.12.0)(jiti@2.6.1)(jsdom@26.0.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.4)
@@ -153,6 +171,9 @@ importers:
'@hono/node-server':
specifier: ^1.14.0
version: 1.19.14(hono@4.12.14)
+ '@openrouter/ai-sdk-provider':
+ specifier: ^2.8.0
+ version: 2.8.0(ai@6.0.168(zod@4.3.6))(zod@4.3.6)
'@temporalio/client':
specifier: ^1.11.0
version: 1.16.0
@@ -162,6 +183,9 @@ importers:
'@workflow-builder/types':
specifier: workspace:*
version: link:../../packages/types
+ ai:
+ specifier: ^6.0.168
+ version: 6.0.168(zod@4.3.6)
dotenv:
specifier: ^17.4.2
version: 17.4.2
@@ -207,7 +231,7 @@ importers:
version: 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@synergycodes/overflow-ui':
specifier: 1.0.0-beta.27
- version: 1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ version: 1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@xyflow/react':
specifier: 'catalog:'
version: 12.10.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -440,7 +464,7 @@ importers:
version: 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@synergycodes/overflow-ui':
specifier: 1.0.0-beta.27
- version: 1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ version: 1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@xyflow/react':
specifier: ^12.0.0
version: 12.10.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -723,6 +747,9 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
+ '@braintree/sanitize-url@7.1.2':
+ resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
+
'@capsizecss/unpack@4.0.0':
resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==}
engines: {node: '>=18'}
@@ -785,6 +812,9 @@ packages:
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
+ '@chevrotain/types@11.1.2':
+ resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
+
'@commitlint/cli@21.0.1':
resolution: {integrity: sha512-8vq10krmbJwBkvzXKhbs4o4JQEVscd3pqOlWuDUaDBwbeL694/P33UC29tZQFTAgPU9fVJ2+f2m3zw16yKWxHg==}
engines: {node: '>=22.12.0'}
@@ -1574,6 +1604,9 @@ packages:
'@iconify/utils@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
+ '@iconify/utils@3.1.3':
+ resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==}
+
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
@@ -1910,6 +1943,9 @@ packages:
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+ '@mermaid-js/parser@1.1.1':
+ resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==}
+
'@microsoft/api-extractor-model@7.33.8':
resolution: {integrity: sha512-aIcoQggPyer3B6Ze3usz0YWC/oBwUHfRH5ETUsr+oT2BRA6SfTJl7IKPcPZkX4UR+PohowzW4uMxsvjrn8vm+w==}
@@ -2198,6 +2234,17 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
+ '@reduxjs/toolkit@2.12.0':
+ resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
@@ -2406,6 +2453,9 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
engines: {node: '>=14'}
@@ -2664,24 +2714,99 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-axis@3.0.6':
+ resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
+
+ '@types/d3-brush@3.0.6':
+ resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
+
+ '@types/d3-chord@3.0.6':
+ resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
+
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+ '@types/d3-contour@3.0.6':
+ resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
+
+ '@types/d3-delaunay@6.0.4':
+ resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
+
+ '@types/d3-dispatch@3.0.7':
+ resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
+
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+ '@types/d3-dsv@3.0.7':
+ resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-fetch@3.0.7':
+ resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
+
+ '@types/d3-force@3.0.10':
+ resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
+
+ '@types/d3-format@3.0.4':
+ resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
+
+ '@types/d3-geo@3.1.0':
+ resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
+
+ '@types/d3-hierarchy@3.1.7':
+ resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
+
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-polygon@3.0.2':
+ resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
+
+ '@types/d3-quadtree@3.0.6':
+ resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
+
+ '@types/d3-random@3.0.3':
+ resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
+
+ '@types/d3-scale-chromatic@3.1.0':
+ resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
+
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+ '@types/d3-shape@3.1.8':
+ resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
+
+ '@types/d3-time-format@4.0.3':
+ resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+ '@types/d3@7.4.3':
+ resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -2706,6 +2831,9 @@ packages:
'@types/express@5.0.5':
resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==}
+ '@types/geojson@7946.0.16':
+ resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
+
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
@@ -2797,6 +2925,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
'@types/validator@13.15.4':
resolution: {integrity: sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==}
@@ -2854,6 +2985,9 @@ packages:
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
+ '@upsetjs/venn.js@2.0.0':
+ resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
+
'@vercel/oidc@3.2.0':
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
engines: {node: '>= 20'}
@@ -3518,6 +3652,10 @@ packages:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
common-ancestor-path@1.0.1:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
@@ -3589,6 +3727,12 @@ packages:
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
+ cose-base@1.0.3:
+ resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
+
+ cose-base@2.2.0:
+ resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
+
cosmiconfig-typescript-loader@6.3.0:
resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==}
engines: {node: '>=v18'}
@@ -3670,10 +3814,51 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ cytoscape-cose-bilkent@4.1.0:
+ resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape-fcose@2.2.0:
+ resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape@3.34.0:
+ resolution: {integrity: sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==}
+ engines: {node: '>=0.10'}
+
+ d3-array@2.12.1:
+ resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
+
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-axis@3.0.0:
+ resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
+ engines: {node: '>=12'}
+
+ d3-brush@3.0.0:
+ resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
+ engines: {node: '>=12'}
+
+ d3-chord@3.0.1:
+ resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
+ engines: {node: '>=12'}
+
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
+ d3-contour@4.0.2:
+ resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
+ engines: {node: '>=12'}
+
+ d3-delaunay@6.0.4:
+ resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
+ engines: {node: '>=12'}
+
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
@@ -3682,18 +3867,88 @@ packages:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
+ d3-dsv@3.0.1:
+ resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
+ d3-fetch@3.0.1:
+ resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
+ engines: {node: '>=12'}
+
+ d3-force@3.0.0:
+ resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ engines: {node: '>=12'}
+
+ d3-geo@3.1.1:
+ resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
+ engines: {node: '>=12'}
+
+ d3-hierarchy@3.1.2:
+ resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
+ engines: {node: '>=12'}
+
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
+ d3-path@1.0.9:
+ resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-polygon@3.0.1:
+ resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
+ engines: {node: '>=12'}
+
+ d3-quadtree@3.0.1:
+ resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
+ engines: {node: '>=12'}
+
+ d3-random@3.0.1:
+ resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
+ engines: {node: '>=12'}
+
+ d3-sankey@0.12.3:
+ resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
+
+ d3-scale-chromatic@3.1.0:
+ resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
+ d3-shape@1.3.7:
+ resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
@@ -3708,6 +3963,13 @@ packages:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
+ d3@7.9.0:
+ resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
+ engines: {node: '>=12'}
+
+ dagre-d3-es@7.0.14:
+ resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
+
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
@@ -3727,8 +3989,8 @@ packages:
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
- dayjs@1.11.13:
- resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+ dayjs@1.11.21:
+ resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@@ -3751,6 +4013,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
decimal.js@10.5.0:
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
@@ -3775,6 +4040,9 @@ packages:
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+ delaunator@5.1.0:
+ resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
+
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@@ -3852,6 +4120,9 @@ packages:
dompurify@3.3.0:
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
+ dompurify@3.4.11:
+ resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
+
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -4520,6 +4791,9 @@ packages:
h3@1.15.5:
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
+ hachure-fill@0.5.2:
+ resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
+
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -4635,6 +4909,9 @@ packages:
html-to-image@1.11.11:
resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==}
+ html-url-attributes@3.0.1:
+ resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
+
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -4705,6 +4982,9 @@ packages:
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
+ immer@11.1.8:
+ resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==}
+
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -4738,6 +5018,13 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
+ internmap@1.0.1:
+ resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
+
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
inversify@6.0.1:
resolution: {integrity: sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ==}
@@ -5042,9 +5329,16 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
+ katex@0.16.47:
+ resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==}
+ hasBin: true
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ khroma@2.1.0:
+ resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
+
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
@@ -5068,6 +5362,12 @@ packages:
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
+ layout-base@1.0.2:
+ resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
+
+ layout-base@2.0.1:
+ resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
+
levn@0.3.0:
resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==}
engines: {node: '>= 0.8.0'}
@@ -5199,6 +5499,11 @@ packages:
engines: {node: '>= 18'}
hasBin: true
+ marked@16.4.2:
+ resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -5296,6 +5601,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
+ mermaid@11.15.0:
+ resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==}
+
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -5716,6 +6024,9 @@ packages:
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+ path-data-parser@0.1.0:
+ resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -5784,6 +6095,12 @@ packages:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
+ points-on-curve@0.2.0:
+ resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
+
+ points-on-path@0.2.1:
+ resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
+
possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
engines: {node: '>= 0.4'}
@@ -5934,6 +6251,12 @@ packages:
react-is@19.0.0:
resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==}
+ react-markdown@10.1.0:
+ resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
+ peerDependencies:
+ '@types/react': '>=18'
+ react: '>=18'
+
react-mentions-ts@5.4.7:
resolution: {integrity: sha512-bTK6joPmyvLckVf1v7vE2xSSeqvL4ZwuzFvGZpt+IrtdDOdFZjiwTUBo5920kiE6WbH/v1PP81xAo6pQ0NQ0Pg==}
engines: {node: '>=20'}
@@ -5950,6 +6273,18 @@ packages:
react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-redux@9.3.0:
+ resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
+ peerDependencies:
+ '@types/react': ^18.2.25 || ^19
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@@ -6012,6 +6347,14 @@ packages:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
+ recharts@3.9.0:
+ resolution: {integrity: sha512-dCEcE9y20c8H2tkVeByrAXhhnBJk6/QLbxKmn+dJUptOfc5NMjwRh1jo0vZPRLD+5dMrHrP+hPEsfbGBMfnf5Q==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
recma-build-jsx@1.0.0:
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
@@ -6026,6 +6369,14 @@ packages:
recma-stringify@1.0.0:
resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
reflect-metadata@0.1.13:
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
@@ -6123,6 +6474,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ reselect@5.2.0:
+ resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -6170,11 +6524,17 @@ packages:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
+ robust-predicates@3.0.3:
+ resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
+
rollup@4.57.1:
resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ roughjs@4.6.6:
+ resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
+
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
@@ -6185,6 +6545,9 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ rw@1.3.3:
+ resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
@@ -6483,6 +6846,9 @@ packages:
stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
+ stylis@4.4.0:
+ resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==}
+
suf-log@2.5.3:
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
@@ -6579,6 +6945,9 @@ packages:
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -6650,6 +7019,10 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
+ ts-dedent@2.3.0:
+ resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==}
+ engines: {node: '>=6.10'}
+
tsconfck@3.1.6:
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
engines: {node: ^18 || >=20}
@@ -6966,6 +7339,9 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-vendor@37.3.6:
+ resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
+
vite-bundle-analyzer@0.17.1:
resolution: {integrity: sha512-ubjLhkuRgOSBNck+6xBbQmjmh8SeLTG4alEM5PX2TNzyGhKLwWlyCz1YG0an3RQnscbhVzSb6kYteoHXhP///A==}
hasBin: true
@@ -7723,6 +8099,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@braintree/sanitize-url@7.1.2': {}
+
'@capsizecss/unpack@4.0.0':
dependencies:
fontkitten: 1.0.2
@@ -7872,6 +8250,8 @@ snapshots:
human-id: 4.1.3
prettier: 2.8.8
+ '@chevrotain/types@11.1.2': {}
+
'@commitlint/cli@21.0.1(@types/node@22.12.0)(conventional-commits-parser@6.4.0)(typescript@5.6.3)':
dependencies:
'@commitlint/format': 21.0.1
@@ -8549,6 +8929,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@iconify/utils@3.1.3':
+ dependencies:
+ '@antfu/install-pkg': 1.1.0
+ '@iconify/types': 2.0.0
+ import-meta-resolve: 4.2.0
+
'@img/colour@1.0.0':
optional: true
@@ -8843,12 +9229,12 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
- '@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ '@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(dayjs@1.11.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@mantine/hooks': 7.17.8(react@19.1.0)
clsx: 2.1.1
- dayjs: 1.11.13
+ dayjs: 1.11.21
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
@@ -8902,6 +9288,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@mermaid-js/parser@1.1.1':
+ dependencies:
+ '@chevrotain/types': 11.1.2
+
'@microsoft/api-extractor-model@7.33.8(@types/node@22.12.0)':
dependencies:
'@microsoft/tsdoc': 0.16.0
@@ -9162,6 +9552,18 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
+ '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1))(react@19.1.0)':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.1.8
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.2.0
+ optionalDependencies:
+ react: 19.1.0
+ react-redux: 9.3.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1)
+
'@rollup/pluginutils@5.3.0(rollup@4.57.1)':
dependencies:
'@types/estree': 1.0.8
@@ -9343,6 +9745,8 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@standard-schema/utils@0.3.0': {}
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.7
@@ -9494,12 +9898,12 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
- '@synergycodes/overflow-ui@1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ '@synergycodes/overflow-ui@1.0.0-beta.27(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
'@floating-ui/react': 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- '@mantine/dates': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@mantine/dates': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@7.17.8(react@19.1.0))(dayjs@1.11.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@mui/base': 5.0.0-beta.62(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@mui/material': 6.5.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@phosphor-icons/react': 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -9666,18 +10070,81 @@ snapshots:
dependencies:
'@types/node': 22.12.0
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-axis@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-brush@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-chord@3.0.6': {}
+
'@types/d3-color@3.1.3': {}
+ '@types/d3-contour@3.0.6':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-delaunay@6.0.4': {}
+
+ '@types/d3-dispatch@3.0.7': {}
+
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
+ '@types/d3-dsv@3.0.7': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-fetch@3.0.7':
+ dependencies:
+ '@types/d3-dsv': 3.0.7
+
+ '@types/d3-force@3.0.10': {}
+
+ '@types/d3-format@3.0.4': {}
+
+ '@types/d3-geo@3.1.0':
+ dependencies:
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-hierarchy@3.1.7': {}
+
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-polygon@3.0.2': {}
+
+ '@types/d3-quadtree@3.0.6': {}
+
+ '@types/d3-random@3.0.3': {}
+
+ '@types/d3-scale-chromatic@3.1.0': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
'@types/d3-selection@3.0.11': {}
+ '@types/d3-shape@3.1.8':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time-format@4.0.3': {}
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
@@ -9687,6 +10154,39 @@ snapshots:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
+ '@types/d3@7.4.3':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-axis': 3.0.6
+ '@types/d3-brush': 3.0.6
+ '@types/d3-chord': 3.0.6
+ '@types/d3-color': 3.1.3
+ '@types/d3-contour': 3.0.6
+ '@types/d3-delaunay': 6.0.4
+ '@types/d3-dispatch': 3.0.7
+ '@types/d3-drag': 3.0.7
+ '@types/d3-dsv': 3.0.7
+ '@types/d3-ease': 3.0.2
+ '@types/d3-fetch': 3.0.7
+ '@types/d3-force': 3.0.10
+ '@types/d3-format': 3.0.4
+ '@types/d3-geo': 3.1.0
+ '@types/d3-hierarchy': 3.1.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-path': 3.1.1
+ '@types/d3-polygon': 3.0.2
+ '@types/d3-quadtree': 3.0.6
+ '@types/d3-random': 3.0.3
+ '@types/d3-scale': 4.0.9
+ '@types/d3-scale-chromatic': 3.1.0
+ '@types/d3-selection': 3.0.11
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-time-format': 4.0.3
+ '@types/d3-timer': 3.0.2
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -9722,6 +10222,8 @@ snapshots:
'@types/express-serve-static-core': 5.1.0
'@types/serve-static': 1.15.10
+ '@types/geojson@7946.0.16': {}
+
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -9807,6 +10309,8 @@ snapshots:
'@types/unist@3.0.3': {}
+ '@types/use-sync-external-store@0.0.6': {}
+
'@types/validator@13.15.4': {}
'@types/yauzl@2.10.3':
@@ -9893,6 +10397,11 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
+ '@upsetjs/venn.js@2.0.0':
+ optionalDependencies:
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
'@vercel/oidc@3.2.0': {}
'@vitejs/plugin-react@4.3.4(vite@6.4.1(@types/node@22.12.0)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.4))':
@@ -10761,6 +11270,8 @@ snapshots:
commander@7.2.0: {}
+ commander@8.3.0: {}
+
common-ancestor-path@1.0.1: {}
compare-func@2.0.0:
@@ -10824,6 +11335,14 @@ snapshots:
core-js@3.46.0:
optional: true
+ cose-base@1.0.3:
+ dependencies:
+ layout-base: 1.0.2
+
+ cose-base@2.2.0:
+ dependencies:
+ layout-base: 2.0.1
+
cosmiconfig-typescript-loader@6.3.0(@types/node@22.12.0)(cosmiconfig@9.0.1(typescript@5.6.3))(typescript@5.6.3):
dependencies:
'@types/node': 22.12.0
@@ -10923,8 +11442,50 @@ snapshots:
csstype@3.1.3: {}
+ cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0):
+ dependencies:
+ cose-base: 1.0.3
+ cytoscape: 3.34.0
+
+ cytoscape-fcose@2.2.0(cytoscape@3.34.0):
+ dependencies:
+ cose-base: 2.2.0
+ cytoscape: 3.34.0
+
+ cytoscape@3.34.0: {}
+
+ d3-array@2.12.1:
+ dependencies:
+ internmap: 1.0.1
+
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-axis@3.0.0: {}
+
+ d3-brush@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3-chord@3.0.1:
+ dependencies:
+ d3-path: 3.1.0
+
d3-color@3.1.0: {}
+ d3-contour@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-delaunay@6.0.4:
+ dependencies:
+ delaunator: 5.1.0
+
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
@@ -10932,14 +11493,82 @@ snapshots:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
+ d3-dsv@3.0.1:
+ dependencies:
+ commander: 7.2.0
+ iconv-lite: 0.6.3
+ rw: 1.3.3
+
d3-ease@3.0.1: {}
+ d3-fetch@3.0.1:
+ dependencies:
+ d3-dsv: 3.0.1
+
+ d3-force@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-timer: 3.0.1
+
+ d3-format@3.1.2: {}
+
+ d3-geo@3.1.1:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-hierarchy@3.1.2: {}
+
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
+ d3-path@1.0.9: {}
+
+ d3-path@3.1.0: {}
+
+ d3-polygon@3.0.1: {}
+
+ d3-quadtree@3.0.1: {}
+
+ d3-random@3.0.1: {}
+
+ d3-sankey@0.12.3:
+ dependencies:
+ d3-array: 2.12.1
+ d3-shape: 1.3.7
+
+ d3-scale-chromatic@3.1.0:
+ dependencies:
+ d3-color: 3.1.0
+ d3-interpolate: 3.0.1
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
d3-selection@3.0.0: {}
+ d3-shape@1.3.7:
+ dependencies:
+ d3-path: 1.0.9
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
@@ -10959,6 +11588,44 @@ snapshots:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
+ d3@7.9.0:
+ dependencies:
+ d3-array: 3.2.4
+ d3-axis: 3.0.0
+ d3-brush: 3.0.0
+ d3-chord: 3.0.1
+ d3-color: 3.1.0
+ d3-contour: 4.0.2
+ d3-delaunay: 6.0.4
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-dsv: 3.0.1
+ d3-ease: 3.0.1
+ d3-fetch: 3.0.1
+ d3-force: 3.0.0
+ d3-format: 3.1.2
+ d3-geo: 3.1.1
+ d3-hierarchy: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-path: 3.1.0
+ d3-polygon: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-random: 3.0.1
+ d3-scale: 4.0.2
+ d3-scale-chromatic: 3.1.0
+ d3-selection: 3.0.0
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ d3-timer: 3.0.1
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ d3-zoom: 3.0.0
+
+ dagre-d3-es@7.0.14:
+ dependencies:
+ d3: 7.9.0
+ lodash-es: 4.17.21
+
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@@ -10984,7 +11651,7 @@ snapshots:
date-fns@4.1.0: {}
- dayjs@1.11.13: {}
+ dayjs@1.11.21: {}
de-indent@1.0.2: {}
@@ -10996,6 +11663,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js-light@2.5.1: {}
+
decimal.js@10.5.0: {}
decode-named-character-reference@1.3.0:
@@ -11020,6 +11689,10 @@ snapshots:
defu@6.1.4: {}
+ delaunator@5.1.0:
+ dependencies:
+ robust-predicates: 3.0.3
+
delayed-stream@1.0.0: {}
depd@2.0.0: {}
@@ -11085,6 +11758,10 @@ snapshots:
'@types/trusted-types': 2.0.7
optional: true
+ dompurify@3.4.11:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
@@ -11900,6 +12577,8 @@ snapshots:
ufo: 1.6.3
uncrypto: 0.1.3
+ hachure-fill@0.5.2: {}
+
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
@@ -12133,6 +12812,8 @@ snapshots:
html-to-image@1.11.11: {}
+ html-url-attributes@3.0.1: {}
+
html-void-elements@3.0.0: {}
html-whitespace-sensitive-tag-names@3.0.1: {}
@@ -12212,6 +12893,8 @@ snapshots:
immer@10.1.1: {}
+ immer@11.1.8: {}
+
import-fresh@3.3.0:
dependencies:
parent-module: 1.0.1
@@ -12237,6 +12920,10 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
+ internmap@1.0.1: {}
+
+ internmap@2.0.3: {}
+
inversify@6.0.1: {}
iobuffer@5.4.0: {}
@@ -12561,10 +13248,16 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
+ katex@0.16.47:
+ dependencies:
+ commander: 8.3.0
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
+ khroma@2.1.0: {}
+
kleur@3.0.3: {}
kleur@4.1.5: {}
@@ -12591,6 +13284,10 @@ snapshots:
kolorist@1.8.0: {}
+ layout-base@1.0.2: {}
+
+ layout-base@2.0.1: {}
+
levn@0.3.0:
dependencies:
prelude-ls: 1.1.2
@@ -12726,6 +13423,8 @@ snapshots:
marked@15.0.12: {}
+ marked@16.4.2: {}
+
math-intrinsics@1.1.0: {}
md5@2.3.0:
@@ -12952,6 +13651,30 @@ snapshots:
merge2@1.4.1: {}
+ mermaid@11.15.0:
+ dependencies:
+ '@braintree/sanitize-url': 7.1.2
+ '@iconify/utils': 3.1.3
+ '@mermaid-js/parser': 1.1.1
+ '@types/d3': 7.4.3
+ '@upsetjs/venn.js': 2.0.0
+ cytoscape: 3.34.0
+ cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0)
+ cytoscape-fcose: 2.2.0(cytoscape@3.34.0)
+ d3: 7.9.0
+ d3-sankey: 0.12.3
+ dagre-d3-es: 7.0.14
+ dayjs: 1.11.21
+ dompurify: 3.4.11
+ es-toolkit: 1.46.1
+ katex: 0.16.47
+ khroma: 2.1.0
+ marked: 16.4.2
+ roughjs: 4.6.6
+ stylis: 4.4.0
+ ts-dedent: 2.3.0
+ uuid: 11.1.0
+
micromark-core-commonmark@2.0.3:
dependencies:
decode-named-character-reference: 1.3.0
@@ -13565,6 +14288,8 @@ snapshots:
path-browserify@1.0.1: {}
+ path-data-parser@0.1.0: {}
+
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -13612,6 +14337,13 @@ snapshots:
pluralize@8.0.0: {}
+ points-on-curve@0.2.0: {}
+
+ points-on-path@0.2.1:
+ dependencies:
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+
possible-typed-array-names@1.0.0: {}
postcss-nested@6.2.0(postcss@8.5.6):
@@ -13770,6 +14502,24 @@ snapshots:
react-is@19.0.0: {}
+ react-markdown@10.1.0(@types/react@19.1.8)(react@19.1.0):
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/react': 19.1.8
+ devlop: 1.1.0
+ hast-util-to-jsx-runtime: 2.3.6
+ html-url-attributes: 3.0.1
+ mdast-util-to-hast: 13.2.1
+ react: 19.1.0
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
react-mentions-ts@5.4.7(class-variance-authority@0.7.1)(clsx@2.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwind-merge@3.5.0):
dependencies:
class-variance-authority: 0.7.1
@@ -13783,6 +14533,15 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
+ react-redux@9.3.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.1.0
+ use-sync-external-store: 1.4.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ redux: 5.0.1
+
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0):
@@ -13843,6 +14602,26 @@ snapshots:
readdirp@5.0.0: {}
+ recharts@3.9.0(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@19.0.0)(react@19.1.0)(redux@5.0.1):
+ dependencies:
+ '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1))(react@19.1.0)
+ clsx: 2.1.1
+ decimal.js-light: 2.5.1
+ es-toolkit: 1.46.1
+ eventemitter3: 5.0.1
+ immer: 10.1.1
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-is: 19.0.0
+ react-redux: 9.3.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1)
+ reselect: 5.2.0
+ tiny-invariant: 1.3.3
+ use-sync-external-store: 1.4.0(react@19.1.0)
+ victory-vendor: 37.3.6
+ transitivePeerDependencies:
+ - '@types/react'
+ - redux
+
recma-build-jsx@1.0.0:
dependencies:
'@types/estree': 1.0.8
@@ -13872,6 +14651,12 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
reflect-metadata@0.1.13: {}
reflect.getprototypeof@1.0.10:
@@ -14035,6 +14820,8 @@ snapshots:
require-from-string@2.0.2: {}
+ reselect@5.2.0: {}
+
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@@ -14090,6 +14877,8 @@ snapshots:
rgbcolor@1.0.1:
optional: true
+ robust-predicates@3.0.3: {}
+
rollup@4.57.1:
dependencies:
'@types/estree': 1.0.8
@@ -14121,6 +14910,13 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.57.1
fsevents: 2.3.3
+ roughjs@4.6.6:
+ dependencies:
+ hachure-fill: 0.5.2
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+ points-on-path: 0.2.1
+
router@2.2.0:
dependencies:
debug: 4.4.3
@@ -14137,6 +14933,8 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
+ rw@1.3.3: {}
+
rxjs@7.8.1:
dependencies:
tslib: 2.8.1
@@ -14510,6 +15308,8 @@ snapshots:
stylis@4.2.0: {}
+ stylis@4.4.0: {}
+
suf-log@2.5.3:
dependencies:
s.color: 0.0.15
@@ -14605,6 +15405,8 @@ snapshots:
tiny-inflate@1.0.3: {}
+ tiny-invariant@1.3.3: {}
+
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -14656,6 +15458,8 @@ snapshots:
dependencies:
typescript: 5.6.3
+ ts-dedent@2.3.0: {}
+
tsconfck@3.1.6(typescript@5.6.3):
optionalDependencies:
typescript: 5.6.3
@@ -14945,6 +15749,23 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ victory-vendor@37.3.6:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
vite-bundle-analyzer@0.17.1: {}
vite-node@3.0.4(@types/node@22.12.0)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.4):