refactor: markdown split form agent
This commit is contained in:
29
frontend/package-lock.json
generated
29
frontend/package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"markdown-to-jsx": "^9.7.9",
|
||||
"marked": "^17.0.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nunjucks": "^3.2.4",
|
||||
@@ -5949,6 +5950,34 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-to-jsx": {
|
||||
"version": "9.7.9",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.7.9.tgz",
|
||||
"integrity": "sha512-0gO/MOmtM9N2m2F4SUwMqskQe4qih9vaA8M3MEPu51UqjX5MMb/IYSIomLVIyHjmkWql20GuXiyYsPzt6c54FA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.0.0",
|
||||
"solid-js": ">=1.0.0",
|
||||
"vue": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"solid-js": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.4",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"markdown-to-jsx": "^9.7.9",
|
||||
"marked": "^17.0.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nunjucks": "^3.2.4",
|
||||
|
||||
@@ -39,7 +39,13 @@ import {
|
||||
useRenderingNodeState,
|
||||
type RenderingNodeData,
|
||||
} from './useRenderingNodeState'
|
||||
import { ImageOutputView, MarkdownOutputView, RawOutputView } from './views'
|
||||
import {
|
||||
ImageOutputView,
|
||||
MarkdownJsxView,
|
||||
RawOutputView,
|
||||
StaticMarkdownHtmlView,
|
||||
StreamingMarkdownView,
|
||||
} from './views'
|
||||
|
||||
export type { RenderingNodeData }
|
||||
|
||||
@@ -114,10 +120,19 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <MarkdownOutputView state={state} streaming={false} />
|
||||
const isConfigMarkdown =
|
||||
state.sourceNodeType === 'config' && state.rawLanguage === 'markdown'
|
||||
if (isConfigMarkdown) {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
<MarkdownJsxView markdown={state.resolvedContent ?? ''} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <StaticMarkdownHtmlView state={state} />
|
||||
}
|
||||
if (state.loading && state.streamingMarkdown !== null) {
|
||||
return <MarkdownOutputView state={state} streaming={true} />
|
||||
return <StreamingMarkdownView state={state} />
|
||||
}
|
||||
if (state.loading) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Renders markdown as React (JSX) using markdown-to-jsx.
|
||||
* Used for config node markdown preview to avoid dangerouslySetInnerHTML and support
|
||||
* safe, composable rendering. See https://markdown-to-jsx.quantizor.dev/
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import Markdown from 'markdown-to-jsx/react'
|
||||
|
||||
const MARKDOWN_PREVIEW_CLASS =
|
||||
'rendering-markdown min-h-0 flex-1 w-full overflow-auto p-3 text-sm [&_h1]:text-xl [&_h2]:text-lg [&_h3]:text-base [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-2 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded text-muted-foreground'
|
||||
|
||||
export type MarkdownJsxViewProps = {
|
||||
/** Raw markdown string (e.g. resolved content from config node). */
|
||||
markdown: string
|
||||
/** Optional wrapper className (defaults to MARKDOWN_PREVIEW_CLASS). */
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MarkdownJsxView({ markdown, className = MARKDOWN_PREVIEW_CLASS }: MarkdownJsxViewProps) {
|
||||
if (!markdown.trim()) {
|
||||
return <div className={className} />
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<Markdown>{markdown}</Markdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import type { RenderingNodeState } from '../useRenderingNodeState'
|
||||
|
||||
const MARKDOWN_CLASS = 'rendering-markdown p-3 text-sm [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-1.5 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded text-muted-foreground'
|
||||
const MARKDOWN_MAIN_CLASS = 'rendering-markdown min-h-0 flex-1 w-full overflow-auto p-3 text-sm [&_h1]:text-xl [&_h2]:text-lg [&_h3]:text-base [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-2 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded'
|
||||
|
||||
export type MarkdownOutputViewProps = {
|
||||
/** Final content: reasoning + think + main */
|
||||
state: RenderingNodeState
|
||||
/** When true, show streaming content (streamingThinkSplit, streamingPreviewHtml, streamingMarkdown) instead of final */
|
||||
streaming: boolean
|
||||
}
|
||||
|
||||
export function MarkdownOutputView({ state, streaming }: MarkdownOutputViewProps) {
|
||||
if (streaming) {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
{state.streamingThinkSplit.think ? (
|
||||
<Collapsible defaultOpen={false} className="group shrink-0 border-b border-border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-left text-xs font-medium text-muted-foreground hover:bg-muted/50 nodrag nopan">
|
||||
Thinking
|
||||
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{state.streamingPreviewHtml ? (
|
||||
<div className={MARKDOWN_CLASS + ' max-h-48 overflow-auto'} dangerouslySetInnerHTML={{ __html: state.streamingThinkSplit.think }} />
|
||||
) : (
|
||||
<pre className="rendering-markdown max-h-48 overflow-auto whitespace-pre-wrap break-words p-3 text-sm font-sans text-muted-foreground">
|
||||
{state.streamingThinkSplit.think}
|
||||
</pre>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
{state.streamingPreviewHtml ? (
|
||||
<div className={MARKDOWN_MAIN_CLASS} dangerouslySetInnerHTML={{ __html: state.streamingThinkSplit.main || state.streamingPreviewHtml }} />
|
||||
) : (
|
||||
<pre className="min-h-0 flex-1 w-full overflow-auto whitespace-pre-wrap break-words p-3 text-sm font-sans">
|
||||
{state.streamingThinkSplit.main || state.streamingMarkdown}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
{state.reasoningHtml ? (
|
||||
<Collapsible defaultOpen={false} className="group shrink-0 border-b border-border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-left text-xs font-medium text-muted-foreground hover:bg-muted/50 nodrag nopan">
|
||||
Reasoning
|
||||
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className={MARKDOWN_CLASS + ' max-h-48 overflow-auto'} dangerouslySetInnerHTML={{ __html: state.reasoningHtml }} />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
{state.renderedThinkSplit.think ? (
|
||||
<Collapsible defaultOpen={false} className="group shrink-0 border-b border-border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-left text-xs font-medium text-muted-foreground hover:bg-muted/50 nodrag nopan">
|
||||
Thinking
|
||||
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className={MARKDOWN_CLASS + ' max-h-48 overflow-auto'} dangerouslySetInnerHTML={{ __html: state.renderedThinkSplit.think }} />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
<div className={MARKDOWN_MAIN_CLASS} dangerouslySetInnerHTML={{ __html: state.renderedThinkSplit.main }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Renders final (non-streaming) markdown as HTML: reasoning + think collapsibles + main.
|
||||
* Used for agent final output and any other source that uses the marked → HTML pipeline.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import type { RenderingNodeState } from '../useRenderingNodeState'
|
||||
|
||||
const MARKDOWN_CLASS =
|
||||
'rendering-markdown p-3 text-sm [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-1.5 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded text-muted-foreground'
|
||||
const MARKDOWN_MAIN_CLASS =
|
||||
'rendering-markdown min-h-0 flex-1 w-full overflow-auto p-3 text-sm [&_h1]:text-xl [&_h2]:text-lg [&_h3]:text-base [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-2 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded'
|
||||
|
||||
export type StaticMarkdownHtmlViewProps = {
|
||||
state: RenderingNodeState
|
||||
}
|
||||
|
||||
export function StaticMarkdownHtmlView({ state }: StaticMarkdownHtmlViewProps) {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
{state.reasoningHtml ? (
|
||||
<Collapsible defaultOpen={false} className="group shrink-0 border-b border-border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-left text-xs font-medium text-muted-foreground hover:bg-muted/50 nodrag nopan">
|
||||
Reasoning
|
||||
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div
|
||||
className={MARKDOWN_CLASS + ' max-h-48 overflow-auto'}
|
||||
dangerouslySetInnerHTML={{ __html: state.reasoningHtml }}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
{state.renderedThinkSplit.think ? (
|
||||
<Collapsible defaultOpen={false} className="group shrink-0 border-b border-border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-left text-xs font-medium text-muted-foreground hover:bg-muted/50 nodrag nopan">
|
||||
Thinking
|
||||
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div
|
||||
className={MARKDOWN_CLASS + ' max-h-48 overflow-auto'}
|
||||
dangerouslySetInnerHTML={{ __html: state.renderedThinkSplit.think }}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
<div
|
||||
className={MARKDOWN_MAIN_CLASS}
|
||||
dangerouslySetInnerHTML={{ __html: state.renderedThinkSplit.main }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Renders streaming markdown from the agent node.
|
||||
* Uses marked → HTML for live preview during stream; think/reasoning in a collapsible.
|
||||
* Kept separate from config-node markdown preview (MarkdownJsxView) so streaming (agent)
|
||||
* and static markdown (config) are distinct code paths.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import type { RenderingNodeState } from '../useRenderingNodeState'
|
||||
|
||||
const MARKDOWN_CLASS =
|
||||
'rendering-markdown p-3 text-sm [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-1.5 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded text-muted-foreground'
|
||||
const MARKDOWN_MAIN_CLASS =
|
||||
'rendering-markdown min-h-0 flex-1 w-full overflow-auto p-3 text-sm [&_h1]:text-xl [&_h2]:text-lg [&_h3]:text-base [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-2 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded'
|
||||
|
||||
export type StreamingMarkdownViewProps = {
|
||||
state: RenderingNodeState
|
||||
}
|
||||
|
||||
export function StreamingMarkdownView({ state }: StreamingMarkdownViewProps) {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
{state.streamingThinkSplit.think ? (
|
||||
<Collapsible defaultOpen={false} className="group shrink-0 border-b border-border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-left text-xs font-medium text-muted-foreground hover:bg-muted/50 nodrag nopan">
|
||||
Thinking
|
||||
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{state.streamingPreviewHtml ? (
|
||||
<div
|
||||
className={MARKDOWN_CLASS + ' max-h-48 overflow-auto'}
|
||||
dangerouslySetInnerHTML={{ __html: state.streamingThinkSplit.think }}
|
||||
/>
|
||||
) : (
|
||||
<pre className="rendering-markdown max-h-48 overflow-auto whitespace-pre-wrap break-words p-3 text-sm font-sans text-muted-foreground">
|
||||
{state.streamingThinkSplit.think}
|
||||
</pre>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
{state.streamingPreviewHtml ? (
|
||||
<div
|
||||
className={MARKDOWN_MAIN_CLASS}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: state.streamingThinkSplit.main || state.streamingPreviewHtml,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="min-h-0 flex-1 w-full overflow-auto whitespace-pre-wrap break-words p-3 text-sm font-sans">
|
||||
{state.streamingThinkSplit.main || state.streamingMarkdown}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
/**
|
||||
* Output view components: "Display" step of the pipeline (see lib/graph/rendering.ts).
|
||||
* Which view is used comes from the source's outputType ('image' | 'html').
|
||||
*
|
||||
* - Streaming (agent): StreamingMarkdownView
|
||||
* - Config markdown preview: MarkdownJsxView (markdown-to-jsx)
|
||||
* - Agent final / other HTML: StaticMarkdownHtmlView
|
||||
* RenderingNode chooses the view directly; no shared dispatcher.
|
||||
*/
|
||||
|
||||
export { ImageOutputView } from './ImageOutputView'
|
||||
export { MarkdownOutputView } from './MarkdownOutputView'
|
||||
export { MarkdownJsxView } from './MarkdownJsxView'
|
||||
export { RawOutputView } from './RawOutputView'
|
||||
export { StaticMarkdownHtmlView } from './StaticMarkdownHtmlView'
|
||||
export { StreamingMarkdownView } from './StreamingMarkdownView'
|
||||
|
||||
export type { ImageOutputViewProps } from './ImageOutputView'
|
||||
export type { MarkdownOutputViewProps } from './MarkdownOutputView'
|
||||
export type { MarkdownJsxViewProps } from './MarkdownJsxView'
|
||||
export type { RawOutputViewProps } from './RawOutputView'
|
||||
export type { StaticMarkdownHtmlViewProps } from './StaticMarkdownHtmlView'
|
||||
export type { StreamingMarkdownViewProps } from './StreamingMarkdownView'
|
||||
|
||||
Reference in New Issue
Block a user