mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 03:41:10 +02:00
feat(templates): two more non-AI templates + split gallery into two sections
Closes out T1 with three templates per category as discussed. The gallery now renders agent-templates and workbench-templates as two distinct labeled sections — the earlier implicit "everything's a template for an agent" framing is gone. Seed handlers (new): - apps/mana/apps/web/src/lib/modules/habits/seed.ts — title-based idempotency (there's no description column on LocalHabit). If a non-deleted habit with the same title exists, the seed is skipped. - apps/mana/apps/web/src/lib/companion/goals/seed.ts — title-based idempotency on companionGoals where status !== 'abandoned'. - Both pulled in via side-effect imports in missions/setup.ts so the handler registry is populated before any apply. New templates: - 🏋️ Fitness (wellness) — scene body/habits/stretch/sleep + 3 habit seeds (Täglich 30min Bewegung, 3× Woche Training, 2L Wasser) + 1 goal seed (3 Workouts pro Woche). No agent. - 💻 Deep Work (work) — scene todo/calendar/notes/times + 2 habit seeds (1 wichtigste Aufgabe pro Tag, 4h Deep Work pro Tag) + 1 goal seed (20h Deep Work pro Woche). No agent. Gallery two-section layout: - Title "Templates" (not "Agent-Templates") — broader framing. - Section 1: "🤖 Agent-Templates" — filters ALL_TEMPLATES where category ∈ {'ai','delight'}: Recherche-Agent, Kontext-Agent, Today-Agent. - Section 2: "🎨 Workbench-Templates" — filters to the rest: Calmness, Fitness, Deep Work. - Each section gets a short intro paragraph so users understand the distinction before scanning the cards. - Cards themselves unchanged; rendering extracted into a {#snippet templateCard(t)} shared between both sections. - Per-category arrays computed once at module-load time (const in <script>); no per-render filter cost. Result: each section has 3 templates, categorised by "does this create an AI agent" rather than by use-case. Keeps the separation honest — Agent-Templates set up autonomous work; Workbench-Templates set up the user's own workspace. Tests: shared-ai 26/26, webapp svelte-check 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f76d3926e
commit
9161c0b3ab
7 changed files with 390 additions and 34 deletions
62
apps/mana/apps/web/src/lib/companion/goals/seed.ts
Normal file
62
apps/mana/apps/web/src/lib/companion/goals/seed.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Goals module seed handler — applied by the workbench-template
|
||||
* applicator when a template includes `seeds.goals`.
|
||||
*
|
||||
* Idempotency: matched by exact-title. A non-deleted goal with the
|
||||
* same title is treated as "already seeded". Users can rename the
|
||||
* goal freely without breaking idempotency because we only check on
|
||||
* apply, not on update.
|
||||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import { registerSeedHandler, type SeedOutcome } from '$lib/data/ai/agents/seed-registry';
|
||||
import { goalStore } from './store';
|
||||
import type { LocalGoal, GoalMetric, GoalTarget } from './types';
|
||||
|
||||
interface GoalSeed {
|
||||
title: string;
|
||||
description?: string;
|
||||
moduleId: string;
|
||||
metric: GoalMetric;
|
||||
target: GoalTarget;
|
||||
}
|
||||
|
||||
registerSeedHandler({
|
||||
moduleName: 'goals',
|
||||
async apply(items) {
|
||||
const outcomes: SeedOutcome[] = [];
|
||||
const existing = await db.table<LocalGoal>('companionGoals').toArray();
|
||||
const existingTitles = new Set(
|
||||
existing.filter((g) => g.status !== 'abandoned').map((g) => g.title)
|
||||
);
|
||||
|
||||
for (const item of items) {
|
||||
const seed = item.data as GoalSeed;
|
||||
|
||||
if (existingTitles.has(seed.title)) {
|
||||
outcomes.push({ stableId: item.stableId, outcome: 'skipped-exists' });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await goalStore.create({
|
||||
title: seed.title,
|
||||
description: seed.description,
|
||||
moduleId: seed.moduleId,
|
||||
metric: seed.metric,
|
||||
target: seed.target,
|
||||
});
|
||||
existingTitles.add(seed.title);
|
||||
outcomes.push({ stableId: item.stableId, outcome: 'created' });
|
||||
} catch (err) {
|
||||
outcomes.push({
|
||||
stableId: item.stableId,
|
||||
outcome: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return outcomes;
|
||||
},
|
||||
});
|
||||
|
|
@ -27,6 +27,8 @@ import { runAgentsBootstrap } from '../agents/bootstrap';
|
|||
// handler to a rarely-used module slows down the hot path otherwise.
|
||||
// See docs/plans/workbench-templates.md §T1.
|
||||
import '$lib/modules/meditate/seed';
|
||||
import '$lib/modules/habits/seed';
|
||||
import '$lib/companion/goals/seed';
|
||||
import type { AiPlanInput, AiPlanOutput } from './planner/types';
|
||||
|
||||
/** Default interval between tick scans. One minute is fine for foreground use. */
|
||||
|
|
|
|||
60
apps/mana/apps/web/src/lib/modules/habits/seed.ts
Normal file
60
apps/mana/apps/web/src/lib/modules/habits/seed.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Habits module seed handler — applied by the workbench-template
|
||||
* applicator when a template includes `seeds.habits`.
|
||||
*
|
||||
* Idempotency: matched by exact-title. A non-deleted habit with the
|
||||
* same title is treated as "already seeded" — user may have already
|
||||
* applied the template or typed a matching habit themselves. Either
|
||||
* way we don't want to create a duplicate.
|
||||
*/
|
||||
|
||||
import { habitTable } from './collections';
|
||||
import type { LocalHabit } from './types';
|
||||
import { registerSeedHandler, type SeedOutcome } from '$lib/data/ai/agents/seed-registry';
|
||||
import { habitsStore } from './stores/habits.svelte';
|
||||
|
||||
interface HabitSeed {
|
||||
title: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
targetPerDay?: number | null;
|
||||
defaultDuration?: number | null;
|
||||
}
|
||||
|
||||
registerSeedHandler({
|
||||
moduleName: 'habits',
|
||||
async apply(items) {
|
||||
const outcomes: SeedOutcome[] = [];
|
||||
const existing = (await habitTable.toArray()) as LocalHabit[];
|
||||
const existingTitles = new Set(existing.filter((h) => !h.deletedAt).map((h) => h.title));
|
||||
|
||||
for (const item of items) {
|
||||
const seed = item.data as HabitSeed;
|
||||
|
||||
if (existingTitles.has(seed.title)) {
|
||||
outcomes.push({ stableId: item.stableId, outcome: 'skipped-exists' });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await habitsStore.createHabit({
|
||||
title: seed.title,
|
||||
icon: seed.icon,
|
||||
color: seed.color,
|
||||
targetPerDay: seed.targetPerDay ?? null,
|
||||
defaultDuration: seed.defaultDuration ?? null,
|
||||
});
|
||||
existingTitles.add(seed.title); // guard against intra-batch duplicates
|
||||
outcomes.push({ stableId: item.stableId, outcome: 'created' });
|
||||
} catch (err) {
|
||||
outcomes.push({
|
||||
stableId: item.stableId,
|
||||
outcome: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return outcomes;
|
||||
},
|
||||
});
|
||||
|
|
@ -15,6 +15,13 @@
|
|||
import { applyTemplate } from '$lib/data/ai/agents/apply-template';
|
||||
|
||||
let selected = $state<AgentTemplate | null>(null);
|
||||
const agentTemplates = ALL_TEMPLATES.filter(
|
||||
(t) => t.category === 'ai' || t.category === 'delight'
|
||||
);
|
||||
const workbenchTemplates = ALL_TEMPLATES.filter(
|
||||
(t) => t.category !== 'ai' && t.category !== 'delight'
|
||||
);
|
||||
|
||||
let applying = $state(false);
|
||||
let result = $state<{
|
||||
agentName?: string;
|
||||
|
|
@ -93,50 +100,72 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Agent-Templates — Mana</title>
|
||||
<title>Templates — Mana</title>
|
||||
</svelte:head>
|
||||
|
||||
{#snippet templateCard(t: AgentTemplate)}
|
||||
<button
|
||||
type="button"
|
||||
class="card"
|
||||
class:selected={selected?.id === t.id}
|
||||
style="--accent: {t.color}"
|
||||
onclick={() => openDetail(t)}
|
||||
>
|
||||
<span class="avatar">{t.agent?.avatar ?? t.icon}</span>
|
||||
<span class="label">{t.label}</span>
|
||||
<span class="tagline">{t.tagline}</span>
|
||||
<span class="meta">
|
||||
{#if t.agent}<span class="chip">Agent</span>{/if}
|
||||
{#if t.scene}<span class="chip">Scene</span>{/if}
|
||||
{#if t.missions && t.missions.length > 0}
|
||||
<span class="chip">{t.missions.length} Mission{t.missions.length !== 1 ? 'en' : ''}</span>
|
||||
{/if}
|
||||
{#if t.seeds}
|
||||
{@const total = Object.values(t.seeds).reduce((s, items) => s + items.length, 0)}
|
||||
<span class="chip">{total} Seed{total !== 1 ? 's' : ''}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
<div class="page">
|
||||
<header class="header">
|
||||
<button type="button" class="back" onclick={() => goto('/')}>
|
||||
<ArrowLeft size={14} /><span>Zurück zum Workbench</span>
|
||||
</button>
|
||||
<h1>Agent-Templates</h1>
|
||||
<h1>Templates</h1>
|
||||
<p class="sub">
|
||||
Vorgefertigte AI-Agenten, die sofort loslaufen. Jedes Template legt einen Agent, eine passende
|
||||
Scene und eine Starter-Mission an — die Mission ist standardmäßig pausiert, damit du bewusst
|
||||
Play drückst.
|
||||
Vorgefertigte Setups für deinen Workbench. Wähle ein Template und du hast in einem Klick eine
|
||||
passende Scene, optionale AI-Agenten und erste Daten in den richtigen Modulen.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
{#each ALL_TEMPLATES as t (t.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="card"
|
||||
class:selected={selected?.id === t.id}
|
||||
style="--accent: {t.color}"
|
||||
onclick={() => openDetail(t)}
|
||||
>
|
||||
<span class="avatar">{t.agent?.avatar ?? t.icon}</span>
|
||||
<span class="label">{t.label}</span>
|
||||
<span class="tagline">{t.tagline}</span>
|
||||
<span class="meta">
|
||||
{#if t.agent}<span class="chip">Agent</span>{/if}
|
||||
{#if t.scene}<span class="chip">Scene</span>{/if}
|
||||
{#if t.missions && t.missions.length > 0}
|
||||
<span class="chip"
|
||||
>{t.missions.length} Mission{t.missions.length !== 1 ? 'en' : ''}</span
|
||||
>
|
||||
{/if}
|
||||
{#if t.seeds}
|
||||
{@const total = Object.values(t.seeds).reduce((s, items) => s + items.length, 0)}
|
||||
<span class="chip">{total} Seed{total !== 1 ? 's' : ''}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2>🤖 Agent-Templates</h2>
|
||||
<p>Benannte AI-Personas mit eigener Policy, Memory und Starter-Mission.</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{#each agentTemplates as t (t.id)}
|
||||
{@render templateCard(t)}
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2>🎨 Workbench-Templates</h2>
|
||||
<p>
|
||||
Starter-Kits ohne AI — Scene-Layout + vor-gefüllte Habits, Goals und Module-Daten. Du
|
||||
arbeitest selbst, das Template nimmt dir die Einrichtung ab.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{#each workbenchTemplates as t (t.id)}
|
||||
{@render templateCard(t)}
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if selected}
|
||||
<section class="detail" style="--accent: {selected.color}">
|
||||
|
|
@ -350,6 +379,22 @@
|
|||
max-width: 60ch;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
.section-head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.section-head p {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
max-width: 60ch;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
|
|
|
|||
83
packages/shared-ai/src/agents/templates/deep-work.ts
Normal file
83
packages/shared-ai/src/agents/templates/deep-work.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { WorkbenchTemplate } from './types';
|
||||
|
||||
/**
|
||||
* Deep Work — work-category Workbench-Template.
|
||||
*
|
||||
* Ein Starter-Kit für konzentrierte Arbeitsphasen. Scene mit Todo /
|
||||
* Calendar / Notes / Times + zwei Habits (Deep-Work-Zeit tracken, 1
|
||||
* "wichtigste Aufgabe" pro Tag definieren) + ein Wochenziel für
|
||||
* Fokus-Stunden.
|
||||
*
|
||||
* Kein AI-Agent. Das Template gibt die Struktur vor, der User bringt
|
||||
* die Arbeit.
|
||||
*/
|
||||
|
||||
export const deepWorkTemplate: WorkbenchTemplate = {
|
||||
id: 'deep-work',
|
||||
version: '1',
|
||||
label: 'Deep Work',
|
||||
icon: '💻',
|
||||
tagline: 'Konzentrierter Arbeitsplatz mit Fokus-Tracking',
|
||||
description: `Ein Starter-Kit für konzentrierte Arbeit. Legt dir eine Scene mit Todo, Kalender, Notes und Time-Tracking an und seed-et zwei Habits plus ein Wochenziel für Deep-Work-Stunden.
|
||||
|
||||
Was drin ist:
|
||||
|
||||
- **Scene**: Todo · Kalender · Notes · Times — alles für fokussierte Sessions nebeneinander.
|
||||
- **2 Habits**:
|
||||
- 🎯 1 wichtigste Aufgabe pro Tag
|
||||
- ⏱ 4h Deep Work pro Tag
|
||||
- **1 Wochenziel**: "20h Deep Work pro Woche" — zählt abgeschlossene Time-Tracking-Sessions.
|
||||
|
||||
Kein Agent. Das Setup ist da; der Rest ist deine Disziplin.`,
|
||||
category: 'work',
|
||||
color: '#1F2937',
|
||||
scene: {
|
||||
name: 'Deep Work',
|
||||
description: 'Fokus, Kalender, Notes, Zeit',
|
||||
openApps: [
|
||||
{ appId: 'todo', widthPx: 540 },
|
||||
{ appId: 'calendar', widthPx: 540 },
|
||||
{ appId: 'notes', widthPx: 440 },
|
||||
{ appId: 'times', widthPx: 340 },
|
||||
],
|
||||
},
|
||||
seeds: {
|
||||
habits: [
|
||||
{
|
||||
stableId: 'template-deepwork:habit:top-task',
|
||||
data: {
|
||||
title: '1 wichtigste Aufgabe pro Tag',
|
||||
icon: '🎯',
|
||||
color: '#1F2937',
|
||||
targetPerDay: 1,
|
||||
defaultDuration: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
stableId: 'template-deepwork:habit:deep-work-hours',
|
||||
data: {
|
||||
title: '4h Deep Work pro Tag',
|
||||
icon: '⏱',
|
||||
color: '#374151',
|
||||
targetPerDay: 4,
|
||||
defaultDuration: 60,
|
||||
},
|
||||
},
|
||||
],
|
||||
goals: [
|
||||
{
|
||||
stableId: 'template-deepwork:goal:weekly-focus',
|
||||
data: {
|
||||
title: '20h Deep Work pro Woche',
|
||||
description: 'Summiert Time-Tracking-Sessions im Times-Modul.',
|
||||
moduleId: 'times',
|
||||
metric: {
|
||||
source: 'event_count',
|
||||
eventType: 'TimeSessionCompleted',
|
||||
},
|
||||
target: { value: 20, period: 'week', comparison: 'gte' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
93
packages/shared-ai/src/agents/templates/fitness.ts
Normal file
93
packages/shared-ai/src/agents/templates/fitness.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import type { WorkbenchTemplate } from './types';
|
||||
|
||||
/**
|
||||
* Fitness — wellness-category Workbench-Template.
|
||||
*
|
||||
* Scene für Körper-Arbeit + drei Grund-Habits + ein Wochenziel. Keine
|
||||
* AI. Der User trackt selbst; das Template nimmt ihm nur die initiale
|
||||
* Einrichtung ab.
|
||||
*
|
||||
* Zielgruppe: jemand der "ich will regelmäßig trainieren" sagt und
|
||||
* nicht erst 15 Minuten Setup-Zeit investieren will. 1-Klick-Setup.
|
||||
*/
|
||||
|
||||
export const fitnessTemplate: WorkbenchTemplate = {
|
||||
id: 'fitness',
|
||||
version: '1',
|
||||
label: 'Fitness',
|
||||
icon: '🏋️',
|
||||
tagline: 'Workout-Workspace mit Basis-Habits und Wochenziel',
|
||||
description: `Ein Starter-Kit für regelmäßige Bewegung. Legt dir eine Scene mit Body, Habits, Stretch und Sleep an und seed-et drei Habits und ein Wochenziel — dann kannst du direkt loslegen.
|
||||
|
||||
Was drin ist:
|
||||
|
||||
- **Scene**: Body · Habits · Stretch · Sleep — alles was du zum Tracken brauchst, nebeneinander.
|
||||
- **3 Habits** mit sinnvollen Default-Icons:
|
||||
- 🏃 Täglich 30min Bewegung
|
||||
- 🏋️ 3× Woche Training
|
||||
- 💧 2L Wasser täglich
|
||||
- **1 Wochenziel**: "3 Workouts pro Woche" — der Goal-Tracker zählt TaskCompleted-Events aus dem Body-Modul.
|
||||
|
||||
Kein Agent. Keine Automation. Du trainierst, die Workbench zählt.`,
|
||||
category: 'wellness',
|
||||
color: '#EF4444',
|
||||
scene: {
|
||||
name: 'Fitness',
|
||||
description: 'Bewegung, Kraft, Schlaf',
|
||||
openApps: [
|
||||
{ appId: 'body', widthPx: 540 },
|
||||
{ appId: 'habits', widthPx: 440 },
|
||||
{ appId: 'stretch', widthPx: 340 },
|
||||
{ appId: 'sleep', widthPx: 340 },
|
||||
],
|
||||
},
|
||||
seeds: {
|
||||
habits: [
|
||||
{
|
||||
stableId: 'template-fitness:habit:daily-movement',
|
||||
data: {
|
||||
title: 'Täglich 30min Bewegung',
|
||||
icon: '🏃',
|
||||
color: '#EF4444',
|
||||
targetPerDay: 1,
|
||||
defaultDuration: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
stableId: 'template-fitness:habit:weekly-training',
|
||||
data: {
|
||||
title: '3× Woche Training',
|
||||
icon: '🏋️',
|
||||
color: '#F97316',
|
||||
targetPerDay: null,
|
||||
defaultDuration: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
stableId: 'template-fitness:habit:hydration',
|
||||
data: {
|
||||
title: '2L Wasser täglich',
|
||||
icon: '💧',
|
||||
color: '#0EA5E9',
|
||||
targetPerDay: 8,
|
||||
defaultDuration: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
goals: [
|
||||
{
|
||||
stableId: 'template-fitness:goal:weekly-workouts',
|
||||
data: {
|
||||
title: '3 Workouts pro Woche',
|
||||
description: 'Zählt abgeschlossene Workouts im Body-Modul.',
|
||||
moduleId: 'body',
|
||||
metric: {
|
||||
source: 'event_count',
|
||||
eventType: 'TaskCompleted',
|
||||
},
|
||||
target: { value: 3, period: 'week', comparison: 'gte' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -11,6 +11,8 @@ import { researchTemplate } from './research';
|
|||
import { contextTemplate } from './context';
|
||||
import { todayTemplate } from './today';
|
||||
import { calmnessTemplate } from './calmness';
|
||||
import { fitnessTemplate } from './fitness';
|
||||
import { deepWorkTemplate } from './deep-work';
|
||||
|
||||
export type {
|
||||
// Generalised names (T1 of workbench-templates plan):
|
||||
|
|
@ -34,9 +36,18 @@ export const ALL_TEMPLATES = [
|
|||
contextTemplate,
|
||||
todayTemplate,
|
||||
calmnessTemplate,
|
||||
fitnessTemplate,
|
||||
deepWorkTemplate,
|
||||
] as const;
|
||||
|
||||
export { researchTemplate, contextTemplate, todayTemplate, calmnessTemplate };
|
||||
export {
|
||||
researchTemplate,
|
||||
contextTemplate,
|
||||
todayTemplate,
|
||||
calmnessTemplate,
|
||||
fitnessTemplate,
|
||||
deepWorkTemplate,
|
||||
};
|
||||
|
||||
/** Lookup helper — returns the template matching the given id, or
|
||||
* undefined. Useful for deep-links `/agents/templates?pick=research`. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue