diff --git a/admin/app/services/benchmark_service.ts b/admin/app/services/benchmark_service.ts index 7eeb0b6..bdb5f31 100644 --- a/admin/app/services/benchmark_service.ts +++ b/admin/app/services/benchmark_service.ts @@ -24,6 +24,7 @@ import type { } from '../../types/benchmark.js' import { randomUUID, createHmac } from 'node:crypto' import { DockerService } from './docker_service.js' +import { SERVICE_NAMES } from '../../constants/service_names.js' // HMAC secret for signing submissions to the benchmark repository // This provides basic protection against casual API abuse. @@ -421,7 +422,7 @@ export class BenchmarkService { this._updateStatus('running_ai', 'Running AI benchmark...') - const ollamaAPIURL = await this.dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME) + const ollamaAPIURL = await this.dockerService.getServiceURL(SERVICE_NAMES.OLLAMA) if (!ollamaAPIURL) { throw new Error('AI Assistant service location could not be determined. Ensure AI Assistant is installed and running.') } diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 0d01500..5b352ab 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -6,18 +6,12 @@ import transmit from '@adonisjs/transmit/services/main' import { doResumableDownloadWithRetry } from '../utils/downloads.js' import { join } from 'path' import { ZIM_STORAGE_PATH } from '../utils/fs.js' +import { SERVICE_NAMES } from '../../constants/service_names.js' @inject() export class DockerService { public docker: Docker private activeInstallations: Set = new Set() - public static KIWIX_SERVICE_NAME = 'nomad_kiwix_serve' - public static OLLAMA_SERVICE_NAME = 'nomad_ollama' - public static QDRANT_SERVICE_NAME = 'nomad_qdrant' - public static CYBERCHEF_SERVICE_NAME = 'nomad_cyberchef' - public static FLATNOTES_SERVICE_NAME = 'nomad_flatnotes' - public static KOLIBRI_SERVICE_NAME = 'nomad_kolibri' - public static BENCHMARK_SERVICE_NAME = 'nomad_benchmark' public static NOMAD_NETWORK = 'project-nomad_default' constructor() { @@ -441,7 +435,7 @@ export class DockerService { await new Promise((res) => this.docker.modem.followProgress(pullStream, res)) } - if (service.service_name === DockerService.KIWIX_SERVICE_NAME) { + if (service.service_name === SERVICE_NAMES.KIWIX) { await this._runPreinstallActions__KiwixServe() this._broadcast( service.service_name, @@ -556,12 +550,12 @@ export class DockerService { logger.info(`[DockerService] Kiwix Serve pre-install: Downloading ZIM file to ${filepath}`) this._broadcast( - DockerService.KIWIX_SERVICE_NAME, + SERVICE_NAMES.KIWIX, 'preinstall', `Running pre-install actions for Kiwix Serve...` ) this._broadcast( - DockerService.KIWIX_SERVICE_NAME, + SERVICE_NAMES.KIWIX, 'preinstall', `Downloading Wikipedia ZIM file from ${WIKIPEDIA_ZIM_URL}. This may take some time...` ) @@ -579,13 +573,13 @@ export class DockerService { }) this._broadcast( - DockerService.KIWIX_SERVICE_NAME, + SERVICE_NAMES.KIWIX, 'preinstall', `Downloaded Wikipedia ZIM file to ${filepath}` ) } catch (error) { this._broadcast( - DockerService.KIWIX_SERVICE_NAME, + SERVICE_NAMES.KIWIX, 'preinstall-error', `Failed to download Wikipedia ZIM file: ${error.message}` ) diff --git a/admin/app/services/ollama_service.ts b/admin/app/services/ollama_service.ts index 3e77a54..080d724 100644 --- a/admin/app/services/ollama_service.ts +++ b/admin/app/services/ollama_service.ts @@ -1,6 +1,5 @@ import { inject } from '@adonisjs/core' import { ChatRequest, Ollama } from 'ollama' -import { DockerService } from './docker_service.js' import { NomadOllamaModel } from '../../types/ollama.js' import { FALLBACK_RECOMMENDED_OLLAMA_MODELS } from '../../constants/ollama.js' import fs from 'node:fs/promises' @@ -9,6 +8,7 @@ import logger from '@adonisjs/core/services/logger' import axios from 'axios' import { DownloadModelJob } from '#jobs/download_model_job' import { PassThrough } from 'node:stream' +import { SERVICE_NAMES } from '../../constants/service_names.js' const NOMAD_MODELS_API_BASE_URL = 'https://api.projectnomad.us/api/v1/ollama/models' const MODELS_CACHE_FILE = path.join(process.cwd(), 'storage', 'ollama-models-cache.json') @@ -25,7 +25,7 @@ export class OllamaService { if (!this.ollamaInitPromise) { this.ollamaInitPromise = (async () => { const dockerService = new (await import('./docker_service.js')).DockerService() - const qdrantUrl = await dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME) + const qdrantUrl = await dockerService.getServiceURL(SERVICE_NAMES.OLLAMA) if (!qdrantUrl) { throw new Error('Ollama service is not installed or running.') } @@ -56,7 +56,7 @@ export class OllamaService { return new Promise(async (resolve) => { try { const dockerService = new (await import('./docker_service.js')).DockerService() - const container = dockerService.docker.getContainer(DockerService.OLLAMA_SERVICE_NAME) + const container = dockerService.docker.getContainer(SERVICE_NAMES.OLLAMA) if (!container) { logger.warn('[OllamaService] Ollama container is not running. Cannot download model.') resolve({ @@ -242,7 +242,7 @@ export class OllamaService { const dockerService = new (await import('./docker_service.js')).DockerService() - const ollamAPIURL = await dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME) + const ollamAPIURL = await dockerService.getServiceURL(SERVICE_NAMES.OLLAMA) if (!ollamAPIURL) { logger.warn('[OllamaService] Ollama service is not running. Cannot download model.') return { diff --git a/admin/app/services/rag_service.ts b/admin/app/services/rag_service.ts index b4415be..7573fcd 100644 --- a/admin/app/services/rag_service.ts +++ b/admin/app/services/rag_service.ts @@ -9,6 +9,7 @@ import { PDFParse } from 'pdf-parse' import { createWorker } from 'tesseract.js' import { fromBuffer } from 'pdf2pic' import { OllamaService } from './ollama_service.js' +import { SERVICE_NAMES } from '../../constants/service_names.js' @inject() export class RagService { @@ -26,7 +27,7 @@ export class RagService { private async _initializeQdrantClient() { if (!this.qdrantInitPromise) { this.qdrantInitPromise = (async () => { - const qdrantUrl = await this.dockerService.getServiceURL(DockerService.QDRANT_SERVICE_NAME) + const qdrantUrl = await this.dockerService.getServiceURL(SERVICE_NAMES.QDRANT) if (!qdrantUrl) { throw new Error('Qdrant service is not installed or running.') } diff --git a/admin/app/services/zim_service.ts b/admin/app/services/zim_service.ts index a0a7bcb..a00a389 100644 --- a/admin/app/services/zim_service.ts +++ b/admin/app/services/zim_service.ts @@ -26,6 +26,7 @@ import InstalledTier from '#models/installed_tier' import WikipediaSelection from '#models/wikipedia_selection' import { RunDownloadJob } from '#jobs/run_download_job' import { DownloadCollectionOperation, DownloadRemoteSuccessCallback } from '../../types/files.js' +import { SERVICE_NAMES } from '../../constants/service_names.js' const ZIM_MIME_TYPES = ['application/x-zim', 'application/x-openzim', 'application/octet-stream'] const CATEGORIES_URL = 'https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/collections/kiwix-categories.json' @@ -243,7 +244,7 @@ export class ZimService implements IZimService { // Restart KIWIX container to pick up new ZIM file if (restart) { await this.dockerService - .affectContainer(DockerService.KIWIX_SERVICE_NAME, 'restart') + .affectContainer(SERVICE_NAMES.KIWIX, 'restart') .catch((error) => { logger.error(`[ZimService] Failed to restart KIWIX container:`, error) // Don't stop the download completion, just log the error. }) @@ -434,7 +435,7 @@ export class ZimService implements IZimService { // Restart Kiwix to reflect the change await this.dockerService - .affectContainer(DockerService.KIWIX_SERVICE_NAME, 'restart') + .affectContainer(SERVICE_NAMES.KIWIX, 'restart') .catch((error) => { logger.error(`[ZimService] Failed to restart Kiwix after Wikipedia removal:`, error) }) diff --git a/admin/constants/service_names.ts b/admin/constants/service_names.ts new file mode 100644 index 0000000..8009222 --- /dev/null +++ b/admin/constants/service_names.ts @@ -0,0 +1,8 @@ +export const SERVICE_NAMES = { + KIWIX: 'nomad_kiwix_server', + OLLAMA: 'nomad_ollama', + QDRANT: 'nomad_qdrant', + CYBERCHEF: 'nomad_cyberchef', + FLATNOTES: 'nomad_flatnotes', + KOLIBRI: 'nomad_kolibri', +} diff --git a/admin/database/migrations/1769300000002_update_services_friendly_names.ts b/admin/database/migrations/1769300000002_update_services_friendly_names.ts index 00ebb06..89407ee 100644 --- a/admin/database/migrations/1769300000002_update_services_friendly_names.ts +++ b/admin/database/migrations/1769300000002_update_services_friendly_names.ts @@ -27,6 +27,7 @@ export default class extends BaseSchema { UPDATE services SET friendly_name = 'AI Assistant', powered_by = 'Ollama', + ui_location = '/chat', display_order = 3, description = 'Local AI chat that runs entirely on your hardware - no internet required' WHERE service_name = 'nomad_ollama' diff --git a/admin/database/seeders/service_seeder.ts b/admin/database/seeders/service_seeder.ts index c2e6924..3a2d068 100644 --- a/admin/database/seeders/service_seeder.ts +++ b/admin/database/seeders/service_seeder.ts @@ -1,8 +1,8 @@ import Service from '#models/service' -import { DockerService } from '#services/docker_service' import { BaseSeeder } from '@adonisjs/lucid/seeders' import { ModelAttributes } from '@adonisjs/lucid/types/model' import env from '#start/env' +import { SERVICE_NAMES } from '../../constants/service_names.js' export default class ServiceSeeder extends BaseSeeder { // Use environment variable with fallback to production default @@ -15,7 +15,7 @@ export default class ServiceSeeder extends BaseSeeder { 'created_at' | 'updated_at' | 'metadata' | 'id' >[] = [ { - service_name: DockerService.KIWIX_SERVICE_NAME, + service_name: SERVICE_NAMES.KIWIX, friendly_name: 'Information Library', powered_by: 'Kiwix', display_order: 1, @@ -39,7 +39,7 @@ export default class ServiceSeeder extends BaseSeeder { depends_on: null, }, { - service_name: DockerService.QDRANT_SERVICE_NAME, + service_name: SERVICE_NAMES.QDRANT, friendly_name: 'Qdrant Vector Database', powered_by: null, display_order: 100, // Dependency service, not shown directly @@ -62,7 +62,7 @@ export default class ServiceSeeder extends BaseSeeder { depends_on: null, }, { - service_name: DockerService.OLLAMA_SERVICE_NAME, + service_name: SERVICE_NAMES.OLLAMA, friendly_name: 'AI Assistant', powered_by: 'Ollama', display_order: 3, @@ -78,14 +78,14 @@ export default class ServiceSeeder extends BaseSeeder { }, ExposedPorts: { '11434/tcp': {} }, }), - ui_location: null, + ui_location: '/chat', installed: false, installation_status: 'idle', is_dependency_service: false, - depends_on: DockerService.QDRANT_SERVICE_NAME, + depends_on: SERVICE_NAMES.QDRANT, }, { - service_name: DockerService.CYBERCHEF_SERVICE_NAME, + service_name: SERVICE_NAMES.CYBERCHEF, friendly_name: 'Data Tools', powered_by: 'CyberChef', display_order: 11, @@ -107,7 +107,7 @@ export default class ServiceSeeder extends BaseSeeder { depends_on: null, }, { - service_name: DockerService.FLATNOTES_SERVICE_NAME, + service_name: SERVICE_NAMES.FLATNOTES, friendly_name: 'Notes', powered_by: 'FlatNotes', display_order: 10, @@ -131,7 +131,7 @@ export default class ServiceSeeder extends BaseSeeder { depends_on: null, }, { - service_name: DockerService.KOLIBRI_SERVICE_NAME, + service_name: SERVICE_NAMES.KOLIBRI, friendly_name: 'Education Platform', powered_by: 'Kolibri', display_order: 2, diff --git a/admin/inertia/lib/navigation.ts b/admin/inertia/lib/navigation.ts index 57a77dd..49040ef 100644 --- a/admin/inertia/lib/navigation.ts +++ b/admin/inertia/lib/navigation.ts @@ -16,6 +16,12 @@ export function getServiceLink(ui_location: string): string { // If it's a port number, return a link to the service on that port return `http://${window.location.hostname}:${parsedPort}`; } - // Otherwise, treat it as a path + + const pathPattern = /^\/.+/; + if (pathPattern.test(ui_location)) { + // If it starts with a slash, treat it as a full path + return ui_location; + } + return `/${ui_location}`; } \ No newline at end of file diff --git a/admin/inertia/pages/easy-setup/index.tsx b/admin/inertia/pages/easy-setup/index.tsx index c6d435a..26cbdb6 100644 --- a/admin/inertia/pages/easy-setup/index.tsx +++ b/admin/inertia/pages/easy-setup/index.tsx @@ -18,6 +18,7 @@ import useInternetStatus from '~/hooks/useInternetStatus' import { useSystemInfo } from '~/hooks/useSystemInfo' import classNames from 'classnames' import { CuratedCategory, CategoryTier, CategoryResource } from '../../../types/downloads' +import { SERVICE_NAMES } from '../../../constants/service_names' // Capability definitions - maps user-friendly categories to services interface Capability { @@ -43,7 +44,7 @@ const CORE_CAPABILITIES: Capability[] = [ 'WikiHow articles and tutorials', 'Project Gutenberg books and literature', ], - services: ['nomad_kiwix_serve'], + services: [SERVICE_NAMES.KIWIX], icon: 'IconBooks', }, { @@ -57,7 +58,7 @@ const CORE_CAPABILITIES: Capability[] = [ 'Interactive exercises and quizzes', 'Progress tracking for learners', ], - services: ['nomad_kolibri'], + services: [SERVICE_NAMES.KOLIBRI], icon: 'IconSchool', }, { @@ -71,7 +72,7 @@ const CORE_CAPABILITIES: Capability[] = [ 'Ask questions, get help with writing, brainstorm ideas', 'Runs on your own hardware with local AI models', ], - services: ['nomad_ollama'], + services: [SERVICE_NAMES.OLLAMA], icon: 'IconRobot', }, ] @@ -83,7 +84,7 @@ const ADDITIONAL_TOOLS: Capability[] = [ technicalName: 'FlatNotes', description: 'Simple note-taking app with local storage', features: ['Markdown support', 'All notes stored locally', 'No account required'], - services: ['nomad_flatnotes'], + services: [SERVICE_NAMES.FLATNOTES], icon: 'IconNotes', }, { @@ -96,7 +97,7 @@ const ADDITIONAL_TOOLS: Capability[] = [ 'Encryption and hashing tools', 'Data format conversion', ], - services: ['nomad_cyberchef'], + services: [SERVICE_NAMES.CYBERCHEF], icon: 'IconChefHat', }, ] @@ -804,10 +805,10 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim const renderStep3 = () => { // Check if AI or Information capabilities are selected OR already installed - const isAiSelected = selectedServices.includes('nomad_ollama') || - installedServices.some((s) => s.service_name === 'nomad_ollama') - const isInformationSelected = selectedServices.includes('nomad_kiwix') || - installedServices.some((s) => s.service_name === 'nomad_kiwix') + const isAiSelected = selectedServices.includes(SERVICE_NAMES.OLLAMA) || + installedServices.some((s) => s.service_name === SERVICE_NAMES.OLLAMA) + const isInformationSelected = selectedServices.includes(SERVICE_NAMES.KIWIX) || + installedServices.some((s) => s.service_name === SERVICE_NAMES.KIWIX) return (
diff --git a/admin/inertia/pages/home.tsx b/admin/inertia/pages/home.tsx index 15d59f6..e2ba242 100644 --- a/admin/inertia/pages/home.tsx +++ b/admin/inertia/pages/home.tsx @@ -12,6 +12,7 @@ import AppLayout from '~/layouts/AppLayout' import { getServiceLink } from '~/lib/navigation' import { ServiceSlim } from '../../types/services' import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon' +import { SERVICE_NAMES } from '../../constants/service_names' // Maps is a Core Capability (display_order: 4) const MAPS_ITEM = { @@ -126,7 +127,7 @@ export default function Home(props: { // Add system items items.push(...SYSTEM_ITEMS) - if (props.system.services.find((s) => s.service_name === 'nomad_ollama' && s.installed)) { + if (props.system.services.find((s) => s.service_name === SERVICE_NAMES.OLLAMA && s.installed)) { items.push(KNOWLEDGE_BASE_ITEM) } diff --git a/admin/inertia/pages/settings/benchmark.tsx b/admin/inertia/pages/settings/benchmark.tsx index 318555b..6d188a8 100644 --- a/admin/inertia/pages/settings/benchmark.tsx +++ b/admin/inertia/pages/settings/benchmark.tsx @@ -22,6 +22,7 @@ import { BenchmarkProgress, BenchmarkStatus } from '../../../types/benchmark' import BenchmarkResult from '#models/benchmark_result' import api from '~/lib/api' import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus' +import { SERVICE_NAMES } from '../../../constants/service_names' type BenchmarkProgressWithID = BenchmarkProgress & { benchmark_id: string } @@ -34,7 +35,7 @@ export default function BenchmarkPage(props: { }) { const { subscribe } = useTransmit() const queryClient = useQueryClient() - const aiInstalled = useServiceInstalledStatus('nomad_ollama') + const aiInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const [progress, setProgress] = useState(null) const [isRunning, setIsRunning] = useState(props.benchmark.status !== 'idle') const [showDetails, setShowDetails] = useState(false) diff --git a/admin/inertia/pages/settings/models.tsx b/admin/inertia/pages/settings/models.tsx index 4e042d6..b51ee59 100644 --- a/admin/inertia/pages/settings/models.tsx +++ b/admin/inertia/pages/settings/models.tsx @@ -10,11 +10,12 @@ 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' export default function ModelsPage(props: { models: { availableModels: NomadOllamaModel[]; installedModels: ModelResponse[] } }) { - const { isInstalled } = useServiceInstalledStatus('nomad_ollama') + const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const { addNotification } = useNotifications() const { openModal, closeAllModals } = useModals() diff --git a/admin/inertia/pages/settings/zim/index.tsx b/admin/inertia/pages/settings/zim/index.tsx index aed0e75..d784e99 100644 --- a/admin/inertia/pages/settings/zim/index.tsx +++ b/admin/inertia/pages/settings/zim/index.tsx @@ -9,11 +9,12 @@ import StyledModal from '~/components/StyledModal' import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus' import Alert from '~/components/Alert' import { FileEntry } from '../../../../types/files' +import { SERVICE_NAMES } from '../../../../constants/service_names' export default function ZimPage() { const queryClient = useQueryClient() const { openModal, closeAllModals } = useModals() - const { isInstalled } = useServiceInstalledStatus('nomad_kiwix_serve') + const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.KIWIX) const { data, isLoading } = useQuery({ queryKey: ['zim-files'], queryFn: getFiles, diff --git a/admin/inertia/pages/settings/zim/remote-explorer.tsx b/admin/inertia/pages/settings/zim/remote-explorer.tsx index 594666a..fae5e58 100644 --- a/admin/inertia/pages/settings/zim/remote-explorer.tsx +++ b/admin/inertia/pages/settings/zim/remote-explorer.tsx @@ -36,6 +36,7 @@ import { } from '../../../../types/downloads' import useDownloads from '~/hooks/useDownloads' import ActiveDownloads from '~/components/ActiveDownloads' +import { SERVICE_NAMES } from '../../../../constants/service_names' const CURATED_COLLECTIONS_KEY = 'curated-zim-collections' const CURATED_CATEGORIES_KEY = 'curated-categories' @@ -60,7 +61,7 @@ export default function ZimRemoteExplorer() { const { openModal, closeAllModals } = useModals() const { addNotification } = useNotifications() const { isOnline } = useInternetStatus() - const { isInstalled } = useServiceInstalledStatus('nomad_kiwix_serve') + const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.KIWIX) const { debounce } = useDebounce() const [query, setQuery] = useState('')