Skip to content
Merged
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
27 changes: 25 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ npm test # Run all tests

## Key Implementation Details

**4-Phase Orchestrator Pipeline**: Uses Gemini 3 Flash (`gemini-3-flash-preview`) for all LLM stages:
**Multi-Phase Orchestrator Pipeline**: Uses Gemini 3 Flash (`gemini-3-flash-preview`) for all LLM stages:

| Phase | Component | LLM Calls | Description |
|-------|-----------|-----------|-------------|
| 1 | DataExtractor | 0 | Deterministic extraction (no LLM) |
| 1.1 | DeterministicScorer | 0 | Rubric-based scoring from Phase1Output metrics → `context.deterministicScores` |
| 1.2 | DeterministicTypeMapper | 0 | Map scores → primaryType/controlLevel/distribution → `context.deterministicTypeResult` |
| 1.5 | SessionSummarizer | 1 | LLM-generated 1-line session summaries (batch) |
| 2 | 5 Insight Workers | 5 | Parallel analysis (ThinkingQuality, CommunicationPatterns, LearningBehavior, ContextEfficiency, SessionOutcome) |
| 2 | ProjectSummarizer | 1 | Project-level summaries from activitySessions (parallel with workers) |
Expand Down Expand Up @@ -120,6 +122,19 @@ npm test # Run all tests
>
> **Bug History**: `communicationPatterns` missing from #7 (commit `adf12db`), `sessionOutcome` missing from ALL (fixed 2026-02-07).

> ⚠️ **Dual Data Path Filtering**: `workerInsights` and `translatedAgentInsights` are two independent data paths that converge in the frontend. **Both** must be filtered by ContentGateway for free tier.
>
> **Problem**: `applyTranslatedStrengths()` overlays translated descriptions onto locked teaser data (empty descriptions). If `translatedAgentInsights` is unfiltered, non-empty translated descriptions overwrite `''`, causing `isDomainLocked()` to return `false` → locked domains show "View" instead of "Unlock".
>
> **3-Layer Defense**:
> 1. **API layer**: `createPreviewEvaluation()` in `route.ts` calls `ContentGateway.filterTranslatedInsights('free')` to strip translation data for locked domains
> 2. **Translation overlay**: `applyTranslatedStrengths/GrowthAreas()` in `worker-insights.ts` preserves `description === ''` (locked state) even if translation exists
> 3. **Frontend**: `isPaid` prop passed to `TabbedReportContainer` for explicit lock UI
>
> **Rule**: When adding a new data path that carries content for worker domains (like translations, cached data, etc.), it MUST pass through `ContentGateway` filtering before reaching the frontend. Check `createPreviewEvaluation()` in `app/api/analysis/results/[resultId]/route.ts`.
>
> **Bug History (2026-02-10)**: Non-English free tier reports showed all 5 worker domains fully unlocked because `translatedAgentInsights` bypassed `ContentGateway`.

> ⚠️ **Continuous Scroll Layout**: The report page renders ALL worker sections sequentially (no tabs). `useScrollSpy` hook drives the active section indicator in the `FloatingProgressDots` component. `InsightPreviewCard` is replaced by inline insight rendering within `GrowthCard`.
>
> **How it works** (in `TabbedReportContainer.tsx`):
Expand Down Expand Up @@ -166,7 +181,7 @@ return await analyze(); // Error surfaces to user, root cause can be identified

| Variable | Description |
|----------|-------------|
| `GOOGLE_GEMINI_API_KEY` | Required for 4-phase orchestrator pipeline (Gemini 3 Flash) |
| `GOOGLE_GEMINI_API_KEY` | Required for multi-phase orchestrator pipeline (Gemini 3 Flash) |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL (client-side) |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous key (client-side) |
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key (server-side only) |
Expand All @@ -179,6 +194,14 @@ return await analyze(); // Error surfaces to user, root cause can be identified

> ⚠️ **NEVER use local SST deployment** (`npx sst deploy`). Local SST has critical bugs causing routing failures and inconsistent deployments. Always use GitHub Actions for Lambda deployment.

