Skip to content

Commit 765c54d

Browse files
feat(genius): Enter to generate, auto-dismiss banner, centered spinner
Switch keyboard shortcut to Enter for generation, Shift+Enter for new line. Clear prompt text immediately on generate and show a single large centered spinner instead of two small ones. Auto-dismiss success banner after 5 seconds while keeping history entries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4eccc71 commit 765c54d

File tree

4 files changed

+100
-32
lines changed

4 files changed

+100
-32
lines changed

app/frontend/__tests__/genius-browser.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,67 @@ describe('Genius Browser', () => {
162162
expect(instance.history).toHaveLength(1);
163163
expect(instance.prompt).toBe('');
164164
});
165+
166+
it('clears prompt immediately when generation starts', async () => {
167+
const instance = createInstance();
168+
instance.onboardingComplete = true;
169+
instance.prompt = 'test prompt';
170+
171+
// Use a deferred promise so we can check state mid-generation
172+
let resolve;
173+
mockAgent.generatePlaylist.mockReturnValue(
174+
new Promise((r) => {
175+
resolve = r;
176+
}),
177+
);
178+
179+
const genPromise = instance.generate();
180+
181+
// Prompt should be cleared while still generating
182+
expect(instance.prompt).toBe('');
183+
expect(instance.generating).toBe(true);
184+
185+
resolve({ status: 'success', playlist_name: 'Test', playlist_id: 1, track_count: 5 });
186+
globalThis.window = { dispatchEvent: vi.fn() };
187+
globalThis.CustomEvent = class CustomEvent {
188+
constructor(type, opts) {
189+
this.type = type;
190+
this.detail = opts?.detail;
191+
}
192+
};
193+
await genPromise;
194+
});
195+
196+
it('auto-dismisses success result after 5 seconds', async () => {
197+
const instance = createInstance();
198+
instance.onboardingComplete = true;
199+
instance.prompt = 'test prompt';
200+
mockAgent.generatePlaylist.mockResolvedValue({
201+
status: 'success',
202+
playlist_name: 'Test',
203+
playlist_id: 1,
204+
track_count: 5,
205+
});
206+
207+
globalThis.window = { dispatchEvent: vi.fn() };
208+
globalThis.CustomEvent = class CustomEvent {
209+
constructor(type, opts) {
210+
this.type = type;
211+
this.detail = opts?.detail;
212+
}
213+
};
214+
215+
await instance.generate();
216+
217+
expect(instance.result).not.toBeNull();
218+
expect(instance.result.status).toBe('success');
219+
220+
// After 5 seconds, result should auto-dismiss
221+
vi.advanceTimersByTime(5000);
222+
expect(instance.result).toBeNull();
223+
// But history entry should remain
224+
expect(instance.history).toHaveLength(1);
225+
});
165226
});
166227

