diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index f2b330d..bf5b939 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -30,16 +30,83 @@ export class ChatView extends LitElement { render() { return html`
- ${this.messages.map((message, index) => html` -
- ${message.role} - ${message.parts.map((part) => this.renderPart(part))} -
- `)} + ${this.groupedMessages().map((group) => group.kind === "message" + ? this.renderMessage(group.message, group.index) + : this.renderMessageGroup(group.messages, group.startIndex))}
`; } + private renderMessage(message: ChatLine, index: number) { + return html` +
+ ${message.role} + ${message.parts.map((part) => this.renderPart(part))} +
+ `; + } + + private renderMessageGroup(messages: ChatLine[], startIndex: number) { + return html` +
+ + events + ${this.groupSummary(messages)} + +
+ ${messages.map((message) => html` +
+ ${message.role} + ${message.parts.map((part) => this.renderPart(part))} +
+ `)} +
+
+ `; + } + + private groupedMessages(): Array<{ kind: "message"; message: ChatLine; index: number } | { kind: "group"; messages: ChatLine[]; startIndex: number }> { + const groups: Array<{ kind: "message"; message: ChatLine; index: number } | { kind: "group"; messages: ChatLine[]; startIndex: number }> = []; + let eventMessages: ChatLine[] = []; + let eventStartIndex = 0; + + const pushEvent = (message: ChatLine, index: number) => { + if (!eventMessages.length) eventStartIndex = index; + eventMessages.push(message); + }; + const flushEvents = () => { + if (!eventMessages.length) return; + groups.push({ kind: "group", messages: eventMessages, startIndex: eventStartIndex }); + eventMessages = []; + }; + + this.messages.forEach((message, index) => { + const readableParts = message.parts.filter((part) => this.isReadablePart(message, part)); + const technicalParts = message.parts.filter((part) => !this.isReadablePart(message, part)); + + if (technicalParts.length) pushEvent({ role: message.role, parts: technicalParts }, index); + if (readableParts.length) { + flushEvents(); + groups.push({ kind: "message", message: { role: message.role, parts: readableParts }, index }); + } + }); + flushEvents(); + return groups; + } + + private isReadablePart(message: ChatLine, part: ChatPart): boolean { + return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system"); + } + + private groupSummary(messages: ChatLine[]): string { + const counts = messages.reduce>((acc, message) => { + acc[message.role] = (acc[message.role] ?? 0) + 1; + return acc; + }, {}); + const details = Object.entries(counts).map(([role, count]) => `${count} ${role}`).join(" · "); + return `${messages.length} ${messages.length === 1 ? "event" : "events"}${details ? ` · ${details}` : ""}`; + } + private renderPart(part: ChatPart) { if (part.type === "text") return html``; if (part.type === "thinking") return html`
thinking
`; @@ -157,7 +224,7 @@ export class ChatView extends LitElement { } private articles(): HTMLElement[] { - return Array.from(this.renderRoot.querySelectorAll("article.msg")); + return Array.from(this.renderRoot.querySelectorAll("article.msg, details.msg")); } private withSuppressedScrollSave(callback: () => void) { diff --git a/src/client/src/components/PromptEditor.ts b/src/client/src/components/PromptEditor.ts index 3a4289b..26a287a 100644 --- a/src/client/src/components/PromptEditor.ts +++ b/src/client/src/components/PromptEditor.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "lit"; +import { LitElement, html, type PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { api, type FileSuggestion, type SlashCommand } from "../api"; import { promptEditorStyles, type CompletionItem } from "./shared"; @@ -17,6 +17,15 @@ export class PromptEditor extends LitElement { @state() private selectedIndex = 0; private requestVersion = 0; + protected willUpdate(changed: PropertyValues) { + if (!changed.has("sessionId")) return; + const previousSessionId = changed.get("sessionId") as string | undefined; + if (previousSessionId) saveDraft(previousSessionId, this.draft); + this.draft = this.sessionId ? loadDraft(this.sessionId) : ""; + this.completions = []; + this.selectedIndex = 0; + } + render() { return html`