Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions app/analyze/[videoId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,42 @@ export default function AnalyzePage() {
</section>
)}

{videoId && topics.length === 0 && pageState === 'IDLE' && !error && (
<section className="flex min-h-[calc(100vh-11rem)] flex-col items-center justify-center px-5 text-center">
<Card className="w-full max-w-2xl border border-slate-200 bg-white/90 p-9 backdrop-blur-sm">
<div className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-slate-900">
No highlights found
</h2>
<p className="mt-1.5 text-sm leading-relaxed text-slate-600">
We processed the transcript but couldn't find any standout highlights matching your criteria. This sometimes happens if the video is very short, has no dialogue, or if the AI filters were too strict.
</p>
</div>
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
<Link
href="/"
className="inline-flex items-center justify-center rounded-full border border-slate-200 px-4 py-2 text-xs font-medium text-slate-700 transition hover:bg-[#f8fafc]"
>
Go to home
</Link>
<button
type="button"
onClick={() => {
// Force regenerate logic
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment 'Force regenerate logic' is misleading. This doesn't force regeneration - it simply calls processVideo which may use cached results if available. The comment should either be removed or clarified to reflect the actual behavior (e.g., 'Retry analysis').

Suggested change
// Force regenerate logic
// Retry analysis (may use cached results)

Copilot uses AI. Check for mistakes.
processVideo(normalizedUrl, mode);
Comment on lines +1931 to +1935

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Force a cache bypass for empty-state retry

The empty-state “Try again” button just calls processVideo(normalizedUrl, mode) without setting the regen/force-regenerate path, so if the current video analysis is already cached with an empty topics array (which is now what triggers this state), the retry will immediately short‑circuit to the same cached empty result and the user can never re-run analysis. This is most visible for older cached analyses that produced zero topics; the UI now suggests a retry that cannot actually regenerate.

Useful? React with 👍 / 👎.

}}
className="inline-flex items-center justify-center rounded-full bg-slate-900 px-4 py-2 text-xs font-medium text-white transition hover:bg-slate-800 disabled:pointer-events-none disabled:opacity-50"
disabled={isModeLoading}
>
Try again
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry button doesn't force regeneration for cached empty results

High Severity

The "Try again" button in the new empty state UI calls processVideo(normalizedUrl, mode) with a comment saying // Force regenerate logic, but it doesn't actually force regeneration. The forceRegenerate flag is derived from URL params at component mount and isn't modified by this button. When a video analysis returns zero topics (the exact scenario this UI handles), the empty result is cached. Clicking "Try again" will load the same cached empty result instead of re-running the analysis, making the retry functionality ineffective.

Fix in Cursor Fix in Web

</div>
</div>
</Card>
</section>
)}

{videoId && topics.length > 0 && pageState === 'IDLE' && (
<div className="mx-auto w-full max-w-7xl px-5 pb-5 pt-0">
{error && (
Expand Down
22 changes: 7 additions & 15 deletions lib/ai-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { generateAIResponse } from '@/lib/ai-client';
import { getProviderKey } from '@/lib/ai-providers';
import { topicGenerationSchema } from '@/lib/schemas';
import { parseTimestampRange } from '@/lib/timestamp-utils';
import { parseTimestampRange, formatTimestamp } from '@/lib/timestamp-utils';
import { getLanguageName } from '@/lib/language-utils';
import { repairJson } from '@/lib/json-utils';
import { z } from 'zod';
Expand Down Expand Up @@ -210,7 +210,7 @@ function buildChunkPrompt(
language?: string
): string {
const transcript = formatTranscriptWithTimestamps(chunk.segments);
const chunkWindow = `[${formatTime(chunk.start)}-${formatTime(chunk.end)}]`;
const chunkWindow = `[${formatTimestamp(chunk.start)}-${formatTimestamp(chunk.end)}]`;
const videoInfoBlock = formatVideoInfoForPrompt(videoInfo);
const themeInstruction = theme
? ` <item>Focus exclusively on material that clearly expresses the theme "${theme}". Skip anything unrelated.</item>\n`
Expand Down Expand Up @@ -283,7 +283,7 @@ function buildReducePrompt(
.map((candidate, idx) => {
const timestamp = candidate.quote?.timestamp ?? '[??:??-??:??]';
const quoteText = candidate.quote?.text ?? '';
const chunkWindow = `[${formatTime(candidate.chunkStart)}-${formatTime(
const chunkWindow = `[${formatTimestamp(candidate.chunkStart)}-${formatTimestamp(
candidate.chunkEnd
)}]`;
return `Candidate ${idx + 1}
Expand Down Expand Up @@ -464,7 +464,7 @@ function buildFallbackTopics(
fallbackTopics.push({
title: theme ? `${theme} — part ${i + 1}` : `Part ${i + 1}`,
quote: {
timestamp: `[${formatTime(startTime)}-${formatTime(endTime)}]`,
timestamp: `[${formatTimestamp(startTime)}-${formatTimestamp(endTime)}]`,
text:
chunkSegments
.map((s) => s.text)
Expand Down Expand Up @@ -602,7 +602,7 @@ ${transcriptWithTimestamps}
{
title: fallbackLabel,
quote: {
timestamp: `[00:00-${formatTime(fallbackEnd)}]`,
timestamp: `[00:00-${formatTimestamp(fallbackEnd)}]`,
text: fullText.substring(0, 200)
}
}
Expand All @@ -629,21 +629,13 @@ function combineTranscript(segments: TranscriptSegment[]): string {
function formatTranscriptWithTimestamps(segments: TranscriptSegment[]): string {
return segments
.map((s) => {
const startTime = formatTime(s.start);
const endTime = formatTime(s.start + s.duration);
const startTime = formatTimestamp(s.start);
const endTime = formatTimestamp(s.start + s.duration);
return `[${startTime}-${endTime}] ${s.text}`;
})
.join('\n');
}

function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs
.toString()
.padStart(2, '0')}`;
}

async function findExactQuotes(
transcript: TranscriptSegment[],
quotes: Array<{ timestamp: string; text: string }>,
Expand Down
5 changes: 4 additions & 1 deletion lib/timestamp-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export function parseTimestamp(timestamp: string): number | null {

// Validate time values
if (hours < 0 || hours >= 24) return null;
if (minutes < 0 || minutes >= 60) return null;
if (minutes < 0) return null;
// Relax minute check to allow MM:SS where MM >= 60 (fallback scenarios)
// unless hours are present, in which case strict 0-59 applies
Comment on lines +31 to +32
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says 'Relax minute check to allow MM:SS where MM >= 60' but this logic only validates that when hours > 0, minutes must be < 60. The relaxation for MM:SS format when hours are absent is implicit (no check exists), which could be confusing. Consider adding an explicit comment or restructuring to make the two cases clearer. For example: // When hours are present, minutes must be 0-59 strictly before line 33, and a comment before line 30 clarifying that without hours, minutes can be >= 60 for MM:SS format timestamps.

Suggested change
// Relax minute check to allow MM:SS where MM >= 60 (fallback scenarios)
// unless hours are present, in which case strict 0-59 applies
// When no hours are present (MM:SS), minutes are allowed to be >= 60 (fallback scenarios)
// When hours are present (HH:MM:SS), minutes must be in the strict range 0-59

Copilot uses AI. Check for mistakes.
if (hours > 0 && minutes >= 60) return null;
if (seconds < 0 || seconds >= 60) return null;

return hours * 3600 + minutes * 60 + seconds;
Expand Down
Loading