307 lines
9.4 KiB
TypeScript
307 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)}>
|
|
Cancel
|
|
{t("side.cancel")}
|
|
</Button>
|
|
|
|
<Button ref={buttonRef} onClick={handleSave}>
|
|
{t("side.save")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|