diff --git a/apps/mana/apps/web/src/lib/modules/picture/components/ReferenceImagePicker.svelte b/apps/mana/apps/web/src/lib/modules/picture/components/ReferenceImagePicker.svelte index 1a8feb84a..04811b340 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/components/ReferenceImagePicker.svelte +++ b/apps/mana/apps/web/src/lib/modules/picture/components/ReferenceImagePicker.svelte @@ -10,6 +10,8 @@
@@ -40,6 +67,36 @@ {/each} + {#if !face$.loading && !face} +
+
+ +
+

Lade ein Gesichtsbild hoch

+

+ Wir brauchen dich auf Bild, damit Try-On Kleidung an dir visualisieren kann. Das Bild + bleibt lokal und wird nur für deine eigenen Generierungen genutzt. +

+
+
+ + {#if faceUploadError} + + {/if} +
+ {/if} +
{#if activeTab === 'garments'} diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte index 2d6bf5446..b064b1a1d 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte @@ -14,6 +14,8 @@ import { Sparkle, UserCircle, Info } from '@mana/shared-icons'; import { getActiveSpace } from '$lib/data/scope'; import { useImageByPrimary } from '$lib/modules/profile/queries'; + import MeImageUploadZone from '$lib/modules/profile/components/MeImageUploadZone.svelte'; + import { ingestMeImageFile } from '$lib/modules/profile/api/me-images'; import { isAccessoryGarment, runGarmentTryOn } from '../api/try-on'; import type { Garment } from '../types'; @@ -40,6 +42,9 @@ let error = $state(null); let lastResultUrl = $state(null); + let uploadingRef = $state(false); + let uploadRefError = $state(null); + const estimatedCredits = 10; async function handleClick() { @@ -60,6 +65,23 @@ running = false; } } + + async function handleRefUpload( + files: File[], + kind: 'face' | 'fullbody', + slot: 'face-ref' | 'body-ref' + ) { + if (files.length === 0) return; + uploadingRef = true; + uploadRefError = null; + try { + await ingestMeImageFile(files[0], { kind, claimSlot: slot }); + } catch (err) { + uploadRefError = err instanceof Error ? err.message : 'Upload fehlgeschlagen'; + } finally { + uploadingRef = false; + } + } {#if !hasPhoto} @@ -67,22 +89,53 @@ Lade erst ein Foto hoch, um dieses Stück an dir zu visualisieren.

{:else if missingFace || missingBody} -
- -
-

Lade erst Referenzbilder hoch, um das Stück an dir zu sehen.

-

- Solo-Try-On braucht ein {accessoryOnly - ? 'Gesichtsbild' - : 'Gesichts- und ein Ganzkörperbild'} - in diesem Space. Öffne dafür - - Meine Bilder - . -

+
+
+ +
+

Für Solo-Try-On brauchen wir dich auf Bild.

+

+ {accessoryOnly + ? 'Ein Gesichtsbild reicht — das Stück wird darauf montiert.' + : 'Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.'} +

+
+ + {#if missingFace} + handleRefUpload(files, 'face', 'face-ref')} + /> + {/if} + {#if missingBody} + handleRefUpload(files, 'fullbody', 'body-ref')} + /> + {/if} + + {#if uploadRefError} + + {/if} + +

+ Weitere Referenzen oder AI-Opt-ins pro Bild: + + Meine Bilder + . +

{:else}
diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte index f96b254b0..54c493017 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte @@ -8,6 +8,8 @@ import { Sparkle, UserCircle, Info } from '@mana/shared-icons'; import { getActiveSpace } from '$lib/data/scope'; import { useImageByPrimary } from '$lib/modules/profile/queries'; + import MeImageUploadZone from '$lib/modules/profile/components/MeImageUploadZone.svelte'; + import { ingestMeImageFile } from '$lib/modules/profile/api/me-images'; import { isAccessoryOnlyOutfit, runOutfitTryOn } from '../api/try-on'; import { CATEGORY_LABELS_SINGULAR } from '../constants'; import type { Garment, Outfit } from '../types'; @@ -35,6 +37,13 @@ let running = $state(false); let error = $state(null); + // Inline ref-upload state. Deliberately local — the missing-ref + // experience lives right here instead of deep-linking to /profile/ + // me-images, because leaving the outfit detail to upload a face photo + // and coming back is jarring (especially inside the workbench card). + let uploadingRef = $state(false); + let uploadRefError = $state(null); + // Rough credit estimate — mirrors the server tariff from the M3 plan // (3 low / 10 medium / 25 high; we default to medium). Shown on the // button so the user knows the hit before clicking. @@ -57,25 +66,76 @@ running = false; } } + + async function handleRefUpload( + files: File[], + kind: 'face' | 'fullbody', + slot: 'face-ref' | 'body-ref' + ) { + if (files.length === 0) return; + uploadingRef = true; + uploadRefError = null; + try { + // Only take the first file — these slots are single-image. + await ingestMeImageFile(files[0], { kind, claimSlot: slot }); + // face$ / body$ live-queries re-run automatically, so the + // missing-block disappears and the button becomes active. + } catch (err) { + uploadRefError = err instanceof Error ? err.message : 'Upload fehlgeschlagen'; + } finally { + uploadingRef = false; + } + } {#if missingFace || missingBody} -
- -
-

Lade erst Referenzbilder hoch, um dich im Outfit zu sehen.

-

- Try-On braucht mindestens ein {accessoryOnly - ? 'Gesichtsbild' - : 'Gesichts- und ein Ganzkörperbild'} - in diesem Space. Öffne dafür - - Meine Bilder - . -

+
+
+ +
+

Für Try-On brauchen wir dich auf Bild.

+

+ {accessoryOnly + ? 'Ein Gesichtsbild reicht — der Rest bleibt wie auf deinem Foto.' + : 'Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.'} +

+
+ + {#if missingFace} + handleRefUpload(files, 'face', 'face-ref')} + /> + {/if} + {#if missingBody} + handleRefUpload(files, 'fullbody', 'body-ref')} + /> + {/if} + + {#if uploadRefError} + + {/if} + +

+ Weitere Referenzen oder AI-Opt-ins pro Bild: + + Meine Bilder + . +

{:else}