167228
describe('onboarding', () => {

app/frontend/js/components/genius-browser.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function createGeniusBrowser(Alpine) {
2525
prompt: '',
2626
generating: false,
2727
result: null, // { status, playlist_id, playlist_name, track_count, message }
28+
_resultTimeout: null,
2829
history: [], // past generations for this session
2930

3031
// Animated prompt cycling
@@ -87,6 +88,10 @@ export function createGeniusBrowser(Alpine) {
8788
this._unlisten();
8889
this._unlisten = null;
8990
}
91+
if (this._resultTimeout) {
92+
clearTimeout(this._resultTimeout);
93+
this._resultTimeout = null;
94+
}
9095
this._stopPromptCycle();
9196
},
9297

@@ -241,6 +246,11 @@ export function createGeniusBrowser(Alpine) {
241246

242247
this.generating = true;
243248
this.result = null;
249+
if (this._resultTimeout) {
250+
clearTimeout(this._resultTimeout);
251+
this._resultTimeout = null;
252+
}
253+
this.prompt = '';
244254

245255
try {
246256
const response = await agent.generatePlaylist(trimmed);
@@ -253,13 +263,19 @@ export function createGeniusBrowser(Alpine) {
253263
playlist_id: response.playlist_id,
254264
track_count: response.track_count,
255265
});
256-
this.prompt = '';
257266
// Notify sidebar to refresh playlists
258267
window.dispatchEvent(new CustomEvent('mt:playlists-updated'));
259268
this.ui.toast(
260269
`Created "${response.playlist_name}" with ${response.track_count} tracks`,
261270
'success',
262271
);
272+
// Auto-dismiss success banner after 5 seconds
273+
this._resultTimeout = setTimeout(() => {
274+
if (this.result?.status === 'success') {
275+
this.result = null;
276+
}
277+
this._resultTimeout = null;
278+
}, 5000);
263279
} else if (response.status === 'no_ollama') {
264280
this.onboardingComplete = false;
265281
this.onboardingStep = 'install-ollama';

app/frontend/views/genius.html

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,17 @@ <h3 class="text-sm font-semibold mb-1">Download AI Model</h3>
198198
</div>
199199
</template>
200200

201-
<!-- Generating indicator -->
202-
<template x-if="generating">
203-
<div class="px-4 pt-4 pb-3">
204-
<div class="flex items-center gap-2 text-xs text-foreground/50">
205-
<div
206-
class="w-3.5 h-3.5 border-2 border-primary/50 border-t-transparent rounded-full animate-spin"
207-
>
208-
</div>
209-
<span>Searching your library and generating playlist...</span>
210-
</div>
201+
<!-- Generating spinner -->
202+
<div
203+
x-show="generating"
204+
class="flex flex-col items-center justify-center h-full gap-4"
205+
>
206+
<div
207+
class="w-14 h-14 border-[3px] border-foreground border-t-transparent rounded-full animate-spin"
208+
>
211209
</div>
212-
</template>
210+
<p class="text-sm text-foreground/40">Generating playlist...</p>
211+
</div>
213212

214213
<!-- History -->
215214
<div class="px-4 pb-4" x-show="history.length > 0">
@@ -248,7 +247,7 @@ <h3 class="text-sm font-semibold mb-1">Download AI Model</h3>
248247
<div class="relative">
249248
<textarea
250249
x-model="prompt"
251-
@keydown.shift.enter.prevent="generate()"
250+
@keydown.enter.prevent="if (!$event.shiftKey) generate(); else { $event.target.setRangeText('\n', $event.target.selectionStart, $event.target.selectionEnd, 'end') }"
252251
placeholder="Describe a playlist..."
253252
class="w-full text-sm bg-muted/50 border border-border rounded-lg px-3 py-2.5 pr-14 resize-none focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary/50 placeholder:text-foreground/30"
254253
rows="2"
@@ -260,30 +259,22 @@ <h3 class="text-sm font-semibold mb-1">Download AI Model</h3>
260259
:disabled="!prompt.trim() || generating"
261260
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-lg transition-colors"
262261
:class="prompt.trim() && !generating ? 'text-primary hover:bg-primary/10' : 'text-foreground/20 cursor-not-allowed'"
263-
title="Generate (Shift+Enter)"
262+
title="Generate (Enter)"
264263
data-testid="genius-generate"
265264
>
266-
<template x-if="!generating">
267-
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
268-
<path
269-
stroke-linecap="round"
270-
stroke-linejoin="round"
271-
stroke-width="2"
272-
d="M13 7l5 5m0 0l-5 5m5-5H6"
273-
>
274-
</path>
275-
</svg>
276-
</template>
277-
<template x-if="generating">
278-
<div
279-
class="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin"
265+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
266+
<path
267+
stroke-linecap="round"
268+
stroke-linejoin="round"
269+
stroke-width="2"
270+
d="M13 7l5 5m0 0l-5 5m5-5H6"
280271
>
281-
</div>
282-
</template>
272+
</path>
273+
</svg>
283274
</button>
284275
</div>
285276
<p class="text-[10px] text-foreground/30 mt-1.5 px-1">
286-
Press Shift+Enter to generate, Enter for new line
277+
Press Enter to generate, Shift+Enter for new line
287278
</p>
288279
</div>
289280
</div>

docs/genius.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,6 @@ The Genius view uses a chat-first layout:
8383
- **Background**: Large transparent glasses graphic centered and rotated ~45 degrees, low opacity so it doesn't obscure controls
8484
- **Empty state**: Animated prompt examples cycle with slow fade transitions ("make me a chill playlist from my library", etc.)
8585
- **Composer**: Positioned at the bottom of the view with a text area and generate button
86-
- **Keyboard shortcut**: Shift+Enter triggers generation, Enter inserts a new line
86+
- **Keyboard shortcut**: Enter triggers generation, Shift+Enter inserts a new line
8787
- **History**: Recent generations listed above the composer when present
8888
- **Onboarding**: Three-step wizard (check Ollama -> download model -> ready) shown before the main interface

0 commit comments

Comments
 (0)