Spaces:
Running
Running
| import { Component, For, Show, createEffect, createSignal, onCleanup } from 'solid-js'; | |
| interface SidebarProps { | |
| activeTab: string; | |
| onTabChange: (tab: string) => void; | |
| // Recording controls | |
| isRecording: boolean; | |
| onToggleRecording: () => void; | |
| // Model state | |
| isModelReady: boolean; | |
| onLoadModel: () => void; | |
| modelState: string; | |
| // Device selection | |
| availableDevices: MediaDeviceInfo[]; | |
| selectedDeviceId: string; | |
| onDeviceSelect: (id: string) => void; | |
| // Audio feedback | |
| audioLevel: number; | |
| } | |
| export const Sidebar: Component<SidebarProps> = (props) => { | |
| const [showDevices, setShowDevices] = createSignal(false); | |
| let triggerContainerRef: HTMLDivElement | undefined; | |
| let popoverRef: HTMLDivElement | undefined; | |
| createEffect(() => { | |
| if (!showDevices()) return; | |
| const onMouseDown = (e: MouseEvent) => { | |
| const target = e.target as Node; | |
| if (triggerContainerRef?.contains(target) || popoverRef?.contains(target)) return; | |
| setShowDevices(false); | |
| }; | |
| const onKeyDown = (e: KeyboardEvent) => { | |
| if (e.key === 'Escape') setShowDevices(false); | |
| }; | |
| document.addEventListener('mousedown', onMouseDown); | |
| document.addEventListener('keydown', onKeyDown); | |
| onCleanup(() => { | |
| document.removeEventListener('mousedown', onMouseDown); | |
| document.removeEventListener('keydown', onKeyDown); | |
| }); | |
| }); | |
| return ( | |
| <aside class="w-20 min-w-[80px] bg-neu-bg flex flex-col items-center py-6 h-full border-r border-sidebar-border/30"> | |
| {/* Power Button - Reflects System Readiness; disabled when model already loaded or loading */} | |
| <div class="mb-8 relative"> | |
| <button | |
| onClick={() => !props.isModelReady && props.modelState !== 'loading' && props.onLoadModel()} | |
| disabled={props.isModelReady || props.modelState === 'loading'} | |
| class="neu-circle-btn text-slate-600 transition-all active:scale-95 disabled:opacity-70 disabled:cursor-not-allowed disabled:active:scale-100" | |
| title={props.modelState === 'loading' ? "Loading..." : props.isModelReady ? "Model Loaded" : "Load Model"} | |
| > | |
| <span class="material-symbols-outlined text-xl">power_settings_new</span> | |
| <span class={`status-led ${props.isModelReady ? 'bg-green-500 shadow-[0_0_8px_#22c55e]' : 'bg-slate-300'}`}></span> | |
| </button> | |
| </div> | |
| <nav class="flex flex-col gap-6 items-center w-full px-2"> | |
| {/* Record Button - Always enabled, recording works even before model is loaded */} | |
| <button | |
| onClick={() => props.onToggleRecording()} | |
| class={`neu-circle-btn transition-all active:scale-95 ${props.isRecording ? 'text-red-500 active' : 'text-slate-500'}`} | |
| title={props.isRecording ? "Stop Recording" : "Start Recording"} | |
| > | |
| <span class="material-symbols-outlined text-xl">mic</span> | |
| </button> | |
| <div class="w-8 h-[1px] bg-slate-300/60 my-2"></div> | |
| {/* Model Selection Icon */} | |
| <button | |
| onClick={() => props.onLoadModel()} | |
| class={`neu-square-btn transition-all active:scale-95 ${props.activeTab === 'ai' ? 'active' : 'text-slate-500'}`} | |
| title="AI Model Selection" | |
| > | |
| <span class="material-symbols-outlined text-xl">psychology</span> | |
| </button> | |
| {/* Device Selection Popover Trigger */} | |
| <div class="relative" ref={(el) => { triggerContainerRef = el; }}> | |
| <button | |
| class={`neu-square-btn transition-all active:scale-95 ${showDevices() ? 'active' : 'text-slate-500'}`} | |
| onClick={() => setShowDevices(!showDevices())} | |
| title="Audio Input Selection" | |
| > | |
| <span class="material-symbols-outlined text-xl">settings_input_composite</span> | |
| </button> | |
| {/* Device Selection Popover */} | |
| <Show when={showDevices()}> | |
| <div ref={(el) => { popoverRef = el; }} class="absolute left-full bottom-0 ml-6 w-64 nm-flat rounded-[32px] p-4 z-50 animate-in fade-in slide-in-from-left-2 duration-200"> | |
| <div class="text-[9px] font-black text-slate-400 p-2 uppercase tracking-widest mb-2 border-b border-slate-200">Mechanical_Input</div> | |
| <div class="flex flex-col gap-1 max-h-64 overflow-y-auto pr-1"> | |
| <For each={props.availableDevices}> | |
| {(device) => ( | |
| <button | |
| class={`w-full text-left px-4 py-3 rounded-2xl text-xs transition-all flex items-center gap-3 ${props.selectedDeviceId === device.deviceId | |
| ? 'nm-inset text-primary font-bold' | |
| : 'text-slate-600 hover:nm-flat' | |
| }`} | |
| onClick={() => { | |
| props.onDeviceSelect(device.deviceId); | |
| setShowDevices(false); | |
| }} | |
| > | |
| <span class="material-symbols-outlined text-lg opacity-40">mic</span> | |
| <span class="truncate font-medium">{device.label || `Channel ${device.deviceId.slice(0, 4)}`}</span> | |
| </button> | |
| )} | |
| </For> | |
| </div> | |
| </div> | |
| </Show> | |
| </div> | |
| {/* Placeholder Items matching design */} | |
| <button class="neu-square-btn text-slate-300 cursor-not-allowed" title="Translation (Pro)"> | |
| <span class="material-symbols-outlined text-xl">translate</span> | |
| </button> | |
| <button class="neu-square-btn text-slate-500" title="Export Transcript" onClick={() => (window as any).appStore?.copyTranscript()}> | |
| <span class="material-symbols-outlined text-xl">download</span> | |
| </button> | |
| </nav> | |
| <div class="mt-auto"> | |
| <button | |
| class={`neu-square-btn transition-all active:scale-95 ${props.activeTab === 'settings' ? 'active' : 'text-slate-500'}`} | |
| onClick={() => props.onTabChange('settings')} | |
| title="Settings" | |
| > | |
| <span class="material-symbols-outlined text-xl">settings</span> | |
| </button> | |
| </div> | |
| </aside> | |
| ); | |
| }; | |
| export default Sidebar; | |