Skip to content

Commit 1cffbca

Browse files
committed
feat(web): rebuild skill selector with new skills lineup
Replace code-review/security-audit (passive, removed from product) with interactive skills: eli5, pair-debug, hot-seat, test-me (all coming soon). Uses shared Card/Chip/Button components with Direction E styling.
1 parent f47c1ea commit 1cffbca

2 files changed

Lines changed: 89 additions & 98 deletions

File tree

apps/web/src/features/session/SkillSelector.test.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,20 @@ describe('SkillSelector', () => {
2020
expect(screen.getByTestId(`skill-card-${skill.name}`)).toBeTruthy()
2121
expect(screen.getByText(skill.label)).toBeTruthy()
2222
expect(screen.getByText(skill.description)).toBeTruthy()
23-
expect(screen.getByText(skill.duration)).toBeTruthy()
2423
}
2524
})
2625

2726
test('renders repo and PR info in header', () => {
2827
render(<SkillSelector {...defaultProps} />)
2928

3029
expect(screen.getByTestId('skill-selector')).toBeTruthy()
31-
// Use getAllByText since repo name may appear in multiple places
3230
const repoElements = screen.getAllByText('acme/helprs')
3331
expect(repoElements.length).toBeGreaterThan(0)
3432
const prElements = screen.getAllByText('#42')
3533
expect(prElements.length).toBeGreaterThan(0)
3634
})
3735

