Skip to content

Comments

feat: Quick Decision Builder with bulk selection#6

Open
nikman21 wants to merge 5 commits intomainfrom
feature/quick-decision-builder
Open

feat: Quick Decision Builder with bulk selection#6
nikman21 wants to merge 5 commits intomainfrom
feature/quick-decision-builder

Conversation

@nikman21
Copy link
Contributor

@nikman21 nikman21 commented Feb 14, 2026

What I Built

Added a Quick Decision Builder feature that streamlines the workflow from feedback to decisions.

New Components

  1. QuickDecisionDialog.tsx - A modal dialog for creating decisions from selected feedback items

    • Pre-fills title, summary, and scope from selected feedback
    • Shows total potential revenue impact
    • Counts bugs vs features in selection
    • Links all selected feedback to the new decision
  2. FeedbackList.tsx - Enhanced with bulk selection

    • Checkboxes to select multiple feedback items
    • Revenue impact badges ($$$/mo) on each item
    • Customer tier badges (Enterprise/Pro/Starter)
    • Selection header showing count and total revenue
    • "Create Decision" button when items are selected

Key Features

  • Bulk Selection: Select multiple feedback items via checkboxes
  • Revenue Badges: Each feedback item shows potential revenue impact
  • Tier Badges: Visual indicators for customer tiers
  • Impact Summary: Header shows total selected items and combined revenue
  • Pre-filled Form: Decision title, summary, and scope auto-populate from feedback
  • One-click Linking: All selected feedback IDs are linked to the decision

How to Test

  1. Go to a project with feedback items
  2. Select 2-3 items using the checkboxes
  3. Click "Create Decision" in the selection header
  4. Verify the dialog pre-fills with feedback content
  5. Submit the decision and verify feedback is linked

Why It Matters

This feature bridges the gap between feedback analysis and decisions. Instead of manually copying feedback IDs and content, users can:

  • See the revenue impact of their decisions at a glance
  • Create decisions in one click from feedback
  • Maintain traceability between feedback and decisions

Preview:

Before After
Manual copy-paste workflow One-click selection
No revenue visibility Revenue badges on every item
Disconnected feedback-decision link Automatic linking

📊 Revenue Impact: Now visible at selection time, not just in analysis

🎯 Workflow: Select → Create Decision → Done (3 clicks vs 15+)

Summary by CodeRabbit

Release Notes

  • New Features
    • Decision management: Create, view, and manage project decisions with detailed scoping and acceptance criteria.
    • Multi-format export: Export decisions to Cursor, Linear, Jira, Slack, Notion, Markdown, or JSON.
    • AI-driven analysis page: View prioritized opportunities with Eisenhower Priority Matrix and actionable insights.
    • Quick decision builder: Create decisions directly from feedback with pre-populated details.
    • Enhanced feedback selection: Select multiple feedback items with revenue and tier visibility for bulk decision creation.

## 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
- 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)
- Added Quote icon import from lucide-react
- Added handleCopyAsQuote function that formats feedback as a markdown quote
- Added copiedQuoteId state to track quote copy status
- Added new Copy Quote button next to Copy for Cursor button
- Quote format: > "feedback text" >\n> — {tier} customer via {source}
- Add QuickDecisionDialog component for creating decisions from selected feedback
- Update FeedbackList with checkbox selection and bulk actions
- Show revenue impact badges ($/mo) on feedback items
- Show customer tier badges (Enterprise/Pro/Starter)
- Calculate total potential revenue when items are selected
- Pre-fill decision form with linked feedback content
@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive decision management system with creation, viewing, and export capabilities. It adds pages for listing, creating, and analyzing decisions; implements export to seven formats (cursor, linear, jira, slack, notion, markdown, json); enhances feedback selection with multi-item support; and includes new UI components for forms, dialogs, and priority visualization.

Changes

Cohort / File(s) Summary
Decision Management API
app/api/projects/[id]/decisions/route.ts, app/api/projects/[id]/decisions/[decisionId]/export/route.ts
Adds POST handler to create decisions with numeric confidenceScore field (default 0.5); introduces GET export endpoint supporting seven formats with validation, decision fetching, and format-specific response headers.
Decision Pages
app/projects/[id]/decisions/page.tsx, app/projects/[id]/decisions/[decisionId]/page.tsx, app/projects/[id]/decisions/new/page.tsx
Adds decision listing page with filtering/export; detail page with confidence visualization and export; creation page with form handling, validation, and redirect on success.
Analysis Page
app/projects/[id]/analysis/page.tsx
Introduces AI-driven analysis view with feedback metrics, opportunity categorization, sorting by impact, Eisenhower matrix display, and unclustered feedback section.
Decision Export System
components/DecisionExportDialog.tsx
Exports comprehensive decision data model and seven format functions (cursor, linear, jira, slack, notion, markdown, json) with live preview dialog UI featuring format selector, description badges, and copy/download actions.
Feedback & Interaction Enhancements
components/FeedbackList.tsx, components/QuickDecisionDialog.tsx, components/PriorityMatrix.tsx
Extends FeedbackList with multi-item selection, revenue/tier badges, copy-as-quote actions; adds QuickDecisionDialog for rapid decision creation from feedback; refactors PriorityMatrix with quadrant categorization, intra-quadrant sorting, and balance metrics.
UI Primitives
components/ui/alert.tsx, components/ui/slider.tsx, components/ui/textarea.tsx
Adds reusable Alert component with variants, Slider wrapper around Radix UI primitives, and styled Textarea component for form use.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Dialog as DecisionExportDialog
    participant API as /api/decisions/[id]/export
    participant DB as Prisma Database
    participant Formatter as formatDecision()

    User->>Dialog: Opens export dialog
    Dialog->>Dialog: Display format options
    User->>Dialog: Selects format (e.g., 'jira')
    Dialog->>Dialog: Live preview updates
    User->>Dialog: Clicks Download
    Dialog->>API: GET ?format=jira
    API->>DB: Fetch decision by ID
    DB-->>API: Return decision + project
    API->>API: Validate ownership
    API->>Formatter: formatDecision(data, 'jira')
    Formatter-->>API: Return formatted string
    API-->>Dialog: Return 200 + formatted content
    Dialog-->>User: Trigger file download
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Decisions hop into view,
Export them far and wide—
To Jira, Slack, and Linear too,
With formats by your side!
From feedback blooms a choice so clear, ✨📊

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Quick Decision Builder with bulk selection' accurately summarizes the main changes—adding a Quick Decision Builder component with bulk selection capability to FeedbackList, which are the primary features of this PR.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ 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/quick-decision-builder

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: ddfed6af73

