Private
Public Access
1
0

Add AI chat with Ollama tool calling for financial queries

Conversational AI chat that uses Ollama's native tool-calling API to query
transactions, spending reports, account balances, dashboard summaries, and
update transaction categories through natural language. Includes persistent
conversation history, a full-page chat UI with sidebar, and rich inline
rendering of tool results (tables, alerts, cards).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-09 23:34:16 -05:00
parent 1ceeca0fb6
commit 846edad2a4
20 changed files with 3237 additions and 0 deletions
@@ -0,0 +1,139 @@
<template>
<!-- Transactions -->
<div v-if="type === 'transactions' && data?.items">
<v-table density="compact" class="text-caption rounded-lg mb-2">
<thead>
<tr>
<th>Date</th>
<th>Payee</th>
<th class="text-right">Amount</th>
<th>Category</th>
</tr>
</thead>
<tbody>
<tr v-for="txn in data.items.slice(0, 15)" :key="txn.id">
<td class="text-no-wrap">{{ formatDate(txn.date) }}</td>
<td>{{ txn.payeeName || 'Unknown' }}</td>
<td class="text-right text-no-wrap" :class="txn.amount >= 0 ? 'text-success' : 'text-error'">
{{ formatCurrency(txn.amount) }}
</td>
<td>{{ txn.categoryName || 'Uncategorized' }}</td>
</tr>
</tbody>
</v-table>
<div v-if="data.totalCount > 15" class="text-caption text-medium-emphasis ml-2">
Showing 15 of {{ data.totalCount }} transactions
</div>
</div>
<!-- Spending Report -->
<div v-else-if="type === 'spending_report'">
<div class="text-subtitle-2 mb-1">
Total Spending: {{ formatCurrency(data.totalSpending) }}
</div>
<v-table density="compact" class="text-caption rounded-lg mb-2">
<thead>
<tr>
<th>Category</th>
<th class="text-right">Amount</th>
<th class="text-right">%</th>
</tr>
</thead>
<tbody>
<tr v-for="cat in data.categories?.slice(0, 15)" :key="cat.categoryName">
<td>{{ cat.categoryName }}</td>
<td class="text-right">{{ formatCurrency(cat.amount) }}</td>
<td class="text-right">{{ cat.percentage.toFixed(1) }}%</td>
</tr>
</tbody>
</v-table>
</div>
<!-- Income vs Expense -->
<div v-else-if="type === 'income_expense'">
<v-table density="compact" class="text-caption rounded-lg mb-2">
<thead>
<tr>
<th>Month</th>
<th class="text-right">Income</th>
<th class="text-right">Expenses</th>
<th class="text-right">Net</th>
</tr>
</thead>
<tbody>
<tr v-for="month in data.months" :key="`${month.year}-${month.month}`">
<td>{{ month.year }}-{{ String(month.month).padStart(2, '0') }}</td>
<td class="text-right text-success">{{ formatCurrency(month.income) }}</td>
<td class="text-right text-error">{{ formatCurrency(month.expenses) }}</td>
<td class="text-right" :class="month.net >= 0 ? 'text-success' : 'text-error'">
{{ formatCurrency(month.net) }}
</td>
</tr>
</tbody>
</v-table>
</div>
<!-- Accounts -->
<div v-else-if="type === 'accounts' && Array.isArray(data)">
<v-table density="compact" class="text-caption rounded-lg mb-2">
<thead>
<tr>
<th>Account</th>
<th>Type</th>
<th class="text-right">Balance</th>
</tr>
</thead>
<tbody>
<tr v-for="acct in data" :key="acct.id">
<td>{{ acct.name }}</td>
<td>{{ acct.type }}</td>
<td class="text-right" :class="acct.balance >= 0 ? 'text-success' : 'text-error'">
{{ formatCurrency(acct.balance) }}
</td>
</tr>
</tbody>
</v-table>
</div>
<!-- Dashboard -->
<div v-else-if="type === 'dashboard'">
<v-card variant="tonal" density="compact" class="mb-2 pa-3">
<div class="d-flex justify-space-between mb-1">
<span class="text-caption text-medium-emphasis">Net Worth</span>
<span class="text-subtitle-2 font-weight-bold">{{ formatCurrency(data.netWorth) }}</span>
</div>
<div class="d-flex justify-space-between mb-1">
<span class="text-caption text-medium-emphasis">Assets</span>
<span class="text-caption text-success">{{ formatCurrency(data.totalAssets) }}</span>
</div>
<div class="d-flex justify-space-between">
<span class="text-caption text-medium-emphasis">Liabilities</span>
<span class="text-caption text-error">{{ formatCurrency(data.totalLiabilities) }}</span>
</div>
</v-card>
</div>
<!-- Category Update -->
<div v-else-if="type === 'category_update'">
<v-alert type="success" variant="tonal" density="compact" class="mb-2 text-caption">
Updated "{{ data.payeeName || 'Transaction' }}" ({{ formatDate(data.date) }}, {{ formatCurrency(data.amount) }})
from "{{ data.oldCategory }}" to "{{ data.newCategory }}"
</v-alert>
</div>
</template>
<script setup lang="ts">
defineProps<{
type: string
data: any
}>()
function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value)
}
function formatDate(value: string): string {
if (!value) return ''
return new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
</script>