feat: improve VS Code dev flow and stabilize sidebar/chat behavior (#754)
* fix: improve session sidebar tooltip and truncation behavior - Keep new-draft tooltip anchored to its trigger button - Fix minimal-mode worktree/group header text truncation - Tune minimal-mode right padding to reduce early label clipping * fix: render reasoning through markdown pipeline - Use Streamdown rendering for reasoning in live chat mode - Remove italic styling from reasoning text - Render expanded reasoning content with MarkdownRenderer * chore: remove legacy electron dependencies - Removed unused Electron packages from root and UI manifests - Deleted obsolete Electron context menu type declaration - Regenerated lockfile after dependency cleanup * fix: handle non-repository folders in git status API - Prevent 500 errors when status is requested outside a valid Git repo - Improve repository detection using `git rev-parse --git-dir` - Reduce noisy server logs for expected non-repo status checks * fix unloaded session chat layout flicker * fix: reduce noisy TTS status polling Cache and dedupe TTS status requests, and only check provider availability when the related voice features are enabled so disabled voice setups stay quiet. * perf: throttle background PR git status refreshes * fix: improve VS Code Explorer file drop mentions in chat - Add Explorer context action to insert selected files as @mentions. - Handle Explorer drag-and-drop to prefill @file mentions instead of attachments. - Prevent duplicate plain-path text when dropping multiple files. * fix: deduplicate recent sessions in VS Code sidebar - Hide sessions from main list when already shown in recent - Apply dedup only in VS Code runtime - Keep session search behavior unchanged * feat: add true HMR dev flow for VS Code extension - Load VS Code webview from Vite dev server with React refresh preamble - Add `vscode:dev` runner that starts watchers and opens Extension Development Host - Update VS Code dev docs and scripts to use the new HMR startup flow * feat: polish VS Code session sidebar and attachment UX - Add resizable sessions sidebar in VS Code layout - Tighten session list spacing and hover behavior in VS Code - Remove bulk file/image attach success toasts while keeping error toasts
This commit is contained in:
committed by
GitHub
parent
ea6d4c4d43
commit
1231fd773e
@@ -132,13 +132,20 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
|
||||
const setPendingInputText = useSessionStore((s) => s.setPendingInputText);
|
||||
const messages = useSessionStore((s) => s.messages);
|
||||
const createSession = useSessionStore((s) => s.createSession);
|
||||
const { currentProviderId, currentModelId, currentAgentName, voiceProvider, speechRate, speechPitch, speechVolume, sayVoice, browserVoice, openaiVoice, summarizeVoiceConversation, summarizeCharacterThreshold } = useConfigStore();
|
||||
|
||||
const { currentProviderId, currentModelId, currentAgentName, voiceModeEnabled, voiceProvider, speechRate, speechPitch, speechVolume, sayVoice, browserVoice, openaiVoice, summarizeVoiceConversation, summarizeCharacterThreshold } = useConfigStore();
|
||||
|
||||
const shouldCheckOpenAIAvailability = voiceModeEnabled && voiceProvider === 'openai';
|
||||
const shouldCheckSayAvailability = voiceModeEnabled && voiceProvider === 'say';
|
||||
|
||||
// Server TTS for mobile (bypasses Safari audio restrictions)
|
||||
const { speak: speakServerTTS, stop: stopServerTTS, isAvailable: isServerTTSAvailable, unlockAudio: unlockServerTTSAudio } = useServerTTS();
|
||||
|
||||
const { speak: speakServerTTS, stop: stopServerTTS, isAvailable: isServerTTSAvailable, unlockAudio: unlockServerTTSAudio } = useServerTTS({
|
||||
enabled: shouldCheckOpenAIAvailability,
|
||||
});
|
||||
|
||||
// macOS Say TTS
|
||||
const { speak: speakSayTTS, stop: stopSayTTS, isAvailable: isSayTTSAvailable, unlockAudio: unlockSayTTSAudio } = useSayTTS();
|
||||
const { speak: speakSayTTS, stop: stopSayTTS, isAvailable: isSayTTSAvailable, unlockAudio: unlockSayTTSAudio } = useSayTTS({
|
||||
enabled: shouldCheckSayAvailability,
|
||||
});
|
||||
|
||||
// Update messages ref when messages change
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,11 +7,16 @@ import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
|
||||
const MAX_BACKGROUND_PR_DIRECTORIES = 50;
|
||||
const MAX_BACKGROUND_PR_DIRECTORIES = 20;
|
||||
const ACTIVE_DIRECTORY_REFRESH_TTL_MS = 15_000;
|
||||
const BACKGROUND_DIRECTORY_REFRESH_TTL_MS = 2 * 60_000;
|
||||
const BRANCH_REFRESH_INTERVAL_MS = 15_000;
|
||||
const MAX_STATUS_FETCH_PER_TICK = 3;
|
||||
const MAX_STATUS_FETCH_ON_RESUME = 5;
|
||||
const STATUS_FETCH_CONCURRENCY = 2;
|
||||
const PR_EVENTUAL_CONSISTENCY_REFRESH_DELAY_MS = 5_000;
|
||||
const RESUME_REFRESH_DEBOUNCE_MS = 700;
|
||||
const RESUME_FORCE_COOLDOWN_MS = 8_000;
|
||||
|
||||
const normalizePath = (value?: string | null): string | null => {
|
||||
if (typeof value !== 'string') {
|
||||
@@ -47,6 +52,11 @@ type PrTarget = {
|
||||
remoteName?: string | null;
|
||||
};
|
||||
|
||||
type BranchRefreshOptions = {
|
||||
forceCurrent?: boolean;
|
||||
maxFetchCount?: number;
|
||||
};
|
||||
|
||||
const getBranchRefreshTtl = (directory: string, currentDirectory: string | null): number => {
|
||||
return directory === currentDirectory ? ACTIVE_DIRECTORY_REFRESH_TTL_MS : BACKGROUND_DIRECTORY_REFRESH_TTL_MS;
|
||||
};
|
||||
@@ -62,6 +72,56 @@ const hasRepoSignalChanged = (previous: BranchCacheEntry | undefined, next: Bran
|
||||
|| previous.behind !== next.behind;
|
||||
};
|
||||
|
||||
const prioritizeDirectoriesForFetch = (
|
||||
directories: string[],
|
||||
cache: Map<string, BranchCacheEntry>,
|
||||
currentDirectory: string | null,
|
||||
): string[] => {
|
||||
return [...directories].sort((left, right) => {
|
||||
const leftPriority = left === currentDirectory ? 0 : 1;
|
||||
const rightPriority = right === currentDirectory ? 0 : 1;
|
||||
if (leftPriority !== rightPriority) {
|
||||
return leftPriority - rightPriority;
|
||||
}
|
||||
|
||||
const leftFetchedAt = cache.get(left)?.fetchedAt ?? 0;
|
||||
const rightFetchedAt = cache.get(right)?.fetchedAt ?? 0;
|
||||
if (leftFetchedAt !== rightFetchedAt) {
|
||||
return leftFetchedAt - rightFetchedAt;
|
||||
}
|
||||
|
||||
return left.localeCompare(right);
|
||||
});
|
||||
};
|
||||
|
||||
const mapWithConcurrency = async <T, R>(
|
||||
values: T[],
|
||||
concurrency: number,
|
||||
mapper: (value: T) => Promise<R>,
|
||||
): Promise<R[]> => {
|
||||
if (values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const safeConcurrency = Math.max(1, Math.min(concurrency, values.length));
|
||||
const results = new Array<R>(values.length);
|
||||
let cursor = 0;
|
||||
|
||||
const worker = async () => {
|
||||
while (true) {
|
||||
const nextIndex = cursor;
|
||||
cursor += 1;
|
||||
if (nextIndex >= values.length) {
|
||||
return;
|
||||
}
|
||||
results[nextIndex] = await mapper(values[nextIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(Array.from({ length: safeConcurrency }, () => worker()));
|
||||
return results;
|
||||
};
|
||||
|
||||
const toPrTargets = (cache: Map<string, BranchCacheEntry>, directories: string[]): PrTarget[] => {
|
||||
const result: PrTarget[] = [];
|
||||
directories.forEach((directory) => {
|
||||
@@ -100,6 +160,10 @@ export const useGitHubPrBackgroundTracking = (
|
||||
const branchCacheRef = React.useRef<Map<string, BranchCacheEntry>>(new Map());
|
||||
const targetsRef = React.useRef<PrTarget[]>([]);
|
||||
const burstTimeoutsRef = React.useRef<Map<string, number>>(new Map());
|
||||
const refreshInFlightRef = React.useRef(false);
|
||||
const pendingRefreshRef = React.useRef<BranchRefreshOptions | null>(null);
|
||||
const resumeRefreshTimeoutRef = React.useRef<number | null>(null);
|
||||
const lastResumeRefreshAtRef = React.useRef(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
branchCacheRef.current = branchCache;
|
||||
@@ -179,25 +243,37 @@ export const useGitHubPrBackgroundTracking = (
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const refreshBranches = async (force = false): Promise<PrTarget[]> => {
|
||||
const refreshBranches = async (options?: BranchRefreshOptions): Promise<PrTarget[]> => {
|
||||
const forceCurrent = options?.forceCurrent === true;
|
||||
const maxFetchCount = Math.max(1, options?.maxFetchCount ?? MAX_STATUS_FETCH_PER_TICK);
|
||||
const now = Date.now();
|
||||
const directoriesToFetch = candidateDirectories.filter((directory) => {
|
||||
const dueDirectories = candidateDirectories.filter((directory) => {
|
||||
const cached = branchCacheRef.current.get(directory);
|
||||
if (!cached) {
|
||||
return true;
|
||||
}
|
||||
if (force) {
|
||||
|
||||
if (forceCurrent && currentDirectory && directory === currentDirectory) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return now - cached.fetchedAt > getBranchRefreshTtl(directory, currentDirectory);
|
||||
});
|
||||
|
||||
const directoriesToFetch = prioritizeDirectoriesForFetch(
|
||||
dueDirectories,
|
||||
branchCacheRef.current,
|
||||
currentDirectory,
|
||||
).slice(0, maxFetchCount);
|
||||
|
||||
if (directoriesToFetch.length === 0) {
|
||||
return toPrTargets(branchCacheRef.current, candidateDirectories);
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
directoriesToFetch.map(async (directory) => {
|
||||
const results = await mapWithConcurrency(
|
||||
directoriesToFetch,
|
||||
STATUS_FETCH_CONCURRENCY,
|
||||
async (directory) => {
|
||||
try {
|
||||
const status = await git.getGitStatus(directory);
|
||||
const branch = typeof status.current === 'string' ? status.current.trim() : '';
|
||||
@@ -211,7 +287,7 @@ export const useGitHubPrBackgroundTracking = (
|
||||
} catch {
|
||||
return { directory, branch: null, tracking: null, ahead: 0, behind: 0 };
|
||||
}
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (cancelled) {
|
||||
@@ -281,13 +357,39 @@ export const useGitHubPrBackgroundTracking = (
|
||||
return toPrTargets(nextCache, candidateDirectories);
|
||||
};
|
||||
|
||||
void refreshBranches();
|
||||
const runRefresh = async (options?: BranchRefreshOptions): Promise<PrTarget[]> => {
|
||||
if (refreshInFlightRef.current) {
|
||||
const previousPending = pendingRefreshRef.current;
|
||||
pendingRefreshRef.current = {
|
||||
forceCurrent: Boolean(previousPending?.forceCurrent || options?.forceCurrent),
|
||||
maxFetchCount: Math.max(
|
||||
previousPending?.maxFetchCount ?? 1,
|
||||
options?.maxFetchCount ?? 1,
|
||||
),
|
||||
};
|
||||
return [];
|
||||
}
|
||||
|
||||
refreshInFlightRef.current = true;
|
||||
try {
|
||||
return await refreshBranches(options);
|
||||
} finally {
|
||||
refreshInFlightRef.current = false;
|
||||
const pending = pendingRefreshRef.current;
|
||||
pendingRefreshRef.current = null;
|
||||
if (pending) {
|
||||
void runRefresh(pending);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void runRefresh({ forceCurrent: true, maxFetchCount: MAX_STATUS_FETCH_ON_RESUME });
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
void refreshBranches();
|
||||
void runRefresh({ maxFetchCount: MAX_STATUS_FETCH_PER_TICK });
|
||||
}, BRANCH_REFRESH_INTERVAL_MS);
|
||||
|
||||
const refreshOnResume = () => {
|
||||
@@ -295,31 +397,44 @@ export const useGitHubPrBackgroundTracking = (
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshBranches(true).then((nextTargets) => {
|
||||
const currentTargets = nextTargets.length > 0 ? nextTargets : targetsRef.current;
|
||||
if (currentTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - lastResumeRefreshAtRef.current < RESUME_FORCE_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTargets = currentDirectory
|
||||
? currentTargets.filter((target) => target.directory === currentDirectory)
|
||||
: [];
|
||||
if (resumeRefreshTimeoutRef.current !== null) {
|
||||
window.clearTimeout(resumeRefreshTimeoutRef.current);
|
||||
}
|
||||
|
||||
if (activeTargets.length > 0) {
|
||||
void refreshPrTargets(activeTargets, {
|
||||
resumeRefreshTimeoutRef.current = window.setTimeout(() => {
|
||||
resumeRefreshTimeoutRef.current = null;
|
||||
lastResumeRefreshAtRef.current = Date.now();
|
||||
void runRefresh({ forceCurrent: true, maxFetchCount: MAX_STATUS_FETCH_ON_RESUME }).then((nextTargets) => {
|
||||
const currentTargets = nextTargets.length > 0 ? nextTargets : targetsRef.current;
|
||||
if (currentTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTargets = currentDirectory
|
||||
? currentTargets.filter((target) => target.directory === currentDirectory)
|
||||
: [];
|
||||
|
||||
if (activeTargets.length > 0) {
|
||||
void refreshPrTargets(activeTargets, {
|
||||
force: true,
|
||||
silent: true,
|
||||
markInitialResolved: true,
|
||||
});
|
||||
}
|
||||
|
||||
void refreshPrTargets(currentTargets, {
|
||||
force: true,
|
||||
onlyExistingPr: true,
|
||||
silent: true,
|
||||
markInitialResolved: true,
|
||||
});
|
||||
}
|
||||
|
||||
void refreshPrTargets(currentTargets, {
|
||||
force: true,
|
||||
onlyExistingPr: true,
|
||||
silent: true,
|
||||
markInitialResolved: true,
|
||||
});
|
||||
});
|
||||
}, RESUME_REFRESH_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
window.addEventListener('focus', refreshOnResume);
|
||||
@@ -330,6 +445,12 @@ export const useGitHubPrBackgroundTracking = (
|
||||
window.clearInterval(intervalId);
|
||||
window.removeEventListener('focus', refreshOnResume);
|
||||
document.removeEventListener('visibilitychange', refreshOnResume);
|
||||
if (resumeRefreshTimeoutRef.current !== null) {
|
||||
window.clearTimeout(resumeRefreshTimeoutRef.current);
|
||||
resumeRefreshTimeoutRef.current = null;
|
||||
}
|
||||
pendingRefreshRef.current = null;
|
||||
refreshInFlightRef.current = false;
|
||||
};
|
||||
}, [candidateDirectories, currentDirectory, git, refreshPrTargets, scheduleBurstRefresh]);
|
||||
|
||||
|
||||
@@ -34,10 +34,18 @@ export function useMessageTTS(): UseMessageTTSReturn {
|
||||
openaiVoice,
|
||||
summarizeMessageTTS,
|
||||
summarizeCharacterThreshold,
|
||||
showMessageTTSButtons,
|
||||
} = useConfigStore();
|
||||
|
||||
const { speak: speakServerTTS, stop: stopServerTTS, isAvailable: isServerTTSAvailable } = useServerTTS();
|
||||
const { speak: speakSayTTS, stop: stopSayTTS, isAvailable: isSayTTSAvailable } = useSayTTS();
|
||||
|
||||
const shouldCheckOpenAIAvailability = showMessageTTSButtons && voiceProvider === 'openai';
|
||||
const shouldCheckSayAvailability = showMessageTTSButtons && voiceProvider === 'say';
|
||||
|
||||
const { speak: speakServerTTS, stop: stopServerTTS, isAvailable: isServerTTSAvailable } = useServerTTS({
|
||||
enabled: shouldCheckOpenAIAvailability,
|
||||
});
|
||||
const { speak: speakSayTTS, stop: stopSayTTS, isAvailable: isSayTTSAvailable } = useSayTTS({
|
||||
enabled: shouldCheckSayAvailability,
|
||||
});
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
|
||||
@@ -19,6 +19,67 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SayTTSStatusCache {
|
||||
available: boolean;
|
||||
voices: Array<{ name: string; locale: string }>;
|
||||
checkedAt: number;
|
||||
}
|
||||
|
||||
interface UseSayTTSOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const SAY_TTS_STATUS_TTL_MS = 30000;
|
||||
let sayTTSStatusCache: SayTTSStatusCache | null = null;
|
||||
let sayTTSStatusRequest: Promise<SayTTSStatusCache> | null = null;
|
||||
|
||||
async function getSayTTSStatus(): Promise<SayTTSStatusCache> {
|
||||
const now = Date.now();
|
||||
if (sayTTSStatusCache && now - sayTTSStatusCache.checkedAt < SAY_TTS_STATUS_TTL_MS) {
|
||||
return sayTTSStatusCache;
|
||||
}
|
||||
|
||||
if (sayTTSStatusRequest) {
|
||||
return sayTTSStatusRequest;
|
||||
}
|
||||
|
||||
sayTTSStatusRequest = (async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tts/say/status');
|
||||
if (!response.ok) {
|
||||
const unavailableStatus: SayTTSStatusCache = {
|
||||
available: false,
|
||||
voices: [],
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
sayTTSStatusCache = unavailableStatus;
|
||||
return unavailableStatus;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const nextStatus: SayTTSStatusCache = {
|
||||
available: Boolean(data.available),
|
||||
voices: Array.isArray(data.voices) ? data.voices : [],
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
sayTTSStatusCache = nextStatus;
|
||||
return nextStatus;
|
||||
} catch {
|
||||
const unavailableStatus: SayTTSStatusCache = {
|
||||
available: false,
|
||||
voices: [],
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
sayTTSStatusCache = unavailableStatus;
|
||||
return unavailableStatus;
|
||||
} finally {
|
||||
sayTTSStatusRequest = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return sayTTSStatusRequest;
|
||||
}
|
||||
|
||||
export interface UseSayTTSReturn {
|
||||
/** Whether TTS is currently playing */
|
||||
isPlaying: boolean;
|
||||
@@ -61,7 +122,8 @@ function getAudioContext(): AudioContext {
|
||||
return sharedAudioContext;
|
||||
}
|
||||
|
||||
export function useSayTTS(): UseSayTTSReturn {
|
||||
export function useSayTTS(options: UseSayTTSOptions = {}): UseSayTTSReturn {
|
||||
const enabled = options.enabled ?? true;
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isAvailable, setIsAvailable] = useState(false);
|
||||
const [voices, setVoices] = useState<Array<{ name: string; locale: string }>>([]);
|
||||
@@ -97,28 +159,27 @@ export function useSayTTS(): UseSayTTSReturn {
|
||||
|
||||
// Check if macOS say is available
|
||||
const checkAvailability = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch('/api/tts/say/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setIsAvailable(data.available);
|
||||
if (data.voices) {
|
||||
setVoices(data.voices);
|
||||
}
|
||||
return data.available;
|
||||
}
|
||||
setIsAvailable(false);
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error('[useSayTTS] Failed to check availability:', err);
|
||||
if (!enabled) {
|
||||
setIsAvailable(false);
|
||||
setVoices([]);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
try {
|
||||
const status = await getSayTTSStatus();
|
||||
setIsAvailable(status.available);
|
||||
setVoices(status.voices);
|
||||
return status.available;
|
||||
} catch {
|
||||
setIsAvailable(false);
|
||||
setVoices([]);
|
||||
return false;
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
// Check availability on mount
|
||||
useEffect(() => {
|
||||
checkAvailability();
|
||||
void checkAvailability();
|
||||
}, [checkAvailability]);
|
||||
|
||||
// Stop current playback
|
||||
|
||||
@@ -19,6 +19,52 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
|
||||
interface ServerTTSStatusCache {
|
||||
available: boolean;
|
||||
checkedAt: number;
|
||||
}
|
||||
|
||||
interface UseServerTTSOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const SERVER_TTS_STATUS_TTL_MS = 30000;
|
||||
let serverTTSStatusCache: ServerTTSStatusCache | null = null;
|
||||
let serverTTSStatusRequest: Promise<boolean> | null = null;
|
||||
|
||||
async function getServerTTSStatus(): Promise<boolean> {
|
||||
const now = Date.now();
|
||||
if (serverTTSStatusCache && now - serverTTSStatusCache.checkedAt < SERVER_TTS_STATUS_TTL_MS) {
|
||||
return serverTTSStatusCache.available;
|
||||
}
|
||||
|
||||
if (serverTTSStatusRequest) {
|
||||
return serverTTSStatusRequest;
|
||||
}
|
||||
|
||||
serverTTSStatusRequest = (async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tts/status');
|
||||
if (!response.ok) {
|
||||
serverTTSStatusCache = { available: false, checkedAt: Date.now() };
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const available = Boolean(data.available);
|
||||
serverTTSStatusCache = { available, checkedAt: Date.now() };
|
||||
return available;
|
||||
} catch {
|
||||
serverTTSStatusCache = { available: false, checkedAt: Date.now() };
|
||||
return false;
|
||||
} finally {
|
||||
serverTTSStatusRequest = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return serverTTSStatusRequest;
|
||||
}
|
||||
|
||||
export interface UseServerTTSReturn {
|
||||
/** Whether TTS is currently playing */
|
||||
isPlaying: boolean;
|
||||
@@ -69,7 +115,8 @@ function getAudioContext(): AudioContext {
|
||||
return sharedAudioContext;
|
||||
}
|
||||
|
||||
export function useServerTTS(): UseServerTTSReturn {
|
||||
export function useServerTTS(options: UseServerTTSOptions = {}): UseServerTTSReturn {
|
||||
const enabled = options.enabled ?? true;
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isAvailable, setIsAvailable] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -82,29 +129,30 @@ export function useServerTTS(): UseServerTTSReturn {
|
||||
|
||||
// Check if server TTS is available
|
||||
const checkAvailability = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch('/api/tts/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Available if server has key OR user has provided their own key
|
||||
const hasServerKey = data.available;
|
||||
const hasClientKey = openaiApiKey && openaiApiKey.trim().length > 0;
|
||||
const available = hasServerKey || hasClientKey;
|
||||
setIsAvailable(available);
|
||||
return available;
|
||||
}
|
||||
setIsAvailable(false);
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error('[useServerTTS] Failed to check availability:', err);
|
||||
if (!enabled) {
|
||||
setIsAvailable(false);
|
||||
return false;
|
||||
}
|
||||
}, [openaiApiKey]);
|
||||
|
||||
const hasClientKey = Boolean(openaiApiKey && openaiApiKey.trim().length > 0);
|
||||
if (hasClientKey) {
|
||||
setIsAvailable(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const hasServerKey = await getServerTTSStatus();
|
||||
setIsAvailable(hasServerKey);
|
||||
return hasServerKey;
|
||||
} catch {
|
||||
setIsAvailable(false);
|
||||
return false;
|
||||
}
|
||||
}, [enabled, openaiApiKey]);
|
||||
|
||||
// Check availability on mount and when API key changes
|
||||
useEffect(() => {
|
||||
checkAvailability();
|
||||
void checkAvailability();
|
||||
}, [checkAvailability]);
|
||||
|
||||
// Stop current playback
|
||||
|
||||
Reference in New Issue
Block a user