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-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
<ChatPanel />
|
<ChatPanel v-if="isChatEnabled" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useNotifications } from '@/composables/useNotifications'
|
import { useNotifications } from '@/composables/useNotifications'
|
||||||
|
import { aiCategorizationApi } from '@/services/aiCategorization'
|
||||||
import ChatPanel from '@/components/chat/ChatPanel.vue'
|
import ChatPanel from '@/components/chat/ChatPanel.vue'
|
||||||
import type { AppNotification } from '@/types'
|
import type { AppNotification } from '@/types'
|
||||||
|
|
||||||
@@ -143,6 +144,17 @@ const authStore = useAuthStore()
|
|||||||
|
|
||||||
const { notifications, unreadCount, markAllAsRead, markAsRead } = useNotifications()
|
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 isDark = computed(() => theme.global.current.value.dark)
|
||||||
|
|
||||||
const navGroups = [
|
const navGroups = [
|
||||||
|
|||||||
@@ -541,6 +541,7 @@ export interface AiCategorizationSettings {
|
|||||||
confidenceThreshold: number
|
confidenceThreshold: number
|
||||||
timeoutSeconds: number
|
timeoutSeconds: number
|
||||||
updatePayeeDefaults: boolean
|
updatePayeeDefaults: boolean
|
||||||
|
isChatEnabled: boolean
|
||||||
systemPrompt: string | null
|
systemPrompt: string | null
|
||||||
userPromptTemplate: string | null
|
userPromptTemplate: string | null
|
||||||
chatContextSize: number
|
chatContextSize: number
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<v-tab value="profile">Profile</v-tab>
|
<v-tab value="profile">Profile</v-tab>
|
||||||
<v-tab value="connections">Bank Connections</v-tab>
|
<v-tab value="connections">Bank Connections</v-tab>
|
||||||
<v-tab value="history">Sync History</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>
|
||||||
|
|
||||||
<v-tabs-window v-model="activeTab" class="mt-4">
|
<v-tabs-window v-model="activeTab" class="mt-4">
|
||||||
@@ -616,24 +616,16 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
|
|
||||||
<!-- Tab 4: AI Categorization -->
|
<!-- Tab 4: AI -->
|
||||||
<v-tabs-window-item value="ai">
|
<v-tabs-window-item value="ai">
|
||||||
<v-alert type="info" variant="tonal" class="mb-4">
|
<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.
|
AI features use a local Ollama LLM. Ollama runs locally on your machine — no data is sent to external services.
|
||||||
Ollama runs locally on your machine — no data is sent to external services.
|
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<v-card>
|
<!-- Card 1: AI Configuration -->
|
||||||
<v-card-title>AI Categorization Settings</v-card-title>
|
<v-card class="mb-4">
|
||||||
|
<v-card-title>AI Configuration</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-switch
|
|
||||||
v-model="aiSettings.isEnabled"
|
|
||||||
label="Enable AI Categorization"
|
|
||||||
color="primary"
|
|
||||||
hide-details
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-row dense>
|
<v-row dense>
|
||||||
<v-col cols="12" sm="6">
|
<v-col cols="12" sm="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@@ -656,6 +648,50 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-row dense>
|
||||||
<v-col cols="12" sm="6">
|
<v-col cols="12" sm="6">
|
||||||
<div class="text-body-2 mb-1">Confidence Threshold: {{ aiSettings.confidenceThreshold.toFixed(2) }}</div>
|
<div class="text-body-2 mb-1">Confidence Threshold: {{ aiSettings.confidenceThreshold.toFixed(2) }}</div>
|
||||||
@@ -690,45 +726,9 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="d-flex ga-2">
|
<v-divider class="mb-4" />
|
||||||
<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
|
<div class="text-subtitle-2 mb-2">Prompt Templates</div>
|
||||||
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>
|
|
||||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
<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:
|
Customize the prompts sent to the AI model. Use placeholders to control where dynamic data is inserted:
|
||||||
<strong>{categories}</strong> = category list,
|
<strong>{categories}</strong> = category list,
|
||||||
@@ -762,9 +762,23 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-card class="mt-4">
|
<!-- Card 3: AI Chat -->
|
||||||
<v-card-title>AI Chat Settings</v-card-title>
|
<v-card class="mb-4">
|
||||||
|
<v-card-title>AI Chat</v-card-title>
|
||||||
<v-card-text>
|
<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>
|
<div class="text-body-2 mb-1">Context Size: {{ aiSettings.chatContextSize.toLocaleString() }} tokens</div>
|
||||||
<v-slider
|
<v-slider
|
||||||
v-model="aiSettings.chatContextSize"
|
v-model="aiSettings.chatContextSize"
|
||||||
@@ -793,8 +807,16 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="aiSaving"
|
||||||
|
@click="saveAiSettings"
|
||||||
|
>
|
||||||
|
Save AI Settings
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<v-snackbar v-model="showAiSuccess" :timeout="4000" color="success">
|
<v-snackbar v-model="showAiSuccess" :timeout="4000" color="success">
|
||||||
AI categorization settings saved
|
AI settings saved successfully
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
<v-snackbar v-model="showAiError" :timeout="6000" color="error">
|
<v-snackbar v-model="showAiError" :timeout="6000" color="error">
|
||||||
{{ aiErrorMessage }}
|
{{ aiErrorMessage }}
|
||||||
@@ -1335,11 +1357,16 @@ const aiSettings = reactive<AiCategorizationSettings>({
|
|||||||
confidenceThreshold: 0.7,
|
confidenceThreshold: 0.7,
|
||||||
timeoutSeconds: 30,
|
timeoutSeconds: 30,
|
||||||
updatePayeeDefaults: true,
|
updatePayeeDefaults: true,
|
||||||
|
isChatEnabled: false,
|
||||||
systemPrompt: null,
|
systemPrompt: null,
|
||||||
userPromptTemplate: null,
|
userPromptTemplate: null,
|
||||||
chatContextSize: 16384,
|
chatContextSize: 16384,
|
||||||
chatBotName: null
|
chatBotName: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isOllamaConfigured = computed(() =>
|
||||||
|
!!aiSettings.ollamaUrl?.trim() && !!aiSettings.modelName?.trim()
|
||||||
|
)
|
||||||
const aiSaving = ref(false)
|
const aiSaving = ref(false)
|
||||||
const aiTesting = ref(false)
|
const aiTesting = ref(false)
|
||||||
const aiTestResult = ref<OllamaConnectionTestResult | null>(null)
|
const aiTestResult = ref<OllamaConnectionTestResult | null>(null)
|
||||||
|
|||||||
@@ -67,13 +67,13 @@ public class AiCategorizationService : IAiCategorizationService
|
|||||||
if (settings == null)
|
if (settings == null)
|
||||||
{
|
{
|
||||||
return new AiCategorizationSettingsResponse(
|
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(
|
return new AiCategorizationSettingsResponse(
|
||||||
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
|
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
|
||||||
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
|
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
|
||||||
settings.SystemPrompt, settings.UserPromptTemplate,
|
settings.IsChatEnabled, settings.SystemPrompt, settings.UserPromptTemplate,
|
||||||
settings.ChatContextSize, settings.ChatBotName);
|
settings.ChatContextSize, settings.ChatBotName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ public class AiCategorizationService : IAiCategorizationService
|
|||||||
settings.ConfidenceThreshold = request.ConfidenceThreshold;
|
settings.ConfidenceThreshold = request.ConfidenceThreshold;
|
||||||
settings.TimeoutSeconds = request.TimeoutSeconds;
|
settings.TimeoutSeconds = request.TimeoutSeconds;
|
||||||
settings.UpdatePayeeDefaults = request.UpdatePayeeDefaults;
|
settings.UpdatePayeeDefaults = request.UpdatePayeeDefaults;
|
||||||
|
settings.IsChatEnabled = request.IsChatEnabled;
|
||||||
settings.SystemPrompt = string.IsNullOrWhiteSpace(request.SystemPrompt) ? null : request.SystemPrompt;
|
settings.SystemPrompt = string.IsNullOrWhiteSpace(request.SystemPrompt) ? null : request.SystemPrompt;
|
||||||
settings.UserPromptTemplate = string.IsNullOrWhiteSpace(request.UserPromptTemplate) ? null : request.UserPromptTemplate;
|
settings.UserPromptTemplate = string.IsNullOrWhiteSpace(request.UserPromptTemplate) ? null : request.UserPromptTemplate;
|
||||||
settings.ChatContextSize = request.ChatContextSize;
|
settings.ChatContextSize = request.ChatContextSize;
|
||||||
@@ -108,7 +109,7 @@ public class AiCategorizationService : IAiCategorizationService
|
|||||||
return new AiCategorizationSettingsResponse(
|
return new AiCategorizationSettingsResponse(
|
||||||
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
|
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
|
||||||
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
|
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
|
||||||
settings.SystemPrompt, settings.UserPromptTemplate,
|
settings.IsChatEnabled, settings.SystemPrompt, settings.UserPromptTemplate,
|
||||||
settings.ChatContextSize, settings.ChatBotName);
|
settings.ChatContextSize, settings.ChatBotName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public record AiCategorizationSettingsResponse(
|
|||||||
double ConfidenceThreshold,
|
double ConfidenceThreshold,
|
||||||
int TimeoutSeconds,
|
int TimeoutSeconds,
|
||||||
bool UpdatePayeeDefaults,
|
bool UpdatePayeeDefaults,
|
||||||
|
bool IsChatEnabled,
|
||||||
string? SystemPrompt,
|
string? SystemPrompt,
|
||||||
string? UserPromptTemplate,
|
string? UserPromptTemplate,
|
||||||
int ChatContextSize,
|
int ChatContextSize,
|
||||||
@@ -22,6 +23,7 @@ public record UpdateAiCategorizationSettingsRequest(
|
|||||||
double ConfidenceThreshold,
|
double ConfidenceThreshold,
|
||||||
int TimeoutSeconds,
|
int TimeoutSeconds,
|
||||||
bool UpdatePayeeDefaults,
|
bool UpdatePayeeDefaults,
|
||||||
|
bool IsChatEnabled,
|
||||||
string? SystemPrompt,
|
string? SystemPrompt,
|
||||||
string? UserPromptTemplate,
|
string? UserPromptTemplate,
|
||||||
int ChatContextSize,
|
int ChatContextSize,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class AiCategorizationSettings
|
|||||||
public double ConfidenceThreshold { get; set; } = 0.7;
|
public double ConfidenceThreshold { get; set; } = 0.7;
|
||||||
public int TimeoutSeconds { get; set; } = 30;
|
public int TimeoutSeconds { get; set; } = 30;
|
||||||
public bool UpdatePayeeDefaults { get; set; } = true;
|
public bool UpdatePayeeDefaults { get; set; } = true;
|
||||||
|
public bool IsChatEnabled { get; set; }
|
||||||
public string? SystemPrompt { get; set; }
|
public string? SystemPrompt { get; set; }
|
||||||
public string? UserPromptTemplate { get; set; }
|
public string? UserPromptTemplate { get; set; }
|
||||||
public int ChatContextSize { get; set; } = 16384;
|
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")
|
b.Property<double>("ConfidenceThreshold")
|
||||||
.HasColumnType("double precision");
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<bool>("IsChatEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("IsEnabled")
|
b.Property<bool>("IsEnabled")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user