diff --git a/public/icons/icon-voicechat-96x96.png b/public/icons/icon-voicechat-96x96.png new file mode 100644 index 000000000..dbf8c589d Binary files /dev/null and b/public/icons/icon-voicechat-96x96.png differ diff --git a/public/manifest.json b/public/manifest.json index dd998673d..a2bf3fcf9 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -66,6 +66,18 @@ "type": "image/png" } ] + }, + { + "name": "New Voice Chat", + "url": "/?newChat=voiceInput", + "description": "Start a new chat with voice input", + "icons": [ + { + "src": "/icons/icon-voicechat-96x96.png", + "sizes": "96x96", + "type": "image/png" + } + ] } ] } diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 9d692104d..5dca3ec93 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -61,7 +61,8 @@ export const CHAT_NOVEL_TITLE = 'Chat'; export interface AppChatIntent { - initialConversationId: string | null; + initialConversationId?: string; + newChat?: 'voiceInput'; } const scrollToBottomSx = { @@ -200,23 +201,6 @@ export function AppChat() { showNextTitleChange.current = true; }, [navigateHistoryInFocusedPane]); - // [effect] Handle the initial conversation intent - React.useEffect(() => { - if (Release.IsNodeDevBuild && intent.initialConversationId === 'null') - return openConversationInFocusedPane(null! /* for debugging purporse */); - intent.initialConversationId && openConversationInFocusedPane(intent.initialConversationId); - }, [intent.initialConversationId, openConversationInFocusedPane]); - - // [effect] Show snackbar with the focused chat title after a history navigation in focused pane - React.useEffect(() => { - if (showNextTitleChange.current) { - showNextTitleChange.current = false; - const title = (focusedChatNumber >= 0 ? `#${focusedChatNumber + 1} · ` : '') + (focusedChatTitle || 'New Chat'); - const id = addSnackbar({ key: 'focused-title', message: title, type: 'center-title' }); - return () => removeSnackbar(id); - } - }, [focusedChatNumber, focusedChatTitle]); - // Execution @@ -485,6 +469,32 @@ export function AppChat() { useSetOptimaAppMenu(focusedMenuItems, 'AppChat'); + // Effects + + // [effect] Handle the conversation intent + React.useEffect(() => { + // Debug: open a null chat + if (Release.IsNodeDevBuild && intent.initialConversationId === 'null') + openConversationInFocusedPane(null! /* for debugging purporse */); + // Open the initial conversation if set + else if (intent.initialConversationId) + openConversationInFocusedPane(intent.initialConversationId); + // Create a new chat if requested + else if (intent.newChat !== undefined) + handleConversationNewInFocusedPane(false, false); + }, [handleConversationNewInFocusedPane, intent.initialConversationId, intent.newChat, openConversationInFocusedPane]); + + // [effect] Show snackbar with the focused chat title after a history navigation in focused pane + React.useEffect(() => { + if (showNextTitleChange.current) { + showNextTitleChange.current = false; + const title = (focusedChatNumber >= 0 ? `#${focusedChatNumber + 1} · ` : '') + (focusedChatTitle || 'New Chat'); + const id = addSnackbar({ key: 'focused-title', message: title, type: 'center-title' }); + return () => removeSnackbar(id); + } + }, [focusedChatNumber, focusedChatTitle]); + + // Shortcuts const handleOpenChatLlmOptions = React.useCallback(() => { diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index cba326b78..cf270b64c 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -13,6 +13,7 @@ import SendIcon from '@mui/icons-material/Send'; import StopOutlinedIcon from '@mui/icons-material/StopOutlined'; import TelegramIcon from '@mui/icons-material/Telegram'; +import type { AppChatIntent } from '../../AppChat'; import { useChatAutoSuggestAttachmentPrompts, useChatMicTimeoutMsValue } from '../../store-app-chat'; import { useAgiAttachmentPrompts } from '~/modules/aifn/agiattachmentprompts/useAgiAttachmentPrompts'; @@ -36,7 +37,7 @@ import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentF import { estimateTextTokens, glueForMessageTokens, marshallWrapDocFragments } from '~/common/stores/chat/chat.tokens'; import { getConversation, isValidConversation, useChatStore } from '~/common/stores/chat/store-chats'; import { getModelParameterValueOrThrow } from '~/common/stores/llms/llms.parameters'; -import { launchAppCall } from '~/common/app.routes'; +import { launchAppCall, removeQueryParam, useRouterQuery } from '~/common/app.routes'; import { lineHeightTextareaMd } from '~/common/app.theme'; import { optimaOpenPreferences } from '~/common/layout/optima/useOptima'; import { platformAwareKeystrokes } from '~/common/components/KeyStroke'; @@ -125,6 +126,7 @@ export function Composer(props: { // external state const { showPromisedOverlay } = useOverlayComponents(); + const { newChat: appChatNewChatIntent } = useRouterQuery>(); const { labsAttachScreenCapture, labsCameraDesktop, labsShowCost, labsShowShortcutBar } = useUXLabsStore(useShallow(state => ({ labsAttachScreenCapture: state.labsAttachScreenCapture, labsCameraDesktop: state.labsCameraDesktop, @@ -398,6 +400,14 @@ export function Composer(props: { }); }, [speechInterimResult]); + React.useEffect(() => { + // auto-start the microphone if appChat was created with a particular intent + if (appChatNewChatIntent === 'voiceInput') { + toggleRecognition(); + void removeQueryParam('newChat'); + } + }, [appChatNewChatIntent, toggleRecognition]); + // Other send actins diff --git a/src/common/app.routes.ts b/src/common/app.routes.ts index d076c6b6d..cef1afe03 100644 --- a/src/common/app.routes.ts +++ b/src/common/app.routes.ts @@ -79,6 +79,7 @@ export async function launchAppChat(conversationId?: DConversationId) { pathname: ROUTE_APP_CHAT, query: !conversationId ? undefined : { initialConversationId: conversationId, + // newChat?: 'voiceInput', } satisfies AppChatIntent, }, ROUTE_APP_CHAT, @@ -97,4 +98,19 @@ export function launchAppCall(conversationId: string, personaId: string) { }, // ROUTE_APP_CALL, ).then(); -} \ No newline at end of file +} + + +/// Query Params utilities + +export function removeQueryParam(key: string): Promise { + const newQuery = { ...Router.query }; + delete newQuery[key]; + return Router.replace({ pathname: Router.pathname, query: newQuery }, undefined, { shallow: true }); +} + +/*export function removeQueryParams(keysToRemove: string[]): Promise { + const newQuery = { ...Router.query }; + keysToRemove.forEach(key => delete newQuery[key]); + return Router.replace({ pathname: Router.pathname, query: newQuery }, undefined, { shallow: true }); +}*/