From 0ff5030ad2c8dddec123c8b1c19e2a206672b2c7 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sat, 25 Apr 2026 18:16:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(comic):=20Mc3=20=E2=80=94=20Story-Create?= =?UTF-8?q?=20nutzt=20Character-Mode=20+=20Quick-Fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story-Anlegen ist jetzt zweigleisig: Default ist Character-Mode (picke einen iterierten Comic-Character mit gepinntem Look), Fallback ist Quick-Mode (rohes face/body/garments wie bisher) als opt-in-Toggle für Spontan-Stories ohne Setup. Datenmodell-Erweiterung (soft, kein Breaking-Change): - LocalComicStory + ComicStory bekommen ein optionales `characterId?: string | null` Feld, plaintext, FK auf comicCharacters.id. Im Quick-Modus null, im Character-Modus die gewählte Character-id. - `characterMediaIds` bleibt das einzige Feld, das runPanelGenerate liest — im Character-Modus enthält es genau die `pinnedVariantMediaId` als single-element-Array (Snapshot zum Story-Create-Zeitpunkt). Re-Pinning eines Characters ändert bestehende Stories also NICHT, weil sie das mediaId fix gespeichert haben. Im Quick-Modus enthält's face + body? + garments[] wie vorher. Beide Modi gehen durch denselben /picture/generate-with-reference-Pfad. - Soft-Migration: bestehende Stories ohne `characterId` zeigen weiterhin keine Character-Linkage und rendern wie vorher (die `characterMediaIds` waren vorher ja schon die Quelle). Neue Komponente: - `CharacterRefPicker.svelte` ersetzt den alten `CharacterPicker` in StoryForm. Mode-Toggle (Character | Quick) erscheint nur wenn Characters existieren — sonst startet's direkt im Quick-Modus. Character-Mode zeigt Grid der usableCharacters (nicht-archived + pinnedVariantId gesetzt) mit Cover, Style-Badge, Active-Border. "+ Neuer Character"-Tile öffnet die Builder-Route. Quick-Modus rendert intern den alten CharacterPicker (face/body/garments) — reuse statt parallel zu pflegen. StoryForm: - 2 neue $state-Felder: `characterId` und (umbenannt-) der bestehende `characterMediaIds`. CharacterRefPicker emittiert beide via onChange-Callback. - createStory bekommt `characterId` mit, das landet auf der Story- Row. canSubmit greift weiterhin auf `characterMediaIds.length > 0` — beide Modi liefern mindestens 1 ref. CharacterBuilder Bugfix: prettier hatte den Add-Prompt-Placeholder mit nested double-quotes zerstört (z.B. "freundlicher Ausdruck" wurde zu invalidem HTML). Auf einfache Liste umgestellt. 8/8 Encryption-Tests weiter grün. check für comic-files clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../comic/components/CharacterBuilder.svelte | 2 +- .../components/CharacterRefPicker.svelte | 233 ++++++++++++++++++ .../modules/comic/components/StoryForm.svelte | 17 +- .../modules/comic/stores/stories.svelte.ts | 5 + .../apps/web/src/lib/modules/comic/types.ts | 21 +- 5 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/modules/comic/components/CharacterRefPicker.svelte diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/CharacterBuilder.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/CharacterBuilder.svelte index 8fba2985d..99c84fff6 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/CharacterBuilder.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/CharacterBuilder.svelte @@ -194,7 +194,7 @@ id="character-add-prompt" type="text" bind:value={addPrompt} - placeholder="z.B. "freundlicher Ausdruck", "casual outfit", "action pose"" + placeholder="z.B. freundlicher Ausdruck, casual outfit, action pose" maxlength={200} class="block w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" disabled={busy} diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/CharacterRefPicker.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/CharacterRefPicker.svelte new file mode 100644 index 000000000..2defd322a --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/comic/components/CharacterRefPicker.svelte @@ -0,0 +1,233 @@ + + + +
+ + {#if usableCharacters.length > 0} +
+ + +
+ {/if} + + {#if mode === 'character'} +
+
+

+ Comic-Character wählen +

+

+ Iterier vorher einen Character mit deinem Stil — alle Panels nutzen dann denselben + gepinnten Look. +

+
+ + {#if usableCharacters.length === 0} + {#if !hasFace} + + {:else} +
+

Noch keine Characters mit Pin.

+

+ Bau einen Comic-Character aus deinem Foto — Stil wählen, 4 Varianten generieren, beste + pinnen. +

+ + + Character bauen + +
+ {/if} + {:else} +
+ {#each usableCharacters as character (character.id)} + {@const isSelected = selectedCharacterId === character.id} + {@const cover$ = usePanelImage(character.pinnedVariantId ?? null)} + {@const cover = cover$.value} + + {/each} + + + + Neuer Character + +
+ {/if} +
+ {:else} + +
+
+

+ Quick-Modus (Roh-Refs) +

+

+ Direkt face-ref + optional body-ref + Garments aus dem Schrank — ohne Character-Iteration. + Konsistenz zwischen Panels schwächer. +

+
+ +
+ {/if} +
diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/StoryForm.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/StoryForm.svelte index f23e1fea2..1ac68f2fa 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/StoryForm.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/StoryForm.svelte @@ -15,10 +15,11 @@ import { comicStoriesStore } from '../stores/stories.svelte'; import type { ComicStyle } from '../types'; import StylePicker from './StylePicker.svelte'; - import CharacterPicker from './CharacterPicker.svelte'; + import CharacterRefPicker from './CharacterRefPicker.svelte'; let title = $state(''); let style = $state('comic'); + let characterId = $state(null); let characterMediaIds = $state([]); let storyContext = $state(''); let submitting = $state(false); @@ -38,6 +39,7 @@ const story = await comicStoriesStore.createStory({ title: title.trim(), style, + characterId, characterMediaIds, storyContext: storyContext.trim() || null, }); @@ -81,10 +83,15 @@

- - (characterMediaIds = next)} + + { + characterId = nextId; + characterMediaIds = nextRefs; + }} disabled={submitting} /> diff --git a/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts b/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts index 5402e58e6..dd377f972 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/comic/stores/stories.svelte.ts @@ -24,6 +24,10 @@ export interface CreateStoryInput { title: string; style: ComicStyle; characterMediaIds: string[]; + /** When the story is bound to a comicCharacter (Character-Mode), the + * FK lands here for display + cross-ref. Quick-Mode stories pass + * `null` and only fill `characterMediaIds` with raw face/body/garments. */ + characterId?: string | null; description?: string | null; storyContext?: string | null; tags?: string[]; @@ -45,6 +49,7 @@ export const comicStoriesStore = { title: input.title, description: input.description ?? null, style: input.style, + characterId: input.characterId ?? null, characterMediaIds: [...input.characterMediaIds], storyContext: input.storyContext ?? null, panelImageIds: [], diff --git a/apps/mana/apps/web/src/lib/modules/comic/types.ts b/apps/mana/apps/web/src/lib/modules/comic/types.ts index 5922652de..445ad1a52 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/types.ts +++ b/apps/mana/apps/web/src/lib/modules/comic/types.ts @@ -65,10 +65,24 @@ export interface LocalComicStory extends BaseRecord { title: string; description?: string | null; style: ComicStyle; + /** + * FK to the comicCharacter that drives this story (Character-Mode, + * Mc3+). Plaintext — used for "Charakter: "-Anzeige im + * DetailView und für `useStoriesByCharacter`-Cross-Refs. + * + * `null` when the story was created in **Quick-Mode** (rohes + * face/body/garments-Setup ohne Character-Iteration). Beide Modi + * funktionieren parallel — die Story hängt am `characterMediaIds`- + * Array für die eigentliche Render-Logik (Snapshot-Pattern). + */ + characterId?: string | null; /** * Reference-image IDs passed unchanged to every panel-generate call. - * Minimum: the primary face-ref from meImages. Optional additions: - * body-ref + up to ~3 wardrobe-garment photos for a costume-setup. + * Character-Mode: enthält genau die `pinnedVariantMediaId` des + * referenzierten comicCharacters zum Story-Create-Zeitpunkt + * (Snapshot — Re-Pinning ändert das nicht rückwirkend). + * Quick-Mode: enthält face-ref + optional body-ref + Wardrobe- + * Garment-Photos. * Capped at 8 by the backend (MAX_REFERENCE_IMAGES in the /picture/ * generate-with-reference endpoint). */ @@ -104,6 +118,8 @@ export interface ComicStory { title: string; description?: string; style: ComicStyle; + /** FK to the comic-character driving this story; undefined in Quick-Mode. */ + characterId?: string; characterMediaIds: string[]; storyContext?: string; panelImageIds: string[]; @@ -122,6 +138,7 @@ export function toStory(local: LocalComicStory): ComicStory { title: local.title, description: local.description ?? undefined, style: local.style, + characterId: local.characterId ?? undefined, characterMediaIds: local.characterMediaIds ?? [], storyContext: local.storyContext ?? undefined, panelImageIds: local.panelImageIds ?? [],