mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 11:39:26 +01:00
feat(AI Assistant): custom name option for AI Assistant
This commit is contained in:
parent
f4d78c8892
commit
f10ca7058e
|
|
@ -54,12 +54,14 @@ export default class SettingsController {
|
|||
const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false, query: null, limit: 15 });
|
||||
const installedModels = await this.ollamaService.getModels();
|
||||
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
|
||||
const aiAssistantCustomName = await KVStore.getValue('ai.assistantCustomName')
|
||||
return inertia.render('settings/models', {
|
||||
models: {
|
||||
availableModels: availableModels?.models || [],
|
||||
installedModels: installedModels || [],
|
||||
settings: {
|
||||
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false
|
||||
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false,
|
||||
aiAssistantCustomName: aiAssistantCustomName ?? '',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,4 +50,15 @@ export default class KVStore extends BaseModel {
|
|||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ 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'
|
||||
import { KV_STORE_SCHEMA, KVStoreKey } from '../../types/kv_store.js'
|
||||
|
||||
|
||||
@inject()
|
||||
|
|
@ -388,7 +388,11 @@ export class SystemService {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ import { SETTINGS_KEYS } from "../../constants/kv_store.js";
|
|||
|
||||
export const updateSettingSchema = vine.compile(vine.object({
|
||||
key: vine.enum(SETTINGS_KEYS),
|
||||
value: vine.any(),
|
||||
value: vine.any().optional(),
|
||||
}))
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import KVStore from '#models/kv_store'
|
||||
import { SystemService } from '#services/system_service'
|
||||
import { defineConfig } from '@adonisjs/inertia'
|
||||
import type { InferSharedProps } from '@adonisjs/inertia/types'
|
||||
|
|
@ -14,6 +15,10 @@ const inertiaConfig = defineConfig({
|
|||
sharedData: {
|
||||
appVersion: () => SystemService.getAppVersion(),
|
||||
environment: process.env.NODE_ENV || 'production',
|
||||
aiAssistantName: async () => {
|
||||
const customName = await KVStore.getValue('ai.assistantCustomName')
|
||||
return (customName && customName.trim()) ? customName : 'AI Assistant'
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
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'];
|
||||
|
|
@ -9,6 +9,7 @@ import StyledModal from '../StyledModal'
|
|||
import api from '~/lib/api'
|
||||
import { DEFAULT_QUERY_REWRITE_MODEL } from '../../../constants/ollama'
|
||||
import { useNotifications } from '~/context/NotificationContext'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
|
||||
interface ChatInterfaceProps {
|
||||
messages: ChatMessage[]
|
||||
|
|
@ -29,6 +30,7 @@ export default function ChatInterface({
|
|||
chatSuggestionsLoading = false,
|
||||
rewriteModelAvailable = false
|
||||
}: ChatInterfaceProps) {
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
const { addNotification } = useNotifications()
|
||||
const [input, setInput] = useState('')
|
||||
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false)
|
||||
|
|
@ -160,7 +162,7 @@ export default function ChatInterface({
|
|||
value={input}
|
||||
onChange={handleInput}
|
||||
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"
|
||||
rows={1}
|
||||
disabled={isLoading}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from '~/lib/classNames'
|
||||
import StyledButton from '../StyledButton'
|
||||
import { router } from '@inertiajs/react'
|
||||
import { router, usePage } from '@inertiajs/react'
|
||||
import { ChatSession } from '../../../types/chat'
|
||||
import { IconMessage } from '@tabler/icons-react'
|
||||
import { useState } from 'react'
|
||||
|
|
@ -23,6 +23,7 @@ export default function ChatSidebar({
|
|||
onClearHistory,
|
||||
isInModal = false,
|
||||
}: ChatSidebarProps) {
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
const [isKnowledgeBaseModalOpen, setIsKnowledgeBaseModalOpen] = useState(
|
||||
() => new URLSearchParams(window.location.search).get('knowledge_base') === 'true'
|
||||
)
|
||||
|
|
@ -139,7 +140,7 @@ export default function ChatSidebar({
|
|||
)}
|
||||
</div>
|
||||
{isKnowledgeBaseModalOpen && (
|
||||
<KnowledgeBaseModal onClose={handleCloseKnowledgeBase} />
|
||||
<KnowledgeBaseModal aiAssistantName={aiAssistantName} onClose={handleCloseKnowledgeBase} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ import { useModals } from '~/context/ModalContext'
|
|||
import StyledModal from '../StyledModal'
|
||||
|
||||
interface KnowledgeBaseModalProps {
|
||||
aiAssistantName?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function KnowledgeBaseModal({ onClose }: KnowledgeBaseModalProps) {
|
||||
export default function KnowledgeBaseModal({ aiAssistantName = "AI Assistant", onClose }: KnowledgeBaseModalProps) {
|
||||
const { addNotification } = useNotifications()
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const fileUploaderRef = useRef<React.ComponentRef<typeof FileUploader>>(null)
|
||||
|
|
@ -140,12 +141,12 @@ export default function KnowledgeBaseModal({ onClose }: KnowledgeBaseModalProps)
|
|||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-desert-stone-dark">
|
||||
AI Assistant Knowledge Base Integration
|
||||
{aiAssistantName} 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,
|
||||
the content, making it directly accessible to {aiAssistantName}. This allows{' '}
|
||||
{aiAssistantName} to reference your specific documents during conversations,
|
||||
providing more accurate and personalized responses based on your uploaded
|
||||
data.
|
||||
</p>
|
||||
|
|
@ -177,8 +178,7 @@ export default function KnowledgeBaseModal({ onClose }: KnowledgeBaseModalProps)
|
|||
</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.
|
||||
Information Library (if installed), making it instantly available to {aiAssistantName} without any extra steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { InputHTMLAttributes } from "react";
|
|||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
name: string;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
inputClassName?: string;
|
||||
|
|
@ -17,6 +18,7 @@ const Input: React.FC<InputProps> = ({
|
|||
className,
|
||||
label,
|
||||
name,
|
||||
helpText,
|
||||
labelClassName,
|
||||
inputClassName,
|
||||
containerClassName,
|
||||
|
|
@ -33,6 +35,7 @@ const Input: React.FC<InputProps> = ({
|
|||
>
|
||||
{label}{required ? "*" : ""}
|
||||
</label>
|
||||
{helpText && <p className="mt-1 text-sm text-gray-500">{helpText}</p>}
|
||||
<div className={classNames("mt-1.5", containerClassName)}>
|
||||
<div className="relative">
|
||||
{leftIcon && (
|
||||
|
|
|
|||
|
|
@ -10,34 +10,37 @@ import {
|
|||
IconWand,
|
||||
IconZoom
|
||||
} from '@tabler/icons-react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import StyledSidebar from '~/components/StyledSidebar'
|
||||
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 }) {
|
||||
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 (
|
||||
<div className="min-h-screen flex flex-row bg-stone-50/90">
|
||||
<StyledSidebar title="Settings" items={navigation} />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Head } from '@inertiajs/react'
|
||||
import { Head, usePage } from '@inertiajs/react'
|
||||
import ChatComponent from '~/components/chat'
|
||||
|
||||
export default function Chat(props: { settings: { chatSuggestionsEnabled: boolean } }) {
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<Head title="AI Assistant" />
|
||||
<Head title={aiAssistantName} />
|
||||
<ChatComponent enabled={true} suggestionsEnabled={props.settings.chatSuggestionsEnabled} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 { useEffect, useState, useMemo } from 'react'
|
||||
import AppLayout from '~/layouts/AppLayout'
|
||||
|
|
@ -32,51 +32,53 @@ interface Capability {
|
|||
icon: string
|
||||
}
|
||||
|
||||
const CORE_CAPABILITIES: Capability[] = [
|
||||
{
|
||||
id: 'information',
|
||||
name: 'Information Library',
|
||||
technicalName: 'Kiwix',
|
||||
description:
|
||||
'Offline access to Wikipedia, medical references, how-to guides, and encyclopedias',
|
||||
features: [
|
||||
'Complete Wikipedia offline',
|
||||
'Medical references and first aid guides',
|
||||
'WikiHow articles and tutorials',
|
||||
'Project Gutenberg books and literature',
|
||||
],
|
||||
services: [SERVICE_NAMES.KIWIX],
|
||||
icon: 'IconBooks',
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
name: 'Education Platform',
|
||||
technicalName: 'Kolibri',
|
||||
description: 'Interactive learning platform with video courses and exercises',
|
||||
features: [
|
||||
'Khan Academy math and science courses',
|
||||
'K-12 curriculum content',
|
||||
'Interactive exercises and quizzes',
|
||||
'Progress tracking for learners',
|
||||
],
|
||||
services: [SERVICE_NAMES.KOLIBRI],
|
||||
icon: 'IconSchool',
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
name: 'AI Assistant',
|
||||
technicalName: 'Ollama',
|
||||
description: 'Local AI chat that runs entirely on your hardware - no internet required',
|
||||
features: [
|
||||
'Private conversations that never leave your device',
|
||||
'No internet connection needed after setup',
|
||||
'Ask questions, get help with writing, brainstorm ideas',
|
||||
'Runs on your own hardware with local AI models',
|
||||
],
|
||||
services: [SERVICE_NAMES.OLLAMA],
|
||||
icon: 'IconRobot',
|
||||
},
|
||||
]
|
||||
function buildCoreCapabilities(aiAssistantName: string): Capability[] {
|
||||
return [
|
||||
{
|
||||
id: 'information',
|
||||
name: 'Information Library',
|
||||
technicalName: 'Kiwix',
|
||||
description:
|
||||
'Offline access to Wikipedia, medical references, how-to guides, and encyclopedias',
|
||||
features: [
|
||||
'Complete Wikipedia offline',
|
||||
'Medical references and first aid guides',
|
||||
'WikiHow articles and tutorials',
|
||||
'Project Gutenberg books and literature',
|
||||
],
|
||||
services: [SERVICE_NAMES.KIWIX],
|
||||
icon: 'IconBooks',
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
name: 'Education Platform',
|
||||
technicalName: 'Kolibri',
|
||||
description: 'Interactive learning platform with video courses and exercises',
|
||||
features: [
|
||||
'Khan Academy math and science courses',
|
||||
'K-12 curriculum content',
|
||||
'Interactive exercises and quizzes',
|
||||
'Progress tracking for learners',
|
||||
],
|
||||
services: [SERVICE_NAMES.KOLIBRI],
|
||||
icon: 'IconSchool',
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
name: aiAssistantName,
|
||||
technicalName: 'Ollama',
|
||||
description: 'Local AI chat that runs entirely on your hardware - no internet required',
|
||||
features: [
|
||||
'Private conversations that never leave your device',
|
||||
'No internet connection needed after setup',
|
||||
'Ask questions, get help with writing, brainstorm ideas',
|
||||
'Runs on your own hardware with local AI models',
|
||||
],
|
||||
services: [SERVICE_NAMES.OLLAMA],
|
||||
icon: 'IconRobot',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const ADDITIONAL_TOOLS: Capability[] = [
|
||||
{
|
||||
|
|
@ -110,6 +112,9 @@ const CURATED_CATEGORIES_KEY = 'curated-categories'
|
|||
const WIKIPEDIA_STATE_KEY = 'wikipedia-state'
|
||||
|
||||
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 [selectedServices, setSelectedServices] = useState<string[]>([])
|
||||
const [selectedMapCollections, setSelectedMapCollections] = useState<string[]>([])
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
IconSettings,
|
||||
IconWifiOff,
|
||||
} from '@tabler/icons-react'
|
||||
import { Head } from '@inertiajs/react'
|
||||
import { Head, usePage } from '@inertiajs/react'
|
||||
import AppLayout from '~/layouts/AppLayout'
|
||||
import { getServiceLink } from '~/lib/navigation'
|
||||
import { ServiceSlim } from '../../types/services'
|
||||
|
|
@ -14,6 +14,7 @@ import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
|
|||
import { useUpdateAvailable } from '~/hooks/useUpdateAvailable'
|
||||
import { useSystemSetting } from '~/hooks/useSystemSetting'
|
||||
import Alert from '~/components/Alert'
|
||||
import { SERVICE_NAMES } from '../../constants/service_names'
|
||||
|
||||
// Maps is a Core Capability (display_order: 4)
|
||||
const MAPS_ITEM = {
|
||||
|
|
@ -90,6 +91,7 @@ export default function Home(props: {
|
|||
}) {
|
||||
const items: DashboardItem[] = []
|
||||
const updateInfo = useUpdateAvailable();
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
|
||||
// Check if user has visited Easy Setup
|
||||
const { data: easySetupVisited } = useSystemSetting({
|
||||
|
|
@ -102,7 +104,8 @@ export default function Home(props: {
|
|||
.filter((service) => service.installed && service.ui_location)
|
||||
.forEach((service) => {
|
||||
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) : '#',
|
||||
target: '_blank',
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Head, Link } from '@inertiajs/react'
|
||||
import { Head, Link, usePage } from '@inertiajs/react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
|
|
@ -34,6 +34,7 @@ export default function BenchmarkPage(props: {
|
|||
currentBenchmarkId: string | null
|
||||
}
|
||||
}) {
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
const { subscribe } = useTransmit()
|
||||
const queryClient = useQueryClient()
|
||||
const aiInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
|
||||
|
|
@ -403,8 +404,8 @@ export default function BenchmarkPage(props: {
|
|||
{showAIRequiredAlert && (
|
||||
<Alert
|
||||
type="warning"
|
||||
title="AI Assistant Required"
|
||||
message="Full benchmark requires AI Assistant to be installed. Install it to measure your complete NOMAD capability and share results with the community."
|
||||
title={`${aiAssistantName} Required`}
|
||||
message={`Full benchmark requires ${aiAssistantName} to be installed. Install it to measure your complete NOMAD capability and share results with the community.`}
|
||||
variant="bordered"
|
||||
dismissible
|
||||
onDismiss={() => setShowAIRequiredAlert(false)}
|
||||
|
|
@ -413,7 +414,7 @@ export default function BenchmarkPage(props: {
|
|||
href="/settings/apps"
|
||||
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>
|
||||
</Alert>
|
||||
)}
|
||||
|
|
@ -444,7 +445,7 @@ export default function BenchmarkPage(props: {
|
|||
icon="IconWand"
|
||||
title={
|
||||
!aiInstalled
|
||||
? 'AI Assistant must be installed to run AI benchmark'
|
||||
? `${aiAssistantName} must be installed to run AI benchmark`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
|
@ -453,7 +454,8 @@ export default function BenchmarkPage(props: {
|
|||
</div>
|
||||
{!aiInstalled && (
|
||||
<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
|
||||
href="/settings/apps"
|
||||
className="text-desert-green hover:underline ml-1"
|
||||
|
|
@ -566,7 +568,7 @@ export default function BenchmarkPage(props: {
|
|||
<Alert
|
||||
type="info"
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Head, router } from '@inertiajs/react'
|
||||
import { Head, router, usePage } from '@inertiajs/react'
|
||||
import { useState } from 'react'
|
||||
import StyledTable from '~/components/StyledTable'
|
||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||
|
|
@ -24,9 +24,10 @@ export default function ModelsPage(props: {
|
|||
models: {
|
||||
availableModels: NomadOllamaModel[]
|
||||
installedModels: ModelResponse[]
|
||||
settings: { chatSuggestionsEnabled: boolean }
|
||||
settings: { chatSuggestionsEnabled: boolean; aiAssistantCustomName: string }
|
||||
}
|
||||
}) {
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
|
||||
const { addNotification } = useNotifications()
|
||||
const { openModal, closeAllModals } = useModals()
|
||||
|
|
@ -34,6 +35,9 @@ export default function ModelsPage(props: {
|
|||
const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState(
|
||||
props.models.settings.chatSuggestionsEnabled
|
||||
)
|
||||
const [aiAssistantCustomName, setAiAssistantCustomName] = useState(
|
||||
props.models.settings.aiAssistantCustomName
|
||||
)
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const [queryUI, setQueryUI] = useState('')
|
||||
|
|
@ -123,7 +127,7 @@ export default function ModelsPage(props: {
|
|||
}
|
||||
|
||||
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)
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -143,18 +147,18 @@ export default function ModelsPage(props: {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
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.
|
||||
Easily manage the {aiAssistantName}'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.
|
||||
</p>
|
||||
{!isInstalled && (
|
||||
<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"
|
||||
variant="solid"
|
||||
className="!mt-6"
|
||||
|
|
@ -173,6 +177,20 @@ export default function ModelsPage(props: {
|
|||
label="Chat Suggestions"
|
||||
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>
|
||||
<ActiveModelDownloads withHeader />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
|
||||
export const KV_STORE_SCHEMA = {
|
||||
'chat.suggestionsEnabled': 'boolean',
|
||||
'rag.docsEmbedded': 'boolean',
|
||||
'system.updateAvailable': 'boolean',
|
||||
'system.latestVersion': 'string',
|
||||
'system.earlyAccess': 'boolean',
|
||||
'ui.hasVisitedEasySetup': 'boolean',
|
||||
'chat.suggestionsEnabled': 'boolean',
|
||||
'rag.docsEmbedded': 'boolean',
|
||||
'system.updateAvailable': 'boolean',
|
||||
'system.latestVersion': 'string',
|
||||
'system.earlyAccess': 'boolean',
|
||||
'ui.hasVisitedEasySetup': 'boolean',
|
||||
'ai.assistantCustomName': 'string',
|
||||
} as const
|
||||
|
||||
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user