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:
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user