mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
fix image load (#1097)
This commit is contained in:
@@ -2878,6 +2878,61 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
: message;
|
||||
});
|
||||
};
|
||||
type AttachedFile = NonNullable<RawMessage['_attachedFiles']>[number];
|
||||
const getAttachmentMergeKey = (file: AttachedFile): string | null => (
|
||||
file.filePath || file.gatewayUrl || null
|
||||
);
|
||||
const preserveExistingAttachmentPreviews = (
|
||||
currentMessages: RawMessage[],
|
||||
nextMessages: RawMessage[],
|
||||
): RawMessage[] => {
|
||||
const currentFilesByMessageKey = new Map<string, Map<string, AttachedFile>>();
|
||||
for (const message of currentMessages) {
|
||||
if (!message._attachedFiles?.length) continue;
|
||||
const filesByKey = new Map<string, AttachedFile>();
|
||||
for (const file of message._attachedFiles) {
|
||||
const key = getAttachmentMergeKey(file);
|
||||
if (!key) continue;
|
||||
if (!file.preview && !file.fileSize && !file.previewStatus) continue;
|
||||
filesByKey.set(key, file);
|
||||
}
|
||||
if (filesByKey.size > 0) {
|
||||
currentFilesByMessageKey.set(getPreviewMergeKey(message), filesByKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFilesByMessageKey.size === 0) return nextMessages;
|
||||
|
||||
return nextMessages.map((message) => {
|
||||
if (!message._attachedFiles?.length) return message;
|
||||
const currentFiles = currentFilesByMessageKey.get(getPreviewMergeKey(message));
|
||||
if (!currentFiles) return message;
|
||||
|
||||
let changed = false;
|
||||
const attachedFiles = message._attachedFiles.map((file) => {
|
||||
const key = getAttachmentMergeKey(file);
|
||||
const currentFile = key ? currentFiles.get(key) : undefined;
|
||||
if (!currentFile) return file;
|
||||
|
||||
let nextFile = file;
|
||||
if (!nextFile.preview && currentFile.preview) {
|
||||
nextFile = { ...nextFile, preview: currentFile.preview };
|
||||
changed = true;
|
||||
}
|
||||
if (!nextFile.fileSize && currentFile.fileSize) {
|
||||
nextFile = { ...nextFile, fileSize: currentFile.fileSize };
|
||||
changed = true;
|
||||
}
|
||||
if (!nextFile.previewStatus && currentFile.previewStatus) {
|
||||
nextFile = { ...nextFile, previewStatus: currentFile.previewStatus };
|
||||
changed = true;
|
||||
}
|
||||
return nextFile;
|
||||
});
|
||||
|
||||
return changed ? { ...message, _attachedFiles: attachedFiles } : message;
|
||||
});
|
||||
};
|
||||
|
||||
const applyLoadFailure = (errorMessage: string | null) => {
|
||||
if (!isCurrentSession()) return;
|
||||
@@ -2920,6 +2975,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
finalMessages = dropRedundantOptimisticUserMessages(currentSessionKey, finalMessages);
|
||||
finalMessages = preserveExistingAttachmentPreviews(get().messages, finalMessages);
|
||||
|
||||
const { pendingFinal, lastUserMessageAt, sending: isSendingNow } = get();
|
||||
const userMsTs = lastUserMessageAt != null ? toMs(lastUserMessageAt) : 0;
|
||||
|
||||
@@ -95,6 +95,61 @@ export function createHistoryActions(
|
||||
: message;
|
||||
});
|
||||
};
|
||||
type AttachedFile = NonNullable<RawMessage['_attachedFiles']>[number];
|
||||
const getAttachmentMergeKey = (file: AttachedFile): string | null => (
|
||||
file.filePath || file.gatewayUrl || null
|
||||
);
|
||||
const preserveExistingAttachmentPreviews = (
|
||||
currentMessages: RawMessage[],
|
||||
nextMessages: RawMessage[],
|
||||
): RawMessage[] => {
|
||||
const currentFilesByMessageKey = new Map<string, Map<string, AttachedFile>>();
|
||||
for (const message of currentMessages) {
|
||||
if (!message._attachedFiles?.length) continue;
|
||||
const filesByKey = new Map<string, AttachedFile>();
|
||||
for (const file of message._attachedFiles) {
|
||||
const key = getAttachmentMergeKey(file);
|
||||
if (!key) continue;
|
||||
if (!file.preview && !file.fileSize && !file.previewStatus) continue;
|
||||
filesByKey.set(key, file);
|
||||
}
|
||||
if (filesByKey.size > 0) {
|
||||
currentFilesByMessageKey.set(getPreviewMergeKey(message), filesByKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFilesByMessageKey.size === 0) return nextMessages;
|
||||
|
||||
return nextMessages.map((message) => {
|
||||
if (!message._attachedFiles?.length) return message;
|
||||
const currentFiles = currentFilesByMessageKey.get(getPreviewMergeKey(message));
|
||||
if (!currentFiles) return message;
|
||||
|
||||
let changed = false;
|
||||
const attachedFiles = message._attachedFiles.map((file) => {
|
||||
const key = getAttachmentMergeKey(file);
|
||||
const currentFile = key ? currentFiles.get(key) : undefined;
|
||||
if (!currentFile) return file;
|
||||
|
||||
let nextFile = file;
|
||||
if (!nextFile.preview && currentFile.preview) {
|
||||
nextFile = { ...nextFile, preview: currentFile.preview };
|
||||
changed = true;
|
||||
}
|
||||
if (!nextFile.fileSize && currentFile.fileSize) {
|
||||
nextFile = { ...nextFile, fileSize: currentFile.fileSize };
|
||||
changed = true;
|
||||
}
|
||||
if (!nextFile.previewStatus && currentFile.previewStatus) {
|
||||
nextFile = { ...nextFile, previewStatus: currentFile.previewStatus };
|
||||
changed = true;
|
||||
}
|
||||
return nextFile;
|
||||
});
|
||||
|
||||
return changed ? { ...message, _attachedFiles: attachedFiles } : message;
|
||||
});
|
||||
};
|
||||
|
||||
const applyLoadFailure = (errorMessage: string | null) => {
|
||||
if (!isCurrentSession()) return;
|
||||
@@ -133,6 +188,7 @@ export function createHistoryActions(
|
||||
}
|
||||
}
|
||||
finalMessages = dropRedundantOptimisticUserMessages(currentSessionKey, finalMessages);
|
||||
finalMessages = preserveExistingAttachmentPreviews(get().messages, finalMessages);
|
||||
|
||||
const { pendingFinal, lastUserMessageAt, sending: isSendingNow } = get();
|
||||
const userMsTs = lastUserMessageAt ? toMs(lastUserMessageAt) : 0;
|
||||
|
||||
@@ -749,6 +749,65 @@ describe('chat history actions', () => {
|
||||
expect(h.read().messages[0]?._attachedFiles?.[0]?.preview).toBe('data:image/png;base64,abc');
|
||||
});
|
||||
|
||||
it('preserves existing image previews when quiet history refresh rebuilds attachments', async () => {
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const gatewayUrl = '/api/chat/media/outgoing/agent%3Amain%3As-1/image-1/full';
|
||||
const h = makeHarness({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
messages: [
|
||||
{
|
||||
id: 'generated-image',
|
||||
role: 'assistant',
|
||||
content: 'Generated image',
|
||||
timestamp: 1000,
|
||||
_attachedFiles: [
|
||||
{
|
||||
fileName: 'generated.png',
|
||||
mimeType: 'image/png',
|
||||
fileSize: 42,
|
||||
preview: 'data:image/png;base64,already-loaded',
|
||||
gatewayUrl,
|
||||
source: 'gateway-media',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const actions = createHistoryActions(h.set as never, h.get as never);
|
||||
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
result: {
|
||||
messages: [
|
||||
{
|
||||
id: 'generated-image',
|
||||
role: 'assistant',
|
||||
content: 'Generated image',
|
||||
timestamp: 1000,
|
||||
_attachedFiles: [
|
||||
{
|
||||
fileName: 'generated.png',
|
||||
mimeType: 'image/png',
|
||||
fileSize: 0,
|
||||
preview: null,
|
||||
gatewayUrl,
|
||||
source: 'gateway-media',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await actions.loadHistory(true);
|
||||
|
||||
expect(h.read().messages[0]?._attachedFiles?.[0]).toMatchObject({
|
||||
fileSize: 42,
|
||||
preview: 'data:image/png;base64,already-loaded',
|
||||
gatewayUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not append an optimistic duplicate when history already includes the user message without timestamp', async () => {
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness({
|
||||
|
||||
Reference in New Issue
Block a user