Private
Public Access
1
0

Restructure AI settings into 3 cards and add AI Chat enable/disable toggle

Splits the AI tab into Configuration, Categorization, and Chat sections
with independent feature toggles. Both toggles are disabled when Ollama
URL or model is not configured. ChatPanel FAB is gated on isChatEnabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-15 23:54:51 -05:00
parent 7f15cd11e3
commit beb0ef521b
9 changed files with 1709 additions and 60 deletions
+14 -2
View File
@@ -122,15 +122,16 @@
</v-container>
</v-main>
<ChatPanel />
<ChatPanel v-if="isChatEnabled" />
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTheme } from 'vuetify'
import { useAuthStore } from '@/stores/auth'
import { useNotifications } from '@/composables/useNotifications'
import { aiCategorizationApi } from '@/services/aiCategorization'
import ChatPanel from '@/components/chat/ChatPanel.vue'
import type { AppNotification } from '@/types'
@@ -143,6 +144,17 @@ const authStore = useAuthStore()
const { notifications, unreadCount, markAllAsRead, markAsRead } = useNotifications()
const isChatEnabled = ref(false)
onMounted(async () => {
try {
const { data } = await aiCategorizationApi.getSettings()
isChatEnabled.value = data.isChatEnabled
} catch {
// Settings not configured — chat stays disabled
}
})
const isDark = computed(() => theme.global.current.value.dark)
const navGroups = [
+1
View File
@@ -541,6 +541,7 @@ export interface AiCategorizationSettings {
confidenceThreshold: number
timeoutSeconds: number
updatePayeeDefaults: boolean
isChatEnabled: boolean
systemPrompt: string | null
userPromptTemplate: string | null
chatContextSize: number
+82 -55
View File
@@ -4,7 +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-tab value="ai">AI</v-tab>
</v-tabs>
<v-tabs-window v-model="activeTab" class="mt-4">
@@ -616,24 +616,16 @@
</v-card>
</v-tabs-window-item>
<!-- Tab 4: AI Categorization -->
<!-- Tab 4: AI -->
<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.
AI features use a local Ollama LLM. 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>
<!-- Card 1: AI Configuration -->
<v-card class="mb-4">
<v-card-title>AI Configuration</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
@@ -656,6 +648,50 @@
</v-col>
</v-row>
<v-btn
variant="tonal"
color="secondary"
:loading="aiTesting"
@click="testAiConnection"
>
Test Connection
</v-btn>
<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>
<!-- Card 2: AI Categorization -->
<v-card class="mb-4">
<v-card-title>AI Categorization</v-card-title>
<v-card-text>
<v-switch
v-model="aiSettings.isEnabled"
label="Enable AI Categorization"
color="primary"
hide-details
class="mb-4"
:disabled="!isOllamaConfigured"
/>
<v-alert v-if="!isOllamaConfigured" type="warning" variant="tonal" class="mb-4">
Configure Ollama connection above to enable AI features
</v-alert>
<v-row dense>
<v-col cols="12" sm="6">
<div class="text-body-2 mb-1">Confidence Threshold: {{ aiSettings.confidenceThreshold.toFixed(2) }}</div>
@@ -690,45 +726,9 @@
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-divider class="mb-4" />
<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-card class="mt-4">
<v-card-title>Prompt Templates</v-card-title>
<v-card-text>
<div class="text-subtitle-2 mb-2">Prompt Templates</div>
<p class="text-body-2 text-medium-emphasis mb-4">
Customize the prompts sent to the AI model. Use placeholders to control where dynamic data is inserted:
<strong>{categories}</strong> = category list,
@@ -762,9 +762,23 @@
</v-card-text>
</v-card>
<v-card class="mt-4">
<v-card-title>AI Chat Settings</v-card-title>
<!-- Card 3: AI Chat -->
<v-card class="mb-4">
<v-card-title>AI Chat</v-card-title>
<v-card-text>
<v-switch
v-model="aiSettings.isChatEnabled"
label="Enable AI Chat"
color="primary"
hide-details
class="mb-4"
:disabled="!isOllamaConfigured"
/>
<v-alert v-if="!isOllamaConfigured" type="warning" variant="tonal" class="mb-4">
Configure Ollama connection above to enable AI features
</v-alert>
<div class="text-body-2 mb-1">Context Size: {{ aiSettings.chatContextSize.toLocaleString() }} tokens</div>
<v-slider
v-model="aiSettings.chatContextSize"
@@ -793,8 +807,16 @@
</v-card-text>
</v-card>
<v-btn
color="primary"
:loading="aiSaving"
@click="saveAiSettings"
>
Save AI Settings
</v-btn>
<v-snackbar v-model="showAiSuccess" :timeout="4000" color="success">
AI categorization settings saved
AI settings saved successfully
</v-snackbar>
<v-snackbar v-model="showAiError" :timeout="6000" color="error">
{{ aiErrorMessage }}
@@ -1335,11 +1357,16 @@ const aiSettings = reactive<AiCategorizationSettings>({
confidenceThreshold: 0.7,
timeoutSeconds: 30,
updatePayeeDefaults: true,
isChatEnabled: false,
systemPrompt: null,
userPromptTemplate: null,
chatContextSize: 16384,
chatBotName: null
})
const isOllamaConfigured = computed(() =>
!!aiSettings.ollamaUrl?.trim() && !!aiSettings.modelName?.trim()
)
const aiSaving = ref(false)
const aiTesting = ref(false)
const aiTestResult = ref<OllamaConnectionTestResult | null>(null)