feat(web): Scope B B2 — 工具箱原文输入 + 4 新生成器预览渲染
buildGenerateRequest 转发 text(扩写/降AI/拆书原文,text_input 策略;空白省略)。
mapPreview 处理单对象产物:续写/扩写/降AI({text}→单 body 卡)、拆书
BookTeardownResult(structure 作标题 + themes/archetypes/hooks 列表字段)——
绕过 firstArrayEntry 的「记录列表」假设,使 4 新生成器声明驱动表单/预览自动可渲染。
原文 textarea 字段(name=text)由既有 FormField 渲染,无需改组件。
This commit is contained in:
@@ -74,6 +74,18 @@ describe("buildGenerateRequest", () => {
|
||||
it("defaults brief to empty string", () => {
|
||||
expect(buildGenerateRequest({})).toEqual({ brief: "" });
|
||||
});
|
||||
|
||||
it("forwards trimmed text (扩写/降AI/拆书 原文输入)", () => {
|
||||
expect(
|
||||
buildGenerateRequest({ brief: "扩写", text: " 原始正文片段 " }),
|
||||
).toEqual({ brief: "扩写", text: "原始正文片段" });
|
||||
});
|
||||
|
||||
it("omits blank/whitespace-only text", () => {
|
||||
expect(buildGenerateRequest({ brief: "x", text: " " })).toEqual({
|
||||
brief: "x",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("previewRows", () => {
|
||||
@@ -137,6 +149,37 @@ describe("mapPreview", () => {
|
||||
it("yields no items for empty preview", () => {
|
||||
expect(mapPreview("IdeaListResult", undefined).items).toEqual([]);
|
||||
});
|
||||
|
||||
it("maps a text-only result (续写/扩写/降AI) into a single body item", () => {
|
||||
for (const kind of ["ContinuationResult", "PolishResult", "DeAiResult"]) {
|
||||
const model = mapPreview(kind, { text: "一段续写/改写后的正文" });
|
||||
expect(model.items).toHaveLength(1);
|
||||
expect(model.items[0]?.heading).toBeNull();
|
||||
expect(model.items[0]?.body).toBe("一段续写/改写后的正文");
|
||||
}
|
||||
});
|
||||
|
||||
it("maps BookTeardownResult structured fields into one card", () => {
|
||||
const model = mapPreview("BookTeardownResult", {
|
||||
themes: ["逆袭", "复仇"],
|
||||
archetypes: ["废柴主角"],
|
||||
structure: "三幕递进",
|
||||
hooks: ["开局打脸", "扮猪吃虎"],
|
||||
});
|
||||
expect(model.items).toHaveLength(1);
|
||||
const item = model.items[0];
|
||||
expect(item?.heading).toBe("三幕递进");
|
||||
const byLabel = (label: string) =>
|
||||
item?.fields.find((f) => f.label === label)?.value;
|
||||
expect(byLabel("主题")).toBe("逆袭;复仇");
|
||||
expect(byLabel("人物原型")).toBe("废柴主角");
|
||||
expect(byLabel("钩子套路")).toBe("开局打脸;扮猪吃虎");
|
||||
});
|
||||
|
||||
it("yields no items for an empty text result", () => {
|
||||
expect(mapPreview("ContinuationResult", { text: "" }).items).toEqual([]);
|
||||
expect(mapPreview("ContinuationResult", undefined).items).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveLegacyRoute", () => {
|
||||
|
||||
@@ -60,6 +60,9 @@ export function buildGenerateRequest(values: FieldValues): ToolGenerateRequest {
|
||||
}
|
||||
const kind = values["kind"]?.trim();
|
||||
if (kind) body.kind = kind;
|
||||
// 原文输入(扩写/降AI/拆书样本,text_input 策略):非空才带(空白省略)。
|
||||
const text = values["text"]?.trim();
|
||||
if (text) body.text = text;
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -142,16 +145,61 @@ export function previewRows(
|
||||
return asRecordArray(firstArrayEntry(preview));
|
||||
}
|
||||
|
||||
// 单对象产物(非列表)的 output_kind:续写/扩写/降AI(纯正文 {text})+ 拆书(结构化对象)。
|
||||
// 这些 preview 本身就是一个对象(无顶层数组),故不走 firstArrayEntry 的「记录列表」假设,
|
||||
// 而归一为「单张卡」(Scope B 竞品快赢生成器)。
|
||||
const SINGLE_OBJECT_KINDS = new Set([
|
||||
"ContinuationResult",
|
||||
"PolishResult",
|
||||
"DeAiResult",
|
||||
"BookTeardownResult",
|
||||
]);
|
||||
|
||||
// output_kind → 行渲染策略。未知 kind 走兜底(heading=首个字符串字段,其余键值列出)。
|
||||
export function mapPreview(
|
||||
outputKind: string,
|
||||
preview: Record<string, unknown> | undefined,
|
||||
): PreviewModel {
|
||||
if (SINGLE_OBJECT_KINDS.has(outputKind)) {
|
||||
const item = singleObjectItem(outputKind, preview);
|
||||
return { kind: outputKind, items: item ? [item] : [] };
|
||||
}
|
||||
const rows = asRecordArray(firstArrayEntry(preview));
|
||||
const items = rows.map((row) => previewItem(outputKind, row));
|
||||
return { kind: outputKind, items };
|
||||
}
|
||||
|
||||
// 把单对象产物归一为一张预览卡:纯正文类 → body;拆书 → structure 作 heading + 列表字段。
|
||||
// preview 缺失/正文为空 → 返 null(mapPreview 据此给空 items,不渲空卡)。
|
||||
function singleObjectItem(
|
||||
outputKind: string,
|
||||
preview: Record<string, unknown> | undefined,
|
||||
): PreviewItem | null {
|
||||
if (!preview) return null;
|
||||
if (outputKind === "BookTeardownResult") {
|
||||
return teardownItem(preview);
|
||||
}
|
||||
const text = asString(preview["text"]);
|
||||
return text ? { heading: null, fields: [], body: text } : null;
|
||||
}
|
||||
|
||||
// 拆书结构化产物 → 一张卡:structure 作标题,themes/archetypes/hooks 各拼成一行字段。
|
||||
function teardownItem(preview: Record<string, unknown>): PreviewItem {
|
||||
const listField = (key: string, label: string): PreviewField[] => {
|
||||
const joined = asStringArray(preview[key]).join(";");
|
||||
return joined ? [{ label, value: joined }] : [];
|
||||
};
|
||||
return {
|
||||
heading: asString(preview["structure"]) || null,
|
||||
fields: [
|
||||
...listField("themes", "主题"),
|
||||
...listField("archetypes", "人物原型"),
|
||||
...listField("hooks", "钩子套路"),
|
||||
],
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
|
||||
function previewItem(
|
||||
outputKind: string,
|
||||
row: Record<string, unknown>,
|
||||
|
||||
Reference in New Issue
Block a user