@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,111 @@
|
|
|
1
|
+
import { json } from "@sveltejs/kit";
|
|
2
|
+
import type { RequestHandler } from "./$types";
|
|
3
|
+
|
|
4
|
+
const API_BASE = process.env.API_URL || "http://localhost:50700";
|
|
5
|
+
|
|
6
|
+
function buildHeaders(request: Request): Headers {
|
|
7
|
+
const headers = new Headers(request.headers);
|
|
8
|
+
headers.delete("content-length");
|
|
9
|
+
return headers;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getCsrfToken(): Promise<string | null> {
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(`${API_BASE}/api/csrf-token`);
|
|
15
|
+
if (response.ok) {
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
return data.token || null;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function proxy(
|
|
26
|
+
request: Request,
|
|
27
|
+
params: { path?: string },
|
|
28
|
+
fetch: typeof globalThis.fetch,
|
|
29
|
+
): Promise<Response> {
|
|
30
|
+
const path = params.path;
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
const targetUrl = `${API_BASE}/api/${path}${url.search}`;
|
|
33
|
+
|
|
34
|
+
const headers = buildHeaders(request);
|
|
35
|
+
|
|
36
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) {
|
|
37
|
+
const csrfToken = await getCsrfToken();
|
|
38
|
+
if (csrfToken) {
|
|
39
|
+
headers.set("x-csrf-token", csrfToken);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const init: RequestInit = {
|
|
44
|
+
method: request.method,
|
|
45
|
+
headers,
|
|
46
|
+
credentials: "include",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
request.method === "POST" ||
|
|
51
|
+
request.method === "PUT" ||
|
|
52
|
+
request.method === "PATCH"
|
|
53
|
+
) {
|
|
54
|
+
// Forward the raw bytes — NOT request.text(). Reading the body as text
|
|
55
|
+
// decodes it as UTF-8, which corrupts binary payloads such as
|
|
56
|
+
// multipart/form-data image uploads (high bytes get replaced with the
|
|
57
|
+
// U+FFFD replacement character, inflating and mangling the file). The
|
|
58
|
+
// original content-type header (with its multipart boundary) is
|
|
59
|
+
// preserved by buildHeaders().
|
|
60
|
+
const body = await request.arrayBuffer();
|
|
61
|
+
if (body.byteLength > 0) {
|
|
62
|
+
init.body = body;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(targetUrl, init);
|
|
68
|
+
// Read the response as raw bytes for the same reason — image/binary
|
|
69
|
+
// responses (e.g. /api/attachments/:id/raw) must pass through intact.
|
|
70
|
+
const data = await response.arrayBuffer();
|
|
71
|
+
|
|
72
|
+
const responseHeaders = new Headers();
|
|
73
|
+
responseHeaders.set(
|
|
74
|
+
"content-type",
|
|
75
|
+
response.headers.get("content-type") || "application/json",
|
|
76
|
+
);
|
|
77
|
+
const cacheControl = response.headers.get("cache-control");
|
|
78
|
+
if (cacheControl) {
|
|
79
|
+
responseHeaders.set("cache-control", cacheControl);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
response.headers.forEach((value, key) => {
|
|
83
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
84
|
+
responseHeaders.append(key, value);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return new Response(data, {
|
|
89
|
+
status: response.status,
|
|
90
|
+
headers: responseHeaders,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("API proxy error:", error);
|
|
94
|
+
return json({ error: "Failed to proxy request" }, { status: 502 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const GET: RequestHandler = async ({ request, params, fetch }) =>
|
|
99
|
+
proxy(request, params, fetch);
|
|
100
|
+
|
|
101
|
+
export const POST: RequestHandler = async ({ request, params, fetch }) =>
|
|
102
|
+
proxy(request, params, fetch);
|
|
103
|
+
|
|
104
|
+
export const PUT: RequestHandler = async ({ request, params, fetch }) =>
|
|
105
|
+
proxy(request, params, fetch);
|
|
106
|
+
|
|
107
|
+
export const PATCH: RequestHandler = async ({ request, params, fetch }) =>
|
|
108
|
+
proxy(request, params, fetch);
|
|
109
|
+
|
|
110
|
+
export const DELETE: RequestHandler = async ({ request, params, fetch }) =>
|
|
111
|
+
proxy(request, params, fetch);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServerLoadEvent } from "@sveltejs/kit";
|
|
2
|
+
import { redirect } from "@sveltejs/kit";
|
|
3
|
+
|
|
4
|
+
export async function load({ params, cookies }: ServerLoadEvent) {
|
|
5
|
+
const sessionCookie = cookies.get("better-auth.session_token");
|
|
6
|
+
if (!sessionCookie) {
|
|
7
|
+
throw redirect(302, "/login");
|
|
8
|
+
}
|
|
9
|
+
return { id: params.id };
|
|
10
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Badge } from "@hiai-gg/hiai-ui/components/ui/badge";
|
|
3
|
+
import { Button } from "@hiai-gg/hiai-ui/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from "@hiai-gg/hiai-ui/components/ui/dropdown-menu";
|
|
10
|
+
import {
|
|
11
|
+
ArrowUpDown,
|
|
12
|
+
ChevronRight,
|
|
13
|
+
Clock,
|
|
14
|
+
File,
|
|
15
|
+
FileText,
|
|
16
|
+
FolderOpen,
|
|
17
|
+
FolderPlus,
|
|
18
|
+
Plus,
|
|
19
|
+
SortAsc,
|
|
20
|
+
} from "lucide-svelte";
|
|
21
|
+
import { goto } from "$app/navigation";
|
|
22
|
+
import { apiFetch } from "$lib/api/client";
|
|
23
|
+
import DocumentCard from "$lib/components/DocumentCard.svelte";
|
|
24
|
+
import FolderCard from "$lib/components/FolderCard.svelte";
|
|
25
|
+
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
|
|
26
|
+
import * as m from "$lib/paraglide/messages.js";
|
|
27
|
+
import type { Document, Folder, SortOption } from "$lib/types.js";
|
|
28
|
+
|
|
29
|
+
const { data } = $props();
|
|
30
|
+
|
|
31
|
+
let sortBy = $state<SortOption>("updated");
|
|
32
|
+
let editingName = $state(false);
|
|
33
|
+
let editName = $state("");
|
|
34
|
+
let showDeleteDialog = $state(false);
|
|
35
|
+
let deleteTargetId = $state<string | null>(null);
|
|
36
|
+
let deleteBusy = $state(false);
|
|
37
|
+
|
|
38
|
+
/** Sort documents by the selected option. */
|
|
39
|
+
const sortedDocuments = $derived.by(() => {
|
|
40
|
+
const docs = [...data.folder.documents];
|
|
41
|
+
switch (sortBy) {
|
|
42
|
+
case "name":
|
|
43
|
+
return docs.sort((a, b) => a.title.localeCompare(b.title));
|
|
44
|
+
case "created":
|
|
45
|
+
return docs.sort(
|
|
46
|
+
(a, b) =>
|
|
47
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
|
48
|
+
);
|
|
49
|
+
default:
|
|
50
|
+
return docs.sort(
|
|
51
|
+
(a, b) =>
|
|
52
|
+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Sort subfolders by name. */
|
|
58
|
+
const sortedSubfolders = $derived(
|
|
59
|
+
[...data.folder.children].sort((a, b) => a.name.localeCompare(b.name)),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const isEmpty = $derived(
|
|
63
|
+
sortedSubfolders.length === 0 && sortedDocuments.length === 0,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function startRename() {
|
|
67
|
+
editName = data.folder.name;
|
|
68
|
+
editingName = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cancelRename() {
|
|
72
|
+
editingName = false;
|
|
73
|
+
editName = "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function submitRename() {
|
|
77
|
+
if (editName.trim() && editName.trim() !== data.folder.name) {
|
|
78
|
+
// In production, call updateFolder(data.folder.id, { name: editName.trim() })
|
|
79
|
+
data.folder.name = editName.trim();
|
|
80
|
+
}
|
|
81
|
+
editingName = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleRenameKeydown(e: KeyboardEvent) {
|
|
85
|
+
if (e.key === "Enter") submitRename();
|
|
86
|
+
if (e.key === "Escape") cancelRename();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleDeleteFolder(id: string) {
|
|
90
|
+
deleteTargetId = id;
|
|
91
|
+
showDeleteDialog = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cancelDeleteFolder() {
|
|
95
|
+
showDeleteDialog = false;
|
|
96
|
+
deleteTargetId = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function confirmDeleteFolder() {
|
|
100
|
+
const id = deleteTargetId;
|
|
101
|
+
if (!id || deleteBusy) return;
|
|
102
|
+
deleteBusy = true;
|
|
103
|
+
try {
|
|
104
|
+
await apiFetch(`/api/folders/${id}`, { method: "DELETE" });
|
|
105
|
+
data.folder.children = data.folder.children.filter(
|
|
106
|
+
(c: Folder) => c.id !== id,
|
|
107
|
+
);
|
|
108
|
+
showDeleteDialog = false;
|
|
109
|
+
deleteTargetId = null;
|
|
110
|
+
} catch (e: unknown) {
|
|
111
|
+
console.error("Failed to delete folder", e);
|
|
112
|
+
} finally {
|
|
113
|
+
deleteBusy = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleRenameFolder(id: string) {
|
|
118
|
+
const folder = data.folder.children.find((c: Folder) => c.id === id);
|
|
119
|
+
const current = folder?.name ?? "";
|
|
120
|
+
const name = prompt(m.folders_rename(), current);
|
|
121
|
+
const trimmed = name?.trim();
|
|
122
|
+
if (!trimmed || trimmed === current) return;
|
|
123
|
+
apiFetch<Folder>(`/api/folders/${id}`, {
|
|
124
|
+
method: "PATCH",
|
|
125
|
+
body: JSON.stringify({ name: trimmed }),
|
|
126
|
+
})
|
|
127
|
+
.then((updated) => {
|
|
128
|
+
const target = data.folder.children.find((c: Folder) => c.id === id);
|
|
129
|
+
if (target) {
|
|
130
|
+
target.name = updated.name;
|
|
131
|
+
target.updatedAt = updated.updatedAt;
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.catch((e: unknown) => console.error("Failed to rename folder", e));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleDeleteDocument(id: string) {
|
|
138
|
+
data.folder.documents = data.folder.documents.filter(
|
|
139
|
+
(d: Document) => d.id !== id,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleDuplicateDocument(id: string) {
|
|
144
|
+
apiFetch<Document>(`/api/documents/${id}/duplicate`, { method: "POST" })
|
|
145
|
+
.then((doc) => {
|
|
146
|
+
data.folder.documents = [...data.folder.documents, doc];
|
|
147
|
+
})
|
|
148
|
+
.catch((e: unknown) => console.error("Failed to duplicate document", e));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getSortLabel(option: SortOption): string {
|
|
152
|
+
switch (option) {
|
|
153
|
+
case "name":
|
|
154
|
+
return m.sort_name();
|
|
155
|
+
case "updated":
|
|
156
|
+
return m.sort_date_modified();
|
|
157
|
+
case "created":
|
|
158
|
+
return m.sort_date_created();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<svelte:head>
|
|
164
|
+
<title>{m.folder_page_title({ name: data.folder.name })}</title>
|
|
165
|
+
</svelte:head>
|
|
166
|
+
|
|
167
|
+
<div class="mx-auto max-w-5xl px-4 py-8">
|
|
168
|
+
<!-- Breadcrumb -->
|
|
169
|
+
<nav class="mb-4 flex items-center gap-1 text-sm text-muted-foreground" aria-label={m.aria_breadcrumb()}>
|
|
170
|
+
{#each data.breadcrumb as crumb, i (crumb.id)}
|
|
171
|
+
{#if i > 0}
|
|
172
|
+
<ChevronRight class="size-3.5 shrink-0" />
|
|
173
|
+
{/if}
|
|
174
|
+
{#if i === data.breadcrumb.length - 1}
|
|
175
|
+
<span class="font-medium text-foreground">{crumb.name}</span>
|
|
176
|
+
{:else}
|
|
177
|
+
<a href="/folders/{crumb.id}" class="transition-colors hover:text-foreground">
|
|
178
|
+
{crumb.name}
|
|
179
|
+
</a>
|
|
180
|
+
{/if}
|
|
181
|
+
{/each}
|
|
182
|
+
</nav>
|
|
183
|
+
|
|
184
|
+
<!-- Header -->
|
|
185
|
+
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
186
|
+
<div class="flex items-center gap-3">
|
|
187
|
+
<FolderOpen class="size-7 shrink-0 text-primary" />
|
|
188
|
+
{#if editingName}
|
|
189
|
+
<input
|
|
190
|
+
type="text"
|
|
191
|
+
bind:value={editName}
|
|
192
|
+
onblur={submitRename}
|
|
193
|
+
onkeydown={handleRenameKeydown}
|
|
194
|
+
class="rounded-md border border-input bg-transparent px-2 py-1 text-2xl font-semibold tracking-tight focus:outline-none focus:ring-1 focus:ring-ring"
|
|
195
|
+
/>
|
|
196
|
+
{:else}
|
|
197
|
+
<button
|
|
198
|
+
class="cursor-pointer text-left text-2xl font-semibold tracking-tight transition-colors hover:text-primary"
|
|
199
|
+
onclick={startRename}
|
|
200
|
+
onkeydown={(e: KeyboardEvent) => e.key === "Enter" && startRename()}
|
|
201
|
+
title={m.folder_click_to_rename()}
|
|
202
|
+
>
|
|
203
|
+
{data.folder.name}
|
|
204
|
+
</button>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div class="flex items-center gap-2">
|
|
209
|
+
<!-- Sort dropdown -->
|
|
210
|
+
<DropdownMenu>
|
|
211
|
+
<DropdownMenuTrigger
|
|
212
|
+
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-input bg-transparent px-3 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
213
|
+
>
|
|
214
|
+
<ArrowUpDown class="size-3.5" />
|
|
215
|
+
{getSortLabel(sortBy)}
|
|
216
|
+
</DropdownMenuTrigger>
|
|
217
|
+
<DropdownMenuContent align="end">
|
|
218
|
+
<DropdownMenuItem onclick={() => (sortBy = "updated")}>
|
|
219
|
+
<Clock class="size-4" />
|
|
220
|
+
{m.sort_date_modified()}
|
|
221
|
+
</DropdownMenuItem>
|
|
222
|
+
<DropdownMenuItem onclick={() => (sortBy = "created")}>
|
|
223
|
+
<File class="size-4" />
|
|
224
|
+
{m.sort_date_created()}
|
|
225
|
+
</DropdownMenuItem>
|
|
226
|
+
<DropdownMenuItem onclick={() => (sortBy = "name")}>
|
|
227
|
+
<SortAsc class="size-4" />
|
|
228
|
+
{m.sort_name()}
|
|
229
|
+
</DropdownMenuItem>
|
|
230
|
+
</DropdownMenuContent>
|
|
231
|
+
</DropdownMenu>
|
|
232
|
+
|
|
233
|
+
<!-- New Subfolder -->
|
|
234
|
+
<Button variant="outline" size="sm" onclick={() => goto(`/folders/new?parent=${data.folder.id}`)}>
|
|
235
|
+
<FolderPlus class="size-3.5" />
|
|
236
|
+
{m.folder_new_subfolder()}
|
|
237
|
+
</Button>
|
|
238
|
+
|
|
239
|
+
<!-- New Document -->
|
|
240
|
+
<Button size="sm" onclick={() => goto(`/docs/new?folder=${data.folder.id}`)}>
|
|
241
|
+
<Plus class="size-3.5" />
|
|
242
|
+
{m.dashboard_new_document()}
|
|
243
|
+
</Button>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{#if isEmpty}
|
|
248
|
+
<!-- Empty state -->
|
|
249
|
+
<div class="flex flex-col items-center justify-center rounded-xl border border-dashed border-border py-20">
|
|
250
|
+
<div class="mb-4 rounded-full bg-muted p-4">
|
|
251
|
+
<FileText class="size-8 text-muted-foreground" />
|
|
252
|
+
</div>
|
|
253
|
+
<h2 class="mb-1 text-lg font-semibold">{m.folder_empty_title()}</h2>
|
|
254
|
+
<p class="mb-4 text-sm text-muted-foreground">
|
|
255
|
+
{m.folder_empty_description()}
|
|
256
|
+
</p>
|
|
257
|
+
<div class="flex gap-2">
|
|
258
|
+
<Button onclick={() => goto(`/docs/new?folder=${data.folder.id}`)}>
|
|
259
|
+
<FileText class="size-4" />
|
|
260
|
+
{m.dashboard_new_document()}
|
|
261
|
+
</Button>
|
|
262
|
+
<Button variant="outline" onclick={() => goto(`/folders/new?parent=${data.folder.id}`)}>
|
|
263
|
+
<FolderPlus class="size-4" />
|
|
264
|
+
{m.folder_new_subfolder()}
|
|
265
|
+
</Button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
{:else}
|
|
269
|
+
<!-- Subfolders section -->
|
|
270
|
+
{#if sortedSubfolders.length > 0}
|
|
271
|
+
<section class="mb-8">
|
|
272
|
+
<h2 class="mb-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
273
|
+
{m.nav_folders()}
|
|
274
|
+
<Badge variant="secondary" class="ml-1.5 text-[10px]">{sortedSubfolders.length}</Badge>
|
|
275
|
+
</h2>
|
|
276
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
277
|
+
{#each sortedSubfolders as folder (folder.id)}
|
|
278
|
+
<FolderCard
|
|
279
|
+
{folder}
|
|
280
|
+
onDelete={handleDeleteFolder}
|
|
281
|
+
onRename={handleRenameFolder}
|
|
282
|
+
/>
|
|
283
|
+
{/each}
|
|
284
|
+
</div>
|
|
285
|
+
</section>
|
|
286
|
+
{/if}
|
|
287
|
+
|
|
288
|
+
<!-- Documents section -->
|
|
289
|
+
{#if sortedDocuments.length > 0}
|
|
290
|
+
<section>
|
|
291
|
+
<h2 class="mb-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
292
|
+
{m.nav_documents()}
|
|
293
|
+
<Badge variant="secondary" class="ml-1.5 text-[10px]">{sortedDocuments.length}</Badge>
|
|
294
|
+
</h2>
|
|
295
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
296
|
+
{#each sortedDocuments as doc (doc.id)}
|
|
297
|
+
<DocumentCard
|
|
298
|
+
document={doc}
|
|
299
|
+
onDelete={handleDeleteDocument}
|
|
300
|
+
onDuplicate={handleDuplicateDocument}
|
|
301
|
+
/>
|
|
302
|
+
{/each}
|
|
303
|
+
</div>
|
|
304
|
+
</section>
|
|
305
|
+
{/if}
|
|
306
|
+
{/if}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<ConfirmDialog
|
|
310
|
+
bind:open={showDeleteDialog}
|
|
311
|
+
title={m.folders_delete_title()}
|
|
312
|
+
description={m.folders_delete_description()}
|
|
313
|
+
confirmLabel={m.action_delete()}
|
|
314
|
+
cancelLabel={m.action_cancel()}
|
|
315
|
+
variant="destructive"
|
|
316
|
+
busy={deleteBusy}
|
|
317
|
+
onConfirm={confirmDeleteFolder}
|
|
318
|
+
onCancel={cancelDeleteFolder}
|
|
319
|
+
/>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getFolder, getFolderPath } from "$lib/api/folders.js";
|
|
2
|
+
import type { PageLoad } from "./$types.js";
|
|
3
|
+
|
|
4
|
+
export const load: PageLoad = async ({ params }) => {
|
|
5
|
+
const [folder, breadcrumb] = await Promise.all([
|
|
6
|
+
getFolder(params.id),
|
|
7
|
+
getFolderPath(params.id),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
folder,
|
|
12
|
+
breadcrumb,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { signIn } from "$lib/auth-client";
|
|
4
|
+
import * as m from "$lib/paraglide/messages.js";
|
|
5
|
+
|
|
6
|
+
let email = $state("");
|
|
7
|
+
let password = $state("");
|
|
8
|
+
let error = $state("");
|
|
9
|
+
let loading = $state(false);
|
|
10
|
+
|
|
11
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
loading = true;
|
|
14
|
+
error = "";
|
|
15
|
+
|
|
16
|
+
const result = await signIn.email({
|
|
17
|
+
email,
|
|
18
|
+
password,
|
|
19
|
+
callbackURL: "/",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (result.error) {
|
|
23
|
+
error = result.error.message ?? m.auth_login_error();
|
|
24
|
+
loading = false;
|
|
25
|
+
} else {
|
|
26
|
+
goto("/");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<svelte:head>
|
|
32
|
+
<title>{m.login_page_title()}</title>
|
|
33
|
+
</svelte:head>
|
|
34
|
+
|
|
35
|
+
<div class="flex min-h-screen items-center justify-center bg-background">
|
|
36
|
+
<form
|
|
37
|
+
onsubmit={handleSubmit}
|
|
38
|
+
class="w-full max-w-sm space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm"
|
|
39
|
+
>
|
|
40
|
+
<div class="space-y-2">
|
|
41
|
+
<div class="flex items-center gap-2">
|
|
42
|
+
<img src="/logo.png" alt="HiAi-Docs" class="h-8 w-auto dark:invert" />
|
|
43
|
+
<h1 class="text-2xl font-semibold tracking-tight">{m.login_title()}</h1>
|
|
44
|
+
</div>
|
|
45
|
+
<p class="text-sm text-muted-foreground">{m.login_subtitle()}</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{#if error}
|
|
49
|
+
<p class="text-sm text-destructive">{error}</p>
|
|
50
|
+
{/if}
|
|
51
|
+
|
|
52
|
+
<div class="space-y-2">
|
|
53
|
+
<label for="email" class="text-sm font-medium">{m.auth_email()}</label>
|
|
54
|
+
<input
|
|
55
|
+
id="email"
|
|
56
|
+
type="email"
|
|
57
|
+
bind:value={email}
|
|
58
|
+
required
|
|
59
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
60
|
+
placeholder="you@example.com"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="space-y-2">
|
|
65
|
+
<label for="password" class="text-sm font-medium">{m.auth_password()}</label>
|
|
66
|
+
<input
|
|
67
|
+
id="password"
|
|
68
|
+
type="password"
|
|
69
|
+
bind:value={password}
|
|
70
|
+
required
|
|
71
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
72
|
+
placeholder="••••••••"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="submit"
|
|
78
|
+
disabled={loading}
|
|
79
|
+
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 transition-colors hover:bg-primary/90 disabled:opacity-50"
|
|
80
|
+
>
|
|
81
|
+
{loading ? m.login_loading() : m.login_submit()}
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<p class="text-center text-sm text-muted-foreground">
|
|
85
|
+
{m.auth_no_account()} <a href="/register" class="text-primary underline underline-offset-4 hover:text-primary/80"
|
|
86
|
+
>{m.register_title()}</a
|
|
87
|
+
>
|
|
88
|
+
</p>
|
|
89
|
+
</form>
|
|
90
|
+
</div>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { signUp } from "$lib/auth-client";
|
|
4
|
+
import * as m from "$lib/paraglide/messages.js";
|
|
5
|
+
|
|
6
|
+
let name = $state("");
|
|
7
|
+
let email = $state("");
|
|
8
|
+
let password = $state("");
|
|
9
|
+
let error = $state("");
|
|
10
|
+
let loading = $state(false);
|
|
11
|
+
|
|
12
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
loading = true;
|
|
15
|
+
error = "";
|
|
16
|
+
|
|
17
|
+
const result = await signUp.email({
|
|
18
|
+
name,
|
|
19
|
+
email,
|
|
20
|
+
password,
|
|
21
|
+
callbackURL: "/",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
error = result.error.message ?? m.auth_signup_error();
|
|
26
|
+
loading = false;
|
|
27
|
+
} else {
|
|
28
|
+
goto("/");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<svelte:head>
|
|
34
|
+
<title>{m.register_page_title()}</title>
|
|
35
|
+
</svelte:head>
|
|
36
|
+
|
|
37
|
+
<div class="flex min-h-screen items-center justify-center bg-background">
|
|
38
|
+
<form onsubmit={handleSubmit} class="w-full max-w-sm space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm">
|
|
39
|
+
<div class="space-y-1">
|
|
40
|
+
<h1 class="text-2xl font-semibold tracking-tight">{m.register_title()}</h1>
|
|
41
|
+
<p class="text-sm text-muted-foreground">{m.register_subtitle()}</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{#if error}
|
|
45
|
+
<p class="text-sm text-destructive">{error}</p>
|
|
46
|
+
{/if}
|
|
47
|
+
|
|
48
|
+
<div class="space-y-2">
|
|
49
|
+
<label for="name" class="text-sm font-medium">{m.settings_name()}</label>
|
|
50
|
+
<input
|
|
51
|
+
id="name"
|
|
52
|
+
type="text"
|
|
53
|
+
bind:value={name}
|
|
54
|
+
required
|
|
55
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
56
|
+
placeholder={m.auth_name_placeholder()}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="space-y-2">
|
|
61
|
+
<label for="email" class="text-sm font-medium">{m.auth_email()}</label>
|
|
62
|
+
<input
|
|
63
|
+
id="email"
|
|
64
|
+
type="email"
|
|
65
|
+
bind:value={email}
|
|
66
|
+
required
|
|
67
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
68
|
+
placeholder="you@example.com"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="space-y-2">
|
|
73
|
+
<label for="password" class="text-sm font-medium">{m.auth_password()}</label>
|
|
74
|
+
<input
|
|
75
|
+
id="password"
|
|
76
|
+
type="password"
|
|
77
|
+
bind:value={password}
|
|
78
|
+
required
|
|
79
|
+
minlength={8}
|
|
80
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
81
|
+
placeholder="••••••••"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<button
|
|
86
|
+
type="submit"
|
|
87
|
+
disabled={loading}
|
|
88
|
+
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 transition-colors hover:bg-primary/90 disabled:opacity-50"
|
|
89
|
+
>
|
|
90
|
+
{loading ? m.register_loading() : m.register_title()}
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<p class="text-center text-sm text-muted-foreground">
|
|
94
|
+
{m.auth_has_account()} <a href="/login" class="text-primary underline underline-offset-4 hover:text-primary/80">{m.auth_login()}</a>
|
|
95
|
+
</p>
|
|
96
|
+
</form>
|
|
97
|
+
</div>
|