mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Composer: Extract some buttons, and support the useIsMobile() hook
This commit is contained in:
@@ -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 ? (
|
||||
<IconButton variant='plain' color='neutral' onClick={() => setOpen(true)}>
|
||||
<AddAPhotoIcon />
|
||||
</IconButton>
|
||||
) : CAMERA_ENABLE_ON_DESKTOP ? (
|
||||
<Button
|
||||
fullWidth variant='plain' color='neutral' onClick={() => setOpen(true)} startDecorator={<AddAPhotoIcon />}
|
||||
sx={{ justifyContent: 'flex-start' }}>
|
||||
OCR
|
||||
</Button>
|
||||
) : undefined}
|
||||
|
||||
{/* The actual capture dialog, which will stream the video */}
|
||||
{open && <CameraCaptureModal onCloseModal={() => setOpen(false)} onOCR={props.onOCR} />}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -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 =
|
||||
<Box sx={{ p: 1, lineHeight: 2 }}>
|
||||
<b>Paste as 📚 Markdown attachment</b><br />
|
||||
Also converts Code and Tables<br />
|
||||
<KeyStroke light combo='Ctrl + Shift + V' />
|
||||
</Box>;
|
||||
|
||||
export function ButtonClipboardPaste(props: { isMobile: boolean, isDeveloperMode: boolean, onPaste: () => void }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton onClick={props.onPaste}>
|
||||
<ContentPasteGoIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip
|
||||
variant='solid' placement='top-start'
|
||||
title={pasteClipboardLegend}>
|
||||
<Button fullWidth variant='plain' color='neutral' startDecorator={<ContentPasteGoIcon />} onClick={props.onPaste}
|
||||
sx={{ justifyContent: 'flex-start' }}>
|
||||
{props.isDeveloperMode ? 'Paste code' : 'Paste'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -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 =
|
||||
<Stack sx={{ p: 1, gap: 1 }}>
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<b>Attach a file</b>
|
||||
</Box>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Text</b></td>
|
||||
<td align='center' style={{ opacity: 0.5 }}>→</td>
|
||||
<td>📝 As-is</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Code</b></td>
|
||||
<td align='center' style={{ opacity: 0.5 }}>→</td>
|
||||
<td>📚 Markdown</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>PDF</b></td>
|
||||
<td width={36} align='center' style={{ opacity: 0.5 }}>→</td>
|
||||
<td>📝 Text (summarized)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Box sx={{ mt: 1, fontSize: '14px' }}>
|
||||
Drag & drop in chat for faster loads ⚡
|
||||
</Box>
|
||||
</Stack>;
|
||||
|
||||
|
||||
export function ButtonFileAttach(props: { isMobile: boolean, onAttachFiles: (files: FileList) => Promise<void> }) {
|
||||
|
||||
// state
|
||||
const attachmentFileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleShowFilePicker = () => attachmentFileInputRef.current?.click();
|
||||
|
||||
const handleLoadAttachment = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 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 ? (
|
||||
<IconButton onClick={handleShowFilePicker}>
|
||||
<AttachFileOutlinedIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip variant='solid' placement='top-start' title={attachFileLegend}>
|
||||
<Button fullWidth variant='plain' color='neutral' onClick={handleShowFilePicker} startDecorator={<AttachFileOutlinedIcon />}
|
||||
sx={{ justifyContent: 'flex-start' }}>
|
||||
Attach
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<input type='file' multiple hidden ref={attachmentFileInputRef} onChange={handleLoadAttachment} />
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -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 */}
|
||||
<IconButton variant='plain' color='neutral' onClick={() => setOpen(true)} sx={hideOnDesktop}>
|
||||
<AddAPhotoIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Also show a button on desktop while in development */}
|
||||
{showOnDesktop && <Button
|
||||
fullWidth variant='plain' color='neutral' onClick={() => setOpen(true)} startDecorator={<AddAPhotoIcon />}
|
||||
sx={{ ...hideOnMobile, justifyContent: 'flex-start' }}>
|
||||
OCR
|
||||
</Button>}
|
||||
|
||||
{/* The actual capture dialog, which will stream the video */}
|
||||
{open && <CameraCaptureModal onCloseModal={() => setOpen(false)} onOCR={props.onOCR} />}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -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 =
|
||||
<Stack sx={{ p: 1, gap: 1 }}>
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<b>Attach a file</b>
|
||||
</Box>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Text</b></td>
|
||||
<td align='center' style={{ opacity: 0.5 }}>→</td>
|
||||
<td>📝 As-is</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Code</b></td>
|
||||
<td align='center' style={{ opacity: 0.5 }}>→</td>
|
||||
<td>📚 Markdown</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>PDF</b></td>
|
||||
<td width={36} align='center' style={{ opacity: 0.5 }}>→</td>
|
||||
<td>📝 Text (summarized)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Box sx={{ mt: 1, fontSize: '14px' }}>
|
||||
Drag & drop in chat for faster loads ⚡
|
||||
</Box>
|
||||
</Stack>;
|
||||
|
||||
const pasteClipboardLegend =
|
||||
<Box sx={{ p: 1, lineHeight: 2 }}>
|
||||
<b>Paste as 📚 Markdown attachment</b><br />
|
||||
Also converts Code and Tables<br />
|
||||
<KeyStroke light combo='Ctrl + Shift + V' />
|
||||
</Box>;
|
||||
|
||||
const MicButton = (props: { variant: VariantProp, color: ColorPaletteProp, onClick: () => void, sx?: SxProps }) =>
|
||||
<Tooltip placement='top' title={
|
||||
<Box sx={{ p: 1, lineHeight: 2, gap: 1 }}>
|
||||
@@ -163,9 +127,9 @@ export function Composer(props: {
|
||||
const [reducerText, setReducerText] = React.useState('');
|
||||
const [reducerTextTokens, setReducerTextTokens] = React.useState(0);
|
||||
const [chatModeMenuAnchor, setChatModeMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
|
||||
const attachmentFileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('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<void> {
|
||||
|
||||
// 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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Box sx={props.sx}>
|
||||
<Grid container spacing={{ xs: 1, md: 2 }}>
|
||||
@@ -475,40 +432,16 @@ export function Composer(props: {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 0, md: 2 } }}>
|
||||
|
||||
{/* [mobile] Mic button */}
|
||||
{isSpeechEnabled && <Box sx={hideOnDesktop}>
|
||||
<MicButton variant={micVariant} color={micColor} onClick={handleMicClicked} />
|
||||
</Box>}
|
||||
{isMobile && isSpeechEnabled && <MicButton variant={micVariant} color={micColor} onClick={handleMicClicked} />}
|
||||
|
||||
{/* Responsive Camera OCR button */}
|
||||
<CameraCaptureButton onOCR={handleCameraOCR} />
|
||||
<ButtonCameraCapture isMobile={isMobile} onOCR={handleCameraOCR} />
|
||||
|
||||
{/* Responsive Attach button */}
|
||||
<IconButton onClick={handleShowFilePicker} sx={{ ...hideOnDesktop }}>
|
||||
<AttachFileOutlinedIcon />
|
||||
</IconButton>
|
||||
<Tooltip
|
||||
variant='solid' placement='top-start'
|
||||
title={attachFileLegend}>
|
||||
<Button fullWidth variant='plain' color='neutral' onClick={handleShowFilePicker} startDecorator={<AttachFileOutlinedIcon />}
|
||||
sx={{ ...hideOnMobile, justifyContent: 'flex-start' }}>
|
||||
Attach
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ButtonFileAttach isMobile={isMobile} onAttachFiles={loadAndAttachFiles} />
|
||||
|
||||
{/* Responsive Paste button */}
|
||||
<IconButton onClick={handlePasteButtonClicked} sx={{ ...hideOnDesktop }}>
|
||||
<ContentPasteGoIcon />
|
||||
</IconButton>
|
||||
<Tooltip
|
||||
variant='solid' placement='top-start'
|
||||
title={pasteClipboardLegend}>
|
||||
<Button fullWidth variant='plain' color='neutral' startDecorator={<ContentPasteGoIcon />} onClick={handlePasteButtonClicked}
|
||||
sx={{ ...hideOnMobile, justifyContent: 'flex-start' }}>
|
||||
{props.isDeveloperMode ? 'Paste code' : 'Paste'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<input type='file' multiple hidden ref={attachmentFileInputRef} onChange={handleLoadAttachment} />
|
||||
<ButtonClipboardPaste isMobile={isMobile} isDeveloperMode={props.isDeveloperMode} onPaste={handlePasteClipboard} />
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -556,7 +489,7 @@ export function Composer(props: {
|
||||
m: 1,
|
||||
display: 'flex', flexDirection: 'column', gap: 1,
|
||||
}}>
|
||||
<MicButton variant={micVariant} color={micColor} onClick={handleMicClicked} sx={hideOnMobile} />
|
||||
{isDesktop && <MicButton variant={micVariant} color={micColor} onClick={handleMicClicked} />}
|
||||
|
||||
{micIsRunning && (
|
||||
<MicContinuationButton
|
||||
@@ -624,19 +557,11 @@ export function Composer(props: {
|
||||
{/* first row of buttons */}
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
|
||||
{/* [mobile, corner] Call secondary button */}
|
||||
{isMobile && isChat && <CallButtonMobile disabled={!APP_CALL_ENABLED || !props.conversationId || !chatLLM} onClick={handleCallClicked} sx={{ mr: { xs: 1, md: 2 } }} />}
|
||||
|
||||
{/* [mobile] [corner] Call button */}
|
||||
{isChat && <CallButtonMobile
|
||||
disabled={!APP_CALL_ENABLED || !props.conversationId || !chatLLM}
|
||||
onClick={handleCallClicked}
|
||||
sx={{ ...hideOnDesktop, mr: { xs: 1, md: 2 } }}
|
||||
/>}
|
||||
|
||||
{/* [mobile] [corner] Draw button */}
|
||||
{(isDraw || isDrawPlus) && <DrawOptionsButtonMobile
|
||||
onClick={handleDrawOptionsClicked}
|
||||
sx={{ ...hideOnDesktop, mr: { xs: 1, md: 2 } }}
|
||||
/>}
|
||||
{/* [mobile, corner] Draw Options secondary button */}
|
||||
{isMobile && (isDraw || isDrawPlus) && <DrawOptionsButtonMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />}
|
||||
|
||||
{/* Responsive Send/Stop buttons */}
|
||||
{assistantTyping
|
||||
@@ -666,13 +591,16 @@ export function Composer(props: {
|
||||
</Box>
|
||||
|
||||
|
||||
{/* [desktop] other buttons (aligned to bottom for now, and mutually exclusive) */}
|
||||
<Box sx={{ flexGrow: 1, flexDirection: 'column', gap: 1, justifyContent: 'flex-end', ...hideOnMobile }}>
|
||||
{/* [desktop] secondary buttons (aligned to bottom for now, and mutually exclusive) */}
|
||||
{isDesktop && <Box sx={{ flexGrow: 1, flexDirection: 'column', gap: 1, justifyContent: 'flex-end' }}>
|
||||
|
||||
{/* [desktop] Call secondary button */}
|
||||
{isChat && <CallButtonDesktop disabled={!APP_CALL_ENABLED || !props.conversationId || !chatLLM} onClick={handleCallClicked} />}
|
||||
|
||||
{/* [desktop] Draw Options secondary button */}
|
||||
{(isDraw || isDrawPlus) && <DrawOptionsButtonDesktop onClick={handleDrawOptionsClicked} />}
|
||||
</Box>
|
||||
|
||||
</Box>}
|
||||
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -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<DModelSourceId | null>(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: {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 1 }}>
|
||||
|
||||
{/* Models: [Select] Add Delete */}
|
||||
<Typography sx={{ mr: 1, ...hideOnMobile }}>
|
||||
{!isMobile && <Typography sx={{ mr: 1 }}>
|
||||
Service:
|
||||
</Typography>
|
||||
</Typography>}
|
||||
|
||||
<Select
|
||||
variant='outlined'
|
||||
@@ -133,12 +134,15 @@ export function ModelsSourceSelector(props: {
|
||||
{sourceItems.map(item => item.component)}
|
||||
</Select>
|
||||
|
||||
<IconButton variant={noSources ? 'solid' : 'plain'} color='primary' onClick={handleShowVendors} disabled={!!vendorsMenuAnchor} sx={{ ...hideOnDesktop }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<Button variant={noSources ? 'solid' : 'plain'} onClick={handleShowVendors} disabled={!!vendorsMenuAnchor} startDecorator={<AddIcon />} sx={{ ...hideOnMobile }}>
|
||||
Add
|
||||
</Button>
|
||||
{isMobile ? (
|
||||
<IconButton variant={noSources ? 'solid' : 'plain'} color='primary' onClick={handleShowVendors} disabled={!!vendorsMenuAnchor}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button variant={noSources ? 'solid' : 'plain'} onClick={handleShowVendors} disabled={!!vendorsMenuAnchor} startDecorator={<AddIcon />}>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
variant='plain' color='neutral' disabled={!enableDeleteButton} sx={{ ml: 'auto' }}
|
||||
|
||||
Reference in New Issue
Block a user