VIBN Frontend for Coolify deployment
This commit is contained in:
229
app/api/cursor/backfill/route.ts
Normal file
229
app/api/cursor/backfill/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
54
app/api/cursor/clear-imports/route.ts
Normal file
54
app/api/cursor/clear-imports/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
192
app/api/cursor/tag-sessions/route.ts
Normal file
192
app/api/cursor/tag-sessions/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user