bug fixes, and performance improvements, and drawer
This commit is contained in:
+88
-16
@@ -1,9 +1,86 @@
|
||||
import type { AssistantContent, FilePart, ImagePart, JSONValue, ModelMessage, ToolContent } from "ai";
|
||||
import { ToolCallType } from "~~/drizzle/schema";
|
||||
import { Err, Ok, type Result } from "~~/types/result";
|
||||
import type { Message, MessageEntity } from "~/composables/useChat";
|
||||
import type { Agent } from "~/composables/useAgents";
|
||||
import { resolveFileUrl } from "~~/utils/url";
|
||||
import { ToolCallType } from "../drizzle/schema";
|
||||
import { Err, Ok, type Result } from "../types/result";
|
||||
import type { Message, MessageEntity } from "../app/composables/useChat";
|
||||
import type { Agent } from "../app/composables/useAgents";
|
||||
import { resolveFileUrl } from "./url";
|
||||
|
||||
export const getActiveMessage = <T extends { id: string; deleted?: boolean | null; activeChildId?: string | null; children: (T | undefined)[] }>(
|
||||
message: T
|
||||
): T | null => {
|
||||
if (message.activeChildId && message.children.length > 0) {
|
||||
const child = message.children.find(c => c?.id === message.activeChildId);
|
||||
if (child) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
if (message.children.length > 0 && message.children[0]) {
|
||||
return message.children[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
export const getMessageCount = (message: {
|
||||
deleted?: boolean | null;
|
||||
children: unknown[];
|
||||
}): number => {
|
||||
if (message.deleted === true) {
|
||||
return message.children.length;
|
||||
}
|
||||
|
||||
return message.children.length + 1;
|
||||
};
|
||||
|
||||
export const getCurrentChildIndex = (message: {
|
||||
deleted?: boolean | null;
|
||||
activeChildId?: string | null;
|
||||
children: ({ id: string } | undefined)[];
|
||||
}): number => {
|
||||
if (message.deleted === true) {
|
||||
if (!message.activeChildId) {
|
||||
return 0;
|
||||
}
|
||||
const idx = message.children.findIndex(c => c?.id === message.activeChildId);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
if (!message.activeChildId) {
|
||||
return 0;
|
||||
}
|
||||
const idx = message.children.findIndex(c => c?.id === message.activeChildId);
|
||||
return idx >= 0 ? idx + 1 : 0;
|
||||
};
|
||||
|
||||
export const getNextActiveChildId = (
|
||||
message: {
|
||||
deleted?: boolean | null;
|
||||
activeChildId?: string | null;
|
||||
children: ({ id: string } | undefined)[];
|
||||
},
|
||||
direction: -1 | 1
|
||||
): string | null => {
|
||||
const allItems = message.deleted === true
|
||||
? message.children
|
||||
: [null, ...message.children];
|
||||
|
||||
const currentIdx = allItems.findIndex(item => {
|
||||
if (!item) {
|
||||
return !message.activeChildId;
|
||||
}
|
||||
return item.id === message.activeChildId;
|
||||
});
|
||||
|
||||
const validCurrentIdx = currentIdx >= 0 ? currentIdx : 0;
|
||||
const newIdx = Math.max(0, Math.min(allItems.length - 1, validCurrentIdx + direction));
|
||||
const newItem = allItems[newIdx];
|
||||
|
||||
return newItem?.id ?? null;
|
||||
};
|
||||
|
||||
export const buildMessageTree = (flatMessages: MessageEntity[]) => {
|
||||
const messagesMap = new Map(flatMessages.map(m => [m.id, { ...m, children: [] as MessageEntity[] }]));
|
||||
@@ -16,26 +93,21 @@ export const buildMessageTree = (flatMessages: MessageEntity[]) => {
|
||||
roots.push(msg);
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
return roots.filter(root => !root.deleted || root.children.length > 0);
|
||||
};
|
||||
|
||||
export const buildFocusedMessageTree = (messages: Readonly<Message[]>): MessageEntity[] => {
|
||||
let focusedMessageTree: MessageEntity[] = [];
|
||||
const focusedMessageTree: MessageEntity[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.activeChildId && message.children.length > 0) {
|
||||
const activeChild = message.children.find(c => c?.id === message.activeChildId);
|
||||
if (activeChild) {
|
||||
focusedMessageTree.push(activeChild);
|
||||
continue;
|
||||
}
|
||||
const activeMessage = getActiveMessage(message);
|
||||
if (activeMessage) {
|
||||
focusedMessageTree.push(activeMessage);
|
||||
}
|
||||
|
||||
focusedMessageTree.push(message);
|
||||
}
|
||||
|
||||
return focusedMessageTree;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MarshallOptions {
|
||||
signFileUrl?: (fileKey: string) => string;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Stable across database Dates, JSON SSE payloads, and Nuxt hydration. */
|
||||
export const serializeTopicSnapshot = (topic: unknown): string => JSON.stringify(topic, (_key, value) => {
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]]));
|
||||
});
|
||||
|
||||
/** Absolute offsets make replay across a snapshot boundary idempotent. */
|
||||
export const appendTextDelta = (current: string, content: string, offset?: number): string => {
|
||||
if (offset === undefined) {
|
||||
return current + content;
|
||||
}
|
||||
if (offset > current.length) {
|
||||
// A gap requires reconciliation, not silently concatenating corrupt text.
|
||||
return current;
|
||||
}
|
||||
const overlap = current.length - offset;
|
||||
return overlap >= content.length ? current : current + content.slice(overlap);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export const provisionalTopicTitle = (message?: {
|
||||
content: string;
|
||||
files?: { name: string }[];
|
||||
fileIds?: string[];
|
||||
}): string => {
|
||||
const line = message?.content.split(/\r?\n/).map(line => line.trim()).find(Boolean);
|
||||
const title = line || message?.files?.[0]?.name || (message?.fileIds?.length ? "Attached files" : "New Topic");
|
||||
return title.replace(/\s+/g, " ").slice(0, 100);
|
||||
};
|
||||
Reference in New Issue
Block a user