mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
ERC x LFS: workspace supports d/d
This commit is contained in:
+4
-14
@@ -5,7 +5,7 @@ import { Box, Button, ColorPaletteProp, SvgIcon } from '@mui/joy';
|
||||
import UploadFileRoundedIcon from '@mui/icons-material/UploadFileRounded';
|
||||
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { getDataTransferFilesOrPromises } from '~/common/util/fileSystemUtils';
|
||||
import { getFirstFileSystemFileHandle } from '~/common/util/fileSystemUtils';
|
||||
import { useDragDropDataTransfer } from '~/common/components/useDragDropDataTransfer';
|
||||
|
||||
import { LiveFileChooseIcon, LiveFileIcon } from '~/common/livefile/liveFile.icons';
|
||||
@@ -41,19 +41,9 @@ export function LiveFileControlButton(props: {
|
||||
// state
|
||||
|
||||
const handleDataTransfer = React.useCallback(async (dataTransfer: DataTransfer) => {
|
||||
// get FileSystemFileHandle objects from the DataTransfer
|
||||
const fileOrFSHandlePromises = getDataTransferFilesOrPromises(dataTransfer.items, false);
|
||||
if (!fileOrFSHandlePromises.length)
|
||||
return;
|
||||
|
||||
// resolve the promises to get the actual files/handles
|
||||
const filesOrHandles = await Promise.all(fileOrFSHandlePromises);
|
||||
for (let filesOrHandle of filesOrHandles) {
|
||||
if (!(filesOrHandle instanceof File) && filesOrHandle?.kind === 'file' && filesOrHandle) {
|
||||
await onPairWithFSFHandle(filesOrHandle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fsfHandle = await getFirstFileSystemFileHandle(dataTransfer);
|
||||
if (fsfHandle)
|
||||
await onPairWithFSFHandle(fsfHandle);
|
||||
}, [onPairWithFSFHandle]);
|
||||
|
||||
const { dragContainerSx, dropComponent, handleContainerDragEnter, handleContainerDragStart } =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
|
||||
import { Box, Button, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, SvgIcon, Typography } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
|
||||
@@ -8,10 +8,13 @@ import type { LiveFileId, LiveFileMetadata } from '~/common/livefile/liveFile.ty
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { LiveFileChooseIcon } from '~/common/livefile/liveFile.icons';
|
||||
import { LiveFilePatchIcon } from '~/common/components/icons/LiveFilePatchIcon';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { getFirstFileSystemFileHandle } from '~/common/util/fileSystemUtils';
|
||||
import { useDragDropDataTransfer } from '~/common/components/useDragDropDataTransfer';
|
||||
|
||||
import type { DWorkspaceId } from './workspace.types';
|
||||
import { useContextWorkspaceId } from './WorkspaceIdProvider';
|
||||
import { useWorkspaceContentsMetadata } from './useWorkspaceContentsMetadata';
|
||||
import { useContextWorkspaceId } from '~/common/stores/workspace/WorkspaceIdProvider';
|
||||
|
||||
|
||||
// configuration
|
||||
@@ -22,13 +25,14 @@ const ENABLE_AUTO_WORKSPACE_PICK = false;
|
||||
* Allows selection of LiveFiles in the current Workspace
|
||||
*/
|
||||
export function WorkspaceLiveFilePicker(props: {
|
||||
autoSelectName: string | null;
|
||||
buttonLabel: string;
|
||||
liveFileId: LiveFileId | null;
|
||||
allowRemove?: boolean;
|
||||
autoSelectName: string | null;
|
||||
labelButton: string;
|
||||
labelTooltip?: string;
|
||||
liveFileId: LiveFileId | null;
|
||||
onSelectLiveFile: (id: LiveFileId | null) => void;
|
||||
onSelectNewFile?: (workspaceId: DWorkspaceId | null) => void;
|
||||
// tooltipLabel?: string;
|
||||
onSelectFileOpen?: (workspaceId: DWorkspaceId | null) => Promise<void>;
|
||||
onSelectFileSystemFileHandle?: (workspaceId: DWorkspaceId | null, fsHandle: FileSystemFileHandle) => Promise<void>;
|
||||
}) {
|
||||
|
||||
// state for anchor
|
||||
@@ -40,7 +44,7 @@ export function WorkspaceLiveFilePicker(props: {
|
||||
|
||||
// set as disabled when empty
|
||||
const haveLiveFiles = wLiveFiles.length > 0;
|
||||
const { autoSelectName, liveFileId, onSelectLiveFile, onSelectNewFile } = props;
|
||||
const { autoSelectName, liveFileId, onSelectLiveFile, onSelectFileOpen, onSelectFileSystemFileHandle } = props;
|
||||
|
||||
|
||||
// [effect] auto-select a LiveFileId
|
||||
@@ -72,16 +76,29 @@ export function WorkspaceLiveFilePicker(props: {
|
||||
}, []);
|
||||
|
||||
const handleSelectLiveFile = React.useCallback((id: LiveFileId | null) => {
|
||||
onSelectLiveFile(id);
|
||||
setMenuAnchor(null);
|
||||
onSelectLiveFile(id);
|
||||
}, [onSelectLiveFile]);
|
||||
|
||||
const handleSelectNewFile = React.useCallback(() => {
|
||||
if (onSelectNewFile) {
|
||||
onSelectNewFile(workspaceId);
|
||||
const handleSelectNewFile = React.useCallback(async () => {
|
||||
if (onSelectFileOpen) {
|
||||
setMenuAnchor(null);
|
||||
await onSelectFileOpen(workspaceId);
|
||||
}
|
||||
}, [onSelectNewFile, workspaceId]);
|
||||
}, [onSelectFileOpen, workspaceId]);
|
||||
|
||||
const handleDataTransferDrop = React.useCallback(async (dataTransfer: DataTransfer) => {
|
||||
if (onSelectFileSystemFileHandle) {
|
||||
const fsfHandle = await getFirstFileSystemFileHandle(dataTransfer);
|
||||
if (fsfHandle) {
|
||||
setMenuAnchor(null);
|
||||
await onSelectFileSystemFileHandle(workspaceId, fsfHandle);
|
||||
}
|
||||
}
|
||||
}, [onSelectFileSystemFileHandle, workspaceId]);
|
||||
|
||||
const { dragContainerSx, dropComponent, handleContainerDragEnter, handleContainerDragStart } =
|
||||
useDragDropDataTransfer(true, 'Select', LiveFileChooseIcon as typeof SvgIcon, 'startDecorator', true, handleDataTransferDrop);
|
||||
|
||||
|
||||
// Note: in the future let this be, we can show a file picker that adds LiveFiles to the workspace
|
||||
@@ -92,35 +109,42 @@ export function WorkspaceLiveFilePicker(props: {
|
||||
|
||||
return <>
|
||||
|
||||
{/*<TooltipOutlined*/}
|
||||
{/* title={tooltipLabel} */}
|
||||
{/* color='success'*/}
|
||||
{/* placement='top-end'*/}
|
||||
{/*>*/}
|
||||
{liveFileId ? (
|
||||
<IconButton
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={handleToggleMenu}
|
||||
>
|
||||
<LiveFilePatchIcon color='success' />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
onClick={handleToggleMenu}
|
||||
endDecorator={<LiveFileChooseIcon color='success' />}
|
||||
// endDecorator={<LiveFilePatchIcon color='success' />}
|
||||
>
|
||||
{props.buttonLabel}
|
||||
</Button>
|
||||
)}
|
||||
{/*</TooltipOutlined>*/}
|
||||
{/* Main Button, also a drop target */}
|
||||
<Box
|
||||
onDragEnter={handleContainerDragEnter}
|
||||
onDragStart={handleContainerDragStart}
|
||||
sx={dragContainerSx}
|
||||
>
|
||||
{liveFileId && (
|
||||
<IconButton
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={handleToggleMenu}
|
||||
>
|
||||
<LiveFilePatchIcon color='success' />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{!liveFileId && (
|
||||
<TooltipOutlined title={props.labelTooltip} color='success' placement='top-end'>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
onClick={handleToggleMenu}
|
||||
endDecorator={<LiveFileChooseIcon />}
|
||||
// endDecorator={<LiveFilePatchIcon color='success' />}
|
||||
>
|
||||
{props.labelButton}
|
||||
</Button>
|
||||
</TooltipOutlined>
|
||||
)}
|
||||
|
||||
{dropComponent}
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Menu: list of workspace files */}
|
||||
{/* Select/Upload file menu */}
|
||||
{!!menuAnchor && (
|
||||
<CloseableMenu
|
||||
open
|
||||
@@ -155,7 +179,7 @@ export function WorkspaceLiveFilePicker(props: {
|
||||
))}
|
||||
|
||||
{/* Pair a new file */}
|
||||
{!!props.onSelectNewFile && (
|
||||
{!!props.onSelectFileOpen && (
|
||||
<MenuItem onClick={handleSelectNewFile} sx={haveLiveFiles ? { minHeight: '3rem' } : undefined}>
|
||||
<ListItemDecorator>
|
||||
<LiveFileChooseIcon />
|
||||
|
||||
@@ -96,3 +96,25 @@ export function getDataTransferFilesOrPromises(items: DataTransferItemList, fall
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Utility function to get the first file system handle from a DataTransfer object.
|
||||
* Note that a DataTransfer object can contain multiple files, but we assume the first is the one.
|
||||
*/
|
||||
export async function getFirstFileSystemFileHandle(dataTransfer: DataTransfer): Promise<FileSystemFileHandle | null> {
|
||||
|
||||
// get FileSystemFileHandle objects from the DataTransfer
|
||||
const fileOrFSHandlePromises = getDataTransferFilesOrPromises(dataTransfer.items, false);
|
||||
if (!fileOrFSHandlePromises.length)
|
||||
return null;
|
||||
|
||||
// resolve the promises to get the actual files/handles
|
||||
const filesOrHandles = await Promise.all(fileOrFSHandlePromises);
|
||||
for (let filesOrHandle of filesOrHandles)
|
||||
if (!(filesOrHandle instanceof File) && filesOrHandle?.kind === 'file' && filesOrHandle)
|
||||
return filesOrHandle;
|
||||
|
||||
// no file system handle found
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,23 +28,15 @@ export function useLiveFilePatch(title: string, code: string, isPartial: boolean
|
||||
|
||||
|
||||
// handlers
|
||||
const handleLiveFileSelected = React.useCallback((id: LiveFileId | null) => {
|
||||
|
||||
const handleSelectLiveFile = React.useCallback((id: LiveFileId | null) => {
|
||||
setLiveFileId(id);
|
||||
}, []);
|
||||
|
||||
const handleSelectNewFile = React.useCallback(async (workspaceId: DWorkspaceId | null) => {
|
||||
// pick a file
|
||||
const fileWithHandle = await fileOpen({ description: 'Insert into file...' }).catch(() => null /* The User closed the files picker */);
|
||||
if (!fileWithHandle)
|
||||
return;
|
||||
const fileSystemFileHandle = fileWithHandle.handle;
|
||||
if (!fileSystemFileHandle) {
|
||||
// setStatus({ message: `Browser does not support LiveFile operations. ${isLiveFileSupported() ? 'No filesystem handles.' : ''}`, mtype: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSelectFileSystemFileHandle = React.useCallback(async (workspaceId: DWorkspaceId | null, fsfHandle: FileSystemFileHandle) => {
|
||||
// Create a new LiveFile and attach it to the workspace
|
||||
try {
|
||||
const newLiveFileId = await liveFileCreateOrThrow(fileSystemFileHandle);
|
||||
const newLiveFileId = await liveFileCreateOrThrow(fsfHandle);
|
||||
setLiveFileId(newLiveFileId);
|
||||
|
||||
// Pair the file with the workspace
|
||||
@@ -59,9 +51,24 @@ export function useLiveFilePatch(title: string, code: string, isPartial: boolean
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating new file:', error);
|
||||
// setStatus({ message: `Error pairing the file: ${error?.message || typeof error === 'string' ? error : 'Unknown error'}`, mtype: 'error' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectFilePicker = React.useCallback(async (workspaceId: DWorkspaceId | null) => {
|
||||
// pick a file
|
||||
const fileWithHandle = await fileOpen({ description: 'Insert into file...' }).catch(() => null /* The User closed the files picker */);
|
||||
if (!fileWithHandle)
|
||||
return;
|
||||
const fileSystemFileHandle = fileWithHandle.handle;
|
||||
if (!fileSystemFileHandle) {
|
||||
// setStatus({ message: `Browser does not support LiveFile operations. ${isLiveFileSupported() ? 'No filesystem handles.' : ''}`, mtype: 'error' });
|
||||
return;
|
||||
}
|
||||
// proceed
|
||||
await handleSelectFileSystemFileHandle(workspaceId, fileSystemFileHandle);
|
||||
}, [handleSelectFileSystemFileHandle]);
|
||||
|
||||
|
||||
// components
|
||||
|
||||
@@ -88,14 +95,16 @@ export function useLiveFilePatch(title: string, code: string, isPartial: boolean
|
||||
{/* Pick LiveFile */}
|
||||
<WorkspaceLiveFilePicker
|
||||
autoSelectName={title}
|
||||
buttonLabel='Insert...'
|
||||
labelButton='Insert...'
|
||||
labelTooltip='Insert this code into a file'
|
||||
liveFileId={liveFileId}
|
||||
onSelectLiveFile={handleLiveFileSelected}
|
||||
onSelectNewFile={handleSelectNewFile}
|
||||
onSelectFileOpen={handleSelectFilePicker}
|
||||
onSelectFileSystemFileHandle={handleSelectFileSystemFileHandle}
|
||||
onSelectLiveFile={handleSelectLiveFile}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
), [handleLiveFileSelected, handleSelectNewFile, isEnabled, liveFileId, title]);
|
||||
), [handleSelectLiveFile, handleSelectFilePicker, handleSelectFileSystemFileHandle, isEnabled, liveFileId, title]);
|
||||
|
||||
|
||||
const actionBar = React.useMemo(() => (!isEnabled || !liveFileId || true) ? null : (
|
||||
|
||||
Reference in New Issue
Block a user