Skip to content

Commit

Permalink
Merge pull request #41 from Royal-lobster/webpage-context
Browse files Browse the repository at this point in the history
Ability to add webpage content as context
  • Loading branch information
Royal-lobster authored Nov 25, 2023
2 parents 12c4943 + 3f1420c commit 5f4cf4a
Show file tree
Hide file tree
Showing 20 changed files with 324 additions and 88 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-bears-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"syncia": minor
---

Adds webpage context
Binary file removed artifacts/chrome.zip
Binary file not shown.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dnd-kit-sortable-tree": "^0.1.58",
"endent": "^2.1.0",
"jotai": "^2.4.3",
"langchain": "^0.0.151",
"langchain": "^0.0.197-rc.1",
"object-hash": "^3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -54,13 +54,13 @@
"@crxjs/vite-plugin": "^2.0.0-beta.15",
"@types/chrome": "^0.0.213",
"@types/node": "^18.13.0",
"dotenv-cli": "^7.3.0",
"@types/object-hash": "^3.0.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.13",
"dotenv-cli": "^7.3.0",
"postcss": "^8.4.21",
"publish-browser-extension": "^1.4.1",
"rome": "12.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Settings/Sections/ChatSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import SectionHeading from '../Elements/SectionHeading'
import FieldWrapper from '../Elements/FieldWrapper'
import { useSettings } from '../../../hooks/useSettings'
import { validateApiKey } from '../../../utils/validApiKey'
import { validateApiKey } from '../../../lib/validApiKey'
import { AvailableModels, Mode } from '../../../config/settings'

const ChatSettings = () => {
Expand Down
22 changes: 22 additions & 0 deletions src/components/Settings/Sections/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SectionHeading from '../Elements/SectionHeading'
import FieldWrapper from '../Elements/FieldWrapper'
import { useSettings } from '../../../hooks/useSettings'
import { ThemeOptions } from '../../../config/settings'
import * as Switch from '@radix-ui/react-switch'

const GeneralSettings = () => {
const [settings, setSettings] = useSettings()
Expand Down Expand Up @@ -40,6 +41,27 @@ const GeneralSettings = () => {
))}
</select>
</FieldWrapper>
<FieldWrapper
title="Webpage Context"
description="Enable Syncia to answer questions based on the current webpage content"
row
>
<Switch.Root
checked={generalSettings.webpageContext}
onCheckedChange={(value) =>
setSettings({
...settings,
general: {
...generalSettings,
webpageContext: value,
},
})
}
className="cdx-w-[42px] cdx-h-[25px] cdx-bg-neutral-500 cdx-rounded-full cdx-relative data-[state=checked]:cdx-bg-blue-500 cdx-outline-none cdx-cursor-default"
>
<Switch.Thumb className="cdx-block cdx-w-[21px] cdx-h-[21px] cdx-bg-white cdx-rounded-full cdx-transition-transform cdx-duration-100 cdx-translate-x-0.5 cdx-will-change-transform data-[state=checked]:cdx-translate-x-[19px]" />
</Switch.Root>
</FieldWrapper>
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sidebar/auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react'
import { useSettings } from '../../../hooks/useSettings'
import { validateApiKey } from '../../../utils/validApiKey'
import { validateApiKey } from '../../../lib/validApiKey'

const Auth = () => {
const [, setSettings] = useSettings()
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sidebar/chat/ChatHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useChatHistory } from '../../../hooks/useChatHistory'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { RiAddLine, RiCloseCircleFill, RiTimeLine } from 'react-icons/ri'
import { generateReadableRelativeDate } from '../../../utils/generateReadableDate'
import { generateReadableRelativeDate } from '../../../lib/generateReadableDate'

