diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 9c303644..4a38216e 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -88,7 +88,11 @@ RUN apt update && apt install -y \
&& rm -rf /tmp/* /var/tmp/* \
&& rm -rf /var/lib/apt/lists/*
-
+# Node.js & npm for frontend build
+RUN apt-get update && \
+ apt-get install -y curl && \
+ curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
+ apt-get install -y nodejs
WORKDIR /tmp
diff --git a/.dockerignore b/.dockerignore
index 9b91c7d3..f5fe77dd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,7 +2,10 @@ resources/
!resources/api
!resources/pipelines
!resources/cache/aymurai
-!resources/cache/tfhub_modules
notebooks/
-*.egg-info/
\ No newline at end of file
+*.egg-info/
+
+frontend/node_modules/
+frontend/out/
+frontend-dist/
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 319cd7f0..a180c827 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -69,6 +69,7 @@ jobs:
- name: Run api tests
env:
DISKCACHE_ROOT: /tmp
+ SQLALCHEMY_DATABASE_URI: sqlite:////tmp/aymurai-test.db
run: uv run --no-sync pytest -q --tb=short --disable-warnings --color=yes --maxfail=5 tests/api
- name: Download pipelines data
diff --git a/.gitignore b/.gitignore
index 2dad2bac..feb0089a 100755
--- a/.gitignore
+++ b/.gitignore
@@ -135,3 +135,9 @@ aymurai/version.py
notebooks/**
!notebooks/**/
!notebooks/**/*.ipynb
+
+# Frontend build output
+frontend-dist/
+
+# pnpm local package store
+.pnpm-store/
diff --git a/Makefile b/Makefile
index 816c1c5b..898fc3ae 100644
--- a/Makefile
+++ b/Makefile
@@ -44,3 +44,19 @@ alembic-regenerate:
cd aymurai && \
uv run alembic revision --autogenerate -m "Create database" && \
uv run alembic upgrade head
+
+# --- Frontend build ---
+FRONTEND_DIR=frontend
+FRONTEND_DIST_DIR=frontend-dist
+
+frontend-install:
+ cd $(FRONTEND_DIR) && pnpm install
+
+frontend-build: frontend-install
+ cd $(FRONTEND_DIR) && pnpm run build:web
+ # Copy build output to frontend-dist
+ rm -rf $(FRONTEND_DIST_DIR)
+ cp -r $(FRONTEND_DIR)/out/renderer $(FRONTEND_DIST_DIR)
+
+frontend-clean:
+ rm -rf $(FRONTEND_DIST_DIR)
diff --git a/README.md b/README.md
index fae091de..573196fe 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,13 @@ docker run -d --name aymurai-backend-gpu --gpus all \
Open Swagger UI:
```text
-http://localhost:8899/docs
+http://localhost:8899/api/docs
+```
+
+Open the bundled frontend:
+
+```text
+http://localhost:8899/
```
## Quick Start (Docker Compose)
@@ -80,6 +86,7 @@ make api-logs
## Runtime Overview
- Framework: `FastAPI`
- Default API port: `8899`
+- Bundled frontend path: `GET /`
- DB engine: `SQLModel` + Alembic migrations on startup
- Default DB URI: `sqlite:////resources/cache/sqlite/database.db`
- Production pipeline configs:
@@ -87,16 +94,16 @@ make api-logs
- `resources/pipelines/production/datapublic/pipeline.json`
## Main Public Endpoints
-- `GET /server/healthcheck`
-- `GET /server/stats/summary`
-- `POST /misc/document-extract` (and deprecated alias `POST /document-extract`)
-- `POST /anonymizer/predict`
-- `POST /anonymizer/disambiguate`
-- `POST /anonymizer/validation`
-- `POST /anonymizer/anonymize-document`
-- `POST /datapublic/predict/{document_id}`
-- `GET /datapublic/validation/document/{document_id}`
-- `POST /datapublic/validation/document/{document_id}`
+- `GET /api/server/healthcheck`
+- `GET /api/server/stats/summary`
+- `POST /api/misc/document-extract` (and deprecated alias `POST /api/document-extract`)
+- `POST /api/anonymizer/predict`
+- `POST /api/anonymizer/disambiguate`
+- `POST /api/anonymizer/validation`
+- `POST /api/anonymizer/anonymize-document`
+- `POST /api/datapublic/predict/{document_id}`
+- `GET /api/datapublic/validation/document/{document_id}`
+- `POST /api/datapublic/validation/document/{document_id}`
For full request/response contracts and examples, see [docs/api/README.md](docs/api/README.md).
diff --git a/aymurai/api/core.py b/aymurai/api/core.py
index c9223455..185dba4f 100644
--- a/aymurai/api/core.py
+++ b/aymurai/api/core.py
@@ -1,9 +1,7 @@
from fastapi.routing import APIRouter
from .endpoints.routers.anonymizer import anonymizer
-from .endpoints.routers.anonymizer import database as anonymizer_database
from .endpoints.routers.datapublic import datapublic
-
from .endpoints.routers.misc import convert, document_extract
from .endpoints.routers.server import stats
@@ -23,11 +21,6 @@
prefix="/anonymizer",
tags=["anonymization/model"],
)
-# router.include_router(
-# anonymizer_database.router,
-# prefix="/anonymizer/database",
-# tags=["anonymization/database"],
-# )
# Datapublic
router.include_router(
diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py
index 6fde1db7..1be1a121 100644
--- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py
+++ b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py
@@ -5,7 +5,6 @@
from collections.abc import Iterable
from threading import Lock
-import torch
from fastapi import Body, Depends, Form, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse
from fastapi.routing import APIRouter
@@ -31,7 +30,7 @@
RenderPolicy,
TextRequest,
)
-from aymurai.settings import settings
+from aymurai.settings import DEFAULT_RENDER_POLICY, settings
from aymurai.text.anonymization import (
InvalidDocumentAnonymizer,
get_anonymizer,
@@ -48,7 +47,6 @@
RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH
-torch.set_num_threads = 100 # FIXME: polemic ?
pipeline_lock = Lock()
@@ -198,10 +196,7 @@ def _merge_render_policy(
Returns:
RenderPolicy: Effective render policy.
"""
- policy = RenderPolicy(
- suffix_mode="auto",
- suffix_threshold=1,
- )
+ policy = RenderPolicy.model_validate(DEFAULT_RENDER_POLICY)
def apply(incoming: RenderPolicy) -> None:
nonlocal policy
diff --git a/aymurai/api/endpoints/routers/datapublic/datapublic.py b/aymurai/api/endpoints/routers/datapublic/datapublic.py
index 91e1f0d5..4b531390 100644
--- a/aymurai/api/endpoints/routers/datapublic/datapublic.py
+++ b/aymurai/api/endpoints/routers/datapublic/datapublic.py
@@ -1,7 +1,6 @@
import os
from threading import Lock
-import torch
from fastapi import Body, Depends, HTTPException, Query
from fastapi.routing import APIRouter
from pydantic import UUID5
@@ -28,7 +27,6 @@
RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH
-torch.set_num_threads = 100 # FIXME: polemic ?
pipeline_lock = Lock()
diff --git a/aymurai/api/endpoints/routers/frontend/__init__.py b/aymurai/api/endpoints/routers/frontend/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/aymurai/api/endpoints/routers/frontend/__init__.py
@@ -0,0 +1 @@
+
diff --git a/aymurai/api/endpoints/routers/frontend/frontend.py b/aymurai/api/endpoints/routers/frontend/frontend.py
new file mode 100644
index 00000000..22ceaf41
--- /dev/null
+++ b/aymurai/api/endpoints/routers/frontend/frontend.py
@@ -0,0 +1,114 @@
+from pathlib import Path
+
+from fastapi import APIRouter, HTTPException
+from fastapi.responses import FileResponse, RedirectResponse, Response
+
+from aymurai.settings import settings
+
+router = APIRouter(include_in_schema=False)
+
+LEGACY_API_ROUTE_PREFIXES = (
+ "anonymizer",
+ "api",
+ "convert",
+ "datapublic",
+ "database",
+ "document-extract",
+ "docs",
+ "misc",
+ "openapi.json",
+ "redoc",
+ "server",
+)
+FEATURE_ROUTE_SLUGS = ("anonymizer", "data_set")
+FEATURE_ROUTE_STEPS = ("", "finish", "onboarding", "preview", "process", "validation")
+LEGACY_FEATURE_ROUTE_SLUGS = {
+ "ANONYMIZER": "anonymizer",
+ "DATA_SET": "data_set",
+}
+
+
+def _frontend_dist_dir() -> Path:
+ return Path(settings.FRONTEND_DIST_DIR).expanduser()
+
+
+def _split_asset_path(asset_path: str) -> list[str]:
+ return [segment for segment in asset_path.lstrip("/").split("/") if segment]
+
+
+def _is_frontend_feature_route(asset_path: str) -> bool:
+ segments = _split_asset_path(asset_path)
+ if not segments or segments[0] not in FEATURE_ROUTE_SLUGS:
+ return False
+
+ return len(segments) == 1 or segments[1] in FEATURE_ROUTE_STEPS
+
+
+def _is_legacy_api_route(asset_path: str) -> bool:
+ first_segment = asset_path.lstrip("/").split("/", maxsplit=1)[0]
+ if _is_frontend_feature_route(asset_path):
+ return False
+ return first_segment in LEGACY_API_ROUTE_PREFIXES
+
+
+def _canonical_frontend_path(asset_path: str) -> str | None:
+ segments = _split_asset_path(asset_path)
+ if not segments:
+ return None
+
+ if segments[0] == "app":
+ canonical_segments = segments[1:]
+ if canonical_segments:
+ canonical_feature = LEGACY_FEATURE_ROUTE_SLUGS.get(canonical_segments[0])
+ if canonical_feature:
+ canonical_segments = [canonical_feature, *canonical_segments[1:]]
+ return "/" + "/".join(canonical_segments)
+
+ canonical_feature = LEGACY_FEATURE_ROUTE_SLUGS.get(segments[0])
+ if canonical_feature:
+ return "/" + "/".join([canonical_feature, *segments[1:]])
+
+ return None
+
+
+def _resolve_frontend_path(asset_path: str) -> Path:
+ dist_dir = _frontend_dist_dir().resolve()
+ requested_path = (dist_dir / asset_path.lstrip("/")).resolve()
+ requested_path.relative_to(dist_dir)
+ return requested_path
+
+
+def _serve_frontend(asset_path: str = "") -> Response:
+ canonical_path = _canonical_frontend_path(asset_path)
+ if canonical_path is not None:
+ return RedirectResponse(canonical_path or "/")
+
+ if asset_path and _is_legacy_api_route(asset_path):
+ raise HTTPException(status_code=404, detail="API route not found.")
+
+ dist_dir = _frontend_dist_dir()
+ index_file = dist_dir / "index.html"
+ if not dist_dir.is_dir() or not index_file.is_file():
+ raise HTTPException(status_code=404, detail="Frontend build not found.")
+
+ if asset_path:
+ try:
+ requested_path = _resolve_frontend_path(asset_path)
+ except ValueError as error:
+ raise HTTPException(
+ status_code=404, detail="Frontend asset not found."
+ ) from error
+
+ if requested_path.is_file():
+ return FileResponse(requested_path)
+
+ if Path(asset_path).suffix:
+ raise HTTPException(status_code=404, detail="Frontend asset not found.")
+
+ return FileResponse(index_file)
+
+
+@router.get("/")
+@router.get("/{asset_path:path}")
+async def frontend_app(asset_path: str = "") -> Response:
+ return _serve_frontend(asset_path)
diff --git a/aymurai/api/main.py b/aymurai/api/main.py
index 4bfd9370..27ab0c70 100644
--- a/aymurai/api/main.py
+++ b/aymurai/api/main.py
@@ -2,19 +2,19 @@
import time
from contextlib import asynccontextmanager
-import torch
from alembic import command
from alembic.config import Config
from fastapi import FastAPI, Request
-from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.formparsers import MultiPartParser
from aymurai.api import core
+from aymurai.api.endpoints.routers.frontend import frontend
+from aymurai.api.startup.database import check_db_connection
+from aymurai.api.utils import configure_torch_threads
from aymurai.logger import get_logger
-from aymurai.settings import settings
from aymurai.pipeline import AymurAIPipeline
-from aymurai.api.startup.database import check_db_connection
+from aymurai.settings import settings
try:
from aymurai.version import __version__
@@ -24,7 +24,7 @@
logger = get_logger(__name__)
-torch.set_num_threads = 100 # FIXME: polemic ?
+configure_torch_threads()
RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH
@@ -54,8 +54,9 @@ async def lifespan(app: FastAPI):
logger.info(">> Running Alembic migrations")
alembic_cfg = Config(str(settings.ALEMBIC_INI_PATH))
command.upgrade(alembic_cfg, "head")
- except Exception as error:
- logger.error("Error while starting up:", error)
+ except Exception:
+ logger.exception("Error while starting up")
+ raise
yield
@@ -64,6 +65,9 @@ async def lifespan(app: FastAPI):
title="AymurAI API",
version=__version__,
lifespan=lifespan,
+ docs_url="/api/docs",
+ redoc_url="/api/redoc",
+ openapi_url="/api/openapi.json",
)
@@ -90,33 +94,20 @@ async def add_process_time_header(request: Request, call_next):
return response
-@api.get("/", response_class=RedirectResponse, include_in_schema=False)
-async def index():
- return "/docs"
-
-
-# @api.get("/docs", include_in_schema=False)
-# async def custom_swagger_ui_html():
-# return get_swagger_ui_html(
-# openapi_url=api.openapi_url,
-# title=f"{api.title} - Swagger UI",
-# swagger_css_url="https://cdn.jsdelivr.net/gh/danielperezrubio/swagger-dark-theme@main/assets/swagger-ui.min.css",
-# )
-
-
################################################################################
# MARK: API ENDPOINTS
################################################################################
# Healthcheck
-@api.get("/server/healthcheck", status_code=200, tags=["server"])
+@api.get("/api/server/healthcheck", status_code=200, tags=["server"])
def healthcheck():
return {"status": "ok"}
# Api endpoints
-api.include_router(core.router)
+api.include_router(core.router, prefix="/api")
+api.include_router(frontend.router)
if __name__ == "__main__":
diff --git a/aymurai/api/utils.py b/aymurai/api/utils.py
index 0132eaac..1944c9d7 100644
--- a/aymurai/api/utils.py
+++ b/aymurai/api/utils.py
@@ -1,4 +1,5 @@
import cachetools
+import torch
from aymurai.logger import get_logger
from aymurai.settings import settings
@@ -9,7 +10,7 @@
mem_cache = cachetools.TTLCache(
maxsize=settings.MEMORY_CACHE_MAXSIZE,
- ttl=settings.MEMORY_CACHE_MAXSIZE,
+ ttl=settings.MEMORY_CACHE_TTL,
getsizeof=lambda _: 0,
)
@@ -19,3 +20,7 @@
@cachetools.cached(cache=mem_cache)
def load_pipeline(path: str):
return AymurAIPipeline.load(path)
+
+
+def configure_torch_threads() -> None:
+ torch.set_num_threads(settings.TORCH_NUM_THREADS)
diff --git a/aymurai/config/__init__.py b/aymurai/config/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/aymurai/config/default_disambiguation_label_policies.yml b/aymurai/config/default_disambiguation_label_policies.yml
new file mode 100644
index 00000000..4de95df8
--- /dev/null
+++ b/aymurai/config/default_disambiguation_label_policies.yml
@@ -0,0 +1,124 @@
+PER:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: true
+
+NUM_EXPEDIENTE:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+CUIJ:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+NUM_ACTUACION:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+DNI:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+LINK:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+TELEFONO:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+FECHA:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+DIRECCION:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+TEXTO_ANONIMIZAR:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+LOC:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+CORREO_ELECTRONICO:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+CUIT_CUIL:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+NACIONALIDAD:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+EDAD:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+NOMBRE_ARCHIVO:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+BANCO:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+NUM_MATRICULA:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+ESTUDIOS:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+IP:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+USUARIX:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+CBU:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+MARCA_AUTOMOVIL:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+PATENTE_DOMINIO:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
+
+NUM_CAJA_AHORRO:
+ disambiguation: fuzzy
+ anonymize: true
+ use_subclass_when_available: false
diff --git a/aymurai/settings.py b/aymurai/settings.py
index 2079f561..259d03ed 100644
--- a/aymurai/settings.py
+++ b/aymurai/settings.py
@@ -1,5 +1,6 @@
import json
import os
+from copy import deepcopy
from pathlib import Path
from dotenv import load_dotenv
@@ -7,10 +8,18 @@
from pydantic_settings import BaseSettings
import aymurai
+from aymurai.utils.yaml_data import load_yaml
PARENT = Path(aymurai.__file__).parent
+DEFAULT_DISAMBIGUATION_LABEL_POLICIES = load_yaml(
+ str(PARENT / "config" / "default_disambiguation_label_policies.yml")
+)
+
+DEFAULT_RENDER_POLICY = {"suffix_mode": "auto", "suffix_threshold": 1}
+
+
def load_env():
load_dotenv(".env")
@@ -63,6 +72,7 @@ def assemble_cors_origins(cls, v) -> list[str]:
# Cachetools settings
MEMORY_CACHE_MAXSIZE: int = 1
MEMORY_CACHE_TTL: int = 60
+ TORCH_NUM_THREADS: int = 4
LIBREOFFICE_BIN: str = "libreoffice"
PDF_WATERMARK_FONT_REGULAR: str | None = None
@@ -76,29 +86,29 @@ def assemble_cors_origins(cls, v) -> list[str]:
THRESHOLD: int = 70
# Label policies (JSON dict: label -> {disambiguation, anonymize})
- DISAMBIGUATION_LABEL_POLICIES: dict | None = None
-
- @field_validator("DISAMBIGUATION_LABEL_POLICIES", mode="before")
- @classmethod
- def parse_label_policies(cls, v):
- if v is None or v == "":
- return None
- if isinstance(v, str):
- return json.loads(v)
- return v
+ DISAMBIGUATION_LABEL_POLICIES: dict | None = Field(
+ default_factory=lambda: deepcopy(DEFAULT_DISAMBIGUATION_LABEL_POLICIES)
+ )
# Render policy (JSON dict)
- RENDER_POLICY: dict | None = None
+ RENDER_POLICY: dict | None = Field(
+ default_factory=lambda: deepcopy(DEFAULT_RENDER_POLICY)
+ )
- @field_validator("RENDER_POLICY", mode="before")
+ @field_validator("DISAMBIGUATION_LABEL_POLICIES", "RENDER_POLICY", mode="before")
@classmethod
- def parse_render_policy(cls, v):
+ def parse_policies(cls, v):
if v is None or v == "":
return None
if isinstance(v, str):
return json.loads(v)
return v
+ FRONTEND_DIST_DIR: str = Field(
+ default="frontend-dist",
+ validation_alias=AliasChoices("AYMURAI_FRONTEND_DIST_DIR", "FRONTEND_DIST_DIR"),
+ )
+
load_env()
settings = Settings()
diff --git a/aymurai/utils/json_encoding.py b/aymurai/utils/json_encoding.py
index 5ff367a9..b5554812 100644
--- a/aymurai/utils/json_encoding.py
+++ b/aymurai/utils/json_encoding.py
@@ -5,6 +5,7 @@
import datetime
from typing import Any
+import numpy as np
import pandas as pd
from aymurai.logger import get_logger
@@ -60,6 +61,10 @@ def default(self, obj: Any) -> Any:
str(obj),
],
}
+ elif isinstance(obj, np.integer):
+ return int(obj)
+ elif isinstance(obj, np.floating):
+ return float(obj)
elif pd.isna(obj):
return "null"
else:
diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile
index bd492f09..9d1b4a0d 100644
--- a/docker/api/Dockerfile
+++ b/docker/api/Dockerfile
@@ -1,3 +1,24 @@
+# syntax=docker/dockerfile:1.7
+
+# -------------------
+# Frontend Build Stage
+# -------------------
+FROM node:lts-bookworm AS frontend-builder
+
+WORKDIR /frontend
+
+RUN npm install --global corepack@latest && \
+ corepack enable pnpm && \
+ corepack prepare pnpm@latest --activate
+
+COPY frontend/ ./
+
+RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
+ pnpm install --frozen-lockfile
+
+RUN pnpm run build:web
+
+
# -------------------
# Build Stage
# -------------------
@@ -74,10 +95,13 @@ COPY --from=builder --chown=app:app /app/.venv /app/.venv
# Copy static resources
COPY resources/pipelines /resources/pipelines
+COPY --from=frontend-builder /frontend/out/renderer /app/frontend-dist
# Set working directory
WORKDIR /app
+ENV FRONTEND_DIST_DIR=/app/frontend-dist
+
# Define start command
CMD uv run fastapi run --port 8899 .venv/lib/python3.10/site-packages/aymurai/api/main.py
diff --git a/docs/api/README.md b/docs/api/README.md
index 212e215e..4b3957c5 100644
--- a/docs/api/README.md
+++ b/docs/api/README.md
@@ -5,30 +5,30 @@ This document describes the currently mounted public API in `aymurai/api/main.py
## Base URL and OpenAPI
- Local base URL: `http://localhost:8899`
-- Swagger UI: `http://localhost:8899/docs`
-- OpenAPI JSON: `http://localhost:8899/openapi.json`
+- Swagger UI: `http://localhost:8899/api/docs`
+- OpenAPI JSON: `http://localhost:8899/api/openapi.json`
## Public Endpoints (Mounted)
| Method | Path | Purpose |
|---|---|---|
-| `GET` | `/server/healthcheck` | Service liveness check |
-| `GET` | `/server/stats/summary` | Runtime CPU/memory stats |
-| `POST` | `/document-extract` | Deprecated alias of `/misc/document-extract` |
-| `POST` | `/misc/document-extract` | Extract normalized paragraphs from uploaded document |
-| `POST` | `/anonymizer/predict` | NER prediction for a paragraph |
-| `POST` | `/anonymizer/disambiguate` | Canonical entity disambiguation + policy merge |
-| `POST` | `/anonymizer/validation` | Fetch paragraph-level manual validation |
-| `POST` | `/anonymizer/anonymize-document` | Compile and export anonymized document |
-| `POST` | `/datapublic/predict/{document_id}` | Predict entities for data-public flow |
-| `GET` | `/datapublic/validation/document/{document_id}` | Read document-level validation |
-| `POST` | `/datapublic/validation/document/{document_id}` | Save document-level validation |
-| `POST` | `/convert/pdf/odt` | Convert PDF to ODT |
-| `POST` | `/convert/pdf/docx` | Convert PDF to DOCX |
-| `POST` | `/convert/docx/odt` | Convert DOCX to ODT |
-| `POST` | `/convert/docx/pdf` | Convert DOCX to PDF |
-| `POST` | `/convert/odt/pdf` | Convert ODT to PDF |
-| `POST` | `/convert/odt/docx` | Convert ODT to DOCX |
+| `GET` | `/api/server/healthcheck` | Service liveness check |
+| `GET` | `/api/server/stats/summary` | Runtime CPU/memory stats |
+| `POST` | `/api/document-extract` | Deprecated alias of `/api/misc/document-extract` |
+| `POST` | `/api/misc/document-extract` | Extract normalized paragraphs from uploaded document |
+| `POST` | `/api/anonymizer/predict` | NER prediction for a paragraph |
+| `POST` | `/api/anonymizer/disambiguate` | Canonical entity disambiguation + policy merge |
+| `POST` | `/api/anonymizer/validation` | Fetch paragraph-level manual validation |
+| `POST` | `/api/anonymizer/anonymize-document` | Compile and export anonymized document |
+| `POST` | `/api/datapublic/predict/{document_id}` | Predict entities for data-public flow |
+| `GET` | `/api/datapublic/validation/document/{document_id}` | Read document-level validation |
+| `POST` | `/api/datapublic/validation/document/{document_id}` | Save document-level validation |
+| `POST` | `/api/convert/pdf/odt` | Convert PDF to ODT |
+| `POST` | `/api/convert/pdf/docx` | Convert PDF to DOCX |
+| `POST` | `/api/convert/docx/odt` | Convert DOCX to ODT |
+| `POST` | `/api/convert/docx/pdf` | Convert DOCX to PDF |
+| `POST` | `/api/convert/odt/pdf` | Convert ODT to PDF |
+| `POST` | `/api/convert/odt/docx` | Convert ODT to DOCX |
## Core Data Contracts
@@ -136,7 +136,7 @@ Note: the JSON snippets below are minimal valid examples. Real payloads may incl
```
```bash
-curl -s http://localhost:8899/server/healthcheck
+curl -s http://localhost:8899/api/server/healthcheck
```
#### `GET /server/stats/summary`
@@ -153,7 +153,7 @@ curl -s http://localhost:8899/server/healthcheck
```
```bash
-curl -s http://localhost:8899/server/stats/summary
+curl -s http://localhost:8899/api/server/stats/summary
```
### Document Extraction
@@ -174,7 +174,7 @@ curl -s http://localhost:8899/server/stats/summary
```bash
curl -s -X POST \
-F "file=@/resources/data/sample/document-01.docx" \
- http://localhost:8899/misc/document-extract
+ http://localhost:8899/api/misc/document-extract
```
Common errors:
@@ -189,7 +189,7 @@ Common errors:
- Response `200`: `DocumentInformation`
```bash
-curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \
+curl -s -X POST "http://localhost:8899/api/anonymizer/predict?use_cache=true" \
-H "Content-Type: application/json" \
-d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}'
```
@@ -218,7 +218,7 @@ curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \
- Response `200`: `DocumentAnnotations` (with `data` and effective `label_policies`)
```bash
-curl -s -X POST http://localhost:8899/anonymizer/disambiguate \
+curl -s -X POST http://localhost:8899/api/anonymizer/disambiguate \
-H "Content-Type: application/json" \
-d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}'
```
@@ -228,7 +228,7 @@ curl -s -X POST http://localhost:8899/anonymizer/disambiguate \
- Response `200`: `list[DocLabel] | null`
```bash
-curl -s -X POST http://localhost:8899/anonymizer/validation \
+curl -s -X POST http://localhost:8899/api/anonymizer/validation \
-H "Content-Type: application/json" \
-d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}'
```
@@ -240,7 +240,7 @@ curl -s -X POST http://localhost:8899/anonymizer/validation \
- Response `200`: binary anonymized `.odt` file
```bash
-curl -X POST http://localhost:8899/anonymizer/anonymize-document \
+curl -X POST http://localhost:8899/api/anonymizer/anonymize-document \
-F "file=@/resources/data/sample/document-01.docx" \
-F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}'
```
@@ -258,7 +258,7 @@ Common errors:
- Response `200`: `DocumentInformation`
```bash
-curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \
+curl -s -X POST "http://localhost:8899/api/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \
-H "Content-Type: application/json" \
-d '{"text":"Buenos Aires, 17 de noviembre de 2024"}'
```
@@ -268,7 +268,7 @@ curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8
- Response `404`: document not found
```bash
-curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc
+curl -s http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc
```
#### `POST /datapublic/validation/document/{document_id}`
@@ -276,7 +276,7 @@ curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-
- Response `200`: empty body
```bash
-curl -s -X POST http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \
+curl -s -X POST http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \
-H "Content-Type: application/json" \
-d '{"materia":"penal","violencia_de_genero":"si"}'
```
@@ -287,12 +287,12 @@ All conversion endpoints use `multipart/form-data` with a `file` field.
| Method | Path | Input | Output |
|---|---|---|---|
-| `POST` | `/convert/pdf/odt` | `.pdf` | `.odt` |
-| `POST` | `/convert/pdf/docx` | `.pdf` | `.docx` |
-| `POST` | `/convert/docx/odt` | `.docx` | `.odt` |
-| `POST` | `/convert/docx/pdf` | `.docx` | `.pdf` |
-| `POST` | `/convert/odt/pdf` | `.odt` | `.pdf` |
-| `POST` | `/convert/odt/docx` | `.odt` | `.docx` |
+| `POST` | `/api/convert/pdf/odt` | `.pdf` | `.odt` |
+| `POST` | `/api/convert/pdf/docx` | `.pdf` | `.docx` |
+| `POST` | `/api/convert/docx/odt` | `.docx` | `.odt` |
+| `POST` | `/api/convert/docx/pdf` | `.docx` | `.pdf` |
+| `POST` | `/api/convert/odt/pdf` | `.odt` | `.pdf` |
+| `POST` | `/api/convert/odt/docx` | `.odt` | `.docx` |
For PDF input endpoints, optional query param:
- `backend=libreoffice|pandoc` (default: `libreoffice`)
@@ -312,9 +312,9 @@ Common errors:
The following route modules exist in code but are not included in `core.router` at runtime:
- `aymurai/api/endpoints/routers/datapublic/dataset.py`
- - includes `/datapublic/dataset/*` CRUD/batch routes, but router is not mounted.
+ - includes `/api/datapublic/dataset/*` CRUD/batch routes, but router is not mounted.
- `aymurai/api/endpoints/routers/anonymizer/database.py`
- - `/anonymizer/database/*` routes exist, include is commented out.
+ - `/api/anonymizer/database/*` routes exist, include is commented out.
- `aymurai/api/endpoints/routers/database/*`
- additional DB admin routes exist, but no mounting in `core.router`.
diff --git a/docs/es/api/README.md b/docs/es/api/README.md
index adb3db4e..d51ec974 100644
--- a/docs/es/api/README.md
+++ b/docs/es/api/README.md
@@ -5,30 +5,30 @@ Este documento describe la API pública actualmente montada en `aymurai/api/main
## Base URL y OpenAPI
- Base local: `http://localhost:8899`
-- Swagger UI: `http://localhost:8899/docs`
-- OpenAPI JSON: `http://localhost:8899/openapi.json`
+- Swagger UI: `http://localhost:8899/api/docs`
+- OpenAPI JSON: `http://localhost:8899/api/openapi.json`
## Endpoints públicos (montados)
| Método | Path | Propósito |
|---|---|---|
-| `GET` | `/server/healthcheck` | Liveness del servicio |
-| `GET` | `/server/stats/summary` | Métricas de CPU/memoria |
-| `POST` | `/document-extract` | Alias deprecado de `/misc/document-extract` |
-| `POST` | `/misc/document-extract` | Extrae párrafos normalizados de un documento |
-| `POST` | `/anonymizer/predict` | Predicción NER por párrafo |
-| `POST` | `/anonymizer/disambiguate` | Desambiguación canónica + merge de políticas |
-| `POST` | `/anonymizer/validation` | Obtiene validación manual por párrafo |
-| `POST` | `/anonymizer/anonymize-document` | Compila y exporta documento anonimizado |
-| `POST` | `/datapublic/predict/{document_id}` | Predicción para flujo data-public |
-| `GET` | `/datapublic/validation/document/{document_id}` | Lee validación a nivel documento |
-| `POST` | `/datapublic/validation/document/{document_id}` | Guarda validación a nivel documento |
-| `POST` | `/convert/pdf/odt` | Convierte PDF a ODT |
-| `POST` | `/convert/pdf/docx` | Convierte PDF a DOCX |
-| `POST` | `/convert/docx/odt` | Convierte DOCX a ODT |
-| `POST` | `/convert/docx/pdf` | Convierte DOCX a PDF |
-| `POST` | `/convert/odt/pdf` | Convierte ODT a PDF |
-| `POST` | `/convert/odt/docx` | Convierte ODT a DOCX |
+| `GET` | `/api/server/healthcheck` | Liveness del servicio |
+| `GET` | `/api/server/stats/summary` | Métricas de CPU/memoria |
+| `POST` | `/api/document-extract` | Alias deprecado de `/api/misc/document-extract` |
+| `POST` | `/api/misc/document-extract` | Extrae párrafos normalizados de un documento |
+| `POST` | `/api/anonymizer/predict` | Predicción NER por párrafo |
+| `POST` | `/api/anonymizer/disambiguate` | Desambiguación canónica + merge de políticas |
+| `POST` | `/api/anonymizer/validation` | Obtiene validación manual por párrafo |
+| `POST` | `/api/anonymizer/anonymize-document` | Compila y exporta documento anonimizado |
+| `POST` | `/api/datapublic/predict/{document_id}` | Predicción para flujo data-public |
+| `GET` | `/api/datapublic/validation/document/{document_id}` | Lee validación a nivel documento |
+| `POST` | `/api/datapublic/validation/document/{document_id}` | Guarda validación a nivel documento |
+| `POST` | `/api/convert/pdf/odt` | Convierte PDF a ODT |
+| `POST` | `/api/convert/pdf/docx` | Convierte PDF a DOCX |
+| `POST` | `/api/convert/docx/odt` | Convierte DOCX a ODT |
+| `POST` | `/api/convert/docx/pdf` | Convierte DOCX a PDF |
+| `POST` | `/api/convert/odt/pdf` | Convierte ODT a PDF |
+| `POST` | `/api/convert/odt/docx` | Convierte ODT a DOCX |
## Contratos de datos principales
@@ -136,7 +136,7 @@ Nota: los snippets JSON de abajo son ejemplos mínimos válidos. Los payloads re
```
```bash
-curl -s http://localhost:8899/server/healthcheck
+curl -s http://localhost:8899/api/server/healthcheck
```
#### `GET /server/stats/summary`
@@ -153,7 +153,7 @@ curl -s http://localhost:8899/server/healthcheck
```
```bash
-curl -s http://localhost:8899/server/stats/summary
+curl -s http://localhost:8899/api/server/stats/summary
```
### Extracción de documentos
@@ -174,7 +174,7 @@ curl -s http://localhost:8899/server/stats/summary
```bash
curl -s -X POST \
-F "file=@/resources/data/sample/document-01.docx" \
- http://localhost:8899/misc/document-extract
+ http://localhost:8899/api/misc/document-extract
```
Errores comunes:
@@ -189,7 +189,7 @@ Errores comunes:
- Respuesta `200`: `DocumentInformation`
```bash
-curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \
+curl -s -X POST "http://localhost:8899/api/anonymizer/predict?use_cache=true" \
-H "Content-Type: application/json" \
-d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}'
```
@@ -218,7 +218,7 @@ curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \
- Respuesta `200`: `DocumentAnnotations` (incluye `data` y `label_policies` efectivas)
```bash
-curl -s -X POST http://localhost:8899/anonymizer/disambiguate \
+curl -s -X POST http://localhost:8899/api/anonymizer/disambiguate \
-H "Content-Type: application/json" \
-d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}'
```
@@ -228,7 +228,7 @@ curl -s -X POST http://localhost:8899/anonymizer/disambiguate \
- Respuesta `200`: `list[DocLabel] | null`
```bash
-curl -s -X POST http://localhost:8899/anonymizer/validation \
+curl -s -X POST http://localhost:8899/api/anonymizer/validation \
-H "Content-Type: application/json" \
-d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}'
```
@@ -240,7 +240,7 @@ curl -s -X POST http://localhost:8899/anonymizer/validation \
- Respuesta `200`: archivo `.odt` anonimizado (binario)
```bash
-curl -X POST http://localhost:8899/anonymizer/anonymize-document \
+curl -X POST http://localhost:8899/api/anonymizer/anonymize-document \
-F "file=@/resources/data/sample/document-01.docx" \
-F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}'
```
@@ -258,7 +258,7 @@ Errores comunes:
- Respuesta `200`: `DocumentInformation`
```bash
-curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \
+curl -s -X POST "http://localhost:8899/api/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \
-H "Content-Type: application/json" \
-d '{"text":"Buenos Aires, 17 de noviembre de 2024"}'
```
@@ -268,7 +268,7 @@ curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8
- Respuesta `404`: documento inexistente
```bash
-curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc
+curl -s http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc
```
#### `POST /datapublic/validation/document/{document_id}`
@@ -276,7 +276,7 @@ curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-
- Respuesta `200`: body vacío
```bash
-curl -s -X POST http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \
+curl -s -X POST http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \
-H "Content-Type: application/json" \
-d '{"materia":"penal","violencia_de_genero":"si"}'
```
@@ -287,12 +287,12 @@ Todos los endpoints de conversión usan `multipart/form-data` con campo `file`.
| Método | Path | Input | Output |
|---|---|---|---|
-| `POST` | `/convert/pdf/odt` | `.pdf` | `.odt` |
-| `POST` | `/convert/pdf/docx` | `.pdf` | `.docx` |
-| `POST` | `/convert/docx/odt` | `.docx` | `.odt` |
-| `POST` | `/convert/docx/pdf` | `.docx` | `.pdf` |
-| `POST` | `/convert/odt/pdf` | `.odt` | `.pdf` |
-| `POST` | `/convert/odt/docx` | `.odt` | `.docx` |
+| `POST` | `/api/convert/pdf/odt` | `.pdf` | `.odt` |
+| `POST` | `/api/convert/pdf/docx` | `.pdf` | `.docx` |
+| `POST` | `/api/convert/docx/odt` | `.docx` | `.odt` |
+| `POST` | `/api/convert/docx/pdf` | `.docx` | `.pdf` |
+| `POST` | `/api/convert/odt/pdf` | `.odt` | `.pdf` |
+| `POST` | `/api/convert/odt/docx` | `.odt` | `.docx` |
Para endpoints con input PDF, query param opcional:
- `backend=libreoffice|pandoc` (default: `libreoffice`)
@@ -312,9 +312,9 @@ Errores comunes:
Las siguientes rutas existen en código pero no están incluidas en `core.router` en runtime:
- `aymurai/api/endpoints/routers/datapublic/dataset.py`
- - define `/datapublic/dataset/*`, pero ese router no está montado.
+ - define `/api/datapublic/dataset/*`, pero ese router no está montado.
- `aymurai/api/endpoints/routers/anonymizer/database.py`
- - define `/anonymizer/database/*`, pero su include está comentado.
+ - define `/api/anonymizer/database/*`, pero su include está comentado.
- `aymurai/api/endpoints/routers/database/*`
- rutas administrativas de DB, sin montaje en `core.router`.
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
new file mode 100644
index 00000000..ff549f0e
--- /dev/null
+++ b/frontend/.dockerignore
@@ -0,0 +1,13 @@
+node_modules
+build
+tmp
+.nvmrc
+.gitignore
+.editorconfig
+.tmp
+.next
+.DS_Store
+*.log
+README.md
+CONTRIBUTING.md
+out
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..e31ba0eb
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,34 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+node_modules/*
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+.husky
+
+# production
+/build
+/dist
+/out
+
+# misc
+.DS_Store
+.env
+.env.development
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Panda CSS generated files
+src/renderer/src/styled
+
+/me/
\ No newline at end of file
diff --git a/frontend/.gitlab-ci.yml b/frontend/.gitlab-ci.yml
new file mode 100644
index 00000000..942f2495
--- /dev/null
+++ b/frontend/.gitlab-ci.yml
@@ -0,0 +1,18 @@
+---
+image: alpine:latest
+
+stages:
+ - analysis
+
+# This creates a temporary cache folder that prevents from
+# the node_modules, and .npm (also apply for .yarn)
+# to be recreated each CI run
+cache:
+ key: ${CI_COMMIT_REF_SLUG}
+ paths:
+ - node_modules
+ - .npm
+
+include:
+ - local: /.gitlab/analysis/lint.yml
+ - local: /.gitlab/analysis/audit.yml
diff --git a/frontend/.gitlab/analysis/audit.yml b/frontend/.gitlab/analysis/audit.yml
new file mode 100644
index 00000000..a66d3a44
--- /dev/null
+++ b/frontend/.gitlab/analysis/audit.yml
@@ -0,0 +1,13 @@
+---
+audit:
+ stage: analysis
+ image: node:14
+ cache:
+ paths:
+ - node_modules
+ - .npm
+ before_script:
+ - npm ci
+ script:
+ - npm audit
+ allow_failure: true
diff --git a/frontend/.gitlab/analysis/lint.yml b/frontend/.gitlab/analysis/lint.yml
new file mode 100644
index 00000000..bcb728c6
--- /dev/null
+++ b/frontend/.gitlab/analysis/lint.yml
@@ -0,0 +1,12 @@
+---
+lint:
+ stage: analysis
+ image: node:14
+ cache:
+ paths:
+ - node_modules
+ - .npm
+ before_script:
+ - npm ci
+ script:
+ - npm run lint
diff --git a/frontend/.npmrc b/frontend/.npmrc
new file mode 100644
index 00000000..dd854dc4
--- /dev/null
+++ b/frontend/.npmrc
@@ -0,0 +1 @@
+public-hoist-pattern[]=@types/*
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 00000000..544138be
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "singleQuote": true
+}
diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json
new file mode 100644
index 00000000..14cfdc59
--- /dev/null
+++ b/frontend/.vscode/settings.json
@@ -0,0 +1,27 @@
+{
+ "editor.formatOnSave": true,
+ "editor.codeActionsOnSave": {
+ "source.fixAll": "explicit",
+ "source.organizeImports": "explicit"
+ },
+ "editor.defaultFormatter": "biomejs.biome",
+ "[typescript]": {
+ "editor.defaultFormatter": "biomejs.biome"
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "biomejs.biome"
+ },
+ "[json]": {
+ "editor.defaultFormatter": "biomejs.biome"
+ },
+ "[javascriptreact]": {
+ "editor.defaultFormatter": "biomejs.biome"
+ },
+ "[jsonc]": {
+ "editor.defaultFormatter": "biomejs.biome"
+ },
+ "editor.rulers": [80, 120],
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "biomejs.biome"
+ }
+}
diff --git a/frontend/CONTRIBUTING.md b/frontend/CONTRIBUTING.md
new file mode 100644
index 00000000..7f08ddba
--- /dev/null
+++ b/frontend/CONTRIBUTING.md
@@ -0,0 +1,86 @@
+# Best Practices
+
+## Linter
+
+This project uses _Create React App_ ESLint configuration as a base. You won't
+be able to commit files with linting errors, and if you do a `--force`
+you'll break CI.
+
+We also use [Markdownlint](https://github.com/DavidAnson/markdownlint) for
+Markdown files.
+
+If you are using **VS Code**, you should install ESLint and Markdownlint
+extensions. Not only it will show you linting errors but also you can set it to
+fix some of them on save. To do so, edit your `settings.json` config file and
+add the following:
+
+```json
+ "eslint.autoFixOnSave": true,
+ "eslint.alwaysShowStatus": true,
+ "eslint.validate": [
+ "javascript",
+ "javascriptreact"
+ ]
+```
+
+## Branches
+
+- `main` is our main branch for nightly builds
+- Try to not develop directly on `main` branch
+- merges to `main` should be through `pull request` practice.
+
+### Branch naming
+
+- `poc/branch-name` for proof of concepts
+- `feat/branch-name` for new features like, components, services,
+validations, flows, etc.
+- `test/branch-name` for unit/integration tests
+- `fix/branch-name` for bugfixing code
+- `hotfix/branch-name` for bugfixing in prod
+- `docs/branch-name` for documentation
+
+### Pull Requests
+
+- Define a reviewer from the team.
+- Close the branch after PR merge (except when it has the `WIP` prefix).
+Otherwise this'll keep a ton unnecessary of UAT environments.
+- Focus on one task at time
+- Make atomic/little commits
+- Push before leaving the office
+- Be sure all of your tests passed
+
+## Commits
+
+We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
+specification. This will allow us to create automatized changelogs and semantic
+versioning. Because of this, _you won't be able to commit messages that do not
+follow this specification_.
+
+```text
+feat: added hat wobble
+^--^ ^---------------^
+| |
+| +-> Summary in past simple tense.
+|
++-------> Type: chore, docs, feat, fix, refactor, style, or test.
+```
+
+### Commit Types
+
+- `feat`: (new feature for the user, not a new feature for build script)
+- `fix`: (bug fix for the user, not a fix to a build script)
+- `docs`: (changes to the documentation)
+- `style`: (formatting, missing semicolons, etc; no production code change)
+- `refactor`: (refactoring production code, eg. renaming a variable)
+- `test`: (adding missing tests, refactoring tests; no production code change)
+- `chore`: (updating grunt tasks etc; no production code change)
+
+## Testing
+
+We use **[Jest](https://jestjs.io/)** for testing and. Tests are automatically
+run during the Validate stage of the CI Pipeline.
+
+The pipeline enforces a 40% minimum coverage as an incentive to write tests.
+It's strongly recommended that you raised this threshold over time.
+
+Jest can be configured via [jest.config.js](jest.config.js).
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..559f844f
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,95 @@
+# AymurAI Desktop App
+
+## Technologies
+
+### Main technologies
+
+- ⚛️ **React** + 🆎 **TypeScript** as the _main framework_ with _type checking_
+tools
+- ⚡ **Electron** as the _deployment and packaging tool_. It also serves the
+purpose of communicating the webapp with the _NodeJS_ process.
+- 🪡 **Stitches** as the _styling library_
+- 🛣️ **Tanstack React Router** as the _routing library_ to navigate across the webapp
+- 📄 **Mammoth** + **ExcelJS** as the _libraries_ to read and write `.docx` and
+datasheet files
+
+### Other technologies
+
+- **pnpm** as _package manager_
+- **biome** as code _linter and formatter_
+
+## Getting started
+
+1. Clone the repository
+
+1. Navigate to the repository folder and install dependencies with
+
+ ```bash
+ pnpm install
+ ```
+
+1. Start the app in development mode with
+
+ ```bash
+ pnpm run dev
+ ```
+
+## Run Aymurai API
+
+1. First download the image
+
+ ```sh
+ docker pull registry.gitlab.com/collective.ai/datagenero-public/aymurai-api-prod
+ ```
+
+2. And then create the corresponding container
+
+ ```sh
+ docker run -p 8899:8899 -h -d registry.gitlab.com/collective.ai/datagenero-public/aymurai-api-prod:latest
+ ```
+
+## Scripts
+
+### Development
+
+- `dev:web`: starts the frontend locally to be viewed in a conventional browser
+- `dev`: starts the frontend locally and creates an _Electron_ instance to view it
+- `start`: previews the production build with Electron
+- `start:web`: previews the production web build in a browser
+
+### Build
+
+- `build:web`: builds the React Vite project
+- `build`: builds both React and Electron projects (without packaging the application)
+
+### Validation
+
+- `lint`: runs the linter (_Biome_) across the entire project
+- `lint:fix`: fixes linter errors automatically
+- `format`: checks code formatting with _Biome_
+- `format:fix`: fixes code formatting automatically
+- `typecheck`: runs type checking on both _Node_ and _Web_ applications
+- `typecheck:node`: runs type checking on the _Electron_ application
+- `typecheck:web`: runs type checking on the _React_ application
+- `validate`: runs linting and type checking on both React and renderer
+- `pre-commit`: runs _LintStaged_
+
+### Deployment
+
+- `build:unpack`: builds and creates an unpacked distribution directory
+- `build:[win|mac|linux]`: builds and packages the application for the desired target
+- `package`: cleans build directory, builds, and packages the app with Electron Forge
+- `make`: cleans build directory, builds, and creates distributables with Electron Forge
+- `postinstall`: installs app dependencies for Electron Builder
+
+## Colaborators
+
+- Melina Gatto (_Project Manager_)
+- Luciana Vega (_Project Manager_)
+- Andy Orlandi (_Product Designer_)
+- Luciano Lapenna (_Developer_)
+- Ender Puentes (_Developer_)
+
+## Special thanks
+
+- Lucia Wainfeld
diff --git a/frontend/a.tml b/frontend/a.tml
new file mode 100644
index 00000000..ffba6f07
--- /dev/null
+++ b/frontend/a.tml
@@ -0,0 +1,18 @@
+// import path from "node:path";
+// import react from "@vitejs/plugin-react";
+// import { defineConfig } from "vite";
+
+// export default defineConfig({
+// plugins: [react()],
+// build: {
+// outDir: "out", // CRA's default build output
+// },
+// resolve: {
+// alias: [
+// {
+// find: "@",
+// replacement: path.resolve(__dirname, "src"),
+// },
+// ],
+// },
+// });
diff --git a/frontend/biome.json b/frontend/biome.json
new file mode 100644
index 00000000..676bcd39
--- /dev/null
+++ b/frontend/biome.json
@@ -0,0 +1,54 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "vcs": {
+ "enabled": false,
+ "clientKind": "git",
+ "useIgnoreFile": false
+ },
+ "files": {
+ "ignoreUnknown": false,
+ "ignore": ["node_modules", "build", ".husky", "out", "routeTree.gen.ts"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space"
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "performance": {
+ "noAccumulatingSpread": "warn",
+ "noDelete": "warn"
+ },
+ "correctness": {
+ "useExhaustiveDependencies": "warn"
+ },
+ "complexity": {
+ "noUselessSwitchCase": "off",
+ "noForEach": "off",
+ "noUselessFragments": "off",
+ "noBannedTypes": "warn"
+ },
+ "style": {
+ "noNonNullAssertion": "warn",
+ "useSelfClosingElements": {
+ "fix": "safe",
+ "level": "error"
+ }
+ },
+ "a11y": {
+ "noSvgWithoutTitle": "off",
+ "useKeyWithClickEvents": "off"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double"
+ }
+ }
+}
diff --git a/frontend/dev-app-update.yml b/frontend/dev-app-update.yml
new file mode 100644
index 00000000..199fb557
--- /dev/null
+++ b/frontend/dev-app-update.yml
@@ -0,0 +1,3 @@
+provider: generic
+url: https://example.com/auto-updates
+updaterCacheDirName: vite-electron-test-updater
diff --git a/frontend/docs/ANONIMIZADO.docx b/frontend/docs/ANONIMIZADO.docx
new file mode 100644
index 00000000..a96d78e1
Binary files /dev/null and b/frontend/docs/ANONIMIZADO.docx differ
diff --git a/frontend/docs/SIN_ANONIMIZAR.docx b/frontend/docs/SIN_ANONIMIZAR.docx
new file mode 100644
index 00000000..c205bc9b
Binary files /dev/null and b/frontend/docs/SIN_ANONIMIZAR.docx differ
diff --git a/frontend/docs/ai.md b/frontend/docs/ai.md
new file mode 100644
index 00000000..cd3f6d84
--- /dev/null
+++ b/frontend/docs/ai.md
@@ -0,0 +1,14 @@
+# AymurAI
+
+La AI está documentada en su swagger accediendo directamente a `{{AYMURAI_URL}}/api/docs` una vez que el servidor está corriendo.
+
+> 💡 Si la AI está corriendo en la red LAN o bajo un ngrok, se debe cambiar la ruta.
+
+## Uso
+
+Para usar la AI en la aplicación se ponen a disposición cuatro funciones en la carpeta `/src/services/aymurai`:
+
+- `document-extract.ts`: obtiene cada párrafo del documento en texto plano para poder renderizarlo en la aplicación.
+- `predict.ts`: genera predicciones sobre los párrafos previamente extraídos.
+- `anonymize.ts`: permite generar un archivo `.odt` con las anotaciones hechas por el usuario y la AI.
+- `health-check.ts`: se utiliza para verificar si la api está funcionando de manera correcta. Se utiliza en la pantalla de inicio.
diff --git a/frontend/docs/authentication.md b/frontend/docs/authentication.md
new file mode 100644
index 00000000..b1932b0c
--- /dev/null
+++ b/frontend/docs/authentication.md
@@ -0,0 +1,74 @@
+# Authentication
+
+La autenticación en la app _AymurAI_ se utiliza únicamente para el guardado de la información de la spreadsheet en el set de datos. Hay dos opciones:
+
+- [_Google OAuth2_](#google-oauth2)
+ (cargando la información en _Google Drive_)
+- [_offline local_](#local)
+ (escribiendo en el _filesystem_).
+
+Actualmente la autenticación con Google se puede realizar en el paso de guardar documento. Anteriormente, esta opción se seleccionaba en la pantalla de inicio, pero fue removida ya que generaba confusión a los usuarios por dar a entender que la aplicación operaba online cuando en realidad la única instancia en la cual lo puede hacer es a la hora de guardar la spreadsheet de Drive.
+
+Para facilitar el proceso se creó el hook [`useLogin`](../src/hooks/useLogin.ts).
+
+```ts
+const { logout, login } = useLogin({
+ onLogout: () => ...,
+});
+```
+
+## Google OAuth2
+
+Utiliza el _social login_ de _Google_ para poder interactuar con la
+[_Spreadsheet Google API_](https://developers.google.com/sheets/api/reference/rest).
+Esto permite leer/escribir en una hoja de cálculo de _Google Drive_.
+
+El funcionamiento es el siguiente:
+
+1. Una vez se presiona el botón Google la app de React se comunica con el
+ proceso de Node para abrir una nueva ventana que cargaría la _consent screen_.
+ El proceso sería:
+
+ 1. Se genera un _challenge code_.
+ 2. Junto al código mencionado se añaden otros datos como el
+ `client_id`, `redirect_uri`, etc.
+ 3. Se genera una URL con los datos anteriores para acceder a
+ `'https://accounts.google.com/o/oauth2/...`.
+ 4. Se abre una nueva ventana con la URL previamente generada.
+
+2. Una vez abierta la _consent screen_ al comunicarse con el proceso de
+ Electron, se añade un _event listener_ para poder tener los datos del usuario y
+ guardarlos en la sesión.
+
+3. Esta comunicación genera un código de autorización que se utiliza para
+ obtener el _access token_ y el _refresh token_. Para actualizar el _access token_
+ se utiliza el _refresh token_ y un timer cada 45min.
+
+> 💡 Para pasar la información devuelta a la app de _React_ se utiliza un
+> [deep link](https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app).
+> Suele suceder que en modo _development_ no quede definido el _protocol_ para el
+> _deep link_, por lo que se debe usar la versión productiva.
+
+
+
+> 💡 [Más información](./excel/spreadsheet-api.md) en cómo se utiliza la
+> _Google Spreadsheet API_ para generar los archivos.
+
+## Local
+
+La app simplemente funciona de manera _local_ y _offline_, leyendo/escribiendo
+sobre un archivo de datasheet en el filesystem. No requiere ninguna acción extra
+por parte del usuario.
+
+Esta opción simplemente inicializa el state de la app con la siguiente
+información:
+
+```ts
+const loginOffline = (funcType: FunctionType) => {
+ setUser({ online: false, function: funcType, token: "" });
+ // ...
+};
+```
+
+> 💡 [Más información](./excel/filesystem.md) en cómo y dónde se genera el
+> archivo `.xlsx`.
diff --git a/frontend/docs/aymurai.md b/frontend/docs/aymurai.md
new file mode 100644
index 00000000..183ed62c
--- /dev/null
+++ b/frontend/docs/aymurai.md
@@ -0,0 +1,70 @@
+# AymurAI
+
+## Tecnologías principales
+
+AymurAI es una aplicación de escritorio basada en
+[NodeJS](https://nodejs.org/en/) que utiliza tres principales tecnologías:
+
+- **[Electron.JS](https://www.electronjs.org/)**: elegido por su capacidad de
+ poder crear aplicaciones multiplataforma, Electron nos permite crear
+ aplicaciones web que pueden ser distribuidas sin la necesidad de tener un
+ browser preinstalado
+- **[React](https://reactjs.org/)**: elegido por su versatilidad, ReactJS es un
+ web framework que nos permite crear todo tipo de web apps
+- **[TypeScript](https://www.typescriptlang.org/)**: un _superset_ (es
+ decir, que añade features al original, pero sin modificar sus principios) de
+ _JavaScript_ que permite crear aplicaciones con la seguridad de que el flujo de
+ datos respeta un orden particular y definido
+
+Esta combinación de frameworks y lenguajes resulta muy eficiente ya que se puede
+crear una web app desde cero, distribuible facilmente y multiplataforma, y con
+herramientas incluidas para poder realizar un desarrollo confiable y
+con la menor cantidad de _bugs_ posibles.
+
+Además, el uso de _Electron_ nos permite combinar las ventajas de usar _NodeJS_
+y todos sus paquetes como proceso principal y luego usar un browser levantado
+por el mismo _Electron_ basado en
+[Chromium](https://www.chromium.org/chromium-projects/) para utilizar las
+[Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) que podemos
+encontrar en cualquier browser.
+
+## Otras tecnologías
+
+Además de las tecnologías principales anteriormente mencionadas se usaron:
+
+- **[Stitches](https://stitches.dev/)**: elegida como librería de estilos, nos
+ permite configurar toda una lista de _tokens_ que pueden ser utilizados a lo
+ largo de toda la aplicación manteniendo una alta consistencia entre los estilos.
+ Esto nos permite configurar y estandarizar colores, espaciados, sombras, etc además
+ de darnos la posibilidad de trabajar con _styled components_ para un
+ ágil desarrollo
+- **[Google OAuth2](https://developers.google.com/identity/protocols/oauth2)**:
+ sistema de autenticación de usuarios basado en [OAuth](https://oauth.net/) que
+ nos permite tener un acceso total a las APIs de _Google_, permitiéndonos editar
+ en tiempo real documentos _Spreadsheet_. Sin este sistema de autenticación no se
+ podría obtener el token que nos permite interaccionar con las APIs
+
+## Estructura
+
+Se dividió la aplicación en 6 páginas, que se presentan en el siguiente orden:
+
+1. **Login**: página en la que se selecciona cómo conectarse a la API de AyMurai y cuál funcionalidad se desea utilizar (anonimizador o set de datos)
+1. **Onboarding**: se nos muestra una guía sobre el funcionamiento de la
+ aplicación y los pasos a seguir para poder realizar la validación de nuestros
+ archivos. Es en este paso donde podremos cargar nuestros documentos
+1. **Preview**: obtendremos una vista previa de los escritos cargados en la
+ página anterior con la oportunidad de cargar nuevos documentos si es necesario.
+ Es aquí donde podemos decidir cuáles son los archivos finales que serán
+ procesados por la inteligencia artificial
+1. **Process**: página de procesamiento. En este paso se realiza la consulta
+ a la inteligencia artificial para obtener las predicciones correspondientes
+ a cada archivo. Esto puede tardar un poco. Podemos detener el procesamiento
+ de los documentos o reemplazarlos en cualquier momento si es necesario
+1. **Validation**: es en esta instancia donde validaremos lo que nos ha devuelto
+ la inteligencia artificial. Tendremos un form donde podremos ir seleccionando
+ las sugerencias hechas por la IA o completar con lo que corresponda en ese momento.
+ Cada vez que se valida un documento, se crea una conexión con la
+ _Spreadsheet API_ de _Google_ para poder enviar los datos finales
+1. **Finish**: podremos ver una vista preview de los documentos que validamos y
+ enviamos, con la posibilidad de revisar el set de datos o de realizar una nueva
+ carga, reiniciando el flujo de trabajo
diff --git a/frontend/docs/excel/filesystem.md b/frontend/docs/excel/filesystem.md
new file mode 100644
index 00000000..28ae5b55
--- /dev/null
+++ b/frontend/docs/excel/filesystem.md
@@ -0,0 +1,69 @@
+# Filesystem
+
+Las funcionalidades de _NodeJS_ sobre el _filesystem_ se aprovechan para poder
+leer y escribir sobre el set de datos de forma local y offline en la
+computadora. Además, también se generan reportes que ayudan a reentrenar el
+modelo de inteligencia artificial.
+
+## Ubicación
+
+Las ubicaciones para los archivos de exportación se encuentran en la carpeta:
+
+```ts
+// Ejemplo windows: C:\Users\[Usuario]\Documents\AymurAI
+const EXPORTS_FOLDER = path.resolve(homedir(), 'Documents/AymurAI');
+```
+
+## API
+
+Para poder formar un canal de comunicación entre la aplicación de _React_
+y _Electron_, se utilizan los [_preload scripts_](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload)
+para exponer una _API_ que permita la lectura y escritura de archivos.
+
+De esta forma el _frontend_ podría acceder a las funcionalidades de _NodeJS_
+utilizando `window.filesystem.excel.read` y `window.filesystem.excel.write` por ejemplo.
+Estas funciones fueron definidas dentro de los archivos de _Electron_.
+
+Para el caso de los reportes se utiliza `window.filesystem.feedback.export`.
+
+> 📚 Se pueden encontrar estos scripts y más yendo al
+[script preloader del proyecto.](../../renderer/preload.ts)
+
+## Tipo de archivo
+
+### Set de datos
+
+El set de datos se exporta en formato _Excel_ con extensión `.xlsx`.
+
+```ts
+const FILENAME = 'set_de_datos.xlsx';
+// Ejemplo windows: C:/Users/[Usuario]/Documents/AymurAI/set_de_datos.xlsx
+const PATH = `${EXPORTS_FOLDER}/${FILENAME}`;
+```
+
+Desde el frontend se utiliza la librería [_ExcelJS_](https://www.npmjs.com/package/exceljs)
+que permite leer un archivo `.xlsx`, modificarlo en memoria y luego escribir los
+cambios en el filesystem, apoyándose en las funcionalidades de _NodeJS_.
+
+```ts
+import { Workbook } from 'exceljs';
+
+// Ejemplo de escritura de archivo
+async function writeXLSX(workbook: Workbook) {
+ const buffer = (await workbook.xlsx.writeBuffer()) as Buffer;
+
+ return window.filesystem.excel.write(buffer);
+}
+```
+
+### Reportes / Feedback
+
+El reporte se exporta en formato _JSON_ con extensión `.json`. Utiliza el nombre
+del archivo procesado y la fecha para generar el nombre del archivo.
+
+```ts
+const FEEDBACK_FOLDER = `${EXPORTS_FOLDER}/feedback`;
+const FILENAME = `${FEEDBACK_FOLDER}/{name}-{date}.json`
+```
+
+Se escribe en el filesystem de forma tradicional.
diff --git a/frontend/docs/excel/spreadsheet-api.md b/frontend/docs/excel/spreadsheet-api.md
new file mode 100644
index 00000000..a2087b40
--- /dev/null
+++ b/frontend/docs/excel/spreadsheet-api.md
@@ -0,0 +1,30 @@
+# Google Spreadsheet API
+
+Para leer y escribir de forma remota en una hoja de cálculo de _Google Sheets_ se
+utiliza la [REST API](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets)
+provista por _Google_.
+
+## Ubicación
+
+La ubicación del dataset está definida de forma __estática__ dentro del
+[código](../../src/utils/config.ts). Esto significa que para poder acceder a
+este archivo se necesita tener accesos de lectura/escritura sobre el archivo.
+
+## Implementación
+
+Dentro del proyecto se crearon una serie de funciones que ayudan a interactuar
+con la API de _Google Sheets_. En general lo que hacen es _envolver_ las
+llamadas a la _REST API_ utilizando el _access token_ provisto por el
+[proceso de autenticación](../authentication.md).
+
+> Estas funciones se pueden encontrar dentro de la carpeta de
+[servicios de google](../../src/services/google/).
+
+Por ejemplo, para obtener el contenido de una hoja de cálculo se utiliza la
+función:
+
+```ts
+import google from 'services/google';
+
+await google(authToken).spreadsheet(id).range('A1:B2');
+```
diff --git a/frontend/docs/images/auth_flows.png b/frontend/docs/images/auth_flows.png
new file mode 100644
index 00000000..8a98cee5
Binary files /dev/null and b/frontend/docs/images/auth_flows.png differ
diff --git a/frontend/docs/images/electron_pkce.png b/frontend/docs/images/electron_pkce.png
new file mode 100644
index 00000000..37c50972
Binary files /dev/null and b/frontend/docs/images/electron_pkce.png differ
diff --git a/frontend/docs/images/initial-screen.png b/frontend/docs/images/initial-screen.png
new file mode 100644
index 00000000..c339bdc9
Binary files /dev/null and b/frontend/docs/images/initial-screen.png differ
diff --git a/frontend/docs/images/submit-form.png b/frontend/docs/images/submit-form.png
new file mode 100644
index 00000000..f2505dc5
Binary files /dev/null and b/frontend/docs/images/submit-form.png differ
diff --git a/frontend/docs/images/url-selector.png b/frontend/docs/images/url-selector.png
new file mode 100644
index 00000000..611d7b1c
Binary files /dev/null and b/frontend/docs/images/url-selector.png differ
diff --git a/frontend/docs/images/validation-form.png b/frontend/docs/images/validation-form.png
new file mode 100644
index 00000000..0680dd98
Binary files /dev/null and b/frontend/docs/images/validation-form.png differ
diff --git a/frontend/docs/routing.md b/frontend/docs/routing.md
new file mode 100644
index 00000000..d84c7e30
--- /dev/null
+++ b/frontend/docs/routing.md
@@ -0,0 +1,30 @@
+# Routing
+
+El proyecto usa `react-router-dom` para manejar las rutas de la aplicación. Las rutas se definen en el archivo `src/index.tsx` y se deben agregar en el objeto `router` de la siguiente manera:
+
+```jsx
+const router = createRouter([
+ {
+ // Main as a layout element
+ path: '/',
+ element: ,
+ children: [
+ { path: 'onboarding', element: },
+ { path: 'preview', element: },
+ { path: 'process', element: },
+ ],
+ },
+ {
+ path: '/login',
+ element: ,
+ },
+]);
+```
+
+Al ser un proyecto basado en _Electron_, el router que utilizamos no es el mismo que se usa en un proyecto web normal. Para este caso, usamos un [_memory router_](https://reactrouter.com/en/main/routers/create-memory-router#creatememoryrouter) que mantiene en memoria el history stack de las rutas.
+
+## Protección de rutas
+
+Existen dos high order components que se encargan de proteger las rutas de la aplicación dependiendo qué se requiere:
+
+- `withAuthProtection`: verifica si el usuario está autenticado y redirige a `/login` si no lo está.
diff --git a/frontend/docs/url_selector.md b/frontend/docs/url_selector.md
new file mode 100644
index 00000000..2eecec44
--- /dev/null
+++ b/frontend/docs/url_selector.md
@@ -0,0 +1,17 @@
+# URL Selector
+
+En la pantalla de inicio el usuario puede elegir cómo desea conectarse a la API de AyMurAI. Hay dos opciones:
+
+- De manera local: esto significa, levantando Docker localmente ya sea de forma manual o utilizando el ejecutable desarrollado por el equipo de Collective AI para levantar la API utilizando **[Miniconda](https://docs.anaconda.com/miniconda/)**.
+
+La aplicación incluye el archivo run_server.bat que, en conjunto con el instalador desarrollado por Collective, procura levantar el server local de modo automático. El funcionamiento de esta feature está condicionada a que el usuario efectivamente tenga el directorio de Miniconda en su PC y esté utilizando Windows. En caso de que dicha ejecución falle, se muestra una alerta al usuario que le indica que deberá hacerlo de forma manual.
+
+- A través de un servidor: los distintos juzgados pueden tener servidores propios a las que se accede a través de su URL.
+
+ Por razones de seguridad, no es conveniente que estas URLs estén en el código de la aplicación, por lo cual el usuario deberá ingrearlas manualmente.
+
+ A fin de mejorar la experiencia de uso, la URL se preserva y autocompleta evitando así que el usuario tenga que reingresarla cada vez que inicia la aplicación.
+
+| Pantalla inicial | Selector |
+| ------------------------------------------------ | ------------------------------------------ |
+|  |  |
diff --git a/frontend/docs/validator/README.md b/frontend/docs/validator/README.md
new file mode 100644
index 00000000..d4d13239
--- /dev/null
+++ b/frontend/docs/validator/README.md
@@ -0,0 +1,55 @@
+# Validación
+
+El proceso de guardar la información analizada por la AI tiene que ser validado
+por un usuario para asegurar que la información es correcta. La validación se
+realiza medianete un formulario en el cual se carga la información que devuelve
+la API y el usuario puede modificarla si es necesario.
+
+
+
+Se desarrollaron varias herramientas para hacer más fácil la generación de las
+distintas partes de la pantalla de validación. Entre ellas está el hook
+`useForm` y el componente de formulario ``.
+
+## Hook `useForm`
+
+Este hook ofrece dos funciones útiles para generar formularios:
+
+- `submit`: una función que se encarga de recolectar toda la información de los
+campos del formulario para poder utilizarla a conveniencia del desarrollador.
+
+ ```tsx
+ // Ejemplo de uso
+ const { submit } = useForm();
+ const handler = submit(data => ...)
+
+ return
+ ```
+
+- `register`: función que permite agregar campos al "state" (más bien una
+lista de referencias a los ``) para luego ser recolectados por el
+`submit`. Acepta un `string` que representa el "id" del campo.
+
+ ```tsx
+ // Ejemplo de uso
+ const { register } = useForm();
+
+ return
+ ```
+
+> 💡 Más información sobre cómo están compuestas y se utilizan estas funciones en
+el archivo del [hook useForm](../../src/hooks/useForm/index.ts)
+
+## Componente ``
+
+La pantalla de validación se constituye por varios `