mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 13:55:19 +00:00
refac: channel input
This commit is contained in:
parent
b2623c9799
commit
06c1426e14
11 changed files with 567 additions and 637 deletions
|
|
@ -17,9 +17,12 @@
|
||||||
getFormattedTime,
|
getFormattedTime,
|
||||||
getUserPosition,
|
getUserPosition,
|
||||||
getUserTimezone,
|
getUserTimezone,
|
||||||
getWeekday
|
getWeekday,
|
||||||
|
extractCurlyBraceWords
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
|
||||||
|
import { getSessionUser } from '$lib/apis/auths';
|
||||||
|
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import RichTextInput from '../common/RichTextInput.svelte';
|
import RichTextInput from '../common/RichTextInput.svelte';
|
||||||
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
||||||
|
|
@ -29,26 +32,16 @@
|
||||||
import FileItem from '../common/FileItem.svelte';
|
import FileItem from '../common/FileItem.svelte';
|
||||||
import Image from '../common/Image.svelte';
|
import Image from '../common/Image.svelte';
|
||||||
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
||||||
import Commands from '../chat/MessageInput/Commands.svelte';
|
|
||||||
import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
|
import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
|
||||||
import { getSessionUser } from '$lib/apis/auths';
|
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
||||||
|
import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
|
||||||
|
import MentionList from './MessageInput/MentionList.svelte';
|
||||||
|
|
||||||
export let placeholder = $i18n.t('Send a Message');
|
export let placeholder = $i18n.t('Send a Message');
|
||||||
|
|
||||||
export let id = null;
|
export let id = null;
|
||||||
|
|
||||||
let draggedOver = false;
|
|
||||||
|
|
||||||
let recording = false;
|
|
||||||
let content = '';
|
|
||||||
let files = [];
|
|
||||||
|
|
||||||
export let chatInputElement;
|
export let chatInputElement;
|
||||||
|
|
||||||
let commandsElement;
|
|
||||||
let filesInputElement;
|
|
||||||
let inputFiles;
|
|
||||||
|
|
||||||
export let typingUsers = [];
|
export let typingUsers = [];
|
||||||
export let inputLoading = false;
|
export let inputLoading = false;
|
||||||
|
|
||||||
|
|
@ -62,15 +55,39 @@
|
||||||
export let acceptFiles = true;
|
export let acceptFiles = true;
|
||||||
export let showFormattingToolbar = true;
|
export let showFormattingToolbar = true;
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
let draggedOver = false;
|
||||||
|
|
||||||
|
let recording = false;
|
||||||
|
let content = '';
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
let filesInputElement;
|
||||||
|
let inputFiles;
|
||||||
|
|
||||||
let showInputVariablesModal = false;
|
let showInputVariablesModal = false;
|
||||||
|
let inputVariablesModalCallback: (variableValues: Record<string, any>) => void;
|
||||||
let inputVariables: Record<string, any> = {};
|
let inputVariables: Record<string, any> = {};
|
||||||
let inputVariableValues = {};
|
let inputVariableValues = {};
|
||||||
|
|
||||||
const inputVariableHandler = async (text: string) => {
|
const inputVariableHandler = async (text: string): Promise<string> => {
|
||||||
inputVariables = extractInputVariables(text);
|
inputVariables = extractInputVariables(text);
|
||||||
if (Object.keys(inputVariables).length > 0) {
|
|
||||||
showInputVariablesModal = true;
|
// No variables? return the original text immediately.
|
||||||
|
if (Object.keys(inputVariables).length === 0) {
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show modal and wait for the user's input.
|
||||||
|
showInputVariablesModal = true;
|
||||||
|
return await new Promise<string>((resolve) => {
|
||||||
|
inputVariablesModalCallback = (variableValues) => {
|
||||||
|
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
||||||
|
replaceVariables(inputVariableValues);
|
||||||
|
showInputVariablesModal = false;
|
||||||
|
resolve(text);
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const textVariableHandler = async (text: string) => {
|
const textVariableHandler = async (text: string) => {
|
||||||
|
|
@ -188,68 +205,87 @@
|
||||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputVariableHandler(text);
|
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceVariables = (variables: Record<string, any>) => {
|
const replaceVariables = (variables: Record<string, any>) => {
|
||||||
if (!chatInputElement) return;
|
|
||||||
console.log('Replacing variables:', variables);
|
console.log('Replacing variables:', variables);
|
||||||
|
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
|
if (chatInput) {
|
||||||
chatInputElement.replaceVariables(variables);
|
chatInputElement.replaceVariables(variables);
|
||||||
chatInputElement.focus();
|
chatInputElement.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setText = async (text?: string) => {
|
export const setText = async (text?: string, cb?: (text: string) => void) => {
|
||||||
if (!chatInputElement) return;
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
|
if (chatInput) {
|
||||||
text = await textVariableHandler(text || '');
|
text = await textVariableHandler(text || '');
|
||||||
|
|
||||||
chatInputElement?.setText(text);
|
chatInputElement?.setText(text);
|
||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
|
|
||||||
|
text = await inputVariableHandler(text);
|
||||||
|
await tick();
|
||||||
|
if (cb) await cb(text);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCommand = () => {
|
const getCommand = () => {
|
||||||
if (!chatInputElement) return;
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
let word = '';
|
let word = '';
|
||||||
|
|
||||||
|
if (chatInput) {
|
||||||
word = chatInputElement?.getWordAtDocPos();
|
word = chatInputElement?.getWordAtDocPos();
|
||||||
|
}
|
||||||
|
|
||||||
return word;
|
return word;
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceCommandWithText = (text) => {
|
const replaceCommandWithText = (text) => {
|
||||||
if (!chatInputElement) return;
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
if (!chatInput) return;
|
||||||
|
|
||||||
chatInputElement?.replaceCommandWithText(text);
|
chatInputElement?.replaceCommandWithText(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const insertTextAtCursor = async (text: string) => {
|
const insertTextAtCursor = async (text: string) => {
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
if (!chatInput) return;
|
||||||
|
|
||||||
text = await textVariableHandler(text);
|
text = await textVariableHandler(text);
|
||||||
|
|
||||||
if (command) {
|
if (command) {
|
||||||
replaceCommandWithText(text);
|
replaceCommandWithText(text);
|
||||||
} else {
|
} else {
|
||||||
const selection = window.getSelection();
|
chatInputElement?.insertContent(text);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
text = await inputVariableHandler(text);
|
||||||
|
await tick();
|
||||||
|
|
||||||
const chatInputContainer = document.getElementById('chat-input-container');
|
const chatInputContainer = document.getElementById('chat-input-container');
|
||||||
if (chatInputContainer) {
|
if (chatInputContainer) {
|
||||||
chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
|
chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
if (chatInputElement) {
|
if (chatInput) {
|
||||||
chatInputElement.focus();
|
chatInput.focus();
|
||||||
|
chatInput.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
const words = extractCurlyBraceWords(prompt);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
const word = words.at(0);
|
||||||
|
await tick();
|
||||||
|
} else {
|
||||||
|
chatInput.scrollTop = chatInput.scrollHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -257,6 +293,7 @@
|
||||||
|
|
||||||
export let showCommands = false;
|
export let showCommands = false;
|
||||||
$: showCommands = ['/'].includes(command?.charAt(0));
|
$: showCommands = ['/'].includes(command?.charAt(0));
|
||||||
|
let suggestions = null;
|
||||||
|
|
||||||
const screenCaptureHandler = async () => {
|
const screenCaptureHandler = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -514,6 +551,49 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
suggestions = [
|
||||||
|
{
|
||||||
|
char: '@',
|
||||||
|
render: getSuggestionRenderer(MentionList, {
|
||||||
|
i18n
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
char: '/',
|
||||||
|
render: getSuggestionRenderer(CommandSuggestionList, {
|
||||||
|
i18n,
|
||||||
|
onSelect: (e) => {
|
||||||
|
const { type, data } = e;
|
||||||
|
|
||||||
|
if (type === 'model') {
|
||||||
|
console.log('Selected model:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('chat-input')?.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
insertTextHandler: insertTextAtCursor,
|
||||||
|
onUpload: (e) => {
|
||||||
|
const { type, data } = e;
|
||||||
|
|
||||||
|
if (type === 'file') {
|
||||||
|
if (files.find((f) => f.id === data.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
files = [
|
||||||
|
...files,
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
status: 'processed'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
loaded = true;
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (chatInputElement) {
|
if (chatInputElement) {
|
||||||
chatInputElement.focus();
|
chatInputElement.focus();
|
||||||
|
|
@ -543,9 +623,10 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FilesOverlay show={draggedOver} />
|
{#if loaded}
|
||||||
|
<FilesOverlay show={draggedOver} />
|
||||||
|
|
||||||
{#if acceptFiles}
|
{#if acceptFiles}
|
||||||
<input
|
<input
|
||||||
bind:this={filesInputElement}
|
bind:this={filesInputElement}
|
||||||
bind:files={inputFiles}
|
bind:files={inputFiles}
|
||||||
|
|
@ -562,24 +643,23 @@
|
||||||
filesInputElement.value = '';
|
filesInputElement.value = '';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<InputVariablesModal
|
<InputVariablesModal
|
||||||
bind:show={showInputVariablesModal}
|
bind:show={showInputVariablesModal}
|
||||||
variables={inputVariables}
|
variables={inputVariables}
|
||||||
onSave={(variableValues) => {
|
onSave={inputVariablesModalCallback}
|
||||||
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
/>
|
||||||
replaceVariables(inputVariableValues);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="bg-transparent">
|
<div class="bg-transparent">
|
||||||
<div
|
<div
|
||||||
class="{($settings?.widescreenMode ?? null)
|
class="{($settings?.widescreenMode ?? null)
|
||||||
? 'max-w-full'
|
? 'max-w-full'
|
||||||
: 'max-w-6xl'} mx-auto inset-x-0 relative"
|
: 'max-w-6xl'} mx-auto inset-x-0 relative"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
|
<div
|
||||||
|
class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center"
|
||||||
|
>
|
||||||
<div class="flex flex-col px-3 w-full">
|
<div class="flex flex-col px-3 w-full">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
{#if scrollEnd === false}
|
{#if scrollEnd === false}
|
||||||
|
|
@ -621,13 +701,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Commands
|
|
||||||
bind:this={commandsElement}
|
|
||||||
show={showCommands}
|
|
||||||
{command}
|
|
||||||
insertTextHandler={insertTextAtCursor}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -667,7 +740,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex flex-col relative w-full rounded-3xl px-1 bg-gray-600/5 dark:bg-gray-400/5 dark:text-gray-100"
|
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
|
|
@ -678,8 +751,8 @@
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Image
|
<Image
|
||||||
src={file.url}
|
src={file.url}
|
||||||
alt="input"
|
alt=""
|
||||||
imageClassName=" h-16 w-16 rounded-xl object-cover"
|
imageClassName=" size-10 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class=" absolute -top-1 -right-1">
|
<div class=" absolute -top-1 -right-1">
|
||||||
|
|
@ -710,6 +783,7 @@
|
||||||
name={file.name}
|
name={file.name}
|
||||||
type={file.type}
|
type={file.type}
|
||||||
size={file?.size}
|
size={file?.size}
|
||||||
|
small={true}
|
||||||
loading={file.status === 'uploading'}
|
loading={file.status === 'uploading'}
|
||||||
dismissible={true}
|
dismissible={true}
|
||||||
edit={true}
|
edit={true}
|
||||||
|
|
@ -728,12 +802,15 @@
|
||||||
|
|
||||||
<div class="px-2.5">
|
<div class="px-2.5">
|
||||||
<div
|
<div
|
||||||
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
|
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-2.5 pb-[5px] px-1 resize-none h-fit max-h-80 overflow-auto"
|
||||||
>
|
>
|
||||||
|
{#key $settings?.richTextInput}
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
|
id="chat-input"
|
||||||
bind:this={chatInputElement}
|
bind:this={chatInputElement}
|
||||||
json={true}
|
json={true}
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
|
richText={$settings?.richTextInput ?? true}
|
||||||
{showFormattingToolbar}
|
{showFormattingToolbar}
|
||||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||||
(!$mobile ||
|
(!$mobile ||
|
||||||
|
|
@ -744,6 +821,7 @@
|
||||||
))}
|
))}
|
||||||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||||
floatingMenuPlacement={'top-start'}
|
floatingMenuPlacement={'top-start'}
|
||||||
|
{suggestions}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const { md } = e;
|
const { md } = e;
|
||||||
content = md;
|
content = md;
|
||||||
|
|
@ -787,10 +865,11 @@
|
||||||
console.info(e);
|
console.info(e);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
|
<div class=" flex justify-between mb-2.5 mx-0.5">
|
||||||
<div class="ml-1 self-end flex space-x-1 flex-1">
|
<div class="ml-1 self-end flex space-x-1 flex-1">
|
||||||
<slot name="menu">
|
<slot name="menu">
|
||||||
{#if acceptFiles}
|
{#if acceptFiles}
|
||||||
|
|
@ -834,9 +913,12 @@
|
||||||
.getUserMedia({ audio: true })
|
.getUserMedia({ audio: true })
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
$i18n.t(
|
||||||
|
`Permission denied when accessing microphone: {{error}}`,
|
||||||
|
{
|
||||||
error: err
|
error: err
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
@ -928,4 +1010,5 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||||
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
|
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
|
||||||
|
import Camera from '$lib/components/icons/Camera.svelte';
|
||||||
|
import Clip from '$lib/components/icons/Clip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -44,34 +46,34 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
||||||
sideOffset={15}
|
sideOffset={4}
|
||||||
alignOffset={-8}
|
alignOffset={-6}
|
||||||
side="top"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
>
|
>
|
||||||
{#if !$mobile}
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
on:click={() => {
|
|
||||||
screenCaptureHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CameraSolid />
|
|
||||||
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<DropdownMenu.Item
|
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadFilesHandler();
|
uploadFilesHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DocumentArrowUpSolid />
|
<Clip />
|
||||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
{#if !$mobile}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
|
on:click={() => {
|
||||||
|
screenCaptureHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Camera />
|
||||||
|
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
||||||
81
src/lib/components/channel/MessageInput/MentionList.svelte
Normal file
81
src/lib/components/channel/MessageInput/MentionList.svelte
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { models } from '$lib/stores';
|
||||||
|
export let query = '';
|
||||||
|
|
||||||
|
export let command: (payload: { id: string; label: string }) => void;
|
||||||
|
export let selectedIndex = 0;
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
$: filteredItems = $models.filter((u) => u.name.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
|
const select = (index: number) => {
|
||||||
|
const item = filteredItems[index];
|
||||||
|
if (item) command(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
selectedIndex = (selectedIndex + filteredItems.length - 1) % filteredItems.length;
|
||||||
|
const item = document.querySelector(`[data-selected="true"]`);
|
||||||
|
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
selectedIndex = (selectedIndex + 1) % filteredItems.length;
|
||||||
|
const item = document.querySelector(`[data-selected="true"]`);
|
||||||
|
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||||
|
select(selectedIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
// tell tiptap we handled it (it will close)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This method will be called from the suggestion renderer
|
||||||
|
// @ts-ignore
|
||||||
|
export function _onKeyDown(event: KeyboardEvent) {
|
||||||
|
return onKeyDown(event);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if filteredItems.length}
|
||||||
|
<div
|
||||||
|
class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-60 p-1"
|
||||||
|
id="suggestions-container"
|
||||||
|
>
|
||||||
|
<div class="overflow-y-auto scrollbar-thin max-h-60">
|
||||||
|
<div class="px-2 text-xs text-gray-500 py-1">
|
||||||
|
{$i18n.t('Models')}
|
||||||
|
</div>
|
||||||
|
{#each filteredItems as item, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => select(i)}
|
||||||
|
on:mousemove={() => {
|
||||||
|
selectedIndex = i;
|
||||||
|
}}
|
||||||
|
class="px-2.5 py-1.5 rounded-xl w-full text-left {i === selectedIndex
|
||||||
|
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
|
||||||
|
: ''}"
|
||||||
|
data-selected={i === selectedIndex}
|
||||||
|
>
|
||||||
|
<div class="truncate">
|
||||||
|
@{item.name}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -138,9 +138,7 @@
|
||||||
id="message-{message.id}"
|
id="message-{message.id}"
|
||||||
dir={$settings.chatDirection}
|
dir={$settings.chatDirection}
|
||||||
>
|
>
|
||||||
<div
|
<div class={`shrink-0 mr-3 w-9`}>
|
||||||
class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
|
||||||
>
|
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
<ProfilePreview user={message.user}>
|
<ProfilePreview user={message.user}>
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
|
|
@ -198,7 +196,7 @@
|
||||||
name={file.name}
|
name={file.name}
|
||||||
type={file.type}
|
type={file.type}
|
||||||
size={file?.size}
|
size={file?.size}
|
||||||
colorClassName="bg-white dark:bg-gray-850 "
|
small={true}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@
|
||||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||||
import Commands from './MessageInput/Commands.svelte';
|
|
||||||
import ToolServersModal from './ToolServersModal.svelte';
|
import ToolServersModal from './ToolServersModal.svelte';
|
||||||
|
|
||||||
import RichTextInput from '../common/RichTextInput.svelte';
|
import RichTextInput from '../common/RichTextInput.svelte';
|
||||||
|
|
@ -77,7 +76,6 @@
|
||||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||||
|
|
||||||
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
||||||
import MentionList from '../common/RichTextInput/MentionList.svelte';
|
|
||||||
import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
|
import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -298,16 +296,6 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCommand = () => {
|
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');
|
const chatInput = document.getElementById('chat-input');
|
||||||
let word = '';
|
let word = '';
|
||||||
|
|
||||||
|
|
@ -319,14 +307,6 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceCommandWithText = (text) => {
|
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');
|
const chatInput = document.getElementById('chat-input');
|
||||||
if (!chatInput) return;
|
if (!chatInput) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
<script>
|
|
||||||
import { knowledge, prompts } from '$lib/stores';
|
|
||||||
|
|
||||||
import { removeLastWordFromString } from '$lib/utils';
|
|
||||||
import { getPrompts } from '$lib/apis/prompts';
|
|
||||||
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
|
||||||
|
|
||||||
import Prompts from './Commands/Prompts.svelte';
|
|
||||||
import Knowledge from './Commands/Knowledge.svelte';
|
|
||||||
import Models from './Commands/Models.svelte';
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
||||||
|
|
||||||
export let show = false;
|
|
||||||
|
|
||||||
export let files = [];
|
|
||||||
export let command = '';
|
|
||||||
|
|
||||||
export let onSelect = (e) => {};
|
|
||||||
export let onUpload = (e) => {};
|
|
||||||
|
|
||||||
export let insertTextHandler = (text) => {};
|
|
||||||
|
|
||||||
let loading = false;
|
|
||||||
let commandElement = null;
|
|
||||||
|
|
||||||
export const selectUp = () => {
|
|
||||||
commandElement?.selectUp();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectDown = () => {
|
|
||||||
commandElement?.selectDown();
|
|
||||||
};
|
|
||||||
|
|
||||||
$: if (show) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
loading = true;
|
|
||||||
await Promise.all([
|
|
||||||
(async () => {
|
|
||||||
prompts.set(await getPrompts(localStorage.token));
|
|
||||||
})(),
|
|
||||||
(async () => {
|
|
||||||
knowledge.set(await getKnowledgeBases(localStorage.token));
|
|
||||||
})()
|
|
||||||
]);
|
|
||||||
loading = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if show}
|
|
||||||
{#if !loading}
|
|
||||||
{#if command?.charAt(0) === '/'}
|
|
||||||
<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('# '))}
|
|
||||||
<Knowledge
|
|
||||||
bind:this={commandElement}
|
|
||||||
command={command.includes('\\#') ? command.slice(2) : command}
|
|
||||||
onSelect={(e) => {
|
|
||||||
const { type, data } = e;
|
|
||||||
|
|
||||||
if (type === 'knowledge') {
|
|
||||||
insertTextHandler('');
|
|
||||||
|
|
||||||
onUpload({
|
|
||||||
type: 'file',
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
} else if (type === 'youtube') {
|
|
||||||
insertTextHandler('');
|
|
||||||
|
|
||||||
onUpload({
|
|
||||||
type: 'youtube',
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
} else if (type === 'web') {
|
|
||||||
insertTextHandler('');
|
|
||||||
|
|
||||||
onUpload({
|
|
||||||
type: 'web',
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if command?.charAt(0) === '@'}
|
|
||||||
<Models
|
|
||||||
bind:this={commandElement}
|
|
||||||
{command}
|
|
||||||
onSelect={(e) => {
|
|
||||||
const { type, data } = e;
|
|
||||||
|
|
||||||
if (type === 'model') {
|
|
||||||
insertTextHandler('');
|
|
||||||
|
|
||||||
onSelect({
|
|
||||||
type: 'model',
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
id="commands-container"
|
|
||||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
|
||||||
>
|
|
||||||
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
|
|
||||||
<div
|
|
||||||
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
|
||||||
>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
||||||
import { all, createLowlight } from 'lowlight';
|
import { all, createLowlight } from 'lowlight';
|
||||||
|
|
||||||
import MentionList from './RichTextInput/MentionList.svelte';
|
import MentionList from '../channel/MessageInput/MentionList.svelte';
|
||||||
import { getSuggestionRenderer } from './RichTextInput/suggestions.js';
|
import { getSuggestionRenderer } from './RichTextInput/suggestions.js';
|
||||||
|
|
||||||
export let oncompositionstart = (e) => {};
|
export let oncompositionstart = (e) => {};
|
||||||
|
|
@ -1369,7 +1369,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showFormattingToolbar}
|
{#if richText && showFormattingToolbar}
|
||||||
<div bind:this={bubbleMenuElement} id="bubble-menu" class="p-0">
|
<div bind:this={bubbleMenuElement} id="bubble-menu" class="p-0">
|
||||||
<FormattingButtons {editor} />
|
<FormattingButtons {editor} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
export let query = '';
|
|
||||||
|
|
||||||
export let command: (payload: { id: string; label: string }) => void;
|
|
||||||
export let selectedIndex = 0;
|
|
||||||
|
|
||||||
let ITEMS = [
|
|
||||||
{ id: '1', label: 'alice' },
|
|
||||||
{ id: '2', label: 'alex' },
|
|
||||||
{ id: '3', label: 'bob' },
|
|
||||||
{ id: '4', label: 'charlie' },
|
|
||||||
{ id: '5', label: 'diana' },
|
|
||||||
{ id: '6', label: 'eve' },
|
|
||||||
{ id: '7', label: 'frank' },
|
|
||||||
{ id: '8', label: 'grace' },
|
|
||||||
{ id: '9', label: 'heidi' },
|
|
||||||
{ id: '10', label: 'ivan' },
|
|
||||||
{ id: '11', label: 'judy' },
|
|
||||||
{ id: '12', label: 'mallory' },
|
|
||||||
{ id: '13', label: 'oscar' },
|
|
||||||
{ id: '14', label: 'peggy' },
|
|
||||||
{ id: '15', label: 'trent' },
|
|
||||||
{ id: '16', label: 'victor' },
|
|
||||||
{ id: '17', label: 'walter' }
|
|
||||||
];
|
|
||||||
|
|
||||||
let items = ITEMS;
|
|
||||||
|
|
||||||
$: items = ITEMS.filter((u) => u.label.toLowerCase().includes(query.toLowerCase())).slice(0, 5);
|
|
||||||
|
|
||||||
const select = (index: number) => {
|
|
||||||
const item = items[index];
|
|
||||||
if (item) command(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
|
|
||||||
|
|
||||||
if (event.key === 'ArrowUp') {
|
|
||||||
selectedIndex = (selectedIndex + items.length - 1) % items.length;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (event.key === 'ArrowDown') {
|
|
||||||
selectedIndex = (selectedIndex + 1) % items.length;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
||||||
select(selectedIndex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
// tell tiptap we handled it (it will close)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This method will be called from the suggestion renderer
|
|
||||||
// @ts-ignore
|
|
||||||
export function _onKeyDown(event: KeyboardEvent) {
|
|
||||||
return onKeyDown(event);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 overflow-y-auto scrollbar-thin max-h-60 w-52"
|
|
||||||
id="suggestions-container"
|
|
||||||
>
|
|
||||||
{#if items.length === 0}
|
|
||||||
<div class=" p-4 text-gray-400">No results</div>
|
|
||||||
{:else}
|
|
||||||
{#each items as item, i}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click={() => select(i)}
|
|
||||||
class=" text-left w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition px-3 py-1 {i ===
|
|
||||||
selectedIndex
|
|
||||||
? 'bg-gray-50 dark:bg-gray-800 font-medium'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
@{item.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
@ -327,7 +327,7 @@ Based on the user's instruction, update and enhance the existing notes or select
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center mb-1.5 pt-1.5">
|
<div class="flex items-center mb-1.5 pt-1.5 pl-1.5 pr-2.5">
|
||||||
<div class=" -translate-x-1.5 flex items-center">
|
<div class=" -translate-x-1.5 flex items-center">
|
||||||
<button
|
<button
|
||||||
class="p-0.5 bg-transparent transition rounded-lg"
|
class="p-0.5 bg-transparent transition rounded-lg"
|
||||||
|
|
@ -358,7 +358,7 @@ Based on the user's instruction, update and enhance the existing notes or select
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center mb-2 flex-1 @container">
|
<div class="flex flex-col items-center flex-1 @container">
|
||||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||||
<div class="mx-auto w-full md:px-0 h-full relative">
|
<div class="mx-auto w-full md:px-0 h-full relative">
|
||||||
<div class=" flex flex-col h-full">
|
<div class=" flex flex-col h-full">
|
||||||
|
|
@ -375,7 +375,7 @@ Based on the user's instruction, update and enhance the existing notes or select
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" pb-2">
|
<div class=" pb-[1rem] pl-1.5 pr-2.5">
|
||||||
{#if selectedContent}
|
{#if selectedContent}
|
||||||
<div class="text-xs rounded-xl px-3.5 py-3 w-full markdown-prose-xs">
|
<div class="text-xs rounded-xl px-3.5 py-3 w-full markdown-prose-xs">
|
||||||
<blockquote>
|
<blockquote>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center mb-1.5 pt-1.5">
|
<div class="flex items-center mb-1.5 pt-1.5 pl-1.5 pr-2.5">
|
||||||
<div class=" -translate-x-1.5 flex items-center">
|
<div class=" -translate-x-1.5 flex items-center">
|
||||||
<button
|
<button
|
||||||
class="p-0.5 bg-transparent transition rounded-lg"
|
class="p-0.5 bg-transparent transition rounded-lg"
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-1">
|
<div class="mt-1 pl-1.5 pr-2.5">
|
||||||
<div class="pb-10">
|
<div class="pb-10">
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
<div class=" text-xs font-medium pb-1">{$i18n.t('Files')}</div>
|
<div class=" text-xs font-medium pb-1">{$i18n.t('Files')}</div>
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="flex max-h-full min-h-full">
|
<div class="flex max-h-full min-h-full">
|
||||||
<div
|
<div
|
||||||
class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
|
class="w-full pt-2 bg-white dark:shadow-lg dark:bg-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue