Skip to content
Merged
1,466 changes: 57 additions & 1,409 deletions app/src/pages/DebugPage.tsx

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions app/src/pages/MapPage.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

import { selectPreviewUrl } from 'src/utils/themedPreview';

/**
* ForceGraph2D's imperative instance (zoom, centerAt, d3Force, refresh, ...).
* react-force-graph-2d types it only loosely, so the map's ref stays untyped
* in this one place and every consumer shares the alias.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ForceGraphHandle = any;

/** Backend response shape from GET /api/specs/map. Mirrors api/schemas.py::SpecMapItem. */
export interface SpecMapItem {
id: string;
Expand Down
1,198 changes: 93 additions & 1,105 deletions app/src/pages/MapPage.tsx

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions app/src/sections/debug/CommonWeaknesses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Box from '@mui/material/Box';

import { SectionHeader } from 'src/components/SectionHeader';
import { fontSize, semanticColors, typography } from 'src/theme';

export interface WeaknessData {
text: string;
count: number;
}

export function CommonWeaknesses({ weaknesses }: { weaknesses: WeaknessData[] }) {
if (weaknesses.length === 0) return null;
const maxWeakness = Math.max(...weaknesses.map(w => w.count), 1);

return (
<>
<Box sx={{ mt: 4 }}>
<SectionHeader prompt="❯" title={<em>common weaknesses</em>} />
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, alignItems: 'baseline' }}>
{weaknesses.map(w => {
const ratio = w.count / maxWeakness;
const size =
ratio >= 0.8
? fontSize.md
: ratio >= 0.5
? fontSize.sm
: ratio >= 0.25
? fontSize.xs
: fontSize.xxs;
const weight = ratio >= 0.5 ? 600 : 400;
const opacity = ratio >= 0.8 ? 1 : ratio >= 0.5 ? 0.85 : ratio >= 0.25 ? 0.7 : 0.55;
return (
<Box
key={w.text}
sx={{
fontFamily: typography.fontFamily,
fontSize: size,
fontWeight: weight,
color: 'var(--ink-soft)',
opacity,
px: 0.75,
py: 0.25,
borderRadius: 0.5,
bgcolor: 'var(--bg-surface)',
border: '1px solid var(--rule)',
}}
>
{w.text}
<Box
component="span"
sx={{ fontSize: fontSize.micro, color: semanticColors.mutedText, ml: 0.5 }}
>
{w.count}
</Box>
</Box>
);
})}
</Box>
</>
);
}
38 changes: 38 additions & 0 deletions app/src/sections/debug/CopyClaudePromptButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import CheckIcon from '@mui/icons-material/Check';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';

import { useCopyCode } from 'src/hooks';
import { colors } from 'src/theme';
import { buildClaudePrompt } from 'src/utils/claudePrompt';

export function CopyClaudePromptButton({
message,
path,
reaction,
}: {
message: string;
path: string | null;
reaction: string | null;
}) {
const { copied, copyToClipboard } = useCopyCode();
return (
<Tooltip title={copied ? 'copied — paste into Claude Code' : 'copy prompt for Claude Code'}>
<IconButton
onClick={() => copyToClipboard(buildClaudePrompt(message, path, reaction))}
aria-label="Copy Claude Code prompt"
size="small"
sx={{
alignSelf: 'start',
color: copied ? colors.success : 'var(--ink-muted)',
border: '1px solid var(--rule)',
borderRadius: 1,
'&:hover': { bgcolor: 'var(--bg-surface)', color: colors.primary },
}}
>
{copied ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
</IconButton>
</Tooltip>
);
}
73 changes: 73 additions & 0 deletions app/src/sections/debug/DailyActivityChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';

import { SectionHeader } from 'src/components/SectionHeader';
import { colors, fontSize, semanticColors, typography } from 'src/theme';

export interface DailyImplPoint {
date: string;
impls_updated: number;
}

export function DailyActivityChart({ data }: { data: DailyImplPoint[] }) {
const maxDaily = Math.max(...data.map(p => p.impls_updated), 1);
const firstDate = data[0]?.date ?? '';
const lastDate = data[data.length - 1]?.date ?? '';

return (
<>
<SectionHeader prompt="❯" title={<em>daily activity</em>} />
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xs,
color: semanticColors.mutedText,
mb: 1,
}}
>
implementation updates · last 30 days
</Typography>
<Box
sx={{ display: 'flex', alignItems: 'flex-end', gap: 0.25, height: 70, overflow: 'hidden' }}
>
{data.map(point => (
<Tooltip key={point.date} title={`${point.date}: ${point.impls_updated} impls`} arrow>
<Box
sx={{
flex: 1,
height: `${Math.max((point.impls_updated / maxDaily) * 100, point.impls_updated > 0 ? 3 : 0)}%`,
bgcolor: colors.primaryDark,
opacity: 0.5,
borderRadius: '2px 2px 0 0',
minHeight: point.impls_updated > 0 ? 2 : 0,
'&:hover': { opacity: 0.8 },
transition: 'opacity 0.15s ease',
}}
/>
</Tooltip>
))}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 0.25 }}>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.micro,
color: semanticColors.mutedText,
}}
>
{firstDate}
</Typography>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.micro,
color: semanticColors.mutedText,
}}
>
{lastDate}
</Typography>
</Box>
</>
);
}
205 changes: 205 additions & 0 deletions app/src/sections/debug/FeedbackSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import React from 'react';

