diff --git a/src/apps/chat/components/composer/llmattachments/LLMAttachmentButton.tsx b/src/apps/chat/components/composer/llmattachments/LLMAttachmentButton.tsx
index 14fa3e9c3..c2390c483 100644
--- a/src/apps/chat/components/composer/llmattachments/LLMAttachmentButton.tsx
+++ b/src/apps/chat/components/composer/llmattachments/LLMAttachmentButton.tsx
@@ -19,7 +19,9 @@ import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
import { RenderImageURL } from '~/modules/blocks/image/RenderImageURL';
import { GoodTooltip } from '~/common/components/GoodTooltip';
+import { LiveFileIcon } from '~/common/livefile/LiveFileIcon';
import { ellipsizeFront, ellipsizeMiddle } from '~/common/util/textUtils';
+import { liveFileInAttachmentFragment } from '~/common/livefile/liveFile';
import type { AttachmentDraft, AttachmentDraftConverterType, AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import type { LLMAttachmentDraft } from './useLLMAttachmentDrafts';
@@ -95,7 +97,7 @@ const converterTypeToIconMap: { [key in AttachmentDraftConverterType]: React.Com
'unhandled': TextureIcon,
};
-function attachmentIcon(attachmentDraft: AttachmentDraft): React.ReactNode {
+function attachmentIcons(attachmentDraft: AttachmentDraft): React.ReactNode {
const activeConterters = attachmentDraft.converters.filter(c => c.isActive);
if (activeConterters.length === 0)
return null;
@@ -115,7 +117,7 @@ function attachmentIcon(attachmentDraft: AttachmentDraft): React.ReactNode {
return
{/* If we have a Web preview, show it first */}
- {/*!imageDataRefs.length &&*/ !!attachmentDraft.input?.urlImage?.webpDataUrl && (
+ {!!attachmentDraft.input?.urlImage?.webpDataUrl && /*!imageDataRefs.length &&*/ (
This was the page.
You can also Add the Screenshot as attachment>}>
: <>
- {attachmentIcon(draft)}
- {isOutputLoading
- ? <>Converting >
- :
- {attachmentLabelText(draft)}
- }
+ {/* Icons: Web Page Screenshot, Converter[s] */}
+ {attachmentIcons(draft)}
+
+ {/* Label */}
+
+ {isOutputLoading ? 'Converting... ' : attachmentLabelText(draft)}
+
+
+ {/* Loading icon */}
+ {isOutputLoading && }
+
+ {/* Live file icon */}
+ {hasLiveFile && }
>}
)}
diff --git a/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx b/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx
index 044cd50b6..a02a22e70 100644
--- a/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx
+++ b/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx
@@ -14,7 +14,9 @@ import { showImageDataRefInNewTab } from '~/modules/blocks/image/RenderImageRefD
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { DMessageAttachmentFragment, isDocPart, isImageRefPart } from '~/common/stores/chat/chat.fragments';
+import { LiveFileIcon } from '~/common/livefile/LiveFileIcon';
import { copyToClipboard } from '~/common/util/clipboardUtils';
+import { liveFileInAttachmentFragment } from '~/common/livefile/liveFile';
import { showImageDataURLInNewTab } from '~/common/util/imageUtils';
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
@@ -55,6 +57,7 @@ export function LLMAttachmentMenu(props: {
const isConverting = draft.outputsConverting;
const isUnconvertible = !draft.converters.length;
const isOutputMissing = !draft.outputFragments.length;
+ const hasLiveFile = draft.outputFragments.some(liveFileInAttachmentFragment);
const isUnmoveable = props.isPositionFirst && props.isPositionLast;
@@ -102,7 +105,7 @@ export function LLMAttachmentMenu(props: {
>
{/* Move Arrows */}
- {!isUnmoveable &&
+ {!isUnmoveable &&
}
- {!isUnmoveable && }
+
+ {/*{(showDetails && canHaveDetails) && */}
+ {/* {draft.ref}*/}
+ {/*}*/}
{/* Render Converters as menu items */}
{!isUnconvertible && (
@@ -178,6 +184,14 @@ export function LLMAttachmentMenu(props: {
) : (
+ {/* LiveFile notice */}
+ {hasLiveFile && !!draftInput && (
+ }>
+ LiveFile is supported
+
+ )}
+
+ {/* <- inputs */}
{!!draftInput && (
🡐 {draftInput.mimeType}{typeof draftInput.dataSize === 'number' ? ` · ${draftInput.dataSize.toLocaleString()} bytes` : ''}
@@ -201,9 +215,12 @@ export function LLMAttachmentMenu(props: {
)}
+
{/**/}
- {/* Converters: {aConverters.map(((converter, idx) => ` ${converter.id}${converter.isActive ? '*' : ''}`)).join(', ')}*/}
+ {/* Converters: {draft.converters.map(((converter, idx) => ` ${converter.id}${converter.isActive ? '*' : ''}`)).join(', ')}*/}
{/**/}
+
+ {/* -> Outputs */}
{isOutputMissing ? (
🡒 ...
diff --git a/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentButton.tsx b/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentButton.tsx
index d3b265534..94637658e 100644
--- a/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentButton.tsx
+++ b/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentButton.tsx
@@ -10,12 +10,14 @@ import TelegramIcon from '@mui/icons-material/Telegram';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import TextureIcon from '@mui/icons-material/Texture';
-import { DMessageAttachmentFragment, DMessageFragmentId, isDocPart } from '~/common/stores/chat/chat.fragments';
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
+import { DMessageAttachmentFragment, DMessageFragmentId, isDocPart } from '~/common/stores/chat/chat.fragments';
+import { LiveFileIcon } from '~/common/livefile/LiveFileIcon';
import { ellipsizeMiddle } from '~/common/util/textUtils';
+import { liveFileInAttachmentFragment } from '~/common/livefile/liveFile';
-function iconForFragment({ part }: DMessageAttachmentFragment): React.ComponentType {
+function buttonIconForFragment({ part }: DMessageAttachmentFragment): React.ComponentType {
switch (part.pt) {
case 'doc':
switch (part.type) {
@@ -86,7 +88,7 @@ export function DocumentFragmentButton(props: {
const buttonText = ellipsizeMiddle(fragment.part.l1Title || fragment.title || 'Document', 28 /* totally arbitrary length */);
- const Icon = iconForFragment(fragment);
+ const Icon = buttonIconForFragment(fragment);
return (
+ {liveFileInAttachmentFragment(fragment) && (
+
+ )}
);
}
diff --git a/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentEditor.tsx b/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentEditor.tsx
index a356cec92..a7033527e 100644
--- a/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentEditor.tsx
+++ b/src/apps/chat/components/message/fragments-attachment-doc/DocumentFragmentEditor.tsx
@@ -73,7 +73,7 @@ export function DocumentFragmentEditor(props: {
if (editedText.length > 0) {
const newData = createDMessageDataInlineText(editedText, fragment.part.data.mimeType);
- const newAttachment = createDocAttachmentFragment(fragmentTitle, fragment.caption, fragment.part.type, newData, fragment.part.ref, fragment.part.meta);
+ const newAttachment = createDocAttachmentFragment(fragmentTitle, fragment.caption, fragment.part.type, newData, fragment.part.ref, fragment.part.meta, fragment._liveFile);
// reuse the same fragment ID, which makes the screen not flash (otherwise the whole editor would disappear as the ID does not exist anymore)
newAttachment.fId = fragmentId;
onFragmentReplace(fragmentId, newAttachment);
@@ -82,7 +82,7 @@ export function DocumentFragmentEditor(props: {
// if the user deleted all text, let's remove the part
handleFragmentDelete();
}
- }, [editedText, fragment.caption, fragment.part, fragmentId, fragmentTitle, handleFragmentDelete, onFragmentReplace]);
+ }, [editedText, fragment._liveFile, fragment.caption, fragment.part, fragmentId, fragmentTitle, handleFragmentDelete, onFragmentReplace]);
return (
diff --git a/src/common/attachment-drafts/attachment.pipeline.ts b/src/common/attachment-drafts/attachment.pipeline.ts
index 5b709c96a..66ac6fe87 100644
--- a/src/common/attachment-drafts/attachment.pipeline.ts
+++ b/src/common/attachment-drafts/attachment.pipeline.ts
@@ -10,7 +10,7 @@ import { createDMessageDataInlineText, createDocAttachmentFragment, DMessageAtta
import type { AttachmentsDraftsStore } from './store-attachment-drafts-slice';
import { AttachmentDraft, AttachmentDraftConverter, AttachmentDraftInput, AttachmentDraftSource, DraftEgoFragmentsInputData, draftInputMimeEgoFragments, draftInputMimeWebpage, DraftWebInputData } from './attachment.types';
import { imageDataToImageAttachmentFragmentViaDBlob } from './attachment.dblobs';
-import { liveFileAddToAttachmentFragment, liveFileIsSupported } from '../livefile/livefile';
+import { liveFileCreate, liveFileInSource } from '../livefile/liveFile';
// configuration
@@ -284,7 +284,7 @@ export function attachmentDefineConverters(source: AttachmentDraftSource, input:
converters.push({ id: 'rich-text-table', name: 'Markdown Table' });
// p2: Text
- converters.push({ id: 'text', name: liveFileIsSupported(source) ? 'Text (Live)' : 'Text' });
+ converters.push({ id: 'text', name: liveFileInSource(source) ? 'Text (Live)' : 'Text' });
// p3: Html
if (textOriginHtml) {
@@ -470,11 +470,13 @@ export async function attachmentPerformConversion(
// text as-is
case 'text':
- const textData = createDMessageDataInlineText(inputDataToString(input.data), input.mimeType);
- const textDocAttachmentFragment = createDocAttachmentFragment(title, caption, 'text/plain', textData, refString, docMeta);
+
// [LiveFile] if we have a handle for this file, transfer it to the Doc Attachment fragment
- liveFileAddToAttachmentFragment(source, textDocAttachmentFragment);
- newFragments.push(textDocAttachmentFragment);
+ const liveFile = (liveFileInSource(source) && source.media === 'file' && converter.id === 'text')
+ ? liveFileCreate(source.fileWithHandle) : undefined;
+
+ const textualInlineData = createDMessageDataInlineText(inputDataToString(input.data), input.mimeType);
+ newFragments.push(createDocAttachmentFragment(title, caption, 'text/plain', textualInlineData, refString, docMeta, liveFile));
break;
// html as-is
diff --git a/src/common/attachment-drafts/attachment.types.ts b/src/common/attachment-drafts/attachment.types.ts
index dbafa8319..4d5d5d9c9 100644
--- a/src/common/attachment-drafts/attachment.types.ts
+++ b/src/common/attachment-drafts/attachment.types.ts
@@ -11,7 +11,7 @@ export type AttachmentDraft = {
readonly id: AttachmentDraftId;
readonly source: AttachmentDraftSource,
label: string;
- ref: string; // will be used in ```ref\n...``` for instance
+ ref: string; // will be used in ```ref\n...``` for instance - TODO: remove?
inputLoading: boolean;
inputError: string | null;
@@ -44,7 +44,7 @@ export type AttachmentDraftSource = {
media: 'file';
origin: AttachmentDraftSourceOriginFile,
fileWithHandle: FileWithHandle;
- refPath: string;
+ refPath: string; // original file name, or path/to/file name
} | {
media: 'text';
method: 'clipboard-read' | AttachmentDraftSourceOriginDTO;
diff --git a/src/common/livefile/LiveFileIcon.ts b/src/common/livefile/LiveFileIcon.ts
new file mode 100644
index 000000000..f3e2c7679
--- /dev/null
+++ b/src/common/livefile/LiveFileIcon.ts
@@ -0,0 +1,3 @@
+import MultipleStopRoundedIcon from '@mui/icons-material/MultipleStopRounded';
+
+export { MultipleStopRoundedIcon as LiveFileIcon };
diff --git a/src/common/livefile/liveFile.ts b/src/common/livefile/liveFile.ts
new file mode 100644
index 000000000..9a2eb4bc2
--- /dev/null
+++ b/src/common/livefile/liveFile.ts
@@ -0,0 +1,26 @@
+import type { FileWithHandle } from 'browser-fs-access';
+
+import type { AttachmentDraftSource } from '~/common/attachment-drafts/attachment.types';
+import type { DMessageAttachmentFragment } from '~/common/stores/chat/chat.fragments';
+
+
+// AttachmentDraft Source
+
+export function liveFileInSource(source: AttachmentDraftSource): boolean {
+ return source.media === 'file' && !!source.fileWithHandle.handle && typeof source.fileWithHandle.handle.getFile === 'function';
+}
+
+
+// DMessageAttachmentFragment
+
+export function liveFileCreate(fileWithHandle: FileWithHandle): DMessageAttachmentFragment['_liveFile'] {
+ return {
+ lft: 'fs',
+ _fsFileHandle: fileWithHandle.handle,
+ };
+}
+
+export function liveFileInAttachmentFragment(attachmentFragment: DMessageAttachmentFragment): boolean {
+ return !!attachmentFragment._liveFile?._fsFileHandle;
+}
+
diff --git a/src/common/livefile/livefile.ts b/src/common/livefile/livefile.ts
deleted file mode 100644
index 6c5246e8c..000000000
--- a/src/common/livefile/livefile.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { AttachmentDraftSource } from '~/common/attachment-drafts/attachment.types';
-import type { DMessageAttachmentFragment } from '~/common/stores/chat/chat.fragments';
-
-
-export function liveFileIsSupported(source: AttachmentDraftSource): boolean {
- return source.media === 'file' && !!source.fileWithHandle.handle && typeof source.fileWithHandle.handle.getFile === 'function';
-}
-
-export function liveFileAddToAttachmentFragment(source: AttachmentDraftSource, attachmentFragment: DMessageAttachmentFragment) {
- if (source.media === 'file' && !!source.fileWithHandle.handle && typeof source.fileWithHandle.handle.getFile === 'function')
- attachmentFragment._fileSystemFileHandle = source.fileWithHandle.handle;
-}
diff --git a/src/common/stores/chat/chat.fragments.ts b/src/common/stores/chat/chat.fragments.ts
index 03135abad..a0a97b855 100644
--- a/src/common/stores/chat/chat.fragments.ts
+++ b/src/common/stores/chat/chat.fragments.ts
@@ -45,7 +45,10 @@ export type DMessageAttachmentFragment = _DMessageFragmentWrapper<'attachment',
title: string; // label of the attachment (filename, named id, content overview, title..)
caption: string; // additional information, such as provenance, content preview, etc.
created: number;
- _fileSystemFileHandle?: FileSystemFileHandle; // [LiveFile] Store the handle to mem/DB - TODO: check if it works across sessions
+ _liveFile?: {
+ lft: 'fs';
+ _fsFileHandle?: FileSystemFileHandle; // [LiveFile] Store the handle to mem/DB - TODO: check if it works across sessions
+ }
};
// Future Examples: up to 1 per message, containing the Rays and Merges that would be used to restore the Beam state - could be volatile (omitted at save)
@@ -201,16 +204,16 @@ export function specialShallowReplaceTextContentFragment(copyFragment: DMessageC
}
-function _createAttachmentFragment(title: string, caption: string, part: DMessageAttachmentFragment['part']): DMessageAttachmentFragment {
- return { ft: 'attachment', fId: agiId('chat-dfragment' /* -attachment */), title, caption, created: Date.now(), part };
+function _createAttachmentFragment(title: string, caption: string, part: DMessageAttachmentFragment['part'], liveFile: DMessageAttachmentFragment['_liveFile']): DMessageAttachmentFragment {
+ return { ft: 'attachment', fId: agiId('chat-dfragment' /* -attachment */), title, caption, created: Date.now(), _liveFile: liveFile, part };
}
-export function createDocAttachmentFragment(l1Title: string, caption: string, type: DMessageDocMimeType, data: DMessageDataInline, ref: string, meta?: DMessageDocMeta): DMessageAttachmentFragment {
- return _createAttachmentFragment(l1Title, caption, createDMessageDocPart(type, data, ref, l1Title, meta));
+export function createDocAttachmentFragment(l1Title: string, caption: string, type: DMessageDocMimeType, data: DMessageDataInline, ref: string, meta?: DMessageDocMeta, liveFile?: DMessageAttachmentFragment['_liveFile']): DMessageAttachmentFragment {
+ return _createAttachmentFragment(l1Title, caption, createDMessageDocPart(type, data, ref, l1Title, meta), liveFile);
}
export function createImageAttachmentFragment(title: string, caption: string, dataRef: DMessageDataRef, imgAltText?: string, width?: number, height?: number): DMessageAttachmentFragment {
- return _createAttachmentFragment(title, caption, createDMessageImageRefPart(dataRef, imgAltText, width, height));
+ return _createAttachmentFragment(title, caption, createDMessageImageRefPart(dataRef, imgAltText, width, height), undefined);
}
export function specialContentPartToDocAttachmentFragment(title: string, caption: string, contentPart: DMessageContentFragment['part'], ref: string, docMeta?: DMessageDocMeta): DMessageAttachmentFragment {
@@ -297,7 +300,9 @@ function _duplicateFragment(fragment: DMessageFragment): DMessageFragment {
return _createContentFragment(_duplicatePart(fragment.part));
case 'attachment':
- return _createAttachmentFragment(fragment.title, fragment.caption, _duplicatePart(fragment.part));
+ // NOTE: check if this is correct - we haven't tested the serialization of FileSystemFileHandle yet
+ const liveFileCopy = fragment._liveFile ? { ...fragment._liveFile } : undefined;
+ return _createAttachmentFragment(fragment.title, fragment.caption, _duplicatePart(fragment.part), liveFileCopy);
case '_ft_sentinel':
return _createSentinelFragment();