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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user