From bbd96d3b6bc537eec663f8052f84de8afed5c5f5 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Wed, 23 Sep 2026 10:42:17 +0800 Subject: [PATCH] feat(report): add dense personal report appendix Co-Authored-By: Claude Code --- BLOCKED.md | 10 + CHANGELOG.md | 8 + .../report-document.v2.schema.json | 119 +- docs/tasks/README.md | 2 +- frontend/DESIGN.md | 4 + .../reports/[reportId]/raw-appendix/route.ts | 48 + frontend/src/app/api/reports/route.ts | 5 +- frontend/src/app/globals.css | 8 + .../personal-report/personal-report-page.tsx | 12 +- .../personal-report/raw-appendix-download.tsx | 35 + .../personal-report/report-actions.tsx | 2 + .../report-calculation-charts.tsx | 20 + .../personal-report/report-fact-tables.tsx | 41 + frontend/src/lib/personal-report-contract.ts | 4 +- .../src/lib/personal-report-generation.ts | 45 +- .../src/lib/personal-report-longform-cover.ts | 5 +- .../lib/personal-report-longform-generate.ts | 87 +- .../lib/personal-report-longform-snapshot.ts | 67 + frontend/src/lib/personal-report-plan.ts | 2 + .../src/lib/personal-report-raw-appendix.ts | 13 + .../src/lib/personal-report-route-core.ts | 5 + .../personal-report-section-service-core.ts | 3 +- frontend/src/lib/reader-appendix-language.ts | 27 + frontend/src/lib/report-evidence-bundle-v2.ts | 7 +- frontend/src/lib/report-fact-table-schema.ts | 27 + frontend/src/lib/report-fact-tables.ts | 128 + frontend/src/lib/report-public-projection.ts | 12 + frontend/src/mastra/personal-report.ts | 12 +- .../database-personal-report-sections.test.ts | 25 + .../report-density-fictional-engine.json | 25705 ++++++++++++++++ .../report-density-fictional-reader.json | 21687 +++++++++++++ frontend/tests/personal-report-api.test.ts | 3 +- .../personal-report-density-retry.test.ts | 142 + .../personal-report-generation-v2.test.ts | 1 + .../personal-report-raw-appendix.test.ts | 112 + frontend/tests/report-fact-tables.test.ts | 70 + scripts/personal_report_contract.py | 11 +- scripts/professional_report_reference.py | 12 + scripts/reader_appendix_language.py | 45 + scripts/reader_dasha_applicability.py | 73 + scripts/report_density_packet.py | 19 + scripts/report_fact_table_contract.py | 61 + tests/test_reader_appendix_language.py | 70 + tests/test_report_density_facts.py | 89 + 44 files changed, 48833 insertions(+), 50 deletions(-) create mode 100644 frontend/src/app/api/reports/[reportId]/raw-appendix/route.ts create mode 100644 frontend/src/components/personal-report/raw-appendix-download.tsx create mode 100644 frontend/src/components/personal-report/report-calculation-charts.tsx create mode 100644 frontend/src/components/personal-report/report-fact-tables.tsx create mode 100644 frontend/src/lib/personal-report-longform-snapshot.ts create mode 100644 frontend/src/lib/personal-report-raw-appendix.ts create mode 100644 frontend/src/lib/reader-appendix-language.ts create mode 100644 frontend/src/lib/report-fact-table-schema.ts create mode 100644 frontend/src/lib/report-fact-tables.ts create mode 100644 frontend/tests/fixtures/report-density-fictional-engine.json create mode 100644 frontend/tests/fixtures/report-density-fictional-reader.json create mode 100644 frontend/tests/personal-report-density-retry.test.ts create mode 100644 frontend/tests/personal-report-raw-appendix.test.ts create mode 100644 frontend/tests/report-fact-tables.test.ts create mode 100644 scripts/reader_appendix_language.py create mode 100644 scripts/reader_dasha_applicability.py create mode 100644 scripts/report_density_packet.py create mode 100644 scripts/report_fact_table_contract.py create mode 100644 tests/test_reader_appendix_language.py create mode 100644 tests/test_report_density_facts.py diff --git a/BLOCKED.md b/BLOCKED.md index 47f66492..2599e791 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,15 @@ # BLOCKED +## 报告密度 BUG-1003~1007 验收缺口(2026-09-23) + +- 本机 Windows build 编译/类型通过,但 page-data 的 SKILL.md 符号链接 EPERM 与基线一致;Static 与首屏 gzip 未验收。 +- Docker 存在但地址池耗尽;没有删除他会话网络。前端全部失败名按基线逐项比对,见 PROGRESS 和失败清单。 +- 预检缺 worktree 镜像目录;未复制镜像、未修改系统权限。quick 初次受 GBK 输出编码阻塞,UTF-8 重跑结果见 PROGRESS。 +- 缺本轮受控登录态与浏览器验收,未借用账号。清单:`docs/testing/report-density-20260922.md`。 +- 源 packet 只有 Sade Sati 三阶段框架,没有三轮日期;该验收项未满足。新计算到附录缓存成功、最终报告落库失败后的重试只有 Markdown 缓存,不补算事实层;恢复结构化层的持久重试尚未实现。 +- Task 5 适用性说明仅随新报告产生;旧缓存不升级,因此旧附录没有新增说明。未提交、推送、部署。 + + ## BUG-1000:聊天人物选择缺少受控登录态(2026-09-22) - 本轮没有登录态,也没有 Chrome。顶栏点击、焦点回收和跨设备刷新不能写成通过。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f00dc06..38e581b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 印度占星 Skill 更新日志 +## 2026-09-23 — 报告增加独立原始附录与计算核对层(未部署) + +- 原始附录独立下载,保留原始表格,清理工程用语;普通 Markdown 下载不变。 +- 新生成长报告附带服务端事实表与最多 16 张真实分盘;旧缓存不重算、不升级。 +- 辅助大运说明仅供交叉核对;Sade Sati 三轮日期仍缺源数据,未宣称完成。 +- Skill 版本不变。尚未提交、推送或部署;完整限制见本轮 PROGRESS。 + + ## 2026-09-22 — 今日星语和生时校正入口边框变轻 首页两枚入口仍是 36px 的按钮,点按区域保持 44px。默认边框改成更淡的 1px,悬停不再换成强调色,键盘焦点仍是更清楚的 3px 描边。Skill 版本不变。 diff --git a/contracts/personal-report/report-document.v2.schema.json b/contracts/personal-report/report-document.v2.schema.json index e239c3ff..b7c1e987 100644 --- a/contracts/personal-report/report-document.v2.schema.json +++ b/contracts/personal-report/report-document.v2.schema.json @@ -109,13 +109,20 @@ "enum": [ "D1", "D2", - "D6", - "D8", + "D3", + "D4", + "D7", "D9", "D10", "D11", + "D12", + "D16", + "D20", "D24", - "D30" + "D27", + "D30", + "D40", + "D60" ] }, "title": { @@ -434,6 +441,103 @@ "evidenceRefs", "claimStatus" ] + }, + "factTable": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "title", + "claimStatus", + "sourcePath", + "note", + "columns", + "rows" + ], + "properties": { + "id": { + "enum": [ + "vimshottari", + "ashtakavarga", + "shadbala", + "functional_roles", + "avasthas", + "special_points", + "sade_sati", + "annual" + ] + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "claimStatus": { + "$ref": "#/definitions/claimStatus" + }, + "sourcePath": { + "type": "string", + "minLength": 1, + "maxLength": 400, + "pattern": "^[A-Za-z0-9_.\\[\\]-]+$" + }, + "note": { + "type": "string", + "maxLength": 500 + }, + "columns": { + "type": "array", + "minItems": 1, + "maxItems": 16, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "rows": { + "type": "array", + "maxItems": 2000, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "sourcePath", + "cells" + ], + "properties": { + "sourcePath": { + "type": "string", + "minLength": 1, + "maxLength": 400, + "pattern": "^[A-Za-z0-9_.\\[\\]-]+$" + }, + "cells": { + "type": "array", + "minItems": 1, + "maxItems": 16, + "items": { + "anyOf": [ + { + "type": "string", + "maxLength": 300 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } } }, "additionalProperties": false, @@ -739,7 +843,7 @@ "$ref": "#/definitions/chart" }, "minItems": 1, - "maxItems": 9, + "maxItems": 16, "description": "maxItems must equal CHART_IDS.length in frontend/src/lib/personal-report-contract.ts" }, "thematicNarrative": { @@ -806,6 +910,13 @@ "type": "string", "minLength": 1, "maxLength": 2000 + }, + "factTables": { + "type": "array", + "maxItems": 8, + "items": { + "$ref": "#/definitions/factTable" + } } }, "required": [ diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 93d747f7..ab18e6e2 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -169,7 +169,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | -| `TASK-report-density-20260922.md` | — | **报告信息密度与原始附录(BUG-1003~1007)**:与源头仓 PL9 全量报告同资料实测:对照 847,632 字符 / 8,044 表格行,我方 309,357 / 1,478。缺口不在引擎——20 张分盘全表、KP 四表、Ashtakavarga 四类、Shadbala 六分量、Avastha、Sahams、年度都已算得出,但 `personal-report-contract.ts` 的 `REPORT_SECTION_KINDS` **没有表格类型**、`CHART_IDS` 只有 9 种,这些表在产品页报告里没有落脚字段。另测出对外原始附录泄漏 925 种 / 3,797 处工程标识符(`parameter_sensitive` 697、`cmd_full_reading` 68、`PyJHora`/`JHora` 35、`PL9` 页码 59),对照物同项为 0。**产品 2026-09-22 三点拍板**:层 1 按 8 组表 + 分盘扩到 16 张;层 2 原始附录直接给 C 端且先全显示;B 类大运族要显示。**决策记录已写明推翻 BUG-999 的两条红线**(专业参考不得作普通下载 fallback;`parameter_sensitive`/「参数敏感」命中整行剔除)——**仅限附录通道**,普通正文投影不动。硬红线:表格层服务端装配、writer 不得写表;对照物的替代大运族日期与 Shadbala 分量在源头仍未闭环(506 行 date mismatch、`production_tuning_allowed=false`),不得照搬升级。让步顺序与串行依赖(任务 3 → 任务 4 同改契约)见任务书 | **待领取** | 分支 `codex/report-density-20260922`;任务 1 参考实现已在该分支本地提交 `cf7e26e4`(未推送,29 passed) | +| `TASK-report-density-20260922.md` | `PROGRESS-report-density-20260922.md` | **报告信息密度与原始附录(BUG-1003~1007)**:与源头仓 PL9 全量报告同资料实测:对照 847,632 字符 / 8,044 表格行,我方 309,357 / 1,478。缺口不在引擎——20 张分盘全表、KP 四表、Ashtakavarga 四类、Shadbala 六分量、Avastha、Sahams、年度都已算得出,但 `personal-report-contract.ts` 的 `REPORT_SECTION_KINDS` **没有表格类型**、`CHART_IDS` 只有 9 种,这些表在产品页报告里没有落脚字段。另测出对外原始附录泄漏 925 种 / 3,797 处工程标识符(`parameter_sensitive` 697、`cmd_full_reading` 68、`PyJHora`/`JHora` 35、`PL9` 页码 59),对照物同项为 0。**产品 2026-09-22 三点拍板**:层 1 按 8 组表 + 分盘扩到 16 张;层 2 原始附录直接给 C 端且先全显示;B 类大运族要显示。**决策记录已写明推翻 BUG-999 的两条红线**(专业参考不得作普通下载 fallback;`parameter_sensitive`/「参数敏感」命中整行剔除)——**仅限附录通道**,普通正文投影不动。硬红线:表格层服务端装配、writer 不得写表;对照物的替代大运族日期与 Shadbala 分量在源头仍未闭环(506 行 date mismatch、`production_tuning_allowed=false`),不得照搬升级。让步顺序与串行依赖(任务 3 → 任务 4 同改契约)见任务书 | **待领取** | 分支 `codex/report-density-20260922`;任务 1 参考实现已在该分支本地提交 `cf7e26e4`(未推送,29 passed) | | `TASK-report-sectioned-generation-20260830.md` | `PROGRESS-report-sectioned-20260830.md` | 分章节生成 | 已合入 | 见 PROGRESS | | `TASK-report-skill-parity-20260901.md` | `PROGRESS-report-skill-parity-20260901.md` | 内容对齐 skill 解读深度 | 已验收 | `90bad10d`、`ef1bd6df` | | `TASK-report-blocked-repairs-20260902.md` | `PROGRESS-report-blocked-repairs-20260902.md` | 全主题 blocked 修复 | 已验收 | `7faf8555` | diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index e60f2650..98aafd26 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -2,6 +2,10 @@ This file adapts the full visual analysis in `CLAUDE_DESIGN.md` to the shipped Jyotisha application. `CLAUDE_DESIGN.md` remains the upstream reference; this file is the implementation contract. +## 报告核对层与独立附录(2026-09-23) + +个人报告保留原 Markdown 阅读、目录隔离与 memo 边界;文后独立渲染服务端分盘和事实表。分盘复用 VedicChartSvg 与 chart-grid/chart-card,不使用 float 或负 margin。事实表默认折叠、可键盘展开,横向溢出仅在表格区域滚动,缺数据不补写。Sade Sati 明确缺三轮日期。操作区增加「下载原始附录」,有独立忙态和简短错误提示,不接普通下载 fallback、不增加 spinner。旧缓存不升级、不重算;没有结构化层就不显示占位盘。文案遵守 VOICE:不把可计算写成已验证。 + ## 校正跨午夜日期与采用(2026-09-20) 复用现有时段选择卡追问一次午夜前/后;不知道或跳过保留填报当天凌晨与深夜两段,不补白昼或次日。选项用中文钟点,继续遵守题卡禁止暴露数字钟点的现有校验,不新增入口、状态或等待动画。 diff --git a/frontend/src/app/api/reports/[reportId]/raw-appendix/route.ts b/frontend/src/app/api/reports/[reportId]/raw-appendix/route.ts new file mode 100644 index 00000000..58e4b1ed --- /dev/null +++ b/frontend/src/app/api/reports/[reportId]/raw-appendix/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; + +import { LONGFORM_APPENDIX_TABLE, parseLongformAppendixRow } from "@/lib/personal-report-longform-appendix"; +import { checkSameOrigin, resolveAllowedReportOrigins } from "@/lib/personal-report-entitlement"; +import { createSupabasePersonalReportService } from "@/lib/personal-report-service"; +import { releaseRawReportAppendix } from "@/lib/personal-report-raw-appendix"; +import { readLongformSnapshot, type LongformSnapshotReader } from "@/lib/personal-report-longform-snapshot"; +import { isSupabaseConfigurationError } from "@/lib/supabase/config"; +import { createServerSupabaseClient } from "@/lib/supabase/server"; + +export const runtime = "nodejs"; +const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const privateHeaders = { "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" }; +const json = (body: unknown, status = 200) => NextResponse.json(body, { status, headers: privateHeaders }); + +type RouteContext = { params: Promise<{ reportId: string }> }; + +/** Cache-only attachment: no birth input, recalculation, or alternate subject selection. */ +export async function POST(request: Request, context: RouteContext) { + try { + const supabase = await createServerSupabaseClient(); + const { data: { user }, error } = await supabase.auth.getUser(); + if (error || !user) return json({ error: "请先登录" }, 401); + const origin = checkSameOrigin(request.url, request.headers.get("origin"), resolveAllowedReportOrigins(process.env), request.headers); + if (!origin.ok) return json({ error: "此处无法下载,请回到报告页重试。" }, 403); + const { reportId } = await context.params; + if (!uuidPattern.test(reportId)) return json({ error: "报告不存在" }, 404); + const report = await createSupabasePersonalReportService(supabase).getOwnedById(user.id, reportId); + if (!report) return json({ error: "报告不存在" }, 404); + if (report.status !== "ready") return json({ error: "报告尚未完成" }, 409); + const result = await supabase.from(LONGFORM_APPENDIX_TABLE) + .select("report_id,user_id,request_id,status,markdown,content_sha256,attempt_count,last_error_code") + .eq("report_id", reportId).eq("user_id", user.id).eq("request_id", report.requestId) + .maybeSingle(); + const cached = result.error ? null : parseLongformAppendixRow(result.data); + if (!cached || cached.reportId !== reportId || cached.userId !== user.id || cached.requestId !== report.requestId + || cached.status !== "ready" || !cached.markdown) { + return json({ error: "这份报告暂无原始附录。" }, 410); + } + const stored = await readLongformSnapshot(supabase as unknown as LongformSnapshotReader, { reportId, userId: user.id, requestId: report.requestId }); + if (stored && (stored.status !== "ready" || stored.snapshot.markdown !== cached.markdown)) { + return json({ error: "原始附录暂时无法下载,请稍后重试。" }, 503); + } + return json(releaseRawReportAppendix(cached.markdown, stored?.status === "ready" ? stored.snapshot.readerDashaApplicability : undefined)); + } catch (error) { + return json({ error: "原始附录暂时无法下载,请稍后重试。" }, isSupabaseConfigurationError(error) ? 503 : 500); + } +} diff --git a/frontend/src/app/api/reports/route.ts b/frontend/src/app/api/reports/route.ts index b36307c5..f3a75c44 100644 --- a/frontend/src/app/api/reports/route.ts +++ b/frontend/src/app/api/reports/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { LONGFORM_SNAPSHOT_SECTION_ID } from "@/lib/personal-report-longform-snapshot"; import { resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone"; import { runConsultationWorkflow } from "@/mastra"; import { createPersonalReportAgent } from "@/mastra/personal-report"; @@ -108,14 +109,14 @@ export async function GET() { if (failed.length > 0) { const { data: sections, error: sectionError } = await supabase .from("personal_report_sections") - .select("request_id, status, last_error_code") + .select("request_id, section_id, status, last_error_code") .eq("user_id", user.id) .in("request_id", failed.map((report) => report.requestId)); if (sectionError) throw sectionError; const grouped = new Map>(); for (const row of Array.isArray(sections) ? sections : []) { const requestId = typeof row.request_id === "string" ? row.request_id : ""; - if (!requestId) continue; + if (!requestId || row.section_id === LONGFORM_SNAPSHOT_SECTION_ID) continue; const list = grouped.get(requestId) ?? []; list.push({ status: typeof row.status === "string" ? row.status : "pending", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 04f79a2f..3dc9523f 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,6 +1,14 @@ @import "tailwindcss"; @import "tw-animate-css"; +.personal-report-fact-tables { margin-block: 2rem; min-width: 0; } +.personal-report-fact-tables details { margin-block: 1rem; padding: 1rem; border: 1px solid var(--color-border); border-radius: 0.5rem; } +.personal-report-fact-tables summary { cursor: pointer; min-height: 2.75rem; } +.personal-report-fact-tables table { width: 100%; border-collapse: collapse; } +.personal-report-fact-tables th, .personal-report-fact-tables td { padding: 0.5rem; text-align: start; border-bottom: 1px solid var(--color-border); overflow-wrap: anywhere; } +@media print { .personal-report-fact-tables details { break-inside: auto; } } + + @theme inline { --color-background: var(--color-canvas); --color-foreground: var(--color-ink); diff --git a/frontend/src/components/personal-report/personal-report-page.tsx b/frontend/src/components/personal-report/personal-report-page.tsx index b016abda..5fdd5820 100644 --- a/frontend/src/components/personal-report/personal-report-page.tsx +++ b/frontend/src/components/personal-report/personal-report-page.tsx @@ -19,6 +19,10 @@ import { InlineSpinner } from "@/components/inline-spinner"; import { SecondaryHeader } from "@/components/secondary-header"; import { ReportActions } from "./report-actions"; +import { ReportFactTables } from "./report-fact-tables"; +import { ReportCalculationCharts } from "./report-calculation-charts"; +import { reportDocumentV2Schema, type ReportDocumentV2 } from "@/lib/personal-report-contract"; +import { reportFactTablesSchema, type ReportFactTable } from "@/lib/report-fact-table-schema"; import { PersonalReportMarkdownView } from "./personal-report-markdown-view"; import { PersonalReportProgressPanel } from "./personal-report-progress-panel"; import { @@ -50,7 +54,7 @@ export type ReportLoadState = | { phase: "failed"; failureCode: string | null; failureSummary?: string | null; appendixLastErrorCode?: string | null } | { phase: "invalid"; message: string } | { phase: "network-error" } - | { phase: "markdown-ready"; markdown: string; reportId: string; createdAt: string } + | { phase: "markdown-ready"; markdown: string; reportId: string; createdAt: string; factTables?: ReportFactTable[]; calculationCharts?: ReportDocumentV2["charts"] } | { phase: "legacy-unavailable" }; /** GET /api/reports/:id envelope view (mirrors the API route's reportView). */ @@ -125,7 +129,11 @@ export function classifyReportEnvelope(statusCode: number, json: unknown): Repor const stored = typeof json.longformMarkdown === "string" ? json.longformMarkdown : ""; const markdown = projectOrdinaryReportMarkdown(stored).trim(); if (markdown.length > 0) { + const facts = reportFactTablesSchema.safeParse(json.factTables); + const charts = reportDocumentV2Schema.shape.charts.safeParse(json.calculationCharts); return { + ...(facts.success && facts.data.length ? { factTables: facts.data } : {}), + ...(charts.success ? { calculationCharts: charts.data } : {}), phase: "markdown-ready", markdown, reportId: typeof view.id === "string" ? view.id : "", @@ -475,6 +483,8 @@ export function PersonalReportPage({ reportId }: { reportId: string }) { />
+ {state.calculationCharts && } + {state.factTables && }
diff --git a/frontend/src/components/personal-report/raw-appendix-download.tsx b/frontend/src/components/personal-report/raw-appendix-download.tsx new file mode 100644 index 00000000..2db75298 --- /dev/null +++ b/frontend/src/components/personal-report/raw-appendix-download.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useState } from "react"; +import { FileText } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { downloadMarkdownReport } from "@/lib/consultation-report-export"; + +export function RawAppendixDownload({ reportId }: { reportId: string }) { + const [busy, setBusy] = useState(false); + const [notice, setNotice] = useState(null); + async function download() { + if (busy) return; + setBusy(true); + setNotice(null); + try { + const response = await fetch(`/api/reports/${encodeURIComponent(reportId)}/raw-appendix`, { + method: "POST", credentials: "same-origin", headers: { Accept: "application/json" }, + }); + const result = await response.json(); + if (!response.ok || result.documentKind !== "raw_appendix" || result.format !== "markdown" + || typeof result.markdown !== "string" || !result.markdown.trim()) throw new Error("unavailable"); + downloadMarkdownReport("个人报告-原始附录", result.markdown); + } catch { + setNotice("原始附录暂时无法下载,请稍后重试。"); + } finally { + setBusy(false); + } + } + return <> + + {notice && {notice}} + ; +} diff --git a/frontend/src/components/personal-report/report-actions.tsx b/frontend/src/components/personal-report/report-actions.tsx index 8d7750b0..e7462846 100644 --- a/frontend/src/components/personal-report/report-actions.tsx +++ b/frontend/src/components/personal-report/report-actions.tsx @@ -20,6 +20,7 @@ import { PERSONAL_REPORT_PRINT_LABEL, } from "@/lib/personal-report-longform-copy"; import { Button } from "@/components/ui/button"; +import { RawAppendixDownload } from "./raw-appendix-download"; interface ReportActionsProps { reportId: string; @@ -83,6 +84,7 @@ export function ReportActions({