enh: shortcode emojis

This commit is contained in:
Timothy Jaeryang Baek
2026-03-29 19:01:50 -05:00
parent 2388dd7dc3
commit 2040095050
4 changed files with 155 additions and 1 deletions
@@ -622,6 +622,25 @@
}
}
})
},
{
char: ':',
allowSpaces: false,
command: ({ editor, range, props }) => {
// Convert the Unicode hex codepoint (e.g. "1F44B") to the actual emoji character (👋)
const codepoint = props.id;
const emoji = String.fromCodePoint(parseInt(codepoint, 16));
editor.chain().focus().deleteRange(range).insertContent(emoji).run();
},
render: getSuggestionRenderer(CommandSuggestionList, {
i18n,
onSelect: (e) => {
document.getElementById('chat-input')?.focus();
},
insertTextHandler: insertTextAtCursor,
onUpload: () => {}
})
}
];
loaded = true;
+20 -1
View File
@@ -407,7 +407,7 @@
let command = '';
export let showCommands = false;
$: showCommands =
['/', '#', '@', '$'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
['/', '#', '@', '$', ':'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
let suggestions = null;
let showTools = false;
@@ -1030,6 +1030,25 @@
document.getElementById('chat-input')?.focus();
},
insertTextHandler: insertTextAtCursor,
onUpload: () => {}
})
},
{
char: ':',
allowSpaces: false,
command: ({ editor, range, props }) => {
// Convert the Unicode hex codepoint (e.g. "1F44B") to the actual emoji character (👋)
const codepoint = props.id;
const emoji = String.fromCodePoint(parseInt(codepoint, 16));
editor.chain().focus().deleteRange(range).insertContent(emoji).run();
},
render: getSuggestionRenderer(CommandSuggestionList, {
i18n,
onSelect: (e) => {
document.getElementById('chat-input')?.focus();
},
insertTextHandler: insertTextAtCursor,
onUpload: () => {}
})
@@ -3,6 +3,7 @@
import Knowledge from './Commands/Knowledge.svelte';
import Models from './Commands/Models.svelte';
import Skills from './Commands/Skills.svelte';
import Emojis from './Commands/Emojis.svelte';
export let char = '';
export let query = '';
@@ -135,6 +136,22 @@
}
}}
/>
{:else if char === ':'}
<Emojis
bind:this={suggestionElement}
{query}
bind:filteredItems
onSelect={(e) => {
const { type, data } = e;
if (type === 'emoji') {
command({
id: data.name,
label: data.shortCodes[0]
});
}
}}
/>
{/if}
</div>
</div>
@@ -0,0 +1,99 @@
<script lang="ts">
import { getContext } from 'svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import emojiShortCodes from '$lib/emoji-shortcodes.json';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let query = '';
export let onSelect = (e) => {};
let selectedIdx = 0;
export let filteredItems = [];
// Build a flat list of { name, shortCodes } for searching
const allEmojis = Object.entries(emojiShortCodes).map(([key, value]) => ({
name: key,
shortCodes: typeof value === 'string' ? [value] : (value as string[])
}));
$: {
if (query && query.length >= 2) {
const q = query.toLowerCase();
filteredItems = allEmojis
.filter(
(emoji) =>
emoji.name.toLowerCase().includes(q) ||
emoji.shortCodes.some((code) => code.toLowerCase().includes(q))
)
.sort((a, b) => {
// Score: 0 = exact match, 1 = prefix match, 2 = substring match
const score = (emoji) => {
if (emoji.shortCodes.some((c) => c.toLowerCase() === q)) return 0;
if (emoji.shortCodes.some((c) => c.toLowerCase().startsWith(q))) return 1;
return 2;
};
return score(a) - score(b);
})
.slice(0, 50);
} else {
filteredItems = [];
}
}
$: if (query) {
selectedIdx = 0;
}
export const selectUp = () => {
selectedIdx = Math.max(0, selectedIdx - 1);
};
export const selectDown = () => {
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
};
export const select = async () => {
const emoji = filteredItems[selectedIdx];
if (emoji) {
onSelect({ type: 'emoji', data: emoji });
}
};
</script>
{#if filteredItems.length > 0}
<div class="px-2 text-xs text-gray-500 py-1">
{$i18n.t('Emojis')}
</div>
{#each filteredItems as emoji, emojiIdx}
<button
class="px-2.5 py-1.5 rounded-xl w-full text-left {emojiIdx === selectedIdx
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
onSelect({ type: 'emoji', data: emoji });
}}
on:mousemove={() => {
selectedIdx = emojiIdx;
}}
on:focus={() => {}}
data-selected={emojiIdx === selectedIdx}
>
<div class="flex items-center gap-2 text-black dark:text-gray-100">
<img
src="{WEBUI_BASE_URL}/assets/emojis/{emoji.name.toLowerCase()}.svg"
alt={emoji.name}
class="size-5 flex-shrink-0"
loading="lazy"
/>
<div class="truncate text-sm">
:{emoji.shortCodes[0]}:
</div>
</div>
</button>
{/each}
{/if}