diff --git a/web/src/pages/trading/components/AnalysisDrawer.vue b/web/src/pages/trading/components/AnalysisDrawer.vue index 686d49e..2760570 100644 --- a/web/src/pages/trading/components/AnalysisDrawer.vue +++ b/web/src/pages/trading/components/AnalysisDrawer.vue @@ -27,14 +27,56 @@ const toolResults = computed(() => { const duration = computed(() => detail.value?.finished_at ? Math.round((new Date(detail.value.finished_at).getTime() - new Date(detail.value.started_at).getTime()) / 1000) : null) const toolCallCount = computed(() => (detail.value?.transcript ?? []).reduce((n, m) => n + (m.role === "assistant" ? m.tool_calls.length : 0), 0)) -function pretty(raw: string | undefined) { +function parseJson(raw: string): unknown { + try { + return JSON.parse(raw) + } catch { + return undefined + } +} + +function inlineJson(value: unknown): string { + if (Array.isArray(value)) + return `[${value.map(inlineJson).join(", ")}]` + if (value && typeof value === "object") + return `{${Object.entries(value).map(([k, v]) => `${JSON.stringify(k)}: ${inlineJson(v)}`).join(", ")}}` + return JSON.stringify(value) +} + +/** 缩进展开,但放得下一行的数组、对象不再拆开:K 线每根一行,而不是每个数字一行 */ +function formatJson(value: unknown, indent = ""): string { + const flat = inlineJson(value) + if (flat.length <= 100 || !value || typeof value !== "object") + return flat + const inner = `${indent} ` + if (Array.isArray(value)) + return `[\n${value.map(v => inner + formatJson(v, inner)).join(",\n")}\n${indent}]` + return `{\n${Object.entries(value).map(([k, v]) => `${inner}${JSON.stringify(k)}: ${formatJson(v, inner)}`).join(",\n")}\n${indent}}` +} + +function formatResult(raw: string | undefined) { if (raw === undefined) return "无结果" - try { - return JSON.stringify(JSON.parse(raw), null, 2) - } catch { - return raw - } + const value = parseJson(raw) + return value === undefined ? raw : formatJson(value) +} + +/** 发给模型的消息是 Markdown,其中整行的 JSON(预取行情、最近决策)改成格式化的代码块 */ +function promptMarkdown(text: string) { + return text.split("\n").map((line) => { + const start = line.indexOf("{") + const value = start < 0 ? undefined : parseJson(line.slice(start)) + if (value === undefined) + return line + const prefix = line.slice(0, start) + // 标题里的是工具参数,短,保持在标题行内 + if (prefix.startsWith("#")) + return `${prefix}\`${inlineJson(value)}\`` + // 列表项里的代码块要缩进到列表内容的位置,才属于这一项 + const pad = " ".repeat(/^\s*[-*] /.exec(prefix)?.[0].length ?? 0) + const block = ["```json", ...formatJson(value).split("\n"), "```"].map(l => pad + l).join("\n") + return `${prefix}\n\n${block}\n` + }).join("\n") } @@ -99,14 +141,24 @@ function pretty(raw: string | undefined) { @@ -129,4 +181,42 @@ function pretty(raw: string | undefined) { color: var(--el-text-color-secondary); font-size: 12px; } +.block-label { + margin-bottom: 4px; + color: var(--el-text-color-secondary); + font-size: 12px; +} +.code-block, +.prompt :deep(pre) { + max-height: 24rem; + margin: 0; + padding: 8px 12px; + overflow: auto; + border: 1px solid var(--el-border-color-lighter); + border-radius: 6px; + background: var(--el-fill-color-light); + font-family: var(--ap-font-mono); + font-size: 12px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} +.prompt { + font-size: 13px; +} +/* 标题里的参数预览过长时截断,不把右侧箭头挤出去 */ +:deep(.el-collapse-item__title) { + flex: 1; + min-width: 0; +} +.prompt :deep(pre) { + margin: 6px 0 10px; +} +.prompt :deep(h2) { + margin-top: 1.2em; + font-size: 1.05em; +} +.prompt :deep(h3) { + font-size: 1em; +}