From acd7e0d6b05434e994573021fb4489a1b77f9803 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 16 Apr 2026 15:00:09 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20update=20architecture=20comparison=20?= =?UTF-8?q?=E2=80=94=205/10=20roadmap=20items=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update report to reflect all completed work: - Matrix: streaming ✅, tool registration updated to 29 tools + MCP - §5.2 Streaming: marked done - §5.3 Tool System: marked done - §6 Table: items 1-3 + 5 struck through with commit refs - §8 Fazit: updated gaps and recommendations 5 of 10 roadmap items complete in one session: 1. SSE Streaming, 2. Dynamic Tool Registry, 3. Budget Enforcement, 5. MCP Server Export (27/29 tools with DB ops), plus Tool Drift Fix. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/modules/storage/routes.ts | 32 + apps/mana/apps/web/src/lib/api/profile.ts | 49 +- .../apps/web/src/lib/app-registry/apps.ts | 2 +- .../onboarding/OnboardingWizard.svelte | 2 + .../onboarding/steps/ContextStep.svelte | 55 ++ .../lib/data/ai/missions/default-resolvers.ts | 68 ++ .../web/src/lib/data/ai/missions/runner.ts | 16 +- .../apps/web/src/lib/data/crypto/registry.ts | 12 +- apps/mana/apps/web/src/lib/data/database.ts | 8 + .../apps/web/src/lib/data/module-registry.ts | 2 + .../lib/modules/kontext/KontextView.svelte | 688 ++++-------------- .../modules/profile/ContextFreeform.svelte | 471 ++++++++++++ .../modules/profile/ContextInterview.svelte | 567 +++++++++++++++ .../modules/profile/ContextOverview.svelte | 471 ++++++++++++ .../src/lib/modules/profile/ListView.svelte | 231 ++++-- .../src/lib/modules/profile/collections.ts | 8 + .../src/lib/modules/profile/module.config.ts | 6 + .../web/src/lib/modules/profile/queries.ts | 18 + .../web/src/lib/modules/profile/questions.ts | 325 +++++++++ .../profile/stores/user-context.svelte.ts | 154 ++++ .../apps/web/src/lib/modules/profile/types.ts | 110 +++ .../ai-agent-architecture-comparison.md | 16 +- .../shared-ai/src/agents/templates/context.ts | 47 +- .../mana-ai/src/db/resolvers/encrypted.ts | 29 + services/mana-ai/src/db/resolvers/index.ts | 2 + services/mana-auth/src/routes/auth.ts | 16 + 26 files changed, 2744 insertions(+), 661 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/components/onboarding/steps/ContextStep.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/profile/ContextFreeform.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/profile/ContextInterview.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/profile/ContextOverview.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/profile/collections.ts create mode 100644 apps/mana/apps/web/src/lib/modules/profile/module.config.ts create mode 100644 apps/mana/apps/web/src/lib/modules/profile/queries.ts create mode 100644 apps/mana/apps/web/src/lib/modules/profile/questions.ts create mode 100644 apps/mana/apps/web/src/lib/modules/profile/stores/user-context.svelte.ts create mode 100644 apps/mana/apps/web/src/lib/modules/profile/types.ts diff --git a/apps/api/src/modules/storage/routes.ts b/apps/api/src/modules/storage/routes.ts index a3ee7c34e..339ceb3c1 100644 --- a/apps/api/src/modules/storage/routes.ts +++ b/apps/api/src/modules/storage/routes.ts @@ -106,6 +106,38 @@ routes.get('/files/:id/download', async (c) => { } }); +// ─── Avatar Upload (profile) ─────────────────────────────── + +const ALLOWED_AVATAR_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']); + +routes.post('/avatar/upload', async (c) => { + const userId = c.get('userId'); + const formData = await c.req.formData(); + const file = formData.get('file') as File | null; + + if (!file) return c.json({ error: 'No file' }, 400); + if (file.size > 5 * 1024 * 1024) return c.json({ error: 'Max 5MB' }, 400); + if (!ALLOWED_AVATAR_TYPES.has(file.type)) { + return c.json({ error: 'Invalid file type. Allowed: JPEG, PNG, GIF, WebP' }, 400); + } + + try { + const buffer = await file.arrayBuffer(); + const { uploadImageToMedia } = await import('../../lib/media'); + const result = await uploadImageToMedia( + buffer, + `avatar-${userId}.${file.name.split('.').pop()}`, + { + app: 'profile', + userId, + } + ); + return c.json({ url: result.urls.thumbnail || result.urls.original, mediaId: result.id }, 201); + } catch (_err) { + return c.json({ error: 'Avatar upload failed' }, 500); + } +}); + // ─── Version Upload ───────────────────────────────────────── routes.post('/files/:id/versions', async (c) => { diff --git a/apps/mana/apps/web/src/lib/api/profile.ts b/apps/mana/apps/web/src/lib/api/profile.ts index 1ba48b6c4..51e7e9446 100644 --- a/apps/mana/apps/web/src/lib/api/profile.ts +++ b/apps/mana/apps/web/src/lib/api/profile.ts @@ -4,7 +4,7 @@ */ import { authStore } from '$lib/stores/auth.svelte'; -import { getManaAuthUrl } from './config'; +import { getManaAuthUrl, getManaApiUrl } from './config'; // Types export interface UserProfile { @@ -32,11 +32,13 @@ export interface DeleteAccountRequest { reason?: string; } -export interface AvatarUploadUrlResponse { - uploadUrl: string; - fileUrl: string; - key: string; - expiresIn: number; +export interface ChangeEmailRequest { + newEmail: string; +} + +export interface AvatarUploadResponse { + url: string; + mediaId: string; } // Helper function for authenticated requests @@ -104,36 +106,31 @@ export const profileService = { }, /** - * Get presigned URL for avatar upload + * Change email address (sends verification to new email) */ - async getAvatarUploadUrl(filename: string): Promise { - return fetchWithAuth('/api/v1/storage/avatar/upload-url', { + async changeEmail(data: ChangeEmailRequest): Promise<{ success: boolean; message: string }> { + return fetchWithAuth('/api/v1/auth/change-email', { method: 'POST', - body: JSON.stringify({ filename }), + body: JSON.stringify(data), }); }, /** - * Upload avatar file using presigned URL, then update profile + * Upload avatar file directly, then update profile */ async uploadAvatar(file: File): Promise<{ success: boolean; user: UserProfile }> { - // 1. Get presigned upload URL - const { uploadUrl, fileUrl } = await this.getAvatarUploadUrl(file.name); + const token = await authStore.getValidToken(); + const formData = new FormData(); + formData.append('file', file); - // 2. Upload file directly to S3/MinIO - const uploadResponse = await fetch(uploadUrl, { - method: 'PUT', - body: file, - headers: { - 'Content-Type': file.type, - }, + const uploadResponse = await fetch(`${getManaApiUrl()}/api/v1/storage/avatar/upload`, { + method: 'POST', + headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: formData, }); - if (!uploadResponse.ok) { - throw new Error('Avatar-Upload fehlgeschlagen'); - } - - // 3. Update profile with new image URL - return this.updateProfile({ image: fileUrl }); + if (!uploadResponse.ok) throw new Error('Avatar-Upload fehlgeschlagen'); + const { url } = (await uploadResponse.json()) as AvatarUploadResponse; + return this.updateProfile({ image: url }); }, }; diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index cc61f2864..b2ff7d44b 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -526,7 +526,7 @@ registerApp({ registerApp({ id: 'kontext', - name: 'Kontext', + name: 'Web-Context', color: '#A78B6F', icon: Scroll, views: { diff --git a/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte b/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte index 838098a71..9d72f1994 100644 --- a/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte +++ b/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte @@ -8,6 +8,7 @@ import AiTierStep from './steps/AiTierStep.svelte'; import SyncStep from './steps/SyncStep.svelte'; import CreditsStep from './steps/CreditsStep.svelte'; + import ContextStep from './steps/ContextStep.svelte'; import CompleteStep from './steps/CompleteStep.svelte'; import { Check } from '@mana/shared-icons'; @@ -30,6 +31,7 @@ const STEPS = [ { id: 'welcome', label: 'Willkommen', component: WelcomeStep }, { id: 'profile', label: 'Profil', component: ProfileStep }, + { id: 'context', label: 'Über dich', component: ContextStep }, { id: 'apps', label: 'Apps', component: AppsStep }, { id: 'ai-tier', label: 'KI', component: AiTierStep }, { id: 'sync', label: 'Sync', component: SyncStep }, diff --git a/apps/mana/apps/web/src/lib/components/onboarding/steps/ContextStep.svelte b/apps/mana/apps/web/src/lib/components/onboarding/steps/ContextStep.svelte new file mode 100644 index 000000000..f40907f3b --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/onboarding/steps/ContextStep.svelte @@ -0,0 +1,55 @@ + + + +
+
+

Erzähl Mana über dich

+

+ Je besser Mana dich kennt, desto relevanter werden Vorschläge und Automatisierungen. Du kannst + alles jederzeit im Profil ändern oder ergänzen. +

+
+ +
+ +
+
+ + diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts b/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts index 3757add4b..6bcb84a67 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts @@ -52,6 +52,72 @@ const kontextResolver: InputResolver = async (ref) => { }; }; +// ── User Context (structured profile + freeform) ────────── + +interface UserContextLike { + id: string; + about?: { bio?: string; occupation?: string; location?: string; languages?: string[] }; + interests?: string[]; + routine?: { wakeUp?: string; workStart?: string; workEnd?: string; bedtime?: string }; + nutrition?: { diet?: string; allergies?: string[]; preferences?: string }; + goals?: string[]; + social?: { communication?: string; workStyle?: string }; + freeform?: string; +} + +const userContextResolver: InputResolver = async (ref) => { + const doc = await db.table('userContext').get(ref.id); + if (!doc) return null; + const [decrypted] = await decryptRecords('userContext', [doc]); + return { + id: ref.id, + module: 'profile', + table: 'userContext', + title: 'Nutzerprofil', + content: buildUserContextText(decrypted), + }; +}; + +function buildUserContextText(ctx: UserContextLike): string { + const lines: string[] = []; + if (ctx.about?.occupation) lines.push(`Beruf: ${ctx.about.occupation}`); + if (ctx.about?.location) lines.push(`Ort: ${ctx.about.location}`); + if (ctx.about?.languages?.length) lines.push(`Sprachen: ${ctx.about.languages.join(', ')}`); + if (ctx.about?.bio) lines.push(`\nBio: ${ctx.about.bio}`); + if (ctx.interests?.length) lines.push(`\nInteressen: ${ctx.interests.join(', ')}`); + if (ctx.routine) { + const r = ctx.routine; + const parts = []; + if (r.wakeUp) parts.push(`Aufstehen ${r.wakeUp}`); + if (r.workStart && r.workEnd) parts.push(`Arbeit ${r.workStart}–${r.workEnd}`); + if (r.bedtime) parts.push(`Schlafenszeit ${r.bedtime}`); + if (parts.length) lines.push(`\nTagesroutine: ${parts.join(', ')}`); + } + if (ctx.nutrition) { + if (ctx.nutrition.diet) lines.push(`Ernährung: ${ctx.nutrition.diet}`); + if (ctx.nutrition.allergies?.length) + lines.push(`Allergien: ${ctx.nutrition.allergies.join(', ')}`); + } + if (ctx.goals?.length) lines.push(`\nZiele: ${ctx.goals.join(', ')}`); + if (ctx.social?.workStyle) lines.push(`Arbeitsweise: ${ctx.social.workStyle}`); + if (ctx.freeform?.trim()) lines.push(`\n---\n${ctx.freeform.trim()}`); + return lines.join('\n'); +} + +const userContextIndexer: InputIndexer = async () => { + const doc = await db.table('userContext').get('singleton'); + if (!doc) return []; + return [ + { + module: 'profile', + table: 'userContext', + id: 'singleton', + label: 'Nutzerprofil', + hint: 'Strukturiertes Profil + Freitext-Kontext', + }, + ]; +}; + interface GoalLike { id: string; title?: string; @@ -224,11 +290,13 @@ export function registerDefaultInputResolvers(): void { if (registered) return; registerInputResolver('notes', notesResolver); registerInputResolver('kontext', kontextResolver); + registerInputResolver('profile', userContextResolver); registerInputResolver('goals', goalsResolver); registerInputResolver('todo', tasksResolver); registerInputResolver('calendar', calendarResolver); registerInputIndexer('notes', notesIndexer); registerInputIndexer('kontext', kontextIndexer); + registerInputIndexer('profile', userContextIndexer); registerInputIndexer('goals', goalsIndexer); registerInputIndexer('todo', tasksIndexer); registerInputIndexer('calendar', calendarIndexer); diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/runner.ts b/apps/mana/apps/web/src/lib/data/ai/missions/runner.ts index 2ce7dbe4b..c1c97abc7 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/runner.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/runner.ts @@ -194,19 +194,9 @@ export async function runMission( const resolvedInputs: ResolvedInput[] = [...baseInputs]; const preStep: AiDebugEntry['preStep'] = { kontextInjected: false }; - // Auto-inject agent-specific kontext doc (if non-empty) — replaces - // the old global singleton inject. Falls back to the global singleton - // when the agent doesn't have its own doc. Decrypted client-side. - const alreadyHasKontext = mission!.inputs.some((i) => i.module === 'kontext'); - if (!alreadyHasKontext) { - const kontextEntry = owningAgent - ? await loadAgentKontextAsResolvedInput(owningAgent.id) - : await loadKontextAsResolvedInput(); - if (kontextEntry) { - resolvedInputs.push(kontextEntry); - preStep.kontextInjected = true; - } - } + // User context and agent kontext are available as explicit mission + // inputs via the input picker — no auto-inject. The user decides + // what context the AI sees. // Pre-step web research: if the objective looks like research, // run the deep-research pipeline (mana-search + mana-llm) and diff --git a/apps/mana/apps/web/src/lib/data/crypto/registry.ts b/apps/mana/apps/web/src/lib/data/crypto/registry.ts index d8ee306c2..512f477de 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -467,11 +467,17 @@ export const ENCRYPTION_REGISTRY: Record = { moodEntries: { enabled: true, fields: ['withWhom', 'notes'] }, moodSettings: { enabled: false, fields: [] }, - // ─── Kontext ───────────────────────────────────────────── - // Singleton markdown document ("Was soll Mana über dich wissen?"). - // Free-form user text — encrypt the content, leave the fixed id plaintext. + // ─── Kontext (legacy — now web-context, URL-crawl only) ── kontextDoc: { enabled: true, fields: ['content'] }, + // ─── User Context (profile hub) ────────────────────────── + // Structured profile sections + freeform markdown. Everything + // except the fixed id and interview progress is user-typed content. + userContext: { + enabled: true, + fields: ['about', 'interests', 'routine', 'nutrition', 'goals', 'social', 'freeform'], + }, + // Per-agent kontext documents — same schema as kontextDoc but keyed // per agent. Content is free-form markdown. agentKontextDocs: { enabled: true, fields: ['content'] }, diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index f1a505f1b..8f25274e0 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -555,6 +555,14 @@ db.version(22).stores({ agentKontextDocs: 'id, agentId', }); +// v23 — User context: structured profile + freeform markdown. +// Singleton record ('id') holding structured sections (about, interests, +// routine, nutrition, goals, social), freeform markdown, and interview +// progress. Replaces kontextDoc as the central "who is the user?" store. +db.version(23).stores({ + userContext: 'id', +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/apps/mana/apps/web/src/lib/data/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index 269631e18..38ab55a0a 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -97,6 +97,7 @@ import { sleepModuleConfig } from '$lib/modules/sleep/module.config'; import { moodModuleConfig } from '$lib/modules/mood/module.config'; import { kontextModuleConfig } from '$lib/modules/kontext/module.config'; import { quizModuleConfig } from '$lib/modules/quiz/module.config'; +import { profileModuleConfig } from '$lib/modules/profile/module.config'; import { aiModuleConfig } from '$lib/data/ai/module.config'; export const MODULE_CONFIGS: readonly ModuleConfig[] = [ @@ -150,6 +151,7 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ moodModuleConfig, kontextModuleConfig, quizModuleConfig, + profileModuleConfig, aiModuleConfig, ]; diff --git a/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte b/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte index c2386a75c..3db69fe4e 100644 --- a/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte +++ b/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte @@ -1,27 +1,13 @@ - -
-
-
- {#if saveState === 'pending'} - Speichert… - {:else if saveState === 'saved'} - Gespeichert - {/if} +
+
+
-
- - +
+

Web-Context

+

Crawle Webseiten und speichere den Inhalt in deinem Profil-Kontext

- {#if urlPanelOpen} -
-
- - - -
-
- - - · - -
- {#if importing || importPhase !== 'idle'} -
    - {#each importPhases as phase (phase.key)} -
  1. - - {phase.label} - {#if phase.active} - {importElapsed}s - {/if} -
  2. - {/each} -
- {/if} - {#if importError} -

{importError}

- {/if} -
- {/if} - - {#if mode === 'edit'} - - {:else if renderedHtml} - -
{@html renderedHtml}
- {:else} - - {/if} - - {#if doc?.content?.trim()} -
- {#if noteSaved} - - {/if} - - {#if noteError} - - {/if} -
+
+ +
+ + + · + +
+ + {#if importing || importPhase !== 'idle'} +
    + {#each importPhases as phase (phase.key)} +
  1. + + {phase.label} + {#if phase.active} + {importElapsed}s + {/if} +
  2. + {/each} +
+ {/if} + + {#if importError} +

{importError}

+ {/if} + + + {#if successMessage} +
+

{successMessage}

+
{/if} + +
+

Importierte Inhalte werden im Freitext-Tab deines Profils gespeichert.

+

Der Crawler respektiert robots.txt und nutzt Rate-Limits.

+
diff --git a/apps/mana/apps/web/src/lib/modules/profile/ContextFreeform.svelte b/apps/mana/apps/web/src/lib/modules/profile/ContextFreeform.svelte new file mode 100644 index 000000000..53a16e17f --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/profile/ContextFreeform.svelte @@ -0,0 +1,471 @@ + + + + + +
+
+
+ {#if saveState === 'pending'}Speichert… + {:else if saveState === 'saved'}Gespeichert{/if} +
+
+ + +
+
+ + {#if urlPanelOpen} +
+
+ + + +
+
+ + + · + +
+ {#if importError}

{importError}

{/if} +
+ {/if} + + {#if mode === 'edit'} + + {:else if renderedHtml} + +
{@html renderedHtml}
+ {:else} + + {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/profile/ContextInterview.svelte b/apps/mana/apps/web/src/lib/modules/profile/ContextInterview.svelte new file mode 100644 index 000000000..6acafadc8 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/profile/ContextInterview.svelte @@ -0,0 +1,567 @@ + + + +
+ {#if !compact} +
+
+
+

{progress.answered} von {progress.total} Fragen beantwortet

+ {/if} + +
+ {#each categories as cat (cat.key)} + {@const cp = categoryProgress[cat.key]} + + {/each} +
+ + {#if currentQuestion} +
+

{currentQuestion.question}

+ {#if currentQuestion.hint}

{currentQuestion.hint}

{/if} + +
+ {#if currentQuestion.inputType === 'text'} + e.key === 'Enter' && handleAnswer()} + /> + {:else if currentQuestion.inputType === 'textarea'} + + {:else if currentQuestion.inputType === 'time'} + + {:else if currentQuestion.inputType === 'choice'} +
+ {#each currentQuestion.choices ?? [] as choice (choice)} + + {/each} +
+ {:else if currentQuestion.inputType === 'tags'} +
+ {#if Array.isArray(inputValue)} +
+ {#each inputValue as tag (tag)}{tag}{/each} +
+ {/if} + +
+ {:else if currentQuestion.inputType === 'weekdays'} +
+ {#each WEEKDAYS as day (day.value)} + + {/each} +
+ {/if} +
+ + {#if answeredSet.has(currentQuestion.id)}

+ Bereits beantwortet — du kannst die Antwort aktualisieren +

{/if} + +
+ + +
+ + +
+ {:else} +

Alle Fragen in dieser Kategorie sind beantwortet!

+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/profile/ContextOverview.svelte b/apps/mana/apps/web/src/lib/modules/profile/ContextOverview.svelte new file mode 100644 index 000000000..133c79dfa --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/profile/ContextOverview.svelte @@ -0,0 +1,471 @@ + + + +
+
+
+ {#if user?.image}Avatar + {:else}
+ {(user?.name ?? 'U').slice(0, 2).toUpperCase()} +
{/if} +
+
+

{user?.name ?? 'Unbekannt'}

+

{user?.email ?? ''}

+ {#if ctx?.about?.occupation}

{ctx.about.occupation}

{/if} + {#if ctx?.about?.location}

{ctx.about.location}

{/if} +
+
+ + {#if progress.percent < 50} + + {/if} + +
+ {#if ctx?.about?.bio || editingField === 'about.bio'} +
+

Über mich

+ {#if editingField === 'about.bio'} + +
+ + +
+ {:else}

startEdit('about.bio', ctx?.about?.bio)}> + {ctx?.about?.bio} +

{/if} +
+ {/if} + +
+

Interessen

+ {#if editingField === 'interests'} +
+
+ {#each editValue as string[] as tag (tag)}{tag}{/each} +
+ { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + addEditTag(); + } + }} + onblur={addEditTag} + /> +
+ + +
+
+ {:else if ctx?.interests?.length} +
startEdit('interests', ctx?.interests ?? [])}> + {#each ctx.interests as tag (tag)}{tag}{/each} +
+ {:else} + + {/if} +
+ + {#if ctx?.routine && (ctx.routine.wakeUp || ctx.routine.workStart || ctx.routine.bedtime || ctx.routine.workDays?.length)} +
+

Tagesablauf

+
+ {#if ctx.routine.wakeUp}
+ Aufstehen{ctx.routine.wakeUp} +
{/if} + {#if ctx.routine.workStart && ctx.routine.workEnd}
+ Arbeit{ctx.routine.workStart} – {ctx.routine.workEnd} +
{/if} + {#if ctx.routine.bedtime}
+ Schlafenszeit{ctx.routine.bedtime} +
{/if} + {#if ctx.routine.workDays?.length}
+ Arbeitstage{ctx.routine.workDays.map((d: number) => WEEKDAY_NAMES[d]).join(', ')} +
{/if} +
+
+ {/if} + + {#if ctx?.nutrition && (ctx.nutrition.diet || ctx.nutrition.allergies?.length)} +
+

Ernährung

+ {#if ctx.nutrition.diet}

{ctx.nutrition.diet}

{/if} + {#if ctx.nutrition.allergies?.length}
+ {#each ctx.nutrition.allergies as a (a)}{a}{/each} +
{/if} + {#if ctx.nutrition.preferences}

+ {ctx.nutrition.preferences} +

{/if} +
+ {/if} + + {#if ctx?.goals?.length} +
+

Ziele

+
startEdit('goals', ctx?.goals ?? [])}> + {#each ctx.goals as goal (goal)}{goal}{/each} +
+
+ {/if} + + {#if ctx?.social && (ctx.social.workStyle || ctx.social.communication)} +
+

Arbeitsstil

+
+ {#if ctx.social.workStyle}
+ Arbeitsweise{ctx.social.workStyle} +
{/if} + {#if ctx.social.communication}
+ Kommunikation{ctx.social.communication} +
{/if} +
+
+ {/if} + + {#if ctx?.about?.languages?.length} +
+

Sprachen

+
+ {#each ctx.about.languages as lang (lang)}{lang}{/each} +
+
+ {/if} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte b/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte index abbd56b72..d52300444 100644 --- a/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte @@ -1,27 +1,29 @@