diff --git a/.env.example b/.env.example index f7a8ecb5..cf439318 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,8 @@ NEXT_PUBLIC_GISCUS_CATEGORY_ID= GEMINI_API_KEY= NEXT_PUBLIC_OAUTH_CLIENT_ID= OAUTH_CLIENT_SECRET= -NEXT_PUBLIC_API_SITE=http://localhost:4000 \ No newline at end of file +NEXT_PUBLIC_API_SITE=http://localhost:4000 + +# Optional: LocationIQ API key for location autocomplete in SpatialInput. +# Obtain a free key at https://locationiq.com +NEXT_PUBLIC_LOCATIONIQ_KEY= \ No newline at end of file diff --git a/.opencode/commands/refactor.md b/.opencode/commands/refactor.md index c678e148..58560f20 100644 --- a/.opencode/commands/refactor.md +++ b/.opencode/commands/refactor.md @@ -2,7 +2,7 @@ description: 'Refactor a specific file for performance, architectural alignment, and readability.' model: 'opencode/claude-opus-4-6 temperature: 0.1 -top_p: 0.90 +top_p: 0.95 max_tokens: 8192 --- diff --git a/AGENTS.md b/AGENTS.md index f77fc9a4..4f70401e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ You operate in two mutually exclusive zones. Before writing any code, determine ### ZONE A: Frontend (Next.js 15, React 19) - Trigger: File path does NOT begin with `src/server/`. -- Ruleset: Read `@CONVENTIONS.md`. +- Ruleset: Read `CONVENTIONS.md`. - Constraint: Never apply Node.js ESM strict `.js` imports. Default to React Server Components. ### ZONE B: Backend (Node.js 22, Hono) diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 3b855bbd..c3c0348d 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -22,6 +22,7 @@ - **Component Exports**: Use `export default function ComponentName` for all components and pages. Avoid named exports for React components unless grouping multiple sub-components in a single file. - **API requests**: Never done from React components, but only from separate async service functions. Use `fetch`. - **File Length**: Refactor & split files beyond 300 lines. +- **SRP**: Strictly align to the Single Responsibility Principle. If a module does more than one conceptual thing, split it. ## 4. Styling & UI @@ -42,3 +43,9 @@ - **Coverage**: Generate `.test.ts` or `.test.tsx` files for every new component or service utility. - **Syntax**: Use Vitest (`describe`, `it`, `expect`). + +## 7. Nested apps (Hono server) + +- **Scope**: `src/server` folder contains server code. +- **Conventions**: Server code has its own conventions in `src/server/CONVENTIONS.md` +- **Linting**: Server code is linted by the same ESLint configuration as the frontend. diff --git a/TESTING_CONVENTIONS.md b/TESTING_CONVENTIONS.md index 14993568..f1c3dfb3 100644 --- a/TESTING_CONVENTIONS.md +++ b/TESTING_CONVENTIONS.md @@ -24,3 +24,8 @@ - **Test Runner API:** Use `describe`, `it`, and `expect` exclusively from `vitest`. - **Spying:** Exclusively use `vi.fn()` and `vi.spyOn()`. Jest imports are strictly forbidden. - **Zero-Tolerance for `any`:** The `any` type is strictly banned. Use `unknown` for ambiguous data payloads, or leverage `DeepPartial` and `ReturnType` to type incomplete mocked genealogical records or query responses accurately. + +## 7. Nested apps (Hono server) + +- **Scope**: `src/server` folder contains server code. +- **Configuration**: Server code has its own Vitest configuration used by `cd src/server` to it. diff --git a/package.json b/package.json index 744fed8e..36fd46ab 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react": "~19.0.0", "react-dom": "~19.0.0", "react-hook-form": "^7.71.2", + "react-imask": "^7.6.1", "react-leaflet": "~5.0.0", "react-leaflet-cluster": "^4.0.0", "react-paginate": "~8.3.0", @@ -61,6 +62,7 @@ "husky": "^9.1.7", "jsdom": "^26.0.0", "lint-staged": "^15.4.3", + "msw": "^2.14.6", "prettier": "^3.5.3", "schema-dts": "^1.1.5", "stylelint": "^16.2.1", @@ -86,7 +88,7 @@ "artifacts:clean": "./scripts/clean-repo/clean-repo.sh", "build": "next build", "data:modernize-russian": "tsx src/modernize-russian/index.ts", - "dev": "next dev --turbopack", + "dev": "PORT=3001 next dev --turbopack", "dev:server": "cd src/server && yarn dev", "docker:typesense:remove": "docker rm typesense-server", "docker:typesense:start": "tsx src/dockerize-typesense/index.ts", diff --git a/specs/000-template.md b/specs/000-template.md index ddc5035c..a52c6ffa 100644 --- a/specs/000-template.md +++ b/specs/000-template.md @@ -49,31 +49,23 @@ context: --- -## 4. Hard Constraints - -- **React 19:** Do not implement manual `useMemo`/`useCallback` unless explicitly bypassing the React Compiler. Maintain strict Client/Server boundaries. -- **Backend ESM:** All relative imports in Hono/Node.js files MUST terminate with explicit `.js` extensions. -- **Isolation:** Do not modify schemas, context files, or unrelated components not explicitly listed in the `targets` frontmatter. - ---- - -## 5. Agentic Verification +## 4. Agentic Verification Execute the following commands to validate the implementation: 1. **Type & Lint Pass:** Run standard formatting and type checks. ```bash - /lint + ./scripts/opencode-check.sh src/path/to/file.ts # Both frontend and backend ``` 2. **Targeted Test Execution:** Run the specific route or backend test. ```bash - /test-route src/path/to/test.test.tsx + yarn test src/path/to/test.test.tsx # OR - /test-server + cd src/server && yarn test src/path/to/test.test.ts ``` @@ -81,5 +73,4 @@ Execute the following commands to validate the implementation: ```bash /verify-esm [Target File Path] - ``` diff --git a/specs/056-workspace-back-navigation.md b/specs/056-workspace-back-navigation.md new file mode 100644 index 00000000..bc7f57ba --- /dev/null +++ b/specs/056-workspace-back-navigation.md @@ -0,0 +1,70 @@ +--- +description: Add a navigation link to the transcription workspace allowing users to return to the project metadata page +status: draft +targets: + - src/app/transcribe/workspace/page.tsx +context: + - src/app/transcribe/workspace/page.module.css +--- + +# Workspace to Project Navigation + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** N/A + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** User enters the transcription workspace view at `/transcribe/workspace/?projectId=[id]`. +- **Behavior:** The workspace lacks a dedicated UI element to navigate back to the project configuration page (`/transcribe/project/?projectId=[id]`) to edit metadata or modify the image set. +- **Log/Trace:** + +```tsx +// Current table header lacks back navigation +
+

