Merge 5d429594aa into 6a4f02dd46
This commit is contained in:
commit
a763731753
|
|
@ -764,7 +764,7 @@ export class OllamaService {
|
||||||
): Promise<{ models: NomadOllamaModel[]; hasMore: boolean } | null> {
|
): Promise<{ models: NomadOllamaModel[]; hasMore: boolean } | null> {
|
||||||
try {
|
try {
|
||||||
const models = await this.retrieveAndRefreshModels(sort, force)
|
const models = await this.retrieveAndRefreshModels(sort, force)
|
||||||
if (!models) {
|
if (!models || models.length === 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'[OllamaService] Returning fallback recommended models due to failure in fetching available models'
|
'[OllamaService] Returning fallback recommended models due to failure in fetching available models'
|
||||||
)
|
)
|
||||||
|
|
@ -819,7 +819,10 @@ export class OllamaService {
|
||||||
try {
|
try {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const cachedModels = await this.readModelsFromCache()
|
const cachedModels = await this.readModelsFromCache()
|
||||||
if (cachedModels) {
|
// An empty cached array (e.g. written from a transient empty upstream
|
||||||
|
// response) must not be treated as valid data — fall through to a
|
||||||
|
// fresh fetch and, failing that, the fallback list.
|
||||||
|
if (cachedModels && cachedModels.length > 0) {
|
||||||
logger.info('[OllamaService] Using cached available models data')
|
logger.info('[OllamaService] Using cached available models data')
|
||||||
return this.sortModels(cachedModels, sort)
|
return this.sortModels(cachedModels, sort)
|
||||||
}
|
}
|
||||||
|
|
@ -832,7 +835,7 @@ export class OllamaService {
|
||||||
const baseUrl = env.get('NOMAD_API_URL') || NOMAD_API_DEFAULT_BASE_URL
|
const baseUrl = env.get('NOMAD_API_URL') || NOMAD_API_DEFAULT_BASE_URL
|
||||||
const fullUrl = new URL(NOMAD_MODELS_API_PATH, baseUrl).toString()
|
const fullUrl = new URL(NOMAD_MODELS_API_PATH, baseUrl).toString()
|
||||||
|
|
||||||
const response = await axios.get(fullUrl)
|
const response = await axios.get(fullUrl, { timeout: 10000 })
|
||||||
if (!response.data || !Array.isArray(response.data.models)) {
|
if (!response.data || !Array.isArray(response.data.models)) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[OllamaService] Invalid response format when fetching available models: ${JSON.stringify(response.data)}`
|
`[OllamaService] Invalid response format when fetching available models: ${JSON.stringify(response.data)}`
|
||||||
|
|
@ -849,6 +852,16 @@ export class OllamaService {
|
||||||
}))
|
}))
|
||||||
.filter((model) => model.tags.length > 0)
|
.filter((model) => model.tags.length > 0)
|
||||||
|
|
||||||
|
// A successful-but-empty upstream response (0 models, or all filtered out
|
||||||
|
// as cloud-only) is a soft failure: return null so the caller serves the
|
||||||
|
// fallback list, and don't poison the 24h cache with an empty array.
|
||||||
|
if (noCloud.length === 0) {
|
||||||
|
logger.warn(
|
||||||
|
'[OllamaService] Nomad API returned no usable (non-cloud) models; using fallback'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
await this.writeModelsToCache(noCloud)
|
await this.writeModelsToCache(noCloud)
|
||||||
return this.sortModels(noCloud, sort)
|
return this.sortModels(noCloud, sort)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import TierSelectionModal from '~/components/TierSelectionModal'
|
||||||
import WikipediaSelector from '~/components/WikipediaSelector'
|
import WikipediaSelector from '~/components/WikipediaSelector'
|
||||||
import LoadingSpinner from '~/components/LoadingSpinner'
|
import LoadingSpinner from '~/components/LoadingSpinner'
|
||||||
import Alert from '~/components/Alert'
|
import Alert from '~/components/Alert'
|
||||||
import { IconCheck, IconChevronDown, IconChevronUp, IconCpu, IconBooks } from '@tabler/icons-react'
|
import { IconCheck, IconCpu, IconBooks } from '@tabler/icons-react'
|
||||||
import StorageProjectionBar from '~/components/StorageProjectionBar'
|
import StorageProjectionBar from '~/components/StorageProjectionBar'
|
||||||
import { useNotifications } from '~/context/NotificationContext'
|
import { useNotifications } from '~/context/NotificationContext'
|
||||||
import useInternetStatus from '~/hooks/useInternetStatus'
|
import useInternetStatus from '~/hooks/useInternetStatus'
|
||||||
|
|
@ -81,30 +81,10 @@ function buildCoreCapabilities(aiAssistantName: string): Capability[] {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDITIONAL_TOOLS: Capability[] = [
|
// Additional tools (Notes, Data Tools, and the rest of the catalog) are no
|
||||||
{
|
// longer surfaced in onboarding — they live in Supply Depot, where the full
|
||||||
id: 'notes',
|
// app catalog is browsable any time. Step 1 keeps the focus on the three core
|
||||||
name: 'Notes',
|
// capabilities and points users to Supply Depot for everything else.
|
||||||
technicalName: 'FlatNotes',
|
|
||||||
description: 'Simple note-taking app with local storage',
|
|
||||||
features: ['Markdown support', 'All notes stored locally', 'No account required'],
|
|
||||||
services: [SERVICE_NAMES.FLATNOTES],
|
|
||||||
icon: 'IconNotes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'datatools',
|
|
||||||
name: 'Data Tools',
|
|
||||||
technicalName: 'CyberChef',
|
|
||||||
description: 'Swiss Army knife for data encoding, encryption, and analysis',
|
|
||||||
features: [
|
|
||||||
'Encode/decode data (Base64, hex, etc.)',
|
|
||||||
'Encryption and hashing tools',
|
|
||||||
'Data format conversion',
|
|
||||||
],
|
|
||||||
services: [SERVICE_NAMES.CYBERCHEF],
|
|
||||||
icon: 'IconChefHat',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
type WizardStep = 1 | 2 | 3 | 4 | 5
|
type WizardStep = 1 | 2 | 3 | 4 | 5
|
||||||
|
|
||||||
|
|
@ -123,14 +103,14 @@ export default function EasySetupWizard(props: {
|
||||||
const [selectedMapCollections, setSelectedMapCollections] = useState<string[]>([])
|
const [selectedMapCollections, setSelectedMapCollections] = useState<string[]>([])
|
||||||
const [selectedAiModels, setSelectedAiModels] = useState<string[]>([])
|
const [selectedAiModels, setSelectedAiModels] = useState<string[]>([])
|
||||||
// Auto-index policy for the AI Assistant Knowledge Base. Defaults to
|
// Auto-index policy for the AI Assistant Knowledge Base. Defaults to
|
||||||
// 'Always' so a new user who keeps the default behavior gets the "just
|
// 'Manual' ("Ask me first"): auto-indexing has real cost and resource
|
||||||
// works" experience — downloads become searchable automatically. Persisted
|
// implications a non-technical user won't anticipate from the toggle alone,
|
||||||
// to KVStore['rag.defaultIngestPolicy'] on wizard submit (same key #894's
|
// so we default to the safe choice and let them opt in. Persisted to
|
||||||
// KB modal toggle reads/writes) so the JIT prompt at first chat sees a
|
// KVStore['rag.defaultIngestPolicy'] on wizard submit (same key #894's KB
|
||||||
// decided policy and doesn't ask again.
|
// modal toggle reads/writes) so the JIT prompt at first chat sees a decided
|
||||||
const [ingestPolicy, setIngestPolicy] = useState<'Always' | 'Manual'>('Always')
|
// policy and doesn't ask again.
|
||||||
|
const [ingestPolicy, setIngestPolicy] = useState<'Always' | 'Manual'>('Manual')
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [showAdditionalTools, setShowAdditionalTools] = useState(false)
|
|
||||||
const [remoteOllamaEnabled, setRemoteOllamaEnabled] = useState(
|
const [remoteOllamaEnabled, setRemoteOllamaEnabled] = useState(
|
||||||
() => !!props.system.remoteOllamaUrl
|
() => !!props.system.remoteOllamaUrl
|
||||||
)
|
)
|
||||||
|
|
@ -591,7 +571,7 @@ export default function EasySetupWizard(props: {
|
||||||
if (capability.id === 'ai' && isSelected) {
|
if (capability.id === 'ai' && isSelected) {
|
||||||
const hasAiSelections =
|
const hasAiSelections =
|
||||||
selectedAiModels.length > 0 ||
|
selectedAiModels.length > 0 ||
|
||||||
ingestPolicy !== 'Always' ||
|
ingestPolicy !== 'Manual' ||
|
||||||
remoteOllamaEnabled
|
remoteOllamaEnabled
|
||||||
if (hasAiSelections) {
|
if (hasAiSelections) {
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
|
|
@ -600,7 +580,7 @@ export default function EasySetupWizard(props: {
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
setSelectedAiModels([])
|
setSelectedAiModels([])
|
||||||
setIngestPolicy('Always')
|
setIngestPolicy('Manual')
|
||||||
setRemoteOllamaEnabled(false)
|
setRemoteOllamaEnabled(false)
|
||||||
setRemoteOllamaUrl('')
|
setRemoteOllamaUrl('')
|
||||||
setRemoteOllamaUrlError(null)
|
setRemoteOllamaUrlError(null)
|
||||||
|
|
@ -723,13 +703,11 @@ export default function EasySetupWizard(props: {
|
||||||
const renderStep1 = () => {
|
const renderStep1 = () => {
|
||||||
// Show all capabilities that exist in the system (including installed ones)
|
// Show all capabilities that exist in the system (including installed ones)
|
||||||
const existingCoreCapabilities = CORE_CAPABILITIES.filter(capabilityExists)
|
const existingCoreCapabilities = CORE_CAPABILITIES.filter(capabilityExists)
|
||||||
const existingAdditionalTools = ADDITIONAL_TOOLS.filter(capabilityExists)
|
|
||||||
|
|
||||||
// Check if ALL capabilities are already installed (nothing left to install)
|
// Check if ALL core capabilities are already installed (nothing left to install)
|
||||||
const allCoreInstalled = existingCoreCapabilities.every(isCapabilityInstalled)
|
|
||||||
const allAdditionalInstalled = existingAdditionalTools.every(isCapabilityInstalled)
|
|
||||||
const allInstalled =
|
const allInstalled =
|
||||||
allCoreInstalled && allAdditionalInstalled && existingCoreCapabilities.length > 0
|
existingCoreCapabilities.length > 0 &&
|
||||||
|
existingCoreCapabilities.every(isCapabilityInstalled)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|
@ -811,29 +789,29 @@ export default function EasySetupWizard(props: {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Additional Tools - Collapsible */}
|
{/* Everything beyond the core capabilities lives in Supply Depot,
|
||||||
{existingAdditionalTools.length > 0 && (
|
the browsable app catalog. Keep onboarding focused and point
|
||||||
<div className="border-t border-desert-stone-light pt-6">
|
users there for Notes, Data Tools, and the rest. */}
|
||||||
<button
|
<div className="border-t border-desert-stone-light pt-6">
|
||||||
onClick={() => setShowAdditionalTools(!showAdditionalTools)}
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 rounded-lg bg-surface-secondary p-4">
|
||||||
className="flex items-center justify-between w-full text-left"
|
<div>
|
||||||
|
<h3 className="text-md font-medium text-text-primary mb-1">
|
||||||
|
Looking for more apps?
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-text-secondary">
|
||||||
|
Notes, data tools, and the full catalog of add-on apps are available any time in
|
||||||
|
Supply Depot.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StyledButton
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => router.visit('/supply-depot')}
|
||||||
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
<h3 className="text-md font-medium text-text-muted">Additional Tools</h3>
|
Open Supply Depot
|
||||||
{showAdditionalTools ? (
|
</StyledButton>
|
||||||
<IconChevronUp size={20} className="text-text-muted" />
|
|
||||||
) : (
|
|
||||||
<IconChevronDown size={20} className="text-text-muted" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{showAdditionalTools && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
|
||||||
{existingAdditionalTools.map((capability) =>
|
|
||||||
renderCapabilityCard(capability, false)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -849,6 +827,20 @@ export default function EasySetupWizard(props: {
|
||||||
regions later.
|
regions later.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mx-auto max-w-2xl rounded-lg border border-border-subtle bg-surface-secondary p-3 text-center">
|
||||||
|
<p className="text-sm text-text-secondary">
|
||||||
|
Only need a specific country, or want the whole world? Individual countries and a full
|
||||||
|
global map can be installed any time from the{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.visit('/settings/maps')}
|
||||||
|
className="font-medium text-desert-green underline"
|
||||||
|
>
|
||||||
|
Maps Manager
|
||||||
|
</button>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{isLoadingMaps ? (
|
{isLoadingMaps ? (
|
||||||
<div className="flex justify-center py-12">
|
<div className="flex justify-center py-12">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|
@ -1161,9 +1153,9 @@ export default function EasySetupWizard(props: {
|
||||||
Capabilities to Install
|
Capabilities to Install
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{[...CORE_CAPABILITIES, ...ADDITIONAL_TOOLS]
|
{CORE_CAPABILITIES.filter((cap) =>
|
||||||
.filter((cap) => cap.services.some((s) => selectedServices.includes(s)))
|
cap.services.some((s) => selectedServices.includes(s))
|
||||||
.map((capability) => (
|
).map((capability) => (
|
||||||
<li key={capability.id} className="flex items-center">
|
<li key={capability.id} className="flex items-center">
|
||||||
<IconCheck size={20} className="text-desert-green mr-2" />
|
<IconCheck size={20} className="text-desert-green mr-2" />
|
||||||
<span className="text-text-primary">
|
<span className="text-text-primary">
|
||||||
|
|
@ -1353,7 +1345,7 @@ export default function EasySetupWizard(props: {
|
||||||
|
|
||||||
<p className="text-sm text-text-secondary">
|
<p className="text-sm text-text-secondary">
|
||||||
{(() => {
|
{(() => {
|
||||||
const count = [...CORE_CAPABILITIES, ...ADDITIONAL_TOOLS].filter((cap) =>
|
const count = CORE_CAPABILITIES.filter((cap) =>
|
||||||
cap.services.some((s) => selectedServices.includes(s))
|
cap.services.some((s) => selectedServices.includes(s))
|
||||||
).length
|
).length
|
||||||
return `${count} ${count === 1 ? 'capability' : 'capabilities'}`
|
return `${count} ${count === 1 ? 'capability' : 'capabilities'}`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue