Spaces:
Running
Running
File size: 6,180 Bytes
b8cc2bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | 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;
|