VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
import { NextRequest, NextResponse } from 'next/server';
import { adminAuth, adminDb } from '@/lib/firebase/admin';
export async function POST(request: NextRequest) {
try {
// Verify authentication using API key
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Unauthorized - Missing API key' },
{ status: 401 }
);
}
const apiKey = authHeader.substring(7);
// Look up user by API key
const apiKeysSnapshot = await adminDb
.collection('apiKeys')
.where('key', '==', apiKey)
.where('isActive', '==', true)
.limit(1)
.get();
if (apiKeysSnapshot.empty) {
return NextResponse.json(
{ error: 'Invalid API key' },
{ status: 401 }
);
}
const apiKeyDoc = apiKeysSnapshot.docs[0];
const apiKeyData = apiKeyDoc.data();
const userId = apiKeyData.userId;
if (!userId) {
return NextResponse.json(
{ error: 'API key not associated with user' },
{ status: 401 }
);
}
// Parse request body
const body = await request.json();
const {
projectId,
workspacePath,
githubUrl,
conversations
} = body;
if (!projectId || !conversations) {
return NextResponse.json(
{ error: 'Missing required fields: projectId, conversations' },
{ status: 400 }
);
}
// Verify user has access to the project
const projectRef = adminDb.collection('projects').doc(projectId);
const projectDoc = await projectRef.get();
if (!projectDoc.exists) {
return NextResponse.json(
{ error: 'Project not found' },
{ status: 404 }
);
}
const projectData = projectDoc.data();
if (projectData?.userId !== userId) {
return NextResponse.json(
{ error: 'Access denied to this project' },
{ status: 403 }
);
}
// Process and store conversations
const { composers, workspaceFiles, totalGenerations } = conversations;
let conversationCount = 0;
let totalMessagesWritten = 0;
// Determine filtering keywords based on project context
// TODO: Make this configurable per project
const projectKeywords = ['vibn', 'project', 'extension', 'collector', 'cursor-monitor'];
const excludeKeywords = ['nhl', 'hockey', 'market', 'transaction'];
// Store each composer (chat session) as a separate document
for (const composer of composers || []) {
if (composer.type !== 'head') continue; // Only process head composers
const conversationId = `cursor-${composer.composerId}`;
const conversationRef = adminDb
.collection('projects')
.doc(projectId)
.collection('cursorConversations')
.doc(conversationId);
const name = composer.name || 'Untitled Conversation';
const nameLower = name.toLowerCase();
// Simple relevance scoring
let relevanceScore = 0;
// Check for project keywords in name
for (const keyword of projectKeywords) {
if (nameLower.includes(keyword)) {
relevanceScore += 2;
}
}
// Penalize for exclude keywords
for (const keyword of excludeKeywords) {
if (nameLower.includes(keyword)) {
relevanceScore -= 3;
}
}
// Check if name mentions files from this workspace
if (workspaceFiles && Array.isArray(workspaceFiles)) {
for (const file of workspaceFiles) {
if (nameLower.includes(file.toLowerCase())) {
relevanceScore += 1;
}
}
}
// Count messages
let messageCount = 0;
if (composer.bubbles && Array.isArray(composer.bubbles)) {
messageCount = composer.bubbles.length;
}
const conversationData = {
userId,
projectId,
conversationId,
composerId: composer.composerId,
name,
createdAt: new Date(composer.createdAt).toISOString(),
lastUpdatedAt: new Date(composer.lastUpdatedAt).toISOString(),
unifiedMode: composer.unifiedMode || false,
forceMode: composer.forceMode || false,
workspacePath,
githubUrl: githubUrl || null,
importedAt: new Date().toISOString(),
relevanceScore, // For filtering
messageCount,
metadata: {
source: 'cursor-monitor-extension',
composerType: composer.type,
}
};
// Write conversation document first
await conversationRef.set(conversationData);
// Store messages in chunks to avoid Firestore batch limit (500 operations)
if (composer.bubbles && Array.isArray(composer.bubbles)) {
const BATCH_SIZE = 400; // Leave room for overhead
for (let i = 0; i < composer.bubbles.length; i += BATCH_SIZE) {
const batch = adminDb.batch();
const chunk = composer.bubbles.slice(i, i + BATCH_SIZE);
for (const bubble of chunk) {
const messageRef = conversationRef
.collection('messages')
.doc(bubble.bubbleId);
batch.set(messageRef, {
bubbleId: bubble.bubbleId,
type: bubble.type, // 1 = user, 2 = AI
role: bubble.type === 1 ? 'user' : bubble.type === 2 ? 'assistant' : 'unknown',
text: bubble.text || '',
createdAt: bubble.createdAt,
requestId: bubble.requestId,
attachedFiles: bubble.attachedFiles || []
});
}
await batch.commit();
totalMessagesWritten += chunk.length;
console.log(`✅ Wrote ${chunk.length} messages (${i + chunk.length}/${composer.bubbles.length}) for ${name}`);
}
}
conversationCount++;
}
// Store workspace metadata for reference
const workspaceMetaRef = adminDb
.collection('projects')
.doc(projectId)
.collection('cursorData')
.doc('workspace-meta');
await workspaceMetaRef.set({
workspacePath,
githubUrl,
workspaceFiles: workspaceFiles || [],
totalGenerations: totalGenerations || 0,
importedAt: new Date().toISOString(),
lastBatchImportedAt: new Date().toISOString(),
}, { merge: true });
console.log(`✅ Imported ${conversationCount} conversations to project ${projectId}`);
const workspaceFilesCount = conversations.workspaceFiles?.length || workspaceFiles?.length || 0;
const generationsCount = conversations.totalGenerations || totalGenerations || 0;
return NextResponse.json({
success: true,
conversationCount,
totalMessages: totalMessagesWritten,
workspaceFilesCount,
totalGenerations: generationsCount,
message: `Successfully imported ${conversationCount} conversations with ${totalMessagesWritten} messages`
});
} catch (error) {
console.error('Error importing Cursor conversations:', error);
return NextResponse.json(
{ error: 'Failed to import conversations', details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import { adminDb } from '@/lib/firebase/admin';
// TEMPORARY: For debugging/testing only - no auth required
export async function GET(request: NextRequest) {
try {
const projectId = request.nextUrl.searchParams.get('projectId');
if (!projectId) {
return NextResponse.json(
{ error: 'Missing projectId' },
{ status: 400 }
);
}
// Delete all cursor conversations for this project
const conversationsSnapshot = await adminDb
.collection('projects')
.doc(projectId)
.collection('cursorConversations')
.get();
const batch = adminDb.batch();
conversationsSnapshot.docs.forEach((doc: FirebaseFirestore.QueryDocumentSnapshot) => {
batch.delete(doc.ref);
});
// Also delete the messages data document
const messagesRef = adminDb
.collection('projects')
.doc(projectId)
.collection('cursorData')
.doc('messages');
batch.delete(messagesRef);
await batch.commit();
return NextResponse.json({
success: true,
deletedCount: conversationsSnapshot.size,
message: 'All cursor conversations cleared'
});
} catch (error) {
console.error('Error clearing cursor conversations:', error);
return NextResponse.json(
{ error: 'Failed to clear conversations', details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,192 @@
import { NextRequest, NextResponse } from 'next/server';
import { adminDb } from '@/lib/firebase/admin';
export async function POST(request: NextRequest) {
try {
const projectId = request.nextUrl.searchParams.get('projectId');
const sessionGapMinutes = parseInt(request.nextUrl.searchParams.get('gap') || '30'); // 30 min default
if (!projectId) {
return NextResponse.json({ error: 'Missing projectId' }, { status: 400 });
}
// Get all conversations sorted by time
const conversationsSnapshot = await adminDb
.collection('projects')
.doc(projectId)
.collection('cursorConversations')
.orderBy('createdAt', 'asc')
.get();
const conversations = conversationsSnapshot.docs.map((doc: FirebaseFirestore.QueryDocumentSnapshot) => {
const data = doc.data();
return {
id: doc.id,
ref: doc.ref,
name: data.name,
createdAt: new Date(data.createdAt),
relevanceScore: data.relevanceScore || 0
};
});
// Step 1: Group by date
const conversationsByDate: Record<string, typeof conversations> = {};
for (const conv of conversations) {
const dateKey = conv.createdAt.toISOString().split('T')[0]; // YYYY-MM-DD
if (!conversationsByDate[dateKey]) {
conversationsByDate[dateKey] = [];
}
conversationsByDate[dateKey].push(conv);
}
// Step 2: Within each date, create sessions based on time gaps
const sessions: any[] = [];
let sessionId = 0;
for (const [date, dayConversations] of Object.entries(conversationsByDate)) {
let currentSession: any = null;
for (const conv of dayConversations) {
if (!currentSession) {
// Start first session of the day
sessionId++;
currentSession = {
sessionId,
date,
startTime: conv.createdAt,
endTime: conv.createdAt,
conversations: [conv]
};
} else {
// Check time gap from last conversation
const gapMs = conv.createdAt.getTime() - currentSession.endTime.getTime();
const gapMinutes = gapMs / (1000 * 60);
if (gapMinutes <= sessionGapMinutes) {
// Same session
currentSession.conversations.push(conv);
currentSession.endTime = conv.createdAt;
} else {
// New session - close current and start new
sessions.push(currentSession);
sessionId++;
currentSession = {
sessionId,
date,
startTime: conv.createdAt,
endTime: conv.createdAt,
conversations: [conv]
};
}
}
}
// Add last session of the day
if (currentSession) {
sessions.push(currentSession);
}
}
// Step 3: Analyze each session and determine project
const projectKeywords = ['vibn', 'extension', 'collector', 'cursor-monitor'];
const excludeKeywords = ['nhl', 'hockey', 'market', 'transaction'];
const analyzedSessions = sessions.map(session => {
const allNames = session.conversations.map((c: any) => c.name.toLowerCase()).join(' ');
let projectTag = 'unknown';
let confidence = 'low';
// Check for strong exclude signals
for (const keyword of excludeKeywords) {
if (allNames.includes(keyword)) {
projectTag = 'other';
confidence = 'high';
break;
}
}
// If not excluded, check for vibn signals
if (projectTag === 'unknown') {
for (const keyword of projectKeywords) {
if (allNames.includes(keyword)) {
projectTag = 'vibn';
confidence = 'high';
break;
}
}
}
// If still unknown, check for generic "project" keyword
if (projectTag === 'unknown' && allNames.includes('project')) {
projectTag = 'vibn';
confidence = 'medium';
}
return {
...session,
projectTag,
confidence,
conversationCount: session.conversations.length
};
});
// Step 4: Update Firestore with session tags
const batch = adminDb.batch();
let updateCount = 0;
for (const session of analyzedSessions) {
for (const conv of session.conversations) {
batch.update(conv.ref, {
sessionId: session.sessionId,
sessionDate: session.date,
sessionProject: session.projectTag,
sessionConfidence: session.confidence
});
updateCount++;
}
}
await batch.commit();
// Return summary
const summary = {
totalConversations: conversations.length,
totalSessions: sessions.length,
sessionGapMinutes,
projectBreakdown: {
vibn: analyzedSessions.filter(s => s.projectTag === 'vibn').length,
other: analyzedSessions.filter(s => s.projectTag === 'other').length,
unknown: analyzedSessions.filter(s => s.projectTag === 'unknown').length
},
conversationBreakdown: {
vibn: analyzedSessions.filter(s => s.projectTag === 'vibn').reduce((sum, s) => sum + s.conversationCount, 0),
other: analyzedSessions.filter(s => s.projectTag === 'other').reduce((sum, s) => sum + s.conversationCount, 0),
unknown: analyzedSessions.filter(s => s.projectTag === 'unknown').reduce((sum, s) => sum + s.conversationCount, 0)
},
sampleSessions: analyzedSessions.slice(0, 10).map(s => ({
sessionId: s.sessionId,
date: s.date,
conversationCount: s.conversationCount,
projectTag: s.projectTag,
confidence: s.confidence,
conversationNames: s.conversations.slice(0, 3).map((c: any) => c.name)
}))
};
return NextResponse.json({
success: true,
updatedConversations: updateCount,
...summary
});
} catch (error) {
console.error('Error tagging sessions:', error);
return NextResponse.json(
{ error: 'Failed to tag sessions', details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}