Add the agent scaffold, mobile web UI, voice auto-leave, and text notifications

This commit is contained in:
Fisto
2026-09-14 16:38:14 +02:00
parent c7aad7547f
commit eab94132bb
27 changed files with 1521 additions and 73 deletions
+108 -3
View File
@@ -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 }));