Makelore 2.0 initial clean snapshot
This commit is contained in:
123
src/pages/Chat/chat-command-message.ts
Normal file
123
src/pages/Chat/chat-command-message.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
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 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user