Files
platform/frontend-v2/src/routes/assistant/+server.ts
Yusuf Suleman 4592e35732
All checks were successful
Security Checks / dependency-audit (push) Successful in 1m13s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 3s
feat: major platform expansion — Brain service, RSS reader, iOS app, AI assistants, Firefox extension
Brain Service:
- Playwright stealth crawler replacing browserless (og:image, Readability, Reddit JSON API)
- AI classification with tag definitions and folder assignment
- YouTube video download via yt-dlp
- Karakeep migration complete (96 items)
- Taxonomy management (folders with icons/colors, tags)
- Discovery shuffle, sort options, search (Meilisearch + pgvector)
- Item tag/folder editing, card color accents

RSS Reader Service:
- Custom FastAPI reader replacing Miniflux
- Feed management (add/delete/refresh), category support
- Full article extraction via Readability
- Background content fetching for new entries
- Mark all read with confirmation
- Infinite scroll, retention cleanup (30/60 day)
- 17 feeds migrated from Miniflux

iOS App (SwiftUI):
- Native iOS 17+ app with @Observable architecture
- Cookie-based auth, configurable gateway URL
- Dashboard with custom background photo + frosted glass widgets
- Full fitness module (today/templates/goals/food library)
- AI assistant chat (fitness + brain, raw JSON state management)
- 120fps ProMotion support

AI Assistants (Gateway):
- Unified dispatcher with fitness/brain domain detection
- Fitness: natural language food logging, photo analysis, multi-item splitting
- Brain: save/append/update/delete notes, search & answer, undo support
- Madiha user gets fitness-only (brain disabled)

Firefox Extension:
- One-click save to Brain from any page
- Login with platform credentials
- Right-click context menu (save page/link/image)
- Notes field for URL saves
- Signed and published on AMO

Other:
- Reader bookmark button routes to Brain (was Karakeep)
- Fitness food library with "Add" button + add-to-meal popup
- Kindle send file size check (25MB SMTP2GO limit)
- Atelier UI as default (useAtelierShell=true)
- Mobile upload box in nav drawer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:56:29 -05:00

141 lines
4.3 KiB
TypeScript

import { json } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
type ChatRole = 'user' | 'assistant';
type ChatMessage = {
role?: ChatRole;
content?: string;
};
type UnifiedState = {
activeDomain?: 'fitness' | 'brain';
fitnessState?: Record<string, unknown>;
brainState?: Record<string, unknown>;
};
function recentMessages(messages: unknown): Array<{ role: ChatRole; content: string }> {
if (!Array.isArray(messages)) return [];
return messages
.filter((message) => !!message && typeof message === 'object')
.map((message) => {
const item = message as ChatMessage;
return {
role: item.role === 'assistant' ? 'assistant' : 'user',
content: typeof item.content === 'string' ? item.content : ''
};
})
.filter((message) => message.content.trim())
.slice(-16);
}
function lastUserMessage(messages: Array<{ role: ChatRole; content: string }>): string {
return [...messages].reverse().find((message) => message.role === 'user')?.content?.trim() || '';
}
function isFitnessIntent(text: string): boolean {
return /\b(calories?|protein|carbs?|fat|sugar|fiber|breakfast|lunch|dinner|snack|meal|food|ate|eaten|log|track|entries|macros?)\b/i.test(text)
|| /\bfor (breakfast|lunch|dinner|snack)\b/i.test(text)
|| /\bhow many calories do i have left\b/i.test(text);
}
function isBrainIntent(text: string): boolean {
return /\b(note|notes|brain|remember|save this|save that|what do i have|what have i saved|find my|delete (?:that|this|note|item)|update (?:that|this|note)|from my notes)\b/i.test(text);
}
function detectDomain(
messages: Array<{ role: ChatRole; content: string }>,
state: UnifiedState,
imageDataUrl?: string | null
): 'fitness' | 'brain' {
const text = lastUserMessage(messages);
if (imageDataUrl) return 'fitness';
if (!text) return state.activeDomain || 'brain';
const fitness = isFitnessIntent(text);
const brain = isBrainIntent(text);
if (fitness && !brain) return 'fitness';
if (brain && !fitness) return 'brain';
if (state.activeDomain === 'fitness' && !brain) return 'fitness';
if (state.activeDomain === 'brain' && !fitness) return 'brain';
return fitness ? 'fitness' : 'brain';
}
export const POST: RequestHandler = async ({ request, fetch, cookies }) => {
if (!cookies.get('platform_session')) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const { messages = [], state = {}, imageDataUrl = null, entryDate = null, action = 'chat', allowBrain = true } = await request
.json()
.catch(() => ({}));
let brainEnabled = !!allowBrain;
try {
const gatewayUrl = env.GATEWAY_URL || 'http://localhost:8100';
const session = cookies.get('platform_session');
if (session) {
const auth = await fetch(`${gatewayUrl}/api/auth/me`, {
headers: { Cookie: `platform_session=${session}` }
});
if (auth.ok) {
const data = await auth.json().catch(() => null);
if (data?.authenticated && data?.user?.username === 'madiha') {
brainEnabled = false;
}
}
}
} catch {
// keep requested allowBrain fallback
}
const chat = recentMessages(messages);
const unifiedState: UnifiedState = state && typeof state === 'object' ? state : {};
const domain = brainEnabled ? detectDomain(chat, unifiedState, imageDataUrl) : 'fitness';
const response = await fetch(domain === 'fitness' ? '/assistant/fitness' : '/assistant/brain', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(
domain === 'fitness'
? {
action,
messages,
draft: unifiedState.fitnessState && 'draft' in unifiedState.fitnessState ? unifiedState.fitnessState.draft : null,
drafts: unifiedState.fitnessState && 'drafts' in unifiedState.fitnessState ? unifiedState.fitnessState.drafts : [],
entryDate,
imageDataUrl
}
: {
messages,
state: unifiedState.brainState || {}
}
)
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
return json(body, { status: response.status });
}
return json({
...body,
domain,
state: {
activeDomain: domain,
fitnessState:
domain === 'fitness'
? {
draft: body?.draft ?? null,
drafts: Array.isArray(body?.drafts) ? body.drafts : []
}
: unifiedState.fitnessState || {},
brainState: domain === 'brain' ? body?.state || {} : unifiedState.brainState || {}
}
});
};