project-nomad/admin/inertia/pages/settings/models.tsx

250 lines
9.7 KiB
TypeScript

import { Head, router } from '@inertiajs/react'
import { useState } from 'react'
import StyledTable from '~/components/StyledTable'
import SettingsLayout from '~/layouts/SettingsLayout'
import { NomadOllamaModel } from '../../../types/ollama'
import StyledButton from '~/components/StyledButton'
import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
import Alert from '~/components/Alert'
import { useNotifications } from '~/context/NotificationContext'
import api from '~/lib/api'
import { useModals } from '~/context/ModalContext'
import StyledModal from '~/components/StyledModal'
import { ModelResponse } from 'ollama'
import { SERVICE_NAMES } from '../../../constants/service_names'
import Switch from '~/components/inputs/Switch'
import StyledSectionHeader from '~/components/StyledSectionHeader'
import { useMutation } from '@tanstack/react-query'
export default function ModelsPage(props: {
models: {
availableModels: NomadOllamaModel[]
installedModels: ModelResponse[]
settings: { chatSuggestionsEnabled: boolean }
}
}) {
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
const { addNotification } = useNotifications()
const { openModal, closeAllModals } = useModals()
const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState(
props.models.settings.chatSuggestionsEnabled
)
async function handleInstallModel(modelName: string) {
try {
const res = await api.downloadModel(modelName)
if (res.success) {
addNotification({
message: `Model download initiated for ${modelName}. It may take some time to complete.`,
type: 'success',
})
}
} catch (error) {
console.error('Error installing model:', error)
addNotification({
message: `There was an error installing the model: ${modelName}. Please try again.`,
type: 'error',
})
}
}
async function handleDeleteModel(modelName: string) {
try {
const res = await api.deleteModel(modelName)
if (res.success) {
addNotification({
message: `Model deleted: ${modelName}.`,
type: 'success',
})
}
closeAllModals()
router.reload()
} catch (error) {
console.error('Error deleting model:', error)
addNotification({
message: `There was an error deleting the model: ${modelName}. Please try again.`,
type: 'error',
})
}
}
async function confirmDeleteModel(model: string) {
openModal(
<StyledModal
title="Delete Model?"
onConfirm={() => {
handleDeleteModel(model)
}}
onCancel={closeAllModals}
open={true}
confirmText="Delete"
cancelText="Cancel"
confirmVariant="primary"
>
<p className="text-gray-700">
Are you sure you want to delete this model? You will need to download it again if you want
to use it in the future.
</p>
</StyledModal>,
'confirm-delete-model-modal'
)
}
const updateSettingMutation = useMutation({
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
return await api.updateSetting(key, value)
},
onSuccess: () => {
addNotification({
message: 'Setting updated successfully.',
type: 'success',
})
},
onError: (error) => {
console.error('Error updating setting:', error)
addNotification({
message: 'There was an error updating the setting. Please try again.',
type: 'error',
})
},
})
return (
<SettingsLayout>
<Head title="AI Assistant 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>
<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.
</p>
{!isInstalled && (
<Alert
title="AI Assistant's dependencies are not installed. Please install them to manage AI models."
type="warning"
variant="solid"
className="!mt-6"
/>
)}
<StyledSectionHeader title="Settings" className="mt-8 mb-4" />
<div className="bg-white rounded-lg border-2 border-gray-200 p-6">
<div className="space-y-4">
<Switch
checked={chatSuggestionsEnabled}
onChange={(newVal) => {
setChatSuggestionsEnabled(newVal)
updateSettingMutation.mutate({ key: 'chat.suggestionsEnabled', value: newVal })
}}
label="Chat Suggestions"
description="Display AI-generated conversation starters in the chat interface"
/>
</div>
</div>
<StyledSectionHeader title="Models" className="mt-12 mb-4" />
<StyledTable<NomadOllamaModel>
className="font-semibold"
rowLines={true}
columns={[
{
accessor: 'name',
title: 'Name',
render(record) {
return (
<div className="flex flex-col">
<p className="text-lg font-semibold">{record.name}</p>
<p className="text-sm text-gray-500">{record.description}</p>
</div>
)
},
},
{
accessor: 'estimated_pulls',
title: 'Estimated Pulls',
},
{
accessor: 'model_last_updated',
title: 'Last Updated',
},
]}
data={props.models.availableModels || []}
expandable={{
expandedRowRender: (record) => (
<div className="pl-14">
<div className="bg-white overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-white">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tag
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Input Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Context Size
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Model Size
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Action
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{record.tags.map((tag, tagIndex) => {
const isInstalled = props.models.installedModels.some(
(mod) => mod.name === tag.name
)
return (
<tr key={tagIndex} className="hover:bg-slate-50">
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm font-medium text-gray-900">
{tag.name}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm text-gray-600">{tag.input || 'N/A'}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm text-gray-600">
{tag.context || 'N/A'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm text-gray-600">{tag.size || 'N/A'}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<StyledButton
variant={isInstalled ? 'danger' : 'primary'}
onClick={() => {
if (!isInstalled) {
handleInstallModel(tag.name)
} else {
confirmDeleteModel(tag.name)
}
}}
icon={isInstalled ? 'IconTrash' : 'IconDownload'}
>
{isInstalled ? 'Delete' : 'Install'}
</StyledButton>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
),
}}
/>
</main>
</div>
</SettingsLayout>
)
}