mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
feat: emoji picker
This commit is contained in:
@@ -8,10 +8,10 @@
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
import { settings, user } from '$lib/stores';
|
||||
import { settings, user, shortCodesToEmojis } from '$lib/stores';
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
@@ -28,17 +28,32 @@
|
||||
import ProfilePreview from './Message/ProfilePreview.svelte';
|
||||
import ChatBubbleOvalEllipsis from '$lib/components/icons/ChatBubbleOvalEllipsis.svelte';
|
||||
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
||||
import ReactionPicker from './Message/ReactionPicker.svelte';
|
||||
|
||||
export let message;
|
||||
export let showUserProfile = true;
|
||||
|
||||
export let onDelete: Function = () => {};
|
||||
export let onEdit: Function = () => {};
|
||||
let showButtons = false;
|
||||
|
||||
let edit = false;
|
||||
let editedContent = null;
|
||||
let showDeleteConfirmDialog = false;
|
||||
|
||||
let reactions = [
|
||||
{
|
||||
name: 'red_circle',
|
||||
user_ids: ['U07KUHZSYER'],
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
name: '+1',
|
||||
user_ids: [$user.id],
|
||||
count: 1
|
||||
}
|
||||
];
|
||||
|
||||
const formatDate = (inputDate) => {
|
||||
const date = dayjs(inputDate);
|
||||
const now = dayjs();
|
||||
@@ -71,23 +86,26 @@
|
||||
: 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
|
||||
>
|
||||
{#if (message.user_id === $user.id || $user.role === 'admin') && !edit}
|
||||
<div class=" absolute invisible group-hover:visible right-1 -top-2 z-30">
|
||||
<div
|
||||
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-30"
|
||||
>
|
||||
<div
|
||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<Tooltip content={$i18n.t('Reactions')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
}}
|
||||
>
|
||||
<FaceSmile />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ReactionPicker onClose={() => (showButtons = false)}>
|
||||
<Tooltip content={$i18n.t('Add Reaction')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
showButtons = true;
|
||||
}}
|
||||
>
|
||||
<FaceSmile />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</ReactionPicker>
|
||||
|
||||
<Tooltip content={$i18n.t('Reply in thread')}>
|
||||
<Tooltip content={$i18n.t('Reply in Thread')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
@@ -252,6 +270,50 @@
|
||||
>(edited)</span
|
||||
>{/if}
|
||||
</div>
|
||||
|
||||
{#if reactions.length > 0}
|
||||
<div>
|
||||
<div class="flex items-center gap-1 mt-1 mb-2">
|
||||
{#each reactions as reaction}
|
||||
<button
|
||||
class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes(
|
||||
$user.id
|
||||
)
|
||||
? ' bg-blue-500/10 outline outline-blue-500/50 outline-1'
|
||||
: 'bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
|
||||
>
|
||||
{#if $shortCodesToEmojis[reaction.name]}
|
||||
<img
|
||||
src="/assets/emojis/{$shortCodesToEmojis[reaction.name].toLowerCase()}.svg"
|
||||
alt={reaction.name}
|
||||
class=" size-4"
|
||||
/>
|
||||
{:else}
|
||||
<div>
|
||||
{reaction.name}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if reaction.user_ids.length > 0}
|
||||
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{reaction.user_ids?.length}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<ReactionPicker>
|
||||
<Tooltip content={$i18n.t('Add Reaction')}>
|
||||
<div
|
||||
class="flex items-center gap-1.5 bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1 transition rounded-xl px-1 py-1 cursor-pointer text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<FaceSmile />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ReactionPicker>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import emojiGroups from '$lib/emoji-groups.json';
|
||||
import emojiShortCodes from '$lib/emoji-shortcodes.json';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
export let onClose = () => {};
|
||||
export let side = 'top';
|
||||
export let align = 'start';
|
||||
|
||||
export let user = null;
|
||||
let show = false;
|
||||
|
||||
let emojis = emojiShortCodes;
|
||||
let search = '';
|
||||
|
||||
$: if (search) {
|
||||
emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
|
||||
if (key.includes(search)) {
|
||||
acc[key] = emojiShortCodes[key];
|
||||
} else {
|
||||
if (Array.isArray(emojiShortCodes[key])) {
|
||||
const filtered = emojiShortCodes[key].filter((emoji) => emoji.includes(search));
|
||||
if (filtered.length) {
|
||||
acc[key] = filtered;
|
||||
}
|
||||
} else {
|
||||
if (emojiShortCodes[key].includes(search)) {
|
||||
acc[key] = emojiShortCodes[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
emojis = emojiShortCodes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root
|
||||
bind:open={show}
|
||||
closeFocus={false}
|
||||
onOpenChange={(state) => {
|
||||
if (!state) {
|
||||
search = '';
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
typeahead={false}
|
||||
>
|
||||
<DropdownMenu.Trigger>
|
||||
<slot />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<slot name="content">
|
||||
<DropdownMenu.Content
|
||||
class="max-w-full w-80 bg-gray-100 dark:bg-gray-850 rounded-lg z-50 shadow-lg text-white"
|
||||
sideOffset={8}
|
||||
{side}
|
||||
{align}
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<div class="mb-1 px-3 pt-2 pb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
placeholder="Search all emojis"
|
||||
bind:value={search}
|
||||
/>
|
||||
</div>
|
||||
<div class=" w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm">
|
||||
<div>
|
||||
{#if Object.keys(emojis).length === 0}
|
||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
|
||||
{:else}
|
||||
{#each Object.keys(emojiGroups) as group}
|
||||
{@const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji])}
|
||||
{#if groupEmojis.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
|
||||
{group}
|
||||
</div>
|
||||
|
||||
<div class="flex mb-2 flex-wrap gap-1">
|
||||
{#each groupEmojis as emoji}
|
||||
<Tooltip
|
||||
content={(typeof emojiShortCodes[emoji] === 'string'
|
||||
? [emojiShortCodes[emoji]]
|
||||
: emojiShortCodes[emoji]
|
||||
)
|
||||
.map((code) => `:${code}:`)
|
||||
.join(', ')}
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-700 transition"
|
||||
>
|
||||
<img
|
||||
src="/assets/emojis/{emoji.toLowerCase()}.svg"
|
||||
alt={emoji}
|
||||
class="size-5"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</slot>
|
||||
</DropdownMenu.Root>
|
||||
@@ -106,6 +106,12 @@
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Emoji graphics provided by
|
||||
<a href="https://github.com/jdecked/twemoji" target="_blank">Twemoji</a>, licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC-BY 4.0</a>.
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||
<img
|
||||
|
||||
5054
src/lib/emoji-groups.json
Normal file
5054
src/lib/emoji-groups.json
Normal file
File diff suppressed because it is too large
Load Diff
1905
src/lib/emoji-shortcodes.json
Normal file
1905
src/lib/emoji-shortcodes.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import { APP_NAME } from '$lib/constants';
|
||||
import { type Writable, writable } from 'svelte/store';
|
||||
import type { GlobalModelConfig, ModelConfig } from '$lib/apis';
|
||||
import type { ModelConfig } from '$lib/apis';
|
||||
import type { Banner } from '$lib/types';
|
||||
import type { Socket } from 'socket.io-client';
|
||||
|
||||
import emojiShortCodes from '$lib/emoji-shortcodes.json';
|
||||
|
||||
// Backend
|
||||
export const WEBUI_NAME = writable(APP_NAME);
|
||||
export const config: Writable<Config | undefined> = writable(undefined);
|
||||
@@ -20,6 +22,18 @@ export const USAGE_POOL: Writable<null | string[]> = writable(null);
|
||||
|
||||
export const theme = writable('system');
|
||||
|
||||
export const shortCodesToEmojis = writable(Object.entries(emojiShortCodes).reduce((acc, [key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
acc[value] = key;
|
||||
} else {
|
||||
for (const v of value) {
|
||||
acc[v] = key;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}));
|
||||
|
||||
export const chatId = writable('');
|
||||
export const chatTitle = writable('');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user