diff --git a/mesop/web/src/app/styles.scss b/mesop/web/src/app/styles.scss index 4cfbc2dd4..025a0f001 100644 --- a/mesop/web/src/app/styles.scss +++ b/mesop/web/src/app/styles.scss @@ -175,10 +175,14 @@ body { @include mat.all-component-themes($light-theme); @include mat.color-variants-backwards-compatibility($light-theme); @include mat.system-level-colors($light-theme); + + --mesop-editor-toolbar-background-rgb: 240, 237, 241; } body.dark-theme { @include mat.system-level-colors($dark-theme); + + --mesop-editor-toolbar-background-rgb: 31, 31, 35; } $density-scales: (-1, -2, -3, -4); @@ -782,3 +786,9 @@ span.linenos.special { .dark-theme .highlight .il { color: #a5d6ff; } /* Literal.Number.Integer.Long */ + +// Styles needed for the editor toolbar. + +.floating-editor-toolbar-autocomplete { + --mat-option-label-text-size: 0.9rem; +} diff --git a/mesop/web/src/editor_toolbar/editor_toolbar.ng.html b/mesop/web/src/editor_toolbar/editor_toolbar.ng.html index 3dc1976b9..ba94ced79 100644 --- a/mesop/web/src/editor_toolbar/editor_toolbar.ng.html +++ b/mesop/web/src/editor_toolbar/editor_toolbar.ng.html @@ -18,8 +18,22 @@ placeholder="Type in the UI change you want..." class="floating-editor-toolbar-textarea" [(ngModel)]="prompt" + (ngModelChange)="onPromptChange($event)" (keydown.enter)="onEnter($event)" + [matAutocomplete]="auto" > + + @for (option of filteredOptions; track option) { + + {{option.icon}} + + + } + @if (responseTime > 0) {
{{responseTime | number:'1.1-1'}}s diff --git a/mesop/web/src/editor_toolbar/editor_toolbar.scss b/mesop/web/src/editor_toolbar/editor_toolbar.scss index 6c71d3553..d1293a477 100644 --- a/mesop/web/src/editor_toolbar/editor_toolbar.scss +++ b/mesop/web/src/editor_toolbar/editor_toolbar.scss @@ -5,7 +5,7 @@ bottom: 28px; height: 48px; z-index: 9; - background: rgba(var(--sys-surface-bright), 1); + background: rgba(var(--mesop-editor-toolbar-background-rgb), 0.7); backdrop-filter: blur(10px); border-radius: 16px; padding: 0 12px; @@ -15,6 +15,11 @@ transition: all 0.3s ease; } +.floating-editor-toolbar:hover, +.floating-editor-toolbar:focus-within { + background: rgba(var(--mesop-editor-toolbar-background-rgb), 0.9); +} + .drag-handle { cursor: grab; } @@ -31,7 +36,7 @@ .floating-editor-toolbar-textarea { border: 0; - background: inherit; + background: transparent; color: var(--sys-on-primary-container); text-shadow: 0 1px 2px rgba(var(--sys-on-primary-container), 0.1); @@ -43,6 +48,6 @@ outline: none; resize: none; overflow: hidden; - margin-top: 4px; + margin-top: 8px; font-size: 0.9rem; } diff --git a/mesop/web/src/editor_toolbar/editor_toolbar.ts b/mesop/web/src/editor_toolbar/editor_toolbar.ts index b12d4d688..18d84685a 100644 --- a/mesop/web/src/editor_toolbar/editor_toolbar.ts +++ b/mesop/web/src/editor_toolbar/editor_toolbar.ts @@ -19,6 +19,20 @@ import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar'; import {CommonModule} from '@angular/common'; import {ErrorDialogService} from '../services/error_dialog_service'; import {MatTooltipModule} from '@angular/material/tooltip'; +import { + MAT_AUTOCOMPLETE_SCROLL_STRATEGY, + MatAutocomplete, + MatAutocompleteModule, + MatAutocompleteTrigger, +} from '@angular/material/autocomplete'; +import {EditorToolbarAutocompleteService} from './editor_toolbar_autocomplete_service'; +import {Overlay} from '@angular/cdk/overlay'; +import {HighlightPipe} from './highlight_pipe'; + +interface PromptOption { + prompt: string; + icon: string; +} @Component({ selector: 'mesop-editor-toolbar', @@ -32,6 +46,15 @@ import {MatTooltipModule} from '@angular/material/tooltip'; MatSnackBarModule, CommonModule, CdkDrag, + MatAutocompleteModule, + HighlightPipe, + ], + providers: [ + { + provide: MAT_AUTOCOMPLETE_SCROLL_STRATEGY, + useFactory: (overlay: Overlay) => () => overlay.scrollStrategies.close(), + deps: [Overlay], + }, ], }) export class EditorToolbar implements OnInit { @@ -42,12 +65,17 @@ export class EditorToolbar implements OnInit { isToolbarExpanded = true; position: {x: number | null; y: number | null} = {x: null, y: null}; @ViewChild('toolbar', {static: true}) toolbar!: ElementRef; + @ViewChild(MatAutocompleteTrigger) + autocompleteTrigger!: MatAutocompleteTrigger; + @ViewChild(MatAutocomplete) + autocomplete!: MatAutocomplete; constructor( private editorToolbarService: EditorToolbarService, private dialog: MatDialog, private snackBar: MatSnackBar, private errorDialogService: ErrorDialogService, + private autocompleteService: EditorToolbarAutocompleteService, ) { const savedState = localStorage.getItem('isToolbarExpanded'); this.isToolbarExpanded = savedState === 'true'; @@ -57,6 +85,25 @@ export class EditorToolbar implements OnInit { this.loadSavedPosition(); } + ngAfterViewInit() { + this.setupAutocompleteScrolling(); + } + + setupAutocompleteScrolling() { + this.autocomplete.opened.subscribe(() => { + setTimeout(() => { + if (this.autocomplete?.panel) { + this.autocomplete.panel.nativeElement.scrollTop = + this.autocomplete.panel.nativeElement.scrollHeight; + } + }); + }); + } + + get filteredOptions(): PromptOption[] { + return this.autocompleteService.getFilteredOptions(); + } + toggleToolbar() { this.isToolbarExpanded = !this.isToolbarExpanded; localStorage.setItem( @@ -73,11 +120,16 @@ export class EditorToolbar implements OnInit { async onEnter(event: Event) { event.preventDefault(); + + if (this.autocompleteTrigger.panelOpen) { + this.autocompleteTrigger.closePanel(); + } await this.sendPrompt(); } async sendPrompt() { const prompt = this.prompt; + this.autocompleteService.addHistoryOption(prompt); this.prompt = ''; this.isLoading = true; this.responseTime = 0; @@ -96,6 +148,7 @@ export class EditorToolbar implements OnInit { if (result) { await this.editorToolbarService.commit(response.afterCode); } + this.autocompleteTrigger.closePanel(); }); } catch (error) { console.error('Error:', error); @@ -191,6 +244,10 @@ export class EditorToolbar implements OnInit { JSON.stringify(this.position), ); } + + onPromptChange(newValue: string) { + this.autocompleteService.updateFilteredOptions(newValue); + } } @Component({ diff --git a/mesop/web/src/editor_toolbar/editor_toolbar_autocomplete_service.ts b/mesop/web/src/editor_toolbar/editor_toolbar_autocomplete_service.ts new file mode 100644 index 000000000..03b4767f8 --- /dev/null +++ b/mesop/web/src/editor_toolbar/editor_toolbar_autocomplete_service.ts @@ -0,0 +1,61 @@ +import {Injectable} from '@angular/core'; + +export interface PromptOption { + prompt: string; + icon: string; +} + +const HISTORY_OPTIONS_KEY = 'MESOP://PROMPT_HISTORY'; + +@Injectable({ + providedIn: 'root', +}) +export class EditorToolbarAutocompleteService { + private allOptions: PromptOption[] = [ + {prompt: 'Add text to the bottom', icon: 'prompt_suggestion'}, + {prompt: 'Create a card', icon: 'prompt_suggestion'}, + {prompt: 'Make it prettier', icon: 'prompt_suggestion'}, + {prompt: 'Add a button', icon: 'prompt_suggestion'}, + {prompt: 'Create a 3x3 grid', icon: 'prompt_suggestion'}, + {prompt: 'Create a side-by-side layout', icon: 'prompt_suggestion'}, + ]; + filteredOptions: PromptOption[]; + + constructor() { + this.loadHistoryOptions(); + this.filteredOptions = this.allOptions; + } + + private loadHistoryOptions() { + const historyOptions = sessionStorage.getItem(HISTORY_OPTIONS_KEY); + if (historyOptions) { + const parsedOptions = JSON.parse(historyOptions) as string[]; + parsedOptions.forEach((option) => { + this.allOptions.push({prompt: option, icon: 'history'}); + }); + } + } + + getFilteredOptions(): PromptOption[] { + return this.filteredOptions; + } + + addHistoryOption(option: string) { + this.allOptions.push({prompt: option, icon: 'history'}); + this.saveHistoryOptions(); + } + + updateFilteredOptions(prompt: string) { + this.filteredOptions = this.allOptions.filter((opt) => + opt.prompt.toLowerCase().includes(prompt.toLowerCase()), + ); + } + + private saveHistoryOptions() { + const historyOptions = this.allOptions + .filter((opt) => opt.icon === 'history') + .map((opt) => opt.prompt) + .slice(-100); // Keep only the 100 most recent options + sessionStorage.setItem(HISTORY_OPTIONS_KEY, JSON.stringify(historyOptions)); + } +} diff --git a/mesop/web/src/editor_toolbar/highlight_pipe.ts b/mesop/web/src/editor_toolbar/highlight_pipe.ts new file mode 100644 index 000000000..ad9cb93b7 --- /dev/null +++ b/mesop/web/src/editor_toolbar/highlight_pipe.ts @@ -0,0 +1,15 @@ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({name: 'highlight', standalone: true}) +export class HighlightPipe implements PipeTransform { + transform(text: string, search: string): string { + const pattern = search + .replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') + .split(' ') + .filter((t) => t.length > 0) + .join('|'); + const regex = new RegExp(pattern, 'gi'); + + return search ? text.replace(regex, (match) => `${match}`) : text; + } +}