feat(chat): improve media handling and caching for user messages (#81)
This commit is contained in:
@@ -105,35 +105,41 @@ export const ChatMessage = memo(function ChatMessage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Images (from assistant/channel content blocks) */}
|
||||
{/* Images from content blocks (Gateway session data — persists across history reloads) */}
|
||||
{images.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{images.map((img, i) => (
|
||||
<img
|
||||
key={i}
|
||||
key={`content-${i}`}
|
||||
src={`data:${img.mimeType};base64,${img.data}`}
|
||||
alt="attachment"
|
||||
className="max-w-xs rounded-lg border"
|
||||
className={cn(
|
||||
'rounded-lg border',
|
||||
isUser ? 'max-w-[200px] max-h-48' : 'max-w-xs',
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File attachments (user-uploaded files) */}
|
||||
{/* File attachments (local preview — shown before history reload) */}
|
||||
{/* Only show _attachedFiles images if no content-block images (avoid duplicates) */}
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{attachedFiles.map((file, i) => (
|
||||
file.mimeType.startsWith('image/') && file.preview ? (
|
||||
{attachedFiles.map((file, i) => {
|
||||
// Skip image attachments if we already have images from content blocks
|
||||
if (file.mimeType.startsWith('image/') && file.preview && images.length > 0) return null;
|
||||
return file.mimeType.startsWith('image/') && file.preview ? (
|
||||
<img
|
||||
key={i}
|
||||
key={`local-${i}`}
|
||||
src={file.preview}
|
||||
alt={file.fileName}
|
||||
className="max-w-xs max-h-48 rounded-lg border"
|
||||
className="max-w-[200px] max-h-48 rounded-lg border"
|
||||
/>
|
||||
) : (
|
||||
<FileCard key={i} file={file} />
|
||||
)
|
||||
))}
|
||||
<FileCard key={`local-${i}`} file={file} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,20 +5,38 @@
|
||||
*/
|
||||
import type { RawMessage, ContentBlock } from '@/stores/chat';
|
||||
|
||||
/**
|
||||
* Clean Gateway metadata from user message text for display.
|
||||
* Strips: [media attached: ... | ...], [message_id: ...],
|
||||
* and the timestamp prefix [Day Date Time Timezone].
|
||||
*/
|
||||
function cleanUserText(text: string): string {
|
||||
return text
|
||||
// Remove [media attached: path (mime) | path] references
|
||||
.replace(/\s*\[media attached:[^\]]*\]/g, '')
|
||||
// Remove [message_id: uuid]
|
||||
.replace(/\s*\[message_id:\s*[^\]]+\]/g, '')
|
||||
// Remove Gateway timestamp prefix like [Fri 2026-02-13 22:39 GMT+8]
|
||||
.replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+[^\]]+\]\s*/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract displayable text from a message's content field.
|
||||
* Handles both string content and array-of-blocks content.
|
||||
* For user messages, strips Gateway-injected metadata.
|
||||
*/
|
||||
export function extractText(message: RawMessage | unknown): string {
|
||||
if (!message || typeof message !== 'object') return '';
|
||||
const msg = message as Record<string, unknown>;
|
||||
const content = msg.content;
|
||||
const isUser = msg.role === 'user';
|
||||
|
||||
let result = '';
|
||||
|
||||
if (typeof content === 'string') {
|
||||
return content.trim().length > 0 ? content : '';
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
result = content.trim().length > 0 ? content : '';
|
||||
} else if (Array.isArray(content)) {
|
||||
const parts: string[] = [];
|
||||
for (const block of content as ContentBlock[]) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
@@ -28,15 +46,18 @@ export function extractText(message: RawMessage | unknown): string {
|
||||
}
|
||||
}
|
||||
const combined = parts.join('\n\n');
|
||||
return combined.trim().length > 0 ? combined : '';
|
||||
result = combined.trim().length > 0 ? combined : '';
|
||||
} else if (typeof msg.text === 'string') {
|
||||
// Fallback: try .text field
|
||||
result = msg.text.trim().length > 0 ? msg.text : '';
|
||||
}
|
||||
|
||||
// Fallback: try .text field
|
||||
if (typeof msg.text === 'string') {
|
||||
return msg.text.trim().length > 0 ? msg.text : '';
|
||||
// Strip Gateway metadata from user messages for clean display
|
||||
if (isUser && result) {
|
||||
result = cleanUserText(result);
|
||||
}
|
||||
|
||||
return '';
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +85,35 @@ export function extractThinking(message: RawMessage | unknown): string | null {
|
||||
return combined.length > 0 ? combined : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract media file references from Gateway-formatted user message text.
|
||||
* Returns array of { filePath, mimeType } from [media attached: path (mime) | path] patterns.
|
||||
*/
|
||||
export function extractMediaRefs(message: RawMessage | unknown): Array<{ filePath: string; mimeType: string }> {
|
||||
if (!message || typeof message !== 'object') return [];
|
||||
const msg = message as Record<string, unknown>;
|
||||
if (msg.role !== 'user') return [];
|
||||
const content = msg.content;
|
||||
|
||||
let text = '';
|
||||
if (typeof content === 'string') {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
text = (content as ContentBlock[])
|
||||
.filter(b => b.type === 'text' && b.text)
|
||||
.map(b => b.text!)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const refs: Array<{ filePath: string; mimeType: string }> = [];
|
||||
const regex = /\[media attached:\s*([^\s(]+)\s*\(([^)]+)\)\s*\|[^\]]*\]/g;
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
refs.push({ filePath: match[1], mimeType: match[2] });
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract image attachments from a message.
|
||||
* Returns array of { mimeType, data } for base64 images.
|
||||
|
||||
@@ -519,7 +519,7 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 120 * 1000); // 120 seconds — enough for gateway to fully initialize
|
||||
}, 600 * 1000); // 600 seconds — enough for gateway to fully initialize
|
||||
|
||||
return () => {
|
||||
if (gatewayTimeoutRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user