11import Image from "next/image" ;
22import Link from "next/link" ;
3- import { FC , useState , useEffect , useRef } from "react" ;
3+ import { FC , useState , useEffect , useRef , useCallback } from "react" ;
44import InitialInputArea from "./InitialInputArea" ;
55import { suggestions } from "@/utils/utils" ;
66import { strategies } from "@/utils/strategies" ;
@@ -56,6 +56,8 @@ const Hero: FC<THeroProps> = ({
5656 type : "loading" | "error" ;
5757 message : string ;
5858 } | null > ( null ) ;
59+ const [ ollamaDown , setOllamaDown ] = useState ( false ) ;
60+ const [ checking , setChecking ] = useState ( false ) ;
5961 const [ ollamaNoModels , setOllamaNoModels ] = useState ( false ) ;
6062 const [ recommendedModel , setRecommendedModel ] = useState ( "llama3.1:8b" ) ;
6163 const [ pullProgress , setPullProgress ] = useState < {
@@ -74,57 +76,59 @@ const Hero: FC<THeroProps> = ({
7476 } , [ ] ) ;
7577
7678 // Check the AI provider is reachable before the user invests in a session.
77- useEffect ( ( ) => {
78- let cancelled = false ;
79-
80- const check = async ( ) => {
81- const settings = loadClientSettings ( ) ;
82- const label = PROVIDER_LABELS [ settings . llmProvider ] || settings . llmProvider ;
83- const warning =
84- settings . llmProvider === "ollama"
85- ? "I can't reach Ollama right now. Make sure it's running on your computer, or pick another provider in Settings."
86- : `I can't reach ${ label } right now. Check your secret key and connection in Settings.` ;
87- try {
88- const res = await fetch ( "/api/models" , {
89- method : "POST" ,
90- headers : { "Content-Type" : "application/json" } ,
91- body : JSON . stringify ( {
92- provider : settings . llmProvider ,
93- baseUrl : settings . llmBaseUrl || undefined ,
94- apiKey : settings . llmApiKey || undefined ,
95- } ) ,
96- } ) ;
97- if ( cancelled ) return ;
98- if ( res . ok ) {
99- const data = await res . json ( ) . catch ( ( ) => ( { models : [ ] } ) ) ;
100- const models : string [ ] = data . models || [ ] ;
101- setProviderWarning ( null ) ;
102- // Ollama is up but has nothing to run — offer a download.
103- setOllamaNoModels (
104- settings . llmProvider === "ollama" && models . length === 0 ,
105- ) ;
106- setRecommendedModel ( settings . llmModel || "llama3.1:8b" ) ;
107- } else {
108- setProviderWarning ( warning ) ;
109- setOllamaNoModels ( false ) ;
110- }
111- } catch {
112- if ( ! cancelled ) {
113- setProviderWarning ( warning ) ;
114- setOllamaNoModels ( false ) ;
115- }
79+ // For Ollama, an unreachable provider starts the guided setup journey
80+ // (install → check again → download a model) instead of a plain warning.
81+ const runProviderCheck = useCallback ( async ( ) => {
82+ setChecking ( true ) ;
83+ const settings = loadClientSettings ( ) ;
84+ const isOllama = settings . llmProvider === "ollama" ;
85+ const label = PROVIDER_LABELS [ settings . llmProvider ] || settings . llmProvider ;
86+ try {
87+ const res = await fetch ( "/api/models" , {
88+ method : "POST" ,
89+ headers : { "Content-Type" : "application/json" } ,
90+ body : JSON . stringify ( {
91+ provider : settings . llmProvider ,
92+ baseUrl : settings . llmBaseUrl || undefined ,
93+ apiKey : settings . llmApiKey || undefined ,
94+ } ) ,
95+ } ) ;
96+ if ( res . ok ) {
97+ const data = await res . json ( ) . catch ( ( ) => ( { models : [ ] } ) ) ;
98+ const models : string [ ] = data . models || [ ] ;
99+ setProviderWarning ( null ) ;
100+ setOllamaDown ( false ) ;
101+ // Ollama is up but has nothing to run — offer a download.
102+ setOllamaNoModels ( isOllama && models . length === 0 ) ;
103+ setRecommendedModel ( settings . llmModel || "llama3.1:8b" ) ;
104+ } else {
105+ throw new Error ( `Provider check failed: ${ res . status } ` ) ;
116106 }
117- } ;
107+ } catch {
108+ setOllamaNoModels ( false ) ;
109+ if ( isOllama ) {
110+ setOllamaDown ( true ) ;
111+ setProviderWarning ( null ) ;
112+ } else {
113+ setOllamaDown ( false ) ;
114+ setProviderWarning (
115+ `I can't reach ${ label } right now. Check your secret key and connection in Settings.` ,
116+ ) ;
117+ }
118+ } finally {
119+ setChecking ( false ) ;
120+ }
121+ } , [ ] ) ;
118122
119- check ( ) ;
120- window . addEventListener ( "settingsChanged" , check ) ;
121- window . addEventListener ( "settingsLoaded" , check ) ;
123+ useEffect ( ( ) => {
124+ runProviderCheck ( ) ;
125+ window . addEventListener ( "settingsChanged" , runProviderCheck ) ;
126+ window . addEventListener ( "settingsLoaded" , runProviderCheck ) ;
122127 return ( ) => {
123- cancelled = true ;
124- window . removeEventListener ( "settingsChanged" , check ) ;
125- window . removeEventListener ( "settingsLoaded" , check ) ;
128+ window . removeEventListener ( "settingsChanged" , runProviderCheck ) ;
129+ window . removeEventListener ( "settingsLoaded" , runProviderCheck ) ;
126130 } ;
127- } , [ ] ) ;
131+ } , [ runProviderCheck ] ) ;
128132
129133 const dismissFirstRun = ( ) => {
130134 setShowFirstRun ( false ) ;
@@ -187,7 +191,8 @@ const Hero: FC<THeroProps> = ({
187191 throw new Error ( "Download did not complete. Please try again." ) ;
188192 }
189193 setPullProgress ( null ) ;
190- setOllamaNoModels ( false ) ;
194+ // Re-probe so the banner clears only once the model is really there.
195+ await runProviderCheck ( ) ;
191196 } catch ( e ) {
192197 setPullProgress ( null ) ;
193198 setPullError (
@@ -238,8 +243,8 @@ const Hero: FC<THeroProps> = ({
238243 < >
239244 < div className = "mx-auto mt-6 flex max-w-3xl flex-col items-center justify-center sm:mt-12" >
240245
241- { /* First-run banner */ }
242- { showFirstRun && (
246+ { /* First-run banner (suppressed while the guided Ollama setup is showing) */ }
247+ { showFirstRun && ! ollamaDown && ! ollamaNoModels && (
243248 < div className = "mb-4 w-full rounded-soft border border-accent bg-accent-soft px-4 py-3" >
244249 < div className = "flex items-start justify-between gap-3" >
245250 < p className = "text-sm text-ink-muted" style = { { lineHeight : 1.6 } } >
@@ -274,8 +279,43 @@ const Hero: FC<THeroProps> = ({
274279 </ div >
275280 ) }
276281
282+ { /* Ollama isn't reachable — guided setup, no terminal needed */ }
283+ { ollamaDown && (
284+ < div className = "mb-4 w-full rounded-soft border border-hairline-strong bg-paper-warm px-4 py-4" >
285+ < p className = "text-sm text-ink-muted" style = { { lineHeight : 1.6 } } >
286+ < strong className = "text-ink" > Set up your AI tutor.</ strong > Study
287+ Buddy works with Ollama — free, private AI that runs on your own
288+ computer. No account needed. Install it, then come back here.
289+ </ p >
290+ < div className = "mt-3 flex flex-wrap items-center gap-3" >
291+ < a
292+ href = "https://ollama.com/download"
293+ target = "_blank"
294+ rel = "noopener noreferrer"
295+ className = "rounded-soft bg-ink px-4 py-2 text-sm font-medium text-paper transition-colors duration-normal hover:bg-accent"
296+ >
297+ Get Ollama (free)
298+ </ a >
299+ < button
300+ type = "button"
301+ onClick = { runProviderCheck }
302+ disabled = { checking }
303+ className = "rounded-soft border border-hairline px-4 py-2 text-sm font-medium text-ink-muted transition-colors duration-normal hover:border-hairline-strong hover:text-accent disabled:cursor-not-allowed disabled:opacity-50"
304+ >
305+ { checking ? "Checking..." : "I've installed it — check again" }
306+ </ button >
307+ < Link
308+ href = "/settings"
309+ className = "text-sm text-ink-muted underline transition-colors duration-normal hover:text-accent"
310+ >
311+ Use a cloud provider instead
312+ </ Link >
313+ </ div >
314+ </ div >
315+ ) }
316+
277317 { /* Ollama is reachable but has no models — offer a one-click download */ }
278- { ! showFirstRun && ! providerWarning && ollamaNoModels && (
318+ { ! providerWarning && ollamaNoModels && (
279319 < div className = "mb-4 w-full rounded-soft border border-hairline-strong bg-paper-warm px-4 py-3" >
280320 { pullProgress ? (
281321 < div >
0 commit comments