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:
@@ -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 = [
|
||||
|
||||
@@ -541,6 +541,7 @@ export interface AiCategorizationSettings {
|
||||
confidenceThreshold: number
|
||||
timeoutSeconds: number
|
||||
updatePayeeDefaults: boolean
|
||||
isChatEnabled: boolean
|
||||
systemPrompt: string | null
|
||||
userPromptTemplate: string | null
|
||||
chatContextSize: number
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -67,13 +67,13 @@ public class AiCategorizationService : IAiCategorizationService
|
||||
if (settings == null)
|
||||
{
|
||||
return new AiCategorizationSettingsResponse(
|
||||
false, "http://localhost:11434", "llama3.1:8b", 0.7, 30, true, null, null, 16384, null);
|
||||
false, "http://localhost:11434", "llama3.1:8b", 0.7, 30, true, false, null, null, 16384, null);
|
||||
}
|
||||
|
||||
return new AiCategorizationSettingsResponse(
|
||||
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
|
||||
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
|
||||
settings.SystemPrompt, settings.UserPromptTemplate,
|
||||
settings.IsChatEnabled, settings.SystemPrompt, settings.UserPromptTemplate,
|
||||
settings.ChatContextSize, settings.ChatBotName);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ public class AiCategorizationService : IAiCategorizationService
|
||||
settings.ConfidenceThreshold = request.ConfidenceThreshold;
|
||||
settings.TimeoutSeconds = request.TimeoutSeconds;
|
||||
settings.UpdatePayeeDefaults = request.UpdatePayeeDefaults;
|
||||
settings.IsChatEnabled = request.IsChatEnabled;
|
||||
settings.SystemPrompt = string.IsNullOrWhiteSpace(request.SystemPrompt) ? null : request.SystemPrompt;
|
||||
settings.UserPromptTemplate = string.IsNullOrWhiteSpace(request.UserPromptTemplate) ? null : request.UserPromptTemplate;
|
||||
settings.ChatContextSize = request.ChatContextSize;
|
||||
@@ -108,7 +109,7 @@ public class AiCategorizationService : IAiCategorizationService
|
||||
return new AiCategorizationSettingsResponse(
|
||||
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
|
||||
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
|
||||
settings.SystemPrompt, settings.UserPromptTemplate,
|
||||
settings.IsChatEnabled, settings.SystemPrompt, settings.UserPromptTemplate,
|
||||
settings.ChatContextSize, settings.ChatBotName);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ public record AiCategorizationSettingsResponse(
|
||||
double ConfidenceThreshold,
|
||||
int TimeoutSeconds,
|
||||
bool UpdatePayeeDefaults,
|
||||
bool IsChatEnabled,
|
||||
string? SystemPrompt,
|
||||
string? UserPromptTemplate,
|
||||
int ChatContextSize,
|
||||
@@ -22,6 +23,7 @@ public record UpdateAiCategorizationSettingsRequest(
|
||||
double ConfidenceThreshold,
|
||||
int TimeoutSeconds,
|
||||
bool UpdatePayeeDefaults,
|
||||
bool IsChatEnabled,
|
||||
string? SystemPrompt,
|
||||
string? UserPromptTemplate,
|
||||
int ChatContextSize,
|
||||
|
||||
@@ -10,6 +10,7 @@ public class AiCategorizationSettings
|
||||
public double ConfidenceThreshold { get; set; } = 0.7;
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
public bool UpdatePayeeDefaults { get; set; } = true;
|
||||
public bool IsChatEnabled { get; set; }
|
||||
public string? SystemPrompt { get; set; }
|
||||
public string? UserPromptTemplate { get; set; }
|
||||
public int ChatContextSize { get; set; } = 16384;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Purrse.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsChatEnabled : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsChatEnabled",
|
||||
table: "ai_categorization_settings",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsChatEnabled",
|
||||
table: "ai_categorization_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,9 @@ namespace Purrse.Data.Migrations
|
||||
b.Property<double>("ConfidenceThreshold")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IsChatEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user