1+ import React , { useState } from 'react' ;
2+ import axios from 'axios' ;
3+ import { FileText , Loader2 , Copy , Download } from 'lucide-react' ;
4+ import { TranscriptResponse } from '../types' ;
5+
6+ interface TranscriptSummaryProps {
7+ transcript : TranscriptResponse ;
8+ apiKey ?: string ;
9+ }
10+
11+ const TranscriptSummary : React . FC < TranscriptSummaryProps > = ( { transcript, apiKey } ) => {
12+ const [ isGenerating , setIsGenerating ] = useState ( false ) ;
13+ const [ summary , setSummary ] = useState < string | null > ( null ) ;
14+ const [ error , setError ] = useState < string | null > ( null ) ;
15+ const [ copied , setCopied ] = useState ( false ) ;
16+
17+ const openAiApiKey = import . meta. env . VITE_OPENAI_API_KEY ;
18+
19+ const generateSummary = async ( ) => {
20+ if ( ! openAiApiKey ) {
21+ setError ( 'OpenAI API key is required for summary generation' ) ;
22+ return ;
23+ }
24+
25+ setIsGenerating ( true ) ;
26+ setError ( null ) ;
27+
28+ try {
29+ const transcriptText = formatTranscriptForAI ( ) ;
30+
31+ const response = await axios . post (
32+ 'https://api.openai.com/v1/chat/completions' ,
33+ {
34+ model : 'gpt-3.5-turbo' ,
35+ messages : [
36+ {
37+ role : 'system' ,
38+ content : 'You are a helpful assistant that summarizes conversations. Create a concise summary that captures the main points, decisions, and action items from the transcript.'
39+ } ,
40+ {
41+ role : 'user' ,
42+ content : `Please summarize this transcript: ${ transcriptText } `
43+ }
44+ ] ,
45+ max_tokens : 500 ,
46+ temperature : 0.7
47+ } ,
48+ {
49+ headers : {
50+ 'Content-Type' : 'application/json' ,
51+ 'Authorization' : `Bearer ${ openAiApiKey } `
52+ }
53+ }
54+ ) ;
55+
56+ const generatedSummary = response . data . choices [ 0 ] . message . content . trim ( ) ;
57+ setSummary ( generatedSummary ) ;
58+ } catch ( err : any ) {
59+ console . error ( 'Error generating summary:' , err ) ;
60+ setError ( `Failed to generate summary: ${ err . message || 'Unknown error' } ` ) ;
61+ } finally {
62+ setIsGenerating ( false ) ;
63+ }
64+ } ;
65+
66+ const formatTranscriptForAI = ( ) => {
67+ let result = '' ;
68+ let currentSpeaker = '' ;
69+ let currentText = '' ;
70+
71+ transcript . words . forEach ( ( word , index ) => {
72+ if ( word . type === 'word' ) {
73+ if ( word . speaker_id !== currentSpeaker ) {
74+ // New speaker, add previous speaker's text
75+ if ( currentSpeaker ) {
76+ result += `${ currentSpeaker . replace ( 'speaker_' , 'Speaker ' ) } : ${ currentText . trim ( ) } \n` ;
77+ }
78+ currentSpeaker = word . speaker_id ;
79+ currentText = word . text ;
80+ } else {
81+ currentText += ' ' + word . text ;
82+ }
83+ }
84+
85+ // Handle last word
86+ if ( index === transcript . words . length - 1 && currentSpeaker ) {
87+ result += `${ currentSpeaker . replace ( 'speaker_' , 'Speaker ' ) } : ${ currentText . trim ( ) } \n` ;
88+ }
89+ } ) ;
90+
91+ return result ;
92+ } ;
93+
94+ const copyToClipboard = ( ) => {
95+ if ( summary ) {
96+ navigator . clipboard . writeText ( summary ) ;
97+ setCopied ( true ) ;
98+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
99+ }
100+ } ;
101+
102+ const downloadSummary = ( ) => {
103+ if ( ! summary ) return ;
104+
105+ const blob = new Blob ( [ summary ] , { type : 'text/plain' } ) ;
106+ const url = URL . createObjectURL ( blob ) ;
107+ const a = document . createElement ( 'a' ) ;
108+ a . href = url ;
109+ a . download = 'transcript-summary.txt' ;
110+ document . body . appendChild ( a ) ;
111+ a . click ( ) ;
112+ document . body . removeChild ( a ) ;
113+ URL . revokeObjectURL ( url ) ;
114+ } ;
115+
116+ return (
117+ < div className = "w-full max-w-4xl mx-auto bg-gray-800 p-8 rounded-xl shadow-xl border border-gray-700 transform hover:scale-[1.01] transition-all duration-300 mt-8" >
118+ < h2 className = "text-xl font-semibold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 flex items-center" >
119+ < FileText className = "mr-2 h-5 w-5" />
120+ AI Transcript Summary
121+ </ h2 >
122+
123+ { ! summary ? (
124+ < div className = "mb-4" >
125+ < p className = "text-gray-300 mb-4" >
126+ Generate an AI-powered summary of your transcript to quickly understand the key points of the conversation.
127+ </ p >
128+
129+ < button
130+ onClick = { generateSummary }
131+ disabled = { isGenerating }
132+ className = { `w-full py-3 px-4 rounded-md font-medium ${
133+ isGenerating
134+ ? 'bg-gray-700 text-gray-500 cursor-not-allowed'
135+ : 'bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-700 transform hover:translate-y-[-2px] hover:shadow-lg'
136+ } transition-all duration-300 flex items-center justify-center`}
137+ >
138+ { isGenerating ? (
139+ < >
140+ < Loader2 className = "animate-spin mr-2 h-5 w-5" />
141+ Generating Summary...
142+ </ >
143+ ) : (
144+ 'Generate AI Summary'
145+ ) }
146+ </ button >
147+ </ div >
148+ ) : (
149+ < div className = "mb-4" >
150+ < div className = "bg-gray-700/50 p-4 rounded-lg border border-gray-600 mb-4" >
151+ < div className = "prose prose-invert max-w-none" >
152+ { summary . split ( '\n' ) . map ( ( paragraph , index ) => (
153+ paragraph ? < p key = { index } className = "mb-2 text-gray-200" > { paragraph } </ p > : null
154+ ) ) }
155+ </ div >
156+ </ div >
157+
158+ < div className = "flex flex-wrap gap-3" >
159+ < button
160+ onClick = { copyToClipboard }
161+ className = "px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors flex-grow"
162+ >
163+ < Copy className = "h-4 w-4 mr-2" />
164+ { copied ? 'Copied!' : 'Copy to Clipboard' }
165+ </ button >
166+
167+ < button
168+ onClick = { downloadSummary }
169+ className = "px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors flex-grow"
170+ >
171+ < Download className = "h-4 w-4 mr-2" />
172+ Download Summary
173+ </ button >
174+
175+ < button
176+ onClick = { ( ) => setSummary ( null ) }
177+ className = "px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors"
178+ >
179+ Regenerate
180+ </ button >
181+ </ div >
182+ </ div >
183+ ) }
184+
185+ { error && (
186+ < div className = "mb-4 p-3 bg-red-900/30 text-red-400 border border-red-800 rounded-md text-sm" >
187+ { error }
188+ </ div >
189+ ) }
190+ </ div >
191+ ) ;
192+ } ;
193+
194+ export default TranscriptSummary ;
0 commit comments