Update dependencies and fix styling issues

This commit is contained in:
n4ze3m 2024-02-07 00:11:07 +05:30
parent a66d8a8418
commit 58966355c3
21 changed files with 1175 additions and 51 deletions

View File

@ -24,6 +24,7 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"langchain": "^0.1.9", "langchain": "^0.1.9",
"lucide-react": "^0.323.0",
"plasmo": "0.84.1", "plasmo": "0.84.1",
"property-information": "^6.4.1", "property-information": "^6.4.1",
"react": "18.2.0", "react": "18.2.0",
@ -78,7 +79,8 @@
"scripting", "scripting",
"declarativeNetRequest", "declarativeNetRequest",
"declarativeNetRequestFeedback", "declarativeNetRequestFeedback",
"action" "action",
"unlimitedStorage"
] ]
} }
} }

View File

@ -14,8 +14,3 @@ chrome.runtime.onMessage.addListener(async (message) => {
chrome.action.onClicked.addListener((tab) => { chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({url: chrome.runtime.getURL("options.html")}); chrome.tabs.create({url: chrome.runtime.getURL("options.html")});
}); });
// listen to commadns
chrome.commands.onCommand.addListener((command) => {
console.log('Command', command)
})

View File

@ -55,34 +55,14 @@ export const PlaygroundMessage = (props: Props) => {
<Markdown message={props.message} /> <Markdown message={props.message} />
</div> </div>
{/* source if aviable */} {/* source if aviable */}
</div>
{props.isBot && (
<div className="flex space-x-2">
{!props.hideCopy && (
<button
onClick={() => {
navigator.clipboard.writeText(props.message)
setIsBtnPressed(true)
}}
className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
{!isBtnPressed ? (
<ClipboardIcon className="w-4 h-4 text-gray-400 group-hover:text-gray-500" />
) : (
<CheckIcon className="w-4 h-4 text-green-400 group-hover:text-green-500" />
)}
</button>
)}
</div>
)}
</div>
</div>
{props.images && ( {props.images && (
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-3 m-auto w-full"> <div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full">
{props.images {props.images
.filter((image) => image.length > 0) .filter((image) => image.length > 0)
.map((image, index) => ( .map((image, index) => (
<div key={index} className="h-full rounded-md shadow relative"> <div
key={index}
className="h-full rounded-md shadow relative">
<img <img
src={image} src={image}
alt="Uploaded" alt="Uploaded"
@ -92,6 +72,28 @@ export const PlaygroundMessage = (props: Props) => {
))} ))}
</div> </div>
)} )}
{props.isBot && (
<div className="flex space-x-2">
{!props.hideCopy && (
<button
onClick={() => {
navigator.clipboard.writeText(props.message)
setIsBtnPressed(true)
}}
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
{!isBtnPressed ? (
<ClipboardIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
) : (
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
)}
</button>
)}
</div>
)}
</div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -0,0 +1,113 @@
import React, { Fragment, useState } from "react"
import { Dialog, Menu, Transition } from "@headlessui/react"
import {
Bars3BottomLeftIcon,
XMarkIcon,
TagIcon,
CircleStackIcon,
CogIcon,
ChatBubbleLeftIcon,
Bars3Icon,
Bars4Icon,
ArrowPathIcon
} from "@heroicons/react/24/outline"
import logoImage from "data-base64:~assets/icon.png"
import { Link, useParams, useLocation, useNavigate } from "react-router-dom"
import { Sidebar } from "./Sidebar"
import { Drawer, Layout, Select } from "antd"
import { useQuery } from "@tanstack/react-query"
import { fetchModels } from "~services/ollama"
import { useMessageOption } from "~hooks/useMessageOption"
import { PanelLeftIcon, Settings2 } from "lucide-react"
const navigation = [
{ name: "Embed", href: "/bot/:id", icon: TagIcon },
{
name: "Preview",
href: "/bot/:id/preview",
icon: ChatBubbleLeftIcon
},
{
name: "Data Sources",
href: "/bot/:id/data-sources",
icon: CircleStackIcon
},
{
name: "Settings",
href: "/bot/:id/settings",
icon: CogIcon
}
]
//@ts-ignore -
function classNames(...classes) {
return classes.filter(Boolean).join(" ")
}
export default function OptionLayout({
children
}: {
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const params = useParams<{ id: string }>()
const location = useLocation()
const {
data: models,
isLoading: isModelsLoading,
refetch: refetchModels,
isFetching: isModelsFetching
} = useQuery({
queryKey: ["fetchModel"],
queryFn: fetchModels
})
const { selectedModel, setSelectedModel } = useMessageOption()
return (
<Layout className="bg-white dark:bg-[#171717] md:flex">
<div className="flex items-center p-3 fixed flex-row justify-between border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-[#171717] w-full z-10">
<div className="flex items-center flex-row gap-3">
<div>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => setSidebarOpen(true)}>
<PanelLeftIcon className="w-6 h-6" />
</button>
</div>
<div>
<Select
value={selectedModel}
onChange={setSelectedModel}
size="large"
loading={isModelsLoading || isModelsFetching}
placeholder="Select a model"
className="w-64"
options={models?.map((model) => ({
label: model.name,
value: model.model
}))}
/>
</div>
</div>
<button className="text-gray-500 dark:text-gray-400">
<CogIcon className="w-6 h-6" />
</button>
</div>
<Layout.Content>{children}</Layout.Content>
<Drawer
title={"Chat History"}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}
>
<Sidebar />
</Drawer>
</Layout>
)
}

View File

@ -0,0 +1,89 @@
import React from "react"
import { PlaygroundForm } from "./PlaygroundForm"
import { PlaygroundChat } from "./PlaygroundChat"
export const Playground = () => {
const drop = React.useRef<HTMLDivElement>(null)
const [dropedFile, setDropedFile] = React.useState<File | undefined>()
const [dropState, setDropState] = React.useState<
"idle" | "dragging" | "error"
>("idle")
React.useEffect(() => {
if (!drop.current) {
return
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDropState("idle")
const files = Array.from(e.dataTransfer?.files || [])
const isImage = files.every((file) => file.type.startsWith("image/"))
if (!isImage) {
setDropState("error")
return
}
const newFiles = Array.from(e.dataTransfer?.files || []).slice(0, 1)
if (newFiles.length > 0) {
setDropedFile(newFiles[0])
}
}
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDropState("dragging")
}
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDropState("idle")
}
drop.current.addEventListener("dragover", handleDragOver)
drop.current.addEventListener("drop", handleDrop)
drop.current.addEventListener("dragenter", handleDragEnter)
drop.current.addEventListener("dragleave", handleDragLeave)
return () => {
if (drop.current) {
drop.current.removeEventListener("dragover", handleDragOver)
drop.current.removeEventListener("drop", handleDrop)
drop.current.removeEventListener("dragenter", handleDragEnter)
drop.current.removeEventListener("dragleave", handleDragLeave)
}
}
}, [])
return (
<div
ref={drop}
className={`${
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800 z-10" : ""
} min-h-screen`}>
<PlaygroundChat />
<div className="flex flex-col items-center">
<div className="flex-grow">
<div className="w-full flex justify-center">
<div className="bottom-0 w-full bg-transparent border-0 fixed pt-2">
<div className="stretch mx-2 flex flex-row gap-3 md:mx-4 lg:mx-auto lg:max-w-2xl xl:max-w-3xl justify-center items-center">
<div className="relative h-full flex-1 items-center justify-center md:flex-col">
<PlaygroundForm dropedFile={dropedFile} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,33 @@
import React from "react"
import { useMessage } from "~hooks/useMessage"
import { useMessageOption } from "~hooks/useMessageOption"
import { PlaygroundMessage } from "./PlaygroundMessage"
export const PlaygroundChat = () => {
const { messages } = useMessageOption()
const divRef = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
if (divRef.current) {
divRef.current.scrollIntoView({ behavior: "smooth" })
}
})
return (
<div className="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out">
{/* {messages.length === 0 && <div>no message</div>} */}
{messages.length > 0 && <div className="w-full h-14 flex-shrink-0"></div>}
{messages.map((message, index) => (
<PlaygroundMessage
key={index}
isBot={message.isBot}
message={message.message}
name={message.name}
images={message.images || []}
/>
))}
{messages.length > 0 && (
<div className="w-full h-32 md:h-48 flex-shrink-0"></div>
)}
<div ref={divRef} />
</div>
)
}

