Spaces:
Running
Running
ui: three explicit construct paths + Save gating + visible AI drafts
Browse filesPI test feedback (2026-08-07):
- Step 2 hint enumerates the three acquisition paths; two buttons next to
the picker ('+ Your own items', 'Draft with AI') replace the generic
'+ Custom construct'
- Save construct is disabled until name + items exist, with an inline
line saying what's missing (the silent dead-click read as broken - the
error banner sits at the top of the page, out of view)
- After drafting, scroll/focus the items box and say 'N items drafted -
they're in the box below' (drafts landed off-screen unnoticed)
- /guide and MANUAL_TESTING.md updated to the new button names
- MANUAL_TESTING.md +13 -3
- backend/app/guide.html +4 -4
- backend/static/assets/index-BJ_J7JUi.js +0 -0
- backend/static/assets/index-T4JgSqf6.js +0 -0
- backend/static/index.html +1 -1
- frontend/src/Workspace.jsx +77 -20
MANUAL_TESTING.md
CHANGED
|
@@ -72,14 +72,14 @@ with a "Sign in (top right)" message. Sign in and retry: accepted.
|
|
| 72 |
|
| 73 |
### Custom construct, typed
|
| 74 |
|
| 75 |
-
1. "+
|
| 76 |
2. Append `(R)` to one line: the form shows "1 item(s) marked reverse-scored".
|
| 77 |
3. Save: it appears in the picker under "My custom constructs"; run metadata
|
| 78 |
will carry the reverse flag (check via Results > metadata download).
|
| 79 |
|
| 80 |
### Custom construct, from file (new)
|
| 81 |
|
| 82 |
-
1. "+
|
| 83 |
2. Try a CSV with `item,reverse` columns (1/true/yes/R = reverse) or a
|
| 84 |
single-column file with `(R)` markers.
|
| 85 |
3. Expect: items fill the textarea ((R) appended where flagged), the filename
|
|
@@ -94,7 +94,7 @@ Needs a generation key on the instance (`GROQ_API_KEY` or `ANTHROPIC_API_KEY`);
|
|
| 94 |
without one the "Draft with AI" tab is hidden entirely. Signed-in only, 20/day
|
| 95 |
(tune locally with `CCR_USER_MAX_GENERATIONS_PER_DAY=2` to test the cap fast).
|
| 96 |
|
| 97 |
-
1. Happy path: sign in > "
|
| 98 |
name + a 1-2 sentence description, press "Draft items". Expect ~10
|
| 99 |
first-person, positively-worded items in the textarea (no (R) items - by
|
| 100 |
design), a "N of 20 used today" counter, the amber "AI-generated · not
|
|
@@ -261,3 +261,13 @@ documented at /product.
|
|
| 261 |
5. Verification: mark a scale Verified; its "unverified" flag disappears
|
| 262 |
platform-wide. Statuses are applied back to the library YAML before
|
| 263 |
production.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
### Custom construct, typed
|
| 74 |
|
| 75 |
+
1. "+ Your own items" > name it, paste items one per line ("Type or paste" tab).
|
| 76 |
2. Append `(R)` to one line: the form shows "1 item(s) marked reverse-scored".
|
| 77 |
3. Save: it appears in the picker under "My custom constructs"; run metadata
|
| 78 |
will carry the reverse flag (check via Results > metadata download).
|
| 79 |
|
| 80 |
### Custom construct, from file (new)
|
| 81 |
|
| 82 |
+
1. "+ Your own items" > the "Upload CSV/XLSX" tab.
|
| 83 |
2. Try a CSV with `item,reverse` columns (1/true/yes/R = reverse) or a
|
| 84 |
single-column file with `(R)` markers.
|
| 85 |
3. Expect: items fill the textarea ((R) appended where flagged), the filename
|
|
|
|
| 94 |
without one the "Draft with AI" tab is hidden entirely. Signed-in only, 20/day
|
| 95 |
(tune locally with `CCR_USER_MAX_GENERATIONS_PER_DAY=2` to test the cap fast).
|
| 96 |
|
| 97 |
+
1. Happy path: sign in > "✦ Draft with AI" (next to the picker). Enter a
|
| 98 |
name + a 1-2 sentence description, press "Draft items". Expect ~10
|
| 99 |
first-person, positively-worded items in the textarea (no (R) items - by
|
| 100 |
design), a "N of 20 used today" counter, the amber "AI-generated · not
|
|
|
|
| 261 |
5. Verification: mark a scale Verified; its "unverified" flag disappears
|
| 262 |
platform-wide. Statuses are applied back to the library YAML before
|
| 263 |
production.
|
| 264 |
+
|
| 265 |
+
### Step 2 layout + Save gating (PI feedback 2026-08-07)
|
| 266 |
+
|
| 267 |
+
1. The Step 2 hint enumerates the three paths (library / own items / AI);
|
| 268 |
+
two buttons sit next to the picker: "+ Your own items" and "✦ Draft with
|
| 269 |
+
AI" (the AI one only when a generation key is configured).
|
| 270 |
+
2. Save is DISABLED until the form has a name and at least one item; a line
|
| 271 |
+
under the button says what's missing. No more silent dead click.
|
| 272 |
+
3. After "Draft items", the page scrolls to the items box, and the notice
|
| 273 |
+
reads "N items drafted - they're in the box below."
|
backend/app/guide.html
CHANGED
|
@@ -156,7 +156,7 @@ synthetic, written for this kit; no real participant data anywhere.</p>
|
|
| 156 |
(IDs, conditions, demographics) are fine; they pass through untouched into the
|
| 157 |
results file.</li>
|
| 158 |
<li><b>Pick your construct(s)</b> - search the library (94 scales from the lab's
|
| 159 |
-
collection)
|
| 160 |
Pick several (up to 10) to score them together in a single run and see how
|
| 161 |
they correlate in your texts.</li>
|
| 162 |
<li><b>Run.</b> Read the warnings panel first (it tells you if something about your
|
|
@@ -284,7 +284,7 @@ are deleted after analysis. Upload
|
|
| 284 |
|
| 285 |
<h3>Custom construct, typed</h3>
|
| 286 |
<ol>
|
| 287 |
-
<li>"+
|
| 288 |
<li>Append <code>(R)</code> to one line: the form shows "1 item(s) marked reverse-scored".</li>
|
| 289 |
<li>Save: it appears in the picker under "My custom constructs"; run metadata carries
|
| 290 |
the reverse flag (check via Results > metadata download).</li>
|
|
@@ -292,7 +292,7 @@ are deleted after analysis. Upload
|
|
| 292 |
|
| 293 |
<h3>Custom construct, from file</h3>
|
| 294 |
<ol>
|
| 295 |
-
<li>"+
|
| 296 |
<li>Try <a href="/samples/construct_items_demo.csv" download>construct_items_demo.csv</a>
|
| 297 |
(<code>item,reverse</code> columns - 1/true/yes/R = reverse),
|
| 298 |
<a href="/samples/construct_items_marker_demo.csv" download>construct_items_marker_demo.csv</a>
|
|
@@ -310,7 +310,7 @@ point, <b>not</b> a validated scale - you review, edit, and delete before saving
|
|
| 310 |
everything the construct touches is labeled "AI-generated · not validated".
|
| 311 |
Signed-in users only, 20 drafts/day.</p>
|
| 312 |
<ol>
|
| 313 |
-
<li><b>Happy path:</b> sign in > "
|
| 314 |
tab. Name: <code>Digital overwhelm</code>. Description: <i>"Feeling that
|
| 315 |
screens, notifications, and online demands exceed one's capacity to keep
|
| 316 |
up."</i> Press "Draft items". Expect in a few seconds: ~10 first-person,
|
|
|
|
| 156 |
(IDs, conditions, demographics) are fine; they pass through untouched into the
|
| 157 |
results file.</li>
|
| 158 |
<li><b>Pick your construct(s)</b> - search the library (94 scales from the lab's
|
| 159 |
+
collection), click "+ Your own items" to type or upload a scale, or "✦ Draft with AI" to generate items for a construct with no validated questionnaire.
|
| 160 |
Pick several (up to 10) to score them together in a single run and see how
|
| 161 |
they correlate in your texts.</li>
|
| 162 |
<li><b>Run.</b> Read the warnings panel first (it tells you if something about your
|
|
|
|
| 284 |
|
| 285 |
<h3>Custom construct, typed</h3>
|
| 286 |
<ol>
|
| 287 |
+
<li>"+ Your own items" > name it, paste items one per line ("Type or paste" tab).</li>
|
| 288 |
<li>Append <code>(R)</code> to one line: the form shows "1 item(s) marked reverse-scored".</li>
|
| 289 |
<li>Save: it appears in the picker under "My custom constructs"; run metadata carries
|
| 290 |
the reverse flag (check via Results > metadata download).</li>
|
|
|
|
| 292 |
|
| 293 |
<h3>Custom construct, from file</h3>
|
| 294 |
<ol>
|
| 295 |
+
<li>"+ Your own items" > the "Upload CSV/XLSX" tab.</li>
|
| 296 |
<li>Try <a href="/samples/construct_items_demo.csv" download>construct_items_demo.csv</a>
|
| 297 |
(<code>item,reverse</code> columns - 1/true/yes/R = reverse),
|
| 298 |
<a href="/samples/construct_items_marker_demo.csv" download>construct_items_marker_demo.csv</a>
|
|
|
|
| 310 |
everything the construct touches is labeled "AI-generated · not validated".
|
| 311 |
Signed-in users only, 20 drafts/day.</p>
|
| 312 |
<ol>
|
| 313 |
+
<li><b>Happy path:</b> sign in > "✦ Draft with AI" (next to the picker) > the "Draft with AI"
|
| 314 |
tab. Name: <code>Digital overwhelm</code>. Description: <i>"Feeling that
|
| 315 |
screens, notifications, and online demands exceed one's capacity to keep
|
| 316 |
up."</i> Press "Draft items". Expect in a few seconds: ~10 first-person,
|
backend/static/assets/index-BJ_J7JUi.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/static/assets/index-T4JgSqf6.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/static/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<link rel="icon" type="image/svg+xml" href="/ccr-logo.svg" />
|
| 7 |
<title>CCR Platform - Contextualized Construct Representations</title>
|
| 8 |
-
<script type="module" crossorigin src="/assets/index-
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-Ckv10U9Q.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<link rel="icon" type="image/svg+xml" href="/ccr-logo.svg" />
|
| 7 |
<title>CCR Platform - Contextualized Construct Representations</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-T4JgSqf6.js"></script>
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-Ckv10U9Q.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
frontend/src/Workspace.jsx
CHANGED
|
@@ -18,7 +18,10 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
|
|
| 18 |
const [uploading, setUploading] = useState(false);
|
| 19 |
const [running, setRunning] = useState(false);
|
| 20 |
const [error, setError] = useState("");
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
const [viewJobId, setViewJobId] = useState(null);
|
| 23 |
const [confirmDelete, setConfirmDelete] = useState(false);
|
| 24 |
const [deleteText, setDeleteText] = useState("");
|
|
@@ -303,10 +306,12 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
|
|
| 303 |
<span className="step-badge">2</span>Construct
|
| 304 |
</h3>
|
| 305 |
<p className="hint">
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
| 310 |
</p>
|
| 311 |
<div className="construct-row">
|
| 312 |
<div className="grow">
|
|
@@ -316,9 +321,22 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
|
|
| 316 |
onToggle={toggleConstruct}
|
| 317 |
/>
|
| 318 |
</div>
|
| 319 |
-
<button
|
| 320 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
</div>
|
| 323 |
|
| 324 |
{selectedConstructs.map((c) => (
|
|
@@ -373,15 +391,17 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
|
|
| 373 |
</details>
|
| 374 |
))}
|
| 375 |
|
| 376 |
-
{
|
| 377 |
<NewConstructForm
|
| 378 |
auth={auth}
|
| 379 |
constructs={constructs}
|
|
|
|
|
|
|
| 380 |
onCreated={async (created) => {
|
| 381 |
const list = await api.listConstructs();
|
| 382 |
setConstructs(list);
|
| 383 |
toggleConstruct(created.id);
|
| 384 |
-
|
| 385 |
}}
|
| 386 |
onError={setError}
|
| 387 |
/>
|
|
@@ -512,7 +532,7 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
|
|
| 512 |
|
| 513 |
const REVERSE_SUFFIX = /\s*\((r|rev|reversed)\)\s*$/i;
|
| 514 |
|
| 515 |
-
function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
| 516 |
const [name, setName] = useState("");
|
| 517 |
const [reference, setReference] = useState("");
|
| 518 |
const [description, setDescription] = useState("");
|
|
@@ -520,9 +540,6 @@ function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
|
| 520 |
const [saving, setSaving] = useState(false);
|
| 521 |
const [parsing, setParsing] = useState(false);
|
| 522 |
const [parseNotes, setParseNotes] = useState([]);
|
| 523 |
-
// Item source chooser: "type" | "upload" | "ai" - one source's controls
|
| 524 |
-
// visible at a time (the flat everything-stacked form read as clutter).
|
| 525 |
-
const [source, setSource] = useState("type");
|
| 526 |
// AI drafting (ITEM_GENERATION.md): genInfo is the provenance stamp echoed
|
| 527 |
// back on save; it survives manual edits (the seed was AI) but is cleared
|
| 528 |
// when items come from a file upload instead.
|
|
@@ -532,6 +549,8 @@ function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
|
| 532 |
const [genNotes, setGenNotes] = useState("");
|
| 533 |
const [genUsage, setGenUsage] = useState(null);
|
| 534 |
const itemFileRef = useRef(null);
|
|
|
|
|
|
|
| 535 |
|
| 536 |
// Simple library name-match (v1 guardrail, PI-approved): if a validated
|
| 537 |
// scale with a similar name exists, say so before anyone generates.
|
|
@@ -563,12 +582,19 @@ function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
|
| 563 |
n_items: nItems,
|
| 564 |
});
|
| 565 |
setItemsText(resp.items.join("\n"));
|
| 566 |
-
setGenInfo(resp.generation);
|
| 567 |
setGenNotes(resp.notes || "");
|
| 568 |
setGenUsage({
|
| 569 |
used: resp.generations_used_today,
|
| 570 |
max: resp.max_generations_per_day,
|
| 571 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
} catch (err) {
|
| 573 |
onError(err.message);
|
| 574 |
} finally {
|
|
@@ -628,7 +654,14 @@ function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
|
| 628 |
description: description.trim(),
|
| 629 |
items: parsed.map((i) => i.text),
|
| 630 |
reverse_scored: parsed.map((i) => i.reverse),
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
});
|
| 633 |
onCreated(created);
|
| 634 |
} catch (err) {
|
|
@@ -759,9 +792,10 @@ function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
|
| 759 |
</p>
|
| 760 |
)}
|
| 761 |
{genInfo && (
|
| 762 |
-
<p className="small
|
| 763 |
-
|
| 764 |
-
edit
|
|
|
|
| 765 |
</p>
|
| 766 |
)}
|
| 767 |
{genNotes && <p className="small muted">Model notes: {genNotes}</p>}
|
|
@@ -771,14 +805,37 @@ function NewConstructForm({ auth, constructs, onCreated, onError }) {
|
|
| 771 |
<label className="field">
|
| 772 |
Items - one per line{source === "type" ? " (or paste a whole scale)" : ""};
|
| 773 |
append (R) to mark a reverse-scored item
|
| 774 |
-
<textarea
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
</label>
|
| 776 |
{nReverse > 0 && (
|
| 777 |
<p className="small muted">{nReverse} item(s) marked reverse-scored.</p>
|
| 778 |
)}
|
| 779 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
{saving ? "Saving…" : "Save construct"}
|
| 781 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
</form>
|
| 783 |
);
|
| 784 |
}
|
|
|
|
| 18 |
const [uploading, setUploading] = useState(false);
|
| 19 |
const [running, setRunning] = useState(false);
|
| 20 |
const [error, setError] = useState("");
|
| 21 |
+
// Custom-construct form: null = closed, else the item source it opens on
|
| 22 |
+
// ("type" | "upload" | "ai"). Lifted here so the Step 2 card can expose the
|
| 23 |
+
// three acquisition paths as explicit buttons (PI feedback 2026-08-07).
|
| 24 |
+
const [constructFormTab, setConstructFormTab] = useState(null);
|
| 25 |
const [viewJobId, setViewJobId] = useState(null);
|
| 26 |
const [confirmDelete, setConfirmDelete] = useState(false);
|
| 27 |
const [deleteText, setDeleteText] = useState("");
|
|
|
|
| 306 |
<span className="step-badge">2</span>Construct
|
| 307 |
</h3>
|
| 308 |
<p className="hint">
|
| 309 |
+
Three ways to add a construct: <b>1)</b> pick validated scales from our
|
| 310 |
+
library of {constructs.filter((c) => c.is_seed).length || "90+"} questionnaires,{" "}
|
| 311 |
+
<b>2)</b> type or upload your own items, or <b>3)</b> draft items with AI from
|
| 312 |
+
a name and description (when no validated scale exists). CCR scores each text
|
| 313 |
+
by its similarity to the items; selecting several constructs runs them
|
| 314 |
+
together on one pass and adds a score-correlation view (up to 10 per run).
|
| 315 |
</p>
|
| 316 |
<div className="construct-row">
|
| 317 |
<div className="grow">
|
|
|
|
| 321 |
onToggle={toggleConstruct}
|
| 322 |
/>
|
| 323 |
</div>
|
| 324 |
+
<button
|
| 325 |
+
className={"ghost" + (constructFormTab && constructFormTab !== "ai" ? " seg-active" : "")}
|
| 326 |
+
onClick={() =>
|
| 327 |
+
setConstructFormTab((t) => (t && t !== "ai" ? null : "type"))
|
| 328 |
+
}
|
| 329 |
+
>
|
| 330 |
+
{constructFormTab && constructFormTab !== "ai" ? "Close" : "+ Your own items"}
|
| 331 |
</button>
|
| 332 |
+
{auth?.generation_available && (
|
| 333 |
+
<button
|
| 334 |
+
className={"ghost" + (constructFormTab === "ai" ? " seg-active" : "")}
|
| 335 |
+
onClick={() => setConstructFormTab((t) => (t === "ai" ? null : "ai"))}
|
| 336 |
+
>
|
| 337 |
+
{constructFormTab === "ai" ? "Close" : "✦ Draft with AI"}
|
| 338 |
+
</button>
|
| 339 |
+
)}
|
| 340 |
</div>
|
| 341 |
|
| 342 |
{selectedConstructs.map((c) => (
|
|
|
|
| 391 |
</details>
|
| 392 |
))}
|
| 393 |
|
| 394 |
+
{constructFormTab && (
|
| 395 |
<NewConstructForm
|
| 396 |
auth={auth}
|
| 397 |
constructs={constructs}
|
| 398 |
+
source={constructFormTab}
|
| 399 |
+
onSourceChange={setConstructFormTab}
|
| 400 |
onCreated={async (created) => {
|
| 401 |
const list = await api.listConstructs();
|
| 402 |
setConstructs(list);
|
| 403 |
toggleConstruct(created.id);
|
| 404 |
+
setConstructFormTab(null);
|
| 405 |
}}
|
| 406 |
onError={setError}
|
| 407 |
/>
|
|
|
|
| 532 |
|
| 533 |
const REVERSE_SUFFIX = /\s*\((r|rev|reversed)\)\s*$/i;
|
| 534 |
|
| 535 |
+
function NewConstructForm({ auth, constructs, source, onSourceChange, onCreated, onError }) {
|
| 536 |
const [name, setName] = useState("");
|
| 537 |
const [reference, setReference] = useState("");
|
| 538 |
const [description, setDescription] = useState("");
|
|
|
|
| 540 |
const [saving, setSaving] = useState(false);
|
| 541 |
const [parsing, setParsing] = useState(false);
|
| 542 |
const [parseNotes, setParseNotes] = useState([]);
|
|
|
|
|
|
|
|
|
|
| 543 |
// AI drafting (ITEM_GENERATION.md): genInfo is the provenance stamp echoed
|
| 544 |
// back on save; it survives manual edits (the seed was AI) but is cleared
|
| 545 |
// when items come from a file upload instead.
|
|
|
|
| 549 |
const [genNotes, setGenNotes] = useState("");
|
| 550 |
const [genUsage, setGenUsage] = useState(null);
|
| 551 |
const itemFileRef = useRef(null);
|
| 552 |
+
const itemsRef = useRef(null);
|
| 553 |
+
const setSource = onSourceChange;
|
| 554 |
|
| 555 |
// Simple library name-match (v1 guardrail, PI-approved): if a validated
|
| 556 |
// scale with a similar name exists, say so before anyone generates.
|
|
|
|
| 582 |
n_items: nItems,
|
| 583 |
});
|
| 584 |
setItemsText(resp.items.join("\n"));
|
| 585 |
+
setGenInfo({ ...resp.generation, n: resp.items.length });
|
| 586 |
setGenNotes(resp.notes || "");
|
| 587 |
setGenUsage({
|
| 588 |
used: resp.generations_used_today,
|
| 589 |
max: resp.max_generations_per_day,
|
| 590 |
});
|
| 591 |
+
// The drafted items land in the textarea below the controls - scroll
|
| 592 |
+
// there so it's obvious they arrived (PI feedback: "how can they see
|
| 593 |
+
// all 10 items generated under the hood?").
|
| 594 |
+
setTimeout(() => {
|
| 595 |
+
itemsRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
| 596 |
+
itemsRef.current?.focus({ preventScroll: true });
|
| 597 |
+
}, 50);
|
| 598 |
} catch (err) {
|
| 599 |
onError(err.message);
|
| 600 |
} finally {
|
|
|
|
| 654 |
description: description.trim(),
|
| 655 |
items: parsed.map((i) => i.text),
|
| 656 |
reverse_scored: parsed.map((i) => i.reverse),
|
| 657 |
+
// provenance stamp when AI-drafted (schema fields only)
|
| 658 |
+
generation: genInfo
|
| 659 |
+
? {
|
| 660 |
+
model: genInfo.model,
|
| 661 |
+
prompt_version: genInfo.prompt_version,
|
| 662 |
+
generated_at: genInfo.generated_at,
|
| 663 |
+
}
|
| 664 |
+
: undefined,
|
| 665 |
});
|
| 666 |
onCreated(created);
|
| 667 |
} catch (err) {
|
|
|
|
| 792 |
</p>
|
| 793 |
)}
|
| 794 |
{genInfo && (
|
| 795 |
+
<p className="small">
|
| 796 |
+
✓ <b>{genInfo.n} items drafted — they're in the box below.</b> Review and
|
| 797 |
+
edit each one, then Save. AI-generated · not validated (model:{" "}
|
| 798 |
+
{genInfo.model}); the saved construct will carry that label.
|
| 799 |
</p>
|
| 800 |
)}
|
| 801 |
{genNotes && <p className="small muted">Model notes: {genNotes}</p>}
|
|
|
|
| 805 |
<label className="field">
|
| 806 |
Items - one per line{source === "type" ? " (or paste a whole scale)" : ""};
|
| 807 |
append (R) to mark a reverse-scored item
|
| 808 |
+
<textarea
|
| 809 |
+
ref={itemsRef}
|
| 810 |
+
rows={6}
|
| 811 |
+
value={itemsText}
|
| 812 |
+
onChange={(e) => setItemsText(e.target.value)}
|
| 813 |
+
/>
|
| 814 |
</label>
|
| 815 |
{nReverse > 0 && (
|
| 816 |
<p className="small muted">{nReverse} item(s) marked reverse-scored.</p>
|
| 817 |
)}
|
| 818 |
+
{/* Save stays disabled until it can actually succeed - a clickable
|
| 819 |
+
button that silently fails read as broken (PI feedback 2026-08-07;
|
| 820 |
+
the error banner sits at the top of the page, out of view here). */}
|
| 821 |
+
<button
|
| 822 |
+
className="primary"
|
| 823 |
+
type="submit"
|
| 824 |
+
disabled={saving || parsing || !name.trim() || parseLines().length === 0}
|
| 825 |
+
>
|
| 826 |
{saving ? "Saving…" : "Save construct"}
|
| 827 |
</button>
|
| 828 |
+
{(!name.trim() || parseLines().length === 0) && (
|
| 829 |
+
<p className="small muted">
|
| 830 |
+
To save, this construct still needs
|
| 831 |
+
{!name.trim() ? " a name" : ""}
|
| 832 |
+
{!name.trim() && parseLines().length === 0 ? " and" : ""}
|
| 833 |
+
{parseLines().length === 0
|
| 834 |
+
? " at least one item - type items above, upload a file, or draft with AI"
|
| 835 |
+
: ""}
|
| 836 |
+
.
|
| 837 |
+
</p>
|
| 838 |
+
)}
|
| 839 |
</form>
|
| 840 |
);
|
| 841 |
}
|