tracking: remove memorystore, ux: change streaming area, more agent markdown formating.
This commit is contained in:
@@ -36,6 +36,9 @@ coverage.xml
|
|||||||
# FastAPI/Uvicorn/runtime files
|
# FastAPI/Uvicorn/runtime files
|
||||||
*.log
|
*.log
|
||||||
*.pid
|
*.pid
|
||||||
|
data/
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite3-*
|
||||||
|
|
||||||
# Frontend dependencies and build artifacts
|
# Frontend dependencies and build artifacts
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -61,3 +61,19 @@ def test_runtime_context_includes_uex_user_identity(tmp_path):
|
|||||||
assert "timezone: America/New_York" in context
|
assert "timezone: America/New_York" in context
|
||||||
assert "specializations: trading,hauling" in context
|
assert "specializations: trading,hauling" in context
|
||||||
assert "hudson@example.test" not in context
|
assert "hudson@example.test" not in context
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_metrics_include_reading_and_writing_rates():
|
||||||
|
metrics = OllamaAgent._stream_metrics(
|
||||||
|
{
|
||||||
|
"prompt_eval_count": 20,
|
||||||
|
"prompt_eval_duration": 2_000_000_000,
|
||||||
|
"eval_count": 30,
|
||||||
|
"eval_duration": 3_000_000_000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert metrics["reading_tokens"] == 20
|
||||||
|
assert metrics["reading_tokens_per_second"] == 10
|
||||||
|
assert metrics["writing_tokens"] == 30
|
||||||
|
assert metrics["writing_tokens_per_second"] == 10
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ class OllamaAgent:
|
|||||||
yield {"type": "token", "content": chunk}
|
yield {"type": "token", "content": chunk}
|
||||||
if message.get("tool_calls"):
|
if message.get("tool_calls"):
|
||||||
tool_calls.extend(message["tool_calls"])
|
tool_calls.extend(message["tool_calls"])
|
||||||
|
if event.get("done"):
|
||||||
|
metrics = self._stream_metrics(event)
|
||||||
|
if metrics:
|
||||||
|
yield {"type": "metrics", **metrics}
|
||||||
|
|
||||||
if not tool_calls:
|
if not tool_calls:
|
||||||
self.messages.append(assistant_message)
|
self.messages.append(assistant_message)
|
||||||
@@ -266,6 +270,25 @@ class OllamaAgent:
|
|||||||
}
|
}
|
||||||
return labels.get(name, f"Running {name}")
|
return labels.get(name, f"Running {name}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _stream_metrics(event: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
prompt_tokens = int(event.get("prompt_eval_count") or 0)
|
||||||
|
prompt_duration = int(event.get("prompt_eval_duration") or 0)
|
||||||
|
output_tokens = int(event.get("eval_count") or 0)
|
||||||
|
output_duration = int(event.get("eval_duration") or 0)
|
||||||
|
|
||||||
|
def rate(tokens: int, duration_ns: int) -> float | None:
|
||||||
|
if not tokens or not duration_ns:
|
||||||
|
return None
|
||||||
|
return tokens / (duration_ns / 1_000_000_000)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"reading_tokens": prompt_tokens,
|
||||||
|
"reading_tokens_per_second": rate(prompt_tokens, prompt_duration),
|
||||||
|
"writing_tokens": output_tokens,
|
||||||
|
"writing_tokens_per_second": rate(output_tokens, output_duration),
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _profile_identity(profile: dict[str, Any]) -> str:
|
def _profile_identity(profile: dict[str, Any]) -> str:
|
||||||
user = profile.get("uex_user")
|
user = profile.get("uex_user")
|
||||||
|
|||||||
+205
-41
@@ -3,7 +3,6 @@ const input = document.getElementById("message-input");
|
|||||||
const messages = document.getElementById("messages");
|
const messages = document.getElementById("messages");
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
const pendingEl = document.getElementById("pending-actions");
|
const pendingEl = document.getElementById("pending-actions");
|
||||||
const activityEl = document.getElementById("activity");
|
|
||||||
const warningEl = document.getElementById("warning");
|
const warningEl = document.getElementById("warning");
|
||||||
const memoryInspectorEl = document.getElementById("memory-inspector");
|
const memoryInspectorEl = document.getElementById("memory-inspector");
|
||||||
const memoryRefreshButton = document.getElementById("memory-refresh");
|
const memoryRefreshButton = document.getElementById("memory-refresh");
|
||||||
@@ -21,49 +20,212 @@ function addMessage(role, text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setMessageMarkdown(node, text) {
|
function setMessageMarkdown(node, text) {
|
||||||
node.innerHTML = renderMarkdown(text);
|
const body = node.querySelector(".message-body") || node;
|
||||||
|
body.innerHTML = renderMarkdown(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMessageActivity(node, text, active = false) {
|
||||||
|
const activity = node.querySelector(".message-activity");
|
||||||
|
if (!activity) return;
|
||||||
|
const phase = activity.querySelector(".message-phase");
|
||||||
|
phase.innerHTML = "";
|
||||||
|
if (text) {
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.textContent = text;
|
||||||
|
phase.appendChild(label);
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
const dots = document.createElement("span");
|
||||||
|
dots.className = "working-dots";
|
||||||
|
dots.innerHTML = "<i></i><i></i><i></i>";
|
||||||
|
phase.appendChild(dots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMessageMetrics(node, metrics) {
|
||||||
|
const metricsEl = node.querySelector(".message-metrics");
|
||||||
|
if (!metricsEl) return;
|
||||||
|
metricsEl.textContent = metrics || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStreamingChrome(node) {
|
||||||
|
if (node.querySelector(".message-activity")) return;
|
||||||
|
node.innerHTML = "";
|
||||||
|
const activity = document.createElement("div");
|
||||||
|
activity.className = "message-activity";
|
||||||
|
const phase = document.createElement("span");
|
||||||
|
phase.className = "message-phase";
|
||||||
|
const metrics = document.createElement("span");
|
||||||
|
metrics.className = "message-metrics";
|
||||||
|
const body = document.createElement("div");
|
||||||
|
body.className = "message-body";
|
||||||
|
activity.append(phase, metrics);
|
||||||
|
node.append(activity, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMarkdown(text) {
|
function renderMarkdown(text) {
|
||||||
const escaped = escapeHtml(text);
|
const lines = text.replace(/\r\n/g, "\n").split(/\n/);
|
||||||
const formatted = escaped
|
|
||||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
||||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
||||||
const lines = formatted.split(/\n/);
|
|
||||||
const output = [];
|
const output = [];
|
||||||
let inList = false;
|
let inList = false;
|
||||||
for (const line of lines) {
|
let inOrderedList = false;
|
||||||
const trimmed = line.trim();
|
let inCode = false;
|
||||||
const item = line.match(/^(\s*)-\s+(.+)$/);
|
let codeLines = [];
|
||||||
if (item) {
|
|
||||||
if (!inList) {
|
const closeLists = () => {
|
||||||
output.push("<ul>");
|
|
||||||
inList = true;
|
|
||||||
}
|
|
||||||
const nestedClass = item[1].length >= 2 ? ' class="nested"' : "";
|
|
||||||
output.push(`<li${nestedClass}>${item[2]}</li>`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inList) {
|
if (inList) {
|
||||||
output.push("</ul>");
|
output.push("</ul>");
|
||||||
inList = false;
|
inList = false;
|
||||||
}
|
}
|
||||||
if (/^---+$/.test(trimmed)) {
|
if (inOrderedList) {
|
||||||
|
output.push("</ol>");
|
||||||
|
inOrderedList = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushCode = () => {
|
||||||
|
output.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
||||||
|
codeLines = [];
|
||||||
|
inCode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTableAt = (index) => {
|
||||||
|
if (index + 1 >= lines.length) return false;
|
||||||
|
return isTableRow(lines[index]) && isTableDivider(lines[index + 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < lines.length; index += 1) {
|
||||||
|
const line = lines[index];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (/^```/.test(trimmed)) {
|
||||||
|
if (inCode) {
|
||||||
|
flushCode();
|
||||||
|
} else {
|
||||||
|
closeLists();
|
||||||
|
inCode = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCode) {
|
||||||
|
codeLines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTableAt(index)) {
|
||||||
|
closeLists();
|
||||||
|
const headers = splitTableRow(lines[index]);
|
||||||
|
const aligns = splitTableRow(lines[index + 1]).map((cell) => tableAlignment(cell));
|
||||||
|
const rows = [];
|
||||||
|
index += 2;
|
||||||
|
while (index < lines.length && isTableRow(lines[index])) {
|
||||||
|
rows.push(splitTableRow(lines[index]));
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
index -= 1;
|
||||||
|
output.push(renderTable(headers, aligns, rows));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unorderedItem = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
||||||
|
if (unorderedItem) {
|
||||||
|
if (inOrderedList) {
|
||||||
|
output.push("</ol>");
|
||||||
|
inOrderedList = false;
|
||||||
|
}
|
||||||
|
if (!inList) {
|
||||||
|
output.push("<ul>");
|
||||||
|
inList = true;
|
||||||
|
}
|
||||||
|
const nestedClass = unorderedItem[1].length >= 2 ? ' class="nested"' : "";
|
||||||
|
output.push(`<li${nestedClass}>${inlineMarkdown(unorderedItem[2])}</li>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedItem = line.match(/^(\s*)\d+\.\s+(.+)$/);
|
||||||
|
if (orderedItem) {
|
||||||
|
if (inList) {
|
||||||
|
output.push("</ul>");
|
||||||
|
inList = false;
|
||||||
|
}
|
||||||
|
if (!inOrderedList) {
|
||||||
|
output.push("<ol>");
|
||||||
|
inOrderedList = true;
|
||||||
|
}
|
||||||
|
const nestedClass = orderedItem[1].length >= 2 ? ' class="nested"' : "";
|
||||||
|
output.push(`<li${nestedClass}>${inlineMarkdown(orderedItem[2])}</li>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeLists();
|
||||||
|
|
||||||
|
if (/^>\s?/.test(trimmed)) {
|
||||||
|
output.push(`<blockquote>${inlineMarkdown(trimmed.replace(/^>\s?/, ""))}</blockquote>`);
|
||||||
|
} else if (/^---+$/.test(trimmed)) {
|
||||||
output.push("<hr>");
|
output.push("<hr>");
|
||||||
} else if (/^###\s+/.test(trimmed)) {
|
} else if (/^#{1,6}\s+/.test(trimmed)) {
|
||||||
output.push(`<h3>${trimmed.replace(/^###\s+/, "")}</h3>`);
|
const level = Math.min(4, trimmed.match(/^#+/)[0].length + 2);
|
||||||
} else if (/^##\s+/.test(trimmed)) {
|
output.push(`<h${level}>${inlineMarkdown(trimmed.replace(/^#{1,6}\s+/, ""))}</h${level}>`);
|
||||||
output.push(`<h3>${trimmed.replace(/^##\s+/, "")}</h3>`);
|
|
||||||
} else if (trimmed) {
|
} else if (trimmed) {
|
||||||
output.push(`<p>${line}</p>`);
|
output.push(`<p>${inlineMarkdown(line)}</p>`);
|
||||||
} else {
|
} else {
|
||||||
output.push("<br>");
|
output.push("<br>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (inList) output.push("</ul>");
|
|
||||||
|
if (inCode) flushCode();
|
||||||
|
closeLists();
|
||||||
return output.join("");
|
return output.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inlineMarkdown(text) {
|
||||||
|
return escapeHtml(text)
|
||||||
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
||||||
|
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||||
|
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTableRow(line) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
return trimmed.includes("|") && /^\|?.+\|.+\|?$/.test(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTableDivider(line) {
|
||||||
|
return splitTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitTableRow(line) {
|
||||||
|
return line
|
||||||
|
.trim()
|
||||||
|
.replace(/^\|/, "")
|
||||||
|
.replace(/\|$/, "")
|
||||||
|
.split("|")
|
||||||
|
.map((cell) => cell.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableAlignment(cell) {
|
||||||
|
const trimmed = cell.trim();
|
||||||
|
if (trimmed.startsWith(":") && trimmed.endsWith(":")) return "center";
|
||||||
|
if (trimmed.endsWith(":")) return "right";
|
||||||
|
return "left";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(headers, aligns, rows) {
|
||||||
|
const head = headers
|
||||||
|
.map((cell, index) => `<th style="text-align:${aligns[index] || "left"}">${inlineMarkdown(cell)}</th>`)
|
||||||
|
.join("");
|
||||||
|
const body = rows
|
||||||
|
.map((row) => {
|
||||||
|
const cells = headers
|
||||||
|
.map((_, index) => `<td style="text-align:${aligns[index] || "left"}">${inlineMarkdown(row[index] || "")}</td>`)
|
||||||
|
.join("");
|
||||||
|
return `<tr>${cells}</tr>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
return `<div class="table-wrap"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
@@ -73,18 +235,16 @@ function escapeHtml(text) {
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActivity(text, active = false) {
|
function formatMetrics(event) {
|
||||||
activityEl.innerHTML = "";
|
const read = formatTokenMetric(event.reading_tokens, event.reading_tokens_per_second);
|
||||||
if (!text) return;
|
const wrote = formatTokenMetric(event.writing_tokens, event.writing_tokens_per_second);
|
||||||
const label = document.createElement("span");
|
return [read && `read ${read}`, wrote && `wrote ${wrote}`].filter(Boolean).join(" | ");
|
||||||
label.textContent = text;
|
}
|
||||||
activityEl.appendChild(label);
|
|
||||||
if (active) {
|
function formatTokenMetric(tokens, speed) {
|
||||||
const dots = document.createElement("span");
|
if (!tokens) return "";
|
||||||
dots.className = "working-dots";
|
const speedText = typeof speed === "number" ? ` @ ${speed.toFixed(1)}/s` : "";
|
||||||
dots.innerHTML = "<i></i><i></i><i></i>";
|
return `${tokens} tok${speedText}`;
|
||||||
activityEl.appendChild(dots);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWarning(text) {
|
function setWarning(text) {
|
||||||
@@ -277,9 +437,11 @@ async function sendMessage() {
|
|||||||
input.disabled = true;
|
input.disabled = true;
|
||||||
addMessage("user", message);
|
addMessage("user", message);
|
||||||
const assistantNode = addMessage("assistant streaming", "");
|
const assistantNode = addMessage("assistant streaming", "");
|
||||||
|
ensureStreamingChrome(assistantNode);
|
||||||
let assistantText = "";
|
let assistantText = "";
|
||||||
statusEl.textContent = "Working";
|
statusEl.textContent = "Working";
|
||||||
setActivity("Thinking", true);
|
setMessageActivity(assistantNode, "Thinking", true);
|
||||||
|
setMessageMetrics(assistantNode, "");
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/chat/stream", {
|
const response = await fetch("/api/chat/stream", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -303,7 +465,9 @@ async function sendMessage() {
|
|||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
const event = JSON.parse(line.slice(6));
|
const event = JSON.parse(line.slice(6));
|
||||||
if (event.type === "status") {
|
if (event.type === "status") {
|
||||||
setActivity(event.message, true);
|
setMessageActivity(assistantNode, event.message, true);
|
||||||
|
} else if (event.type === "metrics") {
|
||||||
|
setMessageMetrics(assistantNode, formatMetrics(event));
|
||||||
} else if (event.type === "warning") {
|
} else if (event.type === "warning") {
|
||||||
setWarning(event.message);
|
setWarning(event.message);
|
||||||
assistantText += event.message;
|
assistantText += event.message;
|
||||||
@@ -328,7 +492,7 @@ async function sendMessage() {
|
|||||||
input.disabled = false;
|
input.disabled = false;
|
||||||
input.focus();
|
input.focus();
|
||||||
statusEl.textContent = "Ready";
|
statusEl.textContent = "Ready";
|
||||||
setActivity("");
|
setMessageActivity(assistantNode, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
<div class="warning" id="warning" hidden></div>
|
<div class="warning" id="warning" hidden></div>
|
||||||
<div class="messages" id="messages"></div>
|
<div class="messages" id="messages"></div>
|
||||||
<div class="composer-wrap">
|
<div class="composer-wrap">
|
||||||
<div class="activity" id="activity" aria-live="polite"></div>
|
|
||||||
<form class="composer" id="chat-form">
|
<form class="composer" id="chat-form">
|
||||||
<textarea id="message-input" rows="2" placeholder="Search listings, draft a reply, prepare an offer..."></textarea>
|
<textarea id="message-input" rows="2" placeholder="Search listings, draft a reply, prepare an offer..."></textarea>
|
||||||
<button type="submit">Send</button>
|
<button type="submit">Send</button>
|
||||||
|
|||||||
+101
-6
@@ -110,11 +110,16 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message p:last-child,
|
.message p:last-child,
|
||||||
.message ul:last-child {
|
.message ul:last-child,
|
||||||
|
.message ol:last-child,
|
||||||
|
.message blockquote:last-child,
|
||||||
|
.message .table-wrap:last-child,
|
||||||
|
.message pre:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message ul {
|
.message ul,
|
||||||
|
.message ol {
|
||||||
margin: 0 0 10px 20px;
|
margin: 0 0 10px 20px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -138,6 +143,14 @@ h2 {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message h4,
|
||||||
|
.message h5,
|
||||||
|
.message h6 {
|
||||||
|
margin: 10px 0 7px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
.message hr {
|
.message hr {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
@@ -155,6 +168,57 @@ h2 {
|
|||||||
font-size: 0.92em;
|
font-size: 0.92em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message pre {
|
||||||
|
max-height: none;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message pre code {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message blockquote {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
background: rgba(68, 194, 165, 0.08);
|
||||||
|
color: #d7dde4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message a {
|
||||||
|
color: #69d7bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 420px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message th,
|
||||||
|
.message td {
|
||||||
|
padding: 8px 9px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message th {
|
||||||
|
background: #20262d;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.message.user {
|
.message.user {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
background: var(--panel-2);
|
background: var(--panel-2);
|
||||||
@@ -173,13 +237,29 @@ h2 {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity {
|
.message-activity {
|
||||||
min-height: 24px;
|
display: flex;
|
||||||
padding: 7px 16px 0;
|
align-items: center;
|
||||||
color: rgba(151, 161, 173, 0.72);
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: rgba(151, 161, 173, 0.82);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-phase {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-metrics {
|
||||||
|
color: rgba(151, 161, 173, 0.56);
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.working-dots {
|
.working-dots {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -381,3 +461,18 @@ pre {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.composer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-metrics {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user