Merge pull request #1888 from mermaid-js/release-promotion
Release live editor
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import CloseIcon from '~icons/material-symbols/close-rounded';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
input: string;
|
||||
onClose: () => void;
|
||||
onHeightChange?: (height: number) => void;
|
||||
onTryFree: () => void;
|
||||
}
|
||||
|
||||
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);
|
||||
const lineHeight =
|
||||
Number.parseFloat(computed.lineHeight) || Number.parseFloat(computed.fontSize) * 1.5 || 0;
|
||||
const paddingTop = Number.parseFloat(computed.paddingTop) || 0;
|
||||
const paddingBottom = Number.parseFloat(computed.paddingBottom) || 0;
|
||||
const minHeight = lineHeight + paddingTop + paddingBottom;
|
||||
const maxLines = 8;
|
||||
const maxHeight = lineHeight * maxLines + paddingTop + paddingBottom;
|
||||
|
||||
textarea.style.height = 'auto';
|
||||
const nextHeight = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight));
|
||||
textarea.style.height = `${nextHeight}px`;
|
||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (input !== undefined) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} onmousedown={handleOutsideClick} />
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
bind:this={container}
|
||||
class={cn(
|
||||
'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'
|
||||
)}
|
||||
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={input}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (input.trim()) {
|
||||
onTryFree();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Describe what to add or change"
|
||||
rows="1"
|
||||
class="focus font-recursive min-h-0 flex-1 resize-none border-none bg-transparent px-1 text-sm font-normal text-foreground placeholder:text-muted-foreground focus:ring-0 focus:outline-none disabled:opacity-50 dark:text-foreground dark:placeholder:text-muted-foreground"
|
||||
style="height: 20px; overflow-y: hidden;"></textarea>
|
||||
<button onclick={onClose} class="text-muted-foreground hover:text-foreground">
|
||||
<CloseIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-recursive text-xs font-normal text-foreground dark:text-foreground"
|
||||
>Sign Up at Mermaid.ai to try AI</span>
|
||||
<Button
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@property --gradient-angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@keyframes gradient-angle-shift {
|
||||
0% {
|
||||
--gradient-angle: 0deg;
|
||||
}
|
||||
100% {
|
||||
--gradient-angle: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container-for-animation {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.button-container-for-animation::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 14px;
|
||||
padding: 2px;
|
||||
background: conic-gradient(
|
||||
from var(--gradient-angle, 0deg),
|
||||
color-mix(in srgb, var(--color-accent) 0%, transparent) 0%,
|
||||
color-mix(in srgb, var(--color-accent) 0%, transparent) 12%,
|
||||
color-mix(in srgb, var(--color-accent) 12%, transparent) 18%,
|
||||
color-mix(in srgb, var(--color-accent) 42%, transparent) 24%,
|
||||
color-mix(in srgb, var(--color-accent) 77%, transparent) 32%,
|
||||
rgba(93, 85, 212, 0.923) 41%,
|
||||
rgba(93, 85, 212, 0.5) 52%,
|
||||
color-mix(in srgb, var(--color-accent) 58%, transparent) 72%,
|
||||
color-mix(in srgb, var(--color-accent) 56%, transparent) 82%,
|
||||
color-mix(in srgb, var(--color-accent) 36%, transparent) 87%,
|
||||
color-mix(in srgb, var(--color-accent) 19%, transparent) 92%,
|
||||
color-mix(in srgb, var(--color-accent) 10%, transparent) 96%,
|
||||
color-mix(in srgb, var(--color-accent) 3%, transparent) 98%,
|
||||
color-mix(in srgb, var(--color-accent) 0%, transparent) 100%
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
.button-container-for-animation.rainbow-border::before {
|
||||
opacity: 1;
|
||||
animation: gradient-angle-shift 2s linear infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { EditorProps } from '$/types';
|
||||
import { env } from '$/util/env';
|
||||
import { stateStore } from '$/util/state';
|
||||
import { stateStore, urlsStore } from '$/util/state';
|
||||
import { AIPromptViewZoneManager } from '$lib/util/AIPromptViewZoneManager';
|
||||
import { initEditor } from '$lib/util/monacoExtra';
|
||||
import { errorDebug } from '$lib/util/util';
|
||||
import { mode } from 'mode-watcher';
|
||||
@@ -9,18 +10,28 @@
|
||||
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 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: {
|
||||
enabled: false
|
||||
},
|
||||
overviewRulerLanes: 0
|
||||
overviewRulerLanes: 0,
|
||||
glyphMargin: true,
|
||||
lineNumbersMinChars: 4
|
||||
} satisfies monaco.editor.IStandaloneEditorConstructionOptions;
|
||||
let currentText = '';
|
||||
let showPopup = $state(false);
|
||||
let popupPosition = $state({ top: 0, lineNumber: 0 });
|
||||
let decorationsCollection: monaco.editor.IEditorDecorationsCollection | undefined;
|
||||
let input = $state('');
|
||||
let lastMouseLine = 0;
|
||||
const aiPromptManager = new AIPromptViewZoneManager();
|
||||
|
||||
const jsonModel = monaco.editor.createModel(
|
||||
'',
|
||||
@@ -33,6 +44,51 @@
|
||||
monaco.Uri.parse('internal://mermaid.mmd')
|
||||
);
|
||||
|
||||
const renderAIPromptGutterGlyphIcon = () => {
|
||||
decorationsCollection?.clear();
|
||||
if (!editor || showPopup) {
|
||||
return;
|
||||
}
|
||||
const model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastMouseLine > 0 && model.id === mermaidModel.id) {
|
||||
decorationsCollection?.set([
|
||||
{
|
||||
range: new monaco.Range(lastMouseLine, 1, lastMouseLine, 1),
|
||||
options: {
|
||||
glyphMarginClassName: 'suggestion-icon'
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
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(() => {
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
@@ -61,6 +117,18 @@
|
||||
initEditor(monaco);
|
||||
errorDebug();
|
||||
editor = monaco.editor.create(divElement, editorOptions);
|
||||
aiPromptManager.setEditor(editor);
|
||||
decorationsCollection = editor.createDecorationsCollection([]);
|
||||
|
||||
editor.onMouseDown((e) => {
|
||||
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.onDidChangeModelContent(({ isFlush }) => {
|
||||
const newText = editor?.getValue();
|
||||
if (!newText || currentText === newText || isFlush) {
|
||||
@@ -79,6 +147,12 @@
|
||||
|
||||
if (editor.getModel()?.id !== model.id) {
|
||||
editor.setModel(model);
|
||||
renderAIPromptGutterGlyphIcon();
|
||||
}
|
||||
|
||||
// Clear decorations if not in 'code' mode, or if the model changes
|
||||
if (editorMode !== 'code' || editor.getModel()?.id !== mermaidModel.id) {
|
||||
decorationsCollection?.clear();
|
||||
}
|
||||
|
||||
// Update editor text if it's different
|
||||
@@ -87,15 +161,31 @@
|
||||
editor.setScrollTop(0);
|
||||
editor.setValue(newText);
|
||||
currentText = newText;
|
||||
renderAIPromptGutterGlyphIcon();
|
||||
}
|
||||
|
||||
// Display/clear errors
|
||||
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
|
||||
});
|
||||
|
||||
editor.onMouseMove((e) => {
|
||||
if (!editor) return;
|
||||
if (showPopup) return;
|
||||
if (editor.getModel()?.id !== mermaidModel.id) return;
|
||||
|
||||
lastMouseLine = e.target.position?.lineNumber ?? 0;
|
||||
renderAIPromptGutterGlyphIcon();
|
||||
});
|
||||
|
||||
editor.onMouseLeave(() => {
|
||||
lastMouseLine = 0;
|
||||
renderAIPromptGutterGlyphIcon();
|
||||
});
|
||||
|
||||
const unsubscribeMode = mode.subscribe((mode) => {
|
||||
if (editor) {
|
||||
monaco.editor.setTheme(`mermaid${mode === 'dark' ? '-dark' : ''}`);
|
||||
divElement?.classList.toggle('mermaid-dark', mode === 'dark');
|
||||
}
|
||||
});
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
@@ -109,15 +199,51 @@
|
||||
resizeObserver.observe(divElement);
|
||||
}
|
||||
|
||||
renderAIPromptGutterGlyphIcon();
|
||||
|
||||
return () => {
|
||||
unsubscribeState();
|
||||
unsubscribeMode();
|
||||
resizeObserver.disconnect();
|
||||
jsonModel.dispose();
|
||||
mermaidModel.dispose();
|
||||
aiPromptManager.destroy();
|
||||
editor?.dispose();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={divElement} id="editor" class="h-full flex-grow overflow-hidden"></div>
|
||||
<div class="relative h-full grow overflow-hidden">
|
||||
<div bind:this={divElement} id="editor" class="h-full w-full"></div>
|
||||
<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(.suggestion-icon) {
|
||||
background-color: #e8eaf9;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
margin-left: 4px;
|
||||
background-image: url('/icons/use-chat.svg');
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(#editor.mermaid-dark .suggestion-icon) {
|
||||
background-color: #2e4d6b;
|
||||
background-image: url('/icons/use-chat-dark.svg');
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,5 +10,6 @@ export const TID = {
|
||||
} as const;
|
||||
|
||||
export const C = {
|
||||
aiLiveEditor: 'ai_live_editor',
|
||||
utmSource: 'mermaid_live_editor'
|
||||
} as const;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -9,68 +9,29 @@
|
||||
|
||||
let { closeBanner }: Props = $props();
|
||||
|
||||
interface VariantConfig {
|
||||
bannerCopy: string;
|
||||
buttonCopy: string;
|
||||
campaign: 'variant_a' | 'variant_b' | 'variant_c';
|
||||
}
|
||||
const utmSource = env.domain === 'mermaid.ai' ? 'mermaid_ai_live' : 'mermaid_live_editor';
|
||||
|
||||
const variants: VariantConfig[] = [
|
||||
{
|
||||
bannerCopy: 'Use code NEWYEAR for 10% off Mermaid Advanced Editor (limited time)',
|
||||
buttonCopy: 'Try now',
|
||||
campaign: 'variant_a'
|
||||
},
|
||||
{
|
||||
bannerCopy: 'Limited time: 10% off Mermaid Advanced Editor with code NEWYEAR',
|
||||
buttonCopy: 'Claim discount',
|
||||
campaign: 'variant_b'
|
||||
},
|
||||
{
|
||||
bannerCopy: 'Try Mermaid Advanced Editor — get 10% off with code NEWYEAR',
|
||||
buttonCopy: 'Get started',
|
||||
campaign: 'variant_c'
|
||||
}
|
||||
];
|
||||
|
||||
// Determine UTM source based on deployment domain
|
||||
const getUtmSource = (): string => {
|
||||
const domain = env.domain;
|
||||
if (domain === 'mermaid.ai') {
|
||||
return 'mermaid_ai_live';
|
||||
}
|
||||
return 'mermaid_live_editor';
|
||||
};
|
||||
|
||||
// Randomly select a variant on component mount
|
||||
const getRandomVariant = (): VariantConfig => {
|
||||
const index = Math.floor(Math.random() * variants.length);
|
||||
return variants[index];
|
||||
};
|
||||
|
||||
const selectedVariant = getRandomVariant();
|
||||
const utmSource = getUtmSource();
|
||||
|
||||
const buildUrl = (variant: VariantConfig): string => {
|
||||
const params = new URLSearchParams({
|
||||
utm_medium: 'banner_ad',
|
||||
utm_campaign: variant.campaign,
|
||||
utm_source: utmSource
|
||||
});
|
||||
return `https://mermaid.ai/?${params.toString()}`;
|
||||
};
|
||||
const url = `https://mermaid.ai/app/user/billing/checkout?${new URLSearchParams({
|
||||
coupon: 'dmIuNbqx',
|
||||
tier: 'plus',
|
||||
utm_campaign: 'newyear',
|
||||
utm_medium: 'banner_ad',
|
||||
utm_source: utmSource
|
||||
}).toString()}`;
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-center bg-[#E0095F] p-1.5" role="banner">
|
||||
<div class="grid grow">
|
||||
<a
|
||||
href={buildUrl(selectedVariant)}
|
||||
href={url}
|
||||
target="_blank"
|
||||
class="col-start-1 row-start-1 flex items-center justify-center gap-4 no-underline">
|
||||
<span class="text-base tracking-wider text-white">{selectedVariant.bannerCopy}</span>
|
||||
<span class="text-base tracking-wider text-white">
|
||||
Limited time: 10% off Mermaid Advanced Editor with code NEWYEAR
|
||||
</span>
|
||||
<Button
|
||||
class="shrink-0 rounded-md bg-[#1E1A2E] px-3 py-1.5 text-base font-semibold tracking-wide text-white hover:bg-[#261A56]">
|
||||
{selectedVariant.buttonCopy}
|
||||
Claim discount
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -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, getUTMSource, 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 = getUTMSource();
|
||||
const params = new URLSearchParams({
|
||||
utm_source: C.utmSource,
|
||||
utm_source: utmSource,
|
||||
utm_medium: medium
|
||||
}).toString();
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { C } from '$/constants';
|
||||
import { env } from './env';
|
||||
import { loadDataFromUrl } from './fileLoaders/loader';
|
||||
import { initLoading } from './loading';
|
||||
@@ -88,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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg id="mySvg" width="64" height="64" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" role="img">
|
||||
<path class="p1"
|
||||
d="M19 4C19 6.50909 19.8727 8.63636 21.6182 10.3818C23.3636 12.1273 25.4909 13 28 13C25.4909 13 23.3636 13.8727 21.6182 15.6182C19.8727 17.3636 19 19.4909 19 22C19 19.4909 18.1273 17.3636 16.3818 15.6182C14.6364 13.8727 12.5091 13 10 13C12.5091 13 14.6364 12.1273 16.3818 10.3818C18.1273 8.63636 19 6.50909 19 4Z"
|
||||
fill="white" />
|
||||
<path class="p2"
|
||||
d="M8.5 16C8.5 17.2545 8.93636 18.3182 9.80909 19.1909C10.6818 20.0636 11.7455 20.5 13 20.5C11.7455 20.5 10.6818 20.9364 9.80909 21.8091C8.93636 22.6818 8.5 23.7455 8.5 25C8.5 23.7455 8.06364 22.6818 7.19091 21.8091C6.31818 20.9364 5.25455 20.5 4 20.5C5.25455 20.5 6.31818 20.0636 7.19091 19.1909C8.06364 18.3182 8.5 17.2545 8.5 16Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 879 B |
@@ -0,0 +1,9 @@
|
||||
<svg id="mySvg" width="64" height="64" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" role="img">
|
||||
<path class="p1"
|
||||
d="M19 4C19 6.50909 19.8727 8.63636 21.6182 10.3818C23.3636 12.1273 25.4909 13 28 13C25.4909 13 23.3636 13.8727 21.6182 15.6182C19.8727 17.3636 19 19.4909 19 22C19 19.4909 18.1273 17.3636 16.3818 15.6182C14.6364 13.8727 12.5091 13 10 13C12.5091 13 14.6364 12.1273 16.3818 10.3818C18.1273 8.63636 19 6.50909 19 4Z"
|
||||
fill="black" />
|
||||
<path class="p2"
|
||||
d="M8.5 16C8.5 17.2545 8.93636 18.3182 9.80909 19.1909C10.6818 20.0636 11.7455 20.5 13 20.5C11.7455 20.5 10.6818 20.9364 9.80909 21.8091C8.93636 22.6818 8.5 23.7455 8.5 25C8.5 23.7455 8.06364 22.6818 7.19091 21.8091C6.31818 20.9364 5.25455 20.5 4 20.5C5.25455 20.5 6.31818 20.0636 7.19091 19.1909C8.06364 18.3182 8.5 17.2545 8.5 16Z"
|
||||
fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 879 B |
Reference in New Issue
Block a user