11import { useEffect , useState } from "react" ;
22import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference" ;
33import System from "@/models/system" ;
4+ import ModalWrapper from "@/components/ModalWrapper" ;
5+ import { useModal } from "@/hooks/useModal" ;
6+ import { X } from "@phosphor-icons/react" ;
7+ import showToast from "@/utils/toast" ;
8+
9+ // Providers that can't be routing targets
10+ const EXCLUDED_PROVIDERS = [ "anythingllm-router" ] ;
411
512export default function LLMProviderModelPicker ( {
613 providerFieldName = "fallback_provider" ,
@@ -14,12 +21,34 @@ export default function LLMProviderModelPicker({
1421 const [ selectedModel , setSelectedModel ] = useState ( defaultModel ) ;
1522 const [ models , setModels ] = useState ( [ ] ) ;
1623 const [ loadingModels , setLoadingModels ] = useState ( false ) ;
24+ const [ settings , setSettings ] = useState ( null ) ;
25+ const { isOpen, openModal, closeModal } = useModal ( ) ;
26+
27+ const availableProviders = AVAILABLE_LLM_PROVIDERS . filter (
28+ ( llm ) => ! EXCLUDED_PROVIDERS . includes ( llm . value )
29+ ) ;
30+
31+ useEffect ( ( ) => {
32+ async function fetchSettings ( ) {
33+ const _settings = await System . keys ( ) ;
34+ setSettings ( _settings ?? { } ) ;
35+ }
36+ fetchSettings ( ) ;
37+ } , [ ] ) ;
38+
39+ function isConfigured ( providerValue ) {
40+ if ( ! settings ) return true ;
41+ const llm = availableProviders . find ( ( l ) => l . value === providerValue ) ;
42+ if ( ! llm ?. requiredConfig ?. length ) return true ;
43+ return llm . requiredConfig . every ( ( key ) => ! ! settings [ key ] ) ;
44+ }
1745
1846 useEffect ( ( ) => {
19- if ( ! selectedProvider ) {
47+ if ( ! selectedProvider || ! settings ) {
2048 setModels ( [ ] ) ;
2149 return ;
2250 }
51+ if ( ! isConfigured ( selectedProvider ) ) return ;
2352
2453 async function fetchModels ( ) {
2554 setLoadingModels ( true ) ;
@@ -29,7 +58,46 @@ export default function LLMProviderModelPicker({
2958 setLoadingModels ( false ) ;
3059 }
3160 fetchModels ( ) ;
32- } , [ selectedProvider ] ) ;
61+ } , [ selectedProvider , settings ] ) ;
62+
63+ function handleProviderChange ( e ) {
64+ const value = e . target . value ;
65+ setSelectedProvider ( value ) ;
66+ setSelectedModel ( "" ) ;
67+ setModels ( [ ] ) ;
68+ if ( value && ! isConfigured ( value ) ) openModal ( ) ;
69+ }
70+
71+ function handleSetupCancel ( ) {
72+ closeModal ( ) ;
73+ if ( ! isConfigured ( selectedProvider ) ) {
74+ setSelectedProvider ( defaultProvider || "" ) ;
75+ setSelectedModel ( defaultModel || "" ) ;
76+ }
77+ }
78+
79+ async function handleSetupSave ( e ) {
80+ e . preventDefault ( ) ;
81+ e . stopPropagation ( ) ;
82+ const data = { } ;
83+ const form = new FormData ( e . target ) ;
84+ for ( const [ key , value ] of form . entries ( ) ) data [ key ] = value ;
85+ const { error } = await System . updateSystem ( data ) ;
86+ if ( error ) {
87+ showToast ( `Failed to save settings: ${ error } ` , "error" ) ;
88+ return ;
89+ }
90+ const _settings = await System . keys ( ) ;
91+ setSettings ( _settings ?? { } ) ;
92+ closeModal ( ) ;
93+ showToast ( "Provider configured successfully" , "success" , { clear : true } ) ;
94+ }
95+
96+ const selectedLlm = availableProviders . find (
97+ ( l ) => l . value === selectedProvider
98+ ) ;
99+ const needsSetup =
100+ selectedProvider && selectedLlm && ! isConfigured ( selectedProvider ) ;
33101
34102 return (
35103 < div className = "flex flex-col gap-y-1.5" >
@@ -46,23 +114,29 @@ export default function LLMProviderModelPicker({
46114 < select
47115 name = { providerFieldName }
48116 value = { selectedProvider }
49- onChange = { ( e ) => {
50- setSelectedProvider ( e . target . value ) ;
51- setSelectedModel ( "" ) ;
52- } }
117+ onChange = { handleProviderChange }
53118 className = "bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-900 text-sm rounded-lg outline-none block w-full p-2.5"
54119 required
55120 >
56121 < option value = "" > Select provider</ option >
57- { AVAILABLE_LLM_PROVIDERS . map ( ( llm ) => (
122+ { availableProviders . map ( ( llm ) => (
58123 < option key = { llm . value } value = { llm . value } >
59124 { llm . name }
125+ { ! isConfigured ( llm . value ) ? " (setup required)" : "" }
60126 </ option >
61127 ) ) }
62128 </ select >
63129 </ div >
64130 < div className = "flex-1" >
65- { loadingModels ? (
131+ { needsSetup ? (
132+ < button
133+ type = "button"
134+ onClick = { openModal }
135+ className = "bg-zinc-800 light:bg-white light:border light:border-slate-300 text-blue-400 light:text-blue-500 text-sm rounded-lg block w-full p-2.5 text-left hover:text-blue-300 light:hover:text-blue-600 transition-colors"
136+ >
137+ Configure { selectedLlm . name } to continue
138+ </ button >
139+ ) : loadingModels ? (
66140 < div className = "bg-zinc-800 light:bg-white light:border light:border-slate-300 text-zinc-400 light:text-slate-500 text-sm rounded-lg p-2.5" >
67141 Loading models...
68142 </ div >
@@ -99,6 +173,71 @@ export default function LLMProviderModelPicker({
99173 ) }
100174 </ div >
101175 </ div >
176+
177+ < ProviderSetupModal
178+ isOpen = { isOpen }
179+ provider = { selectedLlm }
180+ settings = { settings }
181+ onSave = { handleSetupSave }
182+ onClose = { handleSetupCancel }
183+ />
102184 </ div >
103185 ) ;
104186}
187+
188+ function ProviderSetupModal ( { isOpen, provider, settings, onSave, onClose } ) {
189+ if ( ! isOpen || ! provider ) return null ;
190+
191+ return (
192+ < ModalWrapper isOpen = { isOpen } >
193+ < div className = "w-full max-w-2xl bg-zinc-900 light:bg-white rounded-lg shadow-lg border border-zinc-700 light:border-slate-300" >
194+ < div className = "flex items-center justify-between p-6 border-b border-zinc-700 light:border-slate-300" >
195+ < div className = "flex items-center gap-x-3" >
196+ { provider . logo && (
197+ < img
198+ src = { provider . logo }
199+ alt = { `${ provider . name } logo` }
200+ className = "w-8 h-8 rounded-md"
201+ />
202+ ) }
203+ < h3 className = "text-lg font-semibold text-white light:text-slate-900" >
204+ Configure { provider . name }
205+ </ h3 >
206+ </ div >
207+ < button
208+ onClick = { onClose }
209+ type = "button"
210+ className = "p-1 rounded-lg text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 hover:bg-zinc-800 light:hover:bg-slate-100 transition-colors"
211+ >
212+ < X size = { 20 } weight = "bold" />
213+ </ button >
214+ </ div >
215+ < form id = "provider-setup-form" onSubmit = { onSave } >
216+ < div className = "px-6 py-5" >
217+ < p className = "text-sm text-zinc-400 light:text-slate-600 mb-4" >
218+ Enter the required credentials to use { provider . name } as a routing
219+ target.
220+ </ p >
221+ < div className = "space-y-4" > { provider . options ( settings ?? { } ) } </ div >
222+ </ div >
223+ < div className = "flex justify-end gap-x-3 px-6 py-4 border-t border-zinc-700 light:border-slate-300" >
224+ < button
225+ type = "button"
226+ onClick = { onClose }
227+ className = "text-sm font-medium text-zinc-400 light:text-slate-600 hover:text-white light:hover:text-slate-900 px-4 py-2 rounded-lg transition-colors"
228+ >
229+ Cancel
230+ </ button >
231+ < button
232+ type = "submit"
233+ form = "provider-setup-form"
234+ className = "text-sm font-medium bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-lg px-5 py-2 hover:opacity-90 transition-opacity duration-200"
235+ >
236+ Save settings
237+ </ button >
238+ </ div >
239+ </ form >
240+ </ div >
241+ </ ModalWrapper >
242+ ) ;
243+ }
0 commit comments