Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.0.8a #99

Merged
merged 9 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<p align="center">
<img src="https://cdn.discordapp.com/attachments/446426925209092098/1192293382920351744/Screenshot_2024-01-03_at_9.27.06_PM.png?ex=65a88cbe&is=659617be&hm=5d60622b900e4c834ef11a62423045edca40075655cc5597ee1bbda7b2eb2bb4&" alt="landing" width=800 />
</p>

## 🖥️ 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
Expand Down
10 changes: 9 additions & 1 deletion src/components/Chat/ChatContent/CloneChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>(false);

const cloneChat = () => {
if (generating) {
return;
}
const chats = useStore.getState().chats;

if (chats) {
Expand Down Expand Up @@ -44,7 +48,11 @@ const CloneChat = React.memo(() => {
return (
<button
type='button'
className={`text-custom-white transition-opacity cursor-pointer opacity-100`}
className={`text-custom-white transition-opacity ${
generating
? 'cursor-not-allowed opacity-40'
: 'cursor-pointer opacity-100'
}`}
onClick={cloneChat}
>
<div className='-ml-0.5 -mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-neutral-light'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const CommandPrompt = ({
}) => {
const { t } = useTranslation();
const prompts = useStore((state) => state.prompts);
const generating = useStore((state) => state.generating);
const [_prompts, _setPrompts] = useState<Prompt[]>(prompts);
const [input, setInput] = useState<string>('');
const inputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -54,9 +55,17 @@ const CommandPrompt = ({
<button
className={`btn ${
messageIndex % 2 ? 'btn-neutral' : 'btn-neutral-dark'
} btn-small inline-flex h-8 w-8 items-center justify-center`}
} btn-small inline-flex h-8 w-8 items-center justify-center ${
generating
? 'cursor-not-allowed opacity-40'
: 'cursor-pointer opacity-100'
}`}
aria-label='prompt library'
onClick={() => setDropDown(!dropDown)}
onClick={() => {
if (!generating) {
setDropDown(!dropDown);
}
}}
>
/
</button>
Expand Down
45 changes: 23 additions & 22 deletions src/components/Chat/ChatContent/Message/View/ContentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -141,28 +142,11 @@ const ContentView = memo(
)}
</div>
<div className='flex justify-end gap-2 w-full mt-2'>
{isDelete || (
<>
{!useStore.getState().generating &&
role === 'assistant' &&
messageIndex === lastMessageIndex && (
<RefreshButton onClick={handleRefresh} />
)}
{messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}
{messageIndex !== lastMessageIndex && (
<DownButton onClick={handleMoveDown} />
)}

<MarkdownModeButton />
<CopyButton onClick={handleCopy} />
<EditButton
setEditingMessageIndex={setEditingMessageIndex}
messageIndex={messageIndex}
/>
<DeleteButton setIsDelete={setIsDelete} />
</>
)}
{isDelete && (
{generating ? (
<div className='p-1 invisible'>
<TickIcon />
</div>
) : isDelete ? (
<>
<button
className='p-1 text-custom-white hover:text-neutral-dark hover:bg-custom-white/70 hover:rounded'
Expand All @@ -179,6 +163,23 @@ const ContentView = memo(
<TickIcon />
</button>
</>
) : (
<>
{role === 'assistant' && messageIndex === lastMessageIndex && (
<RefreshButton onClick={handleRefresh} />
)}
{messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}
{messageIndex !== lastMessageIndex && (
<DownButton onClick={handleMoveDown} />
)}
<MarkdownModeButton />
<CopyButton onClick={handleCopy} />
<EditButton
setEditingMessageIndex={setEditingMessageIndex}
messageIndex={messageIndex}
/>
<DeleteButton setIsDelete={setIsDelete} />
</>
)}
</div>
</>
Expand Down
102 changes: 69 additions & 33 deletions src/components/Chat/ChatContent/Message/WhisperRecord.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,51 +14,76 @@ const WhisperRecord = ({
_setContent: React.Dispatch<React.SetStateAction<string>>;
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 (
Expand All @@ -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 ? <StopIcon /> : <MicrophoneIcon />}
</button>
Expand Down
24 changes: 20 additions & 4 deletions src/components/MobileBar/MobileBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,32 @@ const MobileBar = () => {
</div>
<button
type='button'
className='-mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-neutral-light'
onClick={goBack}
className={`-mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-neutral-light ${
generating
? 'cursor-not-allowed opacity-40'
: 'cursor-pointer opacity-100'
}`}
onClick={() => {
if (!generating) {
goBack();
}
}}
>
<span className='sr-only'>Open sidebar</span>
<BackIcon height='1em' />
</button>
<button
type='button'
className='-mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-neutral-light mr-4'
onClick={goForward}
className={`-mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-neutral-light ${
generating
? 'cursor-not-allowed opacity-40'
: 'cursor-pointer opacity-100'
}`}
onClick={() => {
if (!generating) {
goForward();
}
}}
>
<span className='sr-only'>Open sidebar</span>
<ForwardIcon height='1em' />
Expand Down
15 changes: 9 additions & 6 deletions src/components/StopGeneratingButton/StopGeneratingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import useStore from '@store/store';
const StopGeneratingButton = () => {
const setGenerating = useStore((state) => state.setGenerating);
const generating = useStore((state) => state.generating);
const isRecording = useStore((state) => state.isRecording);

return generating ? (
<div className='absolute bottom-6 left-0 right-0 m-auto flex md:w-full md:m-auto gap-0 md:gap-2 justify-center'>
return generating && !isRecording ? (
<div
className='absolute bottom-6 left-0 right-0 m-auto flex md:w-full md:m-auto gap-0 md:gap-2 justify-center'
style={{ pointerEvents: 'none' }}
>
<button
className='btn relative btn-neutral border-0 md:border hover:bg-neutral-dark'
className='btn relative btn-neutral border-0 md:border hover:bg-neutral-dark px-6 py-3'
aria-label='stop generating'
onClick={() => setGenerating(false)}
style={{ pointerEvents: 'auto' }}
>
<div className='flex w-full items-center justify-center gap-2'>
<svg
Expand All @@ -31,9 +36,7 @@ const StopGeneratingButton = () => {
</div>
</button>
</div>
) : (
<></>
);
) : null;
};

export default StopGeneratingButton;
Loading
Loading