|
8 | 8 | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='45' fill='%23ff0000'/%3E%3C/svg%3E" type="image/svg+xml">
|
9 | 9 | <!-- Add lamejs library -->
|
10 | 10 | <script src=" https://cdn.jsdelivr.net/npm/[email protected]/lame.min.js" ></script>
|
| 11 | + <!-- Import OpenAI library --> |
| 12 | + <script type="module"> |
| 13 | + import OpenAI from 'https://esm.sh/[email protected]?target=es2020'; |
| 14 | + window.OpenAI = OpenAI; |
| 15 | + </script> |
11 | 16 | <style>
|
12 | 17 | /* Base styles for dark theme */
|
13 | 18 | body {
|
|
112 | 117 | text-align: center;
|
113 | 118 | transition: opacity 0.5s;
|
114 | 119 | }
|
| 120 | + |
| 121 | + /* API Key section */ |
| 122 | + .api-key-section { |
| 123 | + margin: 10px 0; |
| 124 | + padding: 10px; |
| 125 | + border: 1px dashed #555; |
| 126 | + border-radius: 4px; |
| 127 | + background: #222; |
| 128 | + } |
| 129 | + |
| 130 | + .api-key-toggle { |
| 131 | + cursor: pointer; |
| 132 | + color: #007bff; |
| 133 | + user-select: none; |
| 134 | + } |
| 135 | + |
| 136 | + .api-key-input { |
| 137 | + display: none; /* Hidden by default */ |
| 138 | + margin-top: 8px; |
| 139 | + } |
| 140 | + |
| 141 | + .api-key-input input { |
| 142 | + width: calc(100% - 125px); |
| 143 | + margin-right: 10px; |
| 144 | + } |
| 145 | + |
| 146 | + /* Summary section */ |
| 147 | + .summary-container { |
| 148 | + margin-top: 15px; |
| 149 | + padding: 10px; |
| 150 | + border: 1px solid #444; |
| 151 | + border-radius: 4px; |
| 152 | + background: #2c2c2c; |
| 153 | + display: none; /* Hidden until there's a summary */ |
| 154 | + } |
| 155 | + |
| 156 | + .summary-content { |
| 157 | + white-space: pre-wrap; |
| 158 | + font-family: Consolas, monospace; |
| 159 | + font-size: 0.9rem; |
| 160 | + color: #a0e0a0; |
| 161 | + } |
115 | 162 | </style>
|
116 | 163 | </head>
|
117 | 164 |
|
|
133 | 180 | <option value="zh-CN">Chinese (Simplified)</option>
|
134 | 181 | </select>
|
135 | 182 | </div>
|
| 183 | + <button id="summarize">Summarize Text</button> |
| 184 | + </div> |
| 185 | + |
| 186 | + <!-- API Key Section --> |
| 187 | + <div class="api-key-section"> |
| 188 | + <div class="api-key-toggle">⚙️ OpenAI API Settings (click to expand)</div> |
| 189 | + <div class="api-key-input"> |
| 190 | + <input type="password" id="api-key" placeholder="Enter your OpenAI API key (sk-...)"> |
| 191 | + <button id="save-api-key">Save</button> |
| 192 | + </div> |
136 | 193 | </div>
|
137 | 194 |
|
138 | 195 | <h3>Transcription:</h3>
|
139 | 196 | <div id="transcription-container">
|
140 | 197 | <!-- The transcription element is contenteditable (editable by the user) -->
|
141 | 198 | <div id="transcription" contenteditable="true"></div>
|
142 | 199 | </div>
|
| 200 | + |
| 201 | + <!-- Summary Section --> |
| 202 | + <h3 id="summary-heading" style="display:none;">Summary:</h3> |
| 203 | + <div id="summary-container" class="summary-container"> |
| 204 | + <div id="summary-content" class="summary-content"></div> |
| 205 | + </div> |
143 | 206 | </div>
|
144 | 207 |
|
145 | 208 | <script>
|
146 | 209 | // Global variable to track recording state
|
147 | 210 | let isRecording = false;
|
148 | 211 | let exitWarningEnabled = false;
|
149 | 212 | let mp3Encoder = null;
|
| 213 | + let apiKey = localStorage.getItem('openai_api_key') || ''; |
| 214 | + |
| 215 | + // Initialize API key section |
| 216 | + document.addEventListener('DOMContentLoaded', () => { |
| 217 | + const apiKeyToggle = document.querySelector('.api-key-toggle'); |
| 218 | + const apiKeyInput = document.querySelector('.api-key-input'); |
| 219 | + const apiKeyField = document.getElementById('api-key'); |
| 220 | + const saveApiKeyBtn = document.getElementById('save-api-key'); |
| 221 | + const summarizeBtn = document.getElementById('summarize'); |
| 222 | + |
| 223 | + // Set the saved API key if available |
| 224 | + if (apiKey) { |
| 225 | + apiKeyField.value = apiKey; |
| 226 | + } |
| 227 | + |
| 228 | + // Toggle API key input visibility |
| 229 | + apiKeyToggle.addEventListener('click', () => { |
| 230 | + apiKeyInput.style.display = apiKeyInput.style.display === 'none' ? 'block' : 'none'; |
| 231 | + }); |
| 232 | + |
| 233 | + // Save API key to localStorage |
| 234 | + saveApiKeyBtn.addEventListener('click', () => { |
| 235 | + apiKey = apiKeyField.value.trim(); |
| 236 | + localStorage.setItem('openai_api_key', apiKey); |
| 237 | + apiKeyInput.style.display = 'none'; |
| 238 | + showNotification('API key saved'); |
| 239 | + }); |
| 240 | + |
| 241 | + // Summarize button event handler |
| 242 | + summarizeBtn.addEventListener('click', async () => { |
| 243 | + const transcriptionText = document.getElementById('transcription').textContent; |
| 244 | + if (!transcriptionText.trim()) { |
| 245 | + showNotification('No transcription text to summarize'); |
| 246 | + return; |
| 247 | + } |
| 248 | + |
| 249 | + if (!apiKey) { |
| 250 | + showNotification('Please provide an OpenAI API key first'); |
| 251 | + apiKeyInput.style.display = 'block'; |
| 252 | + return; |
| 253 | + } |
| 254 | + |
| 255 | + await summarizeText(transcriptionText); |
| 256 | + }); |
| 257 | + }); |
| 258 | + |
| 259 | + // Function to show a notification |
| 260 | + function showNotification(message, duration = 3000) { |
| 261 | + const notification = document.createElement('div'); |
| 262 | + notification.className = 'notification'; |
| 263 | + notification.textContent = message; |
| 264 | + document.body.appendChild(notification); |
| 265 | + |
| 266 | + setTimeout(() => { |
| 267 | + notification.style.opacity = '0'; |
| 268 | + setTimeout(() => notification.remove(), 500); |
| 269 | + }, duration); |
| 270 | + } |
| 271 | + |
| 272 | + // Function to summarize text using OpenAI API |
| 273 | + async function summarizeText(text) { |
| 274 | + const summaryHeading = document.getElementById('summary-heading'); |
| 275 | + const summaryContainer = document.getElementById('summary-container'); |
| 276 | + const summaryContent = document.getElementById('summary-content'); |
| 277 | + |
| 278 | + summaryHeading.style.display = 'block'; |
| 279 | + summaryContainer.style.display = 'block'; |
| 280 | + summaryContent.textContent = 'Generating summary...'; |
| 281 | + |
| 282 | + try { |
| 283 | + // Create OpenAI client |
| 284 | + const client = new window.OpenAI({ |
| 285 | + apiKey: apiKey, |
| 286 | + dangerouslyAllowBrowser: true, |
| 287 | + }); |
| 288 | + |
| 289 | + // Call the API |
| 290 | + const response = await client.chat.completions.create({ |
| 291 | + model: 'gpt-3.5-turbo', // Using a less expensive model for summaries |
| 292 | + messages: [ |
| 293 | + { |
| 294 | + role: 'system', |
| 295 | + content: 'You are a helpful assistant that summarizes text concisely.' |
| 296 | + }, |
| 297 | + { |
| 298 | + role: 'user', |
| 299 | + content: `Please summarize the following transcription in a clear, concise manner:\n\n${text}` |
| 300 | + } |
| 301 | + ], |
| 302 | + max_tokens: 500 |
| 303 | + }); |
| 304 | + |
| 305 | + // Display the summary |
| 306 | + const summary = response.choices[0].message.content; |
| 307 | + summaryContent.textContent = summary; |
| 308 | + } catch (error) { |
| 309 | + console.error('Error generating summary:', error); |
| 310 | + summaryContent.textContent = `Error generating summary: ${error.message}`; |
| 311 | + } |
| 312 | + } |
150 | 313 |
|
151 | 314 | // Check if the browser supports the required APIs
|
152 | 315 | if (!window.MediaRecorder || !(window.webkitSpeechRecognition || window.SpeechRecognition)) {
|
|
0 commit comments