Fix AI inline popup issues and remove quick edit functionality

This commit is contained in:
saurabhg772244
2026-02-13 13:54:39 +05:30
parent 1166479277
commit 6160617458
3 changed files with 145 additions and 98 deletions
@@ -4,18 +4,31 @@
import CloseIcon from '~icons/material-symbols/close-rounded';
interface Props {
top: number;
show: boolean;
suggestion: string;
input: string;
onClose: () => void;
onHeightChange?: (height: number) => void;
onTryFree: () => void;
}
let { top, show, suggestion = $bindable(), onClose, onTryFree }: Props = $props();
let { show, input = $bindable(), onClose, onHeightChange, onTryFree }: Props = $props();
let textarea = $state<HTMLTextAreaElement>();
let container = $state<HTMLDivElement>();
$effect(() => {
if (!container || !onHeightChange) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target instanceof HTMLElement) {
onHeightChange(entry.target.offsetHeight);
}
}
});
observer.observe(container);
return () => observer.disconnect();
});
function resizeTextarea() {
if (!textarea) return;
const computed = globalThis.getComputedStyle(textarea);
@@ -34,7 +47,7 @@
}
$effect(() => {
if (suggestion !== undefined) {
if (input !== undefined) {
resizeTextarea();
}
});
@@ -58,21 +71,20 @@
<div
bind:this={container}
class={cn(
'button-container-for-animation absolute right-4 left-12 z-50 flex flex-col gap-2 rounded-xl border-2 border-border bg-background p-2 shadow-xl dark:border-border-dark dark:bg-secondary',
!suggestion.trim() && 'rainbow-border'
'button-container-for-animation relative z-50 mr-6 flex w-auto flex-col gap-2 rounded-xl border-2 border-border bg-background p-2 shadow-xl dark:border-border-dark dark:bg-secondary',
!input.trim() && 'rainbow-border'
)}
style="top: {top}px;"
role="dialog"
aria-modal="true"
tabindex="-1">
<div class="relative flex min-h-2 items-start gap-1 px-1">
<textarea
bind:this={textarea}
bind:value={suggestion}
bind:value={input}
onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (suggestion.trim()) {
if (input.trim()) {
onTryFree();
}
}
@@ -88,9 +100,8 @@
<div class="flex items-center justify-between">
<span class="font-recursive text-xs font-normal text-foreground dark:text-foreground"
>Signup to Mermaid.ai to try AI</span>
>Sign Up at Mermaid.ai to try AI</span>
<Button
disabled={!suggestion.trim()}
class="font-recursive h-6 w-16 gap-1.5 rounded-sm bg-accent p-1 text-xs font-medium text-white no-underline hover:bg-accent/90 hover:text-white hover:no-underline active:bg-accent/80 dark:bg-accent dark:text-white! dark:hover:bg-accent/90 dark:active:bg-accent/80"
onclick={onTryFree}>
Try free
+52 -87
View File
@@ -2,7 +2,7 @@
import type { EditorProps } from '$/types';
import { env } from '$/util/env';
import { stateStore, urlsStore } from '$/util/state';
import { isMac } from '$/util/util';
import { AIPromptViewZoneManager } from '$lib/util/AIPromptViewZoneManager';
import { initEditor } from '$lib/util/monacoExtra';
import { errorDebug } from '$lib/util/util';
import { mode } from 'mode-watcher';
@@ -10,11 +10,12 @@
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { onMount } from 'svelte';
import AIGlyphPopup from './AIGlyphPopup.svelte';
import AIPromptPopup from './AIPromptPopup.svelte';
const { onUpdate }: EditorProps = $props();
let divElement: HTMLDivElement | undefined = $state();
let aiPromptPopupElement: HTMLDivElement | undefined = $state();
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let editorOptions = {
minimap: {
@@ -25,13 +26,11 @@
} satisfies monaco.editor.IStandaloneEditorConstructionOptions;
let currentText = '';
let showPopup = $state(false);
let popupPosition = $state({ top: 0 });
let popupPosition = $state({ top: 0, lineNumber: 0 });
let decorationsCollection: monaco.editor.IEditorDecorationsCollection | undefined;
let hintCollection: monaco.editor.IEditorDecorationsCollection | undefined;
let suggestion = $state('');
let input = $state('');
let lastMouseLine = 0;
const quickEditHintStyle = `--quick-edit-hint-text: '${isMac ? 'Quick edit (⌘⏎)' : 'Quick edit (Ctrl+⏎)'}'`;
const aiPromptManager = new AIPromptViewZoneManager();
const jsonModel = monaco.editor.createModel(
'',
@@ -44,14 +43,12 @@
monaco.Uri.parse('internal://mermaid.mmd')
);
const renderQuickEditHint = () => {
hintCollection?.clear();
const renderAIPromptGutterGlyphIcon = () => {
decorationsCollection?.clear();
if (!editor || showPopup) {
return;
}
const model = editor.getModel();
const position = editor.getPosition();
if (!model) {
return;
}
@@ -61,27 +58,34 @@
{
range: new monaco.Range(lastMouseLine, 1, lastMouseLine, 1),
options: {
isWholeLine: true,
glyphMarginClassName: 'suggestion-icon'
}
}
]);
}
};
if (position) {
const { lineNumber } = position;
if (model.getLineContent(lineNumber).trim() === '') {
const column = model.getLineMaxColumn(lineNumber);
hintCollection?.set([
{
range: new monaco.Range(lineNumber, column, lineNumber, column),
options: {
afterContentClassName: 'quick-edit-hint'
}
}
]);
}
const closePopup = () => {
showPopup = false;
input = '';
aiPromptManager.hide();
renderAIPromptGutterGlyphIcon();
};
const toggleAIPopup = (lineNumber: number) => {
if (!divElement || !aiPromptPopupElement) return;
popupPosition = {
top: 0,
lineNumber
};
showPopup = !showPopup;
if (showPopup) {
aiPromptManager.show(popupPosition.lineNumber, aiPromptPopupElement, 100);
editor?.setSelection(new monaco.Range(0, 0, 0, 0));
} else {
aiPromptManager.hide();
}
renderAIPromptGutterGlyphIcon();
};
onMount(() => {
@@ -112,45 +116,19 @@
initEditor(monaco);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
aiPromptManager.setEditor(editor);
decorationsCollection = editor.createDecorationsCollection([]);
hintCollection = editor.createDecorationsCollection([]);
editor.onMouseDown((e) => {
if (
e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN &&
e.target.position?.lineNumber
) {
const mouseEvent = e.event;
if (!divElement) return;
const rect = divElement.getBoundingClientRect();
popupPosition = {
top: mouseEvent.posy - rect.top
};
e.event.browserEvent.stopPropagation();
showPopup = !showPopup;
renderQuickEditHint();
const isGutter = e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN;
if (isGutter && e.target.position?.lineNumber === lastMouseLine && lastMouseLine > 0) {
e.event.preventDefault();
e.event.stopPropagation();
toggleAIPopup(e.target.position.lineNumber);
}
});
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
const position = editor?.getPosition();
if (position) {
popupPosition = {
top:
(editor?.getTopForLineNumber(position.lineNumber) ?? 0) - (editor?.getScrollTop() ?? 0)
};
showPopup = true;
}
renderQuickEditHint();
});
editor.onDidChangeCursorPosition(() => {
renderQuickEditHint();
});
editor.onDidChangeModelContent(({ isFlush }) => {
renderQuickEditHint();
const newText = editor?.getValue();
if (!newText || currentText === newText || isFlush) {
return;
@@ -168,7 +146,7 @@
if (editor.getModel()?.id !== model.id) {
editor.setModel(model);
renderQuickEditHint();
renderAIPromptGutterGlyphIcon();
}
// Clear decorations if not in 'code' mode, or if the model changes
@@ -182,7 +160,7 @@
editor.setScrollTop(0);
editor.setValue(newText);
currentText = newText;
renderQuickEditHint();
renderAIPromptGutterGlyphIcon();
}
// Display/clear errors
@@ -195,12 +173,12 @@
if (editor.getModel()?.id !== mermaidModel.id) return;
lastMouseLine = e.target.position?.lineNumber ?? 0;
renderQuickEditHint();
renderAIPromptGutterGlyphIcon();
});
editor.onMouseLeave(() => {
lastMouseLine = 0;
renderQuickEditHint();
renderAIPromptGutterGlyphIcon();
});
const unsubscribeMode = mode.subscribe((mode) => {
@@ -219,7 +197,7 @@
resizeObserver.observe(divElement);
}
renderQuickEditHint();
renderAIPromptGutterGlyphIcon();
return () => {
unsubscribeState();
@@ -227,41 +205,28 @@
resizeObserver.disconnect();
jsonModel.dispose();
mermaidModel.dispose();
aiPromptManager.destroy();
editor?.dispose();
};
});
</script>
<div class="relative h-full grow overflow-hidden" style={quickEditHintStyle}>
<div class="relative h-full grow overflow-hidden">
<div bind:this={divElement} id="editor" class="h-full w-full"></div>
<AIGlyphPopup
top={popupPosition.top}
show={showPopup}
bind:suggestion
onClose={() => {
showPopup = false;
suggestion = '';
renderQuickEditHint();
}}
onTryFree={() => {
window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank');
showPopup = false;
suggestion = '';
renderQuickEditHint();
}} />
<div bind:this={aiPromptPopupElement}>
<AIPromptPopup
show={showPopup}
bind:input
onHeightChange={(height) => aiPromptManager.updateHeight(height)}
onClose={closePopup}
onTryFree={() => {
window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank');
closePopup();
}} />
</div>
</div>
<style>
:global(.quick-edit-hint::after) {
content: var(--quick-edit-hint-text);
color: #a1a1aa;
font-size: small;
pointer-events: none;
user-select: none;
margin-left: 4px;
opacity: 0.6;
}
:global(.suggestion-icon) {
background-color: #e8eaf9;
width: 20px !important;
+71
View File
@@ -0,0 +1,71 @@
import type * as monaco from 'monaco-editor';
export class AIPromptViewZoneManager {
private editor: monaco.editor.IStandaloneCodeEditor | null = null;
private viewZoneId: string | null = null;
private viewZoneNode: HTMLElement | null = null;
private viewZone: monaco.editor.IViewZone | null = null;
private topPopupSpace = 8;
public setEditor(editor: monaco.editor.IStandaloneCodeEditor): void {
this.editor = editor;
}
public show(lineNumber: number, node: HTMLElement, height: number): void {
if (!this.editor) return;
this.viewZoneNode = node;
this.editor.changeViewZones((changeAccessor) => {
if (this.viewZoneId) {
changeAccessor.removeZone(this.viewZoneId);
}
const domNode = document.createElement('div');
domNode.style.top = '0px';
domNode.style.paddingTop = '8px';
domNode.style.paddingBottom = '0px';
domNode.appendChild(this.viewZoneNode as Node);
this.viewZone = {
afterLineNumber: lineNumber,
heightInPx: height + this.topPopupSpace,
domNode: domNode
};
this.viewZoneId = changeAccessor.addZone(this.viewZone);
});
}
public updateHeight(height: number): void {
if (!this.editor || !this.viewZoneId || !this.viewZone) return;
this.viewZone.heightInPx = height + this.topPopupSpace;
this.editor.changeViewZones((changeAccessor) => {
if (this.viewZoneId) {
changeAccessor.layoutZone(this.viewZoneId);
}
});
}
public hide(): void {
if (!this.editor || !this.viewZoneId) return;
this.editor.changeViewZones((changeAccessor) => {
if (this.viewZoneId) {
changeAccessor.removeZone(this.viewZoneId);
this.viewZoneId = null;
this.viewZone = null;
}
});
if (this.viewZoneNode) {
this.viewZoneNode = null;
}
}
public destroy(): void {
this.hide();
this.editor = null;
}
}