Skip to content

Commit 83e4b52

Browse files
authored
Merge pull request #4 from intellwe/feat/export
FEAT: implement Export transcripts option along with copy transcripts
2 parents 47c4286 + b41d1d4 commit 83e4b52

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

src/components/ExportOptions.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import React, { useState } from 'react';
2+
import { Download, Copy, FileText, FileJson, Share2 } from 'lucide-react';
3+
import { TranscriptResponse } from '../types';
4+
5+
interface ExportOptionsProps {
6+
transcript: TranscriptResponse;
7+
}
8+
9+
const ExportOptions: React.FC<ExportOptionsProps> = ({ transcript }) => {
10+
const [copied, setCopied] = useState(false);
11+
const [exportFormat, setExportFormat] = useState<'txt' | 'docx' | 'json'>('txt');
12+
13+
const formatTranscriptAsText = () => {
14+
let result = '';
15+
let currentSpeaker = '';
16+
let currentText = '';
17+
18+
transcript.words.forEach((word, index) => {
19+
if (word.type === 'word') {
20+
if (currentSpeaker === '') {
21+
// First word
22+
currentSpeaker = word.speaker_id;
23+
currentText = word.text;
24+
} else if (word.speaker_id !== currentSpeaker) {
25+
// Speaker changed
26+
result += `${currentSpeaker.replace('speaker_', 'Speaker ')}: ${currentText.trim()}\n\n`;
27+
currentSpeaker = word.speaker_id;
28+
currentText = word.text;
29+
} else {
30+
// Same speaker continues
31+
currentText += word.text;
32+
}
33+
} else if (word.type === 'spacing') {
34+
currentText += ' ';
35+
}
36+
37+
// Handle last word
38+
if (index === transcript.words.length - 1 && currentSpeaker) {
39+
result += `${currentSpeaker.replace('speaker_', 'Speaker ')}: ${currentText.trim()}\n`;
40+
}
41+
});
42+
43+
return result;
44+
};
45+
46+
const copyToClipboard = () => {
47+
const text = formatTranscriptAsText();
48+
navigator.clipboard.writeText(text);
49+
setCopied(true);
50+
setTimeout(() => setCopied(false), 2000);
51+
};
52+
53+
const downloadTranscript = () => {
54+
let content: string;
55+
let mimeType: string;
56+
let fileExtension: string;
57+
58+
if (exportFormat === 'json') {
59+
content = JSON.stringify(transcript, null, 2);
60+
mimeType = 'application/json';
61+
fileExtension = 'json';
62+
} else {
63+
// Default to txt
64+
content = formatTranscriptAsText();
65+
mimeType = 'text/plain';
66+
fileExtension = 'txt';
67+
}
68+
69+
const blob = new Blob([content], { type: mimeType });
70+
const url = URL.createObjectURL(blob);
71+
const a = document.createElement('a');
72+
a.href = url;
73+
a.download = `transcript.${fileExtension}`;
74+
document.body.appendChild(a);
75+
a.click();
76+
document.body.removeChild(a);
77+
URL.revokeObjectURL(url);
78+
};
79+
80+
const shareTranscript = async () => {
81+
if (navigator.share) {
82+
try {
83+
await navigator.share({
84+
title: 'Audio Transcript',
85+
text: formatTranscriptAsText(),
86+
});
87+
} catch (err) {
88+
console.error('Error sharing:', err);
89+
}
90+
} else {
91+
copyToClipboard();
92+
}
93+
};
94+
95+
return (
96+
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700 mb-8">
97+
<h3 className="text-lg font-medium text-gray-200 mb-3">Export Options</h3>
98+
99+
<div className="flex flex-wrap gap-3 mb-4">
100+
<button
101+
onClick={() => setExportFormat('txt')}
102+
className={`px-3 py-2 rounded-md flex items-center text-sm ${
103+
exportFormat === 'txt'
104+
? 'bg-blue-600 text-white'
105+
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
106+
} transition-colors`}
107+
>
108+
<FileText className="h-4 w-4 mr-2" />
109+
Text
110+
</button>
111+
112+
<button
113+
onClick={() => setExportFormat('json')}
114+
className={`px-3 py-2 rounded-md flex items-center text-sm ${
115+
exportFormat === 'json'
116+
? 'bg-blue-600 text-white'
117+
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
118+
} transition-colors`}
119+
>
120+
<FileJson className="h-4 w-4 mr-2" />
121+
JSON
122+
</button>
123+
</div>
124+
125+
<div className="flex flex-wrap gap-3">
126+
<button
127+
onClick={downloadTranscript}
128+
className="px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-500 rounded-md text-white flex items-center hover:from-green-700 hover:to-emerald-600 transition-colors"
129+
>
130+
<Download className="h-4 w-4 mr-2" />
131+
Download
132+
</button>
133+
134+
<button
135+
onClick={copyToClipboard}
136+
className={`px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors ${
137+
copied ? 'bg-green-600 hover:bg-green-700' : ''
138+
}`}
139+
>
140+
<Copy className="h-4 w-4 mr-2" />
141+
{copied ? 'Copied!' : 'Copy'}
142+
</button>
143+
144+
<button
145+
onClick={shareTranscript}
146+
className="px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors"
147+
>
148+
<Share2 className="h-4 w-4 mr-2" />
149+
Share
150+
</button>
151+
</div>
152+
</div>
153+
);
154+
};
155+
156+
export default ExportOptions;

src/components/TranscriptDisplay.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useMemo, useEffect, useRef } from 'react';
22
import { TranscriptResponse } from '../types';
33
import { User, Clock } from 'lucide-react';
4+
import ExportOptions from './ExportOptions';
45

56
interface TranscriptDisplayProps {
67
transcript: TranscriptResponse | null;
@@ -128,6 +129,8 @@ const TranscriptDisplay: React.FC<TranscriptDisplayProps> = ({ transcript }) =>
128129
</div>
129130
</div>
130131

132+
{transcript && <ExportOptions transcript={transcript} />}
133+
131134
<div className="space-y-4">
132135
{groupedBySpeaker.map((segment, index) => (
133136
<div key={index} className="flex transcript-segment opacity-0">

0 commit comments

Comments
 (0)