diff --git a/README.md b/README.md index bf743a6b..62fe2359 100644 --- a/README.md +++ b/README.md @@ -26,29 +26,28 @@ ## 🔥 Features -- OpenAI Whisper Speech Transcription (desktop-only) -- Searchable prompt pallete to instantly insert frequently used prompts (from - user-defined prompt library) -- Directly tweak model settings, including Max Tokens and Max Context (change - default settings or change it per chat) - -## 🛠️ UI Tweaks - -- Massive color scheme and styling changes -- Reduced empty whitespace to increase text screen real-estate -- Model select dropdown menu within individual chats -- **Many** minor style and QoL changes +- Speech transcription using OpenAI's Whisper +- Searchable prompt pallete to instantly insert frequently used prompts from + user-defined prompt library +- Directly tweak model settings, including Max Tokens and Max Context (change default settings or change it per chat) +- Minimal whitespace, designed to be used maximized or in a small window +- Keyboard shortcuts intended to allow for workflow-optimization hotkeys (using AHK or Karabinerk) +- Beautiful UI with consistent styling

landing

-## 🖥️ Electron-focused development philosophy +## 🌐 Website version + +- Access KoalaClient from the web, or via the desktop app. +- https://client.koaladev.io/ is updated automatically with every commit +- Enable Google Sync to sync your chats across devices + +## 🖥️ Desktop-focused development - Automatically generated desktop builds for every commit -- Minimize to tray on close setting -- Speech transcription with OpenAI Whisper -- Electron-only tweaks +- Desktop-only tweaks - Open links in browser - Right click context menu - Spellcheck diff --git a/src/components/Chat/ChatContent/CloneChat.tsx b/src/components/Chat/ChatContent/CloneChat.tsx index eed9c6bf..9ccad060 100644 --- a/src/components/Chat/ChatContent/CloneChat.tsx +++ b/src/components/Chat/ChatContent/CloneChat.tsx @@ -9,10 +9,14 @@ import CloneIcon from '@icon/CloneIcon'; const CloneChat = React.memo(() => { const setChats = useStore((state) => state.setChats); const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex); + const generating = useStore((state) => state.generating); const [cloned, setCloned] = useState(false); const cloneChat = () => { + if (generating) { + return; + } const chats = useStore.getState().chats; if (chats) { @@ -44,7 +48,11 @@ const CloneChat = React.memo(() => { return ( diff --git a/src/components/Chat/ChatContent/Message/View/ContentView.tsx b/src/components/Chat/ChatContent/Message/View/ContentView.tsx index 3ce50b39..de1e3bed 100644 --- a/src/components/Chat/ChatContent/Message/View/ContentView.tsx +++ b/src/components/Chat/ChatContent/Message/View/ContentView.tsx @@ -59,6 +59,7 @@ const ContentView = memo( ); const inlineLatex = useStore((state) => state.inlineLatex); const markdownMode = useStore((state) => state.markdownMode); + const generating = useStore((state) => state.generating); const handleDelete = () => { const updatedChats: ChatInterface[] = JSON.parse( @@ -141,28 +142,11 @@ const ContentView = memo( )}
- {isDelete || ( - <> - {!useStore.getState().generating && - role === 'assistant' && - messageIndex === lastMessageIndex && ( - - )} - {messageIndex !== 0 && } - {messageIndex !== lastMessageIndex && ( - - )} - - - - - - - )} - {isDelete && ( + {generating ? ( +
+ +
+ ) : isDelete ? ( <> + ) : ( + <> + {role === 'assistant' && messageIndex === lastMessageIndex && ( + + )} + {messageIndex !== 0 && } + {messageIndex !== lastMessageIndex && ( + + )} + + + + + )}
diff --git a/src/components/Chat/ChatContent/Message/WhisperRecord.tsx b/src/components/Chat/ChatContent/Message/WhisperRecord.tsx index e60a79fa..e477126e 100644 --- a/src/components/Chat/ChatContent/Message/WhisperRecord.tsx +++ b/src/components/Chat/ChatContent/Message/WhisperRecord.tsx @@ -1,8 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useWhisper } from '@chengsokdara/use-whisper'; import useStore from '@store/store'; import StopIcon from '@icon/StopIcon'; import MicrophoneIcon from '@icon/MicrophoneIcon'; +import { useTranslation } from 'react-i18next'; const WhisperRecord = ({ cursorPosition, @@ -13,51 +14,76 @@ const WhisperRecord = ({ _setContent: React.Dispatch>; messageIndex: number; }) => { + const { t } = useTranslation('api'); let apiKey = useStore((state) => state.apiKey); const setGenerating = useStore((state) => state.setGenerating); + const generating = useStore((state) => state.generating); + const setError = useStore((state) => state.setError); + const setIsRecording = useStore((state) => state.setIsRecording); + const isRecording = useStore((state) => state.isRecording); apiKey = apiKey || '0'; - const { transcript, startRecording, stopRecording } = useWhisper({ apiKey }); + const { transcript, startRecording, stopRecording } = useWhisper({ + apiKey, + }); useEffect(() => { - if (transcript.text != null) { - _setContent((prev) => { - return prev.replace('◯', transcript.text || ''); - }); - setGenerating(false); + if (generating) { + if (transcript.text != null) { + _setContent((prev) => { + return prev.replace('◯', transcript.text || ''); + }); + setGenerating(false); + } } }, [transcript.text]); - const [isRecording, setIsRecording] = useState(false); - - const handleRecording = () => { - if (isRecording) { + useEffect(() => { + if (!generating) { _setContent((prev) => { - return prev.replace('◉', '◯' || ''); + return prev.replace('◯', ''); }); - stopRecording(); - } else { - _setContent((prev) => { - const startContent = prev.slice(0, cursorPosition); - const endContent = prev.slice(cursorPosition); + } + }, [generating]); + + const handleRecording = () => { + if (apiKey != '0') { + if (isRecording) { + _setContent((prev) => { + return prev.replace('◉', '◯' || ''); + }); + stopRecording(); + } else { + _setContent((prev) => { + const startContent = prev.slice(0, cursorPosition); + const endContent = prev.slice(cursorPosition); - const paddedStart = - !startContent.endsWith(' ') && - !startContent.endsWith('\n') && - startContent.length > 0 - ? ' ' - : ''; - const paddedEnd = - !endContent.startsWith(' ') && !endContent.startsWith('\n') - ? ' ' - : ''; + const paddedStart = + startContent && + !startContent.endsWith(' ') && + !startContent.endsWith('\n') && + startContent.length > 0 + ? ' ' + : ''; + const paddedEnd = + endContent && + !endContent.startsWith(' ') && + !endContent.startsWith('\n') + ? ' ' + : ''; - return startContent + paddedStart + '◉' + paddedEnd + endContent; + return startContent + paddedStart + '◉' + paddedEnd + endContent; + }); + startRecording(); + setGenerating(true); + } + setIsRecording(!isRecording); + } else { + setError(t('noApiKeyWarning') as string); + _setContent((prev) => { + return prev.replace('◯', transcript.text || ''); }); - startRecording(); - setGenerating(true); } - setIsRecording(!isRecording); }; return ( @@ -69,9 +95,19 @@ const WhisperRecord = ({ ? 'btn-primary' : 'btn-neutral-dark' : 'btn-primary' - } btn-small inline-flex p-0 h-8 w-8 items-center justify-center mr-3`} + } btn-small inline-flex p-0 h-8 w-8 items-center justify-center mr-3 ${ + generating && !isRecording + ? 'cursor-not-allowed opacity-40' + : 'cursor-pointer opacity-100' + } + +`} aria-label='whisper' - onClick={handleRecording} + onClick={() => { + if (!generating || isRecording) { + handleRecording(); + } + }} > {isRecording ? : } diff --git a/src/components/MobileBar/MobileBar.tsx b/src/components/MobileBar/MobileBar.tsx index 3a176af2..29480425 100644 --- a/src/components/MobileBar/MobileBar.tsx +++ b/src/components/MobileBar/MobileBar.tsx @@ -58,16 +58,32 @@ const MobileBar = () => { - ) : ( - <> - ); + ) : null; }; export default StopGeneratingButton; diff --git a/src/store/chat-slice.ts b/src/store/chat-slice.ts index 8fab620b..2dfae2b9 100644 --- a/src/store/chat-slice.ts +++ b/src/store/chat-slice.ts @@ -10,6 +10,7 @@ export interface ChatSlice { error: string; folders: FolderCollection; bottomMessageRef: RefObject | null; + isRecording: boolean; setBottomMessageRef: ( bottomMessageRef: RefObject | null ) => void; @@ -20,6 +21,7 @@ export interface ChatSlice { setError: (error: string) => void; setFolders: (folders: FolderCollection) => void; setConfirmEditSubmission: (confirmEditSubmission: boolean) => void; + setIsRecording: (isRecording: boolean) => void; } export const createChatSlice: StoreSlice = (set) => ({ @@ -29,6 +31,13 @@ export const createChatSlice: StoreSlice = (set) => ({ error: '', folders: {}, bottomMessageRef: null, + isRecording: false, + setIsRecording: (isRecording: boolean) => { + set((prev: ChatSlice) => ({ + ...prev, + isRecording: isRecording, + })); + }, setBottomMessageRef: ( bottomMessageRef: RefObject | null ) => { diff --git a/src/store/config-slice.ts b/src/store/config-slice.ts index ab297da7..38668dbc 100644 --- a/src/store/config-slice.ts +++ b/src/store/config-slice.ts @@ -41,7 +41,7 @@ export const createConfigSlice: StoreSlice = (set) => ({ theme: 'dark', hideMenuOptions: false, hideSideMenu: false, - autoTitle: false, + autoTitle: true, closeToTray: false, enterToSubmit: true, confirmEditSubmission: true, diff --git a/src/store/migrate.ts b/src/store/migrate.ts index fa3f8a6f..1278a436 100644 --- a/src/store/migrate.ts +++ b/src/store/migrate.ts @@ -45,7 +45,7 @@ export const migrateV2 = (persistedState: LocalStorageInterfaceV2ToV3) => { frequency_penalty: _defaultChatConfig.frequency_penalty, }; }); - persistedState.autoTitle = false; + persistedState.autoTitle = true; }; export const migrateV3 = (persistedState: LocalStorageInterfaceV3ToV4) => {