From 759483bf92c51284e4593fe604c44a332062fb87 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 16 Jul 2026 14:08:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(desktop,gateway):=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E5=8E=BB=E9=87=8D=20=E2=80=94=E2=80=94=20=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E5=8A=9F=E8=83=BD=E4=B8=80=E4=B8=AA=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E4=B8=80=E4=BB=BD=E6=95=B0=E6=8D=AE=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=9F=A5=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈「UI 太乱」。数出来的:report/kb/memory 各 4 个入口、studio 5 个; 运行历史两套渲染。乱的机制是同一件事写了两遍,改一处另一处必然掉队。 **一份数据一个查法** - 工作台「最近任务」和运行页「运行历史」是同一批数据,却各查各的: 前者走 store.RecentTasks(),后者走 RecentRuns()。于是我给运行历史加的 topic 字段工作台完全没跟上,还在 mono 显示 report_;而且 RecentTasks 没有租户过滤,口径也不一致。现在统一走 RecentRuns,RecentTasks 删掉 (它只有那一个调用点)。前端 recent_runs 也改用 RunSummary 同一个类型。 **一个功能一个入口** - 工作台删掉「入库知识/生成报告/管理记忆」按钮排 —— 跳的目标和上面四张 能力卡片完全重合,同一页给两个入口。 - 「最近任务」现在点击直达运行页对应那条(App 加 goto(view, taskId) → RunsView focusTaskId),而不是只把页面切过去让用户自己再找一遍。 **报告归 RUN,只做启动器** - 报告是「执行」不是「构建」,从 BUILD 组移到 RUN。 - 报告页砍掉「执行轨迹」「报告正文」两个面板 —— 和运行页完全重复,而且 那套是组件本地 state,切页面就没。现在它只有主题输入框。 - 报告改走 App 的全局运行态(新增 onRunReport,与 onRun 同构、复用 attachRun): 提交 → 跳「运行 · 观测」→ 实时看轨迹/正文/导出。App 里 onRun 早就写着 「发起即跳运行页,观测统一收敛在此页」——这条方针一直在,只有报告没遵守。 **顺带清死代码** - 市场(规划中)从导航下架,ViewKey 里的 "market" 和 Boxes 图标一并删。 - PLACEHOLDERS 整块死代码:它列的 home/kb/report/runs 全都早已真实实现, market 是唯一还可能用到的、下架后那个 Placeholder 分支永远走不到 (tsc 报 "Spread types" 就是因为 PLACEHOLDERS[view] 已成 never)。 连同 views/Placeholder.tsx 一起删。 live 验证(真账号,桌面端实机):报告页输入主题 → 提交 → 自动跳运行页 → 顶部「当前运行 · live」→ 轨迹实时跑 → 报告正文流式出 + 导出按钮在位 → report_7a0110a10b534f90 落库带 topic「归口测试」,POST /api/v1/reports → 202。 Co-Authored-By: Claude Opus 4.8 --- sundynix-desktop/frontend/src/App.tsx | 52 +++-- sundynix-desktop/frontend/src/lib/api.ts | 3 +- .../frontend/src/shell/LeftNav.tsx | 6 +- sundynix-desktop/frontend/src/views/Home.tsx | 29 ++- .../frontend/src/views/Placeholder.tsx | 20 -- .../frontend/src/views/ReportView.tsx | 177 +++++------------- .../frontend/src/views/RunsView.tsx | 7 +- .../internal/handler/task_handler.go | 11 +- sundynix-gateway/internal/store/pgsql.go | 17 +- 9 files changed, 110 insertions(+), 212 deletions(-) delete mode 100644 sundynix-desktop/frontend/src/views/Placeholder.tsx diff --git a/sundynix-desktop/frontend/src/App.tsx b/sundynix-desktop/frontend/src/App.tsx index d83fe0a..02bb65a 100644 --- a/sundynix-desktop/frontend/src/App.tsx +++ b/sundynix-desktop/frontend/src/App.tsx @@ -12,11 +12,10 @@ import { ReportView } from "./views/ReportView"; import { RunsView } from "./views/RunsView"; import { UsageView } from "./views/UsageView"; import { Home } from "./views/Home"; -import { Placeholder } from "./views/Placeholder"; import { CommandPalette, type Command } from "./components/CommandPalette"; import { UpdateBanner } from "./components/UpdateBanner"; import { Login } from "./views/Login"; -import { submitTask, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, spaceCurrent, mySpaces, switchSpace, createSpace, type Identity, type AuthUser, type TenantCtx, type MyTenant, type SpaceCtx, type MySpace } from "./lib/api"; +import { submitTask, generateReport, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, spaceCurrent, mySpaces, switchSpace, createSpace, type Identity, type AuthUser, type TenantCtx, type MyTenant, type SpaceCtx, type MySpace } from "./lib/api"; import type { TaskDsl } from "./lib/dsl"; import { emptyRun, type RunState } from "./lib/run"; import { ToastProvider } from "./ui"; @@ -31,16 +30,11 @@ function getSessionId(): string { return s; } -const PLACEHOLDERS: Partial> = { - home: { title: "工作台", desc: "概览:知识库 / 文档 / 近期运行 / 待办报告 / 配额计费 + 快捷入口。" }, - kb: { title: "知识库 (RAG)", desc: "入库流水线监控 · 检索调试台(带来源徽标) · 文档/块浏览 · 知识图谱 · 检索评测。依赖 embedding + 入库 worker + 真实混合检索。" }, - report: { title: "报告生成", desc: "模板库 · 大纲编辑 · 章节并行生成进度 · 实时预览(含引用) · 导出 docx/pdf。依赖 RAG 核心链 + UniOffice。" }, - runs: { title: "运行 · 观测", desc: "实时执行 · 节点轨迹 · 工具调用 · 运行历史复盘。当前运行结果见底部抽屉。" }, - market: { title: "市场 · Packs(规划中)", desc: "垂直包(法律/医疗/金融) · Agent 模板 · 开通向导(建租户→入库→注册模板→应用配置)。依赖多租户 + Pack 格式。" }, -}; - export default function App() { const [view, setView] = useState("home"); + // 从工作台「最近任务」点进来时要定位到具体那条运行,而不是只把页面切过去。 + const [focusRun, setFocusRun] = useState(null); + const goto = (v: ViewKey, taskId?: string) => { setView(v); setFocusRun(taskId ?? null); }; const [user, setUser] = useState(null); const [tenant, setTenant] = useState(null); const [tenants, setTenants] = useState([]); @@ -252,6 +246,34 @@ export default function App() { [identity, attachRun], ); + // 报告生成走和编排任务同一条路:灌进全局运行态 + 跳「运行 · 观测」。 + // 此前报告页自己维护一套 SSE/输出/轨迹本地 state,于是运行页看不到它、 + // 切个页面这套 state 就没了 —— 报告明明在后端跑完了却永远找不回。 + const onRunReport = useCallback( + async (topic: string, kb?: string) => { + closeRef.current?.(); + execCloseRef.current?.(); + stopPoll(); + const t0 = Date.now(); + setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交报告任务" }], exec: [] }); + setFocusRun(null); + setView("runs"); + try { + const taskId = await generateReport(identity, topic, kb); + setRun((r) => ({ + ...r, + phase: "streaming", + taskId, + events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }], + })); + attachRun(taskId, t0); + } catch (e) { + setRun((r) => ({ ...r, phase: "error", error: (e as Error).message })); + } + }, + [identity, attachRun], + ); + // 恢复在途待审任务:登录后若存在 waiting 任务且当前无 live run,挂回它 → 全局审批条重现, // 用户刷新页面/重开 app 也能继续批准(HITL 持久化中断后审批可跨重启、可等数小时)。 const restoredRef = useRef(false); @@ -303,22 +325,20 @@ export default function App() {
{view === "home" ? ( - + ) : view === "studio" ? ( ) : view === "kb" ? ( ) : view === "report" ? ( - + ) : view === "runs" ? ( - + ) : view === "memory" ? ( ) : view === "usage" ? ( - ) : ( - - )} + ) : null}
setCmdOpen(false)} commands={commands} /> diff --git a/sundynix-desktop/frontend/src/lib/api.ts b/sundynix-desktop/frontend/src/lib/api.ts index 33a0020..836c318 100644 --- a/sundynix-desktop/frontend/src/lib/api.ts +++ b/sundynix-desktop/frontend/src/lib/api.ts @@ -648,7 +648,6 @@ export async function deleteMemory(id: Identity, key: string): Promise { // ── 工作台仪表盘聚合(GET /stats/overview)── export interface KeyCount { key: string; count: number } -export interface RecentRun { task_id: string; status: string; detail: string; at: string } export interface Overview { tasks_today: number; tasks_total: number; @@ -662,7 +661,7 @@ export interface Overview { kb_count: number; tokens_today: number; daily_budget: number; - recent_runs: RecentRun[]; + recent_runs: RunSummary[]; // 与「运行」页共用类型,后端也共用 RecentRuns services: Record; } diff --git a/sundynix-desktop/frontend/src/shell/LeftNav.tsx b/sundynix-desktop/frontend/src/shell/LeftNav.tsx index 374261a..063f1aa 100644 --- a/sundynix-desktop/frontend/src/shell/LeftNav.tsx +++ b/sundynix-desktop/frontend/src/shell/LeftNav.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import { cn } from "../ui"; -export type ViewKey = "home" | "studio" | "kb" | "report" | "runs" | "memory" | "usage" | "market"; +export type ViewKey = "home" | "studio" | "kb" | "report" | "runs" | "memory" | "usage"; interface Item { key: ViewKey; @@ -25,11 +25,11 @@ const ITEMS: Item[] = [ { key: "home", label: "工作台", icon: LayoutDashboard, ready: true }, { key: "studio", label: "编排", icon: Workflow, group: "BUILD", ready: true }, { key: "kb", label: "知识库", icon: Database, group: "BUILD", ready: true }, - { key: "report", label: "报告", icon: FileText, group: "BUILD", ready: true }, { key: "runs", label: "运行", icon: Activity, group: "RUN", ready: true }, + // 报告是「执行」不是「构建」:它归 RUN。放 BUILD 里跟编排/知识库并列会让人以为它是个要搭的东西。 + { key: "report", label: "报告", icon: FileText, group: "RUN", ready: true }, { key: "memory", label: "记忆", icon: Bookmark, group: "MANAGE", ready: true }, { key: "usage", label: "用量", icon: Coins, group: "MANAGE", ready: true }, - { key: "market", label: "市场", icon: Boxes, group: "MANAGE", ready: false }, ]; // 左导航:深色,激活态紫色高亮 + 左侧光条,描线图标。 diff --git a/sundynix-desktop/frontend/src/views/Home.tsx b/sundynix-desktop/frontend/src/views/Home.tsx index d392215..8f3a400 100644 --- a/sundynix-desktop/frontend/src/views/Home.tsx +++ b/sundynix-desktop/frontend/src/views/Home.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { Workflow, Database, Bookmark, FileText, Plus, Upload, Activity, Coins, Gauge, ArrowRight, ArrowUpRight, type LucideIcon } from "lucide-react"; -import { statsOverview, type Overview, type RecentRun } from "../lib/api"; +import { Workflow, Database, Bookmark, FileText, Plus, Activity, Coins, Gauge, ArrowRight, ArrowUpRight, type LucideIcon } from "lucide-react"; +import { statsOverview, type Overview, type RunSummary } from "../lib/api"; import type { ViewKey } from "../shell/LeftNav"; import { Button, cn } from "../ui"; @@ -61,15 +61,19 @@ function relTime(iso: string): string { return `${Math.floor(d / 86_400_000)} 天前`; } -function RunRow({ r }: { r: RecentRun }) { +// 一条最近运行。点击直达「运行」页对应那条 —— 光切页面还得让用户自己再找一遍。 +// 有主题(报告类)就显示主题;否则退回 task_id:report_ 这种 id 对用户没有任何意义。 +function RunRow({ r, onOpen }: { r: RunSummary; onOpen: () => void }) { const s = STATUS_LABEL[r.status] ?? { text: r.status, cls: "text-slate-500" }; return ( -
+
+ ); } @@ -79,7 +83,7 @@ function greet(): string { } // 工作台:商业级门面 —— 问候 + 主行动 + 关键指标 + 最近任务 + 快速开始(去基建、去黑话)。 -export function Home({ onSelect, userName, spaceName }: { onSelect: (v: ViewKey) => void; userName?: string; spaceName?: string }) { +export function Home({ onSelect, userName, spaceName }: { onSelect: (v: ViewKey, taskId?: string) => void; userName?: string; spaceName?: string }) { const [ov, setOv] = useState(null); useEffect(() => { let alive = true; @@ -132,7 +136,7 @@ export function Home({ onSelect, userName, spaceName }: { onSelect: (v: ViewKey) )} {hasRuns ? ( -
{ov!.recent_runs.map((r) => )}
+
{ov!.recent_runs.map((r) => onSelect("runs", r.task_id)} />)}
) : (
还没有运行记录
@@ -175,13 +179,8 @@ export function Home({ onSelect, userName, spaceName }: { onSelect: (v: ViewKey) })}
- - {/* 次要动作 */} -
- - - -
+ {/* 原本这里还有一排「入库知识/生成报告/管理记忆」按钮,跳的目标和上面四张卡片 + 完全重合 —— 同一页里同一个功能给两个入口,纯冗余,已删。 */} ); diff --git a/sundynix-desktop/frontend/src/views/Placeholder.tsx b/sundynix-desktop/frontend/src/views/Placeholder.tsx deleted file mode 100644 index b875084..0000000 --- a/sundynix-desktop/frontend/src/views/Placeholder.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Hammer } from "lucide-react"; -import { Badge } from "../ui"; - -// 规划中模块占位 —— 深色,露出信息架构与定位。 -export function Placeholder({ title, desc }: { title: string; desc: string }) { - return ( -
-
-
- -
-
{title}
-

