Skip to content

Comments

feat(analysis): add priority matrix view and fix medium item bug#4

Open
nikman21 wants to merge 3 commits intomainfrom
feature/priority-matrix-analysis
Open

feat(analysis): add priority matrix view and fix medium item bug#4
nikman21 wants to merge 3 commits intomainfrom
feature/priority-matrix-analysis

Conversation

@nikman21
Copy link
Contributor

@nikman21 nikman21 commented Feb 8, 2026

What I Built

Added the missing Analysis page with a fully functional Priority Matrix visualization for PM Analyzer.

Changes Made

  1. **Created ** - New analysis view with:

    • Summary stats (total feedback, bugs, features, opportunities count)
    • Tabs for Matrix, All Opportunities, and Unclustered feedback
    • Priority Matrix visualization using the PriorityMatrix component
    • Ranked opportunities list sorted by impact score
  2. **Fixed ** - Critical bug fix and improvements:

    • BUG FIX: Items with 'medium' impact or effort were not appearing in any quadrant (they were filtered out)
    • Now properly categorizes ALL items into quadrants using a helper function
    • Added priority scoring to sort items within each quadrant
    • Added balance indicator showing % of actionable opportunities
    • Added visual legend explaining each quadrant
    • Improved styling with row labels (High/Low Impact)
    • Added item counts per quadrant

Why This Matters

  • PM Analyzer now has a complete Analysis view - Previously the analysis tab had no real content
  • Priority Matrix actually works - The component existed but wasn't used, and it was broken for medium-priority items
  • Better decision making - PMs can now visually prioritize opportunities by Impact vs Effort

How to Test

  1. Add feedback to a project
  2. Run Analysis (click Analyze button)
  3. Navigate to Analysis tab
  4. View the Priority Matrix with opportunities grouped into quadrants
  5. Switch between tabs to see all opportunities ranked by impact score
  6. Verify medium-impact items appear in appropriate quadrants (not hidden)

Visual Changes

  • Eisenhower matrix with 4 quadrants: Quick Wins, Major Projects, Fillers, Thankless Tasks
  • Color-coded cards (green=quick wins, blue=major projects, yellow=fillers, red=thankless)
  • Impact/Effort badges on each item
  • Balance indicator showing actionable %

Screenshots available on the PR page

Summary by CodeRabbit

  • New Features

    • Decision export: download or copy decisions in multiple formats (Cursor, Linear, Jira, Slack, Notion, Markdown, JSON).
    • Analysis dashboard: project feedback summary, opportunities list, and priority matrix with balance indicator.
    • Decision detail and Decisions list pages with export and navigation actions.
    • Create Decision form with confidence control and submission flow.
  • UI

    • Export dialog with live preview; enhanced Priority Matrix visuals.
    • New UI controls: alerts, slider, and textarea components.

## What Was Built

A comprehensive Decision Export System for PM Analyzer that enables
product managers to export decisions in multiple formats for different
downstream tools.

### Components Created

1. **DecisionExportDialog.tsx** - A reusable export modal component
   - 7 export formats: Cursor/Claude Code, Linear, Jira, Slack, Notion, Markdown, JSON
   - Live preview of export format
   - Copy to clipboard functionality
   - Download as file

2. **Export API Route** (`/api/projects/[id]/decisions/[decisionId]/export`)
   - GET endpoint for programmatic export
   - Supports all 7 formats
   - Returns appropriate Content-Type headers

3. **Decision Detail Page** (`/projects/[id]/decisions/[decisionId]`)
   - Full decision view with all metadata
   - Embedded Export dialog
   - Confidence score badges
   - Linked feedback display

4. **Decisions List Page** (`/projects/[id]/decisions`)
   - All decisions in one place
   - Export button on each decision card
   - Quick filtering by status

### Export Formats

| Format | Use Case |
|--------|----------|
| Cursor/Claude Code | AI pair programming handoff with structured prompts |
| Linear | Issue tracker format for sprint planning |
| Jira | Enterprise issue tracking |
| Slack | Stakeholder communication with emoji confidence |
| Notion | Documentation and wiki export |
| Markdown | General documentation |
| JSON | Programmatic access and integration |

### Why This Matters

Product managers need to hand off decisions to:
- Engineering teams (Linear/Jira)
- AI coding assistants (Cursor)
- Stakeholders (Slack/Notion)
- Documentation systems (Markdown/JSON)

This feature bridges the gap between PM Analyzer's decision-making
capabilities and the tools teams actually use to build software.

---

**Nightly Build (2026-02-07)**
- Created missing Analysis page at /projects/[id]/analysis/
- Added PriorityMatrix component to display opportunities in Eisenhower matrix format
- Fixed bug where items with 'medium' impact/effort weren't appearing in any quadrant
- Added balance indicator showing % of actionable opportunities
- Added sortable quadrants with priority scoring
- Added legend and visual improvements
@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

Adds decision export API and client export dialog with seven formats, three new project pages (decisions list, decision detail, analysis), a dynamic PriorityMatrix refactor, new UI primitives (Alert, Slider, Textarea), and changes to decision creation data (confidence -> confidenceScore).

Changes

Cohort / File(s) Summary
Export API Route
app/api/projects/[id]/decisions/[decisionId]/export/route.ts
New GET handler that validates format, checks decision ownership, assembles DecisionExportData, formats via formatDecision, and returns JSON or downloadable text/markdown attachments.
Decision Pages
app/projects/[id]/decisions/page.tsx, app/projects/[id]/decisions/[decisionId]/page.tsx, app/projects/[id]/decisions/new/page.tsx
Adds decisions list, decision detail, and create-decision pages. List and detail pages fetch decisions and project info; detail computes badges/levels and includes export dialog; new page implements client-side create form submitting confidenceScore (0–1).
Analysis Page
app/projects/[id]/analysis/page.tsx
New analysis dashboard page that fetches latest analysis, parses analysis.content into structured opportunities, renders summary cards, Tabs (Matrix, Opportunities, Unclustered), and PriorityMatrix.
Export Dialog & Formatters
components/DecisionExportDialog.tsx
Client-side modal with format selection, live preview, copy/download actions, ExportFormat types, DecisionExportData interface, EXPORT_FORMATS config, seven formatter functions, and formatDecision dispatcher (exported).
Priority Matrix
components/PriorityMatrix.tsx
Refactors quadrant logic to score-driven classification, adds quadrant metadata, composite priority sorting, balance indicator, per-item badges, and legend.
UI Primitives
components/ui/alert.tsx, components/ui/slider.tsx, components/ui/textarea.tsx
Adds Alert, AlertTitle, AlertDescription, a Radix-based Slider wrapper, and a styled Textarea component (forwardRef-based) for consistent UI building blocks.
API Create Decision Change
app/api/projects/[id]/decisions/route.ts
PR changes create handler to use confidenceScore (number, default 0.5) instead of confidence (string).

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as DecisionExportDialog
    participant API as Export API Route
    participant DB as Database
    participant Formatter as formatDecision

    User->>UI: open export dialog
    UI->>UI: select format, show preview
    User->>UI: request download/copy
    UI->>API: GET /api/projects/{id}/decisions/{decisionId}/export?format=F
    API->>DB: fetch decision (+ project name)
    DB-->>API: decision record
    API->>API: validate project ownership
    API->>Formatter: formatDecision(data, F)
    Formatter-->>API: formatted output
    API-->>UI: response (JSON or file response)
    UI-->>User: provide download or copy
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code, both bright and neat,

