mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
Merge 7d547148d7
into 7513dc7e34
This commit is contained in:
commit
dc8d3c3ee1
@ -42,6 +42,7 @@
|
|||||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||||
import Commands from './MessageInput/Commands.svelte';
|
import Commands from './MessageInput/Commands.svelte';
|
||||||
import ToolServersModal from './ToolServersModal.svelte';
|
import ToolServersModal from './ToolServersModal.svelte';
|
||||||
|
import VariableInputModal from './VariableInputModal.svelte';
|
||||||
|
|
||||||
import RichTextInput from '../common/RichTextInput.svelte';
|
import RichTextInput from '../common/RichTextInput.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
@ -89,6 +90,10 @@
|
|||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
export let codeInterpreterEnabled = false;
|
export let codeInterpreterEnabled = false;
|
||||||
|
|
||||||
|
let showVariableInputModal = false;
|
||||||
|
let activePromptVariables = [];
|
||||||
|
const RESERVED_VARIABLES = ['CLIPBOARD', 'USER_LOCATION', 'USER_NAME', 'USER_LANGUAGE', 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_DATETIME', 'CURRENT_TIMEZONE', 'CURRENT_WEEKDAY'];
|
||||||
|
|
||||||
$: onChange({
|
$: onChange({
|
||||||
prompt,
|
prompt,
|
||||||
files: files
|
files: files
|
||||||
@ -453,6 +458,18 @@
|
|||||||
shiftKey = false;
|
shiftKey = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractCustomVariables = (text: string): string[] => {
|
||||||
|
const regex = /{{\s*(.*?)\s*}}/g;
|
||||||
|
const matches = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
matches.push(match[1]);
|
||||||
|
}
|
||||||
|
return [...new Set(matches)].filter(v => !RESERVED_VARIABLES.includes(v.toUpperCase()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const variableModalSubtitle = $i18n.t('Your prompt uses the highlighted variables as placeholders');
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
||||||
@ -496,6 +513,31 @@
|
|||||||
|
|
||||||
<FilesOverlay show={dragged} />
|
<FilesOverlay show={dragged} />
|
||||||
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
||||||
|
<VariableInputModal
|
||||||
|
bind:show={showVariableInputModal}
|
||||||
|
variables={activePromptVariables}
|
||||||
|
promptRawContent={prompt}
|
||||||
|
subtitle={variableModalSubtitle}
|
||||||
|
on:submit={(e) => {
|
||||||
|
const submittedValues = e.detail;
|
||||||
|
let currentPrompt = prompt;
|
||||||
|
for (const variable in submittedValues) {
|
||||||
|
const value = submittedValues[variable];
|
||||||
|
const regex = new RegExp(`{{\\s*${variable}\\s*}}`, 'g');
|
||||||
|
currentPrompt = currentPrompt.replace(regex, value);
|
||||||
|
}
|
||||||
|
prompt = currentPrompt;
|
||||||
|
showVariableInputModal = false;
|
||||||
|
activePromptVariables = [];
|
||||||
|
|
||||||
|
tick().then(() => {
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
if (chatInput) {
|
||||||
|
chatInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="w-full font-primary">
|
<div class="w-full font-primary">
|
||||||
@ -586,6 +628,15 @@
|
|||||||
const chatInputElement = document.getElementById('chat-input');
|
const chatInputElement = document.getElementById('chat-input');
|
||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
}}
|
}}
|
||||||
|
on:promptselectionprocessed={(e) => {
|
||||||
|
const customVars = extractCustomVariables(prompt);
|
||||||
|
if (customVars.length > 0) {
|
||||||
|
activePromptVariables = customVars;
|
||||||
|
showVariableInputModal = true;
|
||||||
|
}
|
||||||
|
const chatInputElement = document.getElementById('chat-input');
|
||||||
|
chatInputElement?.focus();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,7 +56,8 @@
|
|||||||
{#if show}
|
{#if show}
|
||||||
{#if !loading}
|
{#if !loading}
|
||||||
{#if command?.charAt(0) === '/'}
|
{#if command?.charAt(0) === '/'}
|
||||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
<Prompts bind:this={commandElement} bind:prompt bind:files {command}
|
||||||
|
on:promptapplied={(e) => dispatch('promptselectionprocessed', e.detail)} />
|
||||||
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
|
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
|
||||||
<Knowledge
|
<Knowledge
|
||||||
bind:this={commandElement}
|
bind:this={commandElement}
|
||||||
|
@ -9,9 +9,10 @@
|
|||||||
getUserTimezone,
|
getUserTimezone,
|
||||||
getWeekday
|
getWeekday
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick, getContext, createEventDispatcher } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let files;
|
export let files;
|
||||||
@ -166,11 +167,16 @@
|
|||||||
prompt = fullPrompt;
|
prompt = fullPrompt;
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
if (chatInputElement instanceof HTMLInputElement || chatInputElement instanceof HTMLTextAreaElement) {
|
||||||
|
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
dispatch('promptapplied', { finalPromptContent: prompt });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
132
src/lib/components/chat/VariableInputModal.svelte
Normal file
132
src/lib/components/chat/VariableInputModal.svelte
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let variables: string[] = [];
|
||||||
|
export let subtitle: string = '';
|
||||||
|
export let promptRawContent = '';
|
||||||
|
|
||||||
|
let variableValues: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
$: formattedPromptDisplay = (() => {
|
||||||
|
if (!promptRawContent) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape HTML tags in promptRawContent
|
||||||
|
let escapedPrompt = promptRawContent.replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
|
if (variables && variables.length > 0) {
|
||||||
|
variables.forEach((variable) => {
|
||||||
|
const regex = new RegExp(`{{\\s*${variable}\\s*}}`, 'g');
|
||||||
|
escapedPrompt = escapedPrompt.replace(regex, `<strong>{{${variable}}}</strong>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return escapedPrompt;
|
||||||
|
})();
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const newValues = {};
|
||||||
|
for (const variable of variables) {
|
||||||
|
newValues[variable] = variableValues[variable] || '';
|
||||||
|
}
|
||||||
|
variableValues = newValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
dispatch('submit', variableValues);
|
||||||
|
show = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let modalElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (variables.length > 0 && modalElement) {
|
||||||
|
const firstInput = modalElement.querySelector('input, textarea');
|
||||||
|
if (firstInput) {
|
||||||
|
(firstInput as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:show size="lg" containerClassName="p-3" className="bg-white dark:bg-gray-900 rounded-2xl">
|
||||||
|
<div bind:this={modalElement}>
|
||||||
|
<div class="flex justify-between items-center dark:text-gray-300 px-5 pt-4 pb-2">
|
||||||
|
<h2 class="text-lg font-medium self-center">{$i18n.t('Enter Variable Values')}</h2>
|
||||||
|
<button
|
||||||
|
class="self-center text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
||||||
|
on:click={() => (show = false)}
|
||||||
|
aria-label={$i18n.t('Close')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if subtitle}
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 px-5">{@html subtitle}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if promptRawContent}
|
||||||
|
<div
|
||||||
|
class="max-h-56 overflow-y-auto bg-gray-50 dark:bg-gray-800 p-2 rounded-md my-3 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words mx-5"
|
||||||
|
>
|
||||||
|
{@html formattedPromptDisplay}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-3 px-5">{$i18n.t('You may replace the placeholder variables with values below. Note that if you do not enter a value, the placeholder will be removed from the text.')}</p>
|
||||||
|
|
||||||
|
<div class="p-5 max-h-[60vh] overflow-y-auto">
|
||||||
|
{#each variables as variable (variable)}
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{@html $i18n.t('Insert a value for <strong class="font-semibold text-gray-800 dark:text-gray-100">{{variable}}</strong>', { variable })}
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
id="variable-{variable}"
|
||||||
|
class="w-full bg-gray-50 dark:bg-gray-800 border-gray-300 dark:border-gray-700 rounded-md p-2 text-sm dark:text-gray-100 focus:ring-blue-500 focus:border-blue-500 mt-1"
|
||||||
|
placeholder={$i18n.t('Enter value here')}
|
||||||
|
bind:value={variableValues[variable]}
|
||||||
|
rows={2}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-5 py-4 flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||||
|
on:click={() => (show = false)}
|
||||||
|
>
|
||||||
|
{$i18n.t('Cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
on:click={handleSubmit}
|
||||||
|
>
|
||||||
|
{$i18n.t('Submit')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
Loading…
Reference in New Issue
Block a user