jan/web-app/src/providers/DataProvider.tsx

216 lines
6.6 KiB
TypeScript

import { useMessages } from '@/hooks/useMessages'
import { useModelProvider } from '@/hooks/useModelProvider'
import { useAppUpdater } from '@/hooks/useAppUpdater'
import { useServiceHub } from '@/hooks/useServiceHub'
import { useEffect } from 'react'
import { useMCPServers } from '@/hooks/useMCPServers'
import { useAssistant } from '@/hooks/useAssistant'
import { useNavigate } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useThreads } from '@/hooks/useThreads'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
import { useAppState } from '@/hooks/useAppState'
import { AppEvent, events } from '@janhq/core'
import { localStorageKey } from '@/constants/localStorage'
export function DataProvider() {
const { setProviders, selectedModel, selectedProvider, getProviderByName } =
useModelProvider()
const { setMessages } = useMessages()
const { checkForUpdate } = useAppUpdater()
const { setServers } = useMCPServers()
const { setAssistants, initializeWithLastUsed } = useAssistant()
const { setThreads } = useThreads()
const navigate = useNavigate()
const serviceHub = useServiceHub()
// Local API Server hooks
const {
enableOnStartup,
serverHost,
serverPort,
setServerPort,
apiPrefix,
apiKey,
trustedHosts,
corsEnabled,
verboseLogs,
proxyTimeout,
} = useLocalApiServer()
const { setServerStatus } = useAppState()
useEffect(() => {
console.log('Initializing DataProvider...')
serviceHub.providers().getProviders().then(setProviders)
serviceHub.mcp().getMCPConfig().then((data) => setServers(data.mcpServers ?? {}))
serviceHub.assistants().getAssistants()
.then((data) => {
// Only update assistants if we have valid data
if (data && Array.isArray(data) && data.length > 0) {
setAssistants(data as unknown as Assistant[])
initializeWithLastUsed()
}
})
.catch((error) => {
console.warn('Failed to load assistants, keeping default:', error)
})
serviceHub.deeplink().getCurrent().then(handleDeepLink)
serviceHub.deeplink().onOpenUrl(handleDeepLink)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serviceHub])
useEffect(() => {
serviceHub.threads().fetchThreads().then((threads) => {
setThreads(threads)
threads.forEach((thread) =>
serviceHub.messages().fetchMessages(thread.id).then((messages) =>
setMessages(thread.id, messages)
)
)
})
}, [serviceHub, setThreads, setMessages])
// Check for app updates
useEffect(() => {
// Only check for updates if the auto updater is not disabled
// App might be distributed via other package managers
// or methods that handle updates differently
if (!AUTO_UPDATER_DISABLED) {
checkForUpdate()
}
}, [checkForUpdate])
useEffect(() => {
events.on(AppEvent.onModelImported, () => {
serviceHub.providers().getProviders().then(setProviders)
})
}, [serviceHub, setProviders])
const getLastUsedModel = (): { provider: string; model: string } | null => {
try {
const stored = localStorage.getItem(localStorageKey.lastUsedModel)
return stored ? JSON.parse(stored) : null
} catch (error) {
console.debug('Failed to get last used model from localStorage:', error)
return null
}
}
// Helper function to determine which model to start
const getModelToStart = () => {
// Use last used model if available
const lastUsedModel = getLastUsedModel()
if (lastUsedModel) {
const provider = getProviderByName(lastUsedModel.provider)
if (
provider &&
provider.models.some((m) => m.id === lastUsedModel.model)
) {
return { model: lastUsedModel.model, provider }
}
}
// Use selected model if available
if (selectedModel && selectedProvider) {
const provider = getProviderByName(selectedProvider)
if (provider) {
return { model: selectedModel.id, provider }
}
}
// Use first model from llamacpp provider
const llamacppProvider = getProviderByName('llamacpp')
if (
llamacppProvider &&
llamacppProvider.models &&
llamacppProvider.models.length > 0
) {
return {
model: llamacppProvider.models[0].id,
provider: llamacppProvider,
}
}
return null
}
// Auto-start Local API Server on app startup if enabled
useEffect(() => {
if (enableOnStartup) {
// Validate API key before starting
if (!apiKey || apiKey.toString().trim().length === 0) {
console.warn('Cannot start Local API Server: API key is required')
return
}
const modelToStart = getModelToStart()
// Only start server if we have a model to load
if (!modelToStart) {
console.warn(
'Cannot start Local API Server: No model available to load'
)
return
}
setServerStatus('pending')
// Start the model first
serviceHub.models().startModel(modelToStart.provider, modelToStart.model)
.then(() => {
console.log(`Model ${modelToStart.model} started successfully`)
// Then start the server
return window.core?.api?.startServer({
host: serverHost,
port: serverPort,
prefix: apiPrefix,
apiKey,
trustedHosts,
isCorsEnabled: corsEnabled,
isVerboseEnabled: verboseLogs,
proxyTimeout: proxyTimeout,
})
})
.then((actualPort: number) => {
// Store the actual port that was assigned (important for mobile with port 0)
if (actualPort && actualPort !== serverPort) {
setServerPort(actualPort)
}
setServerStatus('running')
})
.catch((error: unknown) => {
console.error('Failed to start Local API Server on startup:', error)
setServerStatus('stopped')
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serviceHub])
const handleDeepLink = (urls: string[] | null) => {
if (!urls) return
console.log('Received deeplink:', urls)
const deeplink = urls[0]
if (deeplink) {
const url = new URL(deeplink)
const params = url.pathname.split('/').filter((str) => str.length > 0)
if (params.length < 3) return undefined
// const action = params[0]
// const provider = params[1]
const resource = params.slice(1).join('/')
// return { action, provider, resource }
navigate({
to: route.hub.model,
search: {
repo: resource,
},
})
}
}
return null
}