mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Attachments: consolidated/unified menu
This commit is contained in:
@@ -2,9 +2,8 @@ import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import type { FileWithHandle } from 'browser-fs-access';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
|
||||
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import type { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Textarea, Typography } from '@mui/joy';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
@@ -57,14 +56,10 @@ import { providerStarredMessages, StarredMessageItem } from './actile/providerSt
|
||||
import { useActileManager } from './actile/useActileManager';
|
||||
|
||||
import type { AttachmentDraftId, AttachmentDraftsAction } from '~/common/attachment-drafts/attachment.types';
|
||||
import { ButtonAttachCameraMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachClipboard';
|
||||
import { ButtonAttachGoogleDriveMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachGoogleDrive';
|
||||
import { ButtonAttachScreenCaptureMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachScreenCapture';
|
||||
import { ButtonAttachWebMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachWeb';
|
||||
import { hasGoogleDriveCapability, useGoogleDrivePicker } from '~/common/attachment-drafts/attachment-sources/useGoogleDrivePicker';
|
||||
import { AttachmentSourcesMemo } from '~/common/attachment-drafts/attachment-sources/AttachmentSources';
|
||||
import { useAttachmentDrafts } from '~/common/attachment-drafts/useAttachmentDrafts';
|
||||
import { useAttachmentDraftsEnrichment } from '~/common/attachment-drafts/llm-enrichment/useAttachmentDraftsEnrichment';
|
||||
import { useGoogleDrivePicker } from '~/common/attachment-drafts/attachment-sources/useGoogleDrivePicker';
|
||||
import { useWebAttachmentModal } from '~/common/attachment-drafts/attachment-sources/useWebAttachmentModal';
|
||||
|
||||
import type { ChatExecuteMode } from '../../execute-mode/execute-mode.types';
|
||||
@@ -791,42 +786,24 @@ export function Composer(props: {
|
||||
{/* [mobile] Mic button */}
|
||||
{recognitionState.isAvailable && <ButtonMicMemo variant={micVariant} color={micColor === 'danger' ? 'danger' : showTint || micColor} errorMessage={recognitionState.errorMessage} onClick={handleToggleMic} />}
|
||||
|
||||
{/* Responsive Camera OCR button */}
|
||||
{showChatAttachments && <ButtonAttachCameraMemo color={showTint} isMobile onOpenCamera={handleOpenCamera} />}
|
||||
|
||||
{/* [mobile] Attach file button (in draw with image mode) */}
|
||||
{showChatAttachments === 'only-images' && <ButtonAttachFilesMemo color={showTint} isMobile onAttachFiles={handleAttachFiles} fullWidth multiple />}
|
||||
|
||||
{/* [mobile] [+] button */}
|
||||
{/* [mobile] [+] attachment sources menu */}
|
||||
{showChatAttachments === true && (
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }}>
|
||||
<AddCircleOutlineIcon />
|
||||
</MenuButton>
|
||||
<Menu>
|
||||
|
||||
{/* Responsive Open Files button */}
|
||||
<MenuItem>
|
||||
<ButtonAttachFilesMemo onAttachFiles={handleAttachFiles} fullWidth multiple />
|
||||
</MenuItem>
|
||||
|
||||
{/* Responsive Web button */}
|
||||
<MenuItem>
|
||||
<ButtonAttachWebMemo disabled={!hasComposerBrowseCapability} onOpenWebInput={openWebInputDialog} />
|
||||
</MenuItem>
|
||||
|
||||
{/* Responsive Google Drive button */}
|
||||
{hasGoogleDriveCapability && <MenuItem>
|
||||
<ButtonAttachGoogleDriveMemo onOpenGoogleDrivePicker={openGoogleDrivePicker} fullWidth />
|
||||
</MenuItem>}
|
||||
|
||||
{/* Responsive Paste button */}
|
||||
{supportsClipboardRead() && <MenuItem>
|
||||
<ButtonAttachClipboardMemo onAttachClipboard={attachAppendClipboardItems} />
|
||||
</MenuItem>}
|
||||
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
<AttachmentSourcesMemo
|
||||
mode='menu-compact'
|
||||
canBrowse={hasComposerBrowseCapability}
|
||||
hasScreenCapture={false}
|
||||
hasCamera={true}
|
||||
onlyImages={false}
|
||||
onAttachClipboard={attachAppendClipboardItems}
|
||||
onAttachFiles={handleAttachFiles}
|
||||
onAttachScreenCapture={handleAttachScreenCapture}
|
||||
onOpenCamera={handleOpenCamera}
|
||||
onOpenGoogleDrivePicker={openGoogleDrivePicker}
|
||||
onOpenWebInput={openWebInputDialog}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* [Mobile] MultiChat button */}
|
||||
@@ -837,31 +814,26 @@ export function Composer(props: {
|
||||
|
||||
{/* [Desktop, Col1] Insert Multi-modal content buttons */}
|
||||
{isDesktop && showChatAttachments && (
|
||||
<Box sx={{ flexGrow: 0, display: 'grid', gap: (labsAttachScreenCapture && labsCameraDesktop) ? 0.5 : 1, alignSelf: 'flex-start' }}>
|
||||
<Box sx={{ flexGrow: 0, display: 'grid', gap: 0.5, alignSelf: 'flex-start' }}>
|
||||
|
||||
{/*<FormHelperText sx={{ mx: 'auto' }}>*/}
|
||||
{/* Attach*/}
|
||||
{/*</FormHelperText>*/}
|
||||
{/* [desktop] Attachment Sources: inline buttons */}
|
||||
<AttachmentSourcesMemo
|
||||
mode='inline-buttons'
|
||||
color={showTint}
|
||||
canBrowse={hasComposerBrowseCapability}
|
||||
hasScreenCapture={labsAttachScreenCapture && supportsScreenCapture}
|
||||
hasCamera={labsCameraDesktop && supportsCameraCapture()}
|
||||
onlyImages={showChatAttachments === 'only-images'}
|
||||
onAttachClipboard={attachAppendClipboardItems}
|
||||
onAttachFiles={handleAttachFiles}
|
||||
onAttachScreenCapture={handleAttachScreenCapture}
|
||||
onOpenCamera={handleOpenCamera}
|
||||
onOpenGoogleDrivePicker={openGoogleDrivePicker}
|
||||
onOpenWebInput={openWebInputDialog}
|
||||
/>
|
||||
|
||||
{/* Responsive Open Files button */}
|
||||
<ButtonAttachFilesMemo color={showTint} onAttachFiles={handleAttachFiles} fullWidth multiple />
|
||||
|
||||
{/* Responsive Web button */}
|
||||
{showChatAttachments !== 'only-images' && <ButtonAttachWebMemo color={showTint} disabled={!hasComposerBrowseCapability} onOpenWebInput={openWebInputDialog} />}
|
||||
|
||||
{/* Responsive Google Drive button */}
|
||||
{hasGoogleDriveCapability && showChatAttachments !== 'only-images' && <ButtonAttachGoogleDriveMemo color={showTint} onOpenGoogleDrivePicker={openGoogleDrivePicker} />}
|
||||
|
||||
{/* Responsive Paste button */}
|
||||
{supportsClipboardRead() && showChatAttachments !== 'only-images' && <ButtonAttachClipboardMemo color={showTint} onAttachClipboard={attachAppendClipboardItems} />}
|
||||
|
||||
{/* Responsive Screen Capture button */}
|
||||
{labsAttachScreenCapture && supportsScreenCapture && <ButtonAttachScreenCaptureMemo color={showTint} onAttachScreenCapture={handleAttachScreenCapture} />}
|
||||
|
||||
{/* Responsive Camera OCR button */}
|
||||
{labsCameraDesktop && supportsCameraCapture() && <ButtonAttachCameraMemo color={showTint} onOpenCamera={handleOpenCamera} />}
|
||||
|
||||
</Box>)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{/* Top: Textarea & Mic & Overlays, Bottom, Attachment Drafts */}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
import * as React from 'react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import type { FileWithHandle } from 'browser-fs-access';
|
||||
|
||||
import { Box, Button, Checkbox, ColorPaletteProp, Divider, Dropdown, IconButton, ListItem, ListItemDecorator, Menu, MenuButton, MenuItem } from '@mui/joy';
|
||||
import AddRoundedIcon from '@mui/icons-material/AddRounded';
|
||||
import AddToDriveRoundedIcon from '@mui/icons-material/AddToDriveRounded';
|
||||
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
||||
import CameraAltOutlinedIcon from '@mui/icons-material/CameraAltOutlined';
|
||||
import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
|
||||
import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded';
|
||||
import ScreenshotMonitorIcon from '@mui/icons-material/ScreenshotMonitor';
|
||||
|
||||
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ButtonAttachFilesMemo, openFileForAttaching } from '~/common/components/ButtonAttachFiles';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { takeScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
|
||||
import { ButtonAttachCameraMemo } from './ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './ButtonAttachClipboard';
|
||||
import { ButtonAttachGoogleDriveMemo } from './ButtonAttachGoogleDrive';
|
||||
import { ButtonAttachScreenCaptureMemo } from './ButtonAttachScreenCapture';
|
||||
import { ButtonAttachWebMemo } from './ButtonAttachWeb';
|
||||
import { hasGoogleDriveCapability } from './useGoogleDrivePicker';
|
||||
|
||||
|
||||
// configuration
|
||||
export const ATTACH_BUTTON_RADIUS = '18px'; // for the rich (non-compact) menu button
|
||||
|
||||
|
||||
// animations for the rich (non-compact) menu
|
||||
const animationMenu = keyframes` from {opacity: 0;} to {opacity: 1;}`;
|
||||
const animationMenuItem = keyframes` from {opacity: 0;transform: translateY(-6px);} to {opacity: 1;transform: translateY(0);}`;
|
||||
|
||||
const _style = {
|
||||
menuItem: {
|
||||
py: 1,
|
||||
// pl: 3,
|
||||
// pr: 2,
|
||||
minHeight: 60,
|
||||
} as const,
|
||||
menuItemContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0.125,
|
||||
} as const,
|
||||
menuItemName: {
|
||||
typography: 'title-sm',
|
||||
fontWeight: 600,
|
||||
// fontSize: '15px',
|
||||
} as const,
|
||||
menuItemDescription: {
|
||||
fontSize: 'xs',
|
||||
color: 'text.tertiary',
|
||||
fontWeight: 400,
|
||||
} as const,
|
||||
};
|
||||
|
||||
|
||||
// Rich menu item (used in menu-rich mode)
|
||||
function RichMenuItem(props: {
|
||||
name: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
delay?: number;
|
||||
disabled?: boolean;
|
||||
color?: ColorPaletteProp;
|
||||
}) {
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
color={props.color}
|
||||
sx={!props.delay ? _style.menuItem : {
|
||||
..._style.menuItem,
|
||||
animation: `${animationMenuItem} 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${props.delay}s both`,
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
{props.icon}
|
||||
</ListItemDecorator>
|
||||
<Box sx={_style.menuItemContent}>
|
||||
<Box sx={_style.menuItemName}>
|
||||
{props.name}
|
||||
</Box>
|
||||
<Box sx={_style.menuItemDescription}>
|
||||
{props.description}
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Auto-download toggle (shown when browsing capability exists)
|
||||
function AutoDownloadToggle(props: { delay?: number }) {
|
||||
|
||||
// external state
|
||||
const enableComposerAttach = useBrowseStore(s => s.enableComposerAttach);
|
||||
|
||||
const handleToggle = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
useBrowseStore.getState().setEnableComposerAttach(event.target.checked);
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
|
||||
<ListItem
|
||||
sx={{
|
||||
..._style.menuItem,
|
||||
animation: `${animationMenuItem} 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${props.delay}s both`,
|
||||
}}
|
||||
// onClick={(event) => {
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// setEnableComposerAttach(!enableComposerAttach);
|
||||
// }}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
color='neutral'
|
||||
checked={enableComposerAttach}
|
||||
onChange={handleToggle}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
sx={{ ml: 0.375 }}
|
||||
/>
|
||||
</ListItemDecorator>
|
||||
<Box sx={_style.menuItemContent}>
|
||||
<Box sx={{ typography: 'title-sm' }}>
|
||||
Attach pasted URLs
|
||||
</Box>
|
||||
<Box sx={_style.menuItemDescription}>
|
||||
Download and attach pasted web links
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Portable attachment sources component.
|
||||
*
|
||||
* Three modes:
|
||||
* - **menu-compact**: Mobile-style — icon trigger, simple MenuItems (no descriptions/animations)
|
||||
* - **menu-rich**: Desktop-style — labeled button trigger, rich items with descriptions and animations
|
||||
* - **inline-buttons**: Individual source buttons rendered inline (no dropdown)
|
||||
*/
|
||||
export const AttachmentSourcesMemo = React.memo(AttachmentSources);
|
||||
|
||||
function AttachmentSources(props: {
|
||||
// mode
|
||||
mode: 'menu-compact' | 'menu-rich' | 'inline-buttons',
|
||||
color?: ColorPaletteProp, // menu-rich and inline-buttons
|
||||
richButtonStandOut?: boolean, // menu-rich only
|
||||
// source availability - note that hasGoogleDriveCapability is local
|
||||
canBrowse: boolean, // whether browsing is available (for Web button and showing the auto-attach toggle)
|
||||
hasCamera: boolean,
|
||||
// hasGoogleDrive: boolean, // it's now local: hasGoogleDriveCapability
|
||||
hasScreenCapture: boolean,
|
||||
// configuration
|
||||
onlyImages?: boolean, // makes clipboard/drive/web unavailable
|
||||
// callbacks
|
||||
onAttachClipboard: () => void,
|
||||
onAttachFiles: (files: FileWithHandle[], errorMessage: string | null) => void,
|
||||
onAttachScreenCapture: (file: File) => void,
|
||||
onOpenCamera: () => void,
|
||||
onOpenGoogleDrivePicker?: () => void, // optional because requires additional external setup (e.g. user-storage of tokens)
|
||||
onOpenWebInput: () => void,
|
||||
}) {
|
||||
|
||||
// state (screen capture — used in menu modes where the component handles the capture)
|
||||
const [capturingScreen, setCapturingScreen] = React.useState(false);
|
||||
const [screenCaptureError, setScreenCaptureError] = React.useState<string | null>(null);
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
const { onAttachFiles, onAttachScreenCapture } = props;
|
||||
|
||||
const handleAttachFilePicker = React.useCallback(() => {
|
||||
return openFileForAttaching(true, onAttachFiles);
|
||||
}, [onAttachFiles]);
|
||||
|
||||
const handleTakeScreenCapture = React.useCallback(async () => {
|
||||
setScreenCaptureError(null);
|
||||
setCapturingScreen(true);
|
||||
try {
|
||||
const file = await takeScreenCapture();
|
||||
file && onAttachScreenCapture(file);
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setScreenCaptureError(message);
|
||||
}
|
||||
setCapturingScreen(false);
|
||||
}, [onAttachScreenCapture]);
|
||||
|
||||
|
||||
// inline-buttons mode — individual buttons rendered flat (no dropdown)
|
||||
if (props.mode === 'inline-buttons')
|
||||
return <>
|
||||
|
||||
{/* Files */}
|
||||
<ButtonAttachFilesMemo color={props.color} onAttachFiles={props.onAttachFiles} fullWidth multiple />
|
||||
|
||||
{/* Web */}
|
||||
{!props.onlyImages && <ButtonAttachWebMemo color={props.color} disabled={!props.canBrowse} onOpenWebInput={props.onOpenWebInput} />}
|
||||
|
||||
{/* Google Drive */}
|
||||
{hasGoogleDriveCapability && !props.onlyImages && !!props.onOpenGoogleDrivePicker && (
|
||||
<ButtonAttachGoogleDriveMemo color={props.color} onOpenGoogleDrivePicker={props.onOpenGoogleDrivePicker} />
|
||||
)}
|
||||
|
||||
{/* Clipboard */}
|
||||
{supportsClipboardRead() && !props.onlyImages && (
|
||||
<ButtonAttachClipboardMemo color={props.color} onAttachClipboard={props.onAttachClipboard} />
|
||||
)}
|
||||
|
||||
{/* Screen Capture */}
|
||||
{props.hasScreenCapture && (
|
||||
<ButtonAttachScreenCaptureMemo color={props.color} onAttachScreenCapture={props.onAttachScreenCapture} />
|
||||
)}
|
||||
|
||||
{/* Camera */}
|
||||
{props.hasCamera && (
|
||||
<ButtonAttachCameraMemo color={props.color} onOpenCamera={props.onOpenCamera} />
|
||||
)}
|
||||
|
||||
</>;
|
||||
|
||||
|
||||
// menu-compact mode (mobile) — simple icon trigger with flat menu items
|
||||
if (props.mode === 'menu-compact')
|
||||
return <>
|
||||
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }}>
|
||||
<AddRoundedIcon />
|
||||
</MenuButton>
|
||||
<Menu sx={{ '--List-padding': '0.5rem' }}>
|
||||
|
||||
{/* Files */}
|
||||
{/*<MenuItem onClick={handleAttachFilePicker}>*/}
|
||||
{/* <ListItemDecorator><AttachFileRoundedIcon /></ListItemDecorator>*/}
|
||||
{/* {props.onlyImages ? 'Images' : 'File'}*/}
|
||||
{/*</MenuItem>*/}
|
||||
<RichMenuItem name={props.onlyImages ? 'Images' : 'Files'} description='PDF, DOCX, images, code' color={props.color} icon={<AttachFileRoundedIcon />} onClick={handleAttachFilePicker} />
|
||||
|
||||
{/* Web */}
|
||||
{!props.onlyImages && (
|
||||
// <MenuItem onClick={props.onOpenWebInput} disabled={!props.canBrowse}>
|
||||
// <ListItemDecorator><LanguageRoundedIcon /></ListItemDecorator>
|
||||
// Web
|
||||
// </MenuItem>
|
||||
<RichMenuItem name='Web' description='Import from web pages' color={props.color} icon={<LanguageRoundedIcon />} onClick={props.onOpenWebInput} disabled={!props.canBrowse} />
|
||||
)}
|
||||
|
||||
{/* Google Drive */}
|
||||
{!props.onlyImages && hasGoogleDriveCapability && !!props.onOpenGoogleDrivePicker && (
|
||||
// <MenuItem onClick={props.onOpenGoogleDrivePicker}>
|
||||
// <ListItemDecorator><AddToDriveRoundedIcon /></ListItemDecorator>
|
||||
// Drive
|
||||
// </MenuItem>
|
||||
<RichMenuItem name='Drive' description='Attach Google Drive files' color={props.color} icon={<AddToDriveRoundedIcon />} onClick={props.onOpenGoogleDrivePicker} />
|
||||
)}
|
||||
|
||||
{/* Clipboard */}
|
||||
{!props.onlyImages && supportsClipboardRead() && (
|
||||
// <MenuItem onClick={props.onAttachClipboard}>
|
||||
// <ListItemDecorator><ContentPasteGoIcon /></ListItemDecorator>
|
||||
// Paste
|
||||
// </MenuItem>
|
||||
<RichMenuItem name='Clipboard' description='Auto-convert to the best format' color={props.color} icon={<ContentPasteGoIcon />} onClick={props.onAttachClipboard} />
|
||||
)}
|
||||
|
||||
{/* Screen Capture */}
|
||||
{props.hasScreenCapture && (
|
||||
// <MenuItem onClick={handleTakeScreenCapture} disabled={capturingScreen}>
|
||||
// <ListItemDecorator><ScreenshotMonitorIcon /></ListItemDecorator>
|
||||
// Screen
|
||||
// </MenuItem>
|
||||
<RichMenuItem name='Screen' description={screenCaptureError ? `Error: ${screenCaptureError}` : 'Capture windows, tabs, or screens'} color={screenCaptureError ? 'danger' : props.color} icon={<ScreenshotMonitorIcon />} onClick={handleTakeScreenCapture} disabled={capturingScreen} />
|
||||
)}
|
||||
|
||||
{/* 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} />*/}
|
||||
{/*)}*/}
|
||||
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
|
||||
{/* [mobile] Responsive Camera OCR button */}
|
||||
{props.hasCamera && <ButtonAttachCameraMemo isMobile color={props.color} onOpenCamera={props.onOpenCamera} />}
|
||||
|
||||
</>;
|
||||
|
||||
|
||||
// menu-rich mode (desktop) — labeled button trigger with animated, descriptive menu items
|
||||
return (
|
||||
<Dropdown>
|
||||
<MenuButton
|
||||
slots={{ root: Button }}
|
||||
slotProps={{
|
||||
root: {
|
||||
// size: 'sm',
|
||||
variant: 'plain',
|
||||
color: props.color,
|
||||
startDecorator: <AddRoundedIcon />,
|
||||
fullWidth: true, // to match other buttons in the col
|
||||
sx: {
|
||||
minWidth: 100,
|
||||
justifyContent: 'flex-start',
|
||||
borderRadius: ATTACH_BUTTON_RADIUS,
|
||||
textWrap: 'nowrap',
|
||||
...(props.richButtonStandOut && {
|
||||
backgroundColor: 'background.popup',
|
||||
border: '1px solid',
|
||||
borderColor: `${props.color || 'neutral'}.outlinedBorder`,
|
||||
}),
|
||||
// when aria-expanded is true (menu open), remove top border radius
|
||||
'&[aria-expanded="true"]': {
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
backgroundColor: `${props.color || 'neutral'}.softHoverBg`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
Attach
|
||||
</MenuButton>
|
||||
|
||||
<Menu
|
||||
// variant='soft'
|
||||
color={props.color}
|
||||
placement='top-start'
|
||||
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [-10 /* 62 */, -2] } }] }}
|
||||
sx={{
|
||||
minWidth: 280,
|
||||
'--List-padding': '0.5rem',
|
||||
animation: `${animationMenu} 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
|
||||
// boxShadow: '0 16px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
||||
boxShadow: 'md',
|
||||
borderRadius: ATTACH_BUTTON_RADIUS,
|
||||
border: '1px solid',
|
||||
borderColor: `${props.color || 'neutral'}.outlinedBorder`,
|
||||
backgroundColor: 'background.popup',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
||||
{/* File Attachment */}
|
||||
<RichMenuItem
|
||||
name={props.onlyImages ? 'Images' : 'Files'}
|
||||
icon={<AttachFileRoundedIcon />}
|
||||
description={props.onlyImages ? 'PNG, JPG, WEBP images to edit' : 'PDF, DOCX, images, code'}
|
||||
onClick={handleAttachFilePicker}
|
||||
delay={0}
|
||||
/>
|
||||
|
||||
{/* Web/URL Attachment */}
|
||||
{!props.onlyImages && (
|
||||
<RichMenuItem
|
||||
name='Web'
|
||||
icon={<LanguageRoundedIcon />}
|
||||
description='Import from websites, including screenshots'
|
||||
onClick={props.onOpenWebInput}
|
||||
disabled={!props.canBrowse}
|
||||
delay={0.02}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Google Drive Attachment */}
|
||||
{!props.onlyImages && hasGoogleDriveCapability && !!props.onOpenGoogleDrivePicker && (
|
||||
<RichMenuItem
|
||||
name='Drive'
|
||||
icon={<AddToDriveRoundedIcon />}
|
||||
description='Attach Google Drive files'
|
||||
onClick={props.onOpenGoogleDrivePicker}
|
||||
delay={0.04}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Clipboard Attachment */}
|
||||
{!props.onlyImages && supportsClipboardRead() && (
|
||||
<RichMenuItem
|
||||
name='Clipboard'
|
||||
icon={<ContentPasteGoIcon />}
|
||||
description='Auto-converts images and text to the best format'
|
||||
onClick={props.onAttachClipboard}
|
||||
delay={0.06}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Divider before labs features */}
|
||||
{(props.hasScreenCapture || props.hasCamera) && <Divider sx={{ my: 0.5 }} />}
|
||||
|
||||
{/* Screen Capture */}
|
||||
{props.hasScreenCapture && (
|
||||
<RichMenuItem
|
||||
name='Screen'
|
||||
icon={<ScreenshotMonitorIcon />}
|
||||
description={screenCaptureError ? `Error: ${screenCaptureError}` : 'Capture windows, tabs, or screens'}
|
||||
onClick={handleTakeScreenCapture}
|
||||
disabled={capturingScreen}
|
||||
color={screenCaptureError ? 'danger' : undefined}
|
||||
delay={0.08}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Camera */}
|
||||
{props.hasCamera && (
|
||||
<RichMenuItem
|
||||
name='Camera'
|
||||
icon={<CameraAltOutlinedIcon />}
|
||||
description='Capture photos with optional text recognition'
|
||||
onClick={props.onOpenCamera}
|
||||
delay={0.1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* URL Auto-Download Toggle - only show when browse capability exists */}
|
||||
{!props.onlyImages && props.canBrowse && (
|
||||
<AutoDownloadToggle delay={0.12} />
|
||||
)}
|
||||
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user