Collapse high-frequency audio frames in the demo websocket log.
Expandable groups keep protocol and text events readable while still allowing per-frame inspection when debugging audio traffic. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
* as binary websocket messages.
|
* as binary websocket messages.
|
||||||
* - Play `response.audio.delta` frames gaplessly through Web Audio.
|
* - Play `response.audio.delta` frames gaplessly through Web Audio.
|
||||||
* - Render a chat-style history of user transcripts and bot text deltas.
|
* - Render a chat-style history of user transcripts and bot text deltas.
|
||||||
|
* - Collapse high-frequency audio frames into expandable websocket log groups.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SAMPLE_RATE = 16000;
|
const SAMPLE_RATE = 16000;
|
||||||
@@ -16,7 +17,11 @@ const CHANNELS = 1;
|
|||||||
const FRAME_MS = 20;
|
const FRAME_MS = 20;
|
||||||
const PROTOCOL = "va.ws.v1";
|
const PROTOCOL = "va.ws.v1";
|
||||||
const MAX_WS_LOG_LINES = 120;
|
const MAX_WS_LOG_LINES = 120;
|
||||||
const AUDIO_DELTA_LOG_INTERVAL_MS = 1000;
|
const MAX_GROUP_CHILDREN_RENDER = 100;
|
||||||
|
const WS_LOG_GROUP_KEYS = {
|
||||||
|
AUDIO_DELTA: "recv:response.audio.delta",
|
||||||
|
AUDIO_SEND: "send:input.audio",
|
||||||
|
};
|
||||||
|
|
||||||
function defaultWsUrl() {
|
function defaultWsUrl() {
|
||||||
const scheme = location.protocol === "https:" ? "wss:" : "ws:";
|
const scheme = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
@@ -70,13 +75,8 @@ const state = {
|
|||||||
// VU meter smoothing.
|
// VU meter smoothing.
|
||||||
meterLevel: 0,
|
meterLevel: 0,
|
||||||
|
|
||||||
// Compact websocket logging.
|
// Collapsible websocket log groups for high-frequency audio frames.
|
||||||
audioDeltaLogCount: 0,
|
wsLogGroup: null,
|
||||||
audioDeltaLogBytes: 0,
|
|
||||||
lastAudioDeltaLogAt: 0,
|
|
||||||
audioSendLogCount: 0,
|
|
||||||
audioSendLogBytes: 0,
|
|
||||||
lastAudioSendLogAt: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ UI */
|
/* ------------------------------------------------------------------ UI */
|
||||||
@@ -169,6 +169,209 @@ function truncateLogValue(value, maxLength = 160) {
|
|||||||
return `${text.slice(0, maxLength - 1)}…`;
|
return `${text.slice(0, maxLength - 1)}…`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLogTime(date = new Date()) {
|
||||||
|
return date.toLocaleTimeString([], {
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLogBytes(byteCount) {
|
||||||
|
if (byteCount >= 1048576) {
|
||||||
|
return `${(byteCount / 1048576).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
if (byteCount >= 1024) {
|
||||||
|
return `${(byteCount / 1024).toFixed(1)} KB`;
|
||||||
|
}
|
||||||
|
return `${byteCount} bytes`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsLogGroupLabel(groupKey) {
|
||||||
|
if (groupKey === WS_LOG_GROUP_KEYS.AUDIO_DELTA) {
|
||||||
|
return "response.audio.delta";
|
||||||
|
}
|
||||||
|
if (groupKey === WS_LOG_GROUP_KEYS.AUDIO_SEND) {
|
||||||
|
return "input.audio binary";
|
||||||
|
}
|
||||||
|
return "grouped events";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureWsLogReady() {
|
||||||
|
if (els.wsLog.querySelector(".ws-log__empty")) {
|
||||||
|
els.wsLog.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollWsLogToBottom() {
|
||||||
|
els.wsLog.scrollTop = els.wsLog.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimWsLog() {
|
||||||
|
while (els.wsLog.children.length > MAX_WS_LOG_LINES) {
|
||||||
|
const first = els.wsLog.firstElementChild;
|
||||||
|
if (state.wsLogGroup?.element === first) {
|
||||||
|
state.wsLogGroup = null;
|
||||||
|
}
|
||||||
|
first.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeWsLogGroup() {
|
||||||
|
state.wsLogGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWsLogEntry(direction, detail, kind, timeText = formatLogTime()) {
|
||||||
|
const entry = document.createElement("div");
|
||||||
|
entry.className = `ws-log__entry ws-log__entry--${kind}`;
|
||||||
|
|
||||||
|
const time = document.createElement("span");
|
||||||
|
time.className = "ws-log__time";
|
||||||
|
time.textContent = timeText;
|
||||||
|
|
||||||
|
const dir = document.createElement("span");
|
||||||
|
dir.className = "ws-log__direction";
|
||||||
|
dir.textContent =
|
||||||
|
direction === "send"
|
||||||
|
? "SEND"
|
||||||
|
: direction === "recv"
|
||||||
|
? "RECV"
|
||||||
|
: direction.toUpperCase();
|
||||||
|
|
||||||
|
const body = document.createElement("span");
|
||||||
|
body.className = "ws-log__detail";
|
||||||
|
body.textContent = detail;
|
||||||
|
|
||||||
|
entry.append(time, dir, body);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWsLogGroupSummary(group) {
|
||||||
|
group.summaryEl.textContent = `${wsLogGroupLabel(group.key)} ×${group.count} (${formatLogBytes(group.totalBytes)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendWsLogGroupChildDom(group, item) {
|
||||||
|
const entry = createWsLogEntry(
|
||||||
|
group.direction,
|
||||||
|
item.detail,
|
||||||
|
group.kind,
|
||||||
|
item.time,
|
||||||
|
);
|
||||||
|
entry.classList.add("ws-log__entry--child");
|
||||||
|
group.childrenEl.appendChild(entry);
|
||||||
|
|
||||||
|
const childEntries = group.childrenEl.querySelectorAll(".ws-log__entry");
|
||||||
|
if (childEntries.length > MAX_GROUP_CHILDREN_RENDER) {
|
||||||
|
const omit = group.childrenEl.querySelector(".ws-log__group-omit");
|
||||||
|
if (!omit) {
|
||||||
|
const omitted = document.createElement("div");
|
||||||
|
omitted.className = "ws-log__group-omit";
|
||||||
|
omitted.textContent = "… earlier events omitted";
|
||||||
|
group.childrenEl.insertBefore(omitted, group.childrenEl.firstElementChild);
|
||||||
|
}
|
||||||
|
childEntries[0].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWsLogGroupChildren(group) {
|
||||||
|
group.childrenEl.innerHTML = "";
|
||||||
|
const items = group.items;
|
||||||
|
const start = Math.max(0, items.length - MAX_GROUP_CHILDREN_RENDER);
|
||||||
|
if (start > 0) {
|
||||||
|
const omitted = document.createElement("div");
|
||||||
|
omitted.className = "ws-log__group-omit";
|
||||||
|
omitted.textContent = `… ${start} earlier events omitted`;
|
||||||
|
group.childrenEl.appendChild(omitted);
|
||||||
|
}
|
||||||
|
for (let i = start; i < items.length; i += 1) {
|
||||||
|
appendWsLogGroupChildDom(group, items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWsLogGroup(group) {
|
||||||
|
group.collapsed = !group.collapsed;
|
||||||
|
group.childrenEl.hidden = group.collapsed;
|
||||||
|
group.chevronEl.textContent = group.collapsed ? "▶" : "▼";
|
||||||
|
group.headerEl.setAttribute("aria-expanded", group.collapsed ? "false" : "true");
|
||||||
|
|
||||||
|
if (!group.collapsed && group.childrenEl.childElementCount === 0) {
|
||||||
|
renderWsLogGroupChildren(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendWsLogGroupItem(groupKey, direction, kind, itemDetail, byteCount = 0) {
|
||||||
|
ensureWsLogReady();
|
||||||
|
|
||||||
|
let group = state.wsLogGroup;
|
||||||
|
if (!group || group.key !== groupKey) {
|
||||||
|
finalizeWsLogGroup();
|
||||||
|
|
||||||
|
const groupEl = document.createElement("div");
|
||||||
|
groupEl.className = `ws-log__group ws-log__group--${kind}`;
|
||||||
|
|
||||||
|
const header = document.createElement("button");
|
||||||
|
header.type = "button";
|
||||||
|
header.className = "ws-log__group-header";
|
||||||
|
header.setAttribute("aria-expanded", "false");
|
||||||
|
|
||||||
|
const time = document.createElement("span");
|
||||||
|
time.className = "ws-log__time";
|
||||||
|
time.textContent = formatLogTime();
|
||||||
|
|
||||||
|
const dir = document.createElement("span");
|
||||||
|
dir.className = "ws-log__direction";
|
||||||
|
dir.textContent = direction === "send" ? "SEND" : "RECV";
|
||||||
|
|
||||||
|
const chevron = document.createElement("span");
|
||||||
|
chevron.className = "ws-log__group-chevron";
|
||||||
|
chevron.textContent = "▶";
|
||||||
|
chevron.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
const summary = document.createElement("span");
|
||||||
|
summary.className = "ws-log__group-summary";
|
||||||
|
|
||||||
|
header.append(time, dir, chevron, summary);
|
||||||
|
|
||||||
|
const children = document.createElement("div");
|
||||||
|
children.className = "ws-log__group-children";
|
||||||
|
children.hidden = true;
|
||||||
|
|
||||||
|
groupEl.append(header, children);
|
||||||
|
els.wsLog.appendChild(groupEl);
|
||||||
|
|
||||||
|
group = {
|
||||||
|
key: groupKey,
|
||||||
|
direction,
|
||||||
|
kind,
|
||||||
|
element: groupEl,
|
||||||
|
headerEl: header,
|
||||||
|
chevronEl: chevron,
|
||||||
|
summaryEl: summary,
|
||||||
|
childrenEl: children,
|
||||||
|
collapsed: true,
|
||||||
|
count: 0,
|
||||||
|
totalBytes: 0,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
state.wsLogGroup = group;
|
||||||
|
header.addEventListener("click", () => toggleWsLogGroup(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
group.count += 1;
|
||||||
|
group.totalBytes += byteCount;
|
||||||
|
const item = { time: formatLogTime(), detail: itemDetail };
|
||||||
|
group.items.push(item);
|
||||||
|
updateWsLogGroupSummary(group);
|
||||||
|
|
||||||
|
if (!group.collapsed) {
|
||||||
|
appendWsLogGroupChildDom(group, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
trimWsLog();
|
||||||
|
scrollWsLogToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
function compactWsPayload(payload) {
|
function compactWsPayload(payload) {
|
||||||
if (!payload || typeof payload !== "object") return String(payload);
|
if (!payload || typeof payload !== "object") return String(payload);
|
||||||
const compact = { ...payload };
|
const compact = { ...payload };
|
||||||
@@ -191,85 +394,27 @@ function compactWsPayload(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addWsLog(direction, detail, kind = direction) {
|
function addWsLog(direction, detail, kind = direction) {
|
||||||
if (els.wsLog.querySelector(".ws-log__empty")) {
|
finalizeWsLogGroup();
|
||||||
els.wsLog.innerHTML = "";
|
ensureWsLogReady();
|
||||||
}
|
els.wsLog.appendChild(createWsLogEntry(direction, detail, kind));
|
||||||
|
trimWsLog();
|
||||||
const entry = document.createElement("div");
|
scrollWsLogToBottom();
|
||||||
entry.className = `ws-log__entry ws-log__entry--${kind}`;
|
|
||||||
|
|
||||||
const time = document.createElement("span");
|
|
||||||
time.className = "ws-log__time";
|
|
||||||
time.textContent = new Date().toLocaleTimeString([], {
|
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
});
|
|
||||||
|
|
||||||
const dir = document.createElement("span");
|
|
||||||
dir.className = "ws-log__direction";
|
|
||||||
dir.textContent =
|
|
||||||
direction === "send"
|
|
||||||
? "SEND"
|
|
||||||
: direction === "recv"
|
|
||||||
? "RECV"
|
|
||||||
: direction.toUpperCase();
|
|
||||||
|
|
||||||
const body = document.createElement("span");
|
|
||||||
body.className = "ws-log__detail";
|
|
||||||
body.textContent = detail;
|
|
||||||
|
|
||||||
entry.append(time, dir, body);
|
|
||||||
els.wsLog.appendChild(entry);
|
|
||||||
|
|
||||||
while (els.wsLog.children.length > MAX_WS_LOG_LINES) {
|
|
||||||
els.wsLog.firstElementChild.remove();
|
|
||||||
}
|
|
||||||
els.wsLog.scrollTop = els.wsLog.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushAudioDeltaLog() {
|
|
||||||
if (state.audioDeltaLogCount === 0) return;
|
|
||||||
addWsLog(
|
|
||||||
"recv",
|
|
||||||
`response.audio.delta x${state.audioDeltaLogCount} (${state.audioDeltaLogBytes} bytes)`,
|
|
||||||
);
|
|
||||||
state.audioDeltaLogCount = 0;
|
|
||||||
state.audioDeltaLogBytes = 0;
|
|
||||||
state.lastAudioDeltaLogAt = performance.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushAudioSendLog() {
|
|
||||||
if (state.audioSendLogCount === 0) return;
|
|
||||||
addWsLog(
|
|
||||||
"send",
|
|
||||||
`input.audio binary x${state.audioSendLogCount} (${state.audioSendLogBytes} bytes)`,
|
|
||||||
);
|
|
||||||
state.audioSendLogCount = 0;
|
|
||||||
state.audioSendLogBytes = 0;
|
|
||||||
state.lastAudioSendLogAt = performance.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushPendingWsLogs() {
|
|
||||||
flushAudioDeltaLog();
|
|
||||||
flushAudioSendLog();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logWsPayload(direction, payload) {
|
function logWsPayload(direction, payload) {
|
||||||
if (direction === "send") {
|
|
||||||
flushAudioSendLog();
|
|
||||||
} else {
|
|
||||||
flushAudioDeltaLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === "recv" && payload?.type === "response.audio.delta") {
|
if (direction === "recv" && payload?.type === "response.audio.delta") {
|
||||||
state.audioDeltaLogCount += 1;
|
const bytes = payload.bytes || 0;
|
||||||
state.audioDeltaLogBytes += payload.bytes || payload.audio?.length || 0;
|
const detail =
|
||||||
const now = performance.now();
|
payload.seq != null
|
||||||
if (now - state.lastAudioDeltaLogAt >= AUDIO_DELTA_LOG_INTERVAL_MS) {
|
? `seq=${payload.seq} (${bytes} bytes)`
|
||||||
flushAudioDeltaLog();
|
: `(${bytes} bytes)`;
|
||||||
}
|
appendWsLogGroupItem(
|
||||||
|
WS_LOG_GROUP_KEYS.AUDIO_DELTA,
|
||||||
|
"recv",
|
||||||
|
"recv",
|
||||||
|
detail,
|
||||||
|
bytes,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,12 +422,13 @@ function logWsPayload(direction, payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logBinarySend(byteLength) {
|
function logBinarySend(byteLength) {
|
||||||
state.audioSendLogCount += 1;
|
appendWsLogGroupItem(
|
||||||
state.audioSendLogBytes += byteLength;
|
WS_LOG_GROUP_KEYS.AUDIO_SEND,
|
||||||
const now = performance.now();
|
"send",
|
||||||
if (now - state.lastAudioSendLogAt >= AUDIO_DELTA_LOG_INTERVAL_MS) {
|
"send",
|
||||||
flushAudioSendLog();
|
`(${byteLength} bytes)`,
|
||||||
}
|
byteLength,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wsSend(data) {
|
function wsSend(data) {
|
||||||
@@ -292,8 +438,6 @@ function wsSend(data) {
|
|||||||
try {
|
try {
|
||||||
logWsPayload("send", JSON.parse(data));
|
logWsPayload("send", JSON.parse(data));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
flushAudioSendLog();
|
|
||||||
flushAudioDeltaLog();
|
|
||||||
addWsLog("send", truncateLogValue(data));
|
addWsLog("send", truncateLogValue(data));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -313,10 +457,7 @@ function wsSend(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearWsLog() {
|
function clearWsLog() {
|
||||||
state.audioDeltaLogCount = 0;
|
state.wsLogGroup = null;
|
||||||
state.audioDeltaLogBytes = 0;
|
|
||||||
state.audioSendLogCount = 0;
|
|
||||||
state.audioSendLogBytes = 0;
|
|
||||||
els.wsLog.innerHTML =
|
els.wsLog.innerHTML =
|
||||||
'<div class="ws-log__empty">No websocket events yet.</div>';
|
'<div class="ws-log__empty">No websocket events yet.</div>';
|
||||||
}
|
}
|
||||||
@@ -450,7 +591,6 @@ function stopMic() {
|
|||||||
state.micEnabled = false;
|
state.micEnabled = false;
|
||||||
updateMeter(0);
|
updateMeter(0);
|
||||||
if (wasEnabled) {
|
if (wasEnabled) {
|
||||||
flushAudioSendLog();
|
|
||||||
addWsLog("system", "mic capture stopped");
|
addWsLog("system", "mic capture stopped");
|
||||||
}
|
}
|
||||||
setMicButton();
|
setMicButton();
|
||||||
@@ -752,7 +892,7 @@ async function connect() {
|
|||||||
setMicSelectEnabled();
|
setMicSelectEnabled();
|
||||||
setComposerEnabled(false);
|
setComposerEnabled(false);
|
||||||
setBotIndicator(false);
|
setBotIndicator(false);
|
||||||
flushPendingWsLogs();
|
finalizeWsLogGroup();
|
||||||
addWsLog(
|
addWsLog(
|
||||||
"system",
|
"system",
|
||||||
`websocket close code=${event.code}${
|
`websocket close code=${event.code}${
|
||||||
|
|||||||
@@ -405,6 +405,79 @@ body {
|
|||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ws-log__group {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 58px 42px 14px minmax(0, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
align-items: start;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 4px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-header:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-chevron {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.6;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-summary {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-children {
|
||||||
|
margin: 0 0 4px 18px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__entry--child {
|
||||||
|
grid-template-columns: 58px 42px minmax(0, 1fr);
|
||||||
|
opacity: 0.85;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group-omit {
|
||||||
|
padding: 4px 4px 4px 0;
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group--send .ws-log__direction {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-log__group--recv .ws-log__direction {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
/* Controls -------------------------------------------------------------- */
|
/* Controls -------------------------------------------------------------- */
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
@@ -720,10 +793,15 @@ body {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-log__entry {
|
.ws-log__entry,
|
||||||
|
.ws-log__group-header {
|
||||||
grid-template-columns: 54px 38px minmax(0, 1fr);
|
grid-template-columns: 54px 38px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ws-log__group-header {
|
||||||
|
grid-template-columns: 54px 38px 12px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user