feat: renderin imrpovements

This commit is contained in:
2026-03-12 14:48:19 +01:00
parent f43e2df019
commit 909359ea53
5 changed files with 379 additions and 84 deletions

View File

@@ -18,10 +18,13 @@ import FlowContext from '@/lib/graph/flowContext'
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
import { getNodeType } from '@/lib/graph/nodeRegistry'
import { Bot } from 'lucide-react'
import { Checkbox } from '@/components/ui/checkbox'
export type AgentNodeData = {
/** Additional context for the agent (configuration). */
context?: string
/** When true, agent returns reasoning in a "## Reasoning" section and output in "## Output". */
reasoning?: boolean
/** Cached output from last run; set by connected Rendering node. Not displayed on this node. */
outputMarkdown?: string
/** Signature of inputs from last successful run; set by Rendering node when it runs the agent. Used as cache. */
@@ -42,6 +45,12 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
: undefined
const contextText = data?.context ?? ''
const reasoningEnabled = data?.reasoning ?? false
const onReasoningChange = useCallback(
(checked: boolean) => updateData({ reasoning: checked }),
[updateData]
)
const connectedSources = useMemo(() => {
return sourceIds.map((sid) => {
@@ -87,6 +96,21 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
<NodeMenubar nodeId={id} nodeType="agent" />
</div>
<div className="flex flex-col gap-3 p-2 min-h-0 flex-1">
<div className="flex flex-col gap-1.5">
<label htmlFor={`agent-reasoning-${id}`} className="flex items-center gap-2 text-xs font-medium text-foreground cursor-pointer">
<Checkbox
id={`agent-reasoning-${id}`}
checked={reasoningEnabled}
onCheckedChange={(c) => onReasoningChange(c === true)}
className="nodrag nopan"
aria-label="Enable reasoning"
/>
Enable reasoning
</label>
<p className="text-xs text-muted-foreground">
When enabled, the model will output a Reasoning section and an Output section; the renderer shows them separately.
</p>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-foreground">Context</label>
<textarea

View File

@@ -52,13 +52,29 @@ function buildSourceSignature(
return JSON.stringify({ sourceIds: sourceIds.slice().sort(), configContents })
}
/** Parse agent response into reasoning and output when format is "## Reasoning" / "## Output". */
function parseReasoningAndOutput(fullMarkdown: string): { reasoning?: string; output: string } {
const normalized = fullMarkdown.trim()
const outputMatch = normalized.match(/\s*##\s*Output\s*/i)
if (!outputMatch) return { output: fullMarkdown }
const idx = outputMatch.index! + outputMatch[0].length
const output = normalized.slice(idx).trim()
const beforeOutput = normalized.slice(0, outputMatch.index).trim()
const reasoningMatch = beforeOutput.match(/\s*##\s*Reasoning\s*/i)
const reasoning = reasoningMatch
? beforeOutput.slice(reasoningMatch.index! + reasoningMatch[0].length).trim()
: beforeOutput
return { reasoning: reasoning || undefined, output: output || fullMarkdown }
}
export const agentRenderingLogic = {
defaultUpdateMode: 'manual' as const,
getResolvedContent: async (context: SourceRenderingLogicContext): Promise<ResolvedContentResult> => {
const { nodes, sourceNodeId, setNodes, aiConnection } = context
const { nodes, sourceNodeId, setNodes, aiConnection, onStreamingStart, onStreamingChunk } = context
const sourceNode = nodes.find((n) => n.id === sourceNodeId)
const agentData = sourceNode?.data as { context?: string; outputMarkdown?: string } | undefined
const agentData = sourceNode?.data as { context?: string; outputMarkdown?: string; reasoning?: boolean } | undefined
const contextText = agentData?.context ?? ''
const reasoningEnabled = Boolean(agentData?.reasoning)
const sourceIds = (context.edges
.filter((e) => e.target === sourceNodeId)
.map((e) => e.source)) as string[]
@@ -94,8 +110,61 @@ export const agentRenderingLogic = {
content: serializeNodeForContext(nodes, sid),
}))
const sourceSignature = buildSourceSignature(nodes, sourceIds)
const useStream = Boolean(onStreamingChunk)
try {
if (useStream) {
onStreamingStart?.()
const res = await fetch('/api/agent/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
context: contextText.trim() || undefined,
contextNodes,
connection,
reasoning: reasoningEnabled,
}),
})
if (!res.ok) {
const text = await res.text().catch(() => '')
let msg = `Request failed: ${res.status}`
try {
const json = text ? JSON.parse(text) : {}
if (typeof (json as { error?: string }).error === 'string') msg = (json as { error: string }).error
} catch {
if (text) msg = text.slice(0, 200)
}
setAgentData({ outputMarkdown: undefined })
throw new Error(msg)
}
const reader = res.body?.getReader()
const decoder = new TextDecoder()
let markdown = ''
try {
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
markdown += chunk
onStreamingChunk?.(chunk)
}
}
} finally {
reader?.cancel()
}
setAgentData({
outputMarkdown: markdown,
lastRunSourceSignature: sourceSignature,
})
if (reasoningEnabled) {
const { reasoning: reasoningText, output } = parseReasoningAndOutput(markdown)
return { resolved: output, outputTypeId: 'markdown' as const, reasoning: reasoningText }
}
return { resolved: markdown, outputTypeId: 'markdown' }
}
const res = await fetch('/api/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -104,6 +173,7 @@ export const agentRenderingLogic = {
context: contextText.trim() || undefined,
contextNodes,
connection,
reasoning: reasoningEnabled,
}),
})
const json = await res.json().catch(() => ({}))
@@ -117,6 +187,10 @@ export const agentRenderingLogic = {
outputMarkdown: markdown,
lastRunSourceSignature: sourceSignature,
})
if (reasoningEnabled) {
const { reasoning: reasoningText, output } = parseReasoningAndOutput(markdown)
return { resolved: output, outputTypeId: 'markdown' as const, reasoning: reasoningText }
}
return { resolved: markdown, outputTypeId: 'markdown' }
} catch (err: unknown) {
setAgentData({ outputMarkdown: undefined })
@@ -126,6 +200,10 @@ export const agentRenderingLogic = {
const outputMarkdown =
(sourceNode?.data as { outputMarkdown?: string } | undefined)?.outputMarkdown ?? ''
if (reasoningEnabled && outputMarkdown) {
const { reasoning: reasoningText, output } = parseReasoningAndOutput(outputMarkdown)
return { resolved: output, outputTypeId: 'markdown' as const, reasoning: reasoningText }
}
return { resolved: outputMarkdown, outputTypeId: 'markdown' }
},
}