ℹ️ 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".

Comment on lines +3 to +7
import {
formatDecision,
ExportFormat,
DecisionExportData,
} from "@/components/DecisionExportDialog";

Choose a reason for hiding this comment

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

P1 Badge Move export formatter out of the client component

This route imports formatDecision from components/DecisionExportDialog.tsx, but that module is marked with "use client" and pulls in client-only React/UI dependencies. Calling a client export from a server route handler is not safe in Next’s server runtime, so /api/projects/[id]/decisions/[decisionId]/export can fail when invoked. The formatter should live in a server-safe shared module (and be imported by both the dialog and the route) instead of importing from a client component file.

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 Add download behavior when no onExport callback is passed

The Download button only calls onExport when that optional prop exists, and otherwise performs no action. In this commit, both usages of DecisionExportDialog omit onExport (app/projects/[id]/decisions/page.tsx and app/projects/[id]/decisions/[decisionId]/page.tsx), so users click “Download” and nothing is exported. Add a fallback path (e.g., direct file download or calling the new export API) when onExport is undefined.

Useful? React with 👍 / 👎.

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

Choose a reason for hiding this comment

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

P2 Badge Preserve explicit zero confidence values

Using confidence || 0.5 overwrites a valid 0 input with 0.5 because 0 is falsy. The new decision forms send confidence on a 0–1 scale from the slider, so choosing 0% confidence is currently stored as 50%, which corrupts saved decision data and downstream exports/displays. Use a nullish check (??) or explicit numeric validation so 0 is persisted correctly.

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: 19

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

acceptanceCriteria is silently dropped — data loss bug.

Both QuickDecisionDialog and the "New Decision" form send acceptanceCriteria in the POST body, but the route handler neither destructures it nor saves it to the database. Users can enter this field, but it is silently discarded.

Additionally, line 53 uses confidence || 0.5, which treats 0 as falsy and incorrectly converts a valid confidence score of 0 to 0.5. Use the nullish coalescing operator instead: confidence ?? 0.5.

Proposed fix
     const {
       title,
       summary,
       scope,
       risks,
       nonGoals,
       confidence,
       linkedFeedbackIds,
+      acceptanceCriteria,
     } = body;
       data: {
         projectId,
         title,
         summary,
         scope: scope || "",
         risks: risks || "",
         nonGoals: nonGoals || "",
+        acceptanceCriteria: acceptanceCriteria || "",
         confidenceScore: confidence ?? 0.5,
         status: "draft",
         linkedFeedbackIds: linkedFeedbackIds || [],
       },
🤖 Fix all issues with AI agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts:
- Around line 86-88: The generated filename built in the filename constant uses
raw decision.title and only strips whitespace, which can introduce quotes,
slashes, backslashes, and other characters that break the Content-Disposition
header; update the logic that builds filename (the filename const) to: sanitize
decision.title by removing or replacing all characters except ASCII letters,
numbers, hyphen and underscore (collapse runs and trim, and cap length to e.g.
100 chars), percent-encode or provide a RFC5987-safe fallback when setting
headers, and normalize extensions so format === "markdown" => ".md" and all
other formats use ".txt" (avoid producing non-standard extensions like
.cursor/.linear). Ensure you reference and change the filename construction and
the code that sets the Content-Disposition header to use the sanitized/encoded
filename.
- Around line 9-103: Add an authentication and authorization check at the start
of the exported GET handler: call getServerSession() from lib/auth-server.ts and
if it returns no session return a 401 JSON response; then verify the signed-in
user is allowed to access the decision by ensuring they belong to the project
(e.g. query the project using prisma (prisma.project.findFirst or findUnique
with membership/owner check) and confirm project.id === projectId and that
session.user.id is an owner/member), and only proceed to load the Decision (or
return 403) if that authorization check passes; keep the existing
decision.projectId === projectId validation as a secondary data check.

In `@app/api/projects/`[id]/decisions/route.ts:
- Line 53: The code sets confidenceScore using the falsy-check operator
(confidence || 0.5), which treats 0 as falsy; update the assignment to use
nullish coalescing so only null/undefined fall back to 0.5 (replace the use of
|| with ?? for the confidence -> confidenceScore mapping), referencing the
confidence variable and confidenceScore field in the route handler that builds
the decision payload.

In `@app/projects/`[id]/analysis/page.tsx:
- Line 133: The grid container currently uses a fixed "grid grid-cols-4 gap-4"
which forces four columns on all viewports; update the className on that div
(the element with "grid grid-cols-4 gap-4") to use responsive Tailwind
breakpoints (for example: single column on xs, then 2 cols on small, 3 on
medium, 4 on large) so it collapses on small screens and expands on larger ones.
- Around line 92-101: The mapping for opportunities incorrectly derives effort
from impactScore in the opportunities.map block (constructing OpportunityItem),
which prevents valid Priority Matrix quadrants; change the logic to source
effort from the analysis payload (use opp.effort if present) and only fallback
to a neutral default like 'medium' (or an independent estimator) instead of
using opp.impactScore; update the mapping in the same function where
opportunities are created and ensure the OpportunityItem type/shape (and any
downstream UI expecting effort) supports this field.

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 to replace all underscores by using
either decision.status.replaceAll("_", " ") or decision.status.replace(/_/g, "
") so statuses like "ready_for_implementation" become "ready for
implementation"; make the same change for the identical use of decision.status
in app/projects/[id]/decisions/page.tsx (the line noted in the review) to ensure
consistency.

In `@app/projects/`[id]/decisions/page.tsx:
- Around line 54-57: The Filter button currently renders without behavior —
update the Button (and related Filter icon) so it is either functional or
clearly disabled: either add an onClick handler that calls a filter action
(e.g., openFilterPanel, toggleFilters, or setShowFilters) and wire that to the
existing filter state/handler in this component, or mark the Button disabled
(disabled prop and aria-disabled) and add a tooltip/title like "Filters coming
soon" so users know it's intentionally inactive; ensure accessibility attributes
are included and use the same Button and Filter symbols shown in the diff.
- Line 120: The status rendering currently uses decision.status.replace("_", "
") which only replaces the first underscore; update the JSX to use
decision.status.replaceAll("_", " ") so all underscores are converted to spaces
(i.e., change the use of String.prototype.replace to replaceAll for the
expression rendering the status).

In `@components/DecisionExportDialog.tsx`:
- Around line 114-143: formatAsCursor can throw if string fields are
null/undefined; update it to use defensive defaults for all optional fields
referenced (e.g., acceptanceCriteria, scope, risks, nonGoals, title, summary)
and for confidenceScore and linkedFeedbackIds. Replace direct uses like
data.acceptanceCriteria.split("\n") with (data.acceptanceCriteria ||
"").split("\n"), use (data.scope || "") etc., use (data.confidenceScore ?? 0)
for the percentage calc, and ensure linkedFeedbackIds is treated as
(data.linkedFeedbackIds || []) when checking length so the function never calls
string/array methods on undefined; keep the same output structure and formatting
in formatAsCursor and reference DecisionExportData for context.
- Around line 352-357: handleExport currently only calls onExport(format,
content) and does nothing if onExport is not provided, so clicking "Download"
produces no file; change handleExport (and the other similar handler around the
later block) to fall back to client-side download: call updatePreview(format) to
get the string content, create a Blob from that content with an appropriate MIME
type (e.g., text/plain or application/json depending on ExportFormat), create an
object URL via URL.createObjectURL, build an <a> element with download attribute
using a sensible filename (e.g., `${decisionName || 'export'}.${format}`),
programmatically click the anchor, then revoke the URL and clean up the element;
keep the existing onExport call (if provided) but still perform the fallback
download when onExport is undefined.
- Around line 362-377: The custom trigger path in DecisionExportDialog opens the
dialog without initializing the preview; when a custom trigger element is
clicked it only calls setIsOpen(true) but does not call
updatePreview(selectedFormat). Modify the onClick handler used for the custom
trigger (the JSX branch that renders {trigger}) to also invoke
updatePreview(selectedFormat) (or call the same helper used by the default
Button) after setting isOpen so the preview is populated immediately when the
dialog opens.

In `@components/FeedbackList.tsx`:
- Around line 468-477: The QuickDecisionDialog currently passes projectId as
items[0]?.projectId || "" which can be wrong if items becomes empty while
selections remain; change it to derive projectId from the actual selected items
(use selectedItems[0]?.projectId ?? "" or compute a const projectId =
selectedItems[0]?.projectId ?? "" above the JSX) and pass that into the
QuickDecisionDialog prop (reference: QuickDecisionDialog component, variables
selectedItems, items, showQuickDecision, selectedIds).
- Around line 75-81: toggleAll currently compares selectedIds.size to
filtered.length which fails when sizes match but contents differ; change the
logic in toggleAll to check whether every filtered item id is present in
selectedIds (e.g., allSelected = filtered.every(f => selectedIds.has(f.id))) and
use that to decide whether to clear selection or setSelectedIds(new
Set(filtered.map(f => f.id))); apply the same allSelected check to the checkbox
icon condition (the reused derived variable) so the UI reflects actual
membership rather than size comparison.

In `@components/PriorityMatrix.tsx`:
- Line 96: The Tailwind slash opacity modifiers like "text-current/70",
"border-current/20", and "border-current/10" don’t work with the CSS keyword
currentColor; update the affected elements in PriorityMatrix.tsx (e.g., the <p>
showing "No items in this quadrant." and the elements using "border-current/20"
and "border-current/10") to either apply a separate opacity utility (e.g., add
"opacity-70" or "opacity-20"/"opacity-10" on the same element) or replace
currentColor with a concrete color class (e.g., "text-gray-500" and
"border-gray-200") so the intended transparency actually takes effect.
- Line 195: The span element in PriorityMatrix.tsx is using the HTML attribute
"class" which React JSX ignores; change the attribute on the red legend dot (the
span with "w-3 h-3 rounded-full bg-red-500") from class to className so the
Tailwind classes are applied (search for that exact span or the red legend dot
in the PriorityMatrix component and update the attribute).
- Around line 44-52: getQuadrantKey currently treats any non-'low' effort
(including 'medium') as high effort and misclassifies medium-effort items;
update getQuadrantKey(PriorityItem) to explicitly handle 'medium' effort values:
for highImpact, map 'low'→'q1' and 'medium'/'high'→'q2'; for non-highImpact, map
'low'/'medium'→'q3' and 'high'→'q4'. Change the boolean lowEffort check to
direct comparisons against item.effort and add a branch for item.effort ===
'medium' so high-impact medium-effort items go to q2 and medium-impact
medium-effort items go to q3.

In `@components/QuickDecisionDialog.tsx`:
- Around line 375-376: The file currently imports useRouter from
"next/navigation" near the bottom; move the import statement (useRouter from
"next/navigation") into the main import block at the top of
QuickDecisionDialog.tsx alongside the other imports (within the existing import
block around lines 3–28) and then remove the duplicate/bottom import lines (the
import and comment at lines 375–376) so all imports are consolidated at the top.
- Around line 46-60: generateTitle and generateSummary produce awkward text when
selectedFeedback is empty or fields are missing; update both functions
(generateTitle, generateSummary) to guard against empty selectedFeedback and
empty computed arrays: for generateTitle return a sensible default (e.g., "No
feedback selected" or "Review customer feedback") when selectedFeedback.length
=== 0, otherwise compute type/count as before; for generateSummary treat empty
uniqueSources and tiers with fallbacks like "customers" or "unknown source(s)"
and format totalRevenue as $0 when no revenue, ensuring join() is only used when
arrays have items so the final sentence reads naturally even with sparse data.

In `@components/ui/slider.tsx`:
- Line 23: The SliderPrimitive.Thumb contains conflicting border-color classes
(border-slate-200 vs border-slate-900 and dark:border-slate-800 vs
dark:border-slate-50) so remove the unintended duplicates in the className on
SliderPrimitive.Thumb; pick the correct light-mode border (either
border-slate-200 or border-slate-900) and the matching dark-mode border (either
dark:border-slate-800 or dark:border-slate-50), delete the opposite class in
each pair, and ensure the remaining classes preserve the existing rounded, bg,
shadow and focus/disabled utilities.
🧹 Nitpick comments (8)
components/QuickDecisionDialog.tsx (1)

134-153: Custom modal lacks accessibility: no focus trap, no Escape-to-close, no role="dialog".

This hand-rolled overlay doesn't trap focus, handle the Escape key, or set role="dialog" / aria-modal. Screen readers won't announce it as a dialog, and keyboard users can tab behind it. Consider using Radix UI's Dialog primitive (already a project dependency via other Radix imports) or at minimum add onKeyDown for Escape and appropriate ARIA attributes.

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

77-89: getConfidenceLabel and getConfidenceColor are duplicated with QuickDecisionDialog.tsx (Lines 115–127).

Both files contain identical implementations. Extract these into a shared utility (e.g., lib/confidence.ts) to keep them in sync.

app/api/projects/[id]/decisions/[decisionId]/export/route.ts (1)

16-33: Format validation can drift from ExportFormat type.

The validFormats array on lines 19–27 duplicates the ExportFormat union from DecisionExportDialog.tsx. If a format is added/removed in one place but not the other, behavior will diverge silently. Also, the as ExportFormat cast on line 16 happens before validation, so TypeScript won't help catch invalid values.

♻️ Derive from the canonical type to keep them in sync
+const VALID_FORMATS: ExportFormat[] = [
+  "cursor", "linear", "jira", "slack", "notion", "markdown", "json",
+];
+
 export async function GET(
   request: NextRequest,
   { params }: { params: Promise<{ id: string; decisionId: string }> }
 ) {
   try {
     const { id: projectId, decisionId } = await params;
     const { searchParams } = new URL(request.url);
-    const format = (searchParams.get("format") as ExportFormat) || "markdown";
-
-    // Validate format
-    const validFormats = [
-      "cursor",
-      "linear",
-      "jira",
-      "slack",
-      "notion",
-      "markdown",
-      "json",
-    ];
-    if (!validFormats.includes(format)) {
+    const rawFormat = searchParams.get("format") || "markdown";
+    if (!VALID_FORMATS.includes(rawFormat as ExportFormat)) {
       return NextResponse.json(
-        { error: `Invalid format. Valid formats: ${validFormats.join(", ")}` },
+        { error: `Invalid format. Valid formats: ${VALID_FORMATS.join(", ")}` },
         { status: 400 }
       );
     }
+    const format = rawFormat as ExportFormat;

Ideally, export VALID_FORMATS from DecisionExportDialog.tsx alongside the type so both the route and the dialog reference the same source of truth.

components/FeedbackList.tsx (1)

149-172: Duplicate badge rendering for "analyzed" and "ready_for_implementation" statuses.

Both cases return the identical badge ("Ready for Implementation"). If this is intentional, consider merging them with a fallthrough or a combined condition to make the intent explicit and reduce duplication.

♻️ Merge duplicate cases
  const getStatusBadge = (status: string) => {
    switch (status) {
      case "pending_analysis":
        return <Badge variant="secondary">Analyzing...</Badge>;
      case "analyzed":
-       return (
-         <Badge className="bg-blue-500 hover:bg-blue-600">
-           Ready for Implementation
-         </Badge>
-       );
      case "ready_for_implementation":
        return (
          <Badge className="bg-blue-500 hover:bg-blue-600">
            Ready for Implementation
          </Badge>
        );
app/projects/[id]/decisions/[decisionId]/page.tsx (1)

146-161: Read-only checkboxes with no checked state may confuse users.

The checkboxes are readOnly but always render as unchecked, which looks interactive but isn't. Consider using check-mark icons or a styled list instead to better convey that this is a display-only checklist.

♻️ Example: use icons instead of checkboxes
                 <li key={i} className="flex items-start gap-2">
-                  <input
-                    type="checkbox"
-                    className="mt-1.5 rounded border-gray-300"
-                    readOnly
-                  />
+                  <CheckCircle className="w-4 h-4 mt-1 text-gray-300 flex-shrink-0" />
                   <span className="text-gray-700">{criteria.trim()}</span>
                 </li>
app/projects/[id]/decisions/page.tsx (1)

85-97: DecisionExportData construction is duplicated between the list and detail pages.

Both this page and app/projects/[id]/decisions/[decisionId]/page.tsx build the same DecisionExportData object from a decision record. Consider extracting a helper (e.g., toExportData(decision, projectName)) to keep them in sync.

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

76-90: Unvalidated type assertion on analysis.content — runtime errors if shape mismatches.

The as cast on the Prisma JSON field assumes a specific structure. If the stored JSON doesn't match (e.g., older analysis format, missing fields), the page will crash with an uncaught property access error.

Consider adding defensive access or basic validation, e.g.:

const analysisContent = analysis.content as Record<string, any>;
const feedbackCount = analysisContent?.feedbackCount ?? 0;
const summary = analysisContent?.summary ?? { bugs: 0, features: 0, other: 0 };
const opportunities = Array.isArray(analysisContent?.opportunities) ? analysisContent.opportunities : [];
const unclusteredCount = analysisContent?.unclusteredCount ?? 0;
const generatedAt = analysisContent?.generatedAt ?? analysis.createdAt;

248-253: "Create Decision" link doesn't pass opportunity context.

Each opportunity's "Create Decision" button links to /decisions/new without any query parameters (e.g., title, description, or opportunity ID). Users must manually re-enter the information that's already displayed. Consider passing context via query params.

♻️ Example: pass context via search params
-                        <Button size="sm" asChild>
-                          <Link href={`/projects/${projectId}/decisions/new`}>
+                        <Button size="sm" asChild>
+                          <Link href={`/projects/${projectId}/decisions/new?title=${encodeURIComponent(opp.title)}&summary=${encodeURIComponent(opp.description)}`}>
                             <Target className="w-4 h-4 mr-1" />
                             Create Decision
                           </Link>
                         </Button>

Comment on lines +9 to +103
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string; decisionId: string }> }
) {
try {
const { id: projectId, decisionId } = await params;
const { searchParams } = new URL(request.url);
const format = (searchParams.get("format") as ExportFormat) || "markdown";

// Validate format
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 }
);
}

// Fetch the decision
const decision = await prisma.decision.findUnique({
where: { id: decisionId },
include: {
project: {
select: { name: true },
},
},
});

if (!decision) {
return NextResponse.json({ error: "Decision not found" }, { status: 404 });
}

// Verify the decision belongs to the project
if (decision.projectId !== projectId) {
return NextResponse.json(
{ error: "Decision does not belong to this project" },
{ status: 403 }
);
}

// Build export data
const exportData: DecisionExportData = {
id: decision.id,
title: decision.title,
summary: decision.summary,
scope: decision.scope,
nonGoals: decision.nonGoals,
acceptanceCriteria: decision.acceptanceCriteria,
risks: decision.risks,
confidenceScore: decision.confidenceScore,
linkedFeedbackIds: decision.linkedFeedbackIds,
projectName: decision.project.name,
};

// Format the decision
const content = formatDecision(exportData, format);

// Return appropriate response based on format
if (format === "json") {
return NextResponse.json(JSON.parse(content), {
headers: {
"Content-Type": "application/json",
},
});
}

// For text formats, return plain text with appropriate headers
const contentType =
format === "markdown" ? "text/markdown" : "text/plain";
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}"`,
},
});
} catch (error) {
console.error("Error exporting decision:", error);
return NextResponse.json(
{ error: "Failed to export decision" },
{ status: 500 }
);
}
}
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:

