open ocr/tts/transcribe results in dual panels from file viewer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 00:42:02 +00:00
co-authored by Claude Opus 4.6
parent b75f954b0f
commit 7bfa36e5cf
2 changed files with 33 additions and 11 deletions
@@ -12,6 +12,7 @@ type FileViewerProviderProps = {
root?: string;
content?: string;
onOpenFile?: (filePath: string, root: string) => void;
onReplaceView?: (viewPath: string, viewRoot: string, ephemeralPath: string, ephemeralRoot: string) => void;
autoPlay?: boolean;
children: ReactNode;
};
@@ -22,6 +23,7 @@ export const FileViewerProvider = ({
root = 'home',
content: directContent,
onOpenFile,
onReplaceView,
autoPlay = false,
children,
}: FileViewerProviderProps) => {
@@ -71,8 +73,9 @@ export const FileViewerProvider = ({
const handleReadAloud = async () => {
setTtsLoading(true);
try {
const { audioPath, audioRoot } = await files.tts(filePath);
onOpenFile?.(audioPath, audioRoot);
const { audioPath, audioRoot } = await files.tts(filePath, { saveNextTo: true });
setRefreshSignal((n) => n + 1);
onReplaceView?.(filePath, root, audioPath, audioRoot);
} catch {
toast.error('Failed to generate speech audio');
} finally {
@@ -83,8 +86,9 @@ export const FileViewerProvider = ({
const handleOcr = async () => {
setOcrLoading(true);
try {
const { ocrPath, ocrRoot } = await files.ocr(filePath);
onOpenFile?.(ocrPath, ocrRoot);
const { ocrPath, ocrRoot } = await files.ocr(filePath, { saveNextTo: true });
setRefreshSignal((n) => n + 1);
onReplaceView?.(filePath, root, ocrPath, ocrRoot);
} catch {
toast.error('Failed to extract text from image');
} finally {
@@ -95,8 +99,9 @@ export const FileViewerProvider = ({
const handleTranscribe = async () => {
setTranscribeLoading(true);
try {
const { transcriptionPath, transcriptionRoot } = await files.transcribe(filePath);
onOpenFile?.(transcriptionPath, transcriptionRoot);
const { transcriptionPath, transcriptionRoot } = await files.transcribe(filePath, { saveNextTo: true });
setRefreshSignal((n) => n + 1);
onReplaceView?.(filePath, root, transcriptionPath, transcriptionRoot);
} catch {
toast.error('Failed to transcribe audio');
} finally {
@@ -1,9 +1,26 @@
import { type ReactNode, useCallback } from 'react';
import type { ReactNode } from 'react';
import { useCallback } from 'react';
import { useSearchParams } from 'react-router';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { FileViewerProvider } from '../../apps/FileViewer';
import { EmbeddableChat } from '../../apps/Chat/EmbeddableChat';
type SetSearchParams = ReturnType<typeof useSearchParams>[1];
const onReplaceView = (setSearchParams: SetSearchParams) =>
(viewPath: string, viewRoot: string, ephemeralPath: string, ephemeralRoot: string) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set('view', viewPath);
next.set('ephemeral', ephemeralPath);
next.set('ephemeralRoot', ephemeralRoot);
next.delete('ephemeral2');
next.delete('ephemeral2Root');
next.delete('ephemeral2Auto');
return next;
});
};
export function ViewerProvider({ children }: { children: ReactNode }) {
const [searchParams, setSearchParams] = useSearchParams();
const viewPath = searchParams.get('view') ?? '';
@@ -19,7 +36,7 @@ export function ViewerProvider({ children }: { children: ReactNode }) {
};
return (
<FileViewerProvider filePath={viewPath} fileName={fileName} onOpenFile={onOpenFile}>
<FileViewerProvider filePath={viewPath} fileName={fileName} onOpenFile={onOpenFile} onReplaceView={onReplaceView(setSearchParams)}>
{children}
</FileViewerProvider>
);
@@ -42,21 +59,21 @@ export function EphemeralProvider({ children }: { children: ReactNode }) {
};
return (
<FileViewerProvider filePath={ephemeralPath} fileName={fileName} root={ephemeralRoot} onOpenFile={onOpenFile}>
<FileViewerProvider filePath={ephemeralPath} fileName={fileName} root={ephemeralRoot} onOpenFile={onOpenFile} onReplaceView={onReplaceView(setSearchParams)}>
{children}
</FileViewerProvider>
);
}
export function Ephemeral2Provider({ children }: { children: ReactNode }) {
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const ephemeral2Path = searchParams.get('ephemeral2') ?? '';
const ephemeral2Root = searchParams.get('ephemeral2Root') ?? 'home';
const ephemeral2Auto = searchParams.get('ephemeral2Auto') === '1';
const fileName = ephemeral2Path.split('/').pop() ?? '';
return (
<FileViewerProvider filePath={ephemeral2Path} fileName={fileName} root={ephemeral2Root} autoPlay={ephemeral2Auto}>
<FileViewerProvider filePath={ephemeral2Path} fileName={fileName} root={ephemeral2Root} autoPlay={ephemeral2Auto} onReplaceView={onReplaceView(setSearchParams)}>
{children}
</FileViewerProvider>
);