@widgetic/creator 0.3.72 → 0.3.73
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/dist/CreatorApp.svelte +821 -73
- package/dist/components/BrandKitPanel.svelte +556 -0
- package/dist/components/BrandKitPanel.svelte.d.ts +32 -0
- package/dist/components/DesignQuickEdit.svelte +218 -0
- package/dist/components/DesignQuickEdit.svelte.d.ts +33 -0
- package/dist/components/FileUrlSourceControl.svelte +69 -0
- package/dist/components/FileUrlSourceControl.svelte.d.ts +15 -0
- package/dist/components/VersionDropdownRow.svelte +86 -1
- package/dist/components/VersionDropdownRow.svelte.d.ts +10 -0
- package/dist/components/WidgetDetails.svelte +19 -2
- package/dist/components/WidgetShapeLivePreviews.svelte +8 -3
- package/dist/retiredSurface.test.d.ts +1 -0
- package/dist/retiredSurface.test.js +113 -0
- package/dist/stores/websocketStore.d.ts +3 -0
- package/dist/stores/websocketStore.js +4 -0
- package/dist/utils/canvasShapeSnapClient.d.ts +27 -0
- package/dist/utils/canvasShapeSnapClient.js +67 -0
- package/dist/utils/designIntentJevClient.d.ts +32 -0
- package/dist/utils/designIntentJevClient.js +51 -0
- package/dist/utils/designIntentResolver.d.ts +107 -0
- package/dist/utils/designIntentResolver.js +509 -0
- package/dist/utils/designIntentResolver.test.d.ts +1 -0
- package/dist/utils/designIntentResolver.test.js +246 -0
- package/dist/utils/designIntentTelemetry.d.ts +176 -0
- package/dist/utils/designIntentTelemetry.js +474 -0
- package/dist/utils/designIntentTelemetry.test.d.ts +1 -0
- package/dist/utils/designIntentTelemetry.test.js +385 -0
- package/dist/utils/designQuickEditValues.d.ts +51 -0
- package/dist/utils/designQuickEditValues.js +77 -0
- package/dist/utils/designQuickEditValues.test.d.ts +1 -0
- package/dist/utils/designQuickEditValues.test.js +100 -0
- package/dist/utils/pinScrollToBottom.js +9 -2
- package/package.json +2 -2
package/dist/CreatorApp.svelte
CHANGED
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
MessagesApi,
|
|
26
26
|
CompositionsApi,
|
|
27
27
|
UploadsApi,
|
|
28
|
-
|
|
28
|
+
UsersApi,
|
|
29
|
+
ResponseError,
|
|
30
|
+
isRetiredSurfaceError
|
|
29
31
|
} from '@widgetic/api-sdk';
|
|
30
32
|
import type {
|
|
31
33
|
Middleware,
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
AgentModelsGet200ResponseModelsInner
|
|
34
36
|
} from '@widgetic/api-sdk';
|
|
35
37
|
import { parseVersionPreviewError } from './utils/versionPreviewError.js';
|
|
38
|
+
import BrandKitPanel from './components/BrandKitPanel.svelte';
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Idempotency middleware (Phase 2 hardening): the gateway rejects
|
|
@@ -345,6 +348,9 @@
|
|
|
345
348
|
let messagesClient: MessagesApi;
|
|
346
349
|
let compositionsClient: CompositionsApi;
|
|
347
350
|
let uploadsClient: UploadsApi;
|
|
351
|
+
let usersClient: UsersApi;
|
|
352
|
+
/** Credits, and days left while the account is still on the free trial. */
|
|
353
|
+
let trialCounter: { credits: number; days: number | null; trialEnded: boolean } | null = null;
|
|
348
354
|
|
|
349
355
|
// Track which token the authenticated clients were built with
|
|
350
356
|
let authenticatedClientsToken: string | null = null;
|
|
@@ -390,6 +396,9 @@
|
|
|
390
396
|
uploadsClient = apiBasePath
|
|
391
397
|
? new UploadsApi({ basePath: apiBasePath, accessToken: token, middleware: [idempotencyKeyMiddleware] })
|
|
392
398
|
: new UploadsApi({ accessToken: token, middleware: [idempotencyKeyMiddleware] });
|
|
399
|
+
usersClient = apiBasePath
|
|
400
|
+
? new UsersApi({ basePath: apiBasePath, accessToken: token })
|
|
401
|
+
: new UsersApi({ accessToken: token });
|
|
393
402
|
|
|
394
403
|
authenticatedClientsToken = token;
|
|
395
404
|
try {
|
|
@@ -398,6 +407,41 @@
|
|
|
398
407
|
/* websocket store may not be ready yet */
|
|
399
408
|
}
|
|
400
409
|
syncFileBrowserUploadConfigForCreator();
|
|
410
|
+
void refreshTrialCounter();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Credits left and days left, from the same billing status the publish gate uses. */
|
|
414
|
+
async function refreshTrialCounter() {
|
|
415
|
+
if (!usersClient) return;
|
|
416
|
+
try {
|
|
417
|
+
const accepted = await usersClient.usersMeBillingGetRaw();
|
|
418
|
+
const body = await accepted.raw.json();
|
|
419
|
+
const data = (body && typeof body === 'object' && 'data' in body ? body.data : body) as {
|
|
420
|
+
trialActive?: boolean;
|
|
421
|
+
creditsAvailable?: number;
|
|
422
|
+
creditBalance?: number;
|
|
423
|
+
trialDaysLeft?: number | null;
|
|
424
|
+
subscription?: { plan_type?: string | null; planType?: string | null } | null;
|
|
425
|
+
} | null;
|
|
426
|
+
if (!data || (data.creditsAvailable == null && data.creditBalance == null && !data.trialActive)) {
|
|
427
|
+
trialCounter = null;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const credits = Number(data.creditsAvailable ?? data.creditBalance ?? 0);
|
|
431
|
+
const planType = String(data.subscription?.plan_type || data.subscription?.planType || '').toLowerCase();
|
|
432
|
+
const paidPlan = planType === 'solo' || planType === 'teams' || planType === 'team';
|
|
433
|
+
const days = data.trialDaysLeft == null ? null : Number(data.trialDaysLeft);
|
|
434
|
+
trialCounter = {
|
|
435
|
+
credits: Number.isFinite(credits) ? Math.max(0, Math.round(credits)) : 0,
|
|
436
|
+
days: paidPlan || days == null || !Number.isFinite(days) ? null : Math.max(0, Math.round(days)),
|
|
437
|
+
trialEnded: !paidPlan && days === 0,
|
|
438
|
+
};
|
|
439
|
+
if (typeof window !== 'undefined') {
|
|
440
|
+
window.dispatchEvent(new CustomEvent('widgetic:trial-counter'));
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.warn('[Trial] Could not load the credit counter', error);
|
|
444
|
+
}
|
|
401
445
|
}
|
|
402
446
|
|
|
403
447
|
function applySiteSessionToCreator(): boolean {
|
|
@@ -476,9 +520,35 @@
|
|
|
476
520
|
import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
|
|
477
521
|
import HoverTooltip from './components/HoverTooltip.svelte';
|
|
478
522
|
import VersionDropdownRow from './components/VersionDropdownRow.svelte';
|
|
523
|
+
// Type-only: erased at compile time, so the component's code is still
|
|
524
|
+
// dynamically imported below — this line exists so the ref types below can
|
|
525
|
+
// derive from the component's REAL class instead of the `any` a dynamic
|
|
526
|
+
// `import('./components/WidgetDetails.svelte')` resolves to (its module
|
|
527
|
+
// type carries no instance shape, which silently re-opens the any-ref hole
|
|
528
|
+
// this block exists to close).
|
|
529
|
+
import type WidgetDetailsComponent from './components/WidgetDetails.svelte';
|
|
479
530
|
|
|
480
531
|
// WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
|
|
481
|
-
|
|
532
|
+
//
|
|
533
|
+
// The component value and its module promise are **derived types**, not
|
|
534
|
+
// hand-written mirrors — the same contract canvasRef and
|
|
535
|
+
// widgetShapeLivePreviewsRef below carry. WidgetDetails.svelte is classic
|
|
536
|
+
// Svelte 4, so the instance type is `InstanceType` of its component class
|
|
537
|
+
// (imported type-only above — see the note there for why the dynamic import
|
|
538
|
+
// cannot supply this type). `WithoutIndexKeys` (declared with the canvas ref
|
|
539
|
+
// below) strips the string index signature the base plumbing inherits,
|
|
540
|
+
// which the falsification probe for this very file proved lets unknown-member
|
|
541
|
+
// calls compile. A renamed or removed `export function` on
|
|
542
|
+
// WidgetDetails.svelte is now a compile error at every stale CreatorApp call
|
|
543
|
+
// site instead of a silent runtime `undefined`; a new export appears here
|
|
544
|
+
// without any edit to this file. The values stay
|
|
545
|
+
// `| null`/`| undefined` because the module loads lazily — the
|
|
546
|
+
// optionality lives at the assignment, not in the type.
|
|
547
|
+
type WidgetDetailsInstance = WithoutIndexKeys<InstanceType<typeof WidgetDetailsComponent>>;
|
|
548
|
+
// The lazily-assigned value is the COMPONENT (constructor) — what
|
|
549
|
+
// `<svelte:component this={…}>` consumes — while the refs and
|
|
550
|
+
// `getWidgetDetailsRef` hand out INSTANCES. Two shapes, two types.
|
|
551
|
+
let WidgetDetails: typeof WidgetDetailsComponent | undefined = undefined;
|
|
482
552
|
let widgetDetailsRefs: Record<string, any> = {};
|
|
483
553
|
let openWidgetPanelIds: string[] = [];
|
|
484
554
|
/** Stable {#each} keys so draft → real widget does not remount Widget Details. */
|
|
@@ -495,27 +565,76 @@
|
|
|
495
565
|
// WidgetConversionPayload lives on the canvas root export (re-exported from
|
|
496
566
|
// ./canvas); the components barrel only carries the input controls. Type-only
|
|
497
567
|
// import, so the 10K-line Canvas.svelte still never loads.
|
|
498
|
-
import type {
|
|
568
|
+
import type {
|
|
569
|
+
WidgetConversionPayload,
|
|
570
|
+
StrokeRecognitionRequest,
|
|
571
|
+
StrokeRecognitionResult
|
|
572
|
+
} from '@widgetic/canvas';
|
|
499
573
|
|
|
500
574
|
// Chat module for code generation conversation UI
|
|
501
575
|
import { Chat, ContextType } from '@widgetic/chat';
|
|
502
576
|
import type { ClarificationSuggestion } from '@widgetic/chat';
|
|
577
|
+
import DesignQuickEdit from './components/DesignQuickEdit.svelte';
|
|
578
|
+
import { collectDesignProperties, resolveDesignIntent, toDesignControlRequest, type DesignControlRequest } from './utils/designIntentResolver';
|
|
579
|
+
import { isDesignCardAttached } from './utils/designQuickEditValues';
|
|
580
|
+
import { resolveDesignIntentViaJev } from './utils/designIntentJevClient';
|
|
581
|
+
import { recognizeStrokeViaJev } from './utils/canvasShapeSnapClient';
|
|
582
|
+
import {
|
|
583
|
+
recordDesignIntentTelemetry,
|
|
584
|
+
recordDesignIntentJevOutcome,
|
|
585
|
+
isJevCandidateBucket,
|
|
586
|
+
type DesignIntentClassification
|
|
587
|
+
} from './utils/designIntentTelemetry';
|
|
503
588
|
|
|
504
589
|
// Eagerly initiate canvas chunk fetching at script evaluation (parallel with DOM parsing)
|
|
505
590
|
const canvasModulePromise = typeof window !== 'undefined' ? import('@widgetic/canvas') : null;
|
|
506
591
|
|
|
507
|
-
let WidgeticCanvas:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
592
|
+
let WidgeticCanvas: typeof import('@widgetic/canvas').Canvas | null = null;
|
|
593
|
+
/**
|
|
594
|
+
* Pattern reference: `__docs/15-svelte4-component-ref-typing.md`.
|
|
595
|
+
*
|
|
596
|
+
* Drops the string-index keys a Svelte 4 component's instance type inherits
|
|
597
|
+
* from its prop plumbing, keeping only the class's own members. Without
|
|
598
|
+
* this, `canvasRef.anythingAtAll` type-checks and a mistyped method name
|
|
599
|
+
* slips through as a silent runtime `undefined`.
|
|
600
|
+
*/
|
|
601
|
+
type WithoutIndexKeys<T> = { [K in keyof T as string extends K ? never : K]: T[K] };
|
|
602
|
+
/**
|
|
603
|
+
* The canvas component instance, **derived from the component's own type**
|
|
604
|
+
* (`InstanceType` of the `@widgetic/canvas` `Canvas` class), not a
|
|
605
|
+
* hand-written mirror. This component is classic Svelte 4 — a
|
|
606
|
+
* `SvelteComponentTyped` subclass whose instance members are compiled
|
|
607
|
+
* `export function` getters — so the instance type is `InstanceType<…>`
|
|
608
|
+
* (the Svelte 5 call-signature components elsewhere in this file use
|
|
609
|
+
* `ReturnType<…>`; same idea, different component authoring era). A new
|
|
610
|
+
* `export function` on Canvas.svelte appears here after a dist rebuild,
|
|
611
|
+
* and a renamed or removed one is a compile error at the stale call site
|
|
612
|
+
* instead of a silent `undefined` at runtime. The ref is still `| null`
|
|
613
|
+
* and the calls keep their `?.` because the canvas mounts lazily — the
|
|
614
|
+
* optionality lives at the binding, not in the type.
|
|
615
|
+
*/
|
|
616
|
+
let canvasRef: WithoutIndexKeys<InstanceType<typeof import('@widgetic/canvas').Canvas>> | null = null;
|
|
617
|
+
/**
|
|
618
|
+
* HTML iframe overlays over Fabric WidgetShapes (W3).
|
|
619
|
+
*
|
|
620
|
+
* Typed as the component's **own instance type**, not a hand-written
|
|
621
|
+
* mirror: a Svelte 5 component is a call-signature type whose return is
|
|
622
|
+
* the exported instance surface, so `ReturnType` of the imported component
|
|
623
|
+
* is exactly what `bind:this` delivers. The previous literal shape
|
|
624
|
+
* (`sync`/`requestReload`/`cancelReloads`/`setSelectedVersion`, all
|
|
625
|
+
* optional) had to be edited in lockstep with the component's `export
|
|
626
|
+
* function` block — a new export left every call site type-unchecked
|
|
627
|
+
* against it, a signature change drifted silently. Derived, both
|
|
628
|
+
* directions are compile errors at the stale site instead. The ref is
|
|
629
|
+
* still `| null` and the calls keep their `?.` because the canvas mounts
|
|
630
|
+
* lazily — the optionality lives at the binding, not in the type.
|
|
631
|
+
*/
|
|
632
|
+
let widgetShapeLivePreviewsRef: ReturnType<typeof WidgetShapeLivePreviews> | null = null;
|
|
514
633
|
/** Selected WidgetShape id for chrome highlight (from Fabric selection). */
|
|
515
634
|
let canvasSelectedWidgetShapeId: string | null = null;
|
|
516
|
-
let widgetDetailsModulePromise: Promise<
|
|
635
|
+
let widgetDetailsModulePromise: Promise<typeof WidgetDetailsComponent> | null = null;
|
|
517
636
|
|
|
518
|
-
function loadWidgetDetailsModule(): Promise<
|
|
637
|
+
function loadWidgetDetailsModule(): Promise<typeof WidgetDetailsComponent> {
|
|
519
638
|
if (!widgetDetailsModulePromise) {
|
|
520
639
|
widgetDetailsModulePromise = import('./components/WidgetDetails.svelte').then((wdModule) => {
|
|
521
640
|
WidgetDetails = wdModule.default;
|
|
@@ -932,6 +1051,7 @@
|
|
|
932
1051
|
let chatMethods: {
|
|
933
1052
|
sendMessage: (content: string, attachments?: File[]) => Promise<void>;
|
|
934
1053
|
addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: ClarificationSuggestion[]) => void;
|
|
1054
|
+
addDesignControlsMessage?: (content: string, designControls: DesignControlRequest) => void;
|
|
935
1055
|
addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
|
|
936
1056
|
getConversationId: () => string | null;
|
|
937
1057
|
addImageAttachment?: (dataUrl: string, fileName?: string) => void;
|
|
@@ -970,6 +1090,45 @@
|
|
|
970
1090
|
// Populated via Chat's onConversationChange callback and passed to generateCode()
|
|
971
1091
|
// so the backend can load multi-turn LLM history (Aider-style incremental edits).
|
|
972
1092
|
let currentChatConversationId: string | null = null;
|
|
1093
|
+
let brandKitEnabled = false;
|
|
1094
|
+
let widgetLanguage = 'svelte';
|
|
1095
|
+
let librariesEnabled = false;
|
|
1096
|
+
let brandPrimary = '#314F2F';
|
|
1097
|
+
let brandSecondary = '#7BA668';
|
|
1098
|
+
let brandTitle = '#0F2010';
|
|
1099
|
+
let brandSub = '#5C6B58';
|
|
1100
|
+
let brandFonts = ['Inter'];
|
|
1101
|
+
let brandKitLibraries: string[] = [];
|
|
1102
|
+
let libraryGitUrl = '';
|
|
1103
|
+
let libraryFileName = '';
|
|
1104
|
+
let libraryUploads: { name: string; content: string; encoding: string }[] = [];
|
|
1105
|
+
let fontUploads: { name: string; content: string; encoding: string }[] = [];
|
|
1106
|
+
|
|
1107
|
+
function brandKitForRequest() {
|
|
1108
|
+
const kit: {
|
|
1109
|
+
colors?: { primary: string; secondary: string; title: string; sub: string };
|
|
1110
|
+
fonts?: string[];
|
|
1111
|
+
libraries?: string[];
|
|
1112
|
+
uploads?: { name: string; content: string; encoding: string }[];
|
|
1113
|
+
fontFiles?: { name: string; content: string; encoding: string }[];
|
|
1114
|
+
} = {};
|
|
1115
|
+
if (brandKitEnabled) {
|
|
1116
|
+
kit.colors = {
|
|
1117
|
+
primary: brandPrimary,
|
|
1118
|
+
secondary: brandSecondary,
|
|
1119
|
+
title: brandTitle,
|
|
1120
|
+
sub: brandSub,
|
|
1121
|
+
};
|
|
1122
|
+
const families = brandFonts.map((family) => family.trim()).filter(Boolean).slice(0, 3);
|
|
1123
|
+
if (families.length > 0) kit.fonts = families;
|
|
1124
|
+
}
|
|
1125
|
+
// Preset libraries are a debug test. An empty list is an explicit ban.
|
|
1126
|
+
if (librariesEnabled && debugMode) kit.libraries = [...brandKitLibraries];
|
|
1127
|
+
if (brandKitEnabled && fontUploads.length > 0) kit.fontFiles = fontUploads.map((file) => ({ ...file }));
|
|
1128
|
+
if (libraryUploads.length > 0) kit.uploads = libraryUploads.map((file) => ({ ...file }));
|
|
1129
|
+
if (!kit.colors && !kit.fonts && !kit.libraries && !kit.uploads && !kit.fontFiles) return undefined;
|
|
1130
|
+
return kit;
|
|
1131
|
+
}
|
|
973
1132
|
|
|
974
1133
|
/** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
|
|
975
1134
|
function promptImpliesVisionReference(prompt: string): boolean {
|
|
@@ -1143,6 +1302,40 @@
|
|
|
1143
1302
|
}
|
|
1144
1303
|
});
|
|
1145
1304
|
|
|
1305
|
+
// Shadow-mode measurement: bucket every send by how it could be resolved
|
|
1306
|
+
// (literal/arithmetic, bounded-set semantic, free text, new property, or
|
|
1307
|
+
// not a design ask). Records + logs only — it never changes behavior.
|
|
1308
|
+
// The bucket also decides whether the Jev fallback is worth a dial.
|
|
1309
|
+
// Inspect from the console: `__widgeticDesignIntentTelemetry.report()`.
|
|
1310
|
+
// The whole classification is kept, not just the bucket: the Jev dial below is
|
|
1311
|
+
// attributed to the *ask shape* inside that bucket (a property was named, or
|
|
1312
|
+
// only a value was), which is what makes its fall-through reasons actionable.
|
|
1313
|
+
let designIntentClassification: DesignIntentClassification | null = null;
|
|
1314
|
+
try {
|
|
1315
|
+
designIntentClassification = recordDesignIntentTelemetry(
|
|
1316
|
+
content,
|
|
1317
|
+
getPanelDisplayState(codegenWidgetId).compositionDesignSchema
|
|
1318
|
+
);
|
|
1319
|
+
} catch {
|
|
1320
|
+
/* telemetry must never break a send */
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Deterministic design-tweak fast path: explicit values and arithmetic
|
|
1324
|
+
// resolve against the loaded design schema in code, so an unambiguous
|
|
1325
|
+
// property change never pays for a 30–300s codegen turn.
|
|
1326
|
+
if (!attachments?.length && !sendOptions?.skipIntentGate) {
|
|
1327
|
+
if (tryApplyDesignIntent(codegenWidgetId, content)) return;
|
|
1328
|
+
// Jev takes over ONLY the bounded-set-semantic bucket — the residual
|
|
1329
|
+
// where the property or value needs judgment but the value space is
|
|
1330
|
+
// closed. Free text and new properties can only fall through (Jev cannot
|
|
1331
|
+
// generate) and a non-design ask is not ours at all, so none of them is
|
|
1332
|
+
// ever dialed.
|
|
1333
|
+
const dialJev = isJevCandidateBucket(designIntentClassification?.bucket);
|
|
1334
|
+
if (dialJev && (await tryApplyDesignIntentViaJev(codegenWidgetId, content, designIntentClassification))) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1146
1339
|
const effectivePromptImages =
|
|
1147
1340
|
panelPromptImages.length > 0
|
|
1148
1341
|
? panelPromptImages
|
|
@@ -2126,6 +2319,12 @@
|
|
|
2126
2319
|
// Suggestion chips from a persisted clarification card — restored
|
|
2127
2320
|
// on reload exactly as they were served.
|
|
2128
2321
|
suggestions: Array.isArray(m.suggestions) && m.suggestions.length > 0 ? m.suggestions : undefined,
|
|
2322
|
+
// The inline design-control card this turn carried, restored on
|
|
2323
|
+
// reload so the reply keeps its controls. The server has already
|
|
2324
|
+
// normalized the payload to a renderable shape (see the messages
|
|
2325
|
+
// service); `compositionId` on it decides whether the card can still
|
|
2326
|
+
// read this composition's live values.
|
|
2327
|
+
designControls: m.design_controls ?? m.designControls ?? undefined,
|
|
2129
2328
|
isCommit: m.is_commit || m.isCommit || false,
|
|
2130
2329
|
// What the turn shipped, in the commit's own words — persisted beside
|
|
2131
2330
|
// is_commit by the codegen pipeline and rendered on the turn's commit
|
|
@@ -2305,6 +2504,10 @@
|
|
|
2305
2504
|
}>,
|
|
2306
2505
|
suggestions?: ClarificationSuggestion[],
|
|
2307
2506
|
persistWidgetId?: string | null,
|
|
2507
|
+
/** The inline design-control card this reply carries, when a directed
|
|
2508
|
+
* design tweak was applied with no codegen. Persisted with the reply so
|
|
2509
|
+
* the card survives a reload and re-attaches to its composition. */
|
|
2510
|
+
designControls?: DesignControlRequest | null,
|
|
2308
2511
|
): Promise<any> {
|
|
2309
2512
|
try {
|
|
2310
2513
|
// Draft convert panels hold a client-only conversation until the widget
|
|
@@ -2342,6 +2545,9 @@
|
|
|
2342
2545
|
// Clarification-card suggestions ride the same write as the reply
|
|
2343
2546
|
// they belong to, so a reload re-renders the card with its chips.
|
|
2344
2547
|
...(suggestions && suggestions.length > 0 ? { suggestions } : {}),
|
|
2548
|
+
// Same for the inline design-control card: one write, so a reload
|
|
2549
|
+
// re-renders the reply and its controls — not half a turn.
|
|
2550
|
+
...(designControls ? { design_controls: designControls } : {}),
|
|
2345
2551
|
};
|
|
2346
2552
|
const persistMessage = async (payload: typeof createPayload) => {
|
|
2347
2553
|
const response = await messagesClient.createMessageRaw({
|
|
@@ -2559,6 +2765,10 @@
|
|
|
2559
2765
|
// API returns camelCase (via responseTransformMiddleware), fallback to snake_case
|
|
2560
2766
|
commitSha?: string | null;
|
|
2561
2767
|
commit_sha?: string | null;
|
|
2768
|
+
/** What the change shipped, in the pipeline's own words — the same title
|
|
2769
|
+
* the chat turn's commit chrome shows. Null on pre-title versions. */
|
|
2770
|
+
commitTitle?: string | null;
|
|
2771
|
+
commit_title?: string | null;
|
|
2562
2772
|
artifactUrl?: string;
|
|
2563
2773
|
artifact_url?: string;
|
|
2564
2774
|
/**
|
|
@@ -2583,6 +2793,8 @@
|
|
|
2583
2793
|
let isHistoryOpen = false;
|
|
2584
2794
|
let isLoadingHistory = false;
|
|
2585
2795
|
let isSettingLiveVersion: number | null = null; // track which version is being set live
|
|
2796
|
+
/** The version whose title rename is in flight, as `widgetId:version`. */
|
|
2797
|
+
let renamingTitleFor: string | null = null;
|
|
2586
2798
|
let liveHistoryPanelTop = 0;
|
|
2587
2799
|
let liveHistoryPanelRight = 0;
|
|
2588
2800
|
/** True once publish history fetch completed for the current widget (or widget was never published). */
|
|
@@ -2687,8 +2899,8 @@
|
|
|
2687
2899
|
// COMPOSITION EDITOR STATE
|
|
2688
2900
|
// =============================================================================
|
|
2689
2901
|
let compositionEditorOpen = false;
|
|
2690
|
-
let compositionDesignSchema:
|
|
2691
|
-
let compositionContentSchema:
|
|
2902
|
+
let compositionDesignSchema: DesignSchemaView | null = null;
|
|
2903
|
+
let compositionContentSchema: ContentSchemaView | null = null;
|
|
2692
2904
|
let compositionSchemasLoading = false;
|
|
2693
2905
|
let compositionSchemasError: string | null = null;
|
|
2694
2906
|
|
|
@@ -2722,6 +2934,9 @@
|
|
|
2722
2934
|
}
|
|
2723
2935
|
|
|
2724
2936
|
function handleChatMessagesResize(widgetId: string, deltaPx: number): void {
|
|
2937
|
+
// Sub-pixel footer jitter was rewriting the chat height every frame and
|
|
2938
|
+
// re-rendering the whole creator while code generation streamed.
|
|
2939
|
+
if (!Number.isFinite(deltaPx) || Math.abs(deltaPx) < 2) return;
|
|
2725
2940
|
const measured = document
|
|
2726
2941
|
.querySelector(`[data-widget-chat-height="${widgetId}"]`)
|
|
2727
2942
|
?.getBoundingClientRect().height;
|
|
@@ -2729,8 +2944,8 @@
|
|
|
2729
2944
|
measured && measured > 0
|
|
2730
2945
|
? measured
|
|
2731
2946
|
: (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
|
|
2732
|
-
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
|
|
2733
|
-
|
|
2947
|
+
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, Math.round(current + deltaPx)));
|
|
2948
|
+
if (Math.abs(next - current) < 2) return;
|
|
2734
2949
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
2735
2950
|
}
|
|
2736
2951
|
|
|
@@ -2773,19 +2988,77 @@
|
|
|
2773
2988
|
});
|
|
2774
2989
|
}
|
|
2775
2990
|
|
|
2776
|
-
|
|
2991
|
+
/** A component property as the design schema carries it. Field set is open —
|
|
2992
|
+
* consumers read known keys and every schema shape is producer-defined — but
|
|
2993
|
+
* the keys the editor itself reads are named. */
|
|
2994
|
+
interface DesignSchemaPropertyView {
|
|
2995
|
+
id?: string;
|
|
2996
|
+
name?: string;
|
|
2997
|
+
description?: string;
|
|
2998
|
+
inputController?: { type?: string } & Record<string, unknown>;
|
|
2999
|
+
[key: string]: unknown;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
/** One component group of the design schema (the `componentGroups` array's
|
|
3003
|
+
* members). Open to extra keys; the editor reads `name` and `components`. */
|
|
3004
|
+
interface DesignSchemaGroupView {
|
|
3005
|
+
name?: string;
|
|
3006
|
+
components?: Array<{
|
|
3007
|
+
properties?: DesignSchemaPropertyView[];
|
|
3008
|
+
[key: string]: unknown;
|
|
3009
|
+
}>;
|
|
3010
|
+
[key: string]: unknown;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* The design schema as this file's pipeline actually carries it: after
|
|
3015
|
+
* `normalizeDesignSchemaFromApiResponse` it is **either a group array (the
|
|
3016
|
+
* merged single-group form) or null** — the envelope variants
|
|
3017
|
+
* (`componentGroups` on an object, legacy `theme`-only payloads) are
|
|
3018
|
+
* collapsed upstream of this type. The API's generated model
|
|
3019
|
+
* (`GetCompositionDesignSchema200Response`) types `componentGroups` only as
|
|
3020
|
+
* `Array<object>`, so these views encode the shape the normalizer
|
|
3021
|
+
* guarantees, not the shape the OpenAPI spec guesses.
|
|
3022
|
+
*/
|
|
3023
|
+
type DesignSchemaView = DesignSchemaGroupView[];
|
|
3024
|
+
|
|
3025
|
+
/** One content-item entry of the content schema (the `contentItems` array's
|
|
3026
|
+
* members). `id` and `properties` are what the merge/preview helpers read. */
|
|
3027
|
+
interface ContentSchemaItemView {
|
|
3028
|
+
id?: string;
|
|
3029
|
+
properties?: unknown[];
|
|
3030
|
+
[key: string]: unknown;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
/**
|
|
3034
|
+
* The content schema as this file's pipeline carries it: the raw 200
|
|
3035
|
+
* payload's object form (may arrive wrapped as `{ data: … }` from the
|
|
3036
|
+
* response envelope), with the two keys the editor reads named —
|
|
3037
|
+
* `contentItems` (array form; the legacy object form is tolerated at the
|
|
3038
|
+
* read sites and Array-guarded before use) plus the open remainder.
|
|
3039
|
+
*/
|
|
3040
|
+
interface ContentSchemaView {
|
|
3041
|
+
/** Response-envelope artifact of the upstream API; tolerated, never authored here. */
|
|
3042
|
+
data?: { contentItems?: ContentSchemaItemView[] | Record<string, unknown> };
|
|
3043
|
+
contentItems?: ContentSchemaItemView[] | Record<string, unknown>;
|
|
3044
|
+
[key: string]: unknown;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function normalizeDesignSchemaFromApiResponse(designRes: unknown): DesignSchemaView | null {
|
|
2777
3048
|
if (!designRes) return null;
|
|
2778
3049
|
const payload = (designRes as { data?: unknown })?.data ?? designRes;
|
|
2779
3050
|
if (!payload || typeof payload !== 'object') return null;
|
|
2780
|
-
if (Array.isArray(payload)) return payload;
|
|
3051
|
+
if (Array.isArray(payload)) return payload as DesignSchemaView;
|
|
2781
3052
|
const record = payload as Record<string, unknown>;
|
|
2782
3053
|
const groups = record.componentGroups ?? record.component_groups;
|
|
2783
3054
|
if (Array.isArray(groups)) {
|
|
2784
3055
|
if (groups.length > 1) {
|
|
2785
|
-
|
|
3056
|
+
// The view-shape coercion happens here, at the normalizer — the one
|
|
3057
|
+
// place raw API JSON is pronounced to be the editor's schema view.
|
|
3058
|
+
const mergedComponents = groups.flatMap((g: { components?: DesignSchemaPropertyView[] }) => g.components || []);
|
|
2786
3059
|
return [{ name: (groups[0] as { name?: string })?.name || 'Design', components: mergedComponents }];
|
|
2787
3060
|
}
|
|
2788
|
-
return groups;
|
|
3061
|
+
return groups as DesignSchemaView;
|
|
2789
3062
|
}
|
|
2790
3063
|
// Legacy VM default schema: { theme: { colors: ... } } — not editable via PropsEditor
|
|
2791
3064
|
if (record.theme && !groups) return null;
|
|
@@ -2836,10 +3109,10 @@
|
|
|
2836
3109
|
}
|
|
2837
3110
|
}
|
|
2838
3111
|
|
|
2839
|
-
function normalizeContentSchemaFromApiResponse(contentRes: unknown):
|
|
3112
|
+
function normalizeContentSchemaFromApiResponse(contentRes: unknown): ContentSchemaView | null {
|
|
2840
3113
|
if (!contentRes) return null;
|
|
2841
3114
|
const payload = (contentRes as { data?: unknown })?.data ?? contentRes;
|
|
2842
|
-
return payload && typeof payload === 'object' ? payload : null;
|
|
3115
|
+
return payload && typeof payload === 'object' ? (payload as ContentSchemaView) : null;
|
|
2843
3116
|
}
|
|
2844
3117
|
|
|
2845
3118
|
const SCHEMA_LOAD_TIMEOUT_MS = 20000;
|
|
@@ -2849,8 +3122,8 @@
|
|
|
2849
3122
|
const schemaLoadWaitQueue: string[] = [];
|
|
2850
3123
|
/** Per-widget schema cache — survives focus switches and snapshot merges. */
|
|
2851
3124
|
const widgetSchemaCache = new Map<string, {
|
|
2852
|
-
design:
|
|
2853
|
-
content:
|
|
3125
|
+
design: DesignSchemaView | null;
|
|
3126
|
+
content: ContentSchemaView | null;
|
|
2854
3127
|
error: string | null;
|
|
2855
3128
|
}>();
|
|
2856
3129
|
|
|
@@ -3075,8 +3348,8 @@
|
|
|
3075
3348
|
compositionSchemasError = null;
|
|
3076
3349
|
}
|
|
3077
3350
|
|
|
3078
|
-
let designSchema:
|
|
3079
|
-
let contentSchema:
|
|
3351
|
+
let designSchema: DesignSchemaView | null = null;
|
|
3352
|
+
let contentSchema: ContentSchemaView | null = null;
|
|
3080
3353
|
let schemaError: string | null = null;
|
|
3081
3354
|
|
|
3082
3355
|
try {
|
|
@@ -3099,7 +3372,7 @@
|
|
|
3099
3372
|
|
|
3100
3373
|
designSchema = ensureSubtitleFontInDesignSchema(
|
|
3101
3374
|
normalizeDesignSchemaFromApiResponse(designRes)
|
|
3102
|
-
);
|
|
3375
|
+
) as DesignSchemaView | null;
|
|
3103
3376
|
contentSchema = normalizeContentSchemaFromApiResponse(contentRes);
|
|
3104
3377
|
|
|
3105
3378
|
if (!designSchema && !contentSchema) {
|
|
@@ -3462,6 +3735,206 @@
|
|
|
3462
3735
|
return out;
|
|
3463
3736
|
}
|
|
3464
3737
|
|
|
3738
|
+
/**
|
|
3739
|
+
* Deterministic design-tweak fast path for the canvas chat.
|
|
3740
|
+
*
|
|
3741
|
+
* `off` — never resolve; every message goes to codegen (shipped behavior).
|
|
3742
|
+
* `shadow` — resolve + log, but still send to codegen (measure the flip rate).
|
|
3743
|
+
* `enforce` — apply the resolved change to the live preview, persist it like
|
|
3744
|
+
* any other design edit, render the inline controller in chat, and
|
|
3745
|
+
* skip codegen entirely — no LLM call.
|
|
3746
|
+
*/
|
|
3747
|
+
const DESIGN_INTENT_RESOLVER = (
|
|
3748
|
+
(import.meta.env as unknown as Record<string, string | undefined>).VITE_DESIGN_INTENT_RESOLVER ??
|
|
3749
|
+
'enforce'
|
|
3750
|
+
) as 'off' | 'shadow' | 'enforce';
|
|
3751
|
+
|
|
3752
|
+
/** Apply design values to the live preview + persist, mirroring the editor path. */
|
|
3753
|
+
function applyDesignValuesToPreview(widgetId: string, changes: Record<string, unknown>): void {
|
|
3754
|
+
const ps = getPanelDisplayState(widgetId);
|
|
3755
|
+
const base = ps.liveDesignValues ?? {};
|
|
3756
|
+
const next = { ...base, ...changes };
|
|
3757
|
+
if (widgetId === focusedPanelWidgetId) {
|
|
3758
|
+
liveDesignValues = next;
|
|
3759
|
+
} else {
|
|
3760
|
+
patchPanelSnapshot(widgetId, { liveDesignValues: next });
|
|
3761
|
+
}
|
|
3762
|
+
getWidgetDetailsRef(widgetId)?.sendMessageToPreview({
|
|
3763
|
+
type: 'widgetic:update',
|
|
3764
|
+
design: flattenDesignValuesForPreview(next)
|
|
3765
|
+
});
|
|
3766
|
+
compositionDirty = true;
|
|
3767
|
+
if (widgetId === focusedPanelWidgetId) {
|
|
3768
|
+
syncFocusedPanelEditorSnapshot();
|
|
3769
|
+
// Design edits are committed as soon as they are made, so the next codegen
|
|
3770
|
+
// turn already sees the updated values.
|
|
3771
|
+
if (selectedComposition && selectedWidgetId === widgetId) {
|
|
3772
|
+
void updateCompositionValues();
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
/**
|
|
3778
|
+
* Property ids present in a design schema, memoized by schema identity.
|
|
3779
|
+
*
|
|
3780
|
+
* The chat design card asks whether the property it controls still exists so
|
|
3781
|
+
* it can disable a control whose property a regeneration dropped instead of
|
|
3782
|
+
* writing a dead value. That question is asked for every open panel on every
|
|
3783
|
+
* display revision (i.e. on every slider tick), so re-walking the schema each
|
|
3784
|
+
* time is wasteful. `undefined` means there is no schema to judge by yet —
|
|
3785
|
+
* the card then leaves its controls live rather than disabling them.
|
|
3786
|
+
*/
|
|
3787
|
+
let designPropertyIdsCache: { schema: unknown; ids: Set<string> } | null = null;
|
|
3788
|
+
function designPropertyIdsFor(schema: unknown): Set<string> | undefined {
|
|
3789
|
+
if (!schema) return undefined;
|
|
3790
|
+
if (designPropertyIdsCache?.schema === schema) return designPropertyIdsCache.ids;
|
|
3791
|
+
const ids = new Set(collectDesignProperties(schema).map((property) => property.id));
|
|
3792
|
+
designPropertyIdsCache = { schema, ids };
|
|
3793
|
+
return ids;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
/**
|
|
3797
|
+
* Try to satisfy a chat message as a deterministic design tweak (no LLM).
|
|
3798
|
+
* Returns true when the message was handled and codegen must be skipped.
|
|
3799
|
+
*/
|
|
3800
|
+
function tryApplyDesignIntent(widgetId: string, message: string): boolean {
|
|
3801
|
+
if (DESIGN_INTENT_RESOLVER === 'off') return false;
|
|
3802
|
+
const ps = getPanelDisplayState(widgetId);
|
|
3803
|
+
const schema = ps.compositionDesignSchema;
|
|
3804
|
+
if (!schema) return false;
|
|
3805
|
+
|
|
3806
|
+
const liveValues = ps.liveDesignValues ?? {};
|
|
3807
|
+
const resolution = resolveDesignIntent(message, schema, liveValues);
|
|
3808
|
+
if (!resolution) return false;
|
|
3809
|
+
|
|
3810
|
+
if (DESIGN_INTENT_RESOLVER === 'shadow') {
|
|
3811
|
+
console.log('[DesignIntent] shadow', resolution);
|
|
3812
|
+
return false;
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
const changes: Record<string, unknown> = {};
|
|
3816
|
+
for (const change of resolution.changes) changes[change.propertyId] = change.value;
|
|
3817
|
+
applyDesignValuesToPreview(widgetId, changes);
|
|
3818
|
+
// The card records the composition the change landed in, so a reload can
|
|
3819
|
+
// re-attach it to that composition's live values instead of whatever
|
|
3820
|
+
// composition happens to be selected when the conversation is reopened.
|
|
3821
|
+
const card = toDesignControlRequest(
|
|
3822
|
+
resolution,
|
|
3823
|
+
schema,
|
|
3824
|
+
{ ...liveValues, ...changes },
|
|
3825
|
+
ps.selectedComposition?.id ?? null
|
|
3826
|
+
);
|
|
3827
|
+
getChatMethodsForWidget(widgetId)?.addDesignControlsMessage?.(resolution.summary, card);
|
|
3828
|
+
return true;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/**
|
|
3832
|
+
* Jev-backed fallback for the deterministic resolver: when the message names
|
|
3833
|
+
* a design property semantically and no literal value can be parsed, ask the
|
|
3834
|
+
* gateway to classify the property and a bounded value. Acts only when the
|
|
3835
|
+
* gateway says `acted` (enforce mode + confident verdict); otherwise the
|
|
3836
|
+
* caller generates exactly as before. Never throws, never blocks a send.
|
|
3837
|
+
*
|
|
3838
|
+
* The send's own classification rides along so the shadow telemetry can split
|
|
3839
|
+
* every dial's outcome by the ask shape that produced it — a fall-through where
|
|
3840
|
+
* the property was named indicts the value option strings, one where it wasn't
|
|
3841
|
+
* indicts the property strings or the confidence floor.
|
|
3842
|
+
*/
|
|
3843
|
+
async function tryApplyDesignIntentViaJev(
|
|
3844
|
+
widgetId: string,
|
|
3845
|
+
message: string,
|
|
3846
|
+
classification: DesignIntentClassification | null
|
|
3847
|
+
): Promise<boolean> {
|
|
3848
|
+
const ps = getPanelDisplayState(widgetId);
|
|
3849
|
+
const schema = ps.compositionDesignSchema;
|
|
3850
|
+
if (!schema || !apiBasePath) return false;
|
|
3851
|
+
|
|
3852
|
+
const result = await resolveDesignIntentViaJev({
|
|
3853
|
+
apiBasePath,
|
|
3854
|
+
token: authenticatedClientsToken,
|
|
3855
|
+
message,
|
|
3856
|
+
designSchema: schema,
|
|
3857
|
+
currentValues: ps.liveDesignValues ?? {}
|
|
3858
|
+
});
|
|
3859
|
+
if (!result) {
|
|
3860
|
+
recordDesignIntentJevOutcome({
|
|
3861
|
+
source: 'client_unavailable',
|
|
3862
|
+
acted: false,
|
|
3863
|
+
reason: 'no_response',
|
|
3864
|
+
classification
|
|
3865
|
+
});
|
|
3866
|
+
return false;
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
recordDesignIntentJevOutcome({
|
|
3870
|
+
source: result.source,
|
|
3871
|
+
acted: result.acted,
|
|
3872
|
+
reason: result.reason,
|
|
3873
|
+
classification
|
|
3874
|
+
});
|
|
3875
|
+
|
|
3876
|
+
if (!result.acted || !result.property) {
|
|
3877
|
+
if (debugMode || result.source === 'jev') {
|
|
3878
|
+
console.log('[DesignIntentJev] fall through:', result.reason, result.property ?? '');
|
|
3879
|
+
}
|
|
3880
|
+
return false;
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
const property = collectDesignProperties(schema).find((p) => p.id === result.property);
|
|
3884
|
+
const label = property?.label ?? result.property;
|
|
3885
|
+
applyDesignValuesToPreview(widgetId, { [result.property]: result.value });
|
|
3886
|
+
const card: DesignControlRequest = {
|
|
3887
|
+
summary: `Set ${label} to ${String(result.value)}.`,
|
|
3888
|
+
properties: [
|
|
3889
|
+
{
|
|
3890
|
+
id: result.property,
|
|
3891
|
+
label,
|
|
3892
|
+
controllerType: property?.controllerType ?? '',
|
|
3893
|
+
value: result.value,
|
|
3894
|
+
options: property?.options,
|
|
3895
|
+
min: property?.min,
|
|
3896
|
+
max: property?.max,
|
|
3897
|
+
step: property?.step,
|
|
3898
|
+
helpText: property?.helpText
|
|
3899
|
+
}
|
|
3900
|
+
],
|
|
3901
|
+
// Same attribution as the deterministic path: the card is persisted
|
|
3902
|
+
// with the composition it was authored against (see `toDesignControlRequest`).
|
|
3903
|
+
compositionId: ps.selectedComposition?.id ?? null
|
|
3904
|
+
};
|
|
3905
|
+
getChatMethodsForWidget(widgetId)?.addDesignControlsMessage?.(card.summary, card);
|
|
3906
|
+
return true;
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
/**
|
|
3910
|
+
* Classify a hand-drawn canvas stroke the Canvas's own geometry engine could
|
|
3911
|
+
* not decide.
|
|
3912
|
+
*
|
|
3913
|
+
* The Canvas measures the stroke and gates out every shape the measurements
|
|
3914
|
+
* rule out, then asks here only when several shapes remain. The gateway owns
|
|
3915
|
+
* the JEV decision and its confidence gate; this returns its shape, or null
|
|
3916
|
+
* (leave the stroke exactly as drawn) when it declines, degrades, or is not
|
|
3917
|
+
* configured. Null is always a valid answer, so nothing here can block a draw.
|
|
3918
|
+
*/
|
|
3919
|
+
async function handleRecognizeStroke(
|
|
3920
|
+
request: StrokeRecognitionRequest
|
|
3921
|
+
): Promise<StrokeRecognitionResult | null> {
|
|
3922
|
+
const reply = await recognizeStrokeViaJev({
|
|
3923
|
+
apiBasePath,
|
|
3924
|
+
token: authenticatedClientsToken,
|
|
3925
|
+
request
|
|
3926
|
+
});
|
|
3927
|
+
if (debugMode) {
|
|
3928
|
+
console.log(
|
|
3929
|
+
'[CanvasShapeSnap]',
|
|
3930
|
+
`candidates: ${request.candidates.map((candidate) => candidate.shape).join(', ')}`,
|
|
3931
|
+
`→ ${reply?.shape ?? 'as drawn'}`,
|
|
3932
|
+
`(${reply?.source ?? 'client_unavailable'} / ${reply?.reason ?? 'no_response'})`
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
return reply;
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3465
3938
|
/** Handle property change from PropsEditor — send to widget preview iframe */
|
|
3466
3939
|
function handleEditorPropertyChange(event: CustomEvent<{ schema: string; propertyId: string; value: unknown; opacity?: number; itemId?: string }>) {
|
|
3467
3940
|
const { schema, propertyId, value, itemId } = event.detail;
|
|
@@ -3506,10 +3979,12 @@
|
|
|
3506
3979
|
/** Handle content items reorder/add/delete from PropsEditor */
|
|
3507
3980
|
function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
|
|
3508
3981
|
const incoming = Array.isArray(event.detail.items) ? event.detail.items : [];
|
|
3509
|
-
|
|
3510
|
-
?? compositionContentSchema?.data?.contentItems
|
|
3511
|
-
|
|
3512
|
-
|
|
3982
|
+
const schemaItems = compositionContentSchema?.contentItems
|
|
3983
|
+
?? compositionContentSchema?.data?.contentItems;
|
|
3984
|
+
liveContentItems = mergeContentItemsWithSchema(
|
|
3985
|
+
incoming,
|
|
3986
|
+
Array.isArray(schemaItems) ? schemaItems : []
|
|
3987
|
+
);
|
|
3513
3988
|
// console.log('[PropsEditor→Preview] Content items changed:', liveContentItems.length, 'items');
|
|
3514
3989
|
getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
|
|
3515
3990
|
type: 'widgetic:update',
|
|
@@ -4721,8 +5196,7 @@
|
|
|
4721
5196
|
return !cid || cid === targetCanvasId;
|
|
4722
5197
|
}) || fromList[0] || null;
|
|
4723
5198
|
if (primaryId) {
|
|
4724
|
-
|
|
4725
|
-
await persistPrimaryWidgetId(primaryId);
|
|
5199
|
+
console.warn('[CreatorApp] Canvas has no saved primary — showing', primaryId.slice(0, 8), 'without replacing the saved one');
|
|
4726
5200
|
}
|
|
4727
5201
|
}
|
|
4728
5202
|
if (primaryId) {
|
|
@@ -8628,11 +9102,22 @@ Requirements:
|
|
|
8628
9102
|
promptImages = imagesToSend;
|
|
8629
9103
|
savePromptImagesToSession();
|
|
8630
9104
|
}
|
|
8631
|
-
} else if (promptImpliesVisionReference(prompt)) {
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
9105
|
+
} else if (promptImpliesVisionReference(prompt) && !options.forkUserMessage) {
|
|
9106
|
+
const shapeDataUrl =
|
|
9107
|
+
typeof canvasRef?.getWidgetShapeDataUrl === 'function'
|
|
9108
|
+
? canvasRef.getWidgetShapeDataUrl(generationWidgetId)
|
|
9109
|
+
: null;
|
|
9110
|
+
if (typeof shapeDataUrl === 'string' && shapeDataUrl.startsWith('data:image/') && shapeDataUrl.length > 100) {
|
|
9111
|
+
const compressedShape = await compressImageDataUrlForCodegen(shapeDataUrl);
|
|
9112
|
+
if (compressedShape.startsWith('data:image/') && compressedShape.length > 100) {
|
|
9113
|
+
imagesToSend = [compressedShape];
|
|
9114
|
+
}
|
|
9115
|
+
} else {
|
|
9116
|
+
console.warn(
|
|
9117
|
+
'[WidgetCreator] Codegen proceeding WITHOUT image although prompt references one — widget',
|
|
9118
|
+
generationWidgetId,
|
|
9119
|
+
);
|
|
9120
|
+
}
|
|
8636
9121
|
}
|
|
8637
9122
|
let coderModelForRequest = selectedModelId || defaultModelId;
|
|
8638
9123
|
if (imagesToSend?.length || attachmentIdsToSend?.length) {
|
|
@@ -8691,7 +9176,9 @@ Requirements:
|
|
|
8691
9176
|
? undefined
|
|
8692
9177
|
: resolvePlannerModelIdForGeneration(prompt, coderModelForRequest);
|
|
8693
9178
|
const runtimeLogsForRequest = collectRuntimeLogsForCodegen(generationWidgetId);
|
|
8694
|
-
|
|
9179
|
+
// The generated client types this call as void and drops the 202 body.
|
|
9180
|
+
// Read the raw JSON so operationId and the intent-gate payload survive.
|
|
9181
|
+
const accepted = await agentClient.agentCodeGeneratePostRaw({
|
|
8695
9182
|
generateCodeRequest: {
|
|
8696
9183
|
repositoryId: repoId,
|
|
8697
9184
|
prompt,
|
|
@@ -8704,8 +9191,15 @@ Requirements:
|
|
|
8704
9191
|
compileFixOnly: options.compileFixOnly === true,
|
|
8705
9192
|
skipIntentGate: options.skipIntentGate === true,
|
|
8706
9193
|
chipIntent: options.chipIntent,
|
|
9194
|
+
brandKit: brandKitForRequest(),
|
|
8707
9195
|
} as GenerateCodeRequest
|
|
8708
9196
|
});
|
|
9197
|
+
let response: { data?: Record<string, unknown>; error?: string; message?: string } | null = null;
|
|
9198
|
+
try {
|
|
9199
|
+
response = await accepted.raw.json();
|
|
9200
|
+
} catch (parseError) {
|
|
9201
|
+
console.warn('[WidgetCreator] Code generation response was not JSON', parseError);
|
|
9202
|
+
}
|
|
8709
9203
|
// console.log('[WidgetCreator] Code generation request sent for widget', generationWidgetId.substring(0, 8), 'with model:', coderModelForRequest, 'planner:', plannerModelIdForRequest || 'none', 'conversationId:', activeConversationId || 'none', 'images:', imagesToSend?.length || 0, 'attachmentIds:', attachmentIdsToSend?.length || 0, 'runtimeLogs:', runtimeLogsForRequest?.length ?? 0);
|
|
8710
9204
|
// console.log('Widget Creator: Code generation response:', response);
|
|
8711
9205
|
|
|
@@ -8759,7 +9253,8 @@ Requirements:
|
|
|
8759
9253
|
return;
|
|
8760
9254
|
}
|
|
8761
9255
|
|
|
8762
|
-
|
|
9256
|
+
const tokenEnvelope = response as unknown as { error?: string } | undefined;
|
|
9257
|
+
if (tokenEnvelope?.error === 'TOKEN_EXPIRED') {
|
|
8763
9258
|
patchCodegenUiState(generationWidgetId, {
|
|
8764
9259
|
generateCodeStatus: '❌ Authentication expired. Please login again.',
|
|
8765
9260
|
lastGenerationFailed: true,
|
|
@@ -8789,9 +9284,9 @@ Requirements:
|
|
|
8789
9284
|
return;
|
|
8790
9285
|
}
|
|
8791
9286
|
|
|
8792
|
-
const generateData = (
|
|
9287
|
+
const generateData = (response?.data ?? response) as {
|
|
8793
9288
|
operationId?: string;
|
|
8794
|
-
};
|
|
9289
|
+
} | null;
|
|
8795
9290
|
const operationId = generateData?.operationId;
|
|
8796
9291
|
|
|
8797
9292
|
// The Worker serves rebuilt code at the same preview URL, so the panel
|
|
@@ -8874,6 +9369,7 @@ Requirements:
|
|
|
8874
9369
|
clearActiveOperationId();
|
|
8875
9370
|
|
|
8876
9371
|
const completedCommitSha = getOperationCommitId(op.metadata);
|
|
9372
|
+
const completedCommitTitle = getOperationCommitTitle(op.metadata);
|
|
8877
9373
|
const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
|
|
8878
9374
|
const completedCached = getOperationCached(op.metadata);
|
|
8879
9375
|
const completedCachedFiles = getOperationCachedFiles(op.metadata);
|
|
@@ -8894,6 +9390,11 @@ Requirements:
|
|
|
8894
9390
|
|
|
8895
9391
|
if (completedCommitSha && generationWidgetId) {
|
|
8896
9392
|
saveCommitIdForWidget(generationWidgetId, completedCommitSha);
|
|
9393
|
+
// The publish path stamps its optimistic row from the publish
|
|
9394
|
+
// response; a generation stamps this turn's draft row from the
|
|
9395
|
+
// operation, so the version menu shows what the turn shipped
|
|
9396
|
+
// without waiting for the history refetch below.
|
|
9397
|
+
stampCodegenTitleIntoPanelHistory(generationWidgetId, completedCommitSha, completedCommitTitle);
|
|
8897
9398
|
}
|
|
8898
9399
|
|
|
8899
9400
|
// Mark pending publish when this widget already has a live version.
|
|
@@ -8932,6 +9433,10 @@ Requirements:
|
|
|
8932
9433
|
const alignCommitSha = completedCommitSha;
|
|
8933
9434
|
const alignGeneratedVersion = (attempt: number) => {
|
|
8934
9435
|
void loadPublishHistoryForPanel(alignWidgetId, { silent: true }).then(() => {
|
|
9436
|
+
// The refetch is authoritative, but a snapshot that raced the
|
|
9437
|
+
// row's title (or an API row still missing it) gets the
|
|
9438
|
+
// operation's label — the guard inside keeps a user rename.
|
|
9439
|
+
stampCodegenTitleIntoPanelHistory(generationWidgetId, completedCommitSha, completedCommitTitle);
|
|
8935
9440
|
const versionId = findVersionIdMatchingCommit(alignWidgetId, alignCommitSha);
|
|
8936
9441
|
const matchedByCommit = !!alignCommitSha
|
|
8937
9442
|
&& !!versionId
|
|
@@ -9118,6 +9623,17 @@ Requirements:
|
|
|
9118
9623
|
);
|
|
9119
9624
|
}
|
|
9120
9625
|
|
|
9626
|
+
// Design-contract violations mean some design controls will silently
|
|
9627
|
+
// do nothing (the property isn't wired, or the widget never applies
|
|
9628
|
+
// design updates). Say which, right where the user is looking — a
|
|
9629
|
+
// control that looks live but is dead is the worst failure mode here.
|
|
9630
|
+
// Served from the operation metadata; the server also embeds it in the
|
|
9631
|
+
// persisted reply, so it survives a reload.
|
|
9632
|
+
const designContractWarning = getOperationDesignContractWarning(op.metadata);
|
|
9633
|
+
if (designContractWarning) {
|
|
9634
|
+
panelChat?.addAssistantMessage(designContractWarning, true);
|
|
9635
|
+
}
|
|
9636
|
+
|
|
9121
9637
|
setTimeout(async () => {
|
|
9122
9638
|
if (!getWidgetDetailsRef(generationWidgetId)) return;
|
|
9123
9639
|
const activePanelRef = getWidgetDetailsRef(generationWidgetId);
|
|
@@ -9164,6 +9680,7 @@ Requirements:
|
|
|
9164
9680
|
patchCodegenUiState(generationWidgetId, {
|
|
9165
9681
|
generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
|
|
9166
9682
|
});
|
|
9683
|
+
void refreshTrialCounter();
|
|
9167
9684
|
setTimeout(async () => {
|
|
9168
9685
|
await panelChat?.reloadConversationMessages?.();
|
|
9169
9686
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
@@ -9213,6 +9730,26 @@ Requirements:
|
|
|
9213
9730
|
return;
|
|
9214
9731
|
}
|
|
9215
9732
|
|
|
9733
|
+
// Handle retired-surface 410 (PLANNER_REMOVED / VM_REMOVED): the server
|
|
9734
|
+
// says this running bundle predates a removal — no retry can fix it.
|
|
9735
|
+
if (isRetiredSurfaceError(error)) {
|
|
9736
|
+
console.warn('Widget Creator: retired surface answered 410', { code: error.code, message: error.message });
|
|
9737
|
+
patchCodegenUiState(generationWidgetId, {
|
|
9738
|
+
generateCodeStatus: '⚠️ This app needs a refresh to continue.',
|
|
9739
|
+
lastGenerationFailed: true,
|
|
9740
|
+
lastGenerationError: error.message,
|
|
9741
|
+
});
|
|
9742
|
+
showToast('warning', 'This app needs a refresh', {
|
|
9743
|
+
description: error.message,
|
|
9744
|
+
duration: 12000,
|
|
9745
|
+
action: {
|
|
9746
|
+
label: 'Reload',
|
|
9747
|
+
onClick: () => window.location.reload()
|
|
9748
|
+
}
|
|
9749
|
+
});
|
|
9750
|
+
return;
|
|
9751
|
+
}
|
|
9752
|
+
|
|
9216
9753
|
// Handle rate limiting (429) — too many API requests in the time window
|
|
9217
9754
|
if (httpStatus === 429) {
|
|
9218
9755
|
console.warn('Widget Creator: Rate limited (429) during code generation');
|
|
@@ -9303,7 +9840,8 @@ Requirements:
|
|
|
9303
9840
|
widgetId: string,
|
|
9304
9841
|
version: number,
|
|
9305
9842
|
artifactUrl?: string | null,
|
|
9306
|
-
commitSha?: string | null
|
|
9843
|
+
commitSha?: string | null,
|
|
9844
|
+
commitTitle?: string | null
|
|
9307
9845
|
): void {
|
|
9308
9846
|
const jsPath = resolvePublishedJsPath(widgetId, version, artifactUrl);
|
|
9309
9847
|
const prevSnap = getPanelState(widgetId);
|
|
@@ -9327,13 +9865,36 @@ Requirements:
|
|
|
9327
9865
|
: w
|
|
9328
9866
|
);
|
|
9329
9867
|
|
|
9868
|
+
// The optimistic row replaces the refetch the publish flow used to wait
|
|
9869
|
+
// for, so it must carry every field the dropdown renders — not just the
|
|
9870
|
+
// sha/artifact pair. The title comes from the publish response (or the
|
|
9871
|
+
// WebSocket payload for cross-tab/background publishes); when the caller
|
|
9872
|
+
// had none, an already-known row for this version keeps its title rather
|
|
9873
|
+
// than being downgraded to untitled (the set-live path re-merges a row the
|
|
9874
|
+
// panel already displayed).
|
|
9875
|
+
const previousRow = prevSnap.publishHistory.find((r) => r.version === version);
|
|
9876
|
+
const titleFromCaller = typeof commitTitle === 'string' && commitTitle.trim() !== ''
|
|
9877
|
+
? commitTitle
|
|
9878
|
+
: null;
|
|
9879
|
+
const titleForEntry = titleFromCaller
|
|
9880
|
+
?? (previousRow?.commitTitle ?? previousRow?.commit_title ?? null);
|
|
9330
9881
|
const optimisticHistoryEntry = normalizedCommit
|
|
9331
9882
|
? {
|
|
9332
9883
|
version,
|
|
9333
9884
|
commitSha: normalizedCommit,
|
|
9334
9885
|
commit_sha: normalizedCommit,
|
|
9886
|
+
commitTitle: titleForEntry,
|
|
9887
|
+
commit_title: titleForEntry,
|
|
9335
9888
|
artifactUrl: artifactUrl ?? jsPath,
|
|
9336
9889
|
artifact_url: artifactUrl ?? jsPath,
|
|
9890
|
+
// Draft/Live split and timestamps: publishing this version is what
|
|
9891
|
+
// made it live, and a re-merge must not erase the original dates.
|
|
9892
|
+
publishedAt: previousRow?.publishedAt ?? previousRow?.published_at ?? new Date().toISOString(),
|
|
9893
|
+
published_at: previousRow?.publishedAt ?? previousRow?.published_at ?? new Date().toISOString(),
|
|
9894
|
+
createdAt: previousRow?.createdAt ?? previousRow?.created_at ?? new Date().toISOString(),
|
|
9895
|
+
created_at: previousRow?.createdAt ?? previousRow?.created_at ?? new Date().toISOString(),
|
|
9896
|
+
isPublished: true,
|
|
9897
|
+
is_published: true,
|
|
9337
9898
|
}
|
|
9338
9899
|
: null;
|
|
9339
9900
|
const mergedHistory = optimisticHistoryEntry
|
|
@@ -9389,6 +9950,44 @@ Requirements:
|
|
|
9389
9950
|
void loadPublishHistoryForPanel(widgetId, { silent: true });
|
|
9390
9951
|
}
|
|
9391
9952
|
|
|
9953
|
+
/**
|
|
9954
|
+
* Stamp the version title a codegen turn shipped onto that turn's history row.
|
|
9955
|
+
*
|
|
9956
|
+
* The publish path does this for its optimistic entry (the title rides the
|
|
9957
|
+
* publish response into `applyPublishSuccessToWidgetState`); a generation
|
|
9958
|
+
* only refetched the history, so the fresh draft row kept whatever title the
|
|
9959
|
+
* refetch returned — the operation's own label never reached the panel.
|
|
9960
|
+
*
|
|
9961
|
+
* Same discipline as the publish optimistic entry: the row the server already
|
|
9962
|
+
* wrote keeps its title (a user rename must not be reverted), and only the
|
|
9963
|
+
* row this turn *created* is stamped. Best-effort and synchronous — the
|
|
9964
|
+
* authoritative refetch that follows corrects anything this gets wrong.
|
|
9965
|
+
*/
|
|
9966
|
+
function stampCodegenTitleIntoPanelHistory(widgetId: string, commitSha: string | null, commitTitle: string | null): void {
|
|
9967
|
+
if (!commitSha || !commitTitle) return;
|
|
9968
|
+
|
|
9969
|
+
const snap = getPanelState(widgetId);
|
|
9970
|
+
const row = snap.publishHistory.find(
|
|
9971
|
+
(r) => (r.commitSha ?? r.commit_sha) === commitSha && !(r.publishedAt ?? r.published_at)
|
|
9972
|
+
);
|
|
9973
|
+
// The server's recordGeneration wrote this row with the same title; only a
|
|
9974
|
+
// stale snapshot makes the stamp a visible change. Never overwrite an
|
|
9975
|
+
// existing title (a user rename) with the pipeline's wording.
|
|
9976
|
+
const currentTitle = row?.commitTitle ?? row?.commit_title ?? null;
|
|
9977
|
+
if (!row || (currentTitle && currentTitle !== commitTitle)) return;
|
|
9978
|
+
|
|
9979
|
+
const history = snap.publishHistory.map((r) =>
|
|
9980
|
+
r === row
|
|
9981
|
+
? { ...r, commitTitle: commitTitle, commit_title: commitTitle }
|
|
9982
|
+
: r
|
|
9983
|
+
);
|
|
9984
|
+
patchPanelSnapshot(widgetId, { publishHistory: history });
|
|
9985
|
+
shapePublishHistoryByWidgetId = {
|
|
9986
|
+
...shapePublishHistoryByWidgetId,
|
|
9987
|
+
[widgetId]: [...history],
|
|
9988
|
+
};
|
|
9989
|
+
}
|
|
9990
|
+
|
|
9392
9991
|
/**
|
|
9393
9992
|
* Publish the current widget: saves first, then triggers the production build job.
|
|
9394
9993
|
* Flow: Save → fetch latest component → POST /canvases/{id}/components/{id}/publish
|
|
@@ -9516,10 +10115,11 @@ Requirements:
|
|
|
9516
10115
|
setCrossTabPublishing(false, capturedWidgetId);
|
|
9517
10116
|
|
|
9518
10117
|
const publishEnvelope = publishData as unknown as {
|
|
9519
|
-
data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
|
|
10118
|
+
data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null; commitTitle?: string | null };
|
|
9520
10119
|
version?: number;
|
|
9521
10120
|
artifactUrl?: string | null;
|
|
9522
10121
|
commitSha?: string | null;
|
|
10122
|
+
commitTitle?: string | null;
|
|
9523
10123
|
};
|
|
9524
10124
|
const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
|
|
9525
10125
|
const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
|
|
@@ -9528,7 +10128,13 @@ Requirements:
|
|
|
9528
10128
|
?? publishEnvelope?.commitSha
|
|
9529
10129
|
?? commitShaForPublish
|
|
9530
10130
|
?? null;
|
|
9531
|
-
|
|
10131
|
+
// The version row's title, returned so the optimistic history entry can
|
|
10132
|
+
// show the real label instead of waiting for the history refetch.
|
|
10133
|
+
const commitTitle =
|
|
10134
|
+
publishEnvelope?.data?.commitTitle
|
|
10135
|
+
?? publishEnvelope?.commitTitle
|
|
10136
|
+
?? null;
|
|
10137
|
+
applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha, commitTitle);
|
|
9532
10138
|
|
|
9533
10139
|
if (!publishOpts.silent) {
|
|
9534
10140
|
showToast('success', 'Widget published!', {
|
|
@@ -9547,10 +10153,8 @@ Requirements:
|
|
|
9547
10153
|
publishedPanelRef?.invalidatePublishedArtifactCache?.();
|
|
9548
10154
|
await publishedPanelRef?.refreshPreview({ withRetry: true });
|
|
9549
10155
|
|
|
9550
|
-
if (!publishOpts.silent) {
|
|
9551
|
-
isHistoryOpen = true;
|
|
9552
|
-
}
|
|
9553
10156
|
await loadPublishHistory();
|
|
10157
|
+
void refreshTrialCounter();
|
|
9554
10158
|
|
|
9555
10159
|
} catch (err: any) {
|
|
9556
10160
|
console.error('[Publish] Error publishing widget:', err);
|
|
@@ -9948,6 +10552,13 @@ Requirements:
|
|
|
9948
10552
|
return typeof raw === 'string' && /^[a-f0-9]{7,40}$/i.test(raw) ? raw : null;
|
|
9949
10553
|
}
|
|
9950
10554
|
|
|
10555
|
+
/** The version title a completed codegen operation shipped ("Update header styles"), from operation metadata. */
|
|
10556
|
+
function getOperationCommitTitle(metadata?: Record<string, unknown>): string | null {
|
|
10557
|
+
if (!metadata) return null;
|
|
10558
|
+
const raw = metadata.commitTitle ?? metadata.commit_title;
|
|
10559
|
+
return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null;
|
|
10560
|
+
}
|
|
10561
|
+
|
|
9951
10562
|
function getOperationPreviewBuildError(metadata?: Record<string, unknown>): string | null {
|
|
9952
10563
|
if (!metadata) return null;
|
|
9953
10564
|
const raw = metadata.previewBuildError ?? metadata.preview_build_error;
|
|
@@ -9955,6 +10566,17 @@ Requirements:
|
|
|
9955
10566
|
return sanitizePreviewBuildError(raw);
|
|
9956
10567
|
}
|
|
9957
10568
|
|
|
10569
|
+
/**
|
|
10570
|
+
* The short design-contract warning the codegen service attached to a
|
|
10571
|
+
* completed operation, naming the design properties that will silently do
|
|
10572
|
+
* nothing. Null when the contract was clean (or nothing checked it).
|
|
10573
|
+
*/
|
|
10574
|
+
function getOperationDesignContractWarning(metadata?: Record<string, unknown>): string | null {
|
|
10575
|
+
if (!metadata) return null;
|
|
10576
|
+
const raw = metadata.designContractWarning ?? metadata.design_contract_warning;
|
|
10577
|
+
return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null;
|
|
10578
|
+
}
|
|
10579
|
+
|
|
9958
10580
|
/** True when the operation completed from the semantic cache (no new generation ran). */
|
|
9959
10581
|
function getOperationCached(metadata?: Record<string, unknown>): boolean {
|
|
9960
10582
|
return metadata?.cached === true;
|
|
@@ -10203,8 +10825,8 @@ Requirements:
|
|
|
10203
10825
|
liveDesignValues: Record<string, unknown>;
|
|
10204
10826
|
liveContentValues: Record<string, unknown>;
|
|
10205
10827
|
liveContentItems: any[];
|
|
10206
|
-
compositionDesignSchema:
|
|
10207
|
-
compositionContentSchema:
|
|
10828
|
+
compositionDesignSchema: DesignSchemaView | null;
|
|
10829
|
+
compositionContentSchema: ContentSchemaView | null;
|
|
10208
10830
|
compositions: any[];
|
|
10209
10831
|
selectedComposition: any;
|
|
10210
10832
|
compositionDirty: boolean;
|
|
@@ -10827,8 +11449,10 @@ Requirements:
|
|
|
10827
11449
|
return null;
|
|
10828
11450
|
}
|
|
10829
11451
|
|
|
10830
|
-
//
|
|
10831
|
-
|
|
11452
|
+
// 'preview' is the explicit post-codegen source. An unpublished commit must
|
|
11453
|
+
// not replace Live: on open the dropdown stays on the published version,
|
|
11454
|
+
// and this URL has to be that same artifact.
|
|
11455
|
+
if (source === 'preview') {
|
|
10832
11456
|
if (workerUrl) return workerUrl;
|
|
10833
11457
|
}
|
|
10834
11458
|
|
|
@@ -10856,7 +11480,9 @@ Requirements:
|
|
|
10856
11480
|
if (newestDraft != null) return `v${newestDraft}`;
|
|
10857
11481
|
return null;
|
|
10858
11482
|
}
|
|
10859
|
-
|
|
11483
|
+
if (liveVersion != null) return `v${liveVersion}`;
|
|
11484
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
11485
|
+
return newestDraft != null ? `v${newestDraft}` : null;
|
|
10860
11486
|
}
|
|
10861
11487
|
|
|
10862
11488
|
function formatPublishHistoryDate(record: {
|
|
@@ -10957,7 +11583,9 @@ Requirements:
|
|
|
10957
11583
|
const ps = getPanelDisplayState(widgetId);
|
|
10958
11584
|
const liveVersion =
|
|
10959
11585
|
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10960
|
-
|
|
11586
|
+
if (liveVersion != null) return `v:${liveVersion}`;
|
|
11587
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
11588
|
+
return newestDraft != null ? `v:${newestDraft}` : null;
|
|
10961
11589
|
}
|
|
10962
11590
|
|
|
10963
11591
|
function selectVersionInDropdowns(widgetId: string, versionId: string): void {
|
|
@@ -11687,7 +12315,7 @@ Requirements:
|
|
|
11687
12315
|
? getWidgetDetailsRef(selectedWidgetId)
|
|
11688
12316
|
: null;
|
|
11689
12317
|
|
|
11690
|
-
function getWidgetDetailsRef(widgetId?: string | null):
|
|
12318
|
+
function getWidgetDetailsRef(widgetId?: string | null): WidgetDetailsInstance | null {
|
|
11691
12319
|
const id = widgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
|
|
11692
12320
|
if (!id) return null;
|
|
11693
12321
|
const instanceKey = panelInstanceKeyByWidgetId[id] ?? id;
|
|
@@ -12610,7 +13238,9 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
|
|
|
12610
13238
|
applyPublishSuccessToWidgetState(
|
|
12611
13239
|
publishWidgetId,
|
|
12612
13240
|
publishedVersion,
|
|
12613
|
-
publishResult.artifactUrl ?? null
|
|
13241
|
+
publishResult.artifactUrl ?? null,
|
|
13242
|
+
publishResult.commitSha ?? null,
|
|
13243
|
+
publishResult.commitTitle ?? null
|
|
12614
13244
|
);
|
|
12615
13245
|
const publishedName = getWidgetById(publishWidgetId)?.name || 'Widget';
|
|
12616
13246
|
showToast('success', `"${publishedName}" published!`, {
|
|
@@ -12828,7 +13458,7 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
|
12828
13458
|
widgetId,
|
|
12829
13459
|
version
|
|
12830
13460
|
});
|
|
12831
|
-
const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
|
|
13461
|
+
const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string; commitTitle?: string; commit_title?: string } | undefined;
|
|
12832
13462
|
const liveData = nested?.data ?? nested ?? {};
|
|
12833
13463
|
const history = getPanelDisplayState(widgetId).publishHistory;
|
|
12834
13464
|
const record = history.find((row) => row.version === version);
|
|
@@ -12838,7 +13468,12 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
|
12838
13468
|
const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
|
|
12839
13469
|
|| (liveData as { artifact_url?: string }).artifact_url
|
|
12840
13470
|
|| record?.artifactUrl || record?.artifact_url || null;
|
|
12841
|
-
|
|
13471
|
+
// The row's current title — set-live re-merges a row the panel already
|
|
13472
|
+
// showed, so the response value keeps the label without a refetch.
|
|
13473
|
+
const commitTitle = (liveData as { commitTitle?: string; commit_title?: string }).commitTitle
|
|
13474
|
+
|| (liveData as { commit_title?: string }).commit_title
|
|
13475
|
+
|| record?.commitTitle || record?.commit_title || null;
|
|
13476
|
+
applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha, commitTitle);
|
|
12842
13477
|
shapePreviewSourceByWidgetId = {
|
|
12843
13478
|
...shapePreviewSourceByWidgetId,
|
|
12844
13479
|
[widgetId]: `v:${version}`,
|
|
@@ -12865,6 +13500,63 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
|
12865
13500
|
}
|
|
12866
13501
|
}
|
|
12867
13502
|
|
|
13503
|
+
/**
|
|
13504
|
+
* Renames one version's title — the owner's correction of the wording the
|
|
13505
|
+
* pipeline authored. Optimistic on the row; a failure rolls the row back
|
|
13506
|
+
* and says so in a toast.
|
|
13507
|
+
*/
|
|
13508
|
+
async function renameVersionTitleForWidget(widgetId: string, version: number, title: string) {
|
|
13509
|
+
if (!widgetId || !$userSession?.jwt) return;
|
|
13510
|
+
if (!widgetsClient) {
|
|
13511
|
+
showToast('error', 'Widgets API client is not configured');
|
|
13512
|
+
return;
|
|
13513
|
+
}
|
|
13514
|
+
|
|
13515
|
+
const renameKey = `${widgetId}:${version}`;
|
|
13516
|
+
renamingTitleFor = renameKey;
|
|
13517
|
+
|
|
13518
|
+
// Optimistic: write the trimmed title into the row immediately so the
|
|
13519
|
+
// dropdown does not flicker while the PATCH is in flight.
|
|
13520
|
+
const history = getPanelDisplayState(widgetId).publishHistory;
|
|
13521
|
+
const record = history.find((row) => Number(row.version) === version);
|
|
13522
|
+
const previousTitle = record
|
|
13523
|
+
? (record.commitTitle ?? record.commit_title ?? null)
|
|
13524
|
+
: null;
|
|
13525
|
+
if (record) {
|
|
13526
|
+
record.commitTitle = title === '' ? null : title;
|
|
13527
|
+
record.commit_title = title === '' ? null : title;
|
|
13528
|
+
}
|
|
13529
|
+
panelDisplayRevision += 1;
|
|
13530
|
+
|
|
13531
|
+
try {
|
|
13532
|
+
const res = await widgetsClient.renameWidgetPublishHistoryTitle({
|
|
13533
|
+
widgetId,
|
|
13534
|
+
version,
|
|
13535
|
+
renameWidgetPublishHistoryTitleRequest: { commitTitle: title },
|
|
13536
|
+
});
|
|
13537
|
+
const stored = (res as { data?: { commitTitle?: string | null } }).data?.commitTitle;
|
|
13538
|
+
// The stored value is authoritative (server trims and bounds).
|
|
13539
|
+
if (record) {
|
|
13540
|
+
record.commitTitle = stored ?? null;
|
|
13541
|
+
record.commit_title = stored ?? null;
|
|
13542
|
+
}
|
|
13543
|
+
panelDisplayRevision += 1;
|
|
13544
|
+
showToast('success', stored ? 'Version renamed' : 'Version title cleared');
|
|
13545
|
+
} catch (err) {
|
|
13546
|
+
// Roll the row back; nothing else needs reloading — the write did not land.
|
|
13547
|
+
if (record) {
|
|
13548
|
+
record.commitTitle = previousTitle;
|
|
13549
|
+
record.commit_title = previousTitle;
|
|
13550
|
+
}
|
|
13551
|
+
panelDisplayRevision += 1;
|
|
13552
|
+
showToast('error', 'Could not rename the version', {
|
|
13553
|
+
description: err instanceof ResponseError ? 'The server refused the rename.' : 'Check your connection and try again.',
|
|
13554
|
+
});
|
|
13555
|
+
} finally {
|
|
13556
|
+
renamingTitleFor = null;
|
|
13557
|
+
}
|
|
13558
|
+
}
|
|
13559
|
+
|
|
12868
13560
|
async function setLiveVersion(version: number) {
|
|
12869
13561
|
if (!selectedWidgetId) return;
|
|
12870
13562
|
await setLiveVersionForWidget(selectedWidgetId, version);
|
|
@@ -12980,8 +13672,12 @@ $: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId)
|
|
|
12980
13672
|
}
|
|
12981
13673
|
if (isGeneratingCode || confirmGenerateDialogOpen) return;
|
|
12982
13674
|
const widgetForDialog = getWidgetById(capturedWidgetId);
|
|
13675
|
+
const panelSnap = getPanelState(capturedWidgetId);
|
|
12983
13676
|
const hasRepo = !!normalizeRepositoryId(widgetForDialog);
|
|
12984
|
-
const
|
|
13677
|
+
const publishedVersion = panelSnap.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widgetForDialog);
|
|
13678
|
+
const hasNoCode = !panelSnap.lastCommitId
|
|
13679
|
+
&& publishedVersion == null
|
|
13680
|
+
&& (panelSnap.publishHistory?.length ?? 0) === 0;
|
|
12985
13681
|
const hasPrompt = codeGenerationPrompt !== DEFAULT_PROMPT || promptImages.length > 0;
|
|
12986
13682
|
const hasPreview = widgetDetails?.hasValidPreview() ?? false;
|
|
12987
13683
|
if (hasRepo && hasNoCode && !hasPreview && hasPrompt) {
|
|
@@ -15032,6 +15728,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15032
15728
|
|
|
15033
15729
|
<!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
|
|
15034
15730
|
<div class="wc-top-bar-right flex items-center gap-3 shrink-0">
|
|
15731
|
+
{#if trialCounter}
|
|
15732
|
+
<span
|
|
15733
|
+
class="wc-trial-counter shrink-0 whitespace-nowrap rounded-md border border-[#314F2F]/20 bg-[#f4f7f4] px-2 py-1 text-[11px] font-medium text-[#314F2F]"
|
|
15734
|
+
title={trialCounter.days == null ? 'Credits left on this account' : 'Free trial: credits left and days left'}
|
|
15735
|
+
>
|
|
15736
|
+
{trialCounter.credits} credits{trialCounter.trialEnded
|
|
15737
|
+
? ' · trial ended'
|
|
15738
|
+
: trialCounter.days == null
|
|
15739
|
+
? ''
|
|
15740
|
+
: ` · ${trialCounter.days} ${trialCounter.days === 1 ? 'day' : 'days'}`}
|
|
15741
|
+
</span>
|
|
15742
|
+
{/if}
|
|
15035
15743
|
{#if debugModeAvailable}
|
|
15036
15744
|
<HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
|
|
15037
15745
|
<button
|
|
@@ -15121,6 +15829,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15121
15829
|
canvasComponentReady = false;
|
|
15122
15830
|
}}
|
|
15123
15831
|
onConvertToWidget={handleConvertToWidget}
|
|
15832
|
+
onRecognizeStroke={handleRecognizeStroke}
|
|
15124
15833
|
onWidgetImageClick={handleWidgetImageClick}
|
|
15125
15834
|
onCanvasDirty={() => emitCreatorSaveState(true)}
|
|
15126
15835
|
onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
|
|
@@ -15136,8 +15845,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15136
15845
|
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
15137
15846
|
// console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
|
|
15138
15847
|
return;
|
|
15139
|
-
}
|
|
15140
|
-
const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
|
|
15848
|
+
} const active = canvasRef?.getSelectedObject?.();
|
|
15141
15849
|
if (active?._isWidgetImage && active?._widgetId) {
|
|
15142
15850
|
return;
|
|
15143
15851
|
}
|
|
@@ -15529,7 +16237,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15529
16237
|
{publishWidgetStatus}
|
|
15530
16238
|
publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
15531
16239
|
lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
|
|
15532
|
-
widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath
|
|
16240
|
+
widgetJsPath={ps.selectedWidgetJsPath ?? (typeof panelWidget?.jsPath === 'string' ? panelWidget.jsPath : null)}
|
|
15533
16241
|
{isGeneratingInOtherTab}
|
|
15534
16242
|
{isPublishingInOtherTab}
|
|
15535
16243
|
{isSettingLiveVersion}
|
|
@@ -15931,7 +16639,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15931
16639
|
<Portal target="wc-live-history-portal-{panelWidgetId}">
|
|
15932
16640
|
<button
|
|
15933
16641
|
type="button"
|
|
15934
|
-
class="wc-live-history-backdrop fixed
|
|
16642
|
+
class="wc-live-history-backdrop fixed left-0 right-0 bottom-0 top-14 z-[9998] cursor-default bg-transparent"
|
|
15935
16643
|
aria-label="Close version history"
|
|
15936
16644
|
onclick={() => { isHistoryOpen = false; }}
|
|
15937
16645
|
></button>
|
|
@@ -15955,12 +16663,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15955
16663
|
{@const rowIsViewing = rowVersion === Number(viewingVersion)}
|
|
15956
16664
|
{@const rowIsLive = !rowIsDraft && rowVersion === Number(panelPublishedVersion)}
|
|
15957
16665
|
{@const rowDateLabel = formatPublishHistoryDate(record)}
|
|
15958
|
-
{@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)}
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
dateLabel={rowDateLabel}
|
|
16666
|
+
{@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)} <VersionDropdownRow
|
|
16667
|
+
versionLabel={'v' + rowVersion}
|
|
16668
|
+
commitSha={record.commitSha ?? record.commit_sha ?? null}
|
|
16669
|
+
showCommit={debugMode}
|
|
16670
|
+
dateLabel={rowDateLabel}
|
|
15964
16671
|
isLive={rowIsLive}
|
|
15965
16672
|
isDraft={rowIsDraft}
|
|
15966
16673
|
selected={rowIsViewing}
|
|
@@ -16126,9 +16833,50 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
16126
16833
|
? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
|
|
16127
16834
|
: `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
|
|
16128
16835
|
>
|
|
16836
|
+
<!-- Cast to any below: @widgetic/chat is type-checked against its own
|
|
16837
|
+
Svelte install, so its Snippet type is nominally distinct from this
|
|
16838
|
+
package's. renderDesignControls is a real snippet either way. -->
|
|
16839
|
+
{#snippet renderDesignControls(spec)}
|
|
16840
|
+
<!-- `ps` is rebuilt on every panel display revision, so an attached
|
|
16841
|
+
card re-reads `liveValues` and follows a drag made in the
|
|
16842
|
+
PropsEditor panel; the card's own edits come back through the
|
|
16843
|
+
same live state via `onChange`. A persisted card only gets that
|
|
16844
|
+
live state while its own composition is the one loaded. -->
|
|
16845
|
+
{@const cardAttached = isDesignCardAttached(spec, ps.selectedComposition?.id ?? null)}
|
|
16846
|
+
<DesignQuickEdit
|
|
16847
|
+
{spec}
|
|
16848
|
+
liveValues={cardAttached ? ps.liveDesignValues : undefined}
|
|
16849
|
+
knownPropertyIds={cardAttached ? designPropertyIdsFor(ps.compositionDesignSchema) : undefined}
|
|
16850
|
+
detached={!cardAttached}
|
|
16851
|
+
onChange={(id, value) => applyDesignValuesToPreview(panelWidgetId, { [id]: value })}
|
|
16852
|
+
/>
|
|
16853
|
+
{/snippet}
|
|
16854
|
+
{#snippet generationOptionChecks()}
|
|
16855
|
+
<BrandKitPanel
|
|
16856
|
+
part="checks"
|
|
16857
|
+
showPresetLibraries={debugMode}
|
|
16858
|
+
suspendPickers={!!generatingWidgetIds[panelWidgetId]}
|
|
16859
|
+
bind:brandKitEnabled
|
|
16860
|
+
bind:librariesEnabled
|
|
16861
|
+
bind:libraries={brandKitLibraries}
|
|
16862
|
+
bind:libraryGitUrl
|
|
16863
|
+
bind:libraryFileName
|
|
16864
|
+
bind:libraryUploads
|
|
16865
|
+
bind:fontUploads
|
|
16866
|
+
bind:primary={brandPrimary}
|
|
16867
|
+
bind:secondary={brandSecondary}
|
|
16868
|
+
bind:title={brandTitle}
|
|
16869
|
+
bind:sub={brandSub}
|
|
16870
|
+
bind:fonts={brandFonts}
|
|
16871
|
+
showLanguage={debugMode}
|
|
16872
|
+
bind:language={widgetLanguage}
|
|
16873
|
+
/>
|
|
16874
|
+
{/snippet}
|
|
16129
16875
|
<Chat class="code-generation-chat w-full h-full rounded-none border-0"
|
|
16876
|
+
actionBarExtras={generationOptionChecks}
|
|
16130
16877
|
style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
|
|
16131
16878
|
resizeExpandsContainer={true}
|
|
16879
|
+
renderDesignControls={renderDesignControls as any}
|
|
16132
16880
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
16133
16881
|
showLogs={debugMode}
|
|
16134
16882
|
showHeader={false}
|
|
@@ -16150,6 +16898,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
16150
16898
|
// Debug-only surfaces: assistant "Updated N files: ..." summaries
|
|
16151
16899
|
// and heuristic code-block auto-detection. Hidden in production.
|
|
16152
16900
|
showGenerationSummaries: debugMode,
|
|
16901
|
+
markComplete: false,
|
|
16153
16902
|
// Keep false — debug autoCodeDetection mislabels "Code generation failed: …" as JavaScript.
|
|
16154
16903
|
autoCodeDetection: false,
|
|
16155
16904
|
}
|
|
@@ -16167,10 +16916,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
16167
16916
|
onCompletionToggle={(detail) => handleChatCompletionToggle(panelWidgetId, detail)}
|
|
16168
16917
|
onLoadConversations={handleLoadConversations}
|
|
16169
16918
|
onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
|
|
16170
|
-
onCreateConversation={handleCreateConversation}
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
}
|
|
16919
|
+
onCreateConversation={handleCreateConversation} onSaveMessage={(conversationId, content, messageType, attachments, suggestions, designControls) =>
|
|
16920
|
+
handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId, designControls)
|
|
16921
|
+
}
|
|
16174
16922
|
onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
|
|
16175
16923
|
handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
|
|
16176
16924
|
}
|