Транскрипція

+ +
+``` + +### Target / Resolved State + +- **Condition:** User enters the transcription workspace view. +- **Behavior:** The user is presented with a clear "Back to Project" button/link (e.g., using `lucide-react` `ArrowLeft` icon). Clicking this element redirects to `/transcribe/project/?projectId=[projectId]`. +- **Schema/Type Alteration:** + +```tsx +// No schema change; UI alteration only +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/page.tsx + +1. Import `Link` from `next/link` or an appropriate icon like `ArrowLeft` from `lucide-react`. +2. Locate a suitable header region (e.g., above or inside `styles.tableHeader`, or at the top of the workspace layout). +3. Inject the navigation element to route to `/transcribe/project/?projectId=${projectId}`. +4. Ensure the element is only rendered when `projectId` is available in state (though the component handles missing `projectId` by returning early with a loading state). +5. If necessary, apply class names and update `src/app/transcribe/workspace/page.module.css` for consistent spacing and alignment. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx +``` diff --git a/specs/057-workspace-image-viewer-state.md b/specs/057-workspace-image-viewer-state.md new file mode 100644 index 00000000..e07bbfc9 --- /dev/null +++ b/specs/057-workspace-image-viewer-state.md @@ -0,0 +1,74 @@ +--- +description: Display current image in workspace and persist selection in browser storage +status: draft +targets: + - src/app/transcribe/workspace/page.tsx +context: [] +--- + +# Workspace Persistent Image Viewer + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State (Browser LocalStorage) + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** User accesses the transcription workspace page. +- **Behavior:** The left panel shows a static placeholder regardless of the loaded project images. There is no state tracking which image is currently selected, nor any persistence across browser sessions. +- **Log/Trace:** + +```tsx +
+ +

Зображення для транскрибування

+

(Панель перегляду)

