diff --git a/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md b/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md index 53dcacd..899ded3 100644 --- a/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md +++ b/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md @@ -3,8 +3,9 @@ title: Integrate name suggestions into encounter registration UI status: todo type: task +priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T15:56:44Z +updated_at: 2026-02-11T20:23:40Z parent: nuzlocke-tracker-igl3 --- @@ -12,17 +13,23 @@ Show name suggestions in the encounter registration flow so users can pick a nic ## Requirements -- When a user clicks an encounter slot and registers a new Pokemon, display 5-10 name suggestions below/near the nickname input +- When a user registers a new Pokemon encounter, display 5-10 name suggestions below/near the nickname input - Each suggestion is a clickable chip/button that fills in the nickname field - Include a "regenerate" button to get a fresh batch of suggestions - Only show suggestions if the run has a naming scheme selected - The nickname input should still be editable for manual entry +## Implementation Notes + +- **Data fetching**: Call `GET /api/v1/runs/{run_id}/name-suggestions?count=10` to get suggestions from the backend. +- **Regeneration**: Each call to the endpoint returns a fresh random batch (backend handles exclusion of used names). +- **No dictionary data in frontend**: All suggestion logic lives in the backend. + ## Checklist - [ ] Add a name suggestions component (chips/buttons with regenerate) - [ ] Integrate the component into the encounter registration modal/form -- [ ] Wire up the name suggestion engine to the component +- [ ] Wire up the backend API endpoint to the component via React Query - [ ] Ensure clicking a suggestion populates the nickname field -- [ ] Ensure regenerate fetches a new batch without repeating prior suggestions +- [ ] Ensure regenerate fetches a new batch from the API - [ ] Hide suggestions gracefully if no naming scheme is set on the run \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md b/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md index 4a6d948..7529b85 100644 --- a/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md +++ b/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md @@ -5,24 +5,32 @@ status: todo type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T15:56:48Z +updated_at: 2026-02-11T20:23:35Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-bi4e --- -Build the core logic that picks random name suggestions from the dictionary based on a selected naming scheme (category). +Build the backend service and API endpoint that picks random name suggestions from the dictionary based on a selected naming scheme. ## Requirements -- Given a category and a list of already-used names in the run, return 5-10 unique suggestions -- Suggestions must not include names already assigned to other Pokemon in the same run +- Given a category and a run ID, return 5-10 unique suggestions +- The engine queries the run's existing encounter nicknames and excludes them from suggestions - Support regeneration (return a fresh batch, avoiding previously shown suggestions where possible) - Handle edge case where category is nearly exhausted gracefully (return fewer suggestions) +## Implementation Notes + +- **Backend service**: A Python module that loads the dictionary JSON, filters by category, excludes used names, and picks random suggestions. +- **API endpoint**: `GET /api/v1/runs/{run_id}/name-suggestions?count=10` — reads the run's `naming_scheme`, fetches encounter nicknames, returns suggestions. +- **No new DB tables needed**: Used names come from `Encounter.nickname` on the run's encounters. +- **Caching**: Load the dictionary once and cache in memory (it's static data). + ## Checklist -- [ ] Create a service/utility module for name suggestion logic +- [ ] Create a service module for name suggestion logic (e.g. `services/name_suggestions.py`) +- [ ] Implement dictionary loading with in-memory caching - [ ] Implement random selection from a category with exclusion of used names -- [ ] Implement regeneration that avoids repeating previous suggestions +- [ ] Add API endpoint for fetching suggestions - [ ] Add unit tests for the suggestion logic \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-igl3--name-generation.md b/.beans/nuzlocke-tracker-igl3--name-generation.md index 072a2a5..7397b49 100644 --- a/.beans/nuzlocke-tracker-igl3--name-generation.md +++ b/.beans/nuzlocke-tracker-igl3--name-generation.md @@ -5,14 +5,22 @@ status: todo type: epic priority: normal created_at: 2026-02-05T13:45:15Z -updated_at: 2026-02-11T15:57:27Z +updated_at: 2026-02-11T20:23:14Z --- 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. +## Architecture Decisions + +- **Dictionary storage**: Static JSON file in `backend/src/app/seeds/data/`, alongside other seed data. Not exposed to frontend directly. +- **Dictionary format**: Category-keyed structure (`{ "mythology": ["Apollo", ...], "space": ["Nova", ...] }`) for fast lookup by naming scheme. Words may appear in multiple categories. +- **Suggestion logic**: Backend service with API endpoint. Frontend calls the backend to get suggestions. +- **Used-name tracking**: No new storage needed. The existing `Encounter.nickname` field already tracks assigned names. The suggestion engine queries encounter nicknames for the current run and excludes them. +- **Naming scheme per run**: Dedicated nullable `naming_scheme` column on `NuzlockeRun` (not in the `rules` JSONB). + ## Approach -- **Static dictionary**: A local data file (JSON) containing words tagged with categories (e.g. mythology, food, space, nature, warriors, music, etc.) +- **Static dictionary**: A local data file (JSON) containing words organised by category (e.g. mythology, food, space, nature, warriors, music, etc.) - **~150-200 words per category**: A typical Nuzlocke has ~100 encounters, so this provides ample variety without repetition. - **Name suggestion UX**: When registering a new encounter, the user is shown 5-10 suggested names from their chosen naming scheme. They can click one to select it, or regenerate for a fresh batch. - **Naming scheme selection**: Users pick a naming scheme (category) per run, either at run creation or in run settings. diff --git a/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md b/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md index 47d6d35..dda5241 100644 --- a/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md +++ b/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-m86o title: Add naming scheme selection to run configuration -status: todo +status: in-progress type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T15:56:48Z +updated_at: 2026-02-11T20:35:59Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-bi4e @@ -15,14 +15,24 @@ Allow users to select a naming scheme (category) for their Nuzlocke run. ## Requirements -- Add a `namingScheme` field to the run model/settings (optional, nullable — user may not want auto-naming) +- Add a `naming_scheme` column to the NuzlockeRun model (nullable string — user may not want auto-naming) - Provide a dropdown/selector in run creation and run settings where the user can pick a category -- List available categories dynamically from the dictionary data +- List available categories dynamically by querying a backend endpoint that reads the dictionary file - Allow changing the naming scheme mid-run +## Implementation Notes + +- **Storage**: Dedicated nullable `naming_scheme` column on `NuzlockeRun` (not in the `rules` JSONB). This is a first-class run setting. +- **Migration**: Add an Alembic migration for the new column. +- **API**: Add/update the run creation and update endpoints to accept `namingScheme`. +- **Categories endpoint**: Add a GET endpoint that returns the list of available category names from the dictionary file, so the frontend can populate the dropdown. + ## Checklist -- [ ] Add `namingScheme` field to the NuzlockeRun type/model -- [ ] Add naming scheme selector to run creation UI -- [ ] Add naming scheme selector to run settings UI -- [ ] Persist the selected naming scheme with the run data \ No newline at end of file +- [x] Add `naming_scheme` nullable column to NuzlockeRun model +- [x] Create Alembic migration for the new column +- [x] Update run Pydantic schemas to include `namingScheme` +- [x] Update run creation and update endpoints to persist the naming scheme +- [x] Add GET endpoint to list available naming categories from the dictionary +- [x] Add naming scheme selector to run creation UI +- [x] Add naming scheme selector to run settings UI \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md b/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md index 98d66ba..c03721d 100644 --- a/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md +++ b/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md @@ -5,25 +5,34 @@ status: todo type: task priority: normal created_at: 2026-02-11T15:56:26Z -updated_at: 2026-02-11T15:56:48Z +updated_at: 2026-02-11T20:23:29Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-c6ly --- -Create a JSON data file containing themed words for nickname generation. +Create a JSON data file containing themed words for nickname generation, stored in the backend alongside other seed data. ## Requirements -- Each word entry has: `word` (string) and `categories` (string array) +- Store at `backend/src/app/seeds/data/name_dictionary.json` +- Category-keyed structure for fast lookup: + ```json + { + "mythology": ["Apollo", "Athena", "Loki", ...], + "space": ["Apollo", "Nova", "Nebula", ...], + "food": ["Basil", "Sage", "Pepper", ...] + } + ``` +- Words may appear in multiple categories - Categories should include themes like: mythology, food, space, nature, warriors, music, literature, gems, ocean, weather, etc. - Target 150-200 words per category -- Words can belong to multiple categories - Words should be short, punchy, and suitable as Pokemon nicknames (ideally 1-2 words, max ~12 characters) +- This file is NOT seeded into the database — it is read directly by the backend service at runtime ## Checklist -- [ ] Define the data schema / TypeScript type for dictionary entries -- [ ] Create the JSON data file with initial categories +- [ ] Create `backend/src/app/seeds/data/name_dictionary.json` with the category-keyed structure - [ ] Populate each category with 150-200 words -- [ ] Validate no duplicates exist within the file \ No newline at end of file +- [ ] Validate no duplicates exist within a single category +- [ ] Add a utility function to load the dictionary from disk (with caching) \ No newline at end of file diff --git a/backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py b/backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py new file mode 100644 index 0000000..7ab0eda --- /dev/null +++ b/backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py @@ -0,0 +1,29 @@ +"""add naming_scheme to nuzlocke_runs + +Revision ID: e5f6a7b8c9d1 +Revises: d4e5f6a7b9c0 +Create Date: 2026-02-11 12:00:00.000000 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e5f6a7b8c9d1" +down_revision: str | Sequence[str] | None = "d4e5f6a7b9c0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "nuzlocke_runs", + sa.Column("naming_scheme", sa.String(50), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("nuzlocke_runs", "naming_scheme") diff --git a/backend/src/app/api/runs.py b/backend/src/app/api/runs.py index 5db7d65..a63e296 100644 --- a/backend/src/app/api/runs.py +++ b/backend/src/app/api/runs.py @@ -19,10 +19,16 @@ from app.schemas.run import ( RunResponse, RunUpdate, ) +from app.services.naming import get_naming_categories router = APIRouter() +@router.get("/naming-categories", response_model=list[str]) +async def list_naming_categories(): + return get_naming_categories() + + @router.post("", response_model=RunResponse, status_code=201) async def create_run(data: RunCreate, session: AsyncSession = Depends(get_session)): # Validate game exists @@ -35,6 +41,7 @@ async def create_run(data: RunCreate, session: AsyncSession = Depends(get_sessio name=data.name, status="active", rules=data.rules, + naming_scheme=data.naming_scheme, ) session.add(run) await session.commit() diff --git a/backend/src/app/models/nuzlocke_run.py b/backend/src/app/models/nuzlocke_run.py index 01269a4..795eff4 100644 --- a/backend/src/app/models/nuzlocke_run.py +++ b/backend/src/app/models/nuzlocke_run.py @@ -22,6 +22,7 @@ class NuzlockeRun(Base): ) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) hof_encounter_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None) + naming_scheme: Mapped[str | None] = mapped_column(String(50), nullable=True) game: Mapped["Game"] = relationship(back_populates="runs") encounters: Mapped[list["Encounter"]] = relationship(back_populates="run") diff --git a/backend/src/app/schemas/run.py b/backend/src/app/schemas/run.py index 32f7431..aa1abff 100644 --- a/backend/src/app/schemas/run.py +++ b/backend/src/app/schemas/run.py @@ -9,6 +9,7 @@ class RunCreate(CamelModel): game_id: int name: str rules: dict = {} + naming_scheme: str | None = None class RunUpdate(CamelModel): @@ -16,6 +17,7 @@ class RunUpdate(CamelModel): status: str | None = None rules: dict | None = None hof_encounter_ids: list[int] | None = None + naming_scheme: str | None = None class RunResponse(CamelModel): @@ -25,6 +27,7 @@ class RunResponse(CamelModel): status: str rules: dict hof_encounter_ids: list[int] | None = None + naming_scheme: str | None = None started_at: datetime completed_at: datetime | None diff --git a/backend/src/app/services/naming.py b/backend/src/app/services/naming.py new file mode 100644 index 0000000..42d9305 --- /dev/null +++ b/backend/src/app/services/naming.py @@ -0,0 +1,18 @@ +import json +from functools import lru_cache +from pathlib import Path + +DICTIONARY_PATH = Path(__file__).resolve().parents[1] / "seeds" / "data" / "name_dictionary.json" + + +@lru_cache(maxsize=1) +def _load_dictionary() -> dict[str, list[str]]: + if not DICTIONARY_PATH.exists(): + return {} + with open(DICTIONARY_PATH) as f: + return json.load(f) + + +def get_naming_categories() -> list[str]: + """Return sorted list of available naming category names.""" + return sorted(_load_dictionary().keys()) diff --git a/frontend/src/api/runs.ts b/frontend/src/api/runs.ts index 280c47f..2a7e4d4 100644 --- a/frontend/src/api/runs.ts +++ b/frontend/src/api/runs.ts @@ -28,3 +28,7 @@ export function updateRun( export function deleteRun(id: number): Promise { return api.del(`/runs/${id}`) } + +export function getNamingCategories(): Promise { + return api.get('/runs/naming-categories') +} diff --git a/frontend/src/hooks/useRuns.ts b/frontend/src/hooks/useRuns.ts index 68fb31e..7843523 100644 --- a/frontend/src/hooks/useRuns.ts +++ b/frontend/src/hooks/useRuns.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { getRuns, getRun, createRun, updateRun, deleteRun } from '../api/runs' +import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories } from '../api/runs' import type { CreateRunInput, UpdateRunInput } from '../types/game' export function useRuns() { @@ -51,3 +51,11 @@ export function useDeleteRun() { }, }) } + +export function useNamingCategories() { + return useQuery({ + queryKey: ['naming-categories'], + queryFn: getNamingCategories, + staleTime: Infinity, + }) +} diff --git a/frontend/src/pages/NewRun.tsx b/frontend/src/pages/NewRun.tsx index 90e7fd9..2ddeac3 100644 --- a/frontend/src/pages/NewRun.tsx +++ b/frontend/src/pages/NewRun.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { GameGrid, RulesConfiguration, StepIndicator } from '../components' import { useGames, useGameRoutes } from '../hooks/useGames' -import { useCreateRun, useRuns } from '../hooks/useRuns' +import { useCreateRun, useRuns, useNamingCategories } from '../hooks/useRuns' import type { Game, NuzlockeRules } from '../types' import { DEFAULT_RULES } from '../types' import { RULE_DEFINITIONS } from '../types/rules' @@ -14,11 +14,13 @@ export function NewRun() { const { data: games, isLoading, error } = useGames() const { data: runs } = useRuns() const createRun = useCreateRun() + const { data: namingCategories } = useNamingCategories() const [step, setStep] = useState(1) const [selectedGame, setSelectedGame] = useState(null) const [rules, setRules] = useState(DEFAULT_RULES) const [runName, setRunName] = useState('') + const [namingScheme, setNamingScheme] = useState(null) const { data: routes } = useGameRoutes(selectedGame?.id ?? null) const hiddenRules = useMemo(() => { @@ -44,7 +46,7 @@ export function NewRun() { const handleCreate = () => { if (!selectedGame) return createRun.mutate( - { gameId: selectedGame.id, name: runName, rules }, + { gameId: selectedGame.id, name: runName, rules, namingScheme }, { onSuccess: (data) => navigate(`/runs/${data.id}`) }, ) } @@ -180,6 +182,33 @@ export function NewRun() { /> + {namingCategories && namingCategories.length > 0 && ( +
+ + +

+ Get nickname suggestions from a themed word list when catching Pokemon. +

+
+ )} +

Summary @@ -203,6 +232,14 @@ export function NewRun() { {enabledRuleCount} of {totalRuleCount} enabled

+
+
Naming Scheme
+
+ {namingScheme + ? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1) + : 'None'} +
+
diff --git a/frontend/src/pages/RunDashboard.tsx b/frontend/src/pages/RunDashboard.tsx index fcda00b..b328840 100644 --- a/frontend/src/pages/RunDashboard.tsx +++ b/frontend/src/pages/RunDashboard.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { useParams, Link } from 'react-router-dom' -import { useRun, useUpdateRun } from '../hooks/useRuns' +import { useRun, useUpdateRun, useNamingCategories } from '../hooks/useRuns' import { useGameRoutes } from '../hooks/useGames' import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters' import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components' @@ -51,6 +51,7 @@ export function RunDashboard() { const createEncounter = useCreateEncounter(runIdNum) const updateEncounter = useUpdateEncounter(runIdNum) const updateRun = useUpdateRun(runIdNum) + const { data: namingCategories } = useNamingCategories() const [selectedEncounter, setSelectedEncounter] = useState(null) const [showEndRun, setShowEndRun] = useState(false) @@ -197,6 +198,37 @@ export function RunDashboard() { + {/* Naming Scheme */} + {namingCategories && namingCategories.length > 0 && ( +
+

+ Naming Scheme +

+ {isActive ? ( + + ) : ( + + {run.namingScheme + ? run.namingScheme.charAt(0).toUpperCase() + run.namingScheme.slice(1) + : 'None'} + + )} +
+ )} + {/* Active Team */}
diff --git a/frontend/src/types/game.ts b/frontend/src/types/game.ts index 8937bf5..87ad1ef 100644 --- a/frontend/src/types/game.ts +++ b/frontend/src/types/game.ts @@ -90,6 +90,7 @@ export interface NuzlockeRun { status: RunStatus rules: NuzlockeRules hofEncounterIds: number[] | null + namingScheme: string | null startedAt: string completedAt: string | null } @@ -132,6 +133,7 @@ export interface CreateRunInput { gameId: number name: string rules?: NuzlockeRules + namingScheme?: string | null } export interface UpdateRunInput { @@ -139,6 +141,7 @@ export interface UpdateRunInput { status?: RunStatus rules?: NuzlockeRules hofEncounterIds?: number[] + namingScheme?: string | null } export interface CreateEncounterInput {