Files
makelore/src/pages/Chat/chat-command-message.ts
2026-07-29 17:22:35 +08:00

124 lines
3.9 KiB
TypeScript

import type { AttachedFileMeta, ContentBlock, RawMessage } from '@/types/chat';
const IMAGE_DATA_URL = /^data:(image\/[A-Za-z0-9.+-]+);base64,([A-Za-z0-9+/]+={0,2})$/iu;
export interface StoredAttachmentForRestore {
file: AttachedFileMeta;
imageDataUrl?: string;
}
function contentBlocks(message: RawMessage): ContentBlock[] {
return Array.isArray(message.content) ? message.content as ContentBlock[] : [];
}
export function getRestorableUserText(message: RawMessage | undefined): string {
if (!message || message.role !== 'user') return '';
if (typeof message.content === 'string') return message.content;
return contentBlocks(message)
.filter((block) => block.type === 'text' && block.synthetic !== true)
.map((block) => block.text ?? '')
.filter((text) => text.length > 0)
.join('\n\n')
.trim();
}
function isUndoable(message: RawMessage | undefined): message is RawMessage {
return Boolean(message?.id && message.role === 'user');
}
function cursorIndex(messages: readonly RawMessage[], revertMessageID?: string): number | null {
if (!revertMessageID) return messages.length;
const index = messages.findIndex((message) => message.id === revertMessageID);
return index >= 0 ? index : null;
}
export function findUndoableUserMessage(
messages: readonly RawMessage[],
revertMessageID?: string,
): RawMessage | null {
const limit = cursorIndex(messages, revertMessageID);
if (limit === null) return null;
for (let index = limit - 1; index >= 0; index -= 1) {
if (isUndoable(messages[index])) return messages[index];
}
return null;
}
export function findRedoUserMessage(
messages: readonly RawMessage[],
revertMessageID: string,
): RawMessage | null {
const index = cursorIndex(messages, revertMessageID);
if (index === null || index === messages.length) return null;
for (let next = index + 1; next < messages.length; next += 1) {
const message = messages[next];
if (message?.id && message.role === 'user') return message;
}
return null;
}
interface ValidatedImageDataUrl {
dataUrl: string;
mimeType: string;
}
function validatedImageDataUrl(
value: string | null | undefined,
): ValidatedImageDataUrl | undefined {
const candidate = value?.trim();
if (!candidate) return undefined;
const match = IMAGE_DATA_URL.exec(candidate);
if (!match || match[2].length % 4 !== 0) return undefined;
return {
dataUrl: candidate,
mimeType: match[1].toLowerCase(),
};
}
function sameMimeType(left: string | undefined, right: string): boolean {
return typeof left !== 'string'
|| left.trim().toLowerCase() === right.trim().toLowerCase();
}
function normalizedMimeType(value: string): string {
return value.trim().toLowerCase();
}
function isImageMimeType(value: string): boolean {
return normalizedMimeType(value).startsWith('image/');
}
export function getStoredAttachmentsForRestore(
message: RawMessage,
): StoredAttachmentForRestore[] {
const imageBlocks = contentBlocks(message).filter((block) => block.type === 'image');
const files = message._attachedFiles ?? [];
const imageFileCount = files.filter((file) => isImageMimeType(file.mimeType)).length;
const canPairByPosition = imageBlocks.length === imageFileCount;
let imageIndex = 0;
return files.map((file) => {
if (!isImageMimeType(file.mimeType)) return { file };
const block = imageBlocks[imageIndex];
imageIndex += 1;
const preview = validatedImageDataUrl(file.preview);
if (preview?.mimeType === normalizedMimeType(file.mimeType)) {
return { file, imageDataUrl: preview.dataUrl };
}
if (!canPairByPosition || !block) return { file };
if (block.alt && block.alt !== file.fileName) return { file };
const embedded = validatedImageDataUrl(block.url);
if (
!embedded
|| embedded.mimeType !== normalizedMimeType(file.mimeType)
|| !sameMimeType(block.mimeType, embedded.mimeType)
) {
return { file };
}
return { file, imageDataUrl: embedded.dataUrl };
});
}