diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx
index ee206c704..adca71759 100644
--- a/src/apps/chat/AppChat.tsx
+++ b/src/apps/chat/AppChat.tsx
@@ -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;
diff --git a/src/apps/chat/components/composer/ChatModeMenu.tsx b/src/apps/chat/components/composer/ChatModeMenu.tsx
deleted file mode 100644
index 9db328a10..000000000
--- a/src/apps/chat/components/composer/ChatModeMenu.tsx
+++ /dev/null
@@ -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 (
-
-
- {/**/}
- {/**/}
- {/**/}
-
- {/* ChatMode items */}
- {Object.entries(ChatModeItems)
- .filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
- .map(([key, data]) =>
- )}
-
-
- );
-}
\ 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 738ede0ea..2786ee39b 100644
--- a/src/apps/chat/components/composer/Composer.tsx
+++ b/src/apps/chat/components/composer/Composer.tsx
@@ -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('generate-content');
const [composeText, debouncedText, setComposeText] = useDebouncer('', 300, 1200, true);
const [micContinuation, setMicContinuation] = React.useState(false);
const [speechInterimResult, setSpeechInterimResult] = React.useState(null);
const [isDragging, setIsDragging] = React.useState(false);
- const [chatModeMenuAnchor, setChatModeMenuAnchor] = React.useState(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 => {
+
+ const handleSendAction = React.useCallback(async (_chatExecuteMode: ChatExecuteMode, composerText: string): Promise => {
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) => {
- 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 */}
@@ -911,14 +898,7 @@ export function Composer(props: {
{/* Mode Menu */}
- {!!chatModeMenuAnchor && (
-
- )}
+ {chatExecuteMenuComponent}
{/* Camera (when open) */}
{cameraCaptureComponent}
diff --git a/src/apps/chat/editors/_handleExecute.ts b/src/apps/chat/editors/_handleExecute.ts
index ed7c4fafd..c2756ebdb 100644
--- a/src/apps/chat/editors/_handleExecute.ts
+++ b/src/apps/chat/editors/_handleExecute.ts
@@ -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;
}
}
diff --git a/src/apps/chat/execute-mode/ExecuteModeMenu.tsx b/src/apps/chat/execute-mode/ExecuteModeMenu.tsx
new file mode 100644
index 000000000..acaef3a1a
--- /dev/null
+++ b/src/apps/chat/execute-mode/ExecuteModeMenu.tsx
@@ -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 (
+
+
+ {/**/}
+ {/**/}
+ {/**/}
+
+ {/* Items */}
+ {Object.entries(ExecuteModeItems)
+ .filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
+ .map(([key, data]) =>
+ ,
+ )}
+
+
+ );
+}
+
+function newLineShortcut(shortcut: string, enterIsNewLine: boolean) {
+ if (shortcut === 'ENTER')
+ return enterIsNewLine ? 'Shift + Enter' : 'Enter';
+ return shortcut;
+}
diff --git a/src/apps/chat/execute-mode/execute-mode.items.ts b/src/apps/chat/execute-mode/execute-mode.items.ts
new file mode 100644
index 000000000..d0b0b50d5
--- /dev/null
+++ b/src/apps/chat/execute-mode/execute-mode.items.ts
@@ -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',
+ },
+};
diff --git a/src/apps/chat/execute-mode/execute-mode.types.ts b/src/apps/chat/execute-mode/execute-mode.types.ts
new file mode 100644
index 000000000..13c25e67c
--- /dev/null
+++ b/src/apps/chat/execute-mode/execute-mode.types.ts
@@ -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'
+ ;
diff --git a/src/apps/chat/execute-mode/useChatExecuteMode.tsx b/src/apps/chat/execute-mode/useChatExecuteMode.tsx
new file mode 100644
index 000000000..3803f0712
--- /dev/null
+++ b/src/apps/chat/execute-mode/useChatExecuteMode.tsx
@@ -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('generate-content');
+ const [chatExecuteModeMenuAnchor, setChatExecuteModeMenuAnchor] = React.useState(null);
+
+
+ const handleMenuHide = React.useCallback(() => setChatExecuteModeMenuAnchor(null), []);
+
+ const handleMenuShow = React.useCallback((event: React.MouseEvent) => {
+ setChatExecuteModeMenuAnchor(anchor => anchor ? null : event.currentTarget);
+ }, []);
+
+ const handleChangeMode = React.useCallback((mode: ChatExecuteMode) => {
+ handleMenuHide();
+ setChatExecuteMode(mode);
+ }, [handleMenuHide]);
+
+
+ const chatExecuteMenuComponent = React.useMemo(() => !!chatExecuteModeMenuAnchor && (
+
+ ), [capabilityHasT2I, chatExecuteMode, chatExecuteModeMenuAnchor, handleMenuHide, handleChangeMode, isMobile]);
+
+
+ return {
+ chatExecuteMode,
+ chatExecuteMenuComponent,
+ chatExecuteMenuShown: !!chatExecuteModeMenuAnchor,
+ showChatExecuteMenu: handleMenuShow,
+ };
+}