Spaces:
Running
Running
Christian Kniep
commited on
Commit
·
1fff71f
1
Parent(s):
3d37e62
new webapp
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.development +18 -0
- .env.example +22 -0
- .env.production +13 -0
- .pytest_cache/CACHEDIR.TAG +4 -0
- .pytest_cache/README.md +8 -0
- .pytest_cache/v/cache/lastfailed +1 -0
- .pytest_cache/v/cache/nodeids +29 -0
- .svelte-kit/ambient.d.ts +321 -0
- .svelte-kit/generated/client/app.js +35 -0
- .svelte-kit/generated/client/matchers.js +1 -0
- .svelte-kit/generated/client/nodes/0.js +3 -0
- .svelte-kit/generated/client/nodes/1.js +1 -0
- .svelte-kit/generated/client/nodes/2.js +1 -0
- .svelte-kit/generated/client/nodes/3.js +2 -0
- .svelte-kit/generated/client/nodes/4.js +1 -0
- .svelte-kit/generated/client/nodes/5.js +3 -0
- .svelte-kit/generated/root.svelte +61 -0
- .svelte-kit/generated/server/internal.js +53 -0
- .svelte-kit/non-ambient.d.ts +46 -0
- .svelte-kit/tsconfig.json +52 -0
- .svelte-kit/types/route_meta_data.json +17 -0
- .svelte-kit/types/src/routes/$types.d.ts +26 -0
- .svelte-kit/types/src/routes/auth/callback/$types.d.ts +20 -0
- .svelte-kit/types/src/routes/auth/callback/proxy+page.ts +37 -0
- .svelte-kit/types/src/routes/login/$types.d.ts +18 -0
- .svelte-kit/types/src/routes/proxy+layout.ts +32 -0
- .svelte-kit/types/src/routes/session/[id]/$types.d.ts +21 -0
- .svelte-kit/types/src/routes/session/[id]/proxy+page.ts +19 -0
- Dockerfile +9 -29
- README.md +406 -17
- app.py +0 -81
- data/contacts.db +0 -0
- data/migrations/001_add_producer_fields.sql +16 -0
- migrations/001_create_tables.sql +30 -0
- package-lock.json +0 -0
- pyproject.toml +33 -0
- requirements.txt +35 -16
- src/__init__.py +0 -1
- src/__pycache__/app.cpython-311.pyc +0 -0
- src/app.py +216 -0
- src/lib/components/ChatMessage.svelte +140 -0
- src/lib/components/ErrorAlert.svelte +26 -0
- src/lib/components/LoadingSpinner.svelte +16 -0
- src/lib/components/LoginForm.svelte +94 -0
- src/lib/components/MessageInput.svelte +157 -0
- src/lib/components/ModeSelector.svelte +87 -0
- src/lib/components/SessionHeader.svelte +107 -0
- src/lib/components/SessionList.svelte +324 -0
- src/lib/services/api.ts +255 -0
- src/lib/services/auth.ts +206 -0
.env.development
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Configuration
|
| 2 |
+
VITE_API_URL=http://localhost:4004
|
| 3 |
+
VITE_API_TIMEOUT=30000
|
| 4 |
+
|
| 5 |
+
# Authentication
|
| 6 |
+
VITE_ENABLE_MOCK_AUTH=true
|
| 7 |
+
VITE_MOCK_USER_ID=testuser
|
| 8 |
+
VITE_MOCK_USERNAME=Test User
|
| 9 | |
| 10 |
+
|
| 11 |
+
# OAuth (for production)
|
| 12 |
+
VITE_OAUTH_CLIENT_ID=
|
| 13 |
+
VITE_OAUTH_REDIRECT_URI=http://localhost:5173/auth/callback
|
| 14 |
+
VITE_OAUTH_PROVIDER_URL=https://huggingface.co
|
| 15 |
+
|
| 16 |
+
# Feature Flags
|
| 17 |
+
VITE_ENABLE_COMPARISON=true
|
| 18 |
+
VITE_SESSION_LIMIT=20
|
.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flask Configuration
|
| 2 |
+
FLASK_APP=src.app:app
|
| 3 |
+
FLASK_ENV=development
|
| 4 |
+
SECRET_KEY=your-secret-key-here-generate-with-secrets-token-hex
|
| 5 |
+
|
| 6 |
+
# HuggingFace OAuth
|
| 7 |
+
HF_CLIENT_ID=your-huggingface-client-id
|
| 8 |
+
HF_CLIENT_SECRET=your-huggingface-client-secret
|
| 9 |
+
HF_REDIRECT_URI=http://localhost:5000/callback
|
| 10 |
+
|
| 11 |
+
# Backend API
|
| 12 |
+
BACKEND_API_URL=http://localhost:8080/v1
|
| 13 |
+
BACKEND_API_TIMEOUT=5
|
| 14 |
+
|
| 15 |
+
# Database
|
| 16 |
+
DATABASE_PATH=data/contacts.db
|
| 17 |
+
|
| 18 |
+
# Session Configuration
|
| 19 |
+
SESSION_COOKIE_SECURE=False
|
| 20 |
+
SESSION_COOKIE_HTTPONLY=True
|
| 21 |
+
SESSION_COOKIE_SAMESITE=Lax
|
| 22 |
+
PERMANENT_SESSION_LIFETIME=2592000
|
.env.production
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Configuration
|
| 2 |
+
VITE_API_URL=https://api.your-domain.com
|
| 3 |
+
VITE_API_TIMEOUT=30000
|
| 4 |
+
|
| 5 |
+
# Authentication
|
| 6 |
+
VITE_ENABLE_MOCK_AUTH=false
|
| 7 |
+
VITE_OAUTH_CLIENT_ID=production-client-id
|
| 8 |
+
VITE_OAUTH_REDIRECT_URI=https://your-domain.com/auth/callback
|
| 9 |
+
VITE_OAUTH_PROVIDER_URL=https://huggingface.co
|
| 10 |
+
|
| 11 |
+
# Feature Flags
|
| 12 |
+
VITE_ENABLE_COMPARISON=true
|
| 13 |
+
VITE_SESSION_LIMIT=20
|
.pytest_cache/CACHEDIR.TAG
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Signature: 8a477f597d28d172789f06886806bc55
|
| 2 |
+
# This file is a cache directory tag created by pytest.
|
| 3 |
+
# For information about cache directory tags, see:
|
| 4 |
+
# https://bford.info/cachedir/spec.html
|
.pytest_cache/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pytest cache directory #
|
| 2 |
+
|
| 3 |
+
This directory contains data from the pytest's cache plugin,
|
| 4 |
+
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
| 5 |
+
|
| 6 |
+
**Do not** commit this to version control.
|
| 7 |
+
|
| 8 |
+
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
|
.pytest_cache/v/cache/lastfailed
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{}
|
.pytest_cache/v/cache/nodeids
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_apostrophes_removed",
|
| 3 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_basic_lowercase",
|
| 4 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_case_insensitivity",
|
| 5 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_collision_scenarios",
|
| 6 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_dots_removed",
|
| 7 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_empty_string",
|
| 8 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_hyphens_removed",
|
| 9 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_idempotency",
|
| 10 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_leading_trailing_special_chars",
|
| 11 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_mixed_special_characters",
|
| 12 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_numbers_preserved",
|
| 13 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_only_special_chars",
|
| 14 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_real_world_examples",
|
| 15 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_spaces_removed",
|
| 16 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_special_characters",
|
| 17 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_underscores_and_symbols",
|
| 18 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_unicode_letters_preserved",
|
| 19 |
+
"tests/unit/test_contact_utils.py::TestNormalizeContactName::test_whitespace_variations",
|
| 20 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_collision_prevention",
|
| 21 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_different_normalized_names_separate_sequences",
|
| 22 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_different_users_separate_sequences",
|
| 23 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_first_sequence_number",
|
| 24 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_gaps_in_sequence",
|
| 25 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_incremental_sequence",
|
| 26 |
+
"tests/unit/test_storage_service.py::TestGetNextSequenceNumber::test_multiple_sequences",
|
| 27 |
+
"tests/unit/test_storage_service.py::TestProducerIdFormat::test_collision_examples",
|
| 28 |
+
"tests/unit/test_storage_service.py::TestProducerIdFormat::test_producer_id_format"
|
| 29 |
+
]
|
.svelte-kit/ambient.d.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
// this file is generated — do not edit it
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
/// <reference types="@sveltejs/kit" />
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured).
|
| 9 |
+
*
|
| 10 |
+
* _Unlike_ [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.
|
| 11 |
+
*
|
| 12 |
+
* ```ts
|
| 13 |
+
* import { API_KEY } from '$env/static/private';
|
| 14 |
+
* ```
|
| 15 |
+
*
|
| 16 |
+
* Note that all environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
|
| 17 |
+
*
|
| 18 |
+
* ```
|
| 19 |
+
* MY_FEATURE_FLAG=""
|
| 20 |
+
* ```
|
| 21 |
+
*
|
| 22 |
+
* You can override `.env` values from the command line like so:
|
| 23 |
+
*
|
| 24 |
+
* ```sh
|
| 25 |
+
* MY_FEATURE_FLAG="enabled" npm run dev
|
| 26 |
+
* ```
|
| 27 |
+
*/
|
| 28 |
+
declare module '$env/static/private' {
|
| 29 |
+
export const VITE_API_URL: string;
|
| 30 |
+
export const VITE_API_TIMEOUT: string;
|
| 31 |
+
export const VITE_ENABLE_MOCK_AUTH: string;
|
| 32 |
+
export const VITE_MOCK_USER_ID: string;
|
| 33 |
+
export const VITE_MOCK_USERNAME: string;
|
| 34 |
+
export const VITE_MOCK_EMAIL: string;
|
| 35 |
+
export const VITE_OAUTH_CLIENT_ID: string;
|
| 36 |
+
export const VITE_OAUTH_REDIRECT_URI: string;
|
| 37 |
+
export const VITE_OAUTH_PROVIDER_URL: string;
|
| 38 |
+
export const VITE_ENABLE_COMPARISON: string;
|
| 39 |
+
export const VITE_SESSION_LIMIT: string;
|
| 40 |
+
export const SHELL: string;
|
| 41 |
+
export const npm_command: string;
|
| 42 |
+
export const LSCOLORS: string;
|
| 43 |
+
export const USER_ZDOTDIR: string;
|
| 44 |
+
export const npm_config_userconfig: string;
|
| 45 |
+
export const COLORTERM: string;
|
| 46 |
+
export const XDG_CONFIG_DIRS: string;
|
| 47 |
+
export const VSCODE_DEBUGPY_ADAPTER_ENDPOINTS: string;
|
| 48 |
+
export const npm_config_cache: string;
|
| 49 |
+
export const API_PORT: string;
|
| 50 |
+
export const LESS: string;
|
| 51 |
+
export const XPC_FLAGS: string;
|
| 52 |
+
export const TERM_PROGRAM_VERSION: string;
|
| 53 |
+
export const JAEGER_AGENT_PORT: string;
|
| 54 |
+
export const NODE: string;
|
| 55 |
+
export const MOCK_OAUTH_USER_NAME: string;
|
| 56 |
+
export const JAVA_HOME: string;
|
| 57 |
+
export const __CFBundleIdentifier: string;
|
| 58 |
+
export const SSH_AUTH_SOCK: string;
|
| 59 |
+
export const MallocNanoZone: string;
|
| 60 |
+
export const MOCK_OAUTH_ENABLED: string;
|
| 61 |
+
export const JAEGER_AGENT_HOST: string;
|
| 62 |
+
export const PYDEVD_DISABLE_FILE_VALIDATION: string;
|
| 63 |
+
export const OSLogRateLimit: string;
|
| 64 |
+
export const JAEGER_SAMPLER_TYPE: string;
|
| 65 |
+
export const COLOR: string;
|
| 66 |
+
export const OPENAI_API_KEY: string;
|
| 67 |
+
export const STREAMLIT_SERVER_ADDRESS: string;
|
| 68 |
+
export const npm_config_local_prefix: string;
|
| 69 |
+
export const SDKMAN_CANDIDATES_DIR: string;
|
| 70 |
+
export const HOMEBREW_PREFIX: string;
|
| 71 |
+
export const npm_config_globalconfig: string;
|
| 72 |
+
export const OPENAI_MODEL: string;
|
| 73 |
+
export const EDITOR: string;
|
| 74 |
+
export const PWD: string;
|
| 75 |
+
export const NIX_PROFILES: string;
|
| 76 |
+
export const JAEGER_SAMPLER_PARAM: string;
|
| 77 |
+
export const LOGNAME: string;
|
| 78 |
+
export const PORT: string;
|
| 79 |
+
export const npm_config_init_module: string;
|
| 80 |
+
export const BUILDKIT_PROGRESS: string;
|
| 81 |
+
export const __NIX_DARWIN_SET_ENVIRONMENT_DONE: string;
|
| 82 |
+
export const _: string;
|
| 83 |
+
export const BUNDLED_DEBUGPY_PATH: string;
|
| 84 |
+
export const VSCODE_GIT_ASKPASS_NODE: string;
|
| 85 |
+
export const VSCODE_INJECTION: string;
|
| 86 |
+
export const COMMAND_MODE: string;
|
| 87 |
+
export const HOME: string;
|
| 88 |
+
export const LANG: string;
|
| 89 |
+
export const LS_COLORS: string;
|
| 90 |
+
export const npm_package_version: string;
|
| 91 |
+
export const LLM_PROVIDER: string;
|
| 92 |
+
export const PYTHONSTARTUP: string;
|
| 93 |
+
export const NIX_SSL_CERT_FILE: string;
|
| 94 |
+
export const MEMORY_BACKEND_PORT: string;
|
| 95 |
+
export const TMPDIR: string;
|
| 96 |
+
export const GIT_ASKPASS: string;
|
| 97 |
+
export const JAEGER_ENABLED: string;
|
| 98 |
+
export const JAEGER_UI_PORT: string;
|
| 99 |
+
export const PROMPT: string;
|
| 100 |
+
export const INIT_CWD: string;
|
| 101 |
+
export const NIX_USER_PROFILE_DIR: string;
|
| 102 |
+
export const INFOPATH: string;
|
| 103 |
+
export const npm_lifecycle_script: string;
|
| 104 |
+
export const MOCK_OAUTH_USER_AVATAR: string;
|
| 105 |
+
export const VSCODE_GIT_ASKPASS_EXTRA_ARGS: string;
|
| 106 |
+
export const VSCODE_PYTHON_AUTOACTIVATE_GUARD: string;
|
| 107 |
+
export const npm_config_npm_version: string;
|
| 108 |
+
export const STREAMLIT_SERVER_PORT: string;
|
| 109 |
+
export const TERM: string;
|
| 110 |
+
export const npm_package_name: string;
|
| 111 |
+
export const PYTHON_BASIC_REPL: string;
|
| 112 |
+
export const ZSH: string;
|
| 113 |
+
export const npm_config_prefix: string;
|
| 114 |
+
export const ZDOTDIR: string;
|
| 115 |
+
export const OPENAI_MAX_TOKENS: string;
|
| 116 |
+
export const USER: string;
|
| 117 |
+
export const GIT_PAGER: string;
|
| 118 |
+
export const VSCODE_GIT_IPC_HANDLE: string;
|
| 119 |
+
export const HOMEBREW_CELLAR: string;
|
| 120 |
+
export const SDKMAN_DIR: string;
|
| 121 |
+
export const API_BEARER_TOKEN: string;
|
| 122 |
+
export const npm_lifecycle_event: string;
|
| 123 |
+
export const SHLVL: string;
|
| 124 |
+
export const PAGER: string;
|
| 125 |
+
export const HOMEBREW_REPOSITORY: string;
|
| 126 |
+
export const SDKMAN_CANDIDATES_API: string;
|
| 127 |
+
export const STREAMLIT_BROWSER_GATHER_USAGE_STATS: string;
|
| 128 |
+
export const XPC_SERVICE_NAME: string;
|
| 129 |
+
export const npm_config_user_agent: string;
|
| 130 |
+
export const OPENAI_TEMPERATURE: string;
|
| 131 |
+
export const MOCK_OAUTH_USER_EMAIL: string;
|
| 132 |
+
export const TERMINFO_DIRS: string;
|
| 133 |
+
export const npm_execpath: string;
|
| 134 |
+
export const npm_package_json: string;
|
| 135 |
+
export const VSCODE_GIT_ASKPASS_MAIN: string;
|
| 136 |
+
export const XDG_DATA_DIRS: string;
|
| 137 |
+
export const npm_config_noproxy: string;
|
| 138 |
+
export const APP_PORT: string;
|
| 139 |
+
export const PATH: string;
|
| 140 |
+
export const npm_config_node_gyp: string;
|
| 141 |
+
export const ORIGINAL_XDG_CURRENT_DESKTOP: string;
|
| 142 |
+
export const MOCK_OAUTH_USER_ID: string;
|
| 143 |
+
export const SDKMAN_PLATFORM: string;
|
| 144 |
+
export const npm_config_global_prefix: string;
|
| 145 |
+
export const npm_node_execpath: string;
|
| 146 |
+
export const API_AUTH_TOKEN: string;
|
| 147 |
+
export const OLDPWD: string;
|
| 148 |
+
export const __CF_USER_TEXT_ENCODING: string;
|
| 149 |
+
export const TERM_PROGRAM: string;
|
| 150 |
+
export const NODE_ENV: string;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Similar to [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
|
| 155 |
+
*
|
| 156 |
+
* Values are replaced statically at build time.
|
| 157 |
+
*
|
| 158 |
+
* ```ts
|
| 159 |
+
* import { PUBLIC_BASE_URL } from '$env/static/public';
|
| 160 |
+
* ```
|
| 161 |
+
*/
|
| 162 |
+
declare module '$env/static/public' {
|
| 163 |
+
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node) (or running [`vite preview`](https://svelte.dev/docs/kit/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured).
|
| 168 |
+
*
|
| 169 |
+
* This module cannot be imported into client-side code.
|
| 170 |
+
*
|
| 171 |
+
* ```ts
|
| 172 |
+
* import { env } from '$env/dynamic/private';
|
| 173 |
+
* console.log(env.DEPLOYMENT_SPECIFIC_VARIABLE);
|
| 174 |
+
* ```
|
| 175 |
+
*
|
| 176 |
+
* > [!NOTE] In `dev`, `$env/dynamic` always includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
|
| 177 |
+
*/
|
| 178 |
+
declare module '$env/dynamic/private' {
|
| 179 |
+
export const env: {
|
| 180 |
+
VITE_API_URL: string;
|
| 181 |
+
VITE_API_TIMEOUT: string;
|
| 182 |
+
VITE_ENABLE_MOCK_AUTH: string;
|
| 183 |
+
VITE_MOCK_USER_ID: string;
|
| 184 |
+
VITE_MOCK_USERNAME: string;
|
| 185 |
+
VITE_MOCK_EMAIL: string;
|
| 186 |
+
VITE_OAUTH_CLIENT_ID: string;
|
| 187 |
+
VITE_OAUTH_REDIRECT_URI: string;
|
| 188 |
+
VITE_OAUTH_PROVIDER_URL: string;
|
| 189 |
+
VITE_ENABLE_COMPARISON: string;
|
| 190 |
+
VITE_SESSION_LIMIT: string;
|
| 191 |
+
SHELL: string;
|
| 192 |
+
npm_command: string;
|
| 193 |
+
LSCOLORS: string;
|
| 194 |
+
USER_ZDOTDIR: string;
|
| 195 |
+
npm_config_userconfig: string;
|
| 196 |
+
COLORTERM: string;
|
| 197 |
+
XDG_CONFIG_DIRS: string;
|
| 198 |
+
VSCODE_DEBUGPY_ADAPTER_ENDPOINTS: string;
|
| 199 |
+
npm_config_cache: string;
|
| 200 |
+
API_PORT: string;
|
| 201 |
+
LESS: string;
|
| 202 |
+
XPC_FLAGS: string;
|
| 203 |
+
TERM_PROGRAM_VERSION: string;
|
| 204 |
+
JAEGER_AGENT_PORT: string;
|
| 205 |
+
NODE: string;
|
| 206 |
+
MOCK_OAUTH_USER_NAME: string;
|
| 207 |
+
JAVA_HOME: string;
|
| 208 |
+
__CFBundleIdentifier: string;
|
| 209 |
+
SSH_AUTH_SOCK: string;
|
| 210 |
+
MallocNanoZone: string;
|
| 211 |
+
MOCK_OAUTH_ENABLED: string;
|
| 212 |
+
JAEGER_AGENT_HOST: string;
|
| 213 |
+
PYDEVD_DISABLE_FILE_VALIDATION: string;
|
| 214 |
+
OSLogRateLimit: string;
|
| 215 |
+
JAEGER_SAMPLER_TYPE: string;
|
| 216 |
+
COLOR: string;
|
| 217 |
+
OPENAI_API_KEY: string;
|
| 218 |
+
STREAMLIT_SERVER_ADDRESS: string;
|
| 219 |
+
npm_config_local_prefix: string;
|
| 220 |
+
SDKMAN_CANDIDATES_DIR: string;
|
| 221 |
+
HOMEBREW_PREFIX: string;
|
| 222 |
+
npm_config_globalconfig: string;
|
| 223 |
+
OPENAI_MODEL: string;
|
| 224 |
+
EDITOR: string;
|
| 225 |
+
PWD: string;
|
| 226 |
+
NIX_PROFILES: string;
|
| 227 |
+
JAEGER_SAMPLER_PARAM: string;
|
| 228 |
+
LOGNAME: string;
|
| 229 |
+
PORT: string;
|
| 230 |
+
npm_config_init_module: string;
|
| 231 |
+
BUILDKIT_PROGRESS: string;
|
| 232 |
+
__NIX_DARWIN_SET_ENVIRONMENT_DONE: string;
|
| 233 |
+
_: string;
|
| 234 |
+
BUNDLED_DEBUGPY_PATH: string;
|
| 235 |
+
VSCODE_GIT_ASKPASS_NODE: string;
|
| 236 |
+
VSCODE_INJECTION: string;
|
| 237 |
+
COMMAND_MODE: string;
|
| 238 |
+
HOME: string;
|
| 239 |
+
LANG: string;
|
| 240 |
+
LS_COLORS: string;
|
| 241 |
+
npm_package_version: string;
|
| 242 |
+
LLM_PROVIDER: string;
|
| 243 |
+
PYTHONSTARTUP: string;
|
| 244 |
+
NIX_SSL_CERT_FILE: string;
|
| 245 |
+
MEMORY_BACKEND_PORT: string;
|
| 246 |
+
TMPDIR: string;
|
| 247 |
+
GIT_ASKPASS: string;
|
| 248 |
+
JAEGER_ENABLED: string;
|
| 249 |
+
JAEGER_UI_PORT: string;
|
| 250 |
+
PROMPT: string;
|
| 251 |
+
INIT_CWD: string;
|
| 252 |
+
NIX_USER_PROFILE_DIR: string;
|
| 253 |
+
INFOPATH: string;
|
| 254 |
+
npm_lifecycle_script: string;
|
| 255 |
+
MOCK_OAUTH_USER_AVATAR: string;
|
| 256 |
+
VSCODE_GIT_ASKPASS_EXTRA_ARGS: string;
|
| 257 |
+
VSCODE_PYTHON_AUTOACTIVATE_GUARD: string;
|
| 258 |
+
npm_config_npm_version: string;
|
| 259 |
+
STREAMLIT_SERVER_PORT: string;
|
| 260 |
+
TERM: string;
|
| 261 |
+
npm_package_name: string;
|
| 262 |
+
PYTHON_BASIC_REPL: string;
|
| 263 |
+
ZSH: string;
|
| 264 |
+
npm_config_prefix: string;
|
| 265 |
+
ZDOTDIR: string;
|
| 266 |
+
OPENAI_MAX_TOKENS: string;
|
| 267 |
+
USER: string;
|
| 268 |
+
GIT_PAGER: string;
|
| 269 |
+
VSCODE_GIT_IPC_HANDLE: string;
|
| 270 |
+
HOMEBREW_CELLAR: string;
|
| 271 |
+
SDKMAN_DIR: string;
|
| 272 |
+
API_BEARER_TOKEN: string;
|
| 273 |
+
npm_lifecycle_event: string;
|
| 274 |
+
SHLVL: string;
|
| 275 |
+
PAGER: string;
|
| 276 |
+
HOMEBREW_REPOSITORY: string;
|
| 277 |
+
SDKMAN_CANDIDATES_API: string;
|
| 278 |
+
STREAMLIT_BROWSER_GATHER_USAGE_STATS: string;
|
| 279 |
+
XPC_SERVICE_NAME: string;
|
| 280 |
+
npm_config_user_agent: string;
|
| 281 |
+
OPENAI_TEMPERATURE: string;
|
| 282 |
+
MOCK_OAUTH_USER_EMAIL: string;
|
| 283 |
+
TERMINFO_DIRS: string;
|
| 284 |
+
npm_execpath: string;
|
| 285 |
+
npm_package_json: string;
|
| 286 |
+
VSCODE_GIT_ASKPASS_MAIN: string;
|
| 287 |
+
XDG_DATA_DIRS: string;
|
| 288 |
+
npm_config_noproxy: string;
|
| 289 |
+
APP_PORT: string;
|
| 290 |
+
PATH: string;
|
| 291 |
+
npm_config_node_gyp: string;
|
| 292 |
+
ORIGINAL_XDG_CURRENT_DESKTOP: string;
|
| 293 |
+
MOCK_OAUTH_USER_ID: string;
|
| 294 |
+
SDKMAN_PLATFORM: string;
|
| 295 |
+
npm_config_global_prefix: string;
|
| 296 |
+
npm_node_execpath: string;
|
| 297 |
+
API_AUTH_TOKEN: string;
|
| 298 |
+
OLDPWD: string;
|
| 299 |
+
__CF_USER_TEXT_ENCODING: string;
|
| 300 |
+
TERM_PROGRAM: string;
|
| 301 |
+
NODE_ENV: string;
|
| 302 |
+
[key: `PUBLIC_${string}`]: undefined;
|
| 303 |
+
[key: `${string}`]: string | undefined;
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/**
|
| 308 |
+
* Similar to [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
|
| 309 |
+
*
|
| 310 |
+
* Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead.
|
| 311 |
+
*
|
| 312 |
+
* ```ts
|
| 313 |
+
* import { env } from '$env/dynamic/public';
|
| 314 |
+
* console.log(env.PUBLIC_DEPLOYMENT_SPECIFIC_VARIABLE);
|
| 315 |
+
* ```
|
| 316 |
+
*/
|
| 317 |
+
declare module '$env/dynamic/public' {
|
| 318 |
+
export const env: {
|
| 319 |
+
[key: `PUBLIC_${string}`]: string | undefined;
|
| 320 |
+
}
|
| 321 |
+
}
|
.svelte-kit/generated/client/app.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { matchers } from './matchers.js';
|
| 2 |
+
|
| 3 |
+
export const nodes = [
|
| 4 |
+
() => import('./nodes/0'),
|
| 5 |
+
() => import('./nodes/1'),
|
| 6 |
+
() => import('./nodes/2'),
|
| 7 |
+
() => import('./nodes/3'),
|
| 8 |
+
() => import('./nodes/4'),
|
| 9 |
+
() => import('./nodes/5')
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
export const server_loads = [];
|
| 13 |
+
|
| 14 |
+
export const dictionary = {
|
| 15 |
+
"/": [2],
|
| 16 |
+
"/auth/callback": [3],
|
| 17 |
+
"/login": [4],
|
| 18 |
+
"/session/[id]": [5]
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export const hooks = {
|
| 22 |
+
handleError: (({ error }) => { console.error(error) }),
|
| 23 |
+
|
| 24 |
+
reroute: (() => {}),
|
| 25 |
+
transport: {}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
|
| 29 |
+
export const encoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.encode]));
|
| 30 |
+
|
| 31 |
+
export const hash = false;
|
| 32 |
+
|
| 33 |
+
export const decode = (type, value) => decoders[type](value);
|
| 34 |
+
|
| 35 |
+
export { default as root } from '../root.svelte';
|
.svelte-kit/generated/client/matchers.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export const matchers = {};
|
.svelte-kit/generated/client/nodes/0.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as universal from "../../../../src/routes/+layout.ts";
|
| 2 |
+
export { universal };
|
| 3 |
+
export { default as component } from "../../../../src/routes/+layout.svelte";
|
.svelte-kit/generated/client/nodes/1.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export { default as component } from "../../../../node_modules/@sveltejs/kit/src/runtime/components/svelte-4/error.svelte";
|
.svelte-kit/generated/client/nodes/2.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export { default as component } from "../../../../src/routes/+page.svelte";
|
.svelte-kit/generated/client/nodes/3.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as universal from "../../../../src/routes/auth/callback/+page.ts";
|
| 2 |
+
export { universal };
|
.svelte-kit/generated/client/nodes/4.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export { default as component } from "../../../../src/routes/login/+page.svelte";
|
.svelte-kit/generated/client/nodes/5.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as universal from "../../../../src/routes/session/[id]/+page.ts";
|
| 2 |
+
export { universal };
|
| 3 |
+
export { default as component } from "../../../../src/routes/session/[id]/+page.svelte";
|
.svelte-kit/generated/root.svelte
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- This file is generated by @sveltejs/kit — do not edit it! -->
|
| 2 |
+
|
| 3 |
+
<script>
|
| 4 |
+
import { setContext, afterUpdate, onMount, tick } from 'svelte';
|
| 5 |
+
import { browser } from '$app/environment';
|
| 6 |
+
|
| 7 |
+
// stores
|
| 8 |
+
export let stores;
|
| 9 |
+
export let page;
|
| 10 |
+
|
| 11 |
+
export let constructors;
|
| 12 |
+
export let components = [];
|
| 13 |
+
export let form;
|
| 14 |
+
export let data_0 = null;
|
| 15 |
+
export let data_1 = null;
|
| 16 |
+
|
| 17 |
+
if (!browser) {
|
| 18 |
+
setContext('__svelte__', stores);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
$: stores.page.set(page);
|
| 22 |
+
afterUpdate(stores.page.notify);
|
| 23 |
+
|
| 24 |
+
let mounted = false;
|
| 25 |
+
let navigated = false;
|
| 26 |
+
let title = null;
|
| 27 |
+
|
| 28 |
+
onMount(() => {
|
| 29 |
+
const unsubscribe = stores.page.subscribe(() => {
|
| 30 |
+
if (mounted) {
|
| 31 |
+
navigated = true;
|
| 32 |
+
tick().then(() => {
|
| 33 |
+
title = document.title || 'untitled page';
|
| 34 |
+
});
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
mounted = true;
|
| 39 |
+
return unsubscribe;
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
</script>
|
| 44 |
+
|
| 45 |
+
{#if constructors[1]}
|
| 46 |
+
<svelte:component this={constructors[0]} bind:this={components[0]} data={data_0} params={page.params}>
|
| 47 |
+
<svelte:component this={constructors[1]} bind:this={components[1]} data={data_1} {form} params={page.params} />
|
| 48 |
+
</svelte:component>
|
| 49 |
+
|
| 50 |
+
{:else}
|
| 51 |
+
<svelte:component this={constructors[0]} bind:this={components[0]} data={data_0} {form} params={page.params} />
|
| 52 |
+
|
| 53 |
+
{/if}
|
| 54 |
+
|
| 55 |
+
{#if mounted}
|
| 56 |
+
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
|
| 57 |
+
{#if navigated}
|
| 58 |
+
{title}
|
| 59 |
+
{/if}
|
| 60 |
+
</div>
|
| 61 |
+
{/if}
|
.svelte-kit/generated/server/internal.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import root from '../root.svelte';
|
| 3 |
+
import { set_building, set_prerendering } from '__sveltekit/environment';
|
| 4 |
+
import { set_assets } from '$app/paths/internal/server';
|
| 5 |
+
import { set_manifest, set_read_implementation } from '__sveltekit/server';
|
| 6 |
+
import { set_private_env, set_public_env } from '../../../node_modules/@sveltejs/kit/src/runtime/shared-server.js';
|
| 7 |
+
|
| 8 |
+
export const options = {
|
| 9 |
+
app_template_contains_nonce: false,
|
| 10 |
+
async: false,
|
| 11 |
+
csp: {"mode":"auto","directives":{"upgrade-insecure-requests":false,"block-all-mixed-content":false},"reportOnly":{"upgrade-insecure-requests":false,"block-all-mixed-content":false}},
|
| 12 |
+
csrf_check_origin: true,
|
| 13 |
+
csrf_trusted_origins: [],
|
| 14 |
+
embedded: false,
|
| 15 |
+
env_public_prefix: 'PUBLIC_',
|
| 16 |
+
env_private_prefix: '',
|
| 17 |
+
hash_routing: false,
|
| 18 |
+
hooks: null, // added lazily, via `get_hooks`
|
| 19 |
+
preload_strategy: "modulepreload",
|
| 20 |
+
root,
|
| 21 |
+
service_worker: false,
|
| 22 |
+
service_worker_options: undefined,
|
| 23 |
+
templates: {
|
| 24 |
+
app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<meta name=\"description\" content=\"PrepMate - Chat session manager with memory integration\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.ico\" />\n\t\t\n\t\t<!-- Bootstrap CSS -->\n\t\t<link \n\t\t\thref=\"https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css\" \n\t\t\trel=\"stylesheet\" \n\t\t\tintegrity=\"sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM\" \n\t\t\tcrossorigin=\"anonymous\"\n\t\t/>\n\t\t\n\t\t<!-- Bootstrap Icons -->\n\t\t<link \n\t\t\trel=\"stylesheet\" \n\t\t\thref=\"https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css\"\n\t\t/>\n\t\t\n\t\t" + head + "\n\t</head>\n\t<body>\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t\t\n\t\t<!-- Bootstrap JS Bundle -->\n\t\t<script \n\t\t\tsrc=\"https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js\" \n\t\t\tintegrity=\"sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz\" \n\t\t\tcrossorigin=\"anonymous\"\n\t\t></script>\n\t</body>\n</html>\n",
|
| 25 |
+
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
| 26 |
+
},
|
| 27 |
+
version_hash: "n000i8"
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export async function get_hooks() {
|
| 31 |
+
let handle;
|
| 32 |
+
let handleFetch;
|
| 33 |
+
let handleError;
|
| 34 |
+
let handleValidationError;
|
| 35 |
+
let init;
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
let reroute;
|
| 39 |
+
let transport;
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
return {
|
| 43 |
+
handle,
|
| 44 |
+
handleFetch,
|
| 45 |
+
handleError,
|
| 46 |
+
handleValidationError,
|
| 47 |
+
init,
|
| 48 |
+
reroute,
|
| 49 |
+
transport
|
| 50 |
+
};
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export { set_assets, set_building, set_manifest, set_prerendering, set_private_env, set_public_env, set_read_implementation };
|
.svelte-kit/non-ambient.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
// this file is generated — do not edit it
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
declare module "svelte/elements" {
|
| 6 |
+
export interface HTMLAttributes<T> {
|
| 7 |
+
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
|
| 8 |
+
'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;
|
| 9 |
+
'data-sveltekit-preload-code'?:
|
| 10 |
+
| true
|
| 11 |
+
| ''
|
| 12 |
+
| 'eager'
|
| 13 |
+
| 'viewport'
|
| 14 |
+
| 'hover'
|
| 15 |
+
| 'tap'
|
| 16 |
+
| 'off'
|
| 17 |
+
| undefined
|
| 18 |
+
| null;
|
| 19 |
+
'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
|
| 20 |
+
'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
|
| 21 |
+
'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export {};
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
declare module "$app/types" {
|
| 29 |
+
export interface AppTypes {
|
| 30 |
+
RouteId(): "/" | "/auth" | "/auth/callback" | "/login" | "/session" | "/session/[id]";
|
| 31 |
+
RouteParams(): {
|
| 32 |
+
"/session/[id]": { id: string }
|
| 33 |
+
};
|
| 34 |
+
LayoutParams(): {
|
| 35 |
+
"/": { id?: string };
|
| 36 |
+
"/auth": Record<string, never>;
|
| 37 |
+
"/auth/callback": Record<string, never>;
|
| 38 |
+
"/login": Record<string, never>;
|
| 39 |
+
"/session": { id?: string };
|
| 40 |
+
"/session/[id]": { id: string }
|
| 41 |
+
};
|
| 42 |
+
Pathname(): "/" | "/auth" | "/auth/" | "/auth/callback" | "/auth/callback/" | "/login" | "/login/" | "/session" | "/session/" | `/session/${string}` & {} | `/session/${string}/` & {};
|
| 43 |
+
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
|
| 44 |
+
Asset(): "/favicon.ico" | "/robots.txt" | string & {};
|
| 45 |
+
}
|
| 46 |
+
}
|
.svelte-kit/tsconfig.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"paths": {
|
| 4 |
+
"$lib": [
|
| 5 |
+
"../src/lib"
|
| 6 |
+
],
|
| 7 |
+
"$lib/*": [
|
| 8 |
+
"../src/lib/*"
|
| 9 |
+
],
|
| 10 |
+
"$app/types": [
|
| 11 |
+
"./types/index.d.ts"
|
| 12 |
+
]
|
| 13 |
+
},
|
| 14 |
+
"rootDirs": [
|
| 15 |
+
"..",
|
| 16 |
+
"./types"
|
| 17 |
+
],
|
| 18 |
+
"verbatimModuleSyntax": true,
|
| 19 |
+
"isolatedModules": true,
|
| 20 |
+
"lib": [
|
| 21 |
+
"esnext",
|
| 22 |
+
"DOM",
|
| 23 |
+
"DOM.Iterable"
|
| 24 |
+
],
|
| 25 |
+
"moduleResolution": "bundler",
|
| 26 |
+
"module": "esnext",
|
| 27 |
+
"noEmit": true,
|
| 28 |
+
"target": "esnext"
|
| 29 |
+
},
|
| 30 |
+
"include": [
|
| 31 |
+
"ambient.d.ts",
|
| 32 |
+
"non-ambient.d.ts",
|
| 33 |
+
"./types/**/$types.d.ts",
|
| 34 |
+
"../vite.config.js",
|
| 35 |
+
"../vite.config.ts",
|
| 36 |
+
"../src/**/*.js",
|
| 37 |
+
"../src/**/*.ts",
|
| 38 |
+
"../src/**/*.svelte",
|
| 39 |
+
"../tests/**/*.js",
|
| 40 |
+
"../tests/**/*.ts",
|
| 41 |
+
"../tests/**/*.svelte"
|
| 42 |
+
],
|
| 43 |
+
"exclude": [
|
| 44 |
+
"../node_modules/**",
|
| 45 |
+
"../src/service-worker.js",
|
| 46 |
+
"../src/service-worker/**/*.js",
|
| 47 |
+
"../src/service-worker.ts",
|
| 48 |
+
"../src/service-worker/**/*.ts",
|
| 49 |
+
"../src/service-worker.d.ts",
|
| 50 |
+
"../src/service-worker/**/*.d.ts"
|
| 51 |
+
]
|
| 52 |
+
}
|
.svelte-kit/types/route_meta_data.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"/": [
|
| 3 |
+
"src/routes/+layout.ts",
|
| 4 |
+
"src/routes/+layout.ts"
|
| 5 |
+
],
|
| 6 |
+
"/auth/callback": [
|
| 7 |
+
"src/routes/auth/callback/+page.ts",
|
| 8 |
+
"src/routes/+layout.ts"
|
| 9 |
+
],
|
| 10 |
+
"/login": [
|
| 11 |
+
"src/routes/+layout.ts"
|
| 12 |
+
],
|
| 13 |
+
"/session/[id]": [
|
| 14 |
+
"src/routes/session/[id]/+page.ts",
|
| 15 |
+
"src/routes/+layout.ts"
|
| 16 |
+
]
|
| 17 |
+
}
|
.svelte-kit/types/src/routes/$types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type * as Kit from '@sveltejs/kit';
|
| 2 |
+
|
| 3 |
+
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
| 4 |
+
// @ts-ignore
|
| 5 |
+
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
|
| 6 |
+
type RouteParams = { };
|
| 7 |
+
type RouteId = '/';
|
| 8 |
+
type MaybeWithVoid<T> = {} extends T ? T | void : T;
|
| 9 |
+
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
|
| 10 |
+
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
|
| 11 |
+
type EnsureDefined<T> = T extends null | undefined ? {} : T;
|
| 12 |
+
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
|
| 13 |
+
export type Snapshot<T = any> = Kit.Snapshot<T>;
|
| 14 |
+
type PageParentData = EnsureDefined<LayoutData>;
|
| 15 |
+
type LayoutRouteId = RouteId | "/" | "/auth/callback" | "/login" | "/session/[id]" | null
|
| 16 |
+
type LayoutParams = RouteParams & { id?: string }
|
| 17 |
+
type LayoutParentData = EnsureDefined<{}>;
|
| 18 |
+
|
| 19 |
+
export type PageServerData = null;
|
| 20 |
+
export type PageData = Expand<PageParentData>;
|
| 21 |
+
export type PageProps = { params: RouteParams; data: PageData }
|
| 22 |
+
export type LayoutServerData = null;
|
| 23 |
+
export type LayoutLoad<OutputData extends OutputDataShape<LayoutParentData> = OutputDataShape<LayoutParentData>> = Kit.Load<LayoutParams, LayoutServerData, LayoutParentData, OutputData, LayoutRouteId>;
|
| 24 |
+
export type LayoutLoadEvent = Parameters<LayoutLoad>[0];
|
| 25 |
+
export type LayoutData = Expand<Omit<LayoutParentData, keyof Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+layout.js').load>>>> & OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+layout.js').load>>>>>>;
|
| 26 |
+
export type LayoutProps = { params: LayoutParams; data: LayoutData; children: import("svelte").Snippet }
|
.svelte-kit/types/src/routes/auth/callback/$types.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type * as Kit from '@sveltejs/kit';
|
| 2 |
+
|
| 3 |
+
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
| 4 |
+
// @ts-ignore
|
| 5 |
+
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
|
| 6 |
+
type RouteParams = { };
|
| 7 |
+
type RouteId = '/auth/callback';
|
| 8 |
+
type MaybeWithVoid<T> = {} extends T ? T | void : T;
|
| 9 |
+
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
|
| 10 |
+
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
|
| 11 |
+
type EnsureDefined<T> = T extends null | undefined ? {} : T;
|
| 12 |
+
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
|
| 13 |
+
export type Snapshot<T = any> = Kit.Snapshot<T>;
|
| 14 |
+
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;
|
| 15 |
+
|
| 16 |
+
export type PageServerData = null;
|
| 17 |
+
export type PageLoad<OutputData extends OutputDataShape<PageParentData> = OutputDataShape<PageParentData>> = Kit.Load<RouteParams, PageServerData, PageParentData, OutputData, RouteId>;
|
| 18 |
+
export type PageLoadEvent = Parameters<PageLoad>[0];
|
| 19 |
+
export type PageData = Expand<Omit<PageParentData, keyof Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.js').load>>>> & OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.js').load>>>>>>;
|
| 20 |
+
export type PageProps = { params: RouteParams; data: PageData }
|
.svelte-kit/types/src/routes/auth/callback/proxy+page.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-nocheck
|
| 2 |
+
import { redirect } from '@sveltejs/kit';
|
| 3 |
+
import type { PageLoad } from './$types';
|
| 4 |
+
import { handleOAuthCallback } from '$lib/services/auth';
|
| 5 |
+
import { browser } from '$app/environment';
|
| 6 |
+
|
| 7 |
+
export const load = async ({ url }: Parameters<PageLoad>[0]) => {
|
| 8 |
+
if (!browser) {
|
| 9 |
+
return {};
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const code = url.searchParams.get('code');
|
| 13 |
+
const state = url.searchParams.get('state');
|
| 14 |
+
const error = url.searchParams.get('error');
|
| 15 |
+
|
| 16 |
+
// Handle OAuth error
|
| 17 |
+
if (error) {
|
| 18 |
+
console.error('OAuth error:', error);
|
| 19 |
+
throw redirect(302, '/login?error=' + encodeURIComponent(error));
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Handle successful authorization
|
| 23 |
+
if (code && state) {
|
| 24 |
+
try {
|
| 25 |
+
await handleOAuthCallback(code, state);
|
| 26 |
+
// Redirect to home on success
|
| 27 |
+
throw redirect(302, '/');
|
| 28 |
+
} catch (err) {
|
| 29 |
+
console.error('OAuth callback error:', err);
|
| 30 |
+
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
|
| 31 |
+
throw redirect(302, '/login?error=' + encodeURIComponent(errorMessage));
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// No code or state - redirect to login
|
| 36 |
+
throw redirect(302, '/login');
|
| 37 |
+
};
|
.svelte-kit/types/src/routes/login/$types.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type * as Kit from '@sveltejs/kit';
|
| 2 |
+
|
| 3 |
+
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
| 4 |
+
// @ts-ignore
|
| 5 |
+
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
|
| 6 |
+
type RouteParams = { };
|
| 7 |
+
type RouteId = '/login';
|
| 8 |
+
type MaybeWithVoid<T> = {} extends T ? T | void : T;
|
| 9 |
+
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
|
| 10 |
+
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
|
| 11 |
+
type EnsureDefined<T> = T extends null | undefined ? {} : T;
|
| 12 |
+
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
|
| 13 |
+
export type Snapshot<T = any> = Kit.Snapshot<T>;
|
| 14 |
+
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
|
| 15 |
+
|
| 16 |
+
export type PageServerData = null;
|
| 17 |
+
export type PageData = Expand<PageParentData>;
|
| 18 |
+
export type PageProps = { params: RouteParams; data: PageData }
|
.svelte-kit/types/src/routes/proxy+layout.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-nocheck
|
| 2 |
+
import { redirect } from '@sveltejs/kit';
|
| 3 |
+
import { browser } from '$app/environment';
|
| 4 |
+
import { getItem } from '$lib/services/storage';
|
| 5 |
+
import { STORAGE_KEY_AUTH_TOKEN } from '$lib/utils/constants';
|
| 6 |
+
import type { LayoutLoad } from './$types';
|
| 7 |
+
|
| 8 |
+
export const load = async ({ url }: Parameters<LayoutLoad>[0]) => {
|
| 9 |
+
if (!browser) {
|
| 10 |
+
return {};
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// Check if user is authenticated
|
| 14 |
+
const token = getItem<string>(STORAGE_KEY_AUTH_TOKEN);
|
| 15 |
+
const isAuthenticated = !!token;
|
| 16 |
+
|
| 17 |
+
// Define public routes that don't require authentication
|
| 18 |
+
const publicRoutes = ['/login', '/auth/callback', '/'];
|
| 19 |
+
const isPublicRoute = publicRoutes.some((route) => url.pathname.startsWith(route));
|
| 20 |
+
|
| 21 |
+
// Redirect to login if not authenticated and trying to access protected route
|
| 22 |
+
if (!isAuthenticated && !isPublicRoute) {
|
| 23 |
+
throw redirect(302, '/login');
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Redirect to home if authenticated and trying to access login page
|
| 27 |
+
if (isAuthenticated && url.pathname === '/login') {
|
| 28 |
+
throw redirect(302, '/');
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
return {};
|
| 32 |
+
};
|
.svelte-kit/types/src/routes/session/[id]/$types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type * as Kit from '@sveltejs/kit';
|
| 2 |
+
|
| 3 |
+
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
| 4 |
+
// @ts-ignore
|
| 5 |
+
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
|
| 6 |
+
type RouteParams = { id: string };
|
| 7 |
+
type RouteId = '/session/[id]';
|
| 8 |
+
type MaybeWithVoid<T> = {} extends T ? T | void : T;
|
| 9 |
+
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
|
| 10 |
+
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
|
| 11 |
+
type EnsureDefined<T> = T extends null | undefined ? {} : T;
|
| 12 |
+
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
|
| 13 |
+
export type Snapshot<T = any> = Kit.Snapshot<T>;
|
| 14 |
+
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;
|
| 15 |
+
|
| 16 |
+
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
|
| 17 |
+
export type PageServerData = null;
|
| 18 |
+
export type PageLoad<OutputData extends OutputDataShape<PageParentData> = OutputDataShape<PageParentData>> = Kit.Load<RouteParams, PageServerData, PageParentData, OutputData, RouteId>;
|
| 19 |
+
export type PageLoadEvent = Parameters<PageLoad>[0];
|
| 20 |
+
export type PageData = Expand<Omit<PageParentData, keyof Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.js').load>>>> & OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.js').load>>>>>>;
|
| 21 |
+
export type PageProps = { params: RouteParams; data: PageData }
|
.svelte-kit/types/src/routes/session/[id]/proxy+page.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-nocheck
|
| 2 |
+
import type { PageLoad } from './$types';
|
| 3 |
+
import { sessionStore } from '$lib/stores/session';
|
| 4 |
+
import { error } from '@sveltejs/kit';
|
| 5 |
+
|
| 6 |
+
export const load = async ({ params }: Parameters<PageLoad>[0]) => {
|
| 7 |
+
const { id } = params;
|
| 8 |
+
|
| 9 |
+
if (!id) {
|
| 10 |
+
throw error(404, 'Session not found');
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// Load session data
|
| 14 |
+
await sessionStore.loadSession(id);
|
| 15 |
+
|
| 16 |
+
return {
|
| 17 |
+
sessionId: id
|
| 18 |
+
};
|
| 19 |
+
};
|
Dockerfile
CHANGED
|
@@ -1,40 +1,20 @@
|
|
| 1 |
-
# Dockerfile for API Session Chat Frontend
|
| 2 |
-
# Optimized for Hugging Face Spaces (2 vCPU, 16 GB RAM)
|
| 3 |
-
|
| 4 |
FROM python:3.11-slim
|
| 5 |
|
| 6 |
-
# Set working directory
|
| 7 |
WORKDIR /app
|
| 8 |
|
| 9 |
-
|
| 10 |
-
ENV PYTHONUNBUFFERED=1 \
|
| 11 |
-
PYTHONDONTWRITEBYTECODE=1 \
|
| 12 |
-
PIP_NO_CACHE_DIR=1 \
|
| 13 |
-
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 14 |
-
|
| 15 |
-
# Install system dependencies (minimal for requests[security])
|
| 16 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 17 |
-
build-essential \
|
| 18 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
|
| 20 |
-
# Copy requirements first for better caching
|
| 21 |
COPY requirements.txt .
|
| 22 |
-
|
| 23 |
-
# Install Python dependencies
|
| 24 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
|
| 26 |
-
|
| 27 |
-
COPY
|
| 28 |
-
|
| 29 |
-
# Create data directory
|
| 30 |
-
RUN mkdir -p data/sessions
|
| 31 |
|
| 32 |
-
|
| 33 |
-
EXPOSE 8501
|
| 34 |
|
| 35 |
-
|
| 36 |
-
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 37 |
-
CMD curl -f http://localhost:8501/_stcore/health || exit 1
|
| 38 |
|
| 39 |
-
# Run
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
|
|
|
| 7 |
COPY requirements.txt .
|
|
|
|
|
|
|
| 8 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
|
| 10 |
+
COPY src/ src/
|
| 11 |
+
COPY migrations/ migrations/
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
RUN mkdir -p data
|
|
|
|
| 14 |
|
| 15 |
+
EXPOSE 7860
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# Run with single worker to fix OAuth session persistence
|
| 18 |
+
# TODO: Implement server-side session storage (Redis/Memcached) for multi-worker support
|
| 19 |
+
# Increase timeout to 120s to handle slow LLM responses
|
| 20 |
+
CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:7860", "--timeout", "120", "--graceful-timeout", "120", "src.app:app"]
|
README.md
CHANGED
|
@@ -1,17 +1,406 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PrepMate Webapp - Profile and Contact Management UI# PrepMate Webapp
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
Flask-based web application for managing user profiles and contact sessions with HuggingFace OAuth authentication.Modern web application for PrepMate built with Svelte, SvelteKit, and Bootstrap 5.
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
## Quick Start## Technology Stack
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
### Prerequisites- **Framework**: Svelte 4.2+ with SvelteKit 2.0+
|
| 14 |
+
|
| 15 |
+
- **Language**: TypeScript 5.3+ (strict mode)
|
| 16 |
+
|
| 17 |
+
- Python 3.11+- **Styling**: Bootstrap 5.3 + Bootstrap Icons 1.11
|
| 18 |
+
|
| 19 |
+
- HuggingFace OAuth app credentials- **Build Tool**: Vite 5.0+
|
| 20 |
+
|
| 21 |
+
- Backend API running (Go server)- **Testing**: Vitest (unit) + Playwright (e2e)
|
| 22 |
+
|
| 23 |
+
- **HTTP Client**: Native Fetch API
|
| 24 |
+
|
| 25 |
+
### Local Development
|
| 26 |
+
|
| 27 |
+
## Prerequisites
|
| 28 |
+
|
| 29 |
+
1. **Install dependencies:**
|
| 30 |
+
|
| 31 |
+
```bash- Node.js 20.x or later
|
| 32 |
+
|
| 33 |
+
pip install -r requirements.txt- npm 10.x or later
|
| 34 |
+
|
| 35 |
+
```- Docker (for backend services)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
2. **Configure environment:**## Quick Start
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
|
| 43 |
+
cp .env.example .env### 1. Install Dependencies
|
| 44 |
+
|
| 45 |
+
# Edit .env with your HF OAuth credentials
|
| 46 |
+
|
| 47 |
+
``````bash
|
| 48 |
+
|
| 49 |
+
npm install
|
| 50 |
+
|
| 51 |
+
3. **Initialize database:**```
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
|
| 55 |
+
python -c "from src.services.storage_service import init_db; init_db()"### 2. Start Backend Services
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
From the repository root:
|
| 60 |
+
|
| 61 |
+
4. **Run development server:**
|
| 62 |
+
|
| 63 |
+
```bash```bash
|
| 64 |
+
|
| 65 |
+
export FLASK_APP=src.app:appdocker compose up -d api postgres memory-backend
|
| 66 |
+
|
| 67 |
+
export FLASK_ENV=development```
|
| 68 |
+
|
| 69 |
+
flask run --port=5000
|
| 70 |
+
|
| 71 |
+
```### 3. Start Development Server
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
Access at: http://localhost:5000```bash
|
| 76 |
+
|
| 77 |
+
npm run dev
|
| 78 |
+
|
| 79 |
+
### Docker Development```
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
```bashThe webapp will be available at `http://localhost:5173`
|
| 84 |
+
|
| 85 |
+
# From repository root
|
| 86 |
+
|
| 87 |
+
docker-compose up webapp## Development
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Available Scripts
|
| 92 |
+
|
| 93 |
+
Access at: http://localhost:5000
|
| 94 |
+
|
| 95 |
+
- `npm run dev` - Start development server with hot reload
|
| 96 |
+
|
| 97 |
+
## Project Structure- `npm run build` - Build for production
|
| 98 |
+
|
| 99 |
+
- `npm run preview` - Preview production build locally
|
| 100 |
+
|
| 101 |
+
```- `npm test` - Run unit tests in watch mode
|
| 102 |
+
|
| 103 |
+
webapp/- `npm run test:unit` - Run unit tests once
|
| 104 |
+
|
| 105 |
+
├── src/- `npm run test:e2e` - Run end-to-end tests
|
| 106 |
+
|
| 107 |
+
│ ├── app.py # Flask application- `npm run test:e2e:ui` - Run e2e tests with UI
|
| 108 |
+
|
| 109 |
+
│ ├── models/ # Data models- `npm run check` - Type-check TypeScript
|
| 110 |
+
|
| 111 |
+
│ ├── services/ # Business logic- `npm run lint` - Lint code with ESLint
|
| 112 |
+
|
| 113 |
+
│ ├── routes/ # HTTP routes- `npm run format` - Format code with Prettier
|
| 114 |
+
|
| 115 |
+
│ ├── templates/ # Jinja2 templates
|
| 116 |
+
|
| 117 |
+
│ └── static/ # CSS/JS assets### Project Structure
|
| 118 |
+
|
| 119 |
+
├── tests/ # Unit and integration tests
|
| 120 |
+
|
| 121 |
+
├── data/ # SQLite database```
|
| 122 |
+
|
| 123 |
+
├── migrations/ # Database migrationswebapp/
|
| 124 |
+
|
| 125 |
+
└── requirements.txt # Python dependencies├── src/
|
| 126 |
+
|
| 127 |
+
```│ ├── lib/
|
| 128 |
+
|
| 129 |
+
│ │ ├── components/ # Svelte UI components
|
| 130 |
+
|
| 131 |
+
## Features│ │ ├── services/ # Business logic & API clients
|
| 132 |
+
|
| 133 |
+
│ │ ├── stores/ # Svelte stores (state management)
|
| 134 |
+
|
| 135 |
+
- **User Profile Management**: Maintain personal facts via HuggingFace OAuth│ │ ├── types/ # TypeScript type definitions
|
| 136 |
+
|
| 137 |
+
- **Contact Sessions**: Create isolated sessions for different contacts│ │ └── utils/ # Helper functions
|
| 138 |
+
|
| 139 |
+
- **Fact Management**: Add/edit facts with 2000 character limit│ ├── routes/ # SvelteKit pages (file-based routing)
|
| 140 |
+
|
| 141 |
+
- **Message Exchange**: Send/receive messages with backend LLM integration│ └── app.html # HTML template
|
| 142 |
+
|
| 143 |
+
- **Contact Navigation**: Search and sort contacts by recent activity├── static/ # Static assets (favicon, robots.txt)
|
| 144 |
+
|
| 145 |
+
├── tests/
|
| 146 |
+
|
| 147 |
+
## Testing│ ├── unit/ # Vitest unit tests
|
| 148 |
+
|
| 149 |
+
│ └── e2e/ # Playwright e2e tests
|
| 150 |
+
|
| 151 |
+
```bash└── package.json
|
| 152 |
+
|
| 153 |
+
# Unit tests```
|
| 154 |
+
|
| 155 |
+
pytest tests/unit/ -v
|
| 156 |
+
|
| 157 |
+
### Environment Variables
|
| 158 |
+
|
| 159 |
+
# Integration tests
|
| 160 |
+
|
| 161 |
+
pytest tests/integration/ -vCreate `.env.development` for local development:
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# Coverage report```bash
|
| 166 |
+
|
| 167 |
+
pytest --cov=src --cov-report=html# API Configuration
|
| 168 |
+
|
| 169 |
+
```VITE_API_URL=http://localhost:4004
|
| 170 |
+
|
| 171 |
+
VITE_API_TIMEOUT=30000
|
| 172 |
+
|
| 173 |
+
## Configuration
|
| 174 |
+
|
| 175 |
+
# Authentication
|
| 176 |
+
|
| 177 |
+
See `.env.example` for all available environment variables.VITE_ENABLE_MOCK_AUTH=true
|
| 178 |
+
|
| 179 |
+
VITE_MOCK_USER_ID=testuser
|
| 180 |
+
|
| 181 |
+
Key settings:VITE_MOCK_USERNAME=Test User
|
| 182 |
+
|
| 183 |
+
- `HF_CLIENT_ID`: HuggingFace OAuth app client [email protected]
|
| 184 |
+
|
| 185 |
+
- `HF_CLIENT_SECRET`: HuggingFace OAuth app secret
|
| 186 |
+
|
| 187 |
+
- `BACKEND_API_URL`: Backend API base URL (default: http://api:4004/v1)# Feature Flags
|
| 188 |
+
|
| 189 |
+
- `SECRET_KEY`: Flask session secret (generate with `python -c "import secrets; print(secrets.token_hex(32))"`)VITE_ENABLE_COMPARISON=true
|
| 190 |
+
|
| 191 |
+
VITE_SESSION_LIMIT=20
|
| 192 |
+
|
| 193 |
+
## Troubleshooting```
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
See [quickstart.md](../specs/012-profile-contact-ui/quickstart.md) for detailed setup instructions and troubleshooting.## Testing
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
### Unit Tests
|
| 201 |
+
|
| 202 |
+
```bash
|
| 203 |
+
# Run all unit tests
|
| 204 |
+
npm test
|
| 205 |
+
|
| 206 |
+
# Run specific test file
|
| 207 |
+
npm test -- src/lib/services/api.test.ts
|
| 208 |
+
|
| 209 |
+
# Run with coverage
|
| 210 |
+
npm test -- --coverage
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### End-to-End Tests
|
| 214 |
+
|
| 215 |
+
```bash
|
| 216 |
+
# Run all e2e tests
|
| 217 |
+
npm run test:e2e
|
| 218 |
+
|
| 219 |
+
# Run with UI
|
| 220 |
+
npm run test:e2e:ui
|
| 221 |
+
|
| 222 |
+
# Run specific browser
|
| 223 |
+
npm run test:e2e -- --project=chromium
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
## Building for Production
|
| 227 |
+
|
| 228 |
+
```bash
|
| 229 |
+
# Create optimized production build
|
| 230 |
+
npm run build
|
| 231 |
+
|
| 232 |
+
# Preview production build locally
|
| 233 |
+
npm run preview
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
The build output will be in the `build/` directory.
|
| 237 |
+
|
| 238 |
+
## Docker
|
| 239 |
+
|
| 240 |
+
### Build and Run
|
| 241 |
+
|
| 242 |
+
```bash
|
| 243 |
+
# Build Docker image
|
| 244 |
+
docker build -t prepmate-webapp .
|
| 245 |
+
|
| 246 |
+
# Run container
|
| 247 |
+
docker run -p 5173:80 prepmate-webapp
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
### Docker Compose
|
| 251 |
+
|
| 252 |
+
From the repository root:
|
| 253 |
+
|
| 254 |
+
```bash
|
| 255 |
+
# Start all services including webapp
|
| 256 |
+
docker compose up -d
|
| 257 |
+
|
| 258 |
+
# View logs
|
| 259 |
+
docker compose logs -f webapp
|
| 260 |
+
|
| 261 |
+
# Rebuild after changes
|
| 262 |
+
docker compose build webapp && docker compose up -d webapp
|
| 263 |
+
```
|
| 264 |
+
|
| 265 |
+
The webapp will be available at `http://localhost:5173`
|
| 266 |
+
|
| 267 |
+
## Architecture
|
| 268 |
+
|
| 269 |
+
### State Management
|
| 270 |
+
|
| 271 |
+
The webapp uses Svelte stores for reactive state management:
|
| 272 |
+
|
| 273 |
+
- **Auth Store** (`stores/auth.ts`): User authentication state
|
| 274 |
+
- **Session Store** (`stores/session.ts`): Active session and sessions list
|
| 275 |
+
- **UI Store** (`stores/ui.ts`): UI state (sidebar visibility, loading, errors)
|
| 276 |
+
|
| 277 |
+
### API Integration
|
| 278 |
+
|
| 279 |
+
The webapp communicates with the Go API backend using a centralized API client (`services/api.ts`) that:
|
| 280 |
+
|
| 281 |
+
- Wraps native Fetch API
|
| 282 |
+
- Handles authentication (Bearer token + X-User-ID headers)
|
| 283 |
+
- Implements retry logic and timeout handling
|
| 284 |
+
- Provides type-safe methods for all endpoints
|
| 285 |
+
|
| 286 |
+
### Routing
|
| 287 |
+
|
| 288 |
+
SvelteKit provides file-based routing:
|
| 289 |
+
|
| 290 |
+
- `/` - Home page (session list)
|
| 291 |
+
- `/login` - Login page (OAuth or mock)
|
| 292 |
+
- `/session/[id]` - Session detail (chat interface)
|
| 293 |
+
- `/auth/callback` - OAuth callback handler
|
| 294 |
+
|
| 295 |
+
## Contributing
|
| 296 |
+
|
| 297 |
+
### Code Style
|
| 298 |
+
|
| 299 |
+
- TypeScript strict mode enabled
|
| 300 |
+
- ESLint for linting
|
| 301 |
+
- Prettier for formatting
|
| 302 |
+
- Follow existing patterns in components and services
|
| 303 |
+
|
| 304 |
+
### Testing Requirements
|
| 305 |
+
|
| 306 |
+
- Unit tests for all services and stores (>80% coverage goal)
|
| 307 |
+
- Unit tests for complex components
|
| 308 |
+
- E2e tests for critical user flows
|
| 309 |
+
- All tests must pass before committing
|
| 310 |
+
|
| 311 |
+
### Pull Request Process
|
| 312 |
+
|
| 313 |
+
1. Create feature branch from `main`
|
| 314 |
+
2. Implement changes with tests
|
| 315 |
+
3. Run `npm run check` and `npm run lint`
|
| 316 |
+
4. Run all tests (`npm test` and `npm run test:e2e`)
|
| 317 |
+
5. Update documentation if needed
|
| 318 |
+
6. Submit PR with clear description
|
| 319 |
+
|
| 320 |
+
## Troubleshooting
|
| 321 |
+
|
| 322 |
+
### Port Already in Use
|
| 323 |
+
|
| 324 |
+
```bash
|
| 325 |
+
# Find process using port 5173
|
| 326 |
+
lsof -i :5173
|
| 327 |
+
|
| 328 |
+
# Kill process
|
| 329 |
+
kill -9 <PID>
|
| 330 |
+
|
| 331 |
+
# Or use different port
|
| 332 |
+
npm run dev -- --port 5174
|
| 333 |
+
```
|
| 334 |
+
|
| 335 |
+
### Node Modules Issues
|
| 336 |
+
|
| 337 |
+
```bash
|
| 338 |
+
# Clear and reinstall
|
| 339 |
+
rm -rf node_modules package-lock.json
|
| 340 |
+
npm install
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
### API Connection Errors
|
| 344 |
+
|
| 345 |
+
```bash
|
| 346 |
+
# Verify API is running
|
| 347 |
+
curl http://localhost:4004/health
|
| 348 |
+
|
| 349 |
+
# Check docker logs
|
| 350 |
+
docker compose logs api
|
| 351 |
+
|
| 352 |
+
# Restart API
|
| 353 |
+
docker compose restart api
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
### Build Errors
|
| 357 |
+
|
| 358 |
+
```bash
|
| 359 |
+
# Clear caches
|
| 360 |
+
rm -rf .svelte-kit node_modules/.vite
|
| 361 |
+
|
| 362 |
+
# Rebuild
|
| 363 |
+
npm run build
|
| 364 |
+
```
|
| 365 |
+
|
| 366 |
+
## Performance
|
| 367 |
+
|
| 368 |
+
### Bundle Size
|
| 369 |
+
|
| 370 |
+
Target: <500KB gzipped
|
| 371 |
+
|
| 372 |
+
Check bundle size after build:
|
| 373 |
+
|
| 374 |
+
```bash
|
| 375 |
+
npm run build
|
| 376 |
+
ls -lh build/
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
### Lighthouse Scores
|
| 380 |
+
|
| 381 |
+
Target: >90 performance score
|
| 382 |
+
|
| 383 |
+
Run Lighthouse audit in Chrome DevTools or:
|
| 384 |
+
|
| 385 |
+
```bash
|
| 386 |
+
npm run build
|
| 387 |
+
npm run preview
|
| 388 |
+
# Then run Lighthouse on http://localhost:5173
|
| 389 |
+
```
|
| 390 |
+
|
| 391 |
+
## Security
|
| 392 |
+
|
| 393 |
+
- All user inputs are validated and sanitized
|
| 394 |
+
- XSS protection via proper escaping
|
| 395 |
+
- CSRF protection for state-changing operations
|
| 396 |
+
- HTTPS enforced in production
|
| 397 |
+
- Authentication tokens stored securely
|
| 398 |
+
- Dependencies audited regularly (`npm audit`)
|
| 399 |
+
|
| 400 |
+
## License
|
| 401 |
+
|
| 402 |
+
See repository root LICENSE file.
|
| 403 |
+
|
| 404 |
+
## Support
|
| 405 |
+
|
| 406 |
+
For issues and questions, see the main repository README.
|
app.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
API Session Chat Frontend
|
| 3 |
-
|
| 4 |
-
Streamlit-based chat interface for interacting with an external API server.
|
| 5 |
-
Supports session management, multiple message modes, and reference session comparison.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import streamlit as st
|
| 9 |
-
from src.ui.pages import main
|
| 10 |
-
|
| 11 |
-
# Configure page
|
| 12 |
-
st.set_page_config(
|
| 13 |
-
page_title="API Session Chat",
|
| 14 |
-
page_icon="💬",
|
| 15 |
-
layout="wide",
|
| 16 |
-
initial_sidebar_state="expanded"
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
# Add header with MemMachine branding and links
|
| 20 |
-
st.markdown("""
|
| 21 |
-
<style>
|
| 22 |
-
.header-links {
|
| 23 |
-
position: fixed;
|
| 24 |
-
top: 60px;
|
| 25 |
-
right: 20px;
|
| 26 |
-
z-index: 999;
|
| 27 |
-
display: flex;
|
| 28 |
-
gap: 15px;
|
| 29 |
-
align-items: center;
|
| 30 |
-
background: var(--background-color);
|
| 31 |
-
padding: 8px;
|
| 32 |
-
border-radius: 8px;
|
| 33 |
-
}
|
| 34 |
-
.header-links .powered-by {
|
| 35 |
-
color: #0066cc;
|
| 36 |
-
font-weight: bold;
|
| 37 |
-
margin-right: 5px;
|
| 38 |
-
font-size: 20px;
|
| 39 |
-
}
|
| 40 |
-
.header-links a {
|
| 41 |
-
text-decoration: none;
|
| 42 |
-
color: inherit;
|
| 43 |
-
display: flex;
|
| 44 |
-
align-items: center;
|
| 45 |
-
padding: 5px;
|
| 46 |
-
border-radius: 5px;
|
| 47 |
-
transition: background-color 0.2s;
|
| 48 |
-
}
|
| 49 |
-
.header-links a:hover {
|
| 50 |
-
background-color: rgba(128, 128, 128, 0.2);
|
| 51 |
-
}
|
| 52 |
-
.header-links img {
|
| 53 |
-
width: 24px;
|
| 54 |
-
height: 24px;
|
| 55 |
-
border-radius: 4px;
|
| 56 |
-
}
|
| 57 |
-
.header-links svg {
|
| 58 |
-
width: 24px;
|
| 59 |
-
height: 24px;
|
| 60 |
-
}
|
| 61 |
-
</style>
|
| 62 |
-
<div class="header-links">
|
| 63 |
-
<span class="powered-by">Powered by MemMachine</span>
|
| 64 |
-
<a href="https://memmachine.ai/" target="_blank" title="MemMachine">
|
| 65 |
-
<img src="https://avatars.githubusercontent.com/u/226739620?s=48&v=4" alt="MemMachine"/>
|
| 66 |
-
</a>
|
| 67 |
-
<a href="https://github.com/MemMachine/MemMachine" target="_blank" title="GitHub Repository">
|
| 68 |
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 69 |
-
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
| 70 |
-
</svg>
|
| 71 |
-
</a>
|
| 72 |
-
<a href="https://discord.gg/usydANvKqD" target="_blank" title="Discord Community">
|
| 73 |
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 74 |
-
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
| 75 |
-
</svg>
|
| 76 |
-
</a>
|
| 77 |
-
</div>
|
| 78 |
-
""", unsafe_allow_html=True)
|
| 79 |
-
|
| 80 |
-
if __name__ == "__main__":
|
| 81 |
-
main.render()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/contacts.db
ADDED
|
Binary file (41 kB). View file
|
|
|
data/migrations/001_add_producer_fields.sql
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Migration: Add producer identifier fields to contact_sessions table
|
| 2 |
+
-- Date: 2025-11-17
|
| 3 |
+
-- Feature: 001-refine-memory-producer-logic
|
| 4 |
+
-- Purpose: Enable unique producer identifiers for contact-specific facts
|
| 5 |
+
|
| 6 |
+
BEGIN TRANSACTION;
|
| 7 |
+
|
| 8 |
+
-- Add new columns for producer logic
|
| 9 |
+
ALTER TABLE contact_sessions ADD COLUMN normalized_name TEXT;
|
| 10 |
+
ALTER TABLE contact_sessions ADD COLUMN sequence_number INTEGER DEFAULT 1;
|
| 11 |
+
ALTER TABLE contact_sessions ADD COLUMN producer_id TEXT;
|
| 12 |
+
|
| 13 |
+
-- Create index for efficient sequence number lookups
|
| 14 |
+
CREATE INDEX IF NOT EXISTS idx_contact_sessions_normalized ON contact_sessions(user_id, normalized_name);
|
| 15 |
+
|
| 16 |
+
COMMIT;
|
migrations/001_create_tables.sql
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Database Schema for Profile and Contact Management UI
|
| 2 |
+
-- Feature: 012-profile-contact-ui
|
| 3 |
+
-- Version: 1.0.0
|
| 4 |
+
|
| 5 |
+
-- User profiles table
|
| 6 |
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
| 7 |
+
user_id VARCHAR(255) PRIMARY KEY NOT NULL,
|
| 8 |
+
display_name VARCHAR(255) NOT NULL,
|
| 9 |
+
profile_picture_url TEXT,
|
| 10 |
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 11 |
+
last_login TIMESTAMP NOT NULL,
|
| 12 |
+
session_id VARCHAR(255) NOT NULL UNIQUE
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
-- Contact sessions table
|
| 16 |
+
CREATE TABLE IF NOT EXISTS contact_sessions (
|
| 17 |
+
session_id VARCHAR(255) PRIMARY KEY NOT NULL,
|
| 18 |
+
user_id VARCHAR(255) NOT NULL,
|
| 19 |
+
contact_name VARCHAR(255) NOT NULL,
|
| 20 |
+
contact_description TEXT CHECK(LENGTH(contact_description) <= 500),
|
| 21 |
+
is_reference BOOLEAN NOT NULL DEFAULT 0,
|
| 22 |
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 23 |
+
last_interaction TIMESTAMP NOT NULL,
|
| 24 |
+
FOREIGN KEY (user_id) REFERENCES user_profiles(user_id) ON DELETE CASCADE
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
-- Indexes for performance
|
| 28 |
+
CREATE INDEX IF NOT EXISTS idx_contact_sessions_user ON contact_sessions(user_id);
|
| 29 |
+
CREATE INDEX IF NOT EXISTS idx_contact_sessions_sort ON contact_sessions(user_id, last_interaction DESC);
|
| 30 |
+
CREATE INDEX IF NOT EXISTS idx_contact_sessions_reference ON contact_sessions(is_reference);
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "prepmate-webapp"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Profile and Contact Management UI for PrepMate"
|
| 5 |
+
requires-python = ">=3.11"
|
| 6 |
+
|
| 7 |
+
[tool.ruff]
|
| 8 |
+
target-version = "py311"
|
| 9 |
+
line-length = 100
|
| 10 |
+
|
| 11 |
+
[tool.ruff.lint]
|
| 12 |
+
select = [
|
| 13 |
+
"E", # pycodestyle errors
|
| 14 |
+
"W", # pycodestyle warnings
|
| 15 |
+
"F", # pyflakes
|
| 16 |
+
"I", # isort
|
| 17 |
+
"B", # flake8-bugbear
|
| 18 |
+
"C4", # flake8-comprehensions
|
| 19 |
+
]
|
| 20 |
+
ignore = [
|
| 21 |
+
"E501", # line too long (handled by formatter)
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[tool.ruff.format]
|
| 25 |
+
quote-style = "double"
|
| 26 |
+
indent-style = "space"
|
| 27 |
+
|
| 28 |
+
[tool.pytest.ini_options]
|
| 29 |
+
testpaths = ["tests"]
|
| 30 |
+
python_files = ["test_*.py"]
|
| 31 |
+
python_classes = ["Test*"]
|
| 32 |
+
python_functions = ["test_*"]
|
| 33 |
+
addopts = "-v --strict-markers"
|
requirements.txt
CHANGED
|
@@ -1,16 +1,35 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
#
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core web framework
|
| 2 |
+
Flask==3.0.0
|
| 3 |
+
Flask-WTF==1.2.1
|
| 4 |
+
|
| 5 |
+
# Authentication
|
| 6 |
+
Authlib==1.2.1
|
| 7 |
+
|
| 8 |
+
# HTTP client
|
| 9 |
+
requests==2.31.0
|
| 10 |
+
|
| 11 |
+
# Distributed tracing
|
| 12 |
+
opentelemetry-api==1.21.0
|
| 13 |
+
opentelemetry-sdk==1.21.0
|
| 14 |
+
opentelemetry-exporter-otlp-proto-http==1.21.0
|
| 15 |
+
opentelemetry-instrumentation-flask==0.42b0
|
| 16 |
+
opentelemetry-instrumentation-requests==0.42b0
|
| 17 |
+
|
| 18 |
+
# Database
|
| 19 |
+
SQLAlchemy==2.0.23
|
| 20 |
+
|
| 21 |
+
# Configuration
|
| 22 |
+
python-dotenv==1.0.0
|
| 23 |
+
|
| 24 |
+
# WSGI server
|
| 25 |
+
gunicorn==21.2.0
|
| 26 |
+
|
| 27 |
+
# Development dependencies
|
| 28 |
+
pytest==7.4.3
|
| 29 |
+
pytest-flask==1.3.0
|
| 30 |
+
playwright==1.40.0
|
| 31 |
+
ruff==0.1.7
|
| 32 |
+
|
| 33 |
+
# Testing utilities
|
| 34 |
+
pytest-cov==4.1.0
|
| 35 |
+
pytest-mock==3.12.0
|
src/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""Source package for API Session Chat Frontend."""
|
|
|
|
|
|
src/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
src/app.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flask application entry point.
|
| 3 |
+
Feature: 012-profile-contact-ui
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
from datetime import timedelta
|
| 11 |
+
|
| 12 |
+
from flask import Flask, jsonify, request, g
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
from opentelemetry import trace
|
| 15 |
+
from opentelemetry.trace import Status, StatusCode
|
| 16 |
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
| 17 |
+
|
| 18 |
+
from .services.auth_service import auth_service
|
| 19 |
+
from .services.storage_service import init_db
|
| 20 |
+
from .utils.tracing import init_tracer
|
| 21 |
+
|
| 22 |
+
# Load environment variables
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class JSONFormatter(logging.Formatter):
|
| 27 |
+
"""Custom JSON formatter for structured logging."""
|
| 28 |
+
|
| 29 |
+
def format(self, record):
|
| 30 |
+
"""Format log record as JSON."""
|
| 31 |
+
log_obj = {
|
| 32 |
+
"timestamp": self.formatTime(record, self.datefmt),
|
| 33 |
+
"level": record.levelname,
|
| 34 |
+
"module": record.module,
|
| 35 |
+
"message": record.getMessage(),
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Add exception info if present
|
| 39 |
+
if record.exc_info:
|
| 40 |
+
log_obj["exception"] = self.formatException(record.exc_info)
|
| 41 |
+
|
| 42 |
+
# Add extra fields if present
|
| 43 |
+
if hasattr(record, "request_id"):
|
| 44 |
+
log_obj["request_id"] = record.request_id
|
| 45 |
+
if hasattr(record, "user_id"):
|
| 46 |
+
log_obj["user_id"] = record.user_id
|
| 47 |
+
if hasattr(record, "duration_ms"):
|
| 48 |
+
log_obj["duration_ms"] = record.duration_ms
|
| 49 |
+
if hasattr(record, "backend_latency_ms"):
|
| 50 |
+
log_obj["backend_latency_ms"] = record.backend_latency_ms
|
| 51 |
+
if hasattr(record, "status_code"):
|
| 52 |
+
log_obj["status_code"] = record.status_code
|
| 53 |
+
if hasattr(record, "method"):
|
| 54 |
+
log_obj["method"] = record.method
|
| 55 |
+
if hasattr(record, "path"):
|
| 56 |
+
log_obj["path"] = record.path
|
| 57 |
+
|
| 58 |
+
return json.dumps(log_obj)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def create_app():
|
| 62 |
+
"""Create and configure Flask application."""
|
| 63 |
+
app = Flask(__name__)
|
| 64 |
+
|
| 65 |
+
# Configuration
|
| 66 |
+
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-key-change-me")
|
| 67 |
+
app.config["SESSION_COOKIE_SECURE"] = os.getenv("SESSION_COOKIE_SECURE", "False") == "True"
|
| 68 |
+
app.config["SESSION_COOKIE_HTTPONLY"] = (
|
| 69 |
+
os.getenv("SESSION_COOKIE_HTTPONLY", "True") == "True"
|
| 70 |
+
)
|
| 71 |
+
app.config["SESSION_COOKIE_SAMESITE"] = os.getenv("SESSION_COOKIE_SAMESITE", "Lax")
|
| 72 |
+
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(
|
| 73 |
+
seconds=int(os.getenv("PERMANENT_SESSION_LIFETIME", "2592000"))
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Initialize database
|
| 77 |
+
init_db()
|
| 78 |
+
|
| 79 |
+
# Initialize OAuth
|
| 80 |
+
auth_service.init_app(app)
|
| 81 |
+
|
| 82 |
+
# Initialize Jaeger tracing
|
| 83 |
+
init_tracer("prepmate-webapp")
|
| 84 |
+
|
| 85 |
+
# Configure logging
|
| 86 |
+
setup_logging(app)
|
| 87 |
+
|
| 88 |
+
# Register middleware
|
| 89 |
+
register_middleware(app)
|
| 90 |
+
|
| 91 |
+
# Register blueprints
|
| 92 |
+
from .routes import auth, profile, contacts
|
| 93 |
+
|
| 94 |
+
app.register_blueprint(auth.bp)
|
| 95 |
+
app.register_blueprint(profile.bp)
|
| 96 |
+
app.register_blueprint(contacts.contacts_bp)
|
| 97 |
+
|
| 98 |
+
# Root route - redirect to login
|
| 99 |
+
@app.route("/")
|
| 100 |
+
def index():
|
| 101 |
+
"""Root route - redirect to login page."""
|
| 102 |
+
from flask import session, redirect, url_for, render_template
|
| 103 |
+
|
| 104 |
+
if "user_id" in session:
|
| 105 |
+
return redirect(url_for("profile.view_profile"))
|
| 106 |
+
return render_template("login.html")
|
| 107 |
+
|
| 108 |
+
# Health check endpoint
|
| 109 |
+
@app.route("/health")
|
| 110 |
+
def health():
|
| 111 |
+
"""Health check endpoint for monitoring."""
|
| 112 |
+
return jsonify({"status": "ok"}), 200
|
| 113 |
+
|
| 114 |
+
return app
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def setup_logging(app):
|
| 118 |
+
"""Configure structured JSON logging."""
|
| 119 |
+
log_level = os.getenv("LOG_LEVEL", "INFO")
|
| 120 |
+
|
| 121 |
+
# Create handler with JSON formatter
|
| 122 |
+
handler = logging.StreamHandler()
|
| 123 |
+
handler.setFormatter(JSONFormatter())
|
| 124 |
+
|
| 125 |
+
# Configure root logger
|
| 126 |
+
logging.basicConfig(
|
| 127 |
+
level=getattr(logging, log_level),
|
| 128 |
+
handlers=[handler],
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# Configure app logger
|
| 132 |
+
app.logger.handlers = [handler]
|
| 133 |
+
app.logger.setLevel(getattr(logging, log_level))
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def register_middleware(app):
|
| 137 |
+
"""Register Flask middleware for request logging and timing."""
|
| 138 |
+
|
| 139 |
+
@app.before_request
|
| 140 |
+
def before_request():
|
| 141 |
+
"""Start request timer and generate request ID, create tracing span."""
|
| 142 |
+
g.start_time = time.time()
|
| 143 |
+
g.request_id = request.headers.get("X-Request-ID", os.urandom(8).hex())
|
| 144 |
+
|
| 145 |
+
# Skip tracing for health endpoint
|
| 146 |
+
if request.path == "/health":
|
| 147 |
+
return
|
| 148 |
+
|
| 149 |
+
# Extract parent span context from headers if present
|
| 150 |
+
try:
|
| 151 |
+
ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
|
| 152 |
+
except Exception:
|
| 153 |
+
ctx = None
|
| 154 |
+
|
| 155 |
+
# Start a new span for this request
|
| 156 |
+
tracer = trace.get_tracer(__name__)
|
| 157 |
+
span = tracer.start_span(
|
| 158 |
+
name=f"{request.method} {request.path}",
|
| 159 |
+
context=ctx,
|
| 160 |
+
)
|
| 161 |
+
span.set_attribute("http.method", request.method)
|
| 162 |
+
span.set_attribute("http.url", request.url)
|
| 163 |
+
span.set_attribute("request_id", g.request_id)
|
| 164 |
+
g.span = span
|
| 165 |
+
|
| 166 |
+
@app.after_request
|
| 167 |
+
def after_request(response):
|
| 168 |
+
"""Log request completion with duration and finish span."""
|
| 169 |
+
if hasattr(g, "start_time"):
|
| 170 |
+
duration_ms = (time.time() - g.start_time) * 1000
|
| 171 |
+
|
| 172 |
+
# Get user_id from session if available
|
| 173 |
+
user_id = None
|
| 174 |
+
try:
|
| 175 |
+
from flask import session
|
| 176 |
+
|
| 177 |
+
user_id = session.get("user_id")
|
| 178 |
+
except Exception:
|
| 179 |
+
pass
|
| 180 |
+
|
| 181 |
+
# Log request with structured data
|
| 182 |
+
extra = {
|
| 183 |
+
"request_id": g.request_id,
|
| 184 |
+
"duration_ms": round(duration_ms, 2),
|
| 185 |
+
"status_code": response.status_code,
|
| 186 |
+
"method": request.method,
|
| 187 |
+
"path": request.path,
|
| 188 |
+
}
|
| 189 |
+
if user_id:
|
| 190 |
+
extra["user_id"] = user_id
|
| 191 |
+
if hasattr(g, "backend_latency_ms"):
|
| 192 |
+
extra["backend_latency_ms"] = round(g.backend_latency_ms, 2)
|
| 193 |
+
|
| 194 |
+
app.logger.info(
|
| 195 |
+
f"{request.method} {request.path} {response.status_code}",
|
| 196 |
+
extra=extra,
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# Finish tracing span
|
| 200 |
+
if hasattr(g, "span"):
|
| 201 |
+
span = g.span
|
| 202 |
+
span.set_attribute("http.status_code", response.status_code)
|
| 203 |
+
if response.status_code >= 400:
|
| 204 |
+
span.set_status(Status(StatusCode.ERROR))
|
| 205 |
+
if hasattr(g, "backend_latency_ms"):
|
| 206 |
+
span.set_attribute("backend_latency_ms", round(g.backend_latency_ms, 2))
|
| 207 |
+
span.end()
|
| 208 |
+
|
| 209 |
+
return response
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# Create app instance
|
| 213 |
+
app = create_app()
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "5000")), debug=True)
|
src/lib/components/ChatMessage.svelte
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Message } from '$lib/types/api';
|
| 3 |
+
import { formatDateTime, formatTime } from '$lib/utils/formatters';
|
| 4 |
+
|
| 5 |
+
export let message: Message;
|
| 6 |
+
export let isUser = false; // true for user messages, false for assistant messages
|
| 7 |
+
|
| 8 |
+
function getModeColor(mode: string): string {
|
| 9 |
+
switch (mode) {
|
| 10 |
+
case 'chat':
|
| 11 |
+
return 'primary';
|
| 12 |
+
case 'memorize':
|
| 13 |
+
return 'success';
|
| 14 |
+
case 'parse':
|
| 15 |
+
return 'info';
|
| 16 |
+
default:
|
| 17 |
+
return 'secondary';
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function getModeIcon(mode: string): string {
|
| 22 |
+
switch (mode) {
|
| 23 |
+
case 'chat':
|
| 24 |
+
return 'bi-chat-left-text';
|
| 25 |
+
case 'memorize':
|
| 26 |
+
return 'bi-journal-bookmark';
|
| 27 |
+
case 'parse':
|
| 28 |
+
return 'bi-code-slash';
|
| 29 |
+
default:
|
| 30 |
+
return 'bi-chat-dots';
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
</script>
|
| 34 |
+
|
| 35 |
+
<div class="chat-message mb-3" class:user-message={isUser} class:assistant-message={!isUser}>
|
| 36 |
+
<div class="message-container" class:ms-auto={isUser}>
|
| 37 |
+
<div class="message-header d-flex justify-content-between align-items-center mb-1">
|
| 38 |
+
<span class="badge bg-{getModeColor(message.mode)}">
|
| 39 |
+
<i class="{getModeIcon(message.mode)} me-1"></i>
|
| 40 |
+
{message.mode}
|
| 41 |
+
</span>
|
| 42 |
+
<small class="text-muted ms-2" title={formatDateTime(message.created_at)}>
|
| 43 |
+
{formatTime(message.created_at)}
|
| 44 |
+
</small>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="message-content">
|
| 47 |
+
{message.content}
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<style>
|
| 53 |
+
.chat-message {
|
| 54 |
+
display: flex;
|
| 55 |
+
width: 100%;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.message-container {
|
| 59 |
+
max-width: 70%;
|
| 60 |
+
padding: 0.75rem 1rem;
|
| 61 |
+
border-radius: 1rem;
|
| 62 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* User messages - right-aligned, blue background */
|
| 66 |
+
.user-message .message-container {
|
| 67 |
+
background-color: #0d6efd;
|
| 68 |
+
color: white;
|
| 69 |
+
border-bottom-right-radius: 0.25rem;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.user-message .message-header .badge {
|
| 73 |
+
background-color: rgba(255, 255, 255, 0.2) !important;
|
| 74 |
+
color: white;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.user-message .message-header .text-muted {
|
| 78 |
+
color: rgba(255, 255, 255, 0.8) !important;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Assistant messages - left-aligned, gray background */
|
| 82 |
+
.assistant-message {
|
| 83 |
+
justify-content: flex-start;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.assistant-message .message-container {
|
| 87 |
+
background-color: #f8f9fa;
|
| 88 |
+
color: #212529;
|
| 89 |
+
border-bottom-left-radius: 0.25rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.message-content {
|
| 93 |
+
white-space: pre-wrap;
|
| 94 |
+
word-break: break-word;
|
| 95 |
+
line-height: 1.5;
|
| 96 |
+
font-size: 0.95rem;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.message-header {
|
| 100 |
+
font-size: 0.875rem;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* Responsive breakpoints */
|
| 104 |
+
@media (max-width: 576px) {
|
| 105 |
+
.message-container {
|
| 106 |
+
max-width: 90%;
|
| 107 |
+
padding: 0.625rem 0.875rem;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.message-content {
|
| 111 |
+
font-size: 0.9rem;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.message-header {
|
| 115 |
+
font-size: 0.8125rem;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.badge {
|
| 119 |
+
font-size: 0.75rem;
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
@media (min-width: 577px) and (max-width: 768px) {
|
| 124 |
+
.message-container {
|
| 125 |
+
max-width: 85%;
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@media (min-width: 769px) and (max-width: 992px) {
|
| 130 |
+
.message-container {
|
| 131 |
+
max-width: 75%;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
@media (min-width: 1200px) {
|
| 136 |
+
.message-container {
|
| 137 |
+
max-width: 65%;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
</style>
|
src/lib/components/ErrorAlert.svelte
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
// Error alert component with Bootstrap alert styling
|
| 3 |
+
export let message: string;
|
| 4 |
+
export let dismissible: boolean = true;
|
| 5 |
+
export let onDismiss: (() => void) | null = null;
|
| 6 |
+
|
| 7 |
+
function handleDismiss() {
|
| 8 |
+
if (onDismiss) {
|
| 9 |
+
onDismiss();
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
</script>
|
| 13 |
+
|
| 14 |
+
<div class="alert alert-danger {dismissible ? 'alert-dismissible' : ''} fade show" role="alert">
|
| 15 |
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
| 16 |
+
{message}
|
| 17 |
+
{#if dismissible}
|
| 18 |
+
<button
|
| 19 |
+
type="button"
|
| 20 |
+
class="btn-close"
|
| 21 |
+
data-bs-dismiss="alert"
|
| 22 |
+
aria-label="Close"
|
| 23 |
+
on:click={handleDismiss}
|
| 24 |
+
></button>
|
| 25 |
+
{/if}
|
| 26 |
+
</div>
|
src/lib/components/LoadingSpinner.svelte
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
// Loading spinner component with Bootstrap styling
|
| 3 |
+
</script>
|
| 4 |
+
|
| 5 |
+
<div class="d-flex justify-content-center align-items-center p-4">
|
| 6 |
+
<div class="spinner-border text-primary" role="status">
|
| 7 |
+
<span class="visually-hidden">Loading...</span>
|
| 8 |
+
</div>
|
| 9 |
+
</div>
|
| 10 |
+
|
| 11 |
+
<style>
|
| 12 |
+
.spinner-border {
|
| 13 |
+
width: 3rem;
|
| 14 |
+
height: 3rem;
|
| 15 |
+
}
|
| 16 |
+
</style>
|
src/lib/components/LoginForm.svelte
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { initiateOAuthLogin } from '$lib/services/auth';
|
| 3 |
+
import { ENABLE_MOCK_AUTH } from '$lib/utils/constants';
|
| 4 |
+
|
| 5 |
+
let loading = false;
|
| 6 |
+
let error: string | null = null;
|
| 7 |
+
|
| 8 |
+
async function handleLogin() {
|
| 9 |
+
loading = true;
|
| 10 |
+
error = null;
|
| 11 |
+
|
| 12 |
+
try {
|
| 13 |
+
await initiateOAuthLogin();
|
| 14 |
+
// OAuth will redirect, mock auth will complete
|
| 15 |
+
} catch (err) {
|
| 16 |
+
error = err instanceof Error ? err.message : 'Login failed';
|
| 17 |
+
console.error('Login error:', err);
|
| 18 |
+
} finally {
|
| 19 |
+
loading = false;
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
</script>
|
| 23 |
+
|
| 24 |
+
<div class="login-form card shadow-sm">
|
| 25 |
+
<div class="card-body p-3 p-md-4">
|
| 26 |
+
<div class="text-center mb-3 mb-md-4">
|
| 27 |
+
<i class="bi bi-chat-dots-fill text-primary login-icon"></i>
|
| 28 |
+
<h2 class="mt-3 fs-4 fs-md-3">Welcome to PrepMate</h2>
|
| 29 |
+
<p class="text-muted mb-0 fs-6">Sign in to manage your chat sessions</p>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
{#if error}
|
| 33 |
+
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
| 34 |
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
| 35 |
+
{error}
|
| 36 |
+
<button
|
| 37 |
+
type="button"
|
| 38 |
+
class="btn-close"
|
| 39 |
+
aria-label="Close"
|
| 40 |
+
on:click={() => (error = null)}
|
| 41 |
+
></button>
|
| 42 |
+
</div>
|
| 43 |
+
{/if}
|
| 44 |
+
|
| 45 |
+
<div class="d-grid gap-3">
|
| 46 |
+
<button
|
| 47 |
+
type="button"
|
| 48 |
+
class="btn btn-primary btn-lg"
|
| 49 |
+
on:click={handleLogin}
|
| 50 |
+
disabled={loading}
|
| 51 |
+
>
|
| 52 |
+
{#if loading}
|
| 53 |
+
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"
|
| 54 |
+
></span>
|
| 55 |
+
{:else}
|
| 56 |
+
<i class="bi bi-box-arrow-in-right me-2"></i>
|
| 57 |
+
{/if}
|
| 58 |
+
{ENABLE_MOCK_AUTH ? 'Login (Mock)' : 'Login with HuggingFace'}
|
| 59 |
+
</button>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
{#if ENABLE_MOCK_AUTH}
|
| 63 |
+
<div class="alert alert-warning mt-3 mb-0" role="alert">
|
| 64 |
+
<i class="bi bi-info-circle-fill me-2"></i>
|
| 65 |
+
<strong>Development Mode:</strong> Using mock authentication
|
| 66 |
+
</div>
|
| 67 |
+
{/if}
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<style>
|
| 72 |
+
.login-form {
|
| 73 |
+
max-width: 100%;
|
| 74 |
+
margin: 0 auto;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.login-icon {
|
| 78 |
+
font-size: 2.5rem;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Mobile optimizations */
|
| 82 |
+
@media (max-width: 576px) {
|
| 83 |
+
.login-icon {
|
| 84 |
+
font-size: 2rem;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Larger screens */
|
| 89 |
+
@media (min-width: 768px) {
|
| 90 |
+
.login-icon {
|
| 91 |
+
font-size: 3rem;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
</style>
|
src/lib/components/MessageInput.svelte
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { createEventDispatcher } from 'svelte';
|
| 3 |
+
import type { MessageMode } from '$lib/types/enums';
|
| 4 |
+
import ModeSelector from './ModeSelector.svelte';
|
| 5 |
+
import { validateMessageContent } from '$lib/utils/validators';
|
| 6 |
+
|
| 7 |
+
export let disabled = false;
|
| 8 |
+
export let loading = false;
|
| 9 |
+
|
| 10 |
+
const dispatch = createEventDispatcher<{
|
| 11 |
+
send: { content: string; mode: MessageMode };
|
| 12 |
+
}>();
|
| 13 |
+
|
| 14 |
+
let content = '';
|
| 15 |
+
let selectedMode: MessageMode = 'chat';
|
| 16 |
+
let error: string | null = null;
|
| 17 |
+
|
| 18 |
+
function handleKeyDown(event: KeyboardEvent) {
|
| 19 |
+
// Cmd/Ctrl + Enter to send
|
| 20 |
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
| 21 |
+
event.preventDefault();
|
| 22 |
+
handleSubmit();
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function handleSubmit() {
|
| 27 |
+
error = null;
|
| 28 |
+
|
| 29 |
+
// Validate content
|
| 30 |
+
const validationError = validateMessageContent(content);
|
| 31 |
+
if (validationError) {
|
| 32 |
+
error = validationError;
|
| 33 |
+
return;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Dispatch send event
|
| 37 |
+
dispatch('send', {
|
| 38 |
+
content: content.trim(),
|
| 39 |
+
mode: selectedMode
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// Clear input
|
| 43 |
+
content = '';
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
$: canSend = content.trim().length > 0 && !loading && !disabled;
|
| 47 |
+
</script>
|
| 48 |
+
|
| 49 |
+
<div class="message-input border-top bg-white p-2 p-md-3">
|
| 50 |
+
{#if error}
|
| 51 |
+
<div class="alert alert-danger alert-sm mb-2" role="alert">
|
| 52 |
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
| 53 |
+
{error}
|
| 54 |
+
</div>
|
| 55 |
+
{/if}
|
| 56 |
+
|
| 57 |
+
<div class="input-container">
|
| 58 |
+
<!-- Mode Selector -->
|
| 59 |
+
<div class="mb-2">
|
| 60 |
+
<ModeSelector bind:selectedMode disabled={loading || disabled} />
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<!-- Message Input -->
|
| 64 |
+
<div class="d-flex gap-2">
|
| 65 |
+
<textarea
|
| 66 |
+
class="form-control"
|
| 67 |
+
placeholder="Type your message..."
|
| 68 |
+
bind:value={content}
|
| 69 |
+
on:keydown={handleKeyDown}
|
| 70 |
+
{disabled}
|
| 71 |
+
rows="2"
|
| 72 |
+
maxlength="10000"
|
| 73 |
+
></textarea>
|
| 74 |
+
|
| 75 |
+
<button
|
| 76 |
+
type="button"
|
| 77 |
+
class="btn btn-primary send-btn"
|
| 78 |
+
on:click={handleSubmit}
|
| 79 |
+
disabled={!canSend}
|
| 80 |
+
title="Send message"
|
| 81 |
+
>
|
| 82 |
+
{#if loading}
|
| 83 |
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 84 |
+
{:else}
|
| 85 |
+
<i class="bi bi-send-fill"></i>
|
| 86 |
+
{/if}
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<!-- Character Counter -->
|
| 91 |
+
<div class="d-flex justify-content-between align-items-center mt-2">
|
| 92 |
+
<small class="text-muted d-none d-sm-block">
|
| 93 |
+
<i class="bi bi-info-circle me-1"></i>
|
| 94 |
+
<span class="d-none d-md-inline">Press Cmd/Ctrl+Enter to send</span>
|
| 95 |
+
<span class="d-inline d-md-none">Cmd/Ctrl+Enter</span>
|
| 96 |
+
</small>
|
| 97 |
+
<small class="text-muted ms-auto">
|
| 98 |
+
{content.length} / 10k
|
| 99 |
+
</small>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<style>
|
| 105 |
+
.message-input {
|
| 106 |
+
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.input-container {
|
| 110 |
+
max-width: 100%;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
textarea {
|
| 114 |
+
resize: vertical;
|
| 115 |
+
min-height: 60px;
|
| 116 |
+
max-height: 200px;
|
| 117 |
+
font-size: 1rem;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.send-btn {
|
| 121 |
+
min-width: 48px;
|
| 122 |
+
height: auto;
|
| 123 |
+
align-self: stretch;
|
| 124 |
+
display: flex;
|
| 125 |
+
align-items: center;
|
| 126 |
+
justify-content: center;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.alert-sm {
|
| 130 |
+
padding: 0.5rem 0.75rem;
|
| 131 |
+
font-size: 0.875rem;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* Mobile optimizations */
|
| 135 |
+
@media (max-width: 576px) {
|
| 136 |
+
textarea {
|
| 137 |
+
min-height: 50px;
|
| 138 |
+
font-size: 16px; /* Prevents zoom on iOS */
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.send-btn {
|
| 142 |
+
min-width: 44px;
|
| 143 |
+
padding: 0.5rem;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.message-input {
|
| 147 |
+
padding: 0.5rem !important;
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/* Tablet and up */
|
| 152 |
+
@media (min-width: 768px) {
|
| 153 |
+
textarea {
|
| 154 |
+
min-height: 80px;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
</style>
|
src/lib/components/ModeSelector.svelte
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { MessageMode } from '$lib/types/enums';
|
| 3 |
+
|
| 4 |
+
export let selectedMode: MessageMode = 'chat';
|
| 5 |
+
export let disabled = false;
|
| 6 |
+
|
| 7 |
+
const modes: { value: MessageMode; label: string; icon: string; color: string }[] = [
|
| 8 |
+
{ value: 'chat', label: 'Chat', icon: 'bi-chat-left-text', color: 'primary' },
|
| 9 |
+
{ value: 'memorize', label: 'Memorize', icon: 'bi-journal-bookmark', color: 'success' },
|
| 10 |
+
{ value: 'parse', label: 'Parse', icon: 'bi-code-slash', color: 'info' }
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
function selectMode(mode: MessageMode) {
|
| 14 |
+
if (!disabled) {
|
| 15 |
+
selectedMode = mode;
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
</script>
|
| 19 |
+
|
| 20 |
+
<div class="mode-selector">
|
| 21 |
+
<div class="btn-group" role="group" aria-label="Message mode selector">
|
| 22 |
+
{#each modes as mode}
|
| 23 |
+
<button
|
| 24 |
+
type="button"
|
| 25 |
+
class="btn btn-outline-{mode.color}"
|
| 26 |
+
class:active={selectedMode === mode.value}
|
| 27 |
+
on:click={() => selectMode(mode.value)}
|
| 28 |
+
{disabled}
|
| 29 |
+
title="{mode.label} mode"
|
| 30 |
+
>
|
| 31 |
+
<i class="{mode.icon} me-1"></i>
|
| 32 |
+
<span class="mode-label">{mode.label}</span>
|
| 33 |
+
</button>
|
| 34 |
+
{/each}
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<style>
|
| 39 |
+
.mode-selector .btn-group {
|
| 40 |
+
width: 100%;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.mode-selector .btn {
|
| 44 |
+
flex: 1;
|
| 45 |
+
font-size: 0.875rem;
|
| 46 |
+
padding: 0.5rem 0.75rem;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.mode-selector .btn.active {
|
| 50 |
+
font-weight: 600;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Mobile optimizations */
|
| 54 |
+
@media (max-width: 576px) {
|
| 55 |
+
.mode-selector .btn {
|
| 56 |
+
font-size: 0.8125rem;
|
| 57 |
+
padding: 0.4rem 0.5rem;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.mode-label {
|
| 61 |
+
display: inline;
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* Very small screens - icon only */
|
| 66 |
+
@media (max-width: 380px) {
|
| 67 |
+
.mode-label {
|
| 68 |
+
display: none;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.mode-selector .btn {
|
| 72 |
+
padding: 0.5rem;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.mode-selector .btn i {
|
| 76 |
+
margin: 0 !important;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/* Larger screens */
|
| 81 |
+
@media (min-width: 768px) {
|
| 82 |
+
.mode-selector .btn {
|
| 83 |
+
font-size: 0.9375rem;
|
| 84 |
+
padding: 0.5rem 1rem;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
</style>
|
src/lib/components/SessionHeader.svelte
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { sessionStore, activeSession } from '$lib/stores/session';
|
| 3 |
+
import { formatDateTime } from '$lib/utils/formatters';
|
| 4 |
+
import type { Session } from '$lib/types/api';
|
| 5 |
+
|
| 6 |
+
export let session: Session;
|
| 7 |
+
|
| 8 |
+
let updatingReference = false;
|
| 9 |
+
|
| 10 |
+
async function toggleReference() {
|
| 11 |
+
updatingReference = true;
|
| 12 |
+
await sessionStore.updateSessionReference(session.id, !session.is_reference);
|
| 13 |
+
updatingReference = false;
|
| 14 |
+
}
|
| 15 |
+
</script>
|
| 16 |
+
|
| 17 |
+
<div class="session-header border-bottom bg-white p-2 p-md-3 shadow-sm">
|
| 18 |
+
<div class="d-flex justify-content-between align-items-start">
|
| 19 |
+
<div class="flex-grow-1 min-w-0 me-2">
|
| 20 |
+
<div class="d-flex align-items-center mb-1 mb-md-2">
|
| 21 |
+
<h4 class="mb-0 me-2 text-truncate session-title">{session.title}</h4>
|
| 22 |
+
{#if session.is_reference}
|
| 23 |
+
<span class="badge bg-info flex-shrink-0">
|
| 24 |
+
<i class="bi bi-star-fill me-1"></i>
|
| 25 |
+
<span class="d-none d-md-inline">Reference</span>
|
| 26 |
+
</span>
|
| 27 |
+
{/if}
|
| 28 |
+
</div>
|
| 29 |
+
<div class="text-muted small session-meta">
|
| 30 |
+
<span class="d-inline-block">
|
| 31 |
+
<i class="bi bi-calendar3 me-1"></i>
|
| 32 |
+
<span class="d-none d-md-inline">Created </span>{formatDateTime(session.created_at)}
|
| 33 |
+
</span>
|
| 34 |
+
<span class="ms-2 ms-md-3 d-inline-block">
|
| 35 |
+
<i class="bi bi-chat-dots me-1"></i>
|
| 36 |
+
{session.messages.length} <span class="d-none d-sm-inline">messages</span>
|
| 37 |
+
</span>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="btn-group flex-shrink-0" role="group">
|
| 42 |
+
<button
|
| 43 |
+
type="button"
|
| 44 |
+
class="btn btn-outline-secondary btn-sm"
|
| 45 |
+
class:active={session.is_reference}
|
| 46 |
+
on:click={toggleReference}
|
| 47 |
+
disabled={updatingReference}
|
| 48 |
+
title={session.is_reference ? 'Remove from reference' : 'Mark as reference'}
|
| 49 |
+
>
|
| 50 |
+
{#if updatingReference}
|
| 51 |
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 52 |
+
{:else}
|
| 53 |
+
<i class="bi bi-star{session.is_reference ? '-fill' : ''}"></i>
|
| 54 |
+
{/if}
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<style>
|
| 61 |
+
.session-header {
|
| 62 |
+
position: sticky;
|
| 63 |
+
top: 0;
|
| 64 |
+
z-index: 10;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.min-w-0 {
|
| 68 |
+
min-width: 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.session-title {
|
| 72 |
+
font-size: 1.25rem;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.session-meta {
|
| 76 |
+
font-size: 0.875rem;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.btn-group .btn.active {
|
| 80 |
+
background-color: #0dcaf0;
|
| 81 |
+
border-color: #0dcaf0;
|
| 82 |
+
color: white;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* Mobile optimizations */
|
| 86 |
+
@media (max-width: 576px) {
|
| 87 |
+
.session-title {
|
| 88 |
+
font-size: 1rem;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.session-meta {
|
| 92 |
+
font-size: 0.8125rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.badge {
|
| 96 |
+
font-size: 0.7rem;
|
| 97 |
+
padding: 0.2rem 0.35rem;
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* Tablet and up */
|
| 102 |
+
@media (min-width: 768px) {
|
| 103 |
+
.session-title {
|
| 104 |
+
font-size: 1.5rem;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
</style>
|
src/lib/components/SessionList.svelte
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { sessionStore, activeSessions, sessionCount } from '$lib/stores/session';
|
| 3 |
+
import { formatRelativeTime } from '$lib/utils/formatters';
|
| 4 |
+
import { validateSessionTitle } from '$lib/utils/validators';
|
| 5 |
+
import { SESSION_LIMIT, SESSION_LIMIT_WARNING } from '$lib/utils/constants';
|
| 6 |
+
import { goto } from '$app/navigation';
|
| 7 |
+
|
| 8 |
+
// Modal state
|
| 9 |
+
let showCreateModal = false;
|
| 10 |
+
let showDeleteModal = false;
|
| 11 |
+
let deleteSessionId: string | null = null;
|
| 12 |
+
let deleteSessionTitle = '';
|
| 13 |
+
|
| 14 |
+
// Form state
|
| 15 |
+
let newSessionTitle = '';
|
| 16 |
+
let titleError = '';
|
| 17 |
+
let creating = false;
|
| 18 |
+
|
| 19 |
+
function openCreateModal() {
|
| 20 |
+
newSessionTitle = '';
|
| 21 |
+
titleError = '';
|
| 22 |
+
showCreateModal = true;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function closeCreateModal() {
|
| 26 |
+
showCreateModal = false;
|
| 27 |
+
newSessionTitle = '';
|
| 28 |
+
titleError = '';
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
async function handleCreateSession() {
|
| 32 |
+
// Validate title
|
| 33 |
+
const validationError = validateSessionTitle(newSessionTitle);
|
| 34 |
+
if (validationError) {
|
| 35 |
+
titleError = validationError;
|
| 36 |
+
return;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
creating = true;
|
| 40 |
+
const sessionId = await sessionStore.createSession(newSessionTitle);
|
| 41 |
+
creating = false;
|
| 42 |
+
|
| 43 |
+
if (sessionId) {
|
| 44 |
+
closeCreateModal();
|
| 45 |
+
// Navigate to new session
|
| 46 |
+
goto(`/session/${sessionId}`);
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function openDeleteModal(sessionId: string, title: string) {
|
| 51 |
+
deleteSessionId = sessionId;
|
| 52 |
+
deleteSessionTitle = title;
|
| 53 |
+
showDeleteModal = true;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function closeDeleteModal() {
|
| 57 |
+
showDeleteModal = false;
|
| 58 |
+
deleteSessionId = null;
|
| 59 |
+
deleteSessionTitle = '';
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async function handleDeleteSession() {
|
| 63 |
+
if (!deleteSessionId) return;
|
| 64 |
+
|
| 65 |
+
const success = await sessionStore.deleteSession(deleteSessionId);
|
| 66 |
+
if (success) {
|
| 67 |
+
closeDeleteModal();
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function handleSessionClick(sessionId: string) {
|
| 72 |
+
goto(`/session/${sessionId}`);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Check if at limit
|
| 76 |
+
$: atLimit = $sessionCount >= SESSION_LIMIT;
|
| 77 |
+
$: nearLimit = $sessionCount >= SESSION_LIMIT_WARNING;
|
| 78 |
+
</script>
|
| 79 |
+
|
| 80 |
+
<div class="session-list h-100 d-flex flex-column">
|
| 81 |
+
<!-- Header -->
|
| 82 |
+
<div class="session-list-header p-2 p-md-3 border-bottom bg-light">
|
| 83 |
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
| 84 |
+
<h5 class="mb-0 fs-6 fs-md-5">
|
| 85 |
+
<i class="bi bi-chat-left-dots me-2"></i>
|
| 86 |
+
<span class="d-none d-sm-inline">Sessions</span>
|
| 87 |
+
</h5>
|
| 88 |
+
<button
|
| 89 |
+
class="btn btn-primary btn-sm"
|
| 90 |
+
on:click={openCreateModal}
|
| 91 |
+
disabled={atLimit}
|
| 92 |
+
title={atLimit ? 'Session limit reached' : 'Create new session'}
|
| 93 |
+
>
|
| 94 |
+
<i class="bi bi-plus-lg"></i>
|
| 95 |
+
<span class="d-none d-lg-inline ms-1">New</span>
|
| 96 |
+
</button>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
{#if nearLimit}
|
| 100 |
+
<div class="alert alert-{atLimit ? 'danger' : 'warning'} alert-sm mb-0 py-1 px-2" role="alert">
|
| 101 |
+
<small>
|
| 102 |
+
{#if atLimit}
|
| 103 |
+
<i class="bi bi-exclamation-triangle-fill me-1"></i>
|
| 104 |
+
Session limit reached ({$sessionCount}/{SESSION_LIMIT})
|
| 105 |
+
{:else}
|
| 106 |
+
<i class="bi bi-info-circle-fill me-1"></i>
|
| 107 |
+
{$sessionCount}/{SESSION_LIMIT} sessions
|
| 108 |
+
{/if}
|
| 109 |
+
</small>
|
| 110 |
+
</div>
|
| 111 |
+
{/if}
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<!-- Session List -->
|
| 115 |
+
<div class="session-list-body flex-grow-1 overflow-auto">
|
| 116 |
+
{#if $activeSessions.length === 0}
|
| 117 |
+
<div class="text-center text-muted p-4">
|
| 118 |
+
<i class="bi bi-chat-dots" style="font-size: 3rem; opacity: 0.3;"></i>
|
| 119 |
+
<p class="mt-2 mb-0">No sessions yet</p>
|
| 120 |
+
<small>Create a session to get started</small>
|
| 121 |
+
</div>
|
| 122 |
+
{:else}
|
| 123 |
+
<div class="list-group list-group-flush">
|
| 124 |
+
{#each $activeSessions as session (session.id)}
|
| 125 |
+
<button
|
| 126 |
+
type="button"
|
| 127 |
+
class="list-group-item list-group-item-action px-2 px-md-3 py-2"
|
| 128 |
+
class:active={session.is_active}
|
| 129 |
+
on:click={() => handleSessionClick(session.id)}
|
| 130 |
+
>
|
| 131 |
+
<div class="d-flex w-100 justify-content-between align-items-start">
|
| 132 |
+
<div class="flex-grow-1 text-start me-2 min-w-0">
|
| 133 |
+
<div class="d-flex align-items-center mb-1">
|
| 134 |
+
<h6 class="mb-0 text-truncate session-title">
|
| 135 |
+
{session.title}
|
| 136 |
+
</h6>
|
| 137 |
+
{#if session.is_reference}
|
| 138 |
+
<span class="badge bg-info ms-2 flex-shrink-0" title="Reference session">
|
| 139 |
+
<i class="bi bi-star-fill"></i>
|
| 140 |
+
</span>
|
| 141 |
+
{/if}
|
| 142 |
+
</div>
|
| 143 |
+
<small class="text-muted d-block text-truncate">
|
| 144 |
+
<span class="d-none d-md-inline">{formatRelativeTime(session.last_interaction)} · </span>
|
| 145 |
+
{session.message_count} msg
|
| 146 |
+
</small>
|
| 147 |
+
</div>
|
| 148 |
+
<button
|
| 149 |
+
type="button"
|
| 150 |
+
class="btn btn-sm btn-outline-danger flex-shrink-0"
|
| 151 |
+
on:click|stopPropagation={() => openDeleteModal(session.id, session.title)}
|
| 152 |
+
title="Delete session"
|
| 153 |
+
>
|
| 154 |
+
<i class="bi bi-trash"></i>
|
| 155 |
+
</button>
|
| 156 |
+
</div>
|
| 157 |
+
</button>
|
| 158 |
+
{/each}
|
| 159 |
+
</div>
|
| 160 |
+
{/if}
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<!-- Create Session Modal -->
|
| 165 |
+
{#if showCreateModal}
|
| 166 |
+
<div class="modal show d-block" tabindex="-1" role="dialog">
|
| 167 |
+
<div class="modal-dialog modal-dialog-centered" role="document">
|
| 168 |
+
<div class="modal-content">
|
| 169 |
+
<div class="modal-header">
|
| 170 |
+
<h5 class="modal-title">Create New Session</h5>
|
| 171 |
+
<button
|
| 172 |
+
type="button"
|
| 173 |
+
class="btn-close"
|
| 174 |
+
aria-label="Close"
|
| 175 |
+
on:click={closeCreateModal}
|
| 176 |
+
></button>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="modal-body">
|
| 179 |
+
<form on:submit|preventDefault={handleCreateSession}>
|
| 180 |
+
<div class="mb-3">
|
| 181 |
+
<label for="sessionTitle" class="form-label">Session Title</label>
|
| 182 |
+
<input
|
| 183 |
+
type="text"
|
| 184 |
+
class="form-control"
|
| 185 |
+
class:is-invalid={titleError}
|
| 186 |
+
id="sessionTitle"
|
| 187 |
+
bind:value={newSessionTitle}
|
| 188 |
+
placeholder="Enter session title..."
|
| 189 |
+
maxlength="200"
|
| 190 |
+
required
|
| 191 |
+
/>
|
| 192 |
+
{#if titleError}
|
| 193 |
+
<div class="invalid-feedback">{titleError}</div>
|
| 194 |
+
{/if}
|
| 195 |
+
<small class="form-text text-muted">Max 200 characters</small>
|
| 196 |
+
</div>
|
| 197 |
+
</form>
|
| 198 |
+
</div>
|
| 199 |
+
<div class="modal-footer">
|
| 200 |
+
<button type="button" class="btn btn-secondary" on:click={closeCreateModal}>
|
| 201 |
+
Cancel
|
| 202 |
+
</button>
|
| 203 |
+
<button
|
| 204 |
+
type="button"
|
| 205 |
+
class="btn btn-primary"
|
| 206 |
+
on:click={handleCreateSession}
|
| 207 |
+
disabled={creating || !newSessionTitle.trim()}
|
| 208 |
+
>
|
| 209 |
+
{#if creating}
|
| 210 |
+
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
| 211 |
+
{/if}
|
| 212 |
+
Create
|
| 213 |
+
</button>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="modal-backdrop show"></div>
|
| 219 |
+
{/if}
|
| 220 |
+
|
| 221 |
+
<!-- Delete Confirmation Modal -->
|
| 222 |
+
{#if showDeleteModal}
|
| 223 |
+
<div class="modal show d-block" tabindex="-1" role="dialog">
|
| 224 |
+
<div class="modal-dialog modal-dialog-centered" role="document">
|
| 225 |
+
<div class="modal-content">
|
| 226 |
+
<div class="modal-header">
|
| 227 |
+
<h5 class="modal-title">Delete Session</h5>
|
| 228 |
+
<button
|
| 229 |
+
type="button"
|
| 230 |
+
class="btn-close"
|
| 231 |
+
aria-label="Close"
|
| 232 |
+
on:click={closeDeleteModal}
|
| 233 |
+
></button>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="modal-body">
|
| 236 |
+
<p>Are you sure you want to delete this session?</p>
|
| 237 |
+
<p class="text-muted mb-0">
|
| 238 |
+
<strong>{deleteSessionTitle}</strong>
|
| 239 |
+
</p>
|
| 240 |
+
<p class="text-danger mt-2 mb-0">
|
| 241 |
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
| 242 |
+
This action cannot be undone.
|
| 243 |
+
</p>
|
| 244 |
+
</div>
|
| 245 |
+
<div class="modal-footer">
|
| 246 |
+
<button type="button" class="btn btn-secondary" on:click={closeDeleteModal}>
|
| 247 |
+
Cancel
|
| 248 |
+
</button>
|
| 249 |
+
<button type="button" class="btn btn-danger" on:click={handleDeleteSession}>
|
| 250 |
+
<i class="bi bi-trash me-2"></i>
|
| 251 |
+
Delete
|
| 252 |
+
</button>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
<div class="modal-backdrop show"></div>
|
| 258 |
+
{/if}
|
| 259 |
+
|
| 260 |
+
<style>
|
| 261 |
+
.session-list {
|
| 262 |
+
background-color: #f8f9fa;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.session-list-body {
|
| 266 |
+
position: relative;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.list-group-item {
|
| 270 |
+
cursor: pointer;
|
| 271 |
+
border-left: 3px solid transparent;
|
| 272 |
+
transition: all 0.2s ease;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.list-group-item:hover {
|
| 276 |
+
border-left-color: #0d6efd;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.list-group-item.active {
|
| 280 |
+
border-left-color: #0d6efd;
|
| 281 |
+
background-color: #e7f1ff;
|
| 282 |
+
color: #000;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.session-title {
|
| 286 |
+
max-width: 100%;
|
| 287 |
+
font-size: 0.95rem;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.min-w-0 {
|
| 291 |
+
min-width: 0;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.modal {
|
| 295 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.alert-sm {
|
| 299 |
+
font-size: 0.875rem;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* Mobile optimizations */
|
| 303 |
+
@media (max-width: 576px) {
|
| 304 |
+
.session-title {
|
| 305 |
+
font-size: 0.875rem;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.list-group-item {
|
| 309 |
+
border-left-width: 2px;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.badge {
|
| 313 |
+
font-size: 0.7rem;
|
| 314 |
+
padding: 0.2rem 0.35rem;
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* Larger screens */
|
| 319 |
+
@media (min-width: 1200px) {
|
| 320 |
+
.session-title {
|
| 321 |
+
font-size: 1rem;
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
</style>
|
src/lib/services/api.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Base API client with fetch wrapper
|
| 2 |
+
import {
|
| 3 |
+
API_URL,
|
| 4 |
+
API_TIMEOUT,
|
| 5 |
+
HEADER_AUTHORIZATION,
|
| 6 |
+
HEADER_USER_ID,
|
| 7 |
+
HEADER_CONTENT_TYPE,
|
| 8 |
+
MAX_RETRIES,
|
| 9 |
+
RETRY_DELAY
|
| 10 |
+
} from '../utils/constants';
|
| 11 |
+
import type {
|
| 12 |
+
Session,
|
| 13 |
+
SessionMetadata,
|
| 14 |
+
ListSessionsResponse,
|
| 15 |
+
CreateSessionRequest,
|
| 16 |
+
CreateSessionResponse,
|
| 17 |
+
SendMessageRequest,
|
| 18 |
+
SendMessageResponse,
|
| 19 |
+
UserProfile,
|
| 20 |
+
APIError,
|
| 21 |
+
ComparisonResult
|
| 22 |
+
} from '../types/api';
|
| 23 |
+
import type { ClientError } from '../types/client';
|
| 24 |
+
|
| 25 |
+
class APIClient {
|
| 26 |
+
private baseURL: string;
|
| 27 |
+
private token: string | null = null;
|
| 28 |
+
private userId: string | null = null;
|
| 29 |
+
|
| 30 |
+
constructor(baseURL: string = API_URL) {
|
| 31 |
+
this.baseURL = baseURL;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Set authentication token and user ID
|
| 36 |
+
*/
|
| 37 |
+
setAuth(token: string, userId: string): void {
|
| 38 |
+
this.token = token;
|
| 39 |
+
this.userId = userId;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Clear authentication
|
| 44 |
+
*/
|
| 45 |
+
clearAuth(): void {
|
| 46 |
+
this.token = null;
|
| 47 |
+
this.userId = null;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Make HTTP request with retry logic and timeout
|
| 52 |
+
*/
|
| 53 |
+
private async request<T>(
|
| 54 |
+
endpoint: string,
|
| 55 |
+
options: RequestInit = {},
|
| 56 |
+
retries: number = MAX_RETRIES
|
| 57 |
+
): Promise<T> {
|
| 58 |
+
const url = `${this.baseURL}${endpoint}`;
|
| 59 |
+
|
| 60 |
+
// Add authentication headers
|
| 61 |
+
const headers: Record<string, string> = {
|
| 62 |
+
[HEADER_CONTENT_TYPE]: 'application/json'
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
// Merge existing headers
|
| 66 |
+
if (options.headers) {
|
| 67 |
+
Object.entries(options.headers).forEach(([key, value]) => {
|
| 68 |
+
if (typeof value === 'string') {
|
| 69 |
+
headers[key] = value;
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if (this.token) {
|
| 75 |
+
headers[HEADER_AUTHORIZATION] = `Bearer ${this.token}`;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if (this.userId && !endpoint.includes('/user/profile')) {
|
| 79 |
+
headers[HEADER_USER_ID] = this.userId;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Create abort controller for timeout
|
| 83 |
+
const controller = new AbortController();
|
| 84 |
+
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT);
|
| 85 |
+
|
| 86 |
+
try {
|
| 87 |
+
const response = await fetch(url, {
|
| 88 |
+
...options,
|
| 89 |
+
headers,
|
| 90 |
+
signal: controller.signal
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
clearTimeout(timeoutId);
|
| 94 |
+
|
| 95 |
+
if (!response.ok) {
|
| 96 |
+
const error: APIError = await response.json();
|
| 97 |
+
throw this.createClientError(error, response.status, false);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Handle 204 No Content
|
| 101 |
+
if (response.status === 204) {
|
| 102 |
+
return {} as T;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
return await response.json();
|
| 106 |
+
} catch (error) {
|
| 107 |
+
clearTimeout(timeoutId);
|
| 108 |
+
|
| 109 |
+
// Handle abort/timeout
|
| 110 |
+
if (error instanceof Error && error.name === 'AbortError') {
|
| 111 |
+
if (retries > 0) {
|
| 112 |
+
await this.delay(RETRY_DELAY);
|
| 113 |
+
return this.request<T>(endpoint, options, retries - 1);
|
| 114 |
+
}
|
| 115 |
+
throw this.createClientError(
|
| 116 |
+
{ error: 'Request timeout', status: 408 },
|
| 117 |
+
408,
|
| 118 |
+
true
|
| 119 |
+
);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Handle network errors
|
| 123 |
+
if (error instanceof TypeError) {
|
| 124 |
+
if (retries > 0) {
|
| 125 |
+
await this.delay(RETRY_DELAY);
|
| 126 |
+
return this.request<T>(endpoint, options, retries - 1);
|
| 127 |
+
}
|
| 128 |
+
throw this.createClientError(
|
| 129 |
+
{ error: 'Network error', status: 0 },
|
| 130 |
+
0,
|
| 131 |
+
true
|
| 132 |
+
);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Re-throw client errors
|
| 136 |
+
if (this.isClientError(error)) {
|
| 137 |
+
throw error;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Unknown error
|
| 141 |
+
throw this.createClientError({ error: String(error), status: 500 }, 500, false);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Create typed client error
|
| 147 |
+
*/
|
| 148 |
+
private createClientError(apiError: APIError, status: number, retryable: boolean): ClientError {
|
| 149 |
+
let type: ClientError['type'] = 'unknown';
|
| 150 |
+
|
| 151 |
+
if (status === 401 || status === 403) {
|
| 152 |
+
type = 'auth';
|
| 153 |
+
} else if (status >= 400 && status < 500) {
|
| 154 |
+
type = 'validation';
|
| 155 |
+
} else if (status >= 500) {
|
| 156 |
+
type = 'server';
|
| 157 |
+
} else if (status === 0 || status === 408) {
|
| 158 |
+
type = 'network';
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return {
|
| 162 |
+
message: apiError.error,
|
| 163 |
+
type,
|
| 164 |
+
cause: apiError,
|
| 165 |
+
retryable
|
| 166 |
+
};
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Type guard for ClientError
|
| 171 |
+
*/
|
| 172 |
+
private isClientError(error: unknown): error is ClientError {
|
| 173 |
+
return (
|
| 174 |
+
typeof error === 'object' &&
|
| 175 |
+
error !== null &&
|
| 176 |
+
'message' in error &&
|
| 177 |
+
'type' in error &&
|
| 178 |
+
'retryable' in error
|
| 179 |
+
);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/**
|
| 183 |
+
* Delay utility for retry logic
|
| 184 |
+
*/
|
| 185 |
+
private delay(ms: number): Promise<void> {
|
| 186 |
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Session Management Methods
|
| 190 |
+
async createSession(title: string): Promise<CreateSessionResponse> {
|
| 191 |
+
const request: CreateSessionRequest = {
|
| 192 |
+
title,
|
| 193 |
+
user_id: this.userId || ''
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
return this.request<CreateSessionResponse>('/v1/sessions', {
|
| 197 |
+
method: 'POST',
|
| 198 |
+
body: JSON.stringify(request)
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
async listSessions(): Promise<SessionMetadata[]> {
|
| 203 |
+
const response = await this.request<ListSessionsResponse>('/v1/sessions', {
|
| 204 |
+
method: 'GET'
|
| 205 |
+
});
|
| 206 |
+
return response.sessions;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
async getSession(sessionId: string): Promise<Session> {
|
| 210 |
+
return this.request<Session>(`/v1/sessions/${sessionId}`, {
|
| 211 |
+
method: 'GET'
|
| 212 |
+
});
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
async updateSession(sessionId: string, isReference: boolean): Promise<CreateSessionResponse> {
|
| 216 |
+
return this.request<CreateSessionResponse>(`/v1/sessions/${sessionId}`, {
|
| 217 |
+
method: 'PATCH',
|
| 218 |
+
body: JSON.stringify({ is_reference: isReference })
|
| 219 |
+
});
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
async deleteSession(sessionId: string): Promise<void> {
|
| 223 |
+
await this.request<void>(`/v1/sessions/${sessionId}`, {
|
| 224 |
+
method: 'DELETE'
|
| 225 |
+
});
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
async sendMessage(
|
| 229 |
+
sessionId: string,
|
| 230 |
+
request: SendMessageRequest
|
| 231 |
+
): Promise<SendMessageResponse> {
|
| 232 |
+
return this.request<SendMessageResponse>(`/v1/sessions/${sessionId}/messages`, {
|
| 233 |
+
method: 'POST',
|
| 234 |
+
body: JSON.stringify(request)
|
| 235 |
+
});
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
async compareSession(sessionId: string, referenceSessionId: string): Promise<ComparisonResult> {
|
| 239 |
+
return this.request<ComparisonResult>(
|
| 240 |
+
`/v1/sessions/${sessionId}/compare?reference_id=${referenceSessionId}`,
|
| 241 |
+
{
|
| 242 |
+
method: 'GET'
|
| 243 |
+
}
|
| 244 |
+
);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
async getUserProfile(): Promise<UserProfile> {
|
| 248 |
+
return this.request<UserProfile>('/v1/user/profile', {
|
| 249 |
+
method: 'GET'
|
| 250 |
+
});
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// Export singleton instance
|
| 255 |
+
export const apiClient = new APIClient();
|
src/lib/services/auth.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authStore } from '$lib/stores/auth';
|
| 2 |
+
import { apiClient } from '$lib/services/api';
|
| 3 |
+
import {
|
| 4 |
+
OAUTH_CLIENT_ID,
|
| 5 |
+
OAUTH_REDIRECT_URI,
|
| 6 |
+
OAUTH_PROVIDER_URL,
|
| 7 |
+
ENABLE_MOCK_AUTH,
|
| 8 |
+
MOCK_USER_ID,
|
| 9 |
+
MOCK_USERNAME,
|
| 10 |
+
MOCK_EMAIL,
|
| 11 |
+
API_TOKEN
|
| 12 |
+
} from '$lib/utils/constants';
|
| 13 |
+
import type { UserProfile } from '$lib/types/api';
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Authentication service for OAuth2 PKCE and mock auth
|
| 17 |
+
*/
|
| 18 |
+
|
| 19 |
+
// PKCE helper functions
|
| 20 |
+
function generateRandomString(length: number): string {
|
| 21 |
+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
| 22 |
+
const values = crypto.getRandomValues(new Uint8Array(length));
|
| 23 |
+
return Array.from(values)
|
| 24 |
+
.map((x) => possible[x % possible.length])
|
| 25 |
+
.join('');
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
async function sha256(plain: string): Promise<ArrayBuffer> {
|
| 29 |
+
const encoder = new TextEncoder();
|
| 30 |
+
const data = encoder.encode(plain);
|
| 31 |
+
return crypto.subtle.digest('SHA-256', data);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function base64urlencode(buffer: ArrayBuffer): string {
|
| 35 |
+
const bytes = new Uint8Array(buffer);
|
| 36 |
+
let str = '';
|
| 37 |
+
for (const byte of bytes) {
|
| 38 |
+
str += String.fromCharCode(byte);
|
| 39 |
+
}
|
| 40 |
+
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Generate PKCE code verifier and challenge
|
| 45 |
+
*/
|
| 46 |
+
export async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
|
| 47 |
+
const verifier = generateRandomString(128);
|
| 48 |
+
const hashed = await sha256(verifier);
|
| 49 |
+
const challenge = base64urlencode(hashed);
|
| 50 |
+
return { verifier, challenge };
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Initiate OAuth2 login flow
|
| 55 |
+
*/
|
| 56 |
+
export async function initiateOAuthLogin(): Promise<void | UserProfile> {
|
| 57 |
+
if (ENABLE_MOCK_AUTH) {
|
| 58 |
+
// Use mock auth in development
|
| 59 |
+
return mockLogin();
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Generate PKCE parameters
|
| 63 |
+
const { verifier, challenge } = await generatePKCE();
|
| 64 |
+
|
| 65 |
+
// Store verifier in sessionStorage for callback
|
| 66 |
+
sessionStorage.setItem('pkce_verifier', verifier);
|
| 67 |
+
|
| 68 |
+
// Build authorization URL
|
| 69 |
+
const params = new URLSearchParams({
|
| 70 |
+
client_id: OAUTH_CLIENT_ID,
|
| 71 |
+
redirect_uri: OAUTH_REDIRECT_URI,
|
| 72 |
+
response_type: 'code',
|
| 73 |
+
scope: 'openid profile email',
|
| 74 |
+
code_challenge: challenge,
|
| 75 |
+
code_challenge_method: 'S256',
|
| 76 |
+
state: generateRandomString(32) // CSRF protection
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// Store state for validation
|
| 80 |
+
sessionStorage.setItem('oauth_state', params.get('state')!);
|
| 81 |
+
|
| 82 |
+
// Redirect to OAuth provider
|
| 83 |
+
window.location.href = `${OAUTH_PROVIDER_URL}?${params.toString()}`;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Handle OAuth callback after authorization
|
| 88 |
+
*/
|
| 89 |
+
export async function handleOAuthCallback(
|
| 90 |
+
code: string,
|
| 91 |
+
state: string
|
| 92 |
+
): Promise<UserProfile | null> {
|
| 93 |
+
// Validate state
|
| 94 |
+
const storedState = sessionStorage.getItem('oauth_state');
|
| 95 |
+
if (!storedState || storedState !== state) {
|
| 96 |
+
throw new Error('Invalid OAuth state - possible CSRF attack');
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Get verifier
|
| 100 |
+
const verifier = sessionStorage.getItem('pkce_verifier');
|
| 101 |
+
if (!verifier) {
|
| 102 |
+
throw new Error('PKCE verifier not found');
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Clean up stored values
|
| 106 |
+
sessionStorage.removeItem('oauth_state');
|
| 107 |
+
sessionStorage.removeItem('pkce_verifier');
|
| 108 |
+
|
| 109 |
+
// Exchange authorization code for token
|
| 110 |
+
const tokenResponse = await fetch(`${OAUTH_PROVIDER_URL}/token`, {
|
| 111 |
+
method: 'POST',
|
| 112 |
+
headers: {
|
| 113 |
+
'Content-Type': 'application/x-www-form-urlencoded'
|
| 114 |
+
},
|
| 115 |
+
body: new URLSearchParams({
|
| 116 |
+
client_id: OAUTH_CLIENT_ID,
|
| 117 |
+
code,
|
| 118 |
+
code_verifier: verifier,
|
| 119 |
+
grant_type: 'authorization_code',
|
| 120 |
+
redirect_uri: OAUTH_REDIRECT_URI
|
| 121 |
+
})
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
if (!tokenResponse.ok) {
|
| 125 |
+
throw new Error('Failed to exchange authorization code for token');
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
const tokenData = await tokenResponse.json();
|
| 129 |
+
const accessToken = tokenData.access_token;
|
| 130 |
+
const expiresIn = tokenData.expires_in || 3600; // Default 1 hour
|
| 131 |
+
|
| 132 |
+
// Set auth headers for API client
|
| 133 |
+
apiClient.setAuth(accessToken, ''); // user_id will be set after profile fetch
|
| 134 |
+
|
| 135 |
+
// Fetch user profile from backend
|
| 136 |
+
const userProfile = await apiClient.getUserProfile();
|
| 137 |
+
|
| 138 |
+
// Calculate token expiry
|
| 139 |
+
const tokenExpiry = Date.now() + expiresIn * 1000;
|
| 140 |
+
|
| 141 |
+
// Update API client with user_id
|
| 142 |
+
apiClient.setAuth(accessToken, userProfile.user_id);
|
| 143 |
+
|
| 144 |
+
// Update auth store
|
| 145 |
+
authStore.login(
|
| 146 |
+
{
|
| 147 |
+
user_id: userProfile.user_id,
|
| 148 |
+
username: userProfile.username,
|
| 149 |
+
email: userProfile.email,
|
| 150 |
+
avatar_url: userProfile.avatar_url,
|
| 151 |
+
tokenExpiry
|
| 152 |
+
},
|
| 153 |
+
accessToken,
|
| 154 |
+
tokenExpiry
|
| 155 |
+
);
|
| 156 |
+
|
| 157 |
+
return userProfile;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/**
|
| 161 |
+
* Mock authentication for development
|
| 162 |
+
*/
|
| 163 |
+
export async function mockLogin(): Promise<UserProfile> {
|
| 164 |
+
// Use configured API token for mock auth
|
| 165 |
+
const mockToken = API_TOKEN;
|
| 166 |
+
const tokenExpiry = Date.now() + 24 * 60 * 60 * 1000; // 24 hours
|
| 167 |
+
|
| 168 |
+
// Set auth headers
|
| 169 |
+
apiClient.setAuth(mockToken, MOCK_USER_ID);
|
| 170 |
+
|
| 171 |
+
// Create mock user profile
|
| 172 |
+
const mockProfile: UserProfile = {
|
| 173 |
+
user_id: MOCK_USER_ID,
|
| 174 |
+
username: MOCK_USERNAME,
|
| 175 |
+
email: MOCK_EMAIL,
|
| 176 |
+
avatar_url: undefined
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
// Update auth store
|
| 180 |
+
authStore.login(
|
| 181 |
+
{
|
| 182 |
+
...mockProfile,
|
| 183 |
+
tokenExpiry
|
| 184 |
+
},
|
| 185 |
+
mockToken,
|
| 186 |
+
tokenExpiry
|
| 187 |
+
);
|
| 188 |
+
|
| 189 |
+
return mockProfile;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/**
|
| 193 |
+
* Logout user
|
| 194 |
+
*/
|
| 195 |
+
export function logout(): void {
|
| 196 |
+
// Clear API client auth
|
| 197 |
+
apiClient.clearAuth();
|
| 198 |
+
|
| 199 |
+
// Clear auth store
|
| 200 |
+
authStore.logout();
|
| 201 |
+
|
| 202 |
+
// Redirect to home
|
| 203 |
+
if (typeof window !== 'undefined') {
|
| 204 |
+
window.location.href = '/';
|
| 205 |
+
}
|
| 206 |
+
}
|