Extract chat grouping model

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 14:15:18 +02:00
parent d62ca94e4c
commit 63d70529c4
2 changed files with 50 additions and 44 deletions
+47
View File
@@ -0,0 +1,47 @@
import type { ChatLine, ChatPart } from "./components/shared";
export type ChatGroup =
| { kind: "message"; message: ChatLine; index: number }
| { kind: "group"; messages: ChatLine[]; startIndex: number };
export function groupChatMessages(messages: ChatLine[]): ChatGroup[] {
const groups: ChatGroup[] = [];
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 = [];
};
messages.forEach((message, index) => {
const readableParts = message.parts.filter((part) => isReadablePart(message, part));
const technicalParts = message.parts.filter((part) => !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;
}
export function summarizeChatGroup(messages: ChatLine[]): string {
const counts = messages.reduce<Record<string, number>>((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}` : ""}`;
}
function isReadablePart(message: ChatLine, part: ChatPart): boolean {
return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "bash");
}
+3 -44
View File
@@ -1,5 +1,6 @@
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { groupChatMessages, summarizeChatGroup } from "../chatGroups";
import type { ChatLine, ChatPart } from "./shared"; import type { ChatLine, ChatPart } from "./shared";
import { chatStyles } from "./shared"; import { chatStyles } from "./shared";
import "./FormattedText"; import "./FormattedText";
@@ -30,7 +31,7 @@ export class ChatView extends LitElement {
render() { render() {
return html` return html`
<div class="chat" @scroll=${this.onScroll}> <div class="chat" @scroll=${this.onScroll}>
${this.groupedMessages().map((group) => group.kind === "message" ${groupChatMessages(this.messages).map((group) => group.kind === "message"
? this.renderMessage(group.message, group.index) ? this.renderMessage(group.message, group.index)
: this.renderMessageGroup(group.messages, group.startIndex))} : this.renderMessageGroup(group.messages, group.startIndex))}
</div> </div>
@@ -51,7 +52,7 @@ export class ChatView extends LitElement {
<details class="msg event-group" data-index=${startIndex}> <details class="msg event-group" data-index=${startIndex}>
<summary> <summary>
<b class="label">events</b> <b class="label">events</b>
<span>${this.groupSummary(messages)}</span> <span>${summarizeChatGroup(messages)}</span>
</summary> </summary>
<div class="group-body"> <div class="group-body">
${messages.map((message) => html` ${messages.map((message) => html`
@@ -65,48 +66,6 @@ export class ChatView extends LitElement {
`; `;
} }
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" || message.role === "bash");
}
private groupSummary(messages: ChatLine[]): string {
const counts = messages.reduce<Record<string, number>>((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) { private renderPart(part: ChatPart) {
if (part.type === "text") return html`<formatted-text class="part" .text=${part.text}></formatted-text>`; if (part.type === "text") return html`<formatted-text class="part" .text=${part.text}></formatted-text>`;
if (part.type === "thinking") return html`<details class="part"><summary>thinking</summary><formatted-text .text=${part.text}></formatted-text></details>`; if (part.type === "thinking") return html`<details class="part"><summary>thinking</summary><formatted-text .text=${part.text}></formatted-text></details>`;