2024-12-23 02:40:01 +00:00
|
|
|
|
<script lang="ts">
|
|
|
|
|
|
import { toast } from 'svelte-sonner';
|
2024-12-23 21:43:58 +00:00
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
2025-07-02 20:04:13 +00:00
|
|
|
|
import heic2any from 'heic2any';
|
2024-12-23 21:43:58 +00:00
|
|
|
|
|
|
|
|
|
|
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
|
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
|
|
|
2025-07-08 21:49:43 +00:00
|
|
|
|
import { config, mobile, settings, socket, user } from '$lib/stores';
|
|
|
|
|
|
import {
|
|
|
|
|
|
blobToFile,
|
|
|
|
|
|
compressImage,
|
|
|
|
|
|
extractInputVariables,
|
|
|
|
|
|
getCurrentDateTime,
|
|
|
|
|
|
getFormattedDate,
|
|
|
|
|
|
getFormattedTime,
|
|
|
|
|
|
getUserPosition,
|
|
|
|
|
|
getUserTimezone,
|
|
|
|
|
|
getWeekday
|
|
|
|
|
|
} from '$lib/utils';
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
|
|
|
|
|
import Tooltip from '../common/Tooltip.svelte';
|
|
|
|
|
|
import RichTextInput from '../common/RichTextInput.svelte';
|
|
|
|
|
|
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
2024-12-23 21:43:58 +00:00
|
|
|
|
import InputMenu from './MessageInput/InputMenu.svelte';
|
|
|
|
|
|
import { uploadFile } from '$lib/apis/files';
|
|
|
|
|
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
|
|
|
|
|
import FileItem from '../common/FileItem.svelte';
|
|
|
|
|
|
import Image from '../common/Image.svelte';
|
|
|
|
|
|
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
2025-07-08 21:49:43 +00:00
|
|
|
|
import Commands from '../chat/MessageInput/Commands.svelte';
|
|
|
|
|
|
import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
|
|
|
|
|
export let placeholder = $i18n.t('Send a Message');
|
|
|
|
|
|
export let transparentBackground = false;
|
|
|
|
|
|
|
2024-12-31 10:16:07 +00:00
|
|
|
|
export let id = null;
|
|
|
|
|
|
|
2024-12-23 21:43:58 +00:00
|
|
|
|
let draggedOver = false;
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
2024-12-23 21:43:58 +00:00
|
|
|
|
let recording = false;
|
2024-12-23 02:40:01 +00:00
|
|
|
|
let content = '';
|
2024-12-23 21:43:58 +00:00
|
|
|
|
let files = [];
|
|
|
|
|
|
|
2025-07-07 15:26:12 +00:00
|
|
|
|
export let chatInputElement;
|
2025-07-08 21:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
let commandsElement;
|
2024-12-23 21:43:58 +00:00
|
|
|
|
let filesInputElement;
|
|
|
|
|
|
let inputFiles;
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
2024-12-27 05:51:09 +00:00
|
|
|
|
export let typingUsers = [];
|
2025-07-07 15:26:12 +00:00
|
|
|
|
export let inputLoading = false;
|
|
|
|
|
|
|
|
|
|
|
|
export let onSubmit: Function = (e) => {};
|
|
|
|
|
|
export let onChange: Function = (e) => {};
|
|
|
|
|
|
export let onStop: Function = (e) => {};
|
2024-12-27 05:51:09 +00:00
|
|
|
|
|
2024-12-23 04:56:51 +00:00
|
|
|
|
export let scrollEnd = true;
|
2024-12-31 08:51:43 +00:00
|
|
|
|
export let scrollToBottom: Function = () => {};
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
2025-07-07 15:26:12 +00:00
|
|
|
|
export let acceptFiles = true;
|
2025-07-08 23:15:20 +00:00
|
|
|
|
export let showFormattingButtons = true;
|
2025-07-07 15:26:12 +00:00
|
|
|
|
|
2025-07-08 21:49:43 +00:00
|
|
|
|
let showInputVariablesModal = false;
|
|
|
|
|
|
let inputVariables: Record<string, any> = {};
|
|
|
|
|
|
let inputVariableValues = {};
|
|
|
|
|
|
|
|
|
|
|
|
const inputVariableHandler = async (text: string) => {
|
|
|
|
|
|
inputVariables = extractInputVariables(text);
|
|
|
|
|
|
if (Object.keys(inputVariables).length > 0) {
|
|
|
|
|
|
showInputVariablesModal = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inputVariableHandler(text);
|
|
|
|
|
|
return text;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const replaceVariables = (variables: Record<string, any>) => {
|
|
|
|
|
|
if (!chatInputElement) return;
|
|
|
|
|
|
console.log('Replacing variables:', variables);
|
|
|
|
|
|
|
|
|
|
|
|
chatInputElement.replaceVariables(variables);
|
|
|
|
|
|
chatInputElement.focus();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const setText = async (text?: string) => {
|
|
|
|
|
|
if (!chatInputElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
text = await textVariableHandler(text || '');
|
|
|
|
|
|
|
|
|
|
|
|
chatInputElement?.setText(text);
|
|
|
|
|
|
chatInputElement?.focus();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getCommand = () => {
|
|
|
|
|
|
if (!chatInputElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
let word = '';
|
|
|
|
|
|
word = chatInputElement?.getWordAtDocPos();
|
|
|
|
|
|
|
|
|
|
|
|
return word;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const replaceCommandWithText = (text) => {
|
|
|
|
|
|
if (!chatInputElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
chatInputElement?.replaceCommandWithText(text);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const insertTextAtCursor = async (text: string) => {
|
|
|
|
|
|
text = await textVariableHandler(text);
|
|
|
|
|
|
|
|
|
|
|
|
if (command) {
|
|
|
|
|
|
replaceCommandWithText(text);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
const chatInputContainer = document.getElementById('chat-input-container');
|
|
|
|
|
|
if (chatInputContainer) {
|
|
|
|
|
|
chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
if (chatInputElement) {
|
|
|
|
|
|
chatInputElement.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let command = '';
|
|
|
|
|
|
|
|
|
|
|
|
export let showCommands = false;
|
|
|
|
|
|
$: showCommands = ['/'].includes(command?.charAt(0));
|
|
|
|
|
|
|
2024-12-23 21:43:58 +00:00
|
|
|
|
const screenCaptureHandler = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Request screen media
|
|
|
|
|
|
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
|
|
|
|
|
video: { cursor: 'never' },
|
|
|
|
|
|
audio: false
|
|
|
|
|
|
});
|
|
|
|
|
|
// Once the user selects a screen, temporarily create a video element
|
|
|
|
|
|
const video = document.createElement('video');
|
|
|
|
|
|
video.srcObject = mediaStream;
|
|
|
|
|
|
// Ensure the video loads without affecting user experience or tab switching
|
|
|
|
|
|
await video.play();
|
|
|
|
|
|
// Set up the canvas to match the video dimensions
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
canvas.width = video.videoWidth;
|
|
|
|
|
|
canvas.height = video.videoHeight;
|
|
|
|
|
|
// Grab a single frame from the video stream using the canvas
|
|
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
|
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
// Stop all video tracks (stop screen sharing) after capturing the image
|
|
|
|
|
|
mediaStream.getTracks().forEach((track) => track.stop());
|
|
|
|
|
|
|
|
|
|
|
|
// bring back focus to this current tab, so that the user can see the screen capture
|
|
|
|
|
|
window.focus();
|
|
|
|
|
|
|
|
|
|
|
|
// Convert the canvas to a Base64 image URL
|
|
|
|
|
|
const imageUrl = canvas.toDataURL('image/png');
|
|
|
|
|
|
// Add the captured image to the files array to render it
|
|
|
|
|
|
files = [...files, { type: 'image', url: imageUrl }];
|
|
|
|
|
|
// Clean memory: Clear video srcObject
|
|
|
|
|
|
video.srcObject = null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Handle any errors (e.g., user cancels screen sharing)
|
|
|
|
|
|
console.error('Error capturing screen:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const inputFilesHandler = async (inputFiles) => {
|
2025-07-02 20:04:13 +00:00
|
|
|
|
inputFiles.forEach(async (file) => {
|
2025-05-15 19:53:07 +00:00
|
|
|
|
console.info('Processing file:', {
|
2024-12-23 21:43:58 +00:00
|
|
|
|
name: file.name,
|
|
|
|
|
|
type: file.type,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
extension: file.name.split('.').at(-1)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
($config?.file?.max_size ?? null) !== null &&
|
|
|
|
|
|
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
|
|
|
|
|
|
) {
|
2025-05-15 19:53:07 +00:00
|
|
|
|
console.error('File exceeds max size limit:', {
|
2024-12-23 21:43:58 +00:00
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
|
|
|
|
|
|
});
|
|
|
|
|
|
toast.error(
|
|
|
|
|
|
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
|
|
|
|
|
|
maxSize: $config?.file?.max_size
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
if (file['type'].startsWith('image/')) {
|
|
|
|
|
|
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
|
|
|
|
|
// Quick shortcut so we don’t do unnecessary work.
|
|
|
|
|
|
const settingsCompression = settings?.imageCompression ?? false;
|
|
|
|
|
|
const configWidth = config?.file?.image_compression?.width ?? null;
|
|
|
|
|
|
const configHeight = config?.file?.image_compression?.height ?? null;
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
// If neither settings nor config wants compression, return original URL.
|
|
|
|
|
|
if (!settingsCompression && !configWidth && !configHeight) {
|
|
|
|
|
|
return imageUrl;
|
|
|
|
|
|
}
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
// Default to null (no compression unless set)
|
|
|
|
|
|
let width = null;
|
|
|
|
|
|
let height = null;
|
2025-06-16 12:52:57 +00:00
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
// If user/settings want compression, pick their preferred size.
|
|
|
|
|
|
if (settingsCompression) {
|
|
|
|
|
|
width = settings?.imageCompressionSize?.width ?? null;
|
|
|
|
|
|
height = settings?.imageCompressionSize?.height ?? null;
|
|
|
|
|
|
}
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
// 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;
|
2024-12-25 06:28:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
// Do the compression if required
|
|
|
|
|
|
if (width || height) {
|
|
|
|
|
|
return await compressImage(imageUrl, width, height);
|
|
|
|
|
|
}
|
|
|
|
|
|
return imageUrl;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2024-12-23 21:43:58 +00:00
|
|
|
|
files = [
|
|
|
|
|
|
...files,
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'image',
|
2024-12-25 06:28:14 +00:00
|
|
|
|
url: `${imageUrl}`
|
2024-12-23 21:43:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
};
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2025-07-02 20:04:13 +00:00
|
|
|
|
reader.readAsDataURL(
|
|
|
|
|
|
file['type'] === 'image/heic'
|
|
|
|
|
|
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
|
|
|
|
|
: file
|
|
|
|
|
|
);
|
2024-12-23 21:43:58 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
uploadFileHandler(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const uploadFileHandler = async (file) => {
|
|
|
|
|
|
const tempItemId = uuidv4();
|
|
|
|
|
|
const fileItem = {
|
|
|
|
|
|
type: 'file',
|
|
|
|
|
|
file: '',
|
|
|
|
|
|
id: null,
|
|
|
|
|
|
url: '',
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
collection_name: '',
|
|
|
|
|
|
status: 'uploading',
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
error: '',
|
|
|
|
|
|
itemId: tempItemId
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (fileItem.size == 0) {
|
|
|
|
|
|
toast.error($i18n.t('You cannot upload an empty file.'));
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
files = [...files, fileItem];
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// During the file upload, file content is automatically extracted.
|
2025-05-23 20:36:30 +00:00
|
|
|
|
|
|
|
|
|
|
// If the file is an audio file, provide the language for STT.
|
|
|
|
|
|
let metadata = null;
|
|
|
|
|
|
if (
|
|
|
|
|
|
(file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
|
|
|
|
|
$settings?.audio?.stt?.language
|
|
|
|
|
|
) {
|
|
|
|
|
|
metadata = {
|
|
|
|
|
|
language: $settings?.audio?.stt?.language
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uploadedFile = await uploadFile(localStorage.token, file, metadata);
|
2024-12-23 21:43:58 +00:00
|
|
|
|
|
|
|
|
|
|
if (uploadedFile) {
|
2025-05-15 19:53:07 +00:00
|
|
|
|
console.info('File upload completed:', {
|
2024-12-23 21:43:58 +00:00
|
|
|
|
id: uploadedFile.id,
|
|
|
|
|
|
name: fileItem.name,
|
|
|
|
|
|
collection: uploadedFile?.meta?.collection_name
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (uploadedFile.error) {
|
2025-05-15 19:53:07 +00:00
|
|
|
|
console.error('File upload warning:', uploadedFile.error);
|
2024-12-23 21:43:58 +00:00
|
|
|
|
toast.warning(uploadedFile.error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fileItem.status = 'uploaded';
|
|
|
|
|
|
fileItem.file = uploadedFile;
|
|
|
|
|
|
fileItem.id = uploadedFile.id;
|
|
|
|
|
|
fileItem.collection_name =
|
|
|
|
|
|
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
|
|
|
|
|
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
|
|
|
|
|
|
|
|
|
|
|
files = files;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
files = files.filter((item) => item?.itemId !== tempItemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2025-01-30 05:56:28 +00:00
|
|
|
|
toast.error(`${e}`);
|
2024-12-23 21:43:58 +00:00
|
|
|
|
files = files.filter((item) => item?.itemId !== tempItemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
|
draggedOver = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onDragOver = (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
// Check if a file is being draggedOver.
|
|
|
|
|
|
if (e.dataTransfer?.types?.includes('Files')) {
|
|
|
|
|
|
draggedOver = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
draggedOver = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onDragLeave = () => {
|
|
|
|
|
|
draggedOver = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onDrop = async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
2025-07-07 15:26:12 +00:00
|
|
|
|
if (e.dataTransfer?.files && acceptFiles) {
|
2024-12-23 21:43:58 +00:00
|
|
|
|
const inputFiles = Array.from(e.dataTransfer?.files);
|
|
|
|
|
|
if (inputFiles && inputFiles.length > 0) {
|
|
|
|
|
|
console.log(inputFiles);
|
|
|
|
|
|
inputFilesHandler(inputFiles);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
draggedOver = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const submitHandler = async () => {
|
2025-01-01 03:42:49 +00:00
|
|
|
|
if (content === '' && files.length === 0) {
|
2024-12-23 02:40:01 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onSubmit({
|
2024-12-23 21:43:58 +00:00
|
|
|
|
content,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
files: files
|
|
|
|
|
|
}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
content = '';
|
2024-12-23 21:43:58 +00:00
|
|
|
|
files = [];
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
2025-07-06 13:32:03 +00:00
|
|
|
|
if (chatInputElement) {
|
2025-07-08 08:31:22 +00:00
|
|
|
|
chatInputElement?.setText('');
|
|
|
|
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
2025-07-06 13:32:03 +00:00
|
|
|
|
chatInputElement.focus();
|
|
|
|
|
|
}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
};
|
2024-12-23 21:43:58 +00:00
|
|
|
|
|
2024-12-27 05:51:09 +00:00
|
|
|
|
$: if (content) {
|
|
|
|
|
|
onChange();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-23 21:43:58 +00:00
|
|
|
|
onMount(async () => {
|
|
|
|
|
|
window.setTimeout(() => {
|
2025-07-06 13:32:03 +00:00
|
|
|
|
if (chatInputElement) {
|
|
|
|
|
|
chatInputElement.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100);
|
2024-12-23 21:43:58 +00:00
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
|
|
|
|
|
const dropzoneElement = document.getElementById('channel-container');
|
|
|
|
|
|
|
|
|
|
|
|
dropzoneElement?.addEventListener('dragover', onDragOver);
|
|
|
|
|
|
dropzoneElement?.addEventListener('drop', onDrop);
|
|
|
|
|
|
dropzoneElement?.addEventListener('dragleave', onDragLeave);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
|
|
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
|
|
|
|
|
|
const dropzoneElement = document.getElementById('channel-container');
|
|
|
|
|
|
|
|
|
|
|
|
if (dropzoneElement) {
|
|
|
|
|
|
dropzoneElement?.removeEventListener('dragover', onDragOver);
|
|
|
|
|
|
dropzoneElement?.removeEventListener('drop', onDrop);
|
|
|
|
|
|
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2024-12-23 02:40:01 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2024-12-23 21:43:58 +00:00
|
|
|
|
<FilesOverlay show={draggedOver} />
|
|
|
|
|
|
|
2025-07-07 15:26:12 +00:00
|
|
|
|
{#if acceptFiles}
|
|
|
|
|
|
<input
|
|
|
|
|
|
bind:this={filesInputElement}
|
|
|
|
|
|
bind:files={inputFiles}
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
hidden
|
|
|
|
|
|
multiple
|
|
|
|
|
|
on:change={async () => {
|
|
|
|
|
|
if (inputFiles && inputFiles.length > 0) {
|
|
|
|
|
|
inputFilesHandler(Array.from(inputFiles));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.error($i18n.t(`File not found.`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
filesInputElement.value = '';
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/if}
|
2024-12-23 21:43:58 +00:00
|
|
|
|
|
2025-07-08 21:49:43 +00:00
|
|
|
|
<InputVariablesModal
|
|
|
|
|
|
bind:show={showInputVariablesModal}
|
|
|
|
|
|
variables={inputVariables}
|
|
|
|
|
|
onSave={(variableValues) => {
|
|
|
|
|
|
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
|
|
|
|
|
replaceVariables(inputVariableValues);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2024-12-31 08:51:43 +00:00
|
|
|
|
<div class="bg-transparent">
|
2024-12-25 03:20:38 +00:00
|
|
|
|
<div
|
|
|
|
|
|
class="{($settings?.widescreenMode ?? null)
|
|
|
|
|
|
? 'max-w-full'
|
2025-07-07 15:26:12 +00:00
|
|
|
|
: 'max-w-6xl'} mx-auto inset-x-0 relative"
|
2024-12-25 03:20:38 +00:00
|
|
|
|
>
|
2024-12-27 05:51:09 +00:00
|
|
|
|
<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="relative">
|
|
|
|
|
|
{#if scrollEnd === false}
|
|
|
|
|
|
<div
|
|
|
|
|
|
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
|
|
|
|
|
|
on:click={() => {
|
|
|
|
|
|
scrollEnd = true;
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
class="w-5 h-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
|
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
|
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="relative">
|
2024-12-31 10:23:26 +00:00
|
|
|
|
<div class=" -mt-5">
|
2024-12-27 05:51:09 +00:00
|
|
|
|
{#if typingUsers.length > 0}
|
|
|
|
|
|
<div class=" text-xs px-4 mb-1">
|
2024-12-31 10:23:26 +00:00
|
|
|
|
<span class=" font-normal text-black dark:text-white">
|
2024-12-27 05:51:09 +00:00
|
|
|
|
{typingUsers.map((user) => user.name).join(', ')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{$i18n.t('is typing...')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
</div>
|
2025-07-08 21:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
<Commands
|
|
|
|
|
|
bind:this={commandsElement}
|
|
|
|
|
|
show={showCommands}
|
|
|
|
|
|
{command}
|
|
|
|
|
|
insertTextHandler={insertTextAtCursor}
|
|
|
|
|
|
/>
|
2024-12-27 05:51:09 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2024-12-23 02:40:01 +00:00
|
|
|
|
<div class="">
|
|
|
|
|
|
{#if recording}
|
|
|
|
|
|
<VoiceRecording
|
|
|
|
|
|
bind:recording
|
2025-05-03 18:53:23 +00:00
|
|
|
|
onCancel={async () => {
|
2024-12-23 02:40:01 +00:00
|
|
|
|
recording = false;
|
|
|
|
|
|
|
|
|
|
|
|
await tick();
|
2025-07-06 13:32:03 +00:00
|
|
|
|
|
|
|
|
|
|
if (chatInputElement) {
|
|
|
|
|
|
chatInputElement.focus();
|
|
|
|
|
|
}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
}}
|
2025-05-03 18:53:23 +00:00
|
|
|
|
onConfirm={async (data) => {
|
|
|
|
|
|
const { text, filename } = data;
|
2024-12-23 02:40:01 +00:00
|
|
|
|
content = `${content}${text} `;
|
|
|
|
|
|
recording = false;
|
|
|
|
|
|
|
|
|
|
|
|
await tick();
|
2025-07-06 13:32:03 +00:00
|
|
|
|
|
|
|
|
|
|
if (chatInputElement) {
|
|
|
|
|
|
chatInputElement.focus();
|
|
|
|
|
|
}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
<form
|
|
|
|
|
|
class="w-full flex gap-1.5"
|
|
|
|
|
|
on:submit|preventDefault={() => {
|
|
|
|
|
|
submitHandler();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
2024-12-31 08:51:43 +00:00
|
|
|
|
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"
|
2025-04-07 00:02:39 +00:00
|
|
|
|
dir={$settings?.chatDirection ?? 'auto'}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
>
|
2024-12-23 21:43:58 +00:00
|
|
|
|
{#if files.length > 0}
|
2025-02-04 05:25:16 +00:00
|
|
|
|
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
|
2024-12-23 21:43:58 +00:00
|
|
|
|
{#each files as file, fileIdx}
|
|
|
|
|
|
{#if file.type === 'image'}
|
|
|
|
|
|
<div class=" relative group">
|
|
|
|
|
|
<div class="relative">
|
|
|
|
|
|
<Image
|
|
|
|
|
|
src={file.url}
|
|
|
|
|
|
alt="input"
|
|
|
|
|
|
imageClassName=" h-16 w-16 rounded-xl object-cover"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class=" absolute -top-1 -right-1">
|
|
|
|
|
|
<button
|
2025-02-04 04:06:17 +00:00
|
|
|
|
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
2024-12-23 21:43:58 +00:00
|
|
|
|
type="button"
|
|
|
|
|
|
on:click={() => {
|
|
|
|
|
|
files.splice(fileIdx, 1);
|
|
|
|
|
|
files = files;
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
class="w-4 h-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
<FileItem
|
|
|
|
|
|
item={file}
|
|
|
|
|
|
name={file.name}
|
|
|
|
|
|
type={file.type}
|
|
|
|
|
|
size={file?.size}
|
|
|
|
|
|
loading={file.status === 'uploading'}
|
|
|
|
|
|
dismissible={true}
|
|
|
|
|
|
edit={true}
|
|
|
|
|
|
on:dismiss={() => {
|
|
|
|
|
|
files.splice(fileIdx, 1);
|
|
|
|
|
|
files = files;
|
|
|
|
|
|
}}
|
|
|
|
|
|
on:click={() => {
|
|
|
|
|
|
console.log(file);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
2025-02-04 04:06:17 +00:00
|
|
|
|
<div class="px-2.5">
|
2024-12-31 10:16:07 +00:00
|
|
|
|
<div
|
2025-07-07 15:26:12 +00:00
|
|
|
|
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"
|
2024-12-31 10:16:07 +00:00
|
|
|
|
>
|
|
|
|
|
|
<RichTextInput
|
2025-07-06 13:32:03 +00:00
|
|
|
|
bind:this={chatInputElement}
|
|
|
|
|
|
json={true}
|
2024-12-31 10:16:07 +00:00
|
|
|
|
messageInput={true}
|
2025-07-08 23:15:20 +00:00
|
|
|
|
{showFormattingButtons}
|
2025-07-06 13:32:03 +00:00
|
|
|
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
|
|
|
|
|
(!$mobile ||
|
|
|
|
|
|
!(
|
|
|
|
|
|
'ontouchstart' in window ||
|
|
|
|
|
|
navigator.maxTouchPoints > 0 ||
|
|
|
|
|
|
navigator.msMaxTouchPoints > 0
|
|
|
|
|
|
))}
|
2024-12-31 10:16:07 +00:00
|
|
|
|
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
2025-07-06 13:32:03 +00:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const { md } = e;
|
|
|
|
|
|
content = md;
|
2025-07-08 21:49:43 +00:00
|
|
|
|
command = getCommand();
|
2025-07-06 13:32:03 +00:00
|
|
|
|
}}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
on:keydown={async (e) => {
|
|
|
|
|
|
e = e.detail.event;
|
|
|
|
|
|
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
2025-07-08 21:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
const commandsContainerElement = document.getElementById('commands-container');
|
|
|
|
|
|
|
|
|
|
|
|
if (commandsContainerElement) {
|
|
|
|
|
|
if (commandsContainerElement && e.key === 'ArrowUp') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
commandsElement.selectUp();
|
|
|
|
|
|
|
|
|
|
|
|
const commandOptionButton = [
|
|
|
|
|
|
...document.getElementsByClassName('selected-command-option-button')
|
|
|
|
|
|
]?.at(-1);
|
|
|
|
|
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (commandsContainerElement && e.key === 'ArrowDown') {
|
2024-12-23 02:40:01 +00:00
|
|
|
|
e.preventDefault();
|
2025-07-08 21:49:43 +00:00
|
|
|
|
commandsElement.selectDown();
|
|
|
|
|
|
|
|
|
|
|
|
const commandOptionButton = [
|
|
|
|
|
|
...document.getElementsByClassName('selected-command-option-button')
|
|
|
|
|
|
]?.at(-1);
|
|
|
|
|
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
2024-12-23 02:40:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-08 21:49:43 +00:00
|
|
|
|
if (commandsContainerElement && e.key === 'Tab') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
const commandOptionButton = [
|
|
|
|
|
|
...document.getElementsByClassName('selected-command-option-button')
|
|
|
|
|
|
]?.at(-1);
|
|
|
|
|
|
|
|
|
|
|
|
commandOptionButton?.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (commandsContainerElement && e.key === 'Enter') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
const commandOptionButton = [
|
|
|
|
|
|
...document.getElementsByClassName('selected-command-option-button')
|
|
|
|
|
|
]?.at(-1);
|
|
|
|
|
|
|
|
|
|
|
|
if (commandOptionButton) {
|
|
|
|
|
|
commandOptionButton?.click();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.getElementById('send-message-button')?.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!$mobile ||
|
|
|
|
|
|
!(
|
|
|
|
|
|
'ontouchstart' in window ||
|
|
|
|
|
|
navigator.maxTouchPoints > 0 ||
|
|
|
|
|
|
navigator.msMaxTouchPoints > 0
|
|
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Prevent Enter key from creating a new line
|
|
|
|
|
|
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
|
|
|
|
|
if (e.keyCode === 13 && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Submit the content when Enter key is pressed
|
|
|
|
|
|
if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
|
|
|
|
|
|
submitHandler();
|
|
|
|
|
|
}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (e.key === 'Escape') {
|
2025-05-15 19:53:07 +00:00
|
|
|
|
console.info('Escape');
|
2024-12-23 02:40:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
}}
|
2024-12-31 10:16:07 +00:00
|
|
|
|
on:paste={async (e) => {
|
|
|
|
|
|
e = e.detail.event;
|
2025-05-15 19:53:07 +00:00
|
|
|
|
console.info(e);
|
2024-12-23 02:40:01 +00:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2024-12-31 10:16:07 +00:00
|
|
|
|
</div>
|
2025-02-04 03:18:31 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-02-04 06:11:01 +00:00
|
|
|
|
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
|
2025-07-07 20:07:50 +00:00
|
|
|
|
<div class="ml-1 self-end flex space-x-1 flex-1">
|
2025-07-07 15:26:12 +00:00
|
|
|
|
<slot name="menu">
|
|
|
|
|
|
{#if acceptFiles}
|
|
|
|
|
|
<InputMenu
|
|
|
|
|
|
{screenCaptureHandler}
|
|
|
|
|
|
uploadFilesHandler={() => {
|
|
|
|
|
|
filesInputElement.click();
|
|
|
|
|
|
}}
|
2025-02-04 03:18:31 +00:00
|
|
|
|
>
|
2025-07-07 15:26:12 +00:00
|
|
|
|
<button
|
|
|
|
|
|
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="More"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
class="size-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</InputMenu>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
</slot>
|
2025-02-04 03:18:31 +00:00
|
|
|
|
</div>
|
2024-12-23 02:40:01 +00:00
|
|
|
|
|
2025-02-04 04:13:14 +00:00
|
|
|
|
<div class="self-end flex space-x-1 mr-1">
|
2024-12-23 02:40:01 +00:00
|
|
|
|
{#if content === ''}
|
|
|
|
|
|
<Tooltip content={$i18n.t('Record voice')}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
id="voice-input-button"
|
|
|
|
|
|
class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 mr-0.5 self-center"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
on:click={async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let stream = await navigator.mediaDevices
|
|
|
|
|
|
.getUserMedia({ audio: true })
|
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
|
toast.error(
|
|
|
|
|
|
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
|
|
|
|
|
error: err
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (stream) {
|
|
|
|
|
|
recording = true;
|
|
|
|
|
|
const tracks = stream.getTracks();
|
|
|
|
|
|
tracks.forEach((track) => track.stop());
|
|
|
|
|
|
}
|
|
|
|
|
|
stream = null;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
toast.error($i18n.t('Permission denied when accessing microphone'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
aria-label="Voice Input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
class="w-5 h-5 translate-y-[0.5px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
<div class=" flex items-center">
|
2025-07-07 15:26:12 +00:00
|
|
|
|
{#if inputLoading && onStop}
|
|
|
|
|
|
<div class=" flex items-center">
|
|
|
|
|
|
<Tooltip content={$i18n.t('Stop')}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
|
|
|
|
|
on:click={() => {
|
|
|
|
|
|
onStop();
|
|
|
|
|
|
}}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
>
|
2025-07-07 15:26:12 +00:00
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
class="size-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
|
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
|
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
<div class=" flex items-center">
|
|
|
|
|
|
<Tooltip content={$i18n.t('Send message')}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
id="send-message-button"
|
|
|
|
|
|
class="{content !== '' || files.length !== 0
|
|
|
|
|
|
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
|
|
|
|
|
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
disabled={content === '' && files.length === 0}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 16 16"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
class="size-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
|
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/if}
|
2024-12-23 02:40:01 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|