e39def8f02
- Level-up build-route advisor (src/features/characters/sheet/LevelUpAdvisor.tsx + useLevelUpAdvisor): ~4 system-aware routes plus a custom one; choosing a route expands it into concrete next-level steps. AI when configured, deterministic fallback (src/lib/assistant/levelup.ts) otherwise. Embedded in the existing HP-only LevelUpModal, which stays primary. - Level-up prompts/schemas added to prompts.ts (buildRoutes/buildSteps), each leading with the system constraint + class + next level. - Campaign Insights section on the Assistant page renders deterministic themes, with an optional 'ask the assistant to expand' affordance when AI is on. levelup + prompt unit tests (4 routes both systems, system vocabulary, custom route); e2e for the deterministic route→steps flow and the insights section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
import { useState } from 'react';
|
|
import type { Campaign, Character } from '@/lib/schemas';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Input } from '@/components/ui/Input';
|
|
import { useLevelUpAdvisor } from '@/features/assistant/useLevelUpAdvisor';
|
|
|
|
/**
|
|
* Build-route advisor shown alongside the HP-only level-up flow. Presents ~4
|
|
* system-aware routes (plus a custom one); choosing one expands it into concrete
|
|
* steps. AI-powered when configured, deterministic otherwise.
|
|
*/
|
|
export function LevelUpAdvisor({ campaign, character }: { campaign: Campaign; character: Character }) {
|
|
const { state, routes, steps, chosen, source, message, canUseLlm, fetchRoutes, chooseRoute } =
|
|
useLevelUpAdvisor(campaign, character);
|
|
const [custom, setCustom] = useState('');
|
|
|
|
if (state === 'idle') {
|
|
return (
|
|
<div className="rounded-md border border-line bg-surface p-3">
|
|
<div className="mb-2 text-sm text-muted">Not sure where to take this character?</div>
|
|
<Button size="sm" variant="secondary" onClick={fetchRoutes}>
|
|
{canUseLlm ? 'Suggest build routes (AI)' : 'Suggest build routes'}
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-md border border-line bg-surface p-3" data-testid="levelup-advisor">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted">
|
|
{source === 'llm' ? 'AI build routes' : 'Build routes'}
|
|
</span>
|
|
{state === 'loading' && <span className="text-xs text-muted">Thinking…</span>}
|
|
</div>
|
|
{message && <p className="mb-2 text-xs text-muted">{message}</p>}
|
|
|
|
{routes && (
|
|
<ul className="space-y-2">
|
|
{routes.map((r) => (
|
|
<li key={r.title} className="rounded-md border border-line p-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-medium text-ink">{r.title}</div>
|
|
<div className="text-xs text-muted">{r.summary}</div>
|
|
<div className="text-xs italic text-muted">{r.playstyle}</div>
|
|
</div>
|
|
<Button size="sm" variant={chosen === r.title ? 'primary' : 'secondary'} onClick={() => chooseRoute(r.title)}>
|
|
Choose
|
|
</Button>
|
|
</div>
|
|
{chosen === r.title && steps && (
|
|
<ol className="mt-2 space-y-1 border-t border-line pt-2">
|
|
{steps.map((s, i) => (
|
|
<li key={i} className="text-sm text-ink">
|
|
<span className="font-medium">{s.label}:</span> <span className="text-muted">{s.detail}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
<div className="mt-3 flex gap-2">
|
|
<Input
|
|
value={custom}
|
|
onChange={(e) => setCustom(e.target.value)}
|
|
placeholder="Or describe your own route…"
|
|
aria-label="Custom build route"
|
|
className="text-sm"
|
|
/>
|
|
<Button size="sm" variant="secondary" disabled={!custom.trim()} onClick={() => chooseRoute(custom.trim())}>
|
|
Plan it
|
|
</Button>
|
|
</div>
|
|
{chosen && !routes?.some((r) => r.title === chosen) && steps && (
|
|
<ol className="mt-2 space-y-1 rounded-md border border-line p-2" data-testid="custom-steps">
|
|
{steps.map((s, i) => (
|
|
<li key={i} className="text-sm text-ink">
|
|
<span className="font-medium">{s.label}:</span> <span className="text-muted">{s.detail}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|