mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 05:45:19 +00:00
parent
9b5da77ffc
commit
2e2a63c201
8 changed files with 555 additions and 304 deletions
|
|
@ -96,6 +96,8 @@
|
||||||
let controlPane;
|
let controlPane;
|
||||||
let controlPaneComponent;
|
let controlPaneComponent;
|
||||||
|
|
||||||
|
let messageInput;
|
||||||
|
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let processing = '';
|
let processing = '';
|
||||||
let messagesContainerElement: HTMLDivElement;
|
let messagesContainerElement: HTMLDivElement;
|
||||||
|
|
@ -140,24 +142,39 @@
|
||||||
let params = {};
|
let params = {};
|
||||||
|
|
||||||
$: if (chatIdProp) {
|
$: if (chatIdProp) {
|
||||||
(async () => {
|
navigateHandler();
|
||||||
loading = true;
|
}
|
||||||
|
|
||||||
prompt = '';
|
const navigateHandler = async () => {
|
||||||
files = [];
|
loading = true;
|
||||||
selectedToolIds = [];
|
|
||||||
selectedFilterIds = [];
|
|
||||||
webSearchEnabled = false;
|
|
||||||
imageGenerationEnabled = false;
|
|
||||||
|
|
||||||
if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
prompt = '';
|
||||||
|
messageInput?.setText('');
|
||||||
|
|
||||||
|
files = [];
|
||||||
|
selectedToolIds = [];
|
||||||
|
selectedFilterIds = [];
|
||||||
|
webSearchEnabled = false;
|
||||||
|
imageGenerationEnabled = false;
|
||||||
|
|
||||||
|
const storageChatInput = sessionStorage.getItem(
|
||||||
|
`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chatIdProp && (await loadChat())) {
|
||||||
|
await tick();
|
||||||
|
loading = false;
|
||||||
|
window.setTimeout(() => scrollToBottom(), 0);
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
if (storageChatInput) {
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(
|
const input = JSON.parse(storageChatInput);
|
||||||
sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
console.log(input);
|
||||||
if (!$temporaryChatEnabled) {
|
if (!$temporaryChatEnabled) {
|
||||||
prompt = input.prompt;
|
messageInput?.setText(input.prompt);
|
||||||
files = input.files;
|
files = input.files;
|
||||||
selectedToolIds = input.selectedToolIds;
|
selectedToolIds = input.selectedToolIds;
|
||||||
selectedFilterIds = input.selectedFilterIds;
|
selectedFilterIds = input.selectedFilterIds;
|
||||||
|
|
@ -168,17 +185,12 @@
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chatIdProp && (await loadChat())) {
|
const chatInput = document.getElementById('chat-input');
|
||||||
await tick();
|
chatInput?.focus();
|
||||||
loading = false;
|
} else {
|
||||||
window.setTimeout(() => scrollToBottom(), 0);
|
await goto('/');
|
||||||
const chatInput = document.getElementById('chat-input');
|
}
|
||||||
chatInput?.focus();
|
};
|
||||||
} else {
|
|
||||||
await goto('/');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (selectedModels && chatIdProp !== '') {
|
$: if (selectedModels && chatIdProp !== '') {
|
||||||
saveSessionSelectedModels();
|
saveSessionSelectedModels();
|
||||||
|
|
@ -405,7 +417,7 @@
|
||||||
const inputElement = document.getElementById('chat-input');
|
const inputElement = document.getElementById('chat-input');
|
||||||
|
|
||||||
if (inputElement) {
|
if (inputElement) {
|
||||||
prompt = event.data.text;
|
messageInput?.setText(event.data.text);
|
||||||
inputElement.focus();
|
inputElement.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -443,8 +455,19 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
const storageChatInput = sessionStorage.getItem(
|
||||||
|
`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!chatIdProp) {
|
||||||
|
loading = false;
|
||||||
|
await tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageChatInput) {
|
||||||
prompt = '';
|
prompt = '';
|
||||||
|
messageInput?.setText('');
|
||||||
|
|
||||||
files = [];
|
files = [];
|
||||||
selectedToolIds = [];
|
selectedToolIds = [];
|
||||||
selectedFilterIds = [];
|
selectedFilterIds = [];
|
||||||
|
|
@ -453,12 +476,11 @@
|
||||||
codeInterpreterEnabled = false;
|
codeInterpreterEnabled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(
|
const input = JSON.parse(storageChatInput);
|
||||||
sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
console.log(input);
|
||||||
);
|
|
||||||
|
|
||||||
if (!$temporaryChatEnabled) {
|
if (!$temporaryChatEnabled) {
|
||||||
prompt = input.prompt;
|
messageInput?.setText(input.prompt);
|
||||||
files = input.files;
|
files = input.files;
|
||||||
selectedToolIds = input.selectedToolIds;
|
selectedToolIds = input.selectedToolIds;
|
||||||
selectedFilterIds = input.selectedFilterIds;
|
selectedFilterIds = input.selectedFilterIds;
|
||||||
|
|
@ -469,11 +491,6 @@
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatIdProp) {
|
|
||||||
loading = false;
|
|
||||||
await tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
showControls.subscribe(async (value) => {
|
showControls.subscribe(async (value) => {
|
||||||
if (controlPane && !$mobile) {
|
if (controlPane && !$mobile) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -833,12 +850,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($page.url.searchParams.get('q')) {
|
if ($page.url.searchParams.get('q')) {
|
||||||
prompt = $page.url.searchParams.get('q') ?? '';
|
const q = $page.url.searchParams.get('q') ?? '';
|
||||||
|
messageInput?.setText(q);
|
||||||
|
|
||||||
if (prompt) {
|
if (q) {
|
||||||
if (($page.url.searchParams.get('submit') ?? 'true') === 'true') {
|
if (($page.url.searchParams.get('submit') ?? 'true') === 'true') {
|
||||||
await tick();
|
await tick();
|
||||||
submitPrompt(prompt);
|
submitPrompt(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1071,7 +1089,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMessagePair = async (userPrompt) => {
|
const createMessagePair = async (userPrompt) => {
|
||||||
prompt = '';
|
messageInput?.setText('');
|
||||||
if (selectedModels.length === 0) {
|
if (selectedModels.length === 0) {
|
||||||
toast.error($i18n.t('Model not selected'));
|
toast.error($i18n.t('Model not selected'));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1392,7 +1410,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt = '';
|
messageInput?.setText('');
|
||||||
|
|
||||||
// Reset chat input textarea
|
// Reset chat input textarea
|
||||||
if (!($settings?.richTextInput ?? true)) {
|
if (!($settings?.richTextInput ?? true)) {
|
||||||
|
|
@ -1413,7 +1431,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
files = [];
|
files = [];
|
||||||
prompt = '';
|
messageInput?.setText('');
|
||||||
|
|
||||||
// Create user message
|
// Create user message
|
||||||
let userMessageId = uuidv4();
|
let userMessageId = uuidv4();
|
||||||
|
|
@ -2104,6 +2122,7 @@
|
||||||
|
|
||||||
<div class=" pb-2">
|
<div class=" pb-2">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
|
bind:this={messageInput}
|
||||||
{history}
|
{history}
|
||||||
{taskIds}
|
{taskIds}
|
||||||
{selectedModels}
|
{selectedModels}
|
||||||
|
|
@ -2166,6 +2185,7 @@
|
||||||
<Placeholder
|
<Placeholder
|
||||||
{history}
|
{history}
|
||||||
{selectedModels}
|
{selectedModels}
|
||||||
|
bind:messageInput
|
||||||
bind:files
|
bind:files
|
||||||
bind:prompt
|
bind:prompt
|
||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,13 @@
|
||||||
blobToFile,
|
blobToFile,
|
||||||
compressImage,
|
compressImage,
|
||||||
createMessagesList,
|
createMessagesList,
|
||||||
extractCurlyBraceWords
|
extractCurlyBraceWords,
|
||||||
|
getCurrentDateTime,
|
||||||
|
getFormattedDate,
|
||||||
|
getFormattedTime,
|
||||||
|
getUserPosition,
|
||||||
|
getUserTimezone,
|
||||||
|
getWeekday
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { uploadFile } from '$lib/apis/files';
|
import { uploadFile } from '$lib/apis/files';
|
||||||
import { generateAutoCompletion } from '$lib/apis';
|
import { generateAutoCompletion } from '$lib/apis';
|
||||||
|
|
@ -58,7 +64,6 @@
|
||||||
import Sparkles from '../icons/Sparkles.svelte';
|
import Sparkles from '../icons/Sparkles.svelte';
|
||||||
|
|
||||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let transparentBackground = false;
|
export let transparentBackground = false;
|
||||||
|
|
@ -108,6 +113,220 @@
|
||||||
codeInterpreterEnabled
|
codeInterpreterEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setText = (text?: string) => {
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
|
if (chatInput) {
|
||||||
|
if ($settings?.richTextInput ?? true) {
|
||||||
|
chatInputElement.setText(text);
|
||||||
|
} else {
|
||||||
|
// chatInput.value = text;
|
||||||
|
prompt = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 rightWord = right.match(/^([^\s]*)/)?.[1] || '';
|
||||||
|
return leftWord + rightWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCommand = () => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const textVariableHandler = async (text: string) => {
|
||||||
|
if (text.includes('{{CLIPBOARD}}')) {
|
||||||
|
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||||
|
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||||
|
return '{{CLIPBOARD}}';
|
||||||
|
});
|
||||||
|
|
||||||
|
const clipboardItems = await navigator.clipboard.read();
|
||||||
|
|
||||||
|
let imageUrl = null;
|
||||||
|
for (const item of clipboardItems) {
|
||||||
|
// Check for known image types
|
||||||
|
for (const type of item.types) {
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
const blob = await item.getType(type);
|
||||||
|
imageUrl = URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
files = [
|
||||||
|
...files,
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
url: imageUrl
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{USER_LOCATION}}')) {
|
||||||
|
let location;
|
||||||
|
try {
|
||||||
|
location = await getUserPosition();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error($i18n.t('Location access not allowed'));
|
||||||
|
location = 'LOCATION_UNKNOWN';
|
||||||
|
}
|
||||||
|
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{USER_NAME}}')) {
|
||||||
|
const name = $_user?.name || 'User';
|
||||||
|
text = text.replaceAll('{{USER_NAME}}', name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{USER_LANGUAGE}}')) {
|
||||||
|
const language = localStorage.getItem('locale') || 'en-US';
|
||||||
|
text = text.replaceAll('{{USER_LANGUAGE}}', language);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{CURRENT_DATE}}')) {
|
||||||
|
const date = getFormattedDate();
|
||||||
|
text = text.replaceAll('{{CURRENT_DATE}}', date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{CURRENT_TIME}}')) {
|
||||||
|
const time = getFormattedTime();
|
||||||
|
text = text.replaceAll('{{CURRENT_TIME}}', time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{CURRENT_DATETIME}}')) {
|
||||||
|
const dateTime = getCurrentDateTime();
|
||||||
|
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{CURRENT_TIMEZONE}}')) {
|
||||||
|
const timezone = getUserTimezone();
|
||||||
|
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('{{CURRENT_WEEKDAY}}')) {
|
||||||
|
const weekday = getWeekday();
|
||||||
|
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||||
|
}
|
||||||
|
|
||||||
|
text = await inputVariableHandler(text);
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertTextAtCursor = async (text: string) => {
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
if (!chatInput) return;
|
||||||
|
|
||||||
|
text = await textVariableHandler(text);
|
||||||
|
if (command) {
|
||||||
|
replaceCommandWithText(text);
|
||||||
|
} else {
|
||||||
|
if ($settings?.richTextInput ?? true) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
range.deleteContents();
|
||||||
|
range.insertNode(document.createTextNode(text));
|
||||||
|
range.collapse(false);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const cursor = chatInput.selectionStart;
|
||||||
|
prompt = prompt.slice(0, cursor) + text + prompt.slice(cursor);
|
||||||
|
chatInput.focus();
|
||||||
|
chatInput.setSelectionRange(cursor + text.length, cursor + text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const chatInputContainer = document.getElementById('chat-input-container');
|
||||||
|
if (chatInputContainer) {
|
||||||
|
chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
if (chatInput) {
|
||||||
|
chatInput.focus();
|
||||||
|
chatInput.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
const words = extractCurlyBraceWords(prompt);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
const word = words.at(0);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
if (!($settings?.richTextInput ?? true)) {
|
||||||
|
// Move scroll to the first word
|
||||||
|
chatInput.setSelectionRange(word.startIndex, word.endIndex + 1);
|
||||||
|
chatInput.focus();
|
||||||
|
|
||||||
|
const selectionRow =
|
||||||
|
(word?.startIndex - (word?.startIndex % chatInput.cols)) / chatInput.cols;
|
||||||
|
const lineHeight = chatInput.clientHeight / chatInput.rows;
|
||||||
|
|
||||||
|
chatInput.scrollTop = lineHeight * selectionRow;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chatInput.scrollTop = chatInput.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = '';
|
||||||
|
|
||||||
|
let showCommands = false;
|
||||||
|
$: showCommands = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
|
||||||
|
|
||||||
let showTools = false;
|
let showTools = false;
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
@ -583,20 +802,36 @@
|
||||||
|
|
||||||
<Commands
|
<Commands
|
||||||
bind:this={commandsElement}
|
bind:this={commandsElement}
|
||||||
bind:prompt
|
|
||||||
bind:files
|
bind:files
|
||||||
on:upload={(e) => {
|
show={showCommands}
|
||||||
dispatch('upload', e.detail);
|
{command}
|
||||||
}}
|
insertTextHandler={insertTextAtCursor}
|
||||||
on:select={(e) => {
|
onUpload={(e) => {
|
||||||
const data = e.detail;
|
const { type, data } = e;
|
||||||
|
|
||||||
if (data?.type === 'model') {
|
if (type === 'file') {
|
||||||
atSelectedModel = data.data;
|
if (files.find((f) => f.id === data.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
files = [
|
||||||
|
...files,
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
status: 'processed'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
dispatch('upload', e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSelect={(e) => {
|
||||||
|
const { type, data } = e;
|
||||||
|
|
||||||
|
if (type === 'model') {
|
||||||
|
atSelectedModel = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
document.getElementById('chat-input')?.focus();
|
||||||
chatInputElement?.focus();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -770,8 +1005,12 @@
|
||||||
>
|
>
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
bind:this={chatInputElement}
|
bind:this={chatInputElement}
|
||||||
bind:value={prompt}
|
|
||||||
id="chat-input"
|
id="chat-input"
|
||||||
|
onChange={(e) => {
|
||||||
|
prompt = e.md;
|
||||||
|
command = getCommand();
|
||||||
|
}}
|
||||||
|
json={true}
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||||
(!$mobile ||
|
(!$mobile ||
|
||||||
|
|
@ -990,6 +1229,12 @@
|
||||||
class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
|
class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
|
||||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||||
bind:value={prompt}
|
bind:value={prompt}
|
||||||
|
on:input={() => {
|
||||||
|
command = getCommand();
|
||||||
|
}}
|
||||||
|
on:click={() => {
|
||||||
|
command = getCommand();
|
||||||
|
}}
|
||||||
on:compositionstart={() => (isComposing = true)}
|
on:compositionstart={() => (isComposing = true)}
|
||||||
on:compositionend={() => (isComposing = false)}
|
on:compositionend={() => (isComposing = false)}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
|
|
@ -1137,17 +1382,20 @@
|
||||||
|
|
||||||
if (words.length > 0) {
|
if (words.length > 0) {
|
||||||
const word = words.at(0);
|
const word = words.at(0);
|
||||||
const fullPrompt = prompt;
|
|
||||||
|
|
||||||
prompt = prompt.substring(0, word?.endIndex + 1);
|
if (word && e.target instanceof HTMLTextAreaElement) {
|
||||||
await tick();
|
// Prevent default tab behavior
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||||
|
e.target.focus();
|
||||||
|
|
||||||
e.target.scrollTop = e.target.scrollHeight;
|
const selectionRow =
|
||||||
prompt = fullPrompt;
|
(word?.startIndex - (word?.startIndex % e.target.cols)) /
|
||||||
await tick();
|
e.target.cols;
|
||||||
|
const lineHeight = e.target.clientHeight / e.target.rows;
|
||||||
|
|
||||||
e.preventDefault();
|
e.target.scrollTop = lineHeight * selectionRow;
|
||||||
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.target.style.height = '';
|
e.target.style.height = '';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
import { knowledge, prompts } from '$lib/stores';
|
import { knowledge, prompts } from '$lib/stores';
|
||||||
|
|
||||||
import { removeLastWordFromString } from '$lib/utils';
|
import { removeLastWordFromString } from '$lib/utils';
|
||||||
|
|
@ -15,8 +10,15 @@
|
||||||
import Models from './Commands/Models.svelte';
|
import Models from './Commands/Models.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
|
||||||
export let prompt = '';
|
export let show = false;
|
||||||
|
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
export let command = '';
|
||||||
|
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
export let onUpload = (e) => {};
|
||||||
|
|
||||||
|
export let insertTextHandler = (text) => {};
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let commandElement = null;
|
let commandElement = null;
|
||||||
|
|
@ -29,12 +31,6 @@
|
||||||
commandElement?.selectDown();
|
commandElement?.selectDown();
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = '';
|
|
||||||
$: command = prompt?.split('\n').pop()?.split(' ')?.pop() ?? '';
|
|
||||||
|
|
||||||
let show = false;
|
|
||||||
$: show = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2);
|
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
@ -56,54 +52,63 @@
|
||||||
{#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}
|
||||||
|
{command}
|
||||||
|
onSelect={(e) => {
|
||||||
|
const { type, data } = e;
|
||||||
|
|
||||||
|
if (type === 'prompt') {
|
||||||
|
insertTextHandler(data.content);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{: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}
|
||||||
bind:prompt
|
|
||||||
command={command.includes('\\#') ? command.slice(2) : command}
|
command={command.includes('\\#') ? command.slice(2) : command}
|
||||||
on:youtube={(e) => {
|
onSelect={(e) => {
|
||||||
console.log(e);
|
const { type, data } = e;
|
||||||
dispatch('upload', {
|
|
||||||
type: 'youtube',
|
if (type === 'knowledge') {
|
||||||
data: e.detail
|
insertTextHandler('');
|
||||||
});
|
|
||||||
}}
|
onUpload({
|
||||||
on:url={(e) => {
|
type: 'file',
|
||||||
console.log(e);
|
data: data
|
||||||
dispatch('upload', {
|
});
|
||||||
type: 'web',
|
} else if (type === 'youtube') {
|
||||||
data: e.detail
|
insertTextHandler('');
|
||||||
});
|
|
||||||
}}
|
onUpload({
|
||||||
on:select={(e) => {
|
type: 'youtube',
|
||||||
console.log(e);
|
data: data
|
||||||
if (files.find((f) => f.id === e.detail.id)) {
|
});
|
||||||
return;
|
} else if (type === 'web') {
|
||||||
|
insertTextHandler('');
|
||||||
|
|
||||||
|
onUpload({
|
||||||
|
type: 'web',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
{
|
|
||||||
...e.detail,
|
|
||||||
status: 'processed'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
dispatch('select');
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if command?.charAt(0) === '@'}
|
{:else if command?.charAt(0) === '@'}
|
||||||
<Models
|
<Models
|
||||||
bind:this={commandElement}
|
bind:this={commandElement}
|
||||||
{command}
|
{command}
|
||||||
on:select={(e) => {
|
onSelect={(e) => {
|
||||||
prompt = removeLastWordFromString(prompt, command);
|
const { type, data } = e;
|
||||||
|
|
||||||
dispatch('select', {
|
if (type === 'model') {
|
||||||
type: 'model',
|
insertTextHandler('');
|
||||||
data: e.detail
|
|
||||||
});
|
onSelect({
|
||||||
|
type: 'model',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,15 @@
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
|
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
||||||
import { knowledge } from '$lib/stores';
|
import { knowledge } from '$lib/stores';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let prompt = '';
|
|
||||||
export let command = '';
|
export let command = '';
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
let selectedIdx = 0;
|
let selectedIdx = 0;
|
||||||
|
|
||||||
let items = [];
|
let items = [];
|
||||||
|
|
@ -60,37 +59,12 @@
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const confirmSelect = async (item) => {
|
|
||||||
dispatch('select', item);
|
|
||||||
|
|
||||||
prompt = removeLastWordFromString(prompt, command);
|
const confirmSelect = async (type, data) => {
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
onSelect({
|
||||||
|
type: type,
|
||||||
await tick();
|
data: data
|
||||||
chatInputElement?.focus();
|
});
|
||||||
await tick();
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmSelectWeb = async (url) => {
|
|
||||||
dispatch('url', url);
|
|
||||||
|
|
||||||
prompt = removeLastWordFromString(prompt, command);
|
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
chatInputElement?.focus();
|
|
||||||
await tick();
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmSelectYoutube = async (url) => {
|
|
||||||
dispatch('youtube', url);
|
|
||||||
|
|
||||||
prompt = removeLastWordFromString(prompt, command);
|
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
chatInputElement?.focus();
|
|
||||||
await tick();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const decodeString = (str: string) => {
|
const decodeString = (str: string) => {
|
||||||
|
|
@ -189,7 +163,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if filteredItems.length > 0 || command?.substring(1).startsWith('http')}
|
||||||
<div
|
<div
|
||||||
id="commands-container"
|
id="commands-container"
|
||||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||||
|
|
@ -210,7 +184,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
console.log(item);
|
console.log(item);
|
||||||
confirmSelect(item);
|
confirmSelect('knowledge', item);
|
||||||
}}
|
}}
|
||||||
on:mousemove={() => {
|
on:mousemove={() => {
|
||||||
selectedIdx = idx;
|
selectedIdx = idx;
|
||||||
|
|
@ -298,18 +272,15 @@
|
||||||
</div> -->
|
</div> -->
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if prompt
|
{#if command.substring(1).startsWith('https://www.youtube.com') || command
|
||||||
.split(' ')
|
.substring(1)
|
||||||
.some((s) => s.substring(1).startsWith('https://www.youtube.com') || s
|
.startsWith('https://youtu.be')}
|
||||||
.substring(1)
|
|
||||||
.startsWith('https://youtu.be'))}
|
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
if (isValidHttpUrl(command.substring(1))) {
|
||||||
if (isValidHttpUrl(url)) {
|
confirmSelect('youtube', command.substring(1));
|
||||||
confirmSelectYoutube(url);
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(
|
$i18n.t(
|
||||||
|
|
@ -320,19 +291,18 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
{command.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
|
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
|
||||||
</button>
|
</button>
|
||||||
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{:else if command.substring(1).startsWith('http')}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
if (isValidHttpUrl(command.substring(1))) {
|
||||||
if (isValidHttpUrl(url)) {
|
confirmSelect('web', command.substring(1));
|
||||||
confirmSelectWeb(url);
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(
|
$i18n.t(
|
||||||
|
|
@ -343,7 +313,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
{command}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
|
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,8 @@
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
export let command = '';
|
export let command = '';
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
let selectedIdx = 0;
|
let selectedIdx = 0;
|
||||||
let filteredItems = [];
|
let filteredItems = [];
|
||||||
|
|
@ -71,8 +70,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmSelect = async (model) => {
|
const confirmSelect = async (model) => {
|
||||||
command = '';
|
onSelect({ type: 'model', data: model });
|
||||||
dispatch('select', model);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,8 @@
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let files;
|
|
||||||
|
|
||||||
export let prompt = '';
|
|
||||||
export let command = '';
|
export let command = '';
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
let selectedPromptIdx = 0;
|
let selectedPromptIdx = 0;
|
||||||
let filteredPrompts = [];
|
let filteredPrompts = [];
|
||||||
|
|
@ -58,137 +56,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmPrompt = async (command) => {
|
const confirmPrompt = async (command) => {
|
||||||
let text = command.content;
|
onSelect({ type: 'prompt', data: command });
|
||||||
|
|
||||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
|
||||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
|
||||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
|
||||||
return '{{CLIPBOARD}}';
|
|
||||||
});
|
|
||||||
|
|
||||||
const clipboardItems = await navigator.clipboard.read();
|
|
||||||
|
|
||||||
let imageUrl = null;
|
|
||||||
for (const item of clipboardItems) {
|
|
||||||
// Check for known image types
|
|
||||||
for (const type of item.types) {
|
|
||||||
if (type.startsWith('image/')) {
|
|
||||||
const blob = await item.getType(type);
|
|
||||||
imageUrl = URL.createObjectURL(blob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrl) {
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
url: imageUrl
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{USER_LOCATION}}')) {
|
|
||||||
let location;
|
|
||||||
try {
|
|
||||||
location = await getUserPosition();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error($i18n.t('Location access not allowed'));
|
|
||||||
location = 'LOCATION_UNKNOWN';
|
|
||||||
}
|
|
||||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{USER_NAME}}')) {
|
|
||||||
console.log($user);
|
|
||||||
const name = $user?.name || 'User';
|
|
||||||
text = text.replaceAll('{{USER_NAME}}', name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{USER_LANGUAGE}}')) {
|
|
||||||
const language = localStorage.getItem('locale') || 'en-US';
|
|
||||||
text = text.replaceAll('{{USER_LANGUAGE}}', language);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{CURRENT_DATE}}')) {
|
|
||||||
const date = getFormattedDate();
|
|
||||||
text = text.replaceAll('{{CURRENT_DATE}}', date);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{CURRENT_TIME}}')) {
|
|
||||||
const time = getFormattedTime();
|
|
||||||
text = text.replaceAll('{{CURRENT_TIME}}', time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{CURRENT_DATETIME}}')) {
|
|
||||||
const dateTime = getCurrentDateTime();
|
|
||||||
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{CURRENT_TIMEZONE}}')) {
|
|
||||||
const timezone = getUserTimezone();
|
|
||||||
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.content.includes('{{CURRENT_WEEKDAY}}')) {
|
|
||||||
const weekday = getWeekday();
|
|
||||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = prompt.split('\n');
|
|
||||||
const lastLine = lines.pop();
|
|
||||||
|
|
||||||
const lastLineWords = lastLine.split(' ');
|
|
||||||
const lastWord = lastLineWords.pop();
|
|
||||||
|
|
||||||
if ($settings?.richTextInput ?? true) {
|
|
||||||
lastLineWords.push(
|
|
||||||
`${text.replace(/</g, '<').replace(/>/g, '>').replaceAll('\n', '<br/>')}`
|
|
||||||
);
|
|
||||||
|
|
||||||
lines.push(lastLineWords.join(' '));
|
|
||||||
prompt = lines.join('<br/>');
|
|
||||||
} else {
|
|
||||||
lastLineWords.push(text);
|
|
||||||
lines.push(lastLineWords.join(' '));
|
|
||||||
prompt = lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
if (chatInputContainerElement) {
|
|
||||||
chatInputContainerElement.scrollTop = chatInputContainerElement.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
if (chatInputElement) {
|
|
||||||
chatInputElement.focus();
|
|
||||||
chatInputElement.dispatchEvent(new Event('input'));
|
|
||||||
|
|
||||||
const words = extractCurlyBraceWords(prompt);
|
|
||||||
|
|
||||||
if (words.length > 0) {
|
|
||||||
const word = words.at(0);
|
|
||||||
await tick();
|
|
||||||
|
|
||||||
if (!($settings?.richTextInput ?? true)) {
|
|
||||||
chatInputElement.setSelectionRange(word.startIndex, word.endIndex + 1);
|
|
||||||
chatInputElement.focus();
|
|
||||||
|
|
||||||
// This is a workaround to ensure the cursor is placed correctly
|
|
||||||
// after the text is inserted, especially for multiline inputs.
|
|
||||||
chatInputElement.setSelectionRange(word.startIndex, word.endIndex + 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
@ -213,14 +81,14 @@
|
||||||
id="command-options-container"
|
id="command-options-container"
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
>
|
>
|
||||||
{#each filteredPrompts as prompt, promptIdx}
|
{#each filteredPrompts as promptItem, promptIdx}
|
||||||
<button
|
<button
|
||||||
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
|
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
|
||||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
confirmPrompt(prompt);
|
confirmPrompt(promptItem);
|
||||||
}}
|
}}
|
||||||
on:mousemove={() => {
|
on:mousemove={() => {
|
||||||
selectedPromptIdx = promptIdx;
|
selectedPromptIdx = promptIdx;
|
||||||
|
|
@ -228,11 +96,11 @@
|
||||||
on:focus={() => {}}
|
on:focus={() => {}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black dark:text-gray-100">
|
<div class=" font-medium text-black dark:text-gray-100">
|
||||||
{prompt.command}
|
{promptItem.command}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
||||||
{prompt.title}
|
{promptItem.title}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
export let messageInput = null;
|
||||||
|
|
||||||
export let selectedToolIds = [];
|
export let selectedToolIds = [];
|
||||||
export let selectedFilterIds = [];
|
export let selectedFilterIds = [];
|
||||||
|
|
@ -207,6 +208,7 @@
|
||||||
|
|
||||||
<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
|
<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
|
bind:this={messageInput}
|
||||||
{history}
|
{history}
|
||||||
{selectedModels}
|
{selectedModels}
|
||||||
bind:files
|
bind:files
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,12 @@
|
||||||
// Use turndown-plugin-gfm for proper GFM table support
|
// Use turndown-plugin-gfm for proper GFM table support
|
||||||
turndownService.use(gfm);
|
turndownService.use(gfm);
|
||||||
|
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy, tick } from 'svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
const eventDispatch = createEventDispatcher();
|
const eventDispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import { Fragment } from 'prosemirror-model';
|
||||||
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
|
|
@ -76,6 +78,135 @@
|
||||||
editor.commands.setContent(html);
|
editor.commands.setContent(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getWordAtDocPos = () => {
|
||||||
|
if (!editor) return '';
|
||||||
|
const { state } = editor.view;
|
||||||
|
const pos = state.selection.from;
|
||||||
|
const doc = state.doc;
|
||||||
|
const resolvedPos = doc.resolve(pos);
|
||||||
|
const textBlock = resolvedPos.parent;
|
||||||
|
const paraStart = resolvedPos.start();
|
||||||
|
const text = textBlock.textContent;
|
||||||
|
const offset = resolvedPos.parentOffset;
|
||||||
|
|
||||||
|
let wordStart = offset,
|
||||||
|
wordEnd = offset;
|
||||||
|
while (wordStart > 0 && !/\s/.test(text[wordStart - 1])) wordStart--;
|
||||||
|
while (wordEnd < text.length && !/\s/.test(text[wordEnd])) wordEnd++;
|
||||||
|
|
||||||
|
const word = text.slice(wordStart, wordEnd);
|
||||||
|
|
||||||
|
return word;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns {start, end} of the word at pos
|
||||||
|
function getWordBoundsAtPos(doc, pos) {
|
||||||
|
const resolvedPos = doc.resolve(pos);
|
||||||
|
const textBlock = resolvedPos.parent;
|
||||||
|
const paraStart = resolvedPos.start();
|
||||||
|
const text = textBlock.textContent;
|
||||||
|
|
||||||
|
const offset = resolvedPos.parentOffset;
|
||||||
|
let wordStart = offset,
|
||||||
|
wordEnd = offset;
|
||||||
|
while (wordStart > 0 && !/\s/.test(text[wordStart - 1])) wordStart--;
|
||||||
|
while (wordEnd < text.length && !/\s/.test(text[wordEnd])) wordEnd++;
|
||||||
|
return {
|
||||||
|
start: paraStart + wordStart,
|
||||||
|
end: paraStart + wordEnd
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const replaceCommandWithText = async (text) => {
|
||||||
|
const { state, dispatch } = editor.view;
|
||||||
|
const { selection } = state;
|
||||||
|
const pos = selection.from;
|
||||||
|
|
||||||
|
// Get the plain text of this document
|
||||||
|
// const docText = state.doc.textBetween(0, state.doc.content.size, '\n', '\n');
|
||||||
|
|
||||||
|
// Find the word boundaries at cursor
|
||||||
|
const { start, end } = getWordBoundsAtPos(state.doc, pos);
|
||||||
|
|
||||||
|
let tr = state.tr;
|
||||||
|
|
||||||
|
if (text.includes('\n')) {
|
||||||
|
// Split the text into lines and create a <p> node for each line
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const nodes = lines.map(
|
||||||
|
(line, index) =>
|
||||||
|
index === 0
|
||||||
|
? state.schema.text(line) // First line is plain text
|
||||||
|
: state.schema.nodes.paragraph.create({}, line ? state.schema.text(line) : undefined) // Subsequent lines are paragraphs
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build and dispatch the transaction to replace the word at cursor
|
||||||
|
tr = tr.replaceWith(start, end, nodes);
|
||||||
|
|
||||||
|
let newSelectionPos;
|
||||||
|
|
||||||
|
// +1 because the insert happens at start, so last para starts at (start + sum of all previous nodes' sizes)
|
||||||
|
let lastPos = start;
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
lastPos += nodes[i].nodeSize;
|
||||||
|
}
|
||||||
|
// Place cursor inside the last paragraph at its end
|
||||||
|
newSelectionPos = lastPos;
|
||||||
|
|
||||||
|
tr = tr.setSelection(TextSelection.near(tr.doc.resolve(newSelectionPos)));
|
||||||
|
} else {
|
||||||
|
tr = tr.replaceWith(
|
||||||
|
start,
|
||||||
|
end, // replace this range
|
||||||
|
text !== '' ? state.schema.text(text) : []
|
||||||
|
);
|
||||||
|
|
||||||
|
tr = tr.setSelection(
|
||||||
|
state.selection.constructor.near(tr.doc.resolve(start + text.length + 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(tr);
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
// selectNextTemplate(state, dispatch);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setText = (text: string) => {
|
||||||
|
if (!editor) return;
|
||||||
|
text = text.replaceAll('\n\n', '\n');
|
||||||
|
const { state, view } = editor;
|
||||||
|
|
||||||
|
if (text.includes('\n')) {
|
||||||
|
// Multiple lines: make paragraphs
|
||||||
|
const { schema, tr } = state;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// Map each line to a paragraph node (empty lines -> empty paragraph)
|
||||||
|
const nodes = lines.map((line) =>
|
||||||
|
schema.nodes.paragraph.create({}, line ? schema.text(line) : undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a document fragment containing all parsed paragraphs
|
||||||
|
const fragment = Fragment.fromArray(nodes);
|
||||||
|
|
||||||
|
// Replace current selection with these paragraphs
|
||||||
|
tr.replaceSelectionWith(fragment, false /* don't select new */);
|
||||||
|
|
||||||
|
// You probably want to move the cursor after the inserted content
|
||||||
|
// tr.setSelection(Selection.near(tr.doc.resolve(tr.selection.to)));
|
||||||
|
|
||||||
|
view.dispatch(tr);
|
||||||
|
} else if (text === '') {
|
||||||
|
// Empty: delete selection or paragraph
|
||||||
|
editor.commands.clearContent();
|
||||||
|
} else {
|
||||||
|
editor.commands.setContent(editor.state.schema.text(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNextTemplate(editor.view.state, editor.view.dispatch);
|
||||||
|
};
|
||||||
|
|
||||||
// Function to find the next template in the document
|
// Function to find the next template in the document
|
||||||
function findNextTemplate(doc, from = 0) {
|
function findNextTemplate(doc, from = 0) {
|
||||||
const patterns = [{ start: '{{', end: '}}' }];
|
const patterns = [{ start: '{{', end: '}}' }];
|
||||||
|
|
@ -240,9 +371,18 @@
|
||||||
onChange({
|
onChange({
|
||||||
html: editor.getHTML(),
|
html: editor.getHTML(),
|
||||||
json: editor.getJSON(),
|
json: editor.getJSON(),
|
||||||
md: turndownService.turndown(editor.getHTML())
|
md: turndownService
|
||||||
|
.turndown(
|
||||||
|
editor
|
||||||
|
.getHTML()
|
||||||
|
.replace(/<p><\/p>/g, '<br/>')
|
||||||
|
.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
|
||||||
|
)
|
||||||
|
.replace(/\u00a0/g, ' ')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(html);
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
value = editor.getJSON();
|
value = editor.getJSON();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -308,7 +448,7 @@
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
|
const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
|
||||||
if (event.shiftKey && !isCtrlPressed) {
|
if (event.shiftKey && !isCtrlPressed) {
|
||||||
editor.commands.setHardBreak(); // Insert a hard break
|
editor.commands.enter(); // Insert a new line
|
||||||
view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor
|
view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue