Helquin commited on
Commit
f6d96e2
·
1 Parent(s): a5a2c66

Button moved, toast errors / success/ modal closes on submission.

Browse files
backend/src/api/main.py CHANGED
@@ -2,12 +2,17 @@
2
 
3
  from __future__ import annotations
4
 
 
 
5
  from fastapi import FastAPI, Request
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.responses import JSONResponse
8
 
 
9
  from .routes import index, notes, search
10
 
 
 
11
  app = FastAPI(
12
  title="Document Viewer API",
13
  description="Multi-tenant Obsidian-like documentation system",
@@ -24,6 +29,19 @@ app.add_middleware(
24
  )
25
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  # Error handlers
28
  @app.exception_handler(404)
29
  async def not_found_handler(request: Request, exc: Exception):
 
2
 
3
  from __future__ import annotations
4
 
5
+ import logging
6
+
7
  from fastapi import FastAPI, Request
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import JSONResponse
10
 
11
+ from ..services.database import DatabaseService
12
  from .routes import index, notes, search
13
 
14
+ logger = logging.getLogger(__name__)
15
+
16
  app = FastAPI(
17
  title="Document Viewer API",
18
  description="Multi-tenant Obsidian-like documentation system",
 
29
  )
30
 
31
 
32
+ # Startup event to initialize database
33
+ @app.on_event("startup")
34
+ async def startup_event():
35
+ """Initialize database schema on startup if it doesn't exist."""
36
+ try:
37
+ db_service = DatabaseService()
38
+ db_service.initialize()
39
+ logger.info("Database initialized successfully")
40
+ except Exception as e:
41
+ logger.error(f"Failed to initialize database: {e}")
42
+ raise
43
+
44
+
45
  # Error handlers
46
  @app.exception_handler(404)
47
  async def not_found_handler(request: Request, exc: Exception):
backend/src/api/routes/notes.py CHANGED
@@ -67,13 +67,21 @@ async def create_note(create: NoteCreate):
67
 
68
  try:
69
  note_path = create.note_path
70
-
71
  # Check if note already exists
72
  try:
73
  vault_service.read_note(user_id, note_path)
74
- raise HTTPException(status_code=409, detail=f"Note already exists: {note_path}")
 
 
 
 
 
 
75
  except FileNotFoundError:
76
  pass # Good, note doesn't exist
 
 
77
 
78
  # Prepare metadata
79
  metadata = create.metadata.model_dump() if create.metadata else {}
@@ -103,15 +111,27 @@ async def create_note(create: NoteCreate):
103
  # Return created note
104
  created = written_note["metadata"].get("created")
105
  updated_ts = written_note["metadata"].get("updated")
106
-
107
- if isinstance(created, str):
108
- created = datetime.fromisoformat(created.replace("Z", "+00:00"))
109
- elif not isinstance(created, datetime):
 
 
 
 
 
 
110
  created = datetime.now()
111
-
112
- if isinstance(updated_ts, str):
113
- updated_ts = datetime.fromisoformat(updated_ts.replace("Z", "+00:00"))
114
- elif not isinstance(updated_ts, datetime):
 
 
 
 
 
 
115
  updated_ts = created
116
 
117
  return Note(
@@ -163,15 +183,27 @@ async def get_note(path: str):
163
  metadata = note_data.get("metadata", {})
164
  created = metadata.get("created")
165
  updated = metadata.get("updated")
166
-
167
- if isinstance(created, str):
168
- created = datetime.fromisoformat(created.replace("Z", "+00:00"))
169
- elif not isinstance(created, datetime):
 
 
 
 
 
 
170
  created = datetime.now()
171
-
172
- if isinstance(updated, str):
173
- updated = datetime.fromisoformat(updated.replace("Z", "+00:00"))
174
- elif not isinstance(updated, datetime):
 
 
 
 
 
 
175
  updated = created
176
 
177
  return Note(
@@ -249,15 +281,27 @@ async def update_note(path: str, update: NoteUpdate):
249
  # Return updated note
250
  created = written_note["metadata"].get("created")
251
  updated_ts = written_note["metadata"].get("updated")
252
-
253
- if isinstance(created, str):
254
- created = datetime.fromisoformat(created.replace("Z", "+00:00"))
255
- elif not isinstance(created, datetime):
 
 
 
 
 
 
256
  created = datetime.now()
257
-
258
- if isinstance(updated_ts, str):
259
- updated_ts = datetime.fromisoformat(updated_ts.replace("Z", "+00:00"))
260
- elif not isinstance(updated_ts, datetime):
 
 
 
 
 
 
261
  updated_ts = created
262
 
263
  return Note(
 
67
 
68
  try:
69
  note_path = create.note_path
70
+
71
  # Check if note already exists
72
  try:
73
  vault_service.read_note(user_id, note_path)
74
+ raise HTTPException(
75
+ status_code=409,
76
+ detail={
77
+ "error": "note_already_exists",
78
+ "message": f"A note with the name '{note_path}' already exists. Please choose a different name.",
79
+ }
80
+ )
81
  except FileNotFoundError:
82
  pass # Good, note doesn't exist
83
+ except HTTPException:
84
+ raise # Re-raise HTTP exceptions
85
 
86
  # Prepare metadata
87
  metadata = create.metadata.model_dump() if create.metadata else {}
 
111
  # Return created note
112
  created = written_note["metadata"].get("created")
113
  updated_ts = written_note["metadata"].get("updated")
114
+
115
+ # Parse created timestamp
116
+ try:
117
+ if isinstance(created, str):
118
+ created = datetime.fromisoformat(created.replace("Z", "+00:00"))
119
+ elif isinstance(created, datetime):
120
+ pass # Already a datetime
121
+ else:
122
+ created = datetime.now()
123
+ except (ValueError, TypeError):
124
  created = datetime.now()
125
+
126
+ # Parse updated timestamp
127
+ try:
128
+ if isinstance(updated_ts, str):
129
+ updated_ts = datetime.fromisoformat(updated_ts.replace("Z", "+00:00"))
130
+ elif isinstance(updated_ts, datetime):
131
+ pass # Already a datetime
132
+ else:
133
+ updated_ts = created
134
+ except (ValueError, TypeError):
135
  updated_ts = created
136
 
137
  return Note(
 
183
  metadata = note_data.get("metadata", {})
184
  created = metadata.get("created")
185
  updated = metadata.get("updated")
186
+
187
+ # Parse created timestamp
188
+ try:
189
+ if isinstance(created, str):
190
+ created = datetime.fromisoformat(created.replace("Z", "+00:00"))
191
+ elif isinstance(created, datetime):
192
+ pass # Already a datetime
193
+ else:
194
+ created = datetime.now()
195
+ except (ValueError, TypeError):
196
  created = datetime.now()
197
+
198
+ # Parse updated timestamp
199
+ try:
200
+ if isinstance(updated, str):
201
+ updated = datetime.fromisoformat(updated.replace("Z", "+00:00"))
202
+ elif isinstance(updated, datetime):
203
+ pass # Already a datetime
204
+ else:
205
+ updated = created
206
+ except (ValueError, TypeError):
207
  updated = created
208
 
209
  return Note(
 
281
  # Return updated note
282
  created = written_note["metadata"].get("created")
283
  updated_ts = written_note["metadata"].get("updated")
284
+
285
+ # Parse created timestamp
286
+ try:
287
+ if isinstance(created, str):
288
+ created = datetime.fromisoformat(created.replace("Z", "+00:00"))
289
+ elif isinstance(created, datetime):
290
+ pass # Already a datetime
291
+ else:
292
+ created = datetime.now()
293
+ except (ValueError, TypeError):
294
  created = datetime.now()
295
+
296
+ # Parse updated timestamp
297
+ try:
298
+ if isinstance(updated_ts, str):
299
+ updated_ts = datetime.fromisoformat(updated_ts.replace("Z", "+00:00"))
300
+ elif isinstance(updated_ts, datetime):
301
+ pass # Already a datetime
302
+ else:
303
+ updated_ts = created
304
+ except (ValueError, TypeError):
305
  updated_ts = created
306
 
307
  return Note(
frontend/package-lock.json CHANGED
@@ -28,6 +28,7 @@
28
  "react-router-dom": "^7.9.6",
29
  "remark-gfm": "^4.0.1",
30
  "shadcn-ui": "^0.9.0",
 
31
  "typescript": "~5.9.3",
32
  "vite": "^7.2.2"
33
  },
@@ -118,7 +119,6 @@
118
  "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
119
  "dev": true,
120
  "license": "MIT",
121
- "peer": true,
122
  "dependencies": {
123
  "@babel/code-frame": "^7.27.1",
124
  "@babel/generator": "^7.28.5",
@@ -710,7 +710,6 @@
710
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
711
  "dev": true,
712
  "license": "MIT",
713
- "peer": true,
714
  "engines": {
715
  "node": ">=12"
716
  },
@@ -1736,7 +1735,6 @@
1736
  "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
1737
  "dev": true,
1738
  "license": "MIT",
1739
- "peer": true,
1740
  "engines": {
1741
  "node": "^14.21.3 || >=16"
1742
  },
@@ -3324,7 +3322,6 @@
3324
  "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
3325
  "devOptional": true,
3326
  "license": "MIT",
3327
- "peer": true,
3328
  "dependencies": {
3329
  "undici-types": "~7.16.0"
3330
  }
@@ -3334,7 +3331,6 @@
3334
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
3335
  "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
3336
  "license": "MIT",
3337
- "peer": true,
3338
  "dependencies": {
3339
  "csstype": "^3.0.2"
3340
  }
@@ -3345,7 +3341,6 @@
3345
  "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
3346
  "devOptional": true,
3347
  "license": "MIT",
3348
- "peer": true,
3349
  "peerDependencies": {
3350
  "@types/react": "^19.2.0"
3351
  }
@@ -3409,7 +3404,6 @@
3409
  "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
3410
  "dev": true,
3411
  "license": "MIT",
3412
- "peer": true,
3413
  "dependencies": {
3414
  "@typescript-eslint/scope-manager": "8.46.4",
3415
  "@typescript-eslint/types": "8.46.4",
@@ -3682,7 +3676,6 @@
3682
  "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
3683
  "dev": true,
3684
  "license": "MIT",
3685
- "peer": true,
3686
  "bin": {
3687
  "acorn": "bin/acorn"
3688
  },
@@ -4003,7 +3996,6 @@
4003
  }
4004
  ],
4005
  "license": "MIT",
4006
- "peer": true,
4007
  "dependencies": {
4008
  "baseline-browser-mapping": "^2.8.25",
4009
  "caniuse-lite": "^1.0.30001754",
@@ -4878,7 +4870,6 @@
4878
  "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
4879
  "dev": true,
4880
  "license": "MIT",
4881
- "peer": true,
4882
  "dependencies": {
4883
  "@eslint-community/eslint-utils": "^4.8.0",
4884
  "@eslint-community/regexpp": "^4.12.1",
@@ -5147,7 +5138,6 @@
5147
  "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
5148
  "dev": true,
5149
  "license": "MIT",
5150
- "peer": true,
5151
  "dependencies": {
5152
  "accepts": "^2.0.0",
5153
  "body-parser": "^2.2.0",
@@ -6202,7 +6192,6 @@
6202
  "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
6203
  "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
6204
  "license": "MIT",
6205
- "peer": true,
6206
  "bin": {
6207
  "jiti": "bin/jiti.js"
6208
  }
@@ -8043,7 +8032,6 @@
8043
  }
8044
  ],
8045
  "license": "MIT",
8046
- "peer": true,
8047
  "dependencies": {
8048
  "nanoid": "^3.3.11",
8049
  "picocolors": "^1.1.1",
@@ -8355,7 +8343,6 @@
8355
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
8356
  "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
8357
  "license": "MIT",
8358
- "peer": true,
8359
  "engines": {
8360
  "node": ">=0.10.0"
8361
  }
@@ -8365,7 +8352,6 @@
8365
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
8366
  "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
8367
  "license": "MIT",
8368
- "peer": true,
8369
  "dependencies": {
8370
  "scheduler": "^0.27.0"
8371
  },
@@ -9094,6 +9080,16 @@
9094
  "dev": true,
9095
  "license": "MIT"
9096
  },
 
 
 
 
 
 
 
 
 
 
9097
  "node_modules/source-map": {
9098
  "version": "0.6.1",
9099
  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -9408,7 +9404,6 @@
9408
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
9409
  "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
9410
  "license": "MIT",
9411
- "peer": true,
9412
  "dependencies": {
9413
  "@alloc/quick-lru": "^5.2.0",
9414
  "arg": "^5.0.2",
@@ -9540,7 +9535,6 @@
9540
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
9541
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
9542
  "license": "MIT",
9543
- "peer": true,
9544
  "engines": {
9545
  "node": ">=12"
9546
  },
@@ -9720,7 +9714,6 @@
9720
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
9721
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
9722
  "license": "Apache-2.0",
9723
- "peer": true,
9724
  "bin": {
9725
  "tsc": "bin/tsc",
9726
  "tsserver": "bin/tsserver"
@@ -10123,7 +10116,6 @@
10123
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
10124
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
10125
  "license": "MIT",
10126
- "peer": true,
10127
  "engines": {
10128
  "node": ">=12"
10129
  },
@@ -10314,7 +10306,6 @@
10314
  "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
10315
  "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
10316
  "license": "ISC",
10317
- "peer": true,
10318
  "bin": {
10319
  "yaml": "bin.mjs"
10320
  },
@@ -10441,7 +10432,6 @@
10441
  "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
10442
  "dev": true,
10443
  "license": "MIT",
10444
- "peer": true,
10445
  "funding": {
10446
  "url": "https://github.com/sponsors/colinhacks"
10447
  }
 
28
  "react-router-dom": "^7.9.6",
29
  "remark-gfm": "^4.0.1",
30
  "shadcn-ui": "^0.9.0",
31
+ "sonner": "^2.0.7",
32
  "typescript": "~5.9.3",
33
  "vite": "^7.2.2"
34
  },
 
119
  "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
120
  "dev": true,
121
  "license": "MIT",
 
122
  "dependencies": {
123
  "@babel/code-frame": "^7.27.1",
124
  "@babel/generator": "^7.28.5",
 
710
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
711
  "dev": true,
712
  "license": "MIT",
 
713
  "engines": {
714
  "node": ">=12"
715
  },
 
1735
  "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
1736
  "dev": true,
1737
  "license": "MIT",
 
1738
  "engines": {
1739
  "node": "^14.21.3 || >=16"
1740
  },
 
3322
  "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
3323
  "devOptional": true,
3324
  "license": "MIT",
 
3325
  "dependencies": {
3326
  "undici-types": "~7.16.0"
3327
  }
 
3331
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
3332
  "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
3333
  "license": "MIT",
 
3334
  "dependencies": {
3335
  "csstype": "^3.0.2"
3336
  }
 
3341
  "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
3342
  "devOptional": true,
3343
  "license": "MIT",
 
3344
  "peerDependencies": {
3345
  "@types/react": "^19.2.0"
3346
  }
 
3404
  "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
3405
  "dev": true,
3406
  "license": "MIT",
 
3407
  "dependencies": {
3408
  "@typescript-eslint/scope-manager": "8.46.4",
3409
  "@typescript-eslint/types": "8.46.4",
 
3676
  "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
3677
  "dev": true,
3678
  "license": "MIT",
 
3679
  "bin": {
3680
  "acorn": "bin/acorn"
3681
  },
 
3996
  }
3997
  ],
3998
  "license": "MIT",
 
3999
  "dependencies": {
4000
  "baseline-browser-mapping": "^2.8.25",
4001
  "caniuse-lite": "^1.0.30001754",
 
4870
  "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
4871
  "dev": true,
4872
  "license": "MIT",
 
4873
  "dependencies": {
4874
  "@eslint-community/eslint-utils": "^4.8.0",
4875
  "@eslint-community/regexpp": "^4.12.1",
 
5138
  "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
5139
  "dev": true,
5140
  "license": "MIT",
 
5141
  "dependencies": {
5142
  "accepts": "^2.0.0",
5143
  "body-parser": "^2.2.0",
 
6192
  "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
6193
  "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
6194
  "license": "MIT",
 
6195
  "bin": {
6196
  "jiti": "bin/jiti.js"
6197
  }
 
8032
  }
8033
  ],
8034
  "license": "MIT",
 
8035
  "dependencies": {
8036
  "nanoid": "^3.3.11",
8037
  "picocolors": "^1.1.1",
 
8343
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
8344
  "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
8345
  "license": "MIT",
 
8346
  "engines": {
8347
  "node": ">=0.10.0"
8348
  }
 
8352
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
8353
  "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
8354
  "license": "MIT",
 
8355
  "dependencies": {
8356
  "scheduler": "^0.27.0"
8357
  },
 
9080
  "dev": true,
9081
  "license": "MIT"
9082
  },
9083
+ "node_modules/sonner": {
9084
+ "version": "2.0.7",
9085
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
9086
+ "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
9087
+ "license": "MIT",
9088
+ "peerDependencies": {
9089
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
9090
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
9091
+ }
9092
+ },
9093
  "node_modules/source-map": {
9094
  "version": "0.6.1",
9095
  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
 
9404
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
9405
  "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
9406
  "license": "MIT",
 
9407
  "dependencies": {
9408
  "@alloc/quick-lru": "^5.2.0",
9409
  "arg": "^5.0.2",
 
9535
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
9536
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
9537
  "license": "MIT",
 
9538
  "engines": {
9539
  "node": ">=12"
9540
  },
 
9714
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
9715
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
9716
  "license": "Apache-2.0",
 
9717
  "bin": {
9718
  "tsc": "bin/tsc",
9719
  "tsserver": "bin/tsserver"
 
10116
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
10117
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
10118
  "license": "MIT",
 
10119
  "engines": {
10120
  "node": ">=12"
10121
  },
 
10306
  "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
10307
  "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
10308
  "license": "ISC",
 
10309
  "bin": {
10310
  "yaml": "bin.mjs"
10311
  },
 
10432
  "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
10433
  "dev": true,
10434
  "license": "MIT",
 
10435
  "funding": {
10436
  "url": "https://github.com/sponsors/colinhacks"
10437
  }
frontend/package.json CHANGED
@@ -30,6 +30,7 @@
30
  "react-router-dom": "^7.9.6",
31
  "remark-gfm": "^4.0.1",
32
  "shadcn-ui": "^0.9.0",
 
33
  "typescript": "~5.9.3",
34
  "vite": "^7.2.2"
35
  },
 
30
  "react-router-dom": "^7.9.6",
31
  "remark-gfm": "^4.0.1",
32
  "shadcn-ui": "^0.9.0",
33
+ "sonner": "^2.0.7",
34
  "typescript": "~5.9.3",
35
  "vite": "^7.2.2"
36
  },
frontend/src/App.tsx CHANGED
@@ -4,6 +4,7 @@ import { MainApp } from './pages/MainApp';
4
  import { Login } from './pages/Login';
5
  import { Settings } from './pages/Settings';
6
  import { isAuthenticated, getCurrentUser } from './services/auth';
 
7
  import './App.css';
8
 
9
  // Protected route wrapper with auth check
@@ -59,27 +60,30 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
59
 
60
  function App() {
61
  return (
62
- <BrowserRouter>
63
- <Routes>
64
- <Route path="/login" element={<Login />} />
65
- <Route
66
- path="/"
67
- element={
68
- <ProtectedRoute>
69
- <MainApp />
70
- </ProtectedRoute>
71
- }
72
- />
73
- <Route
74
- path="/settings"
75
- element={
76
- <ProtectedRoute>
77
- <Settings />
78
- </ProtectedRoute>
79
- }
80
- />
81
- </Routes>
82
- </BrowserRouter>
 
 
 
83
  );
84
  }
85
 
 
4
  import { Login } from './pages/Login';
5
  import { Settings } from './pages/Settings';
6
  import { isAuthenticated, getCurrentUser } from './services/auth';
7
+ import { Toaster } from './components/ui/toaster';
8
  import './App.css';
9
 
10
  // Protected route wrapper with auth check
 
60
 
61
  function App() {
62
  return (
63
+ <>
64
+ <BrowserRouter>
65
+ <Routes>
66
+ <Route path="/login" element={<Login />} />
67
+ <Route
68
+ path="/"
69
+ element={
70
+ <ProtectedRoute>
71
+ <MainApp />
72
+ </ProtectedRoute>
73
+ }
74
+ />
75
+ <Route
76
+ path="/settings"
77
+ element={
78
+ <ProtectedRoute>
79
+ <Settings />
80
+ </ProtectedRoute>
81
+ }
82
+ />
83
+ </Routes>
84
+ </BrowserRouter>
85
+ <Toaster />
86
+ </>
87
  );
88
  }
89
 
frontend/src/components/ui/toaster.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster as Sonner } from 'sonner';
2
+
3
+ export function Toaster() {
4
+ return (
5
+ <Sonner
6
+ position="top-center"
7
+ toastOptions={{
8
+ duration: 8000,
9
+ unstyled: true,
10
+ classNames: {
11
+ toast: 'relative flex items-center gap-3 rounded-lg border border-border bg-background p-4 shadow-lg',
12
+ error: 'border-red-500/50 bg-red-50 dark:bg-red-950',
13
+ success: 'border-green-500/50 bg-green-50 dark:bg-green-950',
14
+ warning: 'border-yellow-500/50 bg-yellow-50 dark:bg-yellow-950',
15
+ info: 'border-blue-500/50 bg-blue-50 dark:bg-blue-950',
16
+ },
17
+ }}
18
+ />
19
+ );
20
+ }
frontend/src/hooks/useToast.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { toast } from 'sonner';
2
+
3
+ export function useToast() {
4
+ return {
5
+ error: (message: string) => toast.error(message),
6
+ success: (message: string) => toast.success(message),
7
+ info: (message: string) => toast.info(message),
8
+ warning: (message: string) => toast.warning(message),
9
+ };
10
+ }
frontend/src/lib/markdown.ts CHANGED
@@ -34,24 +34,26 @@ export function createWikilinkComponent(
34
  // Add the wikilink as a clickable element
35
  const linkText = match[1];
36
  parts.push(
37
- <span
38
- key={key++}
39
- className="wikilink cursor-pointer text-primary hover:underline"
40
- onClick={(e) => {
41
- e.preventDefault();
42
- onWikilinkClick?.(linkText);
43
- }}
44
- role="link"
45
- tabIndex={0}
46
- onKeyDown={(e) => {
47
- if (e.key === 'Enter' || e.key === ' ') {
48
  e.preventDefault();
49
  onWikilinkClick?.(linkText);
50
- }
51
- }}
52
- >
53
- [[{linkText}]]
54
- </span>
 
 
 
 
 
 
 
55
  );
56
 
57
  lastIndex = pattern.lastIndex;
@@ -62,102 +64,80 @@ export function createWikilinkComponent(
62
  parts.push(value.slice(lastIndex));
63
  }
64
 
65
- return parts.length > 0 ? <>{parts}</> : value;
66
  },
67
-
68
  // Style code blocks
69
- code: ({ className, children, ...props }) => {
70
  const match = /language-(\w+)/.exec(className || '');
71
  const isInline = !match;
72
 
73
  if (isInline) {
74
- return (
75
- <code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
76
- {children}
77
- </code>
 
 
 
78
  );
79
  }
80
 
81
- return (
82
- <code
83
- className={`${className} block bg-muted p-4 rounded-lg overflow-x-auto text-sm font-mono`}
84
- {...props}
85
- >
86
- {children}
87
- </code>
88
  );
89
  },
90
 
91
  // Style links
92
- a: ({ href, children, ...props }) => {
93
  const isExternal = href?.startsWith('http');
94
- return (
95
- <a
96
- href={href}
97
- className="text-primary hover:underline"
98
- target={isExternal ? '_blank' : undefined}
99
- rel={isExternal ? 'noopener noreferrer' : undefined}
100
- {...props}
101
- >
102
- {children}
103
- </a>
104
  );
105
  },
106
 
107
  // Style headings
108
- h1: ({ children, ...props }) => (
109
- <h1 className="text-3xl font-bold mt-6 mb-4" {...props}>
110
- {children}
111
- </h1>
112
- ),
113
- h2: ({ children, ...props }) => (
114
- <h2 className="text-2xl font-semibold mt-5 mb-3" {...props}>
115
- {children}
116
- </h2>
117
- ),
118
- h3: ({ children, ...props }) => (
119
- <h3 className="text-xl font-semibold mt-4 mb-2" {...props}>
120
- {children}
121
- </h3>
122
- ),
123
 
124
  // Style lists
125
- ul: ({ children, ...props }) => (
126
- <ul className="list-disc list-inside my-2 space-y-1" {...props}>
127
- {children}
128
- </ul>
129
- ),
130
- ol: ({ children, ...props }) => (
131
- <ol className="list-decimal list-inside my-2 space-y-1" {...props}>
132
- {children}
133
- </ol>
134
- ),
135
 
136
  // Style blockquotes
137
- blockquote: ({ children, ...props }) => (
138
- <blockquote className="border-l-4 border-muted-foreground pl-4 italic my-4" {...props}>
139
- {children}
140
- </blockquote>
141
- ),
142
 
143
  // Style tables
144
- table: ({ children, ...props }) => (
145
- <div className="overflow-x-auto my-4">
146
- <table className="min-w-full border-collapse border border-border" {...props}>
147
- {children}
148
- </table>
149
- </div>
150
- ),
151
- th: ({ children, ...props }) => (
152
- <th className="border border-border px-4 py-2 bg-muted font-semibold text-left" {...props}>
153
- {children}
154
- </th>
155
- ),
156
- td: ({ children, ...props }) => (
157
- <td className="border border-border px-4 py-2" {...props}>
158
- {children}
159
- </td>
160
- ),
161
  };
162
  }
163
 
@@ -168,16 +148,15 @@ export function renderBrokenWikilink(
168
  linkText: string,
169
  onCreate?: () => void
170
  ): React.ReactElement {
171
- return (
172
- <span
173
- className="wikilink-broken text-destructive border-b border-dashed border-destructive cursor-pointer hover:bg-destructive/10"
174
- onClick={onCreate}
175
- role="link"
176
- tabIndex={0}
177
- title={`Note "${linkText}" not found. Click to create.`}
178
- >
179
- [[{linkText}]]
180
- </span>
181
  );
182
  }
183
-
 
34
  // Add the wikilink as a clickable element
35
  const linkText = match[1];
36
  parts.push(
37
+ React.createElement(
38
+ 'span',
39
+ {
40
+ key: key++,
41
+ className: 'wikilink cursor-pointer text-primary hover:underline',
42
+ onClick: (e: React.MouseEvent) => {
 
 
 
 
 
43
  e.preventDefault();
44
  onWikilinkClick?.(linkText);
45
+ },
46
+ role: 'link',
47
+ tabIndex: 0,
48
+ onKeyDown: (e: React.KeyboardEvent) => {
49
+ if (e.key === 'Enter' || e.key === ' ') {
50
+ e.preventDefault();
51
+ onWikilinkClick?.(linkText);
52
+ }
53
+ },
54
+ },
55
+ `[[${linkText}]]`
56
+ )
57
  );
58
 
59
  lastIndex = pattern.lastIndex;
 
64
  parts.push(value.slice(lastIndex));
65
  }
66
 
67
+ return parts.length > 0 ? React.createElement(React.Fragment, {}, ...parts) : value;
68
  },
69
+
70
  // Style code blocks
71
+ code: ({ className, children, ...props }: any) => {
72
  const match = /language-(\w+)/.exec(className || '');
73
  const isInline = !match;
74
 
75
  if (isInline) {
76
+ return React.createElement(
77
+ 'code',
78
+ {
79
+ className: 'bg-muted px-1.5 py-0.5 rounded text-sm font-mono',
80
+ ...props,
81
+ },
82
+ children
83
  );
84
  }
85
 
86
+ return React.createElement(
87
+ 'code',
88
+ {
89
+ className: `${className} block bg-muted p-4 rounded-lg overflow-x-auto text-sm font-mono`,
90
+ ...props,
91
+ },
92
+ children
93
  );
94
  },
95
 
96
  // Style links
97
+ a: ({ href, children, ...props }: any) => {
98
  const isExternal = href?.startsWith('http');
99
+ return React.createElement(
100
+ 'a',
101
+ {
102
+ href,
103
+ className: 'text-primary hover:underline',
104
+ target: isExternal ? '_blank' : undefined,
105
+ rel: isExternal ? 'noopener noreferrer' : undefined,
106
+ ...props,
107
+ },
108
+ children
109
  );
110
  },
111
 
112
  // Style headings
113
+ h1: ({ children, ...props }: any) =>
114
+ React.createElement('h1', { className: 'text-3xl font-bold mt-6 mb-4', ...props }, children),
115
+ h2: ({ children, ...props }: any) =>
116
+ React.createElement('h2', { className: 'text-2xl font-semibold mt-5 mb-3', ...props }, children),
117
+ h3: ({ children, ...props }: any) =>
118
+ React.createElement('h3', { className: 'text-xl font-semibold mt-4 mb-2', ...props }, children),
 
 
 
 
 
 
 
 
 
119
 
120
  // Style lists
121
+ ul: ({ children, ...props }: any) =>
122
+ React.createElement('ul', { className: 'list-disc list-inside my-2 space-y-1', ...props }, children),
123
+ ol: ({ children, ...props }: any) =>
124
+ React.createElement('ol', { className: 'list-decimal list-inside my-2 space-y-1', ...props }, children),
 
 
 
 
 
 
125
 
126
  // Style blockquotes
127
+ blockquote: ({ children, ...props }: any) =>
128
+ React.createElement('blockquote', { className: 'border-l-4 border-muted-foreground pl-4 italic my-4', ...props }, children),
 
 
 
129
 
130
  // Style tables
131
+ table: ({ children, ...props }: any) =>
132
+ React.createElement(
133
+ 'div',
134
+ { className: 'overflow-x-auto my-4' },
135
+ React.createElement('table', { className: 'min-w-full border-collapse border border-border', ...props }, children)
136
+ ),
137
+ th: ({ children, ...props }: any) =>
138
+ React.createElement('th', { className: 'border border-border px-4 py-2 bg-muted font-semibold text-left', ...props }, children),
139
+ td: ({ children, ...props }: any) =>
140
+ React.createElement('td', { className: 'border border-border px-4 py-2', ...props }, children),
 
 
 
 
 
 
 
141
  };
142
  }
143
 
 
148
  linkText: string,
149
  onCreate?: () => void
150
  ): React.ReactElement {
151
+ return React.createElement(
152
+ 'span',
153
+ {
154
+ className: 'wikilink-broken text-destructive border-b border-dashed border-destructive cursor-pointer hover:bg-destructive/10',
155
+ onClick: onCreate,
156
+ role: 'link',
157
+ tabIndex: 0,
158
+ title: `Note "${linkText}" not found. Click to create.`,
159
+ },
160
+ `[[${linkText}]]`
161
  );
162
  }
 
frontend/src/pages/MainApp.tsx CHANGED
@@ -13,6 +13,7 @@ import { DirectoryTree } from '@/components/DirectoryTree';
13
  import { SearchBar } from '@/components/SearchBar';
14
  import { NoteViewer } from '@/components/NoteViewer';
15
  import { NoteEditor } from '@/components/NoteEditor';
 
16
  import {
17
  listNotes,
18
  getNote,
@@ -24,6 +25,7 @@ import {
24
  } from '@/services/api';
25
  import {
26
  Dialog,
 
27
  DialogContent,
28
  DialogDescription,
29
  DialogFooter,
@@ -38,6 +40,7 @@ import { normalizeSlug } from '@/lib/wikilink';
38
 
39
  export function MainApp() {
40
  const navigate = useNavigate();
 
41
  const [notes, setNotes] = useState<NoteSummary[]>([]);
42
  const [selectedPath, setSelectedPath] = useState<string | null>(null);
43
  const [currentNote, setCurrentNote] = useState<Note | null>(null);
@@ -49,6 +52,7 @@ export function MainApp() {
49
  const [indexHealth, setIndexHealth] = useState<IndexHealth | null>(null);
50
  const [isNewNoteDialogOpen, setIsNewNoteDialogOpen] = useState(false);
51
  const [newNoteName, setNewNoteName] = useState('');
 
52
 
53
  // T083: Load directory tree on mount
54
  // T119: Load index health
@@ -171,36 +175,76 @@ export function MainApp() {
171
  setIsEditMode(false);
172
  };
173
 
 
 
 
 
 
 
 
 
 
174
  // Handle create new note
175
  const handleCreateNote = async () => {
176
- if (!newNoteName.trim()) return;
177
-
 
 
 
178
  try {
179
- const notePath = newNoteName.endsWith('.md') ? newNoteName : `${newNoteName}.md`;
180
- const note = await createNote({
181
- note_path: notePath,
182
- title: newNoteName.replace(/\.md$/, ''),
183
- body: `# ${newNoteName.replace(/\.md$/, '')}\n\nStart writing your note here...`,
184
- });
185
-
186
- // Refresh notes list
187
- const notesList = await listNotes();
188
- setNotes(notesList);
189
-
190
- // Select the new note
191
- setSelectedPath(note.note_path);
192
- setIsNewNoteDialogOpen(false);
193
- setNewNoteName('');
194
-
195
- // Switch to edit mode for the new note
196
- setIsEditMode(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  } catch (err) {
 
198
  if (err instanceof APIException) {
199
- setError(err.error);
200
- } else {
201
- setError('Failed to create note');
 
202
  }
 
203
  console.error('Error creating note:', err);
 
 
 
 
204
  }
205
  };
206
 
@@ -210,50 +254,9 @@ export function MainApp() {
210
  <div className="border-b border-border p-4">
211
  <div className="flex items-center justify-between">
212
  <h1 className="text-xl font-semibold">📚 Document Viewer</h1>
213
- <div className="flex gap-2">
214
- <Dialog open={isNewNoteDialogOpen} onOpenChange={setIsNewNoteDialogOpen}>
215
- <DialogTrigger asChild>
216
- <Button variant="outline" size="sm">
217
- <Plus className="h-4 w-4 mr-2" />
218
- New Note
219
- </Button>
220
- </DialogTrigger>
221
- <DialogContent>
222
- <DialogHeader>
223
- <DialogTitle>Create New Note</DialogTitle>
224
- <DialogDescription>
225
- Enter a name for your new note. The .md extension will be added automatically if not provided.
226
- </DialogDescription>
227
- </DialogHeader>
228
- <div className="grid gap-4 py-4">
229
- <div className="grid gap-2">
230
- <label htmlFor="note-name" className="text-sm font-medium">Note Name</label>
231
- <Input
232
- id="note-name"
233
- placeholder="my-note"
234
- value={newNoteName}
235
- onChange={(e) => setNewNoteName(e.target.value)}
236
- onKeyDown={(e) => {
237
- if (e.key === 'Enter') {
238
- handleCreateNote();
239
- }
240
- }}
241
- />
242
- </div>
243
- </div> <DialogFooter>
244
- <Button variant="outline" onClick={() => setIsNewNoteDialogOpen(false)}>
245
- Cancel
246
- </Button>
247
- <Button onClick={handleCreateNote} disabled={!newNoteName.trim()}>
248
- Create Note
249
- </Button>
250
- </DialogFooter>
251
- </DialogContent>
252
- </Dialog>
253
- <Button variant="ghost" size="sm" onClick={() => navigate('/settings')}>
254
- <SettingsIcon className="h-4 w-4" />
255
- </Button>
256
- </div>
257
  </div>
258
  </div>
259
 
@@ -264,6 +267,56 @@ export function MainApp() {
264
  <ResizablePanel defaultSize={25} minSize={15} maxSize={40}>
265
  <div className="h-full flex flex-col">
266
  <div className="p-4 space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  <SearchBar onSelectNote={handleSelectNote} />
268
  <Separator />
269
  </div>
 
13
  import { SearchBar } from '@/components/SearchBar';
14
  import { NoteViewer } from '@/components/NoteViewer';
15
  import { NoteEditor } from '@/components/NoteEditor';
16
+ import { useToast } from '@/hooks/useToast';
17
  import {
18
  listNotes,
19
  getNote,
 
25
  } from '@/services/api';
26
  import {
27
  Dialog,
28
+ DialogClose,
29
  DialogContent,
30
  DialogDescription,
31
  DialogFooter,
 
40
 
41
  export function MainApp() {
42
  const navigate = useNavigate();
43
+ const toast = useToast();
44
  const [notes, setNotes] = useState<NoteSummary[]>([]);
45
  const [selectedPath, setSelectedPath] = useState<string | null>(null);
46
  const [currentNote, setCurrentNote] = useState<Note | null>(null);
 
52
  const [indexHealth, setIndexHealth] = useState<IndexHealth | null>(null);
53
  const [isNewNoteDialogOpen, setIsNewNoteDialogOpen] = useState(false);
54
  const [newNoteName, setNewNoteName] = useState('');
55
+ const [isCreatingNote, setIsCreatingNote] = useState(false);
56
 
57
  // T083: Load directory tree on mount
58
  // T119: Load index health
 
175
  setIsEditMode(false);
176
  };
177
 
178
+ // Handle dialog open change
179
+ const handleDialogOpenChange = (open: boolean) => {
180
+ setIsNewNoteDialogOpen(open);
181
+ if (!open) {
182
+ // Clear input when dialog closes
183
+ setNewNoteName('');
184
+ }
185
+ };
186
+
187
  // Handle create new note
188
  const handleCreateNote = async () => {
189
+ if (!newNoteName.trim() || isCreatingNote) return;
190
+
191
+ setIsCreatingNote(true);
192
+ setError(null);
193
+
194
  try {
195
+ const baseName = newNoteName.replace(/\.md$/, '');
196
+ let notePath = newNoteName.endsWith('.md') ? newNoteName : `${newNoteName}.md`;
197
+ let attempt = 1;
198
+ const maxAttempts = 100;
199
+
200
+ // Retry with number suffix if note already exists
201
+ while (attempt <= maxAttempts) {
202
+ try {
203
+ const note = await createNote({
204
+ note_path: notePath,
205
+ title: baseName,
206
+ body: `# ${baseName}\n\nStart writing your note here...`,
207
+ });
208
+
209
+ // Refresh notes list
210
+ const notesList = await listNotes();
211
+ setNotes(notesList);
212
+
213
+ // Select the new note
214
+ setSelectedPath(note.note_path);
215
+ setIsEditMode(true);
216
+ const displayName = notePath.replace(/\.md$/, '');
217
+ toast.success(`Note "${displayName}" created successfully`);
218
+ break;
219
+ } catch (err) {
220
+ if (err instanceof APIException && err.status === 409) {
221
+ // Note already exists, try with number suffix
222
+ attempt++;
223
+ if (attempt <= maxAttempts) {
224
+ notePath = `${baseName} ${attempt}.md`;
225
+ continue;
226
+ } else {
227
+ throw err;
228
+ }
229
+ } else {
230
+ throw err;
231
+ }
232
+ }
233
+ }
234
  } catch (err) {
235
+ let errorMessage = 'Failed to create note';
236
  if (err instanceof APIException) {
237
+ // Use the message field which contains the actual error description
238
+ errorMessage = err.message || err.error;
239
+ } else if (err instanceof Error) {
240
+ errorMessage = err.message;
241
  }
242
+ toast.error(errorMessage);
243
  console.error('Error creating note:', err);
244
+ } finally {
245
+ setIsCreatingNote(false);
246
+ // Always close dialog, regardless of success or failure
247
+ handleDialogOpenChange(false);
248
  }
249
  };
250
 
 
254
  <div className="border-b border-border p-4">
255
  <div className="flex items-center justify-between">
256
  <h1 className="text-xl font-semibold">📚 Document Viewer</h1>
257
+ <Button variant="ghost" size="sm" onClick={() => navigate('/settings')}>
258
+ <SettingsIcon className="h-4 w-4" />
259
+ </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  </div>
261
  </div>
262
 
 
267
  <ResizablePanel defaultSize={25} minSize={15} maxSize={40}>
268
  <div className="h-full flex flex-col">
269
  <div className="p-4 space-y-4">
270
+ <Dialog
271
+ open={isNewNoteDialogOpen}
272
+ onOpenChange={handleDialogOpenChange}
273
+ >
274
+ <DialogTrigger asChild>
275
+ <Button variant="outline" size="sm" className="w-full">
276
+ <Plus className="h-4 w-4 mr-1" />
277
+ New Note
278
+ </Button>
279
+ </DialogTrigger>
280
+ <DialogContent>
281
+ <DialogHeader>
282
+ <DialogTitle>Create New Note</DialogTitle>
283
+ <DialogDescription>
284
+ Enter a name for your new note. The .md extension will be added automatically if not provided.
285
+ </DialogDescription>
286
+ </DialogHeader>
287
+ <div className="grid gap-4 py-4">
288
+ <div className="grid gap-2">
289
+ <label htmlFor="note-name" className="text-sm font-medium">Note Name</label>
290
+ <Input
291
+ id="note-name"
292
+ placeholder="my-note"
293
+ value={newNoteName}
294
+ onChange={(e) => setNewNoteName(e.target.value)}
295
+ onKeyDown={(e) => {
296
+ if (e.key === 'Enter') {
297
+ handleCreateNote();
298
+ }
299
+ }}
300
+ />
301
+ </div>
302
+ </div>
303
+ <DialogFooter>
304
+ <Button
305
+ variant="outline"
306
+ onClick={() => setIsNewNoteDialogOpen(false)}
307
+ disabled={isCreatingNote}
308
+ >
309
+ Cancel
310
+ </Button>
311
+ <Button
312
+ onClick={handleCreateNote}
313
+ disabled={!newNoteName.trim() || isCreatingNote}
314
+ >
315
+ {isCreatingNote ? 'Creating...' : 'Create Note'}
316
+ </Button>
317
+ </DialogFooter>
318
+ </DialogContent>
319
+ </Dialog>
320
  <SearchBar onSelectNote={handleSelectNote} />
321
  <Separator />
322
  </div>