fix(dashboard): rework Insights view with two-pane layout
- Categories list in a left sidebar; insights for the selected category render in a scrollable right pane (mobile collapses sidebar to a horizontal chip rail and keeps the header on a single row). - Removed -webkit-line-clamp on insight content so full text shows. - Fixed scroll: added min-height: 0 to flex containers. - Larger icons (20px) inside per-insight action buttons; close button switched off the invisible btn-icon styling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/insights-two-pane-layout.md
Normal file
5
.changeset/insights-two-pane-layout.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Insights view: two-pane layout (categories sidebar + scrollable detail), full insight content (no line-clamp), larger action icons, fixed scrolling, and mobile single-row header.
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,10 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insights-status-region:empty {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.insights-status-message {
|
.insights-status-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -93,6 +98,7 @@
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Run info */
|
/* Run info */
|
||||||
@@ -157,17 +163,100 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections container */
|
/* Two-pane body */
|
||||||
.insights-sections {
|
.insights-body {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-lg);
|
|
||||||
padding: var(--space-lg);
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual section */
|
/* Sidebar (categories) */
|
||||||
|
.insights-sidebar {
|
||||||
|
width: 240px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-item:hover {
|
||||||
|
background: var(--surface-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-item--active {
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--surface-elevated);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-item--active .insights-category-count {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail pane */
|
||||||
|
.insights-detail {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section (single, in detail pane) */
|
||||||
.insights-section {
|
.insights-section {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -259,15 +348,39 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Borderless icon-prominent action buttons inside insight items */
|
||||||
|
.insight-item-action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast), color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-item-action-btn:hover:not(:disabled) {
|
||||||
|
background: var(--surface-elevated);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-item-action-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.insight-item-content {
|
.insight-item-content {
|
||||||
margin: var(--space-sm) 0 0;
|
margin: var(--space-sm) 0 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
display: -webkit-box;
|
white-space: pre-wrap;
|
||||||
-webkit-line-clamp: 4;
|
word-break: break-word;
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-item-meta {
|
.insight-item-meta {
|
||||||
@@ -275,7 +388,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
margin-top: var(--space-sm);
|
margin-top: var(--space-sm);
|
||||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,27 +435,69 @@
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsive */
|
/* Mobile responsive: stack panes vertically; sidebar becomes a horizontal scroller */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.insights-view-header {
|
.insights-view-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: var(--space-md);
|
gap: var(--space-sm);
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insights-view-title {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.insights-view-title h2 {
|
.insights-view-title h2 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insights-view-actions {
|
.insights-view-actions {
|
||||||
width: 100%;
|
flex-shrink: 0;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.insights-sections {
|
.insights-body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-list {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-list > li {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-category-label {
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-detail {
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
gap: var(--space-md);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.insights-section-header {
|
.insights-section-header {
|
||||||
@@ -375,12 +530,11 @@
|
|||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-item-actions .btn-icon {
|
.insight-item-action-btn {
|
||||||
min-width: 36px;
|
width: 40px;
|
||||||
min-height: 36px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* InsightsView additional mobile refinements */
|
|
||||||
.insights-view-actions .btn {
|
.insights-view-actions .btn {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
@@ -393,6 +547,4 @@
|
|||||||
.insights-view-count {
|
.insights-view-count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* InsightsView - Dashboard component for displaying and managing project insights
|
* InsightsView - Dashboard component for displaying and managing project insights
|
||||||
*
|
*
|
||||||
* Features:
|
* Two-pane layout: categories on the left, insights for the selected category on the right.
|
||||||
* - Displays insights grouped by all supported insight categories
|
|
||||||
* - Manual insight generation trigger
|
|
||||||
* - Per-insight dismiss action
|
|
||||||
* - Per-insight task creation action
|
|
||||||
* - Loading, error, and empty states
|
|
||||||
* - Accessible feedback for all actions
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./InsightsView.css";
|
import "./InsightsView.css";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Sparkles,
|
Sparkles,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -39,7 +33,6 @@ interface InsightsViewProps {
|
|||||||
onCreateTask?: (title: string, description: string) => void;
|
onCreateTask?: (title: string, description: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category icons mapping
|
|
||||||
const CATEGORY_ICONS: Record<InsightCategory, React.ComponentType<{ size?: number; className?: string }>> = {
|
const CATEGORY_ICONS: Record<InsightCategory, React.ComponentType<{ size?: number; className?: string }>> = {
|
||||||
architecture: Building,
|
architecture: Building,
|
||||||
quality: CheckCircle,
|
quality: CheckCircle,
|
||||||
@@ -75,11 +68,33 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
totalCount,
|
totalCount,
|
||||||
} = useInsights(projectId);
|
} = useInsights(projectId);
|
||||||
|
|
||||||
// Track inline feedback messages
|
|
||||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||||
const [statusType, setStatusType] = useState<"success" | "error" | "info">("info");
|
const [statusType, setStatusType] = useState<"success" | "error" | "info">("info");
|
||||||
|
|
||||||
// Clear status message after delay
|
const populatedSections = useMemo(
|
||||||
|
() => sections.filter((section) => section.items.length > 0),
|
||||||
|
[sections],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<InsightCategory | null>(null);
|
||||||
|
|
||||||
|
// Keep selection valid as data changes; default to first populated section.
|
||||||
|
useEffect(() => {
|
||||||
|
if (populatedSections.length === 0) {
|
||||||
|
if (selectedCategory !== null) setSelectedCategory(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stillExists = selectedCategory && populatedSections.some((s) => s.category === selectedCategory);
|
||||||
|
if (!stillExists) {
|
||||||
|
setSelectedCategory(populatedSections[0].category);
|
||||||
|
}
|
||||||
|
}, [populatedSections, selectedCategory]);
|
||||||
|
|
||||||
|
const activeSection: InsightSection | undefined = useMemo(
|
||||||
|
() => populatedSections.find((s) => s.category === selectedCategory) ?? populatedSections[0],
|
||||||
|
[populatedSections, selectedCategory],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusMessage) {
|
if (statusMessage) {
|
||||||
const timer = setTimeout(() => setStatusMessage(null), 5000);
|
const timer = setTimeout(() => setStatusMessage(null), 5000);
|
||||||
@@ -87,7 +102,6 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
}
|
}
|
||||||
}, [statusMessage]);
|
}, [statusMessage]);
|
||||||
|
|
||||||
// Handle manual run
|
|
||||||
const handleRun = useCallback(async () => {
|
const handleRun = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setStatusMessage("Generating insights...");
|
setStatusMessage("Generating insights...");
|
||||||
@@ -104,7 +118,6 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
}
|
}
|
||||||
}, [runInsights, addToast]);
|
}, [runInsights, addToast]);
|
||||||
|
|
||||||
// Handle dismiss
|
|
||||||
const handleDismiss = useCallback(
|
const handleDismiss = useCallback(
|
||||||
async (id: string, title: string) => {
|
async (id: string, title: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -124,7 +137,6 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
[dismiss, addToast],
|
[dismiss, addToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle create task
|
|
||||||
const handleCreateTask = useCallback(
|
const handleCreateTask = useCallback(
|
||||||
async (id: string, title: string) => {
|
async (id: string, title: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -147,29 +159,53 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
[createTaskFromInsight, onCreateTask, addToast],
|
[createTaskFromInsight, onCreateTask, addToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render section
|
const renderCategoryItem = (section: InsightSection) => {
|
||||||
const renderSection = (section: InsightSection) => {
|
|
||||||
const IconComponent = CATEGORY_ICONS[section.category] ?? Sparkles;
|
const IconComponent = CATEGORY_ICONS[section.category] ?? Sparkles;
|
||||||
const isAnyActionInFlight =
|
const isActive = activeSection?.category === section.category;
|
||||||
section.items.some((item) => dismissStates.get(item.id)?.running || createTaskStates.get(item.id)?.running) ?? false;
|
return (
|
||||||
|
<li key={section.category}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`insights-category-item${isActive ? " insights-category-item--active" : ""}`}
|
||||||
|
onClick={() => setSelectedCategory(section.category)}
|
||||||
|
aria-current={isActive ? "true" : undefined}
|
||||||
|
data-testid={`insights-category-${section.category}`}
|
||||||
|
>
|
||||||
|
<IconComponent size={16} className="insights-category-icon" />
|
||||||
|
<span className="insights-category-label">{section.label}</span>
|
||||||
|
<span className="insights-category-count">{section.items.length}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderActiveInsights = () => {
|
||||||
|
if (!activeSection) return null;
|
||||||
|
const IconComponent = CATEGORY_ICONS[activeSection.category] ?? Sparkles;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section key={section.category} className="insights-section" data-testid={`insights-section-${section.category}`}>
|
<section
|
||||||
|
className="insights-section"
|
||||||
|
data-testid={`insights-section-${activeSection.category}`}
|
||||||
|
>
|
||||||
<div className="insights-section-header">
|
<div className="insights-section-header">
|
||||||
<div className="insights-section-title">
|
<div className="insights-section-title">
|
||||||
<IconComponent size={18} className="insights-section-icon" />
|
<IconComponent size={20} className="insights-section-icon" />
|
||||||
<h3>{section.label}</h3>
|
<h3>{activeSection.label}</h3>
|
||||||
<span className="insights-section-count">{section.items.length}</span>
|
<span className="insights-section-count">{activeSection.items.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="insights-section-content">
|
<div className="insights-section-content">
|
||||||
<ul className="insights-list">
|
<ul className="insights-list">
|
||||||
{section.items.map((insight) => {
|
{activeSection.items.map((insight) => {
|
||||||
const dismissState = dismissStates.get(insight.id);
|
const dismissState = dismissStates.get(insight.id);
|
||||||
const createState = createTaskStates.get(insight.id);
|
const createState = createTaskStates.get(insight.id);
|
||||||
const isDismissInFlight = dismissState?.running ?? false;
|
const isDismissInFlight = dismissState?.running ?? false;
|
||||||
const isCreateInFlight = createState?.running ?? false;
|
const isCreateInFlight = createState?.running ?? false;
|
||||||
|
const isAnyActionInFlight = activeSection.items.some(
|
||||||
|
(item) => dismissStates.get(item.id)?.running || createTaskStates.get(item.id)?.running,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={insight.id} className="insight-item" data-insight-id={insight.id}>
|
<li key={insight.id} className="insight-item" data-insight-id={insight.id}>
|
||||||
@@ -177,7 +213,7 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
<h4 className="insight-item-title">{insight.title}</h4>
|
<h4 className="insight-item-title">{insight.title}</h4>
|
||||||
<div className="insight-item-actions">
|
<div className="insight-item-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-icon"
|
className="insight-item-action-btn"
|
||||||
onClick={() => void handleCreateTask(insight.id, insight.title)}
|
onClick={() => void handleCreateTask(insight.id, insight.title)}
|
||||||
disabled={isCreateInFlight || isAnyActionInFlight}
|
disabled={isCreateInFlight || isAnyActionInFlight}
|
||||||
title="Create task from this insight"
|
title="Create task from this insight"
|
||||||
@@ -185,13 +221,13 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
data-testid={`create-task-${insight.id}`}
|
data-testid={`create-task-${insight.id}`}
|
||||||
>
|
>
|
||||||
{isCreateInFlight ? (
|
{isCreateInFlight ? (
|
||||||
<RefreshCw size={14} className="spin" />
|
<RefreshCw size={20} className="spin" />
|
||||||
) : (
|
) : (
|
||||||
<Plus size={14} />
|
<Plus size={20} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-icon"
|
className="insight-item-action-btn"
|
||||||
onClick={() => void handleDismiss(insight.id, insight.title)}
|
onClick={() => void handleDismiss(insight.id, insight.title)}
|
||||||
disabled={isDismissInFlight || isAnyActionInFlight}
|
disabled={isDismissInFlight || isAnyActionInFlight}
|
||||||
title="Dismiss this insight"
|
title="Dismiss this insight"
|
||||||
@@ -199,9 +235,9 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
data-testid={`dismiss-${insight.id}`}
|
data-testid={`dismiss-${insight.id}`}
|
||||||
>
|
>
|
||||||
{isDismissInFlight ? (
|
{isDismissInFlight ? (
|
||||||
<RefreshCw size={14} className="spin" />
|
<RefreshCw size={20} className="spin" />
|
||||||
) : (
|
) : (
|
||||||
<X size={14} />
|
<X size={20} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,9 +279,10 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
<div className="insights-view-actions">
|
<div className="insights-view-actions">
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-icon insights-view-close"
|
className="btn btn-sm insights-view-close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close insights view"
|
aria-label="Close insights view"
|
||||||
|
title="Close"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -282,7 +319,6 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Inline status region */}
|
|
||||||
<div
|
<div
|
||||||
className="insights-status-region"
|
className="insights-status-region"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
@@ -301,7 +337,6 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Run error callout */}
|
|
||||||
{runError && (
|
{runError && (
|
||||||
<div className="insights-error-callout" role="alert" data-testid="run-error">
|
<div className="insights-error-callout" role="alert" data-testid="run-error">
|
||||||
<AlertCircle size={16} />
|
<AlertCircle size={16} />
|
||||||
@@ -309,7 +344,6 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Latest run info */}
|
|
||||||
{latestRun && (
|
{latestRun && (
|
||||||
<div className="insights-run-info" data-testid="latest-run">
|
<div className="insights-run-info" data-testid="latest-run">
|
||||||
<span className="insights-run-status">
|
<span className="insights-run-status">
|
||||||
@@ -348,8 +382,15 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="insights-sections">
|
<div className="insights-body">
|
||||||
{sections.filter((section) => section.items.length > 0).map(renderSection)}
|
<aside className="insights-sidebar" aria-label="Insight categories">
|
||||||
|
<ul className="insights-category-list">
|
||||||
|
{populatedSections.map(renderCategoryItem)}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<div className="insights-detail">
|
||||||
|
{renderActiveInsights()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -232,11 +232,14 @@ describe("InsightsView", () => {
|
|||||||
|
|
||||||
render(<InsightsView {...defaultProps} />);
|
render(<InsightsView {...defaultProps} />);
|
||||||
|
|
||||||
|
// Sidebar lists every populated category
|
||||||
|
expect(screen.getByTestId("insights-category-features")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("insights-category-architecture")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("insights-category-competitive_analysis")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("insights-category-research")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("insights-category-trends")).toBeInTheDocument();
|
||||||
|
// Detail pane shows the first populated section by default
|
||||||
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
|
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("insights-section-architecture")).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId("insights-section-competitive_analysis")).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId("insights-section-research")).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId("insights-section-trends")).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render loading state", () => {
|
it("should render loading state", () => {
|
||||||
@@ -450,12 +453,16 @@ describe("InsightsView", () => {
|
|||||||
|
|
||||||
render(<InsightsView {...defaultProps} />);
|
render(<InsightsView {...defaultProps} />);
|
||||||
|
|
||||||
expect(screen.getAllByTestId(/insights-section-/)).toHaveLength(2);
|
// Sidebar lists exactly the two populated categories
|
||||||
|
expect(screen.getAllByTestId(/^insights-category-/)).toHaveLength(2);
|
||||||
|
expect(screen.getByTestId("insights-category-features")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("insights-category-competitive_analysis")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("insights-category-architecture")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("insights-category-research")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("insights-category-trends")).not.toBeInTheDocument();
|
||||||
|
// Detail shows the first populated section (features)
|
||||||
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
|
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("insights-section-competitive_analysis")).toBeInTheDocument();
|
expect(screen.queryByTestId("insights-section-competitive_analysis")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByTestId("insights-section-architecture")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId("insights-section-research")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId("insights-section-trends")).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render status badge with correct CSS class for generated status", () => {
|
it("should render status badge with correct CSS class for generated status", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user