flen-crypto commited on
Commit
7ac1f6c
·
verified ·
1 Parent(s): 3445ddd

LINK IN OPENAI API BY ADDING A API FUNCTION IN SETTINGS

Browse files
Files changed (3) hide show
  1. components/vinyl-nav.js +2 -2
  2. script.js +95 -1
  3. settings.html +293 -0
components/vinyl-nav.js CHANGED
@@ -106,11 +106,11 @@ class VinylNav extends HTMLElement {
106
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
107
  History
108
  </a>
109
- <a href="#" class="nav-link" onclick="event.preventDefault(); alert('Settings feature coming soon');">
110
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
111
  Settings
112
  </a>
113
- </div>
114
  <button class="mobile-menu-btn">
115
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
116
  </button>
 
106
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
107
  History
108
  </a>
109
+ <a href="settings.html" class="nav-link">
110
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
111
  Settings
112
  </a>
113
+ </div>
114
  <button class="mobile-menu-btn">
115
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
116
  </button>
script.js CHANGED
@@ -449,6 +449,101 @@ function draftAnalysis() {
449
  showToast('Quick preview: Fill in artist/title for full analysis', 'success');
450
  // Could implement lightweight preview mode
451
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
  function requestHelp() {
454
  alert(`VINYL PHOTO GUIDE:
@@ -476,7 +571,6 @@ TIPS:
476
  - Include scale reference if unusual size
477
  - Photograph flaws honestly - reduces returns`);
478
  }
479
-
480
  function showToast(message, type = 'success') {
481
  const existing = document.querySelector('.toast');
482
  if (existing) existing.remove();
 
449
  showToast('Quick preview: Fill in artist/title for full analysis', 'success');
450
  // Could implement lightweight preview mode
451
  }
452
+ async function callOpenAI(messages, temperature = 0.7) {
453
+ const apiKey = localStorage.getItem('openai_api_key');
454
+ const model = localStorage.getItem('openai_model') || 'gpt-4o';
455
+ const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
456
+
457
+ if (!apiKey) {
458
+ showToast('OpenAI API key not configured. Go to Settings.', 'error');
459
+ return null;
460
+ }
461
+
462
+ try {
463
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
464
+ method: 'POST',
465
+ headers: {
466
+ 'Content-Type': 'application/json',
467
+ 'Authorization': `Bearer ${apiKey}`
468
+ },
469
+ body: JSON.stringify({
470
+ model: model,
471
+ messages: messages,
472
+ temperature: temperature,
473
+ max_tokens: maxTokens
474
+ })
475
+ });
476
+
477
+ if (!response.ok) {
478
+ const error = await response.json();
479
+ throw new Error(error.error?.message || 'API request failed');
480
+ }
481
+
482
+ const data = await response.json();
483
+ return data.choices[0].message.content;
484
+ } catch (error) {
485
+ showToast(`OpenAI Error: ${error.message}`, 'error');
486
+ return null;
487
+ }
488
+ }
489
+
490
+ async function generateListingWithAI() {
491
+ const artist = document.getElementById('artistInput').value.trim();
492
+ const title = document.getElementById('titleInput').value.trim();
493
+ const catNo = document.getElementById('catInput').value.trim();
494
+ const year = document.getElementById('yearInput').value.trim();
495
+
496
+ if (!artist || !title) {
497
+ showToast('Please enter at least artist and title', 'error');
498
+ return;
499
+ }
500
+
501
+ const messages = [
502
+ {
503
+ role: 'system',
504
+ content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
505
+ },
506
+ {
507
+ role: 'user',
508
+ content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
509
+ }
510
+ ];
511
+
512
+ showToast('Generating listing with AI...', 'success');
513
+
514
+ const result = await callOpenAI(messages, 0.7);
515
+
516
+ if (result) {
517
+ try {
518
+ const data = JSON.parse(result);
519
+ // Populate the UI with AI-generated content
520
+ if (data.titles) {
521
+ renderTitleOptions(data.titles.map(t => ({
522
+ text: t.length > 80 ? t.substring(0, 77) + '...' : t,
523
+ chars: Math.min(t.length, 80),
524
+ style: 'AI Generated'
525
+ })));
526
+ }
527
+ if (data.description) {
528
+ document.getElementById('htmlOutput').value = data.description;
529
+ }
530
+ if (data.tags) {
531
+ const tagsContainer = document.getElementById('tagsOutput');
532
+ tagsContainer.innerHTML = data.tags.map(t => `
533
+ <span class="px-3 py-1.5 bg-pink-500/10 text-pink-400 rounded-full text-sm border border-pink-500/20">${t}</span>
534
+ `).join('');
535
+ }
536
+ resultsSection.classList.remove('hidden');
537
+ emptyState.classList.add('hidden');
538
+ showToast('AI listing generated!', 'success');
539
+ } catch (e) {
540
+ // If not valid JSON, treat as plain text description
541
+ document.getElementById('htmlOutput').value = result;
542
+ resultsSection.classList.remove('hidden');
543
+ emptyState.classList.add('hidden');
544
+ }
545
+ }
546
+ }
547
 
