Christian Kniep commited on
Commit
1fff71f
·
1 Parent(s): 3d37e62

new webapp

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.development +18 -0
  2. .env.example +22 -0
  3. .env.production +13 -0
  4. .pytest_cache/CACHEDIR.TAG +4 -0
  5. .pytest_cache/README.md +8 -0
  6. .pytest_cache/v/cache/lastfailed +1 -0
  7. .pytest_cache/v/cache/nodeids +29 -0
  8. .svelte-kit/ambient.d.ts +321 -0
  9. .svelte-kit/generated/client/app.js +35 -0
  10. .svelte-kit/generated/client/matchers.js +1 -0
  11. .svelte-kit/generated/client/nodes/0.js +3 -0
  12. .svelte-kit/generated/client/nodes/1.js +1 -0
  13. .svelte-kit/generated/client/nodes/2.js +1 -0
  14. .svelte-kit/generated/client/nodes/3.js +2 -0
  15. .svelte-kit/generated/client/nodes/4.js +1 -0
  16. .svelte-kit/generated/client/nodes/5.js +3 -0
  17. .svelte-kit/generated/root.svelte +61 -0
  18. .svelte-kit/generated/server/internal.js +53 -0
  19. .svelte-kit/non-ambient.d.ts +46 -0
  20. .svelte-kit/tsconfig.json +52 -0
  21. .svelte-kit/types/route_meta_data.json +17 -0
  22. .svelte-kit/types/src/routes/$types.d.ts +26 -0
  23. .svelte-kit/types/src/routes/auth/callback/$types.d.ts +20 -0
  24. .svelte-kit/types/src/routes/auth/callback/proxy+page.ts +37 -0
  25. .svelte-kit/types/src/routes/login/$types.d.ts +18 -0
  26. .svelte-kit/types/src/routes/proxy+layout.ts +32 -0
  27. .svelte-kit/types/src/routes/session/[id]/$types.d.ts +21 -0
  28. .svelte-kit/types/src/routes/session/[id]/proxy+page.ts +19 -0
  29. Dockerfile +9 -29
  30. README.md +406 -17
  31. app.py +0 -81
  32. data/contacts.db +0 -0
  33. data/migrations/001_add_producer_fields.sql +16 -0
  34. migrations/001_create_tables.sql +30 -0
  35. package-lock.json +0 -0
  36. pyproject.toml +33 -0
  37. requirements.txt +35 -16
  38. src/__init__.py +0 -1
  39. src/__pycache__/app.cpython-311.pyc +0 -0
  40. src/app.py +216 -0
  41. src/lib/components/ChatMessage.svelte +140 -0
  42. src/lib/components/ErrorAlert.svelte +26 -0
  43. src/lib/components/LoadingSpinner.svelte +16 -0
  44. src/lib/components/LoginForm.svelte +94 -0
  45. src/lib/components/MessageInput.svelte +157 -0
  46. src/lib/components/ModeSelector.svelte +87 -0
  47. src/lib/components/SessionHeader.svelte +107 -0
  48. src/lib/components/SessionList.svelte +324 -0
  49. src/lib/services/api.ts +255 -0
  50. 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
- # Set environment variables
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
- # Copy application code
27
- COPY . .
28
-
29
- # Create data directory
30
- RUN mkdir -p data/sessions
31
 
32
- # Expose Streamlit port
33
- EXPOSE 8501
34
 
35
- # Health check
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 the application
40
- CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true"]
 
 
 
 
 
 
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
- title: MemPrepMate
3
- emoji: 📚
4
- colorFrom: green
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- short_description: AI Agent with MemMachine to prepare for conversations
9
- app_port: 8501
10
- hf_oauth: true
11
- hf_oauth_scopes:
12
- - email
13
- hf_oauth_authorized_org:
14
- - Memverge
15
- ---
16
-
17
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # API Session Chat Frontend - Requirements
2
-
3
- # Core Dependencies
4
- streamlit>=1.28.0
5
- requests[security]>=2.31.0
6
- tenacity>=8.2.0 # Retry logic for memory backend
7
-
8
- # Testing
9
- pytest>=7.4.0
10
- pytest-mock>=3.11.1
11
- pytest-timeout>=2.1.0
12
- requests-mock>=1.11.0
13
-
14
- # Development
15
- black>=23.0.0
16
- ruff>=0.1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }