Private
Public Access
1
0

Add AI categorization with Ollama: settings, auto-classify on sync/import, and manual classify from Transactions

Adds local LLM-based transaction categorization using Ollama:
- Settings UI to configure Ollama URL, model, confidence threshold
- Auto-categorization during bank sync and file import
- Manual "Classify Uncategorized" button on Transactions screen that respects active filters
- Payee default category learning loop for classified transactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 18:41:12 -05:00
parent 57d1051213
commit 1c7abb7ffc
18 changed files with 2423 additions and 3 deletions
+178 -2
View File
@@ -4,6 +4,7 @@
<v-tab value="profile">Profile</v-tab>
<v-tab value="connections">Bank Connections</v-tab>
<v-tab value="history">Sync History</v-tab>
<v-tab value="ai">AI Categorization</v-tab>
</v-tabs>
<v-tabs-window v-model="activeTab" class="mt-4">
@@ -354,6 +355,124 @@
</v-data-table>
</v-card>
</v-tabs-window-item>
<!-- Tab 4: AI Categorization -->
<v-tabs-window-item value="ai">
<v-alert type="info" variant="tonal" class="mb-4">
AI Categorization uses a local Ollama LLM to automatically categorize transactions when payee matching doesn't produce a category.
Ollama runs locally on your machine — no data is sent to external services.
</v-alert>
<v-card>
<v-card-title>AI Categorization Settings</v-card-title>
<v-card-text>
<v-switch
v-model="aiSettings.isEnabled"
label="Enable AI Categorization"
color="primary"
hide-details
class="mb-4"
/>
<v-row dense>
<v-col cols="12" sm="6">
<v-text-field
v-model="aiSettings.ollamaUrl"
label="Ollama URL"
variant="outlined"
density="compact"
placeholder="http://localhost:11434"
/>
</v-col>
<v-col cols="12" sm="6">
<v-combobox
v-model="aiSettings.modelName"
:items="availableModels"
label="Model"
variant="outlined"
density="compact"
placeholder="llama3.1:8b"
/>
</v-col>
</v-row>
<v-row dense>
<v-col cols="12" sm="6">
<div class="text-body-2 mb-1">Confidence Threshold: {{ aiSettings.confidenceThreshold.toFixed(2) }}</div>
<v-slider
v-model="aiSettings.confidenceThreshold"
:min="0.1"
:max="1.0"
:step="0.05"
thumb-label
color="primary"
hide-details
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model.number="aiSettings.timeoutSeconds"
label="Timeout (seconds)"
variant="outlined"
density="compact"
type="number"
:min="5"
:max="120"
/>
</v-col>
</v-row>
<v-checkbox
v-model="aiSettings.updatePayeeDefaults"
label="Auto-update payee default categories (learning loop)"
color="primary"
hide-details
class="mb-4"
/>
<div class="d-flex ga-2">
<v-btn
color="primary"
:loading="aiSaving"
@click="saveAiSettings"
>
Save
</v-btn>
<v-btn
variant="tonal"
color="secondary"
:loading="aiTesting"
@click="testAiConnection"
>
Test Connection
</v-btn>
</div>
<v-alert
v-if="aiTestResult"
:type="aiTestResult.success ? 'success' : 'error'"
variant="tonal"
class="mt-4"
closable
@click:close="aiTestResult = null"
>
<template v-if="aiTestResult.success">
Connected to Ollama. Available models: {{ aiTestResult.availableModels?.join(', ') || 'none' }}
</template>
<template v-else>
Connection failed: {{ aiTestResult.errorMessage }}
</template>
</v-alert>
</v-card-text>
</v-card>
<v-snackbar v-model="showAiSuccess" :timeout="4000" color="success">
AI categorization settings saved
</v-snackbar>
<v-snackbar v-model="showAiError" :timeout="6000" color="error">
{{ aiErrorMessage }}
</v-snackbar>
</v-tabs-window-item>
</v-tabs-window>
</div>
</template>
@@ -363,7 +482,8 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { bankSyncApi } from '@/services/bankSync'
import { accountsApi } from '@/services/accounts'
import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry, PlaidCredentials } from '@/types'
import { aiCategorizationApi } from '@/services/aiCategorization'
import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry, PlaidCredentials, AiCategorizationSettings, OllamaConnectionTestResult } from '@/types'
const authStore = useAuthStore()
@@ -674,7 +794,63 @@ async function deleteConnection() {
}
}
// AI Categorization
const aiSettings = reactive<AiCategorizationSettings>({
isEnabled: false,
ollamaUrl: 'http://localhost:11434',
modelName: 'llama3.1:8b',
confidenceThreshold: 0.7,
timeoutSeconds: 30,
updatePayeeDefaults: true
})
const aiSaving = ref(false)
const aiTesting = ref(false)
const aiTestResult = ref<OllamaConnectionTestResult | null>(null)
const availableModels = ref<string[]>([])
const showAiSuccess = ref(false)
const showAiError = ref(false)
const aiErrorMessage = ref('')
async function loadAiSettings() {
try {
const { data } = await aiCategorizationApi.getSettings()
Object.assign(aiSettings, data)
} catch {
// Not configured yet — defaults are fine
}
}
async function saveAiSettings() {
aiSaving.value = true
try {
const { data } = await aiCategorizationApi.updateSettings({ ...aiSettings })
Object.assign(aiSettings, data)
showAiSuccess.value = true
} catch (err: any) {
aiErrorMessage.value = err.response?.data?.error || 'Failed to save AI settings'
showAiError.value = true
} finally {
aiSaving.value = false
}
}
async function testAiConnection() {
aiTesting.value = true
aiTestResult.value = null
try {
const { data } = await aiCategorizationApi.testConnection()
aiTestResult.value = data
if (data.availableModels && data.availableModels.length > 0) {
availableModels.value = data.availableModels
}
} catch (err: any) {
aiTestResult.value = { success: false, errorMessage: err.response?.data?.error || 'Connection test failed', availableModels: null }
} finally {
aiTesting.value = false
}
}
onMounted(async () => {
await Promise.all([loadPlaidCredentials(), loadConnections(), loadAccounts(), loadSyncLogs()])
await Promise.all([loadPlaidCredentials(), loadConnections(), loadAccounts(), loadSyncLogs(), loadAiSettings()])
})
</script>