View File

@ -0,0 +1,181 @@
import { useForm } from "@mantine/form"
import { useMutation } from "@tanstack/react-query"
import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize"
import PhotoIcon from "@heroicons/react/24/outline/PhotoIcon"
import XMarkIcon from "@heroicons/react/24/outline/XMarkIcon"
import { toBase64 } from "~libs/to-base64"
import { useMessageOption } from "~hooks/useMessageOption"
import { ArrowPathIcon } from "@heroicons/react/24/outline"
import { Tooltip } from "antd"
type Props = {
dropedFile: File | undefined
}
export const PlaygroundForm = ({ dropedFile }: Props) => {
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const inputRef = React.useRef<HTMLInputElement>(null)
const resetHeight = () => {
const textarea = textareaRef.current
if (textarea) {
textarea.style.height = "auto"
}
}
const form = useForm({
initialValues: {
message: "",
image: ""
}
})
const onInputChange = async (
e: React.ChangeEvent<HTMLInputElement> | File
) => {
if (e instanceof File) {
const base64 = await toBase64(e)
form.setFieldValue("image", base64)
} else {
if (e.target.files) {
const base64 = await toBase64(e.target.files[0])
form.setFieldValue("image", base64)
}
}
}
React.useEffect(() => {
if (dropedFile) {
onInputChange(dropedFile)
}
}, [dropedFile])
useDynamicTextareaSize(textareaRef, form.values.message, 120)
const { onSubmit, selectedModel, chatMode } = useMessageOption()
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
mutationFn: onSubmit
})
return (
<div className="p-3 md:p-6 md:bg-white dark:bg-black border rounded-t-xl border-black/10 dark:border-gray-800">
<div className="flex-grow space-y-6 ">
<div
className={`h-full rounded-md shadow relative ${
form.values.image.length === 0 ? "hidden" : "block"
}`}>
<div>
<img
src={form.values.image}
alt="Uploaded"
className="h-full w-auto object-cover rounded-md min-w-[50px]"
/>
<button
onClick={() => {
form.setFieldValue("image", "")
}}
className="absolute top-2 right-2 bg-white dark:bg-black p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100">
<XMarkIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="flex">
<Tooltip title="New Chat">
<button className="text-gray-500 dark:text-gray-100 mr-3">
<ArrowPathIcon className="h-5 w-5" />
</button>
</Tooltip>
<form
onSubmit={form.onSubmit(async (value) => {
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
return
}
form.reset()
resetHeight()
await sendMessage({
image: value.image,
message: value.message.trim()
})
})}
className="shrink-0 flex-grow flex items-center ">
<div className="flex items-center p-2 rounded-2xl border bg-gray-100 w-full dark:bg-black dark:border-gray-800">
<button
type="button"
onClick={() => {
inputRef.current?.click()
}}
className={`flex ml-3 items-center justify-center dark:text-gray-100 ${
chatMode === "rag" ? "hidden" : "block"
}`}>
<PhotoIcon className="h-5 w-5" />
</button>
<input
id="file-upload"
name="file-upload"
type="file"
className="sr-only"
ref={inputRef}
accept="image/*"
multiple={false}
onChange={onInputChange}
/>
<textarea
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && !isSending) {
e.preventDefault()
form.onSubmit(async (value) => {
if (value.message.trim().length === 0) {
return
}
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
return
}
form.reset()
resetHeight()
await sendMessage({
image: value.image,
message: value.message.trim()
})
})()
}
}}
ref={textareaRef}
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none sm:text-sm focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
required
rows={1}
tabIndex={0}
placeholder="Type a message..."
{...form.getInputProps("message")}
/>
<button
disabled={isSending || form.values.message.length === 0}
className="ml-2 flex items-center justify-center w-10 h-10 text-white bg-black rounded-xl disabled:opacity-50">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-6 w-6"
viewBox="0 0 24 24">
<path d="M9 10L4 15 9 20"></path>
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
</svg>
</button>
</div>
</form>
</div>
{form.errors.message && (
<div className="text-red-500 text-center text-sm mt-1">
{form.errors.message}
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,98 @@
import { CheckIcon, ClipboardIcon } from "@heroicons/react/24/outline"
import Markdown from "../../Common/Markdown"
import React from "react"
type Props = {
message: string
hideCopy?: boolean
botAvatar?: JSX.Element
userAvatar?: JSX.Element
isBot: boolean
name: string
images?: string[]
}
export const PlaygroundMessage = (props: Props) => {
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
React.useEffect(() => {
if (isBtnPressed) {
setTimeout(() => {
setIsBtnPressed(false)
}, 4000)
}
}, [isBtnPressed])
return (
<div
className={`group w-full text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 `}>
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full">
<div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-4 md:py-6 lg:px-0 m-auto w-full">
<div className="w-8 flex flex-col relative items-end">
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
{props.isBot ? (
!props.botAvatar ? (
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
) : (
props.botAvatar
)
) : !props.userAvatar ? (
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
) : (
props.userAvatar
)}
</div>
</div>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 md:gap-3 lg:w-[calc(100%-115px)]">
{props.isBot && (
<span className="absolute mb-8 -top-4 left-0 text-xs text-gray-400 dark:text-gray-500">
{props.name}
</span>
)}
<div className="flex flex-grow flex-col gap-3">
<Markdown message={props.message} />
</div>
{/* source if aviable */}
{props.images && (
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full">
{props.images
.filter((image) => image.length > 0)
.map((image, index) => (
<div
key={index}
className="h-full rounded-md shadow relative">
<img
src={image}
alt="Uploaded"
className="h-full w-auto object-cover rounded-md min-w-[50px]"
/>
</div>
))}
</div>
)}
</div>
{props.isBot && (
<div className="flex space-x-2">
{!props.hideCopy && (
<button
onClick={() => {
navigator.clipboard.writeText(props.message)
setIsBtnPressed(true)
}}
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
{!isBtnPressed ? (
<ClipboardIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
) : (
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
)}
</button>
)}
</div>
)}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,25 @@
import { PencilSquareIcon } from "@heroicons/react/24/outline"
import { useMessage } from "../../../hooks/useMessage"
export const PlaygroundNewChat = () => {
const { setHistory, setMessages, setHistoryId } = useMessage()
const handleClick = () => {
setHistoryId(null)
setMessages([])
setHistory([])
// navigate(`/bot/${params.id}`);
}
return (
<button
onClick={handleClick}
className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800">
<PencilSquareIcon className="mx-3 h-5 w-5" aria-hidden="true" />
<span className="inline-flex font-semibol text-white text-sm">
New Chat
</span>
</button>
)
}

View File

@ -0,0 +1,42 @@
import { Modal } from "antd"
import { useState } from "react"
export const PlaygroundSettings = () => {
const [open, setOpen] = useState(false)
return (
<div className="flex-shrink-0 flex flex-col items-center justify-center py-1 ">
<div className="flex items-center justify-center space-x-2">
<button
onClick={() => setOpen(true)}
className="flex items-center justify-center w-8 h-8 rounded-full transition-colors duration-200 focus:outline-none">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
</div>
<Modal
footer={null}
title="Playground Settings"
open={open}
onCancel={() => setOpen(false)}>
Nothing to see here
</Modal>
</div>
)
}

View File

@ -0,0 +1,47 @@
import { useQuery } from "@tanstack/react-query"
import { PageAssitDatabase } from "~libs/db"
import { Empty, Skeleton } from "antd"
type Props = {}
export const Sidebar = ({}: Props) => {
const { data: chatHistories, status } = useQuery({
queryKey: ["fetchChatHistory"],
queryFn: async () => {
const db = new PageAssitDatabase()
const history = await db.getChatHistories()
return history
}
})
return (
<div className="overflow-y-auto h-[calc(100%-60px)]">
{status === "success" && chatHistories.length === 0 && (
<div className="flex justify-center items-center mt-20 overflow-hidden">
<Empty description="No history yet" />
</div>
)}
{status === "pending" && (
<div className="flex justify-center items-center mt-5">
<Skeleton active paragraph={{ rows: 8 }} />
</div>
)}
{status === "error" && (
<div className="flex justify-center items-center">
<span className="text-red-500">Error loading history</span>
</div>
)}
{status === "success" && chatHistories.length > 0 && (
<div className="flex flex-col gap-2">
{chatHistories.map((chat, index) => (
<div
key={index}
className="flex py-2 px-2 cursor-pointer items-center gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out bg-gray-100 dark:bg-[#232222] dark:text-gray-100 text-gray-800 border hover:bg-gray-200 dark:hover:bg-[#2d2d2d] dark:border-gray-800">
<span className="flex-grow truncate">{chat.title}</span>
</div>
))}
</div>
)}
</div>
)
}

View File

@ -57,7 +57,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
}) })
return ( return (
<div className="p-3 md:p-6 md:bg-white dark:bg-[#1a1919] border rounded-t-xl border-black/10 dark:border-gray-900/50"> <div className="p-3 md:p-6 md:bg-white dark:bg-[#171717] border rounded-t-xl border-black/10 dark:border-gray-900/50">
<div className="flex-grow space-y-6 "> <div className="flex-grow space-y-6 ">
{chatMode === "normal" && form.values.image && ( {chatMode === "normal" && form.values.image && (
<div className="h-full rounded-md shadow relative"> <div className="h-full rounded-md shadow relative">

View File

@ -2,7 +2,7 @@
font-family: 'font'; font-family: 'font';
src: url('font.ttf') format('truetype'); src: url('font.ttf') format('truetype');
} }
body { * {
font-family: 'font' !important; font-family: 'font' !important;
} }

View File

@ -0,0 +1,306 @@
import React from "react"
import { cleanUrl } from "~libs/clean-url"
import { getOllamaURL, systemPromptForNonRag } from "~services/ollama"
import { type ChatHistory, type Message } from "~store/option"
import { ChatOllama } from "@langchain/community/chat_models/ollama"
import {
HumanMessage,
AIMessage,
type MessageContent,
SystemMessage
} from "@langchain/core/messages"
import { useStoreMessageOption } from "~store/option"
import { saveHistory, saveMessage } from "~libs/db"
export type BotResponse = {
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}
const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
image?: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
let content: MessageContent = [
{
type: "text",
text: message.content
}
]
if (message.image) {
content = [
{
type: "image_url",
image_url: message.image
},
{
type: "text",
text: message.content
}
]
}
history.push(
new HumanMessage({
content: content
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}
export const useMessageOption = () => {
const {
history,
messages,
setHistory,
setMessages,
setStreaming,
streaming,
setIsFirstMessage,
historyId,
setHistoryId,
isLoading,
setIsLoading,
isProcessing,
setIsProcessing,
selectedModel,
setSelectedModel,
chatMode,
setChatMode
} = useStoreMessageOption()
const abortControllerRef = React.useRef<AbortController | null>(null)
const clearChat = () => {
stopStreamingRequest()
setMessages([])
setHistory([])
setHistoryId(null)
setIsFirstMessage(true)
setIsLoading(false)
setIsProcessing(false)
setStreaming(false)
}
const normalChatMode = async (message: string, image: string) => {
const url = await getOllamaURL()
if (image.length > 0) {
image = `data:image/jpeg;base64,${image.split(",")[1]}`
}
abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({
model: selectedModel,
baseUrl: cleanUrl(url)
})
let newMessage: Message[] = [
...messages,
{
isBot: false,
name: "You",
message,
sources: [],
images: [image]
},
{
isBot: true,
name: selectedModel,
message: "▋",
sources: []
}
]
const appendingIndex = newMessage.length - 1
setMessages(newMessage)
try {
const prompt = await systemPromptForNonRag()
message = message.trim().replaceAll("\n", " ")
let humanMessage = new HumanMessage({
content: [
{
text: message,
type: "text"
}
]
})
if (image.length > 0) {
humanMessage = new HumanMessage({
content: [
{
text: message,
type: "text"
},
{
image_url: image,
type: "image_url"
}
]
})
}
const applicationChatHistory = generateHistory(history)
if (prompt) {
applicationChatHistory.unshift(
new SystemMessage({
content: [
{
text: prompt,
type: "text"
}
]
})
)
}
const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage],
{
signal: abortControllerRef.current.signal
}
)
let count = 0
for await (const chunk of chunks) {
if (count === 0) {
setIsProcessing(true)
newMessage[appendingIndex].message = chunk.content + "▋"
setMessages(newMessage)
} else {
newMessage[appendingIndex].message =
newMessage[appendingIndex].message.slice(0, -1) +
chunk.content +
"▋"
setMessages(newMessage)
}
count++
}
newMessage[appendingIndex].message = newMessage[
appendingIndex
].message.slice(0, -1)
setHistory([
...history,
{
role: "user",
content: message,
image
},
{
role: "assistant",
content: newMessage[appendingIndex].message
}
])
if (historyId) {
await saveMessage(historyId, selectedModel, "user", message, [image])
await saveMessage(
historyId,
selectedModel,
"assistant",
newMessage[appendingIndex].message,
[]
)
} else {
const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
image
])
await saveMessage(
newHistoryId.id,
selectedModel,
"assistant",
newMessage[appendingIndex].message,
[]
)
setHistoryId(newHistoryId.id)
}
setIsProcessing(false)
} catch (e) {
setIsProcessing(false)
setStreaming(false)
setMessages([
...messages,
{
isBot: true,
name: selectedModel,
message: `Something went wrong. Check out the following logs:
\`\`\`
${e?.message}
\`\`\`
`,
sources: []
}
])
}
}
const onSubmit = async ({
message,
image
}: {
message: string
image: string
}) => {
await normalChatMode(message, image)
}
const stopStreamingRequest = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
abortControllerRef.current = null
}
}
return {
messages,
setMessages,
onSubmit,
setStreaming,
streaming,
setHistory,
historyId,
setHistoryId,
setIsFirstMessage,
isLoading,
setIsLoading,
isProcessing,
stopStreamingRequest,
clearChat,
selectedModel,
setSelectedModel,
chatMode,
setChatMode
}
}

