This commit is contained in:
Classic298 2025-06-21 14:55:48 +02:00 committed by GitHub
commit dc8d3c3ee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 193 additions and 3 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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>

View 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, '&lt;').replace(/>/g, '&gt;');
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>