mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
MP: full document editing
This commit is contained in:
+36
-34
@@ -9,7 +9,7 @@ import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import { BlocksRenderer } from '~/modules/blocks/BlocksRenderer';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import type { DMessageAttachmentFragment, DMessageFragmentId, DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import { createTextAttachmentFragment, DMessageAttachmentFragment, DMessageFragmentId, DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import { marshallWrapText } from '~/common/stores/chat/chat.tokens';
|
||||
|
||||
import { ContentPartTextEdit } from '../fragments-content/ContentPartTextEdit';
|
||||
@@ -28,7 +28,7 @@ export function DocumentFragmentEditor(props: {
|
||||
}) {
|
||||
|
||||
// derived state
|
||||
const { fragment, onFragmentDelete, onFragmentReplace } = props;
|
||||
const { editedText, fragment, onFragmentDelete, onFragmentReplace } = props;
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [isDeleteArmed, setIsDeleteArmed] = React.useState(false);
|
||||
|
||||
@@ -36,34 +36,39 @@ export function DocumentFragmentEditor(props: {
|
||||
const fragmentTitle = fragment.title;
|
||||
const part = fragment.part;
|
||||
|
||||
// handlers
|
||||
|
||||
const handleDeleteFragment = React.useCallback(() => {
|
||||
onFragmentDelete(fragmentId);
|
||||
}, [fragmentId, onFragmentDelete]);
|
||||
|
||||
const handleReplaceFragment = React.useCallback((newFragment: DMessageAttachmentFragment) => {
|
||||
onFragmentReplace(fragmentId, newFragment);
|
||||
}, [fragmentId, onFragmentReplace]);
|
||||
|
||||
|
||||
if (part.pt !== 'text')
|
||||
throw new Error('Unexpected part type: ' + part.pt);
|
||||
|
||||
const handleEditToggle = React.useCallback(() => {
|
||||
// delete
|
||||
|
||||
const handleToggleDeleteArmed = React.useCallback(() => {
|
||||
// setIsEditing(false);
|
||||
setIsDeleteArmed(on => !on);
|
||||
}, []);
|
||||
|
||||
const handleFragmentDelete = React.useCallback(() => {
|
||||
onFragmentDelete(fragmentId);
|
||||
}, [fragmentId, onFragmentDelete]);
|
||||
|
||||
|
||||
// edit
|
||||
|
||||
const handleToggleEdit = React.useCallback(() => {
|
||||
setIsDeleteArmed(false);
|
||||
setIsEditing(on => !on);
|
||||
}, []);
|
||||
|
||||
const handleEditEnterPressed = React.useCallback(() => {
|
||||
// setIsEditing(false);
|
||||
// TODO...
|
||||
}, []);
|
||||
|
||||
const handleDeleteArmedToggle = React.useCallback(() => {
|
||||
setIsEditing(false);
|
||||
setIsDeleteArmed(on => !on);
|
||||
}, []);
|
||||
const handleEditApply = React.useCallback(() => {
|
||||
setIsDeleteArmed(false);
|
||||
if (editedText === undefined)
|
||||
return;
|
||||
if (editedText?.length > 0) {
|
||||
onFragmentReplace(fragmentId, createTextAttachmentFragment(fragmentTitle, editedText));
|
||||
// NOTE: since the former function changes the ID of the fragment, the
|
||||
// whole editor will disappear as a side effect
|
||||
} else
|
||||
handleFragmentDelete();
|
||||
}, [editedText, fragmentId, fragmentTitle, handleFragmentDelete, onFragmentReplace]);
|
||||
|
||||
|
||||
return (
|
||||
@@ -85,8 +90,8 @@ export function DocumentFragmentEditor(props: {
|
||||
contentScaling={props.contentScaling}
|
||||
editedText={props.editedText}
|
||||
setEditedText={props.setEditedText}
|
||||
onEnterPressed={handleEditEnterPressed}
|
||||
onEscapePressed={handleEditToggle}
|
||||
onEnterPressed={handleEditApply}
|
||||
onEscapePressed={handleToggleEdit}
|
||||
/>
|
||||
) : (
|
||||
// Document viewer, including collapse/expand
|
||||
@@ -103,19 +108,18 @@ export function DocumentFragmentEditor(props: {
|
||||
|
||||
{/* Edit / Delete commands */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', mt: 1 }}>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{isDeleteArmed ? (
|
||||
<Button variant='solid' color='neutral' size='sm' onClick={handleDeleteArmedToggle} startDecorator={<CloseRoundedIcon />}>
|
||||
<Button variant='solid' color='neutral' size='sm' onClick={handleToggleDeleteArmed} startDecorator={<CloseRoundedIcon />}>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={handleDeleteArmedToggle} startDecorator={<DeleteOutlineIcon />}>
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={handleToggleDeleteArmed} startDecorator={<DeleteOutlineIcon />}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
{isDeleteArmed && (
|
||||
<Button variant='plain' color='danger' size='sm' onClick={handleDeleteFragment} startDecorator={<DeleteForeverIcon />}>
|
||||
<Button variant='plain' color='danger' size='sm' onClick={handleFragmentDelete} startDecorator={<DeleteForeverIcon />}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
@@ -123,24 +127,22 @@ export function DocumentFragmentEditor(props: {
|
||||
|
||||
<Box sx={{ ml: 'auto', display: 'flex', gap: 1 }}>
|
||||
{isEditing ? (
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={handleEditToggle} startDecorator={<CloseRoundedIcon />}>
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={handleToggleEdit} startDecorator={<CloseRoundedIcon />}>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={handleEditToggle} startDecorator={<EditRoundedIcon />}>
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={handleToggleEdit} startDecorator={<EditRoundedIcon />}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
{isEditing && (
|
||||
<Button variant='plain' color='success' onClick={undefined} size='sm' startDecorator={<CheckRoundedIcon />}>
|
||||
<Button variant='plain' color='success' onClick={handleEditApply} size='sm' startDecorator={<CheckRoundedIcon />}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -25,26 +25,26 @@ export function DocumentFragments(props: {
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [selectedFragmentId, setSelectedFragmentId] = React.useState<DMessageFragmentId | null>(null);
|
||||
const [textAttachmentsEditState, setTextAttachmentsEditState] = React.useState<ChatMessageTextPartEditState | null>(null);
|
||||
const [activeFragmentId, setActiveFragmentId] = React.useState<DMessageFragmentId | null>(null);
|
||||
const [editState, setEditState] = React.useState<ChatMessageTextPartEditState | null>(null);
|
||||
|
||||
|
||||
// selection
|
||||
|
||||
const handleToggleSelectedId = React.useCallback((fragmentId: DMessageFragmentId) => setActiveFragmentId(prevId => prevId === fragmentId ? null : fragmentId), []);
|
||||
|
||||
const selectedFragment = props.attachmentFragments.find(fragment => fragment.fId === activeFragmentId);
|
||||
|
||||
|
||||
// editing
|
||||
|
||||
const handleEditSetText = React.useCallback((fragmentId: DMessageFragmentId, value: string) => setEditState(prevState => ({ ...prevState, [fragmentId]: value })), []);
|
||||
|
||||
// [effect] clear edits on onmount
|
||||
React.useEffect(() => {
|
||||
return () => setTextAttachmentsEditState(null);
|
||||
return () => setEditState(null);
|
||||
}, []);
|
||||
|
||||
const handleToggleSelected = React.useCallback((fragmentId: DMessageFragmentId) => {
|
||||
setSelectedFragmentId(prevId => prevId === fragmentId ? null : fragmentId);
|
||||
}, []);
|
||||
|
||||
const handleSetEditedText = React.useCallback((fragmentId: DMessageFragmentId, value: string) => {
|
||||
setTextAttachmentsEditState(prevState => ({
|
||||
...prevState,
|
||||
[fragmentId]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const selectedFragment = props.attachmentFragments.find(fragment => fragment.fId === selectedFragmentId);
|
||||
|
||||
return (
|
||||
<Box aria-label={`${props.attachmentFragments.length} attachments`} sx={{
|
||||
@@ -68,8 +68,8 @@ export function DocumentFragments(props: {
|
||||
key={attachmentFragment.fId}
|
||||
fragment={attachmentFragment}
|
||||
contentScaling={props.contentScaling}
|
||||
isSelected={selectedFragmentId === attachmentFragment.fId}
|
||||
toggleSelected={handleToggleSelected}
|
||||
isSelected={activeFragmentId === attachmentFragment.fId}
|
||||
toggleSelected={handleToggleSelectedId}
|
||||
/>,
|
||||
)}
|
||||
</Box>
|
||||
@@ -79,8 +79,8 @@ export function DocumentFragments(props: {
|
||||
<DocumentFragmentEditor
|
||||
fragment={selectedFragment}
|
||||
messageRole={props.messageRole}
|
||||
editedText={textAttachmentsEditState?.[selectedFragment.fId]}
|
||||
setEditedText={handleSetEditedText}
|
||||
editedText={editState?.[selectedFragment.fId]}
|
||||
setEditedText={handleEditSetText}
|
||||
contentScaling={props.contentScaling}
|
||||
isMobile={props.isMobile}
|
||||
renderTextAsMarkdown={props.renderTextAsMarkdown}
|
||||
|
||||
@@ -316,12 +316,12 @@ export async function attachmentPerformConversion(
|
||||
|
||||
// text as-is
|
||||
case 'text':
|
||||
newFragments.push(createTextAttachmentFragment(inputDataToString(input.data), ref));
|
||||
newFragments.push(createTextAttachmentFragment(ref, inputDataToString(input.data)));
|
||||
break;
|
||||
|
||||
// html as-is
|
||||
case 'rich-text':
|
||||
newFragments.push(createTextAttachmentFragment(input.altData!, ref || '\n<!DOCTYPE html>'));
|
||||
newFragments.push(createTextAttachmentFragment(ref || '\n<!DOCTYPE html>', input.altData!));
|
||||
break;
|
||||
|
||||
// html to markdown table
|
||||
@@ -333,7 +333,7 @@ export async function attachmentPerformConversion(
|
||||
// fallback to text/plain
|
||||
mdTable = inputDataToString(input.data);
|
||||
}
|
||||
newFragments.push(createTextAttachmentFragment(mdTable, ref));
|
||||
newFragments.push(createTextAttachmentFragment(ref, mdTable));
|
||||
break;
|
||||
|
||||
// image resized (default mime/quality, openai-high-res)
|
||||
@@ -397,7 +397,7 @@ export async function attachmentPerformConversion(
|
||||
},
|
||||
});
|
||||
const imageText = result.data.text;
|
||||
newFragments.push(createTextAttachmentFragment(imageText, ref));
|
||||
newFragments.push(createTextAttachmentFragment(ref, imageText));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -413,7 +413,7 @@ export async function attachmentPerformConversion(
|
||||
// duplicate the ArrayBuffer to avoid mutation
|
||||
const pdfData = new Uint8Array(input.data.slice(0));
|
||||
const pdfText = await pdfToText(pdfData);
|
||||
newFragments.push(createTextAttachmentFragment(pdfText, ref));
|
||||
newFragments.push(createTextAttachmentFragment(ref, pdfText));
|
||||
break;
|
||||
|
||||
// pdf to images
|
||||
|
||||
@@ -188,7 +188,7 @@ function createContentFragment(part: DMessageContentFragment['part']): DMessageC
|
||||
}
|
||||
|
||||
|
||||
export function createTextAttachmentFragment(text: string, title: string): DMessageAttachmentFragment {
|
||||
export function createTextAttachmentFragment(title: string, text: string): DMessageAttachmentFragment {
|
||||
return createAttachmentFragment(title, createDMessageTextPart(text));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user