Skip to content

Commit d0fc929

Browse files
authored
Merge pull request samqin123#4 from SamuelZ12/summary-timestamp
Summary timestamp
2 parents 1aee2c9 + 5bff696 commit d0fc929

File tree

8 files changed

+369
-104
lines changed

8 files changed

+369
-104
lines changed

app/api/generate-summary/route.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function formatTime(seconds: number): string {
1717

1818
export async function POST(request: Request) {
1919
try {
20-
const { transcript, videoInfo, videoId, model = 'gemini-2.5-flash' } = await request.json();
20+
const { transcript, videoInfo, videoId, model = 'gemini-2.5-flash', language = 'English' } = await request.json();
2121

2222
if (!transcript || !Array.isArray(transcript)) {
2323
return NextResponse.json(
@@ -63,6 +63,12 @@ ${fullTranscript}
6363
6464
**Output Requirements:**
6565
66+
**Language Requirement:**
67+
- Your entire output MUST be written in ${language}.
68+
- All section titles, descriptions, and content must be in ${language}.
69+
- Do not mix languages. Everything including headers like "Video Notes", "Context", "Key takeaways" etc. must be translated to ${language}.
70+
- Maintain the same markdown structure but translate all text to ${language}.
71+
6672
**【Video Notes】**
6773
6874
1. **Context**
@@ -94,7 +100,7 @@ Highlight the 1-3 most intriguing and memorable and surprising stories/anecdotes
94100
* All the above content should be presented with clean and clear Markdown sections, paying attention to title hierarchy.
95101
* Note that the transcript might include transcription errors; you should deduce the correct spellings from the context and output the correct versions
96102
* Never over-summarize!
97-
* Format all timestamps as "00:00" and hyperlink the timestamps to the corresponding video sections
103+
* Include timestamps in MM:SS or HH:MM:SS format (e.g., 05:32 or 1:45:30) for important moments
98104
* Do not add new facts; if ambiguous statements appear, maintain the original meaning and note the uncertainty.
99105
* Avoid overly long paragraphs; longer ones can be broken down into multiple logical paragraphs
100106
* Try to preserve the original tone and voice of the video content. When rewriting, make sure your writing is concise, engaging, and highly readable

app/page.tsx

Lines changed: 92 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TopicCard } from "@/components/topic-card";
66
import { RightColumnTabs, type RightColumnTabsHandle } from "@/components/right-column-tabs";
77
import { YouTubePlayer } from "@/components/youtube-player";
88
import { ModelSelector, type GeminiModel } from "@/components/model-selector";
9+
import { LanguageSelector, type Language } from "@/components/language-selector";
910
import { LoadingContext } from "@/components/loading-context";
1011
import { LoadingTips } from "@/components/loading-tips";
1112
import { Topic, TranscriptSegment, VideoInfo, Citation } from "@/lib/types";
@@ -30,11 +31,16 @@ export default function Home() {
3031
const [currentTime, setCurrentTime] = useState(0);
3132
const [transcriptHeight, setTranscriptHeight] = useState<string>("auto");
3233
const [selectedModel, setSelectedModel] = useState<GeminiModel>('gemini-2.5-flash');
34+
const [summaryLanguage, setSummaryLanguage] = useState<Language>('English');
3335
const [citationHighlight, setCitationHighlight] = useState<Citation | null>(null);
3436
const [generationStartTime, setGenerationStartTime] = useState<number | null>(null);
3537
const [processingStartTime, setProcessingStartTime] = useState<number | null>(null);
3638
const rightColumnTabsRef = useRef<RightColumnTabsHandle>(null);
3739

40+
// Play All state (lifted from YouTubePlayer)
41+
const [isPlayingAll, setIsPlayingAll] = useState(false);
42+
const [playAllIndex, setPlayAllIndex] = useState(0);
43+
3844
// Summary generation state
3945
const [summaryContent, setSummaryContent] = useState<string | null>(null);
4046
const [isGeneratingSummary, setIsGeneratingSummary] = useState<boolean>(false);
@@ -170,7 +176,8 @@ export default function Home() {
170176
transcript: fetchedTranscript,
171177
videoInfo: fetchedVideoInfo,
172178
videoId: extractedVideoId,
173-
model: selectedModel
179+
model: selectedModel,
180+
language: summaryLanguage
174181
}),
175182
signal: summaryController.signal,
176183
});
@@ -179,33 +186,11 @@ export default function Home() {
179186
setShowSummaryTab(true);
180187
setIsGeneratingSummary(true);
181188

182-
// Handle summary generation asynchronously (non-blocking)
183-
summaryPromise
184-
.then(async (response) => {
185-
clearTimeout(summaryTimeoutId);
186-
if (!response.ok) {
187-
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
188-
throw new Error(errorData.error || "Failed to generate summary");
189-
}
190-
const { summaryContent: generatedSummary } = await response.json();
191-
setSummaryContent(generatedSummary);
192-
})
193-
.catch(err => {
194-
clearTimeout(summaryTimeoutId);
195-
if (err.name === 'AbortError') {
196-
setSummaryError("Summary generation timed out. The content might be too long.");
197-
} else {
198-
setSummaryError(err instanceof Error ? err.message : "An error occurred generating the summary");
199-
}
200-
})
201-
.finally(() => {
202-
setIsGeneratingSummary(false);
203-
});
204-
205-
// Now await only the topics promise for the main UI
189+
// Wait for topics to complete first (prioritize highlight reels)
206190
const topicsRes = await topicsPromise;
207191
clearTimeout(topicsTimeoutId);
208192

193+
// Check topics response
209194
if (!topicsRes.ok) {
210195
const errorData = await topicsRes.json().catch(() => ({ error: "Unknown error" }));
211196
throw new Error(errorData.error || "Failed to generate topics");
@@ -219,6 +204,31 @@ export default function Home() {
219204
const { topics: generatedTopics } = await topicsRes.json();
220205
setTopics(generatedTopics);
221206

207+
// Handle summary asynchronously in the background
208+
summaryPromise
209+
.then(async (summaryRes) => {
210+
clearTimeout(summaryTimeoutId);
211+
212+
if (!summaryRes.ok) {
213+
const errorData = await summaryRes.json().catch(() => ({ error: "Unknown error" }));
214+
setSummaryError(errorData.error || "Failed to generate summary");
215+
} else {
216+
const { summaryContent: generatedSummary } = await summaryRes.json();
217+
setSummaryContent(generatedSummary);
218+
}
219+
})
220+
.catch((err) => {
221+
clearTimeout(summaryTimeoutId);
222+
if (err.name === 'AbortError') {
223+
setSummaryError("Summary generation timed out. The video might be too long.");
224+
} else {
225+
setSummaryError("Failed to generate summary. Please try again.");
226+
}
227+
})
228+
.finally(() => {
229+
setIsGeneratingSummary(false);
230+
});
231+
222232
} catch (err) {
223233
setError(err instanceof Error ? err.message : "An error occurred");
224234
} finally {
@@ -229,6 +239,10 @@ export default function Home() {
229239
};
230240

231241
const handleCitationClick = (citation: Citation) => {
242+
// Reset Play All mode when clicking a citation
243+
setIsPlayingAll(false);
244+
setPlayAllIndex(0);
245+
232246
setSelectedTopic(null);
233247
setCitationHighlight(citation);
234248

@@ -244,6 +258,10 @@ export default function Home() {
244258
// Prevent rapid sequential clicks and state updates
245259
if (seekToTime === seconds) return;
246260

261+
// Reset Play All mode when clicking any timestamp
262+
setIsPlayingAll(false);
263+
setPlayAllIndex(0);
264+
247265
// Handle topic selection clearing:
248266
// Clear topic if it's a new citation click from AI chat OR
249267
// if clicking outside the current highlight reel (and not within a citation)
@@ -276,6 +294,13 @@ export default function Home() {
276294
};
277295

278296
const handleTopicSelect = (topic: Topic | null) => {
297+
// Reset Play All mode when manually selecting a topic
298+
// (unless it's being called by Play All itself)
299+
if (!isPlayingAll) {
300+
setIsPlayingAll(false);
301+
setPlayAllIndex(0);
302+
}
303+
279304
// Clear citation highlight when selecting a topic
280305
setCitationHighlight(null);
281306
setSelectedTopic(topic);
@@ -294,6 +319,10 @@ export default function Home() {
294319
};
295320

296321
const handlePlayAllCitations = (citations: Citation[]) => {
322+
// Reset Play All mode when playing citations
323+
setIsPlayingAll(false);
324+
setPlayAllIndex(0);
325+
297326
// Clear existing highlights to avoid conflicts
298327
setCitationHighlight(null);
299328

@@ -337,6 +366,22 @@ export default function Home() {
337366
}
338367
};
339368

369+
const handleTogglePlayAll = () => {
370+
if (isPlayingAll) {
371+
// Stop playing all
372+
setIsPlayingAll(false);
373+
} else {
374+
// Start playing all from the beginning
375+
setIsPlayingAll(true);
376+
setPlayAllIndex(0);
377+
// Select the first topic to start playback
378+
if (topics.length > 0) {
379+
setSelectedTopic(topics[0]);
380+
setSeekToTime(topics[0].segments[0].start);
381+
setTimeout(() => setSeekToTime(undefined), 100);
382+
}
383+
}
384+
};
340385

341386
// Dynamically adjust right column height to match video container
342387
useEffect(() => {
@@ -384,13 +429,23 @@ export default function Home() {
384429

385430
<div className="flex flex-col items-center gap-4 mb-8">
386431
<UrlInput onSubmit={processVideo} isLoading={isLoading} />
387-
<div className="flex items-center gap-2">
388-
<span className="text-sm text-muted-foreground">Model:</span>
389-
<ModelSelector
390-
value={selectedModel}
391-
onChange={setSelectedModel}
392-
disabled={isLoading}
393-
/>
432+
<div className="flex items-center gap-4 flex-wrap">
433+
<div className="flex items-center gap-2">
434+
<span className="text-sm text-muted-foreground">Model:</span>
435+
<ModelSelector
436+
value={selectedModel}
437+
onChange={setSelectedModel}
438+
disabled={isLoading}
439+
/>
440+
</div>
441+
<div className="flex items-center gap-2">
442+
<span className="text-sm text-muted-foreground">Language:</span>
443+
<LanguageSelector
444+
value={summaryLanguage}
445+
onChange={setSummaryLanguage}
446+
disabled={isLoading}
447+
/>
448+
</div>
394449
</div>
395450
</div>
396451

@@ -437,6 +492,11 @@ export default function Home() {
437492
onTopicSelect={handleTopicSelect}
438493
onTimeUpdate={handleTimeUpdate}
439494
transcript={transcript}
495+
isPlayingAll={isPlayingAll}
496+
playAllIndex={playAllIndex}
497+
onTogglePlayAll={handleTogglePlayAll}
498+
setPlayAllIndex={setPlayAllIndex}
499+
setIsPlayingAll={setIsPlayingAll}
440500
/>
441501
</div>
442502
</div>

components/chat-message.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export function ChatMessageComponent({ message, onCitationClick, onTimestampClic
217217
)}
218218
</div>
219219

220-
<div className={`flex-1 max-w-[80%] ${isUser ? 'text-right' : ''}`}>
220+
<div className="flex-1 max-w-[80%]">
221221
<Card className={`p-4 ${isUser ? 'bg-primary/5 border-primary/20' : 'bg-muted/30'}`}>
222222
{isUser ? (
223223
<p className="text-sm whitespace-pre-wrap">{message.content}</p>

components/language-selector.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use client';
2+
3+
import {
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectTrigger,
8+
SelectValue,
9+
} from "@/components/ui/select";
10+
import { InfoIcon } from "lucide-react";
11+
import {
12+
Tooltip,
13+
TooltipContent,
14+
TooltipProvider,
15+
TooltipTrigger,
16+
} from "@/components/ui/tooltip";
17+
18+
export type Language =
19+
| 'English'
20+
| 'Spanish'
21+
| 'French'
22+
| 'German'
23+
| 'Italian'
24+
| 'Portuguese'
25+
| 'Dutch'
26+
| 'Russian'
27+
| 'Japanese'
28+
| 'Korean'
29+
| 'Chinese (Simplified)'
30+
| 'Chinese (Traditional)'
31+
| 'Arabic'
32+
| 'Hindi';
33+
34+
interface LanguageInfo {
35+
id: Language;
36+
name: string;
37+
nativeName: string;
38+
}
39+
40+
const languageInfo: LanguageInfo[] = [
41+
{ id: 'English', name: 'English', nativeName: 'English' },
42+
{ id: 'Spanish', name: 'Spanish', nativeName: 'Español' },
43+
{ id: 'French', name: 'French', nativeName: 'Français' },
44+
{ id: 'German', name: 'German', nativeName: 'Deutsch' },
45+
{ id: 'Italian', name: 'Italian', nativeName: 'Italiano' },
46+
{ id: 'Portuguese', name: 'Portuguese', nativeName: 'Português' },
47+
{ id: 'Dutch', name: 'Dutch', nativeName: 'Nederlands' },
48+
{ id: 'Russian', name: 'Russian', nativeName: 'Русский' },
49+
{ id: 'Japanese', name: 'Japanese', nativeName: '日本語' },
50+
{ id: 'Korean', name: 'Korean', nativeName: '한국어' },
51+
{ id: 'Chinese (Simplified)', name: 'Chinese (Simplified)', nativeName: '简体中文' },
52+
{ id: 'Chinese (Traditional)', name: 'Chinese (Traditional)', nativeName: '繁體中文' },
53+
{ id: 'Arabic', name: 'Arabic', nativeName: 'العربية' },
54+
{ id: 'Hindi', name: 'Hindi', nativeName: 'हिन्दी' },
55+
];
56+
57+
interface LanguageSelectorProps {
58+
value: Language;
59+
onChange: (value: Language) => void;
60+
disabled?: boolean;
61+
}
62+
63+
export function LanguageSelector({ value, onChange, disabled }: LanguageSelectorProps) {
64+
const selectedLanguage = languageInfo.find(l => l.id === value);
65+
66+
return (
67+
<div className="flex items-center gap-2">
68+
<Select value={value} onValueChange={onChange} disabled={disabled}>
69+
<SelectTrigger className="w-[200px]">
70+
<SelectValue placeholder="Select a language" />
71+
</SelectTrigger>
72+
<SelectContent>
73+
{languageInfo.map((lang) => (
74+
<SelectItem key={lang.id} value={lang.id}>
75+
<span className="flex items-center justify-between w-full">
76+
<span>{lang.name}</span>
77+
{lang.nativeName !== lang.name && (
78+
<span className="text-muted-foreground ml-2">{lang.nativeName}</span>
79+
)}
80+
</span>
81+
</SelectItem>
82+
))}
83+
</SelectContent>
84+
</Select>
85+
86+
<TooltipProvider>
87+
<Tooltip>
88+
<TooltipTrigger asChild>
89+
<InfoIcon className="h-4 w-4 text-muted-foreground cursor-help" />
90+
</TooltipTrigger>
91+
<TooltipContent className="max-w-xs">
92+
<div className="space-y-2">
93+
<p className="font-semibold">Summary Language</p>
94+
<p className="text-sm">
95+
The video summary will be generated in {selectedLanguage?.nativeName || value}.
96+
</p>
97+
<p className="text-sm text-muted-foreground">
98+
Note: The original transcript language detection and translation quality may vary
99+
depending on the source video's audio clarity and the target language.
100+
</p>
101+
</div>
102+
</TooltipContent>
103+
</Tooltip>
104+
</TooltipProvider>
105+
</div>
106+
);
107+
}

0 commit comments

Comments
 (0)