feat(call2): short/long desc structure in seed (not brand voice)

Call 2 seed now requires benefit-first short + four-part long HTML, returned
as JSON. POST parses and persists both fields; Create-desc gate no longer
requires short (Call 2 writes it). Brand voice/geo stay Settings-injected.
This commit is contained in:
grokbot_ty
2026-09-26 10:35:53 -07:00
parent 076eb17db6
commit c25e3f4927
8 changed files with 242 additions and 41 deletions
+2
View File
@@ -364,6 +364,8 @@ export interface Call1Result {
export interface Call2Result {
ok: boolean;
description: string;
/** Present when Call 2 returned a parsed short_description. */
short_description?: string;
}
/** Call 1 OUT (overnight-whole-app): always fails with 410 call1_disabled. Prefer manual taxonomy. */
@@ -3,6 +3,7 @@
let contentEnabled = false;
let reachable = false;
let description: string | null = null;
let shortDescription: string | null = null;
let error = '';
let busy = false;
@@ -29,10 +30,14 @@
busy = true;
error = '';
description = null;
shortDescription = null;
try {
const res = await fetch(`/api/v1/slabs/${encodeURIComponent(slabId)}/inference/call2`, { method: 'POST' });
const body = await res.json();
if (res.ok) description = body.description;
if (res.ok) {
description = body.description;
shortDescription = body.short_description ?? null;
}
else error = body.error?.message || `call2 failed (${res.status})`;
} catch {
error = 'network error';
@@ -61,7 +66,8 @@
<button on:click={generate} disabled={busy}>{busy ? 'Working…' : 'Generate description (Call 2)'}</button>
</div>
{#if error}<p class="err">{error}</p>{/if}
{#if description}<p class="desc">{description}</p>{/if}
{#if shortDescription}<p class="desc"><strong>Short:</strong> {shortDescription}</p>{/if}
{#if description}<p class="desc"><strong>Long:</strong> {description}</p>{/if}
{/if}
<button class="mini" on:click={checkReadiness}>Recheck readiness</button>
</div>
+8 -7
View File
@@ -136,19 +136,18 @@
let reviewError = '';
// #89: explicit "Create" actions. `createBusy` covers the deterministic
// title/short-desc generation; `canCreateTitle` is its gate (species already
// on the slab from Measure + ≥1 wood category). `canCreateDesc` gates the AI
// long-description on title+short-desc being populated AND the content LLM
// being enabled.
// on the slab from Measure + ≥1 wood category). `canCreateDesc` gates Call 2
// (writes short + long) on title + taxonomy + content LLM — not short/desc/price.
let createBusy = false;
let createError = '';
// Prefer slab.species_id; fall back to Capture handoff if createSlab omitted it.
$: resolvedSpeciesId = effectiveSpeciesId(slab?.species_id, capture?.speciesId ?? null);
$: canCreateTitle = canCreateTitleFromSpecies(resolvedSpeciesId, woodCatIds.length);
// Create-desc gate: recompute on every field change (no blur required). Description and price not required.
// Create-desc gate: recompute on every field change (no blur required).
// Short/description are Call 2 outputs; price is publish-only.
$: createDescMissing = missingCreateDescLabels({
contentLlmEnabled: canGenerateContent,
title,
shortDescription,
speciesId: resolvedSpeciesId,
woodCategoryIds: woodCatIds,
primaryCatId,
@@ -541,10 +540,12 @@
try {
const r = await inferContent(slabId);
description = r.description;
if (r.short_description) shortDescription = r.short_description;
contentSource = 'llm';
// commitReview() no longer sends description (see #89), so persist it explicitly.
// Persist Call 2 short + long (commitReview does not send description — see #89).
const myToken = ++commitToken;
const fresh = await updateSlab(slabId, {
short_description: shortDescription || null,
description: description || null,
content_source: contentSource || null,
client_rev: slab.server_rev,
@@ -859,7 +860,7 @@
data-testid="create-desc"
on:click={generateContent}
disabled={!canCreateDesc || genBusy}
title={canCreateDesc ? 'Generate long description (Call 2)' : `Missing: ${createDescMissing.join(', ')}`}
title={canCreateDesc ? 'Generate short + long description (Call 2)' : `Missing: ${createDescMissing.join(', ')}`}
>
{genBusy ? 'Writing…' : 'Create desc'}
</button>
@@ -9,7 +9,6 @@ import {
const full = {
contentLlmEnabled: true,
title: 'Figured Walnut Live Edge Slab',
shortDescription: 'Figured Walnut Live Edge Slab, 9ft …',
speciesId: 11,
woodCategoryIds: [21],
primaryCatId: 21,
@@ -19,7 +18,7 @@ const full = {
moistureTermId: '326',
};
test('Create desc gate: full set passes; does not require description', () => {
test('Create desc gate: full set passes; does not require short or description', () => {
assert.equal(canCreateDesc(full), true);
assert.deepEqual(missingCreateDesc(full), []);
});
@@ -30,12 +29,11 @@ test('Create desc gate: empty price still passes when other fields full', () =>
assert.deepEqual(missingCreateDesc(full), []);
});
test('Create desc gate: lists each missing field; no description or price key', () => {
test('Create desc gate: lists each missing field; no short/description/price key', () => {
const miss = missingCreateDesc({
...full,
contentLlmEnabled: false,
title: '',
shortDescription: ' ',
speciesId: null,
woodCategoryIds: [],
primaryCatId: '',
@@ -47,7 +45,6 @@ test('Create desc gate: lists each missing field; no description or price key',
assert.deepEqual(miss, [
'content_llm',
'title',
'short_description',
'species',
'wood_categories',
'primary',
@@ -56,6 +53,7 @@ test('Create desc gate: lists each missing field; no description or price key',
'grade',
'moisture',
]);
assert.equal(miss.includes('short_description'), false);
assert.equal(miss.includes('description'), false);
assert.equal(miss.includes('price'), false);
const labels = missingCreateDescLabels({
+2 -4
View File
@@ -1,12 +1,12 @@
/**
* Create-desc gate (Call 2): disable Create desc unless these are present.
* Does NOT require description OR price (price stays publish-only).
* Does NOT require short_description, description, or price — Call 2 writes
* short + long; price stays publish-only.
*/
export type CreateDescInput = {
contentLlmEnabled: boolean;
title: string;
shortDescription: string;
speciesId: number | null | undefined;
woodCategoryIds: number[];
primaryCatId: number | '' | null | undefined;
@@ -20,7 +20,6 @@ export type CreateDescInput = {
export const CREATE_DESC_LABELS: Record<string, string> = {
content_llm: 'content LLM on',
title: 'title',
short_description: 'short description',
species: 'species',
wood_categories: '≥1 wood category',
primary: 'primary wood category',
@@ -41,7 +40,6 @@ export function missingCreateDesc(input: CreateDescInput): string[] {
const missing: string[] = [];
if (!input.contentLlmEnabled) missing.push('content_llm');
if (!String(input.title ?? '').trim()) missing.push('title');
if (!String(input.shortDescription ?? '').trim()) missing.push('short_description');
if (input.speciesId == null || !Number.isFinite(Number(input.speciesId))) missing.push('species');
if (!input.woodCategoryIds?.length) missing.push('wood_categories');
if (!presentId(input.primaryCatId)) missing.push('primary');