feat: renderin imrpovements
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user