feat(cis): add CCTranscriptionPanel Live tab to sidebar (Phase 1)

- Add CCTranscriptionPanel component with Live tab
- Add Zustand transcriptionStore for session state management
- Wire panel into BasePanel sidebar system
- Fix merged switch cases in getIconForPanel, getDescriptionForPanel, renderCurrentPanel
- Add VITE_WHISPERLIVE_URL to .env
This commit is contained in:
2026-05-20 21:34:21 +00:00
parent 0f4956d4a4
commit 2ee4e4afe7
3 changed files with 241 additions and 1 deletions
+68
View File
@@ -0,0 +1,68 @@
import { create } from 'zustand';
export interface TranscriptionSegment {
text: string;
isFinal: boolean;
start: number;
end: number;
}
interface TranscriptionState {
isRecording: boolean;
isConnecting: boolean;
completedSegments: TranscriptionSegment[];
currentSegment: TranscriptionSegment | null;
wordCount: number;
elapsedSeconds: number;
startSession: () => void;
stopSession: () => void;
saveSegment: (text: string, isFinal: boolean, metadata: { start: number; end: number }) => void;
resetSession: () => void;
tickElapsed: () => void;
}
export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
isRecording: false,
isConnecting: false,
completedSegments: [],
currentSegment: null,
wordCount: 0,
elapsedSeconds: 0,
startSession: () => {
set({ isRecording: true, isConnecting: false, elapsedSeconds: 0 });
},
stopSession: () => {
set({ isRecording: false, isConnecting: false });
},
saveSegment: (text: string, isFinal: boolean, metadata: { start: number; end: number }) => {
const { completedSegments, currentSegment } = get();
if (isFinal) {
// Final segment — move current to completed, clear current
const newCompleted = [...completedSegments];
if (currentSegment && currentSegment.text.trim()) {
newCompleted.push({ ...currentSegment, isFinal: true });
}
const newWordCount = newCompleted.reduce(
(sum, seg) => sum + seg.text.trim().split(/\s+/).filter(Boolean).length,
0
);
set({ completedSegments: newCompleted, currentSegment: null, wordCount: newWordCount });
} else {
// In-progress segment
set({ currentSegment: { text, isFinal: false, ...metadata } });
}
},
resetSession: () => {
set({ isRecording: false, isConnecting: false, completedSegments: [], currentSegment: null, wordCount: 0, elapsedSeconds: 0 });
},
tickElapsed: () => {
set((state) => ({ elapsedSeconds: state.elapsedSeconds + 1 }));
},
}));