Allow new attachments for previous messages in a chat. Fixes #945

This commit is contained in:
Enrico Ros
2026-03-03 20:12:12 -08:00
parent 2690380bfd
commit 741980adfc
4 changed files with 74 additions and 28 deletions
@@ -753,16 +753,16 @@ export function Composer(props: {
{recognitionState.isAvailable && <ButtonMicMemo variant={micVariant} color={micColor === 'danger' ? 'danger' : showTint || micColor} errorMessage={recognitionState.errorMessage} onClick={handleToggleMic} />}
{/* [mobile] Attach file button (in draw with image mode) */}
{showChatAttachments === 'only-images' && <ButtonAttachFilesMemo color={showTint} isMobile onAttachFiles={handleAttachFiles} fullWidth multiple />}
{showChatAttachments === 'only-images' && <ButtonAttachFilesMemo color={showTint} isMobile onAttachFiles={handleAttachFiles} multiple />}
{/* [mobile] [+] attachment sources menu */}
{showChatAttachments === true && (
<AttachmentSourcesMemo
mode='menu-compact'
canBrowse={hasComposerBrowseCapability}
hasScreenCapture={false}
hasCamera={true}
onlyImages={false}
hasScreenCapture={supportsScreenCapture}
hasCamera={supportsCameraCapture()}
onlyImages={false /* because if yes, we only show the attach files above */}
onAttachClipboard={attachAppendClipboardItems}
onAttachFiles={handleAttachFiles}
onAttachScreenCapture={handleAttachScreenCapture}
@@ -850,7 +850,7 @@ export function ChatMessage(props: {
)}
{/* [Edit Mode] Add new attachments (right below the Document Fragments) */}
{isEditingText && !!onMessageFragmentAppend && (
{isEditingText && !fromAssistant && !!onMessageFragmentAppend && (
<ChatMessageEditAttachments
ref={attachmentsEditRef}
isMobile={props.isMobile}
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Box } from '@mui/joy';
import type { SxProps } from '@mui/joy/styles/types';
import { Sheet } from '@mui/joy';
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
@@ -27,6 +28,34 @@ export interface EditModeAttachmentsHandle {
}
const _styles = {
box: {
overflow: 'hidden',
p: 0.5,
// looks - exactly from BoxTextArea - the Text editor
boxShadow: 'inset 1px 0px 3px -2px var(--joy-palette-warning-softColor)',
outline: '1px solid',
outlineColor: 'var(--joy-palette-warning-solidBg)',
borderRadius: 'sm',
// layout
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 1,
// shade to the buttons inside this > div > div > button
'& > div > div > button': {
// backgroundColor: 'warning.softActiveBg',
borderColor: 'warning.outlinedBorder',
borderRadius: 'sm',
boxShadow: 'sm',
},
},
} as const satisfies Record<string, SxProps>;
/**
* Encapsulates all attachment wiring for ChatMessage edit mode.
* Owns a standalone attachment drafts store (one per edit session).
@@ -87,14 +116,15 @@ export const ChatMessageEditAttachments = React.forwardRef<EditModeAttachmentsHa
return <>
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 1 }}>
<Sheet color='warning' variant='soft' sx={_styles.box}>
{/* [+] Attachment Sources menu */}
<AttachmentSourcesMemo
mode='menu-compact'
canBrowse={browseCapability.inComposer}
mode='menu-message'
canBrowse={browseCapability.mayWork}
hasScreenCapture={supportsScreenCapture}
hasCamera={supportsCameraCapture()}
// onlyImages={showAttachOnlyImages}
onAttachClipboard={attachAppendClipboardItems}
onAttachFiles={handleAttachFiles}
onAttachScreenCapture={handleAttachScreenCapture}
@@ -104,7 +134,7 @@ export const ChatMessageEditAttachments = React.forwardRef<EditModeAttachmentsHa
/>
{/* Attachment Drafts list */}
{attachmentDrafts.length > 0 && (
{attachmentDrafts.length > 0 ? (
<AttachmentDraftsList
attachmentDraftsStoreApi={storeApiRef.current!}
attachmentDrafts={attachmentDrafts}
@@ -112,9 +142,9 @@ export const ChatMessageEditAttachments = React.forwardRef<EditModeAttachmentsHa
renderDocViewer={renderDocViewer}
renderImageViewer={renderImageViewer}
/>
)}
) : null}
</Box>
</Sheet>
{/* Modal portals */}
{webInputDialogComponent}
@@ -155,7 +155,7 @@ export const AttachmentSourcesMemo = React.memo(AttachmentSources);
function AttachmentSources(props: {
// mode
mode: 'menu-compact' | 'menu-rich' | 'inline-buttons',
mode: 'menu-compact' | 'menu-rich' | 'inline-buttons' | 'menu-message',
color?: ColorPaletteProp, // menu-rich and inline-buttons
richButtonStandOut?: boolean, // menu-rich only
// source availability - note that hasGoogleDriveCapability is local
@@ -206,7 +206,7 @@ function AttachmentSources(props: {
return <>
{/* Files */}
<ButtonAttachFilesMemo color={props.color} onAttachFiles={props.onAttachFiles} fullWidth multiple />
<ButtonAttachFilesMemo color={props.color} onAttachFiles={props.onAttachFiles} /*fullWidth*/ multiple />
{/* Web */}
{!props.onlyImages && <ButtonAttachWebMemo color={props.color} disabled={!props.canBrowse} onOpenWebInput={props.onOpenWebInput} />}
@@ -235,13 +235,28 @@ function AttachmentSources(props: {
// menu-compact mode (mobile) — simple icon trigger with flat menu items
if (props.mode === 'menu-compact')
if (props.mode === 'menu-compact' || props.mode === 'menu-message') {
const isMessage = props.mode === 'menu-message';
return <>
<Dropdown>
<MenuButton slots={{ root: IconButton }}>
<AddRoundedIcon />
</MenuButton>
{!isMessage ? (
<MenuButton slots={{ root: IconButton }}>
<AddRoundedIcon />
</MenuButton>
) : (
<MenuButton slots={{ root: Button }} slotProps={{
root: {
size: 'sm',
variant: 'soft',
color: 'warning',
startDecorator: <AddRoundedIcon />,
sx: { minHeight: '2.25rem', m: -0.25 /* absorb parent's padding */ },
},
} as const}>
Attach
</MenuButton>
)}
<Menu sx={{ '--List-padding': '0.5rem' }}>
{/* Files */}
@@ -252,7 +267,7 @@ function AttachmentSources(props: {
<RichMenuItem name={props.onlyImages ? 'Images' : 'Files'} description='PDF, DOCX, images, code' color={props.color} icon={<AttachFileRoundedIcon />} onClick={handleAttachFilePicker} />
{/* Web */}
{!props.onlyImages && (
{!props.onlyImages && /*props.canBrowse &&*/ (
// <MenuItem onClick={props.onOpenWebInput} disabled={!props.canBrowse}>
// <ListItemDecorator><LanguageRoundedIcon /></ListItemDecorator>
// Web
@@ -288,21 +303,22 @@ function AttachmentSources(props: {
)}
{/* Camera */}
{/*{props.hasCamera && (*/}
{/* // <MenuItem onClick={props.onOpenCamera}>*/}
{/* // <ListItemDecorator><CameraAltOutlinedIcon /></ListItemDecorator>*/}
{/* // Camera*/}
{/* // </MenuItem>*/}
{/* <RichMenuItem name='Camera' description='Capture photos and optional OCR' color={props.color} icon={<CameraAltOutlinedIcon />} onClick={props.onOpenCamera} />*/}
{/*)}*/}
{props.hasCamera && isMessage && (
// <MenuItem onClick={props.onOpenCamera}>
// <ListItemDecorator><CameraAltOutlinedIcon /></ListItemDecorator>
// Camera
// </MenuItem>
<RichMenuItem name='Camera' description='Capture photos and optional OCR' color={props.color} icon={<CameraAltOutlinedIcon />} onClick={props.onOpenCamera} />
)}
</Menu>
</Dropdown>
{/* [mobile] Responsive Camera OCR button */}
{props.hasCamera && <ButtonAttachCameraMemo isMobile color={props.color} onOpenCamera={props.onOpenCamera} />}
{props.hasCamera && !isMessage && <ButtonAttachCameraMemo isMobile color={props.color} onOpenCamera={props.onOpenCamera} />}
</>;
}
// menu-rich mode (desktop) — labeled button trigger with animated, descriptive menu items
@@ -369,7 +385,7 @@ function AttachmentSources(props: {
/>
{/* Web/URL Attachment */}
{!props.onlyImages && (
{!props.onlyImages && /*props.canBrowse &&*/ (
<RichMenuItem
name='Web'
icon={<LanguageRoundedIcon />}