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'; import CloseIcon from '~icons/material-symbols/close-rounded';
interface Props { interface Props {
top: number;
show: boolean; show: boolean;
suggestion: string; input: string;
onClose: () => void; onClose: () => void;
onHeightChange?: (height: number) => void;
onTryFree: () => 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 textarea = $state<HTMLTextAreaElement>();
let container = $state<HTMLDivElement>(); 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() { function resizeTextarea() {
if (!textarea) return; if (!textarea) return;
const computed = globalThis.getComputedStyle(textarea); const computed = globalThis.getComputedStyle(textarea);
@@ -34,7 +47,7 @@
} }
$effect(() => { $effect(() => {
if (suggestion !== undefined) { if (input !== undefined) {
resizeTextarea(); resizeTextarea();
} }
}); });
@@ -58,21 +71,20 @@
<div <div
bind:this={container} bind:this={container}
class={cn( 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', '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',
!suggestion.trim() && 'rainbow-border' !input.trim() && 'rainbow-border'
)} )}
style="top: {top}px;"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
tabindex="-1"> tabindex="-1">
<div class="relative flex min-h-2 items-start gap-1 px-1"> <div class="relative flex min-h-2 items-start gap-1 px-1">
<textarea <textarea
bind:this={textarea} bind:this={textarea}
bind:value={suggestion} bind:value={input}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
if (suggestion.trim()) { if (input.trim()) {
onTryFree(); onTryFree();
} }
} }
@@ -88,9 +100,8 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="font-recursive text-xs font-normal text-foreground dark:text-foreground" <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 <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" 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}> onclick={onTryFree}>
Try free Try free
+52 -87
View File
@@ -2,7 +2,7 @@
import type { EditorProps } from '$/types'; import type { EditorProps } from '$/types';
import { env } from '$/util/env'; import { env } from '$/util/env';
import { stateStore, urlsStore } from '$/util/state'; import { stateStore, urlsStore } from '$/util/state';
import { isMac } from '$/util/util'; import { AIPromptViewZoneManager } from '$lib/util/AIPromptViewZoneManager';
import { initEditor } from '$lib/util/monacoExtra'; import { initEditor } from '$lib/util/monacoExtra';
import { errorDebug } from '$lib/util/util'; import { errorDebug } from '$lib/util/util';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
@@ -10,11 +10,12 @@
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import AIGlyphPopup from './AIGlyphPopup.svelte'; import AIPromptPopup from './AIPromptPopup.svelte';
const { onUpdate }: EditorProps = $props(); const { onUpdate }: EditorProps = $props();
let divElement: HTMLDivElement | undefined = $state(); let divElement: HTMLDivElement | undefined = $state();
let aiPromptPopupElement: HTMLDivElement | undefined = $state();
let editor: monaco.editor.IStandaloneCodeEditor | undefined; let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let editorOptions = { let editorOptions = {
minimap: { minimap: {
@@ -25,13 +26,11 @@
} satisfies monaco.editor.IStandaloneEditorConstructionOptions; } satisfies monaco.editor.IStandaloneEditorConstructionOptions;
let currentText = ''; let currentText = '';
let showPopup = $state(false); let showPopup = $state(false);
let popupPosition = $state({ top: 0 }); let popupPosition = $state({ top: 0, lineNumber: 0 });
let decorationsCollection: monaco.editor.IEditorDecorationsCollection | undefined; let decorationsCollection: monaco.editor.IEditorDecorationsCollection | undefined;
let hintCollection: monaco.editor.IEditorDecorationsCollection | undefined; let input = $state('');
let suggestion = $state('');
let lastMouseLine = 0; let lastMouseLine = 0;
const aiPromptManager = new AIPromptViewZoneManager();
const quickEditHintStyle = `--quick-edit-hint-text: '${isMac ? 'Quick edit (⌘⏎)' : 'Quick edit (Ctrl+⏎)'}'`;
const jsonModel = monaco.editor.createModel( const jsonModel = monaco.editor.createModel(
'', '',
@@ -44,14 +43,12 @@
monaco.Uri.parse('internal://mermaid.mmd') monaco.Uri.parse('internal://mermaid.mmd')
); );
const renderQuickEditHint = () => { const renderAIPromptGutterGlyphIcon = () => {
hintCollection?.clear();
decorationsCollection?.clear(); decorationsCollection?.clear();
if (!editor || showPopup) { if (!editor || showPopup) {
return; return;
} }
const model = editor.getModel(); const model = editor.getModel();
const position = editor.getPosition();
if (!model) { if (!model) {
return; return;
} }
@@ -61,27 +58,34 @@
{ {
range: new monaco.Range(lastMouseLine, 1, lastMouseLine, 1), range: new monaco.Range(lastMouseLine, 1, lastMouseLine, 1),
options: { options: {
isWholeLine: true,
glyphMarginClassName: 'suggestion-icon' glyphMarginClassName: 'suggestion-icon'
} }
} }
]); ]);
} }
};
if (position) { const closePopup = () => {
const { lineNumber } = position; showPopup = false;
if (model.getLineContent(lineNumber).trim() === '') { input = '';
const column = model.getLineMaxColumn(lineNumber); aiPromptManager.hide();
hintCollection?.set([ renderAIPromptGutterGlyphIcon();
{ };
range: new monaco.Range(lineNumber, column, lineNumber, column),
options: { const toggleAIPopup = (lineNumber: number) => {
afterContentClassName: 'quick-edit-hint' 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(() => { onMount(() => {
@@ -112,45 +116,19 @@
initEditor(monaco); initEditor(monaco);
errorDebug(); errorDebug();
editor = monaco.editor.create(divElement, editorOptions); editor = monaco.editor.create(divElement, editorOptions);
aiPromptManager.setEditor(editor);
decorationsCollection = editor.createDecorationsCollection([]); decorationsCollection = editor.createDecorationsCollection([]);
hintCollection = editor.createDecorationsCollection([]);
editor.onMouseDown((e) => { editor.onMouseDown((e) => {
if ( const isGutter = e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN;
e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN && if (isGutter && e.target.position?.lineNumber === lastMouseLine && lastMouseLine > 0) {
e.target.position?.lineNumber e.event.preventDefault();
) { e.event.stopPropagation();
const mouseEvent = e.event; toggleAIPopup(e.target.position.lineNumber);
if (!divElement) return;
const rect = divElement.getBoundingClientRect();
popupPosition = {
top: mouseEvent.posy - rect.top
};
e.event.browserEvent.stopPropagation();
showPopup = !showPopup;
renderQuickEditHint();
} }
}); });
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 }) => { editor.onDidChangeModelContent(({ isFlush }) => {
renderQuickEditHint();
const newText = editor?.getValue(); const newText = editor?.getValue();
if (!newText || currentText === newText || isFlush) { if (!newText || currentText === newText || isFlush) {
return; return;
@@ -168,7 +146,7 @@
if (editor.getModel()?.id !== model.id) { if (editor.getModel()?.id !== model.id) {
editor.setModel(model); editor.setModel(model);
renderQuickEditHint(); renderAIPromptGutterGlyphIcon();
} }
// Clear decorations if not in 'code' mode, or if the model changes // Clear decorations if not in 'code' mode, or if the model changes
@@ -182,7 +160,7 @@
editor.setScrollTop(0); editor.setScrollTop(0);
editor.setValue(newText); editor.setValue(newText);
currentText = newText; currentText = newText;
renderQuickEditHint(); renderAIPromptGutterGlyphIcon();
} }
// Display/clear errors // Display/clear errors
@@ -195,12 +173,12 @@
if (editor.getModel()?.id !== mermaidModel.id) return; if (editor.getModel()?.id !== mermaidModel.id) return;
lastMouseLine = e.target.position?.lineNumber ?? 0; lastMouseLine = e.target.position?.lineNumber ?? 0;
renderQuickEditHint(); renderAIPromptGutterGlyphIcon();
}); });
editor.onMouseLeave(() => { editor.onMouseLeave(() => {
lastMouseLine = 0; lastMouseLine = 0;
renderQuickEditHint(); renderAIPromptGutterGlyphIcon();
}); });
const unsubscribeMode = mode.subscribe((mode) => { const unsubscribeMode = mode.subscribe((mode) => {
@@ -219,7 +197,7 @@
resizeObserver.observe(divElement); resizeObserver.observe(divElement);
} }
renderQuickEditHint(); renderAIPromptGutterGlyphIcon();
return () => { return () => {
unsubscribeState(); unsubscribeState();
@@ -227,41 +205,28 @@
resizeObserver.disconnect(); resizeObserver.disconnect();
jsonModel.dispose(); jsonModel.dispose();
mermaidModel.dispose(); mermaidModel.dispose();
aiPromptManager.destroy();
editor?.dispose(); editor?.dispose();
}; };
}); });
</script> </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> <div bind:this={divElement} id="editor" class="h-full w-full"></div>
<AIGlyphPopup <div bind:this={aiPromptPopupElement}>
top={popupPosition.top} <AIPromptPopup
show={showPopup} show={showPopup}
bind:suggestion bind:input
onClose={() => { onHeightChange={(height) => aiPromptManager.updateHeight(height)}
showPopup = false; onClose={closePopup}
suggestion = ''; onTryFree={() => {
renderQuickEditHint(); window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank');
}} closePopup();
onTryFree={() => { }} />
window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank'); </div>
showPopup = false;
suggestion = '';
renderQuickEditHint();
}} />
</div> </div>
<style> <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) { :global(.suggestion-icon) {
background-color: #e8eaf9; background-color: #e8eaf9;
width: 20px !important; 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;
}
}