chatai/chatbot-ui/components/workspace/workspace-settings.tsx

306 lines
9.4 KiB
TypeScript

import { ChatbotUIContext } from "@/context/context"
import { WORKSPACE_INSTRUCTIONS_MAX } from "@/db/limits"
import {
getWorkspaceImageFromStorage,
uploadWorkspaceImage
} from "@/db/storage/workspace-images"
import { updateWorkspace } from "@/db/workspaces"
import { convertBlobToBase64 } from "@/lib/blob-to-b64"
import { LLMID } from "@/types"
import { IconHome, IconSettings } from "@tabler/icons-react"
import { FC, useContext, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import { Button } from "../ui/button"
import { ChatSettingsForm } from "../ui/chat-settings-form"
import ImagePicker from "../ui/image-picker"
import { Input } from "../ui/input"
import { Label } from "../ui/label"
import { LimitDisplay } from "../ui/limit-display"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger
} from "../ui/sheet"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
import { TextareaAutosize } from "../ui/textarea-autosize"
import { WithTooltip } from "../ui/with-tooltip"
import { DeleteWorkspace } from "./delete-workspace"
import { useTranslation } from "react-i18next";
interface WorkspaceSettingsProps {}
export const WorkspaceSettings: FC<WorkspaceSettingsProps> = ({}) => {
const { t } = useTranslation();
const {
profile,
selectedWorkspace,
setSelectedWorkspace,
setWorkspaces,
setChatSettings,
workspaceImages,
setWorkspaceImages
} = useContext(ChatbotUIContext)
const buttonRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [name, setName] = useState(selectedWorkspace?.name || "")
const [imageLink, setImageLink] = useState("")
const [selectedImage, setSelectedImage] = useState<File | null>(null)
const [description, setDescription] = useState(
selectedWorkspace?.description || ""
)
const [instructions, setInstructions] = useState(
selectedWorkspace?.instructions || ""
)
const [defaultChatSettings, setDefaultChatSettings] = useState({
model: selectedWorkspace?.default_model,
prompt: selectedWorkspace?.default_prompt,
temperature: selectedWorkspace?.default_temperature,
contextLength: selectedWorkspace?.default_context_length,
includeProfileContext: selectedWorkspace?.include_profile_context,
includeWorkspaceInstructions:
selectedWorkspace?.include_workspace_instructions,
embeddingsProvider: selectedWorkspace?.embeddings_provider
})
useEffect(() => {
const workspaceImage =
workspaceImages.find(
image => image.path === selectedWorkspace?.image_path
)?.base64 || ""
setImageLink(workspaceImage)
}, [workspaceImages])
const handleSave = async () => {
if (!selectedWorkspace) return
let imagePath = ""
if (selectedImage) {
imagePath = await uploadWorkspaceImage(selectedWorkspace, selectedImage)
const url = (await getWorkspaceImageFromStorage(imagePath)) || ""
if (url) {
const response = await fetch(url)
const blob = await response.blob()
const base64 = await convertBlobToBase64(blob)
setWorkspaceImages(prev => [
...prev,
{
workspaceId: selectedWorkspace.id,
path: imagePath,
base64,
url
}
])
}
}
const updatedWorkspace = await updateWorkspace(selectedWorkspace.id, {
...selectedWorkspace,
name,
description,
image_path: imagePath,
instructions,
default_model: defaultChatSettings.model,
default_prompt: defaultChatSettings.prompt,
default_temperature: defaultChatSettings.temperature,
default_context_length: defaultChatSettings.contextLength,
embeddings_provider: defaultChatSettings.embeddingsProvider,
include_profile_context: defaultChatSettings.includeProfileContext,
include_workspace_instructions:
defaultChatSettings.includeWorkspaceInstructions
})
if (
defaultChatSettings.model &&
defaultChatSettings.prompt &&
defaultChatSettings.temperature &&
defaultChatSettings.contextLength &&
defaultChatSettings.includeProfileContext &&
defaultChatSettings.includeWorkspaceInstructions &&
defaultChatSettings.embeddingsProvider
) {
setChatSettings({
model: defaultChatSettings.model as LLMID,
prompt: defaultChatSettings.prompt,
temperature: defaultChatSettings.temperature,
contextLength: defaultChatSettings.contextLength,
includeProfileContext: defaultChatSettings.includeProfileContext,
includeWorkspaceInstructions:
defaultChatSettings.includeWorkspaceInstructions,
embeddingsProvider: defaultChatSettings.embeddingsProvider as
| "openai"
| "local"
})
}
setIsOpen(false)
setSelectedWorkspace(updatedWorkspace)
setWorkspaces(workspaces => {
return workspaces.map(workspace => {
if (workspace.id === selectedWorkspace.id) {
return updatedWorkspace
}
return workspace
})
})
toast.success("Workspace updated!")
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
buttonRef.current?.click()
}
}
if (!selectedWorkspace || !profile) return null
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<WithTooltip
display={<div>{t("side.workspaceSettings")}</div>}
trigger={
<IconSettings
className="ml-3 cursor-pointer pr-[5px] hover:opacity-50"
size={32}
onClick={() => setIsOpen(true)}
/>
}
/>
</SheetTrigger>
<SheetContent
className="flex flex-col justify-between"
side="left"
onKeyDown={handleKeyDown}
>
<div className="grow overflow-auto">
<SheetHeader>
<SheetTitle className="flex items-center justify-between">
{t("side.workspaceSettings")}
{selectedWorkspace?.is_home && <IconHome />}
</SheetTitle>
{selectedWorkspace?.is_home && (
<div className="text-sm font-light">
{t("side.workspaceDescription")}
</div>
)}
</SheetHeader>
<Tabs defaultValue="main">
<TabsList className="mt-4 grid w-full grid-cols-2">
<TabsTrigger value="main">{t("side.main")}</TabsTrigger>
<TabsTrigger value="defaults">{t("side.defaults")}</TabsTrigger>
</TabsList>
<TabsContent className="mt-4 space-y-4" value="main">
<>
<div className="space-y-1">
<Label>{t("side.workspaceName")}</Label>
<Input
placeholder={t("side.workspaceNamePlaceholder")}
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
{/* <div className="space-y-1">
<Label>Description</Label>
<Input
placeholder="Description... (optional)"
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div> */}
<div className="space-y-1">
<Label>{t("side.workspaceImage")}</Label>
<ImagePicker
src={imageLink}
image={selectedImage}
onSrcChange={setImageLink}
onImageChange={setSelectedImage}
width={50}
height={50}
/>
</div>
</>
<div className="space-y-1">
<Label>
{t("side.aiResponseInstructions")}
</Label>
<TextareaAutosize
placeholder={t("side.workspaceInstructionsPlaceholder")}
value={instructions}
onValueChange={setInstructions}
minRows={5}
maxRows={10}
maxLength={1500}
/>
<LimitDisplay
used={instructions.length}
limit={WORKSPACE_INSTRUCTIONS_MAX}
/>
</div>
</TabsContent>
<TabsContent className="mt-5" value="defaults">
<div className="mb-4 text-sm">
{t("side.workspaceBeginSettings")}
</div>
<ChatSettingsForm
chatSettings={defaultChatSettings as any}
onChangeChatSettings={setDefaultChatSettings}
/>
</TabsContent>
</Tabs>
</div>
<div className="mt-6 flex justify-between">
<div>
{!selectedWorkspace.is_home && (
<DeleteWorkspace
workspace={selectedWorkspace}
onDelete={() => setIsOpen(false)}
/>
)}
</div>
<div className="space-x-2">
<Button variant="ghost" onClick={() => setIsOpen(false)}>
{t("side.cancel")}
</Button>
<Button ref={buttonRef} onClick={handleSave}>
{t("side.save")}
</Button>
</div>
</div>
</SheetContent>
</Sheet>
)
}