import { Link as RouterLink } from 'react-router-dom';

import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';

import { SectionHeader } from 'src/components/SectionHeader';
import { FEEDBACK_STATUS_OPTIONS, FeedbackMessageFilter } from 'src/sections/debug/constants';
import { CopyClaudePromptButton } from 'src/sections/debug/CopyClaudePromptButton';
import { FeedbackTopList, FeedbackTopPage } from 'src/sections/debug/FeedbackTopList';
import { timeAgo } from 'src/sections/debug/format';
import { debugControlSx } from 'src/sections/debug/styles';
import { colors, fontSize, semanticColors, typography } from 'src/theme';

export interface FeedbackMessage {
contact: string | null;
id: string;
reaction: string | null;
message: string | null;
path: string | null;
status: string;
created_at: string;
}

export function FeedbackSection({
topThumbsUp,
topThumbsDown,
feedbackStatusFilter,
setFeedbackStatusFilter,
feedbackMessages,
handleStatusChange,
}: {
topThumbsUp: FeedbackTopPage[];
topThumbsDown: FeedbackTopPage[];
feedbackStatusFilter: FeedbackMessageFilter;
setFeedbackStatusFilter: (val: FeedbackMessageFilter) => void;
feedbackMessages: FeedbackMessage[];
handleStatusChange: (id: string, newStatus: string) => void;
}) {
return (
<>
{/* Feedback: top pages + messages with triage (issue #5662) */}
<Box sx={{ mt: 4 }}>
<SectionHeader prompt="❯" title={<em>feedback · top pages</em>} />
</Box>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xs,
color: semanticColors.mutedText,
mb: 1,
}}
>
grouped by URL — clickable
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
<FeedbackTopList title="👍 thumbs up" items={topThumbsUp} accent={colors.success} />
<FeedbackTopList title="👎 thumbs down" items={topThumbsDown} accent={colors.error} />
</Box>

<Box sx={{ mt: 4, display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<SectionHeader prompt="❯" title={<em>feedback · messages</em>} />
<Box
component="select"
value={feedbackStatusFilter}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setFeedbackStatusFilter(e.target.value as FeedbackMessageFilter)
}
sx={{ ...debugControlSx, cursor: 'pointer', ml: 'auto' }}
>
<option value="open">open (new + in progress)</option>
<option value="">all statuses</option>
{FEEDBACK_STATUS_OPTIONS.map(s => (
<option key={s} value={s}>
{s.replace('_', ' ')}
</option>
))}
</Box>
</Box>
{feedbackMessages.length === 0 ? (
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xs,
color: semanticColors.mutedText,
py: 2,
}}
>
no messages{feedbackStatusFilter ? ` with status "${feedbackStatusFilter}"` : ''}
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 4 }}>
{feedbackMessages.map(m => (
<Box
key={m.id}
sx={{
border: '1px solid var(--rule)',
borderRadius: 1,
p: 1.5,
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'auto 1fr auto' },
columnGap: 1.5,
rowGap: 0.5,
alignItems: 'start',
bgcolor: m.status === 'new' ? 'var(--bg-surface)' : 'transparent',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25, minWidth: 90 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box component="span" sx={{ fontSize: 14 }}>
{m.reaction === 'thumbs_up'
? '👍'
: m.reaction === 'thumbs_down'
? '👎'
: m.reaction === 'idea'
? '💡'
: m.reaction === 'bug'
? '🪲'
: '—'}
</Box>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xxs,
color: semanticColors.mutedText,
}}
>
{timeAgo(m.created_at)}
</Typography>
</Box>
{m.path && (
<Link
component={RouterLink}
to={m.path}
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xxs,
color: colors.primary,
textDecoration: 'none',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'&:hover': { textDecoration: 'underline' },
}}
>
{m.path}
</Link>
)}
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xs,
color: 'var(--ink)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{m.message}
</Typography>
{m.contact && (
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.xxs,
color: semanticColors.mutedText,
mt: 0.5,
}}
>
contact: {m.contact}
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', gap: 0.75, alignItems: 'start' }}>
{m.message && (
<CopyClaudePromptButton message={m.message} path={m.path} reaction={m.reaction} />
)}
<Box
component="select"
value={m.status}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
handleStatusChange(
m.id,
e.target.value as import('src/sections/debug/constants').FeedbackStatus
)
}
sx={{ ...debugControlSx, cursor: 'pointer', alignSelf: 'start' }}
>
{FEEDBACK_STATUS_OPTIONS.map(s => (
<option key={s} value={s}>
{s.replace('_', ' ')}
</option>
))}
</Box>
</Box>
</Box>
))}
</Box>
)}
</>
);
}
Loading
Loading