@hiai-gg/hiai-docs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.all-contributorsrc +18 -0
  2. package/.claude/settings.local.json +61 -0
  3. package/.dockerignore +113 -0
  4. package/.env.example +68 -0
  5. package/.github/FUNDING.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
  8. package/.github/dependabot.yml +136 -0
  9. package/.github/pull_request_template.md +96 -0
  10. package/.github/workflows/ci.yml +283 -0
  11. package/AGENTS.md +237 -0
  12. package/CODE_OF_CONDUCT.md +134 -0
  13. package/CONTRIBUTING.md +77 -0
  14. package/Caddyfile +50 -0
  15. package/Dockerfile.backend +60 -0
  16. package/LICENSE +21 -0
  17. package/README.md +284 -0
  18. package/RELEASE_CHECKLIST.md +34 -0
  19. package/SECURITY.md +60 -0
  20. package/backend/package.json +43 -0
  21. package/backend/src/__tests__/auth-helpers.test.ts +51 -0
  22. package/backend/src/__tests__/chunker.test.ts +65 -0
  23. package/backend/src/__tests__/config.test.ts +91 -0
  24. package/backend/src/__tests__/csrf.test.ts +91 -0
  25. package/backend/src/__tests__/embedding.test.ts +48 -0
  26. package/backend/src/__tests__/rate-limit.test.ts +46 -0
  27. package/backend/src/__tests__/routes.test.ts +38 -0
  28. package/backend/src/__tests__/schema.test.ts +31 -0
  29. package/backend/src/__tests__/validation.test.ts +556 -0
  30. package/backend/src/api/middleware/auth.ts +56 -0
  31. package/backend/src/api/middleware/csrf.ts +91 -0
  32. package/backend/src/api/middleware/rate-limit.ts +77 -0
  33. package/backend/src/api/middleware/webhook-verify.ts +22 -0
  34. package/backend/src/api/routes/attachments.ts +280 -0
  35. package/backend/src/api/routes/auth.ts +52 -0
  36. package/backend/src/api/routes/collaboration.ts +121 -0
  37. package/backend/src/api/routes/documents.ts +664 -0
  38. package/backend/src/api/routes/folders.ts +226 -0
  39. package/backend/src/api/routes/search.ts +354 -0
  40. package/backend/src/api/routes/share.ts +512 -0
  41. package/backend/src/api/routes/tags.ts +247 -0
  42. package/backend/src/api/routes/versions.ts +99 -0
  43. package/backend/src/api/routes/webhooks.ts +43 -0
  44. package/backend/src/embedding/chunker.ts +74 -0
  45. package/backend/src/embedding/index.ts +117 -0
  46. package/backend/src/embedding/providers/ollama.ts +63 -0
  47. package/backend/src/embedding/providers/openrouter.ts +71 -0
  48. package/backend/src/embedding/utils.ts +13 -0
  49. package/backend/src/embedding/worker.ts +89 -0
  50. package/backend/src/index.ts +147 -0
  51. package/backend/src/lib/auth-helpers.ts +27 -0
  52. package/backend/src/lib/auth.ts +35 -0
  53. package/backend/src/lib/config.ts +73 -0
  54. package/backend/src/lib/db.ts +7 -0
  55. package/backend/src/lib/embedding-queue.ts +12 -0
  56. package/backend/src/lib/logger.ts +18 -0
  57. package/backend/src/lib/markdown-to-doc.ts +45 -0
  58. package/backend/src/lib/minio.ts +46 -0
  59. package/backend/src/lib/redis.ts +19 -0
  60. package/backend/src/lib/yjs-provider.ts +182 -0
  61. package/backend/tests/integration/_harness.ts +754 -0
  62. package/backend/tests/integration/auth.test.ts +296 -0
  63. package/backend/tests/integration/routes.documents.test.ts +459 -0
  64. package/backend/tests/integration/routes.folders.test.ts +337 -0
  65. package/backend/tests/integration/routes.search.test.ts +322 -0
  66. package/backend/tests/integration/routes.share.test.ts +773 -0
  67. package/backend/tests/integration/routes.tags.test.ts +425 -0
  68. package/backend/tests/integration/routes.versions.test.ts +233 -0
  69. package/backend/tsconfig.json +18 -0
  70. package/docker-compose.yml +218 -0
  71. package/docs/API.md +328 -0
  72. package/docs/ARCHITECTURE.md +75 -0
  73. package/docs/DEPLOYMENT.md +113 -0
  74. package/docs/PRODUCTION_STATUS.md +61 -0
  75. package/docs/openapi.json +385 -0
  76. package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
  77. package/frontend/.svelte-kit.old/env.d.ts +1 -0
  78. package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
  79. package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
  80. package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
  81. package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
  82. package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
  83. package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
  84. package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
  85. package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
  86. package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
  87. package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
  88. package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
  89. package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
  90. package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
  91. package/frontend/.svelte-kit.old/generated/root.js +3 -0
  92. package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
  93. package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
  94. package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
  95. package/frontend/.svelte-kit.old/tsconfig.json +59 -0
  96. package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
  97. package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
  98. package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
  99. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
  100. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
  101. package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
  102. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
  103. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
  104. package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
  105. package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
  106. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
  107. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
  108. package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
  109. package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
  110. package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
  111. package/frontend/Dockerfile +44 -0
  112. package/frontend/biome.json +40 -0
  113. package/frontend/components.json +18 -0
  114. package/frontend/messages/en.json +434 -0
  115. package/frontend/package.json +70 -0
  116. package/frontend/project.inlang/settings.json +12 -0
  117. package/frontend/src/app.css +6 -0
  118. package/frontend/src/app.d.ts +13 -0
  119. package/frontend/src/app.html +30 -0
  120. package/frontend/src/hooks.server.ts +10 -0
  121. package/frontend/src/hooks.ts +10 -0
  122. package/frontend/src/lib/api/attachments.ts +45 -0
  123. package/frontend/src/lib/api/client.test.ts +15 -0
  124. package/frontend/src/lib/api/client.ts +57 -0
  125. package/frontend/src/lib/api/documents.ts +83 -0
  126. package/frontend/src/lib/api/folders.ts +180 -0
  127. package/frontend/src/lib/api/search.test.ts +52 -0
  128. package/frontend/src/lib/api/search.ts +128 -0
  129. package/frontend/src/lib/api/settings.ts +95 -0
  130. package/frontend/src/lib/api/share.ts +71 -0
  131. package/frontend/src/lib/api/tags.test.ts +91 -0
  132. package/frontend/src/lib/api/tags.ts +87 -0
  133. package/frontend/src/lib/auth-client.ts +10 -0
  134. package/frontend/src/lib/collaboration.ts +63 -0
  135. package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
  136. package/frontend/src/lib/components/DatePicker.svelte +322 -0
  137. package/frontend/src/lib/components/DocumentCard.svelte +166 -0
  138. package/frontend/src/lib/components/EmptyState.svelte +49 -0
  139. package/frontend/src/lib/components/FolderCard.svelte +93 -0
  140. package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
  141. package/frontend/src/lib/components/SearchBar.svelte +47 -0
  142. package/frontend/src/lib/components/SearchResult.svelte +115 -0
  143. package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
  144. package/frontend/src/lib/components/ShareDialog.svelte +158 -0
  145. package/frontend/src/lib/components/ShareLink.svelte +98 -0
  146. package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
  147. package/frontend/src/lib/components/VersionDiff.svelte +55 -0
  148. package/frontend/src/lib/components/VersionHistory.svelte +96 -0
  149. package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
  150. package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
  151. package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
  152. package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
  153. package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
  154. package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
  155. package/frontend/src/lib/components/editor/markdown.ts +38 -0
  156. package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
  157. package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
  158. package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
  159. package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
  160. package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
  161. package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
  162. package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
  163. package/frontend/src/lib/stores/theme.svelte.ts +97 -0
  164. package/frontend/src/lib/svelte.d.ts +6 -0
  165. package/frontend/src/lib/types.ts +44 -0
  166. package/frontend/src/lib/utils/clipboard.ts +17 -0
  167. package/frontend/src/lib/utils/strip-markdown.ts +59 -0
  168. package/frontend/src/lib/utils.ts +33 -0
  169. package/frontend/src/routes/(app)/+layout.svelte +17 -0
  170. package/frontend/src/routes/(app)/+page.server.ts +10 -0
  171. package/frontend/src/routes/(app)/+page.svelte +303 -0
  172. package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
  173. package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
  174. package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
  175. package/frontend/src/routes/(app)/search/+page.svelte +593 -0
  176. package/frontend/src/routes/(app)/search/+page.ts +25 -0
  177. package/frontend/src/routes/+error.svelte +12 -0
  178. package/frontend/src/routes/+layout.svelte +18 -0
  179. package/frontend/src/routes/+layout.ts +2 -0
  180. package/frontend/src/routes/api/[...path]/+server.ts +111 -0
  181. package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
  182. package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
  183. package/frontend/src/routes/folders/[id]/+page.ts +14 -0
  184. package/frontend/src/routes/login/+page.svelte +90 -0
  185. package/frontend/src/routes/register/+page.svelte +97 -0
  186. package/frontend/src/routes/s/[token]/+page.svelte +496 -0
  187. package/frontend/src/routes/s/[token]/+page.ts +5 -0
  188. package/frontend/src/routes/settings/+page.svelte +175 -0
  189. package/frontend/static/favicon.png +0 -0
  190. package/frontend/static/logo.png +0 -0
  191. package/frontend/svelte.config.js +15 -0
  192. package/frontend/tsconfig.json +15 -0
  193. package/frontend/vite.config.ts +25 -0
  194. package/init.sql +9 -0
  195. package/logo.png +0 -0
  196. package/package.json +39 -0
  197. package/package.public.json +39 -0
  198. package/packages/db/drizzle.config.ts +10 -0
  199. package/packages/db/package.json +30 -0
  200. package/packages/db/src/client.ts +9 -0
  201. package/packages/db/src/index.ts +2 -0
  202. package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
  203. package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
  204. package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
  205. package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
  206. package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
  207. package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
  208. package/packages/db/src/migrations/meta/_journal.json +27 -0
  209. package/packages/db/src/schema.ts +378 -0
  210. package/packages/db/tsconfig.json +17 -0
  211. package/scripts/export-openapi.ts +37 -0
  212. package/scripts/health-check.sh +75 -0
  213. package/scripts/migrate.sh +135 -0
  214. package/scripts/prework_backup.sh +25 -0
  215. package/scripts/release.sh +83 -0
  216. package/tsconfig.json +25 -0
@@ -0,0 +1,496 @@
1
+ <script lang="ts">
2
+ import { Check, Copy, FileText, Folder, Lock } from "lucide-svelte";
3
+ import { marked } from "marked";
4
+ import { page } from "$app/state";
5
+ import * as m from "$lib/paraglide/messages.js";
6
+
7
+ const token = $derived(page.params.token);
8
+
9
+ let password = $state("");
10
+ let requiresPassword = $state(false);
11
+ let error = $state("");
12
+ let loading = $state(false);
13
+ let shareData = $state<{
14
+ type?: string;
15
+ data?: {
16
+ title?: string;
17
+ content?: string;
18
+ contentJson?: object | null;
19
+ name?: string;
20
+ documents?: { title: string }[];
21
+ };
22
+ } | null>(null);
23
+ let copied = $state(false);
24
+ let copiedText = $state(false);
25
+
26
+ // SvelteKit-injected fetch avoids the `window.fetch` warning during SSR/CSR
27
+ // transitions. Falls back to global fetch when running outside a load fn
28
+ // (e.g. in unit tests).
29
+ const kitFetch = $derived(
30
+ (page.data.fetch as typeof fetch | undefined) ?? globalThis.fetch,
31
+ );
32
+
33
+ // Configure marked for safe, GFM-flavored rendering of shared document
34
+ // markdown. The HiAiEditor JSON path (contentJson) is preferred when the
35
+ // server provides it, but `content` (raw markdown) is the universal fallback.
36
+ marked.setOptions({ gfm: true, breaks: false });
37
+
38
+ function renderContent(): string {
39
+ if (!shareData?.data) return "";
40
+ const docJson = shareData.data.contentJson as
41
+ | { content?: unknown }
42
+ | null
43
+ | undefined;
44
+ if (docJson && Array.isArray(docJson.content)) {
45
+ return docToHtml(docJson as ProseMirrorDoc);
46
+ }
47
+ const md = shareData.data.content;
48
+ if (md && md.length > 0) {
49
+ return marked.parse(md, { async: false }) as string;
50
+ }
51
+ return "";
52
+ }
53
+
54
+ type ProseMirrorNode = {
55
+ type: string;
56
+ text?: string;
57
+ content?: ProseMirrorNode[];
58
+ attrs?: Record<string, unknown>;
59
+ marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
60
+ };
61
+
62
+ type ProseMirrorDoc = ProseMirrorNode & { content?: ProseMirrorNode[] };
63
+
64
+ function escapeHtml(s: string): string {
65
+ return s
66
+ .replace(/&/g, "&amp;")
67
+ .replace(/</g, "&lt;")
68
+ .replace(/>/g, "&gt;")
69
+ .replace(/"/g, "&quot;")
70
+ .replace(/'/g, "&#39;");
71
+ }
72
+
73
+ function docToHtml(doc: ProseMirrorDoc): string {
74
+ const renderNode = (node: ProseMirrorNode): string => {
75
+ if (node.type === "text") {
76
+ let html = escapeHtml(node.text ?? "");
77
+ for (const mark of node.marks ?? []) {
78
+ html = wrapMark(mark, html);
79
+ }
80
+ return html;
81
+ }
82
+ const inner = (node.content ?? []).map(renderNode).join("");
83
+ return wrapBlock(node, inner);
84
+ };
85
+ return (doc.content ?? []).map(renderNode).join("");
86
+ }
87
+
88
+ function wrapMark(
89
+ mark: { type: string; attrs?: Record<string, unknown> },
90
+ html: string,
91
+ ): string {
92
+ switch (mark.type) {
93
+ case "bold":
94
+ return `<strong>${html}</strong>`;
95
+ case "italic":
96
+ return `<em>${html}</em>`;
97
+ case "strike":
98
+ case "strikethrough":
99
+ return `<s>${html}</s>`;
100
+ case "underline":
101
+ return `<u>${html}</u>`;
102
+ case "code":
103
+ return `<code>${html}</code>`;
104
+ case "link": {
105
+ const href = (mark.attrs?.href as string) ?? "#";
106
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${html}</a>`;
107
+ }
108
+ case "highlight": {
109
+ const color = (mark.attrs?.color as string) ?? "#fde68a";
110
+ return `<mark style="background-color: ${escapeHtml(color)}">${html}</mark>`;
111
+ }
112
+ default:
113
+ return html;
114
+ }
115
+ }
116
+
117
+ // Returns an inline `style` attribute fragment (` style="text-align: X"`) for
118
+ // the block-level textAlign attribute, or an empty string when alignment is
119
+ // unset / not a recognized value. Whitelists the four values supported by the
120
+ // editor to avoid passing attacker-controlled strings into the markup.
121
+ function alignStyle(attrs?: Record<string, unknown>): string {
122
+ const align = attrs?.textAlign as string | undefined;
123
+ if (
124
+ align !== "left" &&
125
+ align !== "center" &&
126
+ align !== "right" &&
127
+ align !== "justify"
128
+ ) {
129
+ return "";
130
+ }
131
+ return ` style="text-align: ${align}"`;
132
+ }
133
+
134
+ function wrapBlock(node: ProseMirrorNode, inner: string): string {
135
+ const lang = (node.attrs?.language as string) ?? "";
136
+ const align = alignStyle(node.attrs);
137
+ switch (node.type) {
138
+ case "paragraph":
139
+ return `<p${align}>${inner}</p>`;
140
+ case "heading": {
141
+ const level = Math.min(Math.max(Number(node.attrs?.level ?? 1), 1), 6);
142
+ return `<h${level}${align}>${inner}</h${level}>`;
143
+ }
144
+ case "bulletList":
145
+ return `<ul${align}>${inner}</ul>`;
146
+ case "orderedList":
147
+ return `<ol${align}>${inner}</ol>`;
148
+ case "listItem":
149
+ return `<li${align}>${inner}</li>`;
150
+ case "taskList":
151
+ return `<ul data-type="taskList">${inner}</ul>`;
152
+ case "taskItem": {
153
+ // Read-only checkbox reflecting the saved checked state.
154
+ const isChecked =
155
+ node.attrs?.checked === true || node.attrs?.checked === "true";
156
+ const checked = isChecked ? " checked" : "";
157
+ return `<li data-type="taskItem"${isChecked ? ' data-checked="true"' : ""}><label><input type="checkbox" onclick="return false;" class="cursor-default" ${checked} /></label><div>${inner}</div></li>`;
158
+ }
159
+ case "blockquote":
160
+ return `<blockquote${align}>${inner}</blockquote>`;
161
+ case "table":
162
+ return `<table><tbody>${inner}</tbody></table>`;
163
+ case "tableRow":
164
+ return `<tr>${inner}</tr>`;
165
+ case "tableHeader":
166
+ return `<th${align}>${inner}</th>`;
167
+ case "tableCell":
168
+ return `<td${align}>${inner}</td>`;
169
+ case "codeBlock":
170
+ return `<pre><code${lang ? ` class="language-${escapeHtml(lang)}"` : ""}>${inner}</code></pre>`;
171
+ case "horizontalRule":
172
+ return `<hr />`;
173
+ case "hardBreak":
174
+ return `<br />`;
175
+ case "image": {
176
+ const src = (node.attrs?.src as string) ?? "";
177
+ const alt = (node.attrs?.alt as string) ?? "";
178
+ return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" />`;
179
+ }
180
+ default:
181
+ return inner;
182
+ }
183
+ }
184
+
185
+ const renderedContent = $derived(renderContent());
186
+
187
+ async function fetchShare() {
188
+ loading = true;
189
+ try {
190
+ const headers: Record<string, string> = {};
191
+ if (password) headers["x-share-password"] = password;
192
+
193
+ const res = await kitFetch(`/api/share/${token}`, { headers });
194
+ const data = await res.json();
195
+
196
+ if (res.status === 401) {
197
+ // Server signals the share is password-protected when `requiresPassword`
198
+ // is true on the response (initial load with no password supplied).
199
+ // A wrong password comes back as 401 without that flag — we must keep
200
+ // the password form visible and surface a retryable inline error
201
+ // instead of replacing the whole view with a fatal error banner.
202
+ requiresPassword = true;
203
+ password = "";
204
+ error = data.requiresPassword ? "" : m.share_password_incorrect();
205
+ return;
206
+ }
207
+ if (!res.ok) {
208
+ // Non-password failure (expired, missing share, server error). Drop
209
+ // the password form so the user sees the banner with a way home.
210
+ requiresPassword = false;
211
+ password = "";
212
+ error = data.error ?? m.share_load_error();
213
+ return;
214
+ }
215
+ // Success — clear transient auth state so the document view renders.
216
+ shareData = data;
217
+ requiresPassword = false;
218
+ password = "";
219
+ error = "";
220
+ } catch (_e) {
221
+ requiresPassword = false;
222
+ password = "";
223
+ error = m.share_network_error();
224
+ } finally {
225
+ loading = false;
226
+ }
227
+ }
228
+
229
+ function copyUrl() {
230
+ navigator.clipboard.writeText(window.location.href);
231
+ copied = true;
232
+ setTimeout(() => {
233
+ copied = false;
234
+ }, 2000);
235
+ }
236
+
237
+ function copyText() {
238
+ const text = shareData?.data?.content ?? "";
239
+ if (!text) return;
240
+ navigator.clipboard.writeText(text);
241
+ copiedText = true;
242
+ setTimeout(() => {
243
+ copiedText = false;
244
+ }, 2000);
245
+ }
246
+
247
+ fetchShare();
248
+ </script>
249
+
250
+ <svelte:head>
251
+ <title>{m.share_page_title()}</title>
252
+ </svelte:head>
253
+
254
+ <div class="flex min-h-screen items-center justify-center bg-background p-4">
255
+ {#if loading}
256
+ <div class="text-muted-foreground">{m.action_loading()}</div>
257
+ {:else if shareData}
258
+ <div class="w-full max-w-3xl space-y-6">
259
+ <div class="flex items-center justify-between">
260
+ <div class="flex items-center gap-2 text-sm text-muted-foreground">
261
+ {#if shareData.type === "document"}
262
+ <FileText class="h-4 w-4" />
263
+ {:else}
264
+ <Folder class="h-4 w-4" />
265
+ {/if}
266
+ {m.share_via_label()}
267
+ </div>
268
+ <div class="flex items-center gap-2">
269
+ {#if shareData.type === "document" && shareData.data?.content}
270
+ <button
271
+ onclick={copyText}
272
+ class="flex items-center gap-1 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-accent"
273
+ >
274
+ {#if copiedText}
275
+ <Check class="h-3 w-3" /> {m.share_copied()}
276
+ {:else}
277
+ <Copy class="h-3 w-3" /> Copy Text
278
+ {/if}
279
+ </button>
280
+ {/if}
281
+ <button
282
+ onclick={copyUrl}
283
+ class="flex items-center gap-1 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-accent"
284
+ >
285
+ {#if copied}
286
+ <Check class="h-3 w-3" /> {m.share_copied()}
287
+ {:else}
288
+ <Copy class="h-3 w-3" /> {m.share_copy_link()}
289
+ {/if}
290
+ </button>
291
+ </div>
292
+ </div>
293
+
294
+ {#if shareData.type === "document"}
295
+ <article class="rounded-lg border border-border bg-card p-8 shadow-sm">
296
+ <h1 class="mb-6 text-3xl font-bold tracking-tight">{shareData.data?.title ?? ""}</h1>
297
+ {#if renderedContent}
298
+ <div class="shared-doc-body">
299
+ {@html renderedContent}
300
+ </div>
301
+ {:else}
302
+ <p class="text-muted-foreground">{m.share_empty_document()}</p>
303
+ {/if}
304
+ </article>
305
+ {:else}
306
+ <div class="rounded-lg border border-border bg-card p-6 shadow-sm">
307
+ <h1 class="mb-4 text-2xl font-bold">{shareData.data?.name ?? ""}</h1>
308
+ {#if shareData.data?.documents && shareData.data.documents.length > 0}
309
+ <ul class="space-y-2">
310
+ {#each shareData.data.documents as doc}
311
+ <li class="flex items-center gap-2 rounded-md border border-border px-3 py-2">
312
+ <FileText class="h-4 w-4 text-muted-foreground" />
313
+ <span>{doc.title}</span>
314
+ </li>
315
+ {/each}
316
+ </ul>
317
+ {:else}
318
+ <p class="text-muted-foreground">{m.share_folder_empty()}</p>
319
+ {/if}
320
+ </div>
321
+ {/if}
322
+ </div>
323
+ {:else if requiresPassword}
324
+ <form
325
+ onsubmit={(e) => { e.preventDefault(); fetchShare(); }}
326
+ class="w-full max-w-sm space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm"
327
+ >
328
+ <div class="flex items-center gap-2 text-lg font-semibold">
329
+ <Lock class="h-5 w-5" />
330
+ {m.share_password_required()}
331
+ </div>
332
+ {#if error}
333
+ <div
334
+ role="alert"
335
+ class="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
336
+ >
337
+ {error}
338
+ </div>
339
+ {/if}
340
+ <input
341
+ type="password"
342
+ bind:value={password}
343
+ placeholder={m.share_password_placeholder()}
344
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
345
+ />
346
+ <button
347
+ type="submit"
348
+ class="inline-flex h-9 w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90"
349
+ >
350
+ {m.share_access_button()}
351
+ </button>
352
+ </form>
353
+ {:else if error}
354
+ <div class="w-full max-w-md rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center">
355
+ <p class="text-lg font-medium text-destructive">{error}</p>
356
+ <a href="/" class="mt-4 inline-block text-sm text-primary underline">{m.share_go_home()}</a>
357
+ </div>
358
+ {/if}
359
+ </div>
360
+
361
+ <style>
362
+ /* Shared document body — minimal styles for the markdown/JSON HTML output
363
+ rendered via {@html}. The `prose` class from @tailwindcss/typography
364
+ is not installed in this project, so we provide the essentials. */
365
+ .shared-doc-body {
366
+ color: var(--foreground);
367
+ line-height: 1.7;
368
+ font-size: 1rem;
369
+ word-wrap: break-word;
370
+ }
371
+ .shared-doc-body :global(h1) {
372
+ font-size: 1.875rem;
373
+ font-weight: 700;
374
+ margin: 1.5rem 0 0.75rem;
375
+ letter-spacing: -0.02em;
376
+ }
377
+ .shared-doc-body :global(h2) {
378
+ font-size: 1.5rem;
379
+ font-weight: 700;
380
+ margin: 1.25rem 0 0.5rem;
381
+ }
382
+ .shared-doc-body :global(h3) {
383
+ font-size: 1.25rem;
384
+ font-weight: 600;
385
+ margin: 1rem 0 0.5rem;
386
+ }
387
+ .shared-doc-body :global(p) {
388
+ margin: 0.5rem 0;
389
+ }
390
+ .shared-doc-body :global(ul),
391
+ .shared-doc-body :global(ol) {
392
+ padding-left: 1.5rem;
393
+ margin: 0.5rem 0;
394
+ }
395
+ .shared-doc-body :global(ul) {
396
+ list-style-type: disc;
397
+ }
398
+ .shared-doc-body :global(ol) {
399
+ list-style-type: decimal;
400
+ }
401
+ .shared-doc-body :global(li) {
402
+ margin: 0.25rem 0;
403
+ }
404
+ /* Task lists — no bullet, checkbox + content laid out in a row. */
405
+ .shared-doc-body :global(ul[data-type="taskList"]) {
406
+ list-style: none;
407
+ padding-left: 0.25rem;
408
+ }
409
+ .shared-doc-body :global(ul[data-type="taskList"] li) {
410
+ display: flex;
411
+ align-items: flex-start;
412
+ gap: 0.5rem;
413
+ }
414
+ .shared-doc-body :global(ul[data-type="taskList"] li > label) {
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ height: 1.7em;
419
+ margin: 0;
420
+ }
421
+ .shared-doc-body :global(ul[data-type="taskList"] li > div > p) {
422
+ margin: 0;
423
+ }
424
+ .shared-doc-body :global(ul[data-type="taskList"] input[type="checkbox"]) {
425
+ accent-color: var(--primary);
426
+ }
427
+ .shared-doc-body :global(blockquote) {
428
+ border-left: 3px solid var(--border);
429
+ padding-left: 1rem;
430
+ margin: 0.75rem 0;
431
+ color: var(--muted-foreground);
432
+ font-style: italic;
433
+ }
434
+ .shared-doc-body :global(code) {
435
+ background: var(--muted);
436
+ padding: 0.125rem 0.375rem;
437
+ border-radius: 4px;
438
+ font-size: 0.875em;
439
+ font-family: "Fira Code", "Consolas", monospace;
440
+ }
441
+ .shared-doc-body :global(pre) {
442
+ background: var(--muted);
443
+ color: var(--foreground);
444
+ border: 1px solid var(--border);
445
+ padding: 1rem;
446
+ border-radius: 8px;
447
+ font-family: "Fira Code", "Consolas", monospace;
448
+ font-size: 0.875rem;
449
+ line-height: 1.6;
450
+ overflow-x: auto;
451
+ margin: 0.75rem 0;
452
+ }
453
+ .shared-doc-body :global(pre code) {
454
+ background: transparent;
455
+ padding: 0;
456
+ font-size: inherit;
457
+ color: inherit;
458
+ }
459
+ .shared-doc-body :global(hr) {
460
+ border: none;
461
+ border-top: 1px solid var(--border);
462
+ margin: 1.5rem 0;
463
+ }
464
+ .shared-doc-body :global(a) {
465
+ color: var(--primary);
466
+ text-decoration: underline;
467
+ }
468
+ .shared-doc-body :global(img) {
469
+ max-width: 100%;
470
+ height: auto;
471
+ border-radius: 6px;
472
+ margin: 0.75rem 0;
473
+ display: block;
474
+ }
475
+ .shared-doc-body :global(mark) {
476
+ background-color: var(--highlight-default, #fde68a);
477
+ border-radius: 2px;
478
+ padding: 0 2px;
479
+ }
480
+ .shared-doc-body :global(table) {
481
+ border-collapse: collapse;
482
+ width: 100%;
483
+ margin: 0.75rem 0;
484
+ }
485
+ .shared-doc-body :global(th),
486
+ .shared-doc-body :global(td) {
487
+ border: 1px solid var(--border);
488
+ padding: 0.4rem 0.6rem;
489
+ text-align: left;
490
+ vertical-align: top;
491
+ }
492
+ .shared-doc-body :global(th) {
493
+ background: var(--muted);
494
+ font-weight: 600;
495
+ }
496
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { PageLoad } from "./$types";
2
+
3
+ export const load: PageLoad = async ({ params, fetch }) => {
4
+ return { token: params.token, fetch };
5
+ };
@@ -0,0 +1,175 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { goto } from "$app/navigation";
4
+ import {
5
+ deleteAccount,
6
+ getEmbeddingConfig,
7
+ getProfile,
8
+ updateEmbeddingConfig,
9
+ updateProfile,
10
+ } from "$lib/api/settings";
11
+ import { signOut } from "$lib/auth-client";
12
+ import * as m from "$lib/paraglide/messages.js";
13
+ import { themeStore } from "$lib/stores/theme.svelte";
14
+
15
+ let loggingOut = $state(false);
16
+
17
+ async function handleLogout() {
18
+ loggingOut = true;
19
+ try {
20
+ await signOut();
21
+ goto("/login");
22
+ } catch {
23
+ loggingOut = false;
24
+ }
25
+ }
26
+
27
+ let activeTab = $state<"profile" | "embedding" | "danger">("profile");
28
+ let saveStatus = $state<"idle" | "saving" | "saved" | "error">("idle");
29
+
30
+ let name = $state("User");
31
+ let email = $state("user@example.com");
32
+ let embeddingProvider = $state("ollama");
33
+ let embeddingModel = $state("nomic-embed-text");
34
+ let deleteConfirm = $state(false);
35
+
36
+ onMount(async () => {
37
+ try {
38
+ const profile = await getProfile();
39
+ if (profile.name) name = profile.name;
40
+ if (profile.email) email = profile.email;
41
+ } catch {
42
+ // Use defaults
43
+ }
44
+
45
+ const config = getEmbeddingConfig();
46
+ embeddingProvider = config.provider;
47
+ embeddingModel = config.model;
48
+ });
49
+
50
+ async function saveProfile() {
51
+ saveStatus = "saving";
52
+ try {
53
+ await updateProfile({ name });
54
+ saveStatus = "saved";
55
+ setTimeout(() => {
56
+ saveStatus = "idle";
57
+ }, 2000);
58
+ } catch {
59
+ saveStatus = "error";
60
+ }
61
+ }
62
+
63
+ function saveEmbedding() {
64
+ saveStatus = "saving";
65
+ try {
66
+ updateEmbeddingConfig({
67
+ provider: embeddingProvider as "ollama" | "openrouter" | "voyage",
68
+ model: embeddingModel,
69
+ });
70
+ saveStatus = "saved";
71
+ setTimeout(() => {
72
+ saveStatus = "idle";
73
+ }, 2000);
74
+ } catch {
75
+ saveStatus = "error";
76
+ }
77
+ }
78
+
79
+ async function handleDeleteAccount() {
80
+ try {
81
+ await deleteAccount();
82
+ goto("/login");
83
+ } catch {
84
+ alert(m.settings_delete_failed());
85
+ }
86
+ }
87
+ </script>
88
+
89
+ <svelte:head>
90
+ <title>{m.settings_page_title()}</title>
91
+ </svelte:head>
92
+
93
+ <div class="mx-auto max-w-2xl p-6">
94
+ <h1 class="mb-6 text-2xl font-semibold">{m.settings_title()}</h1>
95
+
96
+ <div class="mb-6 flex gap-1 rounded-lg border border-border p-1">
97
+ {#each [["profile", m.settings_profile()], ["embedding", m.settings_tab_embedding()], ["danger", m.settings_tab_danger()]] as [key, label]}
98
+ <button
99
+ onclick={() => { activeTab = key as typeof activeTab; }}
100
+ class="flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors
101
+ {activeTab === key ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
102
+ >
103
+ {label}
104
+ </button>
105
+ {/each}
106
+ </div>
107
+
108
+ {#if activeTab === "profile"}
109
+ <div class="space-y-4 rounded-lg border border-border bg-card p-6">
110
+ <h2 class="text-lg font-medium">{m.settings_profile()}</h2>
111
+ <div class="space-y-2">
112
+ <label for="name" class="text-sm font-medium">{m.settings_name()}</label>
113
+ <input id="name" bind:value={name} class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
114
+ </div>
115
+ <div class="space-y-2">
116
+ <label for="email" class="text-sm font-medium">{m.settings_email()}</label>
117
+ <input id="email" type="email" bind:value={email} class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" disabled />
118
+ </div>
119
+ <div class="flex items-center gap-3">
120
+ <button onclick={saveProfile} disabled={saveStatus === "saving"} class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
121
+ {saveStatus === "saving" ? m.settings_saving() : saveStatus === "saved" ? m.settings_saved_status() : m.settings_save()}
122
+ </button>
123
+ <button
124
+ id="logout-button"
125
+ onclick={handleLogout}
126
+ disabled={loggingOut}
127
+ class="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-destructive hover:text-destructive-foreground disabled:opacity-50"
128
+ >
129
+ {loggingOut ? "…" : m.auth_logout()}
130
+ </button>
131
+ </div>
132
+ </div>
133
+ {/if}
134
+
135
+ {#if activeTab === "embedding"}
136
+ <div class="space-y-4 rounded-lg border border-border bg-card p-6">
137
+ <h2 class="text-lg font-medium">{m.settings_embedding_title()}</h2>
138
+ <div class="space-y-2">
139
+ <label for="provider" class="text-sm font-medium">{m.settings_embedding_provider()}</label>
140
+ <select id="provider" bind:value={embeddingProvider} class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
141
+ <option value="ollama">{m.settings_embedding_provider_ollama()}</option>
142
+ <option value="openrouter">{m.settings_embedding_provider_openrouter()}</option>
143
+ <option value="voyage">{m.settings_embedding_provider_voyage()}</option>
144
+ </select>
145
+ </div>
146
+ <div class="space-y-2">
147
+ <label for="model" class="text-sm font-medium">{m.settings_embedding_model()}</label>
148
+ <input id="model" bind:value={embeddingModel} placeholder="nomic-embed-text" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
149
+ </div>
150
+ <button onclick={saveEmbedding} disabled={saveStatus === "saving"} class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
151
+ {saveStatus === "saving" ? m.settings_saving() : saveStatus === "saved" ? m.settings_saved_status() : m.settings_embedding_save()}
152
+ </button>
153
+ </div>
154
+ {/if}
155
+
156
+ {#if activeTab === "danger"}
157
+ <div class="space-y-4 rounded-lg border border-destructive/50 bg-card p-6">
158
+ <h2 class="text-lg font-medium text-destructive">{m.settings_danger_title()}</h2>
159
+ <p class="text-sm text-muted-foreground">{m.settings_danger_description()}</p>
160
+ {#if !deleteConfirm}
161
+ <button onclick={() => { deleteConfirm = true; }} class="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:bg-destructive/90">{m.settings_delete_account()}</button>
162
+ {:else}
163
+ <div class="flex items-center gap-3">
164
+ <span class="text-sm font-medium">{m.settings_delete_confirm_text()}</span>
165
+ <button onclick={handleDeleteAccount} class="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90">{m.settings_delete_confirm_yes()}</button>
166
+ <button onclick={() => { deleteConfirm = false; }} class="rounded-md border border-border px-3 py-1.5 text-sm font-medium hover:bg-accent">{m.action_cancel()}</button>
167
+ </div>
168
+ {/if}
169
+ </div>
170
+ {/if}
171
+
172
+ <p class="mt-4 text-xs text-muted-foreground">
173
+ {m.settings_theme()}: {themeStore.value} ({themeStore.isDark ? "dark" : "light"})
174
+ </p>
175
+ </div>
Binary file
Binary file
@@ -0,0 +1,15 @@
1
+ import adapter from "@sveltejs/adapter-node";
2
+ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ preprocess: [vitePreprocess()],
7
+ kit: {
8
+ adapter: adapter(),
9
+ alias: {
10
+ "$lib/*": "src/lib/*",
11
+ },
12
+ },
13
+ };
14
+
15
+ export default config;