@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.
- package/.all-contributorsrc +18 -0
- package/.claude/settings.local.json +61 -0
- package/.dockerignore +113 -0
- package/.env.example +68 -0
- package/.github/FUNDING.yml +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
- package/.github/dependabot.yml +136 -0
- package/.github/pull_request_template.md +96 -0
- package/.github/workflows/ci.yml +283 -0
- package/AGENTS.md +237 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +77 -0
- package/Caddyfile +50 -0
- package/Dockerfile.backend +60 -0
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/RELEASE_CHECKLIST.md +34 -0
- package/SECURITY.md +60 -0
- package/backend/package.json +43 -0
- package/backend/src/__tests__/auth-helpers.test.ts +51 -0
- package/backend/src/__tests__/chunker.test.ts +65 -0
- package/backend/src/__tests__/config.test.ts +91 -0
- package/backend/src/__tests__/csrf.test.ts +91 -0
- package/backend/src/__tests__/embedding.test.ts +48 -0
- package/backend/src/__tests__/rate-limit.test.ts +46 -0
- package/backend/src/__tests__/routes.test.ts +38 -0
- package/backend/src/__tests__/schema.test.ts +31 -0
- package/backend/src/__tests__/validation.test.ts +556 -0
- package/backend/src/api/middleware/auth.ts +56 -0
- package/backend/src/api/middleware/csrf.ts +91 -0
- package/backend/src/api/middleware/rate-limit.ts +77 -0
- package/backend/src/api/middleware/webhook-verify.ts +22 -0
- package/backend/src/api/routes/attachments.ts +280 -0
- package/backend/src/api/routes/auth.ts +52 -0
- package/backend/src/api/routes/collaboration.ts +121 -0
- package/backend/src/api/routes/documents.ts +664 -0
- package/backend/src/api/routes/folders.ts +226 -0
- package/backend/src/api/routes/search.ts +354 -0
- package/backend/src/api/routes/share.ts +512 -0
- package/backend/src/api/routes/tags.ts +247 -0
- package/backend/src/api/routes/versions.ts +99 -0
- package/backend/src/api/routes/webhooks.ts +43 -0
- package/backend/src/embedding/chunker.ts +74 -0
- package/backend/src/embedding/index.ts +117 -0
- package/backend/src/embedding/providers/ollama.ts +63 -0
- package/backend/src/embedding/providers/openrouter.ts +71 -0
- package/backend/src/embedding/utils.ts +13 -0
- package/backend/src/embedding/worker.ts +89 -0
- package/backend/src/index.ts +147 -0
- package/backend/src/lib/auth-helpers.ts +27 -0
- package/backend/src/lib/auth.ts +35 -0
- package/backend/src/lib/config.ts +73 -0
- package/backend/src/lib/db.ts +7 -0
- package/backend/src/lib/embedding-queue.ts +12 -0
- package/backend/src/lib/logger.ts +18 -0
- package/backend/src/lib/markdown-to-doc.ts +45 -0
- package/backend/src/lib/minio.ts +46 -0
- package/backend/src/lib/redis.ts +19 -0
- package/backend/src/lib/yjs-provider.ts +182 -0
- package/backend/tests/integration/_harness.ts +754 -0
- package/backend/tests/integration/auth.test.ts +296 -0
- package/backend/tests/integration/routes.documents.test.ts +459 -0
- package/backend/tests/integration/routes.folders.test.ts +337 -0
- package/backend/tests/integration/routes.search.test.ts +322 -0
- package/backend/tests/integration/routes.share.test.ts +773 -0
- package/backend/tests/integration/routes.tags.test.ts +425 -0
- package/backend/tests/integration/routes.versions.test.ts +233 -0
- package/backend/tsconfig.json +18 -0
- package/docker-compose.yml +218 -0
- package/docs/API.md +328 -0
- package/docs/ARCHITECTURE.md +75 -0
- package/docs/DEPLOYMENT.md +113 -0
- package/docs/PRODUCTION_STATUS.md +61 -0
- package/docs/openapi.json +385 -0
- package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
- package/frontend/.svelte-kit.old/env.d.ts +1 -0
- package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
- package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
- package/frontend/.svelte-kit.old/generated/root.js +3 -0
- package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
- package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
- package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
- package/frontend/.svelte-kit.old/tsconfig.json +59 -0
- package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
- package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
- package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
- package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
- package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
- package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
- package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
- package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
- package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
- package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
- package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
- package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
- package/frontend/Dockerfile +44 -0
- package/frontend/biome.json +40 -0
- package/frontend/components.json +18 -0
- package/frontend/messages/en.json +434 -0
- package/frontend/package.json +70 -0
- package/frontend/project.inlang/settings.json +12 -0
- package/frontend/src/app.css +6 -0
- package/frontend/src/app.d.ts +13 -0
- package/frontend/src/app.html +30 -0
- package/frontend/src/hooks.server.ts +10 -0
- package/frontend/src/hooks.ts +10 -0
- package/frontend/src/lib/api/attachments.ts +45 -0
- package/frontend/src/lib/api/client.test.ts +15 -0
- package/frontend/src/lib/api/client.ts +57 -0
- package/frontend/src/lib/api/documents.ts +83 -0
- package/frontend/src/lib/api/folders.ts +180 -0
- package/frontend/src/lib/api/search.test.ts +52 -0
- package/frontend/src/lib/api/search.ts +128 -0
- package/frontend/src/lib/api/settings.ts +95 -0
- package/frontend/src/lib/api/share.ts +71 -0
- package/frontend/src/lib/api/tags.test.ts +91 -0
- package/frontend/src/lib/api/tags.ts +87 -0
- package/frontend/src/lib/auth-client.ts +10 -0
- package/frontend/src/lib/collaboration.ts +63 -0
- package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
- package/frontend/src/lib/components/DatePicker.svelte +322 -0
- package/frontend/src/lib/components/DocumentCard.svelte +166 -0
- package/frontend/src/lib/components/EmptyState.svelte +49 -0
- package/frontend/src/lib/components/FolderCard.svelte +93 -0
- package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
- package/frontend/src/lib/components/SearchBar.svelte +47 -0
- package/frontend/src/lib/components/SearchResult.svelte +115 -0
- package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
- package/frontend/src/lib/components/ShareDialog.svelte +158 -0
- package/frontend/src/lib/components/ShareLink.svelte +98 -0
- package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
- package/frontend/src/lib/components/VersionDiff.svelte +55 -0
- package/frontend/src/lib/components/VersionHistory.svelte +96 -0
- package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
- package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
- package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
- package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
- package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
- package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
- package/frontend/src/lib/components/editor/markdown.ts +38 -0
- package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
- package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
- package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
- package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
- package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
- package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
- package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
- package/frontend/src/lib/stores/theme.svelte.ts +97 -0
- package/frontend/src/lib/svelte.d.ts +6 -0
- package/frontend/src/lib/types.ts +44 -0
- package/frontend/src/lib/utils/clipboard.ts +17 -0
- package/frontend/src/lib/utils/strip-markdown.ts +59 -0
- package/frontend/src/lib/utils.ts +33 -0
- package/frontend/src/routes/(app)/+layout.svelte +17 -0
- package/frontend/src/routes/(app)/+page.server.ts +10 -0
- package/frontend/src/routes/(app)/+page.svelte +303 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
- package/frontend/src/routes/(app)/search/+page.svelte +593 -0
- package/frontend/src/routes/(app)/search/+page.ts +25 -0
- package/frontend/src/routes/+error.svelte +12 -0
- package/frontend/src/routes/+layout.svelte +18 -0
- package/frontend/src/routes/+layout.ts +2 -0
- package/frontend/src/routes/api/[...path]/+server.ts +111 -0
- package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
- package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
- package/frontend/src/routes/folders/[id]/+page.ts +14 -0
- package/frontend/src/routes/login/+page.svelte +90 -0
- package/frontend/src/routes/register/+page.svelte +97 -0
- package/frontend/src/routes/s/[token]/+page.svelte +496 -0
- package/frontend/src/routes/s/[token]/+page.ts +5 -0
- package/frontend/src/routes/settings/+page.svelte +175 -0
- package/frontend/static/favicon.png +0 -0
- package/frontend/static/logo.png +0 -0
- package/frontend/svelte.config.js +15 -0
- package/frontend/tsconfig.json +15 -0
- package/frontend/vite.config.ts +25 -0
- package/init.sql +9 -0
- package/logo.png +0 -0
- package/package.json +39 -0
- package/package.public.json +39 -0
- package/packages/db/drizzle.config.ts +10 -0
- package/packages/db/package.json +30 -0
- package/packages/db/src/client.ts +9 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
- package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
- package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
- package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
- package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
- package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
- package/packages/db/src/migrations/meta/_journal.json +27 -0
- package/packages/db/src/schema.ts +378 -0
- package/packages/db/tsconfig.json +17 -0
- package/scripts/export-openapi.ts +37 -0
- package/scripts/health-check.sh +75 -0
- package/scripts/migrate.sh +135 -0
- package/scripts/prework_backup.sh +25 -0
- package/scripts/release.sh +83 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getDocument } from "$lib/api/documents";
|
|
2
|
+
import type { PageLoad } from "./$types";
|
|
3
|
+
|
|
4
|
+
export const load: PageLoad = async ({ params, fetch }) => {
|
|
5
|
+
try {
|
|
6
|
+
const document = await getDocument(params.id, fetch);
|
|
7
|
+
return { document };
|
|
8
|
+
} catch {
|
|
9
|
+
// Fallback when backend unavailable
|
|
10
|
+
return {
|
|
11
|
+
document: {
|
|
12
|
+
id: params.id,
|
|
13
|
+
title: "Untitled Document",
|
|
14
|
+
content: "",
|
|
15
|
+
folderId: null,
|
|
16
|
+
folderName: "",
|
|
17
|
+
tags: [],
|
|
18
|
+
createdAt: new Date().toISOString(),
|
|
19
|
+
updatedAt: new Date().toISOString(),
|
|
20
|
+
excerpt: "",
|
|
21
|
+
} as Awaited<ReturnType<typeof getDocument>>,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Calendar,
|
|
4
|
+
ChevronLeft,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
FileSearch,
|
|
7
|
+
Folder,
|
|
8
|
+
Loader2,
|
|
9
|
+
RotateCcw,
|
|
10
|
+
Search,
|
|
11
|
+
SlidersHorizontal,
|
|
12
|
+
Tag,
|
|
13
|
+
X,
|
|
14
|
+
} from "lucide-svelte";
|
|
15
|
+
import { goto } from "$app/navigation";
|
|
16
|
+
import { getFilterOptions, type SearchResponse, search } from "$lib/api/search";
|
|
17
|
+
import DatePicker from "$lib/components/DatePicker.svelte";
|
|
18
|
+
import SearchResult from "$lib/components/SearchResult.svelte";
|
|
19
|
+
import * as m from "$lib/paraglide/messages.js";
|
|
20
|
+
import { getSelectedTagName } from "$lib/stores/tag-store.svelte";
|
|
21
|
+
|
|
22
|
+
const { data } = $props();
|
|
23
|
+
|
|
24
|
+
// --- State -------------------------------------------------------------------
|
|
25
|
+
let query = $state("");
|
|
26
|
+
let activeFolder = $state("");
|
|
27
|
+
let activeTags = $state<string[]>([]);
|
|
28
|
+
let dateFrom = $state("");
|
|
29
|
+
let dateTo = $state("");
|
|
30
|
+
let currentPage = $state(1);
|
|
31
|
+
let sortOrder = $state<
|
|
32
|
+
"relevance" | "date_desc" | "date_asc" | "name_asc" | "name_desc"
|
|
33
|
+
>("relevance");
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
query = data.query ?? "";
|
|
37
|
+
activeFolder = data.filters?.folder ?? "";
|
|
38
|
+
activeTags = data.filters?.tags ?? [];
|
|
39
|
+
dateFrom = data.filters?.dateFrom ?? "";
|
|
40
|
+
dateTo = data.filters?.dateTo ?? "";
|
|
41
|
+
currentPage = data.page ?? 1;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let searchResponse = $state<SearchResponse | null>(null);
|
|
45
|
+
let loading = $state(false);
|
|
46
|
+
let showFilters = $state(false);
|
|
47
|
+
|
|
48
|
+
let folders = $state<string[]>([]);
|
|
49
|
+
let tags = $state<Array<{ id: string; name: string; color: string | null }>>(
|
|
50
|
+
[],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const PAGE_SIZE = 5;
|
|
54
|
+
|
|
55
|
+
// --- Derived -----------------------------------------------------------------
|
|
56
|
+
const totalPages = $derived(
|
|
57
|
+
searchResponse ? Math.ceil(searchResponse.total / PAGE_SIZE) : 0,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const hasActiveFilters = $derived(
|
|
61
|
+
activeFolder !== "" ||
|
|
62
|
+
activeTags.length > 0 ||
|
|
63
|
+
dateFrom !== "" ||
|
|
64
|
+
dateTo !== "",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// --- Effects -----------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
// Load filter options on mount
|
|
70
|
+
$effect(() => {
|
|
71
|
+
getFilterOptions().then((opts) => {
|
|
72
|
+
folders = opts.folders;
|
|
73
|
+
tags = opts.tags;
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Run search when query or filters change
|
|
78
|
+
$effect(() => {
|
|
79
|
+
const q = data.query;
|
|
80
|
+
const p = data.page;
|
|
81
|
+
const sort = sortOrder;
|
|
82
|
+
const folder = activeFolder;
|
|
83
|
+
const tags = activeTags;
|
|
84
|
+
const from = dateFrom;
|
|
85
|
+
const to = dateTo;
|
|
86
|
+
// Merge the shared selected tag (set from the sidebar TagList) into the
|
|
87
|
+
// search filter so a tag picked anywhere also narrows search results.
|
|
88
|
+
const sharedTag = getSelectedTagName();
|
|
89
|
+
const effectiveTags =
|
|
90
|
+
sharedTag && !tags.includes(sharedTag) ? [...tags, sharedTag] : tags;
|
|
91
|
+
|
|
92
|
+
if (!q) {
|
|
93
|
+
searchResponse = null;
|
|
94
|
+
loading = false;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
loading = true;
|
|
99
|
+
|
|
100
|
+
search(q, p, PAGE_SIZE, sort, {
|
|
101
|
+
folder: folder || undefined,
|
|
102
|
+
tags: effectiveTags.length > 0 ? effectiveTags : undefined,
|
|
103
|
+
dateFrom: from || undefined,
|
|
104
|
+
dateTo: to || undefined,
|
|
105
|
+
}).then((res) => {
|
|
106
|
+
searchResponse = res;
|
|
107
|
+
loading = false;
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// --- Helpers -----------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function buildUrl(overrides: Record<string, string | undefined>) {
|
|
114
|
+
const params = new URLSearchParams();
|
|
115
|
+
|
|
116
|
+
const q = overrides.q ?? query;
|
|
117
|
+
const folder = overrides.folder ?? activeFolder;
|
|
118
|
+
const t = overrides.tags ?? activeTags.join(",");
|
|
119
|
+
const df = overrides.dateFrom ?? dateFrom;
|
|
120
|
+
const dt = overrides.dateTo ?? dateTo;
|
|
121
|
+
const p = overrides.page ?? String(currentPage);
|
|
122
|
+
|
|
123
|
+
if (q) params.set("q", q);
|
|
124
|
+
if (folder) params.set("folder", folder);
|
|
125
|
+
if (t) params.set("tags", t);
|
|
126
|
+
if (df) params.set("dateFrom", df);
|
|
127
|
+
if (dt) params.set("dateTo", dt);
|
|
128
|
+
if (p && p !== "1") params.set("page", p);
|
|
129
|
+
|
|
130
|
+
return `/search?${params.toString()}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function handleSubmit(e: SubmitEvent) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
currentPage = 1;
|
|
136
|
+
goto(buildUrl({ q: query, page: "1" }), { replaceState: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
140
|
+
if (e.key === "Escape") {
|
|
141
|
+
if (showFilters) {
|
|
142
|
+
showFilters = false;
|
|
143
|
+
} else if (query || hasActiveFilters) {
|
|
144
|
+
clearSearch();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function clearSearch() {
|
|
150
|
+
query = "";
|
|
151
|
+
activeFolder = "";
|
|
152
|
+
activeTags = [];
|
|
153
|
+
dateFrom = "";
|
|
154
|
+
dateTo = "";
|
|
155
|
+
currentPage = 1;
|
|
156
|
+
goto("/search", { replaceState: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toggleFolder(folder: string) {
|
|
160
|
+
activeFolder = activeFolder === folder ? "" : folder;
|
|
161
|
+
currentPage = 1;
|
|
162
|
+
goto(buildUrl({ folder: activeFolder, page: "1" }), { replaceState: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toggleTag(tag: string) {
|
|
166
|
+
activeTags = activeTags.includes(tag)
|
|
167
|
+
? activeTags.filter((t) => t !== tag)
|
|
168
|
+
: [...activeTags, tag];
|
|
169
|
+
currentPage = 1;
|
|
170
|
+
goto(buildUrl({ tags: activeTags.join(","), page: "1" }), {
|
|
171
|
+
replaceState: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function applyDateRange() {
|
|
176
|
+
currentPage = 1;
|
|
177
|
+
goto(buildUrl({ dateFrom, dateTo, page: "1" }), { replaceState: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function clearFilters() {
|
|
181
|
+
activeFolder = "";
|
|
182
|
+
activeTags = [];
|
|
183
|
+
dateFrom = "";
|
|
184
|
+
dateTo = "";
|
|
185
|
+
currentPage = 1;
|
|
186
|
+
goto(
|
|
187
|
+
buildUrl({
|
|
188
|
+
folder: undefined,
|
|
189
|
+
tags: undefined,
|
|
190
|
+
dateFrom: undefined,
|
|
191
|
+
dateTo: undefined,
|
|
192
|
+
page: "1",
|
|
193
|
+
}),
|
|
194
|
+
{
|
|
195
|
+
replaceState: true,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function goToPage(page: number) {
|
|
201
|
+
currentPage = page;
|
|
202
|
+
goto(buildUrl({ page: String(page) }), { replaceState: true });
|
|
203
|
+
}
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
207
|
+
|
|
208
|
+
<svelte:head>
|
|
209
|
+
<title>{data.query ? m.search_title_with_query({query: data.query}) : m.search_title()} - {m.app_name()}</title>
|
|
210
|
+
</svelte:head>
|
|
211
|
+
|
|
212
|
+
<div class="mx-auto max-w-6xl px-4 py-8">
|
|
213
|
+
<!-- Search input -->
|
|
214
|
+
<form onsubmit={handleSubmit} class="relative mb-6">
|
|
215
|
+
<Search
|
|
216
|
+
class="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
|
|
217
|
+
/>
|
|
218
|
+
<input
|
|
219
|
+
type="text"
|
|
220
|
+
bind:value={query}
|
|
221
|
+
placeholder={m.search_input_placeholder()}
|
|
222
|
+
class="h-14 w-full rounded-xl border border-input bg-background pl-12 pr-24 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
223
|
+
/>
|
|
224
|
+
<div class="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-1">
|
|
225
|
+
{#if query}
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
onclick={clearSearch}
|
|
229
|
+
class="rounded-md p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
230
|
+
aria-label={m.search_clear()}
|
|
231
|
+
>
|
|
232
|
+
<X class="h-4 w-4" />
|
|
233
|
+
</button>
|
|
234
|
+
{/if}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onclick={() => (showFilters = !showFilters)}
|
|
238
|
+
class="rounded-md p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground lg:hidden"
|
|
239
|
+
aria-label={m.search_toggle_filters()}
|
|
240
|
+
>
|
|
241
|
+
<SlidersHorizontal class="h-4 w-4" />
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
</form>
|
|
245
|
+
|
|
246
|
+
<div class="flex gap-8">
|
|
247
|
+
<!-- Filter sidebar -->
|
|
248
|
+
<aside
|
|
249
|
+
class="w-56 shrink-0 space-y-6"
|
|
250
|
+
>
|
|
251
|
+
{@render filterPanel()}
|
|
252
|
+
</aside>
|
|
253
|
+
|
|
254
|
+
<!-- Mobile filter sheet -->
|
|
255
|
+
{#if showFilters}
|
|
256
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
257
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
258
|
+
<div
|
|
259
|
+
class="fixed inset-0 z-50 bg-black/50 lg:hidden"
|
|
260
|
+
onclick={() => (showFilters = false)}
|
|
261
|
+
>
|
|
262
|
+
<div
|
|
263
|
+
class="absolute right-0 top-0 h-full w-72 overflow-y-auto bg-background p-6 shadow-xl"
|
|
264
|
+
onclick={(e) => e.stopPropagation()}
|
|
265
|
+
>
|
|
266
|
+
<div class="mb-4 flex items-center justify-between">
|
|
267
|
+
<h3 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
268
|
+
{m.search_filters()}
|
|
269
|
+
</h3>
|
|
270
|
+
<button
|
|
271
|
+
onclick={() => (showFilters = false)}
|
|
272
|
+
class="rounded-md p-1 text-muted-foreground hover:text-foreground"
|
|
273
|
+
aria-label={m.search_close_filters()}
|
|
274
|
+
>
|
|
275
|
+
<X class="h-4 w-4" />
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
{@render filterPanel()}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{/if}
|
|
282
|
+
|
|
283
|
+
<!-- Results area -->
|
|
284
|
+
<main class="min-w-0 flex-1">
|
|
285
|
+
{#if loading}
|
|
286
|
+
{@render loadingState()}
|
|
287
|
+
{:else if searchResponse && searchResponse.items.length > 0}
|
|
288
|
+
{@render resultsList()}
|
|
289
|
+
{:else if data.query}
|
|
290
|
+
{@render noResults()}
|
|
291
|
+
{:else}
|
|
292
|
+
{@render emptyState()}
|
|
293
|
+
{/if}
|
|
294
|
+
</main>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<!-- Filter panel content (reused for sidebar + mobile sheet) -->
|
|
299
|
+
{#snippet filterPanel()}
|
|
300
|
+
<div class="space-y-6">
|
|
301
|
+
<!-- Active filter summary -->
|
|
302
|
+
{#if hasActiveFilters}
|
|
303
|
+
<button
|
|
304
|
+
onclick={clearFilters}
|
|
305
|
+
class="inline-flex w-full items-center justify-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
306
|
+
>
|
|
307
|
+
<RotateCcw class="size-3" />
|
|
308
|
+
{m.search_clear_all_filters()}
|
|
309
|
+
</button>
|
|
310
|
+
{/if}
|
|
311
|
+
|
|
312
|
+
<!-- Folder filter -->
|
|
313
|
+
{#if folders.length > 0}
|
|
314
|
+
<div>
|
|
315
|
+
<h4
|
|
316
|
+
class="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
|
|
317
|
+
>
|
|
318
|
+
<Folder class="size-3.5" />
|
|
319
|
+
{m.search_folders()}
|
|
320
|
+
</h4>
|
|
321
|
+
<div class="space-y-0.5">
|
|
322
|
+
{#each folders as folder}
|
|
323
|
+
<button
|
|
324
|
+
onclick={() => toggleFolder(folder)}
|
|
325
|
+
class="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-sm transition-colors {activeFolder === folder
|
|
326
|
+
? 'bg-primary/10 font-medium text-primary'
|
|
327
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
|
328
|
+
>
|
|
329
|
+
<span class="truncate">{folder}</span>
|
|
330
|
+
{#if activeFolder === folder}
|
|
331
|
+
<X class="size-3 shrink-0" />
|
|
332
|
+
{/if}
|
|
333
|
+
</button>
|
|
334
|
+
{/each}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
{/if}
|
|
338
|
+
|
|
339
|
+
<!-- Tag filter -->
|
|
340
|
+
{#if tags.length > 0}
|
|
341
|
+
<div>
|
|
342
|
+
<h4
|
|
343
|
+
class="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
|
|
344
|
+
>
|
|
345
|
+
<Tag class="size-3.5" />
|
|
346
|
+
{m.search_tags()}
|
|
347
|
+
</h4>
|
|
348
|
+
<div class="flex flex-wrap gap-1.5">
|
|
349
|
+
{#each tags as tag (tag.id)}
|
|
350
|
+
<button
|
|
351
|
+
onclick={() => toggleTag(tag.name)}
|
|
352
|
+
class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium transition-colors {activeTags.includes(tag.name)
|
|
353
|
+
? 'bg-primary text-primary-foreground'
|
|
354
|
+
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'}"
|
|
355
|
+
>
|
|
356
|
+
{#if tag.color}
|
|
357
|
+
<span class="mr-1.5 inline-block size-2 rounded-full" style="background-color: {tag.color}"></span>
|
|
358
|
+
{/if}
|
|
359
|
+
{tag.name}
|
|
360
|
+
</button>
|
|
361
|
+
{/each}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
{/if}
|
|
365
|
+
|
|
366
|
+
<!-- Date range filter -->
|
|
367
|
+
<div>
|
|
368
|
+
<h4
|
|
369
|
+
class="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
|
|
370
|
+
>
|
|
371
|
+
<Calendar class="size-3.5" />
|
|
372
|
+
{m.search_date_range()}
|
|
373
|
+
</h4>
|
|
374
|
+
<div class="space-y-2">
|
|
375
|
+
<div>
|
|
376
|
+
<label for="dateFrom" class="text-xs text-muted-foreground">{m.search_date_from()}</label>
|
|
377
|
+
<div class="mt-0.5">
|
|
378
|
+
<DatePicker
|
|
379
|
+
id="dateFrom"
|
|
380
|
+
bind:value={dateFrom}
|
|
381
|
+
onchange={applyDateRange}
|
|
382
|
+
ariaLabel={m.search_date_from()}
|
|
383
|
+
placeholder={m.search_date_from()}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div>
|
|
388
|
+
<label for="dateTo" class="text-xs text-muted-foreground">{m.search_date_to()}</label>
|
|
389
|
+
<div class="mt-0.5">
|
|
390
|
+
<DatePicker
|
|
391
|
+
id="dateTo"
|
|
392
|
+
bind:value={dateTo}
|
|
393
|
+
onchange={applyDateRange}
|
|
394
|
+
ariaLabel={m.search_date_to()}
|
|
395
|
+
placeholder={m.search_date_to()}
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
{/snippet}
|
|
403
|
+
|
|
404
|
+
<!-- Loading state -->
|
|
405
|
+
{#snippet loadingState()}
|
|
406
|
+
<div class="space-y-4">
|
|
407
|
+
{#each Array(3) as _, i}
|
|
408
|
+
<div class="animate-pulse rounded-lg border border-border bg-card p-5">
|
|
409
|
+
<div class="flex items-start justify-between gap-3">
|
|
410
|
+
<div class="h-5 w-2/3 rounded bg-muted"></div>
|
|
411
|
+
<div class="h-5 w-12 rounded-full bg-muted"></div>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="mt-3 space-y-2">
|
|
414
|
+
<div class="h-4 w-full rounded bg-muted"></div>
|
|
415
|
+
<div class="h-4 w-5/6 rounded bg-muted"></div>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="mt-3 flex gap-4">
|
|
418
|
+
<div class="h-3.5 w-20 rounded bg-muted"></div>
|
|
419
|
+
<div class="h-3.5 w-28 rounded bg-muted"></div>
|
|
420
|
+
<div class="h-3.5 w-24 rounded bg-muted"></div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
{/each}
|
|
424
|
+
<div class="flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground">
|
|
425
|
+
<Loader2 class="size-4 animate-spin" />
|
|
426
|
+
{m.search_searching()}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
{/snippet}
|
|
430
|
+
|
|
431
|
+
<!-- Results list -->
|
|
432
|
+
{#snippet resultsList()}
|
|
433
|
+
<div>
|
|
434
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
435
|
+
<p class="text-sm text-muted-foreground">
|
|
436
|
+
{(searchResponse?.total ?? 0) === 1 ? m.search_result_for({count: searchResponse!.total}) : m.search_results_for({count: searchResponse!.total})}
|
|
437
|
+
"<span class="font-medium text-foreground">{data.query}</span>"
|
|
438
|
+
</p>
|
|
439
|
+
<select
|
|
440
|
+
bind:value={sortOrder}
|
|
441
|
+
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
442
|
+
aria-label={m.search_filter_label_aria()}
|
|
443
|
+
>
|
|
444
|
+
<option value="relevance">{m.sort_relevance()}</option>
|
|
445
|
+
<option value="date_desc">{m.sort_date_newest()}</option>
|
|
446
|
+
<option value="date_asc">{m.sort_date_oldest()}</option>
|
|
447
|
+
<option value="name_asc">{m.sort_name_asc()}</option>
|
|
448
|
+
<option value="name_desc">{m.sort_name_desc()}</option>
|
|
449
|
+
</select>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div class="space-y-3">
|
|
453
|
+
{#each searchResponse?.items ?? [] as result (result.id)}
|
|
454
|
+
<SearchResult
|
|
455
|
+
id={result.id}
|
|
456
|
+
title={result.title}
|
|
457
|
+
snippet={result.snippet}
|
|
458
|
+
score={result.score}
|
|
459
|
+
folderName={result.folder_name ?? ""}
|
|
460
|
+
tags={result.tags ?? []}
|
|
461
|
+
createdAt={result.created_at}
|
|
462
|
+
query={data.query}
|
|
463
|
+
/>
|
|
464
|
+
{/each}
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<!-- Pagination -->
|
|
468
|
+
{#if totalPages > 1}
|
|
469
|
+
<nav class="mt-8 flex items-center justify-center gap-1.5" aria-label={m.search_pages_aria()}>
|
|
470
|
+
<button
|
|
471
|
+
onclick={() => goToPage(currentPage - 1)}
|
|
472
|
+
disabled={currentPage <= 1}
|
|
473
|
+
aria-label={m.search_previous_page()}
|
|
474
|
+
class="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-2.5 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
475
|
+
>
|
|
476
|
+
<ChevronLeft class="h-4 w-4" />
|
|
477
|
+
</button>
|
|
478
|
+
|
|
479
|
+
{#each Array(totalPages) as _, i}
|
|
480
|
+
{@const pageNum = i + 1}
|
|
481
|
+
{#if totalPages <= 7 || pageNum === 1 || pageNum === totalPages || (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)}
|
|
482
|
+
<button
|
|
483
|
+
onclick={() => goToPage(pageNum)}
|
|
484
|
+
aria-label={m.search_page_number({num: pageNum})}
|
|
485
|
+
aria-current={pageNum === currentPage ? "page" : undefined}
|
|
486
|
+
class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-2.5 text-sm font-medium shadow-sm transition-colors {pageNum === currentPage
|
|
487
|
+
? 'bg-primary text-primary-foreground'
|
|
488
|
+
: 'border border-input bg-background text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
|
489
|
+
>
|
|
490
|
+
{pageNum}
|
|
491
|
+
</button>
|
|
492
|
+
{:else if pageNum === 2 || pageNum === totalPages - 1}
|
|
493
|
+
<span class="px-1 text-muted-foreground">...</span>
|
|
494
|
+
{/if}
|
|
495
|
+
{/each}
|
|
496
|
+
|
|
497
|
+
<button
|
|
498
|
+
onclick={() => goToPage(currentPage + 1)}
|
|
499
|
+
disabled={currentPage >= totalPages}
|
|
500
|
+
aria-label={m.search_next_page()}
|
|
501
|
+
class="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-2.5 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
502
|
+
>
|
|
503
|
+
<ChevronRight class="h-4 w-4" />
|
|
504
|
+
</button>
|
|
505
|
+
</nav>
|
|
506
|
+
{/if}
|
|
507
|
+
</div>
|
|
508
|
+
{/snippet}
|
|
509
|
+
|
|
510
|
+
<!-- No results -->
|
|
511
|
+
{#snippet noResults()}
|
|
512
|
+
<div class="flex flex-col items-center justify-center py-20 text-center">
|
|
513
|
+
<FileSearch class="mb-4 h-16 w-16 text-muted-foreground/40" />
|
|
514
|
+
<h2 class="text-xl font-semibold text-foreground">{m.search_no_results()}</h2>
|
|
515
|
+
<p class="mt-2 max-w-sm text-sm text-muted-foreground">
|
|
516
|
+
{m.search_no_results_query({query: data.query ?? ""})}
|
|
517
|
+
{m.search_no_results_tip()}
|
|
518
|
+
</p>
|
|
519
|
+
<div class="mt-6 flex gap-3">
|
|
520
|
+
{#if hasActiveFilters}
|
|
521
|
+
<button
|
|
522
|
+
onclick={clearFilters}
|
|
523
|
+
class="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-input bg-background px-4 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground"
|
|
524
|
+
>
|
|
525
|
+
<RotateCcw class="size-3.5" />
|
|
526
|
+
{m.search_clear_filters()}
|
|
527
|
+
</button>
|
|
528
|
+
{/if}
|
|
529
|
+
<button
|
|
530
|
+
onclick={clearSearch}
|
|
531
|
+
class="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
|
532
|
+
>
|
|
533
|
+
{m.search_new_search()}
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<!-- Search suggestions -->
|
|
538
|
+
<div class="mt-10">
|
|
539
|
+
<p class="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
540
|
+
{m.search_try_searching()}
|
|
541
|
+
</p>
|
|
542
|
+
<div class="flex flex-wrap justify-center gap-2">
|
|
543
|
+
{#each ["getting started", "docker deployment", "API reference", "authentication", "embeddings"] as suggestion}
|
|
544
|
+
<button
|
|
545
|
+
onclick={() => {
|
|
546
|
+
query = suggestion;
|
|
547
|
+
goto(buildUrl({ q: suggestion, page: "1" }), {
|
|
548
|
+
replaceState: true,
|
|
549
|
+
});
|
|
550
|
+
}}
|
|
551
|
+
class="rounded-full border border-border bg-card px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:bg-muted hover:text-foreground"
|
|
552
|
+
>
|
|
553
|
+
{suggestion}
|
|
554
|
+
</button>
|
|
555
|
+
{/each}
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
{/snippet}
|
|
560
|
+
|
|
561
|
+
<!-- Empty state (no query yet) -->
|
|
562
|
+
{#snippet emptyState()}
|
|
563
|
+
<div class="flex flex-col items-center justify-center py-20 text-center">
|
|
564
|
+
<Search class="mb-4 h-16 w-16 text-muted-foreground/40" />
|
|
565
|
+
<h2 class="text-xl font-semibold text-foreground">
|
|
566
|
+
{m.search_empty_title()}
|
|
567
|
+
</h2>
|
|
568
|
+
<p class="mt-2 max-w-sm text-sm text-muted-foreground">
|
|
569
|
+
{m.search_empty_description()}
|
|
570
|
+
</p>
|
|
571
|
+
|
|
572
|
+
<div class="mt-8">
|
|
573
|
+
<p class="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
574
|
+
{m.search_popular()}
|
|
575
|
+
</p>
|
|
576
|
+
<div class="flex flex-wrap justify-center gap-2">
|
|
577
|
+
{#each ["getting started", "docker deployment", "API reference", "authentication", "embeddings", "Svelte 5"] as suggestion}
|
|
578
|
+
<button
|
|
579
|
+
onclick={() => {
|
|
580
|
+
query = suggestion;
|
|
581
|
+
goto(buildUrl({ q: suggestion, page: "1" }), {
|
|
582
|
+
replaceState: true,
|
|
583
|
+
});
|
|
584
|
+
}}
|
|
585
|
+
class="rounded-full border border-border bg-card px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:bg-muted hover:text-foreground"
|
|
586
|
+
>
|
|
587
|
+
{suggestion}
|
|
588
|
+
</button>
|
|
589
|
+
{/each}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
{/snippet}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PageLoad } from "./$types";
|
|
2
|
+
|
|
3
|
+
export const load: PageLoad = async ({ url }) => {
|
|
4
|
+
const q = url.searchParams.get("q") ?? "";
|
|
5
|
+
const folder = url.searchParams.get("folder") ?? undefined;
|
|
6
|
+
const tags =
|
|
7
|
+
url.searchParams.get("tags")?.split(",").filter(Boolean) ?? undefined;
|
|
8
|
+
const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
|
|
9
|
+
const dateTo = url.searchParams.get("dateTo") ?? undefined;
|
|
10
|
+
const page = Math.max(
|
|
11
|
+
1,
|
|
12
|
+
Number.parseInt(url.searchParams.get("page") ?? "1", 10),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
query: q,
|
|
17
|
+
filters: {
|
|
18
|
+
folder: folder || undefined,
|
|
19
|
+
tags: tags && tags.length > 0 ? tags : undefined,
|
|
20
|
+
dateFrom: dateFrom || undefined,
|
|
21
|
+
dateTo: dateTo || undefined,
|
|
22
|
+
},
|
|
23
|
+
page,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import * as m from "$lib/paraglide/messages.js";
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="flex min-h-screen items-center justify-center">
|
|
7
|
+
<div class="text-center">
|
|
8
|
+
<h1 class="text-4xl font-bold">{page.status}</h1>
|
|
9
|
+
<p class="mt-2 text-muted-foreground">{page.error?.message ?? m.error_generic()}</p>
|
|
10
|
+
<a href="/" class="mt-4 inline-block text-primary underline">{m.action_back()}</a>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import "../app.css";
|
|
3
|
+
import { getLocale } from "$lib/paraglide/runtime";
|
|
4
|
+
import { initTheme } from "$lib/stores/theme.svelte";
|
|
5
|
+
|
|
6
|
+
const { children } = $props();
|
|
7
|
+
|
|
8
|
+
initTheme();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<svelte:head>
|
|
12
|
+
<meta name="description" content="Self-hosted AI-first documentation platform" />
|
|
13
|
+
<meta name="og:type" content="website" />
|
|
14
|
+
<meta name="og:title" content="HiAi-Docs" />
|
|
15
|
+
<meta name="og:description" content="AI-first documentation platform with semantic search" />
|
|
16
|
+
</svelte:head>
|
|
17
|
+
|
|
18
|
+
{@render children()}
|