feat: add agent avatar support (#1329) (thanks @dlauer)
This commit is contained in:
@@ -91,6 +91,7 @@
|
||||
|
||||
/* Image avatar support */
|
||||
img.chat-avatar {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import { generateUUID } from "./uuid";
|
||||
import { resetToolStream } from "./app-tool-stream";
|
||||
import { scheduleChatScroll } from "./app-scroll";
|
||||
import { setLastActiveSessionKey } from "./app-settings";
|
||||
import { normalizeBasePath } from "./navigation";
|
||||
import type { GatewayHelloOk } from "./gateway";
|
||||
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
|
||||
import type { ClawdbotApp } from "./app";
|
||||
|
||||
type ChatHost = {
|
||||
@@ -13,6 +16,9 @@ type ChatHost = {
|
||||
chatRunId: string | null;
|
||||
chatSending: boolean;
|
||||
sessionKey: string;
|
||||
basePath: string;
|
||||
hello: GatewayHelloOk | null;
|
||||
chatAvatarUrl: string | null;
|
||||
};
|
||||
|
||||
export function isChatBusy(host: ChatHost) {
|
||||
@@ -124,8 +130,53 @@ export async function refreshChat(host: ChatHost) {
|
||||
await Promise.all([
|
||||
loadChatHistory(host as unknown as ClawdbotApp),
|
||||
loadSessions(host as unknown as ClawdbotApp),
|
||||
refreshChatAvatar(host),
|
||||
]);
|
||||
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
|
||||
}
|
||||
|
||||
export const flushChatQueueForEvent = flushChatQueue;
|
||||
|
||||
type SessionDefaultsSnapshot = {
|
||||
defaultAgentId?: string;
|
||||
};
|
||||
|
||||
function resolveAgentIdForSession(host: ChatHost): string | null {
|
||||
const parsed = parseAgentSessionKey(host.sessionKey);
|
||||
if (parsed?.agentId) return parsed.agentId;
|
||||
const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;
|
||||
const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();
|
||||
return fallback || "main";
|
||||
}
|
||||
|
||||
function buildAvatarMetaUrl(basePath: string, agentId: string): string {
|
||||
const base = normalizeBasePath(basePath);
|
||||
const encoded = encodeURIComponent(agentId);
|
||||
return base ? `${base}/avatar/${encoded}?meta=1` : `/avatar/${encoded}?meta=1`;
|
||||
}
|
||||
|
||||
export async function refreshChatAvatar(host: ChatHost) {
|
||||
if (!host.connected) {
|
||||
host.chatAvatarUrl = null;
|
||||
return;
|
||||
}
|
||||
const agentId = resolveAgentIdForSession(host);
|
||||
if (!agentId) {
|
||||
host.chatAvatarUrl = null;
|
||||
return;
|
||||
}
|
||||
host.chatAvatarUrl = null;
|
||||
const url = buildAvatarMetaUrl(host.basePath, agentId);
|
||||
try {
|
||||
const res = await fetch(url, { method: "GET" });
|
||||
if (!res.ok) {
|
||||
host.chatAvatarUrl = null;
|
||||
return;
|
||||
}
|
||||
const data = (await res.json()) as { avatarUrl?: unknown };
|
||||
const avatarUrl = typeof data.avatarUrl === "string" ? data.avatarUrl.trim() : "";
|
||||
host.chatAvatarUrl = avatarUrl || null;
|
||||
} catch {
|
||||
host.chatAvatarUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
StatusSummary,
|
||||
} from "./types";
|
||||
import type { ChatQueueItem, CronFormState } from "./ui-types";
|
||||
import { refreshChatAvatar } from "./app-chat";
|
||||
import { renderChat } from "./views/chat";
|
||||
import { renderConfig } from "./views/config";
|
||||
import { renderChannels } from "./views/channels";
|
||||
@@ -413,6 +414,7 @@ export function renderApp(state: AppViewState) {
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking: state.settings.chatShowThinking,
|
||||
@@ -422,6 +424,7 @@ export function renderApp(state: AppViewState) {
|
||||
toolMessages: state.chatToolMessages,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
assistantAvatarUrl: state.chatAvatarUrl,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
@@ -432,7 +435,7 @@ export function renderApp(state: AppViewState) {
|
||||
focusMode: state.settings.chatFocusMode,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return loadChatHistory(state);
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () =>
|
||||
state.applySettings({
|
||||
|
||||
@@ -48,6 +48,7 @@ export type AppViewState = {
|
||||
chatToolMessages: unknown[];
|
||||
chatStream: string | null;
|
||||
chatRunId: string | null;
|
||||
chatAvatarUrl: string | null;
|
||||
chatThinkingLevel: string | null;
|
||||
chatQueue: ChatQueueItem[];
|
||||
nodesLoading: boolean;
|
||||
|
||||
@@ -106,6 +106,7 @@ export class ClawdbotApp extends LitElement {
|
||||
@state() chatStream: string | null = null;
|
||||
@state() chatStreamStartedAt: number | null = null;
|
||||
@state() chatRunId: string | null = null;
|
||||
@state() chatAvatarUrl: string | null = null;
|
||||
@state() chatThinkingLevel: string | null = null;
|
||||
@state() chatQueue: ChatQueueItem[] = [];
|
||||
// Sidebar state for tool output viewing
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
} from "./message-extract";
|
||||
import { extractToolCards, renderToolCardSidebar } from "./tool-cards";
|
||||
|
||||
export function renderReadingIndicatorGroup() {
|
||||
export function renderReadingIndicatorGroup(assistantAvatarUrl?: string | null) {
|
||||
return html`
|
||||
<div class="chat-group assistant">
|
||||
${renderAvatar("assistant")}
|
||||
${renderAvatar("assistant", assistantAvatarUrl ?? undefined)}
|
||||
<div class="chat-group-messages">
|
||||
<div class="chat-bubble chat-reading-indicator" aria-hidden="true">
|
||||
<span class="chat-reading-indicator__dots">
|
||||
@@ -31,6 +31,7 @@ export function renderStreamingGroup(
|
||||
text: string,
|
||||
startedAt: number,
|
||||
onOpenSidebar?: (content: string) => void,
|
||||
assistantAvatarUrl?: string | null,
|
||||
) {
|
||||
const timestamp = new Date(startedAt).toLocaleTimeString([], {
|
||||
hour: "numeric",
|
||||
@@ -39,7 +40,7 @@ export function renderStreamingGroup(
|
||||
|
||||
return html`
|
||||
<div class="chat-group assistant">
|
||||
${renderAvatar("assistant")}
|
||||
${renderAvatar("assistant", assistantAvatarUrl ?? undefined)}
|
||||
<div class="chat-group-messages">
|
||||
${renderGroupedMessage(
|
||||
{
|
||||
@@ -61,7 +62,11 @@ export function renderStreamingGroup(
|
||||
|
||||
export function renderMessageGroup(
|
||||
group: MessageGroup,
|
||||
opts: { onOpenSidebar?: (content: string) => void; showReasoning: boolean },
|
||||
opts: {
|
||||
onOpenSidebar?: (content: string) => void;
|
||||
showReasoning: boolean;
|
||||
assistantAvatarUrl?: string | null;
|
||||
},
|
||||
) {
|
||||
const normalizedRole = normalizeRoleForGrouping(group.role);
|
||||
const who =
|
||||
@@ -83,7 +88,7 @@ export function renderMessageGroup(
|
||||
|
||||
return html`
|
||||
<div class="chat-group ${roleClass}">
|
||||
${renderAvatar(group.role)}
|
||||
${renderAvatar(group.role, opts.assistantAvatarUrl ?? undefined)}
|
||||
<div class="chat-group-messages">
|
||||
${group.messages.map((item, index) =>
|
||||
renderGroupedMessage(
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ChatProps = {
|
||||
toolMessages: unknown[];
|
||||
stream: string | null;
|
||||
streamStartedAt: number | null;
|
||||
assistantAvatarUrl?: string | null;
|
||||
draft: string;
|
||||
queue: ChatQueueItem[];
|
||||
connected: boolean;
|
||||
@@ -114,7 +115,7 @@ export function renderChat(props: ChatProps) {
|
||||
: nothing}
|
||||
${repeat(buildChatItems(props), (item) => item.key, (item) => {
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup();
|
||||
return renderReadingIndicatorGroup(props.assistantAvatarUrl ?? null);
|
||||
}
|
||||
|
||||
if (item.kind === "stream") {
|
||||
@@ -122,6 +123,7 @@ export function renderChat(props: ChatProps) {
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
props.assistantAvatarUrl ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,6 +131,7 @@ export function renderChat(props: ChatProps) {
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
assistantAvatarUrl: props.assistantAvatarUrl ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user