Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/L0/_all/mod/_core/onscreen_agent/onscreen-agent.css
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@
.onscreen-agent-composer-input-wrap {
position: relative;
min-width: 0;
cursor: text;
}

.onscreen-agent-set-api-key-overlay {
Expand Down Expand Up @@ -1104,3 +1105,59 @@
max-width: calc(100% - 46px);
}
}

/* ── Mic button ─────────────────────────────────────────────────────────── */
.onscreen-agent-mic-button.is-recording {
color: var(--space-danger, #f87171);
background: color-mix(in srgb, var(--space-danger, #f87171) 12%, transparent);
animation: onscreen-agent-mic-pulse 1.2s ease-in-out infinite;
}

@keyframes onscreen-agent-mic-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}

/* ── Emoji button active state ───────────────────────────────────────────── */
.onscreen-agent-emoji-button.is-active {
color: var(--space-accent, var(--chat-send-background, #818cf8));
background: color-mix(in srgb, var(--space-accent, #818cf8) 12%, transparent);
}

/* ── Emoji picker popover ────────────────────────────────────────────────── */
.onscreen-agent-emoji-picker {
position: fixed;
background: var(--space-surface-2, var(--chat-surface, #1e1e2e));
border: 1px solid var(--space-border, rgba(255, 255, 255, 0.13));
border-radius: 12px;
padding: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
z-index: var(--space-popover-z, 9000);
width: 268px;
max-height: 320px;
overflow-y: auto;
}

.onscreen-agent-emoji-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 2px;
}

.onscreen-agent-emoji-item {
background: none;
border: none;
cursor: pointer;
font-size: 20px;
line-height: 1;
padding: 5px 3px;
border-radius: 6px;
transition: background 0.1s;
display: flex;
align-items: center;
justify-content: center;
}

.onscreen-agent-emoji-item:hover {
background: var(--space-surface-hover, rgba(255, 255, 255, 0.1));
}
56 changes: 54 additions & 2 deletions app/L0/_all/mod/_core/onscreen_agent/panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
'is-edge-hidden-bottom': $store.onscreenAgent.hiddenEdge === 'bottom',
'is-edge-hidden-left': $store.onscreenAgent.hiddenEdge === 'left',
'is-edge-hidden-right': $store.onscreenAgent.hiddenEdge === 'right',
'is-edge-hidden-top': $store.onscreenAgent.hiddenEdge === 'top',
'is-edge-hidden-top': $store.onscreenAgent.hiddenEdge === 'top',h
'is-full': $store.onscreenAgent.isFullMode,
'is-history-below': $store.onscreenAgent.isHistoryBelow,
'is-mode-collapsing': $store.onscreenAgent.isModeTransitionCollapsing,
Expand Down Expand Up @@ -144,7 +144,7 @@
class="composer-attachment-input"
@change="$store.onscreenAgent.handleAttachmentInput($event)"
/>
<div class="onscreen-agent-composer-input-wrap">
<div class="onscreen-agent-composer-input-wrap" @click.self="$refs.input.focus()">
<textarea
x-ref="input"
name="message"
Expand Down Expand Up @@ -194,6 +194,33 @@
>
<x-icon>attach_file</x-icon>
</button>
<button
x-show="$store.onscreenAgent.isFullMode"
type="button"
class="onscreen-agent-inline-button onscreen-agent-emoji-button"
:class="{ 'is-active': $store.onscreenAgent.isEmojiPickerOpen }"
:disabled="$store.onscreenAgent.isComposerInputDisabled"
:aria-expanded="String($store.onscreenAgent.isEmojiPickerOpen)"
aria-controls="onscreen-agent-emoji-picker"
aria-haspopup="true"
@click="$store.onscreenAgent.toggleEmojiPicker($event)"
aria-label="Insert emoji"
title="Insert emoji"
>
<x-icon>emoji_emotions</x-icon>
</button>
<button
x-show="$store.onscreenAgent.isFullMode && $store.onscreenAgent.sttSupported"
type="button"
class="onscreen-agent-inline-button onscreen-agent-mic-button"
:class="{ 'is-recording': $store.onscreenAgent.isRecording }"
:disabled="$store.onscreenAgent.isComposerInputDisabled"
@click="$store.onscreenAgent.handleMicClick()"
:aria-label="$store.onscreenAgent.isRecording ? 'Stop voice input' : 'Start voice input'"
:title="$store.onscreenAgent.isRecording ? 'Stop voice input' : 'Start voice input'"
>
<x-icon x-text="$store.onscreenAgent.isRecording ? 'mic_off' : 'mic'"></x-icon>
</button>
<button
x-show="$store.onscreenAgent.isCompactMode"
type="button"
Expand Down Expand Up @@ -313,6 +340,31 @@
</div>
</template>

<template x-teleport="body">
<div class="space-popover-layer onscreen-agent-popover-layer" x-show="$store.onscreenAgent.isEmojiPickerOpen" x-cloak>
<div
id="onscreen-agent-emoji-picker"
class="onscreen-agent-emoji-picker"
x-show="$store.onscreenAgent.isEmojiPickerOpen"
:style="$store.onscreenAgent.emojiPickerStyle"
@click.outside="$store.onscreenAgent.closeEmojiPicker()"
@keydown.escape.window="$store.onscreenAgent.closeEmojiPicker()"
>
<div class="onscreen-agent-emoji-grid">
<template x-for="emoji in $store.onscreenAgent.emojiList" :key="emoji">
<button
type="button"
class="onscreen-agent-emoji-item"
@click="$store.onscreenAgent.insertEmoji(emoji)"
:title="emoji"
x-text="emoji"
></button>
</template>
</div>
</div>
</div>
</template>

<template x-teleport="body">
<div class="space-dialog-layer">
<dialog
Expand Down
170 changes: 170 additions & 0 deletions app/L0/_all/mod/_core/onscreen_agent/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,16 @@ const model = {
hiddenEdge: "",
shouldCenterInitialPosition: false,

// Emoji picker state
emojiPickerAnchor: null,
emojiPickerPosition: { left: 12, maxHeight: 320, top: 12 },
isEmojiPickerVisible: false,

// STT / mic state
isRecording: false,
sttSupported: !!(window.SpeechRecognition || window.webkitSpeechRecognition),
_sttRecognition: null,

get composerPlaceholder() {
const statusText = typeof this.status === "string" ? this.status.trim() : "";

Expand Down Expand Up @@ -1520,6 +1530,46 @@ const model = {
};
},

get isEmojiPickerOpen() {
return Boolean(this.emojiPickerAnchor);
},

get emojiPickerStyle() {
return {
left: `${this.emojiPickerPosition.left}px`,
maxHeight: `${this.emojiPickerPosition.maxHeight}px`,
top: `${this.emojiPickerPosition.top}px`,
pointerEvents: this.isEmojiPickerVisible ? "auto" : "none",
visibility: this.isEmojiPickerVisible ? "visible" : "hidden"
};
},

get emojiList() {
return [
// Smileys & emotion
"😀", "😂", "🥲", "😊", "😍", "🤩", "😎", "😏", "🤔", "😮",
"😴", "🥹", "😭", "😤", "😡", "🤯", "🥺", "😈", "👀", "🫡",
"😅", "😬", "🤐", "😐", "🙃", "😇", "🤗", "🫠", "😶", "🫤",
// Hearts & affection
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "💕", "💞",
"💓", "💗", "💖", "💝", "🫶", "❤️‍🔥",
// Gestures & people
"👍", "👎", "👏", "🙌", "🤝", "🫂", "💪", "✌️", "🤙", "👌",
"🫰", "👋", "🙏", "🤜", "🤛", "☝️", "🫵",
// Nature & animals
"🌟", "✨", "🌈", "🌸", "🌺", "🍀", "🌻", "🌙", "☀️", "🌊",
"🌿", "🦋", "🐈", "🦊", "🐻", "🌵", "🐝", "🦄", "🐉", "🌴",
// Food & drink
"🍕", "🍔", "🍣", "🍜", "🍦", "🎂", "☕", "🍺", "🥂", "🍷",
"🧃", "🍓", "🍩", "🧁", "🌮",
// Objects & activities
"💻", "📱", "🎮", "🎵", "🎸", "🏆", "🎯", "🎲", "📚", "💡",
"🔥", "💫", "🎉", "🎊", "🛸", "⚡", "🎤", "🎨", "🚀", "🌐",
// Symbols
"✅", "❌", "⚠️", "💯", "🆗", "🔔", "💬", "💤", "♾️", "🔑"
];
},

get isComposerInputDisabled() {
return !this.isInitialized || this.isCompactingHistory;
},
Expand Down Expand Up @@ -4170,6 +4220,126 @@ const model = {
this.composerActionMenuPosition = createComposerActionMenuPosition();
},

closeEmojiPicker() {
this.emojiPickerAnchor = null;
this.isEmojiPickerVisible = false;
this.emojiPickerPosition = { left: 12, maxHeight: 320, top: 12 };
},

openEmojiPicker(anchor) {
this.emojiPickerAnchor = anchor || null;
this.isEmojiPickerVisible = false;
const capturedAnchor = this.emojiPickerAnchor;

globalThis.requestAnimationFrame(() => {
if (!this.isEmojiPickerOpen || this.emojiPickerAnchor !== capturedAnchor) {
return;
}

this.positionEmojiPicker();

globalThis.requestAnimationFrame(() => {
if (!this.isEmojiPickerOpen || this.emojiPickerAnchor !== capturedAnchor) {
return;
}

this.positionEmojiPicker();
this.isEmojiPickerVisible = true;
});
});
},

positionEmojiPicker() {
const picker = document.getElementById("onscreen-agent-emoji-picker");

if (!this.isEmojiPickerOpen || !picker || !this.emojiPickerAnchor) {
return;
}

this.emojiPickerPosition = positionPopover(picker, this.emojiPickerAnchor, {
align: "end",
placement: "top"
});
},

toggleEmojiPicker(event) {
const anchor = event?.currentTarget || null;

if (this.emojiPickerAnchor === anchor) {
this.closeEmojiPicker();
return;
}

this.openEmojiPicker(anchor);
},

insertEmoji(emoji) {
const input = this.refs.input;
const start = input ? (input.selectionStart ?? this.draft.length) : this.draft.length;
const end = input ? (input.selectionEnd ?? this.draft.length) : this.draft.length;
const nextDraft = this.draft.slice(0, start) + emoji + this.draft.slice(end);

this.syncDraft(nextDraft);
this.closeEmojiPicker();

globalThis.requestAnimationFrame(() => {
if (!input) {
return;
}

input.focus();
const nextCursor = start + emoji.length; // .length = UTF-16 units, matches selectionRange
input.setSelectionRange(nextCursor, nextCursor);
});
},

handleMicClick() {
if (this.isRecording) {
this._sttRecognition?.stop();
this.isRecording = false;
return;
}

const SpeechRecognitionClass = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognitionClass) {
return;
}

const recognition = new SpeechRecognitionClass();
recognition.continuous = false;
recognition.interimResults = false;
recognition.lang = navigator.language || "en-US";

recognition.onresult = (event) => {
const transcript = String(event?.results?.[0]?.[0]?.transcript || "").trim();

if (!transcript) {
return;
}

const separator = this.draft && !this.draft.endsWith(" ") ? " " : "";
this.syncDraft(this.draft + separator + transcript);
};

recognition.onend = () => {
this.isRecording = false;
};

recognition.onerror = () => {
this.isRecording = false;
};

this._sttRecognition = recognition;

try {
recognition.start();
this.isRecording = true;
} catch {
this._sttRecognition = null;
}
},

openComposerActionMenu(anchor) {
this.composerActionMenuAnchor = anchor || null;
this.composerActionMenuRenderToken += 1;
Expand Down