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
44 changes: 42 additions & 2 deletions app/api/generate-ai-code-stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions app/api/telemetry/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
182 changes: 157 additions & 25 deletions app/generation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -70,6 +74,7 @@ function AISandboxPage() {
const [showHomeScreen, setShowHomeScreen] = useState(true);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['app', 'src', 'src/components']));
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'code' | 'diff'>('code');
const [homeScreenFading, setHomeScreenFading] = useState(false);
const [homeUrlInput, setHomeUrlInput] = useState('');
const [homeContextInput, setHomeContextInput] = useState('');
Expand All @@ -94,6 +99,10 @@ function AISandboxPage() {
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(false);
const [fileStructure, setFileStructure] = useState<string>('');

const [currentGenerationId, setCurrentGenerationId] = useState<string | null>(null);
const telemetryReportedRef = useRef<Record<string, boolean>>({});
const telemetrySuccessTimeoutRef = useRef<NodeJS.Timeout | null>(null);

const [conversationContext, setConversationContext] = useState<{
scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>;
generatedComponents: Array<{ name: string; path: string; content: string }>;
Expand Down Expand Up @@ -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);
}
}
};

Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -1567,30 +1630,81 @@ Tip: I automatically detect and install npm packages from your code imports (lik
</svg>
</button>
</div>
<div className="bg-gray-900 border border-gray-700 rounded">
<SyntaxHighlighter
language={(() => {
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';
})()}
</SyntaxHighlighter>
{sandboxFiles[selectedFile] && (
<div className="flex bg-[#2d2825] px-4 py-1.5 border-b border-gray-700 gap-2">
<button
onClick={() => setViewMode('diff')}
className={`px-3 py-1 rounded text-xs font-medium transition-all ${
viewMode === 'diff'
? 'bg-orange-500 text-white'
: 'text-gray-300 hover:bg-white/10'
}`}
>
Diff View
</button>
<button
onClick={() => setViewMode('code')}
className={`px-3 py-1 rounded text-xs font-medium transition-all ${
viewMode === 'code'
? 'bg-orange-500 text-white'
: 'text-gray-300 hover:bg-white/10'
}`}
>
Source Code
</button>
</div>
)}
<div className="bg-gray-900 border border-gray-700 rounded overflow-hidden">
{viewMode === 'diff' && sandboxFiles[selectedFile] ? (
<div className="overflow-x-auto text-xs max-h-[60vh]">
<ReactDiffViewer
oldValue={sandboxFiles[selectedFile]}
newValue={(() => {
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',
}
}
}}
/>
</div>
) : (
<SyntaxHighlighter
language={(() => {
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';
})()}
</SyntaxHighlighter>
)}
</div>
</div>
</div>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
Expand Down
Loading