Add adjustable context size, custom chatbot name, and cat icon for AI chat
Settings > AI tab now includes context size slider (2048–131072) with VRAM estimate and a chatbot name field. ChatService reads both dynamically instead of hardcoding num_ctx=16384 and "Purrse AI". Chat panel FAB and empty state use mdi-cat icon to match the rest of the app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,14 +8,14 @@
|
|||||||
elevation="8"
|
elevation="8"
|
||||||
@click="togglePanel"
|
@click="togglePanel"
|
||||||
>
|
>
|
||||||
<v-icon>{{ panelOpen ? 'mdi-close' : 'mdi-robot-outline' }}</v-icon>
|
<v-icon>{{ panelOpen ? 'mdi-close' : 'mdi-cat' }}</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<!-- Chat Panel -->
|
<!-- Chat Panel -->
|
||||||
<v-card v-show="panelOpen" class="chat-panel" elevation="12" rounded="lg">
|
<v-card v-show="panelOpen" class="chat-panel" elevation="12" rounded="lg">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<v-toolbar density="compact" color="primary" class="flex-grow-0">
|
<v-toolbar density="compact" color="primary" class="flex-grow-0">
|
||||||
<v-toolbar-title class="text-body-1 font-weight-medium">Purrse AI</v-toolbar-title>
|
<v-toolbar-title class="text-body-1 font-weight-medium">{{ chatBotName }}</v-toolbar-title>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<div ref="messagesContainer" class="chat-messages">
|
<div ref="messagesContainer" class="chat-messages">
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<div v-if="messages.length === 0 && !loading" class="d-flex flex-column align-center justify-center" style="height: 100%;">
|
<div v-if="messages.length === 0 && !loading" class="d-flex flex-column align-center justify-center" style="height: 100%;">
|
||||||
<v-icon size="48" color="primary" class="mb-3">mdi-robot-outline</v-icon>
|
<v-icon size="48" color="primary" class="mb-3">mdi-cat</v-icon>
|
||||||
<p class="text-body-2 text-medium-emphasis mb-4">Ask me about your finances</p>
|
<p class="text-body-2 text-medium-emphasis mb-4">Ask me about your finances</p>
|
||||||
<div class="d-flex flex-wrap justify-center ga-2 px-4">
|
<div class="d-flex flex-wrap justify-center ga-2 px-4">
|
||||||
<v-chip
|
<v-chip
|
||||||
@@ -140,6 +140,7 @@
|
|||||||
import { ref, nextTick, onMounted, watch } from 'vue'
|
import { ref, nextTick, onMounted, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { chatApi } from '@/services/chat'
|
import { chatApi } from '@/services/chat'
|
||||||
|
import { aiCategorizationApi } from '@/services/aiCategorization'
|
||||||
import { usePageContext } from '@/composables/usePageContext'
|
import { usePageContext } from '@/composables/usePageContext'
|
||||||
import ChatToolResult from '@/views/chat/ChatToolResult.vue'
|
import ChatToolResult from '@/views/chat/ChatToolResult.vue'
|
||||||
import type { ChatConversation, ChatMessage } from '@/types'
|
import type { ChatConversation, ChatMessage } from '@/types'
|
||||||
@@ -155,6 +156,7 @@ const inputMessage = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const messagesContainer = ref<HTMLElement | null>(null)
|
const messagesContainer = ref<HTMLElement | null>(null)
|
||||||
const conversationsLoaded = ref(false)
|
const conversationsLoaded = ref(false)
|
||||||
|
const chatBotName = ref('Purrse AI')
|
||||||
|
|
||||||
let _idCounter = 0
|
let _idCounter = 0
|
||||||
function tempId(): string {
|
function tempId(): string {
|
||||||
@@ -214,6 +216,18 @@ function togglePanel() {
|
|||||||
panelOpen.value = !panelOpen.value
|
panelOpen.value = !panelOpen.value
|
||||||
if (panelOpen.value && !conversationsLoaded.value) {
|
if (panelOpen.value && !conversationsLoaded.value) {
|
||||||
loadConversations()
|
loadConversations()
|
||||||
|
loadChatBotName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChatBotName() {
|
||||||
|
try {
|
||||||
|
const { data } = await aiCategorizationApi.getSettings()
|
||||||
|
if (data.chatBotName) {
|
||||||
|
chatBotName.value = data.chatBotName
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Default name is fine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -521,6 +521,8 @@ export interface AiCategorizationSettings {
|
|||||||
updatePayeeDefaults: boolean
|
updatePayeeDefaults: boolean
|
||||||
systemPrompt: string | null
|
systemPrompt: string | null
|
||||||
userPromptTemplate: string | null
|
userPromptTemplate: string | null
|
||||||
|
chatContextSize: number
|
||||||
|
chatBotName: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OllamaConnectionTestResult {
|
export interface OllamaConnectionTestResult {
|
||||||
|
|||||||
@@ -762,6 +762,37 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<v-card class="mt-4">
|
||||||
|
<v-card-title>AI Chat Settings</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-body-2 mb-1">Context Size: {{ aiSettings.chatContextSize.toLocaleString() }} tokens</div>
|
||||||
|
<v-slider
|
||||||
|
v-model="aiSettings.chatContextSize"
|
||||||
|
:min="2048"
|
||||||
|
:max="131072"
|
||||||
|
:step="2048"
|
||||||
|
thumb-label
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
class="mb-1"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis mb-4">
|
||||||
|
Estimated VRAM: ~{{ Math.round((aiSettings.chatContextSize / 1024) * 75) }} MB — Increase if the AI loses context in long chats. Decrease if Ollama runs out of memory.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="aiSettings.chatBotName"
|
||||||
|
label="Chatbot Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
placeholder="Purrse AI"
|
||||||
|
hint="Custom name shown in the chat panel header and used by the AI"
|
||||||
|
persistent-hint
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
<v-snackbar v-model="showAiSuccess" :timeout="4000" color="success">
|
<v-snackbar v-model="showAiSuccess" :timeout="4000" color="success">
|
||||||
AI categorization settings saved
|
AI categorization settings saved
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
@@ -1305,7 +1336,9 @@ const aiSettings = reactive<AiCategorizationSettings>({
|
|||||||
timeoutSeconds: 30,
|
timeoutSeconds: 30,
|
||||||
updatePayeeDefaults: true,
|
updatePayeeDefaults: true,
|
||||||
systemPrompt: null,
|
systemPrompt: null,
|
||||||
userPromptTemplate: null
|
userPromptTemplate: null,
|
||||||
|
chatContextSize: 16384,
|
||||||
|
chatBotName: null
|
||||||
})
|
})
|
||||||
const aiSaving = ref(false)
|
const aiSaving = ref(false)
|
||||||
const aiTesting = ref(false)
|
const aiTesting = ref(false)
|
||||||
|
|||||||
@@ -67,13 +67,14 @@ 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);
|
false, "http://localhost:11434", "llama3.1:8b", 0.7, 30, true, 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.SystemPrompt, settings.UserPromptTemplate,
|
||||||
|
settings.ChatContextSize, settings.ChatBotName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AiCategorizationSettingsResponse> UpdateSettingsAsync(Guid userId, UpdateAiCategorizationSettingsRequest request)
|
public async Task<AiCategorizationSettingsResponse> UpdateSettingsAsync(Guid userId, UpdateAiCategorizationSettingsRequest request)
|
||||||
@@ -98,6 +99,8 @@ public class AiCategorizationService : IAiCategorizationService
|
|||||||
settings.UpdatePayeeDefaults = request.UpdatePayeeDefaults;
|
settings.UpdatePayeeDefaults = request.UpdatePayeeDefaults;
|
||||||
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.ChatBotName = string.IsNullOrWhiteSpace(request.ChatBotName) ? null : request.ChatBotName;
|
||||||
settings.UpdatedAt = DateTime.UtcNow;
|
settings.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
@@ -105,7 +108,8 @@ 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.SystemPrompt, settings.UserPromptTemplate,
|
||||||
|
settings.ChatContextSize, settings.ChatBotName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OllamaConnectionTestResponse> TestConnectionAsync(Guid userId)
|
public async Task<OllamaConnectionTestResponse> TestConnectionAsync(Guid userId)
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ public class ChatService : IChatService
|
|||||||
var settings = await _db.AiCategorizationSettings.FirstOrDefaultAsync(s => s.UserId == userId);
|
var settings = await _db.AiCategorizationSettings.FirstOrDefaultAsync(s => s.UserId == userId);
|
||||||
var ollamaUrl = settings?.OllamaUrl ?? "http://localhost:11434";
|
var ollamaUrl = settings?.OllamaUrl ?? "http://localhost:11434";
|
||||||
var modelName = settings?.ModelName ?? "llama3.1:8b";
|
var modelName = settings?.ModelName ?? "llama3.1:8b";
|
||||||
|
var chatContextSize = settings?.ChatContextSize ?? 16384;
|
||||||
|
var chatBotName = settings?.ChatBotName;
|
||||||
|
|
||||||
// Save user message
|
// Save user message
|
||||||
var userMessage = new ChatMessage
|
var userMessage = new ChatMessage
|
||||||
@@ -130,10 +132,15 @@ public class ChatService : IChatService
|
|||||||
var ollamaMessages = new List<OllamaToolMessage>();
|
var ollamaMessages = new List<OllamaToolMessage>();
|
||||||
|
|
||||||
// System prompt
|
// System prompt
|
||||||
|
var systemPrompt = string.Format(SystemPromptTemplate, DateTime.UtcNow.ToString("yyyy-MM-dd"));
|
||||||
|
if (!string.IsNullOrWhiteSpace(chatBotName))
|
||||||
|
{
|
||||||
|
systemPrompt += $"\nYour name is {chatBotName}.";
|
||||||
|
}
|
||||||
ollamaMessages.Add(new OllamaToolMessage
|
ollamaMessages.Add(new OllamaToolMessage
|
||||||
{
|
{
|
||||||
Role = "system",
|
Role = "system",
|
||||||
Content = string.Format(SystemPromptTemplate, DateTime.UtcNow.ToString("yyyy-MM-dd"))
|
Content = systemPrompt
|
||||||
});
|
});
|
||||||
|
|
||||||
// Last 20 conversation messages
|
// Last 20 conversation messages
|
||||||
@@ -199,7 +206,7 @@ public class ChatService : IChatService
|
|||||||
Messages = ollamaMessages,
|
Messages = ollamaMessages,
|
||||||
Stream = false,
|
Stream = false,
|
||||||
Tools = tools,
|
Tools = tools,
|
||||||
Options = new OllamaRequestOptions { NumCtx = 16384 }
|
Options = new OllamaRequestOptions { NumCtx = chatContextSize }
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await CallOllamaAsync(ollamaUrl, ollamaRequest);
|
var response = await CallOllamaAsync(ollamaUrl, ollamaRequest);
|
||||||
@@ -272,7 +279,7 @@ public class ChatService : IChatService
|
|||||||
Messages = ollamaMessages,
|
Messages = ollamaMessages,
|
||||||
Stream = false,
|
Stream = false,
|
||||||
Tools = null, // drop tools so the model focuses on the page data
|
Tools = null, // drop tools so the model focuses on the page data
|
||||||
Options = new OllamaRequestOptions { NumCtx = 16384 }
|
Options = new OllamaRequestOptions { NumCtx = chatContextSize }
|
||||||
};
|
};
|
||||||
var retryResponse = await CallOllamaAsync(ollamaUrl, retryRequest);
|
var retryResponse = await CallOllamaAsync(ollamaUrl, retryRequest);
|
||||||
rawContent = retryResponse?.Message?.Content;
|
rawContent = retryResponse?.Message?.Content;
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ public record AiCategorizationSettingsResponse(
|
|||||||
int TimeoutSeconds,
|
int TimeoutSeconds,
|
||||||
bool UpdatePayeeDefaults,
|
bool UpdatePayeeDefaults,
|
||||||
string? SystemPrompt,
|
string? SystemPrompt,
|
||||||
string? UserPromptTemplate);
|
string? UserPromptTemplate,
|
||||||
|
int ChatContextSize,
|
||||||
|
string? ChatBotName);
|
||||||
|
|
||||||
public record UpdateAiCategorizationSettingsRequest(
|
public record UpdateAiCategorizationSettingsRequest(
|
||||||
bool IsEnabled,
|
bool IsEnabled,
|
||||||
@@ -21,7 +23,9 @@ public record UpdateAiCategorizationSettingsRequest(
|
|||||||
int TimeoutSeconds,
|
int TimeoutSeconds,
|
||||||
bool UpdatePayeeDefaults,
|
bool UpdatePayeeDefaults,
|
||||||
string? SystemPrompt,
|
string? SystemPrompt,
|
||||||
string? UserPromptTemplate);
|
string? UserPromptTemplate,
|
||||||
|
int ChatContextSize,
|
||||||
|
string? ChatBotName);
|
||||||
|
|
||||||
// Connection test
|
// Connection test
|
||||||
public record OllamaConnectionTestResponse(
|
public record OllamaConnectionTestResponse(
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class AiCategorizationSettings
|
|||||||
public bool UpdatePayeeDefaults { get; set; } = true;
|
public bool UpdatePayeeDefaults { get; set; } = true;
|
||||||
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 string? ChatBotName { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public User User { get; set; } = null!;
|
public User User { get; set; } = null!;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class AiCategorizationSettingsConfiguration : IEntityTypeConfiguration<Ai
|
|||||||
builder.Property(s => s.ModelName).HasMaxLength(100);
|
builder.Property(s => s.ModelName).HasMaxLength(100);
|
||||||
builder.Property(s => s.SystemPrompt).HasMaxLength(4000);
|
builder.Property(s => s.SystemPrompt).HasMaxLength(4000);
|
||||||
builder.Property(s => s.UserPromptTemplate).HasMaxLength(4000);
|
builder.Property(s => s.UserPromptTemplate).HasMaxLength(4000);
|
||||||
|
builder.Property(s => s.ChatBotName).HasMaxLength(100);
|
||||||
|
|
||||||
builder.HasOne(s => s.User)
|
builder.HasOne(s => s.User)
|
||||||
.WithOne(u => u.AiCategorizationSettings)
|
.WithOne(u => u.AiCategorizationSettings)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Purrse.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddChatSettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ChatBotName",
|
||||||
|
table: "ai_categorization_settings",
|
||||||
|
type: "character varying(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "ChatContextSize",
|
||||||
|
table: "ai_categorization_settings",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ChatBotName",
|
||||||
|
table: "ai_categorization_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ChatContextSize",
|
||||||
|
table: "ai_categorization_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,6 +91,13 @@ namespace Purrse.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ChatBotName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("ChatContextSize")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<double>("ConfidenceThreshold")
|
b.Property<double>("ConfidenceThreshold")
|
||||||
.HasColumnType("double precision");
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user