Exports, matrices, and UI sweet.
From markdown to Slack and Jira's call,
Decisions now travel, large and small.
A carrot-cheered build — let workflows enthrall!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references adding a priority matrix view and fixing a medium item bug, which directly aligns with the main changes in this PR: introducing the Analysis page with the Priority Matrix component and addressing the bug where items with 'medium' impact were filtered out.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/priority-matrix-analysis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 631d43684a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

title: opp.title,
description: opp.description,
impact: opp.impact as 'high' | 'medium' | 'low',
effort: opp.impactScore >= 0.7 ? 'high' : opp.impactScore >= 0.4 ? 'medium' : 'low',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use 0-100 impact score scale when bucketing effort

The analysis pipeline emits impactScore on a 0–100 scale (lib/clustering.ts computes and stores raw percentages), but this mapping treats it as 0–1 (>= 0.7 / >= 0.4). In production data this makes almost every opportunity high effort and distorts quadrant placement, so the priority matrix no longer reflects actual opportunity balance.

Useful? React with 👍 / 👎.

Comment on lines +352 to +356
const handleExport = (format: ExportFormat) => {
const content = updatePreview(format);
if (onExport) {
onExport(format, content);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Implement fallback export when onExport is absent

The Download action only invokes the optional onExport callback, so when that prop is omitted no file is downloaded and no API call is made. Both new usages (app/projects/[id]/decisions/page.tsx and app/projects/[id]/decisions/[decisionId]/page.tsx) render DecisionExportDialog without onExport, which means the primary export button is a no-op for end users.

Useful? React with 👍 / 👎.

return (
<div className="relative">
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate preview when opening via custom trigger

In the custom-trigger branch, opening the dialog only sets isOpen and does not call updatePreview, while preview is initialized as an empty string. On the decisions list page (which uses trigger), the modal opens with a blank preview/copy buffer until the user manually changes format, making the initial export state misleading.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts:
- Around line 86-94: The Content-Disposition filename is built from
user-controlled decision.title (see filename constant and the NextResponse
headers) and can inject headers; sanitize the title before using it by stripping
or replacing unsafe characters (quotes, newlines, carriage returns, slashes,
backslashes, control chars) and collapsing whitespace, restrict to a safe
whitelist (alphanumerics, dash, underscore), fallback to a safe default like
"decision" when the sanitized result is empty, and then use that sanitized value
when building the filename for the Content-Disposition header in the
NextResponse.

In `@app/projects/`[id]/analysis/page.tsx:
- Line 121: The server-rendered call new
Date(analysisContent.generatedAt).toLocaleString() will format using the
server's locale/timezone; change this to a client-side render or a deterministic
UTC format. Either move the date rendering into a small client component (e.g.,
a new ClientDate component that receives analysisContent.generatedAt and uses
toLocaleString() or Intl.DateTimeFormat in the browser) and replace the inline
"Generated {new Date(analysisContent.generatedAt).toLocaleString()}" with that
component, or format on the server using a stable UTC representation (e.g., new
Date(analysisContent.generatedAt).toISOString() or Intl.DateTimeFormat('en-GB',
{timeZone: 'UTC', ...}) ) to ensure users see consistent times; locate the usage
by the token analysisContent.generatedAt in page.tsx and update accordingly.
- Line 133: The summary stat grid uses a fixed "grid-cols-4" which will be
cramped on small viewports; update the container (the div with className
containing "grid grid-cols-4 gap-4") to use responsive Tailwind column classes
(for example replace the fixed grid-cols-4 with a set like grid-cols-1
sm:grid-cols-2 lg:grid-cols-4) so it collapses to fewer columns on mobile while
preserving the 4-column layout on large screens and keep the gap utility intact.
- Around line 92-101: The mapping that builds `opportunities` incorrectly
derives `effort` from `impactScore` using 0.7/0.4 thresholds (in the
`opportunities = analysisContent.opportunities.map((opp) => ...)` block), but
`impactScore` is 0–100; either add a proper orthogonal `effortScore` in the
clustering output (update lib/clustering.ts to compute and emit `effortScore`
alongside `impactScore` and then map `effort` from `opp.effortScore`), or if you
need a hotfix keep current shape and fix thresholds to the 0–100 scale (e.g.,
use `opp.impactScore >= 70 ? 'high' : opp.impactScore >= 40 ? 'medium' : 'low'`)
and document this is temporary; update the mapping to reference the new
`effortScore` field once implemented.

In `@app/projects/`[id]/decisions/[decisionId]/page.tsx:
- Line 62: The code uses decision.status.replace("_", " ") which only replaces
the first underscore; update the rendering in page.tsx to replace all
underscores (e.g., use decision.status.replaceAll("_", " ") or
decision.status.replace(/_/g, " ")) so statuses like "ready_for_handoff" become
"ready for handoff"; locate the occurrence of decision.status.replace("_", " ")
and replace it with a global-replacement approach.

In `@app/projects/`[id]/decisions/page.tsx:
- Line 120: The code uses decision.status.replace("_", " ") which only replaces
the first underscore; update the transform in the component rendering (where
decision.status is used in page.tsx) to replace all underscores—either call
.replaceAll("_", " ") on decision.status or use .replace(/_/g, " ")—so
multi-underscore statuses like "ready_for_handoff" render as "ready for
handoff".

In `@components/DecisionExportDialog.tsx`:
- Around line 362-377: When opening the dialog via the custom trigger prop the
code calls setIsOpen(true) but doesn't populate the preview state; update the
onClick handler for the trigger branch in the DecisionExportDialog component to
also call updatePreview(selectedFormat) (same behavior as the default Button
path) or call a shared openDialog function that invokes setIsOpen(true) and
updatePreview(selectedFormat); reference setIsOpen, updatePreview,
selectedFormat and the trigger JSX branch so the preview is generated when the
dialog opens.
- Around line 379-468: The modal lacks accessibility features; replace the
custom overlay block (the JSX guarded by isOpen and the elements
Card/CardHeader/CardTitle) with an accessible Dialog implementation (use
`@radix-ui/react-dialog` or the project's shadcn/ui Dialog) so it provides
Escape-to-close, backdrop click-to-close, focus trap, and proper ARIA attributes
(role="dialog"/aria-modal/aria-labelledby wired to CardTitle); ensure setIsOpen
is used for onOpenChange (or Dialog onClose) and keep existing handlers like
handleFormatChange, handleExport and selectedFormat logic intact while removing
the manual overlay div and its content in favor of the Dialog component.

In `@components/PriorityMatrix.tsx`:
- Line 96: Replace the invalid Tailwind opacity modifiers that use currentColor
(e.g., the p element using "text-current/70" and the other occurrences
"border-current/20" and "ring-current/50") with a combination of an explicit
color/current token plus a separate opacity utility or a concrete color class;
for example, use "text-current opacity-70" or a specific color like
"text-gray-500" and similarly "border-current border-opacity-20" or
"border-gray-200" and "ring-current ring-opacity-50" or "ring-<color>-50" so the
opacity is applied correctly at runtime; update the className strings in
PriorityMatrix.tsx where those symbols appear.
- Line 195: The span in the PriorityMatrix component uses the HTML attribute
"class" instead of JSX "className"; update the <span class="w-3 h-3 rounded-full
bg-red-500"></span> to use className (and scan the PriorityMatrix component for
any other occurrences of "class" in JSX and replace them with "className") so
JSX is valid and the console warning is eliminated.
- Around line 44-52: getQuadrantKey currently treats only 'high' impact and only
'low' effort as positive, causing all 'medium' values to fall into q4; update
the logic in getQuadrantKey (and references to PriorityItem) so "medium" counts
as the higher-side threshold (e.g., consider impact !== 'low' as highImpact and
effort !== 'high' as lowEffort) and then keep the existing quadrant branches
(q1: highImpact && lowEffort, q2: highImpact && effort === 'high', q3:
!highImpact && lowEffort, else q4); adjust names/comments if needed to reflect
the new threshold semantics.
🧹 Nitpick comments (8)
app/projects/[id]/decisions/page.tsx (2)

54-57: Filter button is a non-functional placeholder.

The Filter button has no onClick handler or linked behavior. Consider either wiring it up or removing it to avoid confusing users.

Would you like me to open an issue to track implementing the filter functionality?


12-31: Two sequential Prisma queries could be combined.

The project and its decisions are fetched in two separate queries. You can include decisions in the project query to save a round-trip.

Proposed consolidation
-  const project = await prisma.project.findUnique({
-    where: { id: projectId },
-    select: { name: true },
-  });
-
-  if (!project) {
-    notFound();
-  }
-
-  const decisions = await prisma.decision.findMany({
-    where: { projectId },
-    orderBy: { createdAt: "desc" },
-  });
+  const project = await prisma.project.findUnique({
+    where: { id: projectId },
+    select: {
+      name: true,
+      decisions: {
+        orderBy: { createdAt: "desc" },
+      },
+    },
+  });
+
+  if (!project) {
+    notFound();
+  }
+
+  const decisions = project.decisions;
app/api/projects/[id]/decisions/[decisionId]/export/route.ts (3)

1-7: Server route imports from a "use client" module.

DecisionExportDialog.tsx is marked "use client". Importing its pure functions and types from a server-side route handler works today but is fragile and architecturally incorrect — the formatting utilities and types should live in a shared module (e.g., lib/decision-export.ts) that both the client component and server route import from.


16-33: Unsafe cast and duplicated format list.

Line 16 casts the query param directly to ExportFormat before validation. The validFormats array on lines 19–27 also duplicates the ExportFormat type definition. If a format is added to the type but not the array (or vice versa), they silently diverge.

Consider validating against the source of truth:

Proposed approach
+import { EXPORT_FORMATS } from "@/lib/decision-export"; // after extraction
+
+const validFormats = EXPORT_FORMATS.map((f) => f.id);
 const format = searchParams.get("format") || "markdown";
-const validFormats = ["cursor", "linear", "jira", "slack", "notion", "markdown", "json"];
 if (!validFormats.includes(format)) {
   return NextResponse.json(
     { error: `Invalid format. Valid formats: ${validFormats.join(", ")}` },
     { status: 400 }
   );
 }

49-55: 403 response leaks the existence of a decision across projects.

Returning 403 when decision.projectId !== projectId tells an attacker the decision exists but belongs to another project. Returning 404 in both cases avoids this information disclosure.

Proposed fix
-    if (decision.projectId !== projectId) {
-      return NextResponse.json(
-        { error: "Decision does not belong to this project" },
-        { status: 403 }
-      );
-    }
+    if (decision.projectId !== projectId) {
+      return NextResponse.json(
+        { error: "Decision not found" },
+        { status: 404 }
+      );
+    }
components/PriorityMatrix.tsx (1)

146-146: Fixed grid height may clip content on small screens.

h-[450px] on the grid container doesn't adapt to viewport height. On smaller screens or when quadrants have many items, the bottom row may be clipped without scrollability at the grid level.

app/projects/[id]/analysis/page.tsx (1)

76-90: No runtime validation of analysis.content — unsafe cast.

analysis.content is cast to a specific shape without any validation. If the stored JSON doesn't match (e.g., missing opportunities, different field names from an older analysis version), this will produce runtime errors or silently render undefined values.

Consider adding a guard or using a schema validation library (e.g., Zod) to parse and validate the content.

app/projects/[id]/decisions/[decisionId]/page.tsx (1)

152-156: Read-only checkbox has no checked state — always appears unchecked.

The checkbox is readOnly but never set to checked, so it's always unchecked. If it's meant as a visual indicator of criteria (not interactive), consider using a styled list marker or disabled checkbox instead to make the non-interactive intent clearer.

Comment on lines +86 to +94
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;

return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Content-Disposition header injection via unsanitized decision.title.

The filename is derived from user-controlled decision.title without sanitizing characters like ", \n, or /. A crafted title could break or inject into the Content-Disposition header.

Proposed fix — sanitize the filename
-    const filename = `decision-${decision.title
-      .toLowerCase()
-      .replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
+    const safeSlug = decision.title
+      .toLowerCase()
+      .replace(/[^a-z0-9]+/g, "-")
+      .replace(/^-|-$/g, "")
+      .slice(0, 100);
+    const ext = format === "markdown" ? "md" : format;
+    const filename = `decision-${safeSlug}.${ext}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
const safeSlug = decision.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 100);
const ext = format === "markdown" ? "md" : format;
const filename = `decision-${safeSlug}.${ext}`;
return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 86
- 94, The Content-Disposition filename is built from user-controlled
decision.title (see filename constant and the NextResponse headers) and can
inject headers; sanitize the title before using it by stripping or replacing
unsafe characters (quotes, newlines, carriage returns, slashes, backslashes,
control chars) and collapsing whitespace, restrict to a safe whitelist
(alphanumerics, dash, underscore), fallback to a safe default like "decision"
when the sanitized result is empty, and then use that sanitized value when
building the filename for the Content-Disposition header in the NextResponse.

Comment on lines +92 to +101
const opportunities: OpportunityItem[] = analysisContent.opportunities.map((opp) => ({
id: opp.id,
title: opp.title,
description: opp.description,
impact: opp.impact as 'high' | 'medium' | 'low',
effort: opp.impactScore >= 0.7 ? 'high' : opp.impactScore >= 0.4 ? 'medium' : 'low',
impactScore: opp.impactScore,
feedbackCount: opp.feedbackCount,
category: opp.category,
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the analysis page file
find . -name "page.tsx" -path "*/projects/*/analysis/*" -type f

Repository: Fresh-Industries/pm-analyzer

Length of output: 109


🏁 Script executed:

# Search for opportunity-related type definitions and schemas
rg -n -i "opportunity|type.*opportunity" --type=ts -A 5 -B 2 | head -100

Repository: Fresh-Industries/pm-analyzer

Length of output: 1706


🏁 Script executed:

# Check for effort-related fields in the codebase
rg -n -i "effort" --type=ts -g '!node_modules' -C 3 | head -80

Repository: Fresh-Industries/pm-analyzer

Length of output: 4938


🏁 Script executed:

# Look for where analysisContent or analysis data comes from
rg -n "analysisContent|analysis\.content" --type=ts -g '!node_modules' -C 3 | head -100

Repository: Fresh-Industries/pm-analyzer

Length of output: 4978


🏁 Script executed:

# Look at the complete opportunities array structure in the type cast
sed -n '76,90p' app/projects/[id]/analysis/page.tsx

Repository: Fresh-Industries/pm-analyzer

Length of output: 473


🏁 Script executed:

# Check where analysis data comes from - look at the agent code
rg -n "opportunities.*Array" --type=ts -A 10 -B 2 lib/agents/

Repository: Fresh-Industries/pm-analyzer

Length of output: 54


🏁 Script executed:

# Check the analysis agent to see what opportunity fields it generates
rg -n "opportunity|opportunities" lib/agents/analysis-agent.ts -A 3 -B 1

Repository: Fresh-Industries/pm-analyzer

Length of output: 140


🏁 Script executed:

# Find files that contain analysis generation logic
fd -name "*analyz*" -type f

Repository: Fresh-Industries/pm-analyzer

Length of output: 302


🏁 Script executed:

# Search for where opportunities are generated or processed
rg -n "opportunities" --type=ts lib/ -B 3 -A 3

Repository: Fresh-Industries/pm-analyzer

Length of output: 1919


🏁 Script executed:

# Check if there's an agent or service that generates this analysis
rg -n "class.*Agent|export.*function.*analyz" --type=ts lib/ -A 5

Repository: Fresh-Industries/pm-analyzer

Length of output: 2236


🏁 Script executed:

# Find where analysis content is generated - look at API routes
fd "analyze" --type f

Repository: Fresh-Industries/pm-analyzer

Length of output: 100


🏁 Script executed:

# Check the analysis API route for opportunities generation
rg -n "opportunities|impactScore" --type=ts app/api/

Repository: Fresh-Industries/pm-analyzer

Length of output: 628


🏁 Script executed:

# Look for any clustering or analysis generation utility
rg -n "impactScore|opportunities" lib/clustering.ts -B 3 -A 3

Repository: Fresh-Industries/pm-analyzer

Length of output: 2055


🏁 Script executed:

# Check the full context of the API route that transforms clustering to opportunities
cat -n app/api/projects/[id]/analyze/route.ts | head -80

Repository: Fresh-Industries/pm-analyzer

Length of output: 2919


🏁 Script executed:

# Verify there's no effortScore or effort field anywhere in clustering output
rg -n "effortScore|effort" lib/clustering.ts

Repository: Fresh-Industries/pm-analyzer

Length of output: 54


🏁 Script executed:

# Double-check if there's any comment or TODO about effort in the analysis page
cat -n app/projects/[id]/analysis/page.tsx | sed -n '90,105p'

Repository: Fresh-Industries/pm-analyzer

Length of output: 701


effort is derived from impactScore using incorrect thresholds, breaking the priority matrix.

Line 97 derives effort from impactScore with thresholds >= 0.7 and >= 0.4, but impactScore is a 0-100 normalized value (as generated in lib/clustering.ts line 287). This means nearly all items get classified as 'high' effort, making the Priority Matrix degenerate into a single dimension.

Additionally, effort and impact should be orthogonal axes. The source clustering data (from lib/clustering.ts) computes only impactScore based on feedback volume and customer tier; no effort metric exists. Either:

  1. Add an effortScore field to the clustering output (based on implementation complexity, scope, etc.)
  2. Use a different heuristic for effort (e.g., feedback count, category-based, or separate estimation)

Correct the thresholds immediately if effort must be derived from impactScore (e.g., opp.impactScore >= 70 ? 'high' : ...), but a proper effort metric is strongly recommended.

🤖 Prompt for AI Agents
In `@app/projects/`[id]/analysis/page.tsx around lines 92 - 101, The mapping that
builds `opportunities` incorrectly derives `effort` from `impactScore` using
0.7/0.4 thresholds (in the `opportunities =
analysisContent.opportunities.map((opp) => ...)` block), but `impactScore` is
0–100; either add a proper orthogonal `effortScore` in the clustering output
(update lib/clustering.ts to compute and emit `effortScore` alongside
`impactScore` and then map `effort` from `opp.effortScore`), or if you need a
hotfix keep current shape and fix thresholds to the 0–100 scale (e.g., use
`opp.impactScore >= 70 ? 'high' : opp.impactScore >= 40 ? 'medium' : 'low'`) and
document this is temporary; update the mapping to reference the new
`effortScore` field once implemented.

AI Analysis
</h1>
<p className="text-gray-500">
Generated {new Date(analysisContent.generatedAt).toLocaleString()}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Server-rendered toLocaleString() uses the server's locale, not the user's.

Since this is a server component, new Date(...).toLocaleString() will format using the server's locale/timezone, which may differ from the user's. Consider rendering dates on the client or using a consistent UTC-based format.

🤖 Prompt for AI Agents
In `@app/projects/`[id]/analysis/page.tsx at line 121, The server-rendered call
new Date(analysisContent.generatedAt).toLocaleString() will format using the
server's locale/timezone; change this to a client-side render or a deterministic
UTC format. Either move the date rendering into a small client component (e.g.,
a new ClientDate component that receives analysisContent.generatedAt and uses
toLocaleString() or Intl.DateTimeFormat in the browser) and replace the inline
"Generated {new Date(analysisContent.generatedAt).toLocaleString()}" with that
component, or format on the server using a stable UTC representation (e.g., new
Date(analysisContent.generatedAt).toISOString() or Intl.DateTimeFormat('en-GB',
{timeZone: 'UTC', ...}) ) to ensure users see consistent times; locate the usage
by the token analysisContent.generatedAt in page.tsx and update accordingly.

</div>

{/* Summary Stats */}
<div className="grid grid-cols-4 gap-4">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

grid-cols-4 is not responsive — will be cramped on mobile.

The summary stat cards use a hard 4-column grid. On small screens this will be very tight or overflow.

Proposed fix
-      <div className="grid grid-cols-4 gap-4">
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="grid grid-cols-4 gap-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
🤖 Prompt for AI Agents
In `@app/projects/`[id]/analysis/page.tsx at line 133, The summary stat grid uses
a fixed "grid-cols-4" which will be cramped on small viewports; update the
container (the div with className containing "grid grid-cols-4 gap-4") to use
responsive Tailwind column classes (for example replace the fixed grid-cols-4
with a set like grid-cols-1 sm:grid-cols-2 lg:grid-cols-4) so it collapses to
fewer columns on mobile while preserving the 4-column layout on large screens
and keep the gap utility intact.

decision.status === "handed_off" ? "default" : "secondary"
}
>
{decision.status.replace("_", " ")}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same .replace("_", " ") issue — only replaces the first underscore.

Same issue as in the decisions list page. A status like "ready_for_handoff" will render as "ready for_handoff".

Proposed fix
-              {decision.status.replace("_", " ")}
+              {decision.status.replaceAll("_", " ")}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{decision.status.replace("_", " ")}
{decision.status.replaceAll("_", " ")}
🤖 Prompt for AI Agents
In `@app/projects/`[id]/decisions/[decisionId]/page.tsx at line 62, The code uses
decision.status.replace("_", " ") which only replaces the first underscore;
update the rendering in page.tsx to replace all underscores (e.g., use
decision.status.replaceAll("_", " ") or decision.status.replace(/_/g, " ")) so
statuses like "ready_for_handoff" become "ready for handoff"; locate the
occurrence of decision.status.replace("_", " ") and replace it with a
global-replacement approach.

Comment on lines +362 to +377
<div className="relative">
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preview is empty when dialog is opened via the custom trigger prop.

When the trigger prop is used (line 364), setIsOpen(true) is called but updatePreview is not. The preview state starts as "" (line 337), so users see a blank preview pane. The default button path (line 371) correctly calls updatePreview(selectedFormat), but the trigger path does not.

Proposed fix
       {trigger ? (
-        <div onClick={() => setIsOpen(true)}>{trigger}</div>
+        <div onClick={() => {
+          setIsOpen(true);
+          updatePreview(selectedFormat);
+        }}>{trigger}</div>
       ) : (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="relative">
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
<div className="relative">
{trigger ? (
<div onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}>{trigger}</div>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
🤖 Prompt for AI Agents
In `@components/DecisionExportDialog.tsx` around lines 362 - 377, When opening the
dialog via the custom trigger prop the code calls setIsOpen(true) but doesn't
populate the preview state; update the onClick handler for the trigger branch in
the DecisionExportDialog component to also call updatePreview(selectedFormat)
(same behavior as the default Button path) or call a shared openDialog function
that invokes setIsOpen(true) and updatePreview(selectedFormat); reference
setIsOpen, updatePreview, selectedFormat and the trigger JSX branch so the
preview is generated when the dialog opens.

Comment on lines +379 to +468
{/* Simple modal overlay */}
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<Card className="w-[700px] max-h-[80vh] overflow-hidden flex flex-col">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle>Export Decision</CardTitle>
<CardDescription>
Choose a format and copy or download the export
</CardDescription>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setIsOpen(false)}
>
</Button>
</div>
</CardHeader>

<CardContent className="flex-1 overflow-hidden flex flex-col gap-4">
{/* Format selector */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium">Format:</label>
<Select value={selectedFormat} onValueChange={handleFormatChange}>
<SelectTrigger className="w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EXPORT_FORMATS.map((format) => (
<SelectItem key={format.id} value={format.id}>
<div className="flex items-center gap-2">
<span className={format.color}>{format.icon}</span>
<span>{format.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>

<div className="ml-auto flex items-center gap-2">
<span
className={cn(
"px-2 py-1 rounded text-xs font-medium",
selectedConfig?.color
)}
>
{selectedConfig?.description}
</span>
</div>
</div>

{/* Preview area */}
<div className="flex-1 overflow-hidden border rounded-lg bg-gray-50 dark:bg-gray-900">
<pre className="p-4 text-sm overflow-auto h-[300px] whitespace-pre-wrap font-mono">
{preview}
</pre>
</div>

{/* Action buttons */}
<div className="flex items-center justify-between pt-2">
<div className="text-xs text-gray-500">
{decision.linkedFeedbackIds.length > 0 && (
<span>
Links to {decision.linkedFeedbackIds.length} feedback items
</span>
)}
</div>
<div className="flex gap-2">
<CopyButton
value={preview}
label="Copy"
copiedLabel="Copied!"
className="px-3 py-1.5 text-sm border rounded hover:bg-gray-50"
/>
<Button
size="sm"
onClick={() => handleExport(selectedFormat)}
>
<Download className="w-4 h-4 mr-1" />
Download
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if a Dialog component already exists in the project
fd -t f 'dialog' --ignore-case -e tsx -e ts | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 107


🏁 Script executed:

# Also search for shadcn/ui or Dialog imports
rg -i "from.*dialog|import.*dialog" --type ts --type tsx | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 99


🏁 Script executed:

# Check the full DecisionExportDialog implementation
cat components/DecisionExportDialog.tsx | head -100

Repository: Fresh-Industries/pm-analyzer

Length of output: 2136


🏁 Script executed:

# Search for Dialog components in the ui directory
fd -type f . components/ui | grep -i dialog

Repository: Fresh-Industries/pm-analyzer

Length of output: 242


🏁 Script executed:

# Check what's available in the ui components folder
ls -la components/ui/ | head -30

Repository: Fresh-Industries/pm-analyzer

Length of output: 782


🏁 Script executed:

# Read the full DecisionExportDialog.tsx file to see the complete implementation
wc -l components/DecisionExportDialog.tsx

Repository: Fresh-Industries/pm-analyzer

Length of output: 111


🏁 Script executed:

# Search for dialog component imports using different approach
rg "Dialog" --type typescript | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 106


🏁 Script executed:

# Check package.json for UI library
cat package.json | jq '.dependencies, .devDependencies' | head -40

Repository: Fresh-Industries/pm-analyzer

Length of output: 1074


🏁 Script executed:

# Read the DecisionExportDialog.tsx file to see the implementation around lines 379-468
tail -n +350 components/DecisionExportDialog.tsx | head -n 150

Repository: Fresh-Industries/pm-analyzer

Length of output: 4486


🏁 Script executed:

# Search for keyboard/escape handling in the file
rg -i "escape|keydown|keypress|backdrop|focus" components/DecisionExportDialog.tsx

Repository: Fresh-Industries/pm-analyzer

Length of output: 54


🏁 Script executed:

# Check if there's any useEffect or event listener setup
rg "useEffect|addEventListener|onKeyDown|onKeyUp" components/DecisionExportDialog.tsx

Repository: Fresh-Industries/pm-analyzer

Length of output: 54


🏁 Script executed:

# Check the entire component structure looking for event handlers on the overlay div
rg -A 5 "className=\"fixed inset-0" components/DecisionExportDialog.tsx

Repository: Fresh-Industries/pm-analyzer

Length of output: 430


Add missing accessibility features to the modal.

The current custom modal implementation is missing critical accessibility features:

  • No Escape key handler to close the modal
  • No backdrop click handler — the overlay div has no click listener
  • No focus trap — keyboard focus can escape the modal
  • No ARIA attributes — missing role="dialog", aria-modal="true", and aria-labelledby to connect the title

The project already has Radix UI available (@radix-ui/react-select is installed). Consider replacing this custom implementation with @radix-ui/react-dialog (add to dependencies) or shadcn/ui's Dialog component wrapper, which handles all of these features automatically and is WCAG-compliant.

🤖 Prompt for AI Agents
In `@components/DecisionExportDialog.tsx` around lines 379 - 468, The modal lacks
accessibility features; replace the custom overlay block (the JSX guarded by
isOpen and the elements Card/CardHeader/CardTitle) with an accessible Dialog
implementation (use `@radix-ui/react-dialog` or the project's shadcn/ui Dialog) so
it provides Escape-to-close, backdrop click-to-close, focus trap, and proper
ARIA attributes (role="dialog"/aria-modal/aria-labelledby wired to CardTitle);
ensure setIsOpen is used for onOpenChange (or Dialog onClose) and keep existing
handlers like handleFormatChange, handleExport and selectedFormat logic intact
while removing the manual overlay div and its content in favor of the Dialog
component.

Comment on lines +44 to +52
function getQuadrantKey(item: PriorityItem): 'q1' | 'q2' | 'q3' | 'q4' {
const highImpact = item.impact === 'high';
const lowEffort = item.effort === 'low';

if (highImpact && lowEffort) return 'q1'; // Quick Wins
if (highImpact && item.effort === 'high') return 'q2'; // Major Projects
if (!highImpact && lowEffort) return 'q3'; // Fillers
return 'q4'; // Thankless Tasks (low impact + high effort)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Medium-effort/impact items are misclassified into Thankless Tasks (q4).

Tracing all 9 combinations through getQuadrantKey:

Impact Effort Quadrant
high low q1 ✅ Quick Wins
high medium q4 ❌ Thankless Tasks
high high q2 ✅ Major Projects
medium low q3 Fillers
medium medium q4 ❌ Thankless Tasks
medium high q4 Thankless Tasks
low low q3 Fillers
low medium q4 Thankless Tasks
low high q4 Thankless Tasks

A high-impact, medium-effort item landing in "Thankless Tasks (Avoid)" is clearly wrong — this is the very bug the PR aims to fix. The root cause is that highImpact only matches 'high' and lowEffort only matches 'low', so 'medium' on either axis falls through to q4.

Proposed fix — treat medium as a threshold rather than a discard
 function getQuadrantKey(item: PriorityItem): 'q1' | 'q2' | 'q3' | 'q4' {
-  const highImpact = item.impact === 'high';
-  const lowEffort = item.effort === 'low';
-  
-  if (highImpact && lowEffort) return 'q1'; // Quick Wins
-  if (highImpact && item.effort === 'high') return 'q2'; // Major Projects
-  if (!highImpact && lowEffort) return 'q3'; // Fillers
-  return 'q4'; // Thankless Tasks (low impact + high effort)
+  const highImpact = item.impact === 'high' || item.impact === 'medium';
+  const lowEffort = item.effort === 'low' || item.effort === 'medium';
+  
+  if (highImpact && lowEffort) return 'q1';  // Quick Wins
+  if (highImpact && !lowEffort) return 'q2'; // Major Projects
+  if (!highImpact && lowEffort) return 'q3'; // Fillers
+  return 'q4';                               // Thankless Tasks
 }

This ensures medium values are distributed sensibly rather than dumped into q4. Adjust thresholds as needed for your product's semantics.

🤖 Prompt for AI Agents
In `@components/PriorityMatrix.tsx` around lines 44 - 52, getQuadrantKey currently
treats only 'high' impact and only 'low' effort as positive, causing all
'medium' values to fall into q4; update the logic in getQuadrantKey (and
references to PriorityItem) so "medium" counts as the higher-side threshold
(e.g., consider impact !== 'low' as highImpact and effort !== 'high' as
lowEffort) and then keep the existing quadrant branches (q1: highImpact &&
lowEffort, q2: highImpact && effort === 'high', q3: !highImpact && lowEffort,
else q4); adjust names/comments if needed to reflect the new threshold
semantics.

<div className="space-y-2 flex-grow overflow-y-auto max-h-[200px] pr-2">
{data.length === 0 ? (
<p className="text-xs italic text-current/80">No items categorized here.</p>
<p className="text-xs italic text-current/70">No items in this quadrant.</p>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tailwind opacity modifiers on currentColor may not work.

text-current/70, border-current/20, and ring-current/50 use opacity modifiers with currentColor. Tailwind's opacity modifiers decompose colors into RGB channels at build time, but currentColor is resolved at render time, so the opacity modifier is silently ignored or produces invalid CSS.

Consider using explicit opacity utilities (e.g., opacity-70) or concrete color classes instead.

Also applies to: 101-101

🤖 Prompt for AI Agents
In `@components/PriorityMatrix.tsx` at line 96, Replace the invalid Tailwind
opacity modifiers that use currentColor (e.g., the p element using
"text-current/70" and the other occurrences "border-current/20" and
"ring-current/50") with a combination of an explicit color/current token plus a
separate opacity utility or a concrete color class; for example, use
"text-current opacity-70" or a specific color like "text-gray-500" and similarly
"border-current border-opacity-20" or "border-gray-200" and "ring-current
ring-opacity-50" or "ring-<color>-50" so the opacity is applied correctly at
runtime; update the className strings in PriorityMatrix.tsx where those symbols
appear.

Fillers (Nice to Have)
</div>
<div className="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-red-500"></span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

class should be className in JSX.

Line 195 uses class instead of className. While modern React may still render this, it's incorrect JSX and will produce a console warning.

Proposed fix
-          <span class="w-3 h-3 rounded-full bg-red-500"></span>
+          <span className="w-3 h-3 rounded-full bg-red-500"></span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span class="w-3 h-3 rounded-full bg-red-500"></span>
<span className="w-3 h-3 rounded-full bg-red-500"></span>
🤖 Prompt for AI Agents
In `@components/PriorityMatrix.tsx` at line 195, The span in the PriorityMatrix
component uses the HTML attribute "class" instead of JSX "className"; update the
<span class="w-3 h-3 rounded-full bg-red-500"></span> to use className (and scan
the PriorityMatrix component for any other occurrences of "class" in JSX and
replace them with "className") so JSX is valid and the console warning is
eliminated.

- Created new decision creation page at /projects/[id]/decisions/new
- Added form with title, summary, scope, non-goals, acceptance criteria, risks, and confidence slider
- Fixed API bug: confidence field was using wrong name
- Added UI components: Slider, Alert, Textarea
- Added Create Decision buttons to analysis opportunities list
- Fixed decision API to use confidenceScore (Float) instead of confidence (String)
@nikman21
Copy link
Contributor Author

nikman21 commented Feb 9, 2026

Additional Changes (Nightly Build Session)

I've added the missing Create Decision page to complete the workflow!

New Features Added

  1. Create Decision Page ()

    • Full form with title, summary, scope, non-goals, acceptance criteria, risks
    • Confidence slider (0-100%) with visual feedback
    • Linked feedback IDs support
  2. UI Components

      • Radix UI slider component
      • Error/feedback alerts
      • Multi-line text input
  3. Bug Fix

    • Fixed API: → (Float, matching schema)

How to Test

  1. Go to Analysis → Click "Create Decision"
  2. Fill the form with your decision details
  3. Submit and see your decision created!

This completes the feedback → analysis → decision workflow.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/projects/[id]/decisions/route.ts (1)

28-56: ⚠️ Potential issue | 🔴 Critical

Add acceptanceCriteria to destructuring and Prisma payload

The Prisma Decision model includes the acceptanceCriteria field (acceptance_criteria in the database), and the form correctly sends this field in the request (line 51 of app/projects/[id]/decisions/new/page.tsx). However, the route handler at app/api/projects/[id]/decisions/route.ts never destructures it from body nor passes it to prisma.decision.create, causing the user's input to be silently lost.

Fix
     const {
       title,
       summary,
       scope,
       risks,
       nonGoals,
       confidence,
+      acceptanceCriteria,
       linkedFeedbackIds,
     } = body;
         nonGoals: nonGoals || "",
+        acceptanceCriteria: acceptanceCriteria || "",
         confidenceScore: confidence ?? 0.5,
🤖 Fix all issues with AI agents
In `@app/api/projects/`[id]/decisions/route.ts:
- Line 53: The line setting confidenceScore currently uses the logical OR which
treats 0 as falsy; in the code that constructs the decision object replace the
fallback expression confidence || 0.5 with the nullish coalescing operator
(confidence ?? 0.5) so a valid 0 value is preserved; locate the place where
confidenceScore is assigned (the confidenceScore: confidence || 0.5 entry) and
change it to use ??, keeping the rest of the decision construction intact.

In `@components/ui/alert.tsx`:
- Around line 37-46: AlertTitle's forwarded ref and props are typed for
HTMLParagraphElement but the component renders an <h5>, causing a type mismatch;
update the generic types on React.forwardRef for AlertTitle to use
HTMLHeadingElement (and React.HTMLAttributes<HTMLHeadingElement>) so the ref and
props match the rendered <h5>, ensuring correct autocomplete and type narrowing
for consumers.
- Around line 49-58: AlertDescription currently forwards a ref typed as
HTMLParagraphElement but renders a <div>, causing a ref type mismatch; fix by
making the forwarded ref and props match the rendered element: update the
generic types on React.forwardRef to HTMLDivElement and
React.HTMLAttributes<HTMLDivElement> (or alternatively change the rendered
element to <p> if a paragraph is intended) so that the ref type, prop types, and
the rendered DOM element are consistent; locate AlertDescription in
components/ui/alert.tsx and adjust the type generics accordingly.

In `@components/ui/slider.tsx`:
- Line 23: Thumb element has conflicting border-color utilities causing dead
CSS; update the className on SliderPrimitive.Thumb to remove the redundant
utilities by deleting border-slate-200 and dark:border-slate-800 so only the
intended border colors (border-slate-900 and dark:border-slate-50) remain.
🧹 Nitpick comments (3)
components/ui/slider.tsx (1)

8-26: forwardRef is no longer required in React 19 — optional modernization.

Since the project uses React 19.2.3, ref can be received as a regular prop, making forwardRef unnecessary. This is entirely optional and can be deferred.

app/projects/[id]/decisions/new/page.tsx (2)

61-64: response.json() may throw on non-JSON error responses.

If the server returns a non-JSON body (e.g., an HTML 502 from a proxy), response.json() will throw a SyntaxError. The outer catch handles it, but the resulting error message ("Unexpected token < in JSON...") is unhelpful to the user.

🛡️ Safer approach
       if (!response.ok) {
-        const data = await response.json();
-        throw new Error(data.error || "Failed to create decision");
+        let message = "Failed to create decision";
+        try {
+          const data = await response.json();
+          message = data.error || message;
+        } catch {
+          // non-JSON response body — use generic message
+        }
+        throw new Error(message);
       }

70-70: Prefer unknown over any for catch clause.

Using err: any bypasses type safety. With unknown, the err.message access on line 71 would need a type guard — but this is the idiomatic TypeScript approach.

♻️ Suggested change
-    } catch (err: any) {
-      setError(err.message || "An error occurred while creating the decision");
+    } catch (err: unknown) {
+      const message = err instanceof Error ? err.message : "An error occurred while creating the decision";
+      setError(message);
     } finally {

risks: risks || "",
nonGoals: nonGoals || "",
confidence: confidence || "medium",
confidenceScore: confidence || 0.5,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: confidence || 0.5 treats 0 as missing — use nullish coalescing.

If the client sends confidence: 0 (a valid 0% confidence score), the || operator evaluates it as falsy and substitutes 0.5. Use ?? to only fall back when the value is null or undefined.

🐛 Proposed fix
-        confidenceScore: confidence || 0.5,
+        confidenceScore: confidence ?? 0.5,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
confidenceScore: confidence || 0.5,
confidenceScore: confidence ?? 0.5,
🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/route.ts at line 53, The line setting
confidenceScore currently uses the logical OR which treats 0 as falsy; in the
code that constructs the decision object replace the fallback expression
confidence || 0.5 with the nullish coalescing operator (confidence ?? 0.5) so a
valid 0 value is preserved; locate the place where confidenceScore is assigned
(the confidenceScore: confidence || 0.5 entry) and change it to use ??, keeping
the rest of the decision construction intact.

Comment on lines +37 to +46
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ref type mismatch: HTMLParagraphElement used but renders <h5>.

AlertTitle forwards a ref typed as HTMLParagraphElement, yet it renders an <h5> element (which is HTMLHeadingElement). A consumer using ref.current would get incorrect autocomplete and type narrowing.

🔧 Proposed fix
 const AlertTitle = React.forwardRef<
-  HTMLParagraphElement,
+  HTMLHeadingElement,
   React.HTMLAttributes<HTMLHeadingElement>
 >(({ className, ...props }, ref) => (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
const AlertTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
🤖 Prompt for AI Agents
In `@components/ui/alert.tsx` around lines 37 - 46, AlertTitle's forwarded ref and
props are typed for HTMLParagraphElement but the component renders an <h5>,
causing a type mismatch; update the generic types on React.forwardRef for
AlertTitle to use HTMLHeadingElement (and
React.HTMLAttributes<HTMLHeadingElement>) so the ref and props match the
rendered <h5>, ensuring correct autocomplete and type narrowing for consumers.

Comment on lines +49 to +58
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ref type mismatch: HTMLParagraphElement used but renders <div>.

AlertDescription forwards a ref typed as HTMLParagraphElement, but renders a <div> (HTMLDivElement).

🔧 Proposed fix
 const AlertDescription = React.forwardRef<
-  HTMLParagraphElement,
+  HTMLDivElement,
-  React.HTMLAttributes<HTMLParagraphElement>
+  React.HTMLAttributes<HTMLDivElement>
 >(({ className, ...props }, ref) => (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
const AlertDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
🤖 Prompt for AI Agents
In `@components/ui/alert.tsx` around lines 49 - 58, AlertDescription currently
forwards a ref typed as HTMLParagraphElement but renders a <div>, causing a ref
type mismatch; fix by making the forwarded ref and props match the rendered
element: update the generic types on React.forwardRef to HTMLDivElement and
React.HTMLAttributes<HTMLDivElement> (or alternatively change the rendered
element to <p> if a paragraph is intended) so that the ref type, prop types, and
the rendered DOM element are consistent; locate AlertDescription in
components/ui/alert.tsx and adjust the type generics accordingly.

<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<SliderPrimitive.Range className="absolute h-full bg-slate-900 dark:bg-slate-50" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-200 border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Conflicting border-color utilities on the Thumb element.

border-slate-200 is immediately overridden by border-slate-900 (and dark:border-slate-800 by dark:border-slate-50). Whether resolved by CSS cascade or twMerge, the first color in each pair is dead code and only adds confusion. Remove the redundant classes.

🧹 Proposed cleanup
-    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-200 border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
+    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-200 border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
🤖 Prompt for AI Agents
In `@components/ui/slider.tsx` at line 23, Thumb element has conflicting
border-color utilities causing dead CSS; update the className on
SliderPrimitive.Thumb to remove the redundant utilities by deleting
border-slate-200 and dark:border-slate-800 so only the intended border colors
(border-slate-900 and dark:border-slate-50) remain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant