diff --git a/frontend-v2/src/lib/components/layout/AppShell.svelte b/frontend-v2/src/lib/components/layout/AppShell.svelte index 4aeeb74..fe10f27 100644 --- a/frontend-v2/src/lib/components/layout/AppShell.svelte +++ b/frontend-v2/src/lib/components/layout/AppShell.svelte @@ -2,6 +2,7 @@ import { page } from '$app/state'; import { BookOpen, + Brain, CalendarDays, CircleDot, Compass, @@ -39,6 +40,7 @@ { id: 'inventory', href: '/inventory', label: 'Inventory', icon: Package2 }, { id: 'reader', href: '/reader', label: 'Reader', icon: BookOpen }, { id: 'media', href: '/media', label: 'Media', icon: LibraryBig }, + { id: 'brain', href: '/brain', label: 'Brain', icon: Brain }, { id: 'settings', href: '/settings', label: 'Settings', icon: Settings2 } ]; diff --git a/frontend-v2/src/lib/components/layout/Navbar.svelte b/frontend-v2/src/lib/components/layout/Navbar.svelte index f623210..9fb3d0d 100644 --- a/frontend-v2/src/lib/components/layout/Navbar.svelte +++ b/frontend-v2/src/lib/components/layout/Navbar.svelte @@ -63,6 +63,9 @@ {#if showApp('media')} Media {/if} + {#if showApp('brain')} + Brain + {/if} diff --git a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte new file mode 100644 index 0000000..22567c1 --- /dev/null +++ b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte @@ -0,0 +1,601 @@ + + + + + + + + + Second brain + Brain + Save anything. Links, notes, files. AI classifies everything automatically so you can find it later without thinking about where to put it. + + + Collection + {total} saved ยท {Object.keys(folderCounts).length} folders + + + + + + + + + {#if captureInput.trim()} + + {capturing ? 'Saving...' : 'Save'} + + {/if} + + + + + + { activeFolder = null; loadItems(); }}> + + All + {total} + + Everything saved across all folders. + + {#each folders.slice(0, 5) as folder} + { activeFolder = folder; loadItems(); }}> + + {folder} + {folderCounts[folder] || 0} + + Items classified under {folder.toLowerCase()}. + + {/each} + + + + + + + + + + Search + Find saved items + + {items.length} item{items.length !== 1 ? 's' : ''} + + + + + {#if searchQuery} + { searchQuery = ''; loadItems(); }}> + + + {/if} + + + + + + Feed + {activeFolder || 'All items'} + + {#if activeFolder} + Items the AI classified under {activeFolder.toLowerCase()}. + {:else} + Your complete saved collection, newest first. + {/if} + + + + + {#if loading} + + {#each [1, 2, 3, 4] as _} + + {/each} + + {:else if items.length === 0} + + No items yet. Paste a URL or note above to get started. + + {:else} + + {#each items as item (item.id)} + selectedItem = item}> + + + {item.title || 'Processing...'} + + {formatDate(item.created_at)} + {#if item.folder}{item.folder}{/if} + {#if item.url}{new URL(item.url).hostname}{/if} + {#each (item.tags || []).slice(0, 2) as tag} + {tag} + {/each} + + + + {#if item.processing_status === 'pending' || item.processing_status === 'processing'} + Processing + {:else if item.processing_status === 'failed'} + Failed + {:else if item.confidence && item.confidence > 0.8} + + {:else if item.confidence} + + {/if} + + + + {/each} + + {/if} + + + + + + + +{#if selectedItem} + + { if (e.target === e.currentTarget) selectedItem = null; }} onkeydown={(e) => { if (e.key === 'Escape') selectedItem = null; }}> + + + + {selectedItem.type} + {selectedItem.title || 'Untitled'} + + selectedItem = null}> + + + + + {#if selectedItem.url} + {selectedItem.url} + {/if} + + {#if selectedItem.summary} + {selectedItem.summary} + {/if} + + + + Folder + {selectedItem.folder || 'โ'} + + + Confidence + {selectedItem.confidence ? (selectedItem.confidence * 100).toFixed(0) + '%' : 'โ'} + + + Status + {selectedItem.processing_status} + + + Saved + {new Date(selectedItem.created_at).toLocaleDateString()} + + + + {#if selectedItem.tags && selectedItem.tags.length > 0} + + {#each selectedItem.tags as tag} + {tag} + {/each} + + {/if} + + + reprocessItem(selectedItem.id)}>Reprocess + { if (confirm('Delete this item?')) deleteItem(selectedItem.id); }}>Delete + {#if selectedItem.url} + Open original + {/if} + + + +{/if} + + diff --git a/frontend-v2/src/lib/pages/budget/AtelierBudgetPage.svelte b/frontend-v2/src/lib/pages/budget/AtelierBudgetPage.svelte index 794fb56..9f87729 100644 --- a/frontend-v2/src/lib/pages/budget/AtelierBudgetPage.svelte +++ b/frontend-v2/src/lib/pages/budget/AtelierBudgetPage.svelte @@ -50,11 +50,11 @@ if (activeTab === 'categorized') return transactions.filter((t) => t.categoryType !== 'uncat'); return transactions; }); - const spendMagnitude = $derived(() => { + const spendMagnitude = $derived.by(() => { const value = Number(headerSpending.replace(/[$,]/g, '')) || 0; return '$' + value.toLocaleString('en-US'); }); - const incomeMagnitude = $derived(() => { + const incomeMagnitude = $derived.by(() => { const value = Number(headerIncome.replace(/[$,]/g, '')) || 0; return '$' + value.toLocaleString('en-US'); }); @@ -184,9 +184,9 @@ .filter((c: any) => c.name !== 'Starting Balances') .map((c: any) => ({ name: c.name, - budgeted: Math.round(c.budgeted / 100), - spent: Math.round(Math.abs(c.spent) / 100), - available: Math.round(c.balance / 100) + budgeted: Math.round(Number(c.budgeted ?? 0) / 100), + spent: Math.round(Math.abs(Number(c.spent ?? 0)) / 100), + available: Math.round(Number(c.balance ?? 0) / 100) })) })) .filter((g: any) => g.categories.length > 0); @@ -313,18 +313,6 @@ selected = next; } - function handleRowKeydown(e: KeyboardEvent) { - if (e.key === 'c' || e.key === 'C') { - e.preventDefault(); - const row = (e.target as HTMLElement).closest('.txn-row'); - const select = row?.querySelector('.cat-select') as HTMLSelectElement | null; - if (select) { - select.focus(); - select.click(); - } - } - } - function formatDateShort(dateStr: string) { const d = new Date(dateStr + 'T00:00:00'); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); @@ -372,6 +360,9 @@ {currentMonthLabel || 'Budget'} Budget + + {uncatCount} waiting for category ยท {formatBalance(onBudgetTotal)} across on-budget accounts + @@ -379,13 +370,13 @@ (activeView = 'transactions')}>Transactions (activeView = 'budget')}>Budget - (accountsOpen = !accountsOpen)}> - {selectedAccountName} + (accountsOpen = !accountsOpen)}> + {activeAccountId ? selectedAccountName : 'Accounts'} - + Spent {spendMagnitude} @@ -405,24 +396,42 @@ - { selectAccount(null); accountsOpen = false; }}> - All accounts - {formatBalance(onBudgetTotal)} - - {#each accounts as acct} - { selectAccount(acct.id || null); accountsOpen = false; }}> - {acct.name} - {formatBalance(acct.balance)} + + Accounts + {accounts.length + offBudgetAccounts.length + 1} visible + + + { selectAccount(null); accountsOpen = false; }}> + + + All accounts + On-budget overview + + {formatBalance(onBudgetTotal)} - {/each} - {#if offBudgetAccounts.length > 0} - {#each offBudgetAccounts as acct} - { selectAccount(acct.id || null); accountsOpen = false; }}> - {acct.name} - {formatBalance(acct.balance)} + {#each accounts as acct} + { selectAccount(acct.id || null); accountsOpen = false; }}> + + + {acct.name} + On budget + + {formatBalance(acct.balance)} {/each} - {/if} + {#if offBudgetAccounts.length > 0} + {#each offBudgetAccounts as acct} + { selectAccount(acct.id || null); accountsOpen = false; }}> + + + {acct.name} + Off budget + + {formatBalance(acct.balance)} + + {/each} + {/if} + {#if activeView === 'transactions'} @@ -430,11 +439,12 @@ Suggested transfers - {suggestedTransfers.length} + {suggestedTransfers.length} open - + {#each suggestedTransfers as s} + {s.from.account} to {s.to.account} {s.from.payee} matched with {s.to.payee} @@ -452,99 +462,108 @@ {/if} - - - { activeTab = 'all'; loadTransactions(); }}>All - { activeTab = 'uncategorized'; loadTransactions(); }}> - Needs category - {#if uncatCount > 0}{uncatCount}{/if} - - { activeTab = 'categorized'; loadTransactions(); }}>Categorized - - - {#if selected.size > 0} - - {selected.size} selected - - {#if canTransfer} - Make transfer - {/if} - {#if bulkCategoryOpen} - bulkCategorize((e.target as HTMLSelectElement).value)}> - Apply category... - {#each sortedCategories() as cat} - {cat} - {/each} - - {:else} - (bulkCategoryOpen = true)}>Set category - {/if} - { selected = new Set(); bulkCategoryOpen = false; }}>Clear - + + + + Ledger + {filteredTransactions.length} visible + + + { activeTab = 'all'; loadTransactions(); }}>All + { activeTab = 'uncategorized'; loadTransactions(); }}> + Needs category + {#if uncatCount > 0}{uncatCount}{/if} + + { activeTab = 'categorized'; loadTransactions(); }}>Categorized - {/if} - - - - {#each filteredTransactions() as txn (txn.id)} - - - toggleSelect(txn.id)} /> - - {txn.date} - - - {txn.payee} - = 0} class:neg={txn.amount < 0}>{formatAmount(txn.amount)} - - - {txn.account} - {#if txn.note}{txn.note}{/if} - - - - {#if txn.categoryType === 'transfer'} - Transfer - {:else if txn.categoryType === 'uncat'} - categorize(txn.id, (e.target as HTMLSelectElement).value)}> - Select category - {#each sortedCategories() as cat} + {#if selected.size > 0} + + {selected.size} selected + + {#if canTransfer} + Make transfer + {/if} + {#if bulkCategoryOpen} + bulkCategorize((e.target as HTMLSelectElement).value)}> + Apply category... + {#each sortedCategories as cat} {cat} {/each} {:else} - {txn.category} + (bulkCategoryOpen = true)}>Set category {/if} + { selected = new Set(); bulkCategoryOpen = false; }}>Clear + + + {/if} + + + + + {#each filteredTransactions as txn (txn.id)} + + + toggleSelect(txn.id)} /> + + + + + {txn.date} + {txn.account} + + {txn.payee} + + {#if txn.note}{txn.note}{/if} + + + + = 0} class:neg={txn.amount < 0}>{formatAmount(txn.amount)} + + {#if txn.categoryType === 'transfer'} + Transfer + {:else if txn.categoryType === 'uncat'} + categorize(txn.id, (e.target as HTMLSelectElement).value)}> + Select category + {#each sortedCategories as cat} + {cat} + {/each} + + {:else} + {txn.category} + {/if} + {/each} - {#if filteredTransactions().length === 0} + {#if filteredTransactions.length === 0} {#if activeTab === 'uncategorized'} No uncategorized transactions right now. {:else} - No transactions to show. - {/if} + No transactions to show. + {/if} {/if} - + - {#if hasMore} - - {loadingMore ? 'Loading...' : 'Load more'} - - {/if} + {#if hasMore} + + {loadingMore ? 'Loading...' : 'Load more'} + + {/if} + {:else} - + + {#each budgetGroups as group} @@ -554,19 +573,32 @@ {#each group.categories as cat} - {cat.name} + + {cat.name} + + {#if cat.budgeted > 0} + Budgeted {formatBudgetAmount(cat.budgeted)} + {/if} + {#if cat.spent > 0} + Spent {formatBudgetAmount(cat.spent)} + {/if} + {#if cat.budgeted === 0 && cat.spent === 0} + Nothing planned yet + {/if} + + - Budgeted {formatBudgetAmount(cat.budgeted)} - Spent {formatBudgetAmount(cat.spent)} 0}> {cat.available < 0 ? '-' : ''}{formatBudgetAmount(Math.abs(cat.available))} + {cat.available < 0 ? 'Over' : 'Left'} {/each} {/each} + {/if} @@ -574,37 +606,21 @@ diff --git a/frontend-v2/src/routes/(app)/+layout.server.ts b/frontend-v2/src/routes/(app)/+layout.server.ts index 222d619..1a7a5b0 100644 --- a/frontend-v2/src/routes/(app)/+layout.server.ts +++ b/frontend-v2/src/routes/(app)/+layout.server.ts @@ -24,7 +24,7 @@ export const load: LayoutServerLoad = async ({ cookies, url }) => { // Hides nav items but does NOT block direct URL access. // This is intentional: all shared services are accessible to all authenticated users. // Hiding reduces clutter for users who don't need certain apps day-to-day. - const allApps = ['tasks', 'trips', 'fitness', 'inventory', 'budget', 'reader', 'media']; + const allApps = ['tasks', 'trips', 'fitness', 'inventory', 'budget', 'reader', 'media', 'brain']; const hiddenByUser: Record = { 'madiha': ['inventory', 'reader'], }; diff --git a/frontend-v2/src/routes/(app)/brain/+page.svelte b/frontend-v2/src/routes/(app)/brain/+page.svelte new file mode 100644 index 0000000..4451465 --- /dev/null +++ b/frontend-v2/src/routes/(app)/brain/+page.svelte @@ -0,0 +1,11 @@ + + +{#if useAtelierShell} + +{:else} + +{/if}
Save anything. Links, notes, files. AI classifies everything automatically so you can find it later without thinking about where to put it.
+ {uncatCount} waiting for category ยท {formatBalance(onBudgetTotal)} across on-budget accounts +