From 2ea7bb7a18eecc9df00398ea142f1e8f000cebec Mon Sep 17 00:00:00 2001
From: Till JS
Date: Thu, 19 Mar 2026 11:15:20 +0100
Subject: [PATCH] feat(context): add SvelteKit web app with Svelte 5 runes
- Add 15 routes: dashboard, spaces, documents, editor, tokens, settings, auth, etc.
- Add 10 components: DocumentCard, DocumentEditor, AIToolbar, SpaceCard, BatchCreateModal, etc.
- Add 7 Svelte 5 rune stores: documents, spaces, tokens, auth, theme, navigation, user-settings
- Add i18n with DE + EN locales
- Add types for Document, Space, AI models, token economy
- Add SvelteKit config with node adapter (port 5192)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
apps/context/apps/web/src/app.css | 10 +
apps/context/apps/web/src/app.html | 12 +
.../web/src/lib/components/AIToolbar.svelte | 222 ++++++++++++
.../lib/components/BatchCreateModal.svelte | 130 +++++++
.../src/lib/components/ConfirmDialog.svelte | 48 +++
.../lib/components/CreateSpaceModal.svelte | 69 ++++
.../src/lib/components/DocumentCard.svelte | 93 +++++
.../src/lib/components/DocumentEditor.svelte | 321 ++++++++++++++++++
.../src/lib/components/MentionInput.svelte | 135 ++++++++
.../web/src/lib/components/SpaceCard.svelte | 85 +++++
.../lib/components/VersionNavigator.svelte | 104 ++++++
.../skeletons/AppLoadingSkeleton.svelte | 72 ++++
.../web/src/lib/components/skeletons/index.ts | 1 +
.../context/apps/web/src/lib/config/editor.ts | 11 +
apps/context/apps/web/src/lib/i18n/index.ts | 40 +++
.../apps/web/src/lib/i18n/locales/de.json | 68 ++++
.../apps/web/src/lib/i18n/locales/en.json | 68 ++++
.../web/src/lib/stores/documents.svelte.ts | 203 +++++++++++
.../apps/web/src/lib/stores/navigation.ts | 5 +
.../apps/web/src/lib/stores/spaces.svelte.ts | 72 ++++
.../apps/web/src/lib/stores/theme.svelte.ts | 6 +
.../apps/web/src/lib/stores/tokens.svelte.ts | 64 ++++
.../src/lib/stores/user-settings.svelte.ts | 18 +
apps/context/apps/web/src/lib/types.ts | 87 +++++
.../apps/web/src/lib/utils/markdown.ts | 62 ++++
apps/context/apps/web/src/lib/utils/text.ts | 49 +++
.../apps/web/src/routes/(app)/+layout.svelte | 305 +++++++++++++++++
.../apps/web/src/routes/(app)/+page.svelte | 131 +++++++
.../src/routes/(app)/documents/+page.svelte | 187 ++++++++++
.../routes/(app)/documents/[id]/+page.svelte | 189 +++++++++++
.../src/routes/(app)/feedback/+page.svelte | 24 ++
.../web/src/routes/(app)/mana/+page.svelte | 13 +
.../web/src/routes/(app)/profile/+page.svelte | 12 +
.../src/routes/(app)/settings/+page.svelte | 26 ++
.../web/src/routes/(app)/spaces/+page.svelte | 142 ++++++++
.../src/routes/(app)/spaces/[id]/+page.svelte | 276 +++++++++++++++
.../web/src/routes/(app)/themes/+page.svelte | 29 ++
.../web/src/routes/(app)/tokens/+page.svelte | 178 ++++++++++
.../(auth)/forgot-password/+page.svelte | 22 ++
.../web/src/routes/(auth)/login/+page.svelte | 62 ++++
.../src/routes/(auth)/register/+page.svelte | 49 +++
.../apps/web/src/routes/+layout.svelte | 39 +++
.../apps/web/src/routes/health/+server.ts | 10 +
.../apps/web/src/routes/offline/+page.svelte | 96 ++++++
apps/context/apps/web/svelte.config.js | 15 +
apps/context/apps/web/tsconfig.json | 14 +
apps/context/apps/web/vite.config.ts | 30 ++
47 files changed, 3904 insertions(+)
create mode 100644 apps/context/apps/web/src/app.css
create mode 100644 apps/context/apps/web/src/app.html
create mode 100644 apps/context/apps/web/src/lib/components/AIToolbar.svelte
create mode 100644 apps/context/apps/web/src/lib/components/BatchCreateModal.svelte
create mode 100644 apps/context/apps/web/src/lib/components/ConfirmDialog.svelte
create mode 100644 apps/context/apps/web/src/lib/components/CreateSpaceModal.svelte
create mode 100644 apps/context/apps/web/src/lib/components/DocumentCard.svelte
create mode 100644 apps/context/apps/web/src/lib/components/DocumentEditor.svelte
create mode 100644 apps/context/apps/web/src/lib/components/MentionInput.svelte
create mode 100644 apps/context/apps/web/src/lib/components/SpaceCard.svelte
create mode 100644 apps/context/apps/web/src/lib/components/VersionNavigator.svelte
create mode 100644 apps/context/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte
create mode 100644 apps/context/apps/web/src/lib/components/skeletons/index.ts
create mode 100644 apps/context/apps/web/src/lib/config/editor.ts
create mode 100644 apps/context/apps/web/src/lib/i18n/index.ts
create mode 100644 apps/context/apps/web/src/lib/i18n/locales/de.json
create mode 100644 apps/context/apps/web/src/lib/i18n/locales/en.json
create mode 100644 apps/context/apps/web/src/lib/stores/documents.svelte.ts
create mode 100644 apps/context/apps/web/src/lib/stores/navigation.ts
create mode 100644 apps/context/apps/web/src/lib/stores/spaces.svelte.ts
create mode 100644 apps/context/apps/web/src/lib/stores/theme.svelte.ts
create mode 100644 apps/context/apps/web/src/lib/stores/tokens.svelte.ts
create mode 100644 apps/context/apps/web/src/lib/stores/user-settings.svelte.ts
create mode 100644 apps/context/apps/web/src/lib/types.ts
create mode 100644 apps/context/apps/web/src/lib/utils/markdown.ts
create mode 100644 apps/context/apps/web/src/lib/utils/text.ts
create mode 100644 apps/context/apps/web/src/routes/(app)/+layout.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/documents/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/documents/[id]/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/feedback/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/mana/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/profile/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/settings/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/spaces/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/spaces/[id]/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/themes/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(app)/tokens/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(auth)/forgot-password/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(auth)/login/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/(auth)/register/+page.svelte
create mode 100644 apps/context/apps/web/src/routes/+layout.svelte
create mode 100644 apps/context/apps/web/src/routes/health/+server.ts
create mode 100644 apps/context/apps/web/src/routes/offline/+page.svelte
create mode 100644 apps/context/apps/web/svelte.config.js
create mode 100644 apps/context/apps/web/tsconfig.json
create mode 100644 apps/context/apps/web/vite.config.ts
diff --git a/apps/context/apps/web/src/app.css b/apps/context/apps/web/src/app.css
new file mode 100644
index 000000000..c29749613
--- /dev/null
+++ b/apps/context/apps/web/src/app.css
@@ -0,0 +1,10 @@
+@import "tailwindcss";
+@import "@manacore/shared-tailwind/themes.css";
+
+/* Scan shared packages for Tailwind classes */
+@source "../../../../packages/shared-ui/src";
+@source "../../../../packages/shared-auth-ui/src";
+@source "../../../../packages/shared-branding/src";
+@source "../../../../packages/shared-theme-ui/src";
+@source "../../../../packages/shared-theme-ui/src/components";
+@source "../../../../packages/shared-theme-ui/src/pages";
diff --git a/apps/context/apps/web/src/app.html b/apps/context/apps/web/src/app.html
new file mode 100644
index 000000000..77a5ff52c
--- /dev/null
+++ b/apps/context/apps/web/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/apps/context/apps/web/src/lib/components/AIToolbar.svelte b/apps/context/apps/web/src/lib/components/AIToolbar.svelte
new file mode 100644
index 000000000..2656054d4
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/AIToolbar.svelte
@@ -0,0 +1,222 @@
+
+
+
+
+
diff --git a/apps/context/apps/web/src/lib/components/BatchCreateModal.svelte b/apps/context/apps/web/src/lib/components/BatchCreateModal.svelte
new file mode 100644
index 000000000..f287058c5
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/BatchCreateModal.svelte
@@ -0,0 +1,130 @@
+
+
+{#if open}
+
+
+
+
Mehrere Dokumente erstellen
+
+
+
+{/if}
diff --git a/apps/context/apps/web/src/lib/components/ConfirmDialog.svelte b/apps/context/apps/web/src/lib/components/ConfirmDialog.svelte
new file mode 100644
index 000000000..b8bd9b388
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/ConfirmDialog.svelte
@@ -0,0 +1,48 @@
+
+
+{#if open}
+
+
+
+
{title}
+
{message}
+
+
+
+
+
+
+{/if}
diff --git a/apps/context/apps/web/src/lib/components/CreateSpaceModal.svelte b/apps/context/apps/web/src/lib/components/CreateSpaceModal.svelte
new file mode 100644
index 000000000..9a3dd1426
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/CreateSpaceModal.svelte
@@ -0,0 +1,69 @@
+
+
+{#if open}
+
+
+
+
Neuen Space erstellen
+
+
+
+{/if}
diff --git a/apps/context/apps/web/src/lib/components/DocumentCard.svelte b/apps/context/apps/web/src/lib/components/DocumentCard.svelte
new file mode 100644
index 000000000..573a7db91
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/DocumentCard.svelte
@@ -0,0 +1,93 @@
+
+
+ (showActions = true)}
+ onmouseleave={() => (showActions = false)}
+>
+
+
+
+
+
+
+
{doc.title}
+ {#if doc.pinned}
+
+ {/if}
+
+ {#if doc.content}
+
+ {truncateText(doc.content.replace(/^#.*\n?/, ''), 150)}
+
+ {/if}
+
+
+ {config.label}
+
+ {#if doc.metadata?.tags?.length}
+ {#each doc.metadata.tags.slice(0, 3) as tag}
+
+ {tag}
+
+ {/each}
+ {/if}
+ {formatDate(doc.updated_at)}
+
+
+
+
+ {#if showActions}
+
+ {#if onTogglePin}
+
+ {/if}
+ {#if onDelete}
+
+ {/if}
+
+ {/if}
+
diff --git a/apps/context/apps/web/src/lib/components/DocumentEditor.svelte b/apps/context/apps/web/src/lib/components/DocumentEditor.svelte
new file mode 100644
index 000000000..132402d6e
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/DocumentEditor.svelte
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+ {#each [{ type: 'text' as DocumentType, icon: FileText, label: 'Text' }, { type: 'context' as DocumentType, icon: Notebook, label: 'Kontext' }, { type: 'prompt' as DocumentType, icon: Lightning, label: 'Prompt' }] as item}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {wordCount} Wörter
+ {#if saving}
+ Speichert...
+ {:else if hasUnsavedChanges}
+ Ungespeichert
+ {:else if lastSavedAt}
+ Gespeichert
+ {/if}
+
+
+
+
+
+ {#if showTags}
+
+
+ {#each tags as tag}
+
+ {tag}
+
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
+ {#if mode === 'edit'}
+
+ {:else}
+
+ {@html processedHtml()}
+
+ {/if}
+
+
+
+ {#if mode === 'edit'}
+
+ Tipp: Tippe @ um andere Dokumente zu
+ referenzieren
+
+ {/if}
+
+
+
diff --git a/apps/context/apps/web/src/lib/components/MentionInput.svelte b/apps/context/apps/web/src/lib/components/MentionInput.svelte
new file mode 100644
index 000000000..7dc4ba459
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/MentionInput.svelte
@@ -0,0 +1,135 @@
+
+
+
diff --git a/apps/context/apps/web/src/lib/components/SpaceCard.svelte b/apps/context/apps/web/src/lib/components/SpaceCard.svelte
new file mode 100644
index 000000000..9503075ee
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/SpaceCard.svelte
@@ -0,0 +1,85 @@
+
+
+ (showActions = true)}
+ onmouseleave={() => (showActions = false)}
+>
+
+
+
+
+
+
+
{space.name}
+ {#if space.pinned}
+
+ {/if}
+
+ {#if space.description}
+
{space.description}
+ {/if}
+
{formatDate(space.created_at)}
+
+
+
+ {#if showActions}
+
+ {#if onTogglePin}
+
+ {/if}
+ {#if onEdit}
+
+ {/if}
+ {#if onDelete}
+
+ {/if}
+
+ {/if}
+
diff --git a/apps/context/apps/web/src/lib/components/VersionNavigator.svelte b/apps/context/apps/web/src/lib/components/VersionNavigator.svelte
new file mode 100644
index 000000000..8511e969b
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/VersionNavigator.svelte
@@ -0,0 +1,104 @@
+
+
+{#if !loading && versions.length > 1}
+
+
+
+
+
+
+ {currentIndex + 1} / {versions.length}
+
+
+
+
+ {#if currentVersion}
+
+ {#if isOriginal}
+ Original
+ {:else if currentVersion.metadata?.generation_type}
+ {currentVersion.metadata.generation_type === 'summary'
+ ? 'Zusammenfassung'
+ : currentVersion.metadata.generation_type === 'continuation'
+ ? 'Fortsetzung'
+ : currentVersion.metadata.generation_type === 'rewrite'
+ ? 'Umformulierung'
+ : currentVersion.metadata.generation_type === 'ideas'
+ ? 'Ideen'
+ : 'KI-Version'}
+ {/if}
+
+ {#if currentVersion.metadata?.model_used}
+ ({currentVersion.metadata.model_used})
+ {/if}
+ {/if}
+
+{/if}
diff --git a/apps/context/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte b/apps/context/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte
new file mode 100644
index 000000000..694e0270d
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/apps/context/apps/web/src/lib/components/skeletons/index.ts b/apps/context/apps/web/src/lib/components/skeletons/index.ts
new file mode 100644
index 000000000..f09e744bb
--- /dev/null
+++ b/apps/context/apps/web/src/lib/components/skeletons/index.ts
@@ -0,0 +1 @@
+export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
diff --git a/apps/context/apps/web/src/lib/config/editor.ts b/apps/context/apps/web/src/lib/config/editor.ts
new file mode 100644
index 000000000..05ba8758c
--- /dev/null
+++ b/apps/context/apps/web/src/lib/config/editor.ts
@@ -0,0 +1,11 @@
+export const EDITOR_CONFIG = {
+ AUTO_SAVE_DELAY: 3000,
+ NEW_DOC_SAVE_DELAY: 2000,
+ MIN_CONTENT_LENGTH: 50,
+ DEBOUNCE_DELAY: 300,
+ SAVE_LOCK_TIMEOUT: 30000,
+ LOCAL_STORAGE_KEYS: {
+ BACKUP_PREFIX: 'doc_backup_',
+ DRAFT_PREFIX: 'doc_draft_',
+ },
+};
diff --git a/apps/context/apps/web/src/lib/i18n/index.ts b/apps/context/apps/web/src/lib/i18n/index.ts
new file mode 100644
index 000000000..ac7cb0ef9
--- /dev/null
+++ b/apps/context/apps/web/src/lib/i18n/index.ts
@@ -0,0 +1,40 @@
+import { browser } from '$app/environment';
+import { init, register, locale, waitLocale } from 'svelte-i18n';
+
+export const supportedLocales = ['de', 'en'] as const;
+export type SupportedLocale = (typeof supportedLocales)[number];
+
+const defaultLocale = 'de';
+
+register('de', () => import('./locales/de.json'));
+register('en', () => import('./locales/en.json'));
+
+function getInitialLocale(): SupportedLocale {
+ if (browser) {
+ const stored = localStorage.getItem('context_locale');
+ if (stored && supportedLocales.includes(stored as SupportedLocale)) {
+ return stored as SupportedLocale;
+ }
+
+ const browserLang = navigator.language.split('-')[0];
+ if (supportedLocales.includes(browserLang as SupportedLocale)) {
+ return browserLang as SupportedLocale;
+ }
+ }
+
+ return defaultLocale;
+}
+
+init({
+ fallbackLocale: defaultLocale,
+ initialLocale: getInitialLocale(),
+});
+
+export function setLocale(newLocale: SupportedLocale) {
+ locale.set(newLocale);
+ if (browser) {
+ localStorage.setItem('context_locale', newLocale);
+ }
+}
+
+export { waitLocale };
diff --git a/apps/context/apps/web/src/lib/i18n/locales/de.json b/apps/context/apps/web/src/lib/i18n/locales/de.json
new file mode 100644
index 000000000..c4d6ed708
--- /dev/null
+++ b/apps/context/apps/web/src/lib/i18n/locales/de.json
@@ -0,0 +1,68 @@
+{
+ "app": {
+ "name": "Context"
+ },
+ "common": {
+ "back": "Zurück",
+ "cancel": "Abbrechen",
+ "save": "Speichern",
+ "delete": "Löschen",
+ "create": "Erstellen",
+ "edit": "Bearbeiten",
+ "loading": "Lade...",
+ "search": "Suchen",
+ "confirm": "Bestätigen",
+ "close": "Schließen",
+ "pin": "Anheften",
+ "unpin": "Lösen"
+ },
+ "nav": {
+ "home": "Übersicht",
+ "spaces": "Spaces",
+ "documents": "Dokumente",
+ "settings": "Einstellungen"
+ },
+ "spaces": {
+ "title": "Spaces",
+ "create": "Neuen Space erstellen",
+ "empty": "Noch keine Spaces vorhanden",
+ "name": "Name",
+ "description": "Beschreibung",
+ "deleteConfirm": "Alle Dokumente in diesem Space werden ebenfalls gelöscht.",
+ "searchPlaceholder": "Spaces durchsuchen..."
+ },
+ "documents": {
+ "title": "Dokumente",
+ "create": "Neues Dokument",
+ "empty": "Keine Dokumente vorhanden",
+ "deleteConfirm": "Das Dokument wird unwiderruflich gelöscht.",
+ "searchPlaceholder": "Dokumente durchsuchen...",
+ "types": {
+ "all": "Alle",
+ "text": "Text",
+ "context": "Kontext",
+ "prompt": "Prompt"
+ },
+ "editor": {
+ "titlePlaceholder": "Titel...",
+ "contentPlaceholder": "Schreibe deinen Text in Markdown...",
+ "preview": "Vorschau",
+ "edit": "Bearbeiten",
+ "words": "Wörter",
+ "saving": "Speichert...",
+ "saved": "Gespeichert",
+ "unsaved": "Ungespeichert",
+ "tags": "Tags",
+ "addTag": "Tag hinzufügen..."
+ }
+ },
+ "settings": {
+ "title": "Einstellungen"
+ },
+ "messages": {
+ "saved": "Gespeichert",
+ "deleted": "Gelöscht",
+ "error": "Ein Fehler ist aufgetreten",
+ "created": "Erstellt"
+ }
+}
diff --git a/apps/context/apps/web/src/lib/i18n/locales/en.json b/apps/context/apps/web/src/lib/i18n/locales/en.json
new file mode 100644
index 000000000..e1ee3db05
--- /dev/null
+++ b/apps/context/apps/web/src/lib/i18n/locales/en.json
@@ -0,0 +1,68 @@
+{
+ "app": {
+ "name": "Context"
+ },
+ "common": {
+ "back": "Back",
+ "cancel": "Cancel",
+ "save": "Save",
+ "delete": "Delete",
+ "create": "Create",
+ "edit": "Edit",
+ "loading": "Loading...",
+ "search": "Search",
+ "confirm": "Confirm",
+ "close": "Close",
+ "pin": "Pin",
+ "unpin": "Unpin"
+ },
+ "nav": {
+ "home": "Overview",
+ "spaces": "Spaces",
+ "documents": "Documents",
+ "settings": "Settings"
+ },
+ "spaces": {
+ "title": "Spaces",
+ "create": "Create new space",
+ "empty": "No spaces yet",
+ "name": "Name",
+ "description": "Description",
+ "deleteConfirm": "All documents in this space will also be deleted.",
+ "searchPlaceholder": "Search spaces..."
+ },
+ "documents": {
+ "title": "Documents",
+ "create": "New document",
+ "empty": "No documents yet",
+ "deleteConfirm": "The document will be permanently deleted.",
+ "searchPlaceholder": "Search documents...",
+ "types": {
+ "all": "All",
+ "text": "Text",
+ "context": "Context",
+ "prompt": "Prompt"
+ },
+ "editor": {
+ "titlePlaceholder": "Title...",
+ "contentPlaceholder": "Write your text in Markdown...",
+ "preview": "Preview",
+ "edit": "Edit",
+ "words": "Words",
+ "saving": "Saving...",
+ "saved": "Saved",
+ "unsaved": "Unsaved",
+ "tags": "Tags",
+ "addTag": "Add tag..."
+ }
+ },
+ "settings": {
+ "title": "Settings"
+ },
+ "messages": {
+ "saved": "Saved",
+ "deleted": "Deleted",
+ "error": "An error occurred",
+ "created": "Created"
+ }
+}
diff --git a/apps/context/apps/web/src/lib/stores/documents.svelte.ts b/apps/context/apps/web/src/lib/stores/documents.svelte.ts
new file mode 100644
index 000000000..27fc1a129
--- /dev/null
+++ b/apps/context/apps/web/src/lib/stores/documents.svelte.ts
@@ -0,0 +1,203 @@
+import type { Document, DocumentType } from '$lib/types';
+import * as docsService from '$lib/services/documents';
+
+let documents = $state([]);
+let currentDocument = $state(null);
+let loading = $state(false);
+let saving = $state(false);
+let error = $state(null);
+
+// Filter state
+let searchQuery = $state('');
+let typeFilter = $state('all');
+let tagFilter = $state([]);
+
+export const documentsStore = {
+ get documents() {
+ return documents;
+ },
+ get currentDocument() {
+ return currentDocument;
+ },
+ get loading() {
+ return loading;
+ },
+ get saving() {
+ return saving;
+ },
+ get error() {
+ return error;
+ },
+ get searchQuery() {
+ return searchQuery;
+ },
+ get typeFilter() {
+ return typeFilter;
+ },
+ get tagFilter() {
+ return tagFilter;
+ },
+
+ get filteredDocuments() {
+ let filtered = documents;
+
+ if (typeFilter !== 'all') {
+ filtered = filtered.filter((d) => d.type === typeFilter);
+ }
+
+ if (searchQuery.trim()) {
+ const q = searchQuery.toLowerCase();
+ filtered = filtered.filter(
+ (d) => d.title.toLowerCase().includes(q) || d.content?.toLowerCase().includes(q)
+ );
+ }
+
+ if (tagFilter.length > 0) {
+ filtered = filtered.filter((d) => tagFilter.some((tag) => d.metadata?.tags?.includes(tag)));
+ }
+
+ return filtered;
+ },
+
+ get allTags() {
+ const tags = new Set();
+ documents.forEach((d) => {
+ d.metadata?.tags?.forEach((t) => tags.add(t));
+ });
+ return Array.from(tags).sort();
+ },
+
+ get stats() {
+ return {
+ total: documents.length,
+ text: documents.filter((d) => d.type === 'text').length,
+ context: documents.filter((d) => d.type === 'context').length,
+ prompt: documents.filter((d) => d.type === 'prompt').length,
+ totalWords: documents.reduce((sum, d) => sum + (d.metadata?.word_count || 0), 0),
+ };
+ },
+
+ setSearchQuery(query: string) {
+ searchQuery = query;
+ },
+
+ setTypeFilter(filter: DocumentType | 'all') {
+ typeFilter = filter;
+ },
+
+ setTagFilter(tags: string[]) {
+ tagFilter = tags;
+ },
+
+ async load(spaceId?: string) {
+ loading = true;
+ error = null;
+ try {
+ documents = await docsService.getDocumentsWithPreview(spaceId);
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Fehler beim Laden der Dokumente';
+ } finally {
+ loading = false;
+ }
+ },
+
+ async loadDocument(id: string) {
+ loading = true;
+ error = null;
+ try {
+ currentDocument = await docsService.getDocumentById(id);
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Fehler beim Laden des Dokuments';
+ } finally {
+ loading = false;
+ }
+ },
+
+ async create(
+ userId: string,
+ content: string,
+ type: DocumentType,
+ spaceId?: string,
+ title?: string
+ ) {
+ saving = true;
+ try {
+ const result = await docsService.createDocument(
+ userId,
+ content,
+ type,
+ spaceId,
+ undefined,
+ title
+ );
+ if (result.data) {
+ documents = [result.data, ...documents];
+ currentDocument = result.data;
+ }
+ return result;
+ } finally {
+ saving = false;
+ }
+ },
+
+ async update(id: string, updates: Partial) {
+ saving = true;
+ try {
+ const result = await docsService.updateDocument(id, updates);
+ if (result.success) {
+ documents = documents.map((d) => (d.id === id ? { ...d, ...updates } : d));
+ if (currentDocument?.id === id) {
+ currentDocument = { ...currentDocument, ...updates };
+ }
+ }
+ return result;
+ } finally {
+ saving = false;
+ }
+ },
+
+ async delete(id: string) {
+ const result = await docsService.deleteDocument(id);
+ if (result.success) {
+ documents = documents.filter((d) => d.id !== id);
+ if (currentDocument?.id === id) {
+ currentDocument = null;
+ }
+ }
+ return result;
+ },
+
+ async togglePinned(id: string) {
+ const doc = documents.find((d) => d.id === id);
+ if (!doc) return;
+ const newPinned = !doc.pinned;
+ const result = await docsService.toggleDocumentPinned(id, newPinned);
+ if (result.success) {
+ documents = documents.map((d) => (d.id === id ? { ...d, pinned: newPinned } : d));
+ if (currentDocument?.id === id) {
+ currentDocument = { ...currentDocument, pinned: newPinned };
+ }
+ }
+ return result;
+ },
+
+ async saveTags(id: string, tags: string[]) {
+ const result = await docsService.saveDocumentTags(id, tags);
+ if (result.success) {
+ documents = documents.map((d) =>
+ d.id === id ? { ...d, metadata: { ...d.metadata, tags } } : d
+ );
+ if (currentDocument?.id === id) {
+ currentDocument = {
+ ...currentDocument,
+ metadata: { ...currentDocument.metadata, tags },
+ };
+ }
+ }
+ return result;
+ },
+
+ clearCurrent() {
+ currentDocument = null;
+ },
+};
diff --git a/apps/context/apps/web/src/lib/stores/navigation.ts b/apps/context/apps/web/src/lib/stores/navigation.ts
new file mode 100644
index 000000000..14c37970e
--- /dev/null
+++ b/apps/context/apps/web/src/lib/stores/navigation.ts
@@ -0,0 +1,5 @@
+import { createSimpleNavigationStores } from '@manacore/shared-stores';
+
+export const { isNavCollapsed } = createSimpleNavigationStores({
+ storageKey: 'context',
+});
diff --git a/apps/context/apps/web/src/lib/stores/spaces.svelte.ts b/apps/context/apps/web/src/lib/stores/spaces.svelte.ts
new file mode 100644
index 000000000..09d2ad010
--- /dev/null
+++ b/apps/context/apps/web/src/lib/stores/spaces.svelte.ts
@@ -0,0 +1,72 @@
+import type { Space } from '$lib/types';
+import * as spacesService from '$lib/services/spaces';
+
+let spaces = $state([]);
+let loading = $state(false);
+let error = $state(null);
+
+export const spacesStore = {
+ get spaces() {
+ return spaces;
+ },
+ get loading() {
+ return loading;
+ },
+ get error() {
+ return error;
+ },
+ get pinnedSpaces() {
+ return spaces.filter((s) => s.pinned);
+ },
+
+ async load() {
+ loading = true;
+ error = null;
+ try {
+ spaces = await spacesService.getSpaces();
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Fehler beim Laden der Spaces';
+ } finally {
+ loading = false;
+ }
+ },
+
+ async getById(id: string): Promise {
+ return spacesService.getSpaceById(id);
+ },
+
+ async create(userId: string, name: string, description?: string) {
+ const result = await spacesService.createSpace(userId, name, description);
+ if (result.data) {
+ spaces = [result.data, ...spaces];
+ }
+ return result;
+ },
+
+ async update(id: string, updates: Partial) {
+ const result = await spacesService.updateSpace(id, updates);
+ if (result.success) {
+ spaces = spaces.map((s) => (s.id === id ? { ...s, ...updates } : s));
+ }
+ return result;
+ },
+
+ async togglePinned(id: string) {
+ const space = spaces.find((s) => s.id === id);
+ if (!space) return;
+ const newPinned = !space.pinned;
+ const result = await spacesService.toggleSpacePinned(id, newPinned);
+ if (result.success) {
+ spaces = spaces.map((s) => (s.id === id ? { ...s, pinned: newPinned } : s));
+ }
+ return result;
+ },
+
+ async delete(id: string) {
+ const result = await spacesService.deleteSpace(id);
+ if (result.success) {
+ spaces = spaces.filter((s) => s.id !== id);
+ }
+ return result;
+ },
+};
diff --git a/apps/context/apps/web/src/lib/stores/theme.svelte.ts b/apps/context/apps/web/src/lib/stores/theme.svelte.ts
new file mode 100644
index 000000000..9da984031
--- /dev/null
+++ b/apps/context/apps/web/src/lib/stores/theme.svelte.ts
@@ -0,0 +1,6 @@
+import { createThemeStore } from '@manacore/shared-theme';
+
+export const theme = createThemeStore({
+ appId: 'context',
+ defaultVariant: 'lume',
+});
diff --git a/apps/context/apps/web/src/lib/stores/tokens.svelte.ts b/apps/context/apps/web/src/lib/stores/tokens.svelte.ts
new file mode 100644
index 000000000..d3df199a2
--- /dev/null
+++ b/apps/context/apps/web/src/lib/stores/tokens.svelte.ts
@@ -0,0 +1,64 @@
+import * as tokensService from '$lib/services/tokens';
+import type { TokenUsageStats } from '$lib/services/tokens';
+
+let balance = $state(0);
+let loading = $state(false);
+let stats = $state(null);
+let transactions = $state([]);
+let timeframe = $state<'day' | 'week' | 'month' | 'year'>('month');
+
+export const tokensStore = {
+ get balance() {
+ return balance;
+ },
+ get loading() {
+ return loading;
+ },
+ get stats() {
+ return stats;
+ },
+ get transactions() {
+ return transactions;
+ },
+ get timeframe() {
+ return timeframe;
+ },
+
+ async loadBalance(userId: string) {
+ balance = await tokensService.getCurrentTokenBalance(userId);
+ },
+
+ async loadStats(userId: string) {
+ loading = true;
+ try {
+ stats = await tokensService.getTokenUsageStats(userId, timeframe);
+ } finally {
+ loading = false;
+ }
+ },
+
+ async loadTransactions(userId: string) {
+ transactions = await tokensService.getTokenTransactions(userId);
+ },
+
+ async loadAll(userId: string) {
+ loading = true;
+ try {
+ await Promise.all([
+ this.loadBalance(userId),
+ this.loadStats(userId),
+ this.loadTransactions(userId),
+ ]);
+ } finally {
+ loading = false;
+ }
+ },
+
+ setTimeframe(tf: 'day' | 'week' | 'month' | 'year') {
+ timeframe = tf;
+ },
+
+ updateBalance(newBalance: number) {
+ balance = newBalance;
+ },
+};
diff --git a/apps/context/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/context/apps/web/src/lib/stores/user-settings.svelte.ts
new file mode 100644
index 000000000..3591e6bf5
--- /dev/null
+++ b/apps/context/apps/web/src/lib/stores/user-settings.svelte.ts
@@ -0,0 +1,18 @@
+import { browser } from '$app/environment';
+import { createUserSettingsStore } from '@manacore/shared-theme';
+import { authStore } from './auth.svelte';
+
+function getAuthUrl(): string {
+ if (browser && typeof window !== 'undefined') {
+ const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
+ .__PUBLIC_MANA_CORE_AUTH_URL__;
+ return injectedUrl || 'http://localhost:3001';
+ }
+ return 'http://localhost:3001';
+}
+
+export const userSettings = createUserSettingsStore({
+ appId: 'context',
+ authUrl: getAuthUrl(),
+ getAccessToken: () => authStore.getAccessToken(),
+});
diff --git a/apps/context/apps/web/src/lib/types.ts b/apps/context/apps/web/src/lib/types.ts
new file mode 100644
index 000000000..adc741c98
--- /dev/null
+++ b/apps/context/apps/web/src/lib/types.ts
@@ -0,0 +1,87 @@
+export type DocumentType = 'text' | 'context' | 'prompt';
+
+export interface DocumentMetadata {
+ tags?: string[];
+ word_count?: number;
+ token_count?: number;
+ parent_document?: string;
+ version?: number;
+ generation_type?: 'summary' | 'continuation' | 'rewrite' | 'ideas';
+ model_used?: string;
+ prompt_used?: string;
+ original_title?: string;
+ version_history?: Array<{
+ id: string;
+ title: string;
+ type: string;
+ created_at: string;
+ is_original: boolean;
+ }>;
+ [key: string]: unknown;
+}
+
+export interface Document {
+ id: string;
+ title: string;
+ content: string | null;
+ type: DocumentType;
+ space_id: string | null;
+ user_id: string;
+ created_at: string;
+ updated_at: string;
+ metadata: DocumentMetadata | null;
+ short_id?: string;
+ pinned?: boolean;
+}
+
+export interface Space {
+ id: string;
+ name: string;
+ description: string | null;
+ user_id: string;
+ created_at: string;
+ settings: Record | null;
+ pinned: boolean;
+ prefix?: string;
+ text_doc_counter?: number;
+ context_doc_counter?: number;
+ prompt_doc_counter?: number;
+}
+
+export type AIProvider = 'azure' | 'google';
+
+export interface AIModelOption {
+ label: string;
+ value: string;
+ provider: AIProvider;
+}
+
+export interface AIGenerationOptions {
+ model?: string;
+ temperature?: number;
+ maxTokens?: number;
+ prompt?: string;
+ documentId?: string;
+ referencedDocuments?: { title: string; content: string }[];
+}
+
+export interface AIGenerationResult {
+ text: string;
+ tokenInfo: {
+ promptTokens: number;
+ completionTokens: number;
+ totalTokens: number;
+ tokensUsed: number;
+ remainingTokens: number;
+ };
+}
+
+export interface TokenCostEstimate {
+ inputTokens: number;
+ outputTokens: number;
+ totalTokens: number;
+ costUsd: number;
+ appTokens: number;
+ basePromptTokens?: number;
+ documentTokens?: number;
+}
diff --git a/apps/context/apps/web/src/lib/utils/markdown.ts b/apps/context/apps/web/src/lib/utils/markdown.ts
new file mode 100644
index 000000000..e11762779
--- /dev/null
+++ b/apps/context/apps/web/src/lib/utils/markdown.ts
@@ -0,0 +1,62 @@
+/**
+ * Extracts a title from markdown content (first H1 heading)
+ */
+export function extractTitleFromMarkdown(content: string): string {
+ if (!content) return 'Neues Dokument';
+
+ const lines = content.split('\n');
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (trimmed.startsWith('# ')) {
+ return trimmed.substring(2).trim();
+ }
+ }
+
+ // Fallback: use first non-empty line truncated
+ const firstLine = lines.find((l) => l.trim().length > 0);
+ if (firstLine) {
+ const clean = firstLine.replace(/^#+\s*/, '').trim();
+ return clean.length > 60 ? clean.substring(0, 57) + '...' : clean;
+ }
+
+ return 'Neues Dokument';
+}
+
+/**
+ * Simple markdown to HTML converter for preview
+ */
+export function markdownToHtml(content: string): string {
+ if (!content) return '';
+
+ let html = content
+ // Code blocks
+ .replace(/```(\w*)\n([\s\S]*?)```/g, '$2
')
+ // Inline code
+ .replace(/`([^`]+)`/g, '$1')
+ // Headers
+ .replace(/^### (.+)$/gm, '$1
')
+ .replace(/^## (.+)$/gm, '$1
')
+ .replace(/^# (.+)$/gm, '$1
')
+ // Bold & italic
+ .replace(/\*\*\*(.+?)\*\*\*/g, '$1')
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ .replace(/\*(.+?)\*/g, '$1')
+ // Blockquotes
+ .replace(/^> (.+)$/gm, '$1
')
+ // Unordered lists
+ .replace(/^[-*] (.+)$/gm, '$1')
+ // Links
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ // Horizontal rules
+ .replace(/^---$/gm, '
')
+ // Line breaks
+ .replace(/\n\n/g, '
')
+ .replace(/\n/g, '
');
+
+ // Wrap in paragraph if not already wrapped
+ if (!html.startsWith('<')) {
+ html = '
' + html + '
';
+ }
+
+ return html;
+}
diff --git a/apps/context/apps/web/src/lib/utils/text.ts b/apps/context/apps/web/src/lib/utils/text.ts
new file mode 100644
index 000000000..8abb408cb
--- /dev/null
+++ b/apps/context/apps/web/src/lib/utils/text.ts
@@ -0,0 +1,49 @@
+/**
+ * Counts words in a text string
+ */
+export function countWords(text: string): number {
+ if (!text) return 0;
+ return text
+ .trim()
+ .split(/\s+/)
+ .filter((w) => w.length > 0).length;
+}
+
+/**
+ * Estimates token count (1 token ≈ 4 characters)
+ */
+export function estimateTokens(text: string): number {
+ if (!text) return 0;
+ return Math.ceil(text.length / 4);
+}
+
+/**
+ * Formats a date string for display
+ */
+export function formatDate(dateString: string): string {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMin = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMin < 1) return 'Gerade eben';
+ if (diffMin < 60) return `vor ${diffMin} Min.`;
+ if (diffHours < 24) return `vor ${diffHours} Std.`;
+ if (diffDays < 7) return `vor ${diffDays} Tagen`;
+
+ return date.toLocaleDateString('de-DE', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ });
+}
+
+/**
+ * Truncates text to a given length
+ */
+export function truncateText(text: string, maxLength: number): string {
+ if (!text || text.length <= maxLength) return text || '';
+ return text.substring(0, maxLength - 3) + '...';
+}
diff --git a/apps/context/apps/web/src/routes/(app)/+layout.svelte b/apps/context/apps/web/src/routes/(app)/+layout.svelte
new file mode 100644
index 000000000..dc0dc677a
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/+layout.svelte
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+
+
+
+ {@render children()}
+
+
+
+
(commandBarOpen = false)}
+ onSearch={handleCommandBarSearch}
+ onSelect={handleCommandBarSelect}
+ quickActions={commandBarQuickActions}
+ placeholder="Schnellzugriff..."
+ emptyText="Keine Ergebnisse"
+ searchingText="Suche..."
+ />
+
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/+page.svelte b/apps/context/apps/web/src/routes/(app)/+page.svelte
new file mode 100644
index 000000000..ee4dcbffa
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/+page.svelte
@@ -0,0 +1,131 @@
+
+
+
+ Context - Dashboard
+
+
+{#if isLoading}
+
+{:else}
+
+
+
+
+
+
+
{spacesStore.spaces.length}
+
Spaces
+
+
+
{documentsStore.stats.total}
+
Dokumente
+
+
+
+ {documentsStore.stats.totalWords.toLocaleString()}
+
+
Wörter
+
+
+
+ {documentsStore.stats.text}/{documentsStore.stats.context}/{documentsStore.stats.prompt}
+
+
Text/Kontext/Prompt
+
+
+
+
+
+
+
+ {#if spacesStore.pinnedSpaces.length > 0}
+
+ Angeheftete Spaces
+
+ {#each spacesStore.pinnedSpaces as space}
+
+ {/each}
+
+
+ {/if}
+
+
+ {#if recentDocs.length > 0}
+
+
+
+ {#each recentDocs as doc}
+
+ {/each}
+
+
+ {:else}
+
+
+
+
+
Noch keine Dokumente
+
+ Erstelle deinen ersten Space und beginne mit dem Schreiben.
+
+
+
+ Ersten Space erstellen
+
+
+ {/if}
+
+{/if}
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/documents/+page.svelte b/apps/context/apps/web/src/routes/(app)/documents/+page.svelte
new file mode 100644
index 000000000..98d19418c
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/documents/+page.svelte
@@ -0,0 +1,187 @@
+
+
+
+ {$_('documents.title')} | Context
+
+
+
+
+
+
{$_('documents.title')}
+
+ {documentsStore.stats.total} Dokumente, {documentsStore.stats.totalWords.toLocaleString()} Wörter
+
+
+
+
+
+
+
+
+ {#each typeFilters as filter}
+
+ {/each}
+
+
+
+
+ documentsStore.setSearchQuery((e.target as HTMLInputElement).value)}
+ placeholder="Dokumente durchsuchen..."
+ class="w-full pl-8 pr-3 py-2 text-sm rounded-lg bg-card border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
+ />
+
+
+
+
+ {#if documentsStore.allTags.length > 0}
+
+ {#each documentsStore.allTags as tag}
+
+ {/each}
+
+ {/if}
+
+
+ {#if documentsStore.loading}
+
Lade Dokumente...
+ {:else if documentsStore.filteredDocuments.length > 0}
+
+ {#each documentsStore.filteredDocuments as doc}
+
+ {/each}
+
+ {:else if documentsStore.searchQuery || documentsStore.typeFilter !== 'all' || documentsStore.tagFilter.length > 0}
+
+
Keine Dokumente gefunden
+
+
+ {:else}
+
+
+
+
+
{$_('documents.empty')}
+
+ Dokumente enthalten dein Wissen, Kontext-Referenzen und AI-Prompts.
+
+
+
+ {/if}
+
+
+ (deleteTarget = null)}
+/>
diff --git a/apps/context/apps/web/src/routes/(app)/documents/[id]/+page.svelte b/apps/context/apps/web/src/routes/(app)/documents/[id]/+page.svelte
new file mode 100644
index 000000000..7d3816b42
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/documents/[id]/+page.svelte
@@ -0,0 +1,189 @@
+
+
+
+ {doc?.title || 'Dokument'} | Context
+
+
+
+ {#if loading}
+
Lade Dokument...
+ {:else if doc}
+
+
+
+ {#if doc.space_id}
+
+
+ Zurück zum Space
+
+ {:else}
+
+
+ Alle Dokumente
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if doc.short_id}
+ ID: {doc.short_id}
+ {/if}
+ {#if doc.metadata?.token_count}
+ {doc.metadata.token_count} Tokens
+ {/if}
+
+ Erstellt: {new Date(doc.created_at).toLocaleDateString('de-DE')}
+
+
+ Aktualisiert: {new Date(doc.updated_at).toLocaleDateString('de-DE')}
+
+
+
+
+ {#if showAI}
+
+ {/if}
+ {:else}
+
+
Dokument nicht gefunden
+
+ Zurück zur Übersicht
+
+
+ {/if}
+
+
+ (showDeleteConfirm = false)}
+/>
diff --git a/apps/context/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/context/apps/web/src/routes/(app)/feedback/+page.svelte
new file mode 100644
index 000000000..bcd779939
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/feedback/+page.svelte
@@ -0,0 +1,24 @@
+
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/mana/+page.svelte b/apps/context/apps/web/src/routes/(app)/mana/+page.svelte
new file mode 100644
index 000000000..6d7c4d495
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/mana/+page.svelte
@@ -0,0 +1,13 @@
+
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/profile/+page.svelte b/apps/context/apps/web/src/routes/(app)/profile/+page.svelte
new file mode 100644
index 000000000..35b348473
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/profile/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/settings/+page.svelte b/apps/context/apps/web/src/routes/(app)/settings/+page.svelte
new file mode 100644
index 000000000..0658271d4
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/settings/+page.svelte
@@ -0,0 +1,26 @@
+
+
+
+
{$_('settings.title')}
+
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/spaces/+page.svelte b/apps/context/apps/web/src/routes/(app)/spaces/+page.svelte
new file mode 100644
index 000000000..b79b43826
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/spaces/+page.svelte
@@ -0,0 +1,142 @@
+
+
+
+ Spaces | Context
+
+
+
+
+
{$_('spaces.title')}
+
+
+
+
+
+
+
+
+
+ {#if spacesStore.loading}
+
Lade Spaces...
+ {:else if filteredSpaces.length > 0}
+
+ {#each filteredSpaces as space}
+
+ {/each}
+
+ {:else if searchQuery}
+
+
Keine Spaces gefunden für "{searchQuery}"
+
+ {:else}
+
+
+
{$_('spaces.empty')}
+
+ Spaces helfen dir, dein Wissen zu organisieren. Erstelle deinen ersten Space, um loszulegen.
+
+
+
+ {/if}
+
+
+ (showCreateModal = false)}
+/>
+
+ (deleteTarget = null)}
+/>
diff --git a/apps/context/apps/web/src/routes/(app)/spaces/[id]/+page.svelte b/apps/context/apps/web/src/routes/(app)/spaces/[id]/+page.svelte
new file mode 100644
index 000000000..9367f83b1
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/spaces/[id]/+page.svelte
@@ -0,0 +1,276 @@
+
+
+
+ {space?.name || 'Space'} | Context
+
+
+
+
+
+
+
+ Spaces
+
+
/
+
{space?.name || '...'}
+
+
+ {#if loading}
+
Lade...
+ {:else if space}
+
+
+ {#if editingName}
+
+
+
+
+
+
+
+
+ {:else}
+
+
+
{space.name}
+ {#if space.description}
+
{space.description}
+ {/if}
+
+ {documentsStore.stats.total} Dokumente
+ {documentsStore.stats.totalWords.toLocaleString()} Wörter
+
+
+
+
+ {/if}
+
+
+
+
+
+ {#each typeFilters as filter}
+
+ {/each}
+
+
+
+
+
+ documentsStore.setSearchQuery((e.target as HTMLInputElement).value)}
+ placeholder="Suchen..."
+ class="pl-8 pr-3 py-1.5 text-sm rounded-lg bg-card border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary w-48"
+ />
+
+
+
+
+
+
+
+ {#if documentsStore.filteredDocuments.length > 0}
+
+ {#each documentsStore.filteredDocuments as doc}
+
+ {/each}
+
+ {:else}
+
+
Keine Dokumente in diesem Space
+
+
+ {/if}
+ {/if}
+
+
+ (deleteTarget = null)}
+/>
+
+ (showBatchCreate = false)}
+/>
diff --git a/apps/context/apps/web/src/routes/(app)/themes/+page.svelte b/apps/context/apps/web/src/routes/(app)/themes/+page.svelte
new file mode 100644
index 000000000..47b5d291f
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/themes/+page.svelte
@@ -0,0 +1,29 @@
+
+
+
+
Alle Themes
+
+
+ {#each THEME_VARIANTS as variant}
+ {@const def = THEME_DEFINITIONS[variant]}
+
+ {/each}
+
+
diff --git a/apps/context/apps/web/src/routes/(app)/tokens/+page.svelte b/apps/context/apps/web/src/routes/(app)/tokens/+page.svelte
new file mode 100644
index 000000000..3e648e3cd
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(app)/tokens/+page.svelte
@@ -0,0 +1,178 @@
+
+
+
+ Token-Management | Context
+
+
+
+
Token-Management
+
+
+
+
+
+
Aktuelles Guthaben
+
+
+
+ {tokensStore.balance.toLocaleString()}
+
+
+
Tokens
+
+
Tokens kaufen
+
+
+
+
+
+
+
Nutzung
+
+ {#each ['day', 'week', 'month', 'year'] as const as tf}
+
+ {/each}
+
+
+
+ {#if tokensStore.loading}
+
Lade Statistiken...
+ {:else if tokensStore.stats}
+
+
+
+ {tokensStore.stats.totalUsed.toLocaleString()}
+
+
Tokens verbraucht
+
+
+ {#if Object.keys(tokensStore.stats.byModel).length > 0}
+
+
Nach Modell
+
+ {#each Object.entries(tokensStore.stats.byModel) as [model, count]}
+
+ {model}
+
+ {(count as number).toLocaleString()}
+
+
+ {/each}
+
+
+ {/if}
+
+ {:else}
+
Keine Daten vorhanden
+ {/if}
+
+
+
+
+
Transaktionshistorie
+
+ {#if tokensStore.transactions.length > 0}
+
+ {#each tokensStore.transactions as tx}
+
+
+
+ {#if tx.amount > 0}
+
+ {:else}
+
+ {/if}
+
+
+
+ {formatTransactionType(tx.transaction_type)}
+
+
+ {#if tx.model_used}
+ {tx.model_used}
+ {/if}
+ {new Date(tx.created_at).toLocaleDateString('de-DE', {
+ day: '2-digit',
+ month: '2-digit',
+ year: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+ {#if tx.prompt_tokens || tx.completion_tokens}
+
+ Input: {tx.prompt_tokens?.toLocaleString()} + Output: {tx.completion_tokens?.toLocaleString()}
+ = {tx.total_tokens?.toLocaleString()}
+
+ {/if}
+
+
+
+ {tx.amount > 0 ? '+' : ''}{tx.amount.toLocaleString()}
+
+
+ {/each}
+
+ {:else}
+
Noch keine Transaktionen
+ {/if}
+
+
diff --git a/apps/context/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps/context/apps/web/src/routes/(auth)/forgot-password/+page.svelte
new file mode 100644
index 000000000..8301e87f1
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(auth)/forgot-password/+page.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/apps/context/apps/web/src/routes/(auth)/login/+page.svelte b/apps/context/apps/web/src/routes/(auth)/login/+page.svelte
new file mode 100644
index 000000000..94445c52b
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(auth)/login/+page.svelte
@@ -0,0 +1,62 @@
+
+
+
+ {translations.title} | Context
+
+
+
diff --git a/apps/context/apps/web/src/routes/(auth)/register/+page.svelte b/apps/context/apps/web/src/routes/(auth)/register/+page.svelte
new file mode 100644
index 000000000..51e3cd57d
--- /dev/null
+++ b/apps/context/apps/web/src/routes/(auth)/register/+page.svelte
@@ -0,0 +1,49 @@
+
+
+
+ {translations.title} | Context
+
+
+
diff --git a/apps/context/apps/web/src/routes/+layout.svelte b/apps/context/apps/web/src/routes/+layout.svelte
new file mode 100644
index 000000000..f599b48aa
--- /dev/null
+++ b/apps/context/apps/web/src/routes/+layout.svelte
@@ -0,0 +1,39 @@
+
+
+
+
+{#if $isLocaleLoading || loading}
+
+{:else}
+
+ {@render children()}
+
+{/if}
diff --git a/apps/context/apps/web/src/routes/health/+server.ts b/apps/context/apps/web/src/routes/health/+server.ts
new file mode 100644
index 000000000..5ae21dca9
--- /dev/null
+++ b/apps/context/apps/web/src/routes/health/+server.ts
@@ -0,0 +1,10 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+
+export const GET: RequestHandler = async () => {
+ return json({
+ status: 'ok',
+ service: 'context-web',
+ timestamp: new Date().toISOString(),
+ });
+};
diff --git a/apps/context/apps/web/src/routes/offline/+page.svelte b/apps/context/apps/web/src/routes/offline/+page.svelte
new file mode 100644
index 000000000..0e0e9125b
--- /dev/null
+++ b/apps/context/apps/web/src/routes/offline/+page.svelte
@@ -0,0 +1,96 @@
+
+
+
+ Offline - Context
+
+
+
+
+
+
+
+ {isOnline ? 'Verbindung wiederhergestellt!' : 'Du bist offline'}
+
+
+
+ {#if isOnline}
+ Du wirst gleich weitergeleitet...
+ {:else}
+ Einige Funktionen sind offline nicht verfügbar. Bitte überprüfe deine Internetverbindung.
+ {/if}
+
+
+ {#if !isOnline}
+
+
+ Zur Startseite
+
+
+
+
+ {:else}
+
+
+ Weiterleitung...
+
+ {/if}
+
+
diff --git a/apps/context/apps/web/svelte.config.js b/apps/context/apps/web/svelte.config.js
new file mode 100644
index 000000000..4ed1e3b74
--- /dev/null
+++ b/apps/context/apps/web/svelte.config.js
@@ -0,0 +1,15 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter({
+ out: 'build',
+ }),
+ },
+};
+
+export default config;
diff --git a/apps/context/apps/web/tsconfig.json b/apps/context/apps/web/tsconfig.json
new file mode 100644
index 000000000..a8f10c8e3
--- /dev/null
+++ b/apps/context/apps/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/apps/context/apps/web/vite.config.ts b/apps/context/apps/web/vite.config.ts
new file mode 100644
index 000000000..14db96e84
--- /dev/null
+++ b/apps/context/apps/web/vite.config.ts
@@ -0,0 +1,30 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+import { SvelteKitPWA } from '@vite-pwa/sveltekit';
+import { createPWAConfig } from '@manacore/shared-pwa';
+import { MANACORE_SHARED_PACKAGES } from '@manacore/shared-vite-config';
+
+export default defineConfig({
+ plugins: [
+ sveltekit(),
+ SvelteKitPWA(
+ createPWAConfig({
+ name: 'Context - Wissensmanagement',
+ shortName: 'Context',
+ description: 'AI-gestütztes Dokumenten- und Wissensmanagement',
+ themeColor: '#0ea5e9',
+ preset: 'minimal',
+ })
+ ),
+ ],
+ server: {
+ port: 5192,
+ strictPort: true,
+ },
+ ssr: {
+ noExternal: [...MANACORE_SHARED_PACKAGES],
+ },
+ optimizeDeps: {
+ exclude: [...MANACORE_SHARED_PACKAGES],
+ },
+});