@rslsp1/fa-app-tools 5.1.0 → 7.0.1
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-3MUI4ZV7.mjs → chunk-B2YV4TNI.mjs} +914 -76
- package/dist/{hfEventTypes-Dkfrcd_t.d.mts → hfEventTypes-BUztJySs.d.mts} +1 -11
- package/dist/{hfEventTypes-C_PX6RFR.d.ts → hfEventTypes-fAdA-0A9.d.ts} +1 -11
- package/dist/index.d.mts +18 -49
- package/dist/index.d.ts +18 -49
- package/dist/index.js +1221 -1548
- package/dist/index.mjs +208 -671
- package/dist/szenen.d.mts +197 -4
- package/dist/szenen.d.ts +197 -4
- package/dist/szenen.js +697 -115
- package/dist/szenen.mjs +63 -4
- package/package.json +1 -1
- package/dist/chunk-NBUS7AWR.mjs +0 -418
- package/dist/chunk-UFXDXENC.mjs +0 -218
- package/dist/hfStateService-W7KO2FOH.mjs +0 -56
- package/dist/project-O4ORKXY5.mjs +0 -8
package/dist/index.js
CHANGED
|
@@ -5,9 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function __init() {
|
|
9
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
-
};
|
|
11
8
|
var __export = (target, all) => {
|
|
12
9
|
for (var name in all)
|
|
13
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -30,7 +27,220 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
27
|
));
|
|
31
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
29
|
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AvatarArchitectApp: () => AvatarArchitectApp,
|
|
34
|
+
CURRENT_TAG_EVENT_VERSION: () => CURRENT_TAG_EVENT_VERSION,
|
|
35
|
+
CollapsibleCard: () => CollapsibleCard,
|
|
36
|
+
CompactDropdown: () => CompactDropdown,
|
|
37
|
+
FaApp: () => FaApp,
|
|
38
|
+
FaToolsBadge: () => FaToolsBadge,
|
|
39
|
+
FlowWorkspacePicker: () => FlowWorkspacePicker,
|
|
40
|
+
GLOBAL_STYLES: () => GLOBAL_STYLES,
|
|
41
|
+
HfMediaSyncTab: () => HfMediaSyncTab,
|
|
42
|
+
HistoryPanel: () => HistoryPanel,
|
|
43
|
+
InspectPanel: () => InspectPanel,
|
|
44
|
+
LIBRARY_CUTOVER_MARKER: () => LIBRARY_CUTOVER_MARKER,
|
|
45
|
+
LIBRARY_SLICE_SCHEMA_VERSION: () => LIBRARY_SLICE_SCHEMA_VERSION,
|
|
46
|
+
LIB_VERSION: () => version,
|
|
47
|
+
LabBlend: () => LabBlend,
|
|
48
|
+
LabCompare: () => LabCompare,
|
|
49
|
+
LabFrameExtractor: () => LabFrameExtractor,
|
|
50
|
+
LabImagePicker: () => LabImagePicker,
|
|
51
|
+
LabLoop: () => LabLoop,
|
|
52
|
+
LabRemix: () => LabRemix,
|
|
53
|
+
LabsTab: () => LabsTab,
|
|
54
|
+
LibraryView: () => LibraryView,
|
|
55
|
+
ListView: () => ListView,
|
|
56
|
+
MediaLibrary: () => MediaLibrary,
|
|
57
|
+
PillButton: () => PillButton,
|
|
58
|
+
PromptTab: () => PromptTab,
|
|
59
|
+
SCOPE_MAP: () => SCOPE_MAP,
|
|
60
|
+
SectionLabel: () => SectionLabel,
|
|
61
|
+
ServerTab: () => ServerTab,
|
|
62
|
+
SetupPanel: () => SetupPanel,
|
|
63
|
+
TAGS_CUTOVER_MARKER: () => TAGS_CUTOVER_MARKER,
|
|
64
|
+
TAGS_SLICE_SCHEMA_VERSION: () => TAGS_SLICE_SCHEMA_VERSION,
|
|
65
|
+
TagBaseSection: () => TagBaseSection,
|
|
66
|
+
TagManager: () => TagManager,
|
|
67
|
+
TagManagerPanel: () => TagManagerPanel,
|
|
68
|
+
autoLabel: () => autoLabel,
|
|
69
|
+
buildBlendInstruction: () => buildBlendInstruction,
|
|
70
|
+
buildCompareInstruction: () => buildCompareInstruction,
|
|
71
|
+
buildFallbackPrompt: () => buildFallbackPrompt,
|
|
72
|
+
buildGenerationPrompt: () => buildGenerationPrompt,
|
|
73
|
+
buildImageGenerationOptions: () => buildImageGenerationOptions,
|
|
74
|
+
buildLoopInstruction: () => buildLoopInstruction,
|
|
75
|
+
buildPromptTabPayload: () => buildPromptTabPayload,
|
|
76
|
+
buildReferenceImageMediaIds: () => buildReferenceImageMediaIds,
|
|
77
|
+
buildRemixInstruction: () => buildRemixInstruction,
|
|
78
|
+
buildScanInstruction: () => buildScanInstruction,
|
|
79
|
+
cleanAiResponse: () => cleanAiResponse,
|
|
80
|
+
createFaStore: () => createFaStore,
|
|
81
|
+
createFlowServices: () => createFlowServices,
|
|
82
|
+
createInMemoryLibraryDataSource: () => createInMemoryLibraryDataSource,
|
|
83
|
+
createInMemoryWorkspacePersistence: () => createInMemoryWorkspacePersistence,
|
|
84
|
+
createLibraryDefault: () => createLibraryDefault,
|
|
85
|
+
createPersistenceCoordinator: () => createPersistenceCoordinator,
|
|
86
|
+
createTagCreatedEvent: () => createTagCreatedEvent,
|
|
87
|
+
createTagUpdatedEvent: () => createTagUpdatedEvent,
|
|
88
|
+
createTagsDefault: () => createTagsDefault,
|
|
89
|
+
deriveTagBaseWorkspaceTags: () => deriveTagBaseWorkspaceTags,
|
|
90
|
+
deriveTagViewModel: () => deriveTagViewModel,
|
|
91
|
+
formatTreeToMarkdown: () => formatTreeToMarkdown,
|
|
92
|
+
frameToGeneration: () => frameToGeneration,
|
|
93
|
+
getFormattedTimestamp: () => getFormattedTimestamp,
|
|
94
|
+
getHFToken: () => getHFToken,
|
|
95
|
+
getSessionClientId: () => getSessionClientId,
|
|
96
|
+
groupGenerationsToLabItems: () => groupGenerationsToLabItems,
|
|
97
|
+
hfBatchArchive: () => hfBatchArchive,
|
|
98
|
+
hfBootstrapFromLegacy: () => hfBootstrapFromLegacy,
|
|
99
|
+
hfListDir: () => hfListDir,
|
|
100
|
+
hfLoadImageAsBase64: () => hfLoadImageAsBase64,
|
|
101
|
+
hfUploadImage: () => hfUploadImage,
|
|
102
|
+
hfUploadSmallFile: () => hfUploadSmallFile,
|
|
103
|
+
injectXMPMetadata: () => injectXMPMetadata,
|
|
104
|
+
interpretSdkError: () => interpretSdkError,
|
|
105
|
+
libraryContract: () => libraryContract,
|
|
106
|
+
libraryReactDescriptor: () => libraryReactDescriptor,
|
|
107
|
+
listCategories: () => listCategories,
|
|
108
|
+
loadHFState: () => loadHFState,
|
|
109
|
+
loadPendingEvents: () => loadPendingEvents,
|
|
110
|
+
loadTagBase: () => loadTagBase,
|
|
111
|
+
parseLegacyLibraryFilters: () => parseLegacyLibraryFilters,
|
|
112
|
+
parseLegacyRecentTags: () => parseLegacyRecentTags,
|
|
113
|
+
parsePromptFile: () => parsePromptFile,
|
|
114
|
+
parsePromptResponse: () => parsePromptResponse,
|
|
115
|
+
runLegacyMigration: () => runLegacyMigration,
|
|
116
|
+
runLibraryLegacyImport: () => runLibraryLegacyImport,
|
|
117
|
+
runTagsLegacyImport: () => runTagsLegacyImport,
|
|
118
|
+
setHFToken: () => setHFToken,
|
|
119
|
+
tagsContract: () => tagsContract,
|
|
120
|
+
tagsReactDescriptor: () => tagsReactDescriptor,
|
|
121
|
+
tsFromEventPath: () => tsFromEventPath,
|
|
122
|
+
useFaSlice: () => useFaSlice,
|
|
123
|
+
useFaSliceMeta: () => useFaSliceMeta,
|
|
124
|
+
useHFState: () => useHFState,
|
|
125
|
+
useKeyboardNavigation: () => useKeyboardNavigation,
|
|
126
|
+
useOnClickOutside: () => useOnClickOutside,
|
|
127
|
+
useTagBase: () => useTagBase,
|
|
128
|
+
validateTagBaseSelection: () => validateTagBaseSelection,
|
|
129
|
+
writeHFEvent: () => writeHFEvent,
|
|
130
|
+
writeTagEvent: () => writeTagEvent
|
|
131
|
+
});
|
|
132
|
+
module.exports = __toCommonJS(index_exports);
|
|
133
|
+
|
|
134
|
+
// package.json
|
|
135
|
+
var version = "7.0.1";
|
|
136
|
+
|
|
137
|
+
// src/hooks/useOnClickOutside.ts
|
|
138
|
+
var import_react = require("react");
|
|
139
|
+
function useOnClickOutside(ref, handler) {
|
|
140
|
+
(0, import_react.useEffect)(() => {
|
|
141
|
+
const listener = (event) => {
|
|
142
|
+
if (!ref.current || ref.current.contains(event.target)) return;
|
|
143
|
+
handler(event);
|
|
144
|
+
};
|
|
145
|
+
document.addEventListener("mousedown", listener);
|
|
146
|
+
document.addEventListener("touchstart", listener);
|
|
147
|
+
return () => {
|
|
148
|
+
document.removeEventListener("mousedown", listener);
|
|
149
|
+
document.removeEventListener("touchstart", listener);
|
|
150
|
+
};
|
|
151
|
+
}, [ref, handler]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/hooks/useKeyboardNavigation.ts
|
|
155
|
+
var import_react2 = require("react");
|
|
156
|
+
function useKeyboardNavigation(history, currentResult, setCurrentResult) {
|
|
157
|
+
(0, import_react2.useEffect)(() => {
|
|
158
|
+
const handleKeyDown = (e) => {
|
|
159
|
+
if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
|
|
160
|
+
if (!currentResult || history.length === 0) return;
|
|
161
|
+
const currentIndex = history.findIndex((h) => h.id === currentResult.id);
|
|
162
|
+
if (e.key === "ArrowRight" && currentIndex < history.length - 1) {
|
|
163
|
+
setCurrentResult(history[currentIndex + 1]);
|
|
164
|
+
} else if (e.key === "ArrowLeft" && currentIndex > 0) {
|
|
165
|
+
setCurrentResult(history[currentIndex - 1]);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
169
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
170
|
+
}, [currentResult, history, setCurrentResult]);
|
|
171
|
+
}
|
|
172
|
+
|
|
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';
|
|
175
|
+
|
|
176
|
+
// src/lib/utils.ts
|
|
177
|
+
var getFormattedTimestamp = () => {
|
|
178
|
+
const d = /* @__PURE__ */ new Date();
|
|
179
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
180
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
181
|
+
};
|
|
182
|
+
var GLOBAL_STYLES = `
|
|
183
|
+
.dark-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
|
|
184
|
+
.dark-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
|
185
|
+
.dark-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
|
|
186
|
+
html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; background: #0e0e0e; font-family: 'Google Sans Text', sans-serif; overflow: hidden; }
|
|
187
|
+
@keyframes prompt-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
|
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
|
+
@keyframes dropdown-enter { from { opacity: 0; transform: scale(0.95) translateY(-5px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
|
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
|
+
.flow-workspace-picker { position: fixed; inset: 0; display: grid; place-items: center; padding: 16px; color: #f5f5f5; background: #050505; overflow: auto; }
|
|
197
|
+
.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
|
+
.flow-workspace-picker__card header { display: grid; gap: 4px; }
|
|
199
|
+
.flow-workspace-picker__card header strong { font-size: 22px; }
|
|
200
|
+
.flow-workspace-picker__card header small, .flow-workspace-picker__choices small { color: #a3a3a3; }
|
|
201
|
+
.flow-workspace-picker__choices { display: grid; gap: 8px; }
|
|
202
|
+
.flow-workspace-picker__choices button { display: flex; justify-content: space-between; align-items: center; min-height: 46px; padding: 10px 12px; color: #f5f5f5; background: #050505; border: 1px solid #404040; border-radius: 8px; font: inherit; cursor: pointer; }
|
|
203
|
+
.flow-workspace-picker__choices button:hover { border-color: #3b82f6; background: #262626; }
|
|
204
|
+
.flow-workspace-picker__back { justify-self: start; padding: 0; color: #a3a3a3; background: transparent; border: 0; font: inherit; cursor: pointer; }
|
|
205
|
+
.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-shell {
|
|
207
|
+
--sz-bg: #0e0e0e; --sz-surface: #171717; --sz-elevated: #262626;
|
|
208
|
+
--sz-border: #404040; --sz-text: #f5f5f5; --sz-muted: #a3a3a3;
|
|
209
|
+
--color-bg-app: #000; --color-bg-surface: #0a0a0a; --color-bg-surface-hover: #171717; --color-bg-elevated: #171717;
|
|
210
|
+
--color-border: #262626; --color-border-hover: #404040;
|
|
211
|
+
--color-text-primary: #fff; --color-text-secondary: #a3a3a3; --color-text-tertiary: #525252;
|
|
212
|
+
--color-accent-blue: #3b82f6; --color-accent-blue-dim: rgba(59,130,246,.2);
|
|
213
|
+
--color-accent-purple: #a855f7; --color-accent-green: #22c55e; --color-accent-green-dim: rgba(34,197,94,.2);
|
|
214
|
+
--color-accent-red: #ef4444; --color-accent-red-dim: rgba(239,68,68,.2);
|
|
215
|
+
--color-success: #22c55e; --color-warning: #eab308; --color-danger: #ef4444;
|
|
216
|
+
--space-xs: .25rem; --space-sm: .5rem; --space-md: 1rem;
|
|
217
|
+
--radius-sm: .25rem; --radius-md: .5rem; --radius-lg: .75rem; --radius-full: 9999px;
|
|
218
|
+
--font-sans: system-ui, -apple-system, sans-serif;
|
|
219
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
220
|
+
position: fixed; inset: 0; display: flex; flex-direction: column;
|
|
221
|
+
color: var(--sz-text); background: var(--sz-bg); overflow: hidden;
|
|
222
|
+
}
|
|
223
|
+
.flow-szenen-shell__header { display: flex; align-items: center; gap: 16px; min-height: 58px; padding: 8px 16px; background: #050505; border-bottom: 1px solid var(--sz-border); }
|
|
224
|
+
.flow-szenen-shell__header button { min-height: 36px; background: var(--sz-elevated); border-color: var(--sz-border); }
|
|
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; } }
|
|
229
|
+
` + SZENEN_VIEW_STYLES;
|
|
230
|
+
|
|
33
231
|
// src/lib/metadata.ts
|
|
232
|
+
var crcTable = (() => {
|
|
233
|
+
let c;
|
|
234
|
+
const table = new Uint32Array(256);
|
|
235
|
+
for (let n = 0; n < 256; n++) {
|
|
236
|
+
c = n;
|
|
237
|
+
for (let k = 0; k < 8; k++) {
|
|
238
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
239
|
+
}
|
|
240
|
+
table[n] = c;
|
|
241
|
+
}
|
|
242
|
+
return table;
|
|
243
|
+
})();
|
|
34
244
|
function calculateCRC(bytes) {
|
|
35
245
|
let crc = 4294967295;
|
|
36
246
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -136,809 +346,39 @@ function injectXMPMetadata(base64, prompt, seed, model, id, tags = [], hierarchy
|
|
|
136
346
|
return base64;
|
|
137
347
|
}
|
|
138
348
|
}
|
|
139
|
-
var crcTable;
|
|
140
|
-
var init_metadata = __esm({
|
|
141
|
-
"src/lib/metadata.ts"() {
|
|
142
|
-
"use strict";
|
|
143
|
-
crcTable = (() => {
|
|
144
|
-
let c;
|
|
145
|
-
const table = new Uint32Array(256);
|
|
146
|
-
for (let n = 0; n < 256; n++) {
|
|
147
|
-
c = n;
|
|
148
|
-
for (let k = 0; k < 8; k++) {
|
|
149
|
-
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
150
|
-
}
|
|
151
|
-
table[n] = c;
|
|
152
|
-
}
|
|
153
|
-
return table;
|
|
154
|
-
})();
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
349
|
|
|
158
|
-
// src/lib/
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
function generateMarkdownReport(nodes, history) {
|
|
165
|
-
let md = "# Avatar Architect - Projekt Report\n\n";
|
|
166
|
-
md += `Exportiert: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
350
|
+
// src/lib/aiHelpers.ts
|
|
351
|
+
function buildGenerationPrompt(hierarchyText, mode = "creative") {
|
|
352
|
+
const safe = hierarchyText.length > 2500 ? hierarchyText.slice(0, 2500) + "..." : hierarchyText;
|
|
353
|
+
if (mode === "literal") {
|
|
354
|
+
return `Translate this hierarchy into a literal image generation prompt. Output ONLY the raw prompt string. No chat.
|
|
167
355
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`;
|
|
173
|
-
});
|
|
174
|
-
md += "\n## Historie der Generationen\n";
|
|
175
|
-
history.forEach((h, i) => {
|
|
176
|
-
md += `### ${i + 1}. Generation (${h.model || "AI"})
|
|
177
|
-
`;
|
|
178
|
-
md += `- **Zeitstempel**: ${new Date(h.timestamp).toLocaleString()}
|
|
179
|
-
`;
|
|
180
|
-
md += `- **Seed**: ${h.seed}
|
|
181
|
-
`;
|
|
182
|
-
md += `- **Prompt**:
|
|
183
|
-
> ${h.prompt}
|
|
356
|
+
HIERARCHY:
|
|
357
|
+
${safe}`;
|
|
358
|
+
}
|
|
359
|
+
return `Create a cinematic, high-quality image generation prompt based on this hierarchy. Translate everything to English. Output ONLY the prompt string. No chat.
|
|
184
360
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return md;
|
|
361
|
+
HIERARCHY:
|
|
362
|
+
${safe}`;
|
|
188
363
|
}
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
edges,
|
|
194
|
-
history: history.map((h) => ({ ...h, base64: void 0 })),
|
|
195
|
-
galleryItems: galleryItems.map((g) => ({ ...g, base64: void 0 })),
|
|
196
|
-
settings,
|
|
197
|
-
workspaceTags: workspaceTags || null,
|
|
198
|
-
recentLabItemIds: recentLabItemIds || [],
|
|
199
|
-
version: "1.3.0"
|
|
200
|
-
};
|
|
201
|
-
zip.file("project.json", JSON.stringify(projectData, null, 2));
|
|
202
|
-
zip.file("projekt_bericht.md", generateMarkdownReport(nodes, history));
|
|
203
|
-
const imgFolder = zip.folder("images");
|
|
204
|
-
if (imgFolder) {
|
|
205
|
-
const allItems = [...history, ...galleryItems];
|
|
206
|
-
for (const item of allItems) {
|
|
207
|
-
if (item.base64) {
|
|
208
|
-
try {
|
|
209
|
-
const cleanB64 = item.base64.includes(",") ? item.base64.split(",")[1] : item.base64;
|
|
210
|
-
const withMeta = injectXMPMetadata(cleanB64, item.prompt || "", item.seed, item.model, item.id, item.tags || []);
|
|
211
|
-
imgFolder.file(`${item.id}.png`, withMeta, { base64: true });
|
|
212
|
-
} catch (e) {
|
|
213
|
-
console.error("Fehler beim Packen eines Bildes:", e);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const blob = await zip.generateAsync({ type: "blob", compression: "STORE" });
|
|
219
|
-
return blobToBase64(blob);
|
|
364
|
+
function buildFallbackPrompt(hierarchyText) {
|
|
365
|
+
const lines = hierarchyText.split("\n");
|
|
366
|
+
const firstConcept = lines.find((l) => l.includes("-"))?.replace(/^[-\s]+/, "") || "character concept";
|
|
367
|
+
return `Cinematic professional photograph of ${firstConcept}, masterpiece, highly detailed, fine art style`;
|
|
220
368
|
}
|
|
221
|
-
|
|
222
|
-
return
|
|
223
|
-
const reader = new FileReader();
|
|
224
|
-
reader.onloadend = () => {
|
|
225
|
-
const result = reader.result;
|
|
226
|
-
resolve({ base64: result.split(",")[1] });
|
|
227
|
-
};
|
|
228
|
-
reader.onerror = reject;
|
|
229
|
-
reader.readAsDataURL(blob);
|
|
230
|
-
});
|
|
369
|
+
function cleanAiResponse(text) {
|
|
370
|
+
return text.replace(/^["']|["']$/g, "").replace(/```/g, "").trim();
|
|
231
371
|
}
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
if (!items) return [];
|
|
241
|
-
return Promise.all(items.map(async (h) => {
|
|
242
|
-
const imgFile = zip.file(`images/${h.id}.png`);
|
|
243
|
-
if (!imgFile) return h;
|
|
244
|
-
const b64 = await imgFile.async("base64");
|
|
245
|
-
return { ...h, base64: `data:image/png;base64,${b64}` };
|
|
246
|
-
}));
|
|
247
|
-
};
|
|
248
|
-
data.history = await loadImgs(data.history || []);
|
|
249
|
-
data.galleryItems = await loadImgs(data.galleryItems || []);
|
|
372
|
+
function buildImageGenerationOptions(prompt, aspectRatio, model, referenceMediaId) {
|
|
373
|
+
const options = {
|
|
374
|
+
prompt: prompt.replace(/\n/g, " ").trim().slice(0, 1500),
|
|
375
|
+
aspectRatio,
|
|
376
|
+
modelDisplayName: model
|
|
377
|
+
};
|
|
378
|
+
if (referenceMediaId && referenceMediaId.length > 5) {
|
|
379
|
+
options.referenceImageMediaIds = [referenceMediaId];
|
|
250
380
|
}
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
var import_jszip;
|
|
254
|
-
var init_project = __esm({
|
|
255
|
-
"src/lib/project.ts"() {
|
|
256
|
-
"use strict";
|
|
257
|
-
import_jszip = __toESM(require("jszip"));
|
|
258
|
-
init_metadata();
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// src/lib/hfEventTypes.ts
|
|
263
|
-
function bildEndungFuer(mimeType) {
|
|
264
|
-
if (mimeType === "image/png") return "png";
|
|
265
|
-
if (mimeType === "image/webp") return "webp";
|
|
266
|
-
return "jpg";
|
|
267
|
-
}
|
|
268
|
-
var HF_BASE, HF_REPO, CURRENT_EVENT_VERSION, SPEICHERBARE_BILDTYPEN;
|
|
269
|
-
var init_hfEventTypes = __esm({
|
|
270
|
-
"src/lib/hfEventTypes.ts"() {
|
|
271
|
-
"use strict";
|
|
272
|
-
HF_BASE = "https://huggingface.co";
|
|
273
|
-
HF_REPO = "RolandSch/fa-app-state";
|
|
274
|
-
CURRENT_EVENT_VERSION = { major: 1, minor: 0 };
|
|
275
|
-
SPEICHERBARE_BILDTYPEN = ["image/png", "image/jpeg", "image/webp"];
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// src/lib/hfEventLog.ts
|
|
280
|
-
function treeUrl(namespace, subdir = "") {
|
|
281
|
-
const parts = [namespace.replace(/\/$/, ""), subdir].filter(Boolean).join("/");
|
|
282
|
-
return `${HF_BASE}/api/datasets/${HF_REPO}/tree/main${parts ? "/" + parts : ""}`;
|
|
283
|
-
}
|
|
284
|
-
async function holeMitZeitlimit(url, init) {
|
|
285
|
-
const abbruch = new AbortController();
|
|
286
|
-
const timer = setTimeout(() => abbruch.abort(), HF_LESE_ZEITLIMIT_MS);
|
|
287
|
-
try {
|
|
288
|
-
return await fetch(url, { ...init, signal: abbruch.signal });
|
|
289
|
-
} catch (error) {
|
|
290
|
-
if (abbruch.signal.aborted) {
|
|
291
|
-
throw new Error(`HF-Anfrage ohne Antwort nach ${HF_LESE_ZEITLIMIT_MS}ms abgebrochen: ${url}`);
|
|
292
|
-
}
|
|
293
|
-
throw error;
|
|
294
|
-
} finally {
|
|
295
|
-
clearTimeout(timer);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
async function mitGeduld(anfrage) {
|
|
299
|
-
let letzte = null;
|
|
300
|
-
for (let versuch = 0; versuch < DROSSEL_VERSUCHE; versuch += 1) {
|
|
301
|
-
const res = await anfrage();
|
|
302
|
-
if (res.status !== 429 && res.status < 500) return res;
|
|
303
|
-
letzte = res;
|
|
304
|
-
if (versuch === DROSSEL_VERSUCHE - 1) break;
|
|
305
|
-
const angesagt = Number(res.headers.get("retry-after"));
|
|
306
|
-
const warten2 = Number.isFinite(angesagt) && angesagt > 0 ? angesagt * 1e3 : DROSSEL_BASIS_MS * 2 ** versuch;
|
|
307
|
-
await schlafen(warten2);
|
|
308
|
-
}
|
|
309
|
-
return letzte;
|
|
310
|
-
}
|
|
311
|
-
async function hfListDir(namespace, subdir, token) {
|
|
312
|
-
const res = await mitGeduld(() => holeMitZeitlimit(treeUrl(namespace, subdir), {
|
|
313
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
314
|
-
}));
|
|
315
|
-
if (res.status === 404) return [];
|
|
316
|
-
if (!res.ok) throw new Error(`HF list failed: ${res.status} ${res.statusText}`);
|
|
317
|
-
return res.json();
|
|
318
|
-
}
|
|
319
|
-
async function hfDownloadJsonByPath(repoPath, token) {
|
|
320
|
-
const res = await mitGeduld(() => holeMitZeitlimit(
|
|
321
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
|
|
322
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
323
|
-
));
|
|
324
|
-
if (!res.ok) throw new Error(`HF download failed: ${res.status}`);
|
|
325
|
-
return res.json();
|
|
326
|
-
}
|
|
327
|
-
async function hfDownloadBinaryByPath(repoPath, token) {
|
|
328
|
-
const res = await mitGeduld(() => holeMitZeitlimit(
|
|
329
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
|
|
330
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
331
|
-
));
|
|
332
|
-
if (!res.ok) throw new Error(`HF download binary failed: ${res.status}`);
|
|
333
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
334
|
-
}
|
|
335
|
-
function toBase64(text) {
|
|
336
|
-
const bytes = new TextEncoder().encode(text);
|
|
337
|
-
let binary = "";
|
|
338
|
-
bytes.forEach((b) => binary += String.fromCharCode(b));
|
|
339
|
-
return btoa(binary);
|
|
340
|
-
}
|
|
341
|
-
async function hfUploadSmallFile(repoPath, content, token, summary = `Update ${repoPath}`) {
|
|
342
|
-
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
|
|
343
|
-
method: "POST",
|
|
344
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
|
|
345
|
-
body: [
|
|
346
|
-
JSON.stringify({ key: "header", value: { summary, description: "" } }),
|
|
347
|
-
JSON.stringify({ key: "file", value: { path: repoPath, encoding: "base64", content: toBase64(content) } })
|
|
348
|
-
].join("\n")
|
|
349
|
-
});
|
|
350
|
-
if (!res.ok) {
|
|
351
|
-
const err = await res.text().catch(() => "");
|
|
352
|
-
throw new Error(`HF upload failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
async function hfBatchArchive(moves, token, summary) {
|
|
356
|
-
const lines = [
|
|
357
|
-
JSON.stringify({ key: "header", value: { summary, description: "" } }),
|
|
358
|
-
...moves.flatMap(({ from, to, content }) => [
|
|
359
|
-
JSON.stringify({ key: "file", value: { path: to, encoding: "base64", content: toBase64(content) } }),
|
|
360
|
-
JSON.stringify({ key: "deletedFile", value: { path: from } })
|
|
361
|
-
])
|
|
362
|
-
];
|
|
363
|
-
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
|
|
364
|
-
method: "POST",
|
|
365
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
|
|
366
|
-
body: lines.join("\n")
|
|
367
|
-
});
|
|
368
|
-
if (!res.ok) {
|
|
369
|
-
const err = await res.text().catch(() => "");
|
|
370
|
-
throw new Error(`HF batch archive failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function tsFromEventPath(repoPath) {
|
|
374
|
-
const filename = repoPath.split("/").pop() || "";
|
|
375
|
-
const base = filename.replace(/_[^_]+\.json$/, "");
|
|
376
|
-
const iso = base.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
377
|
-
const ts = Date.parse(iso);
|
|
378
|
-
return isNaN(ts) ? 0 : ts;
|
|
379
|
-
}
|
|
380
|
-
async function mitBegrenzterParallelitaet(count, limit, task) {
|
|
381
|
-
let cursor = 0;
|
|
382
|
-
let ersterFehler;
|
|
383
|
-
let fehlerAufgetreten = false;
|
|
384
|
-
const worker = async () => {
|
|
385
|
-
while (cursor < count) {
|
|
386
|
-
const index = cursor++;
|
|
387
|
-
try {
|
|
388
|
-
await task(index);
|
|
389
|
-
} catch (error) {
|
|
390
|
-
if (!fehlerAufgetreten) {
|
|
391
|
-
fehlerAufgetreten = true;
|
|
392
|
-
ersterFehler = error;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
|
|
398
|
-
if (fehlerAufgetreten) throw ersterFehler;
|
|
399
|
-
}
|
|
400
|
-
async function loadPendingEvents(namespace, token, sinceTs, knownPaths) {
|
|
401
|
-
const files = await hfListDir(namespace, "events", token);
|
|
402
|
-
const pending = files.filter((f) => f.type === "file" && tsFromEventPath(f.path) > sinceTs && !knownPaths?.has(f.path)).sort((a, b) => a.path.localeCompare(b.path));
|
|
403
|
-
if (!pending.length) return [];
|
|
404
|
-
const raw = new Array(pending.length);
|
|
405
|
-
await mitBegrenzterParallelitaet(pending.length, EVENTDOWNLOAD_PARALLELITAET, async (index) => {
|
|
406
|
-
raw[index] = await hfDownloadJsonByPath(pending[index].path, token);
|
|
407
|
-
});
|
|
408
|
-
for (const f of pending) knownPaths?.add(f.path);
|
|
409
|
-
return raw.flatMap((e) => Array.isArray(e) ? e : [e]);
|
|
410
|
-
}
|
|
411
|
-
async function listEventPaths(namespace, token) {
|
|
412
|
-
const files = await hfListDir(namespace, "events", token);
|
|
413
|
-
return files.filter((f) => f.type === "file").map((f) => f.path).sort((a, b) => a.localeCompare(b));
|
|
414
|
-
}
|
|
415
|
-
function getSessionClientId() {
|
|
416
|
-
return SESSION_CLIENT_ID;
|
|
417
|
-
}
|
|
418
|
-
function tsToFilename(ts) {
|
|
419
|
-
return new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-");
|
|
420
|
-
}
|
|
421
|
-
async function writeHFEvent(namespace, token, type, payload, prevTs, options) {
|
|
422
|
-
const wallTs = Date.now();
|
|
423
|
-
const ts = options?.ordered ? Math.max(wallTs, lastOrderedTs) : wallTs;
|
|
424
|
-
if (options?.ordered) lastOrderedTs = ts;
|
|
425
|
-
const uuid = crypto.randomUUID().slice(0, 8);
|
|
426
|
-
const event = {
|
|
427
|
-
v: CURRENT_EVENT_VERSION,
|
|
428
|
-
type,
|
|
429
|
-
ts,
|
|
430
|
-
prevTs,
|
|
431
|
-
clientId: SESSION_CLIENT_ID,
|
|
432
|
-
payload
|
|
433
|
-
};
|
|
434
|
-
const suffix = options?.ordered ? `${String(++writeSeq % 1e6).padStart(6, "0")}${uuid}` : uuid;
|
|
435
|
-
const filename = `${tsToFilename(ts)}_${suffix}.json`;
|
|
436
|
-
const repoPath = `${namespace}events/${filename}`;
|
|
437
|
-
await hfUploadSmallFile(repoPath, JSON.stringify(event, null, 2), token, `Event: ${type}`);
|
|
438
|
-
return event;
|
|
439
|
-
}
|
|
440
|
-
var DROSSEL_VERSUCHE, DROSSEL_BASIS_MS, schlafen, HF_LESE_ZEITLIMIT_MS, EVENTDOWNLOAD_PARALLELITAET, SESSION_CLIENT_ID, writeSeq, lastOrderedTs;
|
|
441
|
-
var init_hfEventLog = __esm({
|
|
442
|
-
"src/lib/hfEventLog.ts"() {
|
|
443
|
-
"use strict";
|
|
444
|
-
init_hfEventTypes();
|
|
445
|
-
DROSSEL_VERSUCHE = 4;
|
|
446
|
-
DROSSEL_BASIS_MS = 400;
|
|
447
|
-
schlafen = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
448
|
-
HF_LESE_ZEITLIMIT_MS = 2e4;
|
|
449
|
-
EVENTDOWNLOAD_PARALLELITAET = 3;
|
|
450
|
-
SESSION_CLIENT_ID = `client-${crypto.randomUUID().slice(0, 8)}`;
|
|
451
|
-
writeSeq = 0;
|
|
452
|
-
lastOrderedTs = 0;
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// src/lib/hfStateService.ts
|
|
457
|
-
var hfStateService_exports = {};
|
|
458
|
-
__export(hfStateService_exports, {
|
|
459
|
-
HF_TOKEN_KEY: () => HF_TOKEN_KEY,
|
|
460
|
-
getHFToken: () => getHFToken,
|
|
461
|
-
getSessionClientId: () => getSessionClientId,
|
|
462
|
-
hfBatchArchive: () => hfBatchArchive,
|
|
463
|
-
hfBootstrapFromLegacy: () => hfBootstrapFromLegacy,
|
|
464
|
-
hfDeleteProject: () => hfDeleteProject,
|
|
465
|
-
hfDownloadBinaryByPath: () => hfDownloadBinaryByPath,
|
|
466
|
-
hfDownloadJsonByPath: () => hfDownloadJsonByPath,
|
|
467
|
-
hfDownloadProject: () => hfDownloadProject,
|
|
468
|
-
hfListDir: () => hfListDir,
|
|
469
|
-
hfListProjects: () => hfListProjects,
|
|
470
|
-
hfLoadImageAsBase64: () => hfLoadImageAsBase64,
|
|
471
|
-
hfLoadMetadata: () => hfLoadMetadata,
|
|
472
|
-
hfLoadTags: () => hfLoadTags,
|
|
473
|
-
hfSaveMetadata: () => hfSaveMetadata,
|
|
474
|
-
hfSaveTags: () => hfSaveTags,
|
|
475
|
-
hfUploadImage: () => hfUploadImage,
|
|
476
|
-
hfUploadProject: () => hfUploadProject,
|
|
477
|
-
hfUploadProjectForm: () => hfUploadProjectForm,
|
|
478
|
-
hfUploadSmallFile: () => hfUploadSmallFile,
|
|
479
|
-
listEventPaths: () => listEventPaths,
|
|
480
|
-
loadHFState: () => loadHFState,
|
|
481
|
-
loadPendingEvents: () => loadPendingEvents,
|
|
482
|
-
setHFToken: () => setHFToken,
|
|
483
|
-
tsFromEventPath: () => tsFromEventPath,
|
|
484
|
-
writeHFEvent: () => writeHFEvent
|
|
485
|
-
});
|
|
486
|
-
function getHFToken() {
|
|
487
|
-
try {
|
|
488
|
-
return localStorage.getItem(HF_TOKEN_KEY);
|
|
489
|
-
} catch {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function setHFToken(token) {
|
|
494
|
-
try {
|
|
495
|
-
localStorage.setItem(HF_TOKEN_KEY, token);
|
|
496
|
-
} catch {
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
async function loadHFState(namespace, token) {
|
|
500
|
-
const files = await hfListDir(namespace, "", token);
|
|
501
|
-
const stateFiles = files.filter((f) => f.type === "file" && /^state-[\dT\-]+Z\.zip$/.test(f.path.split("/").pop() || "")).sort((a, b) => b.path.localeCompare(a.path));
|
|
502
|
-
if (!stateFiles.length) return null;
|
|
503
|
-
const zipBytes = await hfDownloadBinaryByPath(stateFiles[0].path, token);
|
|
504
|
-
const zip = await import_jszip2.default.loadAsync(zipBytes);
|
|
505
|
-
const [metadataStr, tagsStr, metaStr, openTasksStr] = await Promise.all([
|
|
506
|
-
zip.file("metadata.json")?.async("string"),
|
|
507
|
-
zip.file("tags.json")?.async("string"),
|
|
508
|
-
zip.file("state_meta.json")?.async("string"),
|
|
509
|
-
zip.file("open_tasks.json")?.async("string")
|
|
510
|
-
// optional — ältere Snapshots haben es nicht
|
|
511
|
-
]);
|
|
512
|
-
if (!metadataStr || !tagsStr || !metaStr) throw new Error("state.zip: fehlende Pflicht-Dateien");
|
|
513
|
-
return {
|
|
514
|
-
metadata: JSON.parse(metadataStr),
|
|
515
|
-
tags: JSON.parse(tagsStr),
|
|
516
|
-
meta: JSON.parse(metaStr),
|
|
517
|
-
openTasks: openTasksStr ? JSON.parse(openTasksStr) : {}
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
async function hfBootstrapFromLegacy(namespace, token, onProgress) {
|
|
521
|
-
const log = (msg) => {
|
|
522
|
-
if (onProgress) onProgress(msg);
|
|
523
|
-
};
|
|
524
|
-
log("Lese tags.json \u2026");
|
|
525
|
-
const tags = await fetch(
|
|
526
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/tags.json?download=true`,
|
|
527
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
528
|
-
).then((r) => r.ok ? r.json() : null).catch(() => null);
|
|
529
|
-
log("Lese metadata.json \u2026");
|
|
530
|
-
const metadata = await fetch(
|
|
531
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/metadata.json?download=true`,
|
|
532
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
533
|
-
).then((r) => r.ok ? r.json() : []).catch(() => []);
|
|
534
|
-
const snapshot = {
|
|
535
|
-
metadata: Array.isArray(metadata) ? metadata : [],
|
|
536
|
-
tags: tags?.by_category ? tags : { by_category: {}, all: [] },
|
|
537
|
-
meta: { consolidatedAt: Date.now(), version: 1 }
|
|
538
|
-
};
|
|
539
|
-
log(`Erstelle state.zip (${snapshot.metadata.length} Bilder, ${snapshot.tags.all?.length ?? 0} Tags) \u2026`);
|
|
540
|
-
const zip = new import_jszip2.default();
|
|
541
|
-
zip.file("metadata.json", JSON.stringify(snapshot.metadata, null, 2));
|
|
542
|
-
zip.file("tags.json", JSON.stringify(snapshot.tags, null, 2));
|
|
543
|
-
zip.file("state_meta.json", JSON.stringify(snapshot.meta, null, 2));
|
|
544
|
-
const zipBytes = await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
|
|
545
|
-
const ts = snapshot.meta.consolidatedAt;
|
|
546
|
-
const stateFilename = `state-${new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-")}.zip`;
|
|
547
|
-
const repoPath = `${namespace}${stateFilename}`;
|
|
548
|
-
log("Lade state.zip hoch \u2026");
|
|
549
|
-
await (0, import_hub.uploadFile)({
|
|
550
|
-
repo: { type: "dataset", name: HF_REPO },
|
|
551
|
-
credentials: { accessToken: token },
|
|
552
|
-
file: { path: repoPath, content: new Blob([zipBytes.buffer], { type: "application/zip" }) },
|
|
553
|
-
branch: "main",
|
|
554
|
-
commitTitle: `Bootstrap: initialer State aus Legacy-Daten`
|
|
555
|
-
});
|
|
556
|
-
log(`Fertig \u2014 ${stateFilename}`);
|
|
557
|
-
}
|
|
558
|
-
async function hfListProjects(token) {
|
|
559
|
-
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/tree/main`, {
|
|
560
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
561
|
-
});
|
|
562
|
-
if (!res.ok) throw new Error(`HF list failed: ${res.status} ${res.statusText}`);
|
|
563
|
-
const files = await res.json();
|
|
564
|
-
return files.filter((f) => f.type === "file" && f.path.endsWith(".zip")).map((f) => ({
|
|
565
|
-
id: f.path.replace(/\.zip$/, ""),
|
|
566
|
-
name: f.path.replace(/\.zip$/, ""),
|
|
567
|
-
path: f.path,
|
|
568
|
-
size: f.size,
|
|
569
|
-
isLfs: !!f.lfs
|
|
570
|
-
}));
|
|
571
|
-
}
|
|
572
|
-
async function hfDownloadProject(path, token) {
|
|
573
|
-
const res = await fetch(
|
|
574
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${path}?download=true`,
|
|
575
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
576
|
-
);
|
|
577
|
-
if (!res.ok) throw new Error(`HF download failed: ${res.status} ${res.statusText}`);
|
|
578
|
-
const blob = await res.blob();
|
|
579
|
-
return new File([blob], path, { type: "application/zip" });
|
|
580
|
-
}
|
|
581
|
-
async function hfUploadProject(zipBase64, name, token) {
|
|
582
|
-
const filename = name.endsWith(".zip") ? name : `${name}.zip`;
|
|
583
|
-
const binary = atob(zipBase64);
|
|
584
|
-
const bytes = new Uint8Array(binary.length);
|
|
585
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
586
|
-
await (0, import_hub.uploadFile)({
|
|
587
|
-
repo: { type: "dataset", name: HF_REPO },
|
|
588
|
-
credentials: { accessToken: token },
|
|
589
|
-
file: { path: filename, content: new Blob([bytes], { type: "application/zip" }) },
|
|
590
|
-
branch: "main",
|
|
591
|
-
commitTitle: `Upload ${filename}`
|
|
592
|
-
});
|
|
593
|
-
return { id: filename.replace(/\.zip$/, ""), name: filename.replace(/\.zip$/, ""), path: filename, size: bytes.length, isLfs: true };
|
|
594
|
-
}
|
|
595
|
-
async function hfUploadProjectForm(_zipBase64, _name, _token) {
|
|
596
|
-
throw new Error("hfUploadProjectForm is deprecated. Use hfUploadProject instead.");
|
|
597
|
-
}
|
|
598
|
-
async function hfDeleteProject(path, token) {
|
|
599
|
-
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
|
|
600
|
-
method: "POST",
|
|
601
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
|
|
602
|
-
body: [
|
|
603
|
-
JSON.stringify({ key: "header", value: { summary: `Delete ${path}`, description: "" } }),
|
|
604
|
-
JSON.stringify({ key: "deletedFile", value: { path } })
|
|
605
|
-
].join("\n")
|
|
606
|
-
});
|
|
607
|
-
if (!res.ok) throw new Error(`HF delete failed: ${res.status} ${res.statusText}`);
|
|
608
|
-
}
|
|
609
|
-
async function hfSaveTags(_workspaceTags, _token) {
|
|
610
|
-
throw new Error("hfSaveTags is deprecated. Use writeHFEvent instead.");
|
|
611
|
-
}
|
|
612
|
-
async function hfLoadTags(token) {
|
|
613
|
-
try {
|
|
614
|
-
const res = await fetch(
|
|
615
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/tags.json?download=true`,
|
|
616
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
617
|
-
);
|
|
618
|
-
if (!res.ok) return null;
|
|
619
|
-
return res.json();
|
|
620
|
-
} catch {
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
async function hfSaveMetadata(_entries, _token) {
|
|
625
|
-
throw new Error("hfSaveMetadata is deprecated. Use writeHFEvent instead.");
|
|
626
|
-
}
|
|
627
|
-
async function hfLoadMetadata(token) {
|
|
628
|
-
try {
|
|
629
|
-
const res = await fetch(
|
|
630
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/metadata.json?download=true`,
|
|
631
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
632
|
-
);
|
|
633
|
-
if (!res.ok) return [];
|
|
634
|
-
return res.json();
|
|
635
|
-
} catch {
|
|
636
|
-
return [];
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
async function hfUploadImage(base64, id, token, mimeType = "image/jpeg") {
|
|
640
|
-
const ext = bildEndungFuer(mimeType);
|
|
641
|
-
const binary = atob(base64);
|
|
642
|
-
const bytes = new Uint8Array(binary.length);
|
|
643
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
644
|
-
await (0, import_hub.uploadFile)({
|
|
645
|
-
repo: { type: "dataset", name: HF_REPO },
|
|
646
|
-
credentials: { accessToken: token },
|
|
647
|
-
file: { path: `images/${id}.${ext}`, content: new Blob([bytes], { type: mimeType }) },
|
|
648
|
-
branch: "main",
|
|
649
|
-
commitTitle: `Add image ${id}`
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
async function hfLoadImageAsBase64(id, token, namespace, filename, on404, mimeType, hasThumb) {
|
|
653
|
-
const fetchBase64 = async (repoPath) => {
|
|
654
|
-
try {
|
|
655
|
-
const res = await fetch(
|
|
656
|
-
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
|
|
657
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
658
|
-
);
|
|
659
|
-
if (!res.ok) {
|
|
660
|
-
if (on404 && on404()) return "ABORT";
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
const blob = await res.blob();
|
|
664
|
-
return new Promise((resolve, reject) => {
|
|
665
|
-
const reader = new FileReader();
|
|
666
|
-
reader.onload = () => resolve(reader.result.split(",")[1]);
|
|
667
|
-
reader.onerror = reject;
|
|
668
|
-
reader.readAsDataURL(blob);
|
|
669
|
-
});
|
|
670
|
-
} catch {
|
|
671
|
-
return null;
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
|
-
if (filename) {
|
|
675
|
-
if (hasThumb) {
|
|
676
|
-
const thumb = await fetchBase64(`thumbs/${filename}`);
|
|
677
|
-
if (thumb && thumb !== "ABORT") return thumb;
|
|
678
|
-
}
|
|
679
|
-
const result2 = await fetchBase64(`images/${filename}`);
|
|
680
|
-
return result2 === "ABORT" ? null : result2;
|
|
681
|
-
}
|
|
682
|
-
const ext = bildEndungFuer(mimeType);
|
|
683
|
-
const result = await fetchBase64(`images/${id}.${ext}`);
|
|
684
|
-
return result === "ABORT" ? null : result;
|
|
685
|
-
}
|
|
686
|
-
var import_jszip2, import_hub, HF_TOKEN_KEY;
|
|
687
|
-
var init_hfStateService = __esm({
|
|
688
|
-
"src/lib/hfStateService.ts"() {
|
|
689
|
-
"use strict";
|
|
690
|
-
import_jszip2 = __toESM(require("jszip"));
|
|
691
|
-
import_hub = require("@huggingface/hub");
|
|
692
|
-
init_hfEventTypes();
|
|
693
|
-
init_hfEventLog();
|
|
694
|
-
init_hfEventLog();
|
|
695
|
-
HF_TOKEN_KEY = "hf-token";
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
// src/index.ts
|
|
700
|
-
var index_exports = {};
|
|
701
|
-
__export(index_exports, {
|
|
702
|
-
AvatarArchitectApp: () => AvatarArchitectApp,
|
|
703
|
-
CURRENT_TAG_EVENT_VERSION: () => CURRENT_TAG_EVENT_VERSION,
|
|
704
|
-
CollapsibleCard: () => CollapsibleCard,
|
|
705
|
-
CompactDropdown: () => CompactDropdown,
|
|
706
|
-
FaApp: () => FaApp,
|
|
707
|
-
FaToolsBadge: () => FaToolsBadge,
|
|
708
|
-
FlowWorkspacePicker: () => FlowWorkspacePicker,
|
|
709
|
-
GLOBAL_STYLES: () => GLOBAL_STYLES,
|
|
710
|
-
HistoryPanel: () => HistoryPanel,
|
|
711
|
-
InspectPanel: () => InspectPanel,
|
|
712
|
-
LIBRARY_CUTOVER_MARKER: () => LIBRARY_CUTOVER_MARKER,
|
|
713
|
-
LIBRARY_SLICE_SCHEMA_VERSION: () => LIBRARY_SLICE_SCHEMA_VERSION,
|
|
714
|
-
LIB_VERSION: () => version,
|
|
715
|
-
LabBlend: () => LabBlend,
|
|
716
|
-
LabCompare: () => LabCompare,
|
|
717
|
-
LabFrameExtractor: () => LabFrameExtractor,
|
|
718
|
-
LabImagePicker: () => LabImagePicker,
|
|
719
|
-
LabLoop: () => LabLoop,
|
|
720
|
-
LabRemix: () => LabRemix,
|
|
721
|
-
LabsTab: () => LabsTab,
|
|
722
|
-
LibraryView: () => LibraryView,
|
|
723
|
-
ListView: () => ListView,
|
|
724
|
-
MediaLibrary: () => MediaLibrary,
|
|
725
|
-
PillButton: () => PillButton,
|
|
726
|
-
ProjectSyncTab: () => ProjectSyncTab,
|
|
727
|
-
PromptTab: () => PromptTab,
|
|
728
|
-
SCOPE_MAP: () => SCOPE_MAP,
|
|
729
|
-
SectionLabel: () => SectionLabel,
|
|
730
|
-
ServerTab: () => ServerTab,
|
|
731
|
-
SetupPanel: () => SetupPanel,
|
|
732
|
-
TAGS_CUTOVER_MARKER: () => TAGS_CUTOVER_MARKER,
|
|
733
|
-
TAGS_SLICE_SCHEMA_VERSION: () => TAGS_SLICE_SCHEMA_VERSION,
|
|
734
|
-
TagBaseSection: () => TagBaseSection,
|
|
735
|
-
TagManager: () => TagManager,
|
|
736
|
-
TagManagerPanel: () => TagManagerPanel,
|
|
737
|
-
autoLabel: () => autoLabel,
|
|
738
|
-
buildBlendInstruction: () => buildBlendInstruction,
|
|
739
|
-
buildCompareInstruction: () => buildCompareInstruction,
|
|
740
|
-
buildFallbackPrompt: () => buildFallbackPrompt,
|
|
741
|
-
buildGenerationPrompt: () => buildGenerationPrompt,
|
|
742
|
-
buildImageGenerationOptions: () => buildImageGenerationOptions,
|
|
743
|
-
buildLoopInstruction: () => buildLoopInstruction,
|
|
744
|
-
buildPromptTabPayload: () => buildPromptTabPayload,
|
|
745
|
-
buildReferenceImageMediaIds: () => buildReferenceImageMediaIds,
|
|
746
|
-
buildRemixInstruction: () => buildRemixInstruction,
|
|
747
|
-
buildScanInstruction: () => buildScanInstruction,
|
|
748
|
-
cleanAiResponse: () => cleanAiResponse,
|
|
749
|
-
createFaStore: () => createFaStore,
|
|
750
|
-
createFlowServices: () => createFlowServices,
|
|
751
|
-
createInMemoryLibraryDataSource: () => createInMemoryLibraryDataSource,
|
|
752
|
-
createInMemoryWorkspacePersistence: () => createInMemoryWorkspacePersistence,
|
|
753
|
-
createLibraryDefault: () => createLibraryDefault,
|
|
754
|
-
createPersistenceCoordinator: () => createPersistenceCoordinator,
|
|
755
|
-
createTagCreatedEvent: () => createTagCreatedEvent,
|
|
756
|
-
createTagUpdatedEvent: () => createTagUpdatedEvent,
|
|
757
|
-
createTagsDefault: () => createTagsDefault,
|
|
758
|
-
deriveTagBaseWorkspaceTags: () => deriveTagBaseWorkspaceTags,
|
|
759
|
-
deriveTagViewModel: () => deriveTagViewModel,
|
|
760
|
-
exportProjectToZip: () => exportProjectToZip,
|
|
761
|
-
formatTreeToMarkdown: () => formatTreeToMarkdown,
|
|
762
|
-
frameToGeneration: () => frameToGeneration,
|
|
763
|
-
getFormattedTimestamp: () => getFormattedTimestamp,
|
|
764
|
-
getHFToken: () => getHFToken,
|
|
765
|
-
getSessionClientId: () => getSessionClientId,
|
|
766
|
-
groupGenerationsToLabItems: () => groupGenerationsToLabItems,
|
|
767
|
-
hfBatchArchive: () => hfBatchArchive,
|
|
768
|
-
hfBootstrapFromLegacy: () => hfBootstrapFromLegacy,
|
|
769
|
-
hfDeleteProject: () => hfDeleteProject,
|
|
770
|
-
hfDownloadProject: () => hfDownloadProject,
|
|
771
|
-
hfListDir: () => hfListDir,
|
|
772
|
-
hfListProjects: () => hfListProjects,
|
|
773
|
-
hfLoadImageAsBase64: () => hfLoadImageAsBase64,
|
|
774
|
-
hfUploadImage: () => hfUploadImage,
|
|
775
|
-
hfUploadProjectForm: () => hfUploadProjectForm,
|
|
776
|
-
hfUploadSmallFile: () => hfUploadSmallFile,
|
|
777
|
-
importProjectFromZip: () => importProjectFromZip,
|
|
778
|
-
injectXMPMetadata: () => injectXMPMetadata,
|
|
779
|
-
interpretSdkError: () => interpretSdkError,
|
|
780
|
-
libraryContract: () => libraryContract,
|
|
781
|
-
libraryReactDescriptor: () => libraryReactDescriptor,
|
|
782
|
-
listCategories: () => listCategories,
|
|
783
|
-
loadHFState: () => loadHFState,
|
|
784
|
-
loadPendingEvents: () => loadPendingEvents,
|
|
785
|
-
loadTagBase: () => loadTagBase,
|
|
786
|
-
parseLegacyLibraryFilters: () => parseLegacyLibraryFilters,
|
|
787
|
-
parseLegacyRecentTags: () => parseLegacyRecentTags,
|
|
788
|
-
parsePromptFile: () => parsePromptFile,
|
|
789
|
-
parsePromptResponse: () => parsePromptResponse,
|
|
790
|
-
runLegacyMigration: () => runLegacyMigration,
|
|
791
|
-
runLibraryLegacyImport: () => runLibraryLegacyImport,
|
|
792
|
-
runTagsLegacyImport: () => runTagsLegacyImport,
|
|
793
|
-
setHFToken: () => setHFToken,
|
|
794
|
-
tagsContract: () => tagsContract,
|
|
795
|
-
tagsReactDescriptor: () => tagsReactDescriptor,
|
|
796
|
-
tsFromEventPath: () => tsFromEventPath,
|
|
797
|
-
useFaSlice: () => useFaSlice,
|
|
798
|
-
useFaSliceMeta: () => useFaSliceMeta,
|
|
799
|
-
useHFState: () => useHFState,
|
|
800
|
-
useKeyboardNavigation: () => useKeyboardNavigation,
|
|
801
|
-
useOnClickOutside: () => useOnClickOutside,
|
|
802
|
-
useTagBase: () => useTagBase,
|
|
803
|
-
validateTagBaseSelection: () => validateTagBaseSelection,
|
|
804
|
-
writeHFEvent: () => writeHFEvent,
|
|
805
|
-
writeTagEvent: () => writeTagEvent
|
|
806
|
-
});
|
|
807
|
-
module.exports = __toCommonJS(index_exports);
|
|
808
|
-
|
|
809
|
-
// package.json
|
|
810
|
-
var version = "5.1.0";
|
|
811
|
-
|
|
812
|
-
// src/hooks/useOnClickOutside.ts
|
|
813
|
-
var import_react = require("react");
|
|
814
|
-
function useOnClickOutside(ref, handler) {
|
|
815
|
-
(0, import_react.useEffect)(() => {
|
|
816
|
-
const listener = (event) => {
|
|
817
|
-
if (!ref.current || ref.current.contains(event.target)) return;
|
|
818
|
-
handler(event);
|
|
819
|
-
};
|
|
820
|
-
document.addEventListener("mousedown", listener);
|
|
821
|
-
document.addEventListener("touchstart", listener);
|
|
822
|
-
return () => {
|
|
823
|
-
document.removeEventListener("mousedown", listener);
|
|
824
|
-
document.removeEventListener("touchstart", listener);
|
|
825
|
-
};
|
|
826
|
-
}, [ref, handler]);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// src/hooks/useKeyboardNavigation.ts
|
|
830
|
-
var import_react2 = require("react");
|
|
831
|
-
function useKeyboardNavigation(history, currentResult, setCurrentResult) {
|
|
832
|
-
(0, import_react2.useEffect)(() => {
|
|
833
|
-
const handleKeyDown = (e) => {
|
|
834
|
-
if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
|
|
835
|
-
if (!currentResult || history.length === 0) return;
|
|
836
|
-
const currentIndex = history.findIndex((h) => h.id === currentResult.id);
|
|
837
|
-
if (e.key === "ArrowRight" && currentIndex < history.length - 1) {
|
|
838
|
-
setCurrentResult(history[currentIndex + 1]);
|
|
839
|
-
} else if (e.key === "ArrowLeft" && currentIndex > 0) {
|
|
840
|
-
setCurrentResult(history[currentIndex - 1]);
|
|
841
|
-
}
|
|
842
|
-
};
|
|
843
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
844
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
845
|
-
}, [currentResult, history, setCurrentResult]);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// src/features/szenen/szenenViewStyles.generated.ts
|
|
849
|
-
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.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\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/* 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/* ==========================================================================\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';
|
|
850
|
-
|
|
851
|
-
// src/lib/utils.ts
|
|
852
|
-
var getFormattedTimestamp = () => {
|
|
853
|
-
const d = /* @__PURE__ */ new Date();
|
|
854
|
-
const pad = (n) => n.toString().padStart(2, "0");
|
|
855
|
-
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
856
|
-
};
|
|
857
|
-
var GLOBAL_STYLES = `
|
|
858
|
-
.dark-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
|
|
859
|
-
.dark-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
|
860
|
-
.dark-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
|
|
861
|
-
html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; background: #0e0e0e; font-family: 'Google Sans Text', sans-serif; overflow: hidden; }
|
|
862
|
-
@keyframes prompt-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
|
863
|
-
.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; }
|
|
864
|
-
@keyframes dropdown-enter { from { opacity: 0; transform: scale(0.95) translateY(-5px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
|
865
|
-
.animate-dropdown { animation: dropdown-enter 0.1s ease-out forwards; }
|
|
866
|
-
.flow-szenen-entry { display: grid; gap: 6px; width: 100%; max-width: 280px; text-align: center; }
|
|
867
|
-
.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; }
|
|
868
|
-
.flow-szenen-entry button { display: flex; align-items: center; justify-content: center; gap: 8px; }
|
|
869
|
-
.flow-szenen-entry button:disabled { color: rgba(255,255,255,.3); background: transparent; border-color: rgba(255,255,255,.1); cursor: not-allowed; }
|
|
870
|
-
.flow-szenen-entry small { color: rgba(255,255,255,.35); font-size: 10px; }
|
|
871
|
-
.flow-workspace-picker { position: fixed; inset: 0; display: grid; place-items: center; padding: 16px; color: #f5f5f5; background: #050505; overflow: auto; }
|
|
872
|
-
.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); }
|
|
873
|
-
.flow-workspace-picker__card header { display: grid; gap: 4px; }
|
|
874
|
-
.flow-workspace-picker__card header strong { font-size: 22px; }
|
|
875
|
-
.flow-workspace-picker__card header small, .flow-workspace-picker__choices small { color: #a3a3a3; }
|
|
876
|
-
.flow-workspace-picker__choices { display: grid; gap: 8px; }
|
|
877
|
-
.flow-workspace-picker__choices button { display: flex; justify-content: space-between; align-items: center; min-height: 46px; padding: 10px 12px; color: #f5f5f5; background: #050505; border: 1px solid #404040; border-radius: 8px; font: inherit; cursor: pointer; }
|
|
878
|
-
.flow-workspace-picker__choices button:hover { border-color: #3b82f6; background: #262626; }
|
|
879
|
-
.flow-workspace-picker__back { justify-self: start; padding: 0; color: #a3a3a3; background: transparent; border: 0; font: inherit; cursor: pointer; }
|
|
880
|
-
.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; }
|
|
881
|
-
.flow-szenen-shell {
|
|
882
|
-
--sz-bg: #0e0e0e; --sz-surface: #171717; --sz-elevated: #262626;
|
|
883
|
-
--sz-border: #404040; --sz-text: #f5f5f5; --sz-muted: #a3a3a3;
|
|
884
|
-
--color-bg-app: #000; --color-bg-surface: #0a0a0a; --color-bg-surface-hover: #171717; --color-bg-elevated: #171717;
|
|
885
|
-
--color-border: #262626; --color-border-hover: #404040;
|
|
886
|
-
--color-text-primary: #fff; --color-text-secondary: #a3a3a3; --color-text-tertiary: #525252;
|
|
887
|
-
--color-accent-blue: #3b82f6; --color-accent-blue-dim: rgba(59,130,246,.2);
|
|
888
|
-
--color-accent-purple: #a855f7; --color-accent-green: #22c55e; --color-accent-green-dim: rgba(34,197,94,.2);
|
|
889
|
-
--color-accent-red: #ef4444; --color-accent-red-dim: rgba(239,68,68,.2);
|
|
890
|
-
--color-success: #22c55e; --color-warning: #eab308; --color-danger: #ef4444;
|
|
891
|
-
--space-xs: .25rem; --space-sm: .5rem; --space-md: 1rem;
|
|
892
|
-
--radius-sm: .25rem; --radius-md: .5rem; --radius-lg: .75rem; --radius-full: 9999px;
|
|
893
|
-
--font-sans: system-ui, -apple-system, sans-serif;
|
|
894
|
-
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
895
|
-
position: fixed; inset: 0; display: flex; flex-direction: column;
|
|
896
|
-
color: var(--sz-text); background: var(--sz-bg); overflow: hidden;
|
|
897
|
-
}
|
|
898
|
-
.flow-szenen-shell__header { display: flex; align-items: center; gap: 16px; min-height: 58px; padding: 8px 16px; background: #050505; border-bottom: 1px solid var(--sz-border); }
|
|
899
|
-
.flow-szenen-shell__header button { min-height: 36px; background: var(--sz-elevated); border-color: var(--sz-border); }
|
|
900
|
-
.flow-szenen-shell__header div { display: grid; gap: 2px; }
|
|
901
|
-
.flow-szenen-shell__header small { color: var(--sz-muted); font-family: monospace; }
|
|
902
|
-
.flow-szenen-shell__content { flex: 1; min-height: 0; padding: 16px; overflow: auto; }
|
|
903
|
-
@media (max-width: 760px) { .flow-szenen-shell__content { padding: 8px; } }
|
|
904
|
-
` + SZENEN_VIEW_STYLES;
|
|
905
|
-
|
|
906
|
-
// src/index.ts
|
|
907
|
-
init_metadata();
|
|
908
|
-
init_project();
|
|
909
|
-
|
|
910
|
-
// src/lib/aiHelpers.ts
|
|
911
|
-
function buildGenerationPrompt(hierarchyText, mode = "creative") {
|
|
912
|
-
const safe = hierarchyText.length > 2500 ? hierarchyText.slice(0, 2500) + "..." : hierarchyText;
|
|
913
|
-
if (mode === "literal") {
|
|
914
|
-
return `Translate this hierarchy into a literal image generation prompt. Output ONLY the raw prompt string. No chat.
|
|
915
|
-
|
|
916
|
-
HIERARCHY:
|
|
917
|
-
${safe}`;
|
|
918
|
-
}
|
|
919
|
-
return `Create a cinematic, high-quality image generation prompt based on this hierarchy. Translate everything to English. Output ONLY the prompt string. No chat.
|
|
920
|
-
|
|
921
|
-
HIERARCHY:
|
|
922
|
-
${safe}`;
|
|
923
|
-
}
|
|
924
|
-
function buildFallbackPrompt(hierarchyText) {
|
|
925
|
-
const lines = hierarchyText.split("\n");
|
|
926
|
-
const firstConcept = lines.find((l) => l.includes("-"))?.replace(/^[-\s]+/, "") || "character concept";
|
|
927
|
-
return `Cinematic professional photograph of ${firstConcept}, masterpiece, highly detailed, fine art style`;
|
|
928
|
-
}
|
|
929
|
-
function cleanAiResponse(text) {
|
|
930
|
-
return text.replace(/^["']|["']$/g, "").replace(/```/g, "").trim();
|
|
931
|
-
}
|
|
932
|
-
function buildImageGenerationOptions(prompt, aspectRatio, model, referenceMediaId) {
|
|
933
|
-
const options = {
|
|
934
|
-
prompt: prompt.replace(/\n/g, " ").trim().slice(0, 1500),
|
|
935
|
-
aspectRatio,
|
|
936
|
-
modelDisplayName: model
|
|
937
|
-
};
|
|
938
|
-
if (referenceMediaId && referenceMediaId.length > 5) {
|
|
939
|
-
options.referenceImageMediaIds = [referenceMediaId];
|
|
940
|
-
}
|
|
941
|
-
return options;
|
|
381
|
+
return options;
|
|
942
382
|
}
|
|
943
383
|
var CONTENT_FIRST = ["subject", "clothing", "environment", "mood", "action", "content", "general"];
|
|
944
384
|
var RULES_LAST = ["system", "rules", "safety", "generated", "source", "status", "type", "model"];
|
|
@@ -2812,383 +2252,54 @@ var TaskExecutor = ({ openTasks, onExecute, onTaskCompleted }) => {
|
|
|
2812
2252
|
] });
|
|
2813
2253
|
};
|
|
2814
2254
|
|
|
2815
|
-
// src/components/
|
|
2816
|
-
var import_react17 = require("react");
|
|
2817
|
-
|
|
2818
|
-
var
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
onRefreshServerProjects,
|
|
2830
|
-
onComputeSyncDiff,
|
|
2831
|
-
onExecuteSync,
|
|
2832
|
-
hfToken,
|
|
2833
|
-
onHfLoad,
|
|
2834
|
-
onProjectExportBase64,
|
|
2835
|
-
onHfInitialSync
|
|
2836
|
-
}) => {
|
|
2837
|
-
const projectInputRef = (0, import_react17.useRef)(null);
|
|
2838
|
-
const workspaceInputRef = (0, import_react17.useRef)(null);
|
|
2839
|
-
const [saveName, setSaveName] = (0, import_react17.useState)("");
|
|
2840
|
-
const [isSaving, setIsSaving] = (0, import_react17.useState)(false);
|
|
2841
|
-
const [isExporting, setIsExporting] = (0, import_react17.useState)(false);
|
|
2842
|
-
const [syncState, setSyncState] = (0, import_react17.useState)("idle");
|
|
2843
|
-
const [syncDiff, setSyncDiff] = (0, import_react17.useState)(null);
|
|
2844
|
-
const [selectedLocalIds, setSelectedLocalIds] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
|
|
2845
|
-
const handleExport = async () => {
|
|
2846
|
-
if (!onProjectExport) return;
|
|
2847
|
-
setIsExporting(true);
|
|
2848
|
-
try {
|
|
2849
|
-
await onProjectExport();
|
|
2850
|
-
} finally {
|
|
2851
|
-
setIsExporting(false);
|
|
2852
|
-
}
|
|
2853
|
-
};
|
|
2854
|
-
const handleComputeDiff = async () => {
|
|
2855
|
-
if (!onComputeSyncDiff) return;
|
|
2856
|
-
setSyncState("computing");
|
|
2857
|
-
setSyncDiff(null);
|
|
2858
|
-
try {
|
|
2859
|
-
const diff = await onComputeSyncDiff();
|
|
2860
|
-
setSyncDiff(diff);
|
|
2861
|
-
setSelectedLocalIds(new Set(diff.localOnly.map((g) => g.id)));
|
|
2862
|
-
setSyncState("ready");
|
|
2863
|
-
} catch {
|
|
2864
|
-
setSyncState("error");
|
|
2865
|
-
setTimeout(() => setSyncState("idle"), 4e3);
|
|
2866
|
-
}
|
|
2867
|
-
};
|
|
2868
|
-
const handleExecuteSync = async () => {
|
|
2869
|
-
if (!onExecuteSync || !syncDiff) return;
|
|
2870
|
-
setSyncState("executing");
|
|
2871
|
-
try {
|
|
2872
|
-
await onExecuteSync([...selectedLocalIds]);
|
|
2873
|
-
setSyncState("done");
|
|
2874
|
-
setSyncDiff(null);
|
|
2875
|
-
setTimeout(() => setSyncState("idle"), 3e3);
|
|
2876
|
-
} catch {
|
|
2877
|
-
setSyncState("error");
|
|
2878
|
-
setTimeout(() => setSyncState("idle"), 4e3);
|
|
2879
|
-
}
|
|
2880
|
-
};
|
|
2881
|
-
const toggleLocalId = (id) => {
|
|
2882
|
-
setSelectedLocalIds((prev) => {
|
|
2883
|
-
const next = new Set(prev);
|
|
2884
|
-
if (next.has(id)) next.delete(id);
|
|
2885
|
-
else next.add(id);
|
|
2886
|
-
return next;
|
|
2887
|
-
});
|
|
2888
|
-
};
|
|
2889
|
-
const isWorking = projectActionState === "working" || projectActionState === "working-full";
|
|
2890
|
-
const [hfProjects, setHfProjects] = (0, import_react17.useState)([]);
|
|
2891
|
-
const [hfLoading, setHfLoading] = (0, import_react17.useState)(false);
|
|
2892
|
-
const [hfSaving, setHfSaving] = (0, import_react17.useState)(false);
|
|
2893
|
-
const [hfError, setHfError] = (0, import_react17.useState)(null);
|
|
2894
|
-
const [hfSaveName, setHfSaveName] = (0, import_react17.useState)("");
|
|
2895
|
-
const [hfSyncProgress, setHfSyncProgress] = (0, import_react17.useState)(null);
|
|
2896
|
-
const [hfSyncing, setHfSyncing] = (0, import_react17.useState)(false);
|
|
2897
|
-
const loadHfProjects = async (token) => {
|
|
2898
|
-
setHfLoading(true);
|
|
2899
|
-
setHfError(null);
|
|
2900
|
-
try {
|
|
2901
|
-
setHfProjects(await hfListProjects(token));
|
|
2902
|
-
} catch (e) {
|
|
2903
|
-
setHfError(e.message);
|
|
2904
|
-
} finally {
|
|
2905
|
-
setHfLoading(false);
|
|
2906
|
-
}
|
|
2907
|
-
};
|
|
2908
|
-
(0, import_react17.useEffect)(() => {
|
|
2909
|
-
if (hfToken) loadHfProjects(hfToken);
|
|
2910
|
-
}, [hfToken]);
|
|
2911
|
-
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "absolute inset-0 overflow-y-auto dark-scrollbar", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "p-6 flex flex-col gap-8", children: [
|
|
2912
|
-
topSlot && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: topSlot }),
|
|
2913
|
-
(onProjectExport || onProjectImport || onWorkspaceImport) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
2914
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Projekt-ZIP" }),
|
|
2915
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
2916
|
-
onProjectExport && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "filled", icon: "download", loading: isExporting || isWorking, onClick: handleExport, children: "Exportieren (.zip)" }),
|
|
2917
|
-
onProjectImport && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
|
|
2918
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "outline", icon: "upload", onClick: () => projectInputRef.current?.click(), children: "ZIP laden" }),
|
|
2919
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2920
|
-
"input",
|
|
2921
|
-
{
|
|
2922
|
-
ref: projectInputRef,
|
|
2923
|
-
type: "file",
|
|
2924
|
-
accept: ".zip",
|
|
2925
|
-
className: "hidden",
|
|
2926
|
-
onChange: (e) => {
|
|
2927
|
-
const f = e.target.files?.[0];
|
|
2928
|
-
if (f) onProjectImport(f);
|
|
2929
|
-
e.target.value = "";
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
)
|
|
2933
|
-
] })
|
|
2934
|
-
] }),
|
|
2935
|
-
onWorkspaceImport && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
|
|
2936
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "outline", icon: "upload_file", onClick: () => workspaceInputRef.current?.click(), children: "Tags laden (.json)" }),
|
|
2937
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2938
|
-
"input",
|
|
2939
|
-
{
|
|
2940
|
-
ref: workspaceInputRef,
|
|
2941
|
-
type: "file",
|
|
2942
|
-
accept: ".json",
|
|
2943
|
-
className: "hidden",
|
|
2944
|
-
onChange: (e) => {
|
|
2945
|
-
const f = e.target.files?.[0];
|
|
2946
|
-
if (f) onWorkspaceImport(f);
|
|
2947
|
-
e.target.value = "";
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
)
|
|
2951
|
-
] })
|
|
2952
|
-
] }),
|
|
2953
|
-
(onServerSave || serverProjects || onServerDelete) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
2954
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Server-Speicher" }),
|
|
2955
|
-
onServerSave && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex gap-2", children: [
|
|
2956
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2957
|
-
"input",
|
|
2958
|
-
{
|
|
2959
|
-
type: "text",
|
|
2960
|
-
value: saveName,
|
|
2961
|
-
onChange: (e) => setSaveName(e.target.value),
|
|
2962
|
-
placeholder: "Name (optional)",
|
|
2963
|
-
className: "flex-1 bg-white/5 border border-white/10 rounded-xl px-3 text-[12px] text-white/70 outline-none placeholder:text-white/20",
|
|
2964
|
-
style: { height: 40 }
|
|
2965
|
-
}
|
|
2966
|
-
),
|
|
2967
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2968
|
-
PillButton,
|
|
2969
|
-
{
|
|
2970
|
-
variant: "filled",
|
|
2971
|
-
icon: "cloud_upload",
|
|
2972
|
-
loading: isSaving,
|
|
2973
|
-
onClick: async () => {
|
|
2974
|
-
setIsSaving(true);
|
|
2975
|
-
try {
|
|
2976
|
-
await onServerSave(saveName || void 0);
|
|
2977
|
-
setSaveName("");
|
|
2978
|
-
await onRefreshServerProjects?.();
|
|
2979
|
-
} finally {
|
|
2980
|
-
setIsSaving(false);
|
|
2981
|
-
}
|
|
2982
|
-
},
|
|
2983
|
-
children: "Speichern"
|
|
2984
|
-
}
|
|
2985
|
-
)
|
|
2986
|
-
] }),
|
|
2987
|
-
(!serverProjects || serverProjects.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/20 px-2", children: "Noch nichts gespeichert." }),
|
|
2988
|
-
serverProjects && serverProjects.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex flex-col gap-1", children: serverProjects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/5", children: [
|
|
2989
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
2990
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[11px] text-white/70 font-bold truncate", children: p.name }),
|
|
2991
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/25", children: [
|
|
2992
|
-
new Date(p.saved_at).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }),
|
|
2993
|
-
" \xB7 ",
|
|
2994
|
-
(p.size / 1024).toFixed(0),
|
|
2995
|
-
" KB"
|
|
2996
|
-
] })
|
|
2997
|
-
] }),
|
|
2998
|
-
onServerLoad && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { onClick: () => onServerLoad(p.id), className: "w-8 h-8 flex items-center justify-center text-white/30 active:text-white transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "cloud_download" }) }),
|
|
2999
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { onClick: () => onServerDelete?.(p.id), className: "w-8 h-8 flex items-center justify-center text-white/20 active:text-red-400 transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "delete" }) })
|
|
3000
|
-
] }, p.id)) })
|
|
3001
|
-
] }),
|
|
3002
|
-
hfToken && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
3003
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
3004
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Hugging Face" }),
|
|
3005
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { onClick: () => loadHfProjects(hfToken), className: "text-white/30 hover:text-white/60 transition", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: `material-symbols-outlined text-[18px]${hfLoading ? " animate-spin" : ""}`, children: "refresh" }) })
|
|
3006
|
-
] }),
|
|
3007
|
-
onProjectExportBase64 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex gap-2", children: [
|
|
3008
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3009
|
-
"input",
|
|
3010
|
-
{
|
|
3011
|
-
type: "text",
|
|
3012
|
-
value: hfSaveName,
|
|
3013
|
-
onChange: (e) => setHfSaveName(e.target.value),
|
|
3014
|
-
placeholder: "Name (optional)",
|
|
3015
|
-
className: "flex-1 bg-white/5 border border-white/10 rounded-xl px-3 text-[12px] text-white/70 outline-none placeholder:text-white/20",
|
|
3016
|
-
style: { height: 40 }
|
|
3017
|
-
}
|
|
3018
|
-
),
|
|
3019
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3020
|
-
PillButton,
|
|
3021
|
-
{
|
|
3022
|
-
variant: "filled",
|
|
3023
|
-
icon: "cloud_upload",
|
|
3024
|
-
loading: hfSaving,
|
|
3025
|
-
onClick: async () => {
|
|
3026
|
-
if (!onProjectExportBase64) return;
|
|
3027
|
-
setHfSaving(true);
|
|
3028
|
-
setHfError(null);
|
|
3029
|
-
try {
|
|
3030
|
-
const base64 = await onProjectExportBase64();
|
|
3031
|
-
const name = hfSaveName.trim() || `project_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", "_").replace(":", "")}`;
|
|
3032
|
-
await hfUploadProjectForm(base64, name, hfToken);
|
|
3033
|
-
setHfSaveName("");
|
|
3034
|
-
await loadHfProjects(hfToken);
|
|
3035
|
-
} catch (e) {
|
|
3036
|
-
setHfError(e.message);
|
|
3037
|
-
} finally {
|
|
3038
|
-
setHfSaving(false);
|
|
3039
|
-
}
|
|
3040
|
-
},
|
|
3041
|
-
children: "Speichern"
|
|
3042
|
-
}
|
|
3043
|
-
)
|
|
3044
|
-
] }),
|
|
3045
|
-
onHfInitialSync && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
3046
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3047
|
-
PillButton,
|
|
3048
|
-
{
|
|
3049
|
-
variant: "outline",
|
|
3050
|
-
icon: "cloud_sync",
|
|
3051
|
-
loading: hfSyncing,
|
|
3052
|
-
onClick: async () => {
|
|
3053
|
-
setHfSyncing(true);
|
|
3054
|
-
setHfError(null);
|
|
3055
|
-
setHfSyncProgress({ done: 0, total: 0 });
|
|
3056
|
-
try {
|
|
3057
|
-
await onHfInitialSync((done, total) => setHfSyncProgress({ done, total }));
|
|
3058
|
-
setHfSyncProgress(null);
|
|
3059
|
-
} catch (e) {
|
|
3060
|
-
setHfError(e.message);
|
|
3061
|
-
setHfSyncProgress(null);
|
|
3062
|
-
} finally {
|
|
3063
|
-
setHfSyncing(false);
|
|
3064
|
-
}
|
|
3065
|
-
},
|
|
3066
|
-
children: hfSyncing && hfSyncProgress ? `${hfSyncProgress.done} / ${hfSyncProgress.total} Bilder` : "Initial-Sync nach HF"
|
|
3067
|
-
}
|
|
3068
|
-
),
|
|
3069
|
-
hfSyncing && hfSyncProgress && hfSyncProgress.total > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full h-1 bg-white/10 rounded-full overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3070
|
-
"div",
|
|
3071
|
-
{
|
|
3072
|
-
className: "h-full bg-blue-400 rounded-full transition-all",
|
|
3073
|
-
style: { width: `${Math.round(hfSyncProgress.done / hfSyncProgress.total * 100)}%` }
|
|
3074
|
-
}
|
|
3075
|
-
) })
|
|
3076
|
-
] }),
|
|
3077
|
-
hfError && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-red-400 font-mono px-1", children: hfError }),
|
|
3078
|
-
!hfLoading && hfProjects.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/20 px-2", children: "Noch nichts auf HF gespeichert." }),
|
|
3079
|
-
hfProjects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/5", children: [
|
|
3080
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
3081
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[11px] text-white/70 font-bold truncate", children: p.name }),
|
|
3082
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/25", children: [
|
|
3083
|
-
(p.size / 1024 / 1024).toFixed(1),
|
|
3084
|
-
" MB",
|
|
3085
|
-
p.isLfs ? " \xB7 LFS" : ""
|
|
3086
|
-
] })
|
|
3087
|
-
] }),
|
|
3088
|
-
onHfLoad && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3089
|
-
"button",
|
|
2255
|
+
// src/components/HfMediaSyncTab.tsx
|
|
2256
|
+
var import_react17 = require("react");
|
|
2257
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2258
|
+
var HfMediaSyncTab = ({ topSlot, onHfInitialSync }) => {
|
|
2259
|
+
const [error, setError] = (0, import_react17.useState)(null);
|
|
2260
|
+
const [progress, setProgress] = (0, import_react17.useState)(null);
|
|
2261
|
+
const [syncing, setSyncing] = (0, import_react17.useState)(false);
|
|
2262
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "absolute inset-0 overflow-y-auto dark-scrollbar", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "p-6 flex flex-col gap-8", children: [
|
|
2263
|
+
topSlot && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: topSlot }),
|
|
2264
|
+
onHfInitialSync && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
2265
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Hugging Face" }),
|
|
2266
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
2267
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2268
|
+
PillButton,
|
|
3090
2269
|
{
|
|
2270
|
+
variant: "outline",
|
|
2271
|
+
icon: "cloud_sync",
|
|
2272
|
+
loading: syncing,
|
|
3091
2273
|
onClick: async () => {
|
|
2274
|
+
setSyncing(true);
|
|
2275
|
+
setError(null);
|
|
2276
|
+
setProgress({ done: 0, total: 0 });
|
|
3092
2277
|
try {
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
onHfLoad(file);
|
|
2278
|
+
await onHfInitialSync((done, total) => setProgress({ done, total }));
|
|
2279
|
+
setProgress(null);
|
|
3096
2280
|
} catch (e) {
|
|
3097
|
-
|
|
2281
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
2282
|
+
setProgress(null);
|
|
2283
|
+
} finally {
|
|
2284
|
+
setSyncing(false);
|
|
3098
2285
|
}
|
|
3099
2286
|
},
|
|
3100
|
-
|
|
3101
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "cloud_download" })
|
|
2287
|
+
children: syncing && progress ? `${progress.done} / ${progress.total} Bilder` : "Initial-Sync nach HF"
|
|
3102
2288
|
}
|
|
3103
2289
|
),
|
|
3104
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3105
|
-
"
|
|
2290
|
+
syncing && progress && progress.total > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full h-1 bg-white/10 rounded-full overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2291
|
+
"div",
|
|
3106
2292
|
{
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
try {
|
|
3110
|
-
await hfDeleteProject(p.path, hfToken);
|
|
3111
|
-
await loadHfProjects(hfToken);
|
|
3112
|
-
} catch (e) {
|
|
3113
|
-
setHfError(e.message);
|
|
3114
|
-
}
|
|
3115
|
-
},
|
|
3116
|
-
className: "w-8 h-8 flex items-center justify-center text-white/20 active:text-red-400 transition-colors",
|
|
3117
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "delete" })
|
|
2293
|
+
className: "h-full bg-blue-400 rounded-full transition-all",
|
|
2294
|
+
style: { width: `${Math.round(progress.done / progress.total * 100)}%` }
|
|
3118
2295
|
}
|
|
3119
|
-
)
|
|
3120
|
-
] }, p.id))
|
|
3121
|
-
] }),
|
|
3122
|
-
onComputeSyncDiff && onExecuteSync && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
3123
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Sync" }),
|
|
3124
|
-
(syncState === "idle" || syncState === "computing") && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "outline", icon: "compare_arrows", loading: syncState === "computing", onClick: handleComputeDiff, children: "Diff berechnen" }),
|
|
3125
|
-
syncState === "done" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-2 text-[11px] text-green-400", children: [
|
|
3126
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[16px]", children: "check_circle" }),
|
|
3127
|
-
"Sync abgeschlossen"
|
|
2296
|
+
) })
|
|
3128
2297
|
] }),
|
|
3129
|
-
|
|
3130
|
-
syncDiff && (syncState === "ready" || syncState === "executing") && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
|
|
3131
|
-
syncDiff.localOnly.length === 0 && syncDiff.serverOnly.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/30 px-2", children: "Alles synchron \u2014 keine Unterschiede." }),
|
|
3132
|
-
syncDiff.localOnly.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
3133
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/30 uppercase tracking-widest px-1 font-bold", children: [
|
|
3134
|
-
"Nur lokal (",
|
|
3135
|
-
syncDiff.localOnly.length,
|
|
3136
|
-
")"
|
|
3137
|
-
] }),
|
|
3138
|
-
syncDiff.localOnly.map((gen) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
3139
|
-
"button",
|
|
3140
|
-
{
|
|
3141
|
-
onClick: () => toggleLocalId(gen.id),
|
|
3142
|
-
className: `flex items-center gap-3 px-3 py-2 rounded-xl border transition-all text-left ${selectedLocalIds.has(gen.id) ? "bg-blue-500/10 border-blue-500/30" : "bg-white/3 border-white/5 opacity-60"}`,
|
|
3143
|
-
children: [
|
|
3144
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: `w-4 h-4 rounded border flex items-center justify-center flex-shrink-0 ${selectedLocalIds.has(gen.id) ? "bg-blue-500 border-blue-500" : "border-white/20"}`, children: selectedLocalIds.has(gen.id) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[11px] text-white", children: "check" }) }),
|
|
3145
|
-
gen.base64 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("img", { src: gen.base64, className: "w-10 h-10 rounded object-cover flex-shrink-0" }),
|
|
3146
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
3147
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/60 truncate", children: gen.prompt || "Kein Prompt" }),
|
|
3148
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[9px] text-white/25", children: new Date(gen.timestamp).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }) })
|
|
3149
|
-
] })
|
|
3150
|
-
]
|
|
3151
|
-
},
|
|
3152
|
-
gen.id
|
|
3153
|
-
))
|
|
3154
|
-
] }),
|
|
3155
|
-
syncDiff.serverOnly.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
3156
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/30 uppercase tracking-widest px-1 font-bold", children: [
|
|
3157
|
-
"Nur auf Server (",
|
|
3158
|
-
syncDiff.serverOnly.length,
|
|
3159
|
-
")"
|
|
3160
|
-
] }),
|
|
3161
|
-
syncDiff.serverOnly.map((gen) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-3 px-3 py-2 rounded-xl bg-white/3 border border-white/5 opacity-40", children: [
|
|
3162
|
-
gen.base64 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("img", { src: gen.base64, className: "w-10 h-10 rounded object-cover flex-shrink-0" }),
|
|
3163
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
3164
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/60 truncate", children: gen.prompt || "Kein Prompt" }),
|
|
3165
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[9px] text-white/25", children: new Date(gen.timestamp).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }) })
|
|
3166
|
-
] })
|
|
3167
|
-
] }, gen.id))
|
|
3168
|
-
] }),
|
|
3169
|
-
(syncDiff.localOnly.length > 0 || syncDiff.serverOnly.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
3170
|
-
PillButton,
|
|
3171
|
-
{
|
|
3172
|
-
variant: "solid",
|
|
3173
|
-
icon: "sync",
|
|
3174
|
-
loading: syncState === "executing",
|
|
3175
|
-
disabled: selectedLocalIds.size === 0,
|
|
3176
|
-
onClick: handleExecuteSync,
|
|
3177
|
-
children: [
|
|
3178
|
-
selectedLocalIds.size,
|
|
3179
|
-
" \xFCbertragen"
|
|
3180
|
-
]
|
|
3181
|
-
}
|
|
3182
|
-
)
|
|
3183
|
-
] })
|
|
2298
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-red-400 font-mono px-1", children: error })
|
|
3184
2299
|
] })
|
|
3185
2300
|
] }) });
|
|
3186
2301
|
};
|
|
3187
2302
|
|
|
3188
|
-
// src/components/AvatarArchitectApp.tsx
|
|
3189
|
-
init_project();
|
|
3190
|
-
init_metadata();
|
|
3191
|
-
|
|
3192
2303
|
// src/lib/labHelpers.ts
|
|
3193
2304
|
function groupGenerationsToLabItems(generations) {
|
|
3194
2305
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -3232,95 +2343,394 @@ function frameToGeneration(frame, item) {
|
|
|
3232
2343
|
type: frame.source === "zip-import" ? "import" : "generation"
|
|
3233
2344
|
};
|
|
3234
2345
|
}
|
|
3235
|
-
function buildImageContext(images) {
|
|
3236
|
-
const active = images.filter((i) => i.sendToPromptAI);
|
|
3237
|
-
if (!active.length) return "";
|
|
3238
|
-
const lines = active.map((i) => {
|
|
3239
|
-
const role = i.roleForPrompt ? ` (${i.roleForPrompt})` : "";
|
|
3240
|
-
const promptSnippet = i.item.prompt ? `: "${i.item.prompt.slice(0, 120)}"` : "";
|
|
3241
|
-
return `- ${i.label}${role}${promptSnippet}`;
|
|
2346
|
+
function buildImageContext(images) {
|
|
2347
|
+
const active = images.filter((i) => i.sendToPromptAI);
|
|
2348
|
+
if (!active.length) return "";
|
|
2349
|
+
const lines = active.map((i) => {
|
|
2350
|
+
const role = i.roleForPrompt ? ` (${i.roleForPrompt})` : "";
|
|
2351
|
+
const promptSnippet = i.item.prompt ? `: "${i.item.prompt.slice(0, 120)}"` : "";
|
|
2352
|
+
return `- ${i.label}${role}${promptSnippet}`;
|
|
2353
|
+
});
|
|
2354
|
+
return `
|
|
2355
|
+
|
|
2356
|
+
Reference images:
|
|
2357
|
+
${lines.join("\n")}`;
|
|
2358
|
+
}
|
|
2359
|
+
function buildScanInstruction() {
|
|
2360
|
+
return {
|
|
2361
|
+
text: "Analyze this image and create a detailed image generation prompt that would recreate it. Focus on: subject, style, lighting, composition, color palette, mood, and technical quality. Output ONLY the prompt string.",
|
|
2362
|
+
systemInstruction: "You are an expert image analyst and prompt engineer. Return ONLY the generation prompt, no headers, no quotes, no explanations."
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
function buildRemixInstruction(images, userInstruction, currentPrompt) {
|
|
2366
|
+
const imgContext = buildImageContext(images);
|
|
2367
|
+
return {
|
|
2368
|
+
text: `Current prompt: "${currentPrompt}"
|
|
2369
|
+
|
|
2370
|
+
Instruction: ${userInstruction}${imgContext}
|
|
2371
|
+
|
|
2372
|
+
Create a creative variation of this prompt based on the instruction. Output ONLY the new prompt string.`,
|
|
2373
|
+
systemInstruction: "You are an expert prompt engineer. Return ONLY the new generation prompt, no headers, no quotes, no explanations."
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
function buildBlendInstruction(images, userInstruction) {
|
|
2377
|
+
const imgContext = buildImageContext(images);
|
|
2378
|
+
const promptList = images.map((i) => `${i.label}: "${i.item.prompt}"`).join("\n");
|
|
2379
|
+
return {
|
|
2380
|
+
text: `Blend these prompts into one new prompt:
|
|
2381
|
+
${promptList}
|
|
2382
|
+
|
|
2383
|
+
Instruction: ${userInstruction}${imgContext}
|
|
2384
|
+
|
|
2385
|
+
Output ONLY the blended prompt string.`,
|
|
2386
|
+
systemInstruction: "You are an expert prompt engineer. Blend the given prompts into one coherent generation prompt. Return ONLY the prompt string."
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function buildCompareInstruction(images, userInstruction) {
|
|
2390
|
+
const imgContext = buildImageContext(images);
|
|
2391
|
+
return {
|
|
2392
|
+
text: `${userInstruction}${imgContext}
|
|
2393
|
+
|
|
2394
|
+
Analyze the provided images according to the instruction above.`,
|
|
2395
|
+
systemInstruction: "You are an expert image analyst. Provide a clear, structured analysis. Be concise and actionable."
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
function buildLoopInstruction(rounds, newInstruction) {
|
|
2399
|
+
const history = rounds.map(
|
|
2400
|
+
(r, i) => `Round ${i + 1} prompt: "${r.prompt}"
|
|
2401
|
+
Instruction given: "${r.instruction}"`
|
|
2402
|
+
).join("\n\n");
|
|
2403
|
+
const activeImages = rounds.flatMap((r) => r.images.filter((i) => i.sendToPromptAI));
|
|
2404
|
+
const imgContext = activeImages.length ? buildImageContext(activeImages) : "";
|
|
2405
|
+
return {
|
|
2406
|
+
text: `Previous rounds:
|
|
2407
|
+
${history}
|
|
2408
|
+
|
|
2409
|
+
New instruction: ${newInstruction}${imgContext}
|
|
2410
|
+
|
|
2411
|
+
Improve the prompt cumulatively based on all previous rounds and the new instruction. Output ONLY the new prompt string.`,
|
|
2412
|
+
systemInstruction: "You are an expert prompt engineer refining prompts iteratively. Each round the prompt should improve. Return ONLY the new prompt string."
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function autoLabel(index) {
|
|
2416
|
+
return `Img${String.fromCharCode(65 + index)}`;
|
|
2417
|
+
}
|
|
2418
|
+
function buildReferenceImageMediaIds(images) {
|
|
2419
|
+
return images.filter((i) => i.sendToImageGen && i.frame.mediaId).map((i) => i.frame.mediaId);
|
|
2420
|
+
}
|
|
2421
|
+
function toPromptImages(images) {
|
|
2422
|
+
return images.filter((i) => i.sendToPromptAI && i.frame.base64).map((i) => {
|
|
2423
|
+
const raw = i.frame.base64.includes(",") ? i.frame.base64.split(",")[1] : i.frame.base64;
|
|
2424
|
+
const mimeType = i.frame.base64.startsWith("data:image/png") ? "image/png" : "image/jpeg";
|
|
2425
|
+
return { base64: raw, mimeType };
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/lib/hfStateService.ts
|
|
2430
|
+
var import_jszip = __toESM(require("jszip"));
|
|
2431
|
+
var import_hub = require("@huggingface/hub");
|
|
2432
|
+
|
|
2433
|
+
// src/lib/hfEventTypes.ts
|
|
2434
|
+
var HF_BASE = "https://huggingface.co";
|
|
2435
|
+
var HF_REPO = "RolandSch/fa-app-state";
|
|
2436
|
+
var CURRENT_EVENT_VERSION = { major: 1, minor: 0 };
|
|
2437
|
+
var SPEICHERBARE_BILDTYPEN = ["image/png", "image/jpeg", "image/webp"];
|
|
2438
|
+
function bildEndungFuer(mimeType) {
|
|
2439
|
+
if (mimeType === "image/png") return "png";
|
|
2440
|
+
if (mimeType === "image/webp") return "webp";
|
|
2441
|
+
return "jpg";
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// src/lib/hfEventLog.ts
|
|
2445
|
+
function treeUrl(namespace, subdir = "") {
|
|
2446
|
+
const parts = [namespace.replace(/\/$/, ""), subdir].filter(Boolean).join("/");
|
|
2447
|
+
return `${HF_BASE}/api/datasets/${HF_REPO}/tree/main${parts ? "/" + parts : ""}`;
|
|
2448
|
+
}
|
|
2449
|
+
var DROSSEL_VERSUCHE = 4;
|
|
2450
|
+
var DROSSEL_BASIS_MS = 400;
|
|
2451
|
+
var schlafen = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2452
|
+
var HF_LESE_ZEITLIMIT_MS = 2e4;
|
|
2453
|
+
async function holeMitZeitlimit(url, init) {
|
|
2454
|
+
const abbruch = new AbortController();
|
|
2455
|
+
const timer = setTimeout(() => abbruch.abort(), HF_LESE_ZEITLIMIT_MS);
|
|
2456
|
+
try {
|
|
2457
|
+
return await fetch(url, { ...init, signal: abbruch.signal });
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
if (abbruch.signal.aborted) {
|
|
2460
|
+
throw new Error(`HF-Anfrage ohne Antwort nach ${HF_LESE_ZEITLIMIT_MS}ms abgebrochen: ${url}`);
|
|
2461
|
+
}
|
|
2462
|
+
throw error;
|
|
2463
|
+
} finally {
|
|
2464
|
+
clearTimeout(timer);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
async function mitGeduld(anfrage) {
|
|
2468
|
+
let letzte = null;
|
|
2469
|
+
for (let versuch = 0; versuch < DROSSEL_VERSUCHE; versuch += 1) {
|
|
2470
|
+
const res = await anfrage();
|
|
2471
|
+
if (res.status !== 429 && res.status < 500) return res;
|
|
2472
|
+
letzte = res;
|
|
2473
|
+
if (versuch === DROSSEL_VERSUCHE - 1) break;
|
|
2474
|
+
const angesagt = Number(res.headers.get("retry-after"));
|
|
2475
|
+
const warten2 = Number.isFinite(angesagt) && angesagt > 0 ? angesagt * 1e3 : DROSSEL_BASIS_MS * 2 ** versuch;
|
|
2476
|
+
await schlafen(warten2);
|
|
2477
|
+
}
|
|
2478
|
+
return letzte;
|
|
2479
|
+
}
|
|
2480
|
+
async function hfListDir(namespace, subdir, token) {
|
|
2481
|
+
const res = await mitGeduld(() => holeMitZeitlimit(treeUrl(namespace, subdir), {
|
|
2482
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
2483
|
+
}));
|
|
2484
|
+
if (res.status === 404) return [];
|
|
2485
|
+
if (!res.ok) throw new Error(`HF list failed: ${res.status} ${res.statusText}`);
|
|
2486
|
+
return res.json();
|
|
2487
|
+
}
|
|
2488
|
+
async function hfDownloadJsonByPath(repoPath, token) {
|
|
2489
|
+
const res = await mitGeduld(() => holeMitZeitlimit(
|
|
2490
|
+
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
|
|
2491
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2492
|
+
));
|
|
2493
|
+
if (!res.ok) throw new Error(`HF download failed: ${res.status}`);
|
|
2494
|
+
return res.json();
|
|
2495
|
+
}
|
|
2496
|
+
async function hfDownloadBinaryByPath(repoPath, token) {
|
|
2497
|
+
const res = await mitGeduld(() => holeMitZeitlimit(
|
|
2498
|
+
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
|
|
2499
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2500
|
+
));
|
|
2501
|
+
if (!res.ok) throw new Error(`HF download binary failed: ${res.status}`);
|
|
2502
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
2503
|
+
}
|
|
2504
|
+
function toBase64(text) {
|
|
2505
|
+
const bytes = new TextEncoder().encode(text);
|
|
2506
|
+
let binary = "";
|
|
2507
|
+
bytes.forEach((b) => binary += String.fromCharCode(b));
|
|
2508
|
+
return btoa(binary);
|
|
2509
|
+
}
|
|
2510
|
+
async function hfUploadSmallFile(repoPath, content, token, summary = `Update ${repoPath}`) {
|
|
2511
|
+
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
|
|
2512
|
+
method: "POST",
|
|
2513
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
|
|
2514
|
+
body: [
|
|
2515
|
+
JSON.stringify({ key: "header", value: { summary, description: "" } }),
|
|
2516
|
+
JSON.stringify({ key: "file", value: { path: repoPath, encoding: "base64", content: toBase64(content) } })
|
|
2517
|
+
].join("\n")
|
|
2518
|
+
});
|
|
2519
|
+
if (!res.ok) {
|
|
2520
|
+
const err = await res.text().catch(() => "");
|
|
2521
|
+
throw new Error(`HF upload failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
async function hfBatchArchive(moves, token, summary) {
|
|
2525
|
+
const lines = [
|
|
2526
|
+
JSON.stringify({ key: "header", value: { summary, description: "" } }),
|
|
2527
|
+
...moves.flatMap(({ from, to, content }) => [
|
|
2528
|
+
JSON.stringify({ key: "file", value: { path: to, encoding: "base64", content: toBase64(content) } }),
|
|
2529
|
+
JSON.stringify({ key: "deletedFile", value: { path: from } })
|
|
2530
|
+
])
|
|
2531
|
+
];
|
|
2532
|
+
const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
|
|
2533
|
+
method: "POST",
|
|
2534
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
|
|
2535
|
+
body: lines.join("\n")
|
|
2536
|
+
});
|
|
2537
|
+
if (!res.ok) {
|
|
2538
|
+
const err = await res.text().catch(() => "");
|
|
2539
|
+
throw new Error(`HF batch archive failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
function tsFromEventPath(repoPath) {
|
|
2543
|
+
const filename = repoPath.split("/").pop() || "";
|
|
2544
|
+
const base = filename.replace(/_[^_]+\.json$/, "");
|
|
2545
|
+
const iso = base.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
2546
|
+
const ts = Date.parse(iso);
|
|
2547
|
+
return isNaN(ts) ? 0 : ts;
|
|
2548
|
+
}
|
|
2549
|
+
var EVENTDOWNLOAD_PARALLELITAET = 3;
|
|
2550
|
+
async function mitBegrenzterParallelitaet(count, limit, task) {
|
|
2551
|
+
let cursor = 0;
|
|
2552
|
+
let ersterFehler;
|
|
2553
|
+
let fehlerAufgetreten = false;
|
|
2554
|
+
const worker = async () => {
|
|
2555
|
+
while (cursor < count) {
|
|
2556
|
+
const index = cursor++;
|
|
2557
|
+
try {
|
|
2558
|
+
await task(index);
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
if (!fehlerAufgetreten) {
|
|
2561
|
+
fehlerAufgetreten = true;
|
|
2562
|
+
ersterFehler = error;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
|
|
2568
|
+
if (fehlerAufgetreten) throw ersterFehler;
|
|
2569
|
+
}
|
|
2570
|
+
async function loadPendingEvents(namespace, token, sinceTs, knownPaths) {
|
|
2571
|
+
const files = await hfListDir(namespace, "events", token);
|
|
2572
|
+
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
|
+
if (!pending.length) return [];
|
|
2574
|
+
const raw = new Array(pending.length);
|
|
2575
|
+
await mitBegrenzterParallelitaet(pending.length, EVENTDOWNLOAD_PARALLELITAET, async (index) => {
|
|
2576
|
+
raw[index] = await hfDownloadJsonByPath(pending[index].path, token);
|
|
3242
2577
|
});
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
Reference images:
|
|
3246
|
-
${lines.join("\n")}`;
|
|
2578
|
+
for (const f of pending) knownPaths?.add(f.path);
|
|
2579
|
+
return raw.flatMap((e) => Array.isArray(e) ? e : [e]);
|
|
3247
2580
|
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
systemInstruction: "You are an expert image analyst and prompt engineer. Return ONLY the generation prompt, no headers, no quotes, no explanations."
|
|
3252
|
-
};
|
|
2581
|
+
var SESSION_CLIENT_ID = `client-${crypto.randomUUID().slice(0, 8)}`;
|
|
2582
|
+
function getSessionClientId() {
|
|
2583
|
+
return SESSION_CLIENT_ID;
|
|
3253
2584
|
}
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3256
|
-
return {
|
|
3257
|
-
text: `Current prompt: "${currentPrompt}"
|
|
3258
|
-
|
|
3259
|
-
Instruction: ${userInstruction}${imgContext}
|
|
3260
|
-
|
|
3261
|
-
Create a creative variation of this prompt based on the instruction. Output ONLY the new prompt string.`,
|
|
3262
|
-
systemInstruction: "You are an expert prompt engineer. Return ONLY the new generation prompt, no headers, no quotes, no explanations."
|
|
3263
|
-
};
|
|
2585
|
+
function tsToFilename(ts) {
|
|
2586
|
+
return new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-");
|
|
3264
2587
|
}
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
2588
|
+
var writeSeq = 0;
|
|
2589
|
+
var lastOrderedTs = 0;
|
|
2590
|
+
async function writeHFEvent(namespace, token, type, payload, prevTs, options) {
|
|
2591
|
+
const wallTs = Date.now();
|
|
2592
|
+
const ts = options?.ordered ? Math.max(wallTs, lastOrderedTs) : wallTs;
|
|
2593
|
+
if (options?.ordered) lastOrderedTs = ts;
|
|
2594
|
+
const uuid = crypto.randomUUID().slice(0, 8);
|
|
2595
|
+
const event = {
|
|
2596
|
+
v: CURRENT_EVENT_VERSION,
|
|
2597
|
+
type,
|
|
2598
|
+
ts,
|
|
2599
|
+
prevTs,
|
|
2600
|
+
clientId: SESSION_CLIENT_ID,
|
|
2601
|
+
payload
|
|
3276
2602
|
};
|
|
2603
|
+
const suffix = options?.ordered ? `${String(++writeSeq % 1e6).padStart(6, "0")}${uuid}` : uuid;
|
|
2604
|
+
const filename = `${tsToFilename(ts)}_${suffix}.json`;
|
|
2605
|
+
const repoPath = `${namespace}events/${filename}`;
|
|
2606
|
+
await hfUploadSmallFile(repoPath, JSON.stringify(event, null, 2), token, `Event: ${type}`);
|
|
2607
|
+
return event;
|
|
3277
2608
|
}
|
|
3278
|
-
function buildCompareInstruction(images, userInstruction) {
|
|
3279
|
-
const imgContext = buildImageContext(images);
|
|
3280
|
-
return {
|
|
3281
|
-
text: `${userInstruction}${imgContext}
|
|
3282
2609
|
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
2610
|
+
// src/lib/hfStateService.ts
|
|
2611
|
+
var HF_TOKEN_KEY = "hf-token";
|
|
2612
|
+
function getHFToken() {
|
|
2613
|
+
try {
|
|
2614
|
+
return localStorage.getItem(HF_TOKEN_KEY);
|
|
2615
|
+
} catch {
|
|
2616
|
+
return null;
|
|
2617
|
+
}
|
|
3286
2618
|
}
|
|
3287
|
-
function
|
|
3288
|
-
|
|
3289
|
-
(
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
2619
|
+
function setHFToken(token) {
|
|
2620
|
+
try {
|
|
2621
|
+
localStorage.setItem(HF_TOKEN_KEY, token);
|
|
2622
|
+
} catch {
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
async function loadHFState(namespace, token) {
|
|
2626
|
+
const files = await hfListDir(namespace, "", token);
|
|
2627
|
+
const stateFiles = files.filter((f) => f.type === "file" && /^state-[\dT\-]+Z\.zip$/.test(f.path.split("/").pop() || "")).sort((a, b) => b.path.localeCompare(a.path));
|
|
2628
|
+
if (!stateFiles.length) return null;
|
|
2629
|
+
const zipBytes = await hfDownloadBinaryByPath(stateFiles[0].path, token);
|
|
2630
|
+
const zip = await import_jszip.default.loadAsync(zipBytes);
|
|
2631
|
+
const [metadataStr, tagsStr, metaStr, openTasksStr] = await Promise.all([
|
|
2632
|
+
zip.file("metadata.json")?.async("string"),
|
|
2633
|
+
zip.file("tags.json")?.async("string"),
|
|
2634
|
+
zip.file("state_meta.json")?.async("string"),
|
|
2635
|
+
zip.file("open_tasks.json")?.async("string")
|
|
2636
|
+
// optional — ältere Snapshots haben es nicht
|
|
2637
|
+
]);
|
|
2638
|
+
if (!metadataStr || !tagsStr || !metaStr) throw new Error("state.zip: fehlende Pflicht-Dateien");
|
|
3294
2639
|
return {
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
Improve the prompt cumulatively based on all previous rounds and the new instruction. Output ONLY the new prompt string.`,
|
|
3301
|
-
systemInstruction: "You are an expert prompt engineer refining prompts iteratively. Each round the prompt should improve. Return ONLY the new prompt string."
|
|
2640
|
+
metadata: JSON.parse(metadataStr),
|
|
2641
|
+
tags: JSON.parse(tagsStr),
|
|
2642
|
+
meta: JSON.parse(metaStr),
|
|
2643
|
+
openTasks: openTasksStr ? JSON.parse(openTasksStr) : {}
|
|
3302
2644
|
};
|
|
3303
2645
|
}
|
|
3304
|
-
function
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
2646
|
+
async function hfBootstrapFromLegacy(namespace, token, onProgress) {
|
|
2647
|
+
const log = (msg) => {
|
|
2648
|
+
if (onProgress) onProgress(msg);
|
|
2649
|
+
};
|
|
2650
|
+
log("Lese tags.json \u2026");
|
|
2651
|
+
const tags = await fetch(
|
|
2652
|
+
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/tags.json?download=true`,
|
|
2653
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2654
|
+
).then((r) => r.ok ? r.json() : null).catch(() => null);
|
|
2655
|
+
log("Lese metadata.json \u2026");
|
|
2656
|
+
const metadata = await fetch(
|
|
2657
|
+
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/metadata.json?download=true`,
|
|
2658
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2659
|
+
).then((r) => r.ok ? r.json() : []).catch(() => []);
|
|
2660
|
+
const snapshot = {
|
|
2661
|
+
metadata: Array.isArray(metadata) ? metadata : [],
|
|
2662
|
+
tags: tags?.by_category ? tags : { by_category: {}, all: [] },
|
|
2663
|
+
meta: { consolidatedAt: Date.now(), version: 1 }
|
|
2664
|
+
};
|
|
2665
|
+
log(`Erstelle state.zip (${snapshot.metadata.length} Bilder, ${snapshot.tags.all?.length ?? 0} Tags) \u2026`);
|
|
2666
|
+
const zip = new import_jszip.default();
|
|
2667
|
+
zip.file("metadata.json", JSON.stringify(snapshot.metadata, null, 2));
|
|
2668
|
+
zip.file("tags.json", JSON.stringify(snapshot.tags, null, 2));
|
|
2669
|
+
zip.file("state_meta.json", JSON.stringify(snapshot.meta, null, 2));
|
|
2670
|
+
const zipBytes = await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
|
|
2671
|
+
const ts = snapshot.meta.consolidatedAt;
|
|
2672
|
+
const stateFilename = `state-${new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-")}.zip`;
|
|
2673
|
+
const repoPath = `${namespace}${stateFilename}`;
|
|
2674
|
+
log("Lade state.zip hoch \u2026");
|
|
2675
|
+
await (0, import_hub.uploadFile)({
|
|
2676
|
+
repo: { type: "dataset", name: HF_REPO },
|
|
2677
|
+
credentials: { accessToken: token },
|
|
2678
|
+
file: { path: repoPath, content: new Blob([zipBytes.buffer], { type: "application/zip" }) },
|
|
2679
|
+
branch: "main",
|
|
2680
|
+
commitTitle: `Bootstrap: initialer State aus Legacy-Daten`
|
|
2681
|
+
});
|
|
2682
|
+
log(`Fertig \u2014 ${stateFilename}`);
|
|
3309
2683
|
}
|
|
3310
|
-
function
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
2684
|
+
async function hfUploadImage(base64, id, token, mimeType = "image/jpeg") {
|
|
2685
|
+
const ext = bildEndungFuer(mimeType);
|
|
2686
|
+
const binary = atob(base64);
|
|
2687
|
+
const bytes = new Uint8Array(binary.length);
|
|
2688
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
2689
|
+
await (0, import_hub.uploadFile)({
|
|
2690
|
+
repo: { type: "dataset", name: HF_REPO },
|
|
2691
|
+
credentials: { accessToken: token },
|
|
2692
|
+
file: { path: `images/${id}.${ext}`, content: new Blob([bytes], { type: mimeType }) },
|
|
2693
|
+
branch: "main",
|
|
2694
|
+
commitTitle: `Add image ${id}`
|
|
3315
2695
|
});
|
|
3316
2696
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
2697
|
+
async function hfLoadImageAsBase64(id, token, namespace, filename, on404, mimeType, hasThumb) {
|
|
2698
|
+
const fetchBase64 = async (repoPath) => {
|
|
2699
|
+
try {
|
|
2700
|
+
const res = await fetch(
|
|
2701
|
+
`${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
|
|
2702
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2703
|
+
);
|
|
2704
|
+
if (!res.ok) {
|
|
2705
|
+
if (on404 && on404()) return "ABORT";
|
|
2706
|
+
return null;
|
|
2707
|
+
}
|
|
2708
|
+
const blob = await res.blob();
|
|
2709
|
+
return new Promise((resolve, reject) => {
|
|
2710
|
+
const reader = new FileReader();
|
|
2711
|
+
reader.onload = () => resolve(reader.result.split(",")[1]);
|
|
2712
|
+
reader.onerror = reject;
|
|
2713
|
+
reader.readAsDataURL(blob);
|
|
2714
|
+
});
|
|
2715
|
+
} catch {
|
|
2716
|
+
return null;
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
if (filename) {
|
|
2720
|
+
if (hasThumb) {
|
|
2721
|
+
const thumb = await fetchBase64(`thumbs/${filename}`);
|
|
2722
|
+
if (thumb && thumb !== "ABORT") return thumb;
|
|
2723
|
+
}
|
|
2724
|
+
const result2 = await fetchBase64(`images/${filename}`);
|
|
2725
|
+
return result2 === "ABORT" ? null : result2;
|
|
2726
|
+
}
|
|
2727
|
+
const ext = bildEndungFuer(mimeType);
|
|
2728
|
+
const result = await fetchBase64(`images/${id}.${ext}`);
|
|
2729
|
+
return result === "ABORT" ? null : result;
|
|
2730
|
+
}
|
|
3320
2731
|
|
|
3321
2732
|
// src/hooks/useHFState.ts
|
|
3322
2733
|
var import_react18 = require("react");
|
|
3323
|
-
init_hfStateService();
|
|
3324
2734
|
|
|
3325
2735
|
// src/lib/hfReducer.ts
|
|
3326
2736
|
function applyEvent(state, event) {
|
|
@@ -3619,8 +3029,7 @@ function useHFState(token, namespace) {
|
|
|
3619
3029
|
var import_react19 = require("react");
|
|
3620
3030
|
|
|
3621
3031
|
// src/lib/hfTagBaseService.ts
|
|
3622
|
-
var
|
|
3623
|
-
init_hfStateService();
|
|
3032
|
+
var import_jszip2 = __toESM(require("jszip"));
|
|
3624
3033
|
var CURRENT_TAG_EVENT_VERSION = { major: 1, minor: 0 };
|
|
3625
3034
|
async function loadTagBase(namespace, token) {
|
|
3626
3035
|
const files = await hfListDir(namespace, "", token);
|
|
@@ -3628,7 +3037,7 @@ async function loadTagBase(namespace, token) {
|
|
|
3628
3037
|
if (!tagBaseFiles.length) return null;
|
|
3629
3038
|
const newestPath = tagBaseFiles[0].path;
|
|
3630
3039
|
const zipBytes = await hfDownloadBinaryByPath(newestPath, token);
|
|
3631
|
-
const zip = await
|
|
3040
|
+
const zip = await import_jszip2.default.loadAsync(zipBytes);
|
|
3632
3041
|
const [usersStr, contextsStr, tagsStr, metaStr] = await Promise.all([
|
|
3633
3042
|
zip.file("users.json")?.async("string"),
|
|
3634
3043
|
zip.file("contexts.json")?.async("string"),
|
|
@@ -13472,7 +12881,7 @@ function roundOfRun(run, runsOfShot) {
|
|
|
13472
12881
|
}
|
|
13473
12882
|
|
|
13474
12883
|
// src/features/szenen/areas/RunImagesPanel.tsx
|
|
13475
|
-
var import_react40 = require("react");
|
|
12884
|
+
var import_react40 = __toESM(require("react"));
|
|
13476
12885
|
|
|
13477
12886
|
// src/lib/szenen/elementEvents.ts
|
|
13478
12887
|
var SZENEN_EVENT_PREFIX = "szenen.";
|
|
@@ -13853,6 +13262,130 @@ function findPromptRun(events, runId) {
|
|
|
13853
13262
|
return collectPromptRuns(events).find((run) => run.run_id === runId) ?? null;
|
|
13854
13263
|
}
|
|
13855
13264
|
|
|
13265
|
+
// src/lib/szenen/seedListe.ts
|
|
13266
|
+
var DEFAULT_SEED_VORLAGE = {
|
|
13267
|
+
name: "Default",
|
|
13268
|
+
seeds: "1; 10; 100; 1000",
|
|
13269
|
+
builtin: true
|
|
13270
|
+
};
|
|
13271
|
+
function parseSeedListe(eingabe) {
|
|
13272
|
+
if (!eingabe) return [];
|
|
13273
|
+
return eingabe.split(/[,;]/).map((teil) => teil.trim()).filter((teil) => teil.length > 0);
|
|
13274
|
+
}
|
|
13275
|
+
var GANZZAHL = /^-?\d+$/;
|
|
13276
|
+
function ordneSeedsZu(eingabe, anzahl) {
|
|
13277
|
+
const tokens = Array.isArray(eingabe) ? [...eingabe] : parseSeedListe(eingabe);
|
|
13278
|
+
const eintraege = [];
|
|
13279
|
+
for (let position = 0; position < anzahl; position++) {
|
|
13280
|
+
const roh = tokens[position] ?? null;
|
|
13281
|
+
if (roh === null) {
|
|
13282
|
+
eintraege.push({ position, roh: null, seed: null, ungueltig: false });
|
|
13283
|
+
continue;
|
|
13284
|
+
}
|
|
13285
|
+
const zahl = GANZZAHL.test(roh) ? Number(roh) : Number.NaN;
|
|
13286
|
+
const gueltig = Number.isSafeInteger(zahl);
|
|
13287
|
+
eintraege.push({
|
|
13288
|
+
position,
|
|
13289
|
+
roh,
|
|
13290
|
+
seed: gueltig ? zahl : null,
|
|
13291
|
+
ungueltig: !gueltig
|
|
13292
|
+
});
|
|
13293
|
+
}
|
|
13294
|
+
return {
|
|
13295
|
+
eintraege,
|
|
13296
|
+
ungueltige: eintraege.filter((e) => e.ungueltig),
|
|
13297
|
+
ueberzaehlig: Math.max(0, tokens.length - anzahl)
|
|
13298
|
+
};
|
|
13299
|
+
}
|
|
13300
|
+
function seedFehlerText(ungueltige) {
|
|
13301
|
+
const teile = ungueltige.map((e) => `Bild ${e.position + 1}: "${e.roh}"`);
|
|
13302
|
+
return `Die Seedliste enth\xE4lt keine ganze Zahl an ${teile.length === 1 ? "Position" : "Positionen"} \u2014 ${teile.join(", ")}. Kein Bild wurde importiert.`;
|
|
13303
|
+
}
|
|
13304
|
+
function mitDefaultVorlage(custom) {
|
|
13305
|
+
return [DEFAULT_SEED_VORLAGE, ...custom.filter((v) => !v.builtin)];
|
|
13306
|
+
}
|
|
13307
|
+
|
|
13308
|
+
// src/lib/szenen/zipBilder.ts
|
|
13309
|
+
var BILD_ENDUNGEN = {
|
|
13310
|
+
png: "image/png",
|
|
13311
|
+
jpg: "image/jpeg",
|
|
13312
|
+
jpeg: "image/jpeg",
|
|
13313
|
+
webp: "image/webp"
|
|
13314
|
+
};
|
|
13315
|
+
var UPLOAD_MAX_BYTES = 45 * 1024 * 1024;
|
|
13316
|
+
function byteLaenge(base64) {
|
|
13317
|
+
const fuellzeichen = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
13318
|
+
return Math.floor(base64.length * 3 / 4) - fuellzeichen;
|
|
13319
|
+
}
|
|
13320
|
+
function angekuendigteGroesse(eintrag) {
|
|
13321
|
+
const roh = eintrag._data?.uncompressedSize;
|
|
13322
|
+
return typeof roh === "number" && Number.isFinite(roh) && roh >= 0 ? roh : null;
|
|
13323
|
+
}
|
|
13324
|
+
function bildGroesseVerdikt(archiv, pfad, bytes) {
|
|
13325
|
+
if (bytes === null || bytes <= UPLOAD_MAX_BYTES) return null;
|
|
13326
|
+
return `${archiv}: Das Bild ${pfad} ist entpackt ${mb(bytes)} MB gro\xDF \u2014 die Grenze f\xFCr eine einzelne Bilddatei liegt bei ${UPLOAD_MAX_BYTES / 1024 / 1024} MB. Kein Bild wurde importiert.`;
|
|
13327
|
+
}
|
|
13328
|
+
function mb(bytes) {
|
|
13329
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
13330
|
+
}
|
|
13331
|
+
function istZipDatei(datei) {
|
|
13332
|
+
return /\.zip$/i.test(datei.name);
|
|
13333
|
+
}
|
|
13334
|
+
function endungVon(pfad) {
|
|
13335
|
+
const punkt = pfad.lastIndexOf(".");
|
|
13336
|
+
return punkt >= 0 ? pfad.slice(punkt + 1).toLowerCase() : "";
|
|
13337
|
+
}
|
|
13338
|
+
function istImportierbar(pfad, istVerzeichnis) {
|
|
13339
|
+
if (istVerzeichnis) return false;
|
|
13340
|
+
if (pfad.startsWith("__MACOSX/")) return false;
|
|
13341
|
+
const datei = pfad.slice(pfad.lastIndexOf("/") + 1);
|
|
13342
|
+
if (datei.startsWith(".")) return false;
|
|
13343
|
+
return endungVon(datei) in BILD_ENDUNGEN;
|
|
13344
|
+
}
|
|
13345
|
+
function nachPfad(a, b) {
|
|
13346
|
+
return a.localeCompare(b, "en", { numeric: true, sensitivity: "base" }) || (a < b ? -1 : a > b ? 1 : 0);
|
|
13347
|
+
}
|
|
13348
|
+
async function leseBilderAusZip(datei) {
|
|
13349
|
+
const name = datei.name ?? "Archiv";
|
|
13350
|
+
if (datei.size > UPLOAD_MAX_BYTES) {
|
|
13351
|
+
throw new Error(
|
|
13352
|
+
`${name} ist ${mb(datei.size)} MB gro\xDF \u2014 die Grenze liegt bei ${UPLOAD_MAX_BYTES / 1024 / 1024} MB.`
|
|
13353
|
+
);
|
|
13354
|
+
}
|
|
13355
|
+
const { default: JSZip3 } = await import("jszip");
|
|
13356
|
+
const archiv = await JSZip3.loadAsync(datei).catch((fehler4) => {
|
|
13357
|
+
throw new Error(
|
|
13358
|
+
`${name} konnte nicht als ZIP gelesen werden: ${fehler4 instanceof Error ? fehler4.message : String(fehler4)}`
|
|
13359
|
+
);
|
|
13360
|
+
});
|
|
13361
|
+
const pfade = [];
|
|
13362
|
+
archiv.forEach((pfad, eintrag) => {
|
|
13363
|
+
if (istImportierbar(pfad, eintrag.dir)) pfade.push(pfad);
|
|
13364
|
+
});
|
|
13365
|
+
pfade.sort(nachPfad);
|
|
13366
|
+
if (pfade.length === 0) {
|
|
13367
|
+
throw new Error(
|
|
13368
|
+
`${name} enth\xE4lt kein unterst\xFCtztes Bild (erwartet: ${Object.keys(BILD_ENDUNGEN).map((e) => `.${e}`).join(", ")}).`
|
|
13369
|
+
);
|
|
13370
|
+
}
|
|
13371
|
+
const bilder = [];
|
|
13372
|
+
for (const pfad of pfade) {
|
|
13373
|
+
const eintrag = archiv.file(pfad);
|
|
13374
|
+
if (!eintrag) continue;
|
|
13375
|
+
const angekuendigt = bildGroesseVerdikt(name, pfad, angekuendigteGroesse(eintrag));
|
|
13376
|
+
if (angekuendigt) throw new Error(angekuendigt);
|
|
13377
|
+
const base64 = await eintrag.async("base64");
|
|
13378
|
+
const nachgemessen = bildGroesseVerdikt(name, pfad, byteLaenge(base64));
|
|
13379
|
+
if (nachgemessen) throw new Error(nachgemessen);
|
|
13380
|
+
bilder.push({
|
|
13381
|
+
name: pfad,
|
|
13382
|
+
base64,
|
|
13383
|
+
mimeType: BILD_ENDUNGEN[endungVon(pfad)]
|
|
13384
|
+
});
|
|
13385
|
+
}
|
|
13386
|
+
return bilder;
|
|
13387
|
+
}
|
|
13388
|
+
|
|
13856
13389
|
// src/features/szenen/areas/Bildbausteine.tsx
|
|
13857
13390
|
var import_react39 = require("react");
|
|
13858
13391
|
var import_jsx_runtime35 = require("react/jsx-runtime");
|
|
@@ -14291,6 +13824,7 @@ function useBildQuellen(port, bilder) {
|
|
|
14291
13824
|
const [stand, setStand] = (0, import_react40.useState)(() => ({ quellen: /* @__PURE__ */ new Map(), fehler: /* @__PURE__ */ new Map() }));
|
|
14292
13825
|
const gehalten = (0, import_react40.useRef)(/* @__PURE__ */ new Map());
|
|
14293
13826
|
const gescheitert = (0, import_react40.useRef)(/* @__PURE__ */ new Map());
|
|
13827
|
+
const [versuch, setVersuch] = (0, import_react40.useState)(0);
|
|
14294
13828
|
const schluessel = bilder.map((b) => `${b.media_ref}|${b.mime_type ?? ""}`).join(" ");
|
|
14295
13829
|
(0, import_react40.useEffect)(() => {
|
|
14296
13830
|
const bestand = port.imageLibrary;
|
|
@@ -14299,7 +13833,9 @@ function useBildQuellen(port, bilder) {
|
|
|
14299
13833
|
void (async () => {
|
|
14300
13834
|
for (const bild of bilder) {
|
|
14301
13835
|
if (abgebrochen) return;
|
|
14302
|
-
|
|
13836
|
+
const vorher = gescheitert.current.get(bild.media_ref);
|
|
13837
|
+
const endgueltig = vorher !== void 0 && !vorher.retryable;
|
|
13838
|
+
if (gehalten.current.has(bild.media_ref) || endgueltig) continue;
|
|
14303
13839
|
const ergebnis = await bestand.resolveImage(bild.media_ref, bild.mime_type ?? null);
|
|
14304
13840
|
if (abgebrochen) return;
|
|
14305
13841
|
if (ergebnis.kind === "ok" && ergebnis.value) {
|
|
@@ -14307,8 +13843,12 @@ function useBildQuellen(port, bilder) {
|
|
|
14307
13843
|
bild.media_ref,
|
|
14308
13844
|
`data:${ergebnis.value.mime_type};base64,${ergebnis.value.data}`
|
|
14309
13845
|
);
|
|
13846
|
+
gescheitert.current.delete(bild.media_ref);
|
|
14310
13847
|
} else if (ergebnis.kind === "error") {
|
|
14311
|
-
gescheitert.current.set(bild.media_ref,
|
|
13848
|
+
gescheitert.current.set(bild.media_ref, {
|
|
13849
|
+
message: ergebnis.message,
|
|
13850
|
+
retryable: ergebnis.retryable
|
|
13851
|
+
});
|
|
14312
13852
|
}
|
|
14313
13853
|
setStand({
|
|
14314
13854
|
quellen: new Map(gehalten.current),
|
|
@@ -14319,8 +13859,19 @@ function useBildQuellen(port, bilder) {
|
|
|
14319
13859
|
return () => {
|
|
14320
13860
|
abgebrochen = true;
|
|
14321
13861
|
};
|
|
14322
|
-
}, [port, schluessel]);
|
|
14323
|
-
|
|
13862
|
+
}, [port, schluessel, versuch]);
|
|
13863
|
+
const fehlerNachricht = /* @__PURE__ */ new Map();
|
|
13864
|
+
let wiederholbar = false;
|
|
13865
|
+
for (const [ref, f] of stand.fehler) {
|
|
13866
|
+
fehlerNachricht.set(ref, f.message);
|
|
13867
|
+
if (f.retryable) wiederholbar = true;
|
|
13868
|
+
}
|
|
13869
|
+
return {
|
|
13870
|
+
quellen: stand.quellen,
|
|
13871
|
+
fehler: fehlerNachricht,
|
|
13872
|
+
wiederholbar,
|
|
13873
|
+
erneutVersuchen: () => setVersuch((n) => n + 1)
|
|
13874
|
+
};
|
|
14324
13875
|
}
|
|
14325
13876
|
var BildDetails = ({ bild, fehler: fehler4, aufloesbar }) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "szenen-bilder__meta", "data-testid": `batchbild-${bild.media_ref}`, children: [
|
|
14326
13877
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: bild.media_ref }),
|
|
@@ -14341,8 +13892,17 @@ var BildDetails = ({ bild, fehler: fehler4, aufloesbar }) => /* @__PURE__ */ (0,
|
|
|
14341
13892
|
// Serverroute (A6).
|
|
14342
13893
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("em", { "data-testid": `batchbild-unsupported-${bild.media_ref}`, children: "Dieser Host l\xF6st keine Bilder auf \u2014 nur die Metadaten sind bekannt." })
|
|
14343
13894
|
] });
|
|
13895
|
+
var HERKUNFT_TEXT = {
|
|
13896
|
+
generated_in_app: "in der App erzeugt",
|
|
13897
|
+
external_import: "extern importiert",
|
|
13898
|
+
mixed: "gemischte Herkunft"
|
|
13899
|
+
};
|
|
13900
|
+
var HERKUNFT_KURZ = {
|
|
13901
|
+
generated_in_app: "in der App erzeugt",
|
|
13902
|
+
external_import: "extern importiert"
|
|
13903
|
+
};
|
|
14344
13904
|
var BatchKachel = ({ port, batch }) => {
|
|
14345
|
-
const { quellen, fehler: fehler4 } = useBildQuellen(port, batch.images);
|
|
13905
|
+
const { quellen, fehler: fehler4, wiederholbar, erneutVersuchen } = useBildQuellen(port, batch.images);
|
|
14346
13906
|
const aufloesbar = Boolean(port.imageLibrary);
|
|
14347
13907
|
const bilder = batch.images.map((bild) => ({
|
|
14348
13908
|
id: bild.media_ref,
|
|
@@ -14368,7 +13928,7 @@ var BatchKachel = ({ port, batch }) => {
|
|
|
14368
13928
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("header", { className: "szenen-bilder__batchkopf", children: [
|
|
14369
13929
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("strong", { children: batch.title }),
|
|
14370
13930
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "szenen-bilder__herkunft", children: [
|
|
14371
|
-
batch.source_kind
|
|
13931
|
+
HERKUNFT_TEXT[batch.source_kind],
|
|
14372
13932
|
" \xB7 ",
|
|
14373
13933
|
batch.images.length,
|
|
14374
13934
|
" Bild(er)"
|
|
@@ -14382,6 +13942,16 @@ var BatchKachel = ({ port, batch }) => {
|
|
|
14382
13942
|
satzTitel: batch.title
|
|
14383
13943
|
}
|
|
14384
13944
|
),
|
|
13945
|
+
wiederholbar && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
13946
|
+
"button",
|
|
13947
|
+
{
|
|
13948
|
+
type: "button",
|
|
13949
|
+
className: "szenen-bilder__retry",
|
|
13950
|
+
onClick: erneutVersuchen,
|
|
13951
|
+
"data-testid": `batch-retry-${batch.item_id}`,
|
|
13952
|
+
children: "Bilder erneut laden"
|
|
13953
|
+
}
|
|
13954
|
+
),
|
|
14385
13955
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("details", { className: "szenen-bilder__technik", "data-testid": `batch-technik-${batch.item_id}`, children: [
|
|
14386
13956
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("summary", { children: "Technische Details" }),
|
|
14387
13957
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "szenen-bilder__ids", children: [
|
|
@@ -14390,8 +13960,13 @@ var BatchKachel = ({ port, batch }) => {
|
|
|
14390
13960
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { "data-testid": `batch-item-${batch.item_id}`, children: batch.item_id })
|
|
14391
13961
|
] }),
|
|
14392
13962
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
|
|
14393
|
-
"Batches
|
|
14394
|
-
|
|
13963
|
+
"Batches",
|
|
13964
|
+
" ",
|
|
13965
|
+
batch.origins.length > 0 ? batch.origins.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react40.default.Fragment, { children: [
|
|
13966
|
+
i > 0 ? ", " : "",
|
|
13967
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: h.generation_batch_id }),
|
|
13968
|
+
` (${HERKUNFT_KURZ[h.source_kind]})`
|
|
13969
|
+
] }, h.generation_batch_id)) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: batch.generation_batch_ids.join(", ") })
|
|
14395
13970
|
] })
|
|
14396
13971
|
] }),
|
|
14397
13972
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "szenen-bilder__batchliste", children: batch.images.map((bild) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
@@ -14595,21 +14170,114 @@ var ErzeugenAktion = ({ port, lauf, wahl, wahlFehler, onFertig }) => {
|
|
|
14595
14170
|
)
|
|
14596
14171
|
] });
|
|
14597
14172
|
};
|
|
14173
|
+
var DEFAULT_VORLAGE_KEY = "__default__";
|
|
14598
14174
|
var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
14599
14175
|
const [dateien, setDateien] = (0, import_react40.useState)([]);
|
|
14600
14176
|
const [seeds, setSeeds] = (0, import_react40.useState)({});
|
|
14177
|
+
const [zip, setZip] = (0, import_react40.useState)(null);
|
|
14178
|
+
const [zipLaedt, setZipLaedt] = (0, import_react40.useState)(false);
|
|
14179
|
+
const [seedListe, setSeedListe] = (0, import_react40.useState)("");
|
|
14180
|
+
const [vorlagen, setVorlagen] = (0, import_react40.useState)([DEFAULT_SEED_VORLAGE]);
|
|
14181
|
+
const [vorlageKey, setVorlageKey] = (0, import_react40.useState)(DEFAULT_VORLAGE_KEY);
|
|
14182
|
+
const [vorlagenStand, setVorlagenStand] = (0, import_react40.useState)(0);
|
|
14183
|
+
const [vorlageBenennen, setVorlageBenennen] = (0, import_react40.useState)(false);
|
|
14184
|
+
const [vorlageName, setVorlageName] = (0, import_react40.useState)("");
|
|
14601
14185
|
const [modell, setModell] = (0, import_react40.useState)("");
|
|
14602
14186
|
const [itemId, setItemId] = (0, import_react40.useState)("");
|
|
14603
14187
|
const [laeuft, setLaeuft] = (0, import_react40.useState)(false);
|
|
14604
14188
|
const [notiz, setNotiz] = (0, import_react40.useState)(null);
|
|
14189
|
+
const auswahlMarke = (0, import_react40.useRef)(0);
|
|
14190
|
+
const zipAktiv = zip !== null;
|
|
14191
|
+
(0, import_react40.useEffect)(() => {
|
|
14192
|
+
if (!zipAktiv) return;
|
|
14193
|
+
let abgebrochen = false;
|
|
14194
|
+
void (async () => {
|
|
14195
|
+
const gelesen = port.seedTemplates ? await port.seedTemplates.listSeedTemplates() : null;
|
|
14196
|
+
if (abgebrochen) return;
|
|
14197
|
+
setVorlagen(mitDefaultVorlage(
|
|
14198
|
+
gelesen && gelesen.kind === "ok" ? gelesen.value : []
|
|
14199
|
+
));
|
|
14200
|
+
})();
|
|
14201
|
+
return () => {
|
|
14202
|
+
abgebrochen = true;
|
|
14203
|
+
};
|
|
14204
|
+
}, [zipAktiv, port, vorlagenStand]);
|
|
14605
14205
|
if (!port.imageBatchWrite) {
|
|
14606
14206
|
return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { "data-testid": `zuordnen-unsupported-${lauf.run_id}`, children: "Dieser Host ordnet keine externen Bilder zu \u2014 er hat keinen gesicherten Batch-Schreibweg. Die Historie oben bleibt trotzdem sichtbar." });
|
|
14607
14207
|
}
|
|
14608
|
-
const
|
|
14208
|
+
const zuordnung = zip ? ordneSeedsZu(seedListe, zip.bilder.length) : null;
|
|
14209
|
+
const dateiWahl = async (liste) => {
|
|
14210
|
+
const gewaehlt = [...liste ?? []];
|
|
14211
|
+
const warZip = zip !== null;
|
|
14212
|
+
const meine = auswahlMarke.current += 1;
|
|
14213
|
+
const veraltet = () => auswahlMarke.current !== meine;
|
|
14214
|
+
const zips = gewaehlt.filter(istZipDatei);
|
|
14215
|
+
const nurEinZip = zips.length === 1 && gewaehlt.length === 1;
|
|
14216
|
+
setNotiz(null);
|
|
14217
|
+
setSeeds({});
|
|
14218
|
+
setZip(null);
|
|
14219
|
+
setDateien(zips.length === 0 ? gewaehlt : []);
|
|
14220
|
+
setZipLaedt(nurEinZip);
|
|
14221
|
+
if (zips.length > 0 && zips.length !== gewaehlt.length) {
|
|
14222
|
+
setNotiz({
|
|
14223
|
+
tone: "error",
|
|
14224
|
+
text: `Die Auswahl enth\xE4lt ${zips.length} ZIP-Datei(en) und ${gewaehlt.length - zips.length} Bilddatei(en). Bitte entweder ein ZIP oder einzelne Bilder w\xE4hlen \u2014 nicht beides in einem Import.`
|
|
14225
|
+
});
|
|
14226
|
+
return;
|
|
14227
|
+
}
|
|
14228
|
+
if (zips.length > 1) {
|
|
14229
|
+
setNotiz({
|
|
14230
|
+
tone: "error",
|
|
14231
|
+
text: `${zips.length} ZIP-Dateien gew\xE4hlt (${zips.map((z) => z.name).join(", ")}). Ein Import nimmt genau ein ZIP.`
|
|
14232
|
+
});
|
|
14233
|
+
return;
|
|
14234
|
+
}
|
|
14235
|
+
if (!nurEinZip) return;
|
|
14236
|
+
const datei = zips[0];
|
|
14237
|
+
try {
|
|
14238
|
+
const bilder = await leseBilderAusZip(datei);
|
|
14239
|
+
if (veraltet()) return;
|
|
14240
|
+
setZip({ datei, bilder });
|
|
14241
|
+
if (!warZip) {
|
|
14242
|
+
setSeedListe(DEFAULT_SEED_VORLAGE.seeds);
|
|
14243
|
+
setVorlageKey(DEFAULT_VORLAGE_KEY);
|
|
14244
|
+
}
|
|
14245
|
+
} catch (fehler4) {
|
|
14246
|
+
if (veraltet()) return;
|
|
14247
|
+
setNotiz({
|
|
14248
|
+
tone: "error",
|
|
14249
|
+
text: fehler4 instanceof Error ? fehler4.message : String(fehler4)
|
|
14250
|
+
});
|
|
14251
|
+
}
|
|
14252
|
+
if (veraltet()) return;
|
|
14253
|
+
setZipLaedt(false);
|
|
14254
|
+
};
|
|
14255
|
+
const schreiben = async (bilder, generationBatchId, titel) => {
|
|
14609
14256
|
setLaeuft(true);
|
|
14610
14257
|
setNotiz(null);
|
|
14611
14258
|
try {
|
|
14612
|
-
const
|
|
14259
|
+
const ergebnis = await port.imageBatchWrite.importBatch({
|
|
14260
|
+
runId: lauf.run_id,
|
|
14261
|
+
generationBatchId,
|
|
14262
|
+
images: bilder,
|
|
14263
|
+
title: titel
|
|
14264
|
+
});
|
|
14265
|
+
setNotiz(
|
|
14266
|
+
ergebnis.kind === "ok" ? { tone: "ok", text: `Importiert als Item ${ergebnis.value.item_id} mit ${bilder.length} Bild(ern).` } : noticeFor(ergebnis, "Externe Bilder importieren")
|
|
14267
|
+
);
|
|
14268
|
+
} catch (fehler4) {
|
|
14269
|
+
setNotiz({
|
|
14270
|
+
tone: "error",
|
|
14271
|
+
text: `Externe Bilder importieren: ${fehler4 instanceof Error ? fehler4.message : String(fehler4)}`
|
|
14272
|
+
});
|
|
14273
|
+
}
|
|
14274
|
+
setLaeuft(false);
|
|
14275
|
+
onFertig();
|
|
14276
|
+
};
|
|
14277
|
+
const importieren = async () => {
|
|
14278
|
+
let bilder;
|
|
14279
|
+
try {
|
|
14280
|
+
bilder = [];
|
|
14613
14281
|
for (let index = 0; index < dateien.length; index++) {
|
|
14614
14282
|
const roh = (seeds[index] ?? "").trim();
|
|
14615
14283
|
const seed = roh === "" ? void 0 : Number.parseInt(roh, 10);
|
|
@@ -14622,25 +14290,42 @@ var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
|
14622
14290
|
...seed !== void 0 ? { seed } : {}
|
|
14623
14291
|
});
|
|
14624
14292
|
}
|
|
14625
|
-
const ergebnis = await port.imageBatchWrite.importBatch({
|
|
14626
|
-
runId: lauf.run_id,
|
|
14627
|
-
// Vor dem Aufruf gebildet und stabil (D-113): eine Wiederholung
|
|
14628
|
-
// derselben Einreichung erzeugt kein zweites Item.
|
|
14629
|
-
generationBatchId: `imp-${lauf.run_id}-${dateien.length}-${dateien.map((d) => d.size).join("_")}`,
|
|
14630
|
-
images: bilder,
|
|
14631
|
-
title: `Externer Import zu Lauf ${lauf.run_id}`
|
|
14632
|
-
});
|
|
14633
|
-
setNotiz(
|
|
14634
|
-
ergebnis.kind === "ok" ? { tone: "ok", text: `Importiert als Item ${ergebnis.value.item_id} mit ${bilder.length} Bild(ern).` } : noticeFor(ergebnis, "Externe Bilder importieren")
|
|
14635
|
-
);
|
|
14636
14293
|
} catch (fehler4) {
|
|
14637
14294
|
setNotiz({
|
|
14638
14295
|
tone: "error",
|
|
14639
14296
|
text: `Externe Bilder importieren: ${fehler4 instanceof Error ? fehler4.message : String(fehler4)}`
|
|
14640
14297
|
});
|
|
14298
|
+
return;
|
|
14641
14299
|
}
|
|
14642
|
-
|
|
14643
|
-
|
|
14300
|
+
await schreiben(
|
|
14301
|
+
bilder,
|
|
14302
|
+
// Vor dem Aufruf gebildet und stabil (D-113): eine Wiederholung
|
|
14303
|
+
// derselben Einreichung erzeugt kein zweites Item.
|
|
14304
|
+
`imp-${lauf.run_id}-${dateien.length}-${dateien.map((d) => d.size).join("_")}`,
|
|
14305
|
+
`Externer Import zu Lauf ${lauf.run_id}`
|
|
14306
|
+
);
|
|
14307
|
+
};
|
|
14308
|
+
const zipImportieren = async () => {
|
|
14309
|
+
if (!zip || !zuordnung) return;
|
|
14310
|
+
if (zuordnung.ungueltige.length > 0) {
|
|
14311
|
+
setNotiz({ tone: "error", text: seedFehlerText(zuordnung.ungueltige) });
|
|
14312
|
+
return;
|
|
14313
|
+
}
|
|
14314
|
+
const bilder = zip.bilder.map((bild, index) => {
|
|
14315
|
+
const seed = zuordnung.eintraege[index]?.seed ?? null;
|
|
14316
|
+
return {
|
|
14317
|
+
imageBase64: bild.base64,
|
|
14318
|
+
...modell.trim() ? { model: modell.trim() } : {},
|
|
14319
|
+
// Unbekannt bleibt unbekannt: kein Feld statt eines erfundenen
|
|
14320
|
+
// Werts (§6.1).
|
|
14321
|
+
...seed !== null ? { seed } : {}
|
|
14322
|
+
};
|
|
14323
|
+
});
|
|
14324
|
+
await schreiben(
|
|
14325
|
+
bilder,
|
|
14326
|
+
`imp-${lauf.run_id}-zip-${zip.datei.size}-${bilder.length}`,
|
|
14327
|
+
`Externer Import zu Lauf ${lauf.run_id} (${zip.datei.name})`
|
|
14328
|
+
);
|
|
14644
14329
|
};
|
|
14645
14330
|
const zuordnen = async () => {
|
|
14646
14331
|
setLaeuft(true);
|
|
@@ -14658,35 +14343,63 @@ var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
|
14658
14343
|
setLaeuft(false);
|
|
14659
14344
|
onFertig();
|
|
14660
14345
|
};
|
|
14346
|
+
const vorlageWaehlen = (key) => {
|
|
14347
|
+
setVorlageKey(key);
|
|
14348
|
+
const vorlage = key === DEFAULT_VORLAGE_KEY ? DEFAULT_SEED_VORLAGE : vorlagen.find((v) => String(v.id) === key);
|
|
14349
|
+
if (vorlage) setSeedListe(vorlage.seeds);
|
|
14350
|
+
};
|
|
14351
|
+
const vorlageSpeichern = async () => {
|
|
14352
|
+
const name = vorlageName.trim();
|
|
14353
|
+
if (!name || !seedListe.trim() || !port.seedTemplates?.saveSeedTemplate) return;
|
|
14354
|
+
const ergebnis = await port.seedTemplates.saveSeedTemplate({ name, seeds: seedListe.trim() });
|
|
14355
|
+
if (ergebnis.kind !== "ok") {
|
|
14356
|
+
setNotiz(noticeFor(ergebnis, "Seed-Vorlage speichern"));
|
|
14357
|
+
return;
|
|
14358
|
+
}
|
|
14359
|
+
setVorlageBenennen(false);
|
|
14360
|
+
setVorlageName("");
|
|
14361
|
+
if (ergebnis.value.id !== void 0) setVorlageKey(String(ergebnis.value.id));
|
|
14362
|
+
setVorlagenStand((n) => n + 1);
|
|
14363
|
+
};
|
|
14364
|
+
const vorlageLoeschen = async () => {
|
|
14365
|
+
if (vorlageKey === DEFAULT_VORLAGE_KEY || !port.seedTemplates?.deleteSeedTemplate) return;
|
|
14366
|
+
const ergebnis = await port.seedTemplates.deleteSeedTemplate(vorlageKey);
|
|
14367
|
+
if (ergebnis.kind !== "ok") {
|
|
14368
|
+
setNotiz(noticeFor(ergebnis, "Seed-Vorlage l\xF6schen"));
|
|
14369
|
+
return;
|
|
14370
|
+
}
|
|
14371
|
+
setVorlageKey(DEFAULT_VORLAGE_KEY);
|
|
14372
|
+
setVorlagenStand((n) => n + 1);
|
|
14373
|
+
};
|
|
14661
14374
|
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "szenen-bilder__aktion-inhalt", "data-testid": `zuordnen-${lauf.run_id}`, children: [
|
|
14662
14375
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14663
|
-
"Bilddateien",
|
|
14376
|
+
"Bilddateien oder ein ZIP",
|
|
14664
14377
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14665
14378
|
"input",
|
|
14666
14379
|
{
|
|
14667
14380
|
type: "file",
|
|
14668
|
-
accept: "image
|
|
14381
|
+
accept: "image/*,.zip",
|
|
14669
14382
|
multiple: true,
|
|
14670
14383
|
"data-testid": `zuordnen-dateien-${lauf.run_id}`,
|
|
14671
14384
|
onChange: (e) => {
|
|
14672
|
-
|
|
14673
|
-
setSeeds({});
|
|
14385
|
+
void dateiWahl(e.target.files);
|
|
14674
14386
|
}
|
|
14675
14387
|
}
|
|
14676
14388
|
)
|
|
14677
14389
|
] }),
|
|
14390
|
+
zipLaedt && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { "data-testid": `zuordnen-zip-laedt-${lauf.run_id}`, children: "ZIP wird gelesen \u2026" }),
|
|
14391
|
+
(dateien.length > 0 || zip) && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14392
|
+
"Modell (f\xFCr alle Bilder, leer = unbekannt)",
|
|
14393
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14394
|
+
"input",
|
|
14395
|
+
{
|
|
14396
|
+
value: modell,
|
|
14397
|
+
"data-testid": `zuordnen-modell-${lauf.run_id}`,
|
|
14398
|
+
onChange: (e) => setModell(e.target.value)
|
|
14399
|
+
}
|
|
14400
|
+
)
|
|
14401
|
+
] }),
|
|
14678
14402
|
dateien.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
|
|
14679
|
-
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14680
|
-
"Modell (f\xFCr alle Bilder, leer = unbekannt)",
|
|
14681
|
-
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14682
|
-
"input",
|
|
14683
|
-
{
|
|
14684
|
-
value: modell,
|
|
14685
|
-
"data-testid": `zuordnen-modell-${lauf.run_id}`,
|
|
14686
|
-
onChange: (e) => setModell(e.target.value)
|
|
14687
|
-
}
|
|
14688
|
-
)
|
|
14689
|
-
] }),
|
|
14690
14403
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ol", { "data-testid": `zuordnen-vorschau-${lauf.run_id}`, children: dateien.map((datei, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { children: [
|
|
14691
14404
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
|
|
14692
14405
|
"Position ",
|
|
@@ -14720,6 +14433,157 @@ var ZuordnenAktion = ({ port, lauf, onFertig }) => {
|
|
|
14720
14433
|
}
|
|
14721
14434
|
)
|
|
14722
14435
|
] }),
|
|
14436
|
+
zip && zuordnung && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "szenen-bilder__zip", "data-testid": `zuordnen-zip-${lauf.run_id}`, children: [
|
|
14437
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "szenen-bilder__zipkopf", children: [
|
|
14438
|
+
"ZIP ",
|
|
14439
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { children: zip.datei.name }),
|
|
14440
|
+
" \u2014 ",
|
|
14441
|
+
zip.bilder.length,
|
|
14442
|
+
" Bild(er) erkannt"
|
|
14443
|
+
] }),
|
|
14444
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14445
|
+
"Seeds (pro Bild ein Seed, Trenner , oder ;)",
|
|
14446
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14447
|
+
"input",
|
|
14448
|
+
{
|
|
14449
|
+
"data-testid": `zuordnen-seedliste-${lauf.run_id}`,
|
|
14450
|
+
value: seedListe,
|
|
14451
|
+
placeholder: "z. B. 1; 10; 100; 1000",
|
|
14452
|
+
onChange: (e) => setSeedListe(e.target.value)
|
|
14453
|
+
}
|
|
14454
|
+
)
|
|
14455
|
+
] }),
|
|
14456
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14457
|
+
"Seed-Vorlage",
|
|
14458
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
|
|
14459
|
+
"select",
|
|
14460
|
+
{
|
|
14461
|
+
"data-testid": `zuordnen-seedvorlage-${lauf.run_id}`,
|
|
14462
|
+
value: vorlageKey,
|
|
14463
|
+
onChange: (e) => vorlageWaehlen(e.target.value),
|
|
14464
|
+
children: [
|
|
14465
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("option", { value: DEFAULT_VORLAGE_KEY, children: [
|
|
14466
|
+
"Default (",
|
|
14467
|
+
DEFAULT_SEED_VORLAGE.seeds,
|
|
14468
|
+
")"
|
|
14469
|
+
] }),
|
|
14470
|
+
vorlagen.filter((v) => !v.builtin).map((v) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("option", { value: String(v.id), children: [
|
|
14471
|
+
v.name,
|
|
14472
|
+
" (",
|
|
14473
|
+
v.seeds,
|
|
14474
|
+
")"
|
|
14475
|
+
] }, String(v.id)))
|
|
14476
|
+
]
|
|
14477
|
+
}
|
|
14478
|
+
)
|
|
14479
|
+
] }),
|
|
14480
|
+
(port.seedTemplates?.saveSeedTemplate || port.seedTemplates?.deleteSeedTemplate) && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "szenen-bilder__zipvorlagen", children: [
|
|
14481
|
+
port.seedTemplates?.saveSeedTemplate && !vorlageBenennen && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14482
|
+
"button",
|
|
14483
|
+
{
|
|
14484
|
+
type: "button",
|
|
14485
|
+
"data-testid": `zuordnen-vorlage-neu-${lauf.run_id}`,
|
|
14486
|
+
disabled: !seedListe.trim(),
|
|
14487
|
+
onClick: () => {
|
|
14488
|
+
setVorlageName("");
|
|
14489
|
+
setVorlageBenennen(true);
|
|
14490
|
+
},
|
|
14491
|
+
children: "Als Vorlage speichern"
|
|
14492
|
+
}
|
|
14493
|
+
),
|
|
14494
|
+
port.seedTemplates?.deleteSeedTemplate && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14495
|
+
"button",
|
|
14496
|
+
{
|
|
14497
|
+
type: "button",
|
|
14498
|
+
"data-testid": `zuordnen-vorlage-loeschen-${lauf.run_id}`,
|
|
14499
|
+
disabled: vorlageKey === DEFAULT_VORLAGE_KEY,
|
|
14500
|
+
onClick: () => {
|
|
14501
|
+
void vorlageLoeschen();
|
|
14502
|
+
},
|
|
14503
|
+
children: "Vorlage l\xF6schen"
|
|
14504
|
+
}
|
|
14505
|
+
)
|
|
14506
|
+
] }),
|
|
14507
|
+
vorlageBenennen && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
|
|
14508
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14509
|
+
"Name der Vorlage",
|
|
14510
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14511
|
+
"input",
|
|
14512
|
+
{
|
|
14513
|
+
"data-testid": `zuordnen-vorlage-name-${lauf.run_id}`,
|
|
14514
|
+
value: vorlageName,
|
|
14515
|
+
onChange: (e) => setVorlageName(e.target.value)
|
|
14516
|
+
}
|
|
14517
|
+
)
|
|
14518
|
+
] }),
|
|
14519
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "szenen-bilder__zipvorlagen", children: [
|
|
14520
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14521
|
+
"button",
|
|
14522
|
+
{
|
|
14523
|
+
type: "button",
|
|
14524
|
+
"data-testid": `zuordnen-vorlage-sichern-${lauf.run_id}`,
|
|
14525
|
+
disabled: !vorlageName.trim(),
|
|
14526
|
+
onClick: () => {
|
|
14527
|
+
void vorlageSpeichern();
|
|
14528
|
+
},
|
|
14529
|
+
children: "Speichern"
|
|
14530
|
+
}
|
|
14531
|
+
),
|
|
14532
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14533
|
+
"button",
|
|
14534
|
+
{
|
|
14535
|
+
type: "button",
|
|
14536
|
+
"data-testid": `zuordnen-vorlage-abbrechen-${lauf.run_id}`,
|
|
14537
|
+
onClick: () => setVorlageBenennen(false),
|
|
14538
|
+
children: "Abbrechen"
|
|
14539
|
+
}
|
|
14540
|
+
)
|
|
14541
|
+
] })
|
|
14542
|
+
] }),
|
|
14543
|
+
zuordnung.ueberzaehlig > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { "data-testid": `zuordnen-ueberzaehlig-${lauf.run_id}`, children: [
|
|
14544
|
+
zuordnung.ueberzaehlig,
|
|
14545
|
+
" Seed(s) mehr als Bilder \u2014 die \xFCberz\xE4hligen werden nicht geschrieben."
|
|
14546
|
+
] }),
|
|
14547
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14548
|
+
"ol",
|
|
14549
|
+
{
|
|
14550
|
+
className: "szenen-bilder__zipliste",
|
|
14551
|
+
"data-testid": `zuordnen-zip-vorschau-${lauf.run_id}`,
|
|
14552
|
+
children: zip.bilder.map((bild, index) => {
|
|
14553
|
+
const eintrag = zuordnung.eintraege[index];
|
|
14554
|
+
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { children: [
|
|
14555
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
|
|
14556
|
+
"Position ",
|
|
14557
|
+
index,
|
|
14558
|
+
": ",
|
|
14559
|
+
bild.name
|
|
14560
|
+
] }),
|
|
14561
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
14562
|
+
"span",
|
|
14563
|
+
{
|
|
14564
|
+
"data-testid": `zuordnen-zip-seed-${lauf.run_id}-${index}`,
|
|
14565
|
+
"data-seed": eintrag.seed === null ? "" : String(eintrag.seed),
|
|
14566
|
+
children: eintrag.ungueltig ? `Seed ung\xFCltig: "${eintrag.roh}"` : eintrag.seed === null ? "Seed unbekannt" : `Seed ${eintrag.seed}`
|
|
14567
|
+
}
|
|
14568
|
+
)
|
|
14569
|
+
] }, `${bild.name}-${index}`);
|
|
14570
|
+
})
|
|
14571
|
+
}
|
|
14572
|
+
),
|
|
14573
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
|
|
14574
|
+
"button",
|
|
14575
|
+
{
|
|
14576
|
+
type: "button",
|
|
14577
|
+
"data-testid": `zuordnen-zip-import-${lauf.run_id}`,
|
|
14578
|
+
disabled: laeuft || zipLaedt,
|
|
14579
|
+
onClick: zipImportieren,
|
|
14580
|
+
children: [
|
|
14581
|
+
zip.bilder.length,
|
|
14582
|
+
" Bild(er) aus ZIP importieren"
|
|
14583
|
+
]
|
|
14584
|
+
}
|
|
14585
|
+
)
|
|
14586
|
+
] }),
|
|
14723
14587
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { children: [
|
|
14724
14588
|
"Oder ein bestehendes Library-Item",
|
|
14725
14589
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
@@ -14775,6 +14639,8 @@ var RunsPanel = ({ port, source, contextId }) => {
|
|
|
14775
14639
|
const [nachladen, setNachladen] = (0, import_react41.useState)(0);
|
|
14776
14640
|
const faehig = capabilitiesOf(port);
|
|
14777
14641
|
const quellSchluessel = source.kind === "published" ? source.shotVersionId : source.kind === "draft_chain" ? source.chainId : source.sceneElementId;
|
|
14642
|
+
const aktuelleQuelle = (0, import_react41.useRef)({ kind: source.kind, id: quellSchluessel });
|
|
14643
|
+
aktuelleQuelle.current = { kind: source.kind, id: quellSchluessel };
|
|
14778
14644
|
const [phase, neuLaden] = usePortQuery(
|
|
14779
14645
|
() => {
|
|
14780
14646
|
if (source.kind === "published") return port.runs.listRunsForShot(source.shotVersionId);
|
|
@@ -14785,12 +14651,16 @@ var RunsPanel = ({ port, source, contextId }) => {
|
|
|
14785
14651
|
);
|
|
14786
14652
|
const starten = async () => {
|
|
14787
14653
|
if (source.kind !== "published") return;
|
|
14654
|
+
const gestartetFuer = { kind: source.kind, id: source.shotVersionId };
|
|
14788
14655
|
setLaeuft(true);
|
|
14789
14656
|
const ergebnis = await port.runs.startRun({
|
|
14790
14657
|
shotVersionId: source.shotVersionId,
|
|
14791
14658
|
...anweisung.trim() ? { userInstruction: anweisung.trim() } : {},
|
|
14792
14659
|
contextId
|
|
14793
14660
|
});
|
|
14661
|
+
if (aktuelleQuelle.current.kind !== gestartetFuer.kind || aktuelleQuelle.current.id !== gestartetFuer.id) {
|
|
14662
|
+
return;
|
|
14663
|
+
}
|
|
14794
14664
|
setNotiz(noticeFor(ergebnis, "Prompt-Lauf starten"));
|
|
14795
14665
|
setLaeuft(false);
|
|
14796
14666
|
setNachladen((n) => n + 1);
|
|
@@ -14800,6 +14670,12 @@ var RunsPanel = ({ port, source, contextId }) => {
|
|
|
14800
14670
|
const [gewaehlteRunde, setGewaehlteRunde] = (0, import_react41.useState)(void 0);
|
|
14801
14671
|
const neueste = laeufe.length > 0 ? laeufe[laeufe.length - 1].run_id : null;
|
|
14802
14672
|
const offeneRunde = gewaehlteRunde === void 0 ? neueste : gewaehlteRunde;
|
|
14673
|
+
(0, import_react41.useEffect)(() => {
|
|
14674
|
+
setGewaehlteRunde(void 0);
|
|
14675
|
+
setAnweisung("");
|
|
14676
|
+
setNotiz(null);
|
|
14677
|
+
setLaeuft(false);
|
|
14678
|
+
}, [source.kind, quellSchluessel]);
|
|
14803
14679
|
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "szenen-runs", "data-testid": "laeufe-bereich", "data-shot-version": quellSchluessel, children: [
|
|
14804
14680
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("h4", { children: "Prompt-L\xE4ufe" }),
|
|
14805
14681
|
source.kind === "published" && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "szenen-runs__start", children: [
|
|
@@ -14870,6 +14746,26 @@ var Sekundaertext = ({ titel, text, testId }) => {
|
|
|
14870
14746
|
)
|
|
14871
14747
|
] });
|
|
14872
14748
|
};
|
|
14749
|
+
var LISTE_ANFANG = 3;
|
|
14750
|
+
var SekundaerListe = ({ titel, eintraege, testId, className }) => {
|
|
14751
|
+
const [alle, setAlle] = (0, import_react41.useState)(false);
|
|
14752
|
+
const lang = eintraege.length > LISTE_ANFANG;
|
|
14753
|
+
const gezeigt = lang && !alle ? eintraege.slice(0, LISTE_ANFANG) : eintraege;
|
|
14754
|
+
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("section", { className: "szenen-runs__sekundaer", children: [
|
|
14755
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("h5", { children: titel }),
|
|
14756
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { className, "data-testid": testId, children: gezeigt.map((eintrag, i) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { children: eintrag }, i)) }),
|
|
14757
|
+
lang && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
14758
|
+
"button",
|
|
14759
|
+
{
|
|
14760
|
+
type: "button",
|
|
14761
|
+
className: "szenen-runs__mehr",
|
|
14762
|
+
onClick: () => setAlle(!alle),
|
|
14763
|
+
"data-testid": `${testId}-mehr`,
|
|
14764
|
+
children: alle ? "weniger anzeigen" : `${eintraege.length - LISTE_ANFANG} weitere anzeigen`
|
|
14765
|
+
}
|
|
14766
|
+
)
|
|
14767
|
+
] });
|
|
14768
|
+
};
|
|
14873
14769
|
var RundenEintrag = ({ port, lauf, runde, offen, onUmschalten, itemMoeglich, onItemGespeichert }) => {
|
|
14874
14770
|
const [notiz, setNotiz] = (0, import_react41.useState)(null);
|
|
14875
14771
|
const [laeuft, setLaeuft] = (0, import_react41.useState)(false);
|
|
@@ -14955,11 +14851,23 @@ var RundenEintrag = ({ port, lauf, runde, offen, onUmschalten, itemMoeglich, onI
|
|
|
14955
14851
|
testId: `feedback-${lauf.run_id}`
|
|
14956
14852
|
}
|
|
14957
14853
|
),
|
|
14958
|
-
lauf.output.visibility.assumptions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
|
|
14854
|
+
lauf.output.visibility.assumptions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
14855
|
+
SekundaerListe,
|
|
14856
|
+
{
|
|
14857
|
+
titel: "Annahmen",
|
|
14858
|
+
eintraege: lauf.output.visibility.assumptions,
|
|
14859
|
+
testId: `annahmen-${lauf.run_id}`
|
|
14860
|
+
}
|
|
14861
|
+
),
|
|
14862
|
+
lauf.output.notes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
14863
|
+
SekundaerListe,
|
|
14864
|
+
{
|
|
14865
|
+
titel: "Notizen",
|
|
14866
|
+
eintraege: lauf.output.notes,
|
|
14867
|
+
testId: `notizen-${lauf.run_id}`,
|
|
14868
|
+
className: "szenen-runs__notizen"
|
|
14869
|
+
}
|
|
14870
|
+
)
|
|
14963
14871
|
] }),
|
|
14964
14872
|
lauf.raw_output && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("details", { "data-testid": `diagnose-${lauf.run_id}`, children: [
|
|
14965
14873
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("summary", { children: "Rohantwort (Diagnose)" }),
|
|
@@ -17466,8 +17374,6 @@ var DeriveArea = ({ port, contextId }) => {
|
|
|
17466
17374
|
if (ergebnis.kind === "ok") setLauf(ergebnis.value);
|
|
17467
17375
|
else if (ergebnis.kind === "unsupported") {
|
|
17468
17376
|
setMeldung(`Dieser Host bietet \u201E${ergebnis.capability}" nicht an.`);
|
|
17469
|
-
} else if (ergebnis.kind === "rejected") {
|
|
17470
|
-
setMeldung(ergebnis.errors.map((e) => e.message).join("; "));
|
|
17471
17377
|
} else {
|
|
17472
17378
|
setMeldung(ergebnis.message);
|
|
17473
17379
|
}
|
|
@@ -17557,8 +17463,6 @@ ${teile.map((t) => `- ${t}`).join("\n")}`;
|
|
|
17557
17463
|
});
|
|
17558
17464
|
if (ergebnis.kind === "ok") {
|
|
17559
17465
|
setUebernommen((v) => ({ ...v, [type]: ergebnis.value.elementId }));
|
|
17560
|
-
} else if (ergebnis.kind === "rejected") {
|
|
17561
|
-
setMeldung(ergebnis.errors.map((e) => e.message).join("; "));
|
|
17562
17466
|
} else if (ergebnis.kind === "unsupported") {
|
|
17563
17467
|
setMeldung(`Dieser Host bietet \u201E${ergebnis.capability}" nicht an.`);
|
|
17564
17468
|
} else {
|
|
@@ -19279,7 +19183,6 @@ function createHfElementStore(options) {
|
|
|
19279
19183
|
}
|
|
19280
19184
|
|
|
19281
19185
|
// src/lib/szenen/hfEventChannel.ts
|
|
19282
|
-
init_hfEventLog();
|
|
19283
19186
|
function createHfEventChannel(options) {
|
|
19284
19187
|
const { namespace, token, sinceTs = 0 } = options;
|
|
19285
19188
|
const knownPaths = /* @__PURE__ */ new Set();
|
|
@@ -20846,9 +20749,6 @@ function createClientRunPort(options) {
|
|
|
20846
20749
|
}
|
|
20847
20750
|
|
|
20848
20751
|
// src/lib/szenen/hfImageStore.ts
|
|
20849
|
-
init_hfStateService();
|
|
20850
|
-
init_hfEventLog();
|
|
20851
|
-
init_hfEventTypes();
|
|
20852
20752
|
var LEERER_BESTAND = {
|
|
20853
20753
|
metadata: [],
|
|
20854
20754
|
tags: { by_category: {}, all: [] },
|
|
@@ -21218,11 +21118,14 @@ function ordersForPromptRun(events, runId) {
|
|
|
21218
21118
|
|
|
21219
21119
|
// src/lib/szenen/imageBatchStore.ts
|
|
21220
21120
|
var istText5 = (wert) => typeof wert === "string" && wert.trim().length > 0;
|
|
21221
|
-
function alsBilder(rohe) {
|
|
21121
|
+
function alsBilder(rohe, generationBatchId) {
|
|
21222
21122
|
if (!Array.isArray(rohe)) return [];
|
|
21223
21123
|
return rohe.filter((b) => !!b && typeof b === "object").filter((b) => istText5(b.media_ref) && typeof b.position === "number").map((b) => ({
|
|
21224
21124
|
media_ref: b.media_ref,
|
|
21225
21125
|
position: b.position,
|
|
21126
|
+
// Welcher Provideraufruf dieses Bild geliefert hat (§8) — aus dem
|
|
21127
|
+
// Ereignis, in dem es steht, nicht aus seiner Position abgeleitet.
|
|
21128
|
+
generation_batch_id: generationBatchId,
|
|
21226
21129
|
...istText5(b.asset_id) ? { asset_id: b.asset_id } : {},
|
|
21227
21130
|
// Der bestätigte MIME-Typ (Review 2026-08-18 §5 P2) muss durch das
|
|
21228
21131
|
// Rücklesen hindurch: Genau hier braucht ihn der Lesepfad, um den
|
|
@@ -21235,6 +21138,11 @@ function alsBilder(rohe) {
|
|
|
21235
21138
|
height: b.height ?? null
|
|
21236
21139
|
}));
|
|
21237
21140
|
}
|
|
21141
|
+
function gesamtHerkunft(origins) {
|
|
21142
|
+
const vertreten = new Set(origins.map((o) => o.source_kind));
|
|
21143
|
+
if (vertreten.size === 1) return [...vertreten][0];
|
|
21144
|
+
return "mixed";
|
|
21145
|
+
}
|
|
21238
21146
|
function collectImageBatches(events) {
|
|
21239
21147
|
const byItemId = /* @__PURE__ */ new Map();
|
|
21240
21148
|
for (const event of events) {
|
|
@@ -21244,21 +21152,29 @@ function collectImageBatches(events) {
|
|
|
21244
21152
|
if (payload.source_kind !== "generated_in_app" && payload.source_kind !== "external_import") continue;
|
|
21245
21153
|
const bestehend = byItemId.get(payload.item_id);
|
|
21246
21154
|
if (bestehend?.generation_batch_ids.includes(payload.generation_batch_id)) continue;
|
|
21247
|
-
const bilder = alsBilder(payload.images);
|
|
21155
|
+
const bilder = alsBilder(payload.images, payload.generation_batch_id);
|
|
21156
|
+
const herkunft = {
|
|
21157
|
+
generation_batch_id: payload.generation_batch_id,
|
|
21158
|
+
source_kind: payload.source_kind
|
|
21159
|
+
};
|
|
21248
21160
|
if (!bestehend) {
|
|
21249
21161
|
byItemId.set(payload.item_id, {
|
|
21250
21162
|
item_id: payload.item_id,
|
|
21251
21163
|
prompt_run_id: payload.prompt_run_id,
|
|
21252
21164
|
generation_batch_ids: [payload.generation_batch_id],
|
|
21253
21165
|
source_kind: payload.source_kind,
|
|
21166
|
+
origins: [herkunft],
|
|
21254
21167
|
title: istText5(payload.title) ? payload.title : `Batch aus Lauf ${payload.prompt_run_id}`,
|
|
21255
21168
|
images: bilder
|
|
21256
21169
|
});
|
|
21257
21170
|
continue;
|
|
21258
21171
|
}
|
|
21172
|
+
const origins = [...bestehend.origins, herkunft];
|
|
21259
21173
|
byItemId.set(payload.item_id, {
|
|
21260
21174
|
...bestehend,
|
|
21261
21175
|
generation_batch_ids: [...bestehend.generation_batch_ids, payload.generation_batch_id],
|
|
21176
|
+
origins,
|
|
21177
|
+
source_kind: gesamtHerkunft(origins),
|
|
21262
21178
|
images: [...bestehend.images, ...bilder]
|
|
21263
21179
|
});
|
|
21264
21180
|
}
|
|
@@ -21377,13 +21293,21 @@ async function legeBatchAb(options, input) {
|
|
|
21377
21293
|
`Die Batch-Metadaten fuer ${input.generationBatchId} konnten nicht protokolliert werden, obwohl die Bilder bereits im Bestand liegen: ${error instanceof Error ? error.message : String(error)}`
|
|
21378
21294
|
);
|
|
21379
21295
|
}
|
|
21296
|
+
const origins = [
|
|
21297
|
+
...bestehend?.origins ?? [],
|
|
21298
|
+
{ generation_batch_id: input.generationBatchId, source_kind: input.sourceKind }
|
|
21299
|
+
];
|
|
21380
21300
|
const zusammengefuehrt = {
|
|
21381
21301
|
item_id: itemId,
|
|
21382
21302
|
prompt_run_id: input.runId,
|
|
21383
21303
|
generation_batch_ids: bestehend ? [...bestehend.generation_batch_ids, input.generationBatchId] : [input.generationBatchId],
|
|
21384
|
-
source_kind:
|
|
21304
|
+
source_kind: gesamtHerkunft(origins),
|
|
21305
|
+
origins,
|
|
21385
21306
|
title: titel,
|
|
21386
|
-
images: [
|
|
21307
|
+
images: [
|
|
21308
|
+
...bestehend?.images ?? [],
|
|
21309
|
+
...abgelegt.map((bild) => ({ ...bild, generation_batch_id: input.generationBatchId }))
|
|
21310
|
+
]
|
|
21387
21311
|
};
|
|
21388
21312
|
return { kind: "ok", value: zusammengefuehrt };
|
|
21389
21313
|
}
|
|
@@ -21819,7 +21743,7 @@ function resolveBootstrapTarget(input) {
|
|
|
21819
21743
|
|
|
21820
21744
|
// src/components/AvatarArchitectApp.tsx
|
|
21821
21745
|
var import_jsx_runtime48 = require("react/jsx-runtime");
|
|
21822
|
-
function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onSelectMedia, buildInfo, initialHfToken, hfNamespace, allowDevNamespace, requireWorkspaceSelection = false, serverBaseUrl
|
|
21746
|
+
function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onSelectMedia, buildInfo, initialHfToken, hfNamespace, allowDevNamespace, requireWorkspaceSelection = false, serverBaseUrl }) {
|
|
21823
21747
|
(0, import_react50.useEffect)(() => {
|
|
21824
21748
|
const id = "flow-styles";
|
|
21825
21749
|
if (!document.getElementById(id)) {
|
|
@@ -21838,10 +21762,8 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
21838
21762
|
return null;
|
|
21839
21763
|
}
|
|
21840
21764
|
});
|
|
21841
|
-
const [projectLoaded, setProjectLoaded] = (0, import_react50.useState)(false);
|
|
21842
21765
|
const [hfToken, setHfToken] = (0, import_react50.useState)(initialHfToken || "");
|
|
21843
21766
|
const [hfTokenInput, setHfTokenInput] = (0, import_react50.useState)(initialHfToken || "");
|
|
21844
|
-
const [isLoadingFromHF, setIsLoadingFromHF] = (0, import_react50.useState)(false);
|
|
21845
21767
|
const [hfNamespaceLocal, setHfNamespaceLocal] = (0, import_react50.useState)(() => {
|
|
21846
21768
|
try {
|
|
21847
21769
|
return normalizeSelectableNamespace(localStorage.getItem("aa-hf-namespace"));
|
|
@@ -21945,7 +21867,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
21945
21867
|
bootstrapLog.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { style: { marginTop: 6, fontFamily: "monospace", fontSize: 10, color: "#78716c", lineHeight: 1.6 }, children: bootstrapLog.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { children: l }, i)) })
|
|
21946
21868
|
] })
|
|
21947
21869
|
] });
|
|
21948
|
-
const wsInputRef = (0, import_react50.useRef)(null);
|
|
21949
21870
|
const startApp = (choice) => {
|
|
21950
21871
|
try {
|
|
21951
21872
|
localStorage.setItem("aa-layout", choice);
|
|
@@ -22080,11 +22001,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22080
22001
|
}
|
|
22081
22002
|
});
|
|
22082
22003
|
const [isPromptCollapsed, setIsPromptCollapsed] = (0, import_react50.useState)(false);
|
|
22083
|
-
const [projectActionState, setProjectActionState] = (0, import_react50.useState)("idle");
|
|
22084
|
-
const syncServerDataRef = (0, import_react50.useRef)(null);
|
|
22085
22004
|
const [workspaceTags, setWorkspaceTags] = (0, import_react50.useState)(null);
|
|
22086
|
-
const [serverProjects, setServerProjects] = (0, import_react50.useState)([]);
|
|
22087
|
-
const [isLoadingFromServer, setIsLoadingFromServer] = (0, import_react50.useState)(false);
|
|
22088
22005
|
const [highContrast, setHighContrast] = (0, import_react50.useState)(() => {
|
|
22089
22006
|
try {
|
|
22090
22007
|
return localStorage.getItem("aa-contrast") === "high";
|
|
@@ -22532,51 +22449,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22532
22449
|
console.error("Download Error", e);
|
|
22533
22450
|
}
|
|
22534
22451
|
};
|
|
22535
|
-
const handleProjectExport = async () => {
|
|
22536
|
-
setProjectActionState("working-full");
|
|
22537
|
-
try {
|
|
22538
|
-
const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
|
|
22539
|
-
await onDownload(base64, "application/zip", `avatar_project_${getFormattedTimestamp()}.zip`);
|
|
22540
|
-
setProjectActionState("done");
|
|
22541
|
-
setTimeout(() => setProjectActionState("idle"), 3e3);
|
|
22542
|
-
} catch {
|
|
22543
|
-
setProjectActionState("error");
|
|
22544
|
-
}
|
|
22545
|
-
};
|
|
22546
|
-
const handleProjectImport = async (input) => {
|
|
22547
|
-
const file = input instanceof FileList ? input[0] : input;
|
|
22548
|
-
if (!file) return;
|
|
22549
|
-
setProjectActionState("working-full");
|
|
22550
|
-
try {
|
|
22551
|
-
const data = await importProjectFromZip(file);
|
|
22552
|
-
if (data.nodes) setNodes(data.nodes);
|
|
22553
|
-
if (data.edges) setEdges(data.edges);
|
|
22554
|
-
if (data.history) {
|
|
22555
|
-
setHistory(data.history);
|
|
22556
|
-
if (data.history.length > 0) setCurrentResult(data.history[0]);
|
|
22557
|
-
}
|
|
22558
|
-
if (data.galleryItems) setGalleryItems(data.galleryItems);
|
|
22559
|
-
if (data.settings) {
|
|
22560
|
-
setAspectRatio(data.settings.aspectRatio || "1:1");
|
|
22561
|
-
setSelectedModel(data.settings.selectedModel || "\u{1F34C} Nano Banana Pro");
|
|
22562
|
-
setSeed(data.settings.seed || 0);
|
|
22563
|
-
setSeedMode(data.settings.seedMode || "random");
|
|
22564
|
-
}
|
|
22565
|
-
if (data.workspaceTags?.by_category) setWorkspaceTags(data.workspaceTags);
|
|
22566
|
-
if (data.recentLabItemIds?.length) {
|
|
22567
|
-
const allGens = [...data.history || [], ...data.galleryItems || []];
|
|
22568
|
-
const labItems = groupGenerationsToLabItems(allGens);
|
|
22569
|
-
const recent = data.recentLabItemIds.map((id) => labItems.find((item) => item.id === id)).filter(Boolean);
|
|
22570
|
-
setRecentLabItems(recent);
|
|
22571
|
-
}
|
|
22572
|
-
setProjectActionState("done");
|
|
22573
|
-
setProjectLoaded(true);
|
|
22574
|
-
setTimeout(() => setProjectActionState("idle"), 2e3);
|
|
22575
|
-
} catch {
|
|
22576
|
-
setProjectActionState("error");
|
|
22577
|
-
setTimeout(() => setProjectActionState("idle"), 4e3);
|
|
22578
|
-
}
|
|
22579
|
-
};
|
|
22580
22452
|
const handleWorkspaceImport = (file) => {
|
|
22581
22453
|
const reader = new FileReader();
|
|
22582
22454
|
reader.onload = (ev) => {
|
|
@@ -22598,37 +22470,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22598
22470
|
};
|
|
22599
22471
|
reader.readAsText(file);
|
|
22600
22472
|
};
|
|
22601
|
-
const fetchServerProjects = async () => {
|
|
22602
|
-
if (!onFetchServerProjects) return;
|
|
22603
|
-
try {
|
|
22604
|
-
setServerProjects(await onFetchServerProjects());
|
|
22605
|
-
} catch {
|
|
22606
|
-
}
|
|
22607
|
-
};
|
|
22608
|
-
const handleServerSave = async (name) => {
|
|
22609
|
-
if (!onServerSave) return;
|
|
22610
|
-
setProjectActionState("working-full");
|
|
22611
|
-
try {
|
|
22612
|
-
const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
|
|
22613
|
-
await onServerSave(base64, name || `avatar_project_${getFormattedTimestamp()}`);
|
|
22614
|
-
await fetchServerProjects();
|
|
22615
|
-
setProjectActionState("done");
|
|
22616
|
-
setTimeout(() => setProjectActionState("idle"), 2e3);
|
|
22617
|
-
} catch {
|
|
22618
|
-
setProjectActionState("error");
|
|
22619
|
-
setTimeout(() => setProjectActionState("idle"), 4e3);
|
|
22620
|
-
}
|
|
22621
|
-
};
|
|
22622
|
-
const handleServerLoad = async (id) => {
|
|
22623
|
-
if (!onServerLoad) return;
|
|
22624
|
-
const file = await onServerLoad(id);
|
|
22625
|
-
await handleProjectImport(file);
|
|
22626
|
-
};
|
|
22627
|
-
const handleServerDelete = async (id) => {
|
|
22628
|
-
if (!onServerDelete) return;
|
|
22629
|
-
await onServerDelete(id);
|
|
22630
|
-
await fetchServerProjects();
|
|
22631
|
-
};
|
|
22632
22473
|
const handleHfInitialSync = async (onProgress) => {
|
|
22633
22474
|
if (!hfToken || !effectiveNamespace) return;
|
|
22634
22475
|
const existingIds = new Set((hfState?.metadata || []).map((m) => m.id));
|
|
@@ -22656,45 +22497,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22656
22497
|
onProgress(done, total);
|
|
22657
22498
|
}
|
|
22658
22499
|
};
|
|
22659
|
-
const handleComputeSyncDiff = async () => {
|
|
22660
|
-
if (!onFetchServerProjects || !onServerLoad) throw new Error("Server nicht konfiguriert");
|
|
22661
|
-
const projects = await onFetchServerProjects();
|
|
22662
|
-
if (projects.length === 0) return { localOnly: [...history], serverOnly: [] };
|
|
22663
|
-
const file = await onServerLoad(projects[0].id);
|
|
22664
|
-
const serverData = await importProjectFromZip(file);
|
|
22665
|
-
syncServerDataRef.current = serverData;
|
|
22666
|
-
const localOnly = history.filter((h) => !(serverData.history || []).find((s) => s.id === h.id));
|
|
22667
|
-
const serverOnly = (serverData.history || []).filter((s) => !history.find((h) => h.id === s.id));
|
|
22668
|
-
return { localOnly, serverOnly };
|
|
22669
|
-
};
|
|
22670
|
-
const handleExecuteSync = async (selectedLocalIds) => {
|
|
22671
|
-
const serverData = syncServerDataRef.current;
|
|
22672
|
-
if (!serverData || !onServerSave) return;
|
|
22673
|
-
setProjectActionState("working-full");
|
|
22674
|
-
try {
|
|
22675
|
-
const selectedLocal = history.filter((h) => selectedLocalIds.includes(h.id));
|
|
22676
|
-
const mergedHistory = [...serverData.history || [], ...selectedLocal];
|
|
22677
|
-
const { base64 } = await exportProjectToZip(
|
|
22678
|
-
serverData.nodes || nodes,
|
|
22679
|
-
serverData.edges || edges,
|
|
22680
|
-
mergedHistory,
|
|
22681
|
-
serverData.galleryItems || galleryItems,
|
|
22682
|
-
serverData.settings || { aspectRatio, selectedModel, seed, seedMode },
|
|
22683
|
-
serverData.workspaceTags || workspaceTags,
|
|
22684
|
-
serverData.recentLabItemIds || recentLabItems.map((i) => i.id)
|
|
22685
|
-
);
|
|
22686
|
-
await onServerSave(base64, `sync_${getFormattedTimestamp()}`);
|
|
22687
|
-
await fetchServerProjects();
|
|
22688
|
-
setProjectActionState("done");
|
|
22689
|
-
setTimeout(() => setProjectActionState("idle"), 2e3);
|
|
22690
|
-
} catch {
|
|
22691
|
-
setProjectActionState("error");
|
|
22692
|
-
setTimeout(() => setProjectActionState("idle"), 4e3);
|
|
22693
|
-
}
|
|
22694
|
-
};
|
|
22695
|
-
(0, import_react50.useEffect)(() => {
|
|
22696
|
-
if (activeTab === "setup" || activeTab === "sync") fetchServerProjects();
|
|
22697
|
-
}, [activeTab]);
|
|
22698
22500
|
const mergeWorkspaceTags = (local, remote) => {
|
|
22699
22501
|
if (!remote?.by_category) return local;
|
|
22700
22502
|
const merged = {};
|
|
@@ -22785,11 +22587,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22785
22587
|
}
|
|
22786
22588
|
if (showStart) {
|
|
22787
22589
|
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: [
|
|
22788
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("input", { ref: wsInputRef, type: "file", accept: ".zip", className: "hidden", onChange: (e) => {
|
|
22789
|
-
const f = e.target.files?.[0];
|
|
22790
|
-
if (f) handleProjectImport(f);
|
|
22791
|
-
e.target.value = "";
|
|
22792
|
-
} }),
|
|
22793
22590
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-1", children: [
|
|
22794
22591
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-white/15 text-[44px]", children: "palette" }),
|
|
22795
22592
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/25 text-[10px] font-bold uppercase tracking-[0.25em]", children: "Avatar Architect" }),
|
|
@@ -22813,21 +22610,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22813
22610
|
]
|
|
22814
22611
|
}
|
|
22815
22612
|
),
|
|
22816
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
22817
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
|
|
22818
|
-
"button",
|
|
22819
|
-
{
|
|
22820
|
-
onClick: () => wsInputRef.current?.click(),
|
|
22821
|
-
className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform",
|
|
22822
|
-
style: { height: 56, background: projectLoaded ? "#16a34a" : "#0284c7" },
|
|
22823
|
-
children: [
|
|
22824
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[22px]", children: projectLoaded ? "check_circle" : "folder_zip" }),
|
|
22825
|
-
projectLoaded ? "Projekt geladen \u2713" : "Projekt laden (.zip)"
|
|
22826
|
-
]
|
|
22827
|
-
}
|
|
22828
|
-
),
|
|
22829
|
-
!projectLoaded && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/20 text-[10px] text-center", children: "Baum, Bilder und Einstellungen wiederherstellen" })
|
|
22830
|
-
] }),
|
|
22831
22613
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
22832
22614
|
!initialHfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex gap-2 w-full", children: [
|
|
22833
22615
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
|
|
@@ -22878,64 +22660,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
22878
22660
|
},
|
|
22879
22661
|
ns
|
|
22880
22662
|
))
|
|
22881
|
-
] })
|
|
22882
|
-
hfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
|
|
22883
|
-
"button",
|
|
22884
|
-
{
|
|
22885
|
-
disabled: isLoadingFromHF,
|
|
22886
|
-
onClick: async () => {
|
|
22887
|
-
setIsLoadingFromHF(true);
|
|
22888
|
-
try {
|
|
22889
|
-
const { hfListProjects: hfListProjects2, hfDownloadProject: hfDownloadProject2 } = await Promise.resolve().then(() => (init_hfStateService(), hfStateService_exports));
|
|
22890
|
-
const projects = await hfListProjects2(hfToken);
|
|
22891
|
-
if (projects.length > 0) {
|
|
22892
|
-
const file = await hfDownloadProject2(projects[0].path, hfToken);
|
|
22893
|
-
await handleProjectImport(file);
|
|
22894
|
-
}
|
|
22895
|
-
} catch (e) {
|
|
22896
|
-
alert("HF Fehler: " + e.message);
|
|
22897
|
-
} finally {
|
|
22898
|
-
setIsLoadingFromHF(false);
|
|
22899
|
-
}
|
|
22900
|
-
},
|
|
22901
|
-
className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform disabled:opacity-50",
|
|
22902
|
-
style: { height: 56, background: "#f59e0b" },
|
|
22903
|
-
children: [
|
|
22904
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: `material-symbols-outlined text-[22px]${isLoadingFromHF ? " animate-spin" : ""}`, children: isLoadingFromHF ? "sync" : "cloud_download" }),
|
|
22905
|
-
isLoadingFromHF ? "Laden\u2026" : "Von HF laden"
|
|
22906
|
-
]
|
|
22907
|
-
}
|
|
22908
|
-
),
|
|
22909
|
-
hfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/20 text-[10px] text-center", children: "Letzten Stand von Hugging Face laden" })
|
|
22910
|
-
] }),
|
|
22911
|
-
onFetchServerProjects && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
22912
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
|
|
22913
|
-
"button",
|
|
22914
|
-
{
|
|
22915
|
-
disabled: isLoadingFromServer,
|
|
22916
|
-
onClick: async () => {
|
|
22917
|
-
setIsLoadingFromServer(true);
|
|
22918
|
-
try {
|
|
22919
|
-
const projects = await onFetchServerProjects();
|
|
22920
|
-
setServerProjects(projects);
|
|
22921
|
-
if (projects.length > 0 && onServerLoad) {
|
|
22922
|
-
const file = await onServerLoad(projects[0].id);
|
|
22923
|
-
await handleProjectImport(file);
|
|
22924
|
-
}
|
|
22925
|
-
} catch {
|
|
22926
|
-
} finally {
|
|
22927
|
-
setIsLoadingFromServer(false);
|
|
22928
|
-
}
|
|
22929
|
-
},
|
|
22930
|
-
className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform disabled:opacity-50",
|
|
22931
|
-
style: { height: 56, background: "#7c3aed" },
|
|
22932
|
-
children: [
|
|
22933
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: `material-symbols-outlined text-[22px]${isLoadingFromServer ? " animate-spin" : ""}`, children: isLoadingFromServer ? "sync" : "cloud_download" }),
|
|
22934
|
-
isLoadingFromServer ? "Laden\u2026" : "Vom Server laden"
|
|
22935
|
-
]
|
|
22936
|
-
}
|
|
22937
|
-
),
|
|
22938
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/20 text-[10px] text-center", children: "Letzten Stand vom Server wiederherstellen" })
|
|
22663
|
+
] })
|
|
22939
22664
|
] }),
|
|
22940
22665
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flow-szenen-entry", children: [
|
|
22941
22666
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
|
|
@@ -23293,26 +23018,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
23293
23018
|
}, onTagCreate: handleTagCreate, onTagUpdate: handleTagUpdate, onTagDelete: handleTagDelete, onScanImage: handleScanImage, isScanning: isScanningImage }) }),
|
|
23294
23019
|
activeTab === "setup" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SetupPanel, { onWorkspaceImport: handleWorkspaceImport, buildInfo, tagBase: tagBaseResult }),
|
|
23295
23020
|
activeTab === "sync" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
|
|
23296
|
-
|
|
23021
|
+
HfMediaSyncTab,
|
|
23297
23022
|
{
|
|
23298
23023
|
topSlot: syncTopSlot,
|
|
23299
|
-
onProjectExport: handleProjectExport,
|
|
23300
|
-
onProjectImport: (f) => handleProjectImport(f),
|
|
23301
|
-
onWorkspaceImport: handleWorkspaceImport,
|
|
23302
|
-
projectActionState,
|
|
23303
|
-
serverProjects: onFetchServerProjects ? serverProjects : void 0,
|
|
23304
|
-
onServerSave: onServerSave ? handleServerSave : void 0,
|
|
23305
|
-
onServerLoad: onServerLoad ? handleServerLoad : void 0,
|
|
23306
|
-
onServerDelete: onServerDelete ? handleServerDelete : void 0,
|
|
23307
|
-
onRefreshServerProjects: onFetchServerProjects ? fetchServerProjects : void 0,
|
|
23308
|
-
onComputeSyncDiff: onFetchServerProjects && onServerLoad && onServerSave ? handleComputeSyncDiff : void 0,
|
|
23309
|
-
onExecuteSync: onFetchServerProjects && onServerLoad && onServerSave ? handleExecuteSync : void 0,
|
|
23310
|
-
hfToken: hfToken || void 0,
|
|
23311
|
-
onHfLoad: (f) => handleProjectImport(f),
|
|
23312
|
-
onProjectExportBase64: async () => {
|
|
23313
|
-
const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
|
|
23314
|
-
return base64;
|
|
23315
|
-
},
|
|
23316
23024
|
onHfInitialSync: hfToken ? handleHfInitialSync : void 0
|
|
23317
23025
|
}
|
|
23318
23026
|
),
|
|
@@ -23694,26 +23402,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
23694
23402
|
activeTab === "inspect" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(InspectPanel, { currentResult, history, onSelect: setCurrentResult }),
|
|
23695
23403
|
activeTab === "setup" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SetupPanel, { onWorkspaceImport: handleWorkspaceImport, buildInfo, tagBase: tagBaseResult }),
|
|
23696
23404
|
activeTab === "sync" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
|
|
23697
|
-
|
|
23405
|
+
HfMediaSyncTab,
|
|
23698
23406
|
{
|
|
23699
23407
|
topSlot: syncTopSlot,
|
|
23700
|
-
onProjectExport: handleProjectExport,
|
|
23701
|
-
onProjectImport: (f) => handleProjectImport(f),
|
|
23702
|
-
onWorkspaceImport: handleWorkspaceImport,
|
|
23703
|
-
projectActionState,
|
|
23704
|
-
serverProjects: onFetchServerProjects ? serverProjects : void 0,
|
|
23705
|
-
onServerSave: onServerSave ? handleServerSave : void 0,
|
|
23706
|
-
onServerLoad: onServerLoad ? handleServerLoad : void 0,
|
|
23707
|
-
onServerDelete: onServerDelete ? handleServerDelete : void 0,
|
|
23708
|
-
onRefreshServerProjects: onFetchServerProjects ? fetchServerProjects : void 0,
|
|
23709
|
-
onComputeSyncDiff: onFetchServerProjects && onServerLoad && onServerSave ? handleComputeSyncDiff : void 0,
|
|
23710
|
-
onExecuteSync: onFetchServerProjects && onServerLoad && onServerSave ? handleExecuteSync : void 0,
|
|
23711
|
-
hfToken: hfToken || void 0,
|
|
23712
|
-
onHfLoad: (f) => handleProjectImport(f),
|
|
23713
|
-
onProjectExportBase64: async () => {
|
|
23714
|
-
const { base64 } = await Promise.resolve().then(() => (init_project(), project_exports)).then((m) => m.exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id)));
|
|
23715
|
-
return base64;
|
|
23716
|
-
},
|
|
23717
23408
|
onHfInitialSync: hfToken ? handleHfInitialSync : void 0
|
|
23718
23409
|
}
|
|
23719
23410
|
),
|
|
@@ -23738,10 +23429,6 @@ function FaApp({
|
|
|
23738
23429
|
libToken,
|
|
23739
23430
|
allowDevNamespace,
|
|
23740
23431
|
serverBaseUrl,
|
|
23741
|
-
onFetchServerProjects,
|
|
23742
|
-
onServerSave,
|
|
23743
|
-
onServerLoad,
|
|
23744
|
-
onServerDelete,
|
|
23745
23432
|
buildInfo
|
|
23746
23433
|
}) {
|
|
23747
23434
|
const [hfNamespace, setHfNamespace] = (0, import_react51.useState)(void 0);
|
|
@@ -23768,10 +23455,6 @@ function FaApp({
|
|
|
23768
23455
|
allowDevNamespace: !hfNamespace && allowDevNamespace,
|
|
23769
23456
|
requireWorkspaceSelection: true,
|
|
23770
23457
|
serverBaseUrl,
|
|
23771
|
-
onFetchServerProjects,
|
|
23772
|
-
onServerSave,
|
|
23773
|
-
onServerLoad,
|
|
23774
|
-
onServerDelete,
|
|
23775
23458
|
buildInfo
|
|
23776
23459
|
}
|
|
23777
23460
|
);
|
|
@@ -24336,10 +24019,6 @@ async function runTagsLegacyImport(options) {
|
|
|
24336
24019
|
writeMarker: options.writeMarker
|
|
24337
24020
|
});
|
|
24338
24021
|
}
|
|
24339
|
-
|
|
24340
|
-
// src/index.ts
|
|
24341
|
-
init_hfStateService();
|
|
24342
|
-
init_hfStateService();
|
|
24343
24022
|
// Annotate the CommonJS export names for ESM import in node:
|
|
24344
24023
|
0 && (module.exports = {
|
|
24345
24024
|
AvatarArchitectApp,
|
|
@@ -24350,6 +24029,7 @@ init_hfStateService();
|
|
|
24350
24029
|
FaToolsBadge,
|
|
24351
24030
|
FlowWorkspacePicker,
|
|
24352
24031
|
GLOBAL_STYLES,
|
|
24032
|
+
HfMediaSyncTab,
|
|
24353
24033
|
HistoryPanel,
|
|
24354
24034
|
InspectPanel,
|
|
24355
24035
|
LIBRARY_CUTOVER_MARKER,
|
|
@@ -24366,7 +24046,6 @@ init_hfStateService();
|
|
|
24366
24046
|
ListView,
|
|
24367
24047
|
MediaLibrary,
|
|
24368
24048
|
PillButton,
|
|
24369
|
-
ProjectSyncTab,
|
|
24370
24049
|
PromptTab,
|
|
24371
24050
|
SCOPE_MAP,
|
|
24372
24051
|
SectionLabel,
|
|
@@ -24400,7 +24079,6 @@ init_hfStateService();
|
|
|
24400
24079
|
createTagsDefault,
|
|
24401
24080
|
deriveTagBaseWorkspaceTags,
|
|
24402
24081
|
deriveTagViewModel,
|
|
24403
|
-
exportProjectToZip,
|
|
24404
24082
|
formatTreeToMarkdown,
|
|
24405
24083
|
frameToGeneration,
|
|
24406
24084
|
getFormattedTimestamp,
|
|
@@ -24409,15 +24087,10 @@ init_hfStateService();
|
|
|
24409
24087
|
groupGenerationsToLabItems,
|
|
24410
24088
|
hfBatchArchive,
|
|
24411
24089
|
hfBootstrapFromLegacy,
|
|
24412
|
-
hfDeleteProject,
|
|
24413
|
-
hfDownloadProject,
|
|
24414
24090
|
hfListDir,
|
|
24415
|
-
hfListProjects,
|
|
24416
24091
|
hfLoadImageAsBase64,
|
|
24417
24092
|
hfUploadImage,
|
|
24418
|
-
hfUploadProjectForm,
|
|
24419
24093
|
hfUploadSmallFile,
|
|
24420
|
-
importProjectFromZip,
|
|
24421
24094
|
injectXMPMetadata,
|
|
24422
24095
|
interpretSdkError,
|
|
24423
24096
|
libraryContract,
|