diff --git a/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx b/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx index f9a505c..383e126 100644 --- a/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx +++ b/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx @@ -9,15 +9,13 @@ import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Send, Loader2, Paperclip, X, FileText, RotateCcw, Upload, CheckCircle2, AlertTriangle, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; -import { auth } from "@/lib/firebase/config"; +import { useSession } from "next-auth/react"; import { toast } from "sonner"; import { GitHubRepoPicker } from "@/components/ai/github-repo-picker"; import { PhaseSidebar } from "@/components/ai/phase-sidebar"; import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar"; import { ExtractionResultsEditable } from "@/components/ai/extraction-results-editable"; import type { ChatExtractionData } from "@/lib/ai/chat-extraction-types"; -import { db } from "@/lib/firebase/config"; -import { doc, onSnapshot, getDoc } from "firebase/firestore"; import { VisionForm } from "@/components/ai/vision-form"; interface Message { @@ -72,6 +70,7 @@ export default function GettingStartedPage() { const params = useParams(); const projectId = params.projectId as string; const workspace = params.workspace as string; + const { status: sessionStatus } = useSession(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); @@ -104,74 +103,45 @@ export default function GettingStartedPage() { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); - // Check for vision answers on load + // Load project phase + vision answers from the Postgres-backed API useEffect(() => { if (!projectId) return; - const checkVision = async () => { + const loadProject = async () => { try { - const projectDoc = await getDoc(doc(db, "projects", projectId)); - if (projectDoc.exists()) { - const data = projectDoc.data(); - const hasAnswers = data?.visionAnswers?.allAnswered === true; + const res = await fetch(`/api/projects/${projectId}`); + if (res.ok) { + const data = await res.json(); + const phase = data.project?.currentPhase || 'collector'; + setCurrentPhase(phase); + const hasAnswers = data.project?.visionAnswers?.allAnswered === true; setHasVisionAnswers(hasAnswers); } } catch (error) { - console.error('Error checking vision answers:', error); + console.error('Error loading project:', error); } finally { setCheckingVision(false); } }; - checkVision(); - }, [projectId]); - - // Listen to project phase changes - useEffect(() => { - if (!projectId) return; - - const unsubscribe = onSnapshot( - doc(db, "projects", projectId), - (snapshot) => { - if (snapshot.exists()) { - const data = snapshot.data(); - const phase = data?.currentPhase || "collector"; - setCurrentPhase(phase); - - // Update vision answers status - const hasAnswers = data?.visionAnswers?.allAnswered === true; - setHasVisionAnswers(hasAnswers); - } - } - ); - - return () => unsubscribe(); + loadProject(); }, [projectId]); // Initialize with AI welcome message useEffect(() => { - if (!isInitialized && projectId) { - const unsubscribe = auth.onAuthStateChanged(async (user) => { - if (!user) { - // Not signed in, trigger AI welcome + if (!isInitialized && projectId && sessionStatus !== 'loading') { + const initialize = async () => { + if (sessionStatus === 'unauthenticated') { setIsLoading(false); setIsInitialized(true); - setTimeout(() => { - sendChatMessage("Hello"); - }, 500); + setTimeout(() => sendChatMessage("Hello"), 500); return; } - // User is signed in, load conversation history first + // Signed in via NextAuth — load conversation history try { - const token = await user.getIdToken(); - // Fetch existing conversation history - const historyResponse = await fetch(`/api/ai/conversation?projectId=${projectId}`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); + const historyResponse = await fetch(`/api/ai/conversation?projectId=${projectId}`); let existingMessages: Message[] = []; @@ -231,11 +201,11 @@ export default function GettingStartedPage() { setIsLoading(false); setIsInitialized(true); } - }); + }; - return () => unsubscribe(); + initialize(); } - }, [projectId, isInitialized]); + }, [projectId, isInitialized, sessionStatus]); const sendChatMessage = async (messageContent: string) => { const content = messageContent.trim(); @@ -251,24 +221,10 @@ export default function GettingStartedPage() { setIsSending(true); try { - const user = auth.currentUser; - if (!user) { - toast.error('Please sign in to continue'); - setIsSending(false); - return; - } - - const token = await user.getIdToken(); const response = await fetch('/api/ai/chat', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - projectId, - message: content, - }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId, message: content }), }); if (!response.ok) { @@ -392,21 +348,8 @@ export default function GettingStartedPage() { } try { - const user = auth.currentUser; - if (!user) { - toast.error('Please sign in to reset chat'); - return; - } - - const token = await user.getIdToken(); - - const response = await fetch('/api/ai/conversation/reset', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ projectId }), + const response = await fetch(`/api/ai/conversation?projectId=${projectId}`, { + method: 'DELETE', }); if (response.ok) { @@ -433,20 +376,9 @@ export default function GettingStartedPage() { setExtractionStatus("importing"); setExtractionError(null); - const user = auth.currentUser; - if (!user) { - toast.error("Please sign in to import chats"); - setIsImporting(false); - return; - } - - const token = await user.getIdToken(); const importResponse = await fetch(`/api/projects/${projectId}/knowledge/import-ai-chat`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: extractForm.title || "Imported AI chat", provider: extractForm.provider, diff --git a/app/api/ai/chat/route.ts b/app/api/ai/chat/route.ts index 7e474b0..f18e6cf 100644 --- a/app/api/ai/chat/route.ts +++ b/app/api/ai/chat/route.ts @@ -2,8 +2,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import { GeminiLlmClient } from '@/lib/ai/gemini-client'; import type { LlmClient } from '@/lib/ai/llm-client'; -import { getAdminDb } from '@/lib/firebase/admin'; -import { FieldValue } from 'firebase-admin/firestore'; +import { query } from '@/lib/db-postgres'; import { MODE_SYSTEM_PROMPTS, ChatMode } from '@/lib/ai/chat-modes'; import { resolveChatMode } from '@/lib/server/chat-mode-resolver'; import { @@ -48,37 +47,30 @@ interface ChatRequestBody { overrideMode?: ChatMode; } +const ENSURE_CONV_TABLE = ` + CREATE TABLE IF NOT EXISTS chat_conversations ( + project_id text PRIMARY KEY, + messages jsonb NOT NULL DEFAULT '[]', + updated_at timestamptz NOT NULL DEFAULT NOW() + ) +`; + async function appendConversation( projectId: string, - messages: Array<{ role: 'user' | 'assistant'; content: string }>, + newMessages: Array<{ role: 'user' | 'assistant'; content: string }>, ) { - const adminDb = getAdminDb(); - const docRef = adminDb.collection('chat_conversations').doc(projectId); + await query(ENSURE_CONV_TABLE); + const now = new Date().toISOString(); + const stamped = newMessages.map((m) => ({ ...m, createdAt: now })); - await adminDb.runTransaction(async (tx) => { - const snapshot = await tx.get(docRef); - const existing = (snapshot.exists ? (snapshot.data()?.messages as unknown[]) : []) ?? []; - - const now = new Date().toISOString(); - - const newMessages = messages.map((m) => ({ - role: m.role, - content: m.content, - // Use a simple ISO string for message timestamps to avoid FieldValue - // restrictions inside arrays. - createdAt: now, - })); - - tx.set( - docRef, - { - projectId, - messages: [...existing, ...newMessages], - updatedAt: FieldValue.serverTimestamp(), - }, - { merge: true }, - ); - }); + await query( + `INSERT INTO chat_conversations (project_id, messages, updated_at) + VALUES ($1, $2::jsonb, NOW()) + ON CONFLICT (project_id) DO UPDATE + SET messages = chat_conversations.messages || $2::jsonb, + updated_at = NOW()`, + [projectId, JSON.stringify(stamped)] + ); } export async function POST(request: Request) { @@ -91,13 +83,15 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'projectId and message are required' }, { status: 400 }); } - // Verify project exists - const adminDb = getAdminDb(); - const projectSnapshot = await adminDb.collection('projects').doc(projectId).get(); - if (!projectSnapshot.exists) { + // Verify project exists in Postgres + const projectRows = await query<{ data: any }>( + `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, + [projectId] + ); + if (projectRows.length === 0) { return NextResponse.json({ error: 'Project not found' }, { status: 404 }); } - const projectData = projectSnapshot.data() ?? {}; + const projectData = projectRows[0].data ?? {}; // Resolve chat mode (uses new resolver) const resolvedMode = body.overrideMode ?? await resolveChatMode(projectId); @@ -138,12 +132,13 @@ ${context.sessionHistory.totalSessions > 0 ? `- Complete Cursor session history Use this context to provide specific, grounded responses. The session history shows your complete conversation history with the user - use it to understand what has been built and discussed.`; - // Load existing conversation history - const conversationDoc = await adminDb.collection('chat_conversations').doc(projectId).get(); - const conversationData = conversationDoc.exists ? conversationDoc.data() : null; - const conversationHistory = Array.isArray(conversationData?.messages) - ? conversationData.messages - : []; + // Load existing conversation history from Postgres + await query(ENSURE_CONV_TABLE); + const convRows = await query<{ messages: any[] }>( + `SELECT messages FROM chat_conversations WHERE project_id = $1`, + [projectId] + ); + const conversationHistory: any[] = convRows[0]?.messages ?? []; // Build full message context (history + current message) const messages = [ @@ -251,10 +246,14 @@ Use this context to provide specific, grounded responses. The session history sh if (Object.keys(updates).length > 0) { updates['visionAnswers.updatedAt'] = new Date().toISOString(); - await adminDb.collection('projects').doc(projectId).set(updates, { merge: true }) - .catch((error) => { - console.error('[ai/chat] Failed to store vision answers', error); - }); + await query( + `UPDATE fs_projects + SET data = data || $1::jsonb + WHERE id = $2`, + [JSON.stringify({ visionAnswers: updates }), projectId] + ).catch((error) => { + console.error('[ai/chat] Failed to store vision answers', error); + }); } } @@ -326,12 +325,17 @@ Use this context to provide specific, grounded responses. The session history sh timestamp: new Date().toISOString(), }; - // Persist to project phaseData - await adminDb.collection('projects').doc(projectId).set( - { - 'phaseData.phaseHandoffs.collector': handoff, - }, - { merge: true } + // Persist to project phaseData in Postgres + await query( + `UPDATE fs_projects + SET data = jsonb_set( + data, + '{phaseData,phaseHandoffs,collector}', + $1::jsonb, + true + ) + WHERE id = $2`, + [JSON.stringify(handoff), projectId] ).catch((error) => { console.error('[ai/chat] Failed to persist collector handoff', error); }); @@ -348,9 +352,12 @@ Use this context to provide specific, grounded responses. The session history sh console.log(`[AI Chat] Collector complete - triggering backend extraction`); // Mark collector as complete - await adminDb.collection('projects').doc(projectId).update({ - 'phaseData.collectorCompletedAt': new Date().toISOString(), - }).catch((error) => { + await query( + `UPDATE fs_projects + SET data = jsonb_set(data, '{phaseData,collectorCompletedAt}', $1::jsonb, true) + WHERE id = $2`, + [JSON.stringify(new Date().toISOString()), projectId] + ).catch((error) => { console.error('[ai/chat] Failed to mark collector complete', error); }); @@ -397,44 +404,32 @@ Use this context to provide specific, grounded responses. The session history sh console.log(`[AI Chat] Extraction review complete - transitioning to vision phase`); // Mark extraction review as complete and transition to vision - await adminDb.collection('projects').doc(projectId).update({ - currentPhase: 'vision', - phaseStatus: 'in_progress', - 'phaseData.extractionReviewCompletedAt': new Date().toISOString(), - }).catch((error) => { + await query( + `UPDATE fs_projects + SET data = data + || '{"currentPhase":"vision","phaseStatus":"in_progress"}'::jsonb + || jsonb_build_object('phaseData', + (data->'phaseData') || jsonb_build_object( + 'extractionReviewCompletedAt', $1::text + ) + ) + WHERE id = $2`, + [new Date().toISOString(), projectId] + ).catch((error) => { console.error('[ai/chat] Failed to transition to vision phase', error); }); } } - // Save conversation history - const newConversationHistory = [ - ...conversationHistory, - { - role: 'user' as const, - content: message, - createdAt: new Date().toISOString(), - }, - { - role: 'assistant' as const, - content: reply.reply, - createdAt: new Date().toISOString(), - }, - ]; - - await adminDb.collection('chat_conversations').doc(projectId).set( - { - projectId, - userId: projectData.userId, - messages: newConversationHistory, - updatedAt: new Date().toISOString(), - }, - { merge: true } - ).catch((error) => { + // Save conversation history to Postgres + await appendConversation(projectId, [ + { role: 'user', content: message }, + { role: 'assistant', content: reply.reply }, + ]).catch((error) => { console.error('[ai/chat] Failed to save conversation history', error); }); - console.log(`[AI Chat] Conversation history saved (${newConversationHistory.length} total messages)`); + console.log(`[AI Chat] Conversation history saved (+2 messages)`); // Determine which artifacts were used const artifactsUsed = determineArtifactsUsed(context); diff --git a/app/api/ai/conversation/route.ts b/app/api/ai/conversation/route.ts index b4a03ed..823d900 100644 --- a/app/api/ai/conversation/route.ts +++ b/app/api/ai/conversation/route.ts @@ -1,12 +1,20 @@ import { NextResponse } from 'next/server'; -import { getAdminDb } from '@/lib/firebase/admin'; +import { query } from '@/lib/db-postgres'; + +const ENSURE_TABLE = ` + CREATE TABLE IF NOT EXISTS chat_conversations ( + project_id text PRIMARY KEY, + messages jsonb NOT NULL DEFAULT '[]', + updated_at timestamptz NOT NULL DEFAULT NOW() + ) +`; type StoredMessageRole = 'user' | 'assistant'; type ConversationMessage = { role: StoredMessageRole; content: string; - createdAt?: { _seconds: number; _nanoseconds: number }; + createdAt?: string; }; type ConversationResponse = { @@ -19,36 +27,43 @@ export async function GET(request: Request) { const projectId = (url.searchParams.get('projectId') ?? '').trim(); if (!projectId) { - return NextResponse.json( - { error: 'projectId is required' }, - { status: 400 }, - ); + return NextResponse.json({ error: 'projectId is required' }, { status: 400 }); } - const adminDb = getAdminDb(); - const docRef = adminDb.collection('chat_conversations').doc(projectId); - const snapshot = await docRef.get(); + await query(ENSURE_TABLE); - if (!snapshot.exists) { - const empty: ConversationResponse = { messages: [] }; - return NextResponse.json(empty); - } - - const data = snapshot.data() as { messages?: ConversationMessage[] }; - const messages = Array.isArray(data.messages) ? data.messages : []; + const rows = await query<{ messages: ConversationMessage[] }>( + `SELECT messages FROM chat_conversations WHERE project_id = $1`, + [projectId] + ); + const messages: ConversationMessage[] = rows[0]?.messages ?? []; const response: ConversationResponse = { messages }; return NextResponse.json(response); } catch (error) { - console.error('[ai/conversation] Failed to load conversation', error); - return NextResponse.json( - { - error: 'Failed to load conversation', - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ); + console.error('[GET /api/ai/conversation] Error:', error); + return NextResponse.json({ messages: [] }); } } +export async function DELETE(request: Request) { + try { + const url = new URL(request.url); + const projectId = (url.searchParams.get('projectId') ?? '').trim(); + if (!projectId) { + return NextResponse.json({ error: 'projectId is required' }, { status: 400 }); + } + + await query(ENSURE_TABLE); + await query( + `DELETE FROM chat_conversations WHERE project_id = $1`, + [projectId] + ); + + return NextResponse.json({ ok: true }); + } catch (error) { + console.error('[DELETE /api/ai/conversation] Error:', error); + return NextResponse.json({ error: 'Failed to reset conversation' }, { status: 500 }); + } +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx index 68c2dda..3674d76 100644 --- a/app/auth/page.tsx +++ b/app/auth/page.tsx @@ -43,4 +43,3 @@ export default function AuthPage() { ); } - diff --git a/lib/server/chat-context.ts b/lib/server/chat-context.ts index 63772c6..d0028a3 100644 --- a/lib/server/chat-context.ts +++ b/lib/server/chat-context.ts @@ -5,7 +5,7 @@ * building a compact context object for LLM consumption. */ -import { getAdminDb } from '@/lib/firebase/admin'; +import { query } from '@/lib/db-postgres'; import { retrieveRelevantChunks } from '@/lib/server/vector-memory'; import { embedText } from '@/lib/ai/embeddings'; import { @@ -121,15 +121,16 @@ export async function buildProjectContextForChat( } = options; try { - const adminDb = getAdminDb(); - - // Load project document - const projectSnapshot = await adminDb.collection('projects').doc(projectId).get(); - if (!projectSnapshot.exists) { + // Load project from Postgres + const projectRows = await query<{ data: any }>( + `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, + [projectId] + ); + if (projectRows.length === 0) { throw new Error(`Project ${projectId} not found`); } - const projectData = projectSnapshot.data() ?? {}; + const projectData = projectRows[0].data ?? {}; // Load summaries in parallel const [knowledgeSummary, extractionSummary] = await Promise.all([ diff --git a/lib/server/chat-mode-resolver.ts b/lib/server/chat-mode-resolver.ts index 16e0b1f..ff67b08 100644 --- a/lib/server/chat-mode-resolver.ts +++ b/lib/server/chat-mode-resolver.ts @@ -1,190 +1,91 @@ /** * Chat Mode Resolution Logic - * + * * Determines which chat mode (collector, extraction_review, vision, mvp, marketing, general) - * should be active based on project state. + * should be active based on project state stored in Postgres. */ -import { getAdminDb } from '@/lib/firebase/admin'; +import { query } from '@/lib/db-postgres'; import type { ChatMode } from '@/lib/ai/chat-modes'; /** - * Resolve the appropriate chat mode for a project - * - * Logic: - * 1. No knowledge_items → collector_mode - * 2. Has knowledge but no extractions → collector_mode (needs to run extraction) - * 3. Has extractions but no canonicalProductModel → extraction_review_mode - * 4. Has canonicalProductModel but no mvpPlan → vision_mode - * 5. Has mvpPlan but no marketingPlan → mvp_mode - * 6. Has marketingPlan → marketing_mode - * 7. Otherwise → general_chat_mode - * - * @param projectId - Firestore project ID - * @returns The appropriate chat mode + * Resolve the appropriate chat mode for a project using Postgres (fs_projects). */ export async function resolveChatMode(projectId: string): Promise { try { - const adminDb = getAdminDb(); + const rows = await query<{ data: any }>( + `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, + [projectId] + ); - // Load project data - const projectSnapshot = await adminDb.collection('projects').doc(projectId).get(); - if (!projectSnapshot.exists) { - throw new Error(`Project ${projectId} not found`); - } - - const projectData = projectSnapshot.data() ?? {}; - const phaseData = (projectData.phaseData ?? {}) as Record; - - // Check for knowledge_items (top-level collection) - const knowledgeSnapshot = await adminDb - .collection('knowledge_items') - .where('projectId', '==', projectId) - .limit(1) - .get(); - - const hasKnowledge = !knowledgeSnapshot.empty; - - // Check for chat_extractions (top-level collection) - const extractionsSnapshot = await adminDb - .collection('chat_extractions') - .where('projectId', '==', projectId) - .limit(1) - .get(); - - const hasExtractions = !extractionsSnapshot.empty; - - // Apply resolution logic - // PRIORITY: Check explicit phase transitions FIRST (overrides knowledge checks) - if (projectData.currentPhase === 'extraction_review' || projectData.currentPhase === 'analyzed') { - return 'extraction_review_mode'; - } - - if (projectData.currentPhase === 'vision') { - return 'vision_mode'; - } - - if (projectData.currentPhase === 'mvp') { - return 'mvp_mode'; - } - - if (projectData.currentPhase === 'marketing') { - return 'marketing_mode'; - } - - if (!hasKnowledge) { + if (rows.length === 0) { + console.warn(`[Chat Mode Resolver] Project ${projectId} not found`); return 'collector_mode'; } - if (hasKnowledge && !hasExtractions) { - return 'collector_mode'; // Has knowledge but needs extraction - } + const projectData = rows[0].data ?? {}; + const phaseData = (projectData.phaseData ?? {}) as Record; + const currentPhase: string = projectData.currentPhase ?? 'collector'; - // Fallback: Has extractions but no canonicalProductModel - if (hasExtractions && !phaseData.canonicalProductModel) { - return 'extraction_review_mode'; - } + // Explicit phase overrides + if (currentPhase === 'extraction_review' || currentPhase === 'analyzed') return 'extraction_review_mode'; + if (currentPhase === 'vision') return 'vision_mode'; + if (currentPhase === 'mvp') return 'mvp_mode'; + if (currentPhase === 'marketing') return 'marketing_mode'; - if (phaseData.canonicalProductModel && !phaseData.mvpPlan) { - return 'vision_mode'; - } - - if (phaseData.mvpPlan && !phaseData.marketingPlan) { - return 'mvp_mode'; - } - - if (phaseData.marketingPlan) { - return 'marketing_mode'; - } + // Derive from phase artifacts + if (!phaseData.canonicalProductModel) return 'collector_mode'; + if (!phaseData.mvpPlan) return 'vision_mode'; + if (!phaseData.marketingPlan) return 'mvp_mode'; + if (phaseData.marketingPlan) return 'marketing_mode'; return 'general_chat_mode'; } catch (error) { console.error('[Chat Mode Resolver] Failed to resolve mode:', error); - // Default to collector on error return 'collector_mode'; } } /** - * Get a summary of knowledge_items for context building + * Summarise knowledge items for context building. + * Uses Postgres fs_knowledge_items if available, otherwise returns empty. */ -export async function summarizeKnowledgeItems( - projectId: string -): Promise<{ +export async function summarizeKnowledgeItems(projectId: string): Promise<{ totalCount: number; bySourceType: Record; recentTitles: string[]; }> { try { - const adminDb = getAdminDb(); - const snapshot = await adminDb - .collection('knowledge_items') - .where('projectId', '==', projectId) - .orderBy('createdAt', 'desc') - .limit(20) - .get(); + const rows = await query<{ data: any }>( + `SELECT data FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC LIMIT 20`, + [projectId] + ); - const totalCount = snapshot.size; const bySourceType: Record = {}; const recentTitles: string[] = []; - snapshot.docs.forEach((doc) => { - const data = doc.data(); - const sourceType = data.sourceType ?? 'unknown'; + for (const row of rows) { + const d = row.data ?? {}; + const sourceType = d.sourceType ?? 'unknown'; bySourceType[sourceType] = (bySourceType[sourceType] ?? 0) + 1; + if (d.title && recentTitles.length < 5) recentTitles.push(d.title); + } - if (data.title && recentTitles.length < 5) { - recentTitles.push(data.title); - } - }); - - return { totalCount, bySourceType, recentTitles }; - } catch (error) { - console.error('[Chat Mode Resolver] Failed to summarize knowledge:', error); + return { totalCount: rows.length, bySourceType, recentTitles }; + } catch { + // Table may not exist for older deployments — return empty return { totalCount: 0, bySourceType: {}, recentTitles: [] }; } } /** - * Get a summary of chat_extractions for context building + * Summarise extractions for context building. + * Returns empty defaults — extractions not yet migrated to Postgres. */ -export async function summarizeExtractions( - projectId: string -): Promise<{ +export async function summarizeExtractions(projectId: string): Promise<{ totalCount: number; avgConfidence: number; avgCompletion: number; }> { - try { - const adminDb = getAdminDb(); - const snapshot = await adminDb - .collection('chat_extractions') - .where('projectId', '==', projectId) - .get(); - - if (snapshot.empty) { - return { totalCount: 0, avgConfidence: 0, avgCompletion: 0 }; - } - - let sumConfidence = 0; - let sumCompletion = 0; - let count = 0; - - snapshot.docs.forEach((doc) => { - const data = doc.data(); - sumConfidence += data.overallConfidence ?? 0; - sumCompletion += data.overallCompletion ?? 0; - count++; - }); - - return { - totalCount: count, - avgConfidence: count > 0 ? sumConfidence / count : 0, - avgCompletion: count > 0 ? sumCompletion / count : 0, - }; - } catch (error) { - console.error('[Chat Mode Resolver] Failed to summarize extractions:', error); - return { totalCount: 0, avgConfidence: 0, avgCompletion: 0 }; - } + return { totalCount: 0, avgConfidence: 0, avgCompletion: 0 }; } -