fix(desktop): 模型输出渲染图表 + Markdown 支持表格/代码围栏;ReAct 步数提至可配 12
针对运行页输出的两个渲染缺口 + 一个节点报错: - 图表:模型输出走 chart 块分段渲染(文本 Markdown,```chart 块用 ChartView 出图), 不再把图表 spec 显示成原始代码。 - 表格:轻量 Markdown 组件原本不支持 GFM 表格(| … | 显示成原始竖线)与 ``` 围栏代码, 现补上:表格解析成 <table>、围栏渲染成 <pre>。 - ReAct 步数:reactMaxStep 由常量 8 改 env 可配(REACT_MAX_STEP,默认 12)。研究型任务 (搜索→抓取→推理多轮)8 步偏紧易触顶报 exceeds max steps;提到 12 覆盖多数。 tsc+vite 通过,dispatcher build/vet 绿;wails 重启加载。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,8 +37,37 @@ export function Markdown({ text, className, onLink }: { text: string; className?
|
||||
const blocks: ReactNode[] = [];
|
||||
let para: string[] = [];
|
||||
let list: { ordered: boolean; items: string[] } | null = null;
|
||||
let tableBuf: string[] = []; // 连续的 | … | 行,flush 时判定是否真表格
|
||||
let code: { lang: string; lines: string[] } | null = null; // ``` 围栏代码块
|
||||
let k = 0;
|
||||
|
||||
const cells = (r: string) => r.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
|
||||
const isSep = (s: string) => s.includes("-") && /^\s*\|?[\s:|-]+\|?\s*$/.test(s);
|
||||
const flushTable = () => {
|
||||
if (!tableBuf.length) return;
|
||||
if (tableBuf.length >= 2 && isSep(tableBuf[1])) {
|
||||
const head = cells(tableBuf[0]);
|
||||
const rows = tableBuf.slice(2).map(cells);
|
||||
blocks.push(
|
||||
<div key={`tw${k++}`} className="my-2 overflow-x-auto">
|
||||
<table className="w-full border-collapse text-[13px]">
|
||||
<thead>
|
||||
<tr>{head.map((c, i) => <th key={i} className="border border-line bg-ink-800 px-2 py-1 text-left font-medium text-slate-200">{il(c, `th${k}-${i}`)}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, ri) => (
|
||||
<tr key={ri}>{r.map((c, ci) => <td key={ci} className="border border-line px-2 py-1 align-top text-slate-300">{il(c, `td${k}-${ri}-${ci}`)}</td>)}</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
para.push(...tableBuf); // 不是合法表格 → 当普通段落
|
||||
}
|
||||
tableBuf = [];
|
||||
};
|
||||
|
||||
const flushPara = () => {
|
||||
if (para.length) {
|
||||
blocks.push(
|
||||
@@ -67,20 +96,43 @@ export function Markdown({ text, className, onLink }: { text: string; className?
|
||||
}
|
||||
};
|
||||
|
||||
const flushAll = () => { flushTable(); flushPara(); flushList(); };
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trimEnd();
|
||||
|
||||
// 围栏代码块 ```lang … ```(含未被图表分流的普通代码)。
|
||||
if (code) {
|
||||
if (line.trim().startsWith("```")) {
|
||||
blocks.push(<pre key={`c${k++}`} className="my-2 overflow-x-auto rounded-md border border-line bg-ink-950/60 p-2.5 font-mono text-[12px] leading-relaxed text-slate-300">{code.lines.join("\n")}</pre>);
|
||||
code = null;
|
||||
} else code.lines.push(raw);
|
||||
continue;
|
||||
}
|
||||
if (line.trim().startsWith("```")) {
|
||||
flushAll();
|
||||
code = { lang: line.trim().slice(3), lines: [] };
|
||||
continue;
|
||||
}
|
||||
// 表格行:连续的 | … | 缓冲,flush 时判定真伪。
|
||||
if (line.trim().startsWith("|")) {
|
||||
flushPara();
|
||||
flushList();
|
||||
tableBuf.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h = /^(#{1,3})\s+(.*)$/.exec(line);
|
||||
const ol = /^\d+\.\s+(.*)$/.exec(line);
|
||||
const ul = /^[-*]\s+(.*)$/.exec(line);
|
||||
|
||||
if (line.trim() === "") {
|
||||
flushPara();
|
||||
flushList();
|
||||
flushAll();
|
||||
} else if (/^(---|\*\*\*|___)\s*$/.test(line)) {
|
||||
flushPara();
|
||||
flushList();
|
||||
flushAll();
|
||||
blocks.push(<hr key={`hr${k++}`} className="my-3 border-line" />);
|
||||
} else if (h) {
|
||||
flushTable();
|
||||
flushPara();
|
||||
flushList();
|
||||
const lvl = h[1].length;
|
||||
@@ -88,14 +140,14 @@ export function Markdown({ text, className, onLink }: { text: string; className?
|
||||
const content = il(h[2], `h${k}`);
|
||||
blocks.push(lvl === 1 ? <h1 key={`h${k++}`} className={cls}>{content}</h1> : lvl === 2 ? <h2 key={`h${k++}`} className={cls}>{content}</h2> : <h3 key={`h${k++}`} className={cls}>{content}</h3>);
|
||||
} else if (line.startsWith(">")) {
|
||||
flushPara();
|
||||
flushList();
|
||||
flushAll();
|
||||
blocks.push(
|
||||
<blockquote key={`q${k++}`} className="my-2 border-l-2 border-brand/50 pl-3 text-sm text-slate-400">
|
||||
{il(line.replace(/^>\s?/, ""), `q${k}`)}
|
||||
</blockquote>,
|
||||
);
|
||||
} else if (ol) {
|
||||
flushTable();
|
||||
flushPara();
|
||||
if (!list || !list.ordered) {
|
||||
flushList();
|
||||
@@ -103,6 +155,7 @@ export function Markdown({ text, className, onLink }: { text: string; className?
|
||||
}
|
||||
list.items.push(ol[1]);
|
||||
} else if (ul) {
|
||||
flushTable();
|
||||
flushPara();
|
||||
if (!list || list.ordered) {
|
||||
flushList();
|
||||
@@ -110,12 +163,13 @@ export function Markdown({ text, className, onLink }: { text: string; className?
|
||||
}
|
||||
list.items.push(ul[1]);
|
||||
} else {
|
||||
flushTable();
|
||||
flushList();
|
||||
para.push(line);
|
||||
}
|
||||
}
|
||||
flushPara();
|
||||
flushList();
|
||||
if (code) blocks.push(<pre key={`c${k++}`} className="my-2 overflow-x-auto rounded-md border border-line bg-ink-950/60 p-2.5 font-mono text-[12px] text-slate-300">{code.lines.join("\n")}</pre>);
|
||||
flushAll();
|
||||
|
||||
return <div className={className}>{blocks}</div>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Activity, FileText, History, Wrench } from "lucide-react";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
import { Markdown } from "../components/Markdown";
|
||||
import { ChartView } from "../components/ChartView";
|
||||
import { extractChartBlocks, hasChart } from "../lib/chartspec";
|
||||
import { deriveNodes, emptyRun, type RunState } from "../lib/run";
|
||||
import { listRuns, taskEval, streamExec, streamTokens, type RunSummary, type EvalResult } from "../lib/api";
|
||||
import { Tabs, Panel, Dot, Badge, EmptyState, cn, type TabDef } from "../ui";
|
||||
@@ -141,7 +143,7 @@ export function RunsView({ run }: { run: RunState }) {
|
||||
{/* 右:模型输出(Markdown,固定面板,与之前一致) */}
|
||||
<Panel title="模型输出" icon={FileText}>
|
||||
{cur.output ? (
|
||||
<Markdown text={cur.output} className="text-sm" />
|
||||
<OutputView output={cur.output} />
|
||||
) : (
|
||||
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看其模型输出;或发起新运行,token 在此流式呈现。" />
|
||||
)}
|
||||
@@ -151,6 +153,22 @@ export function RunsView({ run }: { run: RunState }) {
|
||||
);
|
||||
}
|
||||
|
||||
// OutputView:模型输出 = Markdown 渲染;含 ```chart 块时分段(文本走 Markdown,图表走 ChartView)。
|
||||
function OutputView({ output }: { output: string }) {
|
||||
if (!hasChart(output)) return <Markdown text={output} className="text-sm" />;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{extractChartBlocks(output).map((seg, i) =>
|
||||
seg.kind === "chart" ? (
|
||||
<ChartView key={i} spec={seg.spec} />
|
||||
) : (
|
||||
seg.text.trim() && <Markdown key={i} text={seg.text} className="text-sm" />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ToolCalls:从执行事件筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。
|
||||
function ToolCalls({ run }: { run: RunState }) {
|
||||
const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool");
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
)
|
||||
|
||||
// reactMaxStep 限制 ReAct 推理-工具循环的步数上限(控成本/时延)。
|
||||
const reactMaxStep = 8
|
||||
// 研究型任务(搜索→抓取→推理 多轮)8 步偏紧易触顶报错;默认 12,可经 REACT_MAX_STEP 调。
|
||||
func reactMaxStep() int { return envInt("REACT_MAX_STEP", 12) }
|
||||
|
||||
// streamHasToolCall 扫描模型整段流输出,任一片段含 tool call 即判定为工具调用。
|
||||
// 比默认"只看首片段"鲁棒(兼容先吐文本/思考再给 tool call 的模型,如 deepseek)。
|
||||
@@ -180,7 +181,7 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar
|
||||
ag, err := react.NewAgent(ctx, &react.AgentConfig{
|
||||
ToolCallingModel: tcm,
|
||||
ToolsConfig: compose.ToolsNodeConfig{Tools: tools},
|
||||
MaxStep: reactMaxStep,
|
||||
MaxStep: reactMaxStep(),
|
||||
// 默认检查器只看首个流片段;deepseek 等常先吐文本再给 tool call → 漏判。
|
||||
// 改成扫描整段流,任一片段含 tool call 即判定为工具调用。
|
||||
StreamToolCallChecker: streamHasToolCall,
|
||||
|
||||
Reference in New Issue
Block a user