feat(AI Assistant): custom name option for AI Assistant

This commit is contained in:
Jake Turner 2026-03-04 06:29:08 +00:00 committed by Jake Turner
parent b806cefe3a
commit 96beab7e69
18 changed files with 171 additions and 263 deletions

View File

@ -54,12 +54,14 @@ export default class SettingsController {
const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false, query: null, limit: 15 }); const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false, query: null, limit: 15 });
const installedModels = await this.ollamaService.getModels(); const installedModels = await this.ollamaService.getModels();
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled') const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
const aiAssistantCustomName = await KVStore.getValue('ai.assistantCustomName')
return inertia.render('settings/models', { return inertia.render('settings/models', {
models: { models: {
availableModels: availableModels?.models || [], availableModels: availableModels?.models || [],
installedModels: installedModels || [], installedModels: installedModels || [],
settings: { settings: {
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false chatSuggestionsEnabled: chatSuggestionsEnabled ?? false,
aiAssistantCustomName: aiAssistantCustomName ?? '',
} }
} }
}); });

View File

@ -50,4 +50,15 @@ export default class KVStore extends BaseModel {
} }
return setting return setting
} }
/**
* Clear a setting value by key, storing null so getValue returns null.
*/
static async clearValue<K extends KVStoreKey>(key: K): Promise<void> {
const setting = await this.findBy('key', key)
if (setting && setting.value !== null) {
setting.value = null
await setting.save()
}
}
} }

View File

