mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
- Rename 'Models Manager' to 'AI Model Manager' - Rename 'ZIM Manager' to 'Content Manager' - Rename 'ZIM Remote Explorer' to 'Content Explorer' - Rename 'Curated ZIM Collections' to 'Curated Content Collections' - Add tiered category collections (Essential/Standard/Comprehensive) to Content Explorer, matching the Easy Setup Wizard Step 3 for consistency - Reorganize Settings sidebar alphabetically Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
7.9 KiB
TypeScript
201 lines
7.9 KiB
TypeScript
import { Head, router } from '@inertiajs/react'
|
|
import StyledTable from '~/components/StyledTable'
|
|
import SettingsLayout from '~/layouts/SettingsLayout'
|
|
import { NomadOllamaModel, OllamaModelListing } 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'
|
|
|
|
export default function ModelsPage(props: {
|
|
models: { availableModels: NomadOllamaModel[]; installedModels: OllamaModelListing[] }
|
|
}) {
|
|
const { isInstalled } = useServiceInstalledStatus('nomad_openwebui')
|
|
const { addNotification } = useNotifications()
|
|
const { openModal, closeAllModals } = useModals()
|
|
|
|
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'
|
|
)
|
|
}
|
|
|
|
return (
|
|
<SettingsLayout>
|
|
<Head title="AI Model Manager | 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 Model Manager</h1>
|
|
<p className="text-gray-500 mb-4">
|
|
Easily manage the AI models available for Open WebUI. 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="The Open WebUI service is not installed. Please install it to manage AI models."
|
|
type="warning"
|
|
variant="solid"
|
|
className="!mt-6"
|
|
/>
|
|
)}
|
|
<StyledTable<NomadOllamaModel>
|
|
className="font-semibold mt-8"
|
|
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 ? 'TrashIcon' : 'ArrowDownTrayIcon'}
|
|
>
|
|
{isInstalled ? 'Delete' : 'Install'}
|
|
</StyledButton>
|
|
</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
),
|
|
}}
|
|
/>
|
|
</main>
|
|
</div>
|
|
</SettingsLayout>
|
|
)
|
|
}
|