#!/bin/bash
# Check if there's auth middleware protecting API routes
fd "middleware" --type f --extension ts --extension js
echo "---AUTH_PATTERNS---"
# Check for auth patterns in existing API routes
rg -n "getServerSession|auth|currentUser|requireAuth" --type ts -g 'app/api/**'
echo "---NEXTAUTH_CONFIG---"
# Check for NextAuth or auth configuration
fd -e "route.ts" -e "route.js" -e "auth.ts" -e "auth.js" | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 566


🏁 Script executed:

#!/bin/bash
# Search for middleware at root level
echo "---MIDDLEWARE_FILES---"
find . -maxdepth 2 -name "middleware.*" -type f
echo "---EXPORT_ROUTE_CONTENT---"
# Check the export route for any auth imports
head -15 app/api/projects/\[id\]/decisions/\[decisionId\]/export/route.ts
echo "---AUTH_LIB_FILES---"
# Check auth library files
ls -la lib/auth* 2>/dev/null || echo "No auth files in lib/"
echo "---OTHER_API_ROUTES---"
# Check a few other API routes to see auth patterns
find app/api -name "route.ts" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -20 {}'

Repository: Fresh-Industries/pm-analyzer

Length of output: 3688


🏁 Script executed:

#!/bin/bash
# Check if any routes import auth utilities
echo "---ROUTES_USING_AUTH---"
rg -l "from.*auth|import.*auth" app/api --type ts | grep -v "\[...all\]" | head -10
echo "---CHECK_AUTH_SERVER_USAGE---"
rg "getSession|auth\(\)" app/api --type ts | head -20
echo "---LIB_AUTH_FILES_CONTENT---"
echo "=== lib/auth.ts ==="
cat lib/auth.ts
echo "=== lib/auth-server.ts ==="
cat lib/auth-server.ts