{desc}

-
- 规划中 -
-
-
- ); -} diff --git a/sundynix-desktop/frontend/src/views/ReportView.tsx b/sundynix-desktop/frontend/src/views/ReportView.tsx index 530c10d..cfaaaf4 100644 --- a/sundynix-desktop/frontend/src/views/ReportView.tsx +++ b/sundynix-desktop/frontend/src/views/ReportView.tsx @@ -1,151 +1,60 @@ -import { useRef, useState } from "react"; -import { Play, FileText, FileType2, Printer, FileCode } from "lucide-react"; -import { generateReport, streamTokens, streamExec, reportExportUrl, reportFilename, type Identity, type ExecEvent } from "../lib/api"; -import { saveReportAs, printReportHtml, notify } from "../lib/desktop"; -import { ExecTrace } from "../components/ExecTrace"; -import { Markdown } from "../components/Markdown"; -import { Button, Input, Field, Panel, Dot, EmptyState, useToast } from "../ui"; +import { useState } from "react"; +import { FileText, Play } from "lucide-react"; +import { Button, Panel } from "../ui"; -type Phase = "idle" | "running" | "done" | "error"; - -// 报告生成:输入主题(+可选知识库) → 触发后端专用编排 -// (规划大纲 → 各章并行检索+撰写 → 渲染 Word),实时看进度与正文,完成后下载 .docx。 -export function ReportView({ identity }: { identity: Identity }) { - const toast = useToast(); +// 报告生成 —— 只做启动器:输入主题 → 提交 → 自动跳「运行 · 观测」实时看。 +// +// 这里曾经自己维护一整套:SSE 订阅、token 流、执行轨迹、导出按钮、PDF 打印, +// 页面上还挂着「执行轨迹」和「报告正文」两个面板 —— 和运行页完全重复。 +// 而且那套 state 全是组件本地 useState:切个页面就灰飞烟灭,报告明明在后端 +// 好好地跑完了,用户却永远找不回它。 +// 现在轨迹/正文/导出统一由运行页负责(那份是持久的、能复盘、能导出)。 +export function ReportView({ onRunReport, running }: { onRunReport: (topic: string, kb?: string) => void; running: boolean }) { const [topic, setTopic] = useState(""); const [kb, setKb] = useState(""); - const [phase, setPhase] = useState("idle"); - const [out, setOut] = useState(""); - const [exec, setExec] = useState([]); - const [taskId, setTaskId] = useState(""); - const closeRef = useRef<(() => void) | null>(null); - const execCloseRef = useRef<(() => void) | null>(null); - const previewRef = useRef(null); - const running = phase === "running"; - - // 导出 Word / Markdown:后端按需现渲染(导出时再处理),经原生"另存为"或浏览器下载。 - const exportFile = async (format: "docx" | "md") => { - try { - const p = await saveReportAs(reportExportUrl(taskId, format), reportFilename(topic, taskId, format)); - if (p) toast.push("success", "已保存到 " + p); - } catch (e) { - toast.push("error", (e as Error).message); - } - }; - - // 导出 PDF:把预览到的 Markdown(已渲染 HTML)送进打印视图出 PDF(CJK 零字体依赖)。 - const exportPdf = () => { - const html = previewRef.current?.innerHTML; - if (!html) { - toast.push("error", "暂无报告正文可导出"); - return; - } - if (!printReportHtml(topic.trim() || taskId, html)) { - toast.push("error", "打印窗口被拦截,请允许弹出窗口后重试"); - } - }; - - const onGenerate = async () => { + const submit = () => { if (!topic.trim() || running) return; - closeRef.current?.(); - execCloseRef.current?.(); - setPhase("running"); - setOut(""); - setExec([]); - setTaskId(""); - try { - const id = await generateReport(identity, topic.trim(), kb.trim() || undefined); - setTaskId(id); - execCloseRef.current = streamExec( - id, - (ev) => setExec((xs) => [...xs, ev]), - () => {}, - () => {}, - ); - closeRef.current = streamTokens( - id, - (tok) => setOut((o) => o + tok), - () => { - setPhase("done"); - toast.push("success", "报告已生成,可保存 Word"); - notify("报告已生成", topic.trim()); - }, - () => { - setPhase("error"); - toast.push("error", "报告流连接中断"); - }, - ); - } catch (e) { - setPhase("error"); - toast.push("error", (e as Error).message); - } + onRunReport(topic.trim(), kb.trim() || undefined); }; - const tracePhase = running ? "streaming" : phase === "done" ? "done" : phase === "error" ? "error" : "idle"; - return ( -
-
+
+

