From 1923cd4cde40452e0395459be6b7ce8c1c9b56ed Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Sun, 1 Feb 2026 07:24:21 +0000 Subject: [PATCH] feat(AI): chat suggestions and assistant settings --- admin/app/controllers/chats_controller.ts | 22 ++++++ admin/app/controllers/settings_controller.ts | 21 +++++- admin/app/models/kv_store.ts | 9 ++- admin/app/services/chat_service.ts | 70 ++++++++++++++++++- admin/app/services/system_service.ts | 6 ++ admin/app/utils/misc.ts | 20 ++++++ admin/app/validators/settings.ts | 8 +++ admin/constants/kv_store.ts | 3 + admin/constants/ollama.ts | 24 +++++++ admin/inertia/components/BouncingDots.tsx | 29 ++++++++ .../inertia/components/chat/ChatInterface.tsx | 49 ++++++++----- admin/inertia/components/chat/ChatModal.tsx | 8 ++- admin/inertia/components/chat/ChatSidebar.tsx | 2 +- admin/inertia/components/chat/index.tsx | 19 ++++- admin/inertia/components/inputs/Switch.tsx | 63 +++++++++++++++++ .../hooks/useServiceInstalledStatus.tsx | 2 +- admin/inertia/hooks/useSystemSetting.ts | 22 ++++++ admin/inertia/layouts/SettingsLayout.tsx | 4 +- admin/inertia/lib/api.ts | 33 ++++++++- admin/inertia/pages/chat.tsx | 4 +- admin/inertia/pages/settings/benchmark.tsx | 18 +++-- admin/inertia/pages/settings/models.tsx | 59 ++++++++++++++-- admin/start/routes.ts | 6 +- admin/types/kv_store.ts | 2 +- 24 files changed, 460 insertions(+), 43 deletions(-) create mode 100644 admin/app/validators/settings.ts create mode 100644 admin/constants/kv_store.ts create mode 100644 admin/inertia/components/BouncingDots.tsx create mode 100644 admin/inertia/components/inputs/Switch.tsx create mode 100644 admin/inertia/hooks/useSystemSetting.ts diff --git a/admin/app/controllers/chats_controller.ts b/admin/app/controllers/chats_controller.ts index 59739f6..9d412bc 100644 --- a/admin/app/controllers/chats_controller.ts +++ b/admin/app/controllers/chats_controller.ts @@ -2,11 +2,22 @@ import { inject } from '@adonisjs/core' import type { HttpContext } from '@adonisjs/core/http' import { ChatService } from '#services/chat_service' import { createSessionSchema, updateSessionSchema, addMessageSchema } from '#validators/chat' +import { parseBoolean } from '../utils/misc.js' +import KVStore from '#models/kv_store' @inject() export default class ChatsController { constructor(private chatService: ChatService) {} + async inertia({ inertia }: HttpContext) { + const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled') + return inertia.render('chat', { + settings: { + chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled), + }, + }) + } + async index({}: HttpContext) { return await this.chatService.getAllSessions() } @@ -34,6 +45,17 @@ export default class ChatsController { } } + async suggestions({ response }: HttpContext) { + try { + const suggestions = await this.chatService.getChatSuggestions() + return response.status(200).json({ suggestions }) + } catch (error) { + return response.status(500).json({ + error: error instanceof Error ? error.message : 'Failed to get suggestions', + }) + } + } + async update({ params, request, response }: HttpContext) { try { const sessionId = parseInt(params.id) diff --git a/admin/app/controllers/settings_controller.ts b/admin/app/controllers/settings_controller.ts index 609f684..8eb1c16 100644 --- a/admin/app/controllers/settings_controller.ts +++ b/admin/app/controllers/settings_controller.ts @@ -1,9 +1,12 @@ +import KVStore from '#models/kv_store'; import { BenchmarkService } from '#services/benchmark_service'; import { MapService } from '#services/map_service'; import { OllamaService } from '#services/ollama_service'; import { SystemService } from '#services/system_service'; +import { updateSettingSchema } from '#validators/settings'; import { inject } from '@adonisjs/core'; import type { HttpContext } from '@adonisjs/core/http' +import { parseBoolean } from '../utils/misc.js'; @inject() export default class SettingsController { @@ -50,10 +53,14 @@ export default class SettingsController { async models({ inertia }: HttpContext) { const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false }); const installedModels = await this.ollamaService.getModels(); + const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled') return inertia.render('settings/models', { models: { availableModels: availableModels || [], - installedModels: installedModels || [] + installedModels: installedModels || [], + settings: { + chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled) + } } }); } @@ -88,4 +95,16 @@ export default class SettingsController { } }); } + + async getSetting({ request, response }: HttpContext) { + const key = request.qs().key; + const value = await KVStore.getValue(key); + return response.status(200).send({ key, value }); + } + + async updateSetting({ request, response }: HttpContext) { + const reqData = await request.validateUsing(updateSettingSchema); + await this.systemService.updateSetting(reqData.key, reqData.value); + return response.status(200).send({ success: true, message: 'Setting updated successfully' }); + } } \ No newline at end of file diff --git a/admin/app/models/kv_store.ts b/admin/app/models/kv_store.ts index 3c5165f..b483b3f 100644 --- a/admin/app/models/kv_store.ts +++ b/admin/app/models/kv_store.ts @@ -7,6 +7,7 @@ import type { KVStoreKey, KVStoreValue } from '../../types/kv_store.js' * that don't necessitate their own dedicated models. */ export default class KVStore extends BaseModel { + static table = 'kv_store' static namingStrategy = new SnakeCaseNamingStrategy() @column({ isPrimary: true }) @@ -29,7 +30,13 @@ export default class KVStore extends BaseModel { */ static async getValue(key: KVStoreKey): Promise { const setting = await this.findBy('key', key) - return setting?.value ?? null + if (!setting || setting.value === undefined || setting.value === null) { + return null + } + if (typeof setting.value === 'string') { + return setting.value + } + return String(setting.value) } /** diff --git a/admin/app/services/chat_service.ts b/admin/app/services/chat_service.ts index 3a8a700..48a1328 100644 --- a/admin/app/services/chat_service.ts +++ b/admin/app/services/chat_service.ts @@ -5,6 +5,8 @@ import { DateTime } from 'luxon' import { inject } from '@adonisjs/core' import { OllamaService } from './ollama_service.js' import { ChatRequest } from 'ollama' +import { SYSTEM_PROMPTS } from '../../constants/ollama.js' +import { toTitleCase } from '../utils/misc.js' @inject() export class ChatService { @@ -37,9 +39,75 @@ export class ChatService { } } + async getChatSuggestions() { + try { + const models = await this.ollamaService.getModels() + if (!models) { + return [] // If no models are available, return empty suggestions + } + + // Larger models generally give "better" responses, so pick the largest one + const largestModel = models.reduce((prev, current) => { + return prev.size > current.size ? prev : current + }) + + if (!largestModel) { + return [] + } + + const response = await this.ollamaService.chat({ + model: largestModel.name, + messages: [ + { + role: 'user', + content: SYSTEM_PROMPTS.chat_suggestions, + } + ], + stream: false, + }) + + if (response && response.message && response.message.content) { + const content = response.message.content.trim() + + // Handle both comma-separated and newline-separated formats + let suggestions: string[] = [] + + // Try splitting by commas first + if (content.includes(',')) { + suggestions = content.split(',').map((s) => s.trim()) + } + // Fall back to newline separation + else { + suggestions = content + .split(/\r?\n/) + .map((s) => s.trim()) + // Remove numbered list markers (1., 2., 3., etc.) and bullet points + .map((s) => s.replace(/^\d+\.\s*/, '').replace(/^[-*•]\s*/, '')) + // Remove surrounding quotes if present + .map((s) => s.replace(/^["']|["']$/g, '')) + } + + // Filter out empty strings and limit to 3 suggestions + const filtered = suggestions + .filter((s) => s.length > 0) + .slice(0, 3) + + return filtered.map((s) => toTitleCase(s)) + } else { + return [] + } + } catch (error) { + logger.error( + `[ChatService] Failed to get chat suggestions: ${ + error instanceof Error ? error.message : error + }` + ) + return [] + } + } + async getSession(sessionId: number) { try { - console.log('Fetching session with ID:', sessionId); const session = await ChatSession.query().where('id', sessionId).preload('messages').first() if (!session) { diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index ad5c713..c302dc3 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -10,6 +10,8 @@ import path, { join } from 'path' import { getAllFilesystems, getFile } from '../utils/fs.js' import axios from 'axios' import env from '#start/env' +import KVStore from '#models/kv_store' +import { KVStoreKey } from '../../types/kv_store.js' @inject() export class SystemService { @@ -254,6 +256,10 @@ export class SystemService { } } + async updateSetting(key: KVStoreKey, value: any): Promise { + await KVStore.setValue(key, value); + } + /** * Checks the current state of Docker containers against the database records and updates the database accordingly. * It will mark services as not installed if their corresponding containers do not exist, regardless of their running state. diff --git a/admin/app/utils/misc.ts b/admin/app/utils/misc.ts index bda8d23..a81f33d 100644 --- a/admin/app/utils/misc.ts +++ b/admin/app/utils/misc.ts @@ -3,3 +3,23 @@ export function formatSpeed(bytesPerSecond: number): string { if (bytesPerSecond < 1024 * 1024) return `${(bytesPerSecond / 1024).toFixed(1)} KB/s` return `${(bytesPerSecond / (1024 * 1024)).toFixed(1)} MB/s` } + +export function toTitleCase(str: string): string { + return str + .toLowerCase() + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') +} + +export function parseBoolean(value: any): boolean { + if (typeof value === 'boolean') return value + if (typeof value === 'string') { + const lower = value.toLowerCase() + return lower === 'true' || lower === '1' + } + if (typeof value === 'number') { + return value === 1 + } + return false +} \ No newline at end of file diff --git a/admin/app/validators/settings.ts b/admin/app/validators/settings.ts new file mode 100644 index 0000000..0dbab46 --- /dev/null +++ b/admin/app/validators/settings.ts @@ -0,0 +1,8 @@ +import vine from "@vinejs/vine"; +import { SETTINGS_KEYS } from "../../constants/kv_store.js"; + + +export const updateSettingSchema = vine.compile(vine.object({ + key: vine.enum(SETTINGS_KEYS), + value: vine.any(), +})) \ No newline at end of file diff --git a/admin/constants/kv_store.ts b/admin/constants/kv_store.ts new file mode 100644 index 0000000..e9afed7 --- /dev/null +++ b/admin/constants/kv_store.ts @@ -0,0 +1,3 @@ +import { KVStoreKey } from "../types/kv_store.js"; + +export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled']; \ No newline at end of file diff --git a/admin/constants/ollama.ts b/admin/constants/ollama.ts index 65136be..3bf9914 100644 --- a/admin/constants/ollama.ts +++ b/admin/constants/ollama.ts @@ -72,5 +72,29 @@ You have access to the following relevant information from the knowledge base. U ${context} If the user's question is related to this context, incorporate it into your response. Otherwise, respond normally. +`, + chat_suggestions: ` +You are a helpful assistant that generates conversation starter suggestions for a survivalist/prepper using an AI assistant. + +Provide exactly 3 conversation starter topics as direct questions that someone would ask. +These should be clear, complete questions that can start meaningful conversations. + +Examples of good suggestions: +- "How do I purify water in an emergency?" +- "What are the best foods for long-term storage?" +- "Help me create a 72-hour emergency kit" + +Do NOT use: +- Follow-up questions seeking clarification +- Vague or incomplete suggestions +- Questions that assume prior context +- Statements that are not suggestions themselves, such as praise for asking the question +- Direct questions or commands to the user + +Return ONLY the 3 suggestions as a comma-separated list with no additional text, formatting, numbering, or quotation marks. +The suggestions should be in title case. +Ensure that your suggestions are comma-seperated with no conjunctions like "and" or "or". +Do not use line breaks, new lines, or extra spacing to separate the suggestions. +Format: suggestion1, suggestion2, suggestion3 `, } diff --git a/admin/inertia/components/BouncingDots.tsx b/admin/inertia/components/BouncingDots.tsx new file mode 100644 index 0000000..e01c3cc --- /dev/null +++ b/admin/inertia/components/BouncingDots.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx' + +interface BouncingDotsProps { + text: string + containerClassName?: string + textClassName?: string +} + +export default function BouncingDots({ text, containerClassName, textClassName }: BouncingDotsProps) { + return ( +
+ {text} + + + + + +
+ ) +} diff --git a/admin/inertia/components/chat/ChatInterface.tsx b/admin/inertia/components/chat/ChatInterface.tsx index a183a4b..60acf32 100644 --- a/admin/inertia/components/chat/ChatInterface.tsx +++ b/admin/inertia/components/chat/ChatInterface.tsx @@ -4,17 +4,22 @@ import classNames from '~/lib/classNames' import { ChatMessage } from '../../../types/chat' import ChatMessageBubble from './ChatMessageBubble' import ChatAssistantAvatar from './ChatAssistantAvatar' +import BouncingDots from '../BouncingDots' interface ChatInterfaceProps { messages: ChatMessage[] onSendMessage: (message: string) => void isLoading?: boolean + chatSuggestions?: string[] + chatSuggestionsLoading?: boolean } export default function ChatInterface({ messages, onSendMessage, isLoading = false, + chatSuggestions = [], + chatSuggestionsLoading = false, }: ChatInterfaceProps) { const [input, setInput] = useState('') const messagesEndRef = useRef(null) @@ -54,7 +59,7 @@ export default function ChatInterface({ } return ( -
+
{messages.length === 0 ? (
@@ -64,6 +69,30 @@ export default function ChatInterface({

Interact with your installed language models directly in the Command Center.

+ {chatSuggestions && chatSuggestions.length > 0 && !chatSuggestionsLoading && ( +
+

Suggestions:

+
+ {chatSuggestions.map((suggestion, index) => ( + + ))} +
+
+ )} + {/* Display bouncing dots while loading suggestions */} + {chatSuggestionsLoading && }
) : ( @@ -85,23 +114,7 @@ export default function ChatInterface({
-
- Thinking - - - - - -
+
)} diff --git a/admin/inertia/components/chat/ChatModal.tsx b/admin/inertia/components/chat/ChatModal.tsx index 29e4f6c..4de9de5 100644 --- a/admin/inertia/components/chat/ChatModal.tsx +++ b/admin/inertia/components/chat/ChatModal.tsx @@ -1,5 +1,7 @@ import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react' import Chat from './index' +import { useSystemSetting } from '~/hooks/useSystemSetting' +import { parseBoolean } from '../../../app/utils/misc' interface ChatModalProps { open: boolean @@ -7,6 +9,10 @@ interface ChatModalProps { } export default function ChatModal({ open, onClose }: ChatModalProps) { + const settings = useSystemSetting({ + key: "chat.suggestionsEnabled" + }) + return ( - +
diff --git a/admin/inertia/components/chat/ChatSidebar.tsx b/admin/inertia/components/chat/ChatSidebar.tsx index 281adee..a35e26b 100644 --- a/admin/inertia/components/chat/ChatSidebar.tsx +++ b/admin/inertia/components/chat/ChatSidebar.tsx @@ -97,7 +97,7 @@ export default function ChatSidebar({ size="sm" fullWidth > - Models + Models & Settings { diff --git a/admin/inertia/components/chat/index.tsx b/admin/inertia/components/chat/index.tsx index 1dea897..96f5f36 100644 --- a/admin/inertia/components/chat/index.tsx +++ b/admin/inertia/components/chat/index.tsx @@ -14,9 +14,15 @@ interface ChatProps { enabled: boolean isInModal?: boolean onClose?: () => void + suggestionsEnabled?: boolean } -export default function Chat({ enabled, isInModal, onClose }: ChatProps) { +export default function Chat({ + enabled, + isInModal, + onClose, + suggestionsEnabled = false, +}: ChatProps) { const queryClient = useQueryClient() const { openModal, closeAllModals } = useModals() const [activeSessionId, setActiveSessionId] = useState(null) @@ -47,6 +53,15 @@ export default function Chat({ enabled, isInModal, onClose }: ChatProps) { select: (data) => data || [], }) + const { data: chatSuggestions, isLoading: chatSuggestionsLoading } = useQuery({ + queryKey: ['chatSuggestions'], + queryFn: async () => { + const res = await api.getChatSuggestions() + return res ?? [] + }, + enabled: suggestionsEnabled, + }) + const deleteAllSessionsMutation = useMutation({ mutationFn: () => api.deleteAllChatSessions(), onSuccess: () => { @@ -263,6 +278,8 @@ export default function Chat({ enabled, isInModal, onClose }: ChatProps) { messages={messages} onSendMessage={handleSendMessage} isLoading={chatMutation.isPending} + chatSuggestions={chatSuggestions} + chatSuggestionsLoading={chatSuggestionsLoading} />
diff --git a/admin/inertia/components/inputs/Switch.tsx b/admin/inertia/components/inputs/Switch.tsx new file mode 100644 index 0000000..eac0ded --- /dev/null +++ b/admin/inertia/components/inputs/Switch.tsx @@ -0,0 +1,63 @@ +import clsx from 'clsx' + +interface SwitchProps { + checked: boolean + onChange: (checked: boolean) => void + label?: string + description?: string + disabled?: boolean + id?: string +} + +export default function Switch({ + checked, + onChange, + label, + description, + disabled = false, + id, +}: SwitchProps) { + const switchId = id || `switch-${label?.replace(/\s+/g, '-').toLowerCase()}` + + return ( +
+ {(label || description) && ( +
+ {label && ( + + )} + {description &&

{description}

} +
+ )} +
+ +
+
+ ) +} diff --git a/admin/inertia/hooks/useServiceInstalledStatus.tsx b/admin/inertia/hooks/useServiceInstalledStatus.tsx index 9e1c2cc..62b1137 100644 --- a/admin/inertia/hooks/useServiceInstalledStatus.tsx +++ b/admin/inertia/hooks/useServiceInstalledStatus.tsx @@ -5,7 +5,7 @@ import api from '~/lib/api' const useServiceInstalledStatus = (serviceName: string) => { const { data, isFetching } = useQuery({ queryKey: ['installed-services'], - queryFn: () => api.listServices(), + queryFn: () => api.getSystemServices(), }) const isInstalled = data?.some( diff --git a/admin/inertia/hooks/useSystemSetting.ts b/admin/inertia/hooks/useSystemSetting.ts new file mode 100644 index 0000000..4cd0768 --- /dev/null +++ b/admin/inertia/hooks/useSystemSetting.ts @@ -0,0 +1,22 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import api from '~/lib/api' +import { KVStoreKey } from '../../types/kv_store'; + +export type UseSystemSettingProps = Omit< + UseQueryOptions<{ key: string; value: any } | undefined>, + 'queryKey' | 'queryFn' +> & { + key: KVStoreKey +} + +export const useSystemSetting = (props: UseSystemSettingProps) => { + const { key, ...queryOptions } = props + + const queryData = useQuery<{ key: string; value: any } | undefined>({ + ...queryOptions, + queryKey: ['system-setting', key], + queryFn: async () => await api.getSetting(key), + }) + + return queryData +} diff --git a/admin/inertia/layouts/SettingsLayout.tsx b/admin/inertia/layouts/SettingsLayout.tsx index 91eb697..76dc4cd 100644 --- a/admin/inertia/layouts/SettingsLayout.tsx +++ b/admin/inertia/layouts/SettingsLayout.tsx @@ -2,19 +2,19 @@ import { IconArrowBigUpLines, IconChartBar, IconDashboard, - IconDatabaseStar, IconFolder, IconGavel, IconMapRoute, IconSettings, IconTerminal2, + IconWand, IconZoom } from '@tabler/icons-react' import StyledSidebar from '~/components/StyledSidebar' import { getServiceLink } from '~/lib/navigation' const navigation = [ - { name: 'AI Model Manager', href: '/settings/models', icon: IconDatabaseStar, current: false }, + { name: 'AI Assistant', href: '/settings/models', icon: IconWand, current: false }, { name: 'Apps', href: '/settings/apps', icon: IconTerminal2, current: false }, { name: 'Benchmark', href: '/settings/benchmark', icon: IconChartBar, current: false }, { name: 'Content Explorer', href: '/settings/zim/remote-explorer', icon: IconZoom, current: false }, diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index 23a2ce5..b9d10a1 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -135,6 +135,15 @@ class API { })() } + async getChatSuggestions() { + return catchInternal(async () => { + const response = await this.client.get<{ suggestions: string[] }>( + '/chat/suggestions' + ) + return response.data.suggestions + })() + } + async getInternetStatus() { return catchInternal(async () => { const response = await this.client.get('/system/internet-status') @@ -167,14 +176,14 @@ class API { async getBenchmarkResults() { return catchInternal(async () => { - const response = await this.client.get('/benchmark/results') + const response = await this.client.get<{ results: BenchmarkResult[], total: number}>('/benchmark/results') return response.data })() } async getLatestBenchmarkResult() { return catchInternal(async () => { - const response = await this.client.get('/benchmark/results/latest') + const response = await this.client.get<{ result: BenchmarkResult | null}>('/benchmark/results/latest') return response.data })() } @@ -484,6 +493,26 @@ class API { return response.data })() } + + async getSetting(key: string) { + return catchInternal(async () => { + const response = await this.client.get<{ key: string; value: any }>( + '/system/settings', + { params: { key } } + ) + return response.data + })() + } + + async updateSetting(key: string, value: any) { + return catchInternal(async () => { + const response = await this.client.patch<{ success: boolean; message: string }>( + '/system/settings', + { key, value } + ) + return response.data + })() + } } export default new API() diff --git a/admin/inertia/pages/chat.tsx b/admin/inertia/pages/chat.tsx index 1bede8f..3c9c118 100644 --- a/admin/inertia/pages/chat.tsx +++ b/admin/inertia/pages/chat.tsx @@ -1,11 +1,11 @@ import { Head } from '@inertiajs/react' import ChatComponent from '~/components/chat' -export default function Chat() { +export default function Chat(props: { settings: { chatSuggestionsEnabled: boolean } }) { return (
- +
) } diff --git a/admin/inertia/pages/settings/benchmark.tsx b/admin/inertia/pages/settings/benchmark.tsx index 6d188a8..eb984c5 100644 --- a/admin/inertia/pages/settings/benchmark.tsx +++ b/admin/inertia/pages/settings/benchmark.tsx @@ -51,7 +51,10 @@ export default function BenchmarkPage(props: { queryKey: ['benchmark', 'latest'], queryFn: async () => { const res = await api.getLatestBenchmarkResult() - return res ?? null + if (res && res.result) { + return res.result + } + return null }, initialData: props.benchmark.latestResult, }) @@ -61,7 +64,10 @@ export default function BenchmarkPage(props: { queryKey: ['benchmark', 'history'], queryFn: async () => { const res = await api.getBenchmarkResults() - return res ?? [] + if (res && res.results && Array.isArray(res.results)) { + return res.results + } + return [] }, }) @@ -121,7 +127,7 @@ export default function BenchmarkPage(props: { const updateBuilderTag = useMutation({ mutationFn: async ({ benchmarkId, - builderTag + builderTag, }: { benchmarkId: string builderTag: string @@ -149,7 +155,11 @@ export default function BenchmarkPage(props: { // First, save the current builder tag to the benchmark (don't refetch yet) if (currentBuilderTag && !anonymous) { - await updateBuilderTag.mutateAsync({ benchmarkId, builderTag: currentBuilderTag, invalidate: false }) + await updateBuilderTag.mutateAsync({ + benchmarkId, + builderTag: currentBuilderTag, + invalidate: false, + }) } const res = await api.submitBenchmark(benchmarkId, anonymous) diff --git a/admin/inertia/pages/settings/models.tsx b/admin/inertia/pages/settings/models.tsx index b51ee59..69bdae5 100644 --- a/admin/inertia/pages/settings/models.tsx +++ b/admin/inertia/pages/settings/models.tsx @@ -1,4 +1,5 @@ import { Head, router } from '@inertiajs/react' +import { useState } from 'react' import StyledTable from '~/components/StyledTable' import SettingsLayout from '~/layouts/SettingsLayout' import { NomadOllamaModel } from '../../../types/ollama' @@ -11,13 +12,23 @@ import { useModals } from '~/context/ModalContext' import StyledModal from '~/components/StyledModal' import { ModelResponse } from 'ollama' import { SERVICE_NAMES } from '../../../constants/service_names' +import Switch from '~/components/inputs/Switch' +import StyledSectionHeader from '~/components/StyledSectionHeader' +import { useMutation } from '@tanstack/react-query' export default function ModelsPage(props: { - models: { availableModels: NomadOllamaModel[]; installedModels: ModelResponse[] } + models: { + availableModels: NomadOllamaModel[] + installedModels: ModelResponse[] + settings: { chatSuggestionsEnabled: boolean } + } }) { const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const { addNotification } = useNotifications() const { openModal, closeAllModals } = useModals() + const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState( + props.models.settings.chatSuggestionsEnabled + ) async function handleInstallModel(modelName: string) { try { @@ -79,15 +90,35 @@ export default function ModelsPage(props: { ) } + const updateSettingMutation = useMutation({ + mutationFn: async ({ key, value }: { key: string; value: boolean }) => { + return await api.updateSetting(key, value) + }, + onSuccess: () => { + addNotification({ + message: 'Setting updated successfully.', + type: 'success', + }) + }, + onError: (error) => { + console.error('Error updating setting:', error) + addNotification({ + message: 'There was an error updating the setting. Please try again.', + type: 'error', + }) + }, + }) + return ( - +
-

AI Model Manager

+

AI Assistant

- Easily manage the AI models available for AI Assistant. We recommend starting with smaller - models first to see how they perform on your system before moving on to larger ones. + Easily manage the AI Assistant's settings and installed models. We recommend starting + with smaller models first to see how they perform on your system before moving on to + larger ones.

{!isInstalled && ( )} + + +
+
+ { + setChatSuggestionsEnabled(newVal) + updateSettingMutation.mutate({ key: 'chat.suggestionsEnabled', value: newVal }) + }} + label="Chat Suggestions" + description="Display AI-generated conversation starters in the chat interface" + /> +
+
+ - className="font-semibold mt-8" + className="font-semibold" rowLines={true} columns={[ { diff --git a/admin/start/routes.ts b/admin/start/routes.ts index 84dade9..d88ae14 100644 --- a/admin/start/routes.ts +++ b/admin/start/routes.ts @@ -26,7 +26,7 @@ transmit.registerRoutes() router.get('/', [HomeController, 'index']) router.get('/home', [HomeController, 'home']) router.on('/about').renderInertia('about') -router.on('/chat').renderInertia('chat') +router.get('/chat', [ChatsController, 'inertia']) router.on('/knowledge-base').renderInertia('knowledge-base') router.get('/maps', [MapsController, 'index']) @@ -113,6 +113,8 @@ router }) .prefix('/api/chat/sessions') +router.get('/api/chat/suggestions', [ChatsController, 'suggestions']) + router .group(() => { router.post('/upload', [RagController, 'upload']) @@ -133,6 +135,8 @@ router router.post('/update', [SystemController, 'requestSystemUpdate']) router.get('/update/status', [SystemController, 'getSystemUpdateStatus']) router.get('/update/logs', [SystemController, 'getSystemUpdateLogs']) + router.get('/settings', [SettingsController, 'getSetting']) + router.patch('/settings', [SettingsController, 'updateSetting']) }) .prefix('/api/system') diff --git a/admin/types/kv_store.ts b/admin/types/kv_store.ts index f381a84..d875cae 100644 --- a/admin/types/kv_store.ts +++ b/admin/types/kv_store.ts @@ -1,3 +1,3 @@ -export type KVStoreKey = '' +export type KVStoreKey = 'chat.suggestionsEnabled' export type KVStoreValue = string | null \ No newline at end of file