From 830df8c44bb19c858dde3d32f2ee22e05fec6204 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Thu, 12 Feb 2026 13:13:16 +0530 Subject: [PATCH 1/6] add mermaid inline AI --- src/lib/components/AIGlyphPopup.svelte | 148 +++++++++++++++++++++++ src/lib/components/DesktopEditor.svelte | 149 +++++++++++++++++++++++- src/lib/constants.ts | 1 + src/lib/util/state.ts | 8 +- src/lib/util/util.ts | 8 ++ static/icons/use-chat.svg | 9 ++ 6 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 src/lib/components/AIGlyphPopup.svelte create mode 100644 static/icons/use-chat.svg diff --git a/src/lib/components/AIGlyphPopup.svelte b/src/lib/components/AIGlyphPopup.svelte new file mode 100644 index 00000000..53950b59 --- /dev/null +++ b/src/lib/components/AIGlyphPopup.svelte @@ -0,0 +1,148 @@ + + +{#if show} + +{/if} + + diff --git a/src/lib/components/DesktopEditor.svelte b/src/lib/components/DesktopEditor.svelte index cb54ac5b..4e66d2d2 100644 --- a/src/lib/components/DesktopEditor.svelte +++ b/src/lib/components/DesktopEditor.svelte @@ -1,7 +1,8 @@ -
+
+
+ { + showPopup = false; + suggestion = ''; + renderQuickEditHint(); + }} + ontryFree={() => { + window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank'); + showPopup = false; + suggestion = ''; + renderQuickEditHint(); + }} /> +
+ + diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2e71e336..9a83ddba 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -10,5 +10,6 @@ export const TID = { } as const; export const C = { + aiLiveEditor: 'ai_live_editor', utmSource: 'mermaid_live_editor' } as const; diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index 06ffb259..c4dc0e05 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -1,4 +1,3 @@ -import { C } from '$/constants'; import type { ErrorHash, MarkerData, State, ValidatedState } from '$/types'; import { debounce } from 'lodash-es'; import type { MermaidConfig } from 'mermaid'; @@ -12,7 +11,7 @@ import { import { parse } from './mermaid'; import { localStorage, persist } from './persist'; import { deserializeState, pakoSerde, serializeState } from './serde'; -import { errorDebug, formatJSON, MCBaseURL } from './util'; +import { errorDebug, formatJSON, getSource, MCBaseURL } from './util'; export const defaultState: State = { code: `flowchart TD @@ -140,10 +139,11 @@ export const urlsStore = derived([stateStore], ([{ code, serialized }]) => { mermaidChart: ({ medium }: { - medium: 'ai_repair' | 'main_menu' | 'save_diagram' | 'share'; + medium: 'ai_repair' | 'main_menu' | 'save_diagram' | 'share' | 'vibe_diagramming'; }) => { + const utmSource = getSource(); const params = new URLSearchParams({ - utm_source: C.utmSource, + utm_source: utmSource, utm_medium: medium }).toString(); return { diff --git a/src/lib/util/util.ts b/src/lib/util/util.ts index 08552c55..790d2802 100644 --- a/src/lib/util/util.ts +++ b/src/lib/util/util.ts @@ -1,3 +1,4 @@ +import { C } from '../constants'; import { env } from './env'; import { loadDataFromUrl } from './fileLoaders/loader'; import { initLoading } from './loading'; @@ -11,6 +12,13 @@ export const getDomain = (url?: string): string => { return domain; }; +export const getSource = (): string => { + if (typeof window !== 'undefined' && window.location.host.includes('mermaid.ai')) { + return C.aiLiveEditor; + } + return C.utmSource; +}; + export const loadStateFromURL = (): void => { loadState(window.location.hash.slice(1)); }; diff --git a/static/icons/use-chat.svg b/static/icons/use-chat.svg new file mode 100644 index 00000000..86ced9d4 --- /dev/null +++ b/static/icons/use-chat.svg @@ -0,0 +1,9 @@ + \ No newline at end of file From aa3479d5083f83faf31c070eec9cd6ffef8ba84e Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Thu, 12 Feb 2026 15:52:21 +0530 Subject: [PATCH 2/6] code refactor and fix icon hover issue --- src/lib/components/AIGlyphPopup.svelte | 19 ++++--- src/lib/components/DesktopEditor.svelte | 66 ++++++++++++++++--------- src/lib/util/state.ts | 4 +- src/lib/util/util.ts | 16 +++--- 4 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/lib/components/AIGlyphPopup.svelte b/src/lib/components/AIGlyphPopup.svelte index 53950b59..e50bec46 100644 --- a/src/lib/components/AIGlyphPopup.svelte +++ b/src/lib/components/AIGlyphPopup.svelte @@ -7,11 +7,11 @@ top: number; show: boolean; suggestion: string; - onclose: () => void; - ontryFree: () => void; + onClose: () => void; + onTryFree: () => void; } - let { top, show, suggestion = $bindable(), onclose, ontryFree }: Props = $props(); + let { top, show, suggestion = $bindable(), onClose, onTryFree }: Props = $props(); let textarea = $state(); @@ -42,8 +42,7 @@ {#if show}
-
- Signup to Mermaid.ai to try AI
diff --git a/src/lib/components/DesktopEditor.svelte b/src/lib/components/DesktopEditor.svelte index 4e66d2d2..ebe3b422 100644 --- a/src/lib/components/DesktopEditor.svelte +++ b/src/lib/components/DesktopEditor.svelte @@ -29,6 +29,7 @@ let decorationsCollection: monaco.editor.IEditorDecorationsCollection | undefined; let hintCollection: monaco.editor.IEditorDecorationsCollection | undefined; let suggestion = $state(''); + let lastMouseLine = 0; const quickEditHintStyle = `--quick-edit-hint-text: '${isMac ? 'Quick edit (⌘⏎)' : 'Quick edit (Ctrl+⏎)'}'`; @@ -45,27 +46,42 @@ const renderQuickEditHint = () => { hintCollection?.clear(); + decorationsCollection?.clear(); if (!editor || showPopup) { return; } const model = editor.getModel(); const position = editor.getPosition(); - if (!model || !position) { + if (!model) { return; } - const { lineNumber } = position; - if (model.getLineContent(lineNumber).trim() === '') { - const column = model.getLineMaxColumn(lineNumber); - hintCollection?.set([ + if (lastMouseLine > 0 && model.id === mermaidModel.id) { + decorationsCollection?.set([ { - range: new monaco.Range(lineNumber, column, lineNumber, column), + range: new monaco.Range(lastMouseLine, 1, lastMouseLine, 1), options: { - afterContentClassName: 'quick-edit-hint' + 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' + } + } + ]); + } + } }; onMount(() => { @@ -115,6 +131,19 @@ } }); + 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(); }); @@ -164,24 +193,13 @@ if (showPopup) return; if (editor.getModel()?.id !== mermaidModel.id) return; - if (e.target.position?.lineNumber) { - const line = e.target.position.lineNumber; - decorationsCollection?.set([ - { - range: new monaco.Range(line, 1, line, 1), - options: { - isWholeLine: true, - glyphMarginClassName: 'suggestion-icon' - } - } - ]); - } else { - decorationsCollection?.clear(); - } + lastMouseLine = e.target.position?.lineNumber ?? 0; + renderQuickEditHint(); }); editor.onMouseLeave(() => { - decorationsCollection?.clear(); + lastMouseLine = 0; + renderQuickEditHint(); }); const unsubscribeMode = mode.subscribe((mode) => { @@ -219,12 +237,12 @@ top={popupPosition.top} show={showPopup} bind:suggestion - onclose={() => { + onClose={() => { showPopup = false; suggestion = ''; renderQuickEditHint(); }} - ontryFree={() => { + onTryFree={() => { window.open($urlsStore.mermaidChart({ medium: 'vibe_diagramming' }).save, '_blank'); showPopup = false; suggestion = ''; diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index c4dc0e05..b52cd78a 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -11,7 +11,7 @@ import { import { parse } from './mermaid'; import { localStorage, persist } from './persist'; import { deserializeState, pakoSerde, serializeState } from './serde'; -import { errorDebug, formatJSON, getSource, MCBaseURL } from './util'; +import { errorDebug, formatJSON, getUTMSource, MCBaseURL } from './util'; export const defaultState: State = { code: `flowchart TD @@ -141,7 +141,7 @@ export const urlsStore = derived([stateStore], ([{ code, serialized }]) => { }: { medium: 'ai_repair' | 'main_menu' | 'save_diagram' | 'share' | 'vibe_diagramming'; }) => { - const utmSource = getSource(); + const utmSource = getUTMSource(); const params = new URLSearchParams({ utm_source: utmSource, utm_medium: medium diff --git a/src/lib/util/util.ts b/src/lib/util/util.ts index 790d2802..8eb5e2be 100644 --- a/src/lib/util/util.ts +++ b/src/lib/util/util.ts @@ -1,4 +1,4 @@ -import { C } from '../constants'; +import { C } from '$/constants'; import { env } from './env'; import { loadDataFromUrl } from './fileLoaders/loader'; import { initLoading } from './loading'; @@ -12,13 +12,6 @@ export const getDomain = (url?: string): string => { return domain; }; -export const getSource = (): string => { - if (typeof window !== 'undefined' && window.location.host.includes('mermaid.ai')) { - return C.aiLiveEditor; - } - return C.utmSource; -}; - export const loadStateFromURL = (): void => { loadState(window.location.hash.slice(1)); }; @@ -96,3 +89,10 @@ function fallbackCopyToClipboard(text: string) { textArea.remove(); } } + +export const getUTMSource = (): string => { + if (typeof window !== 'undefined' && window.location.host.includes('mermaid.ai')) { + return C.aiLiveEditor; + } + return C.utmSource; +}; From 958e7aa62c213fbc826863319b1b8be3160e7ad4 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Thu, 12 Feb 2026 19:51:54 +0530 Subject: [PATCH 3/6] close popup on outside click and escape button --- src/lib/components/AIGlyphPopup.svelte | 16 ++++++++++++++++ src/lib/components/DesktopEditor.svelte | 1 + 2 files changed, 17 insertions(+) diff --git a/src/lib/components/AIGlyphPopup.svelte b/src/lib/components/AIGlyphPopup.svelte index e50bec46..fb68de86 100644 --- a/src/lib/components/AIGlyphPopup.svelte +++ b/src/lib/components/AIGlyphPopup.svelte @@ -14,6 +14,7 @@ let { top, show, suggestion = $bindable(), onClose, onTryFree }: Props = $props(); let textarea = $state(); + let container = $state(); function resizeTextarea() { if (!textarea) return; @@ -37,10 +38,25 @@ resizeTextarea(); } }); + + function handleKeydown(e: KeyboardEvent) { + if (show && e.key === 'Escape') { + onClose(); + } + } + + function handleOutsideClick(e: MouseEvent) { + if (show && container && e.target instanceof Node && !container.contains(e.target)) { + onClose(); + } + } + + {#if show}
Date: Thu, 12 Feb 2026 20:02:48 +0530 Subject: [PATCH 4/6] remove unused global style --- src/lib/components/DesktopEditor.svelte | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lib/components/DesktopEditor.svelte b/src/lib/components/DesktopEditor.svelte index 9b6a0791..c47b5be1 100644 --- a/src/lib/components/DesktopEditor.svelte +++ b/src/lib/components/DesktopEditor.svelte @@ -262,14 +262,6 @@ opacity: 0.6; } - :global(.ai-glyph-margin) { - background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="24" height="24" viewBox="0 0 24 24"%3E%3Cpath fill="%23a855f7" d="M9 21.035l-1.185-5.917L2 14.154l5.815-1.139L9 7.098l1.328 5.917l5.672 1.139l-5.672 1.157L9 21.035zM15.42 11.22l-1.614-2.822l-2.86-.948l2.86-.928l1.614-2.86l1.248 2.86l2.902.928l-2.902.948l-1.248 2.822zm2.083 10.39l-1.04-2.583l-2.607-.647l2.607-.625l1.04-2.606l.855 2.606l2.585.625l-2.585.647l-.855 2.583z"%2F%3E%3C%2Fsvg%3E'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - } - :global(.suggestion-icon) { background-color: #e8eaf9; width: 20px !important; From 61606174582465c8f08688517fb702e1cf4bf395 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Fri, 13 Feb 2026 13:54:39 +0530 Subject: [PATCH 5/6] Fix AI inline popup issues and remove quick edit functionality --- ...GlyphPopup.svelte => AIPromptPopup.svelte} | 33 +++-- src/lib/components/DesktopEditor.svelte | 139 +++++++----------- src/lib/util/AIPromptViewZoneManager.ts | 71 +++++++++ 3 files changed, 145 insertions(+), 98 deletions(-) rename src/lib/components/{AIGlyphPopup.svelte => AIPromptPopup.svelte} (84%) create mode 100644 src/lib/util/AIPromptViewZoneManager.ts diff --git a/src/lib/components/AIGlyphPopup.svelte b/src/lib/components/AIPromptPopup.svelte similarity index 84% rename from src/lib/components/AIGlyphPopup.svelte rename to src/lib/components/AIPromptPopup.svelte index fb68de86..ded983f1 100644 --- a/src/lib/components/AIGlyphPopup.svelte +++ b/src/lib/components/AIPromptPopup.svelte @@ -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(); let container = $state(); + $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 @@