Update dependencies and fix styling issues
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user