diff --git a/backend/app/services/ai_matching.py b/backend/app/services/ai_matching.py index 3414ce4..343f901 100644 --- a/backend/app/services/ai_matching.py +++ b/backend/app/services/ai_matching.py @@ -108,18 +108,20 @@ Guidelines: - Be generous with scoring when the match is semantically correct even if the naming differs.""" -def _match_single_asset(client_asset_name, client_asset_desc, volume, catalog_text, num_assets, tier_hint=""): +def _match_single_asset(client_asset_name, client_asset_desc, volume, catalog_text, num_assets, tier_hint="", brief_context=""): """Run a single match call to Claude (synchronous, for use in thread pool).""" - tier_instruction = "" + extra = "" if tier_hint: - tier_instruction = f"\nCLIENT TIER: {tier_hint} — match to the {tier_hint} complexity variant if one exists.\n" + extra += f"\nCLIENT TIER: {tier_hint} — match to the {tier_hint} complexity variant if one exists.\n" + if brief_context: + extra += f"\nBRIEF CONTEXT:\n{brief_context}\n" user_msg = f"""Match this client asset to the best GMAL equivalent(s): CLIENT ASSET: Name: {client_asset_name} Description: {client_asset_desc or 'No description provided'} -Volume: {volume}{tier_instruction} +Volume: {volume}{extra} FULL GMAL CATALOG ({num_assets} assets): {catalog_text}""" @@ -147,11 +149,30 @@ async def match_client_assets( """ _clear_cancel(project_id) - # Load project tier mapping if set + # Load project for tier mapping and brief analysis import json as _json from app.models.project import Project proj_result = await db.execute(select(Project).where(Project.id == project_id)) project = proj_result.scalar_one_or_none() + + # Extract brief analysis context for matching (if available) + brief_context = "" + if project and project.brief_analysis: + try: + analysis = _json.loads(project.brief_analysis) + parts = [] + if analysis.get("summary"): + parts.append(f"Brief summary: {analysis['summary']}") + if analysis.get("channels"): + parts.append(f"Channels: {', '.join(analysis['channels'])}") + if analysis.get("deliverable_categories"): + parts.append(f"Deliverable categories: {', '.join(analysis['deliverable_categories'])}") + if analysis.get("objectives"): + parts.append(f"Objectives: {', '.join(analysis['objectives'][:3])}") + brief_context = "\n".join(parts) + except _json.JSONDecodeError: + pass + tier_config = {} if project and project.tier_mapping: try: @@ -265,6 +286,7 @@ async def match_client_assets( catalog_text, len(all_gmals), tier_hint, + brief_context, ) futures.append((snap, future)) diff --git a/backend/app/services/export_excel.py b/backend/app/services/export_excel.py index 752ea9a..e988393 100644 --- a/backend/app/services/export_excel.py +++ b/backend/app/services/export_excel.py @@ -92,7 +92,12 @@ async def export_ratecard_excel(db: AsyncSession, project: Project, efficiency_l ws2 = wb.create_sheet("Asset Detail") await _build_asset_detail_sheet(ws2, db, project, client_assets, gmals) - # Sheet 3: Assumptions & Rates + # Sheet 3: Brief Analysis (if available) + if project.brief_analysis: + ws_brief = wb.create_sheet("Brief Analysis") + _build_brief_analysis_sheet(ws_brief, project) + + # Sheet 4: Assumptions & Rates ws_rates = wb.create_sheet("Assumptions & Rates") _build_assumptions_sheet(ws_rates, roles, lines) @@ -272,6 +277,77 @@ async def _build_asset_detail_sheet(ws, db, project, client_assets, gmals): ws.column_dimensions[get_column_letter(i)].width = w +BRIEF_FILL = PatternFill(start_color="1565C0", end_color="1565C0", fill_type="solid") +PRIORITY_COLORS = { + "red": Font(bold=True, color="D32F2F"), + "amber": Font(bold=True, color="F57F17"), + "green": Font(bold=True, color="2E7D32"), +} + + +def _build_brief_analysis_sheet(ws, project): + """Build the brief analysis sheet from stored JSON.""" + import json + try: + analysis = json.loads(project.brief_analysis) + except (json.JSONDecodeError, TypeError): + ws["A1"] = "No brief analysis available" + return + + ws.cell(row=1, column=1, value=f"Brief Analysis - {project.name}").font = Font(bold=True, size=14) + + row = 3 + if analysis.get("summary"): + ws.cell(row=row, column=1, value="SUMMARY").font = Font(bold=True) + ws.cell(row=row, column=1).fill = BRIEF_FILL + ws.cell(row=row, column=1).font = HEADER_FONT + row += 1 + ws.cell(row=row, column=1, value=analysis["summary"]).alignment = Alignment(wrap_text=True) + row += 2 + + for section, label in [("objectives", "OBJECTIVES"), ("channels", "CHANNELS"), + ("deliverable_categories", "DELIVERABLE CATEGORIES"), + ("audiences", "AUDIENCES"), ("constraints", "CONSTRAINTS")]: + items = analysis.get(section, []) + if items: + ws.cell(row=row, column=1, value=label).font = Font(bold=True) + row += 1 + for item in items: + ws.cell(row=row, column=1, value=f"• {item}") + row += 1 + row += 1 + + if analysis.get("complexity_assessment"): + ws.cell(row=row, column=1, value=f"Complexity: {analysis['complexity_assessment'].upper()}").font = Font(bold=True) + row += 2 + + # Discovery questions + questions = analysis.get("missing_info", []) + if questions: + ws.cell(row=row, column=1, value="DISCOVERY QUESTIONS").font = Font(bold=True, size=12) + row += 1 + headers = ["Priority", "Category", "Question", "Rationale"] + for col, h in enumerate(headers, 1): + ws.cell(row=row, column=col, value=h).font = HEADER_FONT + ws.cell(row=row, column=col).fill = BRIEF_FILL + row += 1 + + for q in questions: + priority = q.get("priority", "") + ws.cell(row=row, column=1, value=priority.upper()) + if priority in PRIORITY_COLORS: + ws.cell(row=row, column=1).font = PRIORITY_COLORS[priority] + ws.cell(row=row, column=2, value=q.get("category", "")) + ws.cell(row=row, column=3, value=q.get("question", "")).alignment = Alignment(wrap_text=True) + ws.cell(row=row, column=4, value=q.get("rationale", "")).alignment = Alignment(wrap_text=True) + row += 1 + + ws.column_dimensions["A"].width = 15 + ws.column_dimensions["B"].width = 20 + ws.column_dimensions["C"].width = 60 + ws.column_dimensions["D"].width = 40 + + ASSUMPTIONS_FILL = PatternFill(start_color="4A148C", end_color="4A148C", fill_type="solid") INPUT_FILL = PatternFill(start_color="FFF9C4", end_color="FFF9C4", fill_type="solid") diff --git a/frontend/src/pages/ProjectView.css b/frontend/src/pages/ProjectView.css index 21f5315..3b17e65 100644 --- a/frontend/src/pages/ProjectView.css +++ b/frontend/src/pages/ProjectView.css @@ -671,6 +671,56 @@ span.conf-none { background: var(--color-danger); } color: var(--color-primary); } +.tier-editor { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.tier-editor-row { + display: flex; + align-items: center; + gap: 8px; +} + +.tier-input { + width: 140px; + font-size: 12px; + padding: 6px 10px; +} + +.tier-select { + width: 120px; + font-size: 12px; + padding: 6px 10px; +} + +.tier-arrow { + color: var(--color-primary); + font-weight: 700; + font-size: 14px; +} + +.tier-remove { + background: none; + border: 1px solid var(--color-border); + color: var(--color-text-muted); + border-radius: 4px; + width: 24px; + height: 24px; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.tier-remove:hover { + color: var(--color-danger); + border-color: var(--color-danger); +} + /* Refine Chat */ .refine-box { background: var(--color-bg-card); diff --git a/frontend/src/pages/ProjectView.tsx b/frontend/src/pages/ProjectView.tsx index e53eb44..5beeaed 100644 --- a/frontend/src/pages/ProjectView.tsx +++ b/frontend/src/pages/ProjectView.tsx @@ -529,16 +529,54 @@ export default function ProjectView() { {tierMapping.tiers.length > 0 && ( -