## Git Workflow

> ⚠️ **Post-merge cleanup**: After merging a PR, always switch back to `main` and pull latest:
> ```bash
> git checkout main && git pull origin main
> ```
> This prevents accidentally continuing work on a stale feature branch.

## Documentation

> ⚠️ **Context Loading**: Before exploring the codebase for architecture, pipeline, file locations, or debugging context, **read `docs/agent/` first**. These docs are optimized for fast lookups (concise, table-based) and cover most questions about system structure, key files, test workflows, and known issues. Only dive into source files when `docs/agent/` doesn't have the specific detail you need.
Expand Down
32 changes: 6 additions & 26 deletions app/api/analysis/results/[resultId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { createClient, type User } from '@supabase/supabase-js';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import type { VerboseEvaluation, PromptPattern, PerDimensionInsight } from '@/lib/models/verbose-evaluation';
import type { VerboseEvaluation, PromptPattern } from '@/lib/models/verbose-evaluation';
import type { AgentOutputs } from '@/lib/models/agent-outputs';
import { aggregateWorkerInsights } from '@/lib/models/agent-outputs';
import { createContentGateway } from '@/lib/analyzer/content-gateway';
Expand Down Expand Up @@ -57,23 +57,6 @@ function createPreviewEvaluation(evaluation: VerboseEvaluation): Partial<Verbose
};
});

// dimensionInsights: strengths full, growthAreas 3 full + 4th truncated
const previewDimensionInsights: PerDimensionInsight[] | undefined = evaluation.dimensionInsights?.map(insight => ({
...insight,
strengths: insight.strengths,
growthAreas: insight.growthAreas?.slice(0, 4).map((area, idx) => {
if (idx < PREVIEW_CONFIG.FULL_ITEMS) {
return area;
}
// 4th item: truncate description and recommendation
return {
...area,
description: truncateText(area.description),
recommendation: truncateText(area.recommendation),
};
}),
}));

return {
// FREE fields - full data
sessionId: evaluation.sessionId,
Expand All @@ -95,7 +78,6 @@ function createPreviewEvaluation(evaluation: VerboseEvaluation): Partial<Verbose

// PREMIUM fields - preview only (3 full + 4th truncated)
promptPatterns: previewPatterns,
dimensionInsights: previewDimensionInsights,

// Other PREMIUM fields - removed
toolUsageDeepDive: undefined,
Expand Down Expand Up @@ -124,8 +106,11 @@ function createPreviewEvaluation(evaluation: VerboseEvaluation): Partial<Verbose
),

// Translated agent insights - needed for non-English users
// Without this, "Your Insights" section shows in English
translatedAgentInsights: evaluation.translatedAgentInsights,
// Filter to strip translations for locked domains (prevents bypassing ContentGateway)
translatedAgentInsights: createContentGateway().filterTranslatedInsights(
evaluation.translatedAgentInsights,
'free'
),
};
}

