@@ -51,6 +51,15 @@ export class ChatUI {
5151 this . agent . setModel ( this . modelSelector . value ) ;
5252 } ) ;
5353
54+ // If in user-provided API key mode, add settings button
55+ if ( this . config . _userProvidedMode ) {
56+ this . initSettingsUI ( ) ;
57+ // If no API key saved yet, show the setup prompt
58+ if ( ! this . config . llm_models ?. length ) {
59+ this . showSettingsPanel ( ) ;
60+ }
61+ }
62+
5463 // Wire agent callbacks
5564 this . agent . onThinkingStart = ( ) => this . showThinking ( ) ;
5665 this . agent . onThinkingEnd = ( ) => this . hideThinking ( ) ;
@@ -75,6 +84,119 @@ export class ChatUI {
7584 }
7685 }
7786
87+ /* ------------------------------------------------------------------ */
88+ /* Settings panel (user-provided API key mode) */
89+ /* ------------------------------------------------------------------ */
90+
91+ initSettingsUI ( ) {
92+ const footer = document . getElementById ( 'chat-footer' ) ;
93+ if ( ! footer ) return ;
94+
95+ const btn = document . createElement ( 'button' ) ;
96+ btn . id = 'settings-btn' ;
97+ btn . title = 'API settings' ;
98+ btn . textContent = '\u2699' ;
99+ btn . addEventListener ( 'click' , ( ) => this . toggleSettingsPanel ( ) ) ;
100+ footer . prepend ( btn ) ;
101+ }
102+
103+ toggleSettingsPanel ( ) {
104+ const existing = document . getElementById ( 'api-settings-panel' ) ;
105+ if ( existing ) {
106+ existing . remove ( ) ;
107+ return ;
108+ }
109+ this . showSettingsPanel ( ) ;
110+ }
111+
112+ showSettingsPanel ( ) {
113+ // Remove any existing panel
114+ document . getElementById ( 'api-settings-panel' ) ?. remove ( ) ;
115+
116+ const llmConfig = this . config . llm || { } ;
117+ const savedKey = localStorage . getItem ( 'geo-agent-api-key' ) || '' ;
118+ const savedEndpoint = localStorage . getItem ( 'geo-agent-endpoint' )
119+ || llmConfig . default_endpoint || 'https://openrouter.ai/api/v1' ;
120+
121+ const panel = document . createElement ( 'div' ) ;
122+ panel . id = 'api-settings-panel' ;
123+ panel . innerHTML = `
124+ <div class="settings-title">API Settings</div>
125+ <label class="settings-label" for="settings-endpoint">Endpoint</label>
126+ <input id="settings-endpoint" type="url" value="${ this . escapeHtml ( savedEndpoint ) } "
127+ placeholder="https://openrouter.ai/api/v1" spellcheck="false">
128+ <label class="settings-label" for="settings-api-key">API Key</label>
129+ <input id="settings-api-key" type="password" value="${ savedKey ? '••••••••' : '' } "
130+ placeholder="sk-..." spellcheck="false"
131+ onfocus="if(this.value.startsWith('••'))this.value=''">
132+ <div class="settings-actions">
133+ <button id="settings-save" class="settings-save-btn">Save</button>
134+ <button id="settings-cancel" class="settings-cancel-btn">Cancel</button>
135+ </div>
136+ <div class="settings-hint">
137+ Keys are stored in your browser only and never sent to this server.
138+ </div>
139+ ` ;
140+
141+ // Insert before messages area
142+ this . messagesEl . parentNode . insertBefore ( panel , this . messagesEl ) ;
143+
144+ // Wire buttons
145+ panel . querySelector ( '#settings-save' ) . addEventListener ( 'click' , ( ) => {
146+ const endpoint = panel . querySelector ( '#settings-endpoint' ) . value . trim ( ) ;
147+ const apiKey = panel . querySelector ( '#settings-api-key' ) . value . trim ( ) ;
148+
149+ if ( ! apiKey || apiKey . startsWith ( '\u2022' ) ) {
150+ // No change to key if user didn't type a new one
151+ if ( ! savedKey ) {
152+ panel . querySelector ( '#settings-api-key' ) . style . borderColor = '#dc3545' ;
153+ return ;
154+ }
155+ } else {
156+ localStorage . setItem ( 'geo-agent-api-key' , apiKey ) ;
157+ }
158+ if ( endpoint ) {
159+ localStorage . setItem ( 'geo-agent-endpoint' , endpoint ) ;
160+ }
161+
162+ // Rebuild LLM models from new settings
163+ this . applyUserLLMConfig ( ) ;
164+ panel . remove ( ) ;
165+ } ) ;
166+
167+ panel . querySelector ( '#settings-cancel' ) . addEventListener ( 'click' , ( ) => {
168+ panel . remove ( ) ;
169+ } ) ;
170+ }
171+
172+ /**
173+ * Rebuild llm_models from localStorage and update the agent.
174+ */
175+ applyUserLLMConfig ( ) {
176+ const llmConfig = this . config . llm || { } ;
177+ const apiKey = localStorage . getItem ( 'geo-agent-api-key' ) ;
178+ const endpoint = localStorage . getItem ( 'geo-agent-endpoint' )
179+ || llmConfig . default_endpoint || 'https://openrouter.ai/api/v1' ;
180+
181+ if ( ! apiKey ) return ;
182+
183+ const models = ( llmConfig . models || [ ] ) . map ( m => ( {
184+ ...m ,
185+ endpoint,
186+ api_key : apiKey ,
187+ } ) ) ;
188+
189+ if ( models . length === 0 ) {
190+ models . push ( { value : 'auto' , label : 'Auto' , endpoint, api_key : apiKey } ) ;
191+ }
192+
193+ this . config . llm_models = models ;
194+ this . config . llm_model = models [ 0 ] ?. value ;
195+ this . agent . config = this . config ;
196+ this . agent . selectedModel = this . config . llm_model ;
197+ this . populateModelSelector ( ) ;
198+ }
199+
78200 /* ------------------------------------------------------------------ */
79201 /* Send handler */
80202 /* ------------------------------------------------------------------ */
@@ -83,6 +205,12 @@ export class ChatUI {
83205 const text = this . inputEl . value . trim ( ) ;
84206 if ( ! text || this . busy ) return ;
85207
208+ // In user-provided mode, check for API key before sending
209+ if ( this . config . _userProvidedMode && ! localStorage . getItem ( 'geo-agent-api-key' ) ) {
210+ this . showSettingsPanel ( ) ;
211+ return ;
212+ }
213+
86214 this . busy = true ;
87215 this . sendBtn . disabled = true ;
88216 this . inputEl . value = '' ;
0 commit comments