+
+``` + +### Target / Resolved State + +- **Condition:** User views the transcription workspace and multiple project images are loaded. +- **Behavior:** The left panel renders the currently selected image from the `images` array. The index of the current image is tracked in state and mirrored to `localStorage` (scoped by `projectId`). When the session is restarted, the component restores the last viewed image index from storage. +- **Schema/Type Alteration:** + +```ts +// Local state for the selected index +const [currentImageIndex, setCurrentImageIndex] = useState(0); +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/page.tsx + +1. Introduce `currentImageIndex` state to keep track of the currently displayed image from the `images` array. +2. Implement a `useEffect` hook that runs on mount (when `projectId` is available and images are loaded) to read the stored index from `localStorage` using a project-specific key (e.g., `koreni_workspace_${projectId}_image_index`). Ensure it defaults to `0` and handles out-of-bounds cases. +3. Implement a `useEffect` hook that writes the `currentImageIndex` to `localStorage` whenever it changes, ensuring the value is persisted for the next session. +4. Replace the static `.imagePlaceholder` in the left panel with an Next.js `` using the `url` from `images[currentImageIndex]`. +5. Update the `.imageInfo` span in `.viewerHeader` to display the metadata (e.g., `storageKey`) of the currently selected image, rather than always showing the first one (`images[0]`). +6. Add Previous/Next navigation controls in the UI to allow changing the `currentImageIndex`, with boundary checks (min 0, max `images.length - 1`). + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx +``` + +2. **Targeted Test Execution:** Run the specific route test or overall app test to ensure no regression. + +```bash + /test src/app/transcribe/workspace/page.tsx +``` diff --git a/specs/058-workspace-image-zoom-scroll.md b/specs/058-workspace-image-zoom-scroll.md new file mode 100644 index 00000000..6cbd1b3f --- /dev/null +++ b/specs/058-workspace-image-zoom-scroll.md @@ -0,0 +1,90 @@ +--- +description: Implement horizontal and vertical zoom and pan capabilities for the workspace Image component. +status: draft +targets: + - src/app/transcribe/workspace/page.tsx +--- + +# Workspace Image Zoom and Scroll + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** The `` component within the transcribe workspace lacks native zoom and two-dimensional pan capabilities. +- **Behavior:** Users cannot visually inspect specific regions of the document by zooming in and scrolling across the image with their mouse. +- **Log/Trace:** + +```ts +// Currently lacks responsive scale and translation state mechanics +const WorkspaceImage = () => { + return ; +} +``` + +### Target / Resolved State + +- **Condition:** The workspace image view requires robust navigational controls for detailed image analysis. +- **Behavior:** The `` component dynamically reflects local state for scale, translateX, and translateY. It registers native mouse events for wheel zoom and pointer drag panning in both directions. +- **Schema/Type Alteration:** + +```ts +interface TransformState { + scale: number; + x: number; + y: number; +} + +interface DragState { + isDragging: boolean; + startX: number; + startY: number; +} +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/page.tsx + +1. **Transform State Management:** Introduce local state variables for `scale`, `translateX`, and `translateY`. Ensure reasonable constraints (e.g., minimum scale > 0.1, maximum scale). +2. **Wheel Zooming:** + - Add a wheel event handler on the image container. + - Calculate the delta from `event.deltaY` to increment or decrement the `scale` state. + - Prevent default scroll behavior while hovering the image block. +3. **Button Zooming:** + - Integrate UI buttons for explicit Zoom In and Zoom Out operations that update the `scale` state. +4. **Drag Scrolling (Panning):** + - Bind `onMouseDown` to the image container to initialize dragging state (`isDragging = true`) and record the initial `clientX` / `clientY`. + - Bind `onMouseMove` to calculate the distance moved since the last point, and update `translateX` and `translateY` states accordingly. + - Bind `onMouseUp` and `onMouseLeave` to terminate the dragging sequence (`isDragging = false`). +5. **CSS Transformation Rendering:** + - Enforce `overflow: hidden` on the outer image container boundary. + - Apply inline styles to the inner container or the Image component: `transform: translate(${translateX}px, ${translateY}px) scale(${scale})`. + - Update cursor styles (`cursor: grab` natively, `cursor: grabbing` on drag). + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx +``` + +2. **Targeted Test Execution:** + +```bash + /test-route src/app/transcribe/workspace/page.test.tsx +``` diff --git a/specs/059-workspace-image-zoom-wheel.md b/specs/059-workspace-image-zoom-wheel.md new file mode 100644 index 00000000..6ce77fb3 --- /dev/null +++ b/specs/059-workspace-image-zoom-wheel.md @@ -0,0 +1,78 @@ +--- +description: Fix image zoom on mouse wheel by resolving early return ref attachment issue +status: draft +targets: + - src/app/transcribe/workspace/page.tsx +context: [] +--- + +# Fix Image Zoom on Mouse Wheel + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** The wheel zoom event listener is attached inside a `useEffect` with an empty dependency array (`[]`). However, the component contains an early return `if (!projectId || isLoading)`, causing `viewerReference.current` to be `null` during the initial mount. +- **Behavior:** The `useEffect` runs once on mount, observes a `null` ref, and exits without attaching the `wheel` event listener. When the loading state completes and the image viewer is rendered, the listener is never attached, causing mouse wheel zooming to silently fail. +- **Log/Trace:** + +```ts +// Runs on initial mount where viewerReference.current is null due to early return +useEffect(() => { + const viewer = viewerReference.current; + if (!viewer) return; // Exits here permanently + // ... +}, []); +``` + +### Target / Resolved State + +- **Condition:** The component finishes loading and renders the `imageViewer` div. +- **Behavior:** The mouse wheel correctly adjusts the image zoom level. The event listener is attached properly when the element becomes available, preventing default page scroll while zooming. +- **Schema/Type Alteration:** + +```ts +// useEffect correctly tracks when the viewer element becomes available +useEffect(() => { + const viewer = viewerReference.current; + if (!viewer) return; + + // ... attach listener +}, [isLoading, projectId]); // or using a callback ref +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/page.tsx + +1. Update the dependency array of the wheel zoom `useEffect` to include `isLoading` and `projectId` so it correctly evaluates and attaches the listener _after_ the early returns have passed and the `imageViewer` div is rendered in the DOM. +2. Ensure that the cleanup function correctly removes the event listener to avoid memory leaks when the component unmounts or re-renders. +3. Verify that using the mouse wheel adjusts the `transform.scale` state correctly without causing the entire page to scroll. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx + +``` + +2. **Targeted Test Execution:** (if applicable UI tests exist) + +```bash + /test src/app/transcribe/workspace/page.tsx +``` diff --git a/specs/060-workspace-image-panning-bug.md b/specs/060-workspace-image-panning-bug.md new file mode 100644 index 00000000..09b26fcc --- /dev/null +++ b/specs/060-workspace-image-panning-bug.md @@ -0,0 +1,71 @@ +--- +description: Fix image panning behavior by preventing default browser drag-and-drop on the viewer image. +status: draft +targets: + - src/app/transcribe/workspace/page.tsx +context: [] +--- + +# Fix Workspace Image Panning Bug + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** The user presses the left mouse button on the workspace image and moves the mouse to pan the view. +- **Behavior:** The browser intercepts the action as a default image drag-and-drop operation. This prevents the custom `onMouseMove` handler from updating the panning state while the button is held. Panning erratically activates only after releasing the button. +- **Log/Trace:** + +```tsx +... +``` + +### Target / Resolved State + +- **Condition:** The user presses the left mouse button on the workspace image and moves the mouse. +- **Behavior:** The image pans smoothly and synchronously with the mouse movement while the button is held down. Panning stops immediately when the button is released. Default browser image dragging is disabled. +- **Schema/Type Alteration:** + +```tsx +// Image component is rendered with draggable={false} + +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/page.tsx + +1. Locate the `` component inside the `viewerReference` container in the render tree. +2. Add the `draggable={false}` attribute to the `` component to disable native HTML drag-and-drop behavior. +3. (Optional but recommended) In `handleMouseDown`, add `event.preventDefault()` or ensure the container's CSS has `user-select: none` to prevent text selection interference during the drag operation. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx +``` diff --git a/specs/061-workspace-page-name-edit.md b/specs/061-workspace-page-name-edit.md new file mode 100644 index 00000000..787ed441 --- /dev/null +++ b/specs/061-workspace-page-name-edit.md @@ -0,0 +1,76 @@ +--- +description: Allow editing pageName for an image and disable transcription table when pageName is missing. +status: draft +targets: + - src/app/transcribe/workspace/page.tsx +context: + - src/app/transcribe/schemata.ts +--- + +# Workspace Page Name Edit + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State / Server Mutation + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** Users are not required to define a `pageName` prior to transcribing data. +- **Behavior:** The transcription table is always enabled, and there is no UI to explicitly enter or update a `pageName` for the current image. +- **Log/Trace:** + +```tsx +// Current table is always interactable +{/* inputs and buttons are enabled */}
+``` + +### Target / Resolved State + +- **Condition:** The current active image does not have a valid `pageName`. +- **Behavior:** + 1. The UI exposes an input field to edit the `pageName` of the current image. The value must start with a digit. The value should be saved to the server upon change/submission. + 2. The transcription table, along with its inputs and action buttons (e.g., "Додати рядок"), must be disabled. + 3. A clear announcement/prompt is displayed explaining that a `pageName` (e.g., "12", "12зв", "12а", "12азв") must be entered. The user is instructed to read it from the image or deduce it. +- **Schema/Type Alteration:** + +```tsx +// Table UI logic depends on pageName presence +const hasPageName = Boolean(images[currentImageIndex]?.pageName); +// If !hasPageName, disable inputs and show prompt +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/page.tsx + +1. Add an input field for `pageName` tied to `images[currentImageIndex]`. It could be placed in the navigation controls or above the transcription table. The input must be in a form, alongside a submission button. +2. Implement a persistence mechanism: when the `pageName` is submitted, it must trigger a server PUT request to `/api/transcribe/projects/:projectId/images/:imageId` to update the backend and, on request success, correspondingly update the local `images` array state. +3. Compute whether the current image has a `pageName`. +4. If `pageName` is missing: + - Disable the "Додати рядок" button. + - Disable all inputs and delete buttons in the transcription rows. + - Render an announcement message (e.g., in place of or alongside the empty state) explicitly requiring the user to input the page's name (mentioning formats like "12", "12зв", "12а", "12азв") by reading it from the image or deducing it. +5. If `pageName` is present, the table and buttons should behave normally. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx +``` + +```bash + /test src/app/transcribe/workspace/page.test.tsx +``` diff --git a/specs/062-workspace-page-name-update-fix.md b/specs/062-workspace-page-name-update-fix.md new file mode 100644 index 00000000..0309394b --- /dev/null +++ b/specs/062-workspace-page-name-update-fix.md @@ -0,0 +1,94 @@ +--- +description: Fix pageName update in workspace returning validation error by implementing a proper PATCH endpoint +status: draft +targets: + - src/app/transcribe/workspace/page.tsx + - src/server/src/app.ts + - src/server/src/handlers/handle-project-image-patch.ts +context: + - src/server/src/handlers/handle-project-image-put.ts + - src/server/src/database/create-project.ts +--- + +# Fix Page Name Update Validation Error + +## 1. Architectural Boundary + +- **Execution Context:** Shared (Server Hono & Client Next.js) +- **Data Scope:** SQLite (Kysely) + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** User tries to save a page name for an image in the workspace (`src/app/transcribe/workspace/page.tsx`). +- **Behavior:** The frontend sends a `PUT` request with `application/json` body containing `{ "pageName": "..." }`. The server's `PUT` endpoint (`handleProjectImagePut.ts`) expects a `multipart/form-data` payload for a full image upload with required `file`, `pageSequence`, and `blurhash` fields, leading to a Zod validation error. +- **Log/Trace:** + +```json +{ + "error": "Invalid fields: [\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"received\": \"NaN\",\n \"path\": [\n \"pageSequence\"\n ],\n \"message\": \"Invalid input: expected number, received NaN\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"blurhash\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n }\n]" +} +``` + +### Target / Resolved State + +- **Condition:** User attempts to update the page name of an existing image. +- **Behavior:** The frontend sends a `PATCH` request to a dedicated endpoint handling partial image updates via JSON. The backend validates the `pageName` and updates the existing database record. +- **Schema/Type Alteration:** + +```ts +// Expected JSON payload for PATCH request +const patchImageSchema = z.object({ + pageName: z.string().nullable().optional(), +}); +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/server/src/handlers/handle-project-image-patch.ts + +1. Create a new handler to process `PATCH` requests. +2. Read the JSON body using `await c.req.json()`. +3. Validate the payload to allow partial updates (e.g., just `pageName`). +4. Execute an `UPDATE` query via Kysely on the `project_images` table for the corresponding `imageId` and `projectId`. +5. The endpoint must return the whole image object, just as `src/server/src/handlers/handle-project-image-get.ts` does. + +### 3.2. src/server/src/app.ts + +1. Import the newly created `handleProjectImagePatch`. +2. Register the route: `app.patch('/api/transcribe/projects/:projectId/images/:imageId', transcribeAuthMiddleware, handleProjectImagePatch);`. + +### 3.3. src/app/transcribe/workspace/page.tsx + +1. Change the `fetch` / `requestApi` call for updating the page name from `method: 'PUT'` to `method: 'PATCH'`. Instead of checking for plain success, unlock the transcribing only if the returned image object has a truthy `pageName` field value. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + /lint src/app/transcribe/workspace/page.tsx + /lint src/server/src/app.ts + /lint src/server/src/handlers/handle-project-image-patch.ts +``` + +2. **Targeted Test Execution:** Validate the server starts successfully and handles the requests. + +```bash + /test-server +``` + +3. **ESM Validation (Backend Only):** + +```bash + /verify-esm src/server/src/handlers/handle-project-image-patch.ts +``` diff --git a/specs/063-transcription-table-configuration.md b/specs/063-transcription-table-configuration.md new file mode 100644 index 00000000..fd222002 --- /dev/null +++ b/specs/063-transcription-table-configuration.md @@ -0,0 +1,107 @@ +--- +description: Refactor transcription table to be configuration-driven based on document types +status: draft +targets: + - src/app/transcribe/workspace/_components/transcription-table.tsx + - src/app/transcribe/workspace/_hooks/use-transcription-rows.ts +context: [] +--- + +# Make Transcription Table Configuration-Driven + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** Table columns and state fields are hardcoded to standard generic rows ("Прізвище", "Ім'я", "Рік народження / Вік", "Примітки"). +- **Behavior:** The application cannot adapt to specific historical document types which have varying layouts and column sets, limiting transcription accuracy and UX. +- **Log/Trace:** + +```ts +export interface TranscriptionRow { + id: string; + lastName: string; + firstName: string; + yearOrAge: string; + notes: string; +} +``` + +### Target / Resolved State + +- **Condition:** The table receives a configuration object specifying dynamic columns for a specific document type. +- **Behavior:** The component dynamically maps through the configured columns to render headers with tooltips (hints), and proper cell inputs (text/number). The row state handles arbitrary keys. +- **Schema/Type Alteration:** + +```ts +export type ColumnExpectedType = 'string' | 'number'; + +export interface ColumnConfig { + id: string; + title: string; + hint: string; + expectedType: ColumnExpectedType; +} + +export interface TranscriptionRow extends Record { + id: string; +} +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/\_hooks/use-transcription-rows.ts + +1. Refactor the `TranscriptionRow` interface to be a dynamic record with a required `id: string` property. +2. Update `useTranscriptionRows` hook to accept a parameter for `columns: ColumnConfig[]`. +3. Modify the initial row and `addRow` logic to dynamically populate empty strings for keys matching the provided `columns` configuration instead of the hardcoded default keys. +4. Modify `updateRow` signature to accept `field: string` (instead of `keyof TranscriptionRow`) and assign it correctly within the row map update. + +### 3.2. src/app/transcribe/workspace/\_components/transcription-table.tsx + +1. Import or define the `ColumnConfig` interface. Update `TranscriptionTableProperties` to require `columns: ColumnConfig[]`. +2. Refactor the `` layout to iterate over `columns`. Keep the static `Дії` (Actions) column at the end. Render the `title` and add tooltip UI logic for the `hint` when it evaluates to a non-empty string. +3. Refactor the `` layout so each `` iterates over `columns` to render `` inputs. Bind the `value` and `onChange` using the `column.id`. +4. Render the `input` HTML element's `type` attribute dynamically, mapping `expectedType === 'number'` to `type="number"` and defaulting to `type="text"`. + +### 3.3. src/app/transcribe/workspace/page.tsx (or parent container) + +1. Instantiate the Proof of Concept (PoC) "confession-list" configuration and pass it into `useTranscriptionRows` and `TranscriptionTable`. +2. The PoC columns array must be strictly defined as: + - `{ id: 'HH', title: '#HH', hint: 'Number of household', expectedType: 'number' }` + - `{ id: 'M', title: '#M', hint: 'Number of male', expectedType: 'number' }` + - `{ id: 'F', title: '#F', hint: 'Number of female', expectedType: 'number' }` + - `{ id: 'Name', title: 'Name', hint: '', expectedType: 'string' }` + - `{ id: 'aM', title: 'aM', hint: 'Age of male', expectedType: 'string' }` + - `{ id: 'aF', title: 'aF', hint: 'Age of female', expectedType: 'string' }` + - `{ id: 'Note', title: 'Note', hint: 'Anything to the right of the age', expectedType: 'string' }` + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + ./scripts/opencode-check.sh src/app/transcribe/workspace/_components/transcription-table.tsx + ./scripts/opencode-check.sh src/app/transcribe/workspace/_hooks/use-transcription-rows.ts + +``` + +2. **Targeted Test Execution:** Run the specific route or frontend tests. + +```bash + yarn test src/app/transcribe/workspace/page.test.tsx + +``` diff --git a/specs/064-workspace-image-restore-index.md b/specs/064-workspace-image-restore-index.md new file mode 100644 index 00000000..2b63e9a4 --- /dev/null +++ b/specs/064-workspace-image-restore-index.md @@ -0,0 +1,73 @@ +--- +description: Fix the last seen project image index restoration issue upon page refresh. +status: draft +targets: + - src/app/transcribe/workspace/_hooks/use-project-images.ts +context: [] +--- + +# Workspace Image Index Restoration Fix + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State / Local Storage + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** Page refresh triggers `useProjectImages` initialization. `projectId` is extracted, triggering the write `useEffect` which saves `currentImageIndex` (initial state `0`) to local storage before images are loaded. +- **Behavior:** The stored image index is overwritten by `0` prematurely, preventing the read `useEffect` from restoring the previously saved index once images are actually loaded and `images.length > 0`. +- **Log/Trace:** + +```ts +// src/app/transcribe/workspace/_hooks/use-project-images.ts +// The write effect runs early when projectId becomes valid, overwriting local storage with 0. +useEffect(() => { + if (!projectId) return; + + const storageKey = `koreni_workspace_${projectId}_image_index`; + localStorage.setItem(storageKey, currentImageIndex.toString()); // overwrites with 0 +}, [projectId, currentImageIndex]); +``` + +### Target / Resolved State + +- **Condition:** Hook initialization correctly orchestrates reading from and writing to local storage. +- **Behavior:** Introduce an initialization flag (`hasRestoredIndex`). The restoration effect attempts to load the index and marks the initialization as complete. The persistence effect waits until `hasRestoredIndex` is true before writing any state back to local storage. +- **Schema/Type Alteration:** + +```ts +// Local state alteration within hook +const [hasRestoredIndex, setHasRestoredIndex] = useState(false); +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/\_hooks/use-project-images.ts + +1. Introduce a new state variable: `const [hasRestoredIndex, setHasRestoredIndex] = useState(false);` +2. Update the first `useEffect` (persistence read): + - Check if `hasRestoredIndex` is already true, and if so, return early. + - At the end of the effect, regardless of whether an index was found or not, set `setHasRestoredIndex(true);`. + - Update its dependency array to include `hasRestoredIndex`. +3. Update the second `useEffect` (persistence write): + - Add a check `if (!hasRestoredIndex) return;` at the beginning to avoid prematurely saving state. + - Add `hasRestoredIndex` to the dependency array. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + ./scripts/opencode-check.sh src/app/transcribe/workspace/_hooks/use-project-images.ts +``` diff --git a/specs/065-workspace-page-name-guessing.md b/specs/065-workspace-page-name-guessing.md new file mode 100644 index 00000000..eacab3eb --- /dev/null +++ b/specs/065-workspace-page-name-guessing.md @@ -0,0 +1,92 @@ +--- +description: Implement page name guessing logic based on the previous two page names +status: draft +targets: + - src/app/transcribe/workspace/_components/page-name-form.tsx +context: + - src/app/transcribe/workspace/page.tsx +--- + +# Page Name Guessing Implementation + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** The user currently has to manually type the page name for every single image, even when it follows a predictable sequence. +- **Behavior:** The `PageNameForm` initializes the input with an empty string or the current image's existing `pageName`. It does not attempt to predict the next page name. +- **Log/Trace:** + +```ts +// pageNameInput is simply initialized to the current image's pageName or an empty string +setPageNameInput(image.pageName || ''); +``` + +### Target / Resolved State + +- **Condition:** The `PageNameForm` mounts or the `image` changes, and the current image does not have a `pageName` yet. +- **Behavior:** The component analyzes the previous two images' page names and populates `pageNameInput` with a guessed value if a known pattern is matched. The guess is not automatically submitted; the user must click "Save". +- **Schema/Type Alteration:** + +```ts +interface PageNameFormProperties { + projectId: string; + image: ProjectImage | undefined; + onImageUpdated: (updatedImage: ProjectImage) => void; + // Need the full list of images to inspect previous pages and check for untitled ones + images?: ProjectImage[]; +} +``` + +--- + +## 3. Execution Pipeline + +### 3.1. src/app/transcribe/workspace/\_components/page-name-form.tsx + +1. Update the `PageNameFormProperties` interface to receive the `images` array (sorted in the correct order). +2. Create a helper function (e.g., `guessNextPageName(currentIndex: number, images: ProjectImage[])`) that implements the following exact rules: + - If this page is the 1st or 2nd (`currentIndex < 2`), return `null` (skip guessing). + - If there are untitled pages before the current one (`images.slice(0, currentIndex).some(img => !img.pageName)`), return `null`. + - Let `prev2` be the page name of the page before the previous one (`images[currentIndex - 2].pageName`). + - Let `prev1` be the page name of the previous one (`images[currentIndex - 1].pageName`). + - Parse integer numbers `N` and `M` where applicable. + - If `prev2` is "N" and `prev1` is "M", where N and M are numbers and N + 1 = M, guess `(M + 1).toString()`. + - If `prev2` is "Nзв" and `prev1` is "M", where N and M are numbers and N + 1 = M, guess `"Mзв"`. + - If `prev2` is "N" and `prev1` is "Nзв", guess `(N + 1).toString()`. + - If `prev2` is "N" and `prev1` is "Nа", guess `(N + 1).toString()`. + - If `prev2` is "Na" and `prev1` is "M", where N + 1 = M, guess `(M + 1).toString()`. (Note: 'a' and 'а' may be mixed, handle parsing gracefully). + - If `prev2` is "Nзв" and `prev1` is "Nа", guess `"Nазв"`. + - If `prev2` is "Nа" and `prev1` is "Naзв", guess `(N + 1).toString()`. + - Otherwise, return `null` (don't guess). +3. Update the `useEffect` that listens to `image` and `images`. If `image.pageName` is missing, calculate the guess using the helper function. If a guess is produced, set it via `setPageNameInput(guessedName)`. +4. Ensure the guess only populates the input field without submitting it (the `disabled` logic or submit handling should remain user-initiated). + +### 3.2. Parent Component (e.g. workspace page or context) + +1. Ensure the `PageNameForm` component is passed the `images` array from the current project so that it has the required context to perform the guessing logic. + +--- + +## 4. Agentic Verification + +Execute the following commands to validate the implementation: + +1. **Type & Lint Pass:** Run standard formatting and type checks. + +```bash + ./scripts/opencode-check.sh src/app/transcribe/workspace/_components/page-name-form.tsx +``` + +2. **Targeted Test Execution:** Add unit tests to verify the guessing logic algorithm covers all the rules accurately. + +```bash + yarn test src/app/transcribe/workspace/_components/page-name-form.test.tsx +``` diff --git a/specs/066-transcription-table-add-row.md b/specs/066-transcription-table-add-row.md new file mode 100644 index 00000000..51d96750 --- /dev/null +++ b/specs/066-transcription-table-add-row.md @@ -0,0 +1,108 @@ +--- +description: Optimize creation of new rows in the transcription table +status: draft +targets: + - src/app/transcribe/workspace/_components/transcription-table.tsx +context: + - src/app/transcribe/workspace/_hooks/use-transcription-rows.ts + - src/app/transcribe/workspace/page.test.tsx +--- + +# Optimize Creation of New Rows in Transcription Table + +## 1. Architectural Boundary + +- **Execution Context:** Client (Next.js & React 19) +- **Data Scope:** Local State + +--- + +## 2. State Transition Matrix + +### Fault / Current State + +- **Condition:** The user needs to add new rows during transcription. +- **Behavior:** The "Додати рядок" (Add Row) button is located at the top of the table. This interrupts keyboard navigation and data entry flow. Additionally, there is no way to insert a row at a specific position (e.g., if a row was accidentally skipped). +- **Log/Trace:** + +```tsx +// src/app/transcribe/workspace/_components/transcription-table.tsx +
+