Repository: Fresh-Industries/pm-analyzer

Length of output: 866


🏁 Script executed:

#!/bin/bash
# Check projects route in detail since it imports auth-client
echo "---PROJECTS_ROUTE_CONTENT---"
cat app/api/projects/route.ts
echo "---CHECK_IF_OTHER_ROUTES_USE_AUTH_CHECKS---"
rg "getServerSession|getSession" app/api --type ts -A 3 | head -30

Repository: Fresh-Industries/pm-analyzer

Length of output: 1470


Add authentication check to the export endpoint.

This route lacks user authentication and is publicly accessible to anyone who can guess a valid projectId and decisionId. The current project-ownership check (decision.projectId === projectId) verifies data consistency but does not authenticate the requesting user. Although the app has a getServerSession() function available in lib/auth-server.ts via better-auth, it is not used here. Add a session check to verify the user is authenticated and authorized to access this decision, similar to what should be done across other API routes that currently lack authentication.

🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 9
- 103, Add an authentication and authorization check at the start of the
exported GET handler: call getServerSession() from lib/auth-server.ts and if it
returns no session return a 401 JSON response; then verify the signed-in user is
allowed to access the decision by ensuring they belong to the project (e.g.
query the project using prisma (prisma.project.findFirst or findUnique with
membership/owner check) and confirm project.id === projectId and that
session.user.id is an owner/member), and only proceed to load the Decision (or
return 403) if that authorization check passes; keep the existing
decision.projectId === projectId validation as a secondary data check.

