fix(chat): fold natal reports and drop homepage topic cards
Independent Staging Quality Gate / validate (push) Failing after 9m47s
Independent Staging Quality Gate / publish (push) Skipped

Homepage no longer waits on /api/onboarding for six starter questions.
Natal answers show the spoken layer first; the Level 2 skeleton sits in a
collapsed 完整分析 block. Wide tables scroll sideways on a phone. Form
inputs are 16px so iOS does not zoom on focus.
This commit is contained in:
jesse-ux
2026-09-15 12:27:16 +08:00
parent 899f955d4b
commit 8144fca27d
46 changed files with 966 additions and 2303 deletions
+60
View File
@@ -0,0 +1,60 @@
/**
* Split a chat answer into the spoken layer (before the first H2) and the
* report skeleton (from that H2 onward). The cut is any ATX H2, so a drifted
* first heading still folds instead of dumping the whole body.
*
* Fenced `##` lines are not cuts. The fence rule matches `chat-markdown-split`.
*/
export type SpokenAndReportSplit = Readonly<{
spoken: string;
report: string;
headings: readonly string[];
}>;
const FENCE = /^\s{0,3}(`{3,}|~{3,})/;
const H2 = /^##\s+\S/;
function extractHeadings(report: string): string[] {
const lines = report.split("\n");
let insideFence = false;
const headings: string[] = [];
for (const line of lines) {
if (FENCE.test(line)) insideFence = !insideFence;
if (!insideFence && H2.test(line)) {
headings.push(line.replace(/^##\s+/, "").trim());
}
}
return headings;
}
export function splitSpokenAndReport(text: string): SpokenAndReportSplit {
const lines = text.split("\n");
let insideFence = false;
let cut = -1;
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index] ?? "";
if (FENCE.test(line)) insideFence = !insideFence;
if (!insideFence && H2.test(line)) {
cut = index;
break;
}
}
if (cut < 0) return { spoken: text, report: "", headings: [] };
const spoken = lines.slice(0, cut).join("\n").replace(/\s+$/u, "");
const report = lines.slice(cut).join("\n");
return { spoken, report, headings: extractHeadings(report) };
}
export function shortenAnswerHeading(heading: string): string {
if (heading === "统一参数与原始结构") return "统一参数";
if (heading === "技法审计表") return "技法审计";
return heading;
}
export function answerDetailCaption(headings: readonly string[]): string {
if (headings.length === 0) return "";
const labels = headings.slice(0, 3).map(shortenAnswerHeading);
if (headings.length <= 3) return labels.join("、");
return `${labels.join("、")}${headings.length}`;
}