Транскрипція

+ +
+``` + +### Target / Resolved State + +- **Condition:** The user needs to add new rows during transcription. +- **Behavior:** The main "Додати рядок" button is moved to the bottom of the table, facilitating continuous data entry. Every row's "Дії" cell includes an inline icon button to insert a new empty row immediately above it. +- **Schema/Type Alteration:** + +```ts +// src/app/transcribe/workspace/_hooks/use-transcription-rows.ts +export function useTranscriptionRows(columns: ColumnConfig[]) { + // ... + const addRow = (index?: number) => { ... }; + // OR introduce: const insertRow = (index: number) => { ... }; +} + +// src/app/transcribe/workspace/_components/transcription-table.tsx +interface TranscriptionTableProperties { + // ... + onAddRow: (index?: number) => void; +} +``` + +--- + +## 3. Execution Pipeline + +### 3.1. `src/app/transcribe/workspace/_hooks/use-transcription-rows.ts` + +1. Refactor `addRow` to optionally accept an `index: number` parameter, or introduce a new `insertRow(index: number)` method. +2. If `index` is provided, insert the new empty row at the specified `index` using array slicing or `splice`. If omitted, append it to the end. + +### 3.2. `src/app/transcribe/workspace/_components/transcription-table.tsx` + +1. Remove the "Додати рядок" `