From 2673aecc5ba16e3352fe77a9ee5210ea85b1d527 Mon Sep 17 00:00:00 2001 From: Meleki Date: Tue, 3 Mar 2026 16:18:00 +0100 Subject: [PATCH] feat: Emoji-Picker Button im Chat-Input (rechts neben Senden) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #emojiBtn links neben #send platziert - Fluent Emoji 3D Rendering via parseFluentEmojis() - Kein Tooltip auf den Emoji-Buttons - Picker öffnet sich rechts-ausgerichtet über dem Button - emoji.userdata.js geladen (Kategorien + Mappings) - Schließt beim Klick außerhalb oder beim destroy() --- wwwroot/css/chat.css | 51 +++++++++++++++++ wwwroot/index.html | 2 + wwwroot/js/emoji.userdata.js | 9 +++ wwwroot/js/pages/chat-page.js | 101 ++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 wwwroot/js/emoji.userdata.js diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 8467fdf..a889b5d 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -318,6 +318,57 @@ color: var(--dangerous); } +/* Emoji Picker */ + +.emoji-picker { + position: fixed; + z-index: 500; + background: var(--f2-bg); + border: var(--border-width) solid var(--f2-border); + border-radius: var(--border-radius); + width: 280px; + max-height: 320px; + overflow-y: auto; + padding: var(--badge-spacing); + display: flex; + flex-direction: column; + gap: var(--box-spacing); + box-shadow: 0 4px 16px rgb(0 0 0 / 0.2); +} + +.emoji-picker-section { + display: flex; + flex-direction: column; + gap: var(--badge-spacing); +} + +.emoji-picker-label { + font-size: var(--font-size); + opacity: var(--semi-transparent); + padding: 0 .2em; +} + +.emoji-picker-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(2.6em, 1fr)); +} + +.emoji-picker-btn { + background: none; + border: none; + padding: .2em .25em; + font-size: 1.3rem; + line-height: 1; + cursor: pointer; + border-radius: var(--border-radius); + transition: background var(--transition-duration); +} + +.emoji-picker-btn:hover { + background: var(--f3-bg); + border: none; +} + .stop-btn:hover { background: rgb(from var(--dangerous) r g b / 0.15); } diff --git a/wwwroot/index.html b/wwwroot/index.html index 1c48b4f..e18ff64 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -217,6 +217,7 @@ + @@ -389,6 +390,7 @@ + diff --git a/wwwroot/js/emoji.userdata.js b/wwwroot/js/emoji.userdata.js new file mode 100644 index 0000000..e34533f --- /dev/null +++ b/wwwroot/js/emoji.userdata.js @@ -0,0 +1,9 @@ +const EMOJI_USERDATA = { + 'Smileys': ['😊','😂','🤣','😄','😁','😅','😆','🙂','😉','😎','🤓','😇','🥳','🤩','😍','🥰','😘','😋','🤔','🤨','😐','😑','🙄','😏','😬','🤫','🤭','😌','😴','🥱','😮','😲','🤯','😳','😱','😨','😰','😢','😭','😤','😡','🤬','💀','😈','👿','🤡','💩','👻','👽','🤖'], + 'Gesten': ['👍','👎','👌','✌️','🤞','🤙','👋','🤚','✋','🖐️','👆','☝️','👇','👈','👉','👊','✊','🤛','🤜','🙏','👏','🙌','🤝','💪','🤟','🤲'], + 'Herzen': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','💕','💖','💗','💘','💝'], + 'Objekte': ['🔥','⭐','✨','💡','💎','🎯','🎮','🎵','🎶','📌','📎','✏️','📝','📚','💻','⌨️','🖥️','📱','🔧','🔨','⚡','🚀','🏆','🎉','🎊','🎁','🎈'], + 'Natur': ['🌞','🌙','⛅','🌈','🌊','🍀','🌸','🌺','🌻','🌲','🍂','❄️','🐱','🐶','🦊','🐻','🐼','🐨','🦁','🐸','🐧','🦄','🐝','🦋'], + 'Essen': ['☕','🍺','🍷','🍕','🍔','🍟','🌭','🍿','🧁','🍰','🍩','🍪','🍫','🍉','🍓','🍑','🥑','🌶️'], + 'Reaktionen': ['✅','❌','⚠️','❓','❗','💯','♻️','🔄','⏳','⏰','🔔','🔕','📢','💬','💭','👀','🫠','🤷','🤦','🥺',] +}; diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index 05f2de0..559c6ab 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -2,6 +2,97 @@ let _streamingPollInterval = null; let _recoveryChainId = null; +let _emojiPickerEl = null; +let _emojiPickerCloseHandler = null; + +function _buildEmojiPicker() { + const picker = document.createElement('div'); + picker.className = 'emoji-picker'; + + for (const [category, emojis] of Object.entries(EMOJI_USERDATA)) { + const section = document.createElement('div'); + section.className = 'emoji-picker-section'; + + const label = document.createElement('div'); + label.className = 'emoji-picker-label'; + label.textContent = category; + section.appendChild(label); + + const grid = document.createElement('div'); + grid.className = 'emoji-picker-grid'; + + for (const emoji of emojis) { + const btn = document.createElement('button'); + btn.className = 'emoji-picker-btn'; + btn.textContent = emoji; + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const input = document.getElementById('input'); + if (input) { + const start = input.selectionStart; + const end = input.selectionEnd; + const val = input.value; + input.value = val.slice(0, start) + emoji + val.slice(end); + const pos = start + emoji.length; + input.setSelectionRange(pos, pos); + input.dispatchEvent(new Event('input')); + input.focus(); + } + closeEmojiPicker(); + }); + // Fluent Emoji im Button rendern + parseFluentEmojis(btn); + grid.appendChild(btn); + } + + section.appendChild(grid); + picker.appendChild(section); + } + + return picker; +} + +function toggleEmojiPicker(anchor) { + if (_emojiPickerEl) { + closeEmojiPicker(); + return; + } + + const picker = _buildEmojiPicker(); + document.body.appendChild(picker); + _emojiPickerEl = picker; + + // Positionierung: über dem Button, rechts ausgerichtet + const rect = anchor.getBoundingClientRect(); + const pickerHeight = 320; + const pickerWidth = 280; + let top = rect.top - pickerHeight - 6; + let left = rect.right - pickerWidth; + + // Falls kein Platz oben → unter dem Button + if (top < 8) top = rect.bottom + 6; + // Falls links abgeschnitten → linksbündig zum Button + if (left < 8) left = 8; + + picker.style.top = top + 'px'; + picker.style.left = left + 'px'; + + _emojiPickerCloseHandler = (e) => { + if (!picker.contains(e.target)) closeEmojiPicker(); + }; + setTimeout(() => document.addEventListener('click', _emojiPickerCloseHandler), 0); +} + +function closeEmojiPicker() { + if (_emojiPickerEl) { + _emojiPickerEl.remove(); + _emojiPickerEl = null; + } + if (_emojiPickerCloseHandler) { + document.removeEventListener('click', _emojiPickerCloseHandler); + _emojiPickerCloseHandler = null; + } +} function startStreamingPoll(sessionId, sendBtn, chainId) { _recoveryChainId = chainId; @@ -190,6 +281,15 @@ registerPage('chat', { inputEl.dispatchEvent(new Event('input')) } + // Emoji Picker + const emojiBtn = document.getElementById('emojiBtn'); + if (emojiBtn) { + emojiBtn.addEventListener('click', (e) => { + e.stopPropagation(); + toggleEmojiPicker(emojiBtn); + }); + } + // Workspace Explorer const explorerBtn = document.getElementById('explorerBtn'); if (explorerBtn) { @@ -275,6 +375,7 @@ registerPage('chat', { // Stream nicht abbrechen (Backend soll weiterlaufen), // aber Frontend-Processing stoppen _chatAbandoned = true; + closeEmojiPicker(); if (this._closeMenu) { document.removeEventListener('click', this._closeMenu); this._closeMenu = null;