Expand All @@ -134,14 +119,9 @@ function createPreviewEvaluation(evaluation: VerboseEvaluation): Partial<Verbose
*/
function getPreviewMetadata(evaluation: VerboseEvaluation) {
const totalPromptPatterns = evaluation.promptPatterns?.length ?? 0;
const totalGrowthAreas = evaluation.dimensionInsights?.reduce(
(sum, d) => sum + (d.growthAreas?.length ?? 0),
0
) ?? 0;

return {
totalPromptPatterns,
totalGrowthAreas,
previewCount: PREVIEW_CONFIG.FULL_ITEMS,
hasPartialItem: PREVIEW_CONFIG.PARTIAL_ITEM,
};
Expand Down
21 changes: 1 addition & 20 deletions app/api/analysis/user/progress/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ interface AnalysisResult {
primaryType?: CodingStyleType;
controlLevel?: AIControlLevel;
overallScore?: number;
dimensionInsights?: Array<{
dimension: keyof DimensionScores;
score?: number;
}>;
// Legacy format dimensions
dimensions?: DimensionScores;
} | null;
Expand Down Expand Up @@ -69,30 +65,15 @@ function getSupabaseAdmin() {

/**
* Extract dimension scores from evaluation object
* Handles both new format (dimensionInsights array) and legacy format (dimensions object)
* Uses legacy dimensions object format
*/
function extractDimensionScores(evaluation: AnalysisResult['evaluation']): DimensionScores | null {
if (!evaluation) return null;

// Try legacy format first (direct dimensions object)
if (evaluation.dimensions) {
return evaluation.dimensions;
}

// Try new format (dimensionInsights array)
if (evaluation.dimensionInsights && Array.isArray(evaluation.dimensionInsights)) {
const scores: Partial<DimensionScores> = {};
for (const insight of evaluation.dimensionInsights) {
if (insight.dimension && typeof insight.score === 'number') {
scores[insight.dimension] = insight.score;
}
}
// Only return if we have all 6 dimensions
if (Object.keys(scores).length >= 6) {
return scores as DimensionScores;
}
}

return null;
}

Expand Down
91 changes: 91 additions & 0 deletions app/api/survey/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Survey API Route
*
* POST: Submit survey response (rating + optional comment)
* No authentication required - public endpoint tied to resultId
*/

import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

interface SurveyRequest {
resultId: string;
rating: number;
comment?: string;
}

function getSupabaseAdmin() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!supabaseUrl || !supabaseKey) {
throw new Error('NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required');
}

return createClient(supabaseUrl, supabaseKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
}

/**
* POST /api/survey
*
* Stores a survey response in the survey_responses table.
*
* @param request - JSON body with { resultId, rating (1-5), comment? }
* @returns { success: true } or error
*/
export async function POST(request: Request) {
try {
const body = (await request.json()) as SurveyRequest;
const { resultId, rating, comment } = body;

// Validate resultId
if (!resultId || typeof resultId !== 'string') {
return NextResponse.json(
{ error: 'resultId is required' },
{ status: 400 }
);
}

// Validate rating (integer 1-5)
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
return NextResponse.json(
{ error: 'rating must be an integer between 1 and 5' },
{ status: 400 }
);
}

// Validate comment (optional, max 500 chars)
if (comment !== undefined && comment !== null) {
if (typeof comment !== 'string' || comment.length > 500) {
return NextResponse.json(
{ error: 'comment must be a string with max 500 characters' },
{ status: 400 }
);
}
}

const supabase = getSupabaseAdmin();
const { error } = await supabase.from('survey_responses').insert({
result_id: resultId,
rating,
comment: comment || null,
});

if (error) {
throw error;
}

return NextResponse.json({ success: true });
} catch (error) {
console.error('[Survey API] Error:', error);
return NextResponse.json(
{ error: 'Failed to submit survey' },
{ status: 500 }
);
}
}
68 changes: 68 additions & 0 deletions app/dashboard/enterprise/EnterpriseOverviewContent.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Enterprise Overview Content Styles
*/

.container {
padding: var(--space-lg);
max-width: var(--content-max-width);
margin: 0 auto;
}

.pageTitle {
font-family: var(--font-primary);
font-size: var(--text-2xl);
font-weight: var(--weight-bold);
color: var(--ink-primary);
margin: 0 0 var(--space-xs);
}

.pageSubtitle {
font-family: var(--font-primary);
font-size: var(--text-sm);
color: var(--ink-muted);
margin: 0 0 var(--space-lg);
}

/* Stat Cards Row */
.statsRow {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-md);
margin-bottom: var(--space-xl);
}

/* Section */
.section {
margin-bottom: var(--space-xl);
}

.sectionTitle {
font-family: var(--font-primary);
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--ink-primary);
margin: 0 0 var(--space-md);
}

/* Charts Row */
.chartsRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
margin-bottom: var(--space-xl);
}

.chartCard {
min-width: 0;
}

/* Responsive */
@media (max-width: 768px) {
.chartsRow {
grid-template-columns: 1fr;
}

.statsRow {
grid-template-columns: repeat(2, 1fr);
}
}
Loading