diff --git a/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx b/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx index 0259accf2..bcf7b1c82 100644 --- a/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx +++ b/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx @@ -1,12 +1,16 @@ import * as React from 'react'; -import { Box, ListDivider, ListItemDecorator, MenuItem, Radio, Typography } from '@mui/joy'; +import { Box, IconButton, Link, ListDivider, ListItem, ListItemDecorator, MenuItem, Radio, Tooltip, Typography } from '@mui/joy'; import ClearIcon from '@mui/icons-material/Clear'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; +import LaunchIcon from '@mui/icons-material/Launch'; import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom'; +import { getImageBlobURLById } from '~/modules/dblobs/dblobs.db'; + +import type { DContentRef } from '~/common/stores/chat/chat.message'; import { CloseableMenu } from '~/common/components/CloseableMenu'; import { copyToClipboard } from '~/common/util/clipboardUtils'; @@ -19,6 +23,22 @@ import type { LLMAttachment } from './useLLMAttachments'; export const DEBUG_LLMATTACHMENTS = true; +/** + * Note: this utility function could be extracted more broadly to chat.message.ts, but + * I don't want to introduce a (circular) dependency from chat.message.ts to dblobs.db.ts. + */ +async function handleShowContentInNewTab(source: DContentRef) { + console.log('handleShowContentInNewTab', source); + let imageUrl: string | null = null; + if (source.reftype === 'url') + imageUrl = source.url; + else if (source.reftype === 'dblob') + imageUrl = await getImageBlobURLById(source.dblobId); + if (imageUrl && typeof window !== 'undefined') + window.open(imageUrl, '_blank', 'noopener,noreferrer'); +} + + export function LLMAttachmentMenu(props: { attachmentDraftsStoreApi: AttachmentDraftsStoreApi, llmAttachment: LLMAttachment, @@ -145,8 +165,16 @@ export function LLMAttachmentMenu(props: { {!isUnconvertible && } {DEBUG_LLMATTACHMENTS && !!aInput && ( - - + + + {isOutputTextInlineable && ( + + + + + + )} + {!!aInput && 🡐 {aInput.mimeType}, {aInput.dataSize.toLocaleString()} bytes @@ -160,11 +188,15 @@ export function LLMAttachmentMenu(props: { ) : ( aOutputParts.map((output, index) => { if (output.atype === 'aimage') { - const resolution = output.width && output.height ? `${output.width}x${output.height}` : 'unknown resolution'; + const resolution = output.width && output.height ? `${output.width} x ${output.height}` : 'unknown resolution'; const mime = output.source.reftype === 'dblob' ? output.source.mimeType : 'unknown image'; return ( - 🡒 {mime}: {resolution}, {output.source.reftype === 'dblob' ? output.source.bytesSize?.toLocaleString() : '(remote)'} bytes + 🡒 {mime.replace('image/', 'img: ')}, {resolution}, {output.source.reftype === 'dblob' ? output.source.bytesSize?.toLocaleString() : '(remote)'} bytes, + {' '} + handleShowContentInNewTab(output.source)}> + show + ); } else if (output.atype === 'atext') { @@ -189,7 +221,7 @@ export function LLMAttachmentMenu(props: { )} - + )} {DEBUG_LLMATTACHMENTS && !!aInput && } diff --git a/src/common/attachment-drafts/attachment.pipeline.ts b/src/common/attachment-drafts/attachment.pipeline.ts index 248d65f6e..af58ea918 100644 --- a/src/common/attachment-drafts/attachment.pipeline.ts +++ b/src/common/attachment-drafts/attachment.pipeline.ts @@ -10,6 +10,13 @@ import type { AttachmentDraft, AttachmentDraftConverter, AttachmentDraftInput, A import { attachmentImageToPartViaDBlob } from './attachment.dblobs'; +// configuration +export const DEFAULT_ADRAFT_IMAGE_MIMETYPE = 'image/webp'; +export const DEFAULT_ADRAFT_IMAGE_QUALITY = 0.98; +const PDF_IMAGE_PAGE_SCALE = 1.5; +const PDF_IMAGE_QUALITY = 0.5; + + // extensions to treat as plain text const PLAIN_TEXT_EXTENSIONS: string[] = ['.ts', '.tsx']; @@ -70,9 +77,6 @@ const IMAGE_MIMETYPES: string[] = [ 'image/gif', ]; -export const DEFAULT_ADRAFT_IMAGE_MIMETYPE = 'image/webp'; -export const DEFAULT_ADRAFT_IMAGE_QUALITY = 0.98; - /** * Creates a new AttachmentDraft object. @@ -430,7 +434,7 @@ export async function attachmentPerformConversion(attachment: Readonly { +export async function pdfToImageDataURLs(pdfBuffer: ArrayBuffer, imageMimeType: string, imageQuality: number /* = 0.95 */, scale: number /*= 1.5*/): Promise { const { getDocument } = await dynamicImportPdfJs(); const pdf = await getDocument({ data: pdfBuffer }).promise; const images: PdfPageImage[] = []; diff --git a/src/modules/dblobs/dblobs.db.ts b/src/modules/dblobs/dblobs.db.ts index da739dafd..6c44fcc5f 100644 --- a/src/modules/dblobs/dblobs.db.ts +++ b/src/modules/dblobs/dblobs.db.ts @@ -48,6 +48,31 @@ export async function deleteDBlobItem(id: string) { } +// Specific item types +async function getImageItemById(id: string) { + return await getItemById(id); +} + +export async function getImageDataURLById(id: string) { + const item = await getImageItemById(id); + return item ? `data:${item.data.mimeType};base64,${item.data.base64}` : null; +} + +export async function getImageBlobURLById(id: string) { + const item = await getImageItemById(id); + if (item) { + const byteCharacters = atob(item.data.base64); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: item.data.mimeType }); + return URL.createObjectURL(blob); + } + return null; +} + // Example usage: async function getAllImages(): Promise { return await getDBlobItemsByType(DBlobMetaDataType.IMAGE);