Fix AI inline popup issues and remove quick edit functionality
This commit is contained in:
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user