Skip to content

Commit 683b197

Browse files
author
Valentin Grüner
committed
feat(webapp): show practice-area standing and its evidence on the profile
Adds compact per-area cards to the developer profile — current standing, guidance, and recent development — and a detail view behind each one: the practices that make up the area, how its recent evidence compares with earlier reviewed work, and the review moments behind the verdict, down to the quoted lines a finding rests on. Delivered feedback can be marked helpful or not helpful. An area with no verdict says which silence it is: nothing observed yet, no opportunity in the reviewed work, or noticed but too uncertain to claim. Severities read as the action they ask for rather than a measured impact.
1 parent 566a3c1 commit 683b197

46 files changed

Lines changed: 6501 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": minor
3+
---
4+
5+
Developers can open a practice-area summary to see its contributing practices and the related feedback behind its status.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"hephaestus": minor
3+
---
4+
5+
A practice area with no verdict now says which kind of silence it is instead of one catch-all "No feedback yet": whether nothing has been observed yet, whether the reviewed work simply offered no opportunity, or whether something was noticed but withheld as still too uncertain. A developer can now tell a working reviewer from an unconfigured one.
6+
7+
Finding severities read as the action they ask for — "Fix now", "Fix before merge", "Nit", "FYI" — instead of claiming a measured "Major impact"/"Minor impact". The severity filter on the practice-area detail page uses the same wording as the findings it selects.
8+
9+
The evidence behind a finding now reads as a quoted file: its path as a header with the line range beside it, the quoted lines below with line numbers, and one block per file when a finding spans several. Long lines scroll instead of being clipped, and a finding no longer repeats a link to the pull request its own heading already links to.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": minor
3+
---
4+
5+
Learners can inspect complete review moments in practice-area details and mark delivered feedback as helpful or not helpful.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": minor
3+
---
4+
5+
Developer profiles now provide compact practice-area summaries of current standing, guidance, and recent development.

