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';
|
||||
|
||||
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
|
||||
@@ -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;
|
||||
|
||||
@@ -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