@rslsp1/fa-app-tools 7.0.1 → 7.4.0
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/{chunk-B2YV4TNI.mjs → chunk-HUGUH4VB.mjs} +704 -46
- package/dist/index.d.mts +26 -4
- package/dist/index.d.ts +26 -4
- package/dist/index.js +654 -101
- package/dist/index.mjs +25 -55
- package/dist/szenen.d.mts +894 -301
- package/dist/szenen.d.ts +894 -301
- package/dist/szenen.js +879 -70
- package/dist/szenen.mjs +162 -3
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -132,7 +132,7 @@ __export(index_exports, {
|
|
|
132
132
|
module.exports = __toCommonJS(index_exports);
|
|
133
133
|
|
|
134
134
|
// package.json
|
|
135
|
-
var version = "7.0
|
|
135
|
+
var version = "7.4.0";
|
|
136
136
|
|
|
137
137
|
// src/hooks/useOnClickOutside.ts
|
|
138
138
|
var import_react = require("react");
|
|
@@ -171,7 +171,7 @@ function useKeyboardNavigation(history, currentResult, setCurrentResult) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// src/features/szenen/szenenViewStyles.generated.ts
|
|
174
|
-
var SZENEN_VIEW_STYLES = '/* ==========================================================================\n SZENEN VIEW\n ========================================================================== */\n\n.szenen-view {\n display: grid;\n gap: var(--space-md);\n width: 100%;\n min-width: 0;\n font-family: var(--font-sans);\n padding: var(--space-md);\n}\n\n@media (max-width: 760px) {\n .szenen-view {\n padding: var(--space-sm);\n gap: var(--space-sm);\n }\n}\n\n.szenen-view__intro {\n display: flex;\n align-items: end;\n justify-content: space-between;\n gap: var(--space-md);\n padding: clamp(1rem, 3vw, 1.75rem);\n color: var(--color-text-primary);\n background:\n radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.16), transparent 34%),\n linear-gradient(135deg, var(--color-bg-surface), var(--color-bg-elevated));\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n}\n\n.szenen-view__intro h2,\n.szenen-view__intro p {\n margin: 0;\n}\n\n.szenen-view__intro h2 {\n margin-bottom: var(--space-xs);\n font-size: clamp(1.25rem, 4vw, 2rem);\n}\n\n.szenen-view__intro > div > p:last-child {\n max-width: 44rem;\n color: var(--color-text-secondary);\n}\n\n.szenen-view__eyebrow {\n margin-bottom: var(--space-xs) !important;\n color: var(--color-accent-teal, #2dd4bf);\n font-size: 0.72rem;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.szenen-view__step {\n flex: none;\n padding: 0.35rem 0.65rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.78rem;\n font-weight: 700;\n}\n\n.szenen-view__nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-view__nav button,\n.szenen-view button {\n min-height: 2.25rem;\n padding: 0.45rem 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-view button:hover:not(:disabled) {\n color: var(--color-text-primary);\n background: var(--color-bg-surface-hover);\n border-color: var(--color-border-hover);\n}\n\n.szenen-view button:focus-visible,\n.szenen-view input:focus-visible,\n.szenen-view select:focus-visible,\n.szenen-view textarea:focus-visible {\n outline: 2px solid var(--color-accent-blue);\n outline-offset: 2px;\n}\n\n.szenen-view button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.szenen-view__nav button.is-active,\n.szenen-view__nav button[aria-selected="true"] {\n color: var(--color-text-primary);\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-view__area {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-view__capability-note,\n.szenen-area__lead {\n margin: 0;\n padding: var(--space-sm) var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-accent-purple);\n border-radius: var(--radius-sm);\n}\n\n.szenen-view__host-notes {\n color: var(--color-text-secondary);\n}\n\n.szenen-view__host-notes > summary {\n cursor: pointer;\n font-size: 0.8rem;\n}\n\n/* `box-sizing` steht hier und nicht beim Host: Die React-App bringt in\n * `master.css` einen globalen `* { box-sizing: border-box }` mit, Flow **nicht**\n * \u2014 dort ragte `width: 100%` plus Innenabstand um die Randbreite aus dem\n * Elternelement heraus und erzeugte auf ~390 px einen horizontalen Scroll. Die\n * gemeinsame Ansicht darf sich auf keinen Host-Reset verlassen. */\n.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\n box-sizing: border-box;\n width: 100%;\n min-height: 2.25rem;\n padding: 0.5rem 0.65rem;\n color: var(--color-text-primary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font: inherit;\n}\n\n.szenen-view textarea {\n min-height: 7rem;\n resize: vertical;\n font-family: var(--font-mono);\n font-size: 0.8rem;\n}\n\n.szenen-view label {\n display: grid;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-view code,\n.szenen-view pre {\n font-family: var(--font-mono);\n overflow-wrap: anywhere;\n}\n\n.szenen-view h3,\n.szenen-view h4,\n.szenen-view h5 {\n margin-top: 0;\n}\n\n.szenen-view ul,\n.szenen-view ol {\n padding-left: 1.25rem;\n}\n\n.szenen-elements,\n.szenen-detail,\n.szenen-scene,\n.szenen-setup,\n.szenen-shot,\n.szenen-media,\n.szenen-runs {\n display: grid;\n gap: var(--space-md);\n min-width: 0;\n}\n\n/* Arbeitsoberflaeche: Liste links, Bearbeitung rechts.\n *\n * Breit stehen beide Spalten nebeneinander \u2014 die Auswahl bleibt sichtbar,\n * waehrend bearbeitet wird. Schmal rendert die Komponente nur eine der beiden;\n * hier steht deshalb keine Regel, die die andere versteckt. */\n@media (min-width: 761px) {\n .szenen-elements {\n grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);\n align-items: start;\n }\n\n .szenen-elements__master,\n .szenen-elements__detail {\n min-width: 0;\n }\n}\n\n.szenen-elements__master,\n.szenen-elements__detail {\n display: grid;\n gap: var(--space-sm);\n align-content: start;\n min-width: 0;\n}\n\n.szenen-elements__toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-elements__heading {\n margin: 0;\n font-size: 1.05rem;\n}\n\n.szenen-elements__scope {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n min-width: 0;\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-elements__scope > select {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.szenen-elements__new {\n flex: none;\n font-weight: 700;\n}\n\n.szenen-elements__create {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-accent-blue);\n border-radius: var(--radius-sm);\n}\n\n.szenen-elements__create label {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-elements__create-actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n}\n\n.szenen-elements__options > summary {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n cursor: pointer;\n}\n\n.szenen-elements__back {\n justify-self: start;\n font-weight: 700;\n}\n\n.szenen-elements__empty-detail {\n margin: 0;\n color: var(--color-text-tertiary);\n}\n\n.szenen-elements__filter,\n.szenen-detail__actions,\n.szenen-runs__start,\n.szenen-scene-editor__add,\n.szenen-scene-editor__add-relation {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-elements__filter > label,\n.szenen-runs__start > label,\n.szenen-scene-editor__add > input {\n flex: 1 1 16rem;\n}\n\n.szenen-elements__filter > label:has(input[type="checkbox"]) {\n display: flex;\n flex: 1 1 16rem;\n flex-direction: row;\n align-items: center;\n}\n\n.szenen-elements__filter input[type="checkbox"] {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-accent-blue);\n}\n\n.szenen-elements__list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-elements__list > li > button {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs) var(--space-sm);\n width: 100%;\n height: 100%;\n text-align: left;\n}\n\n.szenen-elements__list > li > button > .szenen-elements__title {\n flex: 1 1 100%;\n}\n\n.szenen-elements__list > li > button.is-selected,\n.szenen-elements__list > li > button[aria-current="true"] {\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-elements__type {\n color: var(--color-accent-blue);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-elements__title {\n color: var(--color-text-primary);\n font-size: 0.95rem;\n}\n\n.szenen-elements__subtype,\n.szenen-elements__retired {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n}\n\n.szenen-elements__retired {\n color: var(--color-warning);\n}\n\n.szenen-detail {\n margin-top: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n@media (min-width: 761px) {\n .szenen-detail {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n }\n}\n\n.szenen-detail > header,\n.szenen-detail__heads,\n.szenen-detail__draft,\n.szenen-capability {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* .szenen-runs ist ein eigenst\xE4ndiger sichtbarer Bereich, beh\xE4lt Styling auf Desktop */\n@media (min-width: 761px) {\n .szenen-runs {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n }\n}\n\n@media (max-width: 760px) {\n .szenen-runs {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n }\n}\n\n.szenen-detail__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin-bottom: 0;\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__heads ul,\n.szenen-scene-editor ul,\n.szenen-runs > ol,\n.szenen-capability > ul {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-detail__heads li,\n.szenen-scene-editor li,\n.szenen-runs > ol > li,\n.szenen-capability > ul > li {\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n\n/* Stufe 6.7.2 (D-126) \u2014 Advanced-/Debug-Fl\xE4che f\xFCr die rohe, schema-getriebene\n * Feldliste einer Szene. Gestrichelt wie die anderen \u201EAngebot statt\n * Auskunft"-Bereiche, aber ohne Akzentfarbe: sie ist kein Vorschlag, sondern\n * ein Eskalationsweg. */\n.szenen-detail__technical {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__technical > summary {\n margin-bottom: var(--space-sm);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__technical[open] > summary {\n margin-bottom: var(--space-md);\n}\n\n.szenen-detail__field small,\n.szenen-capability small {\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__preview-errors,\n.szenen-detail__notice--error,\n.szenen-view [role="alert"] {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-detail__notice {\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__notice--success,\n.szenen-view [data-tone="success"] {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n border-color: var(--color-accent-green);\n}\n\n.szenen-scene-editor {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-md);\n margin-bottom: 0;\n}\n\n.szenen-scene-editor > section {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* \u2500\u2500 Stufe 6.7.2 (D-126): Node-/Relationskarten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Nodes und Beziehungen sind `<details>` \u2014 dieselbe Interaktionssprache wie\n * \u201ERohantwort (Diagnose)" und \u201EBilder zu diesem Lauf" in RunsPanel.tsx. Kopf\n * (immer sichtbar) tr\xE4gt Inhalt zuerst, Technik zuletzt; Body (nur bei\n * ge\xF6ffneter Karte) tr\xE4gt die Bearbeitungscontrols. Padding wandert von der\n * `<li>` in Kopf/Body, damit die Karte keinen doppelten Rand bekommt. */\n.szenen-scene-editor li {\n padding: 0;\n overflow: hidden;\n}\n\n.szenen-node-karte__kopf,\n.szenen-relation-karte__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs) var(--space-sm);\n padding: var(--space-sm);\n cursor: pointer;\n list-style: none;\n}\n\n.szenen-node-karte__kopf::-webkit-details-marker,\n.szenen-relation-karte__kopf::-webkit-details-marker {\n display: none;\n}\n\n.szenen-node-karte__titel {\n flex: 1 1 12rem;\n min-width: 0;\n color: var(--color-text-primary);\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n\n.szenen-node-karte__typ {\n flex: none;\n padding: 0.15rem 0.55rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n white-space: nowrap;\n}\n\n/* node_id klein, monospace, sekund\xE4r \u2014 sichtbar auch bei zugeklappter Karte,\n * aber optisch klar hinter Beschreibung und Typ. */\n.szenen-node-karte__meta {\n flex: 1 1 100%;\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n.szenen-node-karte__meta code {\n font-size: inherit;\n}\n\n.szenen-node-karte__body,\n.szenen-relation-karte__body {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-sm) var(--space-sm);\n border-top: 1px solid var(--color-border);\n}\n\n/* \xA74 \u2014 Prim\xE4raktion und destruktive Aktion optisch getrennt: \u201ENode/Beziehung\n * entfernen" bekommt einen eigenen, warnenden Ton statt gleichgewichtig neben\n * der normalen Bearbeitung zu stehen. */\n.szenen-node-karte__entfernen,\n.szenen-relation-karte__entfernen {\n justify-self: start;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-relation-karte__kopf {\n font-size: 0.9rem;\n}\n\n.szenen-relation-karte__kopf code {\n overflow-wrap: anywhere;\n}\n\n.szenen-relation-karte__pfeil {\n flex: none;\n color: var(--color-text-secondary);\n}\n\n/* Die Composer-\xD6ffner (\u201E+ Node hinzuf\xFCgen", \u201E+ Beziehung hinzuf\xFCgen") sind\n * Angebote, keine Ausk\xFCnfte \u2014 dasselbe gestrichelte Muster wie beim Folgelauf\n * und der Fehlerrettung (\xA73.3/\xA73.5). */\n.szenen-scene-editor__oeffnen {\n width: 100%;\n border-style: dashed;\n}\n\n.szenen-refpicker {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n margin: var(--space-sm) 0;\n padding: var(--space-sm) 0;\n border: none;\n border-radius: 0;\n}\n\n.szenen-refpicker legend {\n padding: 0 var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n}\n\n.szenen-capability > ul > li {\n display: grid;\n grid-template-columns: minmax(10rem, 1fr) auto minmax(14rem, 2fr);\n align-items: start;\n gap: var(--space-sm);\n}\n\n.szenen-capability__state {\n padding: 0.1rem 0.5rem;\n border-radius: var(--radius-full);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-capability__state--yes {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n}\n\n.szenen-capability__state--no {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n}\n\n.szenen-capability__state--unknown {\n color: #fef08a;\n background: rgba(234, 179, 8, 0.2);\n}\n\n.szenen-runs > ol > li {\n display: grid;\n gap: var(--space-sm);\n}\n\n/* B1 \u2014 die geschlossene Runde. Vorher stand die `run_id` als erstes und war\n die einzige Information, die niemand liest; jetzt beantwortet die Kopfzeile\n welche Runde, ob sie durchging, mit welchem Modell und was ungef\xE4hr\n herauskam. */\n.szenen-runs__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-sm);\n width: 100%;\n padding: var(--space-xs) 0;\n text-align: left;\n color: var(--color-text-secondary);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n\n.szenen-runs__marke {\n font-weight: 700;\n color: var(--color-text-primary);\n}\n\n.szenen-runs__status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n border-radius: var(--radius-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n}\n\n.szenen-runs__status[data-status="succeeded"] {\n border-color: var(--color-success);\n}\n\n.szenen-runs__status[data-status="failed"] {\n border-color: var(--color-danger);\n}\n\n.szenen-runs__modell {\n font-size: 0.75rem;\n}\n\n/* Der Auszug ist die Inhaltsvorschau der Runde. Er bekommt eine eigene Zeile,\n damit er nicht mit Marke und Modell um dieselbe Zeile ringt. */\n.szenen-runs__auszug {\n flex: 1 1 100%;\n min-width: 0;\n color: var(--color-text-tertiary);\n font-size: 0.8125rem;\n}\n\n/* B2 \u2014 ein Fehler steht ausserhalb der aufklappbaren Fl\xE4che: Wer eine rote\n Runde sieht, soll nicht erst klicken m\xFCssen, um zu erfahren, woran sie\n gescheitert ist. */\n.szenen-runs__fehler {\n display: grid;\n gap: var(--space-xs);\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n}\n\n.szenen-runs__fehler p {\n margin: 0;\n}\n\n.szenen-runs__inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-runs__prompt > h5,\n.szenen-runs__sekundaer > h5 {\n margin: 0 0 var(--space-xs);\n}\n\n.szenen-runs__prompt > p {\n margin: 0;\n white-space: pre-wrap;\n}\n\n.szenen-runs__prompt--negativ > p,\n.szenen-runs__sekundaer > p {\n margin: 0;\n color: var(--color-text-secondary);\n white-space: pre-wrap;\n}\n\n.szenen-runs__mehr {\n justify-self: start;\n margin-top: var(--space-xs);\n padding: 0;\n color: var(--color-accent-teal, var(--color-text-secondary));\n background: transparent;\n border: none;\n font-size: 0.75rem;\n text-decoration: underline;\n cursor: pointer;\n}\n\n.szenen-runs > ol > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-runs > ol > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n.szenen-runs details pre {\n max-height: 20rem;\n overflow: auto;\n padding: var(--space-sm);\n white-space: pre-wrap;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border-radius: var(--radius-sm);\n}\n\n.szenen-media > section {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-media__lookup,\n.szenen-media__attach {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-media__lookup > label,\n.szenen-media__attach > label:first-child {\n flex: 1 1 18rem;\n}\n\n.szenen-media__list {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-media__list > li {\n display: grid;\n grid-template-columns: minmax(12rem, 1fr) auto auto auto;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n@media (max-width: 760px) {\n .szenen-view {\n gap: var(--space-sm);\n }\n\n /* C1 \u2014 auf 390 px bleiben drei Kacheln je Reihe gross genug zum Erkennen\n und klein genug, dass vier Bilder eine Reihe sind und keine Seite. */\n .szenen-bildgalerie {\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n }\n\n /* Der Viewer bekommt den ganzen Bildschirm; die Bl\xE4tterkn\xF6pfe bleiben am\n Rand, wo der Daumen sie erreicht. */\n .szenen-bildviewer {\n padding: var(--space-xs);\n }\n\n .szenen-bildviewer__blaettern {\n width: 2.25rem;\n height: 4rem;\n }\n\n .szenen-view__intro {\n align-items: start;\n flex-direction: column;\n gap: var(--space-sm);\n padding: var(--space-md);\n }\n\n .szenen-view__intro h2 {\n font-size: 1.25rem;\n }\n\n .szenen-view__intro > div > p:last-child {\n font-size: 0.9rem;\n }\n\n .szenen-view__nav {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-xs);\n padding: var(--space-xs);\n }\n\n .szenen-view__nav button {\n width: 100%;\n min-width: 0;\n min-height: 2.5rem;\n padding: 0.35rem 0.5rem;\n }\n\n\n .szenen-scene-editor {\n grid-template-columns: 1fr;\n }\n\n .szenen-capability > ul > li {\n grid-template-columns: 1fr;\n }\n\n .szenen-derive,\n .szenen-elements,\n .szenen-scene,\n .szenen-setup,\n .szenen-shot,\n .szenen-media,\n .szenen-runs {\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter > label {\n width: 100%;\n min-width: 0;\n flex: none;\n }\n\n .szenen-elements__filter > label:has(input[type="checkbox"]) {\n flex: none;\n grid-column: 1 / -1;\n }\n\n /* Schmal ist die Liste eine Spalte, und die Auswahl fuehrt weiter \u2014 der\n * Eintrag ist deshalb ein Ziel fuer den Finger, keine Kachel. */\n .szenen-elements__list {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .szenen-elements__list > li > button {\n min-height: 2.75rem;\n }\n\n .szenen-elements__back {\n justify-self: stretch;\n min-height: 2.5rem;\n }\n\n .szenen-detail__actions,\n .szenen-runs__start,\n .szenen-scene-editor__add,\n .szenen-scene-editor__add-relation {\n align-items: stretch;\n flex-direction: column;\n }\n\n .szenen-runs__start > *,\n .szenen-scene-editor__add > *,\n .szenen-scene-editor__add-relation > * {\n width: 100%;\n flex: none;\n }\n}\n\n/* \u2500\u2500 Stufe 6.5: Rekonstruktion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich zeigt drei Dinge nebeneinander: den Vorschlag, das nicht\n * Ableitbare und die Widerspr\xFCche. Die Diagnosen sind deshalb bewusst nicht\n * kleiner gesetzt als der Vorschlag \u2014 wer sie \xFCbersieht, \xFCbernimmt Geratenes.\n */\n\n.szenen-derive {\n display: grid;\n gap: var(--space-md);\n}\n\n.szenen-form {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-field {\n display: grid;\n gap: var(--space-xs);\n}\n\n.szenen-field > span {\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-error {\n padding: var(--space-sm) var(--space-md);\n color: var(--color-danger);\n background: var(--color-accent-red-dim);\n border: 1px solid var(--color-accent-red);\n border-radius: var(--radius-sm);\n}\n\n.szenen-empty {\n padding: var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-diagnose {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-warning);\n border-radius: var(--radius-sm);\n}\n\n/* Ein Widerspruch ist der schwerste der drei Befunde: dort steht eine\n * Entscheidung aus, die das System bewusst nicht getroffen hat (D-74). */\n.szenen-diagnose--konflikt {\n border-left-color: var(--color-danger);\n}\n\n.szenen-diagnose > h4 {\n margin: 0 0 var(--space-sm);\n font-size: 0.9375rem;\n}\n\n.szenen-diagnose ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-proposal {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-proposal > h4 {\n margin: 0 0 var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.9375rem;\n}\n\n.szenen-proposal__payload {\n max-height: 18rem;\n margin: 0 0 var(--space-sm);\n padding: var(--space-sm);\n overflow: auto;\n /* Ein erzeugter Prompt ist ein einziger langer Satz \u2014 ohne Umbruch\n verschwindet er nach rechts aus dem Bild. JSON-Payloads bringen ihre\n eigenen Zeilenumbrueche mit und leiden nicht darunter. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-proposal__done {\n margin: var(--space-sm) 0 0;\n color: var(--color-success);\n}\n\n/* Die Belege eines Laufs: Rohantwort und Systemprompt. Zum Lesen und Kopieren,\n nicht zum Bedienen \u2014 deshalb voll markierbar und in Monospace. */\n.szenen-beleg__text {\n width: 100%;\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n line-height: 1.5;\n white-space: pre;\n background: var(--color-bg-app);\n color: var(--color-text);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n resize: vertical;\n}\n\n/* Der Folgelauf (D-87): abgesetzt, damit er nicht wie Teil des Ergebnisses\n aussieht \u2014 er ist eine Frage, keine Auskunft. */\n.szenen-folgelauf {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-folgelauf > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-run__herkunft {\n margin: 0 0 var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-muted, inherit);\n}\n\n/* Widerspricht das Modell, ist das das Wichtigste auf der Seite. */\n.szenen-diagnose--antwort {\n border-left: 3px solid var(--color-accent, var(--color-border));\n}\n\n/* Der Bildw\xE4hler (D-92) \u2014 Browser und Inspector, dem Labs-Tab nachgebildet,\n aber mit dem CSS dieser Ansicht: eine gemeinsame Komponente \xFCber zwei\n Dom\xE4nengrenzen w\xE4re eine Kopplung, die keiner gewollt hat.\n\n Der Review vom 2026-08-22 (R12.1/R12.2) hat den Ma\xDFstab gesetzt: Der Bestand\n ist ein **Browser**, kein Seiteninhalt. Vierzig Werke d\xFCrfen die Szenenseite\n nicht meterlang machen, und zw\xF6lf Varianten eines Werkes keine zw\xF6lf\n Bildschirmseiten. Deshalb steht hier alles unter zwei Regeln \u2014 begrenzte\n Fl\xE4che mit eigenem Scroll, und Kacheln statt Karten. */\n\n/* \u2500\u2500 Ebene 1: der Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl {\n /* Dieselbe Begr\xFCndung wie beim Inspector: Als Grid-Element w\xFCrde der\n W\xE4hler sonst so breit wie sein breitester Inhalt. */\n min-width: 0;\n}\n\n.szenen-bildwahl__browser {\n /* Die eigene Scrollfl\xE4che ist der ganze Punkt: Der Bestand w\xE4chst nach\n innen, nicht nach unten. Dieselbe Gr\xF6\xDFenordnung wie `max-h-[300px]` im\n Labs-Browser. */\n max-height: 19rem;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__raster {\n display: grid;\n /* Vier Spalten auf schmalen Displays \u2014 der gemeldete Screenshot zeigte eine\n einzige Spalte gro\xDFer Karten. Breiter wird automatisch dichter. */\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__karte {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-surface);\n text-align: left;\n cursor: pointer;\n}\n\n/* Das fokussierte Werk ist klar markiert \u2014 sonst wei\xDF niemand, wessen Galerie\n der Inspector unten zeigt. */\n.szenen-bildwahl__karte.is-fokus {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n/* \u2500\u2500 Die Kachel selbst \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Der Platz steht, bevor das Bild da ist \u2014 sonst springt das Raster, sobald\n die erste Vorschau eintrifft. */\n.szenen-bildkachel {\n display: block;\n width: 100%;\n aspect-ratio: 1;\n overflow: hidden;\n background: var(--color-bg-app);\n}\n\n.szenen-bildkachel img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.szenen-bildkachel__leer {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, var(--color-bg-app), var(--color-bg-surface));\n}\n\n/* R12.3: Eine fehlende Vorschau bleibt kachelgro\xDF. Vorher fiel die Karte auf\n ihren Prompttext zur\xFCck \u2014 und ein Prompt hat im echten Bestand mehrere\n hundert Zeichen. */\n.szenen-bildkachel__zeichen {\n font-size: 0.875rem;\n font-weight: 700;\n color: var(--color-text-secondary);\n opacity: 0.6;\n}\n\n/* Wie viele Varianten das Werk hat \u2014 klein und \xFCberlagernd, nie als\n dominanter Text. */\n.szenen-bildwahl__anzahl {\n position: absolute;\n top: 0.15rem;\n right: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n font-weight: 700;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* Eine Zeile, dann Schluss. Der Prompt eines Werkes ist oft mehrere hundert\n Zeichen lang und w\xE4re als Prim\xE4rdarstellung genau die Textw\xFCste, gegen die\n der Review antritt. */\n.szenen-bildwahl__kacheltitel {\n display: block;\n padding: 0.15rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.3;\n color: var(--color-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Ebene 2: der Inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__inspector {\n /* Ohne `min-width: 0` gewinnt der Inhalt: Der W\xE4hler ist Element eines\n Grids (`.szenen-form`), und dort ist `min-width: auto` die Vorgabe. Der\n Streifen unten ist rund 1000 px breit \u2014 gemessen am 2026-08-22 im echten\n Browser zog er die Auswahl von 374 px auf 1088 px auf und schob die\n ganze Seite waagerecht heraus, statt selbst zu scrollen. */\n min-width: 0;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n /* Sichtbar abgesetzt vom Browser: zwei Ebenen, nicht eine lange Liste. */\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__inspektorkopf {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bildwahl__werktitel {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-text-primary);\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.szenen-bildwahl__zurueck {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n padding: 0.15rem 0.5rem;\n}\n\n/* Der Streifen: waagerecht scrollend, feste kleine Kacheln \u2014 zw\xF6lf Varianten\n bleiben ein \xDCberblick statt zw\xF6lf Bildschirmseiten. */\n.szenen-bildwahl__streifen {\n /* Die Breite kommt vom Elter, nie vom Inhalt \u2014 sonst scrollt hier nichts. */\n max-width: 100%;\n display: flex;\n gap: var(--space-sm);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n\n.szenen-bildwahl__streifen > li {\n flex: 0 0 auto;\n width: 5rem;\n}\n\n.szenen-bildwahl__variante {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-app);\n cursor: pointer;\n}\n\n.szenen-bildwahl__variante.is-gewaehlt {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n.szenen-bildwahl__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Das Gew\xE4hlte \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__gewaehlt {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__gewaehlt li {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.25rem 0.5rem;\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n max-width: 100%;\n}\n\n/* Das gew\xE4hlte Bild wird best\xE4tigt, nicht nur benannt: eine kleine Vorschau\n neben dem Titel. */\n.szenen-bildwahl__gewaehlt .szenen-bildkachel {\n width: 2.25rem;\n flex: 0 0 auto;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__gewaehlt .szenen-bildwahl__kacheltitel {\n min-width: 0;\n max-width: 12rem;\n font-size: 0.75rem;\n}\n\n.szenen-beleg__kopf {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details + details {\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details > summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Die Fehlerrettung (Stufe 6.6, A5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Sie steht im Fehlerblock und muss trotzdem als Werkzeug lesbar sein, nicht\n * als weitere Fehlermeldung. Deshalb abgesetzt wie der Folgelauf \u2014 und aus\n * demselben Grund: Beides sind Angebote, keine Ausk\xFCnfte.\n *\n * Der wichtigste optische Punkt ist die Trennung von Original und\n * Arbeitskopie. Das Original bekommt den ruhigen, \u201Efesten" Hintergrund der\n * Belege; die Kopie sieht aus wie ein normales Eingabefeld, weil sie eines\n * ist. */\n.szenen-rettung {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-rettung > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-rettung textarea[readonly] {\n background: var(--color-bg-surface);\n color: var(--color-text-secondary);\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung textarea:not([readonly]) {\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung__aktionen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0;\n}\n\n/* Ein Befund tr\xE4gt Code, Bezug und Meldung. Der Code steht klein und\n * monospaced davor: Er ist die Kennung, nicht die Aussage. */\n.szenen-diagnose__liste {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-diagnose__liste code {\n font-size: 0.8125rem;\n}\n\n.szenen-diagnose__liste > li[data-severity=\'warning\'] {\n color: var(--color-text-secondary);\n}\n\n/* \u2500\u2500 Die Draft-Kette (Stufe 6.6, C4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Hauptweg. Er steht nach den Einzel\xFCbernahmen und muss als das erkennbar\n * sein, was er ist: nicht eine weitere Option, sondern der k\xFCrzere Weg zum\n * selben Ziel. Deshalb durchgezogener Rahmen statt gestricheltem \u2014 er ist\n * Angebot **und** Empfehlung. */\n.szenen-kette {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-kette > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-kette ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0 0 var(--space-sm);\n padding-left: var(--space-md);\n}\n\n.szenen-kette code {\n font-size: 0.8125rem;\n}\n\n/* Ein Hinweis, der eine Bedingung erklaert \u2014 nicht rot, weil er kein Fehler\n ist, aber abgesetzt, weil er eine Folge hat. */\n.szenen-detail__hint {\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Stufe 6.7: Bilder am Prompt-Lauf (H0/H1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich tr\xE4gt zwei Dinge, die man nie verwechseln darf: den **Eingang**\n * (woraus der Prompt entstand) und den **Ausgang** (was daraus wurde). Sie\n * bekommen deshalb verschiedene Akzentkanten statt nur verschiedener\n * \xDCberschriften \u2014 wer schnell liest, sieht die Trennung, bevor er sie liest.\n */\n.szenen-bilder {\n display: grid;\n gap: var(--space-md);\n margin-top: var(--space-sm);\n}\n\n.szenen-bilder__eingang,\n.szenen-bilder__ausgang {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__eingang {\n border-left: 3px solid var(--color-accent-purple, var(--color-border));\n}\n\n.szenen-bilder__ausgang {\n border-left: 3px solid var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-bilder h5 {\n margin: 0;\n}\n\n.szenen-bilder__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: 0;\n align-items: baseline;\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-bilder__eingangsliste,\n.szenen-bilder__batchliste,\n.szenen-bilder__auftraege {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bilder__eingangsliste > li,\n.szenen-bilder__batchliste > li,\n.szenen-bilder__auftraege > li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n/* Ein unterbrochener Auftrag ist der Fall, f\xFCr den der Auftragssatz existiert.\n Er wird deshalb markiert \u2014 nicht rot, denn er ist kein Fehler, sondern eine\n offene Entscheidung. */\n.szenen-bilder__auftraege > li[data-status="started"] {\n border-left: 3px solid var(--color-warning);\n}\n\n.szenen-bilder__auftraege > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-bilder__auftraege > li[data-status="partial"] {\n border-left: 3px solid var(--color-accent-blue, var(--color-border));\n}\n\n.szenen-bilder__auftraege > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n/* Das Eingangsbild benutzt seit C3 dieselbe Kachel wie W\xE4hler und Galerie.\n Sie ist von Haus aus so breit wie ihr Platz \u2014 in dieser Zeile bekommt sie\n deshalb eine feste Gr\xF6sse, sonst sch\xF6be sie die Metadaten heraus. */\n.szenen-bilder__eingangsliste .szenen-bildkachel {\n flex: 0 0 auto;\n width: 4rem;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batch {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batchkopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-bilder__herkunft {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n/* Review-Befund \u2014 ein transienter Bildfehler ist wiederholbar, kein\n endg\xFCltiges \u201Efehlt". Dezent wie ein Angebot, nicht wie ein Fehlerzustand. */\n.szenen-bilder__retry {\n justify-self: start;\n padding: 0.15rem 0.5rem;\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n/* C4 \u2014 die Provenienz vollst\xE4ndig, aber sekund\xE4r. */\n.szenen-bilder__technik > summary {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-bilder__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n}\n\n/* C5 \u2014 der Verlauf verdichtet, seine Ausreisser nicht. */\n.szenen-bilder__verlauf > summary {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n cursor: pointer;\n}\n\n.szenen-bilder__verlauf[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__auffaellig {\n margin: 0;\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n.szenen-bilder__auftragskennung {\n flex-basis: 100%;\n color: var(--color-text-tertiary);\n font-size: 0.6875rem;\n}\n\n/* \u2500\u2500 C1/C2 \u2014 Galerie und grosse Ansicht \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Das Raster w\xE4chst mit dem Platz: auf 390 px drei bis vier Kacheln je Reihe,\n auf dem Desktop entsprechend mehr. Vorher war jedes Bild eine Zeile aus\n Daumennagel plus vier Metadatenfeldern \u2014 vier Bilder ergaben vier Textzeilen\n und das Protokoll war die Hauptsache. */\n.szenen-bildgalerie {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));\n gap: var(--space-sm);\n min-width: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildgalerie__kachel {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildgalerie__kachel:hover,\n.szenen-bildgalerie__kachel:focus-visible {\n border-color: var(--color-accent-teal, var(--color-text-primary));\n}\n\n.szenen-bildgalerie__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* Die grosse Ansicht liegt \xFCber allem und bringt ihren eigenen Grund mit \u2014\n `position: fixed` bezieht sich im Flow-Canvas auf dessen Rahmen, in React auf\n das Fenster; in beiden F\xE4llen genau das Gew\xFCnschte. */\n.szenen-bildviewer {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: var(--space-sm);\n padding: var(--space-sm);\n /* Deckend, nicht get\xF6nt: Bei 0.92 schimmerte der Laufbereich durch und man\n las Prompttext quer \xFCber dem Bild \u2014 im Browser gemessen. */\n background: #000;\n}\n\n/* Titel, Z\xE4hlung, Schliessen \u2014 drei Spalten, damit ein langer Werktitel die\n Z\xE4hlung nicht in die n\xE4chste Zeile und \xFCber das Bild schiebt. */\n.szenen-bildviewer__kopf {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto auto;\n align-items: center;\n gap: var(--space-sm);\n color: var(--color-text-primary);\n}\n\n.szenen-bildviewer__titel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 600;\n}\n\n.szenen-bildviewer__zaehler {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n}\n\n.szenen-bildviewer__buehne {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-sm);\n min-height: 0;\n min-width: 0;\n}\n\n/* Der Rahmen nimmt den Platz, der zwischen den Bl\xE4tterkn\xF6pfen \xFCbrig ist \u2014\n `min-width: 0`, damit er auch wirklich schrumpft statt sie hinauszuschieben. */\n.szenen-bildviewer__rahmen {\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.szenen-bildviewer__bild {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.szenen-bildviewer__leer {\n color: var(--color-text-secondary);\n text-align: center;\n}\n\n/* Die Bl\xE4tterkn\xF6pfe sind daumengross und liegen an den R\xE4ndern \u2014 auf einem\n Telefon ist der Rand erreichbar, die Bildmitte nicht. */\n.szenen-bildviewer__blaettern {\n flex: 0 0 auto;\n width: 2.75rem;\n height: 3.5rem;\n font-size: 1.75rem;\n line-height: 1;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildviewer__details {\n max-height: 8rem;\n overflow-y: auto;\n color: var(--color-text-secondary);\n}\n\n/* Die Aktionen stehen abgesetzt: Sie sind Angebote, keine Ausk\xFCnfte \u2014 dasselbe\n Muster wie beim Folgelauf und bei der Fehlerrettung. */\n/* D \u2014 die Aktionen stehen abgesetzt und standardm\xE4ssig kompakt: Sie sind\n Angebote, keine Ausk\xFCnfte. Vorher hielten beide Formulare dauerhaft offen und\n machten aus jedem Lauf zwei zus\xE4tzliche Bildschirmseiten. */\n.szenen-bilder__aktion {\n min-width: 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__aktion > summary {\n color: var(--color-text-primary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-bilder__aktion[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion h5 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-bilder__aktion ol {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n font-size: 0.8125rem;\n}\n\n/* Regelwerk-UX Schritt 16 \u2014 der ZIP-Sammelimport.\n *\n * \xA78: Der gemeldete Einsatz ist mobil. Deshalb **ein** Seedfeld statt einer\n * gestapelten Eingabe je Bild, und die Zuordnung Bild -> Seed als umbrechende\n * Zeile statt als Tabelle: Eine Tabelle waere auf 390 px abgeschnitten, und\n * abgeschnitten heisst hier, dass der Nutzer die Zuordnung nicht mehr pruefen\n * kann \u2014 genau die Pruefung, wegen der die Vorschau existiert. */\n.szenen-bilder__zip {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-bilder__zipkopf {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipvorlagen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n margin: 0;\n}\n\n/* Eine laengere Liste bleibt scrollbar, statt die Sektion und damit den\n * Import-Knopf aus dem Blick zu schieben (\xA78, letzter Punkt). */\n.szenen-bilder__zipliste {\n max-height: 16rem;\n overflow-y: auto;\n}\n\n.szenen-bilder__zipliste > li {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-bilder__zipliste > li > span:first-child {\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipliste > li > span:last-child {\n color: var(--color-text-secondary);\n white-space: nowrap;\n}\n\n/* ==========================================================================\n STUFE 6.10 \u2014 ELEMENTEDITOR UND VERSIONSNAVIGATION\n ========================================================================== */\n\n/* \xA74.2 \u2014 vier Sachverhalte, vier Marken. Die Marken stehen in einer eigenen\n * Spalte, damit ANGEZEIGT + BEVORZUGT nebeneinander lesbar bleiben, statt sich\n * durch den Text zu schieben. */\n.szenen-detail__heads li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.szenen-version__titel {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n margin-right: auto;\n}\n\n/* V17 \u2014 Kennungen bleiben vollst\xE4ndig sichtbar, aber optisch sekund\xE4r. */\n.szenen-version__id,\n.szenen-ref__id,\n.szenen-detail__field-key {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n overflow-wrap: anywhere;\n}\n\n.szenen-version__marken {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-version__marken > span {\n padding: 0.1rem 0.5rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.7rem;\n font-weight: 800;\n letter-spacing: 0.08em;\n}\n\n.szenen-version--angezeigt {\n border-color: var(--color-accent-teal, #2dd4bf) !important;\n}\n\n/* V9 \u2014 der Basiswechsel ist eine Entscheidung, keine Nebenwirkung. */\n.szenen-detail__confirm {\n display: grid;\n gap: var(--space-sm);\n margin: var(--space-md) 0;\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-accent-amber, #f59e0b);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__confirm p {\n margin: 0;\n}\n\n/* Untergruppen von Objektfeldern (`prompt_render_settings`, `overrides`,\n `blender_render_settings`) \u2014 seit A1/A2 auf- und zuklappbar. Eine optionale\n Gruppe startet zu: Blender und `overrides` f\xFCllten auf einem 390-px-Display\n sonst mehrere Bildschirmseiten, obwohl die meisten Shots sie nie anfassen. */\n.szenen-detail__group {\n min-width: 0;\n margin: var(--space-md) 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group > summary {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__group[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n/* A2 \u2014 dass eine zugeklappte Gruppe schon Werte tr\xE4gt, muss man sehen, ohne\n sie zu \xF6ffnen. Sonst \xFCbersieht man eine gesetzte Blender-Ausgabe genau so\n lange, bis sie im Ergebnis auftaucht. */\n.szenen-detail__group-status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--color-text-tertiary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group[data-konfiguriert="ja"] > summary .szenen-detail__group-status {\n color: var(--color-text-primary);\n border-color: var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-detail__group-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n/* A3 \u2014 die Kernaussage steht am Feld, die Begr\xFCndung dahinter. Nicht als\n Tooltip: Auf einem Telefon gibt es kein Hover, und was dort unerreichbar ist,\n ist gestrichen \u2014 nur ohne dass es jemand entschieden h\xE4tte. */\n.szenen-detail__warum > summary {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-detail__warum > small {\n display: block;\n padding-top: var(--space-xs);\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__fieldrow {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n margin-bottom: var(--space-md);\n}\n\n/* A4 \u2014 der technische Feldname bleibt lesbar, tritt aber hinter das fachliche\n Label zur\xFCck. */\n.szenen-detail__field-label {\n color: var(--color-text-primary);\n}\n\n.szenen-detail__field {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-detail__field > span {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n}\n\n/* Mehrfachauswahl und Listeneditoren. */\n.szenen-feld__mehrfach,\n.szenen-feld__liste,\n.szenen-feld__reflist,\n.szenen-feld__ref,\n.szenen-feld__zielmodell,\n.szenen-feld__kamera {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-feld__mehrfach > label,\n.szenen-feld__null {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-feld__liste-zeile {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-feld__reflist ol {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-feld__reflist li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs);\n padding: var(--space-xs) var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \xA76.1 \u2014 Titel und Typ zuerst, Kennungen daneben. */\n.szenen-ref__anzeige {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-ref__typ {\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-ref__warn {\n padding: 0.1rem 0.5rem;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n/* \xA76.5 \u2014 abgeleitete Angaben, ausdr\xFCcklich nur Anzeige. */\n.szenen-detail__derived {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__derived dl {\n display: grid;\n grid-template-columns: minmax(10rem, auto) 1fr;\n gap: var(--space-xs) var(--space-sm);\n margin: 0;\n}\n\n.szenen-detail__derived dt {\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__derived dd {\n margin: 0;\n}\n\n.szenen-feld__abgeleitet {\n margin: 0;\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n}\n';
|
|
174
|
+
var SZENEN_VIEW_STYLES = '/* ==========================================================================\n SZENEN VIEW\n ========================================================================== */\n\n.szenen-view {\n display: grid;\n gap: var(--space-md);\n width: 100%;\n min-width: 0;\n font-family: var(--font-sans);\n padding: var(--space-md);\n}\n\n@media (max-width: 760px) {\n .szenen-view {\n padding: var(--space-sm);\n gap: var(--space-sm);\n }\n}\n\n.szenen-view__intro {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n padding: clamp(0.6rem, 1.2vw, 0.9rem) clamp(0.75rem, 1.6vw, 1.1rem);\n color: var(--color-text-primary);\n background:\n radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.16), transparent 34%),\n linear-gradient(135deg, var(--color-bg-surface), var(--color-bg-elevated));\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n}\n\n.szenen-view__intro h2,\n.szenen-view__intro p {\n margin: 0;\n}\n\n.szenen-view__intro h2 {\n margin-bottom: 0.1rem;\n font-size: clamp(1rem, 1.9vw, 1.3rem);\n line-height: 1.2;\n}\n\n.szenen-view__intro > div > p:last-child {\n max-width: 44rem;\n color: var(--color-text-secondary);\n font-size: 0.82rem;\n line-height: 1.35;\n}\n\n.szenen-view__eyebrow {\n margin-bottom: 0.1rem !important;\n color: var(--color-accent-teal, #2dd4bf);\n font-size: 0.62rem;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.szenen-view__step {\n flex: none;\n padding: 0.25rem 0.6rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n.szenen-view__nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-view__nav button,\n.szenen-view button {\n min-height: 2.25rem;\n padding: 0.45rem 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-view button:hover:not(:disabled) {\n color: var(--color-text-primary);\n background: var(--color-bg-surface-hover);\n border-color: var(--color-border-hover);\n}\n\n.szenen-view button:focus-visible,\n.szenen-view input:focus-visible,\n.szenen-view select:focus-visible,\n.szenen-view textarea:focus-visible {\n outline: 2px solid var(--color-accent-blue);\n outline-offset: 2px;\n}\n\n.szenen-view button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.szenen-view__nav button.is-active,\n.szenen-view__nav button[aria-selected="true"] {\n color: var(--color-text-primary);\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-view__area {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-view__capability-note,\n.szenen-area__lead {\n margin: 0;\n padding: var(--space-sm) var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-accent-purple);\n border-radius: var(--radius-sm);\n}\n\n.szenen-view__host-notes {\n color: var(--color-text-secondary);\n}\n\n.szenen-view__host-notes > summary {\n cursor: pointer;\n font-size: 0.8rem;\n}\n\n/* `box-sizing` steht hier und nicht beim Host: Die React-App bringt in\n * `master.css` einen globalen `* { box-sizing: border-box }` mit, Flow **nicht**\n * \u2014 dort ragte `width: 100%` plus Innenabstand um die Randbreite aus dem\n * Elternelement heraus und erzeugte auf ~390 px einen horizontalen Scroll. Die\n * gemeinsame Ansicht darf sich auf keinen Host-Reset verlassen. */\n.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\n box-sizing: border-box;\n width: 100%;\n min-height: 2.25rem;\n padding: 0.5rem 0.65rem;\n color: var(--color-text-primary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font: inherit;\n}\n\n.szenen-view textarea {\n min-height: 7rem;\n resize: vertical;\n font-family: var(--font-mono);\n font-size: 0.8rem;\n}\n\n.szenen-view label {\n display: grid;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-view code,\n.szenen-view pre {\n font-family: var(--font-mono);\n overflow-wrap: anywhere;\n}\n\n.szenen-view h3,\n.szenen-view h4,\n.szenen-view h5 {\n margin-top: 0;\n}\n\n.szenen-view ul,\n.szenen-view ol {\n padding-left: 1.25rem;\n}\n\n.szenen-elements,\n.szenen-detail,\n.szenen-scene,\n.szenen-setup,\n.szenen-shot,\n.szenen-media,\n.szenen-runs {\n display: grid;\n gap: var(--space-md);\n min-width: 0;\n}\n\n/* Arbeitsoberflaeche: Liste links, Bearbeitung rechts.\n *\n * Breit stehen beide Spalten nebeneinander \u2014 die Auswahl bleibt sichtbar,\n * waehrend bearbeitet wird. Schmal rendert die Komponente nur eine der beiden;\n * hier steht deshalb keine Regel, die die andere versteckt. */\n@media (min-width: 761px) {\n .szenen-elements {\n grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);\n align-items: start;\n }\n\n .szenen-elements__master,\n .szenen-elements__detail {\n min-width: 0;\n }\n}\n\n.szenen-elements__master,\n.szenen-elements__detail {\n display: grid;\n gap: var(--space-sm);\n align-content: start;\n min-width: 0;\n}\n\n.szenen-elements__toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-elements__heading {\n margin: 0;\n font-size: 1.05rem;\n}\n\n.szenen-elements__scope {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n min-width: 0;\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-elements__scope > select {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.szenen-elements__new {\n flex: none;\n font-weight: 700;\n}\n\n.szenen-elements__create {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-accent-blue);\n border-radius: var(--radius-sm);\n}\n\n.szenen-elements__create label {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-elements__create-actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n}\n\n.szenen-elements__options > summary {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n cursor: pointer;\n}\n\n.szenen-elements__back {\n justify-self: start;\n font-weight: 700;\n}\n\n.szenen-elements__empty-detail {\n margin: 0;\n color: var(--color-text-tertiary);\n}\n\n.szenen-elements__filter,\n.szenen-detail__actions,\n.szenen-runs__start,\n.szenen-scene-editor__add,\n.szenen-scene-editor__add-relation {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-elements__filter > label,\n.szenen-runs__start > label,\n.szenen-scene-editor__add > input {\n flex: 1 1 16rem;\n}\n\n.szenen-elements__filter > label:has(input[type="checkbox"]) {\n display: flex;\n flex: 1 1 16rem;\n flex-direction: row;\n align-items: center;\n}\n\n.szenen-elements__filter input[type="checkbox"] {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-accent-blue);\n}\n\n.szenen-elements__list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-elements__list > li > button {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs) var(--space-sm);\n width: 100%;\n height: 100%;\n text-align: left;\n}\n\n.szenen-elements__list > li > button > .szenen-elements__title {\n flex: 1 1 100%;\n}\n\n.szenen-elements__list > li > button.is-selected,\n.szenen-elements__list > li > button[aria-current="true"] {\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-elements__type {\n color: var(--color-accent-blue);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-elements__title {\n color: var(--color-text-primary);\n font-size: 0.95rem;\n}\n\n.szenen-elements__subtype,\n.szenen-elements__retired {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n}\n\n.szenen-elements__retired {\n color: var(--color-warning);\n}\n\n.szenen-detail {\n margin-top: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n@media (min-width: 761px) {\n .szenen-detail {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n }\n}\n\n.szenen-detail > header,\n.szenen-detail__heads,\n.szenen-detail__draft,\n.szenen-capability {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* .szenen-runs ist ein eigenst\xE4ndiger sichtbarer Bereich, beh\xE4lt Styling auf Desktop */\n@media (min-width: 761px) {\n .szenen-runs {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n }\n}\n\n@media (max-width: 760px) {\n .szenen-runs {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n }\n}\n\n.szenen-detail__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin-bottom: 0;\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__heads ul,\n.szenen-scene-editor ul,\n.szenen-runs > ol,\n.szenen-capability > ul {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-detail__heads li,\n.szenen-scene-editor li,\n.szenen-runs > ol > li,\n.szenen-capability > ul > li {\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n\n/* Stufe 6.7.2 (D-126) \u2014 Advanced-/Debug-Fl\xE4che f\xFCr die rohe, schema-getriebene\n * Feldliste einer Szene. Gestrichelt wie die anderen \u201EAngebot statt\n * Auskunft"-Bereiche, aber ohne Akzentfarbe: sie ist kein Vorschlag, sondern\n * ein Eskalationsweg. */\n.szenen-detail__technical {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__technical > summary {\n margin-bottom: var(--space-sm);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__technical[open] > summary {\n margin-bottom: var(--space-md);\n}\n\n.szenen-detail__field small,\n.szenen-capability small {\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__preview-errors,\n.szenen-detail__notice--error,\n.szenen-view [role="alert"] {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-detail__notice {\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__notice--success,\n.szenen-view [data-tone="success"] {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n border-color: var(--color-accent-green);\n}\n\n.szenen-scene-editor {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-md);\n margin-bottom: 0;\n}\n\n.szenen-scene-editor > section {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* \u2500\u2500 Stufe 6.7.2 (D-126): Node-/Relationskarten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Nodes und Beziehungen sind `<details>` \u2014 dieselbe Interaktionssprache wie\n * \u201ERohantwort (Diagnose)" und \u201EBilder zu diesem Lauf" in RunsPanel.tsx. Kopf\n * (immer sichtbar) tr\xE4gt Inhalt zuerst, Technik zuletzt; Body (nur bei\n * ge\xF6ffneter Karte) tr\xE4gt die Bearbeitungscontrols. Padding wandert von der\n * `<li>` in Kopf/Body, damit die Karte keinen doppelten Rand bekommt. */\n.szenen-scene-editor li {\n padding: 0;\n overflow: hidden;\n}\n\n.szenen-node-karte__kopf,\n.szenen-relation-karte__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs) var(--space-sm);\n padding: var(--space-sm);\n cursor: pointer;\n list-style: none;\n}\n\n.szenen-node-karte__kopf::-webkit-details-marker,\n.szenen-relation-karte__kopf::-webkit-details-marker {\n display: none;\n}\n\n.szenen-node-karte__titel {\n flex: 1 1 12rem;\n min-width: 0;\n color: var(--color-text-primary);\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n\n.szenen-node-karte__typ {\n flex: none;\n padding: 0.15rem 0.55rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n white-space: nowrap;\n}\n\n/* node_id klein, monospace, sekund\xE4r \u2014 sichtbar auch bei zugeklappter Karte,\n * aber optisch klar hinter Beschreibung und Typ. */\n.szenen-node-karte__meta {\n flex: 1 1 100%;\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n.szenen-node-karte__meta code {\n font-size: inherit;\n}\n\n.szenen-node-karte__body,\n.szenen-relation-karte__body {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-sm) var(--space-sm);\n border-top: 1px solid var(--color-border);\n}\n\n/* \xA74 \u2014 Prim\xE4raktion und destruktive Aktion optisch getrennt: \u201ENode/Beziehung\n * entfernen" bekommt einen eigenen, warnenden Ton statt gleichgewichtig neben\n * der normalen Bearbeitung zu stehen. */\n.szenen-node-karte__entfernen,\n.szenen-relation-karte__entfernen {\n justify-self: start;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-relation-karte__kopf {\n font-size: 0.9rem;\n}\n\n.szenen-relation-karte__kopf code {\n overflow-wrap: anywhere;\n}\n\n.szenen-relation-karte__pfeil {\n flex: none;\n color: var(--color-text-secondary);\n}\n\n/* Die Composer-\xD6ffner (\u201E+ Node hinzuf\xFCgen", \u201E+ Beziehung hinzuf\xFCgen") sind\n * Angebote, keine Ausk\xFCnfte \u2014 dasselbe gestrichelte Muster wie beim Folgelauf\n * und der Fehlerrettung (\xA73.3/\xA73.5). */\n.szenen-scene-editor__oeffnen {\n width: 100%;\n border-style: dashed;\n}\n\n.szenen-refpicker {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n margin: var(--space-sm) 0;\n padding: var(--space-sm) 0;\n border: none;\n border-radius: 0;\n}\n\n.szenen-refpicker legend {\n padding: 0 var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n}\n\n.szenen-capability > ul > li {\n display: grid;\n grid-template-columns: minmax(10rem, 1fr) auto minmax(14rem, 2fr);\n align-items: start;\n gap: var(--space-sm);\n}\n\n.szenen-capability__state {\n padding: 0.1rem 0.5rem;\n border-radius: var(--radius-full);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-capability__state--yes {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n}\n\n.szenen-capability__state--no {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n}\n\n.szenen-capability__state--unknown {\n color: #fef08a;\n background: rgba(234, 179, 8, 0.2);\n}\n\n.szenen-runs > ol > li {\n display: grid;\n gap: var(--space-sm);\n}\n\n/* B1 \u2014 die geschlossene Runde. Vorher stand die `run_id` als erstes und war\n die einzige Information, die niemand liest; jetzt beantwortet die Kopfzeile\n welche Runde, ob sie durchging, mit welchem Modell und was ungef\xE4hr\n herauskam. */\n.szenen-runs__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-sm);\n width: 100%;\n padding: var(--space-xs) 0;\n text-align: left;\n color: var(--color-text-secondary);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n\n.szenen-runs__marke {\n font-weight: 700;\n color: var(--color-text-primary);\n}\n\n.szenen-runs__status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n border-radius: var(--radius-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n}\n\n.szenen-runs__status[data-status="succeeded"] {\n border-color: var(--color-success);\n}\n\n.szenen-runs__status[data-status="failed"] {\n border-color: var(--color-danger);\n}\n\n.szenen-runs__modell {\n font-size: 0.75rem;\n}\n\n/* Der Auszug ist die Inhaltsvorschau der Runde. Er bekommt eine eigene Zeile,\n damit er nicht mit Marke und Modell um dieselbe Zeile ringt. */\n.szenen-runs__auszug {\n flex: 1 1 100%;\n min-width: 0;\n color: var(--color-text-tertiary);\n font-size: 0.8125rem;\n}\n\n/* B2 \u2014 ein Fehler steht ausserhalb der aufklappbaren Fl\xE4che: Wer eine rote\n Runde sieht, soll nicht erst klicken m\xFCssen, um zu erfahren, woran sie\n gescheitert ist. */\n.szenen-runs__fehler {\n display: grid;\n gap: var(--space-xs);\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n}\n\n.szenen-runs__fehler p {\n margin: 0;\n}\n\n.szenen-runs__inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-runs__prompt > h5,\n.szenen-runs__sekundaer > h5 {\n margin: 0 0 var(--space-xs);\n}\n\n.szenen-runs__prompt > p {\n margin: 0;\n white-space: pre-wrap;\n}\n\n.szenen-runs__prompt--negativ > p,\n.szenen-runs__sekundaer > p {\n margin: 0;\n color: var(--color-text-secondary);\n white-space: pre-wrap;\n}\n\n.szenen-runs__mehr {\n justify-self: start;\n margin-top: var(--space-xs);\n padding: 0;\n color: var(--color-accent-teal, var(--color-text-secondary));\n background: transparent;\n border: none;\n font-size: 0.75rem;\n text-decoration: underline;\n cursor: pointer;\n}\n\n.szenen-runs > ol > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-runs > ol > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n.szenen-runs details pre {\n max-height: 20rem;\n overflow: auto;\n padding: var(--space-sm);\n white-space: pre-wrap;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border-radius: var(--radius-sm);\n}\n\n.szenen-media > section {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-media__lookup,\n.szenen-media__attach {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-media__lookup > label,\n.szenen-media__attach > label:first-child {\n flex: 1 1 18rem;\n}\n\n.szenen-media__list {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-media__list > li {\n display: grid;\n grid-template-columns: minmax(12rem, 1fr) auto auto auto;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n@media (max-width: 760px) {\n .szenen-view {\n gap: var(--space-sm);\n }\n\n /* C1 \u2014 auf 390 px bleiben drei Kacheln je Reihe gross genug zum Erkennen\n und klein genug, dass vier Bilder eine Reihe sind und keine Seite. */\n .szenen-bildgalerie {\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n }\n\n /* Der Viewer bekommt den ganzen Bildschirm; die Bl\xE4tterkn\xF6pfe bleiben am\n Rand, wo der Daumen sie erreicht. */\n .szenen-bildviewer {\n padding: var(--space-xs);\n }\n\n .szenen-bildviewer__blaettern {\n width: 2.25rem;\n height: 4rem;\n }\n\n .szenen-view__intro {\n align-items: start;\n flex-direction: column;\n gap: var(--space-xs);\n padding: var(--space-sm);\n }\n\n .szenen-view__intro h2 {\n font-size: 1.05rem;\n }\n\n .szenen-view__intro > div > p:last-child {\n font-size: 0.78rem;\n }\n\n .szenen-view__nav {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-xs);\n padding: var(--space-xs);\n }\n\n .szenen-view__nav button {\n width: 100%;\n min-width: 0;\n min-height: 2.5rem;\n padding: 0.35rem 0.5rem;\n }\n\n\n .szenen-scene-editor {\n grid-template-columns: 1fr;\n }\n\n .szenen-capability > ul > li {\n grid-template-columns: 1fr;\n }\n\n .szenen-derive,\n .szenen-elements,\n .szenen-scene,\n .szenen-setup,\n .szenen-shot,\n .szenen-media,\n .szenen-runs {\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter > label {\n width: 100%;\n min-width: 0;\n flex: none;\n }\n\n .szenen-elements__filter > label:has(input[type="checkbox"]) {\n flex: none;\n grid-column: 1 / -1;\n }\n\n /* Schmal ist die Liste eine Spalte, und die Auswahl fuehrt weiter \u2014 der\n * Eintrag ist deshalb ein Ziel fuer den Finger, keine Kachel. */\n .szenen-elements__list {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .szenen-elements__list > li > button {\n min-height: 2.75rem;\n }\n\n .szenen-elements__back {\n justify-self: stretch;\n min-height: 2.5rem;\n }\n\n .szenen-detail__actions,\n .szenen-runs__start,\n .szenen-scene-editor__add,\n .szenen-scene-editor__add-relation {\n align-items: stretch;\n flex-direction: column;\n }\n\n .szenen-runs__start > *,\n .szenen-scene-editor__add > *,\n .szenen-scene-editor__add-relation > * {\n width: 100%;\n flex: none;\n }\n}\n\n/* \u2500\u2500 Stufe 6.5: Rekonstruktion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich zeigt drei Dinge nebeneinander: den Vorschlag, das nicht\n * Ableitbare und die Widerspr\xFCche. Die Diagnosen sind deshalb bewusst nicht\n * kleiner gesetzt als der Vorschlag \u2014 wer sie \xFCbersieht, \xFCbernimmt Geratenes.\n */\n\n.szenen-derive {\n display: grid;\n gap: var(--space-md);\n}\n\n.szenen-form {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-field {\n display: grid;\n gap: var(--space-xs);\n}\n\n.szenen-field > span {\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-error {\n padding: var(--space-sm) var(--space-md);\n color: var(--color-danger);\n background: var(--color-accent-red-dim);\n border: 1px solid var(--color-accent-red);\n border-radius: var(--radius-sm);\n}\n\n.szenen-empty {\n padding: var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-diagnose {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-warning);\n border-radius: var(--radius-sm);\n}\n\n/* Ein Widerspruch ist der schwerste der drei Befunde: dort steht eine\n * Entscheidung aus, die das System bewusst nicht getroffen hat (D-74). */\n.szenen-diagnose--konflikt {\n border-left-color: var(--color-danger);\n}\n\n.szenen-diagnose > h4 {\n margin: 0 0 var(--space-sm);\n font-size: 0.9375rem;\n}\n\n.szenen-diagnose ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-proposal {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-proposal > h4 {\n margin: 0 0 var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.9375rem;\n}\n\n.szenen-proposal__payload {\n max-height: 18rem;\n margin: 0 0 var(--space-sm);\n padding: var(--space-sm);\n overflow: auto;\n /* Ein erzeugter Prompt ist ein einziger langer Satz \u2014 ohne Umbruch\n verschwindet er nach rechts aus dem Bild. JSON-Payloads bringen ihre\n eigenen Zeilenumbrueche mit und leiden nicht darunter. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-proposal__done {\n margin: var(--space-sm) 0 0;\n color: var(--color-success);\n}\n\n/* Die Belege eines Laufs: Rohantwort und Systemprompt. Zum Lesen und Kopieren,\n nicht zum Bedienen \u2014 deshalb voll markierbar und in Monospace. */\n.szenen-beleg__text {\n width: 100%;\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n line-height: 1.5;\n white-space: pre;\n background: var(--color-bg-app);\n color: var(--color-text);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n resize: vertical;\n}\n\n/* Der Folgelauf (D-87): abgesetzt, damit er nicht wie Teil des Ergebnisses\n aussieht \u2014 er ist eine Frage, keine Auskunft. */\n.szenen-folgelauf {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-folgelauf > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-run__herkunft {\n margin: 0 0 var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-muted, inherit);\n}\n\n/* Widerspricht das Modell, ist das das Wichtigste auf der Seite. */\n.szenen-diagnose--antwort {\n border-left: 3px solid var(--color-accent, var(--color-border));\n}\n\n/* Der Bildw\xE4hler (D-92) \u2014 Browser und Inspector, dem Labs-Tab nachgebildet,\n aber mit dem CSS dieser Ansicht: eine gemeinsame Komponente \xFCber zwei\n Dom\xE4nengrenzen w\xE4re eine Kopplung, die keiner gewollt hat.\n\n Der Review vom 2026-08-22 (R12.1/R12.2) hat den Ma\xDFstab gesetzt: Der Bestand\n ist ein **Browser**, kein Seiteninhalt. Vierzig Werke d\xFCrfen die Szenenseite\n nicht meterlang machen, und zw\xF6lf Varianten eines Werkes keine zw\xF6lf\n Bildschirmseiten. Deshalb steht hier alles unter zwei Regeln \u2014 begrenzte\n Fl\xE4che mit eigenem Scroll, und Kacheln statt Karten. */\n\n/* \u2500\u2500 Ebene 1: der Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl {\n /* Dieselbe Begr\xFCndung wie beim Inspector: Als Grid-Element w\xFCrde der\n W\xE4hler sonst so breit wie sein breitester Inhalt. */\n min-width: 0;\n}\n\n.szenen-bildwahl__browser {\n /* Die eigene Scrollfl\xE4che ist der ganze Punkt: Der Bestand w\xE4chst nach\n innen, nicht nach unten. Dieselbe Gr\xF6\xDFenordnung wie `max-h-[300px]` im\n Labs-Browser. */\n max-height: 19rem;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__raster {\n display: grid;\n /* Vier Spalten auf schmalen Displays \u2014 der gemeldete Screenshot zeigte eine\n einzige Spalte gro\xDFer Karten. Breiter wird automatisch dichter. */\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__karte {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-surface);\n text-align: left;\n cursor: pointer;\n}\n\n/* Das fokussierte Werk ist klar markiert \u2014 sonst wei\xDF niemand, wessen Galerie\n der Inspector unten zeigt. */\n.szenen-bildwahl__karte.is-fokus {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n/* \u2500\u2500 Die Kachel selbst \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Der Platz steht, bevor das Bild da ist \u2014 sonst springt das Raster, sobald\n die erste Vorschau eintrifft. */\n.szenen-bildkachel {\n display: block;\n width: 100%;\n aspect-ratio: 1;\n overflow: hidden;\n background: var(--color-bg-app);\n}\n\n.szenen-bildkachel img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.szenen-bildkachel__leer {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, var(--color-bg-app), var(--color-bg-surface));\n}\n\n/* R12.3: Eine fehlende Vorschau bleibt kachelgro\xDF. Vorher fiel die Karte auf\n ihren Prompttext zur\xFCck \u2014 und ein Prompt hat im echten Bestand mehrere\n hundert Zeichen. */\n.szenen-bildkachel__zeichen {\n font-size: 0.875rem;\n font-weight: 700;\n color: var(--color-text-secondary);\n opacity: 0.6;\n}\n\n/* Wie viele Varianten das Werk hat \u2014 klein und \xFCberlagernd, nie als\n dominanter Text. */\n.szenen-bildwahl__anzahl {\n position: absolute;\n top: 0.15rem;\n right: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n font-weight: 700;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* Eine Zeile, dann Schluss. Der Prompt eines Werkes ist oft mehrere hundert\n Zeichen lang und w\xE4re als Prim\xE4rdarstellung genau die Textw\xFCste, gegen die\n der Review antritt. */\n.szenen-bildwahl__kacheltitel {\n display: block;\n padding: 0.15rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.3;\n color: var(--color-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Ebene 2: der Inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__inspector {\n /* Ohne `min-width: 0` gewinnt der Inhalt: Der W\xE4hler ist Element eines\n Grids (`.szenen-form`), und dort ist `min-width: auto` die Vorgabe. Der\n Streifen unten ist rund 1000 px breit \u2014 gemessen am 2026-08-22 im echten\n Browser zog er die Auswahl von 374 px auf 1088 px auf und schob die\n ganze Seite waagerecht heraus, statt selbst zu scrollen. */\n min-width: 0;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n /* Sichtbar abgesetzt vom Browser: zwei Ebenen, nicht eine lange Liste. */\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__inspektorkopf {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bildwahl__werktitel {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-text-primary);\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.szenen-bildwahl__zurueck {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n padding: 0.15rem 0.5rem;\n}\n\n/* Der Streifen: waagerecht scrollend, feste kleine Kacheln \u2014 zw\xF6lf Varianten\n bleiben ein \xDCberblick statt zw\xF6lf Bildschirmseiten. */\n.szenen-bildwahl__streifen {\n /* Die Breite kommt vom Elter, nie vom Inhalt \u2014 sonst scrollt hier nichts. */\n max-width: 100%;\n display: flex;\n gap: var(--space-sm);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n\n.szenen-bildwahl__streifen > li {\n flex: 0 0 auto;\n width: 5rem;\n}\n\n.szenen-bildwahl__variante {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-app);\n cursor: pointer;\n}\n\n.szenen-bildwahl__variante.is-gewaehlt {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n.szenen-bildwahl__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Das Gew\xE4hlte \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__gewaehlt {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__gewaehlt li {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.25rem 0.5rem;\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n max-width: 100%;\n}\n\n/* Das gew\xE4hlte Bild wird best\xE4tigt, nicht nur benannt: eine kleine Vorschau\n neben dem Titel. */\n.szenen-bildwahl__gewaehlt .szenen-bildkachel {\n width: 2.25rem;\n flex: 0 0 auto;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__gewaehlt .szenen-bildwahl__kacheltitel {\n min-width: 0;\n max-width: 12rem;\n font-size: 0.75rem;\n}\n\n.szenen-beleg__kopf {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details + details {\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details > summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Die Fehlerrettung (Stufe 6.6, A5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Sie steht im Fehlerblock und muss trotzdem als Werkzeug lesbar sein, nicht\n * als weitere Fehlermeldung. Deshalb abgesetzt wie der Folgelauf \u2014 und aus\n * demselben Grund: Beides sind Angebote, keine Ausk\xFCnfte.\n *\n * Der wichtigste optische Punkt ist die Trennung von Original und\n * Arbeitskopie. Das Original bekommt den ruhigen, \u201Efesten" Hintergrund der\n * Belege; die Kopie sieht aus wie ein normales Eingabefeld, weil sie eines\n * ist. */\n.szenen-rettung {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-rettung > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-rettung textarea[readonly] {\n background: var(--color-bg-surface);\n color: var(--color-text-secondary);\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung textarea:not([readonly]) {\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung__aktionen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0;\n}\n\n/* Ein Befund tr\xE4gt Code, Bezug und Meldung. Der Code steht klein und\n * monospaced davor: Er ist die Kennung, nicht die Aussage. */\n.szenen-diagnose__liste {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-diagnose__liste code {\n font-size: 0.8125rem;\n}\n\n.szenen-diagnose__liste > li[data-severity=\'warning\'] {\n color: var(--color-text-secondary);\n}\n\n/* \u2500\u2500 Die Draft-Kette (Stufe 6.6, C4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Hauptweg. Er steht nach den Einzel\xFCbernahmen und muss als das erkennbar\n * sein, was er ist: nicht eine weitere Option, sondern der k\xFCrzere Weg zum\n * selben Ziel. Deshalb durchgezogener Rahmen statt gestricheltem \u2014 er ist\n * Angebot **und** Empfehlung. */\n.szenen-kette {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-kette > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-kette ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0 0 var(--space-sm);\n padding-left: var(--space-md);\n}\n\n.szenen-kette code {\n font-size: 0.8125rem;\n}\n\n/* Ein Hinweis, der eine Bedingung erklaert \u2014 nicht rot, weil er kein Fehler\n ist, aber abgesetzt, weil er eine Folge hat. */\n.szenen-detail__hint {\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Stufe 6.7: Bilder am Prompt-Lauf (H0/H1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich tr\xE4gt zwei Dinge, die man nie verwechseln darf: den **Eingang**\n * (woraus der Prompt entstand) und den **Ausgang** (was daraus wurde). Sie\n * bekommen deshalb verschiedene Akzentkanten statt nur verschiedener\n * \xDCberschriften \u2014 wer schnell liest, sieht die Trennung, bevor er sie liest.\n */\n.szenen-bilder {\n display: grid;\n gap: var(--space-md);\n margin-top: var(--space-sm);\n}\n\n.szenen-bilder__eingang,\n.szenen-bilder__ausgang {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__eingang {\n border-left: 3px solid var(--color-accent-purple, var(--color-border));\n}\n\n.szenen-bilder__ausgang {\n border-left: 3px solid var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-bilder h5 {\n margin: 0;\n}\n\n.szenen-bilder__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: 0;\n align-items: baseline;\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-bilder__eingangsliste,\n.szenen-bilder__batchliste,\n.szenen-bilder__auftraege {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bilder__eingangsliste > li,\n.szenen-bilder__batchliste > li,\n.szenen-bilder__auftraege > li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n/* Ein unterbrochener Auftrag ist der Fall, f\xFCr den der Auftragssatz existiert.\n Er wird deshalb markiert \u2014 nicht rot, denn er ist kein Fehler, sondern eine\n offene Entscheidung. */\n.szenen-bilder__auftraege > li[data-status="started"] {\n border-left: 3px solid var(--color-warning);\n}\n\n.szenen-bilder__auftraege > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-bilder__auftraege > li[data-status="partial"] {\n border-left: 3px solid var(--color-accent-blue, var(--color-border));\n}\n\n.szenen-bilder__auftraege > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n/* Das Eingangsbild benutzt seit C3 dieselbe Kachel wie W\xE4hler und Galerie.\n Sie ist von Haus aus so breit wie ihr Platz \u2014 in dieser Zeile bekommt sie\n deshalb eine feste Gr\xF6sse, sonst sch\xF6be sie die Metadaten heraus. */\n.szenen-bilder__eingangsliste .szenen-bildkachel {\n flex: 0 0 auto;\n width: 4rem;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batch {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batchkopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-bilder__herkunft {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n/* Review-Befund \u2014 ein transienter Bildfehler ist wiederholbar, kein\n endg\xFCltiges \u201Efehlt". Dezent wie ein Angebot, nicht wie ein Fehlerzustand. */\n.szenen-bilder__retry {\n justify-self: start;\n padding: 0.15rem 0.5rem;\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n/* C4 \u2014 die Provenienz vollst\xE4ndig, aber sekund\xE4r. */\n.szenen-bilder__technik > summary {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-bilder__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n}\n\n/* C5 \u2014 der Verlauf verdichtet, seine Ausreisser nicht. */\n.szenen-bilder__verlauf > summary {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n cursor: pointer;\n}\n\n.szenen-bilder__verlauf[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__auffaellig {\n margin: 0;\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n.szenen-bilder__auftragskennung {\n flex-basis: 100%;\n color: var(--color-text-tertiary);\n font-size: 0.6875rem;\n}\n\n/* \u2500\u2500 C1/C2 \u2014 Galerie und grosse Ansicht \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Das Raster w\xE4chst mit dem Platz: auf 390 px drei bis vier Kacheln je Reihe,\n auf dem Desktop entsprechend mehr. Vorher war jedes Bild eine Zeile aus\n Daumennagel plus vier Metadatenfeldern \u2014 vier Bilder ergaben vier Textzeilen\n und das Protokoll war die Hauptsache. */\n.szenen-bildgalerie {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));\n gap: var(--space-sm);\n min-width: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildgalerie__kachel {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildgalerie__kachel:hover,\n.szenen-bildgalerie__kachel:focus-visible {\n border-color: var(--color-accent-teal, var(--color-text-primary));\n}\n\n.szenen-bildgalerie__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* Die grosse Ansicht liegt \xFCber allem und bringt ihren eigenen Grund mit \u2014\n `position: fixed` bezieht sich im Flow-Canvas auf dessen Rahmen, in React auf\n das Fenster; in beiden F\xE4llen genau das Gew\xFCnschte. */\n.szenen-bildviewer {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: var(--space-sm);\n padding: var(--space-sm);\n /* Deckend, nicht get\xF6nt: Bei 0.92 schimmerte der Laufbereich durch und man\n las Prompttext quer \xFCber dem Bild \u2014 im Browser gemessen. */\n background: #000;\n}\n\n/* Titel, Z\xE4hlung, Schliessen \u2014 drei Spalten, damit ein langer Werktitel die\n Z\xE4hlung nicht in die n\xE4chste Zeile und \xFCber das Bild schiebt. */\n.szenen-bildviewer__kopf {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto auto;\n align-items: center;\n gap: var(--space-sm);\n color: var(--color-text-primary);\n}\n\n.szenen-bildviewer__titel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 600;\n}\n\n.szenen-bildviewer__zaehler {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n}\n\n.szenen-bildviewer__buehne {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-sm);\n min-height: 0;\n min-width: 0;\n}\n\n/* Der Rahmen nimmt den Platz, der zwischen den Bl\xE4tterkn\xF6pfen \xFCbrig ist \u2014\n `min-width: 0`, damit er auch wirklich schrumpft statt sie hinauszuschieben. */\n.szenen-bildviewer__rahmen {\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.szenen-bildviewer__bild {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.szenen-bildviewer__leer {\n color: var(--color-text-secondary);\n text-align: center;\n}\n\n/* Die Bl\xE4tterkn\xF6pfe sind daumengross und liegen an den R\xE4ndern \u2014 auf einem\n Telefon ist der Rand erreichbar, die Bildmitte nicht. */\n.szenen-bildviewer__blaettern {\n flex: 0 0 auto;\n width: 2.75rem;\n height: 3.5rem;\n font-size: 1.75rem;\n line-height: 1;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildviewer__details {\n max-height: 8rem;\n overflow-y: auto;\n color: var(--color-text-secondary);\n}\n\n/* Die Aktionen stehen abgesetzt: Sie sind Angebote, keine Ausk\xFCnfte \u2014 dasselbe\n Muster wie beim Folgelauf und bei der Fehlerrettung. */\n/* D \u2014 die Aktionen stehen abgesetzt und standardm\xE4ssig kompakt: Sie sind\n Angebote, keine Ausk\xFCnfte. Vorher hielten beide Formulare dauerhaft offen und\n machten aus jedem Lauf zwei zus\xE4tzliche Bildschirmseiten. */\n.szenen-bilder__aktion {\n min-width: 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__aktion > summary {\n color: var(--color-text-primary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-bilder__aktion[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion h5 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-bilder__aktion ol {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n font-size: 0.8125rem;\n}\n\n/* Regelwerk-UX Schritt 16 \u2014 der ZIP-Sammelimport.\n *\n * \xA78: Der gemeldete Einsatz ist mobil. Deshalb **ein** Seedfeld statt einer\n * gestapelten Eingabe je Bild, und die Zuordnung Bild -> Seed als umbrechende\n * Zeile statt als Tabelle: Eine Tabelle waere auf 390 px abgeschnitten, und\n * abgeschnitten heisst hier, dass der Nutzer die Zuordnung nicht mehr pruefen\n * kann \u2014 genau die Pruefung, wegen der die Vorschau existiert. */\n.szenen-bilder__zip {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-bilder__zipkopf {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipvorlagen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n margin: 0;\n}\n\n/* Eine laengere Liste bleibt scrollbar, statt die Sektion und damit den\n * Import-Knopf aus dem Blick zu schieben (\xA78, letzter Punkt). */\n.szenen-bilder__zipliste {\n max-height: 16rem;\n overflow-y: auto;\n}\n\n.szenen-bilder__zipliste > li {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-bilder__zipliste > li > span:first-child {\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipliste > li > span:last-child {\n color: var(--color-text-secondary);\n white-space: nowrap;\n}\n\n/* ==========================================================================\n STUFE 6.10 \u2014 ELEMENTEDITOR UND VERSIONSNAVIGATION\n ========================================================================== */\n\n/* \xA74.2 \u2014 vier Sachverhalte, vier Marken. Die Marken stehen in einer eigenen\n * Spalte, damit ANGEZEIGT + BEVORZUGT nebeneinander lesbar bleiben, statt sich\n * durch den Text zu schieben. */\n.szenen-detail__heads li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.szenen-version__titel {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n margin-right: auto;\n}\n\n/* V17 \u2014 Kennungen bleiben vollst\xE4ndig sichtbar, aber optisch sekund\xE4r. */\n.szenen-version__id,\n.szenen-ref__id,\n.szenen-detail__field-key {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n overflow-wrap: anywhere;\n}\n\n.szenen-version__marken {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-version__marken > span {\n padding: 0.1rem 0.5rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.7rem;\n font-weight: 800;\n letter-spacing: 0.08em;\n}\n\n.szenen-version--angezeigt {\n border-color: var(--color-accent-teal, #2dd4bf) !important;\n}\n\n/* V9 \u2014 der Basiswechsel ist eine Entscheidung, keine Nebenwirkung. */\n.szenen-detail__confirm {\n display: grid;\n gap: var(--space-sm);\n margin: var(--space-md) 0;\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-accent-amber, #f59e0b);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__confirm p {\n margin: 0;\n}\n\n/* Untergruppen von Objektfeldern (`prompt_render_settings`, `overrides`,\n `blender_render_settings`) \u2014 seit A1/A2 auf- und zuklappbar. Eine optionale\n Gruppe startet zu: Blender und `overrides` f\xFCllten auf einem 390-px-Display\n sonst mehrere Bildschirmseiten, obwohl die meisten Shots sie nie anfassen. */\n.szenen-detail__group {\n min-width: 0;\n margin: var(--space-md) 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group > summary {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__group[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n/* A2 \u2014 dass eine zugeklappte Gruppe schon Werte tr\xE4gt, muss man sehen, ohne\n sie zu \xF6ffnen. Sonst \xFCbersieht man eine gesetzte Blender-Ausgabe genau so\n lange, bis sie im Ergebnis auftaucht. */\n.szenen-detail__group-status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--color-text-tertiary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group[data-konfiguriert="ja"] > summary .szenen-detail__group-status {\n color: var(--color-text-primary);\n border-color: var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-detail__group-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n/* A3 \u2014 die Kernaussage steht am Feld, die Begr\xFCndung dahinter. Nicht als\n Tooltip: Auf einem Telefon gibt es kein Hover, und was dort unerreichbar ist,\n ist gestrichen \u2014 nur ohne dass es jemand entschieden h\xE4tte. */\n.szenen-detail__warum > summary {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-detail__warum > small {\n display: block;\n padding-top: var(--space-xs);\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__fieldrow {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n margin-bottom: var(--space-md);\n}\n\n/* A4 \u2014 der technische Feldname bleibt lesbar, tritt aber hinter das fachliche\n Label zur\xFCck. */\n.szenen-detail__field-label {\n color: var(--color-text-primary);\n}\n\n.szenen-detail__field {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-detail__field > span {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n}\n\n/* Mehrfachauswahl und Listeneditoren. */\n.szenen-feld__mehrfach,\n.szenen-feld__liste,\n.szenen-feld__reflist,\n.szenen-feld__ref,\n.szenen-feld__zielmodell,\n.szenen-feld__kamera {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-feld__mehrfach > label,\n.szenen-feld__null {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-feld__liste-zeile {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-feld__reflist ol {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-feld__reflist li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs);\n padding: var(--space-xs) var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \xA76.1 \u2014 Titel und Typ zuerst, Kennungen daneben. */\n.szenen-ref__anzeige {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-ref__typ {\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-ref__warn {\n padding: 0.1rem 0.5rem;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n/* \xA76.5 \u2014 abgeleitete Angaben, ausdr\xFCcklich nur Anzeige. */\n.szenen-detail__derived {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__derived dl {\n display: grid;\n grid-template-columns: minmax(10rem, auto) 1fr;\n gap: var(--space-xs) var(--space-sm);\n margin: 0;\n}\n\n.szenen-detail__derived dt {\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__derived dd {\n margin: 0;\n}\n\n.szenen-feld__abgeleitet {\n margin: 0;\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n}\n';
|
|
175
175
|
|
|
176
176
|
// src/lib/utils.ts
|
|
177
177
|
var getFormattedTimestamp = () => {
|
|
@@ -188,11 +188,6 @@ var GLOBAL_STYLES = `
|
|
|
188
188
|
.prompt-loading { background: linear-gradient(90deg, rgba(255,255,255,0.02) 25%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 75%); background-size: 200% 100%; animation: prompt-shimmer 2s infinite linear; }
|
|
189
189
|
@keyframes dropdown-enter { from { opacity: 0; transform: scale(0.95) translateY(-5px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
|
190
190
|
.animate-dropdown { animation: dropdown-enter 0.1s ease-out forwards; }
|
|
191
|
-
.flow-szenen-entry { display: grid; gap: 6px; width: 100%; max-width: 280px; text-align: center; }
|
|
192
|
-
.flow-szenen-entry button, .flow-szenen-shell__header button { min-height: 44px; padding: 0 16px; color: #e7e5e4; background: #1d4ed8; border: 1px solid #3b82f6; border-radius: 12px; font: inherit; font-weight: 700; cursor: pointer; }
|
|
193
|
-
.flow-szenen-entry button { display: flex; align-items: center; justify-content: center; gap: 8px; }
|
|
194
|
-
.flow-szenen-entry button:disabled { color: rgba(255,255,255,.3); background: transparent; border-color: rgba(255,255,255,.1); cursor: not-allowed; }
|
|
195
|
-
.flow-szenen-entry small { color: rgba(255,255,255,.35); font-size: 10px; }
|
|
196
191
|
.flow-workspace-picker { position: fixed; inset: 0; display: grid; place-items: center; padding: 16px; color: #f5f5f5; background: #050505; overflow: auto; }
|
|
197
192
|
.flow-workspace-picker__card { box-sizing: border-box; display: grid; gap: 16px; width: min(100%, 448px); padding: 24px; background: #171717; border: 1px solid #404040; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,.55); }
|
|
198
193
|
.flow-workspace-picker__card header { display: grid; gap: 4px; }
|
|
@@ -203,7 +198,7 @@ var GLOBAL_STYLES = `
|
|
|
203
198
|
.flow-workspace-picker__choices button:hover { border-color: #3b82f6; background: #262626; }
|
|
204
199
|
.flow-workspace-picker__back { justify-self: start; padding: 0; color: #a3a3a3; background: transparent; border: 0; font: inherit; cursor: pointer; }
|
|
205
200
|
.flow-workspace-picker__warning { margin: 0; padding: 10px; color: #fbbf24; background: rgba(146,64,14,.2); border: 1px solid rgba(217,119,6,.4); border-radius: 8px; font-size: 12px; }
|
|
206
|
-
.flow-szenen-
|
|
201
|
+
.flow-szenen-pane {
|
|
207
202
|
--sz-bg: #0e0e0e; --sz-surface: #171717; --sz-elevated: #262626;
|
|
208
203
|
--sz-border: #404040; --sz-text: #f5f5f5; --sz-muted: #a3a3a3;
|
|
209
204
|
--color-bg-app: #000; --color-bg-surface: #0a0a0a; --color-bg-surface-hover: #171717; --color-bg-elevated: #171717;
|
|
@@ -217,15 +212,11 @@ var GLOBAL_STYLES = `
|
|
|
217
212
|
--radius-sm: .25rem; --radius-md: .5rem; --radius-lg: .75rem; --radius-full: 9999px;
|
|
218
213
|
--font-sans: system-ui, -apple-system, sans-serif;
|
|
219
214
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
220
|
-
|
|
221
|
-
color: var(--sz-text); background: var(--sz-bg); overflow:
|
|
222
|
-
}
|
|
223
|
-
.flow-szenen-
|
|
224
|
-
.flow-szenen-
|
|
225
|
-
.flow-szenen-shell__header div { display: grid; gap: 2px; }
|
|
226
|
-
.flow-szenen-shell__header small { color: var(--sz-muted); font-family: monospace; }
|
|
227
|
-
.flow-szenen-shell__content { flex: 1; min-height: 0; padding: 16px; overflow: auto; }
|
|
228
|
-
@media (max-width: 760px) { .flow-szenen-shell__content { padding: 8px; } }
|
|
215
|
+
flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 16px;
|
|
216
|
+
color: var(--sz-text); background: var(--sz-bg); overflow: auto;
|
|
217
|
+
}
|
|
218
|
+
.flow-szenen-pane--empty { align-items: center; justify-content: center; text-align: center; color: var(--sz-muted); font-size: 12px; }
|
|
219
|
+
@media (max-width: 760px) { .flow-szenen-pane { padding: 8px; } }
|
|
229
220
|
` + SZENEN_VIEW_STYLES;
|
|
230
221
|
|
|
231
222
|
// src/lib/metadata.ts
|
|
@@ -2441,10 +2432,72 @@ function bildEndungFuer(mimeType) {
|
|
|
2441
2432
|
return "jpg";
|
|
2442
2433
|
}
|
|
2443
2434
|
|
|
2435
|
+
// src/lib/szenen/szenenSnapshot.ts
|
|
2436
|
+
var SZENEN_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
2437
|
+
var SZENEN_SNAPSHOT_PREFIX = "szenen-state-";
|
|
2438
|
+
function istSzenenSnapshotDatei(name) {
|
|
2439
|
+
return name.startsWith(SZENEN_SNAPSHOT_PREFIX) && name.endsWith(".json");
|
|
2440
|
+
}
|
|
2441
|
+
var maxTsOf = (events) => {
|
|
2442
|
+
let max = 0;
|
|
2443
|
+
for (const event of events) if (event.ts > max) max = event.ts;
|
|
2444
|
+
return max;
|
|
2445
|
+
};
|
|
2446
|
+
function aufgenommeneDateien(snapshot) {
|
|
2447
|
+
return new Set(snapshot ? snapshot.consumed.paths : []);
|
|
2448
|
+
}
|
|
2449
|
+
function parseSzenenSnapshot(raw, herkunft) {
|
|
2450
|
+
if (!raw || typeof raw !== "object") {
|
|
2451
|
+
throw new Error(`Szenen-Snapshot ${herkunft}: kein Objekt, sondern ${typeof raw}.`);
|
|
2452
|
+
}
|
|
2453
|
+
const obj = raw;
|
|
2454
|
+
if (obj.schema_version !== SZENEN_SNAPSHOT_SCHEMA_VERSION) {
|
|
2455
|
+
throw new Error(
|
|
2456
|
+
`Szenen-Snapshot ${herkunft}: schema_version ${String(obj.schema_version)} wird nicht gelesen, erwartet ${SZENEN_SNAPSHOT_SCHEMA_VERSION}.`
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
if (!Array.isArray(obj.events)) {
|
|
2460
|
+
throw new Error(`Szenen-Snapshot ${herkunft}: "events" fehlt oder ist kein Array.`);
|
|
2461
|
+
}
|
|
2462
|
+
const events = obj.events;
|
|
2463
|
+
for (const [i, event] of events.entries()) {
|
|
2464
|
+
if (!event || typeof event.type !== "string" || typeof event.ts !== "number") {
|
|
2465
|
+
throw new Error(
|
|
2466
|
+
`Szenen-Snapshot ${herkunft}: Ereignis ${i} hat keinen Typ oder keinen Zeitstempel.`
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
const consumed = obj.consumed;
|
|
2471
|
+
if (!consumed || typeof consumed.count !== "number" || typeof consumed.max_ts !== "number") {
|
|
2472
|
+
throw new Error(`Szenen-Snapshot ${herkunft}: "consumed" fehlt oder ist unvollstaendig.`);
|
|
2473
|
+
}
|
|
2474
|
+
if (!Array.isArray(consumed.paths)) {
|
|
2475
|
+
throw new Error(
|
|
2476
|
+
`Szenen-Snapshot ${herkunft}: "consumed.paths" fehlt. Ohne die aufgenommenen Dateinamen laesst sich nicht entscheiden, welche Datei in events/ noch aussteht.`
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
if (consumed.count !== events.length) {
|
|
2480
|
+
throw new Error(
|
|
2481
|
+
`Szenen-Snapshot ${herkunft}: consumed.count ${consumed.count} passt nicht zu ${events.length} Ereignis(sen) in der Datei \u2014 die Datei ist unvollstaendig.`
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
const echt = maxTsOf(events);
|
|
2485
|
+
if (consumed.max_ts !== echt) {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
`Szenen-Snapshot ${herkunft}: consumed.max_ts ${consumed.max_ts} passt nicht zum groessten Zeitstempel ${echt} in der Datei.`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
return { schema_version: SZENEN_SNAPSHOT_SCHEMA_VERSION, consumed, events };
|
|
2491
|
+
}
|
|
2492
|
+
function mergeSnapshotAndPending(snapshot, pending, bereitsImSnapshot = 0) {
|
|
2493
|
+
if (!snapshot) return { events: pending, bereitsImSnapshot };
|
|
2494
|
+
return { events: [...snapshot.events, ...pending], bereitsImSnapshot };
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2444
2497
|
// src/lib/hfEventLog.ts
|
|
2445
|
-
function treeUrl(namespace, subdir = "") {
|
|
2498
|
+
function treeUrl(namespace, subdir = "", revision = "main") {
|
|
2446
2499
|
const parts = [namespace.replace(/\/$/, ""), subdir].filter(Boolean).join("/");
|
|
2447
|
-
return `${HF_BASE}/api/datasets/${HF_REPO}/tree
|
|
2500
|
+
return `${HF_BASE}/api/datasets/${HF_REPO}/tree/${encodeURIComponent(revision)}${parts ? "/" + parts : ""}`;
|
|
2448
2501
|
}
|
|
2449
2502
|
var DROSSEL_VERSUCHE = 4;
|
|
2450
2503
|
var DROSSEL_BASIS_MS = 400;
|
|
@@ -2477,8 +2530,8 @@ async function mitGeduld(anfrage) {
|
|
|
2477
2530
|
}
|
|
2478
2531
|
return letzte;
|
|
2479
2532
|
}
|
|
2480
|
-
async function hfListDir(namespace, subdir, token) {
|
|
2481
|
-
const res = await mitGeduld(() => holeMitZeitlimit(treeUrl(namespace, subdir), {
|
|
2533
|
+
async function hfListDir(namespace, subdir, token, revision = "main") {
|
|
2534
|
+
const res = await mitGeduld(() => holeMitZeitlimit(treeUrl(namespace, subdir, revision), {
|
|
2482
2535
|
headers: { Authorization: `Bearer ${token}` }
|
|
2483
2536
|
}));
|
|
2484
2537
|
if (res.status === 404) return [];
|
|
@@ -2507,17 +2560,46 @@ function toBase64(text) {
|
|
|
2507
2560
|
bytes.forEach((b) => binary += String.fromCharCode(b));
|
|
2508
2561
|
return btoa(binary);
|
|
2509
2562
|
}
|
|
2510
|
-
|
|
2563
|
+
var HfPreconditionFailedError = class extends Error {
|
|
2564
|
+
constructor(message) {
|
|
2565
|
+
super(message);
|
|
2566
|
+
this.name = "HfPreconditionFailed";
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
async function hfRepoHead(token, revision = "main") {
|
|
2570
|
+
const res = await mitGeduld(() => holeMitZeitlimit(
|
|
2571
|
+
`${HF_BASE}/api/datasets/${HF_REPO}/revision/${encodeURIComponent(revision)}`,
|
|
2572
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2573
|
+
));
|
|
2574
|
+
if (!res.ok) {
|
|
2575
|
+
throw new Error(`HF revision failed: ${res.status} ${res.statusText} (${revision})`);
|
|
2576
|
+
}
|
|
2577
|
+
const daten = await res.json();
|
|
2578
|
+
if (typeof daten.sha !== "string" || daten.sha.length === 0) {
|
|
2579
|
+
throw new Error(
|
|
2580
|
+
`HF revision ${revision}: die Antwort traegt kein "sha". Erhaltene Felder: ${Object.keys(daten || {}).join(", ") || "(keine)"}.`
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
return daten.sha;
|
|
2584
|
+
}
|
|
2585
|
+
async function hfUploadSmallFile(repoPath, content, token, summary = `Update ${repoPath}`, options) {
|
|
2586
|
+
const kopf = { summary, description: "" };
|
|
2587
|
+
if (options?.parentCommit) kopf.parentCommit = options.parentCommit;
|
|
2511
2588
|
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
|
|
2512
2589
|
method: "POST",
|
|
2513
2590
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
|
|
2514
2591
|
body: [
|
|
2515
|
-
JSON.stringify({ key: "header", value:
|
|
2592
|
+
JSON.stringify({ key: "header", value: kopf }),
|
|
2516
2593
|
JSON.stringify({ key: "file", value: { path: repoPath, encoding: "base64", content: toBase64(content) } })
|
|
2517
2594
|
].join("\n")
|
|
2518
2595
|
});
|
|
2519
2596
|
if (!res.ok) {
|
|
2520
2597
|
const err = await res.text().catch(() => "");
|
|
2598
|
+
if (options?.parentCommit && (res.status === 412 || res.status === 409)) {
|
|
2599
|
+
throw new HfPreconditionFailedError(
|
|
2600
|
+
`HF commit abgelehnt: Der Branch hat sich seit ${options.parentCommit} bewegt (${res.status}). Es wurde nichts geschrieben.`
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2521
2603
|
throw new Error(`HF upload failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
|
|
2522
2604
|
}
|
|
2523
2605
|
}
|
|
@@ -2567,8 +2649,8 @@ async function mitBegrenzterParallelitaet(count, limit, task) {
|
|
|
2567
2649
|
await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
|
|
2568
2650
|
if (fehlerAufgetreten) throw ersterFehler;
|
|
2569
2651
|
}
|
|
2570
|
-
async function loadPendingEvents(namespace, token, sinceTs, knownPaths) {
|
|
2571
|
-
const files = await hfListDir(namespace, "events", token);
|
|
2652
|
+
async function loadPendingEvents(namespace, token, sinceTs, knownPaths, revision = "main") {
|
|
2653
|
+
const files = await hfListDir(namespace, "events", token, revision);
|
|
2572
2654
|
const pending = files.filter((f) => f.type === "file" && tsFromEventPath(f.path) > sinceTs && !knownPaths?.has(f.path)).sort((a, b) => a.path.localeCompare(b.path));
|
|
2573
2655
|
if (!pending.length) return [];
|
|
2574
2656
|
const raw = new Array(pending.length);
|
|
@@ -2578,6 +2660,16 @@ async function loadPendingEvents(namespace, token, sinceTs, knownPaths) {
|
|
|
2578
2660
|
for (const f of pending) knownPaths?.add(f.path);
|
|
2579
2661
|
return raw.flatMap((e) => Array.isArray(e) ? e : [e]);
|
|
2580
2662
|
}
|
|
2663
|
+
async function findLatestSzenenSnapshotPath(namespace, token) {
|
|
2664
|
+
const files = await hfListDir(namespace, "", token);
|
|
2665
|
+
const treffer = files.filter((f) => f.type === "file" && istSzenenSnapshotDatei(f.path.split("/").pop() || "")).map((f) => f.path).sort((a, b) => b.localeCompare(a));
|
|
2666
|
+
return treffer[0] ?? null;
|
|
2667
|
+
}
|
|
2668
|
+
async function loadLatestSzenenSnapshot(namespace, token) {
|
|
2669
|
+
const pfad = await findLatestSzenenSnapshotPath(namespace, token);
|
|
2670
|
+
if (!pfad) return null;
|
|
2671
|
+
return parseSzenenSnapshot(await hfDownloadJsonByPath(pfad, token), pfad);
|
|
2672
|
+
}
|
|
2581
2673
|
var SESSION_CLIENT_ID = `client-${crypto.randomUUID().slice(0, 8)}`;
|
|
2582
2674
|
function getSessionClientId() {
|
|
2583
2675
|
return SESSION_CLIENT_ID;
|
|
@@ -2603,7 +2695,13 @@ async function writeHFEvent(namespace, token, type, payload, prevTs, options) {
|
|
|
2603
2695
|
const suffix = options?.ordered ? `${String(++writeSeq % 1e6).padStart(6, "0")}${uuid}` : uuid;
|
|
2604
2696
|
const filename = `${tsToFilename(ts)}_${suffix}.json`;
|
|
2605
2697
|
const repoPath = `${namespace}events/${filename}`;
|
|
2606
|
-
await hfUploadSmallFile(
|
|
2698
|
+
await hfUploadSmallFile(
|
|
2699
|
+
repoPath,
|
|
2700
|
+
JSON.stringify(event, null, 2),
|
|
2701
|
+
token,
|
|
2702
|
+
`Event: ${type}`,
|
|
2703
|
+
options?.parentCommit ? { parentCommit: options.parentCommit } : void 0
|
|
2704
|
+
);
|
|
2607
2705
|
return event;
|
|
2608
2706
|
}
|
|
2609
2707
|
|
|
@@ -5955,7 +6053,8 @@ function capabilitiesOf(port) {
|
|
|
5955
6053
|
imageBatchHistory: typeof port.imageBatchHistory?.listBatchesForPromptRun === "function",
|
|
5956
6054
|
imageOrderHistory: typeof port.imageBatchHistory?.listOrdersForPromptRun === "function",
|
|
5957
6055
|
imageBatchWrite: typeof port.imageBatchWrite?.importBatch === "function",
|
|
5958
|
-
imageGeneration: typeof port.imageGeneration?.startFromPromptRun === "function"
|
|
6056
|
+
imageGeneration: typeof port.imageGeneration?.startFromPromptRun === "function",
|
|
6057
|
+
imageJobs: typeof port.imageJobs?.claimAndRun === "function"
|
|
5959
6058
|
};
|
|
5960
6059
|
}
|
|
5961
6060
|
async function callOptional(capability, operation) {
|
|
@@ -9102,13 +9201,13 @@ function noticeFor(ergebnis, was) {
|
|
|
9102
9201
|
};
|
|
9103
9202
|
}
|
|
9104
9203
|
case "rejected": {
|
|
9105
|
-
const
|
|
9204
|
+
const fehler5 = ergebnis.errors ?? [];
|
|
9106
9205
|
const ungeprueft = ergebnis.unchecked ?? [];
|
|
9107
9206
|
return {
|
|
9108
9207
|
tone: "rejected",
|
|
9109
|
-
text: `${was}: abgelehnt \u2014 ${
|
|
9208
|
+
text: `${was}: abgelehnt \u2014 ${fehler5.length} Befund${fehler5.length === 1 ? "" : "e"}.`,
|
|
9110
9209
|
details: [
|
|
9111
|
-
...
|
|
9210
|
+
...fehler5.map((f) => `${f.path ?? "(ohne Pfad)"}: ${f.message ?? "ohne Meldung"}`),
|
|
9112
9211
|
// Nicht verschweigen: was nicht geprüft wurde, ist nicht in Ordnung,
|
|
9113
9212
|
// sondern ungeprüft.
|
|
9114
9213
|
...ungeprueft.map((u) => `ungepr\xFCft: ${u}`)
|
|
@@ -12608,7 +12707,7 @@ var ERKLAERUNG = {
|
|
|
12608
12707
|
};
|
|
12609
12708
|
var CapabilityPanel = ({ port, elementId, setupPayload }) => {
|
|
12610
12709
|
const [bericht, setBericht] = (0, import_react37.useState)(null);
|
|
12611
|
-
const [
|
|
12710
|
+
const [fehler5, setFehler] = (0, import_react37.useState)(null);
|
|
12612
12711
|
(0, import_react37.useEffect)(() => {
|
|
12613
12712
|
let abgemeldet = false;
|
|
12614
12713
|
setBericht(null);
|
|
@@ -12630,10 +12729,10 @@ var CapabilityPanel = ({ port, elementId, setupPayload }) => {
|
|
|
12630
12729
|
abgemeldet = true;
|
|
12631
12730
|
};
|
|
12632
12731
|
}, [port, elementId, setupPayload]);
|
|
12633
|
-
if (
|
|
12732
|
+
if (fehler5) {
|
|
12634
12733
|
return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("p", { "data-testid": "capability-fehler", role: "alert", children: [
|
|
12635
12734
|
"F\xE4higkeitsbericht nicht erstellbar: ",
|
|
12636
|
-
|
|
12735
|
+
fehler5
|
|
12637
12736
|
] });
|
|
12638
12737
|
}
|
|
12639
12738
|
if (!bericht) return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { "data-testid": "capability-laden", children: "F\xE4higkeitsbericht wird erstellt \u2026" });
|
|
@@ -13000,7 +13099,60 @@ var SZENEN_EVENT_TYPES = {
|
|
|
13000
13099
|
* Der Elementreducer ignoriert auch diesen Typ (`default:`-Zweig); gelesen
|
|
13001
13100
|
* wird mit `collectImageBatches` (`imageBatchStore.ts`).
|
|
13002
13101
|
*/
|
|
13003
|
-
imageBatchPersisted: "szenen.image_batch_persisted"
|
|
13102
|
+
imageBatchPersisted: "szenen.image_batch_persisted",
|
|
13103
|
+
/**
|
|
13104
|
+
* Ein vorbereiteter Promptvorgang — der Durchstich „Szene → ChatGPT → Lauf".
|
|
13105
|
+
*
|
|
13106
|
+
* Der bestehende Promptpfad ruft das Modell selbst
|
|
13107
|
+
* (`runPromptDerivation()`); ein aufrufendes Chatmodell formuliert dagegen
|
|
13108
|
+
* **zwischen** zwei Werkzeugaufrufen. Damit zerfaellt der eine Vorgang in
|
|
13109
|
+
* zwei, und der zweite braucht genau den Stand, den der erste eingefroren
|
|
13110
|
+
* hat — sonst loeste der Abschluss die inzwischen geaenderte Szene neu auf
|
|
13111
|
+
* und der Lauf behauptete eine Herkunft, die er nicht hat (D-22).
|
|
13112
|
+
*
|
|
13113
|
+
* Deshalb ein eigenes Ereignis und **kein** fluechtiger Prozesszustand: Der
|
|
13114
|
+
* Snapshot ueberlebt Neustart, Hostwechsel und Sitzungsende, weil er im
|
|
13115
|
+
* selben autoritativen Log liegt wie der Lauf, der daraus entsteht.
|
|
13116
|
+
*
|
|
13117
|
+
* Der Elementreducer ignoriert diesen Typ (`default:`-Zweig); gelesen wird
|
|
13118
|
+
* mit `collectPromptOperations` (`scenePromptOperation.ts`).
|
|
13119
|
+
*/
|
|
13120
|
+
promptOperationPrepared: "szenen.prompt_operation_prepared",
|
|
13121
|
+
/**
|
|
13122
|
+
* Ein Bildauftrag wartet auf einen ausfuehrenden Flow-Host.
|
|
13123
|
+
*
|
|
13124
|
+
* Die vorhandene Auftragshistorie (`imageGenerationStarted`) beginnt
|
|
13125
|
+
* **unmittelbar vor** dem Provideraufruf und kennt deshalb keinen wartenden
|
|
13126
|
+
* Auftrag. Genau der entsteht hier: Ein Chatmodell beauftragt ueber MCP ein
|
|
13127
|
+
* Bild, der Auftrag wird dauerhaft abgelegt, und der Werkzeugaufruf endet
|
|
13128
|
+
* sofort mit seiner Kennung — er haelt keine Antwort minutenlang offen.
|
|
13129
|
+
*
|
|
13130
|
+
* Traegt **keinen** Prompt: Der steht im referenzierten Lauf, und ihn hier
|
|
13131
|
+
* zu kopieren waere eine zweite Wahrheit ueber denselben eingefrorenen Text
|
|
13132
|
+
* (D-103).
|
|
13133
|
+
*/
|
|
13134
|
+
imageJobRequested: "szenen.image_job_requested",
|
|
13135
|
+
/**
|
|
13136
|
+
* Ein Flow-Host beansprucht einen wartenden Bildauftrag.
|
|
13137
|
+
*
|
|
13138
|
+
* Ein append-only Log kennt kein Lock: Zwei Tabs koennen beide anhaengen.
|
|
13139
|
+
* Deshalb ist die Uebernahme **nicht** der Append, sondern das Ergebnis
|
|
13140
|
+
* einer deterministischen Schiedsregel darueber (`winningClaimOf`) — der
|
|
13141
|
+
* erste Anspruch in Logreihenfolge gewinnt, und jeder Host liest nach
|
|
13142
|
+
* seinem Append erneut, bevor er einen bezahlten Provideraufruf startet.
|
|
13143
|
+
* Ein unterlegener Anspruch bleibt sichtbar stehen; er wird nicht geloescht,
|
|
13144
|
+
* weil das Log das nicht kann und weil der Versuch ein Beleg ist.
|
|
13145
|
+
*/
|
|
13146
|
+
imageJobClaimed: "szenen.image_job_claimed",
|
|
13147
|
+
/**
|
|
13148
|
+
* Der Bildauftrag ist erledigt — mit dem Ausgang, den der Host gesehen hat.
|
|
13149
|
+
*
|
|
13150
|
+
* Getrennt von `imageGenerationFinished`: Jenes gehoert zum bezahlten
|
|
13151
|
+
* Provideraufruf, dieses zur Zustellung. Ein Auftrag kann fehlschlagen,
|
|
13152
|
+
* **bevor** ein Provider gerufen wurde (abgelehntes Profil), und dann gibt
|
|
13153
|
+
* es kein `imageGenerationFinished`, das den Wartenden erloesen wuerde.
|
|
13154
|
+
*/
|
|
13155
|
+
imageJobSettled: "szenen.image_job_settled"
|
|
13004
13156
|
};
|
|
13005
13157
|
function sortForReplay(events) {
|
|
13006
13158
|
return [...events].sort((a, b) => {
|
|
@@ -13353,9 +13505,9 @@ async function leseBilderAusZip(datei) {
|
|
|
13353
13505
|
);
|
|
13354
13506
|
}
|
|
13355
13507
|
const { default: JSZip3 } = await import("jszip");
|
|
13356
|
-
const archiv = await JSZip3.loadAsync(datei).catch((
|
|
13508
|
+
const archiv = await JSZip3.loadAsync(datei).catch((fehler5) => {
|
|
13357
13509
|
throw new Error(
|
|
13358
|
-
`${name} konnte nicht als ZIP gelesen werden: ${
|
|
13510
|
+
`${name} konnte nicht als ZIP gelesen werden: ${fehler5 instanceof Error ? fehler5.message : String(fehler5)}`
|
|
13359
13511
|
);
|
|
13360
13512
|
});
|
|
13361
13513
|
const pfade = [];
|
|
@@ -13552,6 +13704,7 @@ var RunImagesPanel = ({ port, lauf }) => {
|
|
|
13552
13704
|
const neuLesen = () => setNachladen((n) => n + 1);
|
|
13553
13705
|
const [erzeugenOffen, setErzeugenOffen] = (0, import_react40.useState)(null);
|
|
13554
13706
|
const [zuordnenOffen, setZuordnenOffen] = (0, import_react40.useState)(null);
|
|
13707
|
+
const [chatAuftraegeOffen, setChatAuftraegeOffen] = (0, import_react40.useState)(null);
|
|
13555
13708
|
const quelle = sourceOf(lauf);
|
|
13556
13709
|
const [wahlPhase] = usePortQuery(
|
|
13557
13710
|
() => port.imageGeneration ? port.imageGeneration.imageModelChoiceForRun(lauf.run_id) : Promise.resolve({ kind: "ok", value: null }),
|
|
@@ -13566,10 +13719,17 @@ var RunImagesPanel = ({ port, lauf }) => {
|
|
|
13566
13719
|
() => port.imageBatchHistory ? port.imageBatchHistory.listBatchesForPromptRun(lauf.run_id) : Promise.resolve({ kind: "ok", value: [] }),
|
|
13567
13720
|
[port, lauf.run_id, nachladen]
|
|
13568
13721
|
);
|
|
13722
|
+
const [chatAuftraegePhase] = usePortQuery(
|
|
13723
|
+
() => port.imageJobs ? port.imageJobs.listJobs(lauf.run_id) : Promise.resolve({ kind: "ok", value: [] }),
|
|
13724
|
+
[port, lauf.run_id, nachladen]
|
|
13725
|
+
);
|
|
13569
13726
|
const auftraege = auftraegePhase.status === "ready" ? auftraegePhase.value : [];
|
|
13570
13727
|
const batches = batchesPhase.status === "ready" ? batchesPhase.value : [];
|
|
13728
|
+
const chatAuftraege = chatAuftraegePhase.status === "ready" ? chatAuftraegePhase.value : [];
|
|
13729
|
+
const offeneChatAuftraege = chatAuftraege.filter((a) => a.status === "pending").length;
|
|
13571
13730
|
const auffaellige = auftraege.filter((a) => a.status !== "succeeded");
|
|
13572
13731
|
const erzeugenAuf = erzeugenOffen ?? batches.length === 0;
|
|
13732
|
+
const chatAuftraegeAuf = chatAuftraegeOffen ?? offeneChatAuftraege > 0;
|
|
13573
13733
|
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "szenen-bilder", "data-testid": `bilder-${lauf.run_id}`, children: [
|
|
13574
13734
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("section", { className: "szenen-bilder__eingang", "data-testid": `eingang-${lauf.run_id}`, children: [
|
|
13575
13735
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("h5", { children: "Eingang \u2014 woraus dieser Prompt entstand" }),
|
|
@@ -13690,6 +13850,43 @@ var RunImagesPanel = ({ port, lauf }) => {
|
|
|
13690
13850
|
}
|
|
13691
13851
|
)
|
|
13692
13852
|
] }),
|
|
13853
|
+
faehig.imageJobs && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
|
|
13854
|
+
"details",
|
|
13855
|
+
{
|
|
13856
|
+
className: "szenen-bilder__aktion",
|
|
13857
|
+
"data-testid": `auftraege-chat-bereich-${lauf.run_id}`,
|
|
13858
|
+
open: chatAuftraegeAuf,
|
|
13859
|
+
children: [
|
|
13860
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
|
|
13861
|
+
"summary",
|
|
13862
|
+
{
|
|
13863
|
+
onClick: (e) => {
|
|
13864
|
+
e.preventDefault();
|
|
13865
|
+
setChatAuftraegeOffen(!chatAuftraegeAuf);
|
|
13866
|
+
},
|
|
13867
|
+
children: [
|
|
13868
|
+
"Bildauftraege aus dem Chat",
|
|
13869
|
+
offeneChatAuftraege > 0 ? ` (${offeneChatAuftraege} wartend)` : ""
|
|
13870
|
+
]
|
|
13871
|
+
}
|
|
13872
|
+
),
|
|
13873
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
13874
|
+
ChatAuftraegeAktion,
|
|
13875
|
+
{
|
|
13876
|
+
port,
|
|
13877
|
+
lauf,
|
|
13878
|
+
auftraege: chatAuftraege,
|
|
13879
|
+
fehler: chatAuftraegePhase.status === "error" ? chatAuftraegePhase.message : null,
|
|
13880
|
+
laedt: chatAuftraegePhase.status === "loading",
|
|
13881
|
+
onFertig: () => {
|
|
13882
|
+
setChatAuftraegeOffen(true);
|
|
13883
|
+
neuLesen();
|
|
13884
|
+
}
|
|
13885
|
+
}
|
|
13886
|
+
)
|
|
13887
|
+
]
|
|
13888
|
+
}
|
|
13889
|
+
),
|
|
13693
13890
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
|
|
13694
13891
|
"details",
|
|
13695
13892
|
{
|
|
@@ -13873,7 +14070,7 @@ function useBildQuellen(port, bilder) {
|
|
|
13873
14070
|
erneutVersuchen: () => setVersuch((n) => n + 1)
|
|
13874
14071
|
};
|
|
13875
14072
|
}
|
|
13876
|
-
var BildDetails = ({ bild, fehler:
|
|
14073
|
+
var BildDetails = ({ bild, fehler: fehler5, aufloesbar }) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "szenen-bilder__meta", "data-testid": `batchbild-${bild.media_ref}`, children: [
|
|
13877
14074
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: bild.media_ref }),
|
|
13878
14075
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
|
|
13879
14076
|
"Position ",
|
|
@@ -13886,7 +14083,7 @@ var BildDetails = ({ bild, fehler: fehler4, aufloesbar }) => /* @__PURE__ */ (0,
|
|
|
13886
14083
|
"\xD7",
|
|
13887
14084
|
bild.height
|
|
13888
14085
|
] }) : null,
|
|
13889
|
-
|
|
14086
|
+
fehler5 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("em", { "data-testid": `batchbild-fehler-${bild.media_ref}`, children: fehler5 }),
|
|
13890
14087
|
!aufloesbar && // Capability-Grenze, kein Fehler: Dieser Host kann Batches lesen,
|
|
13891
14088
|
// aber keine Bilder auflösen (E). Kein erfundener Fallback auf eine
|
|
13892
14089
|
// Serverroute (A6).
|
|
@@ -13902,18 +14099,18 @@ var HERKUNFT_KURZ = {
|
|
|
13902
14099
|
external_import: "extern importiert"
|
|
13903
14100
|
};
|
|
13904
14101
|
var BatchKachel = ({ port, batch }) => {
|
|
13905
|
-
const { quellen, fehler:
|
|
14102
|
+
const { quellen, fehler: fehler5, wiederholbar, erneutVersuchen } = useBildQuellen(port, batch.images);
|
|
13906
14103
|
const aufloesbar = Boolean(port.imageLibrary);
|
|
13907
14104
|
const bilder = batch.images.map((bild) => ({
|
|
13908
14105
|
id: bild.media_ref,
|
|
13909
14106
|
quelle: quellen.get(bild.media_ref) ?? null,
|
|
13910
14107
|
titel: `Bild ${bild.position + 1}`,
|
|
13911
|
-
...
|
|
14108
|
+
...fehler5.get(bild.media_ref) ? { leerText: "Nicht ladbar" } : {},
|
|
13912
14109
|
details: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
13913
14110
|
BildDetails,
|
|
13914
14111
|
{
|
|
13915
14112
|
bild,
|
|
13916
|
-
fehler:
|
|
14113
|
+
fehler: fehler5.get(bild.media_ref) ?? null,
|
|
13917
14114
|
aufloesbar
|
|
13918
14115
|
}
|
|
13919
14116
|
)
|
|
@@ -13973,7 +14170,7 @@ var BatchKachel = ({ port, batch }) => {
|
|
|
13973
14170
|
BildDetails,
|
|
13974
14171
|
{
|
|
13975
14172
|
bild,
|
|
13976
|
-
fehler:
|
|
14173
|
+
fehler: fehler5.get(bild.media_ref) ?? null,
|
|
13977
14174
|
aufloesbar
|
|
13978
14175
|
}
|
|
13979
14176
|
) }, bild.media_ref)) })
|
|
@@ -14242,11 +14439,11 @@ var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
|
14242
14439
|
setSeedListe(DEFAULT_SEED_VORLAGE.seeds);
|
|
14243
14440
|
setVorlageKey(DEFAULT_VORLAGE_KEY);
|
|
14244
14441
|
}
|
|
14245
|
-
} catch (
|
|
14442
|
+
} catch (fehler5) {
|
|
14246
14443
|
if (veraltet()) return;
|
|
14247
14444
|
setNotiz({
|
|
14248
14445
|
tone: "error",
|
|
14249
|
-
text:
|
|
14446
|
+
text: fehler5 instanceof Error ? fehler5.message : String(fehler5)
|
|
14250
14447
|
});
|
|
14251
14448
|
}
|
|
14252
14449
|
if (veraltet()) return;
|
|
@@ -14265,10 +14462,10 @@ var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
|
14265
14462
|
setNotiz(
|
|
14266
14463
|
ergebnis.kind === "ok" ? { tone: "ok", text: `Importiert als Item ${ergebnis.value.item_id} mit ${bilder.length} Bild(ern).` } : noticeFor(ergebnis, "Externe Bilder importieren")
|
|
14267
14464
|
);
|
|
14268
|
-
} catch (
|
|
14465
|
+
} catch (fehler5) {
|
|
14269
14466
|
setNotiz({
|
|
14270
14467
|
tone: "error",
|
|
14271
|
-
text: `Externe Bilder importieren: ${
|
|
14468
|
+
text: `Externe Bilder importieren: ${fehler5 instanceof Error ? fehler5.message : String(fehler5)}`
|
|
14272
14469
|
});
|
|
14273
14470
|
}
|
|
14274
14471
|
setLaeuft(false);
|
|
@@ -14290,10 +14487,10 @@ var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
|
14290
14487
|
...seed !== void 0 ? { seed } : {}
|
|
14291
14488
|
});
|
|
14292
14489
|
}
|
|
14293
|
-
} catch (
|
|
14490
|
+
} catch (fehler5) {
|
|
14294
14491
|
setNotiz({
|
|
14295
14492
|
tone: "error",
|
|
14296
|
-
text: `Externe Bilder importieren: ${
|
|
14493
|
+
text: `Externe Bilder importieren: ${fehler5 instanceof Error ? fehler5.message : String(fehler5)}`
|
|
14297
14494
|
});
|
|
14298
14495
|
return;
|
|
14299
14496
|
}
|
|
@@ -14629,6 +14826,101 @@ function readAsBase64(datei) {
|
|
|
14629
14826
|
leser.readAsDataURL(datei);
|
|
14630
14827
|
});
|
|
14631
14828
|
}
|
|
14829
|
+
var CHAT_AUFTRAG_TEXT = {
|
|
14830
|
+
pending: "wartet auf \xDCbernahme",
|
|
14831
|
+
// Wie bei `started` (D-119): laufend und abgebrochen sind aus dem Log
|
|
14832
|
+
// nicht unterscheidbar, und es gibt keinen automatischen Retry, der die
|
|
14833
|
+
// Unterscheidung bräuchte.
|
|
14834
|
+
claimed: "\xFCbernommen (l\xE4uft oder unterbrochen)",
|
|
14835
|
+
succeeded: "erfolgreich",
|
|
14836
|
+
partial: "teilweise",
|
|
14837
|
+
failed: "fehlgeschlagen",
|
|
14838
|
+
rejected: "abgelehnt \u2014 es wurde nichts ausgef\xFChrt"
|
|
14839
|
+
};
|
|
14840
|
+
var ChatAuftraegeAktion = ({
|
|
14841
|
+
port,
|
|
14842
|
+
lauf,
|
|
14843
|
+
auftraege,
|
|
14844
|
+
fehler: fehler5,
|
|
14845
|
+
laedt,
|
|
14846
|
+
onFertig
|
|
14847
|
+
}) => {
|
|
14848
|
+
const [laufend, setLaufend] = (0, import_react40.useState)(null);
|
|
14849
|
+
const [notiz, setNotiz] = (0, import_react40.useState)(null);
|
|
14850
|
+
async function uebernehmen(jobId) {
|
|
14851
|
+
if (!port.imageJobs) return;
|
|
14852
|
+
setLaufend(jobId);
|
|
14853
|
+
setNotiz(null);
|
|
14854
|
+
try {
|
|
14855
|
+
const ergebnis = await port.imageJobs.claimAndRun(jobId);
|
|
14856
|
+
if (ergebnis.kind === "ok") {
|
|
14857
|
+
setNotiz({
|
|
14858
|
+
tone: ergebnis.job.status === "succeeded" ? "ok" : "rejected",
|
|
14859
|
+
text: `Auftrag ${jobId}: ${CHAT_AUFTRAG_TEXT[ergebnis.job.status]}.` + (ergebnis.job.diagnostic ? ` ${ergebnis.job.diagnostic}` : "")
|
|
14860
|
+
});
|
|
14861
|
+
} else if (ergebnis.kind === "lost") {
|
|
14862
|
+
setNotiz({ tone: "rejected", text: ergebnis.message });
|
|
14863
|
+
} else {
|
|
14864
|
+
setNotiz({ tone: "error", text: ergebnis.message });
|
|
14865
|
+
}
|
|
14866
|
+
} catch (e) {
|
|
14867
|
+
setNotiz({
|
|
14868
|
+
tone: "error",
|
|
14869
|
+
text: `Die \xDCbernahme von Auftrag ${jobId} ist technisch gescheitert: ` + (e instanceof Error ? e.message : String(e))
|
|
14870
|
+
});
|
|
14871
|
+
} finally {
|
|
14872
|
+
setLaufend(null);
|
|
14873
|
+
onFertig();
|
|
14874
|
+
}
|
|
14875
|
+
}
|
|
14876
|
+
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "szenen-bilder__aktion-inhalt", "data-testid": `chat-auftraege-${lauf.run_id}`, children: [
|
|
14877
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "szenen-area__lead", children: "Auftr\xE4ge, die \xFCber den MCP aus dem Chat kamen. Der Prompt wird nicht kopiert \u2014 ausgef\xFChrt wird der eingefrorene Prompt dieses Laufs." }),
|
|
14878
|
+
fehler5 && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { role: "alert", "data-testid": `chat-auftraege-fehler-${lauf.run_id}`, children: [
|
|
14879
|
+
"Bildauftr\xE4ge aus dem Chat konnten nicht gelesen werden: ",
|
|
14880
|
+
fehler5
|
|
14881
|
+
] }),
|
|
14882
|
+
!fehler5 && !laedt && auftraege.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { "data-testid": `chat-auftraege-leer-${lauf.run_id}`, children: "F\xFCr diesen Lauf liegt kein Auftrag aus dem Chat vor." }),
|
|
14883
|
+
notiz && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14884
|
+
"p",
|
|
14885
|
+
{
|
|
14886
|
+
role: notiz.tone === "error" ? "alert" : void 0,
|
|
14887
|
+
"data-testid": `chat-auftraege-notiz-${lauf.run_id}`,
|
|
14888
|
+
children: notiz.text
|
|
14889
|
+
}
|
|
14890
|
+
),
|
|
14891
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "szenen-bilder__auftraege", children: auftraege.map((auftrag) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { "data-testid": `chat-auftrag-${auftrag.job_id}`, children: [
|
|
14892
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { children: CHAT_AUFTRAG_TEXT[auftrag.status] }),
|
|
14893
|
+
" \u2014 ",
|
|
14894
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
|
|
14895
|
+
auftrag.image_count,
|
|
14896
|
+
" ",
|
|
14897
|
+
auftrag.image_count === 1 ? "Bild" : "Bilder",
|
|
14898
|
+
" mit",
|
|
14899
|
+
" ",
|
|
14900
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: auftrag.execution_target_model_key }),
|
|
14901
|
+
" / ",
|
|
14902
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: auftrag.execution_profile_id }),
|
|
14903
|
+
auftrag.aspect_ratio ? ` im Format ${auftrag.aspect_ratio}` : ""
|
|
14904
|
+
] }),
|
|
14905
|
+
auftrag.diagnostic && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
|
|
14906
|
+
" ",
|
|
14907
|
+
auftrag.diagnostic
|
|
14908
|
+
] }),
|
|
14909
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("br", {}),
|
|
14910
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { className: "szenen-bilder__auftragskennung", children: auftrag.job_id }),
|
|
14911
|
+
auftrag.status === "pending" && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14912
|
+
"button",
|
|
14913
|
+
{
|
|
14914
|
+
type: "button",
|
|
14915
|
+
disabled: laufend !== null,
|
|
14916
|
+
onClick: () => void uebernehmen(auftrag.job_id),
|
|
14917
|
+
"data-testid": `chat-auftrag-uebernehmen-${auftrag.job_id}`,
|
|
14918
|
+
children: laufend === auftrag.job_id ? "\xDCbernahme l\xE4uft\u2026" : "\xDCbernehmen und ausf\xFChren (kostet einen Bildaufruf)"
|
|
14919
|
+
}
|
|
14920
|
+
)
|
|
14921
|
+
] }, auftrag.job_id)) })
|
|
14922
|
+
] });
|
|
14923
|
+
};
|
|
14632
14924
|
|
|
14633
14925
|
// src/features/szenen/areas/RunsPanel.tsx
|
|
14634
14926
|
var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
@@ -19184,12 +19476,21 @@ function createHfElementStore(options) {
|
|
|
19184
19476
|
|
|
19185
19477
|
// src/lib/szenen/hfEventChannel.ts
|
|
19186
19478
|
function createHfEventChannel(options) {
|
|
19187
|
-
const { namespace, token, sinceTs = 0 } = options;
|
|
19479
|
+
const { namespace, token, sinceTs = 0, ohneSnapshot = false } = options;
|
|
19188
19480
|
const knownPaths = /* @__PURE__ */ new Set();
|
|
19189
19481
|
let cachedRawEvents = [];
|
|
19190
19482
|
let syncInFlight = null;
|
|
19191
|
-
|
|
19192
|
-
|
|
19483
|
+
let snapshot = null;
|
|
19484
|
+
let snapshotGeladen = ohneSnapshot;
|
|
19485
|
+
const refresh = async (revision = "main") => {
|
|
19486
|
+
if (!snapshotGeladen) {
|
|
19487
|
+
snapshot = await loadLatestSzenenSnapshot(namespace, token);
|
|
19488
|
+
snapshotGeladen = true;
|
|
19489
|
+
for (const name of aufgenommeneDateien(snapshot)) {
|
|
19490
|
+
knownPaths.add(`${namespace}events/${name}`);
|
|
19491
|
+
}
|
|
19492
|
+
}
|
|
19493
|
+
const neue = await loadPendingEvents(namespace, token, sinceTs, knownPaths, revision);
|
|
19193
19494
|
if (neue.length) cachedRawEvents = [...cachedRawEvents, ...neue];
|
|
19194
19495
|
};
|
|
19195
19496
|
const sync = () => {
|
|
@@ -19200,15 +19501,34 @@ function createHfEventChannel(options) {
|
|
|
19200
19501
|
}
|
|
19201
19502
|
return syncInFlight;
|
|
19202
19503
|
};
|
|
19504
|
+
const sicht = () => {
|
|
19505
|
+
const pending = cachedRawEvents.filter((event) => typeof event.type === "string" && event.type.startsWith(SZENEN_EVENT_PREFIX)).map((event) => ({
|
|
19506
|
+
type: event.type,
|
|
19507
|
+
payload: event.payload,
|
|
19508
|
+
clientId: event.clientId,
|
|
19509
|
+
ts: event.ts
|
|
19510
|
+
}));
|
|
19511
|
+
return mergeSnapshotAndPending(snapshot, pending).events;
|
|
19512
|
+
};
|
|
19203
19513
|
return {
|
|
19204
19514
|
async readEvents() {
|
|
19205
19515
|
await sync();
|
|
19206
|
-
return
|
|
19207
|
-
|
|
19208
|
-
|
|
19209
|
-
|
|
19210
|
-
|
|
19211
|
-
|
|
19516
|
+
return sicht();
|
|
19517
|
+
},
|
|
19518
|
+
async readHead() {
|
|
19519
|
+
return hfRepoHead(token);
|
|
19520
|
+
},
|
|
19521
|
+
async readEventsAt(revision) {
|
|
19522
|
+
await refresh(revision);
|
|
19523
|
+
return sicht();
|
|
19524
|
+
},
|
|
19525
|
+
async appendEventAt(type, payload, parentCommit) {
|
|
19526
|
+
if (!type.startsWith(SZENEN_EVENT_PREFIX)) {
|
|
19527
|
+
throw new Error(
|
|
19528
|
+
`Event-Typ "${type}" gehoert nicht zur Domain szenen. Der geteilte Kanal wird ueber das Praefix "${SZENEN_EVENT_PREFIX}" getrennt.`
|
|
19529
|
+
);
|
|
19530
|
+
}
|
|
19531
|
+
await writeHFEvent(namespace, token, type, payload, [], { ordered: true, parentCommit });
|
|
19212
19532
|
},
|
|
19213
19533
|
async appendEvent(type, payload) {
|
|
19214
19534
|
if (!type.startsWith(SZENEN_EVENT_PREFIX)) {
|
|
@@ -19392,11 +19712,11 @@ async function resolveShotPayload(input) {
|
|
|
19392
19712
|
};
|
|
19393
19713
|
}
|
|
19394
19714
|
const shot = shotPayload;
|
|
19395
|
-
for (const
|
|
19715
|
+
for (const fehler5 of ohneKettenReferenz(
|
|
19396
19716
|
validatePayload("shot", shotPayload).errors,
|
|
19397
19717
|
chain ? "scene_setup_ref" : null
|
|
19398
19718
|
)) {
|
|
19399
|
-
diagnostics.push(diagnosticOfSchemaError(
|
|
19719
|
+
diagnostics.push(diagnosticOfSchemaError(fehler5, `Shot ${shotElementId}`));
|
|
19400
19720
|
}
|
|
19401
19721
|
let setup = null;
|
|
19402
19722
|
let report = null;
|
|
@@ -19508,8 +19828,8 @@ async function resolveScenePayload(input) {
|
|
|
19508
19828
|
"Kein Scene Setup, kein Shot: Der Lauf entsteht aus der Szene allein. Bildaufbau, Kamera und Zielmodell wurden nicht ergaenzt, sondern sind nicht vorhanden (D-135)."
|
|
19509
19829
|
];
|
|
19510
19830
|
const diagnostics = [];
|
|
19511
|
-
for (const
|
|
19512
|
-
diagnostics.push(diagnosticOfSchemaError(
|
|
19831
|
+
for (const fehler5 of validatePayload("scene", scenePayload).errors) {
|
|
19832
|
+
diagnostics.push(diagnosticOfSchemaError(fehler5, `Szene ${sceneElementId}`));
|
|
19513
19833
|
}
|
|
19514
19834
|
const faehig = sceneOnlyCapabilities(scenePayload);
|
|
19515
19835
|
return komponiere({
|
|
@@ -21058,8 +21378,8 @@ function createFlowImageProvider(generateImage) {
|
|
|
21058
21378
|
// Kein Seed: weder als Eingabe dokumentiert noch als Ausgabe
|
|
21059
21379
|
// geliefert — nichts wird erfunden (D-107).
|
|
21060
21380
|
});
|
|
21061
|
-
} catch (
|
|
21062
|
-
ersterFehler =
|
|
21381
|
+
} catch (fehler5) {
|
|
21382
|
+
ersterFehler = fehler5 instanceof Error ? fehler5.message : String(fehler5);
|
|
21063
21383
|
break;
|
|
21064
21384
|
}
|
|
21065
21385
|
}
|
|
@@ -21585,6 +21905,249 @@ function createImageGenerationPort(options) {
|
|
|
21585
21905
|
};
|
|
21586
21906
|
}
|
|
21587
21907
|
|
|
21908
|
+
// src/lib/szenen/imageJobStore.ts
|
|
21909
|
+
var istText6 = (wert) => typeof wert === "string" && wert.trim().length > 0;
|
|
21910
|
+
var SETTLED_STATUS = ["succeeded", "partial", "failed", "rejected"];
|
|
21911
|
+
function collectImageJobs(events) {
|
|
21912
|
+
const nachId = /* @__PURE__ */ new Map();
|
|
21913
|
+
for (const event of events) {
|
|
21914
|
+
if (event.type !== SZENEN_EVENT_TYPES.imageJobRequested) continue;
|
|
21915
|
+
const payload = event.payload;
|
|
21916
|
+
if (!payload || !istText6(payload.job_id) || !istText6(payload.prompt_run_id)) continue;
|
|
21917
|
+
if (nachId.has(payload.job_id)) continue;
|
|
21918
|
+
nachId.set(payload.job_id, {
|
|
21919
|
+
job_id: payload.job_id,
|
|
21920
|
+
prompt_run_id: payload.prompt_run_id,
|
|
21921
|
+
execution_target_model_key: payload.execution_target_model_key ?? "",
|
|
21922
|
+
execution_profile_id: payload.execution_profile_id ?? "",
|
|
21923
|
+
image_count: typeof payload.image_count === "number" ? payload.image_count : 0,
|
|
21924
|
+
...payload.aspect_ratio ? { aspect_ratio: payload.aspect_ratio } : {},
|
|
21925
|
+
requested_by: payload.requested_by ?? "",
|
|
21926
|
+
store_id: payload.store_id ?? "",
|
|
21927
|
+
created_at: payload.created_at ?? "",
|
|
21928
|
+
status: "pending",
|
|
21929
|
+
claim_attempts: []
|
|
21930
|
+
});
|
|
21931
|
+
}
|
|
21932
|
+
for (const event of events) {
|
|
21933
|
+
if (event.type !== SZENEN_EVENT_TYPES.imageJobClaimed) continue;
|
|
21934
|
+
const payload = event.payload;
|
|
21935
|
+
if (!payload || !istText6(payload.job_id) || !istText6(payload.claim_id)) continue;
|
|
21936
|
+
const auftrag = nachId.get(payload.job_id);
|
|
21937
|
+
if (!auftrag) continue;
|
|
21938
|
+
const anspruch = {
|
|
21939
|
+
job_id: payload.job_id,
|
|
21940
|
+
claim_id: payload.claim_id,
|
|
21941
|
+
client_id: payload.client_id ?? "",
|
|
21942
|
+
claimed_at: payload.claimed_at ?? ""
|
|
21943
|
+
};
|
|
21944
|
+
nachId.set(payload.job_id, {
|
|
21945
|
+
...auftrag,
|
|
21946
|
+
claim: auftrag.claim ?? anspruch,
|
|
21947
|
+
claim_attempts: [...auftrag.claim_attempts, anspruch],
|
|
21948
|
+
status: auftrag.status === "pending" ? "claimed" : auftrag.status
|
|
21949
|
+
});
|
|
21950
|
+
}
|
|
21951
|
+
for (const event of events) {
|
|
21952
|
+
if (event.type !== SZENEN_EVENT_TYPES.imageJobSettled) continue;
|
|
21953
|
+
const payload = event.payload;
|
|
21954
|
+
if (!payload || !istText6(payload.job_id)) continue;
|
|
21955
|
+
const auftrag = nachId.get(payload.job_id);
|
|
21956
|
+
if (!auftrag) continue;
|
|
21957
|
+
if (!auftrag.claim || auftrag.claim.claim_id !== payload.claim_id) continue;
|
|
21958
|
+
if (auftrag.status !== "claimed") continue;
|
|
21959
|
+
if (!SETTLED_STATUS.includes(payload.status)) continue;
|
|
21960
|
+
nachId.set(payload.job_id, {
|
|
21961
|
+
...auftrag,
|
|
21962
|
+
status: payload.status,
|
|
21963
|
+
...payload.generation_batch_id ? { generation_batch_id: payload.generation_batch_id } : {},
|
|
21964
|
+
...payload.item_id ? { item_id: payload.item_id } : {},
|
|
21965
|
+
...typeof payload.produced_count === "number" ? { produced_count: payload.produced_count } : {},
|
|
21966
|
+
...payload.diagnostic ? { diagnostic: payload.diagnostic } : {},
|
|
21967
|
+
settled_at: payload.settled_at ?? ""
|
|
21968
|
+
});
|
|
21969
|
+
}
|
|
21970
|
+
return [...nachId.values()];
|
|
21971
|
+
}
|
|
21972
|
+
function jobsForPromptRun(events, runId) {
|
|
21973
|
+
return collectImageJobs(events).filter((j) => j.prompt_run_id === runId);
|
|
21974
|
+
}
|
|
21975
|
+
function findImageJob(events, jobId) {
|
|
21976
|
+
return collectImageJobs(events).find((j) => j.job_id === jobId) ?? null;
|
|
21977
|
+
}
|
|
21978
|
+
function generationBatchIdOf(jobId) {
|
|
21979
|
+
return `job-${jobId}`;
|
|
21980
|
+
}
|
|
21981
|
+
|
|
21982
|
+
// src/lib/szenen/imageJobPort.ts
|
|
21983
|
+
var ANSPRUCH_VERSUCHE = 4;
|
|
21984
|
+
function fehler4(error, aktion) {
|
|
21985
|
+
return {
|
|
21986
|
+
message: `${aktion}: ${error instanceof Error ? error.message : String(error)}`
|
|
21987
|
+
};
|
|
21988
|
+
}
|
|
21989
|
+
function createImageJobPort(options) {
|
|
21990
|
+
const { channel, imageGeneration, storeId, clientId } = options;
|
|
21991
|
+
const newClaimId = options.newClaimId ?? (() => crypto.randomUUID());
|
|
21992
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
21993
|
+
return {
|
|
21994
|
+
async listJobs(runId) {
|
|
21995
|
+
try {
|
|
21996
|
+
return { kind: "ok", value: jobsForPromptRun(await channel.readEvents(), runId) };
|
|
21997
|
+
} catch (error) {
|
|
21998
|
+
return {
|
|
21999
|
+
kind: "error",
|
|
22000
|
+
...fehler4(error, `Die Bildauftraege des Laufs ${runId} konnten nicht gelesen werden`),
|
|
22001
|
+
retryable: true
|
|
22002
|
+
};
|
|
22003
|
+
}
|
|
22004
|
+
},
|
|
22005
|
+
async claimAndRun(jobId) {
|
|
22006
|
+
let letzterGrund = "";
|
|
22007
|
+
for (let versuch = 1; versuch <= ANSPRUCH_VERSUCHE; versuch += 1) {
|
|
22008
|
+
let kopf;
|
|
22009
|
+
try {
|
|
22010
|
+
kopf = await channel.readHead();
|
|
22011
|
+
} catch (error) {
|
|
22012
|
+
return {
|
|
22013
|
+
kind: "error",
|
|
22014
|
+
...fehler4(error, `Der Kopf-Commit des Bestands war nicht lesbar; Bildauftrag ${jobId} wurde nicht uebernommen`),
|
|
22015
|
+
retryable: true
|
|
22016
|
+
};
|
|
22017
|
+
}
|
|
22018
|
+
let auftrag;
|
|
22019
|
+
try {
|
|
22020
|
+
auftrag = findImageJob(await channel.readEventsAt(kopf), jobId);
|
|
22021
|
+
} catch (error) {
|
|
22022
|
+
return {
|
|
22023
|
+
kind: "error",
|
|
22024
|
+
...fehler4(error, `Der Bildauftrag ${jobId} konnte nicht gelesen werden`),
|
|
22025
|
+
retryable: true
|
|
22026
|
+
};
|
|
22027
|
+
}
|
|
22028
|
+
if (!auftrag) {
|
|
22029
|
+
return {
|
|
22030
|
+
kind: "rejected",
|
|
22031
|
+
message: `Der Bildauftrag ${jobId} steht nicht im autoritativen Log dieses Bestands.`
|
|
22032
|
+
};
|
|
22033
|
+
}
|
|
22034
|
+
if (auftrag.store_id !== storeId) {
|
|
22035
|
+
return {
|
|
22036
|
+
kind: "rejected",
|
|
22037
|
+
message: `Der Bildauftrag ${jobId} gilt fuer den Bestand "${auftrag.store_id}", dieser Host arbeitet auf "${storeId}". Nutzer, Umgebung und Bestand muessen zusammenpassen.`
|
|
22038
|
+
};
|
|
22039
|
+
}
|
|
22040
|
+
if (auftrag.status !== "pending") {
|
|
22041
|
+
return {
|
|
22042
|
+
kind: "lost",
|
|
22043
|
+
job: auftrag,
|
|
22044
|
+
message: `Der Bildauftrag ${jobId} ist bereits "${auftrag.status}"` + (auftrag.claim ? ` (Client ${auftrag.claim.client_id})` : "") + ". Er wird nicht erneut ausgefuehrt \u2014 ein zweiter Lauf waere ein zweites Mal bezahlte Rechenzeit."
|
|
22045
|
+
};
|
|
22046
|
+
}
|
|
22047
|
+
const anspruch = {
|
|
22048
|
+
job_id: jobId,
|
|
22049
|
+
claim_id: newClaimId(),
|
|
22050
|
+
client_id: clientId,
|
|
22051
|
+
claimed_at: now().toISOString()
|
|
22052
|
+
};
|
|
22053
|
+
try {
|
|
22054
|
+
await channel.appendEventAt(
|
|
22055
|
+
SZENEN_EVENT_TYPES.imageJobClaimed,
|
|
22056
|
+
anspruch,
|
|
22057
|
+
kopf
|
|
22058
|
+
);
|
|
22059
|
+
} catch (error) {
|
|
22060
|
+
if (istPreconditionFehler(error)) {
|
|
22061
|
+
letzterGrund = error instanceof Error ? error.message : String(error);
|
|
22062
|
+
continue;
|
|
22063
|
+
}
|
|
22064
|
+
return {
|
|
22065
|
+
kind: "error",
|
|
22066
|
+
...fehler4(error, `Der Anspruch auf Bildauftrag ${jobId} konnte nicht angehaengt werden`),
|
|
22067
|
+
retryable: true
|
|
22068
|
+
};
|
|
22069
|
+
}
|
|
22070
|
+
const gewonnen = {
|
|
22071
|
+
...auftrag,
|
|
22072
|
+
status: "claimed",
|
|
22073
|
+
claim: anspruch,
|
|
22074
|
+
claim_attempts: [...auftrag.claim_attempts, anspruch]
|
|
22075
|
+
};
|
|
22076
|
+
return await fuehreAus(gewonnen, anspruch, jobId);
|
|
22077
|
+
}
|
|
22078
|
+
return {
|
|
22079
|
+
kind: "error",
|
|
22080
|
+
message: `Der Bildauftrag ${jobId} liess sich in ${ANSPRUCH_VERSUCHE} Versuchen nicht exklusiv uebernehmen \u2014 der Bestand wird gerade von anderer Seite beschrieben (${letzterGrund}). Es wurde nichts ausgefuehrt und nichts bezahlt; ein neuer Versuch ist gefahrlos.`,
|
|
22081
|
+
retryable: true
|
|
22082
|
+
};
|
|
22083
|
+
}
|
|
22084
|
+
};
|
|
22085
|
+
async function fuehreAus(gewonnen, anspruch, jobId) {
|
|
22086
|
+
const ergebnis = await imageGeneration.startFromPromptRun({
|
|
22087
|
+
runId: gewonnen.prompt_run_id,
|
|
22088
|
+
generationBatchId: generationBatchIdOf(jobId),
|
|
22089
|
+
executionTargetModelKey: gewonnen.execution_target_model_key,
|
|
22090
|
+
executionProfileId: gewonnen.execution_profile_id,
|
|
22091
|
+
imageCount: gewonnen.image_count,
|
|
22092
|
+
...gewonnen.aspect_ratio ? { aspectRatio: gewonnen.aspect_ratio } : {}
|
|
22093
|
+
});
|
|
22094
|
+
const abschluss = {
|
|
22095
|
+
job_id: jobId,
|
|
22096
|
+
claim_id: anspruch.claim_id,
|
|
22097
|
+
...abschlussFelder(ergebnis, jobId),
|
|
22098
|
+
settled_at: now().toISOString()
|
|
22099
|
+
};
|
|
22100
|
+
try {
|
|
22101
|
+
await channel.appendEvent(SZENEN_EVENT_TYPES.imageJobSettled, abschluss);
|
|
22102
|
+
} catch {
|
|
22103
|
+
}
|
|
22104
|
+
const gelesen = await channel.readEvents().catch(() => null);
|
|
22105
|
+
return {
|
|
22106
|
+
kind: "ok",
|
|
22107
|
+
job: (gelesen && findImageJob(gelesen, jobId)) ?? {
|
|
22108
|
+
...gewonnen,
|
|
22109
|
+
status: abschluss.status,
|
|
22110
|
+
...abschluss.generation_batch_id ? { generation_batch_id: abschluss.generation_batch_id } : {},
|
|
22111
|
+
...abschluss.diagnostic ? { diagnostic: abschluss.diagnostic } : {},
|
|
22112
|
+
settled_at: abschluss.settled_at
|
|
22113
|
+
}
|
|
22114
|
+
};
|
|
22115
|
+
}
|
|
22116
|
+
}
|
|
22117
|
+
function istPreconditionFehler(error) {
|
|
22118
|
+
return Boolean(error && typeof error === "object" && error.name === "HfPreconditionFailed");
|
|
22119
|
+
}
|
|
22120
|
+
function abschlussFelder(ergebnis, jobId) {
|
|
22121
|
+
const batchId = generationBatchIdOf(jobId);
|
|
22122
|
+
switch (ergebnis.kind) {
|
|
22123
|
+
case "succeeded":
|
|
22124
|
+
return {
|
|
22125
|
+
status: "succeeded",
|
|
22126
|
+
generation_batch_id: batchId,
|
|
22127
|
+
item_id: ergebnis.batch.item_id,
|
|
22128
|
+
produced_count: ergebnis.batch.images.length
|
|
22129
|
+
};
|
|
22130
|
+
case "partial":
|
|
22131
|
+
return {
|
|
22132
|
+
status: "partial",
|
|
22133
|
+
generation_batch_id: batchId,
|
|
22134
|
+
item_id: ergebnis.batch.item_id,
|
|
22135
|
+
produced_count: ergebnis.produced,
|
|
22136
|
+
diagnostic: `Angefordert ${ergebnis.requested}, geliefert ${ergebnis.produced}.`
|
|
22137
|
+
};
|
|
22138
|
+
case "failed":
|
|
22139
|
+
return { status: "failed", produced_count: 0, diagnostic: ergebnis.message };
|
|
22140
|
+
case "unsupported":
|
|
22141
|
+
return {
|
|
22142
|
+
status: "rejected",
|
|
22143
|
+
produced_count: 0,
|
|
22144
|
+
diagnostic: `Zielmodell "${ergebnis.targetModelKey}" ist in dieser Umgebung nicht ausfuehrbar: ${ergebnis.reason}`
|
|
22145
|
+
};
|
|
22146
|
+
default:
|
|
22147
|
+
return { status: "rejected", produced_count: 0, diagnostic: ergebnis.message };
|
|
22148
|
+
}
|
|
22149
|
+
}
|
|
22150
|
+
|
|
21588
22151
|
// src/lib/szenen/flowViewPort.ts
|
|
21589
22152
|
var defaultContext = {
|
|
21590
22153
|
async chainFor(contextId) {
|
|
@@ -21683,10 +22246,21 @@ function createFlowSzenenViewPort(options) {
|
|
|
21683
22246
|
},
|
|
21684
22247
|
...options.now ? { now: options.now } : {}
|
|
21685
22248
|
}) : void 0;
|
|
22249
|
+
const kanalMitRevision = typeof channel.readHead === "function" && typeof channel.readEventsAt === "function" && typeof channel.appendEventAt === "function";
|
|
22250
|
+
const imageJobs = imageGeneration && hatHfZugangsdaten && kanalMitRevision ? createImageJobPort({
|
|
22251
|
+
channel,
|
|
22252
|
+
imageGeneration,
|
|
22253
|
+
// Dieselbe Form, in der der Server seinen Bestand kennt (`storeId`).
|
|
22254
|
+
storeId: `hf:${options.namespace}`,
|
|
22255
|
+
clientId: options.imageJobClientId ?? crypto.randomUUID(),
|
|
22256
|
+
...options.newImageJobClaimId ? { newClaimId: options.newImageJobClaimId } : {},
|
|
22257
|
+
...options.now ? { now: options.now } : {}
|
|
22258
|
+
}) : void 0;
|
|
21686
22259
|
const port = { elements, context, runs, derivation, imageBatchHistory };
|
|
21687
22260
|
if (imageLibrary) port.imageLibrary = imageLibrary;
|
|
21688
22261
|
if (imageBatchWrite) port.imageBatchWrite = imageBatchWrite;
|
|
21689
22262
|
if (imageGeneration) port.imageGeneration = imageGeneration;
|
|
22263
|
+
if (imageJobs) port.imageJobs = imageJobs;
|
|
21690
22264
|
return port;
|
|
21691
22265
|
}
|
|
21692
22266
|
|
|
@@ -21754,7 +22328,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
21754
22328
|
}
|
|
21755
22329
|
}, []);
|
|
21756
22330
|
const [showStart, setShowStart] = (0, import_react50.useState)(true);
|
|
21757
|
-
const [showSzenen, setShowSzenen] = (0, import_react50.useState)(false);
|
|
21758
22331
|
const [layoutChoice, setLayoutChoice] = (0, import_react50.useState)(() => {
|
|
21759
22332
|
try {
|
|
21760
22333
|
return localStorage.getItem("aa-layout") || null;
|
|
@@ -22567,24 +23140,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22567
23140
|
}
|
|
22568
23141
|
);
|
|
22569
23142
|
}
|
|
22570
|
-
if (showSzenen && szenenPort) {
|
|
22571
|
-
return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("main", { className: "flow-szenen-shell", children: [
|
|
22572
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("header", { className: "flow-szenen-shell__header", children: [
|
|
22573
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", onClick: () => setShowSzenen(false), children: "Zur\xFCck" }),
|
|
22574
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { children: [
|
|
22575
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("strong", { children: "Szenen" }),
|
|
22576
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("small", { children: [
|
|
22577
|
-
effectiveNamespace,
|
|
22578
|
-
" \xB7 User ",
|
|
22579
|
-
tagBaseResult.selectedUserId,
|
|
22580
|
-
" \xB7 Context ",
|
|
22581
|
-
tagBaseResult.selectedContextId
|
|
22582
|
-
] })
|
|
22583
|
-
] })
|
|
22584
|
-
] }),
|
|
22585
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flow-szenen-shell__content", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SzenenView, { port: szenenPort, contextId: szenenKontextId }) })
|
|
22586
|
-
] });
|
|
22587
|
-
}
|
|
22588
23143
|
if (showStart) {
|
|
22589
23144
|
return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "fixed inset-0 bg-[#0e0e0e] flex flex-col items-center justify-center p-6", style: { gap: 28, ...hcStyle }, children: [
|
|
22590
23145
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-1", children: [
|
|
@@ -22662,21 +23217,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22662
23217
|
))
|
|
22663
23218
|
] })
|
|
22664
23219
|
] }),
|
|
22665
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flow-szenen-entry", children: [
|
|
22666
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
|
|
22667
|
-
"button",
|
|
22668
|
-
{
|
|
22669
|
-
type: "button",
|
|
22670
|
-
disabled: !szenenPort || !bootstrapTarget.ready,
|
|
22671
|
-
onClick: () => setShowSzenen(true),
|
|
22672
|
-
children: [
|
|
22673
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined", children: "movie" }),
|
|
22674
|
-
"Szenen \xF6ffnen"
|
|
22675
|
-
]
|
|
22676
|
-
}
|
|
22677
|
-
),
|
|
22678
|
-
!szenenPort && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("small", { children: "F\xFCr Szenen zuerst den HF-Token setzen." })
|
|
22679
|
-
] }),
|
|
22680
23220
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
22681
23221
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/25 text-[10px] uppercase tracking-widest font-bold", children: "Layout w\xE4hlen & starten" }),
|
|
22682
23222
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "grid grid-cols-2 gap-2 w-full", children: [
|
|
@@ -22744,6 +23284,11 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22744
23284
|
);
|
|
22745
23285
|
}
|
|
22746
23286
|
}
|
|
23287
|
+
const szenenPane = szenenPort ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flow-szenen-pane", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SzenenView, { port: szenenPort, contextId: szenenKontextId }) }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flow-szenen-pane flow-szenen-pane--empty", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("p", { children: [
|
|
23288
|
+
"Szenen brauchen einen HF-Token f\xFCr Namespace ",
|
|
23289
|
+
effectiveNamespace || "(noch nicht bestimmt)",
|
|
23290
|
+
" \u2014 auf der Flow-Startseite setzen."
|
|
23291
|
+
] }) });
|
|
22747
23292
|
if (layoutChoice === "mobile" || layoutChoice === "mobile-desktop") {
|
|
22748
23293
|
const mdMode = layoutChoice === "mobile-desktop";
|
|
22749
23294
|
const mdScale = mdMode ? window.innerWidth / 430 : 1;
|
|
@@ -22775,6 +23320,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22775
23320
|
onTagMove: handleTagMove
|
|
22776
23321
|
}
|
|
22777
23322
|
) }),
|
|
23323
|
+
mobileTab === "szenen" && szenenPane,
|
|
22778
23324
|
mobileTab === "stage" && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col flex-1 min-h-0", children: [
|
|
22779
23325
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2 px-3 border-b border-white/5 bg-black/30 shrink-0", style: { height: 52 }, children: [
|
|
22780
23326
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)(CompactDropdown, { value: aspectRatio, onChange: setAspectRatio, options: [{ label: "1:1", value: "1:1" }, { label: "16:9", value: "16:9" }, { label: "9:16", value: "9:16" }] }),
|
|
@@ -23058,7 +23604,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
23058
23604
|
...workspaceTags ? [{ id: "tags", icon: "label", label: "Tags" }] : [],
|
|
23059
23605
|
{ id: "browse", icon: "photo_library", label: "Galerie" },
|
|
23060
23606
|
...serverBaseUrl ? [{ id: "server", icon: "storage", label: "Server" }] : []
|
|
23061
|
-
].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { onClick: () =>
|
|
23607
|
+
].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { onClick: () => setMobileTab(tab.id), className: `flex-none min-w-[64px] flex flex-col items-center justify-center gap-0.5 transition-colors ${mobileTab === tab.id ? "text-white" : "text-white/30"}`, children: [
|
|
23062
23608
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[24px]", children: tab.icon }),
|
|
23063
23609
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-[10px] font-bold uppercase tracking-wide", children: tab.label })
|
|
23064
23610
|
] }, tab.id)) })
|
|
@@ -23253,6 +23799,14 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
23253
23799
|
className: `px-3 h-7 rounded-lg text-[9px] font-bold uppercase tracking-wide transition-colors ${middlePanel === "labs" ? "bg-white/10 text-white" : "text-white/30 hover:text-white/50"}`,
|
|
23254
23800
|
children: "Labs"
|
|
23255
23801
|
}
|
|
23802
|
+
),
|
|
23803
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
|
|
23804
|
+
"button",
|
|
23805
|
+
{
|
|
23806
|
+
onClick: () => setMiddlePanel("szenen"),
|
|
23807
|
+
className: `px-3 h-7 rounded-lg text-[9px] font-bold uppercase tracking-wide transition-colors ${middlePanel === "szenen" ? "bg-white/10 text-white" : "text-white/30 hover:text-white/50"}`,
|
|
23808
|
+
children: "Szenen"
|
|
23809
|
+
}
|
|
23256
23810
|
)
|
|
23257
23811
|
] }),
|
|
23258
23812
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
@@ -23275,7 +23829,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
23275
23829
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("textarea", { value: activePrompt, onChange: (e) => setActivePrompt(e.target.value), className: "w-full bg-transparent border-none outline-none text-[12px] leading-relaxed text-white/80 resize-none h-20 dark-scrollbar", placeholder: "W\xE4hle einen Knoten oder tippe einen Prompt..." }),
|
|
23276
23830
|
activePrompt && !isSynthesizing && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => setActivePrompt(""), className: "absolute top-2 right-2 w-6 h-6 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors text-white/20 hover:text-white", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[14px]", children: "close" }) })
|
|
23277
23831
|
] }) }),
|
|
23278
|
-
middlePanel === "labs" ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(LabsTab, { services: labServices, onResult: (item) => {
|
|
23832
|
+
middlePanel === "szenen" ? szenenPane : middlePanel === "labs" ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(LabsTab, { services: labServices, onResult: (item) => {
|
|
23279
23833
|
const frame = item.frames[0];
|
|
23280
23834
|
if (frame?.base64) setCurrentResult(frameToGeneration(frame, item));
|
|
23281
23835
|
} }) }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex-1 overflow-hidden flex flex-col", children: [
|
|
@@ -23360,8 +23914,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
23360
23914
|
serverBaseUrl && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => {
|
|
23361
23915
|
setActiveTab("server");
|
|
23362
23916
|
setIsRightCollapsed(false);
|
|
23363
|
-
}, className: `w-10 flex items-center justify-center relative transition-colors ${activeTab === "server" ? "text-white" : "text-white/20"}`, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[20px]", children: "storage" }) })
|
|
23364
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => setShowSzenen(true), className: "w-10 flex items-center justify-center relative text-white/20 hover:text-white", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[20px]", children: "movie" }) })
|
|
23917
|
+
}, className: `w-10 flex items-center justify-center relative transition-colors ${activeTab === "server" ? "text-white" : "text-white/20"}`, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[20px]", children: "storage" }) })
|
|
23365
23918
|
] }),
|
|
23366
23919
|
hfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => refreshHF(), disabled: isHfRefreshing, className: "w-10 flex items-center justify-center text-white/20 hover:text-white/60 transition-colors disabled:opacity-30", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: `material-symbols-outlined text-[18px]${isHfRefreshing ? " animate-spin" : ""}`, children: "sync" }) }),
|
|
23367
23920
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => setIsRightCollapsed(!isRightCollapsed), className: "w-10 flex items-center justify-center text-white/20 hover:text-white", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: isRightCollapsed ? "chevron_left" : "chevron_right" }) })
|