Add the agent scaffold, mobile web UI, voice auto-leave, and text notifications
This commit is contained in:
+108
-3
@@ -45,6 +45,7 @@ const state = {
|
||||
takeover: localStorage.getItem("veskslop-takeover") === "1",
|
||||
consoleOpen: false,
|
||||
consoleTab: "events",
|
||||
mobileTab: localStorage.getItem("veskslop-mobile-tab") ?? "chat",
|
||||
voiceReloadAt: 0
|
||||
};
|
||||
|
||||
@@ -419,9 +420,10 @@ async function loadDms() {
|
||||
} catch {
|
||||
state.dms = [];
|
||||
}
|
||||
renderDirect();
|
||||
renderDmRail();
|
||||
renderRightDms();
|
||||
renderRail();
|
||||
updateMobileBadge();
|
||||
}
|
||||
|
||||
async function loadVoice() {
|
||||
@@ -529,6 +531,7 @@ async function selectGuild(guildId) {
|
||||
async function selectChannel(channelId, guildId) {
|
||||
if (guildId && guildId !== state.guildId) state.guildId = guildId;
|
||||
|
||||
const switched = Boolean(state.channelId) && state.channelId !== channelId;
|
||||
state.channelId = channelId;
|
||||
state.channel = findGuildChannel(channelId);
|
||||
|
||||
@@ -544,6 +547,10 @@ async function selectChannel(channelId, guildId) {
|
||||
renderComposer();
|
||||
await loadMessages({ refresh: true });
|
||||
await loadVoice();
|
||||
|
||||
if (switched && isMobileLayout()) {
|
||||
setMobileTab(VOICE_TYPES.has(state.channel?.type) ? "call" : "chat");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
@@ -1068,8 +1075,15 @@ function renderMessages() {
|
||||
{ class: "vk-message-head" },
|
||||
el("span", { class: "vk-message-author", text: author }),
|
||||
isAgent ? el("span", { class: "tag tag-outline vk-tag-agent", text: "agent" }) : null,
|
||||
el("span", { class: "vk-mono vk-dim", text: formatTime(message.timestamp) })
|
||||
el("span", { class: "vk-mono vk-dim", text: formatTime(message.timestamp) }),
|
||||
message.editedAt ? el("span", { class: "vk-mono vk-dim", text: "· edited" }) : null
|
||||
),
|
||||
message.replyTo
|
||||
? el("span", {
|
||||
class: "vk-message-reply",
|
||||
text: `↳ replying to ${message.replyTo.authorName ?? message.replyTo.authorId ?? "a message"}`
|
||||
})
|
||||
: null,
|
||||
el("span", { class: "vk-message-body", text: message.content })
|
||||
);
|
||||
|
||||
@@ -1203,6 +1217,46 @@ function renderRightVoice() {
|
||||
if (!root) return;
|
||||
root.replaceChildren();
|
||||
|
||||
// The phone Call tab shows the live call, not just favourites.
|
||||
const callMembers = isMobileLayout() && state.mobileTab === "call" ? (state.voiceSelf?.members ?? []) : [];
|
||||
|
||||
if (callMembers.length) {
|
||||
$("#voice-count").textContent = `${callMembers.length} in call`;
|
||||
const card = el(
|
||||
"div",
|
||||
{ class: "vk-right-group" },
|
||||
el(
|
||||
"div",
|
||||
{ class: "vk-right-group-head" },
|
||||
el("span", { class: "vk-icon", style: "color: var(--color-accent)", html: VK.icon("volume") }),
|
||||
el("span", { class: "vk-name", text: state.voiceSelf?.channelName ?? "call" })
|
||||
),
|
||||
...callMembers.map(member =>
|
||||
el(
|
||||
"div",
|
||||
{ class: "vk-right-row" },
|
||||
avatar(member, { size: "sm" }),
|
||||
el(
|
||||
"span",
|
||||
{ class: "vk-right-name" },
|
||||
el("span", {
|
||||
class: `vk-name ${state.speaking.has(member.id) ? "speaking" : ""}`.trim(),
|
||||
text: member.username ?? member.id
|
||||
}),
|
||||
el("span", {
|
||||
class: "vk-sub",
|
||||
text: member.id === state.user?.id ? "this client" : member.status ?? ""
|
||||
})
|
||||
),
|
||||
el("span", { class: "vk-right-trail" }, voiceMarks(member))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
root.append(card);
|
||||
return;
|
||||
}
|
||||
|
||||
const inCall = state.favouriteUsers.filter(user => user.channelId);
|
||||
$("#voice-count").textContent = inCall.length ? `${inCall.length} in call` : "";
|
||||
|
||||
@@ -1403,6 +1457,48 @@ function setConsoleTab(tab) {
|
||||
if (tab === "transcript") void refreshTranscript();
|
||||
}
|
||||
|
||||
/* Mobile layout (1c): one pane at a time behind a four-tab bar. */
|
||||
|
||||
const MOBILE_QUERY = window.matchMedia("(max-width: 768px)");
|
||||
const MOBILE_TABS = new Set(["users", "chat", "call", "activity"]);
|
||||
|
||||
function isMobileLayout() {
|
||||
return MOBILE_QUERY.matches;
|
||||
}
|
||||
|
||||
function setMobileTab(tab) {
|
||||
if (!MOBILE_TABS.has(tab)) return;
|
||||
|
||||
state.mobileTab = tab;
|
||||
localStorage.setItem("veskslop-mobile-tab", tab);
|
||||
|
||||
for (const button of $$(".vk-tab")) button.classList.toggle("active", button.dataset.mobileTab === tab);
|
||||
if (isMobileLayout()) $(".vk-app").dataset.mobileTab = tab;
|
||||
renderRightVoice();
|
||||
updateMobileBadge();
|
||||
}
|
||||
|
||||
function syncMobileLayout() {
|
||||
const app = $(".vk-app");
|
||||
if (!app) return;
|
||||
|
||||
if (isMobileLayout()) app.dataset.mobileTab = state.mobileTab;
|
||||
else delete app.dataset.mobileTab;
|
||||
|
||||
for (const button of $$(".vk-tab")) {
|
||||
button.classList.toggle("active", button.dataset.mobileTab === state.mobileTab);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMobileBadge() {
|
||||
const badgeEl = $("#mobile-chat-badge");
|
||||
if (!badgeEl) return;
|
||||
|
||||
const total = state.dms.reduce((sum, dm) => sum + ((dm.unread ?? 0) || (dm.mentions ?? 0)), 0);
|
||||
badgeEl.hidden = total === 0;
|
||||
badgeEl.textContent = total > 99 ? "99+" : String(total);
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
|
||||
function openSettings() {
|
||||
@@ -2175,7 +2271,16 @@ function init() {
|
||||
tab.addEventListener("click", () => setConsoleTab(tab.dataset.consoleTab));
|
||||
}
|
||||
|
||||
$("#raw-events").addEventListener("click", () => setConsoleTab("events"));
|
||||
for (const tab of $$(".vk-tab")) {
|
||||
tab.addEventListener("click", () => setMobileTab(tab.dataset.mobileTab));
|
||||
}
|
||||
MOBILE_QUERY.addEventListener("change", syncMobileLayout);
|
||||
syncMobileLayout();
|
||||
|
||||
$("#raw-events").addEventListener("click", () => {
|
||||
setConsoleTab("events");
|
||||
if (isMobileLayout()) setMobileTab("activity");
|
||||
});
|
||||
$("#transcript-refresh").addEventListener("click", () => void refreshTranscript());
|
||||
$("#channel-refresh").addEventListener("click", () => void loadMessages({ refresh: true }));
|
||||
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@ const ICON_PATHS = {
|
||||
warning: '<path d="M8 2 1.5 13.5h13z"/><path d="M8 6.5v3.5M8 12v.5"/>',
|
||||
refresh: '<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9"/><path d="M13.5 2.5V6H10"/>',
|
||||
play: '<path d="M5 3.5 12.5 8 5 12.5z"/>',
|
||||
microphone: '<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/>'
|
||||
microphone: '<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/>',
|
||||
users: '<circle cx="6" cy="5.5" r="2.5"/><path d="M1.5 13.5c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"/><path d="M11 3.5a2.5 2.5 0 0 1 0 5"/>',
|
||||
activity: '<path d="M1.5 8h3l2-4.5 2.5 9L11 8h3.5"/>'
|
||||
};
|
||||
|
||||
const FILLED = new Set(["star"]);
|
||||
|
||||
+23
-2
@@ -47,7 +47,8 @@
|
||||
type="button"
|
||||
title="Play the call in this browser"
|
||||
>
|
||||
Client audio
|
||||
<span class="vk-icon" data-icon="speaker"></span>
|
||||
<span class="vk-btn-text">Client audio</span>
|
||||
</button>
|
||||
<label class="vk-takeover" title="Read-only unless takeover is on">
|
||||
<input type="checkbox" id="takeover" />
|
||||
@@ -56,7 +57,7 @@
|
||||
</label>
|
||||
<button id="settings-open" class="btn btn-secondary vk-settings" type="button">
|
||||
<span class="vk-icon" data-icon="gear"></span>
|
||||
Settings
|
||||
<span class="vk-btn-text">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -222,6 +223,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav id="mobile-tabs" class="vk-tabbar" aria-label="Sections">
|
||||
<button class="vk-tab" data-mobile-tab="users" type="button">
|
||||
<span class="vk-icon" data-icon="users"></span>
|
||||
<span>Users</span>
|
||||
</button>
|
||||
<button class="vk-tab" data-mobile-tab="chat" type="button">
|
||||
<span class="vk-icon" data-icon="hash"></span>
|
||||
<span>Chat</span>
|
||||
<span id="mobile-chat-badge" class="vk-rail-badge unread" hidden></span>
|
||||
</button>
|
||||
<button class="vk-tab" data-mobile-tab="call" type="button">
|
||||
<span class="vk-icon" data-icon="volume"></span>
|
||||
<span>Call</span>
|
||||
</button>
|
||||
<button class="vk-tab" data-mobile-tab="activity" type="button">
|
||||
<span class="vk-icon" data-icon="activity"></span>
|
||||
<span>Activity</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="settings-overlay" class="vk-overlay" hidden>
|
||||
|
||||
+177
-3
@@ -835,6 +835,11 @@ button {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vk-message-reply {
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
.vk-message-image {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
@@ -1723,15 +1728,184 @@ a.vk-attachment:hover {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.vk-body {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
:root {
|
||||
--vk-header-h: 46px;
|
||||
}
|
||||
|
||||
.vk-rail {
|
||||
.vk-app {
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.vk-header {
|
||||
gap: 8px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.vk-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-health {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#health-uptime,
|
||||
#health-bridge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-takeover-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-scope-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.vk-guild-picker {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vk-guild-button {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#guild-picker-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vk-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
||||
.vk-rail,
|
||||
.vk-center,
|
||||
.vk-right,
|
||||
.vk-console {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="users"] .vk-rail {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="chat"] .vk-center {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="call"] .vk-right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="activity"] .vk-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="activity"] .vk-console {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="activity"] .vk-console-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="activity"] #console-toggle,
|
||||
.vk-app[data-mobile-tab="activity"] .vk-ticker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vk-app[data-mobile-tab="activity"] .vk-console-drawer {
|
||||
display: flex !important;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.vk-settings-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vk-tts-field,
|
||||
.vk-tts-meta .btn,
|
||||
.vk-tts .vk-select {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.vk-send,
|
||||
.vk-composer .vk-icon-btn {
|
||||
min-height: 38px;
|
||||
min-width: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile tab bar (1c) */
|
||||
|
||||
.vk-tabbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.vk-tabbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
flex: none;
|
||||
padding: 6px 8px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: var(--color-surface);
|
||||
box-shadow: inset 0 1px 0 var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.vk-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 44px;
|
||||
padding: 7px 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--color-neutral-500);
|
||||
font: 400 10px/1 var(--font-body);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vk-tab .vk-icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.vk-tab.active {
|
||||
color: var(--color-accent);
|
||||
background: var(--color-accent-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent-700);
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vk-tab .vk-rail-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: calc(50% - 26px);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user