mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +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 Sparkles from '../icons/Sparkles.svelte';
|
||||||
|
|
||||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||||
|
import InputVariablesModal from './MessageInput/InputVariablesModal.svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let transparentBackground = false;
|
export let transparentBackground = false;
|
||||||
|
|
@ -95,6 +96,10 @@
|
||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
export let codeInterpreterEnabled = false;
|
export let codeInterpreterEnabled = false;
|
||||||
|
|
||||||
|
let showInputVariablesModal = false;
|
||||||
|
let inputVariables = {};
|
||||||
|
let inputVariableValues = {};
|
||||||
|
|
||||||
$: onChange({
|
$: onChange({
|
||||||
prompt,
|
prompt,
|
||||||
files: files
|
files: files
|
||||||
|
|
@ -113,74 +118,62 @@
|
||||||
codeInterpreterEnabled
|
codeInterpreterEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setText = (text?: string) => {
|
const extractInputVariables = (text: string): Record<string, any> => {
|
||||||
const chatInput = document.getElementById('chat-input');
|
const regex = /{{\s*([^|}\s]+)\s*\|\s*([^}]+)\s*}}/g;
|
||||||
|
const variables: Record<string, any> = {};
|
||||||
|
let match;
|
||||||
|
|
||||||
if (chatInput) {
|
// Use exec() loop instead of matchAll() for better compatibility
|
||||||
if ($settings?.richTextInput ?? true) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
chatInputElement.setText(text);
|
const varName = match[1].trim();
|
||||||
chatInputElement.focus();
|
const definition = match[2].trim();
|
||||||
} else {
|
variables[varName] = parseVariableDefinition(definition);
|
||||||
chatInput.value = text;
|
|
||||||
prompt = text;
|
|
||||||
|
|
||||||
chatInput.focus();
|
|
||||||
chatInput.dispatchEvent(new Event('input'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getWordAtCursor(text, cursor) {
|
const parseVariableDefinition = (definition: string): Record<string, any> => {
|
||||||
if (typeof text !== 'string' || cursor == null) return '';
|
const [firstPart, ...propertyParts] = definition.split(':');
|
||||||
const left = text.slice(0, cursor);
|
|
||||||
const right = text.slice(cursor);
|
|
||||||
const leftWord = left.match(/(?:^|\s)([^\s]*)$/)?.[1] || '';
|
|
||||||
|
|
||||||
const rightWord = right.match(/^([^\s]*)/)?.[1] || '';
|
// Parse type (explicit or implied)
|
||||||
return leftWord + rightWord;
|
const type = firstPart.startsWith('type=') ? firstPart.slice(5) : firstPart;
|
||||||
}
|
|
||||||
|
|
||||||
const getCommand = () => {
|
// Parse properties using reduce
|
||||||
const chatInput = document.getElementById('chat-input');
|
const properties = propertyParts.reduce((props, part) => {
|
||||||
let word = '';
|
const [propertyName, ...valueParts] = part.split('=');
|
||||||
|
const propertyValue = valueParts.join('='); // Handle values with = signs
|
||||||
|
|
||||||
if (chatInput) {
|
return propertyName && propertyValue
|
||||||
if ($settings?.richTextInput ?? true) {
|
? {
|
||||||
word = chatInputElement?.getWordAtDocPos();
|
...props,
|
||||||
} else {
|
[propertyName.trim()]: parseJsonValue(propertyValue.trim())
|
||||||
const cursor = chatInput ? chatInput.selectionStart : prompt.length;
|
}
|
||||||
word = getWordAtCursor(prompt, cursor);
|
: 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) => {
|
const inputVariableHandler = async (text: string) => {
|
||||||
return text;
|
inputVariables = extractInputVariables(text);
|
||||||
|
|
||||||
|
if (Object.keys(inputVariables).length > 0) {
|
||||||
|
showInputVariablesModal = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const textVariableHandler = async (text: string) => {
|
const textVariableHandler = async (text: string) => {
|
||||||
|
|
@ -262,15 +255,116 @@
|
||||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||||
}
|
}
|
||||||
|
|
||||||
text = await inputVariableHandler(text);
|
inputVariableHandler(text);
|
||||||
return 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 insertTextAtCursor = async (text: string) => {
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
if (!chatInput) return;
|
if (!chatInput) return;
|
||||||
|
|
||||||
text = await textVariableHandler(text);
|
text = await textVariableHandler(text);
|
||||||
|
|
||||||
if (command) {
|
if (command) {
|
||||||
replaceCommandWithText(text);
|
replaceCommandWithText(text);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -731,6 +825,14 @@
|
||||||
|
|
||||||
<FilesOverlay show={dragged} />
|
<FilesOverlay show={dragged} />
|
||||||
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
||||||
|
<InputVariablesModal
|
||||||
|
bind:show={showInputVariablesModal}
|
||||||
|
variables={inputVariables}
|
||||||
|
onSave={(variableValues) => {
|
||||||
|
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
||||||
|
replaceVariables(inputVariableValues);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="w-full font-primary">
|
<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);
|
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 = () => {
|
export const focus = () => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
editor.view.focus();
|
editor.view.focus();
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,7 @@
|
||||||
map.fitBounds(markerGroupLayer.getBounds(), {
|
map.fitBounds(markerGroupLayer.getBounds(), {
|
||||||
maxZoom: 13
|
maxZoom: 13
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error('Error fitting bounds for markers:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue