Skip to content
Merged
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
163 changes: 163 additions & 0 deletions apps/web/src/app/api/pipeline/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { NextResponse } from 'next/server';
import { publishEvent, EventTypes } from '@/lib/cloudevents';
import { analyzeVideoWithGemini } from '@/lib/gemini-video-analyzer';
import { hasGeminiKey } from '@/lib/gemini-client';

const rawBackendUrl = process.env.BACKEND_URL || '';
const BACKEND_URL = rawBackendUrl.startsWith('http') ? rawBackendUrl : 'http://localhost:8000';
const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http');

/**
* POST /api/pipeline
*
* End-to-end pipeline: YouTube URL → Video Analysis → Code Generation → Deployment → Live URL
*
* This is the FULL pipeline that the user's notes describe (PK=999, PK=1021):
* Ingest → Translate → Transport → Execute
*
* Strategies:
* 1. Backend pipeline (FastAPI /api/v1/video-to-software) — full pipeline with agents
* 2. Gemini analysis + frontend deployment — when no backend is available
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The comment above says Strategy 2 is "Gemini analysis + frontend deployment" when the backend is unavailable, but the implementation only performs Gemini analysis and explicitly returns live_url: null with code_generation: null and deployment: null. Please update the comment to match the actual behavior (analysis-only) to avoid misleading future maintainers.

Suggested change
* 2. Gemini analysis + frontend deployment when no backend is available
* 2. Gemini analysis only (video intelligence, no deployment) when no backend is available

Copilot uses AI. Check for mistakes.
*/
export async function POST(request: Request) {
let videoUrl: string | undefined;
try {
const body = await request.json();
const { url, project_type = 'web', deployment_target = 'vercel', features } = body;
videoUrl = url;

if (!url) {
return NextResponse.json({ error: 'Video URL is required' }, { status: 400 });
}

await publishEvent(EventTypes.VIDEO_RECEIVED, { url, pipeline: 'end-to-end' }, url);

// ── Strategy 1: Full backend pipeline (FastAPI video-to-software) ──
if (BACKEND_AVAILABLE) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 300_000); // 5 min for full pipeline
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The timeout duration 300_000 is a magic number. It's better to express it as a calculation to improve readability and maintainability. For even better practice, consider defining it as a named constant at the top of the file (e.g., const PIPELINE_TIMEOUT_MS = 5 * 60 * 1000;).

        const timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000); // 5 min for full pipeline


let response: Response;
try {
response = await fetch(`${BACKEND_URL}/api/v1/video-to-software`, {
Comment on lines +38 to +43
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This route waits synchronously for the full backend video-to-software pipeline and sets a 5-minute fetch timeout. On Vercel (and similar serverless deployments), the function itself typically has a shorter execution limit (and apps/web/vercel.json doesn’t configure a longer maxDuration), so the request may time out before the backend finishes. Consider making this endpoint asynchronous (enqueue + return 202 + poll), or explicitly configuring/validating the serverless timeout budget and failing fast with a clear message when it’s insufficient.

Copilot uses AI. Check for mistakes.
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
video_url: url,
project_type,
deployment_target,
features: features || ['responsive_design', 'modern_ui'],
}),
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}