Comment on lines +86 to +88
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
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

Insufficient filename sanitization — special characters in title can break Content-Disposition header.

decision.title may contain characters like ", \, /, or non-ASCII that the whitespace-only regex won't strip. This can produce a malformed or exploitable Content-Disposition header.

🛡️ Proposed fix: sanitize the title more thoroughly
-    const filename = `decision-${decision.title
-      .toLowerCase()
-      .replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
+    const safeTitle = decision.title
+      .toLowerCase()
+      .replace(/[^a-z0-9]+/g, "-")
+      .replace(/^-|-$/g, "")
+      .slice(0, 100);
+    const extension = format === "markdown" ? "md" : "txt";
+    const filename = `decision-${safeTitle}.${extension}`;

Note: the current extension logic also produces non-standard extensions like .cursor, .linear, etc. Consider using .txt for all non-markdown text formats.

📝 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}`;
const safeTitle = decision.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 100);
const extension = format === "markdown" ? "md" : "txt";
const filename = `decision-${safeTitle}.${extension}`;
🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 86
- 88, The generated filename built in the filename constant uses raw
decision.title and only strips whitespace, which can introduce quotes, slashes,
backslashes, and other characters that break the Content-Disposition header;
update the logic that builds filename (the filename const) to: sanitize
decision.title by removing or replacing all characters except ASCII letters,
numbers, hyphen and underscore (collapse runs and trim, and cap length to e.g.
100 chars), percent-encode or provide a RFC5987-safe fallback when setting
headers, and normalize extensions so format === "markdown" => ".md" and all
other formats use ".txt" (avoid producing non-standard extensions like
.cursor/.linear). Ensure you reference and change the filename construction and
the code that sets the Content-Disposition header to use the sanitized/encoded
filename.

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 | 🟠 Major

confidence || 0.5 treats 0 as falsy — use nullish coalescing.

If a caller sends confidence: 0 (a valid value representing 0%), the || operator will discard it and default to 0.5. Use ?? to only default on null/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 code sets
confidenceScore using the falsy-check operator (confidence || 0.5), which treats
0 as falsy; update the assignment to use nullish coalescing so only
null/undefined fall back to 0.5 (replace the use of || with ?? for the
confidence -> confidenceScore mapping), referencing the confidence variable and
confidenceScore field in the route handler that builds the decision payload.

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 | 🟠 Major

effort is incorrectly derived from impactScore, making the Priority Matrix useless.

Line 97 computes effort directly from impactScore: high impact → high effort, low impact → low effort. This means items can never be "Quick Wins" (high impact, low effort) or "Thankless Tasks" (low impact, high effort) — two of the four quadrants in the Eisenhower matrix will always be empty.

Effort should either come from the analysis data (if available) or be independently estimated.

🐛 Suggested approach: source effort from analysis data or default to medium
  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',
+   effort: (opp as any).effort as 'high' | 'medium' | 'low' ?? 'medium',
    impactScore: opp.impactScore,
    feedbackCount: opp.feedbackCount,
    category: opp.category,
  }));

If the analysis payload doesn't include an effort field yet, consider adding it to the analysis generation step, or default to 'medium' until it's available — rather than coupling effort to impact.

📝 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 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,
}));
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 as any).effort as 'high' | 'medium' | 'low' ?? 'medium',
impactScore: opp.impactScore,
feedbackCount: opp.feedbackCount,
category: opp.category,
}));
🤖 Prompt for AI Agents
In `@app/projects/`[id]/analysis/page.tsx around lines 92 - 101, The mapping for
opportunities incorrectly derives effort from impactScore in the
opportunities.map block (constructing OpportunityItem), which prevents valid
Priority Matrix quadrants; change the logic to source effort from the analysis
payload (use opp.effort if present) and only fallback to a neutral default like
'medium' (or an independent estimator) instead of using opp.impactScore; update
the mapping in the same function where opportunities are created and ensure the
OpportunityItem type/shape (and any downstream UI expecting effort) supports
this field.

</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

4-column grid is not responsive — will be cramped on small screens.

grid-cols-4 forces 4 columns at all viewport sizes.

♻️ Add responsive breakpoints
-      <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 grid container
currently uses a fixed "grid grid-cols-4 gap-4" which forces four columns on all
viewports; update the className on that div (the element with "grid grid-cols-4
gap-4") to use responsive Tailwind breakpoints (for example: single column on
xs, then 2 cols on small, 3 on medium, 4 on large) so it collapses on small
screens and expands on larger ones.

<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

text-current/70 opacity modifier doesn't work with currentColor.

Tailwind's slash-based opacity syntax (e.g., text-red-500/70) works with color values that can be decomposed into channels, but currentColor is a CSS keyword that cannot be modified this way. The same applies to border-current/20 (Line 101) and border-current/10 (Line 119). These will likely be ignored or produce unexpected output.

Consider using opacity-70 on the element or a specific color class instead.

🤖 Prompt for AI Agents
In `@components/PriorityMatrix.tsx` at line 96, The Tailwind slash opacity
modifiers like "text-current/70", "border-current/20", and "border-current/10"
don’t work with the CSS keyword currentColor; update the affected elements in
PriorityMatrix.tsx (e.g., the <p> showing "No items in this quadrant." and the
elements using "border-current/20" and "border-current/10") to either apply a
separate opacity utility (e.g., add "opacity-70" or "opacity-20"/"opacity-10" on
the same element) or replace currentColor with a concrete color class (e.g.,
"text-gray-500" and "border-gray-200") so the intended transparency actually
takes effect.

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 | 🔴 Critical

class should be className in JSX.

Line 195 uses class instead of className. React will emit a warning and the classes won't be applied to this element, leaving the red legend dot unstyled.

🐛 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 element in
PriorityMatrix.tsx is using the HTML attribute "class" which React JSX ignores;
change the attribute on the red legend dot (the span with "w-3 h-3 rounded-full
bg-red-500") from class to className so the Tailwind classes are applied (search
for that exact span or the red legend dot in the PriorityMatrix component and
update the attribute).

Comment on lines +46 to +60
const generateTitle = () => {
const types = selectedFeedback.map((f) => f.type).filter(Boolean);
const type = types[0] || "feature";
const count = selectedFeedback.length;
return `${type === "bug" ? "Fix" : "Implement"} ${count} item${count > 1 ? "s" : ""} from customer feedback`;
};

// Auto-generate summary
const generateSummary = () => {
const uniqueSources = [...new Set(selectedFeedback.map((f) => f.source).filter(Boolean))];
const totalRevenue = selectedFeedback.reduce((sum, f) => sum + (f.revenue || 0), 0);
const tiers = [...new Set(selectedFeedback.map((f) => f.customerTier).filter(Boolean))];

return `Address ${selectedFeedback.length} feedback item${selectedFeedback.length > 1 ? "s" : ""} from ${uniqueSources.join(", ")} (potential impact: $${totalRevenue.toLocaleString()}/mo from ${tiers.join(", ")} customers)`;
};
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

Auto-generated text degrades when feedback data is sparse.

  • generateTitle (Line 46): if selectedFeedback is empty, produces "Implement 0 items from customer feedback."
  • generateSummary (Line 54): if all sources are null, uniqueSources.join(", ") is empty, producing "…from (potential impact…". Similarly, empty tiers.

Consider guarding against empty/missing data:

♻️ Suggested improvement
   const generateSummary = () => {
     const uniqueSources = [...new Set(selectedFeedback.map((f) => f.source).filter(Boolean))];
     const totalRevenue = selectedFeedback.reduce((sum, f) => sum + (f.revenue || 0), 0);
     const tiers = [...new Set(selectedFeedback.map((f) => f.customerTier).filter(Boolean))];

-    return `Address ${selectedFeedback.length} feedback item${selectedFeedback.length > 1 ? "s" : ""} from ${uniqueSources.join(", ")} (potential impact: $${totalRevenue.toLocaleString()}/mo from ${tiers.join(", ")} customers)`;
+    const sourceText = uniqueSources.length > 0 ? ` from ${uniqueSources.join(", ")}` : "";
+    const tierText = tiers.length > 0 ? ` from ${tiers.join(", ")} customers` : "";
+    return `Address ${selectedFeedback.length} feedback item${selectedFeedback.length > 1 ? "s" : ""}${sourceText} (potential impact: $${totalRevenue.toLocaleString()}/mo${tierText})`;
   };
📝 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 generateTitle = () => {
const types = selectedFeedback.map((f) => f.type).filter(Boolean);
const type = types[0] || "feature";
const count = selectedFeedback.length;
return `${type === "bug" ? "Fix" : "Implement"} ${count} item${count > 1 ? "s" : ""} from customer feedback`;
};
// Auto-generate summary
const generateSummary = () => {
const uniqueSources = [...new Set(selectedFeedback.map((f) => f.source).filter(Boolean))];
const totalRevenue = selectedFeedback.reduce((sum, f) => sum + (f.revenue || 0), 0);
const tiers = [...new Set(selectedFeedback.map((f) => f.customerTier).filter(Boolean))];
return `Address ${selectedFeedback.length} feedback item${selectedFeedback.length > 1 ? "s" : ""} from ${uniqueSources.join(", ")} (potential impact: $${totalRevenue.toLocaleString()}/mo from ${tiers.join(", ")} customers)`;
};
const generateTitle = () => {
const types = selectedFeedback.map((f) => f.type).filter(Boolean);
const type = types[0] || "feature";
const count = selectedFeedback.length;
return `${type === "bug" ? "Fix" : "Implement"} ${count} item${count > 1 ? "s" : ""} from customer feedback`;
};
// Auto-generate summary
const generateSummary = () => {
const uniqueSources = [...new Set(selectedFeedback.map((f) => f.source).filter(Boolean))];
const totalRevenue = selectedFeedback.reduce((sum, f) => sum + (f.revenue || 0), 0);
const tiers = [...new Set(selectedFeedback.map((f) => f.customerTier).filter(Boolean))];
const sourceText = uniqueSources.length > 0 ? ` from ${uniqueSources.join(", ")}` : "";
const tierText = tiers.length > 0 ? ` from ${tiers.join(", ")} customers` : "";
return `Address ${selectedFeedback.length} feedback item${selectedFeedback.length > 1 ? "s" : ""}${sourceText} (potential impact: $${totalRevenue.toLocaleString()}/mo${tierText})`;
};
🤖 Prompt for AI Agents
In `@components/QuickDecisionDialog.tsx` around lines 46 - 60, generateTitle and
generateSummary produce awkward text when selectedFeedback is empty or fields
are missing; update both functions (generateTitle, generateSummary) to guard
against empty selectedFeedback and empty computed arrays: for generateTitle
return a sensible default (e.g., "No feedback selected" or "Review customer
feedback") when selectedFeedback.length === 0, otherwise compute type/count as
before; for generateSummary treat empty uniqueSources and tiers with fallbacks
like "customers" or "unknown source(s)" and format totalRevenue as $0 when no
revenue, ensuring join() is only used when arrays have items so the final
sentence reads naturally even with sparse data.

Comment on lines +375 to +376
// Import useRouter at top
import { useRouter } from "next/navigation";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Move the useRouter import to the top of the file with the other imports.

ESM hoisting makes this work at runtime, but placing an import at the bottom of the file (with a comment saying "Import useRouter at top") is confusing and contradicts the stated intent. Move it to the import block at Lines 3–28.

♻️ Proposed fix

Add to the existing import block (e.g., after Line 3):

 import { useState } from "react";
+import { useRouter } from "next/navigation";
 import Link from "next/link";

Then remove Lines 375–376.

🤖 Prompt for AI Agents
In `@components/QuickDecisionDialog.tsx` around lines 375 - 376, The file
currently imports useRouter from "next/navigation" near the bottom; move the
import statement (useRouter from "next/navigation") into the main import block
at the top of QuickDecisionDialog.tsx alongside the other imports (within the
existing import block around lines 3–28) and then remove the duplicate/bottom
import lines (the import and comment at lines 375–376) so all imports are
consolidated at the top.

<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 classes on the Thumb.

border-slate-200 border-slate-900 — the last one wins, making the first dead code. Same for dark:border-slate-800 dark:border-slate-50. Likely one of each pair should be removed.

♻️ Proposed fix (keep the intended color)
-    <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/50 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, The SliderPrimitive.Thumb contains
conflicting border-color classes (border-slate-200 vs border-slate-900 and
dark:border-slate-800 vs dark:border-slate-50) so remove the unintended
duplicates in the className on SliderPrimitive.Thumb; pick the correct
light-mode border (either border-slate-200 or border-slate-900) and the matching
dark-mode border (either dark:border-slate-800 or dark:border-slate-50), delete
the opposite class in each pair, and ensure the remaining classes preserve the
existing rounded, bg, shadow and focus/disabled utilities.

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