mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat(comic): Mc5 — Wardrobe-Hook "Als Comic-Character"
Brücke von Wardrobe nach Comic: User klickt auf einem Outfit oder
einem einzelnen Kleidungsstück „Als Comic-Character", landet im
Character-Builder mit pre-filltem Add-Prompt ("wearing the
Bühnenoutfit"), picked Stil und rendert die ersten 4 Varianten.
Wardrobe-Buttons:
- DetailOutfitView: unterhalb des TryOnButton ein outline-Link
navigiert zu `/comic/character/new?title=…&prompt=wearing+the+
OUTFITNAME+outfit`.
- DetailGarmentView: analog mit `prompt=wearing+GARMENTNAME` für
ein einzelnes Kleidungsstück. Beide nur sichtbar wenn das
Outfit/Garment nicht archiviert ist.
- Sparkle-Icon + dezent neutraler Border-Style (nicht primary —
das ist die TryOn-CTA), hover schaltet auf primary/40.
Comic CharacterBuilder bekommt drei optionale Props:
`initialName?`, `initialAddPrompt?`, `initialStyle?`. Im
extend-Modus ignoriert (Source ist dann der existing-Character),
im create-Modus dienen sie als $state-Initialwerte. Routine read
ist intentional — Mounting passiert frisch pro Route-Visit, also
einmaliges Capture passt.
`/comic/character/new/+page.svelte` parsed jetzt
`page.url.searchParams` für `title`, `prompt`, `style` und reicht
sie als Props durch. style wird gegen die VALID_STYLES-Liste
validiert — defekte URL-Params fallen ohne Crash auf
"unset/default" zurück.
Bewusst NICHT gemacht: Try-On-Output direkt als sourceBodyMediaId
verwenden. Das Try-On-Bild ist im mana-media mit `app='picture'`
getaggt; `verifyMediaOwnership` auf
`/picture/generate-with-reference` akzeptiert nur
`['me','wardrobe','comic']` — der Comic-Generate würde mit
HTTP 404 abbrechen. Lösung wäre eine Server-Route die Picture-
Output als Comic-Asset re-tagged, das ist aber eigene Spec.
Aktueller Pfad ist sauberer: rohe meImages-Refs bleiben Source,
der Add-Prompt steuert den Outfit-Look.
Plan-Doc §11 Mc5 dokumentiert den Pfad + warum kein
Try-On-Reuse.
Comic-Files type-checken sauber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef96948ea0
commit
3d30e39ae7
5 changed files with 80 additions and 13 deletions
|
|
@ -31,27 +31,36 @@
|
|||
* character — name+style+source are locked, only Add-Prompt
|
||||
* is editable per generation. */
|
||||
existing?: ComicCharacter;
|
||||
/** Optional pre-fills for create-mode — used by the wardrobe-
|
||||
* hook (Mc5) to seed an addPrompt like "wearing the
|
||||
* Bühnenoutfit" when the user clicks "Als Comic-Character"
|
||||
* on a Wardrobe-Outfit. Ignored in extend-mode. */
|
||||
initialName?: string;
|
||||
initialAddPrompt?: string;
|
||||
initialStyle?: ComicStyle;
|
||||
/** Called after the first successful variant batch with the
|
||||
* resulting character id, so the parent route can navigate. */
|
||||
onCreated?: (characterId: string) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
let { existing, onClose, onCreated }: Props = $props();
|
||||
let { existing, initialName, initialAddPrompt, initialStyle, onClose, onCreated }: Props =
|
||||
$props();
|
||||
|
||||
const isExtend = $derived(Boolean(existing));
|
||||
|
||||
// Builder state. In extend-mode all of these come from `existing`
|
||||
// at mount time and aren't editable; in create-mode the user fills
|
||||
// them in. Init-time read of `existing` is intentional — the
|
||||
// them in (with optional pre-fills from URL-params via the route
|
||||
// page wrapper). Init-time read is intentional — the
|
||||
// character is always remounted via {#key} when the route id
|
||||
// changes, so capturing the snapshot here is correct.
|
||||
// svelte-ignore state_referenced_locally
|
||||
let name = $state(existing?.name ?? '');
|
||||
let name = $state(existing?.name ?? initialName ?? '');
|
||||
// svelte-ignore state_referenced_locally
|
||||
let style = $state<ComicStyle>(existing?.style ?? 'comic');
|
||||
let style = $state<ComicStyle>(existing?.style ?? initialStyle ?? 'comic');
|
||||
// svelte-ignore state_referenced_locally
|
||||
let addPrompt = $state(existing?.addPrompt ?? '');
|
||||
let addPrompt = $state(existing?.addPrompt ?? initialAddPrompt ?? '');
|
||||
|
||||
type Quality = 'low' | 'medium' | 'high';
|
||||
const QUALITIES: readonly Quality[] = ['low', 'medium', 'high'] as const;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { CheckCircle, PencilSimple, Archive, Trash } from '@mana/shared-icons';
|
||||
import { CheckCircle, PencilSimple, Archive, Sparkle, Trash } from '@mana/shared-icons';
|
||||
import { useGarment, useGarmentSoloTryOns, useOutfitsContainingGarment } from '../queries';
|
||||
import { wardrobeGarmentsStore } from '../stores/garments.svelte';
|
||||
import { garmentPhotoUrl } from '../api/media-url';
|
||||
|
|
@ -234,6 +234,20 @@
|
|||
<!-- Try-on — "wie sähe das an mir aus" -->
|
||||
<GarmentTryOnButton {garment} />
|
||||
|
||||
<!-- Mc5: Comic-Character aus Garment. Pre-fillt den
|
||||
Add-Prompt im Builder mit "wearing X" — User
|
||||
picked dann Stil + rendert die ersten 4 Varianten. -->
|
||||
{#if garment && !garment.isArchived}
|
||||
<a
|
||||
href={`/comic/character/new?title=${encodeURIComponent(garment.name)}&prompt=${encodeURIComponent('wearing ' + garment.name)}`}
|
||||
class="flex w-full items-center justify-center gap-1.5 rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-foreground transition-colors hover:border-primary/40 hover:bg-primary/5"
|
||||
title="Aus diesem Kleidungsstück einen Comic-Character generieren"
|
||||
>
|
||||
<Sparkle size={12} />
|
||||
Als Comic-Character
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<!-- Secondary-action row: "Heute getragen" is the frequent
|
||||
positive action and takes most of the width; Archive and
|
||||
Löschen shrink to icon-only buttons on the right so they
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ArrowLeft, Archive, Heart, PencilSimple, Trash } from '@mana/shared-icons';
|
||||
import { ArrowLeft, Archive, Heart, PencilSimple, Sparkle, Trash } from '@mana/shared-icons';
|
||||
import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy';
|
||||
import { useAllGarments, useOutfit, useOutfitTryOns } from '../queries';
|
||||
import { wardrobeOutfitsStore } from '../stores/outfits.svelte';
|
||||
|
|
@ -136,6 +136,20 @@
|
|||
<!-- Try-On action (M4) -->
|
||||
<TryOnButton {outfit} garments={resolvedGarments} />
|
||||
|
||||
<!-- Mc5: Comic-Character aus Outfit. Pre-fillt den
|
||||
Add-Prompt im Builder, User picked Stil + rendert
|
||||
selbst die ersten 4 Varianten. -->
|
||||
{#if outfit && !outfit.isArchived}
|
||||
<a
|
||||
href={`/comic/character/new?title=${encodeURIComponent(outfit.name)}&prompt=${encodeURIComponent('wearing the ' + outfit.name + ' outfit')}`}
|
||||
class="flex w-full items-center justify-center gap-1.5 rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-foreground transition-colors hover:border-primary/40 hover:bg-primary/5"
|
||||
title="Aus diesem Outfit einen Comic-Character generieren"
|
||||
>
|
||||
<Sparkle size={12} />
|
||||
Als Comic-Character
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if tryOns.length > 0}
|
||||
<div>
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { RoutePage } from '$lib/components/shell';
|
||||
import CharacterBuilder from '$lib/modules/comic/components/CharacterBuilder.svelte';
|
||||
import type { ComicStyle } from '$lib/modules/comic/types';
|
||||
|
||||
const VALID_STYLES: ComicStyle[] = ['comic', 'manga', 'cartoon', 'graphic-novel', 'webtoon'];
|
||||
|
||||
function isValidStyle(s: string | null): s is ComicStyle {
|
||||
return s !== null && (VALID_STYLES as string[]).includes(s);
|
||||
}
|
||||
|
||||
// Optional URL-param prefill — used by the Mc5 wardrobe-hook
|
||||
// ("Als Comic-Character"-Button auf einem Outfit/Garment): we land
|
||||
// here with `?prompt=wearing+the+Bühnenoutfit&style=manga`, the
|
||||
// builder picks them up as initial state. Plain user creates
|
||||
// (no params) are unaffected.
|
||||
const initialName = $derived(page.url.searchParams.get('title') ?? undefined);
|
||||
const initialAddPrompt = $derived(page.url.searchParams.get('prompt') ?? undefined);
|
||||
const styleParam = $derived(page.url.searchParams.get('style'));
|
||||
const initialStyle = $derived(isValidStyle(styleParam) ? styleParam : undefined);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -16,6 +34,6 @@
|
|||
Detail kannst du jederzeit weitere generieren.
|
||||
</p>
|
||||
</header>
|
||||
<CharacterBuilder />
|
||||
<CharacterBuilder {initialName} {initialAddPrompt} {initialStyle} />
|
||||
</div>
|
||||
</RoutePage>
|
||||
|
|
|
|||
|
|
@ -685,11 +685,23 @@ Encryption-Roundtrip-Test.
|
|||
`generate_character_variant` in AI_TOOL_CATALOG.
|
||||
- Persona kann „mach mir einen Manga-Character für Story X" sagen.
|
||||
|
||||
**Mc5 — Wardrobe-Hook** (~2h, optional):
|
||||
- In Wardrobe-DetailOutfitView nach erfolgreichem Try-On ein
|
||||
Knopf „Als Comic-Character speichern" → öffnet Builder mit
|
||||
Try-On-Result als optionalem `sourceBodyMediaId`.
|
||||
- In DetailGarmentView analog für ein einzelnes Kleidungsstück.
|
||||
**Mc5 — Wardrobe-Hook** ✅ shipped:
|
||||
- In Wardrobe-DetailOutfitView ein „Als Comic-Character"-Knopf
|
||||
unterhalb des TryOnButton, navigiert zu
|
||||
`/comic/character/new?title=…&prompt=wearing+the+OUTFITNAME+outfit`.
|
||||
- In DetailGarmentView analog mit `prompt=wearing+GARMENTNAME`.
|
||||
- CharacterBuilder akzeptiert `initialName` / `initialAddPrompt` /
|
||||
`initialStyle`-Props. Die `/comic/character/new`-Route liest
|
||||
URL-Params und reicht sie als initial state durch — der Builder
|
||||
startet mit dem prefillten Add-Prompt, User picked Stil + rendert
|
||||
die ersten 4 Varianten selbst.
|
||||
- Bewusst KEIN Try-On-Output als sourceBodyMediaId: das
|
||||
Try-On-Bild ist mit `app='picture'` getaggt, der
|
||||
`verifyMediaOwnership`-Check des Comic-Endpoints akzeptiert nur
|
||||
`['me', 'wardrobe', 'comic']`. Re-Upload als 'comic' wäre eine
|
||||
zusätzliche Server-Route — Aufwand vs. Nutzen nicht klar.
|
||||
Workflow stattdessen: rohe meImages bleiben Source, der
|
||||
Add-Prompt steuert den Outfit-Look.
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue