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 Commands from './MessageInput/Commands.svelte';
|
||||
import ToolServersModal from './ToolServersModal.svelte';
|
||||
import VariableInputModal from './VariableInputModal.svelte';
|
||||
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
@ -89,6 +90,10 @@
|
||||
export let webSearchEnabled = 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({
|
||||
prompt,
|
||||
files: files
|
||||
@ -453,6 +458,18 @@
|
||||
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 () => {
|
||||
loaded = true;
|
||||
|
||||
@ -496,6 +513,31 @@
|
||||
|
||||
<FilesOverlay show={dragged} />
|
||||
<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}
|
||||
<div class="w-full font-primary">
|
||||
@ -586,6 +628,15 @@
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
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>
|
||||
|
@ -56,7 +56,8 @@
|
||||
{#if show}
|
||||
{#if !loading}
|
||||
{#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('# '))}
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
|
@ -9,9 +9,10 @@
|
||||
getUserTimezone,
|
||||
getWeekday
|
||||
} from '$lib/utils';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { tick, getContext, createEventDispatcher } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let files;
|
||||
@ -166,11 +167,16 @@
|
||||
prompt = fullPrompt;
|
||||
await tick();
|
||||
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
if (chatInputElement instanceof HTMLInputElement || chatInputElement instanceof HTMLTextAreaElement) {
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
}
|
||||
} else {
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
await tick();
|
||||
dispatch('promptapplied', { finalPromptContent: prompt });
|
||||
};
|
||||
</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