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;
+ }
+}