Integrate name suggestions into encounter registration UI

Add clickable suggestion chips below the nickname input in the encounter
modal. Chips are fetched from GET /runs/{id}/name-suggestions via React
Query, shown only when a naming scheme is set. Clicking a chip fills in
the nickname; a regenerate button fetches a fresh random batch. Completes
the Name Generation epic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 21:48:29 +01:00
parent 2ac6c23577
commit 39d18c241e
6 changed files with 69 additions and 13 deletions

View File

@@ -1,11 +1,11 @@
--- ---
# nuzlocke-tracker-bi4e # nuzlocke-tracker-bi4e
title: Integrate name suggestions into encounter registration UI title: Integrate name suggestions into encounter registration UI
status: todo status: completed
type: task type: task
priority: normal priority: normal
created_at: 2026-02-11T15:56:44Z created_at: 2026-02-11T15:56:44Z
updated_at: 2026-02-11T20:23:40Z updated_at: 2026-02-11T20:48:02Z
parent: nuzlocke-tracker-igl3 parent: nuzlocke-tracker-igl3
--- ---
@@ -27,9 +27,9 @@ Show name suggestions in the encounter registration flow so users can pick a nic
## Checklist ## Checklist
- [ ] Add a name suggestions component (chips/buttons with regenerate) - [x] Add a name suggestions component (chips/buttons with regenerate)
- [ ] Integrate the component into the encounter registration modal/form - [x] Integrate the component into the encounter registration modal/form
- [ ] Wire up the backend API endpoint to the component via React Query - [x] Wire up the backend API endpoint to the component via React Query
- [ ] Ensure clicking a suggestion populates the nickname field - [x] Ensure clicking a suggestion populates the nickname field
- [ ] Ensure regenerate fetches a new batch from the API - [x] Ensure regenerate fetches a new batch from the API
- [ ] Hide suggestions gracefully if no naming scheme is set on the run - [x] Hide suggestions gracefully if no naming scheme is set on the run

View File

@@ -1,11 +1,11 @@
--- ---
# nuzlocke-tracker-igl3 # nuzlocke-tracker-igl3
title: Name Generation title: Name Generation
status: todo status: completed
type: epic type: epic
priority: normal priority: normal
created_at: 2026-02-05T13:45:15Z created_at: 2026-02-05T13:45:15Z
updated_at: 2026-02-11T20:44:23Z updated_at: 2026-02-11T20:48:02Z
--- ---
Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes. Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes.
@@ -29,6 +29,6 @@ Implement a dictionary-based nickname generation system for Nuzlocke runs. Inste
- [x] Word dictionary data file exists with multiple categories, each containing 150-200 words - [x] Word dictionary data file exists with multiple categories, each containing 150-200 words
- [x] Name suggestion engine picks random names from the selected category, avoiding duplicates already used in the run - [x] Name suggestion engine picks random names from the selected category, avoiding duplicates already used in the run
- [ ] Encounter registration UI shows 5-10 clickable name suggestions - [x] Encounter registration UI shows 5-10 clickable name suggestions
- [ ] User can regenerate suggestions if none fit - [x] User can regenerate suggestions if none fit
- [x] User can select a naming scheme per run - [x] User can select a naming scheme per run

View File

@@ -32,3 +32,7 @@ export function deleteRun(id: number): Promise<void> {
export function getNamingCategories(): Promise<string[]> { export function getNamingCategories(): Promise<string[]> {
return api.get('/runs/naming-categories') return api.get('/runs/naming-categories')
} }
export function getNameSuggestions(runId: number, count = 10): Promise<string[]> {
return api.get(`/runs/${runId}/name-suggestions?count=${count}`)
}

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useMemo } from 'react' import { useState, useEffect, useMemo } from 'react'
import { useRoutePokemon } from '../hooks/useGames' import { useRoutePokemon } from '../hooks/useGames'
import { useNameSuggestions } from '../hooks/useRuns'
import { import {
EncounterMethodBadge, EncounterMethodBadge,
getMethodLabel, getMethodLabel,
@@ -15,6 +16,8 @@ import type {
interface EncounterModalProps { interface EncounterModalProps {
route: Route route: Route
gameId: number gameId: number
runId: number
namingScheme?: string | null
existing?: EncounterDetail existing?: EncounterDetail
dupedPokemonIds?: Set<number> dupedPokemonIds?: Set<number>
retiredPokemonIds?: Set<number> retiredPokemonIds?: Set<number>
@@ -92,6 +95,8 @@ function pickRandomPokemon(
export function EncounterModal({ export function EncounterModal({
route, route,
gameId, gameId,
runId,
namingScheme,
existing, existing,
dupedPokemonIds, dupedPokemonIds,
retiredPokemonIds, retiredPokemonIds,
@@ -120,6 +125,10 @@ export function EncounterModal({
const isEditing = !!existing const isEditing = !!existing
const showSuggestions = !!namingScheme && status === 'caught' && !isEditing
const { data: suggestions, refetch: regenerate, isFetching: loadingSuggestions } =
useNameSuggestions(showSuggestions ? runId : null)
// Pre-select pokemon when editing // Pre-select pokemon when editing
useEffect(() => { useEffect(() => {
if (existing && routePokemon) { if (existing && routePokemon) {
@@ -380,6 +389,39 @@ export function EncounterModal({
placeholder="Give it a name..." placeholder="Give it a name..."
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
{showSuggestions && suggestions && suggestions.length > 0 && (
<div className="mt-2">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs text-gray-500 dark:text-gray-400">
Suggestions ({namingScheme})
</span>
<button
type="button"
onClick={() => regenerate()}
disabled={loadingSuggestions}
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 disabled:opacity-50 transition-colors"
>
{loadingSuggestions ? 'Loading...' : 'Regenerate'}
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{suggestions.map((name) => (
<button
key={name}
type="button"
onClick={() => setNickname(name)}
className={`px-2.5 py-1 text-xs rounded-full border transition-colors ${
nickname === name
? 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/40 dark:border-blue-600 dark:text-blue-300'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20'
}`}
>
{name}
</button>
))}
</div>
</div>
)}
</div> </div>
)} )}

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner' import { toast } from 'sonner'
import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories } from '../api/runs' import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories, getNameSuggestions } from '../api/runs'
import type { CreateRunInput, UpdateRunInput } from '../types/game' import type { CreateRunInput, UpdateRunInput } from '../types/game'
export function useRuns() { export function useRuns() {
@@ -59,3 +59,11 @@ export function useNamingCategories() {
staleTime: Infinity, staleTime: Infinity,
}) })
} }
export function useNameSuggestions(runId: number | null) {
return useQuery({
queryKey: ['name-suggestions', runId],
queryFn: () => getNameSuggestions(runId!),
enabled: runId !== null,
})
}

View File

@@ -1431,6 +1431,8 @@ export function RunEncounters() {
<EncounterModal <EncounterModal
route={selectedRoute} route={selectedRoute}
gameId={run!.gameId} gameId={run!.gameId}
runId={runIdNum}
namingScheme={run!.namingScheme}
existing={editingEncounter ?? undefined} existing={editingEncounter ?? undefined}
dupedPokemonIds={dupedPokemonIds} dupedPokemonIds={dupedPokemonIds}
retiredPokemonIds={retiredPokemonIds} retiredPokemonIds={retiredPokemonIds}