mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
Add Vite React component bundling, SSE process streaming, preferences persistence, WebSocket terminal proxy, local Ollama integration
- Enable local Ollama service in compose with llm-router dependency - Add SSE /stream/processes endpoint in kali-executor for live process updates - Add WebSocket /ws/execute for real-time terminal command streaming - Implement preferences persistence (provider/model) via dashboard backend - Create Vite build pipeline for React components (VoiceControls, NetworkMap, GuidedWizard) - Update dashboard Dockerfile with Node builder stage for component bundling - Wire dashboard template to mount components and subscribe to SSE/WebSocket streams - Add preferences load/save hooks in UI to persist LLM provider/model selection
This commit is contained in:
124
services/dashboard/components/VoiceControls.jsx
Normal file
124
services/dashboard/components/VoiceControls.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
const VoiceControls = ({ onCommand = () => {}, apiUrl = '/api/voice' }) => {
|
||||
const [state, setState] = useState('idle'); // idle, listening, processing
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const [hotkey, setHotkey] = useState('`'); // backtick
|
||||
const mediaRecorderRef = useRef(null);
|
||||
const audioChunksRef = useRef([]);
|
||||
const hotkeyPressedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
|
||||
hotkeyPressedRef.current = true;
|
||||
startRecording();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.key === hotkey && hotkeyPressedRef.current) {
|
||||
hotkeyPressedRef.current = false;
|
||||
stopRecording();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, [state, hotkey]);
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorderRef.current = new MediaRecorder(stream);
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorderRef.current.ondataavailable = (event) => {
|
||||
audioChunksRef.current.push(event.data);
|
||||
};
|
||||
|
||||
mediaRecorderRef.current.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
|
||||
await sendToTranscribe(audioBlob);
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorderRef.current.start();
|
||||
setState('listening');
|
||||
setTranscript('Listening...');
|
||||
} catch (error) {
|
||||
console.error('Microphone access denied:', error);
|
||||
setTranscript('Microphone access denied');
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorderRef.current && state === 'listening') {
|
||||
mediaRecorderRef.current.stop();
|
||||
setState('processing');
|
||||
setTranscript('Processing...');
|
||||
}
|
||||
};
|
||||
|
||||
const sendToTranscribe = async (audioBlob) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'recording.wav');
|
||||
|
||||
const response = await fetch(`${apiUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
setTranscript(result.text || 'No speech detected');
|
||||
setState('idle');
|
||||
|
||||
if (result.text) {
|
||||
onCommand(result.text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Transcription failed:', error);
|
||||
setTranscript('Transcription failed');
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="voice-controls p-4 bg-sp-grey rounded border border-sp-grey-mid">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className={`voice-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl transition ${
|
||||
state === 'listening'
|
||||
? 'bg-sp-red animate-pulse'
|
||||
: state === 'processing'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-sp-grey-mid hover:bg-sp-red'
|
||||
}`}
|
||||
onMouseDown={startRecording}
|
||||
onMouseUp={stopRecording}
|
||||
disabled={state === 'processing'}
|
||||
>
|
||||
{state === 'listening' ? '🎙️' : state === 'processing' ? '⏳' : '🎤'}
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-sp-white-muted">
|
||||
{state === 'idle' && `Press & hold ${hotkey} or click to speak`}
|
||||
{state === 'listening' && 'Release to stop recording'}
|
||||
{state === 'processing' && 'Processing audio...'}
|
||||
</div>
|
||||
{transcript && (
|
||||
<div className="text-sm text-sp-white mt-1 font-mono">{transcript}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceControls;
|
||||
Reference in New Issue
Block a user