feat: implement AI bouquet generation flow with Gemini/OpenAI

* feat: scaffold message, generating, and map pages and align header steps

* feat: implement AI bouquet generation flow with Gemini/OpenAI

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chaewon Lee
2026-06-09 17:07:38 +09:00
committed by GitHub
parent d0ba482451
commit d8f93f4c17
33 changed files with 2008 additions and 54 deletions

View File

@@ -1,7 +1,7 @@
<script>
let { children } = $props();
let { children, onclick = undefined } = $props();
</script>
<button class="bg-black px-6 py-3 text-white">
<button class="bg-black px-6 py-3 text-white" {onclick}>
{@render children()}
</button>

View File

@@ -1,6 +1,6 @@
<script>
// `step` is 1-based; the matching dot is highlighted as the current step.
let { step = 1, total = 6 } = $props();
let { step = 1, total = 7 } = $props();
const dots = $derived(Array.from({ length: total }, (_, i) => i));
</script>

View File

@@ -1,23 +1,70 @@
<script>
import UploadTile from './UploadTile.svelte';
// One reference image per category, laid out as an interlocking collage
// (offset seams, varied heights) instead of equal quarters.
let { primaryFile = $bindable(null) } = $props();
let colorFile = $state(null);
let seasonFile = $state(null);
let characterFile = $state(null);
let locationFile = $state(null);
$effect(() => {
primaryFile = colorFile ?? seasonFile ?? characterFile ?? locationFile ?? null;
});
const tiles = [
{ key: 'color', label: 'Color', aspect: 'aspect-4/5' },
{ key: 'season', label: 'Season', aspect: 'aspect-4/3' },
{ key: 'character', label: 'Character', aspect: 'aspect-4/3' },
{ key: 'location', label: 'Location', aspect: 'aspect-4/5' }
{
key: 'color',
label: 'Color',
aspect: 'aspect-4/5',
bindFile: () => colorFile,
setFile: (v) => (colorFile = v)
},
{
key: 'season',
label: 'Season',
aspect: 'aspect-4/3',
bindFile: () => seasonFile,
setFile: (v) => (seasonFile = v)
},
{
key: 'character',
label: 'Character',
aspect: 'aspect-4/3',
bindFile: () => characterFile,
setFile: (v) => (characterFile = v)
},
{
key: 'location',
label: 'Location',
aspect: 'aspect-4/5',
bindFile: () => locationFile,
setFile: (v) => (locationFile = v)
}
];
</script>
<div class="moodboard w-full min-h-0 flex-1">
{#each tiles as tile (tile.key)}
<UploadTile
label={tile.label}
class="tile tile-{tile.key} h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto {tile.aspect}"
/>
{/each}
<div class="moodboard min-h-0 w-full flex-1">
<UploadTile
label="Color"
bind:file={colorFile}
class="tile tile-color aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
label="Season"
bind:file={seasonFile}
class="tile tile-season aspect-4/3 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
label="Character"
bind:file={characterFile}
class="tile tile-character aspect-4/3 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
label="Location"
bind:file={locationFile}
class="tile tile-location aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
</div>
<style>

View File

@@ -1,14 +1,25 @@
<script>
import UploadTile from './UploadTile.svelte';
// Two SNS feed screenshots. On desktop they fill the panel edge-to-edge in
// a staggered composition (one raised on the right, one dropped on the
// left); below that they fall back to a simple side-by-side / stacked grid.
let { primaryFile = $bindable(null) } = $props();
let firstFile = $state(null);
let secondFile = $state(null);
$effect(() => {
primaryFile = firstFile ?? secondFile ?? null;
});
</script>
<div class="feed w-full min-h-0 flex-1">
<UploadTile class="tile-one h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto aspect-4/5" />
<UploadTile class="tile-two h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto aspect-4/5" />
<div class="feed min-h-0 w-full flex-1">
<UploadTile
bind:file={firstFile}
class="tile-one aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
<UploadTile
bind:file={secondFile}
class="tile-two aspect-4/5 h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto"
/>
</div>
<style>

View File

@@ -5,15 +5,16 @@
// the chosen image (cover) when filled. Layout (size / grid placement) is
// supplied by the parent via `class` and `style` so the same tile works in
// both the moodboard and the SNS feed.
let { label = null, class: klass = '', style = '' } = $props();
let { label = null, class: klass = '', style = '', file = $bindable(null) } = $props();
let preview = $state(null);
function pick(event) {
const file = event.currentTarget.files?.[0];
if (!file) return;
const picked = event.currentTarget.files?.[0];
if (!picked) return;
if (preview) URL.revokeObjectURL(preview);
preview = URL.createObjectURL(file);
file = picked;
preview = URL.createObjectURL(picked);
}
onDestroy(() => {