Skip to content

Commit 1bc459d

Browse files
spe1020spe1020claude
authored
feat(chef-b): Phase 2 — consolidated prompt, auto-expand textarea, one-tap suggestion chips (#391)
* feat(chef-b): consolidate prompt copy, auto-expand textarea, suggestion chips Phase 2 of the Chef ₿ UX redesign. Prompt copy - Removed the three-line stack ("What's cooking? Tell me what you're craving…" + "Pro Kitchen feature." + the textarea label). Kept only the textarea label "What are you in the mood for?". - Folded the Phase 5 PRO KITCHEN badge in next to the H1 since it was a 1-line change tied to the copy consolidation — small orange-tint pill (bg-primary/10 text-primary border-primary/30) that replaces the standalone sub-line. Textarea auto-expand - rows="2" baseline; `.auto-grow` adds min-height: 6.5rem (~2 rows + scan-pill gutter) and max-height: 14rem (~6 rows) with overflow-y:auto past that. - New `autoSizePrompt()` does `style.height = scrollHeight` after a tick (so chip taps, scan auto-fill, and ingredient add/remove all flow through the same path as user typing). - Reactive `$: promptInput, autoSizePrompt()` is the single source of truth — `on:input` removed, manual calls in handlers not needed. - Scan pill still docked bottom-right inside the textarea. Suggestion chips - Six one-tap prompt seeds beneath the textarea: Cozy vegetarian, 30-min dinner, Use what's in my fridge, High-protein lunch, Kid-friendly, Pantry only. - Tapping a chip replaces the textarea contents, focuses, and parks the cursor at the end so the user can keep typing to refine. - Real `<button>` elements, tab-reachable, Enter/Space activates natively, orange focus-visible ring. - Built as a local `.suggestion-chip` pattern on this page; not extracted to a shared Chip component per the brief — the existing detected-ingredients chips are close but not identical and we'll consolidate later if it earns its keep. - Disabled while a generation is in flight. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chef-b): chips fire generation directly (one-tap presets) Tapping a suggestion chip now fires `/api/zappy` immediately with the chip's label as the prompt — one tap, not two. The textarea is left alone so chips and the custom input stay separate affordances: one-tap presets vs. write-your-own. - `generateRecipe(mode, promptOverride?)` now accepts an optional prompt override so the same code path serves both Cook It (uses promptInput) and chip firing (uses the chip label). Body sent to the API is identical to a Cook It request. - New `fireChip(text)` wraps the call in a try/finally that toggles a `tappedChip` marker — visible to the template so the active chip can render a small spinner inline next to its label while the request is in flight. - All chips + Cook It + Surprise Me are disabled while `status === 'generating'`. The tapped chip keeps a higher opacity than the others so it's clear which one fired. - On error, the existing errorMessage flow surfaces it and the tappedChip marker resets — chip returns to idle. Removed the old `applyChip` (which filled the textarea + focused it). The reactive `$: promptInput, autoSizePrompt()` still handles auto-grow for the textarea — no longer needed for chips since chips don't write to promptInput anymore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(chef-b): expand chip presets + add Nourish program group Regular chips - Dropped "Use what's in my fridge" — the Scan pill in the textarea now covers that flow directly. - Moved "High-protein lunch" out of the regular row into the Nourish group below (as "High protein"). - Added four popular + healthy presets: Mediterranean dinner, One-pot meal, Sheet pan dinner, Hearty salad. Nourish program group - New section below the regular chips with a small leaf-prefix label ("Nourish program") and three chips: High protein, Gut health, Real food. - Each chip carries a small green leaf next to its label so users can see at a glance which prompts align with the Nourish nutrition surface. - Same one-tap fireChip behavior as the regular chips — identical POST to /api/zappy with the chip's label as the prompt. The leaf is a visual program tag, not a separate code path. During firing the leaf is replaced inline by the spinner so the loading state matches the regular chips. The green is the existing Nourish brand color already used in IntelligenceMenu for the same surface — not a new token. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(chef-b): intersperse Nourish chips into the main suggestion row Dropped the dedicated "Nourish program" section and label. The three Nourish-tagged chips (High protein, Gut health, Real food) now sit in the single flex-wrap row alongside the regular presets, each still carrying its green leaf glyph as a visual tag. Order is intentionally interspersed (positions 2, 5, 8 in the 11-chip row) so the row reads as a mixed bag of options rather than a categorized taxonomy. Data model collapsed to a single `Chip = { label, nourish? }` array. Template iterates once; `chip.nourish` toggles the leaf icon. The leaf is replaced inline by the loading spinner while firing, identical to non-Nourish chips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chef-b): never ask clarifying questions on short prompts The OpenAI system instruction left room for Chef ₿ to ask follow- up questions when the user's prompt was a short theme like "Real food" or "Gut health" (one of the new suggestion chips). The chips are designed as one-tap presets — the user expects a recipe to land, not a question they can't reply to (the endpoint is single-turn and there's no chat UI). Added a rule to SYSTEM_INSTRUCTION: always commit to a recipe, treat short theme prompts (with the chip labels listed as examples) as creative direction, make sensible assumptions, and go straight to the recipe. No clarifying questions, no offers to suggest options, no stalling. If we later add a true chat / reply affordance, this guardrail can be loosened. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: spe1020 <sethsager@Seths-MacBook-Air.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6164da1 commit 1bc459d