报告生成

规划大纲 → 各章并行(知识库检索 + LLM 撰写)→ 汇聚 → 渲染真实 Word(.docx)。依赖已配置对话模型;挂知识库则引用其资料。

-
-
- - setTopic(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && onGenerate()} - placeholder="如:2026 年国产大模型产业现状与趋势分析" - /> - - - setKb(e.target.value)} placeholder="如 docs,留空则不挂检索" /> - - - {phase === "done" && taskId ? ( -
- 导出 - - - -
- ) : ( - - )} -
- -
- - - - - - - 报告正文 · {taskId || "未开始"} - - } - > - {out ? ( -
- + +
+
+ + setTopic(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && submit()} + placeholder="如:2026 年国产大模型产业现状与趋势分析" + className="w-full rounded-md border border-line bg-ink-850 px-3 py-2 text-sm text-slate-100 outline-none placeholder:text-slate-600 focus:border-brand/50" + />
- ) : ( - - )} +
+ + setKb(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && submit()} + placeholder="如 docs,留空则不挂检索" + className="w-full rounded-md border border-line bg-ink-850 px-3 py-2 text-sm text-slate-100 outline-none placeholder:text-slate-600 focus:border-brand/50" + /> +
+
+ + 提交后自动跳「运行」,在那里看进度、复盘和导出 Word。 +
+
diff --git a/sundynix-desktop/frontend/src/views/RunsView.tsx b/sundynix-desktop/frontend/src/views/RunsView.tsx index f61cd27..669a690 100644 --- a/sundynix-desktop/frontend/src/views/RunsView.tsx +++ b/sundynix-desktop/frontend/src/views/RunsView.tsx @@ -29,7 +29,7 @@ function relTime(iso: string): string { // 运行 · 观测:左=运行历史,中=tab 切换(轨迹/工具/评测)+状态,右=模型输出(Markdown)。 // 选中历史运行从 Redis 回放;「当前运行」置顶沿用实时订阅。 -export function RunsView({ run }: { run: RunState }) { +export function RunsView({ run, focusTaskId }: { run: RunState; focusTaskId?: string | null }) { const toast = useToast(); const [runs, setRuns] = useState([]); const [sel, setSel] = useState(null); // null = 当前实时运行 @@ -63,6 +63,11 @@ export function RunsView({ run }: { run: RunState }) { if (run.taskId) { setSel(null); setEvalRes(null); setTab("trace"); } }, [run.taskId]); + // 从工作台「最近任务」点进来:直接定位到那一条,别让用户切过来再自己找一遍。 + useEffect(() => { + if (focusTaskId) selectRun(focusTaskId); + }, [focusTaskId, selectRun]); + // 当前实时运行的评测:完成后拉一次。 useEffect(() => { if (!sel && run.taskId && run.phase === "done") taskEval(run.taskId).then(setEvalRes).catch(() => {}); diff --git a/sundynix-gateway/internal/handler/task_handler.go b/sundynix-gateway/internal/handler/task_handler.go index 95d9bd2..7422e8a 100644 --- a/sundynix-gateway/internal/handler/task_handler.go +++ b/sundynix-gateway/internal/handler/task_handler.go @@ -453,13 +453,10 @@ func (h *Handler) StatsOverview(c *gin.Context) { }) } - // 近期运行 feed。 - recent := make([]gin.H, 0, 8) - for _, t := range h.db.RecentTasks(ctx, uid, 8) { - recent = append(recent, gin.H{ - "task_id": t.TaskID, "status": t.Status, "detail": t.Detail, "at": t.CreatedAt, - }) - } + // 近期运行 feed —— 与「运行」页的运行历史共用 RecentRuns,同一份数据不能有两个查法。 + // 此前这里走 RecentTasks(),于是运行历史加了 topic 字段、工作台完全没跟上, + // 还在显示 report_;而且 RecentTasks 没有租户过滤,口径也不一致。 + recent := h.db.RecentRuns(ctx, uid, 8) // 服务健康(与 Health 同口径:本地可判 + milvus/neo4j 经 mcp-go)。 services := gin.H{"gateway": true, "nats": true, "db": h.db.Enabled(), "redis": h.cache.Enabled(), "milvus": false, "neo4j": false} diff --git a/sundynix-gateway/internal/store/pgsql.go b/sundynix-gateway/internal/store/pgsql.go index 2bd7fab..d7c9fdd 100644 --- a/sundynix-gateway/internal/store/pgsql.go +++ b/sundynix-gateway/internal/store/pgsql.go @@ -275,19 +275,8 @@ func (p *Postgres) StatsOverview(ctx context.Context, owner string) *Overview { return o } -// RecentTasks 返回最近 n 条任务(工作台「近期运行」feed)。 -// RecentTasks 返回某用户最近 n 条任务(个人工作台「近期运行」feed)。 -// owner 过滤"我的运行";tenant 由插件自动叠加(双保险:跨用户/跨租户都隔离)。 -func (p *Postgres) RecentTasks(ctx context.Context, owner string, n int) []Task { - if p.db == nil { - return nil - } - var out []Task - p.db.WithContext(ctx).Where("owner = ?", owner).Order("created_at desc").Limit(n).Find(&out) - return out -} - // RunRow 是「运行历史」一行:任务 + 其评测(LEFT JOIN,未评则 level 空)。 +// 工作台「最近任务」与「运行」页共用它 —— 同一份数据只能有一个查法。 type RunRow struct { TaskID string `json:"task_id"` Status string `json:"status"` @@ -308,8 +297,8 @@ func (p *Postgres) RecentRuns(ctx context.Context, owner string, n int) []RunRow } var out []RunRow q := p.db.WithContext(ctx).Table("sundynix_task as t"). - Select("t.task_id, t.status, t.detail, t.created_at as at, " + - "coalesce(e.level,'') as eval_level, coalesce(e.overall,0) as eval_overall, " + + Select("t.task_id, t.status, t.detail, t.created_at as at, "+ + "coalesce(e.level,'') as eval_level, coalesce(e.overall,0) as eval_overall, "+ "coalesce(t.graph->>'topic','') as topic"). Joins("left join sundynix_eval e on e.task_id = t.task_id"). Where("t.deleted_at is null AND t.owner = ?", owner)