mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 04:01:05 +00:00
feat(qa-lab): add Clawfather/Claw avatars and live-watch mode for scenario runs
This commit is contained in:
@@ -90,6 +90,7 @@ export async function createQaLabApp(root: HTMLDivElement) {
|
||||
|
||||
let lastFingerprint = "";
|
||||
let renderDeferred = false;
|
||||
let previousRunnerStatus: string | null = null;
|
||||
|
||||
function stateFingerprint(): string {
|
||||
const msgs = state.snapshot?.messages;
|
||||
@@ -161,6 +162,14 @@ export async function createQaLabApp(root: HTMLDivElement) {
|
||||
state.error = formatErrorMessage(error);
|
||||
}
|
||||
|
||||
/* Auto-switch to chat when a run starts so user can watch live */
|
||||
const currentRunnerStatus = state.bootstrap?.runner.status ?? null;
|
||||
if (currentRunnerStatus === "running" && previousRunnerStatus !== "running") {
|
||||
state.activeTab = "chat";
|
||||
chatScrollLocked = true;
|
||||
}
|
||||
previousRunnerStatus = currentRunnerStatus;
|
||||
|
||||
/* Only re-render when data actually changed; defer if a <select> is open */
|
||||
const fp = stateFingerprint();
|
||||
if (fp !== lastFingerprint) {
|
||||
|
||||
@@ -782,6 +782,36 @@ select {
|
||||
background: var(--bg-inset);
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--danger);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--danger);
|
||||
animation: live-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes live-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-thread-chips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -848,15 +878,14 @@ select {
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -879,12 +908,17 @@ select {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.msg-direction {
|
||||
.msg-role {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.msg-direction {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,17 +198,6 @@ function esc(text: string) {
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
"#7c6cff",
|
||||
"#f59e0b",
|
||||
"#34d399",
|
||||
"#f87171",
|
||||
"#60a5fa",
|
||||
"#a78bfa",
|
||||
"#fb923c",
|
||||
"#e879f9",
|
||||
];
|
||||
|
||||
const MOCK_MODELS: RunnerModelOption[] = [
|
||||
{
|
||||
key: "mock-openai/gpt-5.4",
|
||||
@@ -226,18 +215,6 @@ const MOCK_MODELS: RunnerModelOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function avatarColor(name: string): string {
|
||||
let h = 0;
|
||||
for (const ch of name) {
|
||||
h = (h * 31 + ch.charCodeAt(0)) | 0;
|
||||
}
|
||||
return AVATAR_COLORS[Math.abs(h) % AVATAR_COLORS.length];
|
||||
}
|
||||
|
||||
function avatarInitial(name: string): string {
|
||||
return (name[0] ?? "?").toUpperCase();
|
||||
}
|
||||
|
||||
export function deriveSelectedConversation(state: UiState): string | null {
|
||||
return state.selectedConversationId ?? state.snapshot?.conversations[0]?.id ?? null;
|
||||
}
|
||||
@@ -579,6 +556,7 @@ function renderChatView(state: UiState): string {
|
||||
<div class="chat-channel-header">
|
||||
<span class="chat-channel-name">${esc(activeConversation?.title || selectedConv || "No conversation")}</span>
|
||||
${activeConversation ? `<span class="chat-channel-kind">${activeConversation.kind}</span>` : ""}
|
||||
${state.bootstrap?.runner.status === "running" ? '<span class="live-indicator"><span class="live-dot"></span>LIVE</span>' : ""}
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
@@ -612,12 +590,17 @@ function renderChatView(state: UiState): string {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function messageAvatar(m: Message): { emoji: string; bg: string; role: string } {
|
||||
if (m.direction === "outbound") {
|
||||
return { emoji: "\uD83E\uDD80", bg: "#7c6cff", role: "Claw" }; // 🦀
|
||||
}
|
||||
return { emoji: "\uD83E\uDD9E", bg: "#d97706", role: "Clawfather" }; // 🦞
|
||||
}
|
||||
|
||||
function renderMessage(m: Message): string {
|
||||
const name = m.senderName || m.senderId;
|
||||
const color = avatarColor(name);
|
||||
const initial = avatarInitial(name);
|
||||
const avatar = messageAvatar(m);
|
||||
const dirClass = m.direction === "inbound" ? "msg-direction-inbound" : "msg-direction-outbound";
|
||||
const dirLabel = m.direction === "inbound" ? "user" : "bot";
|
||||
|
||||
const metaTags: string[] = [];
|
||||
if (m.threadId) {
|
||||
@@ -637,11 +620,12 @@ function renderMessage(m: Message): string {
|
||||
|
||||
return `
|
||||
<div class="msg msg-${m.direction}">
|
||||
<div class="msg-avatar" style="background:${color}">${initial}</div>
|
||||
<div class="msg-avatar" style="background:${avatar.bg}">${avatar.emoji}</div>
|
||||
<div class="msg-body">
|
||||
<div class="msg-header">
|
||||
<span class="msg-sender">${esc(name)}</span>
|
||||
<span class="msg-direction ${dirClass}">${dirLabel}</span>
|
||||
<span class="msg-role">${esc(avatar.role)}</span>
|
||||
<span class="msg-direction ${dirClass}">${m.direction === "inbound" ? "\u2B06" : "\u2B07"}</span>
|
||||
<span class="msg-time">${formatTime(m.timestamp)}</span>
|
||||
</div>
|
||||
<div class="msg-text">${esc(m.text)}</div>
|
||||
|
||||
Reference in New Issue
Block a user