feat: ant path rendering on cahnges
This commit is contained in:
@@ -9,9 +9,11 @@ import { getConnectionLabelForTarget } from '../../lib/nodeRegistry'
|
||||
|
||||
const EDGE_STROKE_WIDTH = 2
|
||||
const DOT_MARKER_R = 1.5
|
||||
const EMPTY_PATH_NODE_IDS = new Set<string>()
|
||||
|
||||
export function AnimatedEdge({
|
||||
id,
|
||||
source,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
@@ -25,6 +27,7 @@ export function AnimatedEdge({
|
||||
}: EdgeProps) {
|
||||
const ctx = useContext(FlowContext)
|
||||
const nodes = ctx?.nodes ?? []
|
||||
const pathNodeIds = ctx?.connectionPathNodeIds ?? EMPTY_PATH_NODE_IDS
|
||||
const targetNode = useMemo(() => nodes.find((n: any) => n.id === target), [nodes, target])
|
||||
const derivedLabel = useMemo(
|
||||
() => (targetNode?.type != null ? getConnectionLabelForTarget(targetNode.type) : undefined),
|
||||
@@ -32,6 +35,8 @@ export function AnimatedEdge({
|
||||
)
|
||||
const label = labelProp ?? derivedLabel
|
||||
|
||||
const isOnUpdatingPath = pathNodeIds.has(source) && pathNodeIds.has(target)
|
||||
|
||||
const [edgePath, edgeLabelX, edgeLabelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
@@ -88,7 +93,7 @@ export function AnimatedEdge({
|
||||
strokeWidth: EDGE_STROKE_WIDTH,
|
||||
...style,
|
||||
}}
|
||||
className="animated-edge-path"
|
||||
className={`animated-edge-path${isOnUpdatingPath ? ' animated-edge-path--updating' : ''}`}
|
||||
interactionWidth={interactionWidth}
|
||||
/>
|
||||
{label != null && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { NodeResizer } from "@xyflow/react";
|
||||
|
||||
import { useConnectionPathRole } from "@/lib/flowContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Default min size for resizable nodes (used by NodeResizer). */
|
||||
@@ -34,6 +35,7 @@ export function BaseNode({
|
||||
resizeConstraints,
|
||||
...props
|
||||
}: BaseNodeProps) {
|
||||
const connectionPathRole = useConnectionPathRole(nodeId);
|
||||
const hasSize =
|
||||
dimensions &&
|
||||
dimensions.width > 0 &&
|
||||
@@ -62,9 +64,13 @@ export function BaseNode({
|
||||
"bg-card text-card-foreground relative rounded-md border transition-[border-color,box-shadow] duration-200",
|
||||
"hover:ring-1",
|
||||
selected && "border-primary/50 shadow-[0_0_0_2px_hsl(var(--primary)_/_0.15)] dark:border-primary/35 dark:shadow-[0_0_0_2px_hsl(var(--primary)_/_0.1)]",
|
||||
connectionPathRole === "trigger" && "connection-path-trigger",
|
||||
connectionPathRole === "updating" && "connection-path-updating",
|
||||
connectionPathRole === "on-path" && "connection-path-on-path",
|
||||
className,
|
||||
)}
|
||||
data-selected={selected}
|
||||
data-path-role={connectionPathRole ?? undefined}
|
||||
style={appliedStyle}
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
|
||||
@@ -55,6 +55,8 @@ function serializeNodeForContext(nodes: { id: string; type?: string; data?: unkn
|
||||
function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const flowContext = useContext(FlowContext)
|
||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||
const startConnectionPathUpdate = flowContext?.startConnectionPathUpdate
|
||||
const endConnectionPathUpdate = flowContext?.endConnectionPathUpdate
|
||||
const supportsFullscreen = getNodeType('agent')?.supportsFullscreen
|
||||
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
||||
const { aiConnection } = usePlatform()
|
||||
@@ -88,6 +90,7 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const contextNodes = sourceIds.map((sid) => ({ id: sid, content: serializeNodeForContext(nodes, sid) }))
|
||||
|
||||
updateData({ error: undefined, loading: true })
|
||||
startConnectionPathUpdate?.(id)
|
||||
setRunning(true)
|
||||
try {
|
||||
const res = await fetch('/api/agent', {
|
||||
@@ -107,16 +110,19 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
error: (json as { error?: string }).error ?? `Request failed: ${res.status}`,
|
||||
outputMarkdown: undefined,
|
||||
})
|
||||
endConnectionPathUpdate?.(id)
|
||||
return
|
||||
}
|
||||
const markdown = (json as { markdown?: string }).markdown ?? ''
|
||||
updateData({ loading: false, error: undefined, outputMarkdown: markdown })
|
||||
endConnectionPathUpdate?.(id)
|
||||
} catch (err: unknown) {
|
||||
updateData({
|
||||
loading: false,
|
||||
error: err instanceof Error ? err.message : 'Agent request failed',
|
||||
outputMarkdown: undefined,
|
||||
})
|
||||
endConnectionPathUpdate?.(id)
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
@@ -131,6 +137,7 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
<BaseNode
|
||||
className="min-w-[360px] min-h-[320px]"
|
||||
dimensions={dimensions}
|
||||
nodeId={id}
|
||||
selected={selected}
|
||||
handles={
|
||||
<>
|
||||
|
||||
@@ -50,6 +50,8 @@ type ViewMode = 'preview' | 'raw'
|
||||
function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const flowContext = useContext(FlowContext)
|
||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||
const startConnectionPathUpdate = flowContext?.startConnectionPathUpdate
|
||||
const endConnectionPathUpdate = flowContext?.endConnectionPathUpdate
|
||||
const supportsFullscreen = getNodeType('render')?.supportsFullscreen
|
||||
const [renderedContent, setRenderedContent] = useState<string | null>(null)
|
||||
const [resolvedContent, setResolvedContent] = useState<string | null>(null)
|
||||
@@ -61,6 +63,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const runIdRef = useRef(0)
|
||||
const loadingStartedAtRef = useRef<number | null>(null)
|
||||
const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const isInitialRunRef = useRef(true)
|
||||
const { nodes, edges, setNodes, setEdges, sourceIds, updateData } = useAbstractNode<RenderingNodeData>(id, data ?? {})
|
||||
const viewportWidth = data?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH
|
||||
const viewportHeight = data?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT
|
||||
@@ -195,6 +198,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
message: isAgentSource ? 'Run the Agent node to generate output.' : 'No content on connected configuration node',
|
||||
})
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
return
|
||||
}
|
||||
if (incomingIds.length === 0) {
|
||||
@@ -202,6 +207,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
setResolvedContent(null)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -212,6 +219,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const run = async () => {
|
||||
loadingStartedAtRef.current = Date.now()
|
||||
setLoading(true)
|
||||
if (!isInitialRunRef.current) startConnectionPathUpdate?.(id)
|
||||
setError(null)
|
||||
try {
|
||||
if (srcNode?.type === 'agent') {
|
||||
@@ -223,6 +231,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
setRenderedContent(html)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
return
|
||||
}
|
||||
const configIdsUsed = new Set<string>()
|
||||
@@ -497,6 +507,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
setResolvedContent(null)
|
||||
setError({ kind: 'render', message: `Nunjucks: ${nunjucksErr.message}` })
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -524,10 +536,16 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
if (remaining > 0) {
|
||||
minLoadingTimeoutRef.current = setTimeout(() => {
|
||||
minLoadingTimeoutRef.current = null
|
||||
if (!cancelled && thisRunId === runIdRef.current) setLoading(false)
|
||||
if (!cancelled && thisRunId === runIdRef.current) {
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
}
|
||||
}, remaining)
|
||||
} else {
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,6 +555,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
setRenderedContent(null)
|
||||
setError({ kind: 'render', message: err?.message ?? 'Render error' })
|
||||
setLoading(false)
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,6 +569,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
clearTimeout(minLoadingTimeoutRef.current)
|
||||
minLoadingTimeoutRef.current = null
|
||||
}
|
||||
if (!isInitialRunRef.current) endConnectionPathUpdate?.(id)
|
||||
isInitialRunRef.current = false
|
||||
}
|
||||
// Only re-run when inputs that affect the resolved output change (signatures + source). Debounced to avoid excessive re-renders while typing. retryCount triggers re-run on Retry.
|
||||
}, [id, sourceContent, configTypeId, configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature, viewportWidth, viewportHeight, retryCount, isAgentSource])
|
||||
|
||||
@@ -84,7 +84,7 @@ function VariableNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
)
|
||||
|
||||
return (
|
||||
<BaseNode className="min-w-56 min-h-[180px]" dimensions={dimensions} selected={selected} handles={<OutputHandle id="out" />}>
|
||||
<BaseNode className="min-w-56 min-h-[180px]" dimensions={dimensions} nodeId={id} selected={selected} handles={<OutputHandle id="out" />}>
|
||||
<BaseNodeHeaderRow icon={<Variable className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
|
||||
Reference in New Issue
Block a user