mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Extract ChatExecuteMode
This commit is contained in:
@@ -44,6 +44,8 @@ import { ChatPageMenuItems } from './components/layout-menu/ChatPageMenuItems';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { usePanesManager } from './components/panes/usePanesManager';
|
||||
|
||||
import type { ChatExecuteMode } from './execute-mode/execute-mode.types';
|
||||
|
||||
import { _handleExecute } from './editors/_handleExecute';
|
||||
import { gcChatImageAssets } from './editors/image-generate';
|
||||
|
||||
@@ -52,19 +54,6 @@ import { gcChatImageAssets } from './editors/image-generate';
|
||||
export const CHAT_NOVEL_TITLE = 'Chat';
|
||||
|
||||
|
||||
/**
|
||||
* Mode: how to treat the input from the Composer
|
||||
*/
|
||||
export type ChatModeId =
|
||||
| 'append-user'
|
||||
| 'beam-content'
|
||||
| 'generate-content'
|
||||
| 'generate-image'
|
||||
| 'generate-text-v1'
|
||||
| 'react-content'
|
||||
;
|
||||
|
||||
|
||||
export interface AppChatIntent {
|
||||
initialConversationId: string | null;
|
||||
}
|
||||
@@ -206,8 +195,8 @@ export function AppChat() {
|
||||
|
||||
// Execution
|
||||
|
||||
const handleExecuteAndOutcome = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, callerNameDebug: string) => {
|
||||
const outcome = await _handleExecute(chatModeId, conversationId, callerNameDebug);
|
||||
const handleExecuteAndOutcome = React.useCallback(async (chatExecuteMode: ChatExecuteMode, conversationId: DConversationId, callerNameDebug: string) => {
|
||||
const outcome = await _handleExecute(chatExecuteMode, conversationId, callerNameDebug);
|
||||
if (outcome === 'err-no-chatllm')
|
||||
openModelsSetup();
|
||||
else if (outcome === 'err-t2i-unconfigured')
|
||||
@@ -221,7 +210,7 @@ export function AppChat() {
|
||||
return outcome === true;
|
||||
}, [openModelsSetup, openPreferencesTab]);
|
||||
|
||||
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatModeId: ChatModeId, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata): boolean => {
|
||||
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatExecuteMode: ChatExecuteMode, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata): boolean => {
|
||||
|
||||
// [multicast] send the message to all the panes
|
||||
const uniqueConversationIds = willMulticast
|
||||
@@ -245,7 +234,7 @@ export function AppChat() {
|
||||
ConversationsManager.getHandler(conversation.id).messageAppend(userMessage); // [chat] append user message in each conversation
|
||||
|
||||
// fire/forget
|
||||
void handleExecuteAndOutcome(chatModeId /* various */, conversation.id, 'chat-composer-action'); // append user message, then '*-*'
|
||||
void handleExecuteAndOutcome(chatExecuteMode /* various */, conversation.id, 'chat-composer-action'); // append user message, then '*-*'
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, MenuItem, Radio, Typography } from '@mui/joy';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { KeyStroke, platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import type { ChatModeId } from '../../AppChat';
|
||||
|
||||
|
||||
export function chatModeCanAttach(chatModeId: ChatModeId) {
|
||||
return !!ChatModeItems[chatModeId]?.canAttach;
|
||||
}
|
||||
|
||||
const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
'generate-content': {
|
||||
label: 'Chat',
|
||||
description: 'Persona replies',
|
||||
canAttach: true,
|
||||
},
|
||||
'generate-text-v1': {
|
||||
label: 'Chat (Stable)',
|
||||
description: 'Model replies (stable)',
|
||||
canAttach: true,
|
||||
},
|
||||
'beam-content': {
|
||||
label: 'Beam', // Best of, Auto-Prime, Top Pick, Select Best
|
||||
description: 'Combine multiple models', // Smarter: combine...
|
||||
shortcut: 'Ctrl + Enter',
|
||||
canAttach: true,
|
||||
hideOnDesktop: true,
|
||||
},
|
||||
'append-user': {
|
||||
label: 'Write',
|
||||
description: 'Append a message',
|
||||
shortcut: 'Alt + Enter',
|
||||
canAttach: true,
|
||||
},
|
||||
'generate-image': {
|
||||
label: 'Draw',
|
||||
description: 'AI Image Generation',
|
||||
requiresTTI: true,
|
||||
},
|
||||
'react-content': {
|
||||
label: 'Reason + Act', // · α
|
||||
description: 'Answer questions in multiple steps',
|
||||
},
|
||||
};
|
||||
|
||||
interface ChatModeDescription {
|
||||
label: string;
|
||||
description: string | React.JSX.Element;
|
||||
canAttach?: boolean;
|
||||
highlight?: boolean;
|
||||
shortcut?: string;
|
||||
hideOnDesktop?: boolean;
|
||||
requiresTTI?: boolean;
|
||||
}
|
||||
|
||||
|
||||
function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
|
||||
if (shortcut === 'ENTER')
|
||||
return enterIsNewLine ? 'Shift + Enter' : 'Enter';
|
||||
return shortcut;
|
||||
}
|
||||
|
||||
export function ChatModeMenu(props: {
|
||||
isMobile: boolean,
|
||||
anchorEl: HTMLAnchorElement | null,
|
||||
onClose: () => void,
|
||||
chatModeId: ChatModeId,
|
||||
onSetChatModeId: (chatMode: ChatModeId) => void,
|
||||
capabilityHasTTI: boolean,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
|
||||
return (
|
||||
<CloseableMenu
|
||||
placement='top-end'
|
||||
open anchorEl={props.anchorEl} onClose={props.onClose}
|
||||
sx={{ minWidth: 320 }}
|
||||
>
|
||||
|
||||
{/*<MenuItem color='neutral' selected>*/}
|
||||
{/* Conversation Mode*/}
|
||||
{/*</MenuItem>*/}
|
||||
{/**/}
|
||||
{/*<ListDivider />*/}
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio color={data.highlight ? 'success' : undefined} checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={platformAwareKeystrokes(fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline))} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>)}
|
||||
|
||||
</CloseableMenu>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import SendIcon from '@mui/icons-material/Send';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import type { ChatModeId } from '../../AppChat';
|
||||
import { useChatMicTimeoutMsValue } from '../../store-app-chat';
|
||||
|
||||
import type { DLLM } from '~/modules/llms/store-llms';
|
||||
@@ -56,6 +55,9 @@ import { LLMAttachmentDraftsAction, LLMAttachmentsList } from './llmattachments/
|
||||
import { useAttachmentDrafts } from '~/common/attachment-drafts/useAttachmentDrafts';
|
||||
import { useLLMAttachmentDrafts } from './llmattachments/useLLMAttachmentDrafts';
|
||||
|
||||
import type { ChatExecuteMode } from '../../execute-mode/execute-mode.types';
|
||||
import { chatExecuteModeCanAttach, useChatExecuteMode } from '../../execute-mode/useChatExecuteMode';
|
||||
|
||||
import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
@@ -69,7 +71,6 @@ import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
|
||||
import { ReplyToBubble } from '../message/ReplyToBubble';
|
||||
import { TokenBadgeMemo } from './TokenBadge';
|
||||
import { TokenProgressbarMemo } from './TokenProgressbar';
|
||||
import { chatModeCanAttach, ChatModeMenu } from './ChatModeMenu';
|
||||
import { useComposerStartupText } from './store-composer';
|
||||
|
||||
|
||||
@@ -103,19 +104,19 @@ export function Composer(props: {
|
||||
capabilityHasT2I: boolean;
|
||||
isMulticast: boolean | null;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (conversationId: DConversationId, chatModeId: ChatModeId, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata) => boolean;
|
||||
onAction: (conversationId: DConversationId, chatExecuteMode: ChatExecuteMode, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
setIsMulticast: (on: boolean) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-content');
|
||||
const [composeText, debouncedText, setComposeText] = useDebouncer('', 300, 1200, true);
|
||||
const [micContinuation, setMicContinuation] = React.useState(false);
|
||||
const [speechInterimResult, setSpeechInterimResult] = React.useState<SpeechResult | null>(null);
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
const [chatModeMenuAnchor, setChatModeMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
|
||||
const { chatExecuteMode, chatExecuteMenuShown, showChatExecuteMenu, chatExecuteMenuComponent } =
|
||||
useChatExecuteMode(props.capabilityHasT2I, !!props.isMobile);
|
||||
|
||||
// external state
|
||||
const { openPreferencesTab /*, setIsFocusedMode*/ } = useOptimaLayout();
|
||||
@@ -148,7 +149,7 @@ export function Composer(props: {
|
||||
|
||||
// composer-overlay: for the reply-to state, comes from the conversation overlay
|
||||
const { replyToGenerateText } = useChatOverlayStore(conversationOverlayStore, useShallow(store => ({
|
||||
replyToGenerateText: (chatModeId === 'generate-content' || chatModeId === 'generate-text-v1') ? store.replyToText?.trim() || null : null,
|
||||
replyToGenerateText: (chatExecuteMode === 'generate-content' || chatExecuteMode === 'generate-text-v1') ? store.replyToText?.trim() || null : null,
|
||||
})));
|
||||
|
||||
// don't load URLs if the user is typing a command or there's no capability
|
||||
@@ -172,7 +173,7 @@ export function Composer(props: {
|
||||
const isDesktop = !props.isMobile;
|
||||
const noConversation = !targetConversationId;
|
||||
const noLLM = !props.chatLLM;
|
||||
const showLLMAttachments = chatModeCanAttach(chatModeId);
|
||||
const showLLMAttachments = chatExecuteModeCanAttach(chatExecuteMode);
|
||||
|
||||
|
||||
// tokens derived state
|
||||
@@ -221,11 +222,12 @@ export function Composer(props: {
|
||||
handleReplyToClear();
|
||||
}, [attachmentsRemoveAll, handleReplyToClear, setComposeText]);
|
||||
|
||||
const handleSendAction = React.useCallback(async (_chatModeId: ChatModeId, composerText: string): Promise<boolean> => {
|
||||
|
||||
const handleSendAction = React.useCallback(async (_chatExecuteMode: ChatExecuteMode, composerText: string): Promise<boolean> => {
|
||||
if (!isValidConversation(targetConversationId)) return false;
|
||||
|
||||
// validate some chat mode inputs
|
||||
const isDraw = _chatModeId === 'generate-image';
|
||||
const isDraw = _chatExecuteMode === 'generate-image';
|
||||
const isBlank = !composerText.trim();
|
||||
if (isDraw && isBlank)
|
||||
return false;
|
||||
@@ -235,7 +237,7 @@ export function Composer(props: {
|
||||
if (composerText)
|
||||
fragments.push(createTextContentFragment(composerText));
|
||||
|
||||
const canAttach = chatModeCanAttach(_chatModeId);
|
||||
const canAttach = chatExecuteModeCanAttach(_chatExecuteMode);
|
||||
if (canAttach) {
|
||||
const attachmentFragments = await attachmentsTakeAllFragments('global', 'app-chat');
|
||||
fragments.push(...attachmentFragments);
|
||||
@@ -248,15 +250,16 @@ export function Composer(props: {
|
||||
|
||||
// send the message - NOTE: if successful, the ownership of the fragments is transferred to the receiver, so we just clear them
|
||||
const metadata = replyToGenerateText ? { inReplyToText: replyToGenerateText } : undefined;
|
||||
const enqueued = onAction(targetConversationId, _chatModeId, fragments, metadata);
|
||||
const enqueued = onAction(targetConversationId, _chatExecuteMode, fragments, metadata);
|
||||
if (enqueued)
|
||||
handleClear();
|
||||
return enqueued;
|
||||
}, [attachmentsTakeAllFragments, handleClear, onAction, replyToGenerateText, targetConversationId]);
|
||||
|
||||
|
||||
const handleSendClicked = React.useCallback(async () => {
|
||||
await handleSendAction(chatModeId, composeText); // 'chat/write/...' button
|
||||
}, [chatModeId, composeText, handleSendAction]);
|
||||
await handleSendAction(chatExecuteMode, composeText); // 'chat/write/...' button
|
||||
}, [chatExecuteMode, composeText, handleSendAction]);
|
||||
|
||||
const handleSendTextBeamClicked = React.useCallback(async () => {
|
||||
await handleSendAction('beam-content', composeText); // 'beam' button
|
||||
@@ -284,22 +287,6 @@ export function Composer(props: {
|
||||
}, [composeText, onTextImagine, setComposeText, targetConversationId]);
|
||||
|
||||
|
||||
// Mode menu
|
||||
|
||||
const handleModeSelectorHide = React.useCallback(() => {
|
||||
setChatModeMenuAnchor(null);
|
||||
}, []);
|
||||
|
||||
const handleModeSelectorShow = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
setChatModeMenuAnchor(anchor => anchor ? null : event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleModeChange = React.useCallback((_chatModeId: ChatModeId) => {
|
||||
handleModeSelectorHide();
|
||||
setChatModeId(_chatModeId);
|
||||
}, [handleModeSelectorHide]);
|
||||
|
||||
|
||||
// Actiles
|
||||
|
||||
const onActileCommandPaste = React.useCallback((item: ActileItem) => {
|
||||
@@ -380,12 +367,12 @@ export function Composer(props: {
|
||||
touchShiftEnter();
|
||||
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
|
||||
if (!assistantAbortible)
|
||||
await handleSendAction(chatModeId, composeText); // enter -> send
|
||||
await handleSendAction(chatExecuteMode, composeText); // enter -> send
|
||||
return e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatExecuteMode, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
|
||||
|
||||
// Focus mode
|
||||
@@ -416,7 +403,7 @@ export function Composer(props: {
|
||||
if (autoSend) {
|
||||
if (notUserStop)
|
||||
playSoundUrl('/sounds/mic-off-mid.mp3');
|
||||
void handleSendAction(chatModeId, nextText); // fire/forget
|
||||
void handleSendAction(chatExecuteMode, nextText); // fire/forget
|
||||
} else {
|
||||
if (!micContinuation && notUserStop)
|
||||
playSoundUrl('/sounds/mic-off-mid.mp3');
|
||||
@@ -425,7 +412,7 @@ export function Composer(props: {
|
||||
setComposeText(nextText);
|
||||
}
|
||||
}
|
||||
}, [chatModeId, composeText, composerTextAreaRef, handleSendAction, micContinuation, noConversation, setComposeText]);
|
||||
}, [chatExecuteMode, composeText, composerTextAreaRef, handleSendAction, micContinuation, noConversation, setComposeText]);
|
||||
|
||||
const { isSpeechEnabled, isSpeechError, isRecordingAudio, isRecordingSpeech, toggleRecording } =
|
||||
useSpeechRecognition(onSpeechResultCallback, chatMicTimeoutMs || 2000);
|
||||
@@ -547,11 +534,11 @@ export function Composer(props: {
|
||||
}, [attachAppendDataTransfer, eatDragEvent, setComposeText]);
|
||||
|
||||
|
||||
const isText = chatModeId === 'generate-content' || chatModeId === 'generate-text-v1';
|
||||
const isTextBeam = chatModeId === 'beam-content';
|
||||
const isAppend = chatModeId === 'append-user';
|
||||
const isReAct = chatModeId === 'react-content';
|
||||
const isDraw = chatModeId === 'generate-image';
|
||||
const isText = chatExecuteMode === 'generate-content' || chatExecuteMode === 'generate-text-v1';
|
||||
const isTextBeam = chatExecuteMode === 'beam-content';
|
||||
const isAppend = chatExecuteMode === 'append-user';
|
||||
const isReAct = chatExecuteMode === 'react-content';
|
||||
const isDraw = chatExecuteMode === 'generate-image';
|
||||
|
||||
const showChatReplyTo = !!replyToGenerateText;
|
||||
const showChatExtras = isText && !showChatReplyTo;
|
||||
@@ -873,8 +860,8 @@ export function Composer(props: {
|
||||
{/* Mode expander */}
|
||||
<IconButton
|
||||
variant={assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
|
||||
disabled={noConversation || noLLM || !!chatModeMenuAnchor}
|
||||
onClick={handleModeSelectorShow}
|
||||
disabled={noConversation || noLLM || chatExecuteMenuShown}
|
||||
onClick={showChatExecuteMenu}
|
||||
>
|
||||
<ExpandLessIcon />
|
||||
</IconButton>
|
||||
@@ -911,14 +898,7 @@ export function Composer(props: {
|
||||
</Grid>
|
||||
|
||||
{/* Mode Menu */}
|
||||
{!!chatModeMenuAnchor && (
|
||||
<ChatModeMenu
|
||||
isMobile={isMobile}
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
/>
|
||||
)}
|
||||
{chatExecuteMenuComponent}
|
||||
|
||||
{/* Camera (when open) */}
|
||||
{cameraCaptureComponent}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createTextContentFragment, isContentFragment, isTextPart } from '~/comm
|
||||
import { getConversationSystemPurposeId } from '~/common/stores/chat/store-chats';
|
||||
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ChatModeId } from '../AppChat';
|
||||
import type { ChatExecuteMode } from '../execute-mode/execute-mode.types';
|
||||
import { getInstantAppChatPanesCount } from '../components/panes/usePanesManager';
|
||||
import { textToDrawCommand } from '../commands/CommandsDraw';
|
||||
|
||||
@@ -19,7 +19,7 @@ import { runPersonaUpdatingState } from './chat-persona';
|
||||
import { runReActUpdatingState } from './react-tangent';
|
||||
|
||||
|
||||
export async function _handleExecute(chatModeId: ChatModeId, conversationId: DConversationId, executeCallerNameDebug: string) {
|
||||
export async function _handleExecute(chatExecuteMode: ChatExecuteMode, conversationId: DConversationId, executeCallerNameDebug: string) {
|
||||
|
||||
// Handle missing conversation
|
||||
if (!conversationId)
|
||||
@@ -46,7 +46,7 @@ export async function _handleExecute(chatModeId: ChatModeId, conversationId: DCo
|
||||
|
||||
|
||||
// Handle unconfigured
|
||||
if (!chatLLMId || !chatModeId)
|
||||
if (!chatLLMId || !chatExecuteMode)
|
||||
return !chatLLMId ? 'err-no-chatllm' : 'err-no-chatmode';
|
||||
|
||||
// handle missing last user message (or fragment)
|
||||
@@ -71,7 +71,7 @@ export async function _handleExecute(chatModeId: ChatModeId, conversationId: DCo
|
||||
}
|
||||
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
switch (chatModeId) {
|
||||
switch (chatExecuteMode) {
|
||||
case 'generate-content':
|
||||
return await runPersonaUpdatingState(conversationId, chatLLMId);
|
||||
|
||||
@@ -102,7 +102,7 @@ export async function _handleExecute(chatModeId: ChatModeId, conversationId: DCo
|
||||
return await runReActUpdatingState(cHandler, reactPrompt, chatLLMId);
|
||||
|
||||
default:
|
||||
console.log('Chat execute: issue running', chatModeId, conversationId, lastMessage);
|
||||
console.log('Chat execute: issue running', chatExecuteMode, conversationId, lastMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, MenuItem, Radio, Typography } from '@mui/joy';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { KeyStroke, platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import type { ChatExecuteMode } from './execute-mode.types';
|
||||
import { ExecuteModeItems } from './execute-mode.items';
|
||||
|
||||
|
||||
export function ExecuteModeMenu(props: {
|
||||
isMobile: boolean,
|
||||
hasCapabilityT2I: boolean,
|
||||
anchorEl: HTMLAnchorElement | null,
|
||||
onClose: () => void,
|
||||
chatExecuteMode: ChatExecuteMode,
|
||||
onSetChatExecuteMode: (chatExecuteMode: ChatExecuteMode) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
|
||||
return (
|
||||
<CloseableMenu
|
||||
placement='top-end'
|
||||
open anchorEl={props.anchorEl} onClose={props.onClose}
|
||||
sx={{ minWidth: 320 }}
|
||||
>
|
||||
|
||||
{/*<MenuItem color='neutral' selected>*/}
|
||||
{/* Conversation Mode*/}
|
||||
{/*</MenuItem>*/}
|
||||
{/**/}
|
||||
{/*<ListDivider />*/}
|
||||
|
||||
{/* Items */}
|
||||
{Object.entries(ExecuteModeItems)
|
||||
.filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatExecuteMode(key as ChatExecuteMode)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio color={data.highlight ? 'success' : undefined} checked={key === props.chatExecuteMode} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.hasCapabilityT2I) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatExecuteMode || !!data.shortcut) && (
|
||||
<KeyStroke combo={platformAwareKeystrokes(
|
||||
newLineShortcut(
|
||||
(key === props.chatExecuteMode) ? 'ENTER'
|
||||
: data.shortcut ? data.shortcut
|
||||
: 'ENTER',
|
||||
enterIsNewline,
|
||||
),
|
||||
)} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>,
|
||||
)}
|
||||
|
||||
</CloseableMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function newLineShortcut(shortcut: string, enterIsNewLine: boolean) {
|
||||
if (shortcut === 'ENTER')
|
||||
return enterIsNewLine ? 'Shift + Enter' : 'Enter';
|
||||
return shortcut;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ChatExecuteMode } from './execute-mode.types';
|
||||
|
||||
|
||||
interface ModeDescription {
|
||||
label: string;
|
||||
description: string | React.JSX.Element;
|
||||
canAttach?: boolean;
|
||||
highlight?: boolean;
|
||||
shortcut?: string;
|
||||
hideOnDesktop?: boolean;
|
||||
requiresTTI?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export const ExecuteModeItems: { [key in ChatExecuteMode]: ModeDescription } = {
|
||||
'generate-content': {
|
||||
label: 'Chat',
|
||||
description: 'Persona replies',
|
||||
canAttach: true,
|
||||
},
|
||||
'generate-text-v1': {
|
||||
label: 'Chat (Stable)',
|
||||
description: 'Model replies (stable)',
|
||||
canAttach: true,
|
||||
},
|
||||
'beam-content': {
|
||||
label: 'Beam', // Best of, Auto-Prime, Top Pick, Select Best
|
||||
description: 'Combine multiple models', // Smarter: combine...
|
||||
shortcut: 'Ctrl + Enter',
|
||||
canAttach: true,
|
||||
hideOnDesktop: true,
|
||||
},
|
||||
'append-user': {
|
||||
label: 'Write',
|
||||
description: 'Append a message',
|
||||
shortcut: 'Alt + Enter',
|
||||
canAttach: true,
|
||||
},
|
||||
'generate-image': {
|
||||
label: 'Draw',
|
||||
description: 'AI Image Generation',
|
||||
requiresTTI: true,
|
||||
},
|
||||
'react-content': {
|
||||
label: 'Reason + Act', // · α
|
||||
description: 'Answer questions in multiple steps',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Mode: how to treat the input from the Composer
|
||||
* Was: ChatModeId
|
||||
*/
|
||||
export type ChatExecuteMode =
|
||||
| 'append-user'
|
||||
| 'beam-content'
|
||||
| 'generate-content'
|
||||
| 'generate-image'
|
||||
| 'generate-text-v1'
|
||||
| 'react-content'
|
||||
;
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ChatExecuteMode } from './execute-mode.types';
|
||||
import { ExecuteModeMenu } from './ExecuteModeMenu';
|
||||
import { ExecuteModeItems } from './execute-mode.items';
|
||||
|
||||
|
||||
export function chatExecuteModeCanAttach(chatExecuteMode: ChatExecuteMode) {
|
||||
return !!ExecuteModeItems[chatExecuteMode]?.canAttach;
|
||||
}
|
||||
|
||||
|
||||
export function useChatExecuteMode(capabilityHasT2I: boolean, isMobile: boolean) {
|
||||
|
||||
// state
|
||||
const [chatExecuteMode, setChatExecuteMode] = React.useState<ChatExecuteMode>('generate-content');
|
||||
const [chatExecuteModeMenuAnchor, setChatExecuteModeMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
|
||||
|
||||
|
||||
const handleMenuHide = React.useCallback(() => setChatExecuteModeMenuAnchor(null), []);
|
||||
|
||||
const handleMenuShow = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
setChatExecuteModeMenuAnchor(anchor => anchor ? null : event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleChangeMode = React.useCallback((mode: ChatExecuteMode) => {
|
||||
handleMenuHide();
|
||||
setChatExecuteMode(mode);
|
||||
}, [handleMenuHide]);
|
||||
|
||||
|
||||
const chatExecuteMenuComponent = React.useMemo(() => !!chatExecuteModeMenuAnchor && (
|
||||
<ExecuteModeMenu
|
||||
isMobile={isMobile}
|
||||
hasCapabilityT2I={capabilityHasT2I}
|
||||
anchorEl={chatExecuteModeMenuAnchor}
|
||||
onClose={handleMenuHide}
|
||||
chatExecuteMode={chatExecuteMode}
|
||||
onSetChatExecuteMode={handleChangeMode}
|
||||
/>
|
||||
), [capabilityHasT2I, chatExecuteMode, chatExecuteModeMenuAnchor, handleMenuHide, handleChangeMode, isMobile]);
|
||||
|
||||
|
||||
return {
|
||||
chatExecuteMode,
|
||||
chatExecuteMenuComponent,
|
||||
chatExecuteMenuShown: !!chatExecuteModeMenuAnchor,
|
||||
showChatExecuteMenu: handleMenuShow,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user