feat: update notice after a server update, keep the open tab on reload, chunked Whisper in Quick Dump, compact emoji picker
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m31s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m31s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
20b907078f
commit
55112b940c
17 files changed
+250
-61
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { APP_BUILD } from '../version';
|
||||
|
||||
const CHECK_EVERY_MS = 15 * 60 * 1000;
|
||||
|
||||
export interface AvailableUpdate {
|
||||
version: string;
|
||||
build: string;
|
||||
}
|
||||
|
||||
// Notices when the server runs a newer build than this page (version.json, written at build time and
|
||||
// never cached): when the tab or the installed app comes back to the foreground, every 15 minutes and
|
||||
// when the realtime connection is back after a server restart. "Später" hides it until the next build.
|
||||
export function useUpdateCheck() {
|
||||
const [update, setUpdate] = useState<AvailableUpdate | null>(null);
|
||||
const dismissedRef = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!APP_BUILD) return; // dev server: no version.json
|
||||
let busy = false;
|
||||
const check = async () => {
|
||||
if (busy || document.visibilityState === 'hidden') return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/version.json?t=${Date.now()}`, { cache: 'no-store' });
|
||||
const live = (await res.json()) as Partial<AvailableUpdate>;
|
||||
if (live.build && live.build !== APP_BUILD && live.build !== dismissedRef.current) {
|
||||
setUpdate({ version: live.version || '', build: live.build });
|
||||
}
|
||||
} catch {
|
||||
// offline, or an older server without version.json (answers with the page): try again later
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
};
|
||||
const onVisible = () => document.visibilityState === 'visible' && void check();
|
||||
const onWsStatus = (e: Event) => (e as CustomEvent<{ connected?: boolean }>).detail?.connected && void check();
|
||||
const timer = window.setInterval(() => void check(), CHECK_EVERY_MS);
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
window.addEventListener('focus', onVisible);
|
||||
window.addEventListener('mindsift:ws:status', onWsStatus);
|
||||
void check();
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
document.removeEventListener('visibilitychange', onVisible);
|
||||
window.removeEventListener('focus', onVisible);
|
||||
window.removeEventListener('mindsift:ws:status', onWsStatus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const dismiss = () => {
|
||||
if (update) dismissedRef.current = update.build;
|
||||
setUpdate(null);
|
||||
};
|
||||
return { update, dismiss };
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export interface VoiceInputOptions {
|
||||
continuous?: boolean;
|
||||
/** Live dictation mode (/diktat): auto-restart browser speech recognition on pause, and chunk server recordings every ~8s or ~0.8s of silence. */
|
||||
live?: boolean;
|
||||
/** Server mode only: send the recording in chunks like live mode, so the text arrives while speaking (Quick Dump). */
|
||||
chunked?: boolean;
|
||||
}
|
||||
|
||||
const LIVE_CHUNK_MS = 8000;
|
||||
@@ -25,7 +27,8 @@ const MIN_RESTART_INTERVAL_MS = 300;
|
||||
* is made depends on the server setting, see lib/voice.ts. mode is null until known, "off" or
|
||||
* "unsupported" means: do not show a microphone.
|
||||
*/
|
||||
export function useVoiceInput({ onText, onError, continuous = false, live = false }: VoiceInputOptions) {
|
||||
export function useVoiceInput({ onText, onError, continuous = false, live = false, chunked = false }: VoiceInputOptions) {
|
||||
const chunkServer = live || chunked;
|
||||
const [mode, setMode] = useState<EffectiveVoiceMode | null>(null);
|
||||
const [state, setState] = useState<VoiceState>('idle');
|
||||
const [pendingChunks, setPendingChunks] = useState(0);
|
||||
@@ -149,14 +152,14 @@ export function useVoiceInput({ onText, onError, continuous = false, live = fals
|
||||
}
|
||||
const rec = recorderRef.current;
|
||||
if (!rec) {
|
||||
if (live && state === 'listening') {
|
||||
if (chunkServer && state === 'listening') {
|
||||
setStateSafe('idle');
|
||||
onTextRef.current(transcriptRef.current, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
recorderRef.current = null;
|
||||
if (live) {
|
||||
if (chunkServer) {
|
||||
const idx = chunkIndexRef.current++;
|
||||
setStateSafe('transcribing');
|
||||
processSegmentPromise(idx, rec.stop());
|
||||
@@ -179,7 +182,7 @@ export function useVoiceInput({ onText, onError, continuous = false, live = fals
|
||||
}
|
||||
// fail only touches refs and state setters
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [live, state]);
|
||||
}, [chunkServer, state]);
|
||||
|
||||
const startServer = async () => {
|
||||
const rec = new VoiceRecorder();
|
||||
@@ -201,7 +204,7 @@ export function useVoiceInput({ onText, onError, continuous = false, live = fals
|
||||
return;
|
||||
}
|
||||
timerRef.current = window.setTimeout(() => void stop(), maxSecondsRef.current * 1000);
|
||||
if (live) {
|
||||
if (chunkServer) {
|
||||
let segmentMs = 0;
|
||||
let silentMs = 0;
|
||||
let hadSpeech = false;
|
||||
@@ -325,7 +328,7 @@ export function useVoiceInput({ onText, onError, continuous = false, live = fals
|
||||
else if (current === 'browser') startBrowser();
|
||||
else fail(current === 'off' ? 'Spracheingabe ist ausgeschaltet' : 'Spracheingabe wird von diesem Browser nicht unterstützt');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mode, continuous, live]);
|
||||
}, [mode, continuous, live, chunkServer]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
cancelledRef.current = true;
|
||||
|
||||
Reference in new issue
Block a user