548
  function requestHelp() {
549
  alert(`VINYL PHOTO GUIDE:
 
571
  - Include scale reference if unusual size
572
  - Photograph flaws honestly - reduces returns`);
573
  }
 
574
  function showToast(message, type = 'success') {
575
  const existing = document.querySelector('.toast');
576
  if (existing) existing.remove();
settings.html ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Settings - VinylVault Pro</title>
7
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
+ <link rel="stylesheet" href="style.css">
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+ <script src="https://unpkg.com/feather-icons"></script>
12
+ <script>
13
+ tailwind.config = {
14
+ darkMode: 'class',
15
+ theme: {
16
+ extend: {
17
+ colors: {
18
+ primary: '#7c3aed',
19
+ secondary: '#06b6d4',
20
+ accent: '#f59e0b',
21
+ surface: '#0f172a',
22
+ 'surface-light': '#1e293b',
23
+ }
24
+ }
25
+ }
26
+ }
27
+ </script>
28
+ </head>
29
+ <body class="bg-surface text-gray-100 min-h-screen">
30
+ <!-- Navigation -->
31
+ <vinyl-nav></vinyl-nav>
32
+
33
+ <!-- Main Content -->
34
+ <main class="pt-20 pb-12 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto">
35
+
36
+ <!-- Page Header -->
37
+ <section class="mb-8">
38
+ <h1 class="text-3xl font-bold bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent mb-2">
39
+ Settings
40
+ </h1>
41
+ <p class="text-gray-400">Configure your API keys and preferences</p>
42
+ </section>
43
+
44
+ <!-- API Configuration -->
45
+ <div class="space-y-6">
46
+
47
+ <!-- OpenAI API Section -->
48
+ <div class="bg-surface-light rounded-2xl p-8 border border-gray-800">
49
+ <div class="flex items-center gap-3 mb-6">
50
+ <div class="w-12 h-12 rounded-xl bg-green-500/10 flex items-center justify-center">
51
+ <svg class="w-6 h-6 text-green-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
52
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
53
+ </svg>
54
+ </div>
55
+ <div>
56
+ <h2 class="text-xl font-semibold text-gray-200">OpenAI API</h2>
57
+ <p class="text-sm text-gray-500">Configure your API key for AI-powered listing generation</p>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="space-y-4">
62
+ <div>
63
+ <label class="block text-sm text-gray-400 mb-2">API Key</label>
64
+ <div class="relative">
65
+ <input type="password" id="openaiKey" placeholder="sk-..."
66
+ class="w-full px-4 py-3 bg-surface border border-gray-700 rounded-lg focus:border-primary focus:outline-none text-sm pr-10"
67
+ autocomplete="off">
68
+ <button onclick="toggleApiVisibility()" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300">
69
+ <i data-feather="eye" class="w-5 h-5"></i>
70
+ </button>
71
+ </div>
72
+ <p class="text-xs text-gray-600 mt-2">Your API key is stored locally in your browser and never sent to our servers.</p>
73
+ </div>
74
+
75
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
76
+ <div>
77
+ <label class="block text-sm text-gray-400 mb-2">Model</label>
78
+ <select id="openaiModel" class="w-full px-4 py-2 bg-surface border border-gray-700 rounded-lg focus:border-primary focus:outline-none text-sm">
79
+ <option value="gpt-4o">GPT-4o (Recommended)</option>
80
+ <option value="gpt-4o-mini">GPT-4o Mini (Faster)</option>
81
+ <option value="gpt-4-turbo">GPT-4 Turbo</option>
82
+ </select>
83
+ </div>
84
+ <div>
85
+ <label class="block text-sm text-gray-400 mb-2">Max Tokens</label>
86
+ <input type="number" id="maxTokens" value="2000" min="500" max="4000" step="100"
87
+ class="w-full px-4 py-2 bg-surface border border-gray-700 rounded-lg focus:border-primary focus:outline-none text-sm">
88
+ </div>
89
+ </div>
90
+
91
+ <div class="flex items-center justify-between pt-4 border-t border-gray-800">
92
+ <div class="flex items-center gap-2 text-sm" id="apiStatus">
93
+ <span class="w-2 h-2 rounded-full bg-gray-600"></span>
94
+ <span class="text-gray-500">Not configured</span>
95
+ </div>
96
+ <div class="flex gap-3">
97
+ <button onclick="testOpenAIConnection()" class="px-4 py-2 border border-gray-600 rounded-lg text-sm text-gray-300 hover:border-primary hover:text-primary transition-all flex items-center gap-2">
98
+ Test Connection
99
+ </button>
100
+ <button onclick="saveOpenAISettings()" class="px-6 py-2 bg-primary rounded-lg font-medium text-white hover:bg-primary/90 transition-all flex items-center gap-2">
101
+ <i data-feather="save" class="w-4 h-4"></i>
102
+ Save
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Discogs API Section -->
110
+ <div class="bg-surface-light rounded-2xl p-8 border border-gray-800">
111
+ <div class="flex items-center gap-3 mb-6">
112
+ <div class="w-12 h-12 rounded-xl bg-orange-500/10 flex items-center justify-center">
113
+ <svg class="w-6 h-6 text-orange-400" viewBox="0 0 24 24" fill="currentColor">
114
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/>
115
+ </svg>
116
+ </div>
117
+ <div>
118
+ <h2 class="text-xl font-semibold text-gray-200">Discogs API</h2>
119
+ <p class="text-sm text-gray-500">Connect Discogs for automatic tracklist and release data</p>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="space-y-4">
124
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
125
+ <div>
126
+ <label class="block text-sm text-gray-400 mb-2">Consumer Key</label>
127
+ <input type="text" id="discogsKey" placeholder="Your Discogs consumer key"
128
+ class="w-full px-4 py-3 bg-surface border border-gray-700 rounded-lg focus:border-primary focus:outline-none text-sm">
129
+ </div>
130
+ <div>
131
+ <label class="block text-sm text-gray-400 mb-2">Consumer Secret</label>
132
+ <input type="password" id="discogsSecret" placeholder="Your Discogs consumer secret"
133
+ class="w-full px-4 py-3 bg-surface border border-gray-700 rounded-lg focus:border-primary focus:outline-none text-sm">
134
+ </div>
135
+ </div>
136
+ <p class="text-xs text-gray-600">Get your API credentials at <a href="https://www.discogs.com/settings/developers" target="_blank" class="text-primary hover:underline">discogs.com/settings/developers</a></p>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- General Settings -->
141
+ <div class="bg-surface-light rounded-2xl p-8 border border-gray-800">
142
+ <h2 class="text-xl font-semibold text-gray-200 mb-6">General Preferences</h2>
143
+
144
+ <div class="space-y-4">
145
+ <div class="flex items-center justify-between py-3 border-b border-gray-800">
146
+ <div>
147
+ <p class="text-gray-300 font-medium">Auto-save Listings</p>
148
+ <p class="text-sm text-gray-500">Automatically save listing drafts locally</p>
149
+ </div>
150
+ <label class="relative inline-flex items-center cursor-pointer">
151
+ <input type="checkbox" id="autoSave" class="sr-only peer" checked>
152
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
153
+ </label>
154
+ </div>
155
+
156
+ <div class="flex items-center justify-between py-3 border-b border-gray-800">
157
+ <div>
158
+ <p class="text-gray-300 font-medium">Dark Mode</p>
159
+ <p class="text-sm text-gray-500">Always use dark theme</p>
160
+ </div>
161
+ <label class="relative inline-flex items-center cursor-pointer">
162
+ <input type="checkbox" id="darkMode" class="sr-only peer" checked>
163
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
164
+ </label>
165
+ </div>
166
+
167
+ <div class="flex items-center justify-between py-3">
168
+ <div>
169
+ <p class="text-gray-300 font-medium">Clear Data</p>
170
+ <p class="text-sm text-gray-500">Remove all saved API keys and preferences</p>
171
+ </div>
172
+ <button onclick="clearAllData()" class="px-4 py-2 border border-red-500/50 text-red-400 rounded-lg text-sm hover:bg-red-500/10 transition-all">
173
+ Clear All
174
+ </button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ </div>
180
+ </main>
181
+
182
+ <!-- Footer -->
183
+ <vinyl-footer></vinyl-footer>
184
+
185
+ <!-- Components -->
186
+ <script src="components/vinyl-nav.js"></script>
187
+ <script src="components/vinyl-footer.js"></script>
188
+
189
+ <!-- Main Script -->
190
+ <script src="script.js"></script>
191
+ <script>
192
+ feather.replace();
193
+
194
+ // Load saved settings
195
+ document.addEventListener('DOMContentLoaded', () => {
196
+ const openaiKey = localStorage.getItem('openai_api_key');
197
+ const openaiModel = localStorage.getItem('openai_model') || 'gpt-4o';
198
+ const maxTokens = localStorage.getItem('openai_max_tokens') || '2000';
199
+ const discogsKey = localStorage.getItem('discogs_key');
200
+ const discogsSecret = localStorage.getItem('discogs_secret');
201
+
202
+ if (openaiKey) {
203
+ document.getElementById('openaiKey').value = openaiKey;
204
+ updateApiStatus(true);
205
+ }
206
+ document.getElementById('openaiModel').value = openaiModel;
207
+ document.getElementById('maxTokens').value = maxTokens;
208
+ if (discogsKey) document.getElementById('discogsKey').value = discogsKey;
209
+ if (discogsSecret) document.getElementById('discogsSecret').value = discogsSecret;
210
+ });
211
+
212
+ function toggleApiVisibility() {
213
+ const input = document.getElementById('openaiKey');
214
+ input.type = input.type === 'password' ? 'text' : 'password';
215
+ }
216
+
217
+ function updateApiStatus(connected) {
218
+ const statusEl = document.getElementById('apiStatus');
219
+ if (connected) {
220
+ statusEl.innerHTML = '<span class="w-2 h-2 rounded-full bg-green-500"></span><span class="text-green-400">Connected</span>';
221
+ } else {
222
+ statusEl.innerHTML = '<span class="w-2 h-2 rounded-full bg-gray-600"></span><span class="text-gray-500">Not configured</span>';
223
+ }
224
+ }
225
+
226
+ async function testOpenAIConnection() {
227
+ const key = document.getElementById('openaiKey').value.trim();
228
+ if (!key) {
229
+ showToast('Please enter an API key first', 'error');
230
+ return;
231
+ }
232
+
233
+ showToast('Testing connection...', 'success');
234
+
235
+ try {
236
+ const response = await fetch('https://api.openai.com/v1/models', {
237
+ method: 'GET',
238
+ headers: {
239
+ 'Authorization': `Bearer ${key}`,
240
+ 'Content-Type': 'application/json'
241
+ }
242
+ });
243
+
244
+ if (response.ok) {
245
+ showToast('Connection successful!', 'success');
246
+ updateApiStatus(true);
247
+ } else {
248
+ const error = await response.json();
249
+ showToast(`Error: ${error.error?.message || 'Invalid API key'}`, 'error');
250
+ updateApiStatus(false);
251
+ }
252
+ } catch (err) {
253
+ showToast('Connection failed. Check your internet.', 'error');
254
+ updateApiStatus(false);
255
+ }
256
+ }
257
+
258
+ function saveOpenAISettings() {
259
+ const key = document.getElementById('openaiKey').value.trim();
260
+ const model = document.getElementById('openaiModel').value;
261
+ const maxTokens = document.getElementById('maxTokens').value;
262
+ const discogsKey = document.getElementById('discogsKey').value.trim();
263
+ const discogsSecret = document.getElementById('discogsSecret').value.trim();
264
+
265
+ if (key) {
266
+ localStorage.setItem('openai_api_key', key);
267
+ localStorage.setItem('openai_model', model);
268
+ localStorage.setItem('openai_max_tokens', maxTokens);
269
+ updateApiStatus(true);
270
+ }
271
+ if (discogsKey) localStorage.setItem('discogs_key', discogsKey);
272
+ if (discogsSecret) localStorage.setItem('discogs_secret', discogsSecret);
273
+
274
+ showToast('Settings saved successfully!', 'success');
275
+ }
276
+
277
+ function clearAllData() {
278
+ if (confirm('Are you sure? This will remove all API keys and preferences.')) {
279
+ localStorage.removeItem('openai_api_key');
280
+ localStorage.removeItem('openai_model');
281
+ localStorage.removeItem('openai_max_tokens');
282
+ localStorage.removeItem('discogs_key');
283
+ localStorage.removeItem('discogs_secret');
284
+ document.getElementById('openaiKey').value = '';
285
+ document.getElementById('discogsKey').value = '';
286
+ document.getElementById('discogsSecret').value = '';
287
+ updateApiStatus(false);
288
+ showToast('All data cleared', 'success');
289
+ }
290
+ }
291
+ </script>
292
+ </body>
293
+ </html>