3 files changed

Lines changed: 182 additions & 19 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zap.cooking",
33
"license": "MIT",
4-
"version": "4.2.423",
4+
"version": "4.2.428",
55
"private": true,
66
"scripts": {
77
"dev": "vite dev",

src/routes/api/zappy/+server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Your tone is warm, encouraging, and practical. Never preachy. Never robotic. You
3434
3535
You generate clear, achievable recipes using common ingredients unless the user specifies otherwise. Recipes are guides, not rules. Encourage substitutions and experimentation when helpful.
3636
37+
Always commit to a recipe. Even when the user's prompt is short, vague, or a theme (e.g. "Real food", "Gut health", "30-min dinner", "Cozy vegetarian", "Kid-friendly", "High protein", "Mediterranean dinner", "Pantry only"), treat it as a creative direction. Make sensible assumptions about cuisine, ingredients, and constraints, then go straight to the recipe. Do not ask the user clarifying questions, do not offer to suggest options, do not stall — pick something good and cook.
38+
3739
Keep things focused and human. No long backstories. No unnecessary fluff.
3840
3941
ALWAYS format your recipes exactly like this:

src/routes/zappy/+page.svelte

Lines changed: 179 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { onMount, onDestroy } from 'svelte';
2+
import { onMount, onDestroy, tick } from 'svelte';
33
import { browser } from '$app/environment';
44
import { goto } from '$app/navigation';
55
import { userPublickey } from '$lib/nostr';
@@ -11,6 +11,7 @@
1111
import Modal from '../../components/Modal.svelte';
1212
import LightningIcon from 'phosphor-svelte/lib/Lightning';
1313
import RobotIcon from 'phosphor-svelte/lib/Robot';
14+
import LeafIcon from 'phosphor-svelte/lib/Leaf';
1415
import CookingPotIcon from 'phosphor-svelte/lib/CookingPot';
1516
import CopyIcon from 'phosphor-svelte/lib/Copy';
1617
import CheckIcon from 'phosphor-svelte/lib/Check';
@@ -38,6 +39,70 @@
3839
3940
// Form state
4041
let promptInput = '';
42+
let promptEl: HTMLTextAreaElement;
43+
44+
// Auto-grow the prompt textarea between min (~2 rows) and max
45+
// (~6 rows). CSS sets the bounds via min-height / max-height +
46+
// overflow-y:auto; this keeps the rendered height in sync with
47+
// content. Wrapped in tick() so we read scrollHeight *after* the
48+
// new value is in the DOM (covers chip taps, scan auto-fill,
49+
// ingredient add/remove — all of which assign promptInput
50+
// programmatically).
51+
async function autoSizePrompt() {
52+
await tick();
53+
if (!promptEl) return;
54+
promptEl.style.height = 'auto';
55+
promptEl.style.height = `${promptEl.scrollHeight}px`;
56+
}
57+
58+
// Run autoSize whenever the prompt value changes from any path —
59+
// user typing (via bind:value), chip apply, scan auto-fill, etc.
60+
$: if (browser) {
61+
promptInput;
62+
autoSizePrompt();
63+
}
64+
65+
// One-tap prompt seeds. Tapping a chip fires a generation
66+
// immediately using the chip's label as the prompt — the textarea
67+
// is left alone so chips and the custom input stay separate
68+
// affordances (one-tap presets vs. write-your-own).
69+
//
70+
// The three Nourish-tagged chips carry a green leaf glyph so users
71+
// can see at a glance which prompts align with the Nourish
72+
// nutrition surface. They share the same fireChip behavior and
73+
// POST to /api/zappy like every other chip; the leaf is a visual
74+
// tag only. Order is intentionally interspersed (not grouped) so
75+
// the row reads as a mixed bag of presets rather than a taxonomy.
76+
type Chip = { label: string; nourish?: boolean };
77+
const suggestionChips: Chip[] = [
78+
{ label: 'Cozy vegetarian' },
79+
{ label: 'High protein', nourish: true },
80+
{ label: '30-min dinner' },
81+
{ label: 'Mediterranean dinner' },
82+
{ label: 'Gut health', nourish: true },
83+
{ label: 'One-pot meal' },
84+
{ label: 'Sheet pan dinner' },
85+
{ label: 'Real food', nourish: true },
86+
{ label: 'Hearty salad' },
87+
{ label: 'Kid-friendly' },
88+
{ label: 'Pantry only' }
89+
];
90+
91+
// Tracks which chip (if any) is currently driving the generation.
92+
// Used to show a per-chip spinner while leaving the rest dimmed.
93+
let tappedChip: string | null = null;
94+
95+
async function fireChip(text: string) {
96+
if (status === 'generating') return;
97+
tappedChip = text;
98+
try {
99+
await generateRecipe('prompt', text);
100+
} finally {
101+
// Reset on both success and error so the chip returns to idle
102+
// even when generateRecipe surfaces an error via errorMessage.
103+
tappedChip = null;
104+
}
105+
}
41106
42107
// Rotating placeholder examples
43108
const placeholderExamples = [
@@ -115,21 +180,27 @@
115180
if (zapSuccessTimeout) clearTimeout(zapSuccessTimeout);
116181
});
117182
118-
// Generate recipe from prompt
119-
async function generateRecipe(mode: 'prompt' | 'hungry' = 'prompt') {
183+
// Generate recipe from prompt. `promptOverride` lets one-tap chips
184+
// supply their label as the prompt without writing into the
185+
// textarea — keeps presets and custom input as separate paths.
186+
async function generateRecipe(
187+
mode: 'prompt' | 'hungry' = 'prompt',
188+
promptOverride?: string
189+
) {
120190
if (status === 'generating') return;
121-
if (mode === 'prompt' && !promptInput.trim()) return;
122-
191+
const effectivePrompt = (promptOverride ?? promptInput).trim();
192+
if (mode === 'prompt' && !effectivePrompt) return;
193+
123194
status = 'generating';
124195
errorMessage = '';
125196
output = '';
126-
197+
127198
try {
128199
const response = await fetch('/api/zappy', {
129200
method: 'POST',
130201
headers: { 'Content-Type': 'application/json' },
131202
body: JSON.stringify({
132-
prompt: promptInput.trim(),
203+
prompt: effectivePrompt,
133204
mode,
134205
pubkey: $userPublickey
135206
})
@@ -383,15 +454,18 @@
383454

384455
<div class="flex flex-col max-w-[760px] mx-auto gap-6 pb-8">
385456
<!-- Header -->
386-
<div class="flex flex-col gap-2">
387-
<div class="flex items-center gap-3">
388-
<RobotIcon size={32} class="text-primary" weight="fill" />
389-
<h1>Chef ₿</h1>
390-
</div>
391-
<p class="text-caption">
392-
What's cooking? Tell me what you're craving or show me your fridge!
393-
</p>
394-
<p class="text-caption text-sm">Pro Kitchen feature.</p>
457+
<div class="flex items-center gap-3 flex-wrap">
458+
<RobotIcon size={32} class="text-primary" weight="fill" />
459+
<h1>Chef ₿</h1>
460+
<!-- Pro Kitchen badge — replaces the standalone
461+
"Pro Kitchen feature." line that used to sit below the
462+
H1. Folded in here since it's the same copy-consolidation
463+
pass (Phase 5 work, cheap to do now). -->
464+
<span
465+
class="inline-flex px-2 py-0.5 rounded-full text-[10px] font-bold tracking-wide border bg-primary/10 text-primary border-primary/30"
466+
>
467+
PRO KITCHEN
468+
</span>
395469
</div>
396470

397471
{#if isLoading}
@@ -427,10 +501,11 @@
427501
<div class="relative">
428502
<textarea
429503
id="prompt"
504+
bind:this={promptEl}
430505
bind:value={promptInput}
431506
placeholder={currentPlaceholder}
432-
rows="5"
433-
class="input resize-none text-base w-full pb-12"
507+
rows="2"
508+
class="input auto-grow resize-none text-base w-full pb-12"
434509
disabled={status === 'generating'}
435510
></textarea>
436511
<!-- Scan Fridge — small labeled pill docked in the textarea's
@@ -456,6 +531,35 @@
456531
{/if}
457532
</button>
458533
</div>
534+
535+
<!-- Suggestion chips — one-tap presets. The chip's label IS
536+
the prompt; tapping fires a generation immediately
537+
(textarea is left alone). Nourish-tagged chips carry
538+
a small green leaf next to the label; they're
539+
interspersed (not grouped) so the row reads as a
540+
mixed bag rather than a taxonomy. Built as a local
541+
pattern; not extracted to a shared Chip component
542+
yet. -->
543+
<div class="flex flex-wrap gap-2" aria-label="Prompt suggestions">
544+
{#each suggestionChips as chip}
545+
{@const isFiring = tappedChip === chip.label}
546+
<button
547+
type="button"
548+
class="suggestion-chip"
549+
class:is-loading={isFiring}
550+
on:click={() => fireChip(chip.label)}
551+
disabled={status === 'generating'}
552+
aria-busy={isFiring}
553+
>
554+
{#if isFiring}
555+
<ArrowsClockwiseIcon size={12} class="animate-spin" />
556+
{:else if chip.nourish}
557+
<LeafIcon size={12} weight="fill" class="text-green-500" />
558+
{/if}
559+
{chip.label}
560+
</button>
561+
{/each}
562+
</div>
459563
</div>
460564

461565
<!-- Hidden file input for camera/upload -->
@@ -805,6 +909,63 @@
805909
{/if}
806910

807911
<style>
912+
/* Prompt textarea — starts compact (~2 rows of usable text plus the
913+
scan-pill gutter at the bottom) and grows up to ~6 rows. The JS
914+
`autoSizePrompt` keeps `style.height` in sync with scrollHeight;
915+
max-height + overflow-y here cap the growth and switch to scroll. */
916+
.input.auto-grow {
917+
min-height: 6.5rem;
918+
max-height: 14rem;
919+
overflow-y: auto;
920+
}
921+
922+
/* Suggestion chip — orange-tint button at secondary visual weight,
923+
clearly tappable, focus-visible ring for keyboard users. Pattern
924+
kept local on this page; we'll extract a shared Chip component
925+
later if the detected-ingredients chips end up close enough to
926+
consolidate. */
927+
.suggestion-chip {
928+
display: inline-flex;
929+
align-items: center;
930+
gap: 6px;
931+
height: 30px;
932+
padding: 0 12px;
933+
border-radius: 999px;
934+
border: 0;
935+
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
936+
color: var(--color-primary);
937+
font-size: 13px;
938+
font-weight: 500;
939+
line-height: 1;
940+
cursor: pointer;
941+
transition:
942+
background-color 140ms ease,
943+
transform 140ms ease,
944+
box-shadow 140ms ease,
945+
opacity 140ms ease;
946+
}
947+
.suggestion-chip:hover:not(:disabled) {
948+
background-color: color-mix(in srgb, var(--color-primary) 18%, transparent);
949+
}
950+
.suggestion-chip:active:not(:disabled) {
951+
transform: scale(0.96);
952+
}
953+
.suggestion-chip:focus-visible {
954+
outline: none;
955+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 55%, transparent);
956+
}
957+
.suggestion-chip:disabled {
958+
opacity: 0.5;
959+
cursor: not-allowed;
960+
}
961+
/* The chip currently driving a generation keeps its readability
962+
above the other disabled chips so the user can see WHICH one
963+
fired. Pairs with the inline spinner in the template. */
964+
.suggestion-chip.is-loading:disabled {
965+
opacity: 0.9;
966+
background-color: color-mix(in srgb, var(--color-primary) 16%, transparent);
967+
}
968+
808969
/* Scan Fridge — labeled pill docked in the textarea's bottom-right
809970
corner (Slack/ChatGPT/iMessage convention). Always-visible "Scan"
810971
text means no hover tooltip needed on mobile. Uses the Chef ₿

0 commit comments

Comments
 (0)