if (response.ok) {
const result = await response.json();

await publishEvent(EventTypes.PIPELINE_COMPLETED, {
strategy: 'backend-pipeline',
success: result.status === 'success',
live_url: result.live_url,
github_repo: result.github_repo,
build_status: result.build_status,
}, url);

return NextResponse.json({
id: `pipeline_${Date.now().toString(36)}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Using Date.now().toString(36) does not guarantee a unique ID, as multiple requests could arrive in the same millisecond. This could lead to subtle bugs with data tracking or collisions. It's more robust to use a cryptographically strong random ID generator like crypto.randomUUID(). This same issue is present on line 105.

            id: `pipeline_${crypto.randomUUID()}`,

status: result.status || 'complete',
pipeline: 'backend',
processing_time: result.processing_time,
result: {
live_url: result.live_url,
github_repo: result.github_repo,
build_status: result.build_status,
video_analysis: result.video_analysis,
code_generation: result.code_generation,
deployment: result.deployment,
features_implemented: result.features_implemented,
},
});
}
console.warn(`Backend pipeline returned ${response.status}, falling back`);
} catch (e) {
console.log('Backend pipeline unavailable:', e);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For consistency in logging, it's better to use console.error here to log the failure of the backend pipeline. This aligns with how other errors are logged in this file (e.g., lines 127 and 136) and helps in filtering and prioritizing logs in a production environment.

        console.error('Backend pipeline unavailable:', e);

}
}

// ── Strategy 2: Gemini analysis (video intelligence only, no deployment) ──
if (hasGeminiKey()) {
try {
const startTime = Date.now();
const analysis = await analyzeVideoWithGemini(url);
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The url parameter is passed directly to analyzeVideoWithGemini, which uses it to construct a system prompt for the LLM. This is a potential Prompt Injection vulnerability. An attacker could provide a crafted URL (e.g., containing special characters or instructions) to manipulate the LLM's behavior or output. It is recommended to validate that the url is a legitimate YouTube URL before processing it.

const elapsed = Date.now() - startTime;

await publishEvent(EventTypes.PIPELINE_COMPLETED, {
strategy: 'gemini-analysis-only',
success: true,
note: 'Backend unavailable — analysis only, no deployment',
}, url);

return NextResponse.json({
id: `pipeline_${Date.now().toString(36)}`,
status: 'partial',
pipeline: 'gemini-only',
processing_time: `${(elapsed / 1000).toFixed(1)}s`,
result: {
live_url: null,
github_repo: null,
build_status: 'not_attempted',
video_analysis: {
title: analysis.title,
summary: analysis.summary,
events: analysis.events,
actions: analysis.actions,
topics: analysis.topics,
architectureCode: analysis.architectureCode,
},
code_generation: null,
deployment: null,
message: 'Backend pipeline unavailable. Video analysis complete but code generation and deployment require the Python backend.',
},
});
} catch (e) {
console.error('Gemini analysis failed:', e);
}
}

return NextResponse.json(
{ error: 'No pipeline available. Configure BACKEND_URL for full pipeline or GEMINI_API_KEY for analysis only.' },
{ status: 503 },
);
} catch (error) {
console.error('Pipeline error:', error);
await publishEvent(EventTypes.PIPELINE_FAILED, { error: String(error) }, videoUrl).catch(() => {});
return NextResponse.json(
{ error: 'Pipeline failed', details: String(error) },
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

Returning detailed error messages (details: String(error)) to the client can expose sensitive internal information such as stack traces, environment details, or internal logic. This information can be leveraged by an attacker to further compromise the system. It is safer to log the error internally and return a generic error message to the user.

return NextResponse.json({ error: 'Pipeline failed' }, { status: 500 });

{ status: 500 },
);
}
}

export async function GET() {
return NextResponse.json({
name: 'EventRelay End-to-End Pipeline',
version: '1.0.0',
description: 'YouTube URL → Video Analysis → Code Generation → Deployment → Live URL',
pipeline_stages: [
'1. Ingest: Gemini analyzes video content with Google Search grounding',
'2. Translate: Structured output → VideoPack artifact',
'3. Transport: CloudEvents published at each stage',
'4. Execute: Agents generate code, create repo, deploy to Vercel',
],
backend_available: BACKEND_AVAILABLE,
gemini_available: hasGeminiKey(),
endpoints: {
pipeline: 'POST /api/pipeline - Full end-to-end pipeline',
video: 'POST /api/video - Video analysis only',
},
});
}
3 changes: 3 additions & 0 deletions apps/web/src/app/api/video/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export async function POST(request: Request) {
await publishEvent(EventTypes.VIDEO_RECEIVED, { url }, url);

// ── Strategy 1: Full backend pipeline (skip if no backend configured) ──
// Calls /api/v1/transcript-action for analysis. For full end-to-end
// pipeline (analysis → code gen → deploy), use POST /api/pipeline instead.
if (BACKEND_AVAILABLE) {
try {
const controller = new AbortController();
Expand Down Expand Up @@ -273,6 +275,7 @@ export async function GET() {
frontend_pipeline: 'active',
endpoints: {
analyze: 'POST /api/video - Analyze a video URL',
pipeline: 'POST /api/pipeline - Full end-to-end pipeline (YouTube URL → deployed software)',
},
});
}
78 changes: 55 additions & 23 deletions apps/web/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,7 @@ import TranscriptViewer from '@/components/TranscriptViewer';
import EventList from '@/components/EventList';
import type { ExtractedEvent } from '@/lib/types';
import { useDashboardStore } from '@/store/dashboard-store';

// ============================================
// Types
// ============================================
interface Video {
id: string;
title: string;
url: string;
status: 'processing' | 'complete' | 'failed';
progress: number;
thumbnail?: string;
duration?: string;
processedAt?: string;
transcript?: string;
events?: ExtractedEvent[];
insights?: {
summary: string;
actions: string[];
sentiment: string;
topics: string[];
};
}
import type { PipelineResult, Video } from '@/store/dashboard-store';
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

PipelineResult is imported here but never used in this file. If linting is enabled during build (as is typical with Next.js), this can fail CI or add noise; consider removing the unused type import and only importing Video.

Suggested change
import type { PipelineResult, Video } from '@/store/dashboard-store';
import type { Video } from '@/store/dashboard-store';

Copilot uses AI. Check for mistakes.

// ============================================
// Processing Stage Indicator
Expand Down Expand Up @@ -75,7 +54,9 @@ function VideoCard({
video: Video;
onClick: () => void;
}) {
const stages = ['Ingest', 'Transcribe', 'Analyze', 'Extract'];
const stages = video.pipelineResult !== undefined
? ['Ingest', 'Generate', 'Deploy', 'Live']
: ['Ingest', 'Transcribe', 'Analyze', 'Extract'];
const currentStage = Math.floor((video.progress / 100) * stages.length);

return (
Expand Down Expand Up @@ -173,6 +154,38 @@ function VideoCard({
{/* Quick insights for completed */}
{video.status === 'complete' && video.insights && (
<div className="mt-4 pt-4 border-t border-white/[0.05]">
{/* Pipeline deployment result */}
{video.pipelineResult && (
<div className="mb-3 space-y-2">
{video.pipelineResult.live_url && (
<a
href={video.pipelineResult.live_url}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The live_url from the pipeline result is rendered directly into the href attribute of an anchor tag. If the URL is not validated to ensure it uses a safe protocol (e.g., https:), an attacker could inject a javascript: URL, leading to Cross-Site Scripting (XSS) when the link is clicked. Ensure the URL is validated before rendering.

target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 bg-green-500/10 border border-green-500/20 rounded-lg text-sm text-green-400 hover:bg-green-500/20 transition-colors"
>
🌐 <span className="font-medium">Live:</span> <span className="truncate">{video.pipelineResult.live_url}</span>
</a>
)}
{video.pipelineResult.github_repo && (
<a
href={video.pipelineResult.github_repo}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The github_repo URL is rendered directly into the href attribute. Similar to the live_url, this should be validated to prevent javascript: protocol injection and potential XSS attacks.

target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 bg-purple-500/10 border border-purple-500/20 rounded-lg text-sm text-purple-400 hover:bg-purple-500/20 transition-colors"
>
📦 <span className="font-medium">Repo:</span> <span className="truncate">{video.pipelineResult.github_repo}</span>
</a>
)}
{video.pipelineResult.code_generation && (
<div className="flex items-center gap-2 text-xs text-white/40">
<span>Framework: {video.pipelineResult.code_generation.framework}</span>
<span>•</span>
<span>{video.pipelineResult.code_generation.files_created.length} files</span>
</div>
)}
</div>
)}
<div className="flex flex-wrap gap-2">
{video.insights.topics.slice(0, 3).map((topic) => (
<span
Expand Down Expand Up @@ -507,6 +520,7 @@ function DashboardContent() {
const selectedVideoId = useDashboardStore((s) => s.selectedVideoId);
const selectVideo = useDashboardStore((s) => s.selectVideo);
const processVideo = useDashboardStore((s) => s.processVideo);
const deployPipeline = useDashboardStore((s) => s.deployPipeline);
const extractEvents = useDashboardStore((s) => s.extractEvents);

const selectedVideo = videos.find((v) => v.id === selectedVideoId) || null;
Expand Down Expand Up @@ -537,6 +551,16 @@ function DashboardContent() {
[videoUrl, processVideo],
);

const handleDeployPipeline = useCallback(
() => {
const targetUrl = videoUrl;
if (!targetUrl.trim()) return;
setVideoUrl('');
deployPipeline(targetUrl);
},
[videoUrl, deployPipeline],
);

return (
<div className="min-h-screen text-white">
{/* Navigation */}
Expand Down Expand Up @@ -607,6 +631,14 @@ function DashboardContent() {
>
Analyze
</button>
<button
type="button"
disabled={!videoUrl.trim()}
onClick={handleDeployPipeline}
className="btn py-3 px-6 bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-400 hover:to-emerald-500 text-white font-semibold rounded-xl shadow-lg shadow-green-500/25 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
🚀 Deploy
</button>
</div>
</form>
</div>
Expand Down
Loading
Loading