ui: Improve performance when streaming (#25225)
* ui: Improve performance when streaming * ui: build sibling info map in branching utils Moves the node map and sibling map construction from the .by block into buildSiblingInfoMap() in branching.ts. The map is built once per structural change and only read afterwards, so it does not need SvelteMap reactivity. Keeping the construction in plain TypeScript fixes the svelte/prefer-svelte-reactivity lint error and groups the branching logic where it already lives. --------- Co-authored-by: Pascal <admin@serveurperso.com>
This commit is contained in:
@@ -20,9 +20,9 @@
|
|||||||
agenticInjectSteeringMessage
|
agenticInjectSteeringMessage
|
||||||
} from '$lib/stores/agentic.svelte';
|
} from '$lib/stores/agentic.svelte';
|
||||||
import {
|
import {
|
||||||
|
buildSiblingInfoMap,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
formatMessageForClipboard,
|
formatMessageForClipboard,
|
||||||
getMessageSiblings,
|
|
||||||
hasAgenticContent
|
hasAgenticContent
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
|
||||||
@@ -169,6 +169,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let siblingInfoByMessageId = $derived(buildSiblingInfoMap(allConversationMessages));
|
||||||
|
|
||||||
let displayMessages = $derived.by(() => {
|
let displayMessages = $derived.by(() => {
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -223,18 +225,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const siblingInfo = getMessageSiblings(allConversationMessages, msg.id);
|
const siblingInfo = siblingInfoByMessageId.get(msg.id) ?? {
|
||||||
|
message: msg,
|
||||||
|
siblingIds: [msg.id],
|
||||||
|
currentIndex: 0,
|
||||||
|
totalSiblings: 1
|
||||||
|
};
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
message: msg,
|
message: msg,
|
||||||
toolMessages,
|
toolMessages,
|
||||||
isLastAssistantMessage: false,
|
isLastAssistantMessage: false,
|
||||||
siblingInfo: siblingInfo || {
|
siblingInfo
|
||||||
message: msg,
|
|
||||||
siblingIds: [msg.id],
|
|
||||||
currentIndex: 0,
|
|
||||||
totalSiblings: 1
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,18 +92,14 @@ export function filterByLeafNodeId(
|
|||||||
* Finds the leaf node (message with no children) for a given message branch.
|
* Finds the leaf node (message with no children) for a given message branch.
|
||||||
* Traverses down the tree following the last child until reaching a leaf.
|
* Traverses down the tree following the last child until reaching a leaf.
|
||||||
*
|
*
|
||||||
* @param messages - All messages in the conversation
|
* @param nodeMap - Map of messages keyed by ID
|
||||||
* @param messageId - Starting message ID to find leaf for
|
* @param messageId - Starting message ID to find leaf for
|
||||||
* @returns The leaf node ID, or the original messageId if no children
|
* @returns The leaf node ID, or the original messageId if no children
|
||||||
*/
|
*/
|
||||||
export function findLeafNode(messages: readonly DatabaseMessage[], messageId: string): string {
|
function findLeafNodeInMap(
|
||||||
const nodeMap = new Map<string, DatabaseMessage>();
|
nodeMap: ReadonlyMap<string, DatabaseMessage>,
|
||||||
|
messageId: string
|
||||||
// Build node map for quick lookups
|
): string {
|
||||||
for (const msg of messages) {
|
|
||||||
nodeMap.set(msg.id, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);
|
let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);
|
||||||
while (currentNode && currentNode.children.length > 0) {
|
while (currentNode && currentNode.children.length > 0) {
|
||||||
// Follow the last child (most recent branch)
|
// Follow the last child (most recent branch)
|
||||||
@@ -114,6 +110,22 @@ export function findLeafNode(messages: readonly DatabaseMessage[], messageId: st
|
|||||||
return currentNode?.id ?? messageId;
|
return currentNode?.id ?? messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper around {@link findLeafNodeInMap} for callers that only have
|
||||||
|
* a flat message array.
|
||||||
|
*
|
||||||
|
* Finds the leaf node (message with no children) for a given message branch.
|
||||||
|
* Traverses down the tree following the last child until reaching a leaf.
|
||||||
|
*
|
||||||
|
* @param messages - All messages in the conversation
|
||||||
|
* @param messageId - Starting message ID to find leaf for
|
||||||
|
* @returns The leaf node ID, or the original messageId if no children
|
||||||
|
*/
|
||||||
|
export function findLeafNode(messages: readonly DatabaseMessage[], messageId: string): string {
|
||||||
|
const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
|
||||||
|
return findLeafNodeInMap(nodeMap, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all descendant messages (children, grandchildren, etc.) of a given message.
|
* Finds all descendant messages (children, grandchildren, etc.) of a given message.
|
||||||
* This is used for cascading deletion to remove all messages in a branch.
|
* This is used for cascading deletion to remove all messages in a branch.
|
||||||
@@ -156,21 +168,14 @@ export function findDescendantMessages(
|
|||||||
* Gets sibling information for a message, including all sibling IDs and current position.
|
* Gets sibling information for a message, including all sibling IDs and current position.
|
||||||
* Siblings are messages that share the same parent.
|
* Siblings are messages that share the same parent.
|
||||||
*
|
*
|
||||||
* @param messages - All messages in the conversation
|
* @param nodeMap - Map of messages keyed by ID
|
||||||
* @param messageId - The message to get sibling info for
|
* @param messageId - The message to get sibling info for
|
||||||
* @returns Sibling information including leaf node IDs for navigation
|
* @returns Sibling information including leaf node IDs for navigation
|
||||||
*/
|
*/
|
||||||
export function getMessageSiblings(
|
export function getMessageSiblings(
|
||||||
messages: readonly DatabaseMessage[],
|
nodeMap: ReadonlyMap<string, DatabaseMessage>,
|
||||||
messageId: string
|
messageId: string
|
||||||
): ChatMessageSiblingInfo | null {
|
): ChatMessageSiblingInfo | null {
|
||||||
const nodeMap = new Map<string, DatabaseMessage>();
|
|
||||||
|
|
||||||
// Build node map for quick lookups
|
|
||||||
for (const msg of messages) {
|
|
||||||
nodeMap.set(msg.id, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = nodeMap.get(messageId);
|
const message = nodeMap.get(messageId);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return null;
|
return null;
|
||||||
@@ -203,7 +208,9 @@ export function getMessageSiblings(
|
|||||||
|
|
||||||
// Convert sibling message IDs to their corresponding leaf node IDs
|
// Convert sibling message IDs to their corresponding leaf node IDs
|
||||||
// This allows navigation between different conversation branches
|
// This allows navigation between different conversation branches
|
||||||
const siblingLeafIds = siblingIds.map((siblingId: string) => findLeafNode(messages, siblingId));
|
const siblingLeafIds = siblingIds.map((siblingId: string) =>
|
||||||
|
findLeafNodeInMap(nodeMap, siblingId)
|
||||||
|
);
|
||||||
|
|
||||||
// Find current message's position among siblings
|
// Find current message's position among siblings
|
||||||
const currentIndex = siblingIds.indexOf(messageId);
|
const currentIndex = siblingIds.indexOf(messageId);
|
||||||
@@ -217,85 +224,22 @@ export function getMessageSiblings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a display-ready list of messages with sibling information for UI rendering.
|
* Builds sibling information for every message in a conversation.
|
||||||
* This is the main function used by chat components to render conversation branches.
|
* A single node map is shared across all lookups for O(1) access.
|
||||||
*
|
*
|
||||||
* @param messages - All messages in the conversation
|
* @param messages - All messages in the conversation
|
||||||
* @param leafNodeId - Current leaf node being viewed
|
* @returns Map of message ID to its sibling information
|
||||||
* @returns Array of messages with sibling navigation info
|
|
||||||
*/
|
*/
|
||||||
export function getMessageDisplayList(
|
export function buildSiblingInfoMap(
|
||||||
messages: readonly DatabaseMessage[],
|
messages: readonly DatabaseMessage[]
|
||||||
leafNodeId: string
|
): Map<string, ChatMessageSiblingInfo> {
|
||||||
): ChatMessageSiblingInfo[] {
|
const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
|
||||||
// Get the current conversation path
|
const siblingMap = new Map<string, ChatMessageSiblingInfo>();
|
||||||
const currentPath = filterByLeafNodeId(messages, leafNodeId, true);
|
for (const msg of messages) {
|
||||||
const result: ChatMessageSiblingInfo[] = [];
|
const info = getMessageSiblings(nodeMap, msg.id);
|
||||||
|
if (info) {
|
||||||
// Add sibling info for each message in the current path
|
siblingMap.set(msg.id, info);
|
||||||
for (const message of currentPath) {
|
|
||||||
if (message.type === 'root') {
|
|
||||||
continue; // Skip root messages in display
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblingInfo = getMessageSiblings(messages, message.id);
|
|
||||||
if (siblingInfo) {
|
|
||||||
result.push(siblingInfo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return siblingMap;
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a message has multiple siblings (indicating branching at that point).
|
|
||||||
*
|
|
||||||
* @param messages - All messages in the conversation
|
|
||||||
* @param messageId - The message to check
|
|
||||||
* @returns True if the message has siblings
|
|
||||||
*/
|
|
||||||
export function hasMessageSiblings(
|
|
||||||
messages: readonly DatabaseMessage[],
|
|
||||||
messageId: string
|
|
||||||
): boolean {
|
|
||||||
const siblingInfo = getMessageSiblings(messages, messageId);
|
|
||||||
return siblingInfo ? siblingInfo.totalSiblings > 1 : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the next sibling message ID for navigation.
|
|
||||||
*
|
|
||||||
* @param messages - All messages in the conversation
|
|
||||||
* @param messageId - Current message ID
|
|
||||||
* @returns Next sibling's leaf node ID, or null if at the end
|
|
||||||
*/
|
|
||||||
export function getNextSibling(
|
|
||||||
messages: readonly DatabaseMessage[],
|
|
||||||
messageId: string
|
|
||||||
): string | null {
|
|
||||||
const siblingInfo = getMessageSiblings(messages, messageId);
|
|
||||||
if (!siblingInfo || siblingInfo.currentIndex >= siblingInfo.totalSiblings - 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return siblingInfo.siblingIds[siblingInfo.currentIndex + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the previous sibling message ID for navigation.
|
|
||||||
*
|
|
||||||
* @param messages - All messages in the conversation
|
|
||||||
* @param messageId - Current message ID
|
|
||||||
* @returns Previous sibling's leaf node ID, or null if at the beginning
|
|
||||||
*/
|
|
||||||
export function getPreviousSibling(
|
|
||||||
messages: readonly DatabaseMessage[],
|
|
||||||
messageId: string
|
|
||||||
): string | null {
|
|
||||||
const siblingInfo = getMessageSiblings(messages, messageId);
|
|
||||||
if (!siblingInfo || siblingInfo.currentIndex <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return siblingInfo.siblingIds[siblingInfo.currentIndex - 1];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ export {
|
|||||||
findLeafNode,
|
findLeafNode,
|
||||||
findDescendantMessages,
|
findDescendantMessages,
|
||||||
getMessageSiblings,
|
getMessageSiblings,
|
||||||
getMessageDisplayList,
|
buildSiblingInfoMap
|
||||||
hasMessageSiblings,
|
|
||||||
getNextSibling,
|
|
||||||
getPreviousSibling
|
|
||||||
} from './branching';
|
} from './branching';
|
||||||
|
|
||||||
// Code
|
// Code
|
||||||
|
|||||||
Reference in New Issue
Block a user