mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-03 07:19:27 +02:00
feat(AI Assistant): custom name option for AI Assistant
This commit is contained in:
parent
b806cefe3a
commit
96beab7e69
|
|
@ -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 ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
}))
|
}))
|
||||||
|
|
@ -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'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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[]>([])
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 { 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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user