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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user