diff --git a/app/api/ai/route.ts b/app/api/ai/route.ts index c7791d8..d4bb235 100644 --- a/app/api/ai/route.ts +++ b/app/api/ai/route.ts @@ -59,6 +59,7 @@ export async function POST(req: NextRequest) { // ── 4. Parse body ───────────────────────────────────────────────────────── const body = await req.json(); const { + repoUrl, // ← used for cache lookup repoName, description, entryPoints, @@ -68,7 +69,45 @@ export async function POST(req: NextRequest) { healthMetrics, } = body; - // ── 5. Stream AI response ───────────────────────────────────────────────── + // ── 5. Cache check — skip Groq entirely if AI was already run for this repo + if (repoUrl) { + const { data: cached } = await supabase + .from("analyses") + .select("ai_response") + .eq("repo_url", repoUrl) + .eq("user_id", user.id) + .not("ai_response", "is", null) + .order("created_at", { ascending: false }) + .limit(1) + .maybeSingle(); + + if (cached?.ai_response) { + console.log("[ai] Cache hit — returning stored response, 0 tokens spent"); + const encoder = new TextEncoder(); + const cachedStream = new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode( + typeof cached.ai_response === "string" + ? cached.ai_response + : JSON.stringify(cached.ai_response), + ), + ); + controller.close(); + }, + }); + + return new Response(cachedStream, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + }); + } + } + + // ── 6. Stream AI response ───────────────────────────────────────────────── const responseStream = await streamAnalyzeWithGemini( repoName, description, @@ -79,13 +118,57 @@ export async function POST(req: NextRequest) { healthMetrics, ); - // ── 6. Record usage after stream is kicked off ──────────────────────────── + // ── 7. Tee the stream: client gets it live, we accumulate to cache ──────── + const [clientStream, storeStream] = responseStream.body!.tee(); + + // Background: accumulate full response and store in analyses row + if (repoUrl) { + (async () => { + const reader = storeStream.getReader(); + const decoder = new TextDecoder(); + const chunks: string[] = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(decoder.decode(value, { stream: true })); + } + + const fullResponse = chunks.join(""); + + try { + const { error } = await supabase + .from("analyses") + .update({ ai_response: fullResponse }) + .eq("repo_url", repoUrl) + .eq("user_id", user.id) + .order("created_at", { ascending: false }) + .limit(1); + + if (error) console.error("[ai] Cache store failed:", error.message); + else console.log("[ai] AI response cached for future calls"); + } catch (err) { + console.error("[ai] Cache store threw:", err); + } + })(); + } else { + // No repoUrl — drain the tee'd branch so it doesn't block + storeStream.cancel(); + } + + // ── 8. Record usage after stream is kicked off ──────────────────────────── // Non-blocking — do not await, never fail the response insertAiUsage(supabase, user.id, repoName ?? "unknown").catch((err) => console.error("[ai] Usage insert failed:", err), ); - return responseStream; + return new Response(clientStream, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + }); } catch (error) { console.error("AI Streaming Error:", error); return NextResponse.json( diff --git a/app/api/analyze-pr/route.ts b/app/api/analyze-pr/route.ts index 7b81953..16c2018 100644 --- a/app/api/analyze-pr/route.ts +++ b/app/api/analyze-pr/route.ts @@ -120,7 +120,7 @@ Analyze these code changes and return ONLY a valid JSON object with EXACTLY this method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ - model: "llama-3.3-70b-versatile", + model: "llama-3.1-8b-instant", messages: [ { role: "system", content: systemPrompt }, { role: "user", content: "Analyze this PR and return ONLY the required JSON." }, diff --git a/app/api/analyze/route.ts b/app/api/analyze/route.ts index 21375e4..735919c 100644 --- a/app/api/analyze/route.ts +++ b/app/api/analyze/route.ts @@ -220,6 +220,7 @@ export async function POST(req: NextRequest) { .eq("repo_url", repoUrl) .eq("commit_sha", commitSha) .eq("analysis_version", ANALYSIS_VERSION) + .eq("user_id", userId ?? "") .order("created_at", { ascending: false }) .limit(1) .maybeSingle(); @@ -233,7 +234,11 @@ export async function POST(req: NextRequest) { }, }); - if (!isLocal && !result.cached) { + // ── Always insert a new row for logged-in users so the dashboard + // reflects every analysis, even re-runs of cached repos. + // For cached results, re-use the cached result_json so no + // re-processing is needed but the timestamp is always fresh. + if (!isLocal && userId) { try { const { error: insertError } = await supabase.from("analyses").insert({ repo_url: repoUrl, @@ -250,6 +255,38 @@ export async function POST(req: NextRequest) { console.error("[analyze] DB insert threw:", err); } + // Only run RAG storage for fresh (non-cached) analyses + if (!result.cached) { + try { + await processAndStoreCodebase( + supabase, + repoUrl!, + result.fileContents ?? [], + ); + } catch (err) { + console.error("[analyze] RAG storage failed:", err); + } + } + } + + // For anonymous users, only insert if result is fresh (original behavior) + if (!isLocal && !userId && !result.cached) { + try { + const { error: insertError } = await supabase.from("analyses").insert({ + repo_url: repoUrl, + repo_name: `${result.owner}/${result.repo}`.toLowerCase(), + commit_sha: result.branch, + analysis_version: ANALYSIS_VERSION, + result_json: result, + user_id: null, + }); + if (insertError) { + console.error("[analyze] DB insert error (anon):", insertError.message); + } + } catch (err) { + console.error("[analyze] DB insert threw (anon):", err); + } + try { await processAndStoreCodebase( supabase, diff --git a/app/api/generate-test/route.ts b/app/api/generate-test/route.ts index f59d880..cd96340 100644 --- a/app/api/generate-test/route.ts +++ b/app/api/generate-test/route.ts @@ -46,7 +46,7 @@ export async function POST(req: NextRequest) { "Content-Type": "application/json", }, body: JSON.stringify({ - model: "llama-3.3-70b-versatile", + model: "llama-3.1-8b-instant", messages: [{ role: "user", content: prompt }], response_format: { type: "json_object" }, temperature: 0.1, diff --git a/components/ArchitectureMap.tsx b/components/ArchitectureMap.tsx index 46743b1..b9c8024 100644 --- a/components/ArchitectureMap.tsx +++ b/components/ArchitectureMap.tsx @@ -1589,24 +1589,6 @@ export default function ArchitectureMap({ [apResult], ); - // ── Articulation point computation ──────────────────────────────────────── - const apResult = useMemo( - () => computeArticulationPoints(dependencyGraph), - [dependencyGraph], - ); - - const rankedAPs = useMemo( - () => getRankedArticulationPoints(apResult), - [apResult], - ); - - const apSet = useMemo(() => apResult.articulationPoints, [apResult]); - - const bridgeSet = useMemo( - () => new Set(apResult.bridges.map(([s, t]) => `e-${s}-${t}`)), - [apResult], - ); - const prevPrFilesRef = useRef(""); useEffect(() => { const key = prChangedFiles.join(","); diff --git a/lib/debug/groq-debug.ts b/lib/debug/groq-debug.ts index c65f414..5e955ed 100644 --- a/lib/debug/groq-debug.ts +++ b/lib/debug/groq-debug.ts @@ -87,7 +87,7 @@ Analyze the crash and return ONLY a valid JSON object with this exact structure: Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ - model: "llama-3.3-70b-versatile", + model: "llama-3.1-8b-instant", messages: [ { role: "system", content: systemPrompt }, { diff --git a/lib/gemini.ts b/lib/gemini.ts index f7e703c..908ec57 100644 --- a/lib/gemini.ts +++ b/lib/gemini.ts @@ -35,7 +35,10 @@ export async function streamAnalyzeWithGemini( const groq = wrapOpenAI(baseGroq, tracer); // 2. Build the optimized context text + // Cap at 20 files — topFiles already identifies the most important ones. + // On large repos this prevents runaway token spend with zero quality loss. const fileContentText = fileContents + .slice(0, 20) .map((f) => { const optimized = f.content .replace(/\/\*[\s\S]*?\*\//g, "") @@ -79,7 +82,7 @@ Analyze this codebase and return ONLY a valid JSON object with exactly this stru // 4. Execute the call with `stream: true` so 0xtrace can intercept it const response = await groq.chat.completions.create({ - model: "llama-3.3-70b-versatile", + model: "llama-3.1-8b-instant", messages: [ { role: "system", content: systemPrompt }, { role: "user", content: "Analyze this codebase and stream ONLY the required JSON." }