@@ -320,15 +320,56 @@ export default function PromptBar({ panelId, onSend, onGeneratingChange, cwd, on
320320 ) }
321321 </ AnimatePresence >
322322
323- < div className = "flex items-center gap-3" >
324- { /* Attach button */ }
325- < button
326- onClick = { ( ) => fileInputRef . current ?. click ( ) }
327- className = "px-2 py-2.5 text-[var(--text-secondary)] hover:text-[var(--accent-purple)] transition-colors cursor-pointer text-lg shrink-0"
328- title = "Attach image"
329- >
330- π
331- </ button >
323+ < div className = "relative" >
324+ { /* Waveform overlay when listening */ }
325+ < AnimatePresence >
326+ { voice . isListening && (
327+ < motion . div
328+ className = "absolute inset-0 z-10 flex flex-col items-center justify-center rounded-xl border border-[var(--accent-purple)] bg-[var(--bg-secondary)]"
329+ initial = { { opacity : 0 } }
330+ animate = { { opacity : 1 } }
331+ exit = { { opacity : 0 } }
332+ transition = { { duration : 0.2 } }
333+ >
334+ { /* Waveform bars */ }
335+ < div className = "flex items-center justify-center gap-[2px] h-8 px-4 w-full" >
336+ { voice . audioLevels . map ( ( level , i ) => (
337+ < div
338+ key = { i }
339+ className = "w-[3px] rounded-full bg-[var(--accent-purple)]"
340+ style = { {
341+ height : `${ Math . max ( 3 , level * 28 ) } px` ,
342+ opacity : 0.4 + level * 0.6 ,
343+ transition : 'height 80ms ease-out, opacity 80ms ease-out' ,
344+ } }
345+ />
346+ ) ) }
347+ </ div >
348+ { /* Streaming transcript */ }
349+ { ( voice . finalTranscript || voice . interimTranscript ) && (
350+ < p className = "text-xs text-[var(--text-secondary)] mt-1 px-4 truncate max-w-full" >
351+ < span className = "text-[var(--text-primary)]" > { voice . finalTranscript } </ span >
352+ { voice . interimTranscript && (
353+ < span className = "text-[var(--text-secondary)] italic" > { voice . interimTranscript } </ span >
354+ ) }
355+ </ p >
356+ ) }
357+ </ motion . div >
358+ ) }
359+ </ AnimatePresence >
360+
361+ < textarea
362+ ref = { textareaRef }
363+ value = { prompt }
364+ onChange = { ( e ) => setPrompt ( e . target . value ) }
365+ onKeyDown = { handleKeyDown }
366+ onPaste = { handlePaste }
367+ placeholder = { attachments . length > 0 ? 'Add a message or press Enter to sendβ¦' : isGenerating ? 'Type to steer or queue next messageβ¦' : 'Enter your promptβ¦' }
368+ rows = { 1 }
369+ className = "w-full bg-transparent outline-none text-[var(--text-primary)] placeholder-[var(--text-secondary)] resize-none rounded-xl border border-[var(--border-color)] pl-11 pr-20 py-3 focus:border-[var(--accent-purple)] focus:shadow-[0_0_8px_var(--accent-purple)] transition-shadow"
370+ style = { { lineHeight : `${ LINE_HEIGHT } px` } }
371+ />
372+
332373 < input
333374 ref = { fileInputRef }
334375 type = "file"
@@ -341,74 +382,74 @@ export default function PromptBar({ panelId, onSend, onGeneratingChange, cwd, on
341382 } }
342383 />
343384
344- < div className = "relative flex-1" >
345- { /* Waveform overlay when listening */ }
346- < AnimatePresence >
347- { voice . isListening && (
348- < motion . div
349- className = "absolute inset-0 z-10 flex flex-col items-center justify-center rounded-lg border border-[var(--accent-purple)] bg-[var(--bg-secondary)]"
350- initial = { { opacity : 0 } }
351- animate = { { opacity : 1 } }
352- exit = { { opacity : 0 } }
353- transition = { { duration : 0.2 } }
354- >
355- { /* Waveform bars */ }
356- < div className = "flex items-center justify-center gap-[2px] h-8 px-4 w-full" >
357- { voice . audioLevels . map ( ( level , i ) => (
358- < div
359- key = { i }
360- className = "w-[3px] rounded-full bg-[var(--accent-purple)]"
361- style = { {
362- height : `${ Math . max ( 3 , level * 28 ) } px` ,
363- opacity : 0.4 + level * 0.6 ,
364- transition : 'height 80ms ease-out, opacity 80ms ease-out' ,
365- } }
366- />
367- ) ) }
385+ { /* Attach button β bottom left inside textarea */ }
386+ < button
387+ onClick = { ( ) => fileInputRef . current ?. click ( ) }
388+ className = "absolute left-3 bottom-2.5 z-20 flex items-center justify-center w-7 h-7 rounded-full text-[var(--text-secondary)] hover:text-[var(--accent-purple)] transition-colors cursor-pointer"
389+ title = "Attach image"
390+ >
391+ π
392+ </ button >
393+
394+ { /* Right-side buttons β bottom right inside textarea */ }
395+ < div className = "absolute right-2.5 bottom-2 z-20 flex items-center gap-1" >
396+ { /* Session menu (β―) */ }
397+ < div className = "relative" >
398+ < button
399+ onClick = { ( ) => setSendMenuOpen ( ! sendMenuOpen ) }
400+ className = "flex items-center justify-center w-7 h-7 rounded-full text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] transition-colors cursor-pointer opacity-60 hover:opacity-100"
401+ title = "Session actions"
402+ >
403+ < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "currentColor" > < circle cx = "5" cy = "12" r = "2" /> < circle cx = "12" cy = "12" r = "2" /> < circle cx = "19" cy = "12" r = "2" /> </ svg >
404+ </ button >
405+ { sendMenuOpen && (
406+ < >
407+ < div className = "fixed inset-0 z-40" onClick = { ( ) => setSendMenuOpen ( false ) } />
408+ < div className = "absolute bottom-full right-0 mb-2 z-50 w-44 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] shadow-xl overflow-hidden" >
409+ < button
410+ onClick = { ( ) => { setSendMenuOpen ( false ) ; onNewSession ?.( ) ; } }
411+ className = "w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
412+ >
413+ < span className = "text-[var(--text-secondary)]" > +</ span > New Session
414+ </ button >
415+ < button
416+ onClick = { ( ) => { setSendMenuOpen ( false ) ; onLoadSession ?.( ) ; } }
417+ className = "w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
418+ >
419+ < svg width = "12" height = "12" viewBox = "0 0 16 16" fill = "currentColor" className = "text-[var(--text-secondary)]" > < path d = "M1.5 1.5v13h13v-13h-13zM0 1a1 1 0 011-1h14a1 1 0 011 1v14a1 1 0 01-1 1H1a1 1 0 01-1-1V1zm4.5 2.5h7v1h-7v-1zm0 3h7v1h-7v-1zm0 3h4v1h-4v-1z" /> </ svg >
420+ Load Session
421+ </ button >
422+ < button
423+ onClick = { ( ) => { setSendMenuOpen ( false ) ; onSplitSession ?.( ) ; } }
424+ className = "w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
425+ >
426+ < svg width = "12" height = "12" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.5" strokeLinecap = "round" className = "text-[var(--text-secondary)]" >
427+ < rect x = "1" y = "2" width = "14" height = "12" rx = "1" />
428+ < line x1 = "8" y1 = "2" x2 = "8" y2 = "14" />
429+ </ svg >
430+ Split Session
431+ </ button >
368432 </ div >
369- { /* Streaming transcript */ }
370- { ( voice . finalTranscript || voice . interimTranscript ) && (
371- < p className = "text-xs text-[var(--text-secondary)] mt-1 px-4 truncate max-w-full" >
372- < span className = "text-[var(--text-primary)]" > { voice . finalTranscript } </ span >
373- { voice . interimTranscript && (
374- < span className = "text-[var(--text-secondary)] italic" > { voice . interimTranscript } </ span >
375- ) }
376- </ p >
377- ) }
378- </ motion . div >
433+ </ >
379434 ) }
380- </ AnimatePresence >
381-
382- < textarea
383- ref = { textareaRef }
384- value = { prompt }
385- onChange = { ( e ) => setPrompt ( e . target . value ) }
386- onKeyDown = { handleKeyDown }
387- onPaste = { handlePaste }
388- placeholder = { attachments . length > 0 ? 'Add a message or press Enter to sendβ¦' : isGenerating ? 'Type to steer or queue next messageβ¦' : 'Enter your promptβ¦' }
389- rows = { 1 }
390- className = "w-full bg-transparent outline-none text-[var(--text-primary)] placeholder-[var(--text-secondary)] resize-none rounded-lg border border-[var(--border-color)] px-4 py-3 pr-10 focus:border-[var(--accent-purple)] focus:shadow-[0_0_8px_var(--accent-purple)] transition-shadow"
391- style = { { lineHeight : `${ LINE_HEIGHT } px` } }
392- />
435+ </ div >
393436
394- { /* Mic / Stop button overlay */ }
437+ { /* Mic button */ }
395438 { voice . isSupported && (
396439 < button
397440 onClick = { ( ) => voice . isListening ? voice . stop ( ) : voice . start ( ) }
398- className = { `absolute right-2.5 bottom-2.5 z-20 flex items-center justify-center w-7 h-7 rounded-full transition-all cursor-pointer ${
441+ className = { `flex items-center justify-center w-7 h-7 rounded-full transition-all cursor-pointer ${
399442 voice . isListening
400443 ? 'bg-red-500/15 text-red-400 hover:bg-red-500/25'
401444 : 'text-[var(--text-secondary)] hover:text-[var(--accent-purple)] opacity-50 hover:opacity-100'
402445 } `}
403446 title = { voice . isListening ? 'Stop recording' : 'Voice input' }
404447 >
405448 { voice . isListening ? (
406- /* Stop icon */
407449 < svg width = "12" height = "12" viewBox = "0 0 12 12" fill = "currentColor" >
408450 < rect x = "1" y = "1" width = "10" height = "10" rx = "1.5" />
409451 </ svg >
410452 ) : (
411- /* Microphone icon */
412453 < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
413454 < rect x = "9" y = "1" width = "6" height = "12" rx = "3" />
414455 < path d = "M5 10a7 7 0 0 0 14 0" />
@@ -417,77 +458,40 @@ export default function PromptBar({ panelId, onSend, onGeneratingChange, cwd, on
417458 ) }
418459 </ button >
419460 ) }
420- </ div >
421461
422- < AnimatePresence mode = "wait" >
423- { isGenerating ? (
424- < motion . button
425- key = "stop"
426- initial = { { opacity : 0 , scale : 0.8 } }
427- animate = { { opacity : 1 , scale : 1 } }
428- exit = { { opacity : 0 , scale : 0.8 } }
429- transition = { { duration : 0.15 } }
430- onClick = { handleAbort }
431- className = "px-4 py-3 bg-red-600 text-white font-bold rounded-lg text-sm whitespace-nowrap cursor-pointer flex items-center shrink-0"
432- >
433- βΉ STOP
434- </ motion . button >
435- ) : (
436- < motion . div
437- key = "send-group"
438- initial = { { opacity : 0 , scale : 0.8 } }
439- animate = { { opacity : 1 , scale : 1 } }
440- exit = { { opacity : 0 , scale : 0.8 } }
441- transition = { { duration : 0.15 } }
442- className = "relative flex items-center shrink-0"
443- >
444- < button
445- onClick = { handleSend }
446- className = "px-3 py-3 bg-[var(--accent-gold)] text-black font-bold rounded-l-lg cursor-pointer flex items-center justify-center"
462+ { /* Send / Stop button */ }
463+ < AnimatePresence mode = "wait" >
464+ { isGenerating ? (
465+ < motion . button
466+ key = "stop"
467+ initial = { { opacity : 0 , scale : 0.8 } }
468+ animate = { { opacity : 1 , scale : 1 } }
469+ exit = { { opacity : 0 , scale : 0.8 } }
470+ transition = { { duration : 0.15 } }
471+ onClick = { handleAbort }
472+ className = "flex items-center justify-center w-8 h-8 rounded-lg bg-red-600 text-white cursor-pointer"
473+ title = "Stop generating"
447474 >
448- < svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" strokeLinecap = "round" strokeLinejoin = "round" > < line x1 = "22" y1 = "2" x2 = "11" y2 = "13" /> < polygon points = "22 2 15 22 11 13 2 9 22 2" /> </ svg >
449- </ button >
450- < div className = "w-px bg-black/20 self-stretch" />
451- < button
452- onClick = { ( ) => setSendMenuOpen ( ! sendMenuOpen ) }
453- className = "px-1.5 py-3 bg-[var(--accent-gold)] text-black font-bold rounded-r-lg cursor-pointer flex items-center justify-center hover:bg-[var(--accent-gold)]/80 transition-colors"
475+ < svg width = "12" height = "12" viewBox = "0 0 12 12" fill = "currentColor" >
476+ < rect x = "1" y = "1" width = "10" height = "10" rx = "1.5" />
477+ </ svg >
478+ </ motion . button >
479+ ) : (
480+ < motion . button
481+ key = "send"
482+ initial = { { opacity : 0 , scale : 0.8 } }
483+ animate = { { opacity : 1 , scale : 1 } }
484+ exit = { { opacity : 0 , scale : 0.8 } }
485+ transition = { { duration : 0.15 } }
486+ onClick = { handleSend }
487+ className = "flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--accent-gold)] text-black cursor-pointer"
488+ title = "Send (Enter)"
454489 >
455- < svg width = "10" height = "10" viewBox = "0 0 8 8" fill = "currentColor" className = { `transition-transform ${ sendMenuOpen ? 'rotate-180' : '' } ` } > < path d = "M0 2l4 4 4-4z" /> </ svg >
456- </ button >
457-
458- { sendMenuOpen && (
459- < >
460- < div className = "fixed inset-0 z-40" onClick = { ( ) => setSendMenuOpen ( false ) } />
461- < div className = "absolute bottom-full right-0 mb-2 z-50 w-44 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] shadow-xl overflow-hidden" >
462- < button
463- onClick = { ( ) => { setSendMenuOpen ( false ) ; onNewSession ?.( ) ; } }
464- className = "w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
465- >
466- < span className = "text-[var(--text-secondary)]" > +</ span > New Session
467- </ button >
468- < button
469- onClick = { ( ) => { setSendMenuOpen ( false ) ; onLoadSession ?.( ) ; } }
470- className = "w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
471- >
472- < svg width = "12" height = "12" viewBox = "0 0 16 16" fill = "currentColor" className = "text-[var(--text-secondary)]" > < path d = "M1.5 1.5v13h13v-13h-13zM0 1a1 1 0 011-1h14a1 1 0 011 1v14a1 1 0 01-1 1H1a1 1 0 01-1-1V1zm4.5 2.5h7v1h-7v-1zm0 3h7v1h-7v-1zm0 3h4v1h-4v-1z" /> </ svg >
473- Load Session
474- </ button >
475- < button
476- onClick = { ( ) => { setSendMenuOpen ( false ) ; onSplitSession ?.( ) ; } }
477- className = "w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
478- >
479- < svg width = "12" height = "12" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.5" strokeLinecap = "round" className = "text-[var(--text-secondary)]" >
480- < rect x = "1" y = "2" width = "14" height = "12" rx = "1" />
481- < line x1 = "8" y1 = "2" x2 = "8" y2 = "14" />
482- </ svg >
483- Split Session
484- </ button >
485- </ div >
486- </ >
487- ) }
488- </ motion . div >
489- ) }
490- </ AnimatePresence >
490+ < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" strokeLinecap = "round" strokeLinejoin = "round" > < line x1 = "22" y1 = "2" x2 = "11" y2 = "13" /> < polygon points = "22 2 15 22 11 13 2 9 22 2" /> </ svg >
491+ </ motion . button >
492+ ) }
493+ </ AnimatePresence >
494+ </ div >
491495 </ div >
492496 </ div >
493497 ) ;
0 commit comments