BlockPartHostedResource: enable always-embed

This commit is contained in:
Enrico Ros
2026-04-10 03:16:26 -07:00
parent 685b5c5130
commit 517c18c902
2 changed files with 76 additions and 26 deletions
@@ -1,7 +1,7 @@
import * as React from 'react';
import TimeAgo from 'react-timeago';
import { Box, CircularProgress, Dropdown, IconButton, ListDivider, ListItemDecorator, Menu, MenuButton, MenuItem, Sheet, Typography } from '@mui/joy';
import { Box, Checkbox, CircularProgress, Dropdown, IconButton, ListDivider, ListItemDecorator, Menu, MenuButton, MenuItem, Sheet, Typography } from '@mui/joy';
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
@@ -21,6 +21,7 @@ import { copyBlobPromiseToClipboard, copyToClipboard } from '~/common/util/clipb
import { downloadBlob } from '~/common/util/downloadUtils';
import { humanReadableBytes } from '~/common/util/textUtils';
import { mimeTypeIsPlainText, mimeTypeIsSupportedImage } from '~/common/attachment-drafts/attachment.mimetypes';
import { useAIPreferencesStore } from '~/common/stores/store-ai';
import { useLlmServiceAccess } from '~/common/stores/llms/hooks/useLlmServiceAccess';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
@@ -63,6 +64,7 @@ function AnthropicFileChip(props: {
const { access, fileId, onFragmentDelete, onFragmentReplace } = props;
// external state
const autoEmbedEnabled = useAIPreferencesStore(state => state.vndAntInlineFiles !== 'off');
const { data: metadata, isLoading: metaLoading, error: metaError } = apiQuery.llmAnthropic.fileApiGetMetadata.useQuery({ access, fileId }, {
staleTime: Infinity,
select: _enrichMetadataWithMimeFlags,
@@ -152,29 +154,29 @@ function AnthropicFileChip(props: {
fence += '`';
onFragmentReplace(createTextContentFragment(`${fence}${fileName}\n${text}\n${fence}\n`));
}
// image: get dimensions, store in DBlob, and create a Zync asset reference
// else if (data.httpMimeIsImage) {
//
// const { width, height } = await imageBlobGetDimensions(data.blob).catch(() => ({ width: 0, height: 0 }));
//
// const dblobAssetId = await addDBImageAsset('app-chat', data.blob, {
// label: fileName,
// origin: { ot: 'generated', source: 'ai-text-to-image', generatorName: 'anthropic-code-execution', prompt: '', parameters: {}, generatedAt: new Date().toISOString() },
// metadata: { width, height },
// });
//
// onFragmentReplace(createZyncAssetReferenceContentFragment(
// nanoidToUuidV4(dblobAssetId, 'convert-dblob-to-dasset'),
// fileName,
// 'image',
// {
// pt: 'image_ref',
// dataRef: createDMessageDataRefDBlob(dblobAssetId, data.httpMimeType, data.blob.size),
// ...(fileName ? { altText: fileName } : {}),
// ...(width ? { width } : {}),
// ...(height ? { height } : {}),
// },
// ));
// image: get dimensions, store in DBlob, and create a Zync asset reference
// else if (data.httpMimeIsImage) {
//
// const { width, height } = await imageBlobGetDimensions(data.blob).catch(() => ({ width: 0, height: 0 }));
//
// const dblobAssetId = await addDBImageAsset('app-chat', data.blob, {
// label: fileName,
// origin: { ot: 'generated', source: 'ai-text-to-image', generatorName: 'anthropic-code-execution', prompt: '', parameters: {}, generatedAt: new Date().toISOString() },
// metadata: { width, height },
// });
//
// onFragmentReplace(createZyncAssetReferenceContentFragment(
// nanoidToUuidV4(dblobAssetId, 'convert-dblob-to-dasset'),
// fileName,
// 'image',
// {
// pt: 'image_ref',
// dataRef: createDMessageDataRefDBlob(dblobAssetId, data.httpMimeType, data.blob.size),
// ...(fileName ? { altText: fileName } : {}),
// ...(width ? { width } : {}),
// ...(height ? { height } : {}),
// },
// ));
// }
else
return setActionError('Cannot inline this file type');
@@ -189,6 +191,29 @@ function AnthropicFileChip(props: {
}, [fileContent, refetchFileContent, access, fileId, fileName, onFragmentReplace]);
const handleToggleAutoEmbed = React.useCallback(async () => {
if (autoEmbedEnabled)
return useAIPreferencesStore.getState().setVndAntInlineFiles('off');
if (await showPromisedOverlay('chat-message-auto-embed-notice', { rejectWithValue: false }, ({ onResolve, onUserReject }) =>
<ConfirmationModal
open onClose={onUserReject} onPositive={() => onResolve(true)}
noTitleBar
lowStakes
confirmationText={<>
From now on, files generated by Claude tools (code execution, etc.) will be automatically downloaded and embedded into messages, then removed from Anthropic&apos;s File API.
<br /><br />
You can change this anytime in <b>Settings &gt; Chat AI &gt; Anthropic File Inlining</b>.
</>}
positiveActionText='Enable & Embed'
negativeActionText='Cancel'
/>,
)) {
useAIPreferencesStore.getState().setVndAntInlineFiles('inline-file-and-delete');
await handleInline();
}
}, [autoEmbedEnabled, handleInline, showPromisedOverlay]);
const canCopy = !!metadata?.mimeIsText || !!metadata?.mimeIsImage;
const canInline = !!onFragmentReplace && !!metadata?.mimeIsText; // for images, replace with ... && canCopy
@@ -236,6 +261,13 @@ function AnthropicFileChip(props: {
</IconButton>
</GoodTooltip>
)}
{/*{canInline && (*/}
{/* <GoodTooltip title='Embed in chat'>*/}
{/* <IconButton variant='soft' color='primary' disabled={isBusy} onClick={handleInline} size='sm'>*/}
{/* {busy === 'inline' ? <CircularProgress size='sm' /> : <VerticalAlignBottomIcon sx={{ fontSize: 'lg' }} />}*/}
{/* </IconButton>*/}
{/* </GoodTooltip>*/}
{/*)}*/}
<GoodTooltip title='Download file'>
<IconButton variant='soft' color='primary' disabled={isBusy || isFileGone} onClick={handleDownload} size='sm'>
{busy === 'download' ? <CircularProgress size='sm' /> : <DownloadIcon sx={{ fontSize: 'lg' }} />}
@@ -246,12 +278,29 @@ function AnthropicFileChip(props: {
<MenuButton slots={{ root: IconButton }} slotProps={{ root: { variant: 'soft', color: 'primary', size: 'sm', disabled: isBusy && busy !== 'inline' } }}>
{(busy === 'delete' || busy === 'inline') ? <CircularProgress size='sm' /> : <MoreVertIcon sx={{ fontSize: 'lg' }} />}
</MenuButton>
<Menu placement='bottom-end' sx={{ minWidth: 180 }}>
<Menu placement='bottom-end' sx={{ minWidth: 220 }}>
{/* Inline as doc attachment */}
<MenuItem disabled={!canInline || isBusy} onClick={handleInline}>
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
Inline
<div>
Embed
{!canInline && <Typography level='body-xs' sx={{ opacity: 0.6 }}>
File type not supported
</Typography>}
</div>
</MenuItem>
{/* Auto-embed toggle - shared global preference */}
{!autoEmbedEnabled && <>
<MenuItem disabled={!canInline || isBusy} onClick={handleToggleAutoEmbed}>
<ListItemDecorator><Checkbox checked={autoEmbedEnabled} readOnly color='neutral' /></ListItemDecorator>
<div>
Always embed
<Typography level='body-xs' sx={{ opacity: 0.6 }}>
Change anytime in Settings
</Typography>
</div>
</MenuItem>
</>}
{!!onFragmentDelete && <ListDivider />}
{/* Delete from provider */}
{!!onFragmentDelete && (
@@ -25,6 +25,7 @@ export type GlobalOverlayId = // string - disabled so we keep an orderliness
| 'chat-message-delete-confirmation'
| 'chat-message-delete-aux'
| 'chat-message-delete-hosted-resource'
| 'chat-message-auto-embed-notice'
| 'chat-message-inline-aux'
| 'livefile-overwrite'
| 'shortcuts-confirm-close'