mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-02 16:45:15 -07:00
fix review feedback: restart race, listener cleanup, stable keys, accessibility
This commit is contained in:
@@ -57,8 +57,8 @@ function renderMarkdown(md: string): React.ReactNode[] {
|
|||||||
}
|
}
|
||||||
elements.push(
|
elements.push(
|
||||||
<ul key={elements.length} className="space-y-1 my-2">
|
<ul key={elements.length} className="space-y-1 my-2">
|
||||||
{items.map((item) => (
|
{items.map((item, idx) => (
|
||||||
<li key={item} className="text-sm text-muted-foreground flex gap-2">
|
<li key={idx} className="text-sm text-muted-foreground flex gap-2">
|
||||||
<span className="text-muted-foreground/50 select-none shrink-0">•</span>
|
<span className="text-muted-foreground/50 select-none shrink-0">•</span>
|
||||||
<span>{inlineMarkdown(item)}</span>
|
<span>{inlineMarkdown(item)}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -96,9 +96,9 @@ function renderTable(tableLines: string[], keyBase: number): React.ReactNode {
|
|||||||
<table className="text-sm w-full">
|
<table className="text-sm w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
{headers.map((h) => (
|
{headers.map((h, hIdx) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={hIdx}
|
||||||
className="text-left py-1.5 pr-4 text-muted-foreground font-medium text-xs"
|
className="text-left py-1.5 pr-4 text-muted-foreground font-medium text-xs"
|
||||||
>
|
>
|
||||||
{inlineMarkdown(h)}
|
{inlineMarkdown(h)}
|
||||||
@@ -107,10 +107,10 @@ function renderTable(tableLines: string[], keyBase: number): React.ReactNode {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row) => (
|
{rows.map((row, rowIdx) => (
|
||||||
<tr key={row.join('|')} className="border-b border-border/50">
|
<tr key={rowIdx} className="border-b border-border/50">
|
||||||
{row.map((cell) => (
|
{row.map((cell, cellIdx) => (
|
||||||
<td key={cell} className="py-1.5 pr-4 text-muted-foreground">
|
<td key={cellIdx} className="py-1.5 pr-4 text-muted-foreground">
|
||||||
{inlineMarkdown(cell)}
|
{inlineMarkdown(cell)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ export function GeneralPage() {
|
|||||||
setKeepServerRunningOnClose(checked);
|
setKeepServerRunningOnClose(checked);
|
||||||
platform.lifecycle.setKeepServerRunning(checked).catch((error) => {
|
platform.lifecycle.setKeepServerRunning(checked).catch((error) => {
|
||||||
console.error('Failed to sync setting to Rust:', error);
|
console.error('Failed to sync setting to Rust:', error);
|
||||||
|
setKeepServerRunningOnClose(!checked);
|
||||||
|
toast({
|
||||||
|
title: 'Failed to update setting',
|
||||||
|
description: 'Could not sync setting to backend.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
toast({
|
toast({
|
||||||
title: 'Setting updated',
|
title: 'Setting updated',
|
||||||
|
|||||||
@@ -171,17 +171,21 @@ export function GpuPage() {
|
|||||||
};
|
};
|
||||||
}, [cudaDownloading, serverUrl, refetchCudaStatus]);
|
}, [cudaDownloading, serverUrl, refetchCudaStatus]);
|
||||||
|
|
||||||
|
const clearHealthPolling = useCallback(() => {
|
||||||
|
if (healthPollRef.current) {
|
||||||
|
clearInterval(healthPollRef.current);
|
||||||
|
healthPollRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const startHealthPolling = useCallback(() => {
|
const startHealthPolling = useCallback(() => {
|
||||||
if (healthPollRef.current) return;
|
clearHealthPolling();
|
||||||
|
|
||||||
healthPollRef.current = setInterval(async () => {
|
healthPollRef.current = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await apiClient.getHealth();
|
const result = await apiClient.getHealth();
|
||||||
if (result.status === 'healthy') {
|
if (result.status === 'healthy') {
|
||||||
if (healthPollRef.current) {
|
clearHealthPolling();
|
||||||
clearInterval(healthPollRef.current);
|
|
||||||
healthPollRef.current = null;
|
|
||||||
}
|
|
||||||
setRestartPhase('ready');
|
setRestartPhase('ready');
|
||||||
queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
setTimeout(() => setRestartPhase('idle'), 2000);
|
setTimeout(() => setRestartPhase('idle'), 2000);
|
||||||
@@ -190,7 +194,23 @@ export function GpuPage() {
|
|||||||
// Server still down, keep polling
|
// Server still down, keep polling
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, [queryClient]);
|
}, [queryClient, clearHealthPolling]);
|
||||||
|
|
||||||
|
const restartServerWithPolling = useCallback(
|
||||||
|
async (errorMessage: string) => {
|
||||||
|
setRestartPhase('stopping');
|
||||||
|
try {
|
||||||
|
await platform.lifecycle.restartServer();
|
||||||
|
setRestartPhase('waiting');
|
||||||
|
startHealthPolling();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
clearHealthPolling();
|
||||||
|
setRestartPhase('idle');
|
||||||
|
throw new Error(e instanceof Error ? e.message : errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[platform, startHealthPolling, clearHealthPolling],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -209,24 +229,9 @@ export function GpuPage() {
|
|||||||
|
|
||||||
const handleRestart = async () => {
|
const handleRestart = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setRestartPhase('stopping');
|
|
||||||
try {
|
try {
|
||||||
setRestartPhase('waiting');
|
await restartServerWithPolling('Restart failed');
|
||||||
startHealthPolling();
|
|
||||||
await platform.lifecycle.restartServer();
|
|
||||||
if (healthPollRef.current) {
|
|
||||||
clearInterval(healthPollRef.current);
|
|
||||||
healthPollRef.current = null;
|
|
||||||
}
|
|
||||||
setRestartPhase('ready');
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
setTimeout(() => setRestartPhase('idle'), 2000);
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setRestartPhase('idle');
|
|
||||||
if (healthPollRef.current) {
|
|
||||||
clearInterval(healthPollRef.current);
|
|
||||||
healthPollRef.current = null;
|
|
||||||
}
|
|
||||||
setError(e instanceof Error ? e.message : 'Restart failed');
|
setError(e instanceof Error ? e.message : 'Restart failed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -236,22 +241,8 @@ export function GpuPage() {
|
|||||||
setRestartPhase('stopping');
|
setRestartPhase('stopping');
|
||||||
try {
|
try {
|
||||||
await apiClient.deleteCudaBackend();
|
await apiClient.deleteCudaBackend();
|
||||||
setRestartPhase('waiting');
|
await restartServerWithPolling('Failed to switch to CPU');
|
||||||
startHealthPolling();
|
|
||||||
await platform.lifecycle.restartServer();
|
|
||||||
if (healthPollRef.current) {
|
|
||||||
clearInterval(healthPollRef.current);
|
|
||||||
healthPollRef.current = null;
|
|
||||||
}
|
|
||||||
setRestartPhase('ready');
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
setTimeout(() => setRestartPhase('idle'), 2000);
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setRestartPhase('idle');
|
|
||||||
if (healthPollRef.current) {
|
|
||||||
clearInterval(healthPollRef.current);
|
|
||||||
healthPollRef.current = null;
|
|
||||||
}
|
|
||||||
setError(e instanceof Error ? e.message : 'Failed to switch to CPU');
|
setError(e instanceof Error ? e.message : 'Failed to switch to CPU');
|
||||||
refetchCudaStatus();
|
refetchCudaStatus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function LogsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
entries.map((entry, i) => <LogLine key={`${entry.timestamp}-${i}`} entry={entry} />)
|
entries.map((entry) => <LogLine key={entry.id} entry={entry} />)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function SettingRow({
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<label
|
<label
|
||||||
htmlFor={htmlFor}
|
htmlFor={htmlFor}
|
||||||
className="text-sm font-medium leading-none cursor-pointer select-none"
|
className={`text-sm font-medium leading-none select-none ${htmlFor ? 'cursor-pointer' : ''}`}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function Toaster() {
|
|||||||
<ToastClose />
|
<ToastClose />
|
||||||
</Toast>
|
</Toast>
|
||||||
))}
|
))}
|
||||||
<ToastViewport className={isPlayerOpen ? 'sm:bottom-32' : ''} />
|
<ToastViewport className={isPlayerOpen ? 'sm:bottom-44' : ''} />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors',
|
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
checked ? 'bg-accent' : 'bg-muted-foreground/25',
|
checked ? 'bg-accent' : 'bg-muted-foreground/25',
|
||||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import type { ServerLogEntry } from '@/platform/types';
|
|||||||
|
|
||||||
const MAX_LOG_ENTRIES = 2000;
|
const MAX_LOG_ENTRIES = 2000;
|
||||||
|
|
||||||
|
let nextLogEntryId = 0;
|
||||||
|
|
||||||
export interface LogEntry extends ServerLogEntry {
|
export interface LogEntry extends ServerLogEntry {
|
||||||
|
id: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,9 +20,8 @@ export const useLogStore = create<LogStore>((set) => ({
|
|||||||
entries: [],
|
entries: [],
|
||||||
addEntry: (entry) =>
|
addEntry: (entry) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newEntry: LogEntry = { ...entry, timestamp: Date.now() };
|
const newEntry: LogEntry = { ...entry, id: nextLogEntryId++, timestamp: Date.now() };
|
||||||
const entries = [...state.entries, newEntry];
|
const entries = [...state.entries, newEntry];
|
||||||
// Cap buffer size
|
|
||||||
if (entries.length > MAX_LOG_ENTRIES) {
|
if (entries.length > MAX_LOG_ENTRIES) {
|
||||||
return { entries: entries.slice(entries.length - MAX_LOG_ENTRIES) };
|
return { entries: entries.slice(entries.length - MAX_LOG_ENTRIES) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,16 +88,27 @@ class TauriLifecycle implements PlatformLifecycle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribeToServerLogs(callback: (entry: ServerLogEntry) => void): () => void {
|
subscribeToServerLogs(callback: (entry: ServerLogEntry) => void): () => void {
|
||||||
|
let disposed = false;
|
||||||
let unlisten: (() => void) | null = null;
|
let unlisten: (() => void) | null = null;
|
||||||
|
|
||||||
listen<ServerLogEntry>('server-log', (event) => {
|
void listen<ServerLogEntry>('server-log', (event) => {
|
||||||
callback(event.payload);
|
callback(event.payload);
|
||||||
}).then((fn) => {
|
})
|
||||||
unlisten = fn;
|
.then((fn) => {
|
||||||
});
|
if (disposed) {
|
||||||
|
fn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unlisten = fn;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to subscribe to server logs:', error);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
disposed = true;
|
||||||
unlisten?.();
|
unlisten?.();
|
||||||
|
unlisten = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user