mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
feat: input variables
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
This commit is contained in:
parent
5526b66165
commit
9f87a0cf21
4 changed files with 499 additions and 59 deletions
|
|
@ -64,6 +64,7 @@
|
|||
import Sparkles from '../icons/Sparkles.svelte';
|
||||
|
||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||
import InputVariablesModal from './MessageInput/InputVariablesModal.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let transparentBackground = false;
|
||||
|
|
@ -95,6 +96,10 @@
|
|||
export let webSearchEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
|
||||
let showInputVariablesModal = false;
|
||||
let inputVariables = {};
|
||||
let inputVariableValues = {};
|
||||
|
||||
$: onChange({
|
||||
prompt,
|
||||
files: files
|
||||
|
|
@ -113,74 +118,62 @@
|
|||
codeInterpreterEnabled
|
||||
});
|
||||
|
||||
export const setText = (text?: string) => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const extractInputVariables = (text: string): Record<string, any> => {
|
||||
const regex = /{{\s*([^|}\s]+)\s*\|\s*([^}]+)\s*}}/g;
|
||||
const variables: Record<string, any> = {};
|
||||
let match;
|
||||
|
||||
if (chatInput) {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement.setText(text);
|
||||
chatInputElement.focus();
|
||||
} else {
|
||||
chatInput.value = text;
|
||||
prompt = text;
|
||||
|
||||
chatInput.focus();
|
||||
chatInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
// Use exec() loop instead of matchAll() for better compatibility
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const varName = match[1].trim();
|
||||
const definition = match[2].trim();
|
||||
variables[varName] = parseVariableDefinition(definition);
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
function getWordAtCursor(text, cursor) {
|
||||
if (typeof text !== 'string' || cursor == null) return '';
|
||||
const left = text.slice(0, cursor);
|
||||
const right = text.slice(cursor);
|
||||
const leftWord = left.match(/(?:^|\s)([^\s]*)$/)?.[1] || '';
|
||||
const parseVariableDefinition = (definition: string): Record<string, any> => {
|
||||
const [firstPart, ...propertyParts] = definition.split(':');
|
||||
|
||||
const rightWord = right.match(/^([^\s]*)/)?.[1] || '';
|
||||
return leftWord + rightWord;
|
||||
}
|
||||
// Parse type (explicit or implied)
|
||||
const type = firstPart.startsWith('type=') ? firstPart.slice(5) : firstPart;
|
||||
|
||||
const getCommand = () => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
let word = '';
|
||||
// Parse properties using reduce
|
||||
const properties = propertyParts.reduce((props, part) => {
|
||||
const [propertyName, ...valueParts] = part.split('=');
|
||||
const propertyValue = valueParts.join('='); // Handle values with = signs
|
||||
|
||||
if (chatInput) {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
word = chatInputElement?.getWordAtDocPos();
|
||||
} else {
|
||||
const cursor = chatInput ? chatInput.selectionStart : prompt.length;
|
||||
word = getWordAtCursor(prompt, cursor);
|
||||
return propertyName && propertyValue
|
||||
? {
|
||||
...props,
|
||||
[propertyName.trim()]: parseJsonValue(propertyValue.trim())
|
||||
}
|
||||
: props;
|
||||
}, {});
|
||||
|
||||
return { type, ...properties };
|
||||
};
|
||||
|
||||
const parseJsonValue = (value: string): any => {
|
||||
// Check if it starts with square or curly brackets (JSON)
|
||||
if (/^[\[{]/.test(value)) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value; // Return as string if JSON parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
return word;
|
||||
return value;
|
||||
};
|
||||
|
||||
function getWordBoundsAtCursor(text, cursor) {
|
||||
let start = cursor,
|
||||
end = cursor;
|
||||
while (start > 0 && !/\s/.test(text[start - 1])) --start;
|
||||
while (end < text.length && !/\s/.test(text[end])) ++end;
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function replaceCommandWithText(text) {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (!chatInput) return;
|
||||
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement?.replaceCommandWithText(text);
|
||||
} else {
|
||||
const cursor = chatInput.selectionStart;
|
||||
const { start, end } = getWordBoundsAtCursor(prompt, cursor);
|
||||
prompt = prompt.slice(0, start) + text + prompt.slice(end);
|
||||
chatInput.focus();
|
||||
chatInput.setSelectionRange(start + text.length, start + text.length);
|
||||
}
|
||||
}
|
||||
|
||||
const inputVariableHandler = async (text: string) => {
|
||||
return text;
|
||||
inputVariables = extractInputVariables(text);
|
||||
|
||||
if (Object.keys(inputVariables).length > 0) {
|
||||
showInputVariablesModal = true;
|
||||
}
|
||||
};
|
||||
|
||||
const textVariableHandler = async (text: string) => {
|
||||
|
|
@ -262,15 +255,116 @@
|
|||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||
}
|
||||
|
||||
text = await inputVariableHandler(text);
|
||||
inputVariableHandler(text);
|
||||
return text;
|
||||
};
|
||||
|
||||
const replaceVariables = (variables: Record<string, any>) => {
|
||||
console.log('Replacing variables:', variables);
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
|
||||
if (chatInput) {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement.replaceVariables(variables);
|
||||
chatInputElement.focus();
|
||||
} else {
|
||||
// Get current value from the input element
|
||||
let currentValue = chatInput.value || '';
|
||||
|
||||
// Replace template variables using regex
|
||||
const updatedValue = currentValue.replace(
|
||||
/{{\s*([^|}]+)(?:\|[^}]*)?\s*}}/g,
|
||||
(match, varName) => {
|
||||
const trimmedVarName = varName.trim();
|
||||
return variables.hasOwnProperty(trimmedVarName)
|
||||
? String(variables[trimmedVarName])
|
||||
: match;
|
||||
}
|
||||
);
|
||||
|
||||
// Update the input value
|
||||
chatInput.value = updatedValue;
|
||||
chatInput.focus();
|
||||
chatInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setText = async (text?: string) => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
|
||||
if (chatInput) {
|
||||
text = await textVariableHandler(text || '');
|
||||
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement.setText(text);
|
||||
chatInputElement.focus();
|
||||
} else {
|
||||
chatInput.value = text;
|
||||
prompt = text;
|
||||
|
||||
chatInput.focus();
|
||||
chatInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getCommand = () => {
|
||||
const getWordAtCursor = (text, cursor) => {
|
||||
if (typeof text !== 'string' || cursor == null) return '';
|
||||
const left = text.slice(0, cursor);
|
||||
const right = text.slice(cursor);
|
||||
const leftWord = left.match(/(?:^|\s)([^\s]*)$/)?.[1] || '';
|
||||
|
||||
const rightWord = right.match(/^([^\s]*)/)?.[1] || '';
|
||||
return leftWord + rightWord;
|
||||
};
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
let word = '';
|
||||
|
||||
if (chatInput) {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
word = chatInputElement?.getWordAtDocPos();
|
||||
} else {
|
||||
const cursor = chatInput ? chatInput.selectionStart : prompt.length;
|
||||
word = getWordAtCursor(prompt, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
return word;
|
||||
};
|
||||
|
||||
const replaceCommandWithText = (text) => {
|
||||
const getWordBoundsAtCursor = (text, cursor) => {
|
||||
let start = cursor,
|
||||
end = cursor;
|
||||
while (start > 0 && !/\s/.test(text[start - 1])) --start;
|
||||
while (end < text.length && !/\s/.test(text[end])) ++end;
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (!chatInput) return;
|
||||
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement?.replaceCommandWithText(text);
|
||||
} else {
|
||||
const cursor = chatInput.selectionStart;
|
||||
const { start, end } = getWordBoundsAtCursor(prompt, cursor);
|
||||
prompt = prompt.slice(0, start) + text + prompt.slice(end);
|
||||
chatInput.focus();
|
||||
chatInput.setSelectionRange(start + text.length, start + text.length);
|
||||
}
|
||||
};
|
||||
|
||||
const insertTextAtCursor = async (text: string) => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (!chatInput) return;
|
||||
|
||||
text = await textVariableHandler(text);
|
||||
|
||||
if (command) {
|
||||
replaceCommandWithText(text);
|
||||
} else {
|
||||
|
|
@ -731,6 +825,14 @@
|
|||
|
||||
<FilesOverlay show={dragged} />
|
||||
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
||||
<InputVariablesModal
|
||||
bind:show={showInputVariablesModal}
|
||||
variables={inputVariables}
|
||||
onSave={(variableValues) => {
|
||||
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
||||
replaceVariables(inputVariableValues);
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if loaded}
|
||||
<div class="w-full font-primary">
|
||||
|
|
|
|||
297
src/lib/components/chat/MessageInput/InputVariablesModal.svelte
Normal file
297
src/lib/components/chat/MessageInput/InputVariablesModal.svelte
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { models, config } from '$lib/stores';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import MapSelector from '$lib/components/common/Valves/MapSelector.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let variables = {};
|
||||
|
||||
export let onSave = (e) => {};
|
||||
|
||||
let loading = false;
|
||||
let variableValues = {};
|
||||
|
||||
const submitHandler = async () => {
|
||||
onSave(variableValues);
|
||||
show = false;
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
loading = true;
|
||||
variableValues = {};
|
||||
for (const variable of Object.keys(variables)) {
|
||||
if (variables[variable]?.default !== undefined) {
|
||||
variableValues[variable] = variables[variable].default;
|
||||
} else {
|
||||
variableValues[variable] = '';
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="md">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Input Variables')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="px-1">
|
||||
{#if !loading}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each Object.keys(variables) as variable, idx}
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between mb-1.5">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{variable}
|
||||
|
||||
{#if variables[variable]?.required ?? true}
|
||||
<span class=" text-gray-500">*required</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-0.5 mb-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
{#if variables[variable]?.type === 'select'}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
bind:value={variableValues[variable]}
|
||||
>
|
||||
{#each variables[variable]?.options ?? [] as option}
|
||||
<option value={option} selected={option === variableValues[variable]}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if variables[variable]?.type === 'checkbox'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative size-6 flex justify-center items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={variableValues[variable]}
|
||||
class="size-3.5 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 py-2 text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
placeholder="Enter checkbox label"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else if variables[variable]?.type === 'color'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative size-6">
|
||||
<input
|
||||
type="color"
|
||||
class="size-6 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
|
||||
value={variableValues[variable]}
|
||||
on:input={(e) => {
|
||||
// Convert the color value to uppercase immediately
|
||||
variableValues[variable] = e.target.value.toUpperCase();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 py-2 text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
placeholder="Enter hex color (e.g. #FF0000)"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else if variables[variable]?.type === 'date'}
|
||||
<input
|
||||
type="date"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'datetime-local'}
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'email'}
|
||||
<input
|
||||
type="email"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'month'}
|
||||
<input
|
||||
type="month"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'range'}
|
||||
<input
|
||||
type="range"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'tel'}
|
||||
<input
|
||||
type="tel"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'text'}
|
||||
<input
|
||||
type="text"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'time'}
|
||||
<input
|
||||
type="time"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'url'}
|
||||
<input
|
||||
type="url"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'map'}
|
||||
<!-- EXPERIMENTAL INPUT TYPE, DO NOT USE IN PRODUCTION -->
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MapSelector
|
||||
setViewLocation={((variableValues[variable] ?? '').includes(',') ??
|
||||
false)
|
||||
? variableValues[variable].split(',')
|
||||
: null}
|
||||
onClick={(value) => {
|
||||
variableValues[variable] = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class=" w-full py-1 text-left text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
placeholder="Enter coordinates (e.g. 51.505, -0.09)"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<textarea
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- {#if (valvesSpec.properties[property]?.description ?? null) !== null}
|
||||
<div class="text-xs text-gray-500">
|
||||
{valvesSpec.properties[property].description}
|
||||
</div>
|
||||
{/if} -->
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-white hover:bg-gray-100 text-black dark:bg-black dark:text-white dark:hover:bg-gray-900 transition rounded-full"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -207,6 +207,49 @@
|
|||
selectNextTemplate(editor.view.state, editor.view.dispatch);
|
||||
};
|
||||
|
||||
export const replaceVariables = (variables) => {
|
||||
if (!editor) return;
|
||||
const { state, view } = editor;
|
||||
const { doc } = state;
|
||||
|
||||
// Create a transaction to replace variables
|
||||
let tr = state.tr;
|
||||
let offset = 0; // Track position changes due to text length differences
|
||||
|
||||
// Collect all replacements first to avoid position conflicts
|
||||
const replacements = [];
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (node.isText && node.text) {
|
||||
const text = node.text;
|
||||
const replacedText = text.replace(/{{\s*([^|}]+)(?:\|[^}]*)?\s*}}/g, (match, varName) => {
|
||||
const trimmedVarName = varName.trim();
|
||||
return variables.hasOwnProperty(trimmedVarName)
|
||||
? String(variables[trimmedVarName])
|
||||
: match;
|
||||
});
|
||||
|
||||
if (replacedText !== text) {
|
||||
replacements.push({
|
||||
from: pos,
|
||||
to: pos + text.length,
|
||||
text: replacedText
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply replacements in reverse order to maintain correct positions
|
||||
replacements.reverse().forEach(({ from, to, text }) => {
|
||||
tr = tr.replaceWith(from, to, text !== '' ? state.schema.text(text) : []);
|
||||
});
|
||||
|
||||
// Only dispatch if there are changes
|
||||
if (replacements.length > 0) {
|
||||
view.dispatch(tr);
|
||||
}
|
||||
};
|
||||
|
||||
export const focus = () => {
|
||||
if (editor) {
|
||||
editor.view.focus();
|
||||
|
|
|
|||
|
|
@ -32,9 +32,7 @@
|
|||
map.fitBounds(markerGroupLayer.getBounds(), {
|
||||
maxZoom: 13
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fitting bounds for markers:', error);
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue