ai-changelog-generator-extension 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1366 @@
1
+ import * as vscode from 'vscode'
2
+ import { ExtensionConfigurationManager } from '../services/ExtensionConfigurationManager'
3
+ import { EnvFileService } from '../services/EnvFileService'
4
+ import { StatusService } from '../services/StatusService'
5
+
6
+ export class OnboardingPanelProvider {
7
+ public static currentPanel: OnboardingPanelProvider | undefined
8
+ private readonly _panel: vscode.WebviewPanel
9
+ private _disposables: vscode.Disposable[] = []
10
+
11
+ public static createOrShow(
12
+ extensionUri: vscode.Uri,
13
+ configManager: ExtensionConfigurationManager
14
+ ) {
15
+ // Open in the active editor column, or first column if none active
16
+ const column = vscode.window.activeTextEditor
17
+ ? vscode.window.activeTextEditor.viewColumn
18
+ : vscode.ViewColumn.One
19
+
20
+ // If we already have a panel, show it
21
+ if (OnboardingPanelProvider.currentPanel) {
22
+ OnboardingPanelProvider.currentPanel._panel.reveal(column)
23
+ return
24
+ }
25
+
26
+ // Otherwise, create a new panel in the main editor area
27
+ const panel = vscode.window.createWebviewPanel(
28
+ 'aiChangelogOnboarding',
29
+ '🚀 AI Changelog Setup',
30
+ column || vscode.ViewColumn.One,
31
+ {
32
+ enableScripts: true,
33
+ localResourceRoots: [extensionUri],
34
+ retainContextWhenHidden: true,
35
+ }
36
+ )
37
+
38
+ OnboardingPanelProvider.currentPanel = new OnboardingPanelProvider(
39
+ panel,
40
+ extensionUri,
41
+ configManager
42
+ )
43
+ }
44
+
45
+ private constructor(
46
+ panel: vscode.WebviewPanel,
47
+ extensionUri: vscode.Uri,
48
+ private readonly _configManager: ExtensionConfigurationManager
49
+ ) {
50
+ this._panel = panel
51
+
52
+ // Set the webview's initial html content
53
+ this._panel.webview.html = this._getHtmlForWebview(this._panel.webview)
54
+
55
+ // Listen for when the panel is disposed
56
+ this._panel.onDidDispose(() => this.dispose(), null, this._disposables)
57
+
58
+ // Handle messages from the webview
59
+ this._panel.webview.onDidReceiveMessage(
60
+ async (message) => {
61
+ switch (message.type) {
62
+ case 'testStorageMode':
63
+ await this._testStorageMode(message.mode)
64
+ break
65
+ case 'testProvider':
66
+ await this._testProvider(message.provider)
67
+ break
68
+ case 'testApiKey':
69
+ await this._testApiKey(message.provider, message.apiKey)
70
+ break
71
+ case 'saveConfiguration':
72
+ await this._saveConfiguration(message.config)
73
+ break
74
+ case 'testFullConnection':
75
+ await this._testFullConnection()
76
+ break
77
+ case 'skipSetup':
78
+ await this._skipSetup()
79
+ break
80
+ case 'loadCurrentConfig':
81
+ await this._loadCurrentConfig()
82
+ break
83
+ }
84
+ },
85
+ null,
86
+ this._disposables
87
+ )
88
+
89
+ // Load current configuration
90
+ this._loadCurrentConfig()
91
+ }
92
+
93
+ private async _testStorageMode(mode: string) {
94
+ StatusService.log(`[Onboarding] Testing storage mode: ${mode}`)
95
+
96
+ try {
97
+ if (mode === 'env') {
98
+ const envVars = await EnvFileService.detectEnvironmentVariables()
99
+ const keyCount = Object.keys(envVars).length
100
+
101
+ if (keyCount > 0) {
102
+ // Send detected env vars to auto-fill the form
103
+ this._sendMessage({
104
+ type: 'validationResult',
105
+ step: 'storage',
106
+ success: true,
107
+ message: `✅ Found ${keyCount} configuration(s) in .env files`,
108
+ data: { keys: Object.keys(envVars), envVars },
109
+ })
110
+
111
+ // Also send a message to pre-fill the form with detected values
112
+ this._sendMessage({
113
+ type: 'envDetected',
114
+ envVars,
115
+ })
116
+ } else {
117
+ this._sendMessage({
118
+ type: 'validationResult',
119
+ step: 'storage',
120
+ success: false,
121
+ message: '⚠️ No .env file found. You can create one or choose a different storage mode.',
122
+ })
123
+ }
124
+ } else if (mode === 'secrets') {
125
+ this._sendMessage({
126
+ type: 'validationResult',
127
+ step: 'storage',
128
+ success: true,
129
+ message: '✅ Secure storage is available and ready to use',
130
+ })
131
+ } else if (mode === 'settings') {
132
+ this._sendMessage({
133
+ type: 'validationResult',
134
+ step: 'storage',
135
+ success: true,
136
+ message: '⚠️ Settings storage works but stores keys in plain text (not recommended)',
137
+ })
138
+ }
139
+ } catch (error) {
140
+ const errorMsg = error instanceof Error ? error.message : String(error)
141
+ this._sendMessage({
142
+ type: 'validationResult',
143
+ step: 'storage',
144
+ success: false,
145
+ message: `❌ Error: ${errorMsg}`,
146
+ })
147
+ }
148
+ }
149
+
150
+ private async _testProvider(provider: string) {
151
+ StatusService.log(`[Onboarding] Testing provider: ${provider}`)
152
+
153
+ try {
154
+ if (provider === 'ollama') {
155
+ const config = vscode.workspace.getConfiguration('aiChangelog')
156
+ const host = config.get<string>('ollamaHost', 'http://localhost:11434')
157
+
158
+ try {
159
+ const response = await fetch(`${host}/api/tags`)
160
+ if (response.ok) {
161
+ const data = await response.json()
162
+ const models = data.models?.map((m: any) => m.name) || []
163
+ this._sendMessage({
164
+ type: 'validationResult',
165
+ step: 'provider',
166
+ success: true,
167
+ message: `✅ Ollama is running with ${models.length} model(s)`,
168
+ data: { models, requiresApiKey: false },
169
+ })
170
+ } else {
171
+ throw new Error(`HTTP ${response.status}`)
172
+ }
173
+ } catch (err) {
174
+ this._sendMessage({
175
+ type: 'validationResult',
176
+ step: 'provider',
177
+ success: false,
178
+ message: '❌ Ollama is not running. Please start Ollama first.',
179
+ })
180
+ }
181
+ return
182
+ }
183
+
184
+ if (provider === 'lmstudio') {
185
+ const config = vscode.workspace.getConfiguration('aiChangelog')
186
+ const host = config.get<string>('lmStudioHost', 'http://localhost:1234')
187
+
188
+ try {
189
+ const response = await fetch(`${host}/v1/models`)
190
+ if (response.ok) {
191
+ const data = await response.json()
192
+ const models = data.data?.map((m: any) => m.id) || []
193
+ this._sendMessage({
194
+ type: 'validationResult',
195
+ step: 'provider',
196
+ success: true,
197
+ message: `✅ LM Studio is running with ${models.length} model(s)`,
198
+ data: { models, requiresApiKey: false },
199
+ })
200
+ } else {
201
+ throw new Error(`HTTP ${response.status}`)
202
+ }
203
+ } catch (err) {
204
+ this._sendMessage({
205
+ type: 'validationResult',
206
+ step: 'provider',
207
+ success: false,
208
+ message: '❌ LM Studio is not running. Please start LM Studio first.',
209
+ })
210
+ }
211
+ return
212
+ }
213
+
214
+ // Cloud providers need API keys
215
+ this._sendMessage({
216
+ type: 'validationResult',
217
+ step: 'provider',
218
+ success: true,
219
+ message: `✅ ${this._getProviderDisplayName(provider)} selected. API key required in next step.`,
220
+ data: { requiresApiKey: true },
221
+ })
222
+ } catch (error) {
223
+ const errorMsg = error instanceof Error ? error.message : String(error)
224
+ this._sendMessage({
225
+ type: 'validationResult',
226
+ step: 'provider',
227
+ success: false,
228
+ message: `❌ Error: ${errorMsg}`,
229
+ })
230
+ }
231
+ }
232
+
233
+ private async _testApiKey(provider: string, apiKey: string) {
234
+ StatusService.log(`[Onboarding] Testing API key for ${provider}`)
235
+
236
+ if (!apiKey || apiKey.length < 10) {
237
+ this._sendMessage({
238
+ type: 'validationResult',
239
+ step: 'apiKey',
240
+ success: false,
241
+ message: '❌ API key is too short or empty',
242
+ })
243
+ return
244
+ }
245
+
246
+ // Show loading state
247
+ this._sendMessage({
248
+ type: 'validationResult',
249
+ step: 'apiKey',
250
+ success: null,
251
+ message: '🔄 Testing API connection...',
252
+ })
253
+
254
+ try {
255
+ // Temporarily store the key to test it
256
+ await this._configManager.storeApiKey(provider, apiKey)
257
+
258
+ // Test connection using the command
259
+ await vscode.commands.executeCommand('ai-changelog.testConnection')
260
+
261
+ this._sendMessage({
262
+ type: 'validationResult',
263
+ step: 'apiKey',
264
+ success: true,
265
+ message: `✅ API key is valid! Connection successful.`,
266
+ })
267
+ } catch (error) {
268
+ const errorMsg = error instanceof Error ? error.message : String(error)
269
+ this._sendMessage({
270
+ type: 'validationResult',
271
+ step: 'apiKey',
272
+ success: false,
273
+ message: `❌ Connection failed: ${errorMsg}`,
274
+ })
275
+ }
276
+ }
277
+
278
+ private async _saveConfiguration(config: any) {
279
+ StatusService.log('[Onboarding] Saving configuration...')
280
+
281
+ try {
282
+ const wsConfig = vscode.workspace.getConfiguration('aiChangelog')
283
+
284
+ // Save all settings
285
+ await wsConfig.update('storageMode', config.storageMode, vscode.ConfigurationTarget.Workspace)
286
+ await wsConfig.update('provider', config.provider, vscode.ConfigurationTarget.Workspace)
287
+
288
+ if (config.model) {
289
+ await wsConfig.update('model', config.model, vscode.ConfigurationTarget.Workspace)
290
+ }
291
+
292
+ if (config.temperature !== undefined) {
293
+ await wsConfig.update('temperature', config.temperature, vscode.ConfigurationTarget.Workspace)
294
+ }
295
+
296
+ // Provider-specific settings
297
+ if (config.azureEndpoint) {
298
+ await wsConfig.update('azureEndpoint', config.azureEndpoint, vscode.ConfigurationTarget.Workspace)
299
+ }
300
+ if (config.azureDeploymentName) {
301
+ await wsConfig.update('azureDeploymentName', config.azureDeploymentName, vscode.ConfigurationTarget.Workspace)
302
+ }
303
+ if (config.ollamaHost) {
304
+ await wsConfig.update('ollamaHost', config.ollamaHost, vscode.ConfigurationTarget.Workspace)
305
+ }
306
+ if (config.lmStudioHost) {
307
+ await wsConfig.update('lmStudioHost', config.lmStudioHost, vscode.ConfigurationTarget.Workspace)
308
+ }
309
+
310
+ // Save API key if provided
311
+ if (config.apiKey) {
312
+ await this._configManager.storeApiKey(config.provider, config.apiKey)
313
+ }
314
+
315
+ // Mark setup as complete
316
+ await wsConfig.update('setupCompleted', true, vscode.ConfigurationTarget.Workspace)
317
+
318
+ // Reinitialize config manager
319
+ await this._configManager.initialize()
320
+ StatusService.updateStatus(this._configManager)
321
+
322
+ this._sendMessage({
323
+ type: 'saveComplete',
324
+ success: true,
325
+ message: '✅ Setup complete! You can now use AI Changelog Generator.',
326
+ })
327
+
328
+ StatusService.log('[Onboarding] Configuration saved successfully')
329
+
330
+ // Show success message and close panel after short delay
331
+ setTimeout(() => {
332
+ vscode.window.showInformationMessage(
333
+ '🎉 AI Changelog Generator is ready to use!',
334
+ 'Generate Commit',
335
+ 'Generate Release'
336
+ ).then((action) => {
337
+ if (action === 'Generate Commit') {
338
+ vscode.commands.executeCommand('ai-changelog.generateCommit')
339
+ } else if (action === 'Generate Release') {
340
+ vscode.commands.executeCommand('ai-changelog.generateRelease')
341
+ }
342
+ })
343
+
344
+ this._panel.dispose()
345
+ }, 2000)
346
+ } catch (error) {
347
+ const errorMsg = error instanceof Error ? error.message : String(error)
348
+ this._sendMessage({
349
+ type: 'saveComplete',
350
+ success: false,
351
+ message: `❌ Failed to save: ${errorMsg}`,
352
+ })
353
+ StatusService.log(`[Onboarding] Save failed: ${errorMsg}`)
354
+ }
355
+ }
356
+
357
+ private async _testFullConnection() {
358
+ StatusService.log('[Onboarding] Testing full connection...')
359
+
360
+ this._sendMessage({
361
+ type: 'connectionTest',
362
+ status: 'testing',
363
+ message: '🔄 Testing connection to AI provider...',
364
+ })
365
+
366
+ try {
367
+ await vscode.commands.executeCommand('ai-changelog.testConnection')
368
+
369
+ this._sendMessage({
370
+ type: 'connectionTest',
371
+ status: 'success',
372
+ message: '✅ Connection successful! Everything is working.',
373
+ })
374
+ } catch (error) {
375
+ const errorMsg = error instanceof Error ? error.message : String(error)
376
+ this._sendMessage({
377
+ type: 'connectionTest',
378
+ status: 'error',
379
+ message: `❌ Connection failed: ${errorMsg}`,
380
+ })
381
+ }
382
+ }
383
+
384
+ private async _skipSetup() {
385
+ const wsConfig = vscode.workspace.getConfiguration('aiChangelog')
386
+ await wsConfig.update('setupCompleted', true, vscode.ConfigurationTarget.Workspace)
387
+
388
+ vscode.window.showInformationMessage(
389
+ 'Setup skipped. You can configure settings later from the Settings panel.',
390
+ 'Open Settings'
391
+ ).then((action) => {
392
+ if (action === 'Open Settings') {
393
+ vscode.commands.executeCommand('ai-changelog-settings-view.focus')
394
+ }
395
+ })
396
+
397
+ this._panel.dispose()
398
+ }
399
+
400
+ private async _loadCurrentConfig() {
401
+ const config = vscode.workspace.getConfiguration('aiChangelog')
402
+
403
+ this._sendMessage({
404
+ type: 'configLoaded',
405
+ config: {
406
+ storageMode: config.get<string>('storageMode', 'secrets'),
407
+ provider: config.get<string>('provider', 'openai'),
408
+ model: config.get<string>('model', ''),
409
+ temperature: config.get<number>('temperature', 0.3),
410
+ azureEndpoint: config.get<string>('azureEndpoint', ''),
411
+ azureDeploymentName: config.get<string>('azureDeploymentName', ''),
412
+ ollamaHost: config.get<string>('ollamaHost', 'http://localhost:11434'),
413
+ lmStudioHost: config.get<string>('lmStudioHost', 'http://localhost:1234'),
414
+ },
415
+ })
416
+ }
417
+
418
+ private _getProviderDisplayName(provider: string): string {
419
+ const names: { [key: string]: string } = {
420
+ openai: 'OpenAI',
421
+ anthropic: 'Anthropic (Claude)',
422
+ google: 'Google (Gemini)',
423
+ azure: 'Azure OpenAI',
424
+ bedrock: 'AWS Bedrock',
425
+ ollama: 'Ollama',
426
+ lmstudio: 'LM Studio',
427
+ }
428
+ return names[provider] || provider
429
+ }
430
+
431
+ private _sendMessage(message: any) {
432
+ this._panel.webview.postMessage(message)
433
+ }
434
+
435
+ public dispose() {
436
+ OnboardingPanelProvider.currentPanel = undefined
437
+
438
+ this._panel.dispose()
439
+
440
+ while (this._disposables.length) {
441
+ const disposable = this._disposables.pop()
442
+ if (disposable) {
443
+ disposable.dispose()
444
+ }
445
+ }
446
+ }
447
+
448
+ private _getHtmlForWebview(webview: vscode.Webview): string {
449
+ return `<!DOCTYPE html>
450
+ <html lang="en">
451
+ <head>
452
+ <meta charset="UTF-8">
453
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
454
+ <title>AI Changelog Setup</title>
455
+ <style>
456
+ * {
457
+ margin: 0;
458
+ padding: 0;
459
+ box-sizing: border-box;
460
+ }
461
+
462
+ body {
463
+ font-family: var(--vscode-font-family);
464
+ font-size: var(--vscode-font-size);
465
+ color: var(--vscode-foreground);
466
+ background: var(--vscode-editor-background);
467
+ padding: 0;
468
+ overflow-x: hidden;
469
+ }
470
+
471
+ .container {
472
+ max-width: 700px;
473
+ margin: 0 auto;
474
+ padding: 40px 20px;
475
+ }
476
+
477
+ .header {
478
+ text-align: center;
479
+ margin-bottom: 48px;
480
+ }
481
+
482
+ .header h1 {
483
+ font-size: 32px;
484
+ font-weight: 600;
485
+ margin-bottom: 12px;
486
+ background: linear-gradient(135deg, var(--vscode-textLink-foreground), var(--vscode-textLink-activeForeground));
487
+ -webkit-background-clip: text;
488
+ -webkit-text-fill-color: transparent;
489
+ background-clip: text;
490
+ }
491
+
492
+ .header p {
493
+ color: var(--vscode-descriptionForeground);
494
+ font-size: 14px;
495
+ }
496
+
497
+ .progress-bar {
498
+ display: flex;
499
+ justify-content: space-between;
500
+ margin-bottom: 48px;
501
+ position: relative;
502
+ }
503
+
504
+ .progress-bar::before {
505
+ content: '';
506
+ position: absolute;
507
+ top: 18px;
508
+ left: 0;
509
+ right: 0;
510
+ height: 2px;
511
+ background: var(--vscode-input-border);
512
+ z-index: 0;
513
+ }
514
+
515
+ .progress-fill {
516
+ position: absolute;
517
+ top: 18px;
518
+ left: 0;
519
+ height: 2px;
520
+ background: var(--vscode-textLink-foreground);
521
+ z-index: 1;
522
+ transition: width 0.3s ease;
523
+ }
524
+
525
+ .step-indicator {
526
+ width: 36px;
527
+ height: 36px;
528
+ border-radius: 50%;
529
+ background: var(--vscode-input-background);
530
+ border: 2px solid var(--vscode-input-border);
531
+ display: flex;
532
+ align-items: center;
533
+ justify-content: center;
534
+ font-weight: 600;
535
+ font-size: 14px;
536
+ z-index: 2;
537
+ position: relative;
538
+ transition: all 0.3s ease;
539
+ }
540
+
541
+ .step-indicator.active {
542
+ background: var(--vscode-textLink-foreground);
543
+ border-color: var(--vscode-textLink-foreground);
544
+ color: white;
545
+ }
546
+
547
+ .step-indicator.completed {
548
+ background: var(--vscode-testing-iconPassed);
549
+ border-color: var(--vscode-testing-iconPassed);
550
+ color: white;
551
+ }
552
+
553
+ .step-indicator.completed::after {
554
+ content: '✓';
555
+ }
556
+
557
+ .step {
558
+ display: none;
559
+ animation: fadeIn 0.3s ease;
560
+ }
561
+
562
+ .step.active {
563
+ display: block;
564
+ }
565
+
566
+ @keyframes fadeIn {
567
+ from { opacity: 0; transform: translateY(10px); }
568
+ to { opacity: 1; transform: translateY(0); }
569
+ }
570
+
571
+ .step-title {
572
+ font-size: 24px;
573
+ font-weight: 600;
574
+ margin-bottom: 8px;
575
+ }
576
+
577
+ .step-description {
578
+ color: var(--vscode-descriptionForeground);
579
+ margin-bottom: 32px;
580
+ font-size: 13px;
581
+ line-height: 1.6;
582
+ }
583
+
584
+ .option-group {
585
+ display: flex;
586
+ flex-direction: column;
587
+ gap: 12px;
588
+ margin-bottom: 24px;
589
+ }
590
+
591
+ .option {
592
+ padding: 16px;
593
+ border: 2px solid var(--vscode-input-border);
594
+ border-radius: 8px;
595
+ cursor: pointer;
596
+ transition: all 0.2s ease;
597
+ background: var(--vscode-input-background);
598
+ }
599
+
600
+ .option:hover {
601
+ border-color: var(--vscode-textLink-foreground);
602
+ background: var(--vscode-list-hoverBackground);
603
+ }
604
+
605
+ .option.selected {
606
+ border-color: var(--vscode-textLink-foreground);
607
+ background: var(--vscode-list-activeSelectionBackground);
608
+ }
609
+
610
+ .option-header {
611
+ display: flex;
612
+ align-items: center;
613
+ gap: 12px;
614
+ margin-bottom: 8px;
615
+ }
616
+
617
+ .option-icon {
618
+ font-size: 24px;
619
+ }
620
+
621
+ .option-title {
622
+ font-weight: 600;
623
+ font-size: 14px;
624
+ }
625
+
626
+ .option-badge {
627
+ margin-left: auto;
628
+ padding: 2px 8px;
629
+ border-radius: 12px;
630
+ font-size: 10px;
631
+ font-weight: 600;
632
+ text-transform: uppercase;
633
+ }
634
+
635
+ .option-badge.recommended {
636
+ background: rgba(22, 163, 74, 0.2);
637
+ color: var(--vscode-terminal-ansiGreen);
638
+ }
639
+
640
+ .option-badge.local {
641
+ background: rgba(59, 130, 246, 0.2);
642
+ color: var(--vscode-terminal-ansiBlue);
643
+ }
644
+
645
+ .option-description {
646
+ color: var(--vscode-descriptionForeground);
647
+ font-size: 12px;
648
+ line-height: 1.5;
649
+ }
650
+
651
+ .form-group {
652
+ margin-bottom: 20px;
653
+ }
654
+
655
+ label {
656
+ display: block;
657
+ margin-bottom: 8px;
658
+ font-size: 13px;
659
+ font-weight: 500;
660
+ color: var(--vscode-foreground);
661
+ }
662
+
663
+ input, select {
664
+ width: 100%;
665
+ padding: 10px 12px;
666
+ background: var(--vscode-input-background);
667
+ color: var(--vscode-input-foreground);
668
+ border: 1px solid var(--vscode-input-border);
669
+ border-radius: 4px;
670
+ font-family: inherit;
671
+ font-size: inherit;
672
+ }
673
+
674
+ input[type="password"] {
675
+ font-family: monospace;
676
+ }
677
+
678
+ input:focus, select:focus {
679
+ outline: 1px solid var(--vscode-focusBorder);
680
+ border-color: var(--vscode-focusBorder);
681
+ }
682
+
683
+ .input-hint {
684
+ margin-top: 6px;
685
+ font-size: 11px;
686
+ color: var(--vscode-descriptionForeground);
687
+ font-style: italic;
688
+ }
689
+
690
+ .validation-message {
691
+ margin-top: 12px;
692
+ padding: 12px;
693
+ border-radius: 4px;
694
+ font-size: 13px;
695
+ display: none;
696
+ animation: slideDown 0.2s ease;
697
+ }
698
+
699
+ @keyframes slideDown {
700
+ from { opacity: 0; transform: translateY(-5px); }
701
+ to { opacity: 1; transform: translateY(0); }
702
+ }
703
+
704
+ .validation-message.show {
705
+ display: block;
706
+ }
707
+
708
+ .validation-message.success {
709
+ background: rgba(22, 163, 74, 0.15);
710
+ color: var(--vscode-terminal-ansiGreen);
711
+ border: 1px solid rgba(22, 163, 74, 0.3);
712
+ }
713
+
714
+ .validation-message.error {
715
+ background: rgba(239, 68, 68, 0.15);
716
+ color: var(--vscode-errorForeground);
717
+ border: 1px solid rgba(239, 68, 68, 0.3);
718
+ }
719
+
720
+ .validation-message.loading {
721
+ background: rgba(59, 130, 246, 0.15);
722
+ color: var(--vscode-terminal-ansiBlue);
723
+ border: 1px solid rgba(59, 130, 246, 0.3);
724
+ }
725
+
726
+ .button-group {
727
+ display: flex;
728
+ gap: 12px;
729
+ margin-top: 32px;
730
+ padding-top: 24px;
731
+ border-top: 1px solid var(--vscode-panel-border);
732
+ }
733
+
734
+ button {
735
+ flex: 1;
736
+ padding: 12px 24px;
737
+ border: none;
738
+ border-radius: 4px;
739
+ font-family: inherit;
740
+ font-size: 13px;
741
+ font-weight: 500;
742
+ cursor: pointer;
743
+ transition: all 0.2s ease;
744
+ }
745
+
746
+ button:disabled {
747
+ opacity: 0.5;
748
+ cursor: not-allowed;
749
+ }
750
+
751
+ .btn-primary {
752
+ background: var(--vscode-button-background);
753
+ color: var(--vscode-button-foreground);
754
+ }
755
+
756
+ .btn-primary:hover:not(:disabled) {
757
+ background: var(--vscode-button-hoverBackground);
758
+ }
759
+
760
+ .btn-secondary {
761
+ background: var(--vscode-button-secondaryBackground);
762
+ color: var(--vscode-button-secondaryForeground);
763
+ }
764
+
765
+ .btn-secondary:hover:not(:disabled) {
766
+ background: var(--vscode-button-secondaryHoverBackground);
767
+ }
768
+
769
+ .btn-text {
770
+ background: transparent;
771
+ color: var(--vscode-textLink-foreground);
772
+ border: 1px solid var(--vscode-input-border);
773
+ }
774
+
775
+ .btn-text:hover:not(:disabled) {
776
+ background: var(--vscode-list-hoverBackground);
777
+ }
778
+
779
+ .test-button {
780
+ margin-top: 12px;
781
+ width: auto;
782
+ padding: 8px 20px;
783
+ background: var(--vscode-button-secondaryBackground);
784
+ color: var(--vscode-button-secondaryForeground);
785
+ }
786
+
787
+ .summary-section {
788
+ background: var(--vscode-input-background);
789
+ border: 1px solid var(--vscode-input-border);
790
+ border-radius: 8px;
791
+ padding: 20px;
792
+ margin-bottom: 24px;
793
+ }
794
+
795
+ .summary-item {
796
+ display: flex;
797
+ justify-content: space-between;
798
+ padding: 12px 0;
799
+ border-bottom: 1px solid var(--vscode-panel-border);
800
+ }
801
+
802
+ .summary-item:last-child {
803
+ border-bottom: none;
804
+ }
805
+
806
+ .summary-label {
807
+ color: var(--vscode-descriptionForeground);
808
+ font-size: 13px;
809
+ }
810
+
811
+ .summary-value {
812
+ font-weight: 500;
813
+ font-size: 13px;
814
+ }
815
+
816
+ .success-icon {
817
+ font-size: 64px;
818
+ text-align: center;
819
+ margin-bottom: 24px;
820
+ }
821
+
822
+ .azure-fields, .ollama-fields, .lmstudio-fields {
823
+ display: none;
824
+ margin-top: 20px;
825
+ padding-top: 20px;
826
+ border-top: 1px solid var(--vscode-panel-border);
827
+ }
828
+
829
+ .azure-fields.show, .ollama-fields.show, .lmstudio-fields.show {
830
+ display: block;
831
+ }
832
+ </style>
833
+ </head>
834
+ <body>
835
+ <div class="container">
836
+ <div class="header">
837
+ <h1>🚀 Welcome to AI Changelog Generator</h1>
838
+ <p>Let's get you set up in just a few steps</p>
839
+ </div>
840
+
841
+ <div class="progress-bar">
842
+ <div class="progress-fill" id="progressFill"></div>
843
+ <div class="step-indicator" data-step="1">1</div>
844
+ <div class="step-indicator" data-step="2">2</div>
845
+ <div class="step-indicator" data-step="3">3</div>
846
+ <div class="step-indicator" data-step="4">4</div>
847
+ </div>
848
+
849
+ <!-- Step 1: Storage Mode -->
850
+ <div class="step active" data-step="1">
851
+ <h2 class="step-title">Choose Storage Method</h2>
852
+ <p class="step-description">
853
+ Select how you want to store your AI provider API keys. We recommend secure storage for best security.
854
+ </p>
855
+
856
+ <div class="option-group">
857
+ <div class="option" data-value="secrets" onclick="selectStorage('secrets')">
858
+ <div class="option-header">
859
+ <span class="option-icon">🔒</span>
860
+ <span class="option-title">Secure Storage</span>
861
+ <span class="option-badge recommended">Recommended</span>
862
+ </div>
863
+ <p class="option-description">
864
+ API keys are encrypted and stored in VSCode's secure storage (keychain/credential manager)
865
+ </p>
866
+ </div>
867
+
868
+ <div class="option" data-value="env" onclick="selectStorage('env')">
869
+ <div class="option-header">
870
+ <span class="option-icon">📄</span>
871
+ <span class="option-title">Environment File</span>
872
+ </div>
873
+ <p class="option-description">
874
+ Store API keys in a .env.local file (add to .gitignore to avoid committing)
875
+ </p>
876
+ </div>
877
+
878
+ <div class="option" data-value="settings" onclick="selectStorage('settings')">
879
+ <div class="option-header">
880
+ <span class="option-icon">⚙️</span>
881
+ <span class="option-title">Settings File</span>
882
+ </div>
883
+ <p class="option-description">
884
+ Store in VSCode settings.json (not secure - keys stored in plain text)
885
+ </p>
886
+ </div>
887
+ </div>
888
+
889
+ <div id="storage-validation" class="validation-message"></div>
890
+
891
+ <div class="button-group">
892
+ <button class="btn-text" onclick="skipSetup()">Skip Setup</button>
893
+ <button class="btn-primary" id="storageNext" disabled onclick="nextStep()">Continue →</button>
894
+ </div>
895
+ </div>
896
+
897
+ <!-- Step 2: Provider Selection -->
898
+ <div class="step" data-step="2">
899
+ <h2 class="step-title">Select AI Provider</h2>
900
+ <p class="step-description">
901
+ Choose which AI service you want to use for analyzing your code changes.
902
+ </p>
903
+
904
+ <div class="option-group">
905
+ <div class="option" data-value="openai" onclick="selectProvider('openai')">
906
+ <div class="option-header">
907
+ <span class="option-icon">🤖</span>
908
+ <span class="option-title">OpenAI</span>
909
+ <span class="option-badge recommended">Popular</span>
910
+ </div>
911
+ <p class="option-description">GPT-4 and other OpenAI models</p>
912
+ </div>
913
+
914
+ <div class="option" data-value="anthropic" onclick="selectProvider('anthropic')">
915
+ <div class="option-header">
916
+ <span class="option-icon">🧠</span>
917
+ <span class="option-title">Anthropic Claude</span>
918
+ </div>
919
+ <p class="option-description">Claude 3 models for detailed analysis</p>
920
+ </div>
921
+
922
+ <div class="option" data-value="google" onclick="selectProvider('google')">
923
+ <div class="option-header">
924
+ <span class="option-icon">✨</span>
925
+ <span class="option-title">Google Gemini</span>
926
+ </div>
927
+ <p class="option-description">Gemini models with large context windows</p>
928
+ </div>
929
+
930
+ <div class="option" data-value="azure" onclick="selectProvider('azure')">
931
+ <div class="option-header">
932
+ <span class="option-icon">☁️</span>
933
+ <span class="option-title">Azure OpenAI</span>
934
+ </div>
935
+ <p class="option-description">Enterprise OpenAI deployment on Azure</p>
936
+ </div>
937
+
938
+ <div class="option" data-value="ollama" onclick="selectProvider('ollama')">
939
+ <div class="option-header">
940
+ <span class="option-icon">🦙</span>
941
+ <span class="option-title">Ollama</span>
942
+ <span class="option-badge local">Local</span>
943
+ </div>
944
+ <p class="option-description">Run models locally on your machine</p>
945
+ </div>
946
+
947
+ <div class="option" data-value="lmstudio" onclick="selectProvider('lmstudio')">
948
+ <div class="option-header">
949
+ <span class="option-icon">💻</span>
950
+ <span class="option-title">LM Studio</span>
951
+ <span class="option-badge local">Local</span>
952
+ </div>
953
+ <p class="option-description">Local inference with LM Studio</p>
954
+ </div>
955
+ </div>
956
+
957
+ <div id="provider-validation" class="validation-message"></div>
958
+
959
+ <div class="button-group">
960
+ <button class="btn-secondary" onclick="previousStep()">← Back</button>
961
+ <button class="btn-primary" id="providerNext" disabled onclick="nextStep()">Continue →</button>
962
+ </div>
963
+ </div>
964
+
965
+ <!-- Step 3: API Configuration -->
966
+ <div class="step" data-step="3">
967
+ <h2 class="step-title">Configure API Access</h2>
968
+ <p class="step-description" id="apiStepDescription">
969
+ Enter your API key to connect to the selected provider.
970
+ </p>
971
+
972
+ <div class="form-group" id="apiKeyGroup">
973
+ <label for="apiKey">API Key</label>
974
+ <input type="password" id="apiKey" placeholder="sk-..." />
975
+ <p class="input-hint">Your API key will be stored securely</p>
976
+ <button class="test-button" onclick="testApiKey()">🔌 Test Connection</button>
977
+ </div>
978
+
979
+ <div class="form-group">
980
+ <label for="model">Model (Optional)</label>
981
+ <input type="text" id="model" placeholder="Leave empty for provider default" />
982
+ <p class="input-hint">e.g., gpt-4-turbo, claude-3-5-sonnet-20241022</p>
983
+ </div>
984
+
985
+ <!-- Azure-specific fields -->
986
+ <div class="azure-fields" id="azureFields">
987
+ <div class="form-group">
988
+ <label for="azureEndpoint">Azure Endpoint *</label>
989
+ <input type="text" id="azureEndpoint" placeholder="https://your-resource.openai.azure.com" />
990
+ </div>
991
+ <div class="form-group">
992
+ <label for="azureDeployment">Deployment Name *</label>
993
+ <input type="text" id="azureDeployment" placeholder="gpt-4" />
994
+ </div>
995
+ </div>
996
+
997
+ <!-- Ollama-specific fields -->
998
+ <div class="ollama-fields" id="ollamaFields">
999
+ <div class="form-group">
1000
+ <label for="ollamaHost">Ollama Host</label>
1001
+ <input type="text" id="ollamaHost" value="http://localhost:11434" />
1002
+ </div>
1003
+ </div>
1004
+
1005
+ <!-- LM Studio-specific fields -->
1006
+ <div class="lmstudio-fields" id="lmstudioFields">
1007
+ <div class="form-group">
1008
+ <label for="lmstudioHost">LM Studio Host</label>
1009
+ <input type="text" id="lmstudioHost" value="http://localhost:1234" />
1010
+ </div>
1011
+ </div>
1012
+
1013
+ <div id="apiKey-validation" class="validation-message"></div>
1014
+
1015
+ <div class="button-group">
1016
+ <button class="btn-secondary" onclick="previousStep()">← Back</button>
1017
+ <button class="btn-primary" id="apiNext" onclick="nextStep()">Continue →</button>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <!-- Step 4: Review & Complete -->
1022
+ <div class="step" data-step="4">
1023
+ <h2 class="step-title">Review Configuration</h2>
1024
+ <p class="step-description">
1025
+ Everything looks good! Review your settings and complete the setup.
1026
+ </p>
1027
+
1028
+ <div class="summary-section">
1029
+ <div class="summary-item">
1030
+ <span class="summary-label">Storage Method:</span>
1031
+ <span class="summary-value" id="summaryStorage">-</span>
1032
+ </div>
1033
+ <div class="summary-item">
1034
+ <span class="summary-label">AI Provider:</span>
1035
+ <span class="summary-value" id="summaryProvider">-</span>
1036
+ </div>
1037
+ <div class="summary-item">
1038
+ <span class="summary-label">Model:</span>
1039
+ <span class="summary-value" id="summaryModel">Default</span>
1040
+ </div>
1041
+ <div class="summary-item">
1042
+ <span class="summary-label">API Key:</span>
1043
+ <span class="summary-value" id="summaryApiKey">Configured ✓</span>
1044
+ </div>
1045
+ </div>
1046
+
1047
+ <button class="test-button" onclick="testFullConnection()" style="width: 100%; margin-bottom: 20px;">
1048
+ 🔌 Test Full Connection
1049
+ </button>
1050
+
1051
+ <div id="connection-validation" class="validation-message"></div>
1052
+
1053
+ <div class="button-group">
1054
+ <button class="btn-secondary" onclick="previousStep()">← Back</button>
1055
+ <button class="btn-primary" onclick="completeSetup()">🎉 Complete Setup</button>
1056
+ </div>
1057
+ </div>
1058
+ </div>
1059
+
1060
+ <script>
1061
+ const vscode = acquireVsCodeApi();
1062
+
1063
+ let currentStep = 1;
1064
+ let config = {
1065
+ storageMode: '',
1066
+ provider: '',
1067
+ apiKey: '',
1068
+ model: '',
1069
+ azureEndpoint: '',
1070
+ azureDeploymentName: '',
1071
+ ollamaHost: 'http://localhost:11434',
1072
+ lmStudioHost: 'http://localhost:1234',
1073
+ temperature: 0.3
1074
+ };
1075
+
1076
+ // Load existing config
1077
+ vscode.postMessage({ type: 'loadCurrentConfig' });
1078
+
1079
+ function updateProgress() {
1080
+ const progress = ((currentStep - 1) / 3) * 100;
1081
+ document.getElementById('progressFill').style.width = progress + '%';
1082
+
1083
+ document.querySelectorAll('.step-indicator').forEach((indicator, index) => {
1084
+ indicator.classList.remove('active', 'completed');
1085
+ if (index + 1 < currentStep) {
1086
+ indicator.classList.add('completed');
1087
+ } else if (index + 1 === currentStep) {
1088
+ indicator.classList.add('active');
1089
+ }
1090
+ });
1091
+
1092
+ document.querySelectorAll('.step').forEach((step, index) => {
1093
+ step.classList.remove('active');
1094
+ if (index + 1 === currentStep) {
1095
+ step.classList.add('active');
1096
+ }
1097
+ });
1098
+ }
1099
+
1100
+ function selectStorage(mode) {
1101
+ config.storageMode = mode;
1102
+
1103
+ document.querySelectorAll('[data-step="1"] .option').forEach(opt => {
1104
+ opt.classList.remove('selected');
1105
+ });
1106
+ document.querySelector(\`[data-step="1"] .option[data-value="\${mode}"]\`).classList.add('selected');
1107
+
1108
+ document.getElementById('storageNext').disabled = false;
1109
+
1110
+ // Test the storage mode
1111
+ vscode.postMessage({ type: 'testStorageMode', mode });
1112
+ }
1113
+
1114
+ function selectProvider(provider) {
1115
+ config.provider = provider;
1116
+
1117
+ document.querySelectorAll('[data-step="2"] .option').forEach(opt => {
1118
+ opt.classList.remove('selected');
1119
+ });
1120
+ document.querySelector(\`[data-step="2"] .option[data-value="\${provider}"]\`).classList.add('selected');
1121
+
1122
+ document.getElementById('providerNext').disabled = false;
1123
+
1124
+ // Show/hide provider-specific fields
1125
+ document.getElementById('azureFields').classList.toggle('show', provider === 'azure');
1126
+ document.getElementById('ollamaFields').classList.toggle('show', provider === 'ollama');
1127
+ document.getElementById('lmstudioFields').classList.toggle('show', provider === 'lmstudio');
1128
+
1129
+ // Update API step description
1130
+ const needsKey = !['ollama', 'lmstudio'].includes(provider);
1131
+ document.getElementById('apiStepDescription').textContent = needsKey
1132
+ ? 'Enter your API key to connect to the selected provider.'
1133
+ : 'Configure connection settings for your local provider.';
1134
+
1135
+ document.getElementById('apiKeyGroup').style.display = needsKey ? 'block' : 'none';
1136
+
1137
+ // Test the provider
1138
+ vscode.postMessage({ type: 'testProvider', provider });
1139
+ }
1140
+
1141
+ function testApiKey() {
1142
+ const apiKey = document.getElementById('apiKey').value;
1143
+ if (!apiKey) {
1144
+ showValidation('apiKey', 'error', '❌ Please enter an API key');
1145
+ return;
1146
+ }
1147
+
1148
+ config.apiKey = apiKey;
1149
+ vscode.postMessage({
1150
+ type: 'testApiKey',
1151
+ provider: config.provider,
1152
+ apiKey: apiKey
1153
+ });
1154
+ }
1155
+
1156
+ function testFullConnection() {
1157
+ vscode.postMessage({ type: 'testFullConnection' });
1158
+ }
1159
+
1160
+ function nextStep() {
1161
+ if (currentStep === 3) {
1162
+ // Capture all form values before moving to review
1163
+ config.apiKey = document.getElementById('apiKey').value;
1164
+ config.model = document.getElementById('model').value;
1165
+
1166
+ if (config.provider === 'azure') {
1167
+ config.azureEndpoint = document.getElementById('azureEndpoint').value;
1168
+ config.azureDeploymentName = document.getElementById('azureDeployment').value;
1169
+ } else if (config.provider === 'ollama') {
1170
+ config.ollamaHost = document.getElementById('ollamaHost').value;
1171
+ } else if (config.provider === 'lmstudio') {
1172
+ config.lmStudioHost = document.getElementById('lmstudioHost').value;
1173
+ }
1174
+
1175
+ updateSummary();
1176
+ }
1177
+
1178
+ currentStep++;
1179
+ updateProgress();
1180
+ }
1181
+
1182
+ function previousStep() {
1183
+ currentStep--;
1184
+ updateProgress();
1185
+ }
1186
+
1187
+ function updateSummary() {
1188
+ const storageNames = {
1189
+ secrets: '🔒 Secure Storage',
1190
+ env: '📄 Environment File',
1191
+ settings: '⚙️ Settings File'
1192
+ };
1193
+
1194
+ const providerNames = {
1195
+ openai: '🤖 OpenAI',
1196
+ anthropic: '🧠 Anthropic Claude',
1197
+ google: '✨ Google Gemini',
1198
+ azure: '☁️ Azure OpenAI',
1199
+ bedrock: '🚀 AWS Bedrock',
1200
+ ollama: '🦙 Ollama',
1201
+ lmstudio: '💻 LM Studio'
1202
+ };
1203
+
1204
+ document.getElementById('summaryStorage').textContent = storageNames[config.storageMode] || config.storageMode;
1205
+ document.getElementById('summaryProvider').textContent = providerNames[config.provider] || config.provider;
1206
+ document.getElementById('summaryModel').textContent = config.model || 'Default';
1207
+
1208
+ const hasKey = config.apiKey || ['ollama', 'lmstudio'].includes(config.provider);
1209
+ document.getElementById('summaryApiKey').textContent = hasKey ? 'Configured ✓' : 'Not configured';
1210
+ }
1211
+
1212
+ function completeSetup() {
1213
+ vscode.postMessage({
1214
+ type: 'saveConfiguration',
1215
+ config: config
1216
+ });
1217
+ }
1218
+
1219
+ function skipSetup() {
1220
+ if (confirm('Are you sure you want to skip setup? You can configure settings later.')) {
1221
+ vscode.postMessage({ type: 'skipSetup' });
1222
+ }
1223
+ }
1224
+
1225
+ function showValidation(step, type, message) {
1226
+ const validationEl = document.getElementById(step + '-validation');
1227
+ if (!validationEl) return;
1228
+
1229
+ validationEl.className = 'validation-message show ' + type;
1230
+ validationEl.textContent = message;
1231
+
1232
+ if (type === 'success' || type === 'error') {
1233
+ setTimeout(() => {
1234
+ validationEl.classList.remove('show');
1235
+ }, 5000);
1236
+ }
1237
+ }
1238
+
1239
+ // Handle messages from extension
1240
+ window.addEventListener('message', event => {
1241
+ const message = event.data;
1242
+
1243
+ switch (message.type) {
1244
+ case 'validationResult':
1245
+ const feedbackType = message.success === true ? 'success' :
1246
+ message.success === false ? 'error' :
1247
+ message.success === null ? 'loading' : 'error';
1248
+ showValidation(message.step, feedbackType, message.message);
1249
+
1250
+ // Enable next button on successful validation
1251
+ if (message.success === true && message.step === 'storage') {
1252
+ document.getElementById('storageNext').disabled = false;
1253
+ }
1254
+ if (message.success === true && message.step === 'provider') {
1255
+ document.getElementById('providerNext').disabled = false;
1256
+ }
1257
+ break;
1258
+
1259
+ case 'connectionTest':
1260
+ if (message.status === 'testing') {
1261
+ showValidation('connection', 'loading', message.message);
1262
+ } else if (message.status === 'success') {
1263
+ showValidation('connection', 'success', message.message);
1264
+ } else {
1265
+ showValidation('connection', 'error', message.message);
1266
+ }
1267
+ break;
1268
+
1269
+ case 'saveComplete':
1270
+ if (message.success) {
1271
+ showValidation('connection', 'success', message.message);
1272
+ } else {
1273
+ showValidation('connection', 'error', message.message);
1274
+ }
1275
+ break;
1276
+
1277
+ case 'configLoaded':
1278
+ // Pre-fill form with existing config if available
1279
+ if (message.config) {
1280
+ config = { ...config, ...message.config };
1281
+
1282
+ if (config.storageMode) {
1283
+ const storageOption = document.querySelector(\`[data-step="1"] .option[data-value="\${config.storageMode}"]\`);
1284
+ if (storageOption) storageOption.classList.add('selected');
1285
+ }
1286
+
1287
+ if (config.provider) {
1288
+ const providerOption = document.querySelector(\`[data-step="2"] .option[data-value="\${config.provider}"]\`);
1289
+ if (providerOption) providerOption.classList.add('selected');
1290
+ }
1291
+
1292
+ document.getElementById('model').value = config.model || '';
1293
+ document.getElementById('azureEndpoint').value = config.azureEndpoint || '';
1294
+ document.getElementById('azureDeployment').value = config.azureDeploymentName || '';
1295
+ document.getElementById('ollamaHost').value = config.ollamaHost || 'http://localhost:11434';
1296
+ document.getElementById('lmstudioHost').value = config.lmStudioHost || 'http://localhost:1234';
1297
+ }
1298
+ break;
1299
+
1300
+ case 'envDetected':
1301
+ // Auto-fill form with detected .env values
1302
+ if (message.envVars) {
1303
+ const env = message.envVars;
1304
+
1305
+ // Set provider based on AI_PROVIDER env var
1306
+ if (env.AI_PROVIDER) {
1307
+ config.provider = env.AI_PROVIDER;
1308
+ const providerOption = document.querySelector(\`[data-step="2"] .option[data-value="\${env.AI_PROVIDER}"]\`);
1309
+ if (providerOption) {
1310
+ providerOption.classList.add('selected');
1311
+ document.getElementById('providerNext').disabled = false;
1312
+ }
1313
+ }
1314
+
1315
+ // Auto-fill API keys based on detected provider
1316
+ if (env.OPENAI_API_KEY && (config.provider === 'openai' || !config.provider)) {
1317
+ document.getElementById('apiKey').value = env.OPENAI_API_KEY;
1318
+ config.apiKey = env.OPENAI_API_KEY;
1319
+ }
1320
+ if (env.ANTHROPIC_API_KEY && config.provider === 'anthropic') {
1321
+ document.getElementById('apiKey').value = env.ANTHROPIC_API_KEY;
1322
+ config.apiKey = env.ANTHROPIC_API_KEY;
1323
+ }
1324
+ if (env.AZURE_OPENAI_KEY && config.provider === 'azure') {
1325
+ document.getElementById('apiKey').value = env.AZURE_OPENAI_KEY;
1326
+ config.apiKey = env.AZURE_OPENAI_KEY;
1327
+ }
1328
+ if (env.GOOGLE_API_KEY && config.provider === 'google') {
1329
+ document.getElementById('apiKey').value = env.GOOGLE_API_KEY;
1330
+ config.apiKey = env.GOOGLE_API_KEY;
1331
+ }
1332
+
1333
+ // Auto-fill Azure config
1334
+ if (env.AZURE_OPENAI_ENDPOINT) {
1335
+ document.getElementById('azureEndpoint').value = env.AZURE_OPENAI_ENDPOINT;
1336
+ config.azureEndpoint = env.AZURE_OPENAI_ENDPOINT;
1337
+ }
1338
+ if (env.AZURE_OPENAI_DEPLOYMENT_NAME) {
1339
+ document.getElementById('azureDeployment').value = env.AZURE_OPENAI_DEPLOYMENT_NAME;
1340
+ config.azureDeploymentName = env.AZURE_OPENAI_DEPLOYMENT_NAME;
1341
+ }
1342
+
1343
+ // Auto-fill Ollama host
1344
+ if (env.OLLAMA_HOST) {
1345
+ document.getElementById('ollamaHost').value = env.OLLAMA_HOST;
1346
+ config.ollamaHost = env.OLLAMA_HOST;
1347
+ }
1348
+
1349
+ // Auto-fill LM Studio
1350
+ if (env.LMSTUDIO_BASE_URL) {
1351
+ document.getElementById('lmstudioHost').value = env.LMSTUDIO_BASE_URL;
1352
+ config.lmStudioHost = env.LMSTUDIO_BASE_URL;
1353
+ }
1354
+
1355
+ showValidation('storage', 'success', '\u2705 Configuration loaded from .env.local');
1356
+ }
1357
+ break;
1358
+ }
1359
+ });
1360
+
1361
+ updateProgress();
1362
+ </script>
1363
+ </body>
1364
+ </html>`
1365
+ }
1366
+ }