Update dependencies and fix styling issues
This commit is contained in:
parent
a66d8a8418
commit
58966355c3
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
})
|
|
@ -55,43 +55,45 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
<Markdown message={props.message} />
|
<Markdown message={props.message} />
|
||||||
</div>
|
</div>
|
||||||
{/* source if aviable */}
|
{/* source if aviable */}
|
||||||
</div>
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
{props.isBot && (
|
{props.isBot && (
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
{!props.hideCopy && (
|
{!props.hideCopy && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(props.message)
|
navigator.clipboard.writeText(props.message)
|
||||||
setIsBtnPressed(true)
|
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">
|
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 ? (
|
{!isBtnPressed ? (
|
||||||
<ClipboardIcon className="w-4 h-4 text-gray-400 group-hover:text-gray-500" />
|
<ClipboardIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
) : (
|
) : (
|
||||||
<CheckIcon className="w-4 h-4 text-green-400 group-hover:text-green-500" />
|
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.images && (
|
|
||||||
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-3 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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
113
src/components/Option/Layout.tsx
Normal file
113
src/components/Option/Layout.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
89
src/components/Option/Playground/Playground.tsx
Normal file
89
src/components/Option/Playground/Playground.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
33
src/components/Option/Playground/PlaygroundChat.tsx
Normal file
33
src/components/Option/Playground/PlaygroundChat.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
181
src/components/Option/Playground/PlaygroundForm.tsx
Normal file
181
src/components/Option/Playground/PlaygroundForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
98
src/components/Option/Playground/PlaygroundMessage.tsx
Normal file
98
src/components/Option/Playground/PlaygroundMessage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
25
src/components/Option/Playground/PlaygroundNewChat.tsx
Normal file
25
src/components/Option/Playground/PlaygroundNewChat.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
42
src/components/Option/Playground/PlaygroundSettings.tsx
Normal file
42
src/components/Option/Playground/PlaygroundSettings.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
47
src/components/Option/Sidebar.tsx
Normal file
47
src/components/Option/Sidebar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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">
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
306
src/hooks/useMessageOption.tsx
Normal file
306
src/hooks/useMessageOption.tsx
Normal 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
3
src/libs/class-name.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const classNames = (...classes: string[]) => {
|
||||||
|
return classes.filter(Boolean).join(" ")
|
||||||
|
}
|
111
src/libs/db.ts
Normal file
111
src/libs/db.ts
Normal 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
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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 = () => {
|
||||||
<Routes>
|
const { mode } = useDarkMode()
|
||||||
<Route path="/" element={<OptionIndex />} />
|
|
||||||
</Routes>
|
return (
|
||||||
)
|
<div className={mode === "dark" ? "dark" : "light"}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<OptionIndex />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const SidepanelRouting = () => {
|
export const SidepanelRouting = () => {
|
||||||
const { mode } = useDarkMode()
|
const { mode } = useDarkMode()
|
||||||
|
@ -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
62
src/store/option.tsx
Normal 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 }),
|
||||||
|
}))
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user