3
src/libs/class-name.tsx Normal file
View File

@ -0,0 +1,3 @@
export const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ")
}

111
src/libs/db.ts Normal file
View File

@ -0,0 +1,111 @@
im
type HistoryInfo = {
id: string
title: string
createdAt: number
}
type Message = {
id: string
history_id: string
name: string
role: string
content: string
images?: string[]
createdAt: number
}
type MessageHistory = Message[]
type ChatHistory = HistoryInfo[]
export class PageAssitDatabase {
db: chrome.storage.StorageArea
constructor() {
this.db = chrome.storage.local
}
async getChatHistory(id: string): Promise<MessageHistory> {
return new Promise((resolve, reject) => {
this.db.get(id, (result) => {
resolve(result[id] || [])
})
})
}
async getChatHistories(): Promise<ChatHistory> {
return new Promise((resolve, reject) => {
this.db.get("chatHistories", (result) => {
resolve(result.chatHistories || [])
})
})
}
async addChatHistory(history: HistoryInfo) {
const chatHistories = await this.getChatHistories()
const newChatHistories = [history, ...chatHistories]
this.db.set({ chatHistories: newChatHistories })
}
async addMessage(message: Message) {
const history_id = message.history_id
const chatHistory = await this.getChatHistory(history_id)
const newChatHistory = [message, ...chatHistory]
this.db.set({ [history_id]: newChatHistory })
}
async removeChatHistory(id: string) {
const chatHistories = await this.getChatHistories()
const newChatHistories = chatHistories.filter(
(history) => history.id !== id
)
this.db.set({ chatHistories: newChatHistories })
}
async removeMessage(history_id: string, message_id: string) {
const chatHistory = await this.getChatHistory(history_id)
const newChatHistory = chatHistory.filter(
(message) => message.id !== message_id
)
this.db.set({ [history_id]: newChatHistory })
}
async clear() {
this.db.clear()
}
}
const generateID = () => {
return "pa_xxxx-xxxx-xxx-xxxx".replace(/[x]/g, () => {
const r = Math.floor(Math.random() * 16)
return r.toString(16)
})
}
export const saveHistory = async (title: string) => {
const id = generateID()
const createdAt = Date.now()
const history = { id, title, createdAt }
const db = new PageAssitDatabase()
await db.addChatHistory(history)
return history
}
export const saveMessage = async (
history_id: string,
name: string,
role: string,
content: string,
images: string[]
) => {
const id = generateID()
const createdAt = Date.now()
const message = { id, history_id, name, role, content, images, createdAt }
const db = new PageAssitDatabase()
await db.addMessage(message)
return message
}

View File

@ -7,6 +7,7 @@ import "./css/tailwind.css"
import { ConfigProvider, theme } from "antd" import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~hooks/useDarkmode"
import { OptionRouting } from "~routes"
function IndexOption() { function IndexOption() {
const { mode } = useDarkMode() const { mode } = useDarkMode()
return ( return (
@ -18,6 +19,7 @@ function IndexOption() {
}}> }}>
<StyleProvider hashPriority="high"> <StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<OptionRouting />
<ToastContainer /> <ToastContainer />
</QueryClientProvider> </QueryClientProvider>
</StyleProvider> </StyleProvider>

View File

@ -4,11 +4,17 @@ import { useDarkMode } from "~hooks/useDarkmode"
import { SidepanelSettings } from "./sidepanel-settings" import { SidepanelSettings } from "./sidepanel-settings"
import { OptionIndex } from "./option-index" import { OptionIndex } from "./option-index"
export const OptionRouting = () => ( export const OptionRouting = () => {
const { mode } = useDarkMode()
return (
<div className={mode === "dark" ? "dark" : "light"}>
<Routes> <Routes>
<Route path="/" element={<OptionIndex />} /> <Route path="/" element={<OptionIndex />} />
</Routes> </Routes>
) </div>
)
}
export const SidepanelRouting = () => { export const SidepanelRouting = () => {
const { mode } = useDarkMode() const { mode } = useDarkMode()

View File

@ -1,10 +1,12 @@
import OptionLayout from "~components/Option/Layout"
import { Playground } from "~components/Option/Playground/Playground"
import { SettingsBody } from "~components/Sidepanel/Settings/body" import { SettingsBody } from "~components/Sidepanel/Settings/body"
import { SidepanelSettingsHeader } from "~components/Sidepanel/Settings/header" import { SidepanelSettingsHeader } from "~components/Sidepanel/Settings/header"
export const OptionIndex = () => { export const OptionIndex = () => {
return ( return (
<div className="flex bg-white dark:bg-black flex-col min-h-screen mx-auto max-w-7xl"> <OptionLayout>
hey <Playground />
</div> </OptionLayout>
) )
} }

62
src/store/option.tsx Normal file
View File

@ -0,0 +1,62 @@
import { create } from "zustand"
export type Message = {
isBot: boolean
name: string
message: string
sources: any[]
images?: string[]
}
export type ChatHistory = {
role: "user" | "assistant" | "system"
content: string,
image?: string
}[]
type State = {
messages: Message[]
setMessages: (messages: Message[]) => void
history: ChatHistory
setHistory: (history: ChatHistory) => void
streaming: boolean
setStreaming: (streaming: boolean) => void
isFirstMessage: boolean
setIsFirstMessage: (isFirstMessage: boolean) => void
historyId: string | null
setHistoryId: (history_id: string | null) => void
isLoading: boolean
setIsLoading: (isLoading: boolean) => void
isProcessing: boolean
setIsProcessing: (isProcessing: boolean) => void
selectedModel: string | null
setSelectedModel: (selectedModel: string) => void
chatMode: "normal" | "rag"
setChatMode: (chatMode: "normal" | "rag") => void
isEmbedding: boolean
setIsEmbedding: (isEmbedding: boolean) => void
}
export const useStoreMessageOption = create<State>((set) => ({
messages: [],
setMessages: (messages) => set({ messages }),
history: [],
setHistory: (history) => set({ history }),
streaming: true,
setStreaming: (streaming) => set({ streaming }),
isFirstMessage: true,
setIsFirstMessage: (isFirstMessage) => set({ isFirstMessage }),
historyId: null,
setHistoryId: (historyId) => set({ historyId }),
isLoading: false,
setIsLoading: (isLoading) => set({ isLoading }),
isProcessing: false,
setIsProcessing: (isProcessing) => set({ isProcessing }),
defaultSpeechToTextLanguage: "en-US",
selectedModel: null,
setSelectedModel: (selectedModel) => set({ selectedModel }),
chatMode: "normal",
setChatMode: (chatMode) => set({ chatMode }),
isEmbedding: false,
setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
}))

View File

@ -4962,6 +4962,11 @@ lru-cache@^6.0.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
lucide-react@^0.323.0:
version "0.323.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.323.0.tgz#d1dfae7b212a29bbc513b9d7fd0ce5e8f93e6b13"
integrity sha512-rTXZFILl2Y4d1SG9p1Mdcf17AcPvPvpc/egFIzUrp7IUy60MUQo3Oi1mu8LGYXUVwuRZYsSMt3csHRW5mAovJg==
magic-string@^0.30.0: magic-string@^0.30.0:
version "0.30.6" version "0.30.6"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.6.tgz#996e21b42f944e45591a68f0905d6a740a12506c" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.6.tgz#996e21b42f944e45591a68f0905d6a740a12506c"