const ChatHistory = () => {
const {
Expand Down
76 changes: 50 additions & 26 deletions src/components/Sidebar/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@ import { IoSend } from 'react-icons/io5'
import { HiHand } from 'react-icons/hi'
import ChatHistory from './ChatHistory'
import { useChatHistory } from '../../../hooks/useChatHistory'
import WebPageContentToggle from './WebPageContentToggle'

interface SidebarInputProps {
loading: boolean
submitMessage: (prompt: string) => void
submitMessage: (message: string, context?: string) => void
clearMessages: () => void
chatIsEmpty: boolean
cancelRequest: () => void
isWebpageContextOn: boolean
}

const MAX_MESSAGE_LENGTH = 10000

export function SidebarInput({
loading,
submitMessage,
clearMessages,
chatIsEmpty,
cancelRequest,
isWebpageContextOn,
}: SidebarInputProps) {
const [text, setText] = useState('')
const [delayedLoading, setDelayedLoading] = useState(false)
Expand All @@ -34,11 +39,45 @@ export function SidebarInput({
}
}, [loading])

const handleSubmit = () => {
submitMessage(text)
const handleSubmit = async () => {
let context
if (isWebpageContextOn) {
const pageContent = new Promise((resolve) => {
window.addEventListener('message', function (event) {
if (event.data.action === 'get-page-content') {
resolve(event.data.pageContent)
}
})

window.parent.postMessage({ action: 'get-page-content' }, '*')
})
context = (await pageContent) as string
}
submitMessage(text, isWebpageContextOn ? context : undefined)
setText('')
}

const sendButton = (
<button
type="button"
disabled={loading}
onClick={handleSubmit}
className="cdx-flex cdx-gap-2 disabled:cdx-bg-slate-500 disabled:cdx-text-slate-400 cdx-items-center cdx-bg-blue-500 hover:cdx-bg-blue-700 cdx-text-white cdx-py-2 cdx-px-4 cdx-rounded"
>
<span>Send</span> <IoSend size={10} />
</button>
)

const stopButton = (
<button
type="button"
onClick={cancelRequest}
className="cdx-flex cdx-gap-2 disabled:cdx-bg-slate-500 disabled:cdx-text-slate-400 cdx-items-center cdx-bg-red-500 hover:cdx-bg-red-700 cdx-text-white cdx-py-2 cdx-px-4 cdx-rounded"
>
<HiHand size={18} /> <span>Stop</span>
</button>
)

return (
<div className="cdx-fixed cdx-bottom-0 cdx-left-0 cdx-right-0 cdx-flex cdx-flex-col ">
<div className="cdx-flex cdx-mx-3 cdx-items-center cdx-justify-between">
Expand All @@ -59,7 +98,7 @@ export function SidebarInput({
<div className="cdx-m-2 cdx-rounded-md cdx-border dark:cdx-border-neutral-800 cdx-border-neutral-300 dark:cdx-bg-neutral-900/90 cdx-bg-neutral-200/90 focus:cdx-outline-none focus:cdx-ring-2 focus:cdx-ring-blue-900 focus:cdx-ring-opacity-50">
<TextareaAutosize
minRows={2}
maxLength={10000}
maxLength={MAX_MESSAGE_LENGTH}
placeholder="Type your message here..."
value={text}
disabled={loading}
Expand All @@ -76,29 +115,14 @@ export function SidebarInput({
}}
/>
<div className="cdx-flex cdx-justify-between cdx-items-center cdx-p-3">
<div>
<span className="cdx-text-xs cdx-text-neutral-500 dark:cdx-text-neutral-200">
{text.length} / 10,000
</span>
<span className="cdx-text-xs cdx-text-neutral-500 dark:cdx-text-neutral-200">
{text.length.toLocaleString()} /{' '}
{MAX_MESSAGE_LENGTH.toLocaleString()}
</span>
<div className="cdx-flex cdx-items-center cdx-justify-center cdx-gap-4">
<WebPageContentToggle />
{!delayedLoading ? sendButton : stopButton}
</div>
{!delayedLoading ? (
<button
type="button"
disabled={loading}
onClick={handleSubmit}
className="cdx-flex cdx-gap-2 disabled:cdx-bg-slate-500 disabled:cdx-text-slate-400 cdx-items-center cdx-bg-blue-500 hover:cdx-bg-blue-700 cdx-text-white cdx-py-2 cdx-px-4 cdx-rounded"
>
<span>Send</span> <IoSend size={10} />
</button>
) : (
<button
type="button"
onClick={cancelRequest}
className="cdx-flex cdx-gap-2 disabled:cdx-bg-slate-500 disabled:cdx-text-slate-400 cdx-items-center cdx-bg-red-500 hover:cdx-bg-red-700 cdx-text-white cdx-py-2 cdx-px-4 cdx-rounded"
>
<HiHand size={18} /> <span>Stop</span>
</button>
)}
</div>
</div>
</div>
Expand Down
31 changes: 31 additions & 0 deletions src/components/Sidebar/chat/WebPageContentToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'
import { useSettings } from '../../../hooks/useSettings'
import * as Switch from '@radix-ui/react-switch'

const WebPageContentToggle = () => {
const [settings, setSettings] = useSettings()
return (
<div className="cdx-flex cdx-items-center cdx-justify-center cdx-gap-2 cdx-p-2 cdx-rounded">
<Switch.Root
checked={settings.general.webpageContext}
onCheckedChange={(value) =>
setSettings({
...settings,
general: {
...settings.general,
webpageContext: value,
},
})
}
className="cdx-w-[28px] cdx-h-[16px] cdx-bg-neutral-500 cdx-rounded-full cdx-relative data-[state=checked]:cdx-bg-blue-500 cdx-outline-none cdx-cursor-default"
>
<Switch.Thumb className="cdx-block cdx-w-[14px] cdx-h-[14px] cdx-bg-white cdx-rounded-full cdx-transition-transform cdx-duration-100 cdx-translate-x-0.5 cdx-will-change-transform data-[state=checked]:cdx-translate-x-[14px]" />
</Switch.Root>
<label htmlFor="webpage-context" className="cdx-text-neutral-400">
Add page content
</label>
</div>
)
}

export default WebPageContentToggle
5 changes: 3 additions & 2 deletions src/components/Sidebar/chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react'
import React, { useEffect } from 'react'
import ChatList from './ChatList'
import { SidebarInput } from './ChatInput'
import { useChatCompletion } from '../../../hooks/useChatCompletion'
Expand Down Expand Up @@ -33,7 +33,7 @@ const Chat = ({ settings }: ChatProps) => {
return () => {
window.removeEventListener('message', handleWindowMessage)
}
}, [])
}, [submitQuery])

return (
<>
Expand All @@ -44,6 +44,7 @@ const Chat = ({ settings }: ChatProps) => {
chatIsEmpty={messages.length <= 1}
clearMessages={clearMessages}
cancelRequest={cancelRequest}
isWebpageContextOn={settings.general.webpageContext}
/>
</>
)
Expand Down
2 changes: 2 additions & 0 deletions src/config/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type Settings = {
}
general: {
theme: ThemeOptions
webpageContext: boolean
}
}

Expand All @@ -51,5 +52,6 @@ export const defaultSettings: Settings = {
},
general: {
theme: ThemeOptions.SYSTEM,
webpageContext: true,
},
}
54 changes: 38 additions & 16 deletions src/hooks/useChatCompletion.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AvailableModels, Mode } from '../config/settings'
import endent from 'endent'
import { ChatOpenAI } from 'langchain/chat_models/openai'
import { useCurrentChat, ChatRole } from './useCurrentChat'
import { useMemo } from 'react'
import { AIMessage, HumanMessage, SystemMessage } from 'langchain/schema'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { AvailableModels, Mode } from '../config/settings'
import { ChatRole, useCurrentChat } from './useCurrentChat'
import { getMatchedContent } from '../lib/getMatchedContent'

interface UseChatCompletionProps {
model: AvailableModels
Expand Down Expand Up @@ -37,15 +38,15 @@ export const useChatCompletion = ({
} = useCurrentChat()
const [generating, setGenerating] = useState(false)

const chat = useMemo(
const llm = useMemo(
() =>
new ChatOpenAI({
streaming: true,
openAIApiKey: apiKey,
modelName: model,
temperature: Number(mode),
}),
[],
[apiKey, model, mode],
)

const previousMessages = messages.map((msg) => {
Expand All @@ -61,22 +62,43 @@ export const useChatCompletion = ({

const controller = new AbortController()

const submitQuery = async (query: string) => {
const submitQuery = async (query: string, context?: string) => {
await addNewMessage(ChatRole.USER, query)
const messages = [
new SystemMessage(systemPrompt),
...previousMessages,
new HumanMessage(query),
]
const options = {
signal: controller.signal,
callbacks: [{ handleLLMNewToken: updateAssistantMessage }],
}

setGenerating(true)
chat.call(messages, options).then(() => {
commitToStoredMessages()
setGenerating(false)
})

/**
* If context is provided, we need to use the LLM to get the relevant documents
* and then run the LLM on those documents. We use in memory vector store to
* get the relevant documents
*/
let matchedContext
if (context) {
matchedContext = await getMatchedContent(query, context, apiKey)
}

const expandedQuery = matchedContext
? endent`
### Context
${matchedContext}
### Question:
${query}
`
: query

const messages = [
new SystemMessage(systemPrompt),
...previousMessages,
new HumanMessage(expandedQuery),
]

await llm.call(messages, options)
commitToStoredMessages()
setGenerating(false)
}

const cancelRequest = () => {
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function useStorage<T>(
): [T, SetValue<T>] {
const [storedValue, setStoredValue] = useAtom(atom)

// biome-ignore lint/correctness/useExhaustiveDependencies: This works fine. i don't want to change it.
useEffect(() => {
readStorage<T>(key, area).then((res) => {
if (res) setStoredValue(res)
Expand Down
10 changes: 10 additions & 0 deletions src/lib/createSHA256Hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const createSHA256Hash = async (content: string): Promise<string> => {
const encoder = new TextEncoder()
const data = encoder.encode(content)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
return hashHex
}
File renamed without changes.
Loading

0 comments on commit 5f4cf4a

Please sign in to comment.