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;