fix: Mobile responsiveness for Focus Tools & AI chat improvements
continuous-integration/drone/push Build is passing

Mobile Responsiveness:
- Fix headers on all Focus Tools pages (responsive text sizes, proper spacing)
- Fix BodyDoublingLobby form grid (stacks on mobile)
- Add shrink-0 and min-w-0 to prevent flex overflow issues

AI Chat Improvements:
- Add labelName parameter to createTask tool for easier label assignment
- Auto-match or create labels when labelName is provided
- Improve system prompt to act immediately without confirmation
- Clearer instructions about using tools and handling labels
- Better support for Ollama models with function calling
This commit is contained in:
Paul Nothaft
2026-01-16 15:58:00 +01:00
parent 7ce4f7efdc
commit f165ae52e6
12 changed files with 3682 additions and 2219 deletions
@@ -134,13 +134,13 @@ export function BodyDoublingLobby() {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Users className="h-6 w-6 text-primary" />
<h2 className="text-xl sm:text-2xl font-bold flex items-center gap-2">
<Users className="h-5 w-5 sm:h-6 sm:w-6 text-primary" />
{t('adhd.bodyDoubling.title')}
</h2>
<p className="text-muted-foreground">
<p className="text-sm sm:text-base text-muted-foreground">
{t('adhd.bodyDoubling.description')}
</p>
</div>
@@ -190,7 +190,7 @@ export function BodyDoublingLobby() {
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label htmlFor="startsAt">{t('adhd.bodyDoubling.startTime')}</Label>
<Input
+8 -7
View File
@@ -80,21 +80,22 @@ export default function ADHDDashboardPage() {
return (
<div className="max-w-6xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-3 sm:gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
className="shrink-0"
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-400 to-pink-500 text-white">
<Brain className="h-6 w-6" />
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
<div className="p-2 sm:p-3 rounded-xl bg-gradient-to-br from-purple-400 to-pink-500 text-white shrink-0">
<Brain className="h-5 w-5 sm:h-6 sm:w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.dashboard.title', 'ADHD Dashboard')}</h1>
<p className="text-muted-foreground">{t('adhd.dashboard.subtitle', 'Tools designed for how your brain works')}</p>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-bold truncate">{t('adhd.dashboard.title', 'ADHD Dashboard')}</h1>
<p className="text-sm sm:text-base text-muted-foreground truncate">{t('adhd.dashboard.subtitle', 'Tools designed for how your brain works')}</p>
</div>
</div>
</div>
+8 -7
View File
@@ -24,21 +24,22 @@ export default function BodyDoublingPage() {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-3 sm:gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
className="shrink-0"
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-green-400 to-teal-500 text-white">
<Users className="h-6 w-6" />
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
<div className="p-2 sm:p-3 rounded-xl bg-gradient-to-br from-green-400 to-teal-500 text-white shrink-0">
<Users className="h-5 w-5 sm:h-6 sm:w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.bodyDoubling.title')}</h1>
<p className="text-muted-foreground">{t('adhd.bodyDoubling.description')}</p>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-bold">{t('adhd.bodyDoubling.title')}</h1>
<p className="text-sm sm:text-base text-muted-foreground">{t('adhd.bodyDoubling.description')}</p>
</div>
</div>
</div>
+8 -7
View File
@@ -28,21 +28,22 @@ export default function QuickWinsPage() {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-3 sm:gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
className="shrink-0"
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-yellow-400 to-orange-500 text-white">
<Zap className="h-6 w-6" />
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
<div className="p-2 sm:p-3 rounded-xl bg-gradient-to-br from-yellow-400 to-orange-500 text-white shrink-0">
<Zap className="h-5 w-5 sm:h-6 sm:w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.quickWins.title')}</h1>
<p className="text-muted-foreground">{t('adhd.quickWins.description')}</p>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-bold">{t('adhd.quickWins.title')}</h1>
<p className="text-sm sm:text-base text-muted-foreground">{t('adhd.quickWins.description')}</p>
</div>
</div>
</div>
+8 -7
View File
@@ -54,21 +54,22 @@ export default function SingleTaskPage() {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-3 sm:gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
className="shrink-0"
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-400 to-indigo-500 text-white">
<Focus className="h-6 w-6" />
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
<div className="p-2 sm:p-3 rounded-xl bg-gradient-to-br from-blue-400 to-indigo-500 text-white shrink-0">
<Focus className="h-5 w-5 sm:h-6 sm:w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.singleTask.title')}</h1>
<p className="text-muted-foreground">{t('adhd.singleTask.description')}</p>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-bold">{t('adhd.singleTask.title')}</h1>
<p className="text-sm sm:text-base text-muted-foreground">{t('adhd.singleTask.description')}</p>
</div>
</div>
</div>
+34
View File
@@ -0,0 +1,34 @@
import { chromium } from '@playwright/test';
async function openBrowser() {
const browser = await chromium.launch({
headless: false,
slowMo: 100
});
const context = await browser.newContext();
const page = await context.newPage();
// Navigate to app
await page.goto('http://localhost:2100');
await page.waitForTimeout(2000);
// Check if we need to login
const loginInput = page.locator('input[name="username"]');
if (await loginInput.isVisible({ timeout: 3000 }).catch(() => false)) {
console.log('Logging in as admin...');
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'admin');
await page.click('button[type="submit"]');
await page.waitForTimeout(2000);
console.log('Logged in! Browser is open.');
} else {
console.log('Already logged in or different page. Browser is open.');
}
// Keep browser open
console.log('Press Ctrl+C to close the browser.');
await new Promise(() => {}); // Keep running indefinitely
}
openBrowser().catch(console.error);
+2376 -1845
View File
File diff suppressed because it is too large Load Diff
+1158
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'list',
use: {
baseURL: 'http://localhost:2100',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
timeout: 120000,
});
+55 -14
View File
@@ -60,15 +60,26 @@ Answer the user's questions based on this context. Be concise, helpful, and frie
### 🛠️ AVAILABLE TOOLS
You can create, search, update, and delete tasks using the provided tools.
**CRITICAL: ALWAYS USE TOOLS IMMEDIATELY**
- When the user asks to create a task, IMMEDIATELY call createTask - DO NOT ask for confirmation.
- When the user provides multiple tasks, call createTask for EACH task in parallel.
- Never say "I will create..." or "Should I create..." - just DO IT.
**1. Task Management**
- **Create**: Use 'createTask'. Title is required.
- *Labels*: Use 'labelName' parameter with the label text (e.g., "Work", "Personal", "Shopping"). The system will automatically match or create the label.
- *Bulk Creation*: If the user provides a list of tasks, call 'createTask' multiple times in parallel.
- *Relative Dates*: Understand natural language! "Morgen" = Tomorrow, "Next Friday" = Date of next Friday. Always calculate the specific ISO string based on 'Current Date/Time'.
- *Priority*: Infer priority from context. Urgent/important = high, regular = medium, minor = low.
- *Returns*: The tool returns the created Task ID. Remember this ID for immediate edits.
- **Update/Delete**: First SEARCH for the task ID using 'searchTasks' (search by title), then use 'updateTask' or 'deleteTask'.
- *Editing Recently Created*: If the user says "Change that to...", refer to the ID of the task you just created.
- *Delete All*: Search all, then delete each.
- **Labels**: Use 'getLabels' tags.
### 🏷️ LABEL RULES
- When the user mentions a category like "Work task", "Personal errand", "Shopping item" - use the labelName parameter.
- Common labels: Work, Personal, Shopping, Health, Finance, Home, Family, Study
- The system automatically matches existing labels or creates new ones.
### 🛡️ SAFETY RULES
- **DELETION CONFIRMATION**: Before using 'deleteTask', you MUST:
@@ -89,10 +100,12 @@ You can create, search, update, and delete tasks using the provided tools.
### 📅 DATE & TIME RULES
- **"Today"**: Use the 'Current Date/Time' context.
- **"Morgen" / "Tomorrow"**: Add 1 day to Current Date.
- **"Next week"**: Add 7 days to Current Date.
- **Scheduling**: When using 'scheduleTask', inform the user specifically *when* you scheduled it.
IMPORTANT: Do NOT show Task IDs to the user. Reference tasks by Title.
CRITICAL: After executing tools, provide a concise summary of your actions.`;
CRITICAL: After executing tools, provide a concise summary of your actions.
CRITICAL: ACT IMMEDIATELY. Do not ask for confirmation before creating tasks.`;
}
// Variable replacement if using stored template
@@ -311,19 +324,20 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
type: "function",
function: {
name: "createTask",
description: "Create a new task for the user.",
description: "Create a new task for the user. ALWAYS call this tool immediately when the user asks to create a task - do not ask for confirmation first.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "The title of the task (required)." },
description: { type: "string" },
priority: { type: "string", enum: ["low", "medium", "high"] },
status: { type: "string", enum: ["todo", "inProgress", "done"] },
dueDate: { type: "string", description: "ISO 8601 format (YYYY-MM-DD)." },
labelId: { type: "string" },
description: { type: "string", description: "Optional task description." },
priority: { type: "string", enum: ["low", "medium", "high"], description: "Task priority. Default: medium." },
status: { type: "string", enum: ["todo", "inProgress", "done"], description: "Task status. Default: todo." },
dueDate: { type: "string", description: "Due date in ISO 8601 format (YYYY-MM-DD). Calculate from relative dates like 'tomorrow', 'next week'." },
labelId: { type: "string", description: "The label ID (UUID). Use getLabels first to find the correct ID." },
labelName: { type: "string", description: "Alternative: Label name (e.g., 'Work', 'Personal'). Will be matched or created automatically." },
estimatedDuration: { type: "integer", description: "Estimated duration in minutes." },
parentTaskId: { type: "string" },
startDate: { type: "string" }
parentTaskId: { type: "string", description: "Parent task ID for subtasks." },
startDate: { type: "string", description: "Start date in ISO 8601 format." }
},
required: ["title"]
}
@@ -523,13 +537,40 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
result = { success: true, labelId: newLabel.id, message: "Label created." };
}
} else if (fnName === "createTask") {
// Handle labelName -> labelId conversion
let resolvedLabelId = args.labelId || null;
let labelMessage = "";
if (!resolvedLabelId && args.labelName) {
const labels = await this.storage.getLabels(user.id);
const matchedLabel = labels.find(l =>
l.name.toLowerCase() === args.labelName.toLowerCase() ||
l.name.toLowerCase().includes(args.labelName.toLowerCase()) ||
args.labelName.toLowerCase().includes(l.name.toLowerCase())
);
if (matchedLabel) {
resolvedLabelId = matchedLabel.id;
labelMessage = ` with label "${matchedLabel.name}"`;
} else {
// Create new label if not found
const newLabel = await this.storage.createLabel({
name: args.labelName,
color: "#6366f1",
creatorId: user.id
});
resolvedLabelId = newLabel.id;
labelMessage = ` with new label "${args.labelName}"`;
}
}
const taskData = {
title: args.title,
description: args.description || null,
priority: args.priority || "medium",
status: args.status || "todo",
dueDate: args.dueDate ? new Date(args.dueDate) : null,
labelId: args.labelId || null,
labelId: resolvedLabelId,
estimatedDuration: args.estimatedDuration || null,
parentTaskId: args.parentTaskId || null,
startDate: args.startDate ? new Date(args.startDate) : null,
@@ -541,10 +582,10 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
action: "CREATE",
entityType: "TASK",
entityId: createdTask.id,
details: { title: createdTask.title },
details: { title: createdTask.title, labelId: resolvedLabelId },
source: "AI"
});
result = { success: true, taskId: createdTask.id, message: "Task created." };
result = { success: true, taskId: createdTask.id, message: `Task created${labelMessage}.` };
} else if (fnName === "searchTasks") {
const tasks = await this.storage.searchTasks(args.query, user.id);
const labels = await this.storage.getLabels(user.id);
+1 -3
View File
@@ -1,6 +1,4 @@
{
"status": "failed",
"failedTests": [
"5e4c35f9f3e132884f88-fffc858497b1fef540d4"
]
"failedTests": []
}
@@ -1,324 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e6]:
- list [ref=e8]:
- listitem [ref=e9]:
- button "Logo TaskFlow Personal" [ref=e10] [cursor=pointer]:
- img "Logo" [ref=e12]
- generic [ref=e13]:
- generic [ref=e14]: TaskFlow
- generic [ref=e15]: Personal
- list [ref=e17]:
- listitem [ref=e18]:
- button "Focus" [ref=e19] [cursor=pointer]:
- img [ref=e20]
- generic [ref=e24]: Focus
- listitem [ref=e25]:
- button "Tasks" [ref=e26] [cursor=pointer]:
- img [ref=e27]
- generic [ref=e30]: Tasks
- listitem [ref=e31]:
- button "Calendar" [ref=e32] [cursor=pointer]:
- img [ref=e33]
- generic [ref=e35]: Calendar
- listitem [ref=e36]:
- button "Week" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- generic [ref=e39]: Week
- listitem [ref=e40]:
- button "Unscheduled Tasks" [ref=e41] [cursor=pointer]:
- img [ref=e42]
- generic [ref=e46]: Unscheduled Tasks
- listitem [ref=e47]:
- button "Kanban" [ref=e48] [cursor=pointer]:
- img [ref=e49]
- generic [ref=e54]: Kanban
- listitem [ref=e55]:
- button "Achievements" [ref=e56] [cursor=pointer]:
- img [ref=e57]
- generic [ref=e63]: Achievements
- listitem [ref=e64]:
- button "Leaderboard" [ref=e65] [cursor=pointer]:
- img [ref=e66]
- generic [ref=e69]: Leaderboard
- listitem [ref=e70]:
- button "AI Chat" [ref=e71] [cursor=pointer]:
- img [ref=e72]
- generic [ref=e75]: AI Chat
- listitem [ref=e76]:
- button "Settings" [ref=e77] [cursor=pointer]:
- img [ref=e78]
- generic [ref=e81]: Settings
- generic [ref=e82]:
- generic [ref=e83] [cursor=pointer]:
- generic [ref=e84]:
- generic [ref=e85]:
- img [ref=e87]
- generic [ref=e93]:
- text: Level 4
- generic [ref=e94]: Artisan
- generic [ref=e95]:
- img [ref=e96]
- generic [ref=e98]: "2"
- generic [ref=e99]:
- generic [ref=e100]:
- generic [ref=e101]: 884 XP
- generic [ref=e102]: 1000 XP
- progressbar [ref=e103]
- generic [ref=e105]:
- button "theme.light" [ref=e107] [cursor=pointer]:
- img
- generic [ref=e108]: theme.light
- button "Notifications" [ref=e110] [cursor=pointer]:
- img
- button "Logout" [ref=e112] [cursor=pointer]:
- img
- button "Toggle Sidebar" [ref=e114] [cursor=pointer]:
- img
- generic [ref=e115]: Toggle Sidebar
- button "Toggle Sidebar" [ref=e116] [cursor=pointer]
- main [ref=e117]:
- generic [ref=e118]:
- main [ref=e119]:
- generic [ref=e120]:
- heading "Settings" [level=1] [ref=e122]
- generic [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- img [ref=e126]
- text: Account
- generic [ref=e129]: Manage your account settings
- generic [ref=e130]:
- generic [ref=e131]:
- paragraph [ref=e132]: Username
- paragraph [ref=e133]: admin
- generic [ref=e134]:
- paragraph [ref=e135]: Email
- generic [ref=e136]:
- paragraph [ref=e137]: admin@example.com
- button [ref=e138] [cursor=pointer]:
- img
- generic [ref=e139]:
- paragraph [ref=e140]: User ID
- paragraph [ref=e141]: 3aa35824-197c-43d2-bdde-3fbe0bdc0d4b
- button "Change Password" [ref=e143] [cursor=pointer]
- generic [ref=e144]:
- generic [ref=e145]:
- generic [ref=e146]:
- img [ref=e147]
- text: Notifications
- generic [ref=e150]: Get alerted about upcoming and overdue tasks.
- generic [ref=e152]:
- generic [ref=e153]:
- paragraph [ref=e154]: Enable Browser Notifications
- paragraph [ref=e155]: Permission denied by browser. Please reset site permissions.
- switch [disabled] [ref=e156]
- generic [ref=e157]:
- generic [ref=e158]:
- generic [ref=e159]:
- img [ref=e160]
- text: Social & Privacy
- generic [ref=e163]: Manage your visibility and social features
- generic [ref=e164]:
- generic [ref=e165]:
- generic [ref=e166]:
- paragraph [ref=e167]: Public Leaderboard
- paragraph [ref=e168]: Show my profile on the global leaderboard
- switch [ref=e169] [cursor=pointer]
- generic [ref=e170]:
- generic [ref=e171]:
- paragraph [ref=e172]: Allow others to find me
- paragraph [ref=e173]: Allow users to search for me to share tasks
- switch [ref=e174] [cursor=pointer]
- generic [ref=e175]:
- generic [ref=e176]:
- paragraph [ref=e177]: Enable AI Assistant
- paragraph [ref=e178]: Allow the AI assistant to help you with tasks and organization.
- switch [checked] [ref=e179] [cursor=pointer]
- generic [ref=e180]:
- generic [ref=e181]:
- paragraph [ref=e182]: Two-Factor Authentication
- paragraph [ref=e183]: Secure your account with email-based 2FA
- switch [ref=e184] [cursor=pointer]
- button "Share access to all tasks..." [ref=e186] [cursor=pointer]:
- img
- text: Share access to all tasks...
- generic [ref=e187]:
- generic [ref=e188]:
- generic [ref=e189]:
- img [ref=e190]
- text: Language
- generic [ref=e193]: Choose your preferred language
- combobox [ref=e195] [cursor=pointer]:
- generic: English
- img [ref=e196]
- generic [ref=e198]:
- generic [ref=e200]:
- generic [ref=e201]:
- generic [ref=e202]:
- img [ref=e203]
- text: Task Labels
- generic [ref=e206]: Create and manage labels to organize your tasks
- button "Create Label" [ref=e207] [cursor=pointer]:
- img
- text: Create Label
- generic [ref=e209]:
- generic [ref=e211]:
- generic [ref=e214]: Work
- generic [ref=e215]:
- button [ref=e216] [cursor=pointer]:
- img
- button [ref=e217] [cursor=pointer]:
- img
- generic [ref=e219]:
- generic [ref=e222]: Personal
- generic [ref=e223]:
- button [ref=e224] [cursor=pointer]:
- img
- button [ref=e225] [cursor=pointer]:
- img
- generic [ref=e227]:
- generic [ref=e230]: Urgent
- generic [ref=e231]:
- button [ref=e232] [cursor=pointer]:
- img
- button [ref=e233] [cursor=pointer]:
- img
- generic [ref=e235]:
- generic [ref=e238]: Study
- generic [ref=e239]:
- button [ref=e240] [cursor=pointer]:
- img
- button [ref=e241] [cursor=pointer]:
- img
- generic [ref=e243]:
- generic [ref=e246]: DockerTestLabel
- generic [ref=e247]:
- button [ref=e248] [cursor=pointer]:
- img
- button [ref=e249] [cursor=pointer]:
- img
- generic [ref=e251]:
- generic [ref=e254]: DockerTestLabel
- generic [ref=e255]:
- button "Share Label" [ref=e256] [cursor=pointer]:
- img
- button [ref=e257] [cursor=pointer]:
- img
- button [ref=e258] [cursor=pointer]:
- img
- generic [ref=e260]:
- generic [ref=e263]: DockerFinalLabel
- generic [ref=e264]:
- button "Share Label" [ref=e265] [cursor=pointer]:
- img
- button [ref=e266] [cursor=pointer]:
- img
- button [ref=e267] [cursor=pointer]:
- img
- generic [ref=e269]:
- generic [ref=e272]: Work
- generic [ref=e273]:
- button "Share Label" [ref=e274] [cursor=pointer]:
- img
- button [ref=e275] [cursor=pointer]:
- img
- button [ref=e276] [cursor=pointer]:
- img
- generic [ref=e278]:
- generic [ref=e281]: Personal
- generic [ref=e282]:
- button "Share Label" [ref=e283] [cursor=pointer]:
- img
- button [ref=e284] [cursor=pointer]:
- img
- button [ref=e285] [cursor=pointer]:
- img
- generic [ref=e287]:
- generic [ref=e290]: Work
- generic [ref=e291]:
- button "Share Label" [ref=e292] [cursor=pointer]:
- img
- button [ref=e293] [cursor=pointer]:
- img
- button [ref=e294] [cursor=pointer]:
- img
- generic [ref=e296]:
- generic [ref=e299]: Personal
- generic [ref=e300]:
- button "Share Label" [ref=e301] [cursor=pointer]:
- img
- button [ref=e302] [cursor=pointer]:
- img
- button [ref=e303] [cursor=pointer]:
- img
- generic [ref=e305]:
- generic [ref=e308]: Work
- generic [ref=e309]:
- button "Share Label" [ref=e310] [cursor=pointer]:
- img
- button [ref=e311] [cursor=pointer]:
- img
- button [ref=e312] [cursor=pointer]:
- img
- generic [ref=e314]:
- generic [ref=e317]: Personal
- generic [ref=e318]:
- button "Share Label" [ref=e319] [cursor=pointer]:
- img
- button [ref=e320] [cursor=pointer]:
- img
- button [ref=e321] [cursor=pointer]:
- img
- generic [ref=e322]:
- generic [ref=e323]:
- generic [ref=e324]:
- img [ref=e325]
- text: Project Templates
- generic [ref=e329]: Use templates to quickly create projects
- button "Manage Templates" [ref=e331] [cursor=pointer]:
- img
- text: Manage Templates
- generic [ref=e332]:
- generic [ref=e333]:
- generic [ref=e334]:
- img [ref=e335]
- text: Data Export
- generic [ref=e340]: Download your data as a JSON file.
- generic [ref=e341]:
- generic [ref=e342]:
- checkbox "Tasks" [checked] [ref=e343] [cursor=pointer]:
- generic:
- img
- generic [ref=e344]: Tasks
- generic [ref=e345]:
- checkbox "Labels" [checked] [ref=e346] [cursor=pointer]:
- generic:
- img
- generic [ref=e347]: Labels
- generic [ref=e348]:
- checkbox "System Settings (Admin)" [ref=e349] [cursor=pointer]
- generic [ref=e350]: System Settings (Admin)
- button "Export Data" [ref=e352] [cursor=pointer]:
- img
- text: Export Data
- generic [ref=e353]:
- generic [ref=e354]:
- generic [ref=e355]:
- img [ref=e356]
- text: Admin Settings
- generic [ref=e359]: System administration and configuration
- generic [ref=e360]:
- button "Manage Users" [ref=e361] [cursor=pointer]:
- img
- text: Manage Users
- button "System Settings (AI/SMTP)" [ref=e362] [cursor=pointer]:
- img
- text: System Settings (AI/SMTP)
- button [ref=e364] [cursor=pointer]:
- img
- region "Notifications (F8)":
- list
```