1- import { useQuery } from "@tanstack/react-query" ;
1+ import { useMutation , useQuery } from "@tanstack/react-query" ;
22import { createFileRoute } from "@tanstack/react-router" ;
3+ import { Play } from "lucide-react" ;
34import { useEffect , useMemo , useState } from "react" ;
45
56import NoteEditor , { type JSONContent } from "@hypr/tiptap/editor" ;
67import { EMPTY_TIPTAP_DOC } from "@hypr/tiptap/shared" ;
78import "@hypr/tiptap/styles.css" ;
9+ import { cn } from "@hypr/utils" ;
810
911import {
1012 FileInfo ,
@@ -27,16 +29,74 @@ export const Route = createFileRoute("/_view/app/file-transcription")({
2729
2830function Component ( ) {
2931 const [ file , setFile ] = useState < File | null > ( null ) ;
32+ const [ fileId , setFileId ] = useState < string | null > ( null ) ;
3033 const [ pipelineId , setPipelineId ] = useState < string | null > ( null ) ;
3134 const [ transcript , setTranscript ] = useState < string | null > ( null ) ;
32- const [ uploadError , setUploadError ] = useState < string | null > ( null ) ;
3335 const [ noteContent , setNoteContent ] = useState < JSONContent > ( EMPTY_TIPTAP_DOC ) ;
3436 const [ isMounted , setIsMounted ] = useState ( false ) ;
3537
3638 useEffect ( ( ) => {
3739 setIsMounted ( true ) ;
3840 } , [ ] ) ;
3941
42+ const uploadMutation = useMutation ( {
43+ mutationFn : async ( selectedFile : File ) => {
44+ const base64Data = await new Promise < string > ( ( resolve , reject ) => {
45+ const reader = new FileReader ( ) ;
46+ reader . onload = ( ) => {
47+ const result = reader . result ?. toString ( ) . split ( "," ) [ 1 ] ;
48+ if ( ! result ) {
49+ reject ( new Error ( "Failed to read file" ) ) ;
50+ } else {
51+ resolve ( result ) ;
52+ }
53+ } ;
54+ reader . onerror = ( ) => reject ( new Error ( "Failed to read file" ) ) ;
55+ reader . readAsDataURL ( selectedFile ) ;
56+ } ) ;
57+
58+ const uploadResult = await uploadAudioFile ( {
59+ data : {
60+ fileName : selectedFile . name ,
61+ fileType : selectedFile . type ,
62+ fileData : base64Data ,
63+ } ,
64+ } ) ;
65+
66+ if ( "error" in uploadResult && uploadResult . error ) {
67+ throw new Error ( uploadResult . message || "Failed to upload file" ) ;
68+ }
69+ if ( ! ( "fileId" in uploadResult ) ) {
70+ throw new Error ( "Failed to get file ID" ) ;
71+ }
72+
73+ return uploadResult . fileId ;
74+ } ,
75+ onSuccess : ( newFileId ) => {
76+ setFileId ( newFileId ) ;
77+ } ,
78+ } ) ;
79+
80+ const startPipelineMutation = useMutation ( {
81+ mutationFn : async ( fileIdArg : string ) => {
82+ const pipelineResult = await startAudioPipeline ( {
83+ data : { fileId : fileIdArg } ,
84+ } ) ;
85+
86+ if ( "error" in pipelineResult && pipelineResult . error ) {
87+ throw new Error ( pipelineResult . message || "Failed to start pipeline" ) ;
88+ }
89+ if ( ! ( "pipelineId" in pipelineResult ) ) {
90+ throw new Error ( "Failed to get pipeline ID" ) ;
91+ }
92+
93+ return pipelineResult . pipelineId ;
94+ } ,
95+ onSuccess : ( newPipelineId ) => {
96+ setPipelineId ( newPipelineId ) ;
97+ } ,
98+ } ) ;
99+
40100 const pipelineStatusQuery = useQuery ( {
41101 queryKey : [ "audioPipelineStatus" , pipelineId ] ,
42102 queryFn : async ( ) : Promise < StatusStateType > => {
@@ -70,8 +130,11 @@ function Component() {
70130 } , [ pipelineStatusQuery . data ] ) ;
71131
72132 const isProcessing =
73- ! ! pipelineId &&
74- ! [ "DONE" , "ERROR" ] . includes ( pipelineStatusQuery . data ?. status ?? "" ) ;
133+ ( ! ! pipelineId &&
134+ ! [ "DONE" , "ERROR" ] . includes ( pipelineStatusQuery . data ?. status ?? "" ) ) ||
135+ startPipelineMutation . isPending ;
136+
137+ const pipelineStatus = pipelineStatusQuery . data ?. status ;
75138
76139 const status = ( ( ) => {
77140 if ( pipelineStatusQuery . data ?. status === "ERROR" ) {
@@ -80,86 +143,64 @@ function Component() {
80143 if ( pipelineStatusQuery . data ?. status === "DONE" || transcript ) {
81144 return "done" as const ;
82145 }
83- if ( pipelineId ) {
146+ if ( pipelineStatus === "LLM_RUNNING" ) {
147+ return "summarizing" as const ;
148+ }
149+ if ( pipelineStatus === "TRANSCRIBED" ) {
150+ return "summarizing" as const ;
151+ }
152+ if ( pipelineStatus === "TRANSCRIBING" ) {
84153 return "transcribing" as const ;
85154 }
155+ if ( pipelineStatus === "QUEUED" || pipelineId ) {
156+ return "queued" as const ;
157+ }
158+ if ( uploadMutation . isPending ) {
159+ return "uploading" as const ;
160+ }
161+ if ( fileId ) {
162+ return "uploaded" as const ;
163+ }
86164 return "idle" as const ;
87165 } ) ( ) ;
88166
89167 const errorMessage =
90- uploadError ??
168+ ( uploadMutation . error instanceof Error
169+ ? uploadMutation . error . message
170+ : null ) ??
171+ ( startPipelineMutation . error instanceof Error
172+ ? startPipelineMutation . error . message
173+ : null ) ??
91174 ( pipelineStatusQuery . isError && pipelineStatusQuery . error instanceof Error
92175 ? pipelineStatusQuery . error . message
93176 : null ) ??
94177 ( pipelineStatusQuery . data ?. status === "ERROR"
95- ? pipelineStatusQuery . data . error
178+ ? ( pipelineStatusQuery . data . error ?? null )
96179 : null ) ;
97180
98- const handleFileSelect = async ( selectedFile : File ) => {
181+ const handleFileSelect = ( selectedFile : File ) => {
99182 setFile ( selectedFile ) ;
100- setTranscript ( null ) ;
101- setUploadError ( null ) ;
183+ setFileId ( null ) ;
102184 setPipelineId ( null ) ;
185+ setTranscript ( null ) ;
186+ uploadMutation . reset ( ) ;
187+ startPipelineMutation . reset ( ) ;
188+ uploadMutation . mutate ( selectedFile ) ;
189+ } ;
103190
104- try {
105- const reader = new FileReader ( ) ;
106- reader . readAsDataURL ( selectedFile ) ;
107-
108- reader . onload = async ( ) => {
109- const base64Data = reader . result ?. toString ( ) . split ( "," ) [ 1 ] ;
110- if ( ! base64Data ) {
111- setUploadError ( "Failed to read file" ) ;
112- return ;
113- }
114-
115- const uploadResult = await uploadAudioFile ( {
116- data : {
117- fileName : selectedFile . name ,
118- fileType : selectedFile . type ,
119- fileData : base64Data ,
120- } ,
121- } ) ;
122-
123- if ( "error" in uploadResult && uploadResult . error ) {
124- setUploadError ( uploadResult . message || "Failed to upload file" ) ;
125- return ;
126- }
127-
128- if ( ! ( "fileId" in uploadResult ) ) {
129- setUploadError ( "Failed to get file ID" ) ;
130- return ;
131- }
132-
133- const pipelineResult = await startAudioPipeline ( {
134- data : {
135- fileId : uploadResult . fileId ,
136- } ,
137- } ) ;
138-
139- if ( "error" in pipelineResult && pipelineResult . error ) {
140- setUploadError ( pipelineResult . message || "Failed to start pipeline" ) ;
141- return ;
142- }
143-
144- if ( "pipelineId" in pipelineResult ) {
145- setPipelineId ( pipelineResult . pipelineId ) ;
146- }
147- } ;
148-
149- reader . onerror = ( ) => {
150- setUploadError ( "Failed to read file" ) ;
151- } ;
152- } catch ( err ) {
153- setUploadError ( err instanceof Error ? err . message : "Unknown error" ) ;
154- }
191+ const handleStartTranscription = ( ) => {
192+ if ( ! fileId ) return ;
193+ startPipelineMutation . mutate ( fileId ) ;
155194 } ;
156195
157196 const handleRemoveFile = ( ) => {
158197 setFile ( null ) ;
159- setTranscript ( null ) ;
160- setUploadError ( null ) ;
198+ setFileId ( null ) ;
161199 setPipelineId ( null ) ;
200+ setTranscript ( null ) ;
162201 setNoteContent ( EMPTY_TIPTAP_DOC ) ;
202+ uploadMutation . reset ( ) ;
203+ startPipelineMutation . reset ( ) ;
163204 } ;
164205
165206 const mentionConfig = useMemo (
@@ -224,11 +265,32 @@ function Component() {
224265 disabled = { isProcessing }
225266 />
226267 ) : (
227- < FileInfo
228- fileName = { file . name }
229- fileSize = { file . size }
230- onRemove = { handleRemoveFile }
231- />
268+ < div className = "space-y-4" >
269+ < FileInfo
270+ fileName = { file . name }
271+ fileSize = { file . size }
272+ onRemove = { handleRemoveFile }
273+ isUploading = { uploadMutation . isPending }
274+ isProcessing = { isProcessing }
275+ />
276+ { status === "uploaded" && (
277+ < button
278+ onClick = { handleStartTranscription }
279+ className = { cn ( [
280+ "w-full flex items-center justify-center gap-2" ,
281+ "px-4 py-3 rounded-lg" ,
282+ "bg-gradient-to-t from-stone-600 to-stone-500" ,
283+ "text-white font-medium" ,
284+ "shadow-md hover:shadow-lg" ,
285+ "hover:scale-[101%] active:scale-[99%]" ,
286+ "transition-all" ,
287+ ] ) }
288+ >
289+ < Play size = { 18 } />
290+ Start Transcription
291+ </ button >
292+ ) }
293+ </ div >
232294 ) }
233295
234296 < div >
0 commit comments