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

Adds Remove Messages and adds setting page improvements #46

Merged
merged 8 commits into from
Nov 26, 2023
8 changes: 8 additions & 0 deletions .changeset/quick-donuts-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"syncia": minor
---

- adds ability to remove messages
- makes screenshots to show all with object fit contain
- fixed model names showing numbers
- make open ai key show and hide and hide by default
Binary file modified artifacts/chrome.zip
Binary file not shown.
58 changes: 43 additions & 15 deletions src/components/Settings/Sections/ChatSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import FieldWrapper from '../Elements/FieldWrapper'
import { useSettings } from '../../../hooks/useSettings'
import { validateApiKey } from '../../../lib/validApiKey'
import { AvailableModels, Mode } from '../../../config/settings'
import { capitalizeText } from '../../../lib/capitalizeText'
import { useState } from 'react'
import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'

const ChatSettings = () => {
const [settings, setSettings] = useSettings()
const [showPassword, setShowPassword] = useState(false)

const chatSettings = settings.chat

const apiKeyInputRef = React.useRef<HTMLInputElement>(null)
Expand Down Expand Up @@ -81,14 +86,29 @@ const ChatSettings = () => {
onSubmit={handleOpenAiKeySubmit}
>
<div className="cdx-flex cdx-gap-2 cdx-items-center">
<input
required
pattern="sk-[a-zA-Z0-9]{48}"
className="input"
ref={apiKeyInputRef}
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
defaultValue={chatSettings.openAIKey || ''}
/>
<div className="cdx-relative cdx-w-full">
<input
required
pattern="sk-[a-zA-Z0-9]{48}"
ref={apiKeyInputRef}
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
defaultValue={chatSettings.openAIKey || ''}
type={showPassword ? 'text' : 'password'}
className="input"
/>

<button
type="button"
className="cdx-absolute cdx-right-4 cdx-top-1/2 cdx-transform cdx--translate-y-1/2 cdx-text-neutral-500 dark:cdx-text-neutral-200 cdx-bg-transparent cdx-outline-none cdx-cursor-pointer"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<AiOutlineEyeInvisible size={18} />
) : (
<AiOutlineEye size={18} />
)}
</button>
</div>
<button type="submit" className="btn">
Update
</button>
Expand All @@ -109,9 +129,15 @@ const ChatSettings = () => {
className="input cdx-w-44"
onChange={handleModalChange}
>
{Object.values(AvailableModels).map((modal) => (
{Object.keys(AvailableModels).map((modal) => (
<option key={modal} value={modal}>
{modal}
{capitalizeText(
modal
.toLowerCase()
.replace('gpt', 'GPT')
.replace('3_5', '3.5')
.replaceAll('_', ' '),
)}
</option>
))}
</select>
Expand All @@ -131,11 +157,13 @@ const ChatSettings = () => {
onChange={handleModeChange}
className="input cdx-w-36"
>
{Object.entries(Mode).map(([mode, value]) => (
<option key={value} value={value}>
{mode.replace('_', ' ').toLowerCase()}
</option>
))}
{Object.keys(Mode)
.filter((v) => Number.isNaN(Number(v)))
.map((value) => (
<option key={value} value={value}>
{capitalizeText(value.replace('_', ' ').toLowerCase())}
</option>
))}
</select>
</FieldWrapper>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Settings/Sections/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import FieldWrapper from '../Elements/FieldWrapper'
import { useSettings } from '../../../hooks/useSettings'
import { ThemeOptions } from '../../../config/settings'
import * as Switch from '@radix-ui/react-switch'
import { capitalizeText } from '../../../lib/capitalizeText'

const GeneralSettings = () => {
const [settings, setSettings] = useSettings()
Expand Down Expand Up @@ -36,7 +37,7 @@ const GeneralSettings = () => {
>
{Object.values(ThemeOptions).map((theme) => (
<option key={theme} value={theme}>
{theme}
{capitalizeText(theme)}
</option>
))}
</select>
Expand Down
19 changes: 15 additions & 4 deletions src/components/Sidebar/chat/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import remarkBreaks from 'remark-breaks'
import rehypeRaw from 'rehype-raw'
import { ChatMessage, ChatRole } from '../../../hooks/useCurrentChat'
import FilePreviewBar from './FilePreviewBar'
import { RiCloseLine } from 'react-icons/ri'

interface ChatListProps {
messages: ChatMessage[]
removeMessagePair: (timestamp: number) => void
}

const ChatList = ({ messages }: ChatListProps) => {
const ChatList = ({ messages, removeMessagePair }: ChatListProps) => {
const containerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
Expand All @@ -37,10 +39,10 @@ const ChatList = ({ messages }: ChatListProps) => {
height={300}
width={300}
/>
<h1 className="cdx-text-xl cdx-text-gray-500">
<h1 className="cdx-text-xl cdx-text-gray-500 dark:cdx-text-gray-400">
Start a new conversation 🎉
</h1>
<p className="cdx-text-gray-500 cdx-mt-1 cdx-leading-tight cdx-font-light">
<p className="cdx-text-gray-500 dark:cdx-text-gray-400 cdx-mt-1 cdx-leading-tight cdx-font-light">
Type your message at the bottom <br /> and press send button
</p>
</div>
Expand All @@ -50,9 +52,18 @@ const ChatList = ({ messages }: ChatListProps) => {
.map((msg, i) => (
<div
data-user={msg.role === ChatRole.USER ? 'true' : undefined}
className="markdown cdx-px-4 cdx-py-2 data-[user]:cdx-border-l-2 cdx-border-blue-400 data-[user]:cdx-bg-black/5 data-[user]:dark:cdx-bg-neutral-900/50 cdx-max-w-[400px]"
className="markdown cdx-group cdx-relative cdx-px-4 cdx-py-2 data-[user]:cdx-border-l-2 cdx-border-blue-400 data-[user]:cdx-bg-black/5 data-[user]:dark:cdx-bg-neutral-900/50 cdx-max-w-[400px]"
key={`${msg.timestamp}-${i}`}
>
{msg.role === ChatRole.USER && (
<button
type="button"
onClick={() => removeMessagePair(msg.timestamp)}
className="cdx-absolute group-hover:cdx-visible cdx-invisible cdx-right-2 cdx-top-2 cdx-p-0.5 cdx-bg-black/20 cdx-rounded"
>
<RiCloseLine />
</button>
)}
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeRaw]}
Expand Down
47 changes: 28 additions & 19 deletions src/components/Sidebar/chat/FilePreviewBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,37 @@ interface FilePreviewBarProps {

const FilePreviewBar = ({ files, removeFile }: FilePreviewBarProps) => {
if (files.length === 0) return null

return (
<div className="cdx-flex cdx-gap-2 cdx-m-2">
{files.map((file) => (
<div key={file.id} className="cdx-flex cdx-relative">
<div className="cdx-flex-grow">
<img
src={URL.createObjectURL(file.blob)}
alt="preview"
className="cdx-w-14 cdx-h-14 cdx-object-cover cdx-rounded cdx-bg-neutral-500"
/>
</div>
{removeFile && (
<button
onClick={() => removeFile(file.id)}
type="button"
className="cdx-absolute cdx-top-0.5 cdx-right-0.5 cdx-bg-black/30 cdx-rounded-full cdx-text-neutral-500 dark:cdx-text-neutral-200 cdx-ml-2"
{files.map((file) => {
const imageUrl = URL.createObjectURL(file.blob)
return (
<div key={file.id} className="cdx-flex cdx-relative">
<a
href={imageUrl}
target="_blank"
rel="noopener noreferrer"
className="cdx-block cdx-flex-grow"
>
<RiCloseLine size={16} />
</button>
)}
</div>
))}
<img
src={imageUrl}
alt="preview"
className="cdx-w-14 cdx-h-14 cdx-object-contain cdx-rounded dark:cdx-bg-neutral-800 cdx-bg-neutral-400"
/>
</a>
{removeFile && (
<button
onClick={() => removeFile(file.id)}
type="button"
className="cdx-absolute cdx-top-0.5 cdx-right-0.5 cdx-bg-black/30 cdx-rounded-full cdx-text-neutral-500 dark:cdx-text-neutral-200 cdx-ml-2"
>
<RiCloseLine size={16} />
</button>
)}
</div>
)
})}
</div>
)
}
Expand Down
22 changes: 14 additions & 8 deletions src/components/Sidebar/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ interface ChatProps {
}

const Chat = ({ settings }: ChatProps) => {
const { messages, submitQuery, clearMessages, generating, cancelRequest } =
useChatCompletion({
model: settings.chat.modal,
apiKey: settings.chat.openAIKey!,
mode: settings.chat.mode,
systemPrompt: SYSTEM_PROMPT,
})
const {
messages,
submitQuery,
clearMessages,
generating,
cancelRequest,
removeMessagePair,
} = useChatCompletion({
model: settings.chat.modal,
apiKey: settings.chat.openAIKey!,
mode: settings.chat.mode,
systemPrompt: SYSTEM_PROMPT,
})

useEffect(() => {
const handleWindowMessage = (event: MessageEvent) => {
Expand All @@ -37,7 +43,7 @@ const Chat = ({ settings }: ChatProps) => {

return (
<>
<ChatList messages={messages} />
<ChatList messages={messages} removeMessagePair={removeMessagePair} />
<SidebarInput
loading={generating}
submitMessage={submitQuery}
Expand Down
2 changes: 1 addition & 1 deletion src/config/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const defaultSettings: Settings = {
},
chat: {
openAIKey: null,
modal: AvailableModels.GPT_3_5_TURBO,
modal: AvailableModels.GPT_4_VISION,
mode: Mode.BALANCED,
},
general: {
Expand Down
10 changes: 9 additions & 1 deletion src/hooks/useChatCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const useChatCompletion = ({
addNewMessage,
commitToStoredMessages,
clearMessages,
removeMessagePair,
} = useCurrentChat()
const [generating, setGenerating] = useState(false)

Expand Down Expand Up @@ -124,5 +125,12 @@ export const useChatCompletion = ({
setGenerating(false)
}

return { messages, submitQuery, generating, cancelRequest, clearMessages }
return {
messages,
submitQuery,
generating,
cancelRequest,
clearMessages,
removeMessagePair,
}
}
11 changes: 11 additions & 0 deletions src/hooks/useCurrentChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ export const useCurrentChat = () => {
setMessages((m) => [...m, newMessage])
}

const removeMessagePair = (timestamp: number) => {
setMessages((p) => {
const index = p.findIndex((msg) => msg.timestamp === timestamp)
if (index === -1 || p[index].role !== ChatRole.USER) return p
p.splice(index, 2) // remove the user message and the assistant message
return [...p]
})
commitToStoredMessages()
}

const commitToStoredMessages = async () => {
if (!currentChatIdRef.current) return
setStorage(getStoredChatKey(currentChatIdRef.current), messagesRef.current)
Expand All @@ -152,5 +162,6 @@ export const useCurrentChat = () => {
commitToStoredMessages,
clearMessages,
currentChatId,
removeMessagePair,
}
}
6 changes: 6 additions & 0 deletions src/lib/capitalizeText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const capitalizeText = (text: string): string => {
return text
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
6 changes: 5 additions & 1 deletion src/lib/getScreenshotImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import html2canvas from 'html2canvas'
* 2. Grab the screen image with canvas
* 3. Crop the image with the user's selection
* 4. Return the cropped image as a blob
*
* TODO: This approach is not ideal as the website visible to user may not be the same as the one
* captured by html2canvas. For example, if the user has adblock installed, the website may look
* different to the one captured by html2canvas. We should consider another approach to capture
*/
export const getScreenshotImage = async (): Promise<Blob> => {
// Create a snipping tool view for the user to select the area of the screen
Expand All @@ -21,7 +25,7 @@ export const getScreenshotImage = async (): Promise<Blob> => {

const snipeSelection: HTMLDivElement = document.createElement('div')
snipeSelection.style.position = 'fixed'
snipeSelection.style.border = '1px solid #fff'
snipeSelection.style.border = '1px solid #ffffff2a'
snipeSelection.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'
snipeSelection.style.zIndex = '2147483647' // Maximum z-index

Expand Down