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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"markdown-to-jsx": "^9.7.9",
|
||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
@@ -5949,6 +5950,34 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"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": {
|
"node_modules/marked": {
|
||||||
"version": "17.0.4",
|
"version": "17.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"markdown-to-jsx": "^9.7.9",
|
||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ import {
|
|||||||
useRenderingNodeState,
|
useRenderingNodeState,
|
||||||
type RenderingNodeData,
|
type RenderingNodeData,
|
||||||
} from './useRenderingNodeState'
|
} from './useRenderingNodeState'
|
||||||
import { ImageOutputView, MarkdownOutputView, RawOutputView } from './views'
|
import {
|
||||||
|
ImageOutputView,
|
||||||
|
MarkdownJsxView,
|
||||||
|
RawOutputView,
|
||||||
|
StaticMarkdownHtmlView,
|
||||||
|
StreamingMarkdownView,
|
||||||
|
} from './views'
|
||||||
|
|
||||||
export type { RenderingNodeData }
|
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) {
|
if (state.loading && state.streamingMarkdown !== null) {
|
||||||
return <MarkdownOutputView state={state} streaming={true} />
|
return <StreamingMarkdownView state={state} />
|
||||||
}
|
}
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
return (
|
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).
|
* 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').
|
* 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 { ImageOutputView } from './ImageOutputView'
|
||||||
export { MarkdownOutputView } from './MarkdownOutputView'
|
export { MarkdownJsxView } from './MarkdownJsxView'
|
||||||
export { RawOutputView } from './RawOutputView'
|
export { RawOutputView } from './RawOutputView'
|
||||||
|
export { StaticMarkdownHtmlView } from './StaticMarkdownHtmlView'
|
||||||
|
export { StreamingMarkdownView } from './StreamingMarkdownView'
|
||||||
|
|
||||||
export type { ImageOutputViewProps } from './ImageOutputView'
|
export type { ImageOutputViewProps } from './ImageOutputView'
|
||||||
export type { MarkdownOutputViewProps } from './MarkdownOutputView'
|
export type { MarkdownJsxViewProps } from './MarkdownJsxView'
|
||||||
export type { RawOutputViewProps } from './RawOutputView'
|
export type { RawOutputViewProps } from './RawOutputView'
|
||||||
|
export type { StaticMarkdownHtmlViewProps } from './StaticMarkdownHtmlView'
|
||||||
|
export type { StreamingMarkdownViewProps } from './StreamingMarkdownView'
|
||||||
|
|||||||
Reference in New Issue
Block a user