馃 distributed transcription service thistle.dunkirk.sh
1import { css, html, LitElement } from "lit"; 2import { customElement, property, state } from "lit/decorators.js"; 3 4interface MeetingTime { 5 id: string; 6 label: string; 7} 8 9@customElement("upload-recording-modal") 10export class UploadRecordingModal extends LitElement { 11 @property({ type: Boolean }) open = false; 12 @property({ type: String }) classId = ""; 13 @property({ type: Array }) meetingTimes: MeetingTime[] = []; 14 15 @state() private selectedFile: File | null = null; 16 @state() private selectedMeetingTimeId: string | null = null; 17 @state() private uploading = false; 18 @state() private error: string | null = null; 19 20 static override styles = css` 21 :host { 22 display: none; 23 } 24 25 :host([open]) { 26 display: block; 27 } 28 29 .overlay { 30 position: fixed; 31 top: 0; 32 left: 0; 33 right: 0; 34 bottom: 0; 35 background: rgba(0, 0, 0, 0.5); 36 display: flex; 37 align-items: center; 38 justify-content: center; 39 z-index: 1000; 40 } 41 42 .modal { 43 background: var(--background); 44 border-radius: 8px; 45 padding: 2rem; 46 max-width: 32rem; 47 width: 90%; 48 max-height: 90vh; 49 overflow-y: auto; 50 box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); 51 } 52 53 .modal-header { 54 display: flex; 55 justify-content: space-between; 56 align-items: center; 57 margin-bottom: 1.5rem; 58 } 59 60 .modal-header h2 { 61 margin: 0; 62 color: var(--text); 63 font-size: 1.5rem; 64 } 65 66 .close-button { 67 background: none; 68 border: none; 69 font-size: 1.5rem; 70 color: var(--paynes-gray); 71 cursor: pointer; 72 padding: 0; 73 width: 2rem; 74 height: 2rem; 75 display: flex; 76 align-items: center; 77 justify-content: center; 78 border-radius: 4px; 79 transition: background 0.2s; 80 } 81 82 .close-button:hover { 83 background: var(--secondary); 84 } 85 86 .form-group { 87 margin-bottom: 1.5rem; 88 } 89 90 label { 91 display: block; 92 font-weight: 500; 93 color: var(--text); 94 margin-bottom: 0.5rem; 95 font-size: 0.875rem; 96 } 97 98 .file-input-wrapper { 99 position: relative; 100 border: 2px dashed var(--secondary); 101 border-radius: 8px; 102 padding: 2rem; 103 text-align: center; 104 cursor: pointer; 105 transition: all 0.2s; 106 } 107 108 .file-input-wrapper:hover { 109 border-color: var(--accent); 110 background: color-mix(in srgb, var(--accent) 5%, transparent); 111 } 112 113 .file-input-wrapper.has-file { 114 border-color: var(--accent); 115 background: color-mix(in srgb, var(--accent) 10%, transparent); 116 } 117 118 input[type="file"] { 119 position: absolute; 120 opacity: 0; 121 width: 100%; 122 height: 100%; 123 top: 0; 124 left: 0; 125 cursor: pointer; 126 } 127 128 .file-input-label { 129 color: var(--paynes-gray); 130 font-size: 0.875rem; 131 } 132 133 .file-input-label strong { 134 color: var(--accent); 135 } 136 137 .selected-file { 138 margin-top: 0.5rem; 139 color: var(--text); 140 font-weight: 500; 141 } 142 143 select { 144 width: 100%; 145 padding: 0.75rem; 146 border: 1px solid var(--secondary); 147 border-radius: 4px; 148 font-size: 0.875rem; 149 color: var(--text); 150 background: var(--background); 151 cursor: pointer; 152 } 153 154 select:focus { 155 outline: none; 156 border-color: var(--primary); 157 } 158 159 .help-text { 160 font-size: 0.75rem; 161 color: var(--paynes-gray); 162 margin-top: 0.25rem; 163 } 164 165 .error { 166 background: color-mix(in srgb, red 10%, transparent); 167 border: 1px solid red; 168 color: red; 169 padding: 0.75rem; 170 border-radius: 4px; 171 margin-bottom: 1rem; 172 font-size: 0.875rem; 173 } 174 175 .modal-footer { 176 display: flex; 177 gap: 0.75rem; 178 justify-content: flex-end; 179 margin-top: 2rem; 180 } 181 182 button { 183 padding: 0.75rem 1.5rem; 184 border-radius: 4px; 185 font-size: 0.875rem; 186 font-weight: 600; 187 cursor: pointer; 188 transition: opacity 0.2s; 189 border: none; 190 } 191 192 button:hover:not(:disabled) { 193 opacity: 0.9; 194 } 195 196 button:disabled { 197 opacity: 0.5; 198 cursor: not-allowed; 199 } 200 201 .btn-cancel { 202 background: var(--secondary); 203 color: var(--text); 204 } 205 206 .btn-upload { 207 background: var(--accent); 208 color: var(--white); 209 } 210 211 .uploading-text { 212 display: flex; 213 align-items: center; 214 gap: 0.5rem; 215 } 216 `; 217 218 private handleFileSelect(e: Event) { 219 const input = e.target as HTMLInputElement; 220 if (input.files && input.files.length > 0) { 221 this.selectedFile = input.files[0] ?? null; 222 this.error = null; 223 } 224 } 225 226 private handleMeetingTimeChange(e: Event) { 227 const select = e.target as HTMLSelectElement; 228 this.selectedMeetingTimeId = select.value || null; 229 } 230 231 private handleClose() { 232 if (this.uploading) return; 233 this.open = false; 234 this.selectedFile = null; 235 this.selectedMeetingTimeId = null; 236 this.error = null; 237 this.dispatchEvent(new CustomEvent("close")); 238 } 239 240 private async handleUpload() { 241 if (!this.selectedFile) { 242 this.error = "Please select a file to upload"; 243 return; 244 } 245 246 if (!this.selectedMeetingTimeId) { 247 this.error = "Please select a meeting time"; 248 return; 249 } 250 251 this.uploading = true; 252 this.error = null; 253 254 try { 255 const formData = new FormData(); 256 formData.append("audio", this.selectedFile); 257 formData.append("class_id", this.classId); 258 formData.append("meeting_time_id", this.selectedMeetingTimeId); 259 260 const response = await fetch("/api/transcriptions", { 261 method: "POST", 262 body: formData, 263 }); 264 265 if (!response.ok) { 266 const data = await response.json(); 267 throw new Error(data.error || "Upload failed"); 268 } 269 270 // Success - close modal and notify parent 271 this.dispatchEvent(new CustomEvent("upload-success")); 272 this.handleClose(); 273 } catch (error) { 274 console.error("Upload failed:", error); 275 this.error = 276 error instanceof Error 277 ? error.message 278 : "Upload failed. Please try again."; 279 } finally { 280 this.uploading = false; 281 } 282 } 283 284 override render() { 285 if (!this.open) return null; 286 287 return html` 288 <div class="overlay" @click=${(e: Event) => e.target === e.currentTarget && this.handleClose()}> 289 <div class="modal"> 290 <div class="modal-header"> 291 <h2>Upload Recording</h2> 292 <button class="close-button" @click=${this.handleClose} ?disabled=${this.uploading}> 293294 </button> 295 </div> 296 297 ${this.error ? html`<div class="error">${this.error}</div>` : ""} 298 299 <form @submit=${(e: Event) => e.preventDefault()}> 300 <div class="form-group"> 301 <label>Audio File</label> 302 <div class="file-input-wrapper ${this.selectedFile ? "has-file" : ""}"> 303 <input 304 type="file" 305 accept="audio/*,video/mp4,.mp3,.wav,.m4a,.aac,.ogg,.webm,.flac" 306 @change=${this.handleFileSelect} 307 ?disabled=${this.uploading} 308 /> 309 <div class="file-input-label"> 310 ${ 311 this.selectedFile 312 ? html`<div class="selected-file">馃搸 ${this.selectedFile.name}</div>` 313 : html` 314 <div>馃摛 <strong>Choose a file</strong> or drag it here</div> 315 <div style="margin-top: 0.5rem; font-size: 0.75rem;"> 316 Supported: MP3, WAV, M4A, AAC, OGG, WebM, FLAC, MP4 317 </div> 318 ` 319 } 320 </div> 321 </div> 322 <div class="help-text">Maximum file size: 100MB</div> 323 </div> 324 325 <div class="form-group"> 326 <label for="meeting-time">Meeting Time</label> 327 <select 328 id="meeting-time" 329 @change=${this.handleMeetingTimeChange} 330 ?disabled=${this.uploading} 331 required 332 > 333 <option value="">Select a meeting time...</option> 334 ${this.meetingTimes.map( 335 (meeting) => html` 336 <option value=${meeting.id}>${meeting.label}</option> 337 `, 338 )} 339 </select> 340 <div class="help-text"> 341 Select which meeting this recording is for 342 </div> 343 </div> 344 </form> 345 346 <div class="modal-footer"> 347 <button class="btn-cancel" @click=${this.handleClose} ?disabled=${this.uploading}> 348 Cancel 349 </button> 350 <button 351 class="btn-upload" 352 @click=${this.handleUpload} 353 ?disabled=${this.uploading || !this.selectedFile || !this.selectedMeetingTimeId} 354 > 355 ${ 356 this.uploading 357 ? html`<span class="uploading-text">Uploading...</span>` 358 : "Upload" 359 } 360 </button> 361 </div> 362 </div> 363 </div> 364 `; 365 } 366}