Update dependencies and fix styling issues
This commit is contained in:
parent
a66d8a8418
commit
58966355c3
@ -24,6 +24,7 @@
|
||||
"axios": "^1.6.7",
|
||||
"html-to-text": "^9.0.5",
|
||||
"langchain": "^0.1.9",
|
||||
"lucide-react": "^0.323.0",
|
||||
"plasmo": "0.84.1",
|
||||
"property-information": "^6.4.1",
|
||||
"react": "18.2.0",
|
||||
@ -78,7 +79,8 @@
|
||||
"scripting",
|
||||
"declarativeNetRequest",
|
||||
"declarativeNetRequestFeedback",
|
||||
"action"
|
||||
"action",
|
||||
"unlimitedStorage"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,3 @@ chrome.runtime.onMessage.addListener(async (message) => {
|
||||
chrome.action.onClicked.addListener((tab) => {
|
||||
chrome.tabs.create({url: chrome.runtime.getURL("options.html")});
|
||||
});
|
||||
|
||||
// listen to commadns
|
||||
chrome.commands.onCommand.addListener((command) => {
|
||||
console.log('Command', command)
|
||||
})
|
@ -55,34 +55,14 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
<Markdown message={props.message} />
|
||||
</div>
|
||||
{/* 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 && (
|
||||
<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
|
||||
.filter((image) => image.length > 0)
|
||||
.map((image, index) => (
|
||||
<div key={index} className="h-full rounded-md shadow relative">
|
||||
<div
|
||||
key={index}
|
||||
className="h-full rounded-md shadow relative">
|
||||
<img
|
||||
src={image}
|
||||
alt="Uploaded"
|
||||
@ -92,6 +72,28 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
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 (
|
||||
<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 ">
|
||||
{chatMode === "normal" && form.values.image && (
|
||||
<div className="h-full rounded-md shadow relative">
|
||||
|
@ -2,7 +2,7 @@
|
||||
font-family: 'font';
|
||||
src: url('font.ttf') format('truetype');
|
||||
}
|
||||
body {
|
||||
* {
|
||||
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 { StyleProvider } from "@ant-design/cssinjs"
|
||||
import { useDarkMode } from "~hooks/useDarkmode"
|
||||
import { OptionRouting } from "~routes"
|
||||
function IndexOption() {
|
||||
const { mode } = useDarkMode()
|
||||
return (
|
||||
@ -18,6 +19,7 @@ function IndexOption() {
|
||||
}}>
|
||||
<StyleProvider hashPriority="high">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OptionRouting />
|
||||
<ToastContainer />
|
||||
</QueryClientProvider>
|
||||
</StyleProvider>
|
||||
|
@ -4,11 +4,17 @@ import { useDarkMode } from "~hooks/useDarkmode"
|
||||
import { SidepanelSettings } from "./sidepanel-settings"
|
||||
import { OptionIndex } from "./option-index"
|
||||
|
||||
export const OptionRouting = () => (
|
||||
export const OptionRouting = () => {
|
||||
const { mode } = useDarkMode()
|
||||
|
||||
return (
|
||||
<div className={mode === "dark" ? "dark" : "light"}>
|
||||
<Routes>
|
||||
<Route path="/" element={<OptionIndex />} />
|
||||
</Routes>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SidepanelRouting = () => {
|
||||
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 { SidepanelSettingsHeader } from "~components/Sidepanel/Settings/header"
|
||||
|
||||
export const OptionIndex = () => {
|
||||
return (
|
||||
<div className="flex bg-white dark:bg-black flex-col min-h-screen mx-auto max-w-7xl">
|
||||
hey
|
||||
</div>
|
||||
<OptionLayout>
|
||||
<Playground />
|
||||
</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"
|
||||
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:
|
||||
version "0.30.6"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.6.tgz#996e21b42f944e45591a68f0905d6a740a12506c"
|
||||
|
Loading…
x
Reference in New Issue
Block a user