38-
test('calls onSelectSkill when an available skill card is clicked', () => {
36+
test('calls onSelectSkill when challenge-me card is clicked', () => {
3937
const onSelectSkill = vi.fn()
4038
render(<SkillSelector {...defaultProps} onSelectSkill={onSelectSkill} />)
4139

@@ -47,16 +45,23 @@ describe('SkillSelector', () => {
4745
const onSelectSkill = vi.fn()
4846
render(<SkillSelector {...defaultProps} onSelectSkill={onSelectSkill} />)
4947

50-
fireEvent.click(screen.getByTestId('skill-card-code-review'))
51-
fireEvent.click(screen.getByTestId('skill-card-security-audit'))
48+
fireEvent.click(screen.getByTestId('skill-card-eli5'))
49+
fireEvent.click(screen.getByTestId('skill-card-pair-debug'))
50+
fireEvent.click(screen.getByTestId('skill-card-hot-seat'))
51+
fireEvent.click(screen.getByTestId('skill-card-test-me'))
5252
expect(onSelectSkill).not.toHaveBeenCalled()
5353
})
5454

55-
test('shows Coming soon badge for unreleased skills', () => {
55+
test('shows "soon" badge for unreleased skills', () => {
5656
render(<SkillSelector {...defaultProps} />)
5757

58-
const badges = screen.getAllByText('Coming soon')
59-
expect(badges.length).toBe(2)
58+
const badges = screen.getAllByText('soon')
59+
expect(badges.length).toBe(4)
60+
})
61+
62+
test('shows DEFAULT badge for challenge-me', () => {
63+
render(<SkillSelector {...defaultProps} />)
64+
expect(screen.getByText('DEFAULT')).toBeTruthy()
6065
})
6166

6267
test('disables cards when disabled prop is true', () => {
@@ -82,10 +87,9 @@ describe('SkillSelector', () => {
8287
expect(brandElements.length).toBeGreaterThan(0)
8388
})
8489

85-
test('renders disclaimer text', () => {
86-
render(<SkillSelector {...defaultProps} />)
87-
expect(
88-
screen.getByText(/AI-generated content may be inaccurate/),
89-
).toBeTruthy()
90+
test('renders 5 skills total (1 active + 4 coming soon)', () => {
91+
expect(SKILLS.length).toBe(5)
92+
expect(SKILLS.filter(s => !s.comingSoon).length).toBe(1)
93+
expect(SKILLS.filter(s => s.comingSoon).length).toBe(4)
9094
})
9195
})
Lines changed: 72 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
/**
2-
* SkillSelector — displays available skills as cards with a Run button.
3-
*
4-
* Hardcoded skill list for now (will be dynamic later).
2+
* SkillSelector — displays available skills as cards.
3+
* Skills per spec: challenge-me (active), eli5/pair-debug/hot-seat/test-me (coming soon).
54
*/
65

6+
import { Button, Card, Chip, Overline } from '../../shared/components'
77
import type { Skill } from './containerTypes'
88

99
const SKILLS: Skill[] = [
1010
{
1111
name: 'challenge-me',
1212
label: 'Challenge Me',
13-
description: 'Socratic quiz -- tests your understanding of your own PR changes',
13+
description: 'Socratic quiz tests your understanding of your own PR changes. 3 questions targeting failure modes, system knowledge, and design trade-offs.',
1414
duration: '5-10 min',
1515
},
1616
{
17-
name: 'code-review',
18-
label: 'Code Review',
19-
description: 'Multi-layer adversarial code review',
20-
duration: '3-5 min',
17+
name: 'eli5',
18+
label: 'ELI5',
19+
description: 'Explain Like I\'m 5 — can you vulgarize your own code? Picks complex sections and asks you to explain them simply.',
20+
duration: '5-8 min',
2121
comingSoon: true,
2222
},
2323
{
24-
name: 'security-audit',
25-
label: 'Security Audit',
26-
description: 'Vulnerability scan on your diff',
27-
duration: '1-3 min',
24+
name: 'pair-debug',
25+
label: 'Pair Debug',
26+
description: 'Find the subtle bug Claude injected into your code. Ask questions, narrow it down, submit your diagnosis.',
27+
duration: '5-12 min',
28+
comingSoon: true,
29+
},
30+
{
31+
name: 'hot-seat',
32+
label: 'Hot Seat',
33+
description: 'Architecture hot seat — defend your design choices under pressure. Claude plays devil\'s advocate on your decisions.',
34+
duration: '5-10 min',
35+
comingSoon: true,
36+
},
37+
{
38+
name: 'test-me',
39+
label: 'Test Me',
40+
description: 'Predict whether test cases pass or fail on your code. Claude writes tests, you predict the outcome and explain why.',
41+
duration: '5-8 min',
2842
comingSoon: true,
2943
},
3044
]
@@ -47,93 +61,66 @@ export default function SkillSelector({
4761
return (
4862
<div
4963
data-testid="skill-selector"
50-
className="min-h-screen bg-primary text-text-primary font-sans flex flex-col items-center justify-center px-6"
64+
className="min-h-screen bg-bg text-ink font-sans flex flex-col items-center justify-center px-6 py-12"
5165
>
5266
{/* Header */}
5367
<div className="text-center mb-10 max-w-lg">
54-
<h1 className="font-mono text-[24px] font-bold mb-2">
55-
<span style={{ color: '#E2A039' }}>helPRs</span>
56-
</h1>
57-
<p className="text-text-secondary text-[14px]">
58-
<span className="font-mono text-text-primary">{repoFullName}</span>
59-
{' '}
60-
<span className="text-text-muted">#{prNumber}</span>
68+
<h1 className="font-mono text-lg font-bold text-accent mb-2">helPRs</h1>
69+
<p className="text-ink2 text-sm">
70+
Run something on{' '}
71+
<span className="font-mono text-ink">{repoFullName}</span>
72+
<span className="text-dim">#{prNumber}</span>
6173
</p>
62-
<p className="text-text-muted text-[13px] mt-2">Select a skill to run against this PR</p>
6374
</div>
6475

6576
{/* Skill cards */}
66-
<div className="grid gap-4 w-full max-w-lg">
77+
<div className="grid gap-3 w-full max-w-lg">
6778
{SKILLS.map((skill) => (
68-
<button
79+
<Card
6980
key={skill.name}
70-
data-testid={`skill-card-${skill.name}`}
71-
onClick={() => !skill.comingSoon && onSelectSkill(skill.name)}
72-
disabled={disabled || skill.comingSoon}
73-
className="text-left p-5 rounded-[10px] transition-all duration-150 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
74-
style={{
75-
background: '#1a1717',
76-
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.06), 0 2px 4px rgba(0, 0, 0, 0.2), 0 8px 24px -8px rgba(0, 0, 0, 0.3)',
77-
}}
78-
onMouseEnter={(e) => {
79-
if (!disabled) {
80-
e.currentTarget.style.boxShadow = '0 0 0 1px rgba(226, 160, 57, 0.3), 0 4px 12px rgba(0, 0, 0, 0.3), 0 16px 48px -12px rgba(0, 0, 0, 0.4)'
81-
}
82-
}}
83-
onMouseLeave={(e) => {
84-
e.currentTarget.style.boxShadow = '0 0 0 1px rgba(255, 255, 255, 0.06), 0 2px 4px rgba(0, 0, 0, 0.2), 0 8px 24px -8px rgba(0, 0, 0, 0.3)'
85-
}}
81+
hover={!skill.comingSoon}
82+
className={`${skill.comingSoon ? 'opacity-60' : 'cursor-pointer'}`}
8683
>
87-
<div className="flex items-start justify-between gap-4">
88-
<div className="flex-1 min-w-0">
89-
<h2 className="font-mono text-[15px] font-medium text-text-primary mb-1">
90-
{skill.label}
91-
</h2>
92-
<p className="text-text-secondary text-[13px] leading-relaxed">
93-
{skill.description}
94-
</p>
95-
</div>
96-
<div className="flex flex-col items-end gap-2 shrink-0">
97-
<span
98-
className="text-[11px] font-mono px-2 py-0.5 rounded-full"
99-
style={{
100-
color: 'rgba(255, 255, 255, 0.5)',
101-
background: 'rgba(255, 255, 255, 0.04)',
102-
border: '1px solid rgba(255, 255, 255, 0.06)',
103-
}}
104-
>
105-
{skill.duration}
106-
</span>
107-
{skill.comingSoon ? (
108-
<span
109-
className="text-[11px] font-mono px-2 py-0.5 rounded-full"
110-
style={{
111-
color: 'rgba(226, 160, 57, 0.7)',
112-
background: 'rgba(226, 160, 57, 0.08)',
113-
border: '1px solid rgba(226, 160, 57, 0.15)',
114-
}}
115-
>
116-
Coming soon
117-
</span>
118-
) : (
119-
<span
120-
className="text-[12px] font-semibold font-mono"
121-
style={{ color: '#E2A039' }}
122-
>
123-
Run &rarr;
124-
</span>
125-
)}
84+
<button
85+
data-testid={`skill-card-${skill.name}`}
86+
onClick={() => !skill.comingSoon && onSelectSkill(skill.name)}
87+
disabled={disabled || skill.comingSoon}
88+
className="w-full text-left disabled:cursor-not-allowed cursor-pointer"
89+
>
90+
<div className="flex items-start justify-between gap-4">
91+
<div className="flex-1 min-w-0">
92+
<div className="flex items-center gap-2 mb-1">
93+
<h2 className="font-mono text-sm font-semibold text-ink">
94+
{skill.label}
95+
</h2>
96+
{!skill.comingSoon && (
97+
<Chip variant="accent">DEFAULT</Chip>
98+
)}
99+
</div>
100+
<p className="text-ink2 text-[13px] leading-relaxed">
101+
{skill.description}
102+
</p>
103+
</div>
104+
<div className="flex flex-col items-end gap-2 shrink-0">
105+
<Chip>{skill.duration}</Chip>
106+
{skill.comingSoon ? (
107+
<Chip variant="warn">soon</Chip>
108+
) : (
109+
<Button variant="ghost" className="text-accent text-xs font-semibold px-0">
110+
Run &rarr;
111+
</Button>
112+
)}
113+
</div>
126114
</div>
127-
</div>
128-
</button>
115+
</button>
116+
</Card>
129117
))}
130118
</div>
131119

132-
{/* Footer disclaimer */}
133-
<p className="text-text-muted text-[11px] mt-8 text-center max-w-md">
134-
Skills run in ephemeral containers using your configured Claude credentials.
135-
AI-generated content may be inaccurate -- verify important findings.
136-
</p>
120+
{/* Footer */}
121+
<Overline className="mt-8 text-center max-w-md">
122+
// skills run in ephemeral containers using your configured Claude credentials
123+
</Overline>
137124
</div>
138125
)
139126
}

0 commit comments

Comments
 (0)