@ -12,7 +12,7 @@ import { getAllFilesystems, getFile } from '../utils/fs.js'
import axios from 'axios' import axios from 'axios'
import env from '#start/env' import env from '#start/env'
import KVStore from '#models/kv_store' import KVStore from '#models/kv_store'
import { KVStoreKey } from '../../types/kv_store.js' import { KV_STORE_SCHEMA, KVStoreKey } from '../../types/kv_store.js'
@inject() @inject()
@ -388,7 +388,11 @@ export class SystemService {
} }
async updateSetting(key: KVStoreKey, value: any): Promise<void> { async updateSetting(key: KVStoreKey, value: any): Promise<void> {
await KVStore.setValue(key, value); if ((value === '' || value === undefined || value === null) && KV_STORE_SCHEMA[key] === 'string') {
await KVStore.clearValue(key)
} else {
await KVStore.setValue(key, value)
}
} }
/** /**

View File

@ -4,5 +4,5 @@ import { SETTINGS_KEYS } from "../../constants/kv_store.js";
export const updateSettingSchema = vine.compile(vine.object({ export const updateSettingSchema = vine.compile(vine.object({
key: vine.enum(SETTINGS_KEYS), key: vine.enum(SETTINGS_KEYS),
value: vine.any(), value: vine.any().optional(),
})) }))

View File

@ -1,3 +1,4 @@
import KVStore from '#models/kv_store'
import { SystemService } from '#services/system_service' import { SystemService } from '#services/system_service'
import { defineConfig } from '@adonisjs/inertia' import { defineConfig } from '@adonisjs/inertia'
import type { InferSharedProps } from '@adonisjs/inertia/types' import type { InferSharedProps } from '@adonisjs/inertia/types'
@ -14,6 +15,10 @@ const inertiaConfig = defineConfig({
sharedData: { sharedData: {
appVersion: () => SystemService.getAppVersion(), appVersion: () => SystemService.getAppVersion(),
environment: process.env.NODE_ENV || 'production', environment: process.env.NODE_ENV || 'production',
aiAssistantName: async () => {
const customName = await KVStore.getValue('ai.assistantCustomName')
return (customName && customName.trim()) ? customName : 'AI Assistant'
},
}, },
/** /**

View File

@ -1,3 +1,3 @@
import { KVStoreKey } from "../types/kv_store.js"; import { KVStoreKey } from "../types/kv_store.js";
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'ui.hasVisitedEasySetup', 'system.earlyAccess']; export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'ui.hasVisitedEasySetup', 'system.earlyAccess', 'ai.assistantCustomName'];

View File

@ -9,6 +9,7 @@ import StyledModal from '../StyledModal'
import api from '~/lib/api' import api from '~/lib/api'
import { DEFAULT_QUERY_REWRITE_MODEL } from '../../../constants/ollama' import { DEFAULT_QUERY_REWRITE_MODEL } from '../../../constants/ollama'
import { useNotifications } from '~/context/NotificationContext' import { useNotifications } from '~/context/NotificationContext'
import { usePage } from '@inertiajs/react'
interface ChatInterfaceProps { interface ChatInterfaceProps {
messages: ChatMessage[] messages: ChatMessage[]
@ -29,6 +30,7 @@ export default function ChatInterface({
chatSuggestionsLoading = false, chatSuggestionsLoading = false,
rewriteModelAvailable = false rewriteModelAvailable = false
}: ChatInterfaceProps) { }: ChatInterfaceProps) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const { addNotification } = useNotifications() const { addNotification } = useNotifications()
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false) const [downloadDialogOpen, setDownloadDialogOpen] = useState(false)
@ -160,7 +162,7 @@ export default function ChatInterface({
value={input} value={input}
onChange={handleInput} onChange={handleInput}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Type your message... (Shift+Enter for new line)" placeholder={`Type your message to ${aiAssistantName}... (Shift+Enter for new line)`}
className="w-full resize-none rounded-lg border border-gray-300 px-4 py-3 pr-12 focus:outline-none focus:ring-2 focus:ring-desert-green focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500" className="w-full resize-none rounded-lg border border-gray-300 px-4 py-3 pr-12 focus:outline-none focus:ring-2 focus:ring-desert-green focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
rows={1} rows={1}
disabled={isLoading} disabled={isLoading}

View File

@ -1,6 +1,6 @@
import classNames from '~/lib/classNames' import classNames from '~/lib/classNames'
import StyledButton from '../StyledButton' import StyledButton from '../StyledButton'
import { router } from '@inertiajs/react' import { router, usePage } from '@inertiajs/react'
import { ChatSession } from '../../../types/chat' import { ChatSession } from '../../../types/chat'
import { IconMessage } from '@tabler/icons-react' import { IconMessage } from '@tabler/icons-react'
import { useState } from 'react' import { useState } from 'react'
@ -23,6 +23,7 @@ export default function ChatSidebar({
onClearHistory, onClearHistory,
isInModal = false, isInModal = false,
}: ChatSidebarProps) { }: ChatSidebarProps) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const [isKnowledgeBaseModalOpen, setIsKnowledgeBaseModalOpen] = useState( const [isKnowledgeBaseModalOpen, setIsKnowledgeBaseModalOpen] = useState(
() => new URLSearchParams(window.location.search).get('knowledge_base') === 'true' () => new URLSearchParams(window.location.search).get('knowledge_base') === 'true'
) )
@ -139,7 +140,7 @@ export default function ChatSidebar({
)} )}
</div> </div>
{isKnowledgeBaseModalOpen && ( {isKnowledgeBaseModalOpen && (
<KnowledgeBaseModal onClose={handleCloseKnowledgeBase} /> <KnowledgeBaseModal aiAssistantName={aiAssistantName} onClose={handleCloseKnowledgeBase} />
)} )}
</div> </div>
) )

View File

@ -11,10 +11,11 @@ import { useModals } from '~/context/ModalContext'
import StyledModal from '../StyledModal' import StyledModal from '../StyledModal'
interface KnowledgeBaseModalProps { interface KnowledgeBaseModalProps {
aiAssistantName?: string
onClose: () => void onClose: () => void
} }
export default function KnowledgeBaseModal({ onClose }: KnowledgeBaseModalProps) { export default function KnowledgeBaseModal({ aiAssistantName = "AI Assistant", onClose }: KnowledgeBaseModalProps) {
const { addNotification } = useNotifications() const { addNotification } = useNotifications()
const [files, setFiles] = useState<File[]>([]) const [files, setFiles] = useState<File[]>([])
const fileUploaderRef = useRef<React.ComponentRef<typeof FileUploader>>(null) const fileUploaderRef = useRef<React.ComponentRef<typeof FileUploader>>(null)
@ -140,12 +141,12 @@ export default function KnowledgeBaseModal({ onClose }: KnowledgeBaseModalProps)
</div> </div>
<div> <div>
<p className="font-medium text-desert-stone-dark"> <p className="font-medium text-desert-stone-dark">
AI Assistant Knowledge Base Integration {aiAssistantName} Knowledge Base Integration
</p> </p>
<p className="text-sm text-desert-stone"> <p className="text-sm text-desert-stone">
When you upload documents to your Knowledge Base, NOMAD processes and embeds When you upload documents to your Knowledge Base, NOMAD processes and embeds
the content, making it directly accessible to the AI Assistant. This allows the content, making it directly accessible to {aiAssistantName}. This allows{' '}
the AI Assistant to reference your specific documents during conversations, {aiAssistantName} to reference your specific documents during conversations,
providing more accurate and personalized responses based on your uploaded providing more accurate and personalized responses based on your uploaded
data. data.
</p> </p>
@ -177,8 +178,7 @@ export default function KnowledgeBaseModal({ onClose }: KnowledgeBaseModalProps)
</p> </p>
<p className="text-sm text-desert-stone"> <p className="text-sm text-desert-stone">
NOMAD will automatically discover and extract any content you save to your NOMAD will automatically discover and extract any content you save to your
Information Library (if installed), making it instantly available to the AI Information Library (if installed), making it instantly available to {aiAssistantName} without any extra steps.
Assistant without any extra steps.
</p> </p>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@ import { InputHTMLAttributes } from "react";
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> { export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
name: string; name: string;
label: string; label: string;
helpText?: string;
className?: string; className?: string;
labelClassName?: string; labelClassName?: string;
inputClassName?: string; inputClassName?: string;
@ -17,6 +18,7 @@ const Input: React.FC<InputProps> = ({
className, className,
label, label,
name, name,
helpText,
labelClassName, labelClassName,
inputClassName, inputClassName,
containerClassName, containerClassName,
@ -33,6 +35,7 @@ const Input: React.FC<InputProps> = ({
> >
{label}{required ? "*" : ""} {label}{required ? "*" : ""}
</label> </label>
{helpText && <p className="mt-1 text-sm text-gray-500">{helpText}</p>}
<div className={classNames("mt-1.5", containerClassName)}> <div className={classNames("mt-1.5", containerClassName)}>
<div className="relative"> <div className="relative">
{leftIcon && ( {leftIcon && (

View File

@ -10,34 +10,37 @@ import {
IconWand, IconWand,
IconZoom IconZoom
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { usePage } from '@inertiajs/react'
import StyledSidebar from '~/components/StyledSidebar' import StyledSidebar from '~/components/StyledSidebar'
import { getServiceLink } from '~/lib/navigation' import { getServiceLink } from '~/lib/navigation'
const navigation = [
{ 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 },
{ name: 'Content Manager', href: '/settings/zim', icon: IconFolder, current: false },
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
{
name: 'Service Logs & Metrics',
href: getServiceLink('9999'),
icon: IconDashboard,
current: false,
target: '_blank',
},
{
name: 'Check for Updates',
href: '/settings/update',
icon: IconArrowBigUpLines,
current: false,
},
{ name: 'System', href: '/settings/system', icon: IconSettings, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
]
export default function SettingsLayout({ children }: { children: React.ReactNode }) { export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const navigation = [
{ name: aiAssistantName, 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 },
{ name: 'Content Manager', href: '/settings/zim', icon: IconFolder, current: false },
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
{
name: 'Service Logs & Metrics',
href: getServiceLink('9999'),
icon: IconDashboard,
current: false,
target: '_blank',
},
{
name: 'Check for Updates',
href: '/settings/update',
icon: IconArrowBigUpLines,
current: false,
},
{ name: 'System', href: '/settings/system', icon: IconSettings, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
]
return ( return (
<div className="min-h-screen flex flex-row bg-stone-50/90"> <div className="min-h-screen flex flex-row bg-stone-50/90">
<StyledSidebar title="Settings" items={navigation} /> <StyledSidebar title="Settings" items={navigation} />

View File

@ -1,10 +1,11 @@
import { Head } from '@inertiajs/react' import { Head, usePage } from '@inertiajs/react'
import ChatComponent from '~/components/chat' import ChatComponent from '~/components/chat'
export default function Chat(props: { settings: { chatSuggestionsEnabled: boolean } }) { export default function Chat(props: { settings: { chatSuggestionsEnabled: boolean } }) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
return ( return (
<div className="w-full h-full"> <div className="w-full h-full">
<Head title="AI Assistant" /> <Head title={aiAssistantName} />
<ChatComponent enabled={true} suggestionsEnabled={props.settings.chatSuggestionsEnabled} /> <ChatComponent enabled={true} suggestionsEnabled={props.settings.chatSuggestionsEnabled} />
</div> </div>
) )

View File

@ -1,4 +1,4 @@
import { Head, router } from '@inertiajs/react' import { Head, router, usePage } from '@inertiajs/react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'
import AppLayout from '~/layouts/AppLayout' import AppLayout from '~/layouts/AppLayout'
@ -32,51 +32,53 @@ interface Capability {
icon: string icon: string
} }
const CORE_CAPABILITIES: Capability[] = [ function buildCoreCapabilities(aiAssistantName: string): Capability[] {
{ return [
id: 'information', {
name: 'Information Library', id: 'information',
technicalName: 'Kiwix', name: 'Information Library',
description: technicalName: 'Kiwix',
'Offline access to Wikipedia, medical references, how-to guides, and encyclopedias', description:
features: [ 'Offline access to Wikipedia, medical references, how-to guides, and encyclopedias',
'Complete Wikipedia offline', features: [
'Medical references and first aid guides', 'Complete Wikipedia offline',
'WikiHow articles and tutorials', 'Medical references and first aid guides',
'Project Gutenberg books and literature', 'WikiHow articles and tutorials',
], 'Project Gutenberg books and literature',
services: [SERVICE_NAMES.KIWIX], ],
icon: 'IconBooks', services: [SERVICE_NAMES.KIWIX],
}, icon: 'IconBooks',
{ },
id: 'education', {
name: 'Education Platform', id: 'education',
technicalName: 'Kolibri', name: 'Education Platform',
description: 'Interactive learning platform with video courses and exercises', technicalName: 'Kolibri',
features: [ description: 'Interactive learning platform with video courses and exercises',
'Khan Academy math and science courses', features: [
'K-12 curriculum content', 'Khan Academy math and science courses',
'Interactive exercises and quizzes', 'K-12 curriculum content',
'Progress tracking for learners', 'Interactive exercises and quizzes',
], 'Progress tracking for learners',
services: [SERVICE_NAMES.KOLIBRI], ],
icon: 'IconSchool', services: [SERVICE_NAMES.KOLIBRI],
}, icon: 'IconSchool',
{ },
id: 'ai', {
name: 'AI Assistant', id: 'ai',
technicalName: 'Ollama', name: aiAssistantName,
description: 'Local AI chat that runs entirely on your hardware - no internet required', technicalName: 'Ollama',
features: [ description: 'Local AI chat that runs entirely on your hardware - no internet required',
'Private conversations that never leave your device', features: [
'No internet connection needed after setup', 'Private conversations that never leave your device',
'Ask questions, get help with writing, brainstorm ideas', 'No internet connection needed after setup',
'Runs on your own hardware with local AI models', 'Ask questions, get help with writing, brainstorm ideas',
], 'Runs on your own hardware with local AI models',
services: [SERVICE_NAMES.OLLAMA], ],
icon: 'IconRobot', services: [SERVICE_NAMES.OLLAMA],
}, icon: 'IconRobot',
] },
]
}
const ADDITIONAL_TOOLS: Capability[] = [ const ADDITIONAL_TOOLS: Capability[] = [
{ {
@ -110,6 +112,9 @@ const CURATED_CATEGORIES_KEY = 'curated-categories'
const WIKIPEDIA_STATE_KEY = 'wikipedia-state' const WIKIPEDIA_STATE_KEY = 'wikipedia-state'
export default function EasySetupWizard(props: { system: { services: ServiceSlim[] } }) { export default function EasySetupWizard(props: { system: { services: ServiceSlim[] } }) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const CORE_CAPABILITIES = buildCoreCapabilities(aiAssistantName)
const [currentStep, setCurrentStep] = useState<WizardStep>(1) const [currentStep, setCurrentStep] = useState<WizardStep>(1)
const [selectedServices, setSelectedServices] = useState<string[]>([]) const [selectedServices, setSelectedServices] = useState<string[]>([])
const [selectedMapCollections, setSelectedMapCollections] = useState<string[]>([]) const [selectedMapCollections, setSelectedMapCollections] = useState<string[]>([])

View File

@ -6,7 +6,7 @@ import {
IconSettings, IconSettings,
IconWifiOff, IconWifiOff,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { Head } from '@inertiajs/react' import { Head, usePage } from '@inertiajs/react'
import AppLayout from '~/layouts/AppLayout' import AppLayout from '~/layouts/AppLayout'
import { getServiceLink } from '~/lib/navigation' import { getServiceLink } from '~/lib/navigation'
import { ServiceSlim } from '../../types/services' import { ServiceSlim } from '../../types/services'
@ -14,6 +14,7 @@ import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
import { useUpdateAvailable } from '~/hooks/useUpdateAvailable' import { useUpdateAvailable } from '~/hooks/useUpdateAvailable'
import { useSystemSetting } from '~/hooks/useSystemSetting' import { useSystemSetting } from '~/hooks/useSystemSetting'
import Alert from '~/components/Alert' import Alert from '~/components/Alert'
import { SERVICE_NAMES } from '../../constants/service_names'
// Maps is a Core Capability (display_order: 4) // Maps is a Core Capability (display_order: 4)
const MAPS_ITEM = { const MAPS_ITEM = {
@ -90,6 +91,7 @@ export default function Home(props: {
}) { }) {
const items: DashboardItem[] = [] const items: DashboardItem[] = []
const updateInfo = useUpdateAvailable(); const updateInfo = useUpdateAvailable();
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
// Check if user has visited Easy Setup // Check if user has visited Easy Setup
const { data: easySetupVisited } = useSystemSetting({ const { data: easySetupVisited } = useSystemSetting({
@ -102,7 +104,8 @@ export default function Home(props: {
.filter((service) => service.installed && service.ui_location) .filter((service) => service.installed && service.ui_location)
.forEach((service) => { .forEach((service) => {
items.push({ items.push({
label: service.friendly_name || service.service_name, // Inject custom AI Assistant name if this is the chat service
label: service.service_name === SERVICE_NAMES.OLLAMA && aiAssistantName ? aiAssistantName : (service.friendly_name || service.service_name),
to: service.ui_location ? getServiceLink(service.ui_location) : '#', to: service.ui_location ? getServiceLink(service.ui_location) : '#',
target: '_blank', target: '_blank',
description: description:

View File

@ -1,153 +0,0 @@
import { Head } from '@inertiajs/react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useRef, useState } from 'react'
import FileUploader from '~/components/file-uploader'
import StyledButton from '~/components/StyledButton'
import StyledSectionHeader from '~/components/StyledSectionHeader'
import StyledTable from '~/components/StyledTable'
import { useNotifications } from '~/context/NotificationContext'
import AppLayout from '~/layouts/AppLayout'
import api from '~/lib/api'
export default function KnowledgeBase() {
const { addNotification } = useNotifications()
const [files, setFiles] = useState<File[]>([])
const fileUploaderRef = useRef<React.ComponentRef<typeof FileUploader>>(null)
const { data: storedFiles = [], isLoading: isLoadingFiles } = useQuery({
queryKey: ['storedFiles'],
queryFn: () => api.getStoredRAGFiles(),
select: (data) => data || [],
})
const uploadMutation = useMutation({
mutationFn: (file: File) => api.uploadDocument(file),
onSuccess: (data) => {
addNotification({
type: 'success',
message: data?.message || 'Document uploaded and queued for processing',
})
setFiles([])
if (fileUploaderRef.current) {
fileUploaderRef.current.clear()
}
},
onError: (error: any) => {
addNotification({
type: 'error',
message: error?.message || 'Failed to upload document',
})
},
})
const handleUpload = () => {
if (files.length > 0) {
uploadMutation.mutate(files[0])
}
}
return (
<AppLayout>
<Head title="Knowledge Base" />
<main className="px-6 lg:px-12 py-6 lg:py-8">
<div className="bg-white rounded-lg border shadow-md overflow-hidden">
<div className="p-6">
<FileUploader
ref={fileUploaderRef}
minFiles={1}
maxFiles={1}
onUpload={(uploadedFiles) => {
setFiles(Array.from(uploadedFiles))
}}
/>
<div className="flex justify-center gap-4 my-6">
<StyledButton
variant="primary"
size="lg"
icon="IconUpload"
onClick={handleUpload}
disabled={files.length === 0 || uploadMutation.isPending}
loading={uploadMutation.isPending}
>
Upload
</StyledButton>
</div>
</div>
<div className="border-t bg-white p-6">
<h3 className="text-lg font-semibold text-desert-green mb-4">
Why upload documents to your Knowledge Base?
</h3>
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-desert-green text-white flex items-center justify-center text-sm font-bold">
1
</div>
<div>
<p className="font-medium text-desert-stone-dark">
AI Assistant Knowledge Base Integration
</p>
<p className="text-sm text-desert-stone">
When you upload documents to your Knowledge Base, NOMAD processes and embeds the
content, making it directly accessible to the AI Assistant. This allows the AI
Assistant to reference your specific documents during conversations, providing
more accurate and personalized responses based on your uploaded data.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-desert-green text-white flex items-center justify-center text-sm font-bold">
2
</div>
<div>
<p className="font-medium text-desert-stone-dark">
Enhanced Document Processing with OCR
</p>
<p className="text-sm text-desert-stone">
NOMAD includes built-in Optical Character Recognition (OCR) capabilities,
allowing it to extract text from image-based documents such as scanned PDFs or
photos. This means that even if your documents are not in a standard text
format, NOMAD can still process and embed their content for AI access.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-desert-green text-white flex items-center justify-center text-sm font-bold">
3
</div>
<div>
<p className="font-medium text-desert-stone-dark">
Information Library Integration
</p>
<p className="text-sm text-desert-stone">
NOMAD will automatically discover and extract any content you save to your
Information Library (if installed), making it instantly available to the AI
Assistant without any extra steps.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="my-12">
<StyledSectionHeader title="Stored Knowledge Base Files" />
<StyledTable<{ source: string }>
className="font-semibold"
rowLines={true}
columns={[
{
accessor: 'source',
title: 'File Name',
render(record) {
return <span className="text-gray-700">{record.source}</span>
},
},
]}
data={storedFiles.map((source) => ({ source }))}
loading={isLoadingFiles}
/>
</div>
</main>
</AppLayout>
)
}

View File

@ -1,4 +1,4 @@
import { Head, Link } from '@inertiajs/react' import { Head, Link, usePage } from '@inertiajs/react'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import SettingsLayout from '~/layouts/SettingsLayout' import SettingsLayout from '~/layouts/SettingsLayout'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
@ -34,6 +34,7 @@ export default function BenchmarkPage(props: {
currentBenchmarkId: string | null currentBenchmarkId: string | null
} }
}) { }) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const { subscribe } = useTransmit() const { subscribe } = useTransmit()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const aiInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const aiInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
@ -403,8 +404,8 @@ export default function BenchmarkPage(props: {
{showAIRequiredAlert && ( {showAIRequiredAlert && (
<Alert <Alert
type="warning" type="warning"
title="AI Assistant Required" title={`${aiAssistantName} Required`}
message="Full benchmark requires AI Assistant to be installed. Install it to measure your complete NOMAD capability and share results with the community." message={`Full benchmark requires ${aiAssistantName} to be installed. Install it to measure your complete NOMAD capability and share results with the community.`}
variant="bordered" variant="bordered"
dismissible dismissible
onDismiss={() => setShowAIRequiredAlert(false)} onDismiss={() => setShowAIRequiredAlert(false)}
@ -413,7 +414,7 @@ export default function BenchmarkPage(props: {
href="/settings/apps" href="/settings/apps"
className="text-sm text-desert-green hover:underline mt-2 inline-block font-medium" className="text-sm text-desert-green hover:underline mt-2 inline-block font-medium"
> >
Go to Apps to install AI Assistant Go to Apps to install {aiAssistantName}
</Link> </Link>
</Alert> </Alert>
)} )}
@ -444,7 +445,7 @@ export default function BenchmarkPage(props: {
icon="IconWand" icon="IconWand"
title={ title={
!aiInstalled !aiInstalled
? 'AI Assistant must be installed to run AI benchmark' ? `${aiAssistantName} must be installed to run AI benchmark`
: undefined : undefined
} }
> >
@ -453,7 +454,8 @@ export default function BenchmarkPage(props: {
</div> </div>
{!aiInstalled && ( {!aiInstalled && (
<p className="text-sm text-desert-stone-dark"> <p className="text-sm text-desert-stone-dark">
<span className="text-amber-600">Note:</span> AI Assistant is not installed. <span className="text-amber-600">Note:</span> {aiAssistantName} is not
installed.
<Link <Link
href="/settings/apps" href="/settings/apps"
className="text-desert-green hover:underline ml-1" className="text-desert-green hover:underline ml-1"
@ -566,7 +568,7 @@ export default function BenchmarkPage(props: {
<Alert <Alert
type="info" type="info"
title="Partial Benchmark" title="Partial Benchmark"
message={`This ${latestResult.benchmark_type} benchmark cannot be shared with the community. Run a Full Benchmark with AI Assistant installed to share your results.`} message={`This ${latestResult.benchmark_type} benchmark cannot be shared with the community. Run a Full Benchmark with ${aiAssistantName} installed to share your results.`}
variant="bordered" variant="bordered"
/> />
)} )}

View File

@ -1,4 +1,4 @@
import { Head, router } from '@inertiajs/react' import { Head, router, usePage } from '@inertiajs/react'
import { useState } from 'react' import { useState } from 'react'
import StyledTable from '~/components/StyledTable' import StyledTable from '~/components/StyledTable'
import SettingsLayout from '~/layouts/SettingsLayout' import SettingsLayout from '~/layouts/SettingsLayout'
@ -24,9 +24,10 @@ export default function ModelsPage(props: {
models: { models: {
availableModels: NomadOllamaModel[] availableModels: NomadOllamaModel[]
installedModels: ModelResponse[] installedModels: ModelResponse[]
settings: { chatSuggestionsEnabled: boolean } settings: { chatSuggestionsEnabled: boolean; aiAssistantCustomName: string }
} }
}) { }) {
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
const { addNotification } = useNotifications() const { addNotification } = useNotifications()
const { openModal, closeAllModals } = useModals() const { openModal, closeAllModals } = useModals()
@ -34,6 +35,9 @@ export default function ModelsPage(props: {
const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState( const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState(
props.models.settings.chatSuggestionsEnabled props.models.settings.chatSuggestionsEnabled
) )
const [aiAssistantCustomName, setAiAssistantCustomName] = useState(
props.models.settings.aiAssistantCustomName
)
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [queryUI, setQueryUI] = useState('') const [queryUI, setQueryUI] = useState('')
@ -123,7 +127,7 @@ export default function ModelsPage(props: {
} }
const updateSettingMutation = useMutation({ const updateSettingMutation = useMutation({
mutationFn: async ({ key, value }: { key: string; value: boolean }) => { mutationFn: async ({ key, value }: { key: string; value: boolean | string }) => {
return await api.updateSetting(key, value) return await api.updateSetting(key, value)
}, },
onSuccess: () => { onSuccess: () => {
@ -143,18 +147,18 @@ export default function ModelsPage(props: {
return ( return (
<SettingsLayout> <SettingsLayout>
<Head title="AI Assistant Settings | Project N.O.M.A.D." /> <Head title={`${aiAssistantName} Settings | Project N.O.M.A.D.`} />
<div className="xl:pl-72 w-full"> <div className="xl:pl-72 w-full">
<main className="px-12 py-6"> <main className="px-12 py-6">
<h1 className="text-4xl font-semibold mb-4">AI Assistant</h1> <h1 className="text-4xl font-semibold mb-4">{aiAssistantName}</h1>
<p className="text-gray-500 mb-4"> <p className="text-gray-500 mb-4">
Easily manage the AI Assistant's settings and installed models. We recommend starting Easily manage the {aiAssistantName}'s settings and installed models. We recommend
with smaller models first to see how they perform on your system before moving on to starting with smaller models first to see how they perform on your system before moving
larger ones. on to larger ones.
</p> </p>
{!isInstalled && ( {!isInstalled && (
<Alert <Alert
title="AI Assistant's dependencies are not installed. Please install them to manage AI models." title={`${aiAssistantName}'s dependencies are not installed. Please install them to manage AI models.`}
type="warning" type="warning"
variant="solid" variant="solid"
className="!mt-6" className="!mt-6"
@ -173,6 +177,20 @@ export default function ModelsPage(props: {
label="Chat Suggestions" label="Chat Suggestions"
description="Display AI-generated conversation starters in the chat interface" description="Display AI-generated conversation starters in the chat interface"
/> />
<Input
name="aiAssistantCustomName"
label="Assistant Name"
helpText='Give your AI assistant a custom name that will be used in the chat interface and other areas of the application.'
placeholder="AI Assistant"
value={aiAssistantCustomName}
onChange={(e) => setAiAssistantCustomName(e.target.value)}
onBlur={() =>
updateSettingMutation.mutate({
key: 'ai.assistantCustomName',
value: aiAssistantCustomName,
})
}
/>
</div> </div>
</div> </div>
<ActiveModelDownloads withHeader /> <ActiveModelDownloads withHeader />

View File

@ -1,11 +1,12 @@
export const KV_STORE_SCHEMA = { export const KV_STORE_SCHEMA = {
'chat.suggestionsEnabled': 'boolean', 'chat.suggestionsEnabled': 'boolean',
'rag.docsEmbedded': 'boolean', 'rag.docsEmbedded': 'boolean',
'system.updateAvailable': 'boolean', 'system.updateAvailable': 'boolean',
'system.latestVersion': 'string', 'system.latestVersion': 'string',
'system.earlyAccess': 'boolean', 'system.earlyAccess': 'boolean',
'ui.hasVisitedEasySetup': 'boolean', 'ui.hasVisitedEasySetup': 'boolean',
'ai.assistantCustomName': 'string',
} as const } as const
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string