| Philip Zeyliger | 2c4db09 | 2025-04-28 16:57:50 -0700 | [diff] [blame] | 1 | import { css, html, LitElement } from "lit"; |
| 2 | import { customElement, property, state } from "lit/decorators.js"; |
| 3 | import { AgentMessage, State } from "../types"; |
| 4 | |
| 5 | @customElement("sketch-restart-modal") |
| 6 | export class SketchRestartModal extends LitElement { |
| 7 | @property({ type: Boolean }) |
| 8 | open = false; |
| 9 | |
| 10 | @property({ attribute: false }) |
| 11 | containerState: State | null = null; |
| 12 | |
| 13 | @property({ attribute: false }) |
| 14 | messages: AgentMessage[] = []; |
| 15 | |
| 16 | @state() |
| 17 | private restartType: "initial" | "current" | "other" = "current"; |
| 18 | |
| 19 | @state() |
| 20 | private customRevision = ""; |
| 21 | |
| 22 | @state() |
| 23 | private promptOption: "suggested" | "original" | "new" = "suggested"; |
| 24 | |
| 25 | @state() |
| 26 | private commitDescriptions: Record<string, string> = { |
| 27 | current: "", |
| 28 | initial: "", |
| 29 | }; |
| 30 | |
| 31 | @state() |
| 32 | private suggestedPrompt = ""; |
| 33 | |
| 34 | @state() |
| 35 | private originalPrompt = ""; |
| 36 | |
| 37 | @state() |
| 38 | private newPrompt = ""; |
| 39 | |
| 40 | @state() |
| 41 | private isLoading = false; |
| 42 | |
| 43 | @state() |
| 44 | private isSuggestionLoading = false; |
| 45 | |
| 46 | @state() |
| 47 | private isOriginalPromptLoading = false; |
| 48 | |
| 49 | @state() |
| 50 | private errorMessage = ""; |
| 51 | |
| 52 | static styles = css` |
| 53 | :host { |
| 54 | display: block; |
| 55 | font-family: |
| 56 | system-ui, |
| 57 | -apple-system, |
| 58 | BlinkMacSystemFont, |
| 59 | "Segoe UI", |
| 60 | Roboto, |
| 61 | sans-serif; |
| 62 | } |
| 63 | |
| 64 | .modal-description { |
| 65 | margin: 0 0 20px 0; |
| 66 | color: #555; |
| 67 | font-size: 14px; |
| 68 | line-height: 1.5; |
| 69 | } |
| 70 | |
| 71 | .container-message { |
| 72 | margin: 10px 0; |
| 73 | padding: 8px 12px; |
| 74 | background-color: #f8f9fa; |
| 75 | border-left: 4px solid #6c757d; |
| 76 | color: #555; |
| 77 | font-size: 14px; |
| 78 | line-height: 1.5; |
| 79 | border-radius: 4px; |
| 80 | } |
| 81 | |
| 82 | .modal-backdrop { |
| 83 | position: fixed; |
| 84 | top: 0; |
| 85 | left: 0; |
| 86 | width: 100%; |
| 87 | height: 100%; |
| 88 | background-color: rgba(0, 0, 0, 0.5); |
| 89 | z-index: 1000; |
| 90 | display: flex; |
| 91 | justify-content: center; |
| 92 | align-items: center; |
| 93 | opacity: 0; |
| 94 | pointer-events: none; |
| 95 | transition: opacity 0.2s ease-in-out; |
| 96 | } |
| 97 | |
| 98 | .modal-backdrop.open { |
| 99 | opacity: 1; |
| 100 | pointer-events: auto; |
| 101 | } |
| 102 | |
| 103 | .modal-container { |
| 104 | background: white; |
| 105 | border-radius: 8px; |
| 106 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| 107 | width: 600px; |
| 108 | max-width: 90%; |
| 109 | max-height: 90vh; |
| 110 | overflow-y: auto; |
| 111 | padding: 20px; |
| 112 | } |
| 113 | |
| 114 | .modal-header { |
| 115 | display: flex; |
| 116 | justify-content: space-between; |
| 117 | align-items: center; |
| 118 | margin-bottom: 20px; |
| 119 | border-bottom: 1px solid #eee; |
| 120 | padding-bottom: 10px; |
| 121 | } |
| 122 | |
| 123 | .modal-title { |
| 124 | font-size: 18px; |
| 125 | font-weight: 600; |
| 126 | margin: 0; |
| 127 | } |
| 128 | |
| 129 | .close-button { |
| 130 | background: none; |
| 131 | border: none; |
| 132 | font-size: 18px; |
| 133 | cursor: pointer; |
| 134 | color: #666; |
| 135 | } |
| 136 | |
| 137 | .close-button:hover { |
| 138 | color: #333; |
| 139 | } |
| 140 | |
| 141 | .form-group { |
| 142 | margin-bottom: 16px; |
| 143 | } |
| 144 | |
| 145 | .horizontal-radio-group { |
| 146 | display: flex; |
| 147 | flex-wrap: wrap; |
| 148 | gap: 16px; |
| 149 | margin-bottom: 16px; |
| 150 | } |
| 151 | |
| 152 | .revision-option { |
| 153 | border: 1px solid #e0e0e0; |
| 154 | border-radius: 4px; |
| 155 | padding: 8px 12px; |
| 156 | min-width: 180px; |
| 157 | cursor: pointer; |
| 158 | transition: all 0.2s; |
| 159 | } |
| 160 | |
| 161 | .revision-option label { |
| 162 | font-size: 0.9em; |
| 163 | font-weight: bold; |
| 164 | } |
| 165 | |
| 166 | .revision-option:hover { |
| 167 | border-color: #2196f3; |
| 168 | background-color: #f5f9ff; |
| 169 | } |
| 170 | |
| 171 | .revision-option.selected { |
| 172 | border-color: #2196f3; |
| 173 | background-color: #e3f2fd; |
| 174 | } |
| 175 | |
| 176 | .revision-option input[type="radio"] { |
| 177 | margin-right: 8px; |
| 178 | } |
| 179 | |
| 180 | .revision-description { |
| 181 | margin-top: 4px; |
| 182 | color: #666; |
| 183 | font-size: 0.8em; |
| 184 | font-family: monospace; |
| 185 | white-space: nowrap; |
| 186 | overflow: hidden; |
| 187 | text-overflow: ellipsis; |
| 188 | max-width: 200px; |
| 189 | } |
| 190 | |
| 191 | .form-group label { |
| 192 | display: block; |
| 193 | margin-bottom: 8px; |
| 194 | font-weight: 500; |
| 195 | } |
| 196 | |
| 197 | .radio-group { |
| 198 | margin-bottom: 8px; |
| 199 | } |
| 200 | |
| 201 | .radio-option { |
| 202 | display: flex; |
| 203 | align-items: center; |
| 204 | margin-bottom: 8px; |
| 205 | } |
| 206 | |
| 207 | .radio-option input { |
| 208 | margin-right: 8px; |
| 209 | } |
| 210 | |
| 211 | .custom-revision { |
| 212 | margin-left: 24px; |
| 213 | margin-top: 8px; |
| 214 | width: calc(100% - 24px); |
| 215 | padding: 6px 8px; |
| 216 | border: 1px solid #ddd; |
| 217 | border-radius: 4px; |
| 218 | display: block; |
| 219 | } |
| 220 | |
| 221 | .prompt-container { |
| 222 | position: relative; |
| 223 | margin-top: 16px; |
| 224 | } |
| 225 | |
| 226 | .prompt-textarea { |
| 227 | display: block; |
| 228 | box-sizing: border-box; |
| 229 | width: 100%; |
| 230 | min-height: 120px; |
| 231 | padding: 8px; |
| 232 | border: 1px solid #ddd; |
| 233 | border-radius: 4px; |
| 234 | font-family: inherit; |
| 235 | resize: vertical; |
| 236 | background-color: white; |
| 237 | color: #333; |
| 238 | } |
| 239 | |
| 240 | .prompt-textarea.disabled { |
| 241 | background-color: #f5f5f5; |
| 242 | color: #999; |
| 243 | cursor: not-allowed; |
| 244 | } |
| 245 | |
| 246 | .actions { |
| 247 | display: flex; |
| 248 | justify-content: flex-end; |
| 249 | gap: 12px; |
| 250 | margin-top: 20px; |
| 251 | } |
| 252 | |
| 253 | .btn { |
| 254 | padding: 8px 16px; |
| 255 | border-radius: 4px; |
| 256 | font-weight: 500; |
| 257 | cursor: pointer; |
| 258 | border: none; |
| 259 | } |
| 260 | |
| 261 | .btn-cancel { |
| 262 | background: #f2f2f2; |
| 263 | color: #333; |
| 264 | } |
| 265 | |
| 266 | .btn-restart { |
| 267 | background: #4caf50; |
| 268 | color: white; |
| 269 | } |
| 270 | |
| 271 | .btn:disabled { |
| 272 | opacity: 0.6; |
| 273 | cursor: not-allowed; |
| 274 | } |
| 275 | |
| 276 | .error-message { |
| 277 | color: #e53935; |
| 278 | margin-top: 16px; |
| 279 | font-size: 14px; |
| 280 | } |
| 281 | |
| 282 | .loading-indicator { |
| 283 | display: inline-block; |
| 284 | margin-right: 8px; |
| 285 | margin-left: 8px; |
| 286 | width: 16px; |
| 287 | height: 16px; |
| 288 | border: 2px solid rgba(255, 255, 255, 0.3); |
| 289 | border-radius: 50%; |
| 290 | border-top-color: white; |
| 291 | animation: spin 1s linear infinite; |
| 292 | } |
| 293 | |
| 294 | .prompt-container .loading-overlay { |
| 295 | position: absolute; |
| 296 | top: 0; |
| 297 | left: 0; |
| 298 | right: 0; |
| 299 | bottom: 0; |
| 300 | background: rgba(255, 255, 255, 0.7); |
| 301 | display: flex; |
| 302 | align-items: center; |
| 303 | justify-content: center; |
| 304 | z-index: 2; |
| 305 | border-radius: 4px; |
| 306 | } |
| 307 | |
| 308 | .prompt-container .loading-overlay .loading-indicator { |
| 309 | width: 24px; |
| 310 | height: 24px; |
| 311 | border: 2px solid rgba(0, 0, 0, 0.1); |
| 312 | border-top-color: #2196f3; |
| 313 | border-radius: 50%; |
| 314 | animation: spin 1s linear infinite; |
| 315 | } |
| 316 | |
| 317 | .radio-option .loading-indicator { |
| 318 | display: inline-block; |
| 319 | width: 12px; |
| 320 | height: 12px; |
| 321 | border: 1.5px solid rgba(0, 0, 0, 0.2); |
| 322 | border-top-color: #2196f3; |
| 323 | vertical-align: middle; |
| 324 | margin-left: 8px; |
| 325 | } |
| 326 | |
| 327 | .radio-option .status-ready { |
| 328 | display: inline-block; |
| 329 | width: 16px; |
| 330 | height: 16px; |
| 331 | color: #4caf50; |
| 332 | margin-left: 8px; |
| 333 | font-weight: bold; |
| 334 | vertical-align: middle; |
| 335 | } |
| 336 | |
| 337 | @keyframes spin { |
| 338 | to { |
| 339 | transform: rotate(360deg); |
| 340 | } |
| 341 | } |
| 342 | `; |
| 343 | |
| 344 | constructor() { |
| 345 | super(); |
| 346 | this.handleEscape = this.handleEscape.bind(this); |
| 347 | } |
| 348 | |
| 349 | connectedCallback() { |
| 350 | super.connectedCallback(); |
| 351 | document.addEventListener("keydown", this.handleEscape); |
| 352 | } |
| 353 | |
| 354 | // Handle keyboard navigation |
| 355 | firstUpdated() { |
| 356 | if (this.shadowRoot) { |
| 357 | // Set up proper tab navigation by ensuring all focusable elements are included |
| 358 | const focusableElements = |
| 359 | this.shadowRoot.querySelectorAll('[tabindex="0"]'); |
| 360 | if (focusableElements.length > 0) { |
| 361 | // Set initial focus when modal opens |
| 362 | (focusableElements[0] as HTMLElement).focus(); |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | disconnectedCallback() { |
| 368 | super.disconnectedCallback(); |
| 369 | document.removeEventListener("keydown", this.handleEscape); |
| 370 | } |
| 371 | |
| 372 | handleEscape(e: KeyboardEvent) { |
| 373 | if (e.key === "Escape" && this.open) { |
| 374 | this.closeModal(); |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | closeModal() { |
| 379 | this.open = false; |
| 380 | this.dispatchEvent(new CustomEvent("close")); |
| 381 | } |
| 382 | |
| 383 | async loadCommitDescription( |
| 384 | revision: string, |
| 385 | target: "current" | "initial" | "other" = "other", |
| 386 | ) { |
| 387 | try { |
| 388 | const response = await fetch( |
| 389 | `./commit-description?revision=${encodeURIComponent(revision)}`, |
| 390 | ); |
| 391 | if (!response.ok) { |
| 392 | throw new Error( |
| 393 | `Failed to load commit description: ${response.statusText}`, |
| 394 | ); |
| 395 | } |
| 396 | |
| 397 | const data = await response.json(); |
| 398 | |
| 399 | if (target === "other") { |
| 400 | // For custom revisions, update the customRevision directly |
| 401 | this.customRevision = `${revision.slice(0, 8)} - ${data.description}`; |
| 402 | } else { |
| 403 | // For known targets, update the commitDescriptions object |
| 404 | this.commitDescriptions = { |
| 405 | ...this.commitDescriptions, |
| 406 | [target]: data.description, |
| 407 | }; |
| 408 | } |
| 409 | } catch (error) { |
| 410 | console.error(`Error loading commit description for ${revision}:`, error); |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | handleRevisionChange(e?: Event) { |
| 415 | if (e) { |
| 416 | const target = e.target as HTMLInputElement; |
| 417 | this.restartType = target.value as "initial" | "current" | "other"; |
| 418 | } |
| 419 | |
| 420 | // Load commit description for any custom revision if needed |
| 421 | if ( |
| 422 | this.restartType === "other" && |
| 423 | this.customRevision && |
| 424 | !this.customRevision.includes(" - ") |
| 425 | ) { |
| 426 | this.loadCommitDescription(this.customRevision, "other"); |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | handleCustomRevisionChange(e: Event) { |
| 431 | const target = e.target as HTMLInputElement; |
| 432 | this.customRevision = target.value; |
| 433 | } |
| 434 | |
| 435 | handlePromptOptionChange(e: Event) { |
| 436 | const target = e.target as HTMLInputElement; |
| 437 | this.promptOption = target.value as "suggested" | "original" | "new"; |
| 438 | |
| 439 | if ( |
| 440 | this.promptOption === "suggested" && |
| 441 | !this.isSuggestionLoading && |
| 442 | this.suggestedPrompt === "" |
| 443 | ) { |
| 444 | this.loadSuggestedPrompt(); |
| 445 | } else if ( |
| 446 | this.promptOption === "original" && |
| 447 | !this.isOriginalPromptLoading && |
| 448 | this.originalPrompt === "" |
| 449 | ) { |
| 450 | this.loadOriginalPrompt(); |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | handleSuggestedPromptChange(e: Event) { |
| 455 | const target = e.target as HTMLTextAreaElement; |
| 456 | this.suggestedPrompt = target.value; |
| 457 | } |
| 458 | |
| 459 | handleOriginalPromptChange(e: Event) { |
| 460 | const target = e.target as HTMLTextAreaElement; |
| 461 | this.originalPrompt = target.value; |
| 462 | } |
| 463 | |
| 464 | handleNewPromptChange(e: Event) { |
| 465 | const target = e.target as HTMLTextAreaElement; |
| 466 | this.newPrompt = target.value; |
| 467 | } |
| 468 | |
| 469 | async loadSuggestedPrompt() { |
| 470 | try { |
| 471 | this.isSuggestionLoading = true; |
| 472 | this.errorMessage = ""; |
| 473 | |
| 474 | const response = await fetch("./suggest-reprompt"); |
| 475 | if (!response.ok) { |
| 476 | throw new Error(`Failed to load suggestion: ${response.statusText}`); |
| 477 | } |
| 478 | |
| 479 | const data = await response.json(); |
| 480 | this.suggestedPrompt = data.prompt; |
| 481 | } catch (error) { |
| 482 | console.error("Error loading suggested prompt:", error); |
| 483 | this.errorMessage = |
| 484 | error instanceof Error ? error.message : "Failed to load suggestion"; |
| 485 | } finally { |
| 486 | this.isSuggestionLoading = false; |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | async loadOriginalPrompt() { |
| 491 | try { |
| 492 | this.isOriginalPromptLoading = true; |
| 493 | this.errorMessage = ""; |
| 494 | |
| 495 | // Get the first message index from the container state |
| 496 | const firstMessageIndex = this.containerState?.first_message_index || 0; |
| 497 | |
| 498 | // Find the first user message after the first_message_index |
| 499 | let firstUserMessage = ""; |
| 500 | |
| 501 | if (this.messages && this.messages.length > 0) { |
| 502 | for (const msg of this.messages) { |
| 503 | // Only look at messages starting from first_message_index |
| 504 | if (msg.idx >= firstMessageIndex && msg.type === "user") { |
| 505 | // Simply use the content field if it's a string |
| 506 | if (typeof msg.content === "string") { |
| 507 | firstUserMessage = msg.content; |
| 508 | } else { |
| 509 | // Fallback to stringifying content field for any other type |
| 510 | firstUserMessage = JSON.stringify(msg.content); |
| 511 | } |
| 512 | break; |
| 513 | } |
| 514 | } |
| 515 | } |
| 516 | |
| 517 | if (!firstUserMessage) { |
| 518 | console.warn("Could not find original user message", this.messages); |
| 519 | } |
| 520 | |
| 521 | this.originalPrompt = firstUserMessage; |
| 522 | } catch (error) { |
| 523 | console.error("Error loading original prompt:", error); |
| 524 | this.errorMessage = |
| 525 | error instanceof Error |
| 526 | ? error.message |
| 527 | : "Failed to load original prompt"; |
| 528 | } finally { |
| 529 | this.isOriginalPromptLoading = false; |
| 530 | } |
| 531 | } |
| 532 | |
| 533 | async handleRestart() { |
| 534 | try { |
| 535 | this.isLoading = true; |
| 536 | this.errorMessage = ""; |
| 537 | |
| 538 | let revision = ""; |
| 539 | switch (this.restartType) { |
| 540 | case "initial": |
| 541 | // We'll leave revision empty for this case, backend will handle it |
| 542 | break; |
| 543 | case "current": |
| 544 | // We'll leave revision empty for this case too, backend will use current HEAD |
| 545 | break; |
| 546 | case "other": |
| 547 | revision = this.customRevision.trim(); |
| 548 | if (!revision) { |
| 549 | throw new Error("Please enter a valid revision"); |
| 550 | } |
| 551 | break; |
| 552 | } |
| 553 | |
| 554 | // Determine which prompt to use based on selected option |
| 555 | let initialPrompt = ""; |
| 556 | switch (this.promptOption) { |
| 557 | case "suggested": |
| 558 | initialPrompt = this.suggestedPrompt.trim(); |
| 559 | if (!initialPrompt && this.isSuggestionLoading) { |
| 560 | throw new Error( |
| 561 | "Suggested prompt is still loading. Please wait or choose another option.", |
| 562 | ); |
| 563 | } |
| 564 | break; |
| 565 | case "original": |
| 566 | initialPrompt = this.originalPrompt.trim(); |
| 567 | if (!initialPrompt && this.isOriginalPromptLoading) { |
| 568 | throw new Error( |
| 569 | "Original prompt is still loading. Please wait or choose another option.", |
| 570 | ); |
| 571 | } |
| 572 | break; |
| 573 | case "new": |
| 574 | initialPrompt = this.newPrompt.trim(); |
| 575 | break; |
| 576 | } |
| 577 | |
| 578 | // Validate we have a prompt when needed |
| 579 | if (!initialPrompt && this.promptOption !== "new") { |
| 580 | throw new Error( |
| 581 | "Unable to get prompt text. Please enter a new prompt or try again.", |
| 582 | ); |
| 583 | } |
| 584 | |
| 585 | const response = await fetch("./restart", { |
| 586 | method: "POST", |
| 587 | headers: { |
| 588 | "Content-Type": "application/json", |
| 589 | }, |
| 590 | body: JSON.stringify({ |
| 591 | revision: revision, |
| 592 | initial_prompt: initialPrompt, |
| 593 | }), |
| 594 | }); |
| 595 | |
| 596 | if (!response.ok) { |
| 597 | const errorText = await response.text(); |
| 598 | throw new Error(`Failed to restart: ${errorText}`); |
| 599 | } |
| 600 | |
| 601 | // Reload the page after successful restart |
| 602 | window.location.reload(); |
| 603 | } catch (error) { |
| 604 | console.error("Error restarting conversation:", error); |
| 605 | this.errorMessage = |
| 606 | error instanceof Error |
| 607 | ? error.message |
| 608 | : "Failed to restart conversation"; |
| 609 | } finally { |
| 610 | this.isLoading = false; |
| 611 | } |
| 612 | } |
| 613 | |
| 614 | updated(changedProperties: Map<string, any>) { |
| 615 | if (changedProperties.has("open") && this.open) { |
| 616 | // Reset form when opening |
| 617 | this.restartType = "current"; |
| 618 | this.customRevision = ""; |
| 619 | this.promptOption = "suggested"; |
| 620 | this.suggestedPrompt = ""; |
| 621 | this.originalPrompt = ""; |
| 622 | this.newPrompt = ""; |
| 623 | this.errorMessage = ""; |
| 624 | this.commitDescriptions = { |
| 625 | current: "", |
| 626 | initial: "", |
| 627 | }; |
| 628 | |
| 629 | // Pre-load all available prompts and commit descriptions in the background |
| 630 | setTimeout(() => { |
| 631 | // Load prompt data |
| 632 | this.loadSuggestedPrompt(); |
| 633 | this.loadOriginalPrompt(); |
| 634 | |
| 635 | // Load commit descriptions |
| 636 | this.loadCommitDescription("HEAD", "current"); |
| 637 | if (this.containerState?.initial_commit) { |
| 638 | this.loadCommitDescription( |
| 639 | this.containerState.initial_commit, |
| 640 | "initial", |
| 641 | ); |
| 642 | } |
| 643 | |
| 644 | // Set focus to the first radio button for keyboard navigation |
| 645 | if (this.shadowRoot) { |
| 646 | const firstInput = this.shadowRoot.querySelector( |
| 647 | 'input[type="radio"]', |
| 648 | ) as HTMLElement; |
| 649 | if (firstInput) { |
| 650 | firstInput.focus(); |
| 651 | } |
| 652 | } |
| 653 | }, 0); |
| 654 | } |
| 655 | } |
| 656 | |
| 657 | render() { |
| 658 | const inContainer = this.containerState?.in_container || false; |
| 659 | |
| 660 | return html` |
| 661 | <div class="modal-backdrop ${this.open ? "open" : ""}"> |
| 662 | <div class="modal-container"> |
| 663 | <div class="modal-header"> |
| 664 | <h2 class="modal-title">Restart Conversation</h2> |
| 665 | <button class="close-button" @click=${this.closeModal}>×</button> |
| 666 | </div> |
| 667 | |
| 668 | <p class="modal-description"> |
| 669 | Restarting the conversation hides the history from the agent. If you |
| 670 | want the agent to take a different direction, restart with a new |
| 671 | prompt. |
| 672 | </p> |
| 673 | |
| 674 | <div class="form-group"> |
| 675 | <label>Reset to which revision?</label> |
| 676 | <div class="horizontal-radio-group"> |
| 677 | <div |
| 678 | class="revision-option ${this.restartType === "current" |
| 679 | ? "selected" |
| 680 | : ""}" |
| 681 | @click=${() => { |
| 682 | this.restartType = "current"; |
| 683 | this.handleRevisionChange(); |
| 684 | }} |
| 685 | > |
| 686 | <input |
| 687 | type="radio" |
| 688 | id="restart-current" |
| 689 | name="restart-type" |
| 690 | value="current" |
| 691 | ?checked=${this.restartType === "current"} |
| 692 | @change=${this.handleRevisionChange} |
| 693 | tabindex="0" |
| 694 | /> |
| 695 | <label for="restart-current">Current HEAD</label> |
| 696 | ${this.commitDescriptions.current |
| 697 | ? html`<div class="revision-description"> |
| 698 | ${this.commitDescriptions.current} |
| 699 | </div>` |
| 700 | : ""} |
| 701 | </div> |
| 702 | |
| 703 | ${inContainer |
| 704 | ? html` |
| 705 | <div |
| 706 | class="revision-option ${this.restartType === "initial" |
| 707 | ? "selected" |
| 708 | : ""}" |
| 709 | @click=${() => { |
| 710 | this.restartType = "initial"; |
| 711 | this.handleRevisionChange(); |
| 712 | }} |
| 713 | > |
| 714 | <input |
| 715 | type="radio" |
| 716 | id="restart-initial" |
| 717 | name="restart-type" |
| 718 | value="initial" |
| 719 | ?checked=${this.restartType === "initial"} |
| 720 | @change=${this.handleRevisionChange} |
| 721 | tabindex="0" |
| 722 | /> |
| 723 | <label for="restart-initial">Initial commit</label> |
| 724 | ${this.commitDescriptions.initial |
| 725 | ? html`<div class="revision-description"> |
| 726 | ${this.commitDescriptions.initial} |
| 727 | </div>` |
| 728 | : ""} |
| 729 | </div> |
| 730 | |
| 731 | <div |
| 732 | class="revision-option ${this.restartType === "other" |
| 733 | ? "selected" |
| 734 | : ""}" |
| 735 | @click=${() => { |
| 736 | this.restartType = "other"; |
| 737 | this.handleRevisionChange(); |
| 738 | }} |
| 739 | > |
| 740 | <input |
| 741 | type="radio" |
| 742 | id="restart-other" |
| 743 | name="restart-type" |
| 744 | value="other" |
| 745 | ?checked=${this.restartType === "other"} |
| 746 | @change=${this.handleRevisionChange} |
| 747 | tabindex="0" |
| 748 | /> |
| 749 | <label for="restart-other">Other revision</label> |
| 750 | </div> |
| 751 | ` |
| 752 | : html` |
| 753 | <div class="container-message"> |
| 754 | Additional revision options are not available because |
| 755 | Sketch is not running inside a container. |
| 756 | </div> |
| 757 | `} |
| 758 | </div> |
| 759 | |
| 760 | ${this.restartType === "other" && inContainer |
| 761 | ? html` |
| 762 | <input |
| 763 | type="text" |
| 764 | class="custom-revision" |
| 765 | placeholder="Enter commit hash" |
| 766 | .value=${this.customRevision} |
| 767 | @input=${this.handleCustomRevisionChange} |
| 768 | tabindex="0" |
| 769 | /> |
| 770 | ` |
| 771 | : ""} |
| 772 | </div> |
| 773 | |
| 774 | <div class="form-group"> |
| 775 | <label>Prompt options:</label> |
| 776 | <div class="radio-group"> |
| 777 | <div class="radio-option"> |
| 778 | <input |
| 779 | type="radio" |
| 780 | id="prompt-suggested" |
| 781 | name="prompt-type" |
| 782 | value="suggested" |
| 783 | ?checked=${this.promptOption === "suggested"} |
| 784 | @change=${this.handlePromptOptionChange} |
| 785 | tabindex="0" |
| 786 | /> |
| 787 | <label for="prompt-suggested"> |
| 788 | Suggest prompt based on history (default) |
| 789 | </label> |
| 790 | </div> |
| 791 | |
| 792 | <div class="radio-option"> |
| 793 | <input |
| 794 | type="radio" |
| 795 | id="prompt-original" |
| 796 | name="prompt-type" |
| 797 | value="original" |
| 798 | ?checked=${this.promptOption === "original"} |
| 799 | @change=${this.handlePromptOptionChange} |
| 800 | tabindex="0" |
| 801 | /> |
| 802 | <label for="prompt-original"> Original prompt </label> |
| 803 | </div> |
| 804 | |
| 805 | <div class="radio-option"> |
| 806 | <input |
| 807 | type="radio" |
| 808 | id="prompt-new" |
| 809 | name="prompt-type" |
| 810 | value="new" |
| 811 | ?checked=${this.promptOption === "new"} |
| 812 | @change=${this.handlePromptOptionChange} |
| 813 | tabindex="0" |
| 814 | /> |
| 815 | <label for="prompt-new">New prompt</label> |
| 816 | </div> |
| 817 | </div> |
| 818 | </div> |
| 819 | |
| 820 | <div class="prompt-container"> |
| 821 | ${this.promptOption === "suggested" |
| 822 | ? html` |
| 823 | <textarea |
| 824 | class="prompt-textarea${this.isSuggestionLoading |
| 825 | ? " disabled" |
| 826 | : ""}" |
| 827 | placeholder="Loading suggested prompt..." |
| 828 | .value=${this.suggestedPrompt} |
| 829 | ?disabled=${this.isSuggestionLoading} |
| 830 | @input=${this.handleSuggestedPromptChange} |
| 831 | tabindex="0" |
| 832 | ></textarea> |
| 833 | ${this.isSuggestionLoading |
| 834 | ? html` |
| 835 | <div class="loading-overlay"> |
| 836 | <div class="loading-indicator"></div> |
| 837 | </div> |
| 838 | ` |
| 839 | : ""} |
| 840 | ` |
| 841 | : this.promptOption === "original" |
| 842 | ? html` |
| 843 | <textarea |
| 844 | class="prompt-textarea${this.isOriginalPromptLoading |
| 845 | ? " disabled" |
| 846 | : ""}" |
| 847 | placeholder="Loading original prompt..." |
| 848 | .value=${this.originalPrompt} |
| 849 | ?disabled=${this.isOriginalPromptLoading} |
| 850 | @input=${this.handleOriginalPromptChange} |
| 851 | tabindex="0" |
| 852 | ></textarea> |
| 853 | ${this.isOriginalPromptLoading |
| 854 | ? html` |
| 855 | <div class="loading-overlay"> |
| 856 | <div class="loading-indicator"></div> |
| 857 | </div> |
| 858 | ` |
| 859 | : ""} |
| 860 | ` |
| 861 | : html` |
| 862 | <textarea |
| 863 | class="prompt-textarea" |
| 864 | placeholder="Enter a new prompt..." |
| 865 | .value=${this.newPrompt} |
| 866 | @input=${this.handleNewPromptChange} |
| 867 | tabindex="0" |
| 868 | ></textarea> |
| 869 | `} |
| 870 | </div> |
| 871 | |
| 872 | ${this.errorMessage |
| 873 | ? html` <div class="error-message">${this.errorMessage}</div> ` |
| 874 | : ""} |
| 875 | |
| 876 | <div class="actions"> |
| 877 | <button |
| 878 | class="btn btn-cancel" |
| 879 | @click=${this.closeModal} |
| 880 | ?disabled=${this.isLoading} |
| 881 | tabindex="0" |
| 882 | > |
| 883 | Cancel |
| 884 | </button> |
| 885 | <button |
| 886 | class="btn btn-restart" |
| 887 | @click=${this.handleRestart} |
| 888 | ?disabled=${this.isLoading} |
| 889 | tabindex="0" |
| 890 | > |
| 891 | ${this.isLoading |
| 892 | ? html`<span class="loading-indicator"></span>` |
| 893 | : ""} |
| 894 | Restart |
| 895 | </button> |
| 896 | </div> |
| 897 | </div> |
| 898 | </div> |
| 899 | `; |
| 900 | } |
| 901 | } |
| 902 | |
| 903 | declare global { |
| 904 | interface HTMLElementTagNameMap { |
| 905 | "sketch-restart-modal": SketchRestartModal; |
| 906 | } |
| 907 | } |