mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-07 02:58:21 +00:00
refac
This commit is contained in:
parent
be35c8f658
commit
8a4547104c
7 changed files with 314 additions and 121 deletions
|
|
@ -225,7 +225,17 @@
|
|||
}
|
||||
};
|
||||
|
||||
const submitHandler = async ({ content, data }) => {
|
||||
const submitHandler = async ({
|
||||
content,
|
||||
data,
|
||||
channel_id,
|
||||
reply_to_message
|
||||
}: {
|
||||
content: string;
|
||||
data: any;
|
||||
channel_id: string;
|
||||
reply_to_message: any;
|
||||
}) => {
|
||||
if (!content && (data?.files ?? []).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -236,7 +246,7 @@
|
|||
temp_id: tempId,
|
||||
content: content,
|
||||
data: data,
|
||||
reply_to_id: replyToMessage?.id ?? null
|
||||
reply_to_id: reply_to_message?.id ?? null
|
||||
};
|
||||
|
||||
const ts = Date.now() * 1000000; // nanoseconds
|
||||
|
|
@ -246,23 +256,21 @@
|
|||
id: tempId,
|
||||
user_id: $user?.id,
|
||||
user: $user,
|
||||
reply_to_message: replyToMessage ?? null,
|
||||
reply_to_message: reply_to_message ?? null,
|
||||
created_at: ts,
|
||||
updated_at: ts
|
||||
},
|
||||
...messages
|
||||
];
|
||||
|
||||
const res = await sendMessage(localStorage.token, id, message).catch((error) => {
|
||||
const res = await sendMessage(localStorage.token, channel_id, message).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
if (res && id === channel_id && messagesContainerElement) {
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
}
|
||||
|
||||
replyToMessage = null;
|
||||
};
|
||||
|
||||
const onChange = async () => {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { tick, getContext, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
import { config, mobile, settings, socket, user } from '$lib/stores';
|
||||
import { channelRequestQueues, config, mobile, settings, socket, user } from '$lib/stores';
|
||||
import { processingQueueChannels, processNextInQueue } from '$lib/utils/channelQueue';
|
||||
import QueuedMessageItem from '../chat/MessageInput/QueuedMessageItem.svelte';
|
||||
import {
|
||||
convertHeicToJpeg,
|
||||
isHeicImage,
|
||||
|
|
@ -43,10 +46,10 @@
|
|||
import XMark from '../icons/XMark.svelte';
|
||||
|
||||
export let placeholder = $i18n.t('Type here...');
|
||||
export let chatInputElement;
|
||||
export let chatInputElement: any;
|
||||
|
||||
export let id = null;
|
||||
export let channel = null;
|
||||
export let id: string | null = null;
|
||||
export let channel: any = null;
|
||||
|
||||
export let typingUsers = [];
|
||||
export let inputLoading = false;
|
||||
|
|
@ -65,7 +68,7 @@
|
|||
export let userSuggestions = false;
|
||||
export let channelSuggestions = false;
|
||||
|
||||
export let replyToMessage = null;
|
||||
export let replyToMessage: any = null;
|
||||
|
||||
export let typingUsersClassName = 'from-white dark:from-gray-900';
|
||||
|
||||
|
|
@ -74,7 +77,11 @@
|
|||
|
||||
let recording = false;
|
||||
let content = '';
|
||||
let files = [];
|
||||
let files: any[] = [];
|
||||
let isDestroyed = false;
|
||||
$: parentId = id === 'root' ? null : id;
|
||||
$: queueKey = JSON.stringify([$user?.id, channel?.id, parentId]);
|
||||
$: messageQueue = $channelRequestQueues[queueKey] ?? [];
|
||||
|
||||
let filesInputElement;
|
||||
let inputFiles;
|
||||
|
|
@ -378,69 +385,66 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (file['type'].startsWith('image/') || isHeicImage(file)) {
|
||||
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||
// Quick shortcut so we don’t do unnecessary work.
|
||||
const settingsCompression =
|
||||
(settings?.imageCompression && settings?.imageCompressionInChannels) ?? false;
|
||||
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||
|
||||
// If neither settings nor config wants compression, return original URL.
|
||||
if (!settingsCompression && !configWidth && !configHeight) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Default to null (no compression unless set)
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
// If user/settings want compression, pick their preferred size.
|
||||
if (settingsCompression) {
|
||||
width = settings?.imageCompressionSize?.width ?? null;
|
||||
height = settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
// Apply config limits as an upper bound if any
|
||||
if (configWidth && (width === null || width > configWidth)) {
|
||||
width = configWidth;
|
||||
}
|
||||
if (configHeight && (height === null || height > configHeight)) {
|
||||
height = configHeight;
|
||||
}
|
||||
|
||||
// Do the compression if required
|
||||
if (width || height) {
|
||||
return await compressImage(imageUrl, width, height);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
const imageFile = isHeicImage(file) ? await convertHeicToJpeg(file) : file;
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = async (event) => {
|
||||
let imageUrl = event.target.result;
|
||||
|
||||
// Compress the image if settings or config require it
|
||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||
|
||||
const blob = await (await fetch(imageUrl)).blob();
|
||||
const compressedFile = new File([blob], imageFile.name, { type: imageFile.type });
|
||||
|
||||
uploadFileHandler(compressedFile, false);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(imageFile);
|
||||
} else {
|
||||
uploadFileHandler(file);
|
||||
}
|
||||
const image = file.type.startsWith('image/') || isHeicImage(file);
|
||||
uploadFileHandler(file, !image, image);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFileHandler = async (file, process = true) => {
|
||||
const prepareImageFileHandler = async (file: File) => {
|
||||
const compressImageHandler = async (imageUrl: string, settings: any = {}, config: any = {}) => {
|
||||
// Quick shortcut so we don’t do unnecessary work.
|
||||
const settingsCompression =
|
||||
(settings?.imageCompression && settings?.imageCompressionInChannels) ?? false;
|
||||
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||
|
||||
// If neither settings nor config wants compression, return original URL.
|
||||
if (!settingsCompression && !configWidth && !configHeight) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Default to null (no compression unless set)
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
// If user/settings want compression, pick their preferred size.
|
||||
if (settingsCompression) {
|
||||
width = settings?.imageCompressionSize?.width ?? null;
|
||||
height = settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
// Apply config limits as an upper bound if any
|
||||
if (configWidth && (width === null || width > configWidth)) {
|
||||
width = configWidth;
|
||||
}
|
||||
if (configHeight && (height === null || height > configHeight)) {
|
||||
height = configHeight;
|
||||
}
|
||||
|
||||
// Do the compression if required
|
||||
if (width || height) {
|
||||
return await compressImage(imageUrl, width, height);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
const imageFile = isHeicImage(file) ? await convertHeicToJpeg(file) : file;
|
||||
const imageUrl = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(imageFile);
|
||||
});
|
||||
const compressed = await compressImageHandler(imageUrl, $settings, $config);
|
||||
const blob = await (await fetch(compressed as string)).blob();
|
||||
return new File([blob], imageFile.name, { type: imageFile.type });
|
||||
};
|
||||
|
||||
const uploadFileHandler = async (file: File, process = true, prepareImage = false) => {
|
||||
const targetChannelId = channel.id;
|
||||
const targetQueueKey = queueKey;
|
||||
const senderId = $user?.id;
|
||||
const tempItemId = uuidv4();
|
||||
const fileItem = {
|
||||
const fileItem: any = {
|
||||
type: 'file',
|
||||
file: '',
|
||||
id: null,
|
||||
|
|
@ -461,10 +465,12 @@
|
|||
files = [...files, fileItem];
|
||||
|
||||
try {
|
||||
if (prepareImage) file = await prepareImageFileHandler(file);
|
||||
if (get(user)?.id !== senderId) return;
|
||||
// During the file upload, file content is automatically extracted.
|
||||
// If the file is an audio file, provide the language for STT.
|
||||
let metadata = {
|
||||
channel_id: channel.id,
|
||||
channel_id: targetChannelId,
|
||||
// If the file is an audio file, provide the language for STT.
|
||||
...((file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
||||
$settings?.audio?.stt?.language
|
||||
|
|
@ -498,11 +504,23 @@
|
|||
|
||||
files = files;
|
||||
} else {
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
throw new Error($i18n.t('Failed to upload file.'));
|
||||
}
|
||||
} catch (e) {
|
||||
fileItem.status = 'error';
|
||||
fileItem.error = String(e);
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
} finally {
|
||||
channelRequestQueues.update((q) => {
|
||||
for (const item of q[targetQueueKey] ?? []) {
|
||||
item.files = item.files.map((file) =>
|
||||
file.itemId === fileItem.itemId ? fileItem : file
|
||||
);
|
||||
}
|
||||
return q;
|
||||
});
|
||||
void processNextInQueue(targetQueueKey);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -541,28 +559,72 @@
|
|||
draggedOver = false;
|
||||
};
|
||||
|
||||
const editQueuedMessage = (messageId: string) => {
|
||||
if (content || files.length || replyToMessage) return;
|
||||
const entry = messageQueue.find((item) => item.id === messageId);
|
||||
if (!entry || entry.sending) return;
|
||||
deleteQueuedMessage(messageId);
|
||||
content = entry.prompt;
|
||||
files = entry.files;
|
||||
replyToMessage = entry.reply_to_message;
|
||||
chatInputElement?.setText(content);
|
||||
chatInputElement?.focus();
|
||||
};
|
||||
|
||||
const deleteQueuedMessage = (messageId: string) => {
|
||||
channelRequestQueues.update((q) => ({
|
||||
...q,
|
||||
[queueKey]: (q[queueKey] ?? []).filter((item) => item.id !== messageId || item.sending)
|
||||
}));
|
||||
void processNextInQueue(queueKey);
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (content === '' && files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
content,
|
||||
data: {
|
||||
files: files
|
||||
}
|
||||
});
|
||||
if (disabled || !$user?.id || (content === '' && files.length === 0)) return;
|
||||
|
||||
const targetQueueKey = queueKey;
|
||||
const entry = {
|
||||
id: uuidv4(),
|
||||
prompt: content,
|
||||
files,
|
||||
user_id: $user.id,
|
||||
channel_id: channel.id,
|
||||
parent_id: parentId,
|
||||
reply_to_message: replyToMessage
|
||||
};
|
||||
const queued =
|
||||
processingQueueChannels.has(targetQueueKey) ||
|
||||
messageQueue.length > 0 ||
|
||||
files.some((file) => ['uploading', 'error'].includes(file.status));
|
||||
content = '';
|
||||
files = [];
|
||||
replyToMessage = null;
|
||||
chatInputElement?.setText('');
|
||||
|
||||
if (chatInputElement) {
|
||||
chatInputElement?.setText('');
|
||||
|
||||
await tick();
|
||||
|
||||
chatInputElement.focus();
|
||||
if (queued) {
|
||||
channelRequestQueues.update((q) => ({
|
||||
...q,
|
||||
[targetQueueKey]: [...(q[targetQueueKey] ?? []), entry]
|
||||
}));
|
||||
void processNextInQueue(targetQueueKey);
|
||||
} else {
|
||||
processingQueueChannels.add(targetQueueKey);
|
||||
try {
|
||||
await onSubmit({
|
||||
content: entry.prompt,
|
||||
data: { files: entry.files },
|
||||
channel_id: entry.channel_id,
|
||||
parent_id: entry.parent_id,
|
||||
reply_to_message: entry.reply_to_message
|
||||
});
|
||||
} finally {
|
||||
processingQueueChannels.delete(targetQueueKey);
|
||||
void processNextInQueue(targetQueueKey);
|
||||
}
|
||||
}
|
||||
|
||||
await tick();
|
||||
if (!isDestroyed && targetQueueKey === queueKey) chatInputElement?.focus();
|
||||
};
|
||||
|
||||
$: if (content) {
|
||||
|
|
@ -570,6 +632,9 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = channelRequestQueues.subscribe(() => {
|
||||
files = [...files];
|
||||
});
|
||||
suggestions = [
|
||||
{
|
||||
char: '@',
|
||||
|
|
@ -659,7 +724,6 @@
|
|||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
let isDestroyed = false;
|
||||
let dropzoneElement: HTMLElement | null = null;
|
||||
const initialize = async () => {
|
||||
await tick();
|
||||
|
|
@ -675,6 +739,7 @@
|
|||
initialize();
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
isDestroyed = true;
|
||||
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
|
|
@ -802,11 +867,31 @@
|
|||
/>
|
||||
{:else}
|
||||
<form
|
||||
class="w-full flex gap-1.5"
|
||||
class="w-full flex flex-col gap-1.5"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
{#if messageQueue.length > 0}
|
||||
<div
|
||||
class="mb-1 mx-2 py-0.5 px-1.5 rounded-2xl bg-white dark:bg-gray-900/60 border border-gray-100 dark:border-gray-800/50 overflow-x-hidden overflow-y-auto max-h-[25vh]"
|
||||
>
|
||||
{#each messageQueue as entry (entry.id)}
|
||||
<QueuedMessageItem
|
||||
id={entry.id}
|
||||
content={entry.prompt}
|
||||
files={entry.files}
|
||||
sending={entry.sending ?? false}
|
||||
error={entry.error ?? ''}
|
||||
onSendNow={(messageId) => processNextInQueue(queueKey, messageId)}
|
||||
onEdit={content || files.length || replyToMessage
|
||||
? undefined
|
||||
: editQueuedMessage}
|
||||
onDelete={deleteQueuedMessage}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
id="message-input-container"
|
||||
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850/30 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-0.5 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
if (type === 'message') {
|
||||
if ((data?.parent_id ?? null) === threadId) {
|
||||
if (messages) {
|
||||
messages = [data, ...messages];
|
||||
messages = [{ ...data, temp_id: null }, ...messages];
|
||||
|
||||
if (typingUsers.find((user) => user.id === event.user.id)) {
|
||||
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
|
||||
|
|
@ -141,22 +141,32 @@
|
|||
}
|
||||
};
|
||||
|
||||
const submitHandler = async ({ content, data }) => {
|
||||
const submitHandler = async ({
|
||||
content,
|
||||
data,
|
||||
channel_id,
|
||||
parent_id,
|
||||
reply_to_message
|
||||
}: {
|
||||
content: string;
|
||||
data: any;
|
||||
channel_id: string;
|
||||
parent_id: string | null;
|
||||
reply_to_message: any;
|
||||
}) => {
|
||||
if (!content && (data?.files ?? []).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await sendMessage(localStorage.token, channel.id, {
|
||||
parent_id: threadId,
|
||||
reply_to_id: replyToMessage?.id ?? null,
|
||||
const res = await sendMessage(localStorage.token, channel_id, {
|
||||
parent_id: parent_id ?? undefined,
|
||||
reply_to_id: reply_to_message?.id ?? null,
|
||||
content: content,
|
||||
data: data
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
replyToMessage = null;
|
||||
};
|
||||
|
||||
const onChange = async () => {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,25 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { isRasterImageContentType } from '$lib/utils';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
export let id: string;
|
||||
export let content: string;
|
||||
export let files: any[] = [];
|
||||
export let onSendNow: (id: string) => void;
|
||||
export let onEdit: (id: string) => void;
|
||||
export let onEdit: ((id: string) => void) | undefined;
|
||||
export let onDelete: (id: string) => void;
|
||||
export let sending = false;
|
||||
export let error = '';
|
||||
$: uploadFailed = files.some((file) => file.status === 'error');
|
||||
$: sendDisabled = sending || files.some((file) => ['uploading', 'error'].includes(file.status));
|
||||
$: sendLabel = sending
|
||||
? $i18n.t('Sending...')
|
||||
: uploadFailed
|
||||
? $i18n.t('Upload failed')
|
||||
: sendDisabled
|
||||
? $i18n.t('Waiting for upload')
|
||||
: $i18n.t('Send now');
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||
|
|
@ -71,37 +82,33 @@
|
|||
</p>
|
||||
{/if}
|
||||
|
||||
{#if files.some((file) => file.status === 'error')}
|
||||
{#if uploadFailed}
|
||||
<span class="shrink-0 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Upload failed')}
|
||||
</span>
|
||||
{:else if error}
|
||||
<span class="shrink-0 text-xs text-red-500" title={error}
|
||||
>{$i18n.t('Failed to send message')}</span
|
||||
>
|
||||
{:else if sending}
|
||||
<Spinner className="size-3 shrink-0" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<!-- Send immediately -->
|
||||
<Tooltip
|
||||
content={files.some((file) => ['uploading', 'error'].includes(file.status))
|
||||
? $i18n.t('Waiting for upload')
|
||||
: $i18n.t('Send now')}
|
||||
>
|
||||
<Tooltip content={sendLabel}>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 transition-colors {files.some((file) =>
|
||||
['uploading', 'error'].includes(file.status)
|
||||
)
|
||||
class="p-1 text-gray-400 transition-colors {sendDisabled
|
||||
? 'opacity-40 cursor-not-allowed'
|
||||
: 'hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300'}"
|
||||
disabled={files.some((file) => ['uploading', 'error'].includes(file.status))}
|
||||
disabled={sendDisabled}
|
||||
on:click={() => {
|
||||
if (!files.some((file) => ['uploading', 'error'].includes(file.status))) {
|
||||
onSendNow(id);
|
||||
}
|
||||
if (!sendDisabled) onSendNow(id);
|
||||
}}
|
||||
aria-label={files.some((file) => ['uploading', 'error'].includes(file.status))
|
||||
? $i18n.t('Waiting for upload')
|
||||
: $i18n.t('Send now')}
|
||||
aria-label={sendLabel}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -124,8 +131,9 @@
|
|||
<Tooltip content={$i18n.t('Edit')}>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
on:click={() => onEdit(id)}
|
||||
class="p-1 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
disabled={sending || !onEdit}
|
||||
on:click={() => onEdit?.(id)}
|
||||
aria-label={$i18n.t('Edit')}
|
||||
>
|
||||
<EditPencil className="size-3.5" />
|
||||
|
|
@ -136,7 +144,8 @@
|
|||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
class="p-1 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
disabled={sending}
|
||||
on:click={() => onDelete(id)}
|
||||
aria-label={$i18n.t('Delete')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -124,6 +124,19 @@ export const chatRequestQueues: Writable<
|
|||
Record<string, { id: string; prompt: string; files: any[] }[]>
|
||||
> = writable({});
|
||||
|
||||
export type ChannelQueuedMessage = {
|
||||
id: string;
|
||||
prompt: string;
|
||||
files: any[];
|
||||
user_id: string;
|
||||
channel_id: string;
|
||||
parent_id: string | null;
|
||||
reply_to_message: any;
|
||||
sending?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
export const channelRequestQueues = writable<Record<string, ChannelQueuedMessage[]>>({});
|
||||
|
||||
export const sidebarWidth = writable(245);
|
||||
|
||||
export type SettingsModalRequest = {
|
||||
|
|
|
|||
51
src/lib/utils/channelQueue.ts
Normal file
51
src/lib/utils/channelQueue.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { get } from 'svelte/store';
|
||||
import { sendMessage } from '$lib/apis/channels';
|
||||
import { channelRequestQueues, user } from '$lib/stores';
|
||||
|
||||
export const processingQueueChannels = new Set<string>();
|
||||
|
||||
export const processNextInQueue = async (key: string, messageId?: string) => {
|
||||
if (processingQueueChannels.has(key)) return;
|
||||
processingQueueChannels.add(key);
|
||||
try {
|
||||
while (get(channelRequestQueues)[key]?.length) {
|
||||
const queue = get(channelRequestQueues)[key] ?? [];
|
||||
const item = messageId ? queue.find((m) => m.id === messageId) : queue[0];
|
||||
if (
|
||||
!item ||
|
||||
item.user_id !== get(user)?.id ||
|
||||
(item.error && !messageId) ||
|
||||
item.files.some((file) => ['uploading', 'error'].includes(file.status))
|
||||
)
|
||||
return;
|
||||
|
||||
messageId = undefined;
|
||||
item.sending = true;
|
||||
item.error = undefined;
|
||||
channelRequestQueues.update((q) => q);
|
||||
try {
|
||||
const result = await sendMessage(localStorage.token, item.channel_id, {
|
||||
temp_id: item.id,
|
||||
parent_id: item.parent_id ?? undefined,
|
||||
reply_to_id: item.reply_to_message?.id ?? null,
|
||||
content: item.prompt,
|
||||
data: { files: item.files }
|
||||
});
|
||||
if (!result) throw new Error('Failed to send message');
|
||||
channelRequestQueues.update((q) => {
|
||||
if (q[key]) q[key] = q[key].filter((m) => m.id !== item.id);
|
||||
return q;
|
||||
});
|
||||
} catch (error) {
|
||||
// Logout can clear this entry while the request is pending.
|
||||
if (!get(channelRequestQueues)[key]?.includes(item)) continue;
|
||||
item.sending = false;
|
||||
item.error = String(error);
|
||||
channelRequestQueues.update((q) => q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
processingQueueChannels.delete(key);
|
||||
}
|
||||
};
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
playingNotificationSound,
|
||||
channels,
|
||||
channelId,
|
||||
channelRequestQueues,
|
||||
terminalServers,
|
||||
connectedUserTerminals,
|
||||
showControls,
|
||||
|
|
@ -1245,7 +1246,12 @@
|
|||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
user.subscribe(async (value) => {
|
||||
let queueUserId = $user?.id;
|
||||
const unsubscribeQueueUser = user.subscribe(async (value) => {
|
||||
if (queueUserId !== value?.id) {
|
||||
channelRequestQueues.set({});
|
||||
queueUserId = value?.id;
|
||||
}
|
||||
if (value) {
|
||||
$socket?.off('events', chatEventHandler);
|
||||
$socket?.off('events:channel', channelEventHandler);
|
||||
|
|
@ -1264,6 +1270,15 @@
|
|||
}
|
||||
});
|
||||
|
||||
/** @param {BeforeUnloadEvent} event */
|
||||
const beforeUnloadHandler = (event) => {
|
||||
if (Object.values($channelRequestQueues).some((queue) => queue.length)) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler);
|
||||
|
||||
let backendConfig = null;
|
||||
try {
|
||||
backendConfig = await getBackendConfig();
|
||||
|
|
@ -1399,6 +1414,8 @@
|
|||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('pagehide', handlePageHidden);
|
||||
window.removeEventListener('pageshow', handlePageVisible);
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||
unsubscribeQueueUser();
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue