diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index 344a7d6..fcabed9 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -13,6 +13,7 @@ import { FileManifest } from '@/types/file-manifest'; import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation'; import { appConfig } from '@/config/app.config'; import { getNetworkPrompt, type BlockchainNetwork } from '@/lib/prompts'; +import { getDb } from '@/lib/mongo'; // Force dynamic route to enable streaming export const dynamic = 'force-dynamic'; @@ -115,8 +116,27 @@ export async function POST(request: NextRequest) { // const { prompt, model = 'openai/gpt-oss-20b', context, isEdit = false } = await request.json(); // AFTER: Extract chain parameter from request - const { prompt, model = 'openai/gpt-oss-20b', context, isEdit = false, chain } = await request.json(); + const { prompt, model = 'openai/gpt-oss-20b', context, isEdit = false, chain, generationId } = await request.json(); + const finalGenerationId = generationId || `gen-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + try { + const db = await getDb(); + await db.collection('telemetry').insertOne({ + generationId: finalGenerationId, + sandboxId: context?.sandboxId || 'pending', + model, + chain: chain || 'solana', + isEdit, + promptLength: prompt ? prompt.length : 0, + promptSummary: prompt ? prompt.substring(0, 100) : '', + status: 'generating', + timestamp: new Date() + }); + console.log('[telemetry] Initialized logging for generationId:', finalGenerationId); + } catch (err) { + console.error('[telemetry] Error inserting initial telemetry record:', err); + } + console.log('[generate-ai-code-stream] Received request:'); console.log('[generate-ai-code-stream] - prompt:', prompt); console.log('[generate-ai-code-stream] - isEdit:', isEdit); @@ -208,7 +228,7 @@ export async function POST(request: NextRequest) { (async () => { try { // Send initial status - await sendProgress({ type: 'status', message: 'Initializing AI...' }); + await sendProgress({ type: 'status', message: 'Initializing AI...', generationId: finalGenerationId }); // No keep-alive needed - sandbox provisioned for 10 minutes @@ -1909,9 +1929,29 @@ Provide the complete file content without any truncation. Include all necessary console.log('[generate-ai-code-stream] Updated conversation history with edit:', editRecord); } + + try { + const db = await getDb(); + await db.collection('telemetry').updateOne( + { generationId: finalGenerationId }, + { $set: { status: 'generated', generatedAt: new Date() } } + ); + } catch (err) { + console.error('[telemetry] Error updating telemetry record to generated:', err); + } } catch (error) { console.error('[generate-ai-code-stream] Stream processing error:', error); + try { + const db = await getDb(); + await db.collection('telemetry').updateOne( + { generationId: finalGenerationId }, + { $set: { status: 'generation-failed', error: (error as Error).message, failedAt: new Date() } } + ); + } catch (err) { + console.error('[telemetry] Error updating telemetry record to failed:', err); + } + await sendProgress({ type: 'error', message: (error as Error).message diff --git a/app/api/telemetry/route.ts b/app/api/telemetry/route.ts new file mode 100644 index 0000000..c4216a8 --- /dev/null +++ b/app/api/telemetry/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/mongo'; + +export async function POST(request: NextRequest) { + try { + const { generationId, sandboxId, status, errors } = await request.json(); + + if (!generationId) { + return NextResponse.json({ + success: false, + error: 'generationId is required' + }, { status: 400 }); + } + + if (!status || !['success', 'compile-error'].includes(status)) { + return NextResponse.json({ + success: false, + error: 'status must be either success or compile-error' + }, { status: 400 }); + } + + const db = await getDb(); + const updateData: any = { + status, + updatedAt: new Date() + }; + + if (sandboxId) { + updateData.sandboxId = sandboxId; + } + + if (errors) { + updateData.errors = errors; + } + + const result = await db.collection('telemetry').updateOne( + { generationId }, + { $set: updateData } + ); + + if (result.matchedCount === 0) { + // Fallback: if record doesn't exist, upsert it so we don't lose the telemetry + await db.collection('telemetry').updateOne( + { generationId }, + { + $set: { + generationId, + sandboxId: sandboxId || 'unknown', + status, + errors: errors || [], + timestamp: new Date(), + updatedAt: new Date() + } + }, + { upsert: true } + ); + } + + console.log(`[telemetry] Successfully logged status '${status}' for generation: ${generationId}`); + + return NextResponse.json({ + success: true, + message: 'Telemetry logged successfully' + }); + + } catch (error) { + console.error('[telemetry-api] Error logging telemetry:', error); + return NextResponse.json({ + success: false, + error: (error as Error).message + }, { status: 500 }); + } +} diff --git a/app/generation/page.tsx b/app/generation/page.tsx index b6c57af..0e9331f 100644 --- a/app/generation/page.tsx +++ b/app/generation/page.tsx @@ -10,6 +10,10 @@ import HeaderBrandKit from '@/components/shared/header/BrandKit/BrandKit'; import { HeaderProvider } from '@/components/shared/header/HeaderContext'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import dynamic from 'next/dynamic'; + +const ReactDiffViewer = dynamic(() => import('@alexbruf/react-diff-viewer'), { ssr: false }); + // Import icons from centralized module to avoid Turbopack chunk issues import { FiFile, @@ -70,6 +74,7 @@ function AISandboxPage() { const [showHomeScreen, setShowHomeScreen] = useState(true); const [expandedFolders, setExpandedFolders] = useState>(new Set(['app', 'src', 'src/components'])); const [selectedFile, setSelectedFile] = useState(null); + const [viewMode, setViewMode] = useState<'code' | 'diff'>('code'); const [homeScreenFading, setHomeScreenFading] = useState(false); const [homeUrlInput, setHomeUrlInput] = useState(''); const [homeContextInput, setHomeContextInput] = useState(''); @@ -94,6 +99,10 @@ function AISandboxPage() { const [hasInitialSubmission, setHasInitialSubmission] = useState(false); const [fileStructure, setFileStructure] = useState(''); + const [currentGenerationId, setCurrentGenerationId] = useState(null); + const telemetryReportedRef = useRef>({}); + const telemetrySuccessTimeoutRef = useRef(null); + const [conversationContext, setConversationContext] = useState<{ scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; generatedComponents: Array<{ name: string; path: string; content: string }>; @@ -1250,6 +1259,22 @@ Tip: I automatically detect and install npm packages from your code imports (lik ...prev, isEdit: false })); + + // Schedule telemetry success check in 10 seconds + if (currentGenerationId) { + if (telemetrySuccessTimeoutRef.current) { + clearTimeout(telemetrySuccessTimeoutRef.current); + } + telemetrySuccessTimeoutRef.current = setTimeout(() => { + if (currentGenerationId && !telemetryReportedRef.current[currentGenerationId]) { + const hasErrors = buildErrors && buildErrors.length > 0; + if (!hasErrors) { + reportTelemetry('success'); + telemetryReportedRef.current[currentGenerationId] = true; + } + } + }, 10000); + } } }; @@ -1347,6 +1372,44 @@ Tip: I automatically detect and install npm packages from your code imports (lik await applyGeneratedCode(code, isEdit); }; + const reportTelemetry = async (status: 'success' | 'compile-error', errors?: any[]) => { + const genId = currentGenerationId; + if (!genId) { + console.log('[telemetry] No current generation ID to report telemetry for.'); + return; + } + try { + await fetch('/api/telemetry', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + generationId: genId, + sandboxId: sandboxData?.sandboxId, + status, + errors + }) + }); + console.log(`[telemetry] Reported status '${status}' for generation ${genId}`); + } catch (err) { + console.error('[telemetry] Error reporting telemetry:', err); + } + }; + + useEffect(() => { + if (currentGenerationId && buildErrors.length > 0 && !telemetryReportedRef.current[currentGenerationId]) { + const isSandboxError = buildErrors.some(e => + e.type === 'http-error' && (e.message.includes('500') || e.message.includes('502') || e.message.includes('503')) + ); + if (!isSandboxError) { + reportTelemetry('compile-error', buildErrors); + telemetryReportedRef.current[currentGenerationId] = true; + if (telemetrySuccessTimeoutRef.current) { + clearTimeout(telemetrySuccessTimeoutRef.current); + } + } + } + }, [buildErrors, currentGenerationId]); + const handleBuildError = (errors: Array<{ type: string; message: string; package?: string }>) => { // Check if this is a sandbox connection error const isSandboxError = errors.some(e => @@ -1567,30 +1630,81 @@ Tip: I automatically detect and install npm packages from your code imports (lik -
- { - const ext = selectedFile.split('.').pop()?.toLowerCase(); - if (ext === 'css') return 'css'; - if (ext === 'json') return 'json'; - if (ext === 'html') return 'html'; - return 'jsx'; - })()} - style={vscDarkPlus} - customStyle={{ - margin: 0, - padding: '1rem', - fontSize: '0.875rem', - background: 'transparent', - }} - showLineNumbers={true} - > - {(() => { - // Find the file content from generated files - const file = generationProgress.files.find(f => f.path === selectedFile); - return file?.content || '// File content will appear here'; - })()} - + {sandboxFiles[selectedFile] && ( +
+ + +
+ )} +
+ {viewMode === 'diff' && sandboxFiles[selectedFile] ? ( +
+ { + const file = generationProgress.files.find(f => f.path === selectedFile); + return file?.content || ''; + })()} + splitView={true} + useDarkTheme={true} + leftTitle="Original" + rightTitle="AI Edits" + styles={{ + variables: { + dark: { + diffViewerBackground: '#111827', + gutterBackground: '#1f2937', + gutterColor: '#9ca3af', + codeFoldBackground: '#374151', + emptyLineBackground: '#111827', + } + } + }} + /> +
+ ) : ( + { + const ext = selectedFile.split('.').pop()?.toLowerCase(); + if (ext === 'css') return 'css'; + if (ext === 'json') return 'json'; + if (ext === 'html') return 'html'; + return 'jsx'; + })()} + style={vscDarkPlus} + customStyle={{ + margin: 0, + padding: '1rem', + fontSize: '0.875rem', + background: 'transparent', + }} + showLineNumbers={true} + > + {(() => { + // Find the file content from generated files + const file = generationProgress.files.find(f => f.path === selectedFile); + return file?.content || sandboxFiles[selectedFile] || '// File content will appear here'; + })()} + + )}
@@ -2129,6 +2243,13 @@ Tip: I automatically detect and install npm packages from your code imports (lik if (data.type === 'status') { setGenerationProgress(prev => ({ ...prev, status: data.message })); + if (data.generationId) { + setCurrentGenerationId(data.generationId); + telemetryReportedRef.current[data.generationId] = false; + if (telemetrySuccessTimeoutRef.current) { + clearTimeout(telemetrySuccessTimeoutRef.current); + } + } } else if (data.type === 'thinking') { setGenerationProgress(prev => ({ ...prev, @@ -2526,7 +2647,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik const handleFileClick = async (filePath: string) => { setSelectedFile(filePath); - // TODO: Add file content fetching logic here + if (sandboxFiles && sandboxFiles[filePath]) { + setViewMode('diff'); + } else { + setViewMode('code'); + } }; const getFileIcon = (fileName: string) => { @@ -2990,6 +3115,13 @@ Tip: I automatically detect and install npm packages from your code imports (lik if (data.type === 'status') { setGenerationProgress(prev => ({ ...prev, status: data.message })); + if (data.generationId) { + setCurrentGenerationId(data.generationId); + telemetryReportedRef.current[data.generationId] = false; + if (telemetrySuccessTimeoutRef.current) { + clearTimeout(telemetrySuccessTimeoutRef.current); + } + } } else if (data.type === 'thinking') { setGenerationProgress(prev => ({ ...prev, diff --git a/lib/mongo.ts b/lib/mongo.ts index 9fefa08..b8463e4 100644 --- a/lib/mongo.ts +++ b/lib/mongo.ts @@ -29,7 +29,8 @@ export async function getDb(): Promise { db.collection('users').createIndex({ email: 1 }, { unique: true }), db.collection('sessions').createIndex({ id: 1 }, { unique: true }), db.collection('sessions').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }), - db.collection('projects').createIndex({ userId: 1, sandboxId: 1 }, { unique: true }) + db.collection('projects').createIndex({ userId: 1, sandboxId: 1 }, { unique: true }), + db.collection('telemetry').createIndex({ generationId: 1 }, { unique: true }) ]) global._mongoIndexesCreated = true } diff --git a/package.json b/package.json index 759348e..756983f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@ai-sdk/google": "^2.0.4", "@ai-sdk/groq": "^2.0.0", "@ai-sdk/openai": "^2.0.4", + "@alexbruf/react-diff-viewer": "^3.1.2", "@anthropic-ai/sdk": "^0.57.0", "@e2b/code-interpreter": "^2.0.0", "@mendable/firecrawl-js": "^4.3.3",