webapp/src/components/admin/curated-catalog/CuratedCatalogTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Link } from "@tanstack/react-router";
22
import { GripVertical, MoreHorizontal } from "lucide-react";
33
import type { CuratedArea, CuratedPracticeSummary } from "@/api/types.gen";
4-
import { getAreaVisual } from "@/components/admin/practice-catalog/area-visuals";
54
import { automatedReviewLimitationLabel } from "@/components/admin/practice-catalog/evidence-presentation";
65
import {
76
type CatalogEntryMoveActions,
87
type CatalogMoveActions,
98
SortableCatalogTree,
109
UNASSIGNED_CATALOG_BUCKET,
1110
} from "@/components/admin/practice-catalog/SortableCatalogTree";
11+
import { getAreaVisual } from "@/components/shared/area-visuals";
1212
import { Badge } from "@/components/ui/badge";
1313
import { Button } from "@/components/ui/button";
1414
import {

webapp/src/components/admin/practice-catalog/AreaVisualPicker.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { Check, Search } from "lucide-react";
22
import { useId, useState } from "react";
3-
import { Button } from "@/components/ui/button";
4-
import { Input } from "@/components/ui/input";
5-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
6-
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
7-
import { cn } from "@/lib/utils";
83
import {
94
areaSeed,
105
COLOR_KEYS,
@@ -14,7 +9,12 @@ import {
149
iconLabel,
1510
iconSearchText,
1611
PILL,
17-
} from "./area-visuals";
12+
} from "@/components/shared/area-visuals";
13+
import { Button } from "@/components/ui/button";
14+
import { Input } from "@/components/ui/input";
15+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
16+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
17+
import { cn } from "@/lib/utils";
1818

1919
export interface AreaVisualPickerProps {
2020
id?: string;

webapp/src/components/admin/practice-reviews/ReviewPracticeLink.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Link } from "@tanstack/react-router";
22
import type { Practice, ReviewPracticeArea } from "@/api/types.gen";
3-
import { getAreaVisual } from "@/components/admin/practice-catalog/area-visuals";
43
import { PracticeDetailHoverCard } from "@/components/admin/practice-catalog/PracticeDetailHoverCard";
4+
import { getAreaVisual } from "@/components/shared/area-visuals";
55
import { cn } from "@/lib/utils";
66

77
export interface ReviewPracticeLinkProps {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { EvidenceFileBlock } from "./EvidenceFileBlock";
3+
4+
const meta = {
5+
component: EvidenceFileBlock,
6+
title: "Profile/EvidenceFileBlock",
7+
parameters: {
8+
docs: {
9+
description: {
10+
component:
11+
"One quoted file behind a finding: path as header, lines beneath. The path truncates in the " +
12+
"middle so the file name survives, and long lines scroll with the line gutter pinned.",
13+
},
14+
},
15+
},
16+
decorators: [
17+
(Story) => (
18+
<div className="max-w-lg p-4">
19+
<Story />
20+
</div>
21+
),
22+
],
23+
} satisfies Meta<typeof EvidenceFileBlock>;
24+
25+
export default meta;
26+
type Story = StoryObj<typeof meta>;
27+
28+
export const Default: Story = {
29+
args: {
30+
location: {
31+
path: "server/src/main/java/de/tum/cit/aet/hephaestus/practices/review/ReadyAndTraceableHandoff.java",
32+
startLine: 60,
33+
endLine: 64,
34+
redacted: false,
35+
snippet: [
36+
" public DeliveryResult handoff(ReviewRequest request) {",
37+
" var context = contextBuilder.build(request);",
38+
" ReviewResult result = reviewService.evaluate(context);",
39+
" return deliveryService.publish(result, request.recipient());",
40+
" }",
41+
].join("\n"),
42+
},
43+
},
44+
};
45+
46+
/** A single named line, the shape most findings carry. */
47+
export const SingleLine: Story = {
48+
args: {
49+
location: {
50+
path: "webapp/src/routes/_authenticated/w/$workspaceSlug/user/$username/index.tsx",
51+
startLine: 118,
52+
endLine: 118,
53+
redacted: false,
54+
snippet:
55+
" const statusesQuery = useQuery(getPracticeAreaStatusesOptions({ path: { workspaceSlug } }));",
56+
},
57+
},
58+
};
59+
60+
/** Collapsed: the second and later files of a finding start closed so the panel stays scannable. */
61+
export const Collapsed: Story = {
62+
args: {
63+
defaultOpen: false,
64+
location: {
65+
path: "server/src/main/resources/db/changelog/1786939608194_changelog.xml",
66+
startLine: 12,
67+
endLine: 14,
68+
redacted: false,
69+
snippet:
70+
'<changeSet id="1786939608194-1" author="hephaestus">\n <addColumn tableName="observation" />\n</changeSet>',
71+
},
72+
},
73+
};
74+
75+
/** The quote was withheld: the place is still named, and the block says the omission was deliberate. */
76+
export const Redacted: Story = {
77+
args: {
78+
location: {
79+
path: "docs/contributor/practice-catalogue.md",
80+
startLine: 31,
81+
endLine: 44,
82+
redacted: true,
83+
},
84+
},
85+
};
86+
87+
/** No directory to absorb truncation — the file name carries the whole identity. */
88+
export const BareFileName: Story = {
89+
args: {
90+
location: {
91+
path: "CHANGELOG.md",
92+
startLine: 1,
93+
endLine: 1,
94+
redacted: false,
95+
snippet: "## 0.14.0",
96+
},
97+
},
98+
};
99+
100+
/** A line far wider than the block: it scrolls, and the gutter stays pinned to the left edge. */
101+
export const LongLines: Story = {
102+
args: {
103+
location: {
104+
path: "server/src/main/java/de/tum/cit/aet/hephaestus/practices/observation/ObservationRepository.java",
105+
startLine: 1,
106+
endLine: 2,
107+
redacted: false,
108+
snippet: [
109+
'SELECT o.agent_job_id AS "jobId", MAX(o.observed_at) AS "reviewedAt" FROM observation o JOIN practice p ON p.id = o.practice_id JOIN practice_area a ON a.id = p.practice_area_id',
110+
"WHERE o.about_user_id = :aboutUserId AND p.workspace_id = :workspaceId AND a.slug = :areaSlug AND o.presence <> 'NOT_APPLICABLE'",
111+
].join("\n"),
112+
},
113+
},
114+
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { ChevronDownIcon, FileCodeIcon } from "lucide-react";
2+
import { useState } from "react";
3+
import { cn } from "@/lib/utils";
4+
import type { EvidenceLocation } from "./evidence";
5+
import { evidenceLineRangeLabel, splitPath } from "./evidence";
6+
7+
interface EvidenceFileBlockProps {
8+
location: EvidenceLocation;
9+
/** Collapsed blocks render the header only; the first block of a finding starts open. */
10+
defaultOpen?: boolean;
11+
}
12+
13+
/**
14+
* One quoted file: its path as a header, its lines below.
15+
*
16+
* <p>A citation binds its quote to the exact lines it came from, so the header and the body always describe
17+
* the same place. A quote that was withheld says so instead of rendering an empty block — a reader can tell
18+
* a redacted quote from a citation that never carried one.
19+
*
20+
* <p>The path truncates in the MIDDLE, never at the end: the file name is the identity a reader scans for,
21+
* so `src/main/java/…/ReadyAndTraceableHandoff.java` stays readable where a trailing ellipsis would cut the
22+
* name off. Long code lines scroll inside the block; the line gutter stays pinned so a scrolled line keeps
23+
* its number.
24+
*/
25+
export function EvidenceFileBlock({ location, defaultOpen = true }: EvidenceFileBlockProps) {
26+
const [isOpen, setIsOpen] = useState(defaultOpen);
27+
const { directory, fileName } = splitPath(location.path);
28+
const rangeLabel = evidenceLineRangeLabel(location);
29+
const lines = location.snippet?.split("\n") ?? [];
30+
const firstLineNumber = location.startLine;
31+
const hasSnippet = lines.length > 0;
32+
const bodyId = `evidence-${location.path.replace(/[^\w-]/g, "-")}-${firstLineNumber}`;
33+
34+
return (
35+
<figure className="min-w-0 overflow-hidden rounded-md border">
36+
<figcaption
37+
className={cn(
38+
"flex min-w-0 items-center gap-2 bg-code-header px-2.5 py-1.5",
39+
isOpen && hasSnippet && "border-b",
40+
)}
41+
>
42+
<FileCodeIcon className="size-3.5 shrink-0 text-muted-foreground" aria-hidden />
43+
{/* Two spans, not one truncating span: the directory absorbs the overflow so the file name
44+
can never be the part that disappears. */}
45+
<span className="flex min-w-0 flex-1 font-mono text-xs" title={location.path}>
46+
{directory && <span className="truncate text-muted-foreground">{directory}</span>}
47+
<span className="shrink-0 font-medium">{fileName}</span>
48+
</span>
49+
<span className="shrink-0 font-mono text-xs text-muted-foreground">{rangeLabel}</span>
50+
{location.redacted && (
51+
<span className="shrink-0 text-xs text-muted-foreground italic">quote withheld</span>
52+
)}
53+
{hasSnippet && (
54+
<button
55+
type="button"
56+
aria-expanded={isOpen}
57+
aria-controls={bodyId}
58+
aria-label={`${isOpen ? "Hide" : "Show"} the quoted lines of ${fileName}`}
59+
className="-me-1 shrink-0 rounded p-0.5 text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
60+
onClick={() => setIsOpen((open) => !open)}
61+
>
62+
<ChevronDownIcon
63+
className={cn("size-3.5 transition-transform", isOpen && "rotate-180")}
64+
/>
65+
</button>
66+
)}
67+
</figcaption>
68+
{hasSnippet && isOpen && (
69+
<pre
70+
id={bodyId}
71+
className="max-h-72 overflow-auto bg-code py-2 font-mono text-xs leading-relaxed"
72+
>
73+
<code>
74+
{lines.map((line, index) => {
75+
const lineNumber = firstLineNumber + index;
76+
return (
77+
<span
78+
// Line position IS the identity here; a repeated blank line is not a duplicate row.
79+
key={`${bodyId}-${lineNumber}`}
80+
className="grid grid-cols-[auto_1fr]"
81+
>
82+
<span className="sticky left-0 select-none bg-inherit pe-3 ps-2.5 text-end tabular-nums text-muted-foreground">
83+
{lineNumber}
84+
</span>
85+
<span className="pe-2.5">{line || " "}</span>
86+
</span>
87+
);
88+
})}
89+
</code>
90+
</pre>
91+
)}
92+
</figure>
93+
);
94+
}

0 commit comments

Comments
 (0)