@polycode-projects/the-mechanical-code-talker 2.8.9 → 2.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/services/sprite-catalog-viz.mjs +191 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -283,6 +283,78 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
|
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
// ---- scene composer (pure) ----
|
|
287
|
+
//
|
|
288
|
+
// The free-text "there is a..." box (PLAN_GAMES_UPLIFT_V3.md's own precedent:
|
|
289
|
+
// adventure-viz.mjs's roomSceneObjects/room-frame) over THIS page's own
|
|
290
|
+
// already-real classes — never a general NLU pass. extractSceneItems is the
|
|
291
|
+
// one pure, unit-testable piece; the DOM index it's matched against
|
|
292
|
+
// (className -> real swatch labels, read straight off this page's own
|
|
293
|
+
// already-rendered `.card`/`.swatch-label` markup at load time — see this
|
|
294
|
+
// module's own header for why that beats re-embedding the same SVG data a
|
|
295
|
+
// second time) is built client-side in the inline script below, since
|
|
296
|
+
// walking rendered DOM has no meaning in this pure module.
|
|
297
|
+
|
|
298
|
+
/** `text`'s lowercase word runs with their token index, the unit
|
|
299
|
+
* extractSceneItems matches class names against — punctuation never fuses
|
|
300
|
+
* two real words into one token nor splits one real word into two. */
|
|
301
|
+
function tokenizeSceneText(text) {
|
|
302
|
+
const tokens = [];
|
|
303
|
+
const re = /[A-Za-z]+/g;
|
|
304
|
+
let m;
|
|
305
|
+
while ((m = re.exec(String(text ?? "")))) tokens.push({ word: m[0].toLowerCase() });
|
|
306
|
+
return tokens;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Every real catalog class the free-typed `text` names, in the order each
|
|
310
|
+
* first appears, paired with the real material label (one of that SAME
|
|
311
|
+
* class's own swatch labels — never another class's, never a fabricated
|
|
312
|
+
* one) immediately preceding it, or `null`. `classIndex` is
|
|
313
|
+
* `{className: {materials}}` with `materials` keyed by lowercase label
|
|
314
|
+
* (sprite-catalog-viz's own client-side `buildClassIndexFromDom` output, or
|
|
315
|
+
* an equivalent test fixture) — a class name absent from `classIndex` can
|
|
316
|
+
* never match, and a modifier word that isn't one of ITS matched class's
|
|
317
|
+
* own material labels is silently dropped rather than guessed at, the same
|
|
318
|
+
* honest-miss posture an unrecognized class name gets (an unmatched word,
|
|
319
|
+
* e.g. "red" before a lamp with no red material, is never an error, just
|
|
320
|
+
* silently not drawn). A multi-word class name (e.g. "body of water") is
|
|
321
|
+
* checked, at every token position, before any shorter class that would
|
|
322
|
+
* otherwise claim part of it — candidates are tried longest-word-count
|
|
323
|
+
* first, so the longer name always wins the position it starts at. Pure. */
|
|
324
|
+
export function extractSceneItems(text, classIndex) {
|
|
325
|
+
const index = classIndex || {};
|
|
326
|
+
const candidates = Object.keys(index)
|
|
327
|
+
.map((name) => ({ name, words: name.toLowerCase().split(/\s+/).filter(Boolean) }))
|
|
328
|
+
.filter((c) => c.words.length)
|
|
329
|
+
.sort((a, b) => b.words.length - a.words.length || b.name.length - a.name.length);
|
|
330
|
+
const tokens = tokenizeSceneText(text);
|
|
331
|
+
const used = new Array(tokens.length).fill(false);
|
|
332
|
+
const items = [];
|
|
333
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
334
|
+
if (used[i]) continue;
|
|
335
|
+
const hit = candidates.find(({ words }) => {
|
|
336
|
+
if (i + words.length > tokens.length) return false;
|
|
337
|
+
for (let k = 0; k < words.length; k += 1) {
|
|
338
|
+
if (used[i + k] || tokens[i + k].word !== words[k]) return false;
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
});
|
|
342
|
+
if (!hit) continue;
|
|
343
|
+
let materialLabel = null;
|
|
344
|
+
if (i > 0 && !used[i - 1]) {
|
|
345
|
+
const materials = index[hit.name]?.materials || {};
|
|
346
|
+
const prevWord = tokens[i - 1].word;
|
|
347
|
+
if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
|
|
348
|
+
materialLabel = prevWord;
|
|
349
|
+
used[i - 1] = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (let k = 0; k < hit.words.length; k += 1) used[i + k] = true;
|
|
353
|
+
items.push({ className: hit.name, materialLabel });
|
|
354
|
+
}
|
|
355
|
+
return items;
|
|
356
|
+
}
|
|
357
|
+
|
|
286
358
|
// ---- rendering ----
|
|
287
359
|
|
|
288
360
|
function chainHtml(chain) {
|
|
@@ -361,7 +433,26 @@ ${THEME_TOKENS_CSS}
|
|
|
361
433
|
main { max-width: 1180px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
362
434
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
363
435
|
h1 { font-size: 1.4rem; margin: .3rem 0 .6rem; text-wrap: balance; }
|
|
364
|
-
|
|
436
|
+
|
|
437
|
+
.composer { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: .6rem 0 1.3rem; }
|
|
438
|
+
@media (max-width: 700px) { .composer { grid-template-columns: 1fr; } }
|
|
439
|
+
.composer .panel { background: var(--card); border: 1px solid var(--line); border-top: 2px solid var(--taught); padding: .75rem .85rem; min-width: 0; }
|
|
440
|
+
.composer h2 { font-family: ${SERIF_STACK}; font-variant: small-caps; font-size: .82rem; letter-spacing: .04em; color: var(--muted); font-weight: 600; margin: 0 0 .55rem; }
|
|
441
|
+
.composeform { display: flex; align-items: center; gap: .5rem; }
|
|
442
|
+
.composeform .prompt { color: var(--taught); font-size: .8rem; white-space: nowrap; }
|
|
443
|
+
.composeform input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: .38rem .6rem; min-width: 0; }
|
|
444
|
+
.composeform input:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
445
|
+
.pills { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
|
|
446
|
+
.pill { font-family: ${MONO_STACK}; font-size: .7rem; padding: .22rem .55rem; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); color: var(--ink); cursor: pointer; }
|
|
447
|
+
.pill:hover { border-color: var(--taught); }
|
|
448
|
+
.pill:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
449
|
+
.scene-frame { min-height: 5.6rem; display: flex; align-items: center; }
|
|
450
|
+
.scene-row { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-start; width: 100%; }
|
|
451
|
+
.scene-card { display: flex; flex-direction: column; align-items: center; width: 74px; }
|
|
452
|
+
.scene-sprite { width: 60px; height: 60px; border-radius: 8px; background: var(--bg); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; padding: 8px; box-sizing: border-box; }
|
|
453
|
+
.scene-sprite svg { width: 100%; height: 100%; display: block; }
|
|
454
|
+
.scene-label { font-size: .7rem; text-align: center; color: var(--ink); margin-top: .3rem; line-height: 1.2; }
|
|
455
|
+
.empty-note { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); }
|
|
365
456
|
.topbar { position: sticky; top: 0; z-index: 2; background: var(--bg); display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .6rem 0; margin: 1rem 0 1.2rem; }
|
|
366
457
|
.jump { font-family: ${MONO_STACK}; font-size: .72rem; padding: .18rem .55rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); color: var(--ink); text-decoration: none; }
|
|
367
458
|
.jump:hover { border-color: var(--taught); }
|
|
@@ -395,21 +486,39 @@ ${THEME_TOKENS_CSS}
|
|
|
395
486
|
.swatch-caption { font-family: ${MONO_STACK}; font-size: .58rem; color: var(--muted); line-height: 1.25; margin-top: .15rem; word-break: break-word; }
|
|
396
487
|
.swatch-treat { display: block; opacity: .8; }
|
|
397
488
|
footer.page { max-width: 74ch; margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
398
|
-
@media (prefers-reduced-motion: no-preference) { .jump, .swatch { transition: border-color .12s ease, opacity .12s ease; } }
|
|
489
|
+
@media (prefers-reduced-motion: no-preference) { .jump, .swatch, .pill { transition: border-color .12s ease, opacity .12s ease; } }
|
|
399
490
|
</style>
|
|
400
491
|
</head>
|
|
401
492
|
<body>
|
|
402
493
|
<main>
|
|
403
494
|
<div class="eyebrow">tmct · the sprite library</div>
|
|
404
|
-
<h1>
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
495
|
+
<h1>Sprites</h1>
|
|
496
|
+
<div class="composer">
|
|
497
|
+
<section class="panel compose-panel" aria-label="Describe a scene">
|
|
498
|
+
<h2>describe a scene</h2>
|
|
499
|
+
<form class="composeform" id="composeForm">
|
|
500
|
+
<span class="prompt mono">there is a</span>
|
|
501
|
+
<input id="composeq" type="text" autocomplete="off"
|
|
502
|
+
placeholder="red lamp, a doctor with a hat, and a cabinet"
|
|
503
|
+
aria-label="Continue the sentence: there is a…">
|
|
504
|
+
</form>
|
|
505
|
+
<div class="pills" id="composePills" role="group" aria-label="quick words to add">
|
|
506
|
+
<button type="button" class="pill" data-fill="doctor">doctor</button>
|
|
507
|
+
<button type="button" class="pill" data-fill="hat">hat</button>
|
|
508
|
+
<button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
|
|
509
|
+
<button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
|
|
510
|
+
<button type="button" class="pill" data-fill="cat">cat</button>
|
|
511
|
+
<button type="button" class="pill" data-fill="garden">garden</button>
|
|
512
|
+
</div>
|
|
513
|
+
</section>
|
|
514
|
+
<section class="panel viewer-panel" aria-label="The composed scene">
|
|
515
|
+
<h2>the scene</h2>
|
|
516
|
+
<div class="scene-frame" id="sceneFrame">
|
|
517
|
+
<div class="scene-row" id="sceneRow" aria-live="polite"></div>
|
|
518
|
+
<span class="empty-note" id="sceneEmpty">nothing recognized yet — try “a doctor with a hat, and a cabinet”.</span>
|
|
519
|
+
</div>
|
|
520
|
+
</section>
|
|
521
|
+
</div>
|
|
413
522
|
<div class="topbar">
|
|
414
523
|
<nav aria-label="Jump to group">${navHtml}</nav>
|
|
415
524
|
<div class="filter">
|
|
@@ -445,6 +554,77 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
445
554
|
}
|
|
446
555
|
q.addEventListener("input", apply);
|
|
447
556
|
apply();
|
|
557
|
+
|
|
558
|
+
// ---- the scene composer — reads the class/material index straight off
|
|
559
|
+
// THIS page's own already-rendered card and swatch-label markup (this
|
|
560
|
+
// module's own header explains why: never a second embedded copy of the
|
|
561
|
+
// same swatch data), so the composed scene below only ever shows a sprite
|
|
562
|
+
// this same page already proved the resolver draws.
|
|
563
|
+
const esc = ${escapeHtml.toString()};
|
|
564
|
+
const tokenizeSceneText = ${tokenizeSceneText.toString()};
|
|
565
|
+
const extractSceneItems = ${extractSceneItems.toString()};
|
|
566
|
+
|
|
567
|
+
function buildClassIndexFromDom() {
|
|
568
|
+
const index = {};
|
|
569
|
+
for (const card of cards) {
|
|
570
|
+
const largeRow = card.querySelector('.tier-row[data-tier="large"]');
|
|
571
|
+
if (!largeRow) continue;
|
|
572
|
+
let defaultSvg = null;
|
|
573
|
+
const materials = {};
|
|
574
|
+
for (const swatch of largeRow.querySelectorAll(".swatch")) {
|
|
575
|
+
const labelEl = swatch.querySelector(".swatch-label");
|
|
576
|
+
const svgEl = swatch.querySelector(".swatch-img");
|
|
577
|
+
if (!labelEl || !svgEl) continue;
|
|
578
|
+
const svg = svgEl.innerHTML;
|
|
579
|
+
if (swatch.classList.contains("plain")) defaultSvg = svg;
|
|
580
|
+
else if (swatch.classList.contains("fallback")) { if (!defaultSvg) defaultSvg = svg; }
|
|
581
|
+
else materials[labelEl.textContent.trim().toLowerCase()] = svg;
|
|
582
|
+
}
|
|
583
|
+
if (!defaultSvg) {
|
|
584
|
+
const firstSvg = largeRow.querySelector(".swatch-img");
|
|
585
|
+
if (firstSvg) defaultSvg = firstSvg.innerHTML;
|
|
586
|
+
}
|
|
587
|
+
index[card.dataset.cls] = { defaultSvg, materials };
|
|
588
|
+
}
|
|
589
|
+
return index;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const classIndex = buildClassIndexFromDom();
|
|
593
|
+
const composeqEl = document.getElementById("composeq");
|
|
594
|
+
const composeFormEl = document.getElementById("composeForm");
|
|
595
|
+
const composePillsEl = document.getElementById("composePills");
|
|
596
|
+
const sceneRowEl = document.getElementById("sceneRow");
|
|
597
|
+
const sceneEmptyEl = document.getElementById("sceneEmpty");
|
|
598
|
+
|
|
599
|
+
function renderScene(text) {
|
|
600
|
+
const items = extractSceneItems(text, classIndex);
|
|
601
|
+
if (!items.length) {
|
|
602
|
+
sceneRowEl.innerHTML = "";
|
|
603
|
+
sceneEmptyEl.hidden = false;
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
sceneEmptyEl.hidden = true;
|
|
607
|
+
sceneRowEl.innerHTML = items.map((item) => {
|
|
608
|
+
const entry = classIndex[item.className];
|
|
609
|
+
if (!entry) return "";
|
|
610
|
+
const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
|
|
611
|
+
const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
|
|
612
|
+
return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
|
|
613
|
+
}).join("");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
|
|
617
|
+
composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
|
|
618
|
+
composePillsEl.addEventListener("click", (e) => {
|
|
619
|
+
const btn = e.target.closest(".pill");
|
|
620
|
+
if (!btn) return;
|
|
621
|
+
const phrase = btn.dataset.fill || "";
|
|
622
|
+
const current = composeqEl.value.trim();
|
|
623
|
+
composeqEl.value = current ? current + ", a " + phrase : phrase;
|
|
624
|
+
composeqEl.focus();
|
|
625
|
+
renderScene(composeqEl.value);
|
|
626
|
+
});
|
|
627
|
+
renderScene("");
|
|
448
628
|
})();
|
|
449
629
|
</script>
|
|
450
630
|
</body>
|