This commit is contained in:
chriscrosstalk 2026-07-16 14:01:48 -05:00 committed by GitHub
commit a763731753
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 72 additions and 67 deletions

View File

@ -764,7 +764,7 @@ export class OllamaService {
): Promise<{ models: NomadOllamaModel[]; hasMore: boolean } | null> {
try {
const models = await this.retrieveAndRefreshModels(sort, force)
if (!models) {
if (!models || models.length === 0) {
logger.warn(
'[OllamaService] Returning fallback recommended models due to failure in fetching available models'
)
@ -819,7 +819,10 @@ export class OllamaService {
try {
if (!force) {
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')
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 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)) {
logger.warn(
`[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)
// 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)
return this.sortModels(noCloud, sort)
} catch (error) {

View File

@ -11,7 +11,7 @@ import TierSelectionModal from '~/components/TierSelectionModal'
import WikipediaSelector from '~/components/WikipediaSelector'
import LoadingSpinner from '~/components/LoadingSpinner'
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 { useNotifications } from '~/context/NotificationContext'
import useInternetStatus from '~/hooks/useInternetStatus'
@ -81,30 +81,10 @@ function buildCoreCapabilities(aiAssistantName: string): Capability[] {
]
}
const ADDITIONAL_TOOLS: Capability[] = [
{
id: 'notes',
name: 'Notes',
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',
},
]
// 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
// app catalog is browsable any time. Step 1 keeps the focus on the three core
// capabilities and points users to Supply Depot for everything else.
type WizardStep = 1 | 2 | 3 | 4 | 5
@ -123,14 +103,14 @@ export default function EasySetupWizard(props: {
const [selectedMapCollections, setSelectedMapCollections] = useState<string[]>([])
const [selectedAiModels, setSelectedAiModels] = useState<string[]>([])
// Auto-index policy for the AI Assistant Knowledge Base. Defaults to
// 'Always' so a new user who keeps the default behavior gets the "just
// works" experience — downloads become searchable automatically. Persisted
// to KVStore['rag.defaultIngestPolicy'] on wizard submit (same key #894's
// KB modal toggle reads/writes) so the JIT prompt at first chat sees a
// decided policy and doesn't ask again.
const [ingestPolicy, setIngestPolicy] = useState<'Always' | 'Manual'>('Always')
// 'Manual' ("Ask me first"): auto-indexing has real cost and resource
// implications a non-technical user won't anticipate from the toggle alone,
// so we default to the safe choice and let them opt in. Persisted to
// KVStore['rag.defaultIngestPolicy'] on wizard submit (same key #894's KB
// modal toggle reads/writes) so the JIT prompt at first chat sees a decided
// policy and doesn't ask again.
const [ingestPolicy, setIngestPolicy] = useState<'Always' | 'Manual'>('Manual')
const [isProcessing, setIsProcessing] = useState(false)
const [showAdditionalTools, setShowAdditionalTools] = useState(false)
const [remoteOllamaEnabled, setRemoteOllamaEnabled] = useState(
() => !!props.system.remoteOllamaUrl
)
@ -591,7 +571,7 @@ export default function EasySetupWizard(props: {
if (capability.id === 'ai' && isSelected) {
const hasAiSelections =
selectedAiModels.length > 0 ||
ingestPolicy !== 'Always' ||
ingestPolicy !== 'Manual' ||
remoteOllamaEnabled
if (hasAiSelections) {
const confirmed = window.confirm(
@ -600,7 +580,7 @@ export default function EasySetupWizard(props: {
if (!confirmed) return
}
setSelectedAiModels([])
setIngestPolicy('Always')
setIngestPolicy('Manual')
setRemoteOllamaEnabled(false)
setRemoteOllamaUrl('')
setRemoteOllamaUrlError(null)
@ -723,13 +703,11 @@ export default function EasySetupWizard(props: {
const renderStep1 = () => {
// Show all capabilities that exist in the system (including installed ones)
const existingCoreCapabilities = CORE_CAPABILITIES.filter(capabilityExists)
const existingAdditionalTools = ADDITIONAL_TOOLS.filter(capabilityExists)
// Check if ALL capabilities are already installed (nothing left to install)
const allCoreInstalled = existingCoreCapabilities.every(isCapabilityInstalled)
const allAdditionalInstalled = existingAdditionalTools.every(isCapabilityInstalled)
// Check if ALL core capabilities are already installed (nothing left to install)
const allInstalled =
allCoreInstalled && allAdditionalInstalled && existingCoreCapabilities.length > 0
existingCoreCapabilities.length > 0 &&
existingCoreCapabilities.every(isCapabilityInstalled)
return (
<div className="space-y-8">
@ -811,29 +789,29 @@ export default function EasySetupWizard(props: {
</div>
)}
{/* Additional Tools - Collapsible */}
{existingAdditionalTools.length > 0 && (
<div className="border-t border-desert-stone-light pt-6">
<button
onClick={() => setShowAdditionalTools(!showAdditionalTools)}
className="flex items-center justify-between w-full text-left"
{/* Everything beyond the core capabilities lives in Supply Depot,
the browsable app catalog. Keep onboarding focused and point
users there for Notes, Data Tools, and the rest. */}
<div className="border-t border-desert-stone-light pt-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 rounded-lg bg-surface-secondary p-4">
<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>
{showAdditionalTools ? (
<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>
)}
Open Supply Depot
</StyledButton>
</div>
)}
</div>
</>
)}
</div>
@ -849,6 +827,20 @@ export default function EasySetupWizard(props: {
regions later.
</p>
</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 ? (
<div className="flex justify-center py-12">
<LoadingSpinner />
@ -1161,9 +1153,9 @@ export default function EasySetupWizard(props: {
Capabilities to Install
</h3>
<ul className="space-y-2">
{[...CORE_CAPABILITIES, ...ADDITIONAL_TOOLS]
.filter((cap) => cap.services.some((s) => selectedServices.includes(s)))
.map((capability) => (
{CORE_CAPABILITIES.filter((cap) =>
cap.services.some((s) => selectedServices.includes(s))
).map((capability) => (
<li key={capability.id} className="flex items-center">
<IconCheck size={20} className="text-desert-green mr-2" />
<span className="text-text-primary">
@ -1353,7 +1345,7 @@ export default function EasySetupWizard(props: {
<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))
).length
return `${count} ${count === 1 ? 'capability' : 'capabilities'}`