diff --git a/src/apps/chat/components/composer/ButtonCameraCapture.tsx b/src/apps/chat/components/composer/ButtonCameraCapture.tsx
new file mode 100644
index 000000000..c69691af0
--- /dev/null
+++ b/src/apps/chat/components/composer/ButtonCameraCapture.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+
+import { Button, IconButton } from '@mui/joy';
+import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
+
+import { CameraCaptureModal } from './CameraCaptureModal';
+
+const CAMERA_ENABLE_ON_DESKTOP = false; // process.env.NODE_ENV === 'development';
+
+
+export function ButtonCameraCapture(props: { isMobile: boolean, onOCR: (ocrText: string) => void }) {
+ // state
+ const [open, setOpen] = React.useState(false);
+
+ return <>
+
+ {/* The Button */}
+ {props.isMobile ? (
+ setOpen(true)}>
+
+
+ ) : CAMERA_ENABLE_ON_DESKTOP ? (
+
+ ) : undefined}
+
+ {/* The actual capture dialog, which will stream the video */}
+ {open && setOpen(false)} onOCR={props.onOCR} />}
+
+ >;
+}
\ No newline at end of file
diff --git a/src/apps/chat/components/composer/ButtonClipboardPaste.tsx b/src/apps/chat/components/composer/ButtonClipboardPaste.tsx
new file mode 100644
index 000000000..178cb0a95
--- /dev/null
+++ b/src/apps/chat/components/composer/ButtonClipboardPaste.tsx
@@ -0,0 +1,31 @@
+import * as React from 'react';
+
+import { Box, Button, IconButton, Tooltip } from '@mui/joy';
+import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
+
+import { KeyStroke } from '~/common/components/KeyStroke';
+
+
+const pasteClipboardLegend =
+
+ Paste as ๐ Markdown attachment
+ Also converts Code and Tables
+
+ ;
+
+export function ButtonClipboardPaste(props: { isMobile: boolean, isDeveloperMode: boolean, onPaste: () => void }) {
+ return props.isMobile ? (
+
+
+
+ ) : (
+
+ } onClick={props.onPaste}
+ sx={{ justifyContent: 'flex-start' }}>
+ {props.isDeveloperMode ? 'Paste code' : 'Paste'}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/apps/chat/components/composer/ButtonFileAttach.tsx b/src/apps/chat/components/composer/ButtonFileAttach.tsx
new file mode 100644
index 000000000..7dfbe3971
--- /dev/null
+++ b/src/apps/chat/components/composer/ButtonFileAttach.tsx
@@ -0,0 +1,70 @@
+import { Box, Button, IconButton, Stack, Tooltip } from '@mui/joy';
+import * as React from 'react';
+import AttachFileOutlinedIcon from '@mui/icons-material/AttachFileOutlined';
+
+const attachFileLegend =
+
+
+ Attach a file
+
+
+
+
+ | Text |
+ โ |
+ ๐ As-is |
+
+
+ | Code |
+ โ |
+ ๐ Markdown |
+
+
+ | PDF |
+ โ |
+ ๐ Text (summarized) |
+
+
+
+
+ Drag & drop in chat for faster loads โก
+
+ ;
+
+
+export function ButtonFileAttach(props: { isMobile: boolean, onAttachFiles: (files: FileList) => Promise }) {
+
+ // state
+ const attachmentFileInputRef = React.useRef(null);
+
+ const handleShowFilePicker = () => attachmentFileInputRef.current?.click();
+
+ const handleLoadAttachment = (event: React.ChangeEvent) => {
+ // NOTE: resetting the target value allows for the selector dialog to pop-up again
+ const files = event.target?.files;
+ if (files && files.length >= 1)
+ props.onAttachFiles(files).finally(() => event.target.value = '');
+ else
+ event.target.value = '';
+ };
+
+ return <>
+
+ {/* Mobile icon or Desktop button */}
+ {props.isMobile ? (
+
+
+
+ ) : (
+
+ }
+ sx={{ justifyContent: 'flex-start' }}>
+ Attach
+
+
+ )}
+
+
+
+ >;
+}
\ No newline at end of file
diff --git a/src/apps/chat/components/composer/CameraCaptureButton.tsx b/src/apps/chat/components/composer/CameraCaptureButton.tsx
deleted file mode 100644
index 969a6975c..000000000
--- a/src/apps/chat/components/composer/CameraCaptureButton.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as React from 'react';
-
-import { Button, IconButton } from '@mui/joy';
-import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
-
-import { hideOnDesktop, hideOnMobile } from '~/common/app.theme';
-
-import { CameraCaptureModal } from './CameraCaptureModal';
-
-const showOnDesktop = false; // process.env.NODE_ENV === 'development';
-
-
-export function CameraCaptureButton(props: { onOCR: (ocrText: string) => void }) {
- // state
- const [open, setOpen] = React.useState(false);
-
- return <>
-
- {/* The Button */}
- setOpen(true)} sx={hideOnDesktop}>
-
-
-
- {/* Also show a button on desktop while in development */}
- {showOnDesktop && }
-
- {/* The actual capture dialog, which will stream the video */}
- {open && setOpen(false)} onOCR={props.onOCR} />}
-
- >;
-}
\ No newline at end of file
diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx
index c1b545052..75a984e39 100644
--- a/src/apps/chat/components/composer/Composer.tsx
+++ b/src/apps/chat/components/composer/Composer.tsx
@@ -3,10 +3,8 @@ import { shallow } from 'zustand/shallow';
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
-import AttachFileOutlinedIcon from '@mui/icons-material/AttachFileOutlined';
import AutoModeIcon from '@mui/icons-material/AutoMode';
import CallIcon from '@mui/icons-material/Call';
-import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import MicIcon from '@mui/icons-material/Mic';
@@ -26,21 +24,23 @@ import { KeyStroke } from '~/common/components/KeyStroke';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { countModelTokens } from '~/common/util/token-counter';
import { extractFilePathsWithCommonRadix } from '~/common/util/dropTextUtils';
-import { hideOnDesktop, hideOnMobile } from '~/common/app.theme';
import { htmlTableToMarkdown } from '~/common/util/htmlTableToMarkdown';
import { launchAppCall } from '~/common/app.routes';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { pdfToText } from '~/common/util/pdfToText';
import { useChatStore } from '~/common/state/store-chats';
+import { useDebouncer } from '~/common/components/useDebouncer';
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
+import { useIsMobile } from '~/common/components/useMatchMedia';
import { useUIPreferencesStore } from '~/common/state/store-ui';
-import { CameraCaptureButton } from './CameraCaptureButton';
+import { ButtonCameraCapture } from './ButtonCameraCapture';
+import { ButtonClipboardPaste } from './ButtonClipboardPaste';
+import { ButtonFileAttach } from './ButtonFileAttach';
import { ChatModeId, useComposerStartupText } from './store-composer';
import { ChatModeMenu } from './ChatModeMenu';
import { TokenBadge } from './TokenBadge';
import { TokenProgressbar } from './TokenProgressbar';
-import { useDebouncer } from '~/common/components/useDebouncer';
/// Text template helpers
@@ -59,42 +59,6 @@ const expandPromptTemplate = (template: string, dict: object) => (inputValue: st
};
-const attachFileLegend =
-
-
- Attach a file
-
-
-
-
- | Text |
- โ |
- ๐ As-is |
-
-
- | Code |
- โ |
- ๐ Markdown |
-
-
- | PDF |
- โ |
- ๐ Text (summarized) |
-
-
-
-
- Drag & drop in chat for faster loads โก
-
- ;
-
-const pasteClipboardLegend =
-
- Paste as ๐ Markdown attachment
- Also converts Code and Tables
-
- ;
-
const MicButton = (props: { variant: VariantProp, color: ColorPaletteProp, onClick: () => void, sx?: SxProps }) =>
@@ -163,9 +127,9 @@ export function Composer(props: {
const [reducerText, setReducerText] = React.useState('');
const [reducerTextTokens, setReducerTextTokens] = React.useState(0);
const [chatModeMenuAnchor, setChatModeMenuAnchor] = React.useState(null);
- const attachmentFileInputRef = React.useRef(null);
// external state
+ const isMobile = useIsMobile();
const [chatModeId, setChatModeId] = React.useState('immediate');
const [startupText, setStartupText] = useComposerStartupText();
const [enterIsNewline, experimentalLabs] = useUIPreferencesStore(state => [state.enterIsNewline, state.experimentalLabs], shallow);
@@ -188,7 +152,9 @@ export function Composer(props: {
}
}, [setComposeText, setStartupText, startupText]);
+
// derived state
+ const isDesktop = !isMobile;
const tokenLimit = chatLLM?.contextTokens || 0;
const directTokens = React.useMemo(() => {
return (!debouncedText || !chatLLMId) ? 4 : 4 + countModelTokens(debouncedText, chatLLMId, 'composer text');
@@ -286,7 +252,7 @@ export function Composer(props: {
const micVariant: VariantProp = isRecordingSpeech ? 'solid' : isRecordingAudio ? 'outlined' : 'plain';
- async function loadAndAttachFiles(files: FileList, overrideFileNames: string[]) {
+ async function loadAndAttachFiles(files: FileList, overrideFileNames?: string[]): Promise {
// NOTE: we tried to get the common 'root prefix' of the files here, so that we could attach files with a name that's relative
// to the common root, but the files[].webkitRelativePath property is not providing that information
@@ -295,7 +261,7 @@ export function Composer(props: {
let newText = '';
for (let i = 0; i < files.length; i++) {
const file = files[i];
- const fileName = overrideFileNames.length === files.length ? overrideFileNames[i] : file.name;
+ const fileName = overrideFileNames?.length === files.length ? overrideFileNames[i] : file.name;
let fileText = '';
try {
if (file.type === 'application/pdf')
@@ -335,20 +301,9 @@ export function Composer(props: {
setComposeText(text => text + newText);
};
- const handleShowFilePicker = () => attachmentFileInputRef.current?.click();
-
- const handleLoadAttachment = async (e: React.ChangeEvent) => {
- const files = e.target?.files;
- if (files && files.length >= 1)
- await loadAndAttachFiles(files, []);
-
- // this is needed to allow the same file to be selected again
- e.target.value = '';
- };
-
const handleCameraOCR = (text: string) => text && setComposeText(expandPromptTemplate(PromptTemplates.PasteMarkdown, { clipboard: text }));
- const handlePasteButtonClicked = React.useCallback(async () => {
+ const handlePasteClipboard = React.useCallback(async () => {
for (const clipboardItem of await navigator.clipboard.read()) {
// when pasting html, only process tables as markdown (e.g. from Excel), or fallback to text
@@ -381,14 +336,14 @@ export function Composer(props: {
}
}, [setComposeText]);
- useGlobalShortcut('v', true, true, false, handlePasteButtonClicked);
+ useGlobalShortcut('v', true, true, false, handlePasteClipboard);
- const handleTextareaCtrlV = async (e: React.ClipboardEvent) => {
+ const handleTextareaCtrlV = async (event: React.ClipboardEvent) => {
// paste local files
- if (e.clipboardData.files.length > 0) {
- e.preventDefault();
- await loadAndAttachFiles(e.clipboardData.files, []);
+ if (event.clipboardData.files?.length) {
+ event.preventDefault();
+ await loadAndAttachFiles(event.clipboardData.files, []);
return;
}
@@ -445,6 +400,7 @@ export function Composer(props: {
console.log('Unhandled Drop event. Contents: ', e.dataTransfer.types.map(t => `${t}: ${e.dataTransfer.getData(t)}`));
};
+
const isImmediate = chatModeId === 'immediate';
const isWriteUser = chatModeId === 'write-user';
const isChat = isImmediate || isWriteUser;
@@ -464,6 +420,7 @@ export function Composer(props: {
? 'Chat with me ยท drop source files ยท attach code...'
: /*isProdiaConfigured ?*/ 'Chat ยท /react ยท /imagine ยท drop text files...' /*: 'Chat ยท /react ยท drop text files...'*/;
+
return (
@@ -475,40 +432,16 @@ export function Composer(props: {
{/* [mobile] Mic button */}
- {isSpeechEnabled &&
-
- }
+ {isMobile && isSpeechEnabled && }
{/* Responsive Camera OCR button */}
-
+
{/* Responsive Attach button */}
-
-
-
-
- }
- sx={{ ...hideOnMobile, justifyContent: 'flex-start' }}>
- Attach
-
-
+
{/* Responsive Paste button */}
-
-
-
-
- } onClick={handlePasteButtonClicked}
- sx={{ ...hideOnMobile, justifyContent: 'flex-start' }}>
- {props.isDeveloperMode ? 'Paste code' : 'Paste'}
-
-
-
-
+
@@ -556,7 +489,7 @@ export function Composer(props: {
m: 1,
display: 'flex', flexDirection: 'column', gap: 1,
}}>
-
+ {isDesktop && }
{micIsRunning && (
+ {/* [mobile, corner] Call secondary button */}
+ {isMobile && isChat && }
- {/* [mobile] [corner] Call button */}
- {isChat && }
-
- {/* [mobile] [corner] Draw button */}
- {(isDraw || isDrawPlus) && }
+ {/* [mobile, corner] Draw Options secondary button */}
+ {isMobile && (isDraw || isDrawPlus) && }
{/* Responsive Send/Stop buttons */}
{assistantTyping
@@ -666,13 +591,16 @@ export function Composer(props: {
- {/* [desktop] other buttons (aligned to bottom for now, and mutually exclusive) */}
-
+ {/* [desktop] secondary buttons (aligned to bottom for now, and mutually exclusive) */}
+ {isDesktop &&
+ {/* [desktop] Call secondary button */}
{isChat && }
+ {/* [desktop] Draw Options secondary button */}
{(isDraw || isDrawPlus) && }
-
+
+ }
diff --git a/src/apps/models-modal/ModelsSourceSelector.tsx b/src/apps/models-modal/ModelsSourceSelector.tsx
index 946158dd6..b339723df 100644
--- a/src/apps/models-modal/ModelsSourceSelector.tsx
+++ b/src/apps/models-modal/ModelsSourceSelector.tsx
@@ -11,7 +11,7 @@ import { createModelSourceForVendor, findAllVendors, findVendorById } from '~/mo
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
-import { hideOnDesktop, hideOnMobile } from '~/common/app.theme';
+import { useIsMobile } from '~/common/components/useMatchMedia';
/*function locationIcon(vendor?: IModelVendor | null) {
@@ -43,6 +43,7 @@ export function ModelsSourceSelector(props: {
const [confirmDeletionSourceId, setConfirmDeletionSourceId] = React.useState(null);
// external state
+ const isMobile = useIsMobile();
const { modelSources, addModelSource, removeModelSource } = useModelsStore(state => ({
modelSources: state.sources,
addModelSource: state.addSource, removeModelSource: state.removeSource,
@@ -115,9 +116,9 @@ export function ModelsSourceSelector(props: {
{/* Models: [Select] Add Delete */}
-
+ {!isMobile &&
Service:
-
+ }
-
-
-
- } sx={{ ...hideOnMobile }}>
- Add
-
+ {isMobile ? (
+
+
+
+ ) : (
+ }>
+ Add
+
+ )}