feat: rendering for agents
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useContext, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
AbstractNodeProps,
|
AbstractNodeProps,
|
||||||
createAbstractNodeComponent,
|
createAbstractNodeComponent,
|
||||||
useAbstractNode,
|
useAbstractNode,
|
||||||
} from '@/lib/graph/abstractNode'
|
} from '@/lib/graph/abstractNode'
|
||||||
import { getConfigContent } from '@/lib/graph/configTypes'
|
|
||||||
import {
|
import {
|
||||||
BaseNode,
|
BaseNode,
|
||||||
BaseNodeContent,
|
BaseNodeContent,
|
||||||
@@ -18,50 +17,24 @@ import { InputHandle, OutputHandle } from '@/components/graph/NodeHandles'
|
|||||||
import FlowContext from '@/lib/graph/flowContext'
|
import FlowContext from '@/lib/graph/flowContext'
|
||||||
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
|
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
|
||||||
import { getNodeType } from '@/lib/graph/nodeRegistry'
|
import { getNodeType } from '@/lib/graph/nodeRegistry'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Bot } from 'lucide-react'
|
||||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
|
||||||
import { Bot, Play, Loader2 } from 'lucide-react'
|
|
||||||
|
|
||||||
export type AgentNodeData = {
|
export type AgentNodeData = {
|
||||||
|
/** Additional context for the agent (configuration). */
|
||||||
context?: string
|
context?: string
|
||||||
|
/** Cached output from last run; set by connected Rendering node. Not displayed on this node. */
|
||||||
outputMarkdown?: string
|
outputMarkdown?: string
|
||||||
error?: string
|
/** Signature of inputs from last successful run; set by Rendering node when it runs the agent. Used as cache. */
|
||||||
loading?: boolean
|
|
||||||
/** Signature of inputs (sourceIds + config contents) from the last successful run. Used to show on-hold when current inputs differ. */
|
|
||||||
lastRunSourceSignature?: string
|
lastRunSourceSignature?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = AbstractNodeProps<AgentNodeData>
|
type Props = AbstractNodeProps<AgentNodeData>
|
||||||
|
|
||||||
function serializeNodeForContext(nodes: { id: string; type?: string; data?: unknown }[], nodeId: string): string {
|
|
||||||
const node = nodes.find((n: { id: string }) => n.id === nodeId)
|
|
||||||
if (!node) return `${nodeId}: (not found)`
|
|
||||||
const type = node.type ?? 'unknown'
|
|
||||||
const data = node.data as Record<string, unknown> | undefined
|
|
||||||
if (type === 'config') {
|
|
||||||
const content = getConfigContent(data)
|
|
||||||
return `[config ${nodeId}]\n${content || '(empty)'}`
|
|
||||||
}
|
|
||||||
if (type === 'variable') {
|
|
||||||
const v = data?.value
|
|
||||||
return `[variable ${nodeId}]: ${v === undefined || v === null ? '' : String(v)}`
|
|
||||||
}
|
|
||||||
if (type === 'data') {
|
|
||||||
const rows = (data?.rows as Record<string, string>[] | undefined) ?? []
|
|
||||||
const columns = (data?.columns as string[] | undefined) ?? []
|
|
||||||
const preview = rows.slice(0, 20).map((r) => columns.map((c) => r[c] ?? '').join(', ')).join('\n')
|
|
||||||
return `[data ${nodeId}] ${columns.length} columns, ${rows.length} rows\n${preview}${rows.length > 20 ? '\n...' : ''}`
|
|
||||||
}
|
|
||||||
return `[${type} ${nodeId}]: ${JSON.stringify(data ?? {}).slice(0, 200)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||||
const flowContext = useContext(FlowContext)
|
const flowContext = useContext(FlowContext)
|
||||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||||
const supportsFullscreen = getNodeType('agent')?.supportsFullscreen
|
const supportsFullscreen = getNodeType('agent')?.supportsFullscreen
|
||||||
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
||||||
const { aiConnection } = usePlatform()
|
|
||||||
const [running, setRunning] = useState(false)
|
|
||||||
|
|
||||||
const dimensions =
|
const dimensions =
|
||||||
width != null && height != null && width > 0 && height > 0
|
width != null && height != null && width > 0 && height > 0
|
||||||
@@ -69,9 +42,6 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const contextText = data?.context ?? ''
|
const contextText = data?.context ?? ''
|
||||||
const outputMarkdown = data?.outputMarkdown
|
|
||||||
const error = data?.error
|
|
||||||
const loading = data?.loading ?? false
|
|
||||||
|
|
||||||
const connectedSources = useMemo(() => {
|
const connectedSources = useMemo(() => {
|
||||||
return sourceIds.map((sid) => {
|
return sourceIds.map((sid) => {
|
||||||
@@ -80,84 +50,17 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
})
|
})
|
||||||
}, [sourceIds, nodes])
|
}, [sourceIds, nodes])
|
||||||
|
|
||||||
const sourceSignature = useMemo(() => {
|
|
||||||
const configContents = sourceIds
|
|
||||||
.filter((sid) => {
|
|
||||||
const n = nodes.find((n: { id: string }) => n.id === sid)
|
|
||||||
return (n as { type?: string } | undefined)?.type === 'config'
|
|
||||||
})
|
|
||||||
.map((sid) => getConfigContent((nodes.find((n: { id: string }) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined))
|
|
||||||
return JSON.stringify({ sourceIds: sourceIds.slice().sort(), configContents })
|
|
||||||
}, [sourceIds, nodes])
|
|
||||||
|
|
||||||
const runAgent = useCallback(async () => {
|
|
||||||
const configContents = sourceIds
|
|
||||||
.filter((sid) => {
|
|
||||||
const n = nodes.find((n: { id: string }) => n.id === sid)
|
|
||||||
return (n as { type?: string } | undefined)?.type === 'config'
|
|
||||||
})
|
|
||||||
.map((sid) => getConfigContent((nodes.find((n: { id: string }) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined))
|
|
||||||
const prompt = configContents.length > 0 ? configContents.join('\n\n---\n\n') : 'No prompt provided. Please describe what you want in structured markdown.'
|
|
||||||
const contextNodes = sourceIds.map((sid) => ({ id: sid, content: serializeNodeForContext(nodes, sid) }))
|
|
||||||
|
|
||||||
updateData({ error: undefined, loading: true })
|
|
||||||
setRunning(true)
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/agent', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt,
|
|
||||||
context: contextText.trim() || undefined,
|
|
||||||
contextNodes,
|
|
||||||
connection: aiConnection,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
const json = await res.json().catch(() => ({}))
|
|
||||||
if (!res.ok) {
|
|
||||||
updateData({
|
|
||||||
loading: false,
|
|
||||||
error: (json as { error?: string }).error ?? `Request failed: ${res.status}`,
|
|
||||||
outputMarkdown: undefined,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const markdown = (json as { markdown?: string }).markdown ?? ''
|
|
||||||
updateData({
|
|
||||||
loading: false,
|
|
||||||
error: undefined,
|
|
||||||
outputMarkdown: markdown,
|
|
||||||
lastRunSourceSignature: sourceSignature,
|
|
||||||
})
|
|
||||||
} catch (err: unknown) {
|
|
||||||
updateData({
|
|
||||||
loading: false,
|
|
||||||
error: err instanceof Error ? err.message : 'Agent request failed',
|
|
||||||
outputMarkdown: undefined,
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setRunning(false)
|
|
||||||
}
|
|
||||||
}, [sourceIds, nodes, contextText, sourceSignature, updateData, aiConnection])
|
|
||||||
|
|
||||||
const onContextChange = useCallback(
|
const onContextChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => updateData({ context: e.target.value }),
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => updateData({ context: e.target.value }),
|
||||||
[updateData]
|
[updateData]
|
||||||
)
|
)
|
||||||
|
|
||||||
const pathNodeIds = flowContext?.connectionPathNodeIds
|
// Agent is configuration-only; it does not block the flow. Only the Rendering node reports
|
||||||
const triggerNodeIds = flowContext?.connectionPathTriggerNodeIds ?? []
|
// paused/updating, so the connection path runs trigger → … → agent (on-path) → rendering (paused).
|
||||||
const lastRunSourceSignature = data?.lastRunSourceSignature
|
|
||||||
const hasPendingInputs =
|
|
||||||
pathNodeIds?.has(id) &&
|
|
||||||
triggerNodeIds.length > 0 &&
|
|
||||||
!loading &&
|
|
||||||
sourceSignature !== lastRunSourceSignature
|
|
||||||
|
|
||||||
useSyncConnectionStatus(id, {
|
useSyncConnectionStatus(id, {
|
||||||
updating: running || loading,
|
updating: false,
|
||||||
error: !!error,
|
error: false,
|
||||||
paused: hasPendingInputs && !running && !loading,
|
paused: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,23 +80,6 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
<BaseNodeHeaderRow
|
<BaseNodeHeaderRow
|
||||||
icon={<Bot className="size-4" />}
|
icon={<Bot className="size-4" />}
|
||||||
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
||||||
right={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="shrink-0 h-7 nodrag nopan"
|
|
||||||
onClick={runAgent}
|
|
||||||
disabled={running || loading}
|
|
||||||
>
|
|
||||||
{running || loading ? (
|
|
||||||
<Loader2 className="size-3.5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Play className="size-3.5" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1.5">Run</span>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||||
/>
|
/>
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
@@ -224,19 +110,14 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
<p className="text-xs text-muted-foreground">
|
||||||
<p className="text-xs text-destructive">{error}</p>
|
Connect to a Renderer node and click Run there to generate output.
|
||||||
)}
|
</p>
|
||||||
{outputMarkdown != null && outputMarkdown !== '' && !error && (
|
|
||||||
<div className="text-xs text-muted-foreground border rounded p-2 max-h-24 overflow-auto">
|
|
||||||
<span className="font-medium">Output:</span> {outputMarkdown.length} chars (connect to Renderer to view)
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</BaseNodeContent>
|
</BaseNodeContent>
|
||||||
<BaseNodeFooter>
|
<BaseNodeFooter>
|
||||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="agent">
|
<NodeFooterEdgeIndicators nodeId={id} nodeType="agent">
|
||||||
{outputMarkdown != null ? `${outputMarkdown.length} chars` : error ? 'Error' : '—'}
|
—
|
||||||
</NodeFooterEdgeIndicators>
|
</NodeFooterEdgeIndicators>
|
||||||
</BaseNodeFooter>
|
</BaseNodeFooter>
|
||||||
</BaseNode>
|
</BaseNode>
|
||||||
|
|||||||
@@ -1,15 +1,131 @@
|
|||||||
/**
|
/**
|
||||||
* Agent node rendering logic: provides the agent's markdown output to the Rendering node.
|
* Agent node rendering logic: when the Rendering node runs, it calls the agent API
|
||||||
* Update mode is manual (user clicks Run on the renderer).
|
* and updates the agent node's output. Update mode is manual (user clicks Run on the renderer).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SourceRenderingLogic } from '@/lib/graph/sourceRenderingLogic'
|
import { getConfigContent } from '@/lib/graph/configTypes'
|
||||||
|
import type { ResolvedContentResult, SourceRenderingLogicContext } from '@/lib/graph/sourceRenderingLogic'
|
||||||
|
import type { AiConnection } from '@/app/kosmos/KosmosContext'
|
||||||
|
|
||||||
export const agentRenderingLogic: SourceRenderingLogic = {
|
function serializeNodeForContext(
|
||||||
defaultUpdateMode: 'manual',
|
nodes: { id: string; type?: string; data?: unknown }[],
|
||||||
getResolvedContent: async (context) => {
|
nodeId: string
|
||||||
const sourceNode = context.nodes.find((n) => n.id === context.sourceNodeId)
|
): string {
|
||||||
const outputMarkdown = (sourceNode?.data as { outputMarkdown?: string } | undefined)?.outputMarkdown ?? ''
|
const node = nodes.find((n) => n.id === nodeId)
|
||||||
|
if (!node) return `${nodeId}: (not found)`
|
||||||
|
const type = node.type ?? 'unknown'
|
||||||
|
const data = node.data as Record<string, unknown> | undefined
|
||||||
|
if (type === 'config') {
|
||||||
|
const content = getConfigContent(data)
|
||||||
|
return `[config ${nodeId}]\n${content || '(empty)'}`
|
||||||
|
}
|
||||||
|
if (type === 'variable') {
|
||||||
|
const v = data?.value
|
||||||
|
return `[variable ${nodeId}]: ${v === undefined || v === null ? '' : String(v)}`
|
||||||
|
}
|
||||||
|
if (type === 'data') {
|
||||||
|
const rows = (data?.rows as Record<string, string>[] | undefined) ?? []
|
||||||
|
const columns = (data?.columns as string[] | undefined) ?? []
|
||||||
|
const preview = rows
|
||||||
|
.slice(0, 20)
|
||||||
|
.map((r) => columns.map((c) => r[c] ?? '').join(', '))
|
||||||
|
.join('\n')
|
||||||
|
return `[data ${nodeId}] ${columns.length} columns, ${rows.length} rows\n${preview}${rows.length > 20 ? '\n...' : ''}`
|
||||||
|
}
|
||||||
|
return `[${type} ${nodeId}]: ${JSON.stringify(data ?? {}).slice(0, 200)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSourceSignature(
|
||||||
|
nodes: { id: string; type?: string; data?: unknown }[],
|
||||||
|
sourceIds: string[]
|
||||||
|
): string {
|
||||||
|
const configContents = sourceIds
|
||||||
|
.filter((sid) => {
|
||||||
|
const n = nodes.find((nn) => nn.id === sid)
|
||||||
|
return (n as { type?: string } | undefined)?.type === 'config'
|
||||||
|
})
|
||||||
|
.map((sid) =>
|
||||||
|
getConfigContent(
|
||||||
|
(nodes.find((n) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return JSON.stringify({ sourceIds: sourceIds.slice().sort(), configContents })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agentRenderingLogic = {
|
||||||
|
defaultUpdateMode: 'manual' as const,
|
||||||
|
getResolvedContent: async (context: SourceRenderingLogicContext): Promise<ResolvedContentResult> => {
|
||||||
|
const { nodes, sourceNodeId, setNodes, aiConnection } = context
|
||||||
|
const sourceNode = nodes.find((n) => n.id === sourceNodeId)
|
||||||
|
const agentData = sourceNode?.data as { context?: string; outputMarkdown?: string } | undefined
|
||||||
|
const contextText = agentData?.context ?? ''
|
||||||
|
const sourceIds = (context.edges
|
||||||
|
.filter((e) => e.target === sourceNodeId)
|
||||||
|
.map((e) => e.source)) as string[]
|
||||||
|
|
||||||
|
type NodeWithData = { id: string; type?: string; data?: unknown }
|
||||||
|
const setAgentData = (patch: Record<string, unknown>) => {
|
||||||
|
if (!setNodes) return
|
||||||
|
setNodes((prev: unknown[]) =>
|
||||||
|
(prev as NodeWithData[]).map((n) =>
|
||||||
|
n.id === sourceNodeId ? { ...n, data: { ...(n.data as object), ...patch } } : n
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setNodes && aiConnection) {
|
||||||
|
const connection = aiConnection as AiConnection
|
||||||
|
const configContents = sourceIds
|
||||||
|
.filter((sid) => {
|
||||||
|
const n = nodes.find((nn) => nn.id === sid)
|
||||||
|
return (n as { type?: string } | undefined)?.type === 'config'
|
||||||
|
})
|
||||||
|
.map((sid) =>
|
||||||
|
getConfigContent(
|
||||||
|
(nodes.find((n) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const prompt =
|
||||||
|
configContents.length > 0
|
||||||
|
? configContents.join('\n\n---\n\n')
|
||||||
|
: 'No prompt provided. Please describe what you want in structured markdown.'
|
||||||
|
const contextNodes = sourceIds.map((sid) => ({
|
||||||
|
id: sid,
|
||||||
|
content: serializeNodeForContext(nodes, sid),
|
||||||
|
}))
|
||||||
|
const sourceSignature = buildSourceSignature(nodes, sourceIds)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/agent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt,
|
||||||
|
context: contextText.trim() || undefined,
|
||||||
|
contextNodes,
|
||||||
|
connection,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const json = await res.json().catch(() => ({}))
|
||||||
|
if (!res.ok) {
|
||||||
|
setAgentData({ outputMarkdown: undefined })
|
||||||
|
const msg = (json as { error?: string }).error ?? `Request failed: ${res.status}`
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
const markdown = (json as { markdown?: string }).markdown ?? ''
|
||||||
|
setAgentData({
|
||||||
|
outputMarkdown: markdown,
|
||||||
|
lastRunSourceSignature: sourceSignature,
|
||||||
|
})
|
||||||
|
return { resolved: markdown, outputTypeId: 'markdown' }
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setAgentData({ outputMarkdown: undefined })
|
||||||
|
throw err instanceof Error ? err : new Error('Agent request failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputMarkdown =
|
||||||
|
(sourceNode?.data as { outputMarkdown?: string } | undefined)?.outputMarkdown ?? ''
|
||||||
return { resolved: outputMarkdown, outputTypeId: 'markdown' }
|
return { resolved: outputMarkdown, outputTypeId: 'markdown' }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useAbstractNode,
|
useAbstractNode,
|
||||||
} from '@/lib/graph/abstractNode'
|
} from '@/lib/graph/abstractNode'
|
||||||
import { getConfigContent, getConfigType, getConfigTypeId } from '@/lib/graph/configTypes'
|
import { getConfigContent, getConfigType, getConfigTypeId } from '@/lib/graph/configTypes'
|
||||||
import { getSourceRenderingLogic } from '@/lib/graph/sourceRenderingLogic'
|
import { getSourceRenderingLogic, type SourceRenderingLogicContext } from '@/lib/graph/sourceRenderingLogic'
|
||||||
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
|
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
|
||||||
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
||||||
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useTheme } from '@/lib/themeContext'
|
import { useTheme } from '@/lib/themeContext'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -79,6 +80,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const lastManualRunTriggerRef = useRef<number>(0)
|
const lastManualRunTriggerRef = useRef<number>(0)
|
||||||
const { nodes, edges, setNodes, setEdges, sourceIds, updateData } = useAbstractNode<RenderingNodeData>(id, data ?? {})
|
const { nodes, edges, setNodes, setEdges, sourceIds, updateData } = useAbstractNode<RenderingNodeData>(id, data ?? {})
|
||||||
|
const { aiConnection } = usePlatform()
|
||||||
const viewportWidth = data?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH
|
const viewportWidth = data?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH
|
||||||
const viewportHeight = data?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT
|
const viewportHeight = data?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT
|
||||||
|
|
||||||
@@ -265,7 +267,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
setResolvedContent(null)
|
setResolvedContent(null)
|
||||||
setError({
|
setError({
|
||||||
kind: 'no-content',
|
kind: 'no-content',
|
||||||
message: isAgentSource ? 'Run the Agent node to generate output, then click Run here.' : 'Click Run to render.',
|
message: 'Click Run to render.',
|
||||||
})
|
})
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -289,11 +291,13 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const context = {
|
const context = {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
sourceNodeId: srcId,
|
sourceNodeId: srcId!,
|
||||||
renderNodeId: id,
|
renderNodeId: id,
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
}
|
setNodes: setNodes ?? undefined,
|
||||||
|
aiConnection,
|
||||||
|
} as SourceRenderingLogicContext
|
||||||
const { resolved, outputTypeId } = await logic.getResolvedContent(context)
|
const { resolved, outputTypeId } = await logic.getResolvedContent(context)
|
||||||
if (cancelled || thisRunId !== runIdRef.current) return
|
if (cancelled || thisRunId !== runIdRef.current) return
|
||||||
setResolvedContent(resolved)
|
setResolvedContent(resolved)
|
||||||
@@ -351,7 +355,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [id, srcId, srcNode?.type, effectiveUpdateMode, runTrigger, sourceContent, sourceSignature, configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature, viewportWidth, viewportHeight, retryCount, nodes, edges, updateData])
|
}, [id, srcId, srcNode?.type, effectiveUpdateMode, runTrigger, sourceContent, sourceSignature, configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature, viewportWidth, viewportHeight, retryCount, nodes, edges, updateData, setNodes, aiConnection])
|
||||||
|
|
||||||
|
|
||||||
const dimensions =
|
const dimensions =
|
||||||
@@ -579,9 +583,9 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
>
|
>
|
||||||
Raw
|
Raw
|
||||||
</MenubarCheckboxItem>
|
</MenubarCheckboxItem>
|
||||||
<MenubarSeparator />
|
|
||||||
{outputType === 'image' && (
|
{outputType === 'image' && (
|
||||||
<MenubarSub>
|
<MenubarSub>
|
||||||
|
<MenubarSeparator />
|
||||||
<MenubarSubTrigger className="text-xs">Viewport size</MenubarSubTrigger>
|
<MenubarSubTrigger className="text-xs">Viewport size</MenubarSubTrigger>
|
||||||
<MenubarSubContent className="min-w-[12rem] p-2">
|
<MenubarSubContent className="min-w-[12rem] p-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export type FlowContextValue = {
|
|||||||
connectionPathPausedSegmentNodeIds: Set<string>
|
connectionPathPausedSegmentNodeIds: Set<string>
|
||||||
/** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */
|
/** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */
|
||||||
connectionPathActiveSegmentNodeIds: Set<string>
|
connectionPathActiveSegmentNodeIds: Set<string>
|
||||||
/** Archon-type nodes that are on hold (e.g. Agent waiting for Run). */
|
/** Nodes that are on hold (e.g. Renderer in manual mode waiting for Run). */
|
||||||
connectionPathPausedNodeIds: string[]
|
connectionPathPausedNodeIds: string[]
|
||||||
/** Add this node as paused (on hold); remove when user continues. */
|
/** Add this node as paused (on hold); remove when user continues. */
|
||||||
addConnectionPathPausedNode: (nodeId: string) => void
|
addConnectionPathPausedNode: (nodeId: string) => void
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function getPathNodeIds(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path nodes from triggers up to and including the first paused node (Archon on hold).
|
* Path nodes from triggers up to and including the first paused node (e.g. Renderer waiting for Run).
|
||||||
* Used to color those edges yellow; rest of path stays blue.
|
* Used to color those edges yellow; rest of path stays blue.
|
||||||
*/
|
*/
|
||||||
export function getPausedSegmentNodeIds(
|
export function getPausedSegmentNodeIds(
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
* - **Trigger** – Node's output changed (e.g. config content, variable value). Reported
|
* - **Trigger** – Node's output changed (e.g. config content, variable value). Reported
|
||||||
* automatically when the node calls `updateData()` from useAbstractNode. Downstream
|
* automatically when the node calls `updateData()` from useAbstractNode. Downstream
|
||||||
* path is computed from triggers + updating nodes.
|
* path is computed from triggers + updating nodes.
|
||||||
* - **Updating** – Node is doing async work (e.g. agent running, renderer loading).
|
* - **Updating** – Node is doing async work (e.g. renderer loading, agent run triggered by renderer).
|
||||||
* Report `updating: true` at start, `updating: false` when done. Incoming/outgoing
|
* Report `updating: true` at start, `updating: false` when done. Incoming/outgoing
|
||||||
* edges on the path show "updating" (blue).
|
* edges on the path show "updating" (blue).
|
||||||
* - **Paused** – Node is on hold (e.g. agent waiting for Run after inputs changed).
|
* - **Paused** – Node is on hold (e.g. renderer in manual mode waiting for Run after inputs changed).
|
||||||
* Report `paused: true` when waiting, `paused: false` when not. Edges in the paused
|
* Report `paused: true` when waiting, `paused: false` when not. Edges in the paused
|
||||||
* segment show "paused" (yellow).
|
* segment show "paused" (yellow).
|
||||||
* - **Error** – Node has an error to show. Report `error: true` when error is set,
|
* - **Error** – Node has an error to show. Report `error: true` when error is set,
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export type SourceRenderingLogicContext = {
|
|||||||
renderNodeId: string
|
renderNodeId: string
|
||||||
viewportWidth?: number
|
viewportWidth?: number
|
||||||
viewportHeight?: number
|
viewportHeight?: number
|
||||||
|
/** Optional: allows source logic to update other nodes (e.g. agent run updates agent node). */
|
||||||
|
setNodes?: (updater: (nodes: { id: string; type?: string; data?: unknown }[]) => { id: string; type?: string; data?: unknown }[]) => void
|
||||||
|
/** Optional: AI connection for agent source (used when Rendering node runs the agent). */
|
||||||
|
aiConnection?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user