From 7a34807fa0abcf6b7f26d59e137f764659f6bf22 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 22 May 2026 08:53:28 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20initial=20commit=20=E2=80=94=20existin?= =?UTF-8?q?g=20single-user=20document=20scanner=20codebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 6 + .planning/codebase/ARCHITECTURE.md | 114 + .planning/codebase/CONCERNS.md | 87 + .planning/codebase/CONVENTIONS.md | 94 + .planning/codebase/INTEGRATIONS.md | 144 + .planning/codebase/STACK.md | 129 + .planning/codebase/STRUCTURE.md | 144 + .planning/codebase/TESTING.md | 87 + backend/Dockerfile | 17 + backend/ai/__init__.py | 36 + backend/ai/anthropic_provider.py | 103 + backend/ai/base.py | 32 + backend/ai/lmstudio_provider.py | 10 + backend/ai/ollama_provider.py | 10 + backend/ai/openai_provider.py | 104 + backend/api/__init__.py | 0 backend/api/documents.py | 101 + backend/api/settings.py | 84 + backend/api/topics.py | 72 + backend/config.py | 51 + .../69eb8545-2e19-4651-903e-6489dbd9f687.json | 14 + .../b77bf1c5-81cb-4e0d-9266-7902b9e7863e.json | 14 + .../cf4dd4cf-dcfb-42f1-957d-bcdba640163b.json | 13 + .../e71d8a85-09a1-4cd8-b602-65aa9216a724.json | 11 + backend/data/settings.json | 23 + backend/data/topics.json | 22 + .../69eb8545-2e19-4651-903e-6489dbd9f687.pdf | Bin 0 -> 38090 bytes .../b77bf1c5-81cb-4e0d-9266-7902b9e7863e.pdf | 12676 ++++++++++++++++ .../cf4dd4cf-dcfb-42f1-957d-bcdba640163b.txt | 1 + .../e71d8a85-09a1-4cd8-b602-65aa9216a724.txt | 1 + backend/main.py | 33 + backend/pytest.ini | 3 + backend/requirements.txt | 15 + backend/services/__init__.py | 0 backend/services/classifier.py | 59 + backend/services/extractor.py | 71 + backend/services/storage.py | 187 + backend/tests/__init__.py | 0 backend/tests/conftest.py | 70 + backend/tests/test_classifier.py | 110 + backend/tests/test_documents.py | 107 + backend/tests/test_extractor.py | 52 + backend/tests/test_health.py | 4 + backend/tests/test_lmstudio.py | 46 + backend/tests/test_settings.py | 60 + backend/tests/test_topics.py | 72 + docker-compose.yml | 25 + frontend/Dockerfile | 10 + frontend/index.html | 12 + frontend/package.json | 22 + frontend/postcss.config.js | 6 + frontend/src/App.vue | 17 + frontend/src/api/client.js | 105 + .../src/components/documents/DocumentCard.vue | 59 + frontend/src/components/layout/AppSidebar.vue | 87 + frontend/src/components/topics/TopicBadge.vue | 15 + .../src/components/topics/TopicManager.vue | 124 + frontend/src/components/upload/DropZone.vue | 62 + .../src/components/upload/UploadProgress.vue | 36 + frontend/src/main.js | 10 + frontend/src/router/index.js | 18 + frontend/src/stores/documents.js | 46 + frontend/src/stores/settings.js | 38 + frontend/src/stores/topics.js | 42 + frontend/src/style.css | 9 + frontend/src/views/DocumentView.vue | 184 + frontend/src/views/HomeView.vue | 63 + frontend/src/views/SettingsView.vue | 223 + frontend/src/views/TopicsView.vue | 82 + frontend/tailwind.config.js | 8 + frontend/vite.config.js | 16 + 71 files changed, 16408 insertions(+) create mode 100644 .env.example create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md create mode 100644 backend/Dockerfile create mode 100644 backend/ai/__init__.py create mode 100644 backend/ai/anthropic_provider.py create mode 100644 backend/ai/base.py create mode 100644 backend/ai/lmstudio_provider.py create mode 100644 backend/ai/ollama_provider.py create mode 100644 backend/ai/openai_provider.py create mode 100644 backend/api/__init__.py create mode 100644 backend/api/documents.py create mode 100644 backend/api/settings.py create mode 100644 backend/api/topics.py create mode 100644 backend/config.py create mode 100644 backend/data/metadata/69eb8545-2e19-4651-903e-6489dbd9f687.json create mode 100644 backend/data/metadata/b77bf1c5-81cb-4e0d-9266-7902b9e7863e.json create mode 100644 backend/data/metadata/cf4dd4cf-dcfb-42f1-957d-bcdba640163b.json create mode 100644 backend/data/metadata/e71d8a85-09a1-4cd8-b602-65aa9216a724.json create mode 100644 backend/data/settings.json create mode 100644 backend/data/topics.json create mode 100644 backend/data/uploads/69eb8545-2e19-4651-903e-6489dbd9f687.pdf create mode 100644 backend/data/uploads/b77bf1c5-81cb-4e0d-9266-7902b9e7863e.pdf create mode 100644 backend/data/uploads/cf4dd4cf-dcfb-42f1-957d-bcdba640163b.txt create mode 100644 backend/data/uploads/e71d8a85-09a1-4cd8-b602-65aa9216a724.txt create mode 100644 backend/main.py create mode 100644 backend/pytest.ini create mode 100644 backend/requirements.txt create mode 100644 backend/services/__init__.py create mode 100644 backend/services/classifier.py create mode 100644 backend/services/extractor.py create mode 100644 backend/services/storage.py create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_classifier.py create mode 100644 backend/tests/test_documents.py create mode 100644 backend/tests/test_extractor.py create mode 100644 backend/tests/test_health.py create mode 100644 backend/tests/test_lmstudio.py create mode 100644 backend/tests/test_settings.py create mode 100644 backend/tests/test_topics.py create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/components/documents/DocumentCard.vue create mode 100644 frontend/src/components/layout/AppSidebar.vue create mode 100644 frontend/src/components/topics/TopicBadge.vue create mode 100644 frontend/src/components/topics/TopicManager.vue create mode 100644 frontend/src/components/upload/DropZone.vue create mode 100644 frontend/src/components/upload/UploadProgress.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/stores/documents.js create mode 100644 frontend/src/stores/settings.js create mode 100644 frontend/src/stores/topics.js create mode 100644 frontend/src/style.css create mode 100644 frontend/src/views/DocumentView.vue create mode 100644 frontend/src/views/HomeView.vue create mode 100644 frontend/src/views/SettingsView.vue create mode 100644 frontend/src/views/TopicsView.vue create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e48b9f5 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Copy to .env and fill in as needed. +# Settings are primarily managed through the in-app Settings UI. +# These are NOT required — the app defaults to LM Studio with no API keys. + +ANTHROPIC_API_KEY= +OPENAI_API_KEY= diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..8316765 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,114 @@ +# ARCHITECTURE — document-scanner + +_Last updated: 2026-05-21_ + +## Summary + +Document Scanner is a two-tier web application: a Vue 3 SPA communicates with a FastAPI backend via a Vite dev-proxy (or directly in production). The backend handles document ingestion, text extraction, AI-based classification, and flat-file persistence. AI provider selection is fully runtime-configurable via a provider pattern abstraction. + +--- + +## System Overview + +``` +Browser (Vue 3 SPA) + │ HTTP/JSON + multipart + ▼ +FastAPI (port 8000) + ├── api/documents.py – upload, list, get, delete, reclassify + ├── api/topics.py – CRUD for topic list + ├── api/settings.py – AI provider config + system prompt + │ + ├── services/ + │ ├── extractor.py – text extraction dispatch + │ ├── classifier.py – orchestrates AI call + topic creation + │ └── storage.py – flat-file JSON + filesystem persistence + │ + └── ai/ – provider abstraction layer + ├── base.py – AIProvider ABC + ClassificationResult + ├── __init__.py – get_provider() factory + ├── anthropic_provider.py + ├── openai_provider.py + ├── ollama_provider.py (subclasses OpenAIProvider) + └── lmstudio_provider.py (subclasses OpenAIProvider) + │ + ▼ + External AI service (Anthropic API / OpenAI API / + Ollama / LM Studio — host.docker.internal) +``` + +--- + +## Request Flow — Document Upload + Classification + +1. Frontend POSTs `multipart/form-data` to `POST /api/documents/upload` +2. `documents.py` saves the file to `data/uploads/`, calls `extractor.extract_text()` +3. Extracted text (truncated to 50,000 chars) is stored in `data/metadata/.json` +4. If `auto_classify=true`, `classifier.classify_document()` is called: + a. Loads current settings from `data/settings.json` → calls `get_provider(settings)` + b. Passes document text + existing topics to `provider.classify()` + c. Any suggested new topics are created via `storage.add_topic()` + d. Document metadata is updated with assigned topics +5. Full document metadata JSON is returned to the frontend + +--- + +## AI Provider Abstraction + +- `AIProvider` (ABC in `ai/base.py`) defines three async methods: + - `classify(document_text, existing_topics, system_prompt) → ClassificationResult` + - `suggest_topics(document_text, system_prompt) → list[str]` + - `health_check() → bool` +- `get_provider(settings: dict)` factory in `ai/__init__.py` reads `settings["active_provider"]` and instantiates the correct class +- `OllamaProvider` and `LMStudioProvider` extend `OpenAIProvider` (both expose OpenAI-compatible endpoints) +- Provider is re-instantiated on every request (stateless; no connection pooling) + +--- + +## Data Persistence + +All state is stored on the local filesystem — no database: + +| Store | Path | Format | Access | +|---|---|---|---| +| Uploaded files | `data/uploads/.` | Original binary | Direct filesystem | +| Document metadata | `data/metadata/.json` | JSON per document | `filelock` protected | +| Topic list | `data/topics.json` | `{"topics": [...]}` | `filelock` protected | +| Settings | `data/settings.json` | JSON object | `filelock` protected | + +`filelock` is used to prevent concurrent write corruption on JSON files. + +--- + +## Frontend Architecture + +- Vue 3 SPA (Options API), Pinia stores, Vue Router 4 +- Three Pinia stores (`documents`, `topics`, `settings`) act as the sole data access layer — components never call the API directly +- `src/api/client.js` is the single HTTP adapter (wraps `fetch`) +- Vite proxies `/api/*` to `http://localhost:8000` in dev mode + +--- + +## Key Patterns + +- **Provider Pattern** — AI backends are interchangeable at runtime via settings +- **Service Layer** — `extractor`, `classifier`, `storage` are pure Python modules; no FastAPI coupling +- **Pinia-as-Facade** — stores encapsulate all async API calls; views stay declarative + +--- + +## Constraints & Notable Decisions + +- All CORS origins allowed (`allow_origins=["*"]`) — suitable for local dev, not production +- No authentication or user model +- Single-worker assumption for file locking (does not scale to multiple uvicorn workers) +- AI provider re-instantiated per request (no connection reuse) +- Data directory is volume-mounted in Docker; no backup or migration strategy + +--- + +## Gaps / Unknowns + +- No API versioning strategy visible +- Frontend has no error boundary or global error handling component +- No pagination on document list endpoint (could be a scaling concern) diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..0a12632 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,87 @@ +# CONCERNS — document-scanner + +_Last updated: 2026-05-21_ + +## Summary + +The codebase is a well-structured local-first prototype. The main concerns are security issues that matter if exposed beyond localhost (open CORS, no file validation, plain-text key storage), several blocking I/O calls in async handlers, and a handful of code duplication issues in the AI provider layer. Overall health is good for a local dev tool; requires hardening before any networked deployment. + +--- + +## Concerns by Severity + +### HIGH + +**1. File type validation is defined but never enforced** +`ALLOWED_MIME_TYPES` is defined in `backend/api/documents.py` but the upload handler never checks it — any file type is accepted. An attacker could upload executable files or crafted archives. + +**2. No file size limit on uploads** +The entire uploaded file is read before any cap is applied. A large file could exhaust memory or disk. No `MAX_UPLOAD_SIZE` check exists at the HTTP boundary. + +**3. API keys stored in plain-text JSON** +`backend/data/settings.json` stores API keys in plaintext. The volume mount in `docker-compose.yml` (`./backend/data:/app/data`) means any process with Docker access can read them. Masking only applies to API responses, not to disk. + +**4. CORS fully open** +`allow_origins=["*"]` in `main.py` means any website can make cross-origin requests to the API, including with credentials if ever added. + +**5. Docker Compose mounts entire backend source as writable volume** +`./backend:/app` gives the container write access to the host source tree. A path traversal or code execution bug in the app could overwrite source files. + +--- + +### MEDIUM + +**6. Blocking I/O in async FastAPI handlers** +`storage.py` uses synchronous file reads/writes and `filelock` blocking calls inside `async def` endpoints. This blocks the uvicorn event loop during every request. Should use `asyncio.to_thread()` or `aiofiles` (which is already in requirements but unused). + +**7. Topic rename does not cascade to documents** +Deleting a topic removes it from document metadata, but renaming is not implemented — there is no rename endpoint. Users have no way to rename a topic without losing document associations. + +**8. `list_metadata` loads all documents before filtering** +`storage.list_metadata()` reads all metadata JSON files on every list request. No pagination at the storage layer — O(N) disk reads per page request as the document count grows. + +**9. `topic_doc_counts()` scans all metadata on every topic request** +Every `GET /api/topics` call triggers a full scan of all metadata files to count documents per topic. Not cached; will degrade linearly. + +**10. `MAX_AI_CHARS` duplicated across 3 files** +The character truncation limit for AI input is duplicated as a magic constant in multiple provider files. The provider-level truncation is effectively dead code since `extractor.py` already truncates to `MAX_STORED_CHARS` (50,000). + +**11. `_parse_classification` / `_parse_suggestions` duplicated between providers** +`anthropic_provider.py` and `openai_provider.py` each define their own JSON parsing helpers for AI responses. `test_classifier.py` only imports from `openai_provider`, meaning the Anthropic variants are untested. + +**12. `health_check()` makes real billed API calls** +The "Test Connection" UI action calls `provider.health_check()`, which makes a real API call to Anthropic/OpenAI — incurring cost and latency every time the user tests connectivity. Should use a cheaper probe (e.g., list models endpoint or a cached status). + +--- + +### LOW + +**13. `uvicorn --reload` hardcoded in docker-compose.yml** +Hot-reload is hardcoded in the production compose file. There is no separate `docker-compose.prod.yml` or build-arg to disable it. + +**14. Unused `shutil` import in `storage.py`** +`import shutil` appears in `storage.py` but is never used. + +**15. Topic IDs are 8-character UUID prefixes** +`str(uuid.uuid4())[:8]` generates IDs with ~4 billion combinations — low collision risk for personal use but not safe at scale or for security-sensitive identifiers. + +**16. `classify_document` request body uses raw `dict`, not a Pydantic model** +The reclassify endpoint accepts an unvalidated `dict` body. Invalid input causes an unformatted 500 rather than a clean 422 validation error. + +**17. No global frontend error handling** +There is no Vue error boundary or global `window.onerror` / `app.config.errorHandler`. Failed API calls in stores may surface as silent failures or unhandled promise rejections. + +**18. No document download endpoint** +Uploaded files are stored in `data/uploads/` but there is no `GET /api/documents/:id/file` endpoint to retrieve the original binary. Files are effectively write-only through the UI. + +**19. `aiofiles` in requirements but never used** +`aiofiles>=23.2` is listed in `requirements.txt` but no code imports it. The blocking I/O concern (item 6) should use it. + +--- + +## Gaps / Unknowns + +- Production deployment path is undefined (no nginx, no TLS, no auth) +- OCR language support for pytesseract is not configured (defaults to English only) +- `suggest_topics` method on all providers is untested — unclear if it is used in the current UI flow +- No backup or recovery strategy for `data/` volume diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..eb69adc --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,94 @@ +# CONVENTIONS — document-scanner + +_Last updated: 2026-05-21_ + +## Summary + +The codebase follows standard Python and Vue 3 conventions without heavy tooling enforcement. Backend uses async/await throughout with type hints on public interfaces. Frontend uses Vue Options API with Pinia stores as the data layer. No linter or formatter configuration is committed. + +--- + +## Python Conventions (Backend) + +### Naming +- Files: `snake_case.py` +- Classes: `PascalCase` (e.g., `AnthropicProvider`, `ClassificationResult`) +- Functions/variables: `snake_case` +- Constants: `UPPER_SNAKE_CASE` (e.g., `MAX_STORED_CHARS`, `DATA_DIR`) +- Private helpers: leading underscore (e.g., `_extract_pdf`, `_parse_classification`) + +### Async +- All API endpoint functions are `async def` +- All `AIProvider` methods are `async def` +- `pytest-asyncio` with `asyncio_mode=auto` (set in `pytest.ini`) + +### Type Hints +- Used on public function signatures in `ai/` layer and `services/` +- Dataclass used for `ClassificationResult` (`@dataclass` with `field(default_factory=...)`) +- Not used consistently in `api/` routers (rely on FastAPI/Pydantic implicit validation) + +### Error Handling +- `extractor.py` wraps all extraction in `try/except Exception` and returns error strings (never raises) +- AI providers raise on hard failures; caller (`classifier.py`) is responsible for propagating +- No global exception handler registered in `main.py` + +### Imports +- Standard library first, then third-party, then local — not enforced by isort +- Heavy library imports (`fitz`, `pytesseract`, `docx`) are deferred inside functions to avoid import-time cost when unused + +### Module Docstrings +- Present on `extractor.py` and `test_classifier.py`; absent elsewhere + +--- + +## JavaScript / Vue Conventions (Frontend) + +### Naming +- Vue files: `PascalCase.vue` (e.g., `DocumentCard.vue`, `AppSidebar.vue`) +- Pinia stores: `camelCase` filename matching store ID (e.g., `documents.js` → `useDocumentsStore`) +- Views: `View.vue` suffix +- Components grouped by domain in subdirectories: `documents/`, `topics/`, `upload/`, `layout/` + +### Vue Style +- Options API used throughout (not Composition API) +- Props defined with type and default; no `defineProps` (Options API syntax) +- `v-model`, `v-for`, `v-if` used directly in templates + +### Pinia Pattern +- Each store encapsulates `state`, `getters`, and `actions` +- Actions call `src/api/client.js` — components never import `client.js` directly +- Stores are the single source of truth; views read from store state + +### API Client +- `src/api/client.js` is the sole HTTP adapter +- All paths are prefixed `/api/` (proxied to backend in dev via Vite config) + +### Styling +- Tailwind CSS utility classes used directly in templates +- No scoped ` diff --git a/frontend/src/components/topics/TopicBadge.vue b/frontend/src/components/topics/TopicBadge.vue new file mode 100644 index 0000000..c8cc767 --- /dev/null +++ b/frontend/src/components/topics/TopicBadge.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/topics/TopicManager.vue b/frontend/src/components/topics/TopicManager.vue new file mode 100644 index 0000000..4a0cd56 --- /dev/null +++ b/frontend/src/components/topics/TopicManager.vue @@ -0,0 +1,124 @@ + + + diff --git a/frontend/src/components/upload/DropZone.vue b/frontend/src/components/upload/DropZone.vue new file mode 100644 index 0000000..68c298e --- /dev/null +++ b/frontend/src/components/upload/DropZone.vue @@ -0,0 +1,62 @@ + + + diff --git a/frontend/src/components/upload/UploadProgress.vue b/frontend/src/components/upload/UploadProgress.vue new file mode 100644 index 0000000..908cf70 --- /dev/null +++ b/frontend/src/components/upload/UploadProgress.vue @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..695ba53 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router/index.js' +import './style.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..ece1f8d --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,18 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/HomeView.vue' +import TopicsView from '../views/TopicsView.vue' +import DocumentView from '../views/DocumentView.vue' +import SettingsView from '../views/SettingsView.vue' + +const routes = [ + { path: '/', component: HomeView }, + { path: '/topics', component: TopicsView }, + { path: '/topics/:name', component: TopicsView }, + { path: '/document/:id', component: DocumentView }, + { path: '/settings', component: SettingsView }, +] + +export default createRouter({ + history: createWebHistory(), + routes, +}) diff --git a/frontend/src/stores/documents.js b/frontend/src/stores/documents.js new file mode 100644 index 0000000..6220e80 --- /dev/null +++ b/frontend/src/stores/documents.js @@ -0,0 +1,46 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as api from '../api/client.js' + +export const useDocumentsStore = defineStore('documents', () => { + const documents = ref([]) + const total = ref(0) + const loading = ref(false) + const error = ref(null) + + async function fetchDocuments({ topic, page = 1, perPage = 20 } = {}) { + loading.value = true + error.value = null + try { + const data = await api.listDocuments({ topic, page, perPage }) + documents.value = data.items + total.value = data.total + } catch (e) { + error.value = e.message + } finally { + loading.value = false + } + } + + async function upload(file, autoClassify = true) { + const doc = await api.uploadDocument(file, autoClassify) + documents.value.unshift(doc) + total.value++ + return doc + } + + async function remove(id) { + await api.deleteDocument(id) + documents.value = documents.value.filter(d => d.id !== id) + total.value-- + } + + async function reclassify(id, topics = null) { + const result = await api.classifyDocument(id, topics) + const idx = documents.value.findIndex(d => d.id === id) + if (idx !== -1) documents.value[idx].topics = result.topics + return result.topics + } + + return { documents, total, loading, error, fetchDocuments, upload, remove, reclassify } +}) diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js new file mode 100644 index 0000000..fc5709c --- /dev/null +++ b/frontend/src/stores/settings.js @@ -0,0 +1,38 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as api from '../api/client.js' + +export const useSettingsStore = defineStore('settings', () => { + const settings = ref(null) + const loading = ref(false) + const error = ref(null) + + async function fetchSettings() { + loading.value = true + error.value = null + try { + settings.value = await api.getSettings() + } catch (e) { + error.value = e.message + } finally { + loading.value = false + } + } + + async function save(patch) { + const updated = await api.patchSettings(patch) + settings.value = updated + return updated + } + + async function testConnection(provider) { + return api.testProvider(provider) + } + + async function resetPrompt() { + const data = await api.getDefaultPrompt() + return data.system_prompt + } + + return { settings, loading, error, fetchSettings, save, testConnection, resetPrompt } +}) diff --git a/frontend/src/stores/topics.js b/frontend/src/stores/topics.js new file mode 100644 index 0000000..7d480f5 --- /dev/null +++ b/frontend/src/stores/topics.js @@ -0,0 +1,42 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as api from '../api/client.js' + +export const useTopicsStore = defineStore('topics', () => { + const topics = ref([]) + const loading = ref(false) + const error = ref(null) + + async function fetchTopics() { + loading.value = true + error.value = null + try { + const data = await api.listTopics() + topics.value = data.topics + } catch (e) { + error.value = e.message + } finally { + loading.value = false + } + } + + async function addTopic(payload) { + const topic = await api.createTopic(payload) + topics.value.push(topic) + return topic + } + + async function editTopic(id, patch) { + const updated = await api.updateTopic(id, patch) + const idx = topics.value.findIndex(t => t.id === id) + if (idx !== -1) topics.value[idx] = updated + return updated + } + + async function removeTopic(id) { + await api.deleteTopic(id) + topics.value = topics.value.filter(t => t.id !== id) + } + + return { topics, loading, error, fetchTopics, addTopic, editTopic, removeTopic } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..7ba313c --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-gray-50 text-gray-900; + } +} diff --git a/frontend/src/views/DocumentView.vue b/frontend/src/views/DocumentView.vue new file mode 100644 index 0000000..f66fbd0 --- /dev/null +++ b/frontend/src/views/DocumentView.vue @@ -0,0 +1,184 @@ + + + diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue new file mode 100644 index 0000000..5b3d553 --- /dev/null +++ b/frontend/src/views/HomeView.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue new file mode 100644 index 0000000..0f943f2 --- /dev/null +++ b/frontend/src/views/SettingsView.vue @@ -0,0 +1,223 @@ + + + diff --git a/frontend/src/views/TopicsView.vue b/frontend/src/views/TopicsView.vue new file mode 100644 index 0000000..faa4734 --- /dev/null +++ b/frontend/src/views/TopicsView.vue @@ -0,0 +1,82 @@ + + + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..6c4bfb2 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{vue,js}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..ebd6662 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: 'http://backend:8000', + changeOrigin: true, + }, + }, + }, +})