chatbot-ui/components/sidebar/items/all/sidebar-create-item.tsx

279 lines
7.7 KiB
TypeScript

import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle
} from "@/components/ui/sheet"
import { ChatbotUIContext } from "@/context/context"
import { createAssistantCollections } from "@/db/assistant-collections"
import { createAssistantFiles } from "@/db/assistant-files"
import { createAssistantTools } from "@/db/assistant-tools"
import { createAssistant, updateAssistant } from "@/db/assistants"
import { createChat } from "@/db/chats"
import { createCollectionFiles } from "@/db/collection-files"
import { createCollection } from "@/db/collections"
import { createFileBasedOnExtension } from "@/db/files"
import { createModel } from "@/db/models"
import { createPreset } from "@/db/presets"
import { createPrompt } from "@/db/prompts"
import {
getAssistantImageFromStorage,
uploadAssistantImage
} from "@/db/storage/assistant-images"
import { createTool } from "@/db/tools"
import { convertBlobToBase64 } from "@/lib/blob-to-b64"
import { Tables, TablesInsert } from "@/supabase/types"
import { ContentType } from "@/types"
import { FC, useContext, useRef, useState } from "react"
import { toast } from "sonner"
import { useTranslation } from 'react-i18next'
interface SidebarCreateItemProps {
isOpen: boolean
isTyping: boolean
onOpenChange: (isOpen: boolean) => void
contentType: ContentType
renderInputs: () => JSX.Element
createState: any
}
export const SidebarCreateItem: FC<SidebarCreateItemProps> = ({
isOpen,
onOpenChange,
contentType,
renderInputs,
createState,
isTyping
}) => {
const { t, i18n } = useTranslation()
const {
selectedWorkspace,
setChats,
setPresets,
setPrompts,
setFiles,
setCollections,
setAssistants,
setAssistantImages,
setTools,
setModels
} = useContext(ChatbotUIContext)
const buttonRef = useRef<HTMLButtonElement>(null)
const [creating, setCreating] = useState(false)
const createFunctions = {
chats: createChat,
presets: createPreset,
prompts: createPrompt,
files: async (
createState: { file: File } & TablesInsert<"files">,
workspaceId: string
) => {
if (!selectedWorkspace) return
const { file, ...rest } = createState
const createdFile = await createFileBasedOnExtension(
file,
rest,
workspaceId,
selectedWorkspace.embeddings_provider as "openai" | "local"
)
return createdFile
},
collections: async (
createState: {
image: File
collectionFiles: TablesInsert<"collection_files">[]
} & Tables<"collections">,
workspaceId: string
) => {
const { collectionFiles, ...rest } = createState
const createdCollection = await createCollection(rest, workspaceId)
const finalCollectionFiles = collectionFiles.map(collectionFile => ({
...collectionFile,
collection_id: createdCollection.id
}))
await createCollectionFiles(finalCollectionFiles)
return createdCollection
},
assistants: async (
createState: {
image: File
files: Tables<"files">[]
collections: Tables<"collections">[]
tools: Tables<"tools">[]
} & Tables<"assistants">,
workspaceId: string
) => {
const { image, files, collections, tools, ...rest } = createState
const createdAssistant = await createAssistant(rest, workspaceId)
let updatedAssistant = createdAssistant
if (image) {
const filePath = await uploadAssistantImage(createdAssistant, image)
updatedAssistant = await updateAssistant(createdAssistant.id, {
image_path: filePath
})
const url = (await getAssistantImageFromStorage(filePath)) || ""
if (url) {
const response = await fetch(url)
const blob = await response.blob()
const base64 = await convertBlobToBase64(blob)
setAssistantImages(prev => [
...prev,
{
assistantId: updatedAssistant.id,
path: filePath,
base64,
url
}
])
}
}
const assistantFiles = files.map(file => ({
user_id: rest.user_id,
assistant_id: createdAssistant.id,
file_id: file.id
}))
const assistantCollections = collections.map(collection => ({
user_id: rest.user_id,
assistant_id: createdAssistant.id,
collection_id: collection.id
}))
const assistantTools = tools.map(tool => ({
user_id: rest.user_id,
assistant_id: createdAssistant.id,
tool_id: tool.id
}))
await createAssistantFiles(assistantFiles)
await createAssistantCollections(assistantCollections)
await createAssistantTools(assistantTools)
return updatedAssistant
},
tools: createTool,
models: createModel
}
const stateUpdateFunctions = {
chats: setChats,
presets: setPresets,
prompts: setPrompts,
files: setFiles,
collections: setCollections,
assistants: setAssistants,
tools: setTools,
models: setModels
}
const handleCreate = async () => {
try {
if (!selectedWorkspace) return
if (isTyping) return // Prevent creation while typing
const createFunction = createFunctions[contentType]
const setStateFunction = stateUpdateFunctions[contentType]
if (!createFunction || !setStateFunction) return
setCreating(true)
const newItem = await createFunction(createState, selectedWorkspace.id)
setStateFunction((prevItems: any) => [...prevItems, newItem])
onOpenChange(false)
setCreating(false)
} catch (error) {
toast.error(`Error creating ${contentType.slice(0, -1)}. ${error}.`)
setCreating(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (!isTyping && e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
buttonRef.current?.click()
}
}
// 判断是否需要首字母大写(且做 -1 截断)
const needsUpperCaseFirstLetter = (language: string) => {
const languagesRequiringUpperCase = ['en', 'de', 'fr', 'es', 'it'];
return languagesRequiringUpperCase.includes(language);
};
// 处理翻译后的 contentType 文本
const getCapitalizedContentType = (translated: string, language: string) => {
if (needsUpperCaseFirstLetter(language)) {
return translated.charAt(0).toUpperCase() + translated.slice(1, -1); // ✅ 按你的要求保留 .slice(1, -1)
}
return translated;
};
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetContent
className="flex min-w-[450px] flex-col justify-between overflow-auto"
side="left"
onKeyDown={handleKeyDown}
>
<div className="grow overflow-auto">
<SheetHeader>
<SheetTitle className="text-2xl font-bold">
{/* Create{" "}
{contentType.charAt(0).toUpperCase() + contentType.slice(1, -1)} */}
{t("side.sidebarCreateNew")}{" "}
{getCapitalizedContentType(t(`contentTypeLabel.${contentType}`), i18n.language)}
</SheetTitle>
</SheetHeader>
<div className="mt-4 space-y-3">{renderInputs()}</div>
</div>
<SheetFooter className="mt-2 flex justify-between">
<div className="flex grow justify-end space-x-2">
<Button
disabled={creating}
variant="outline"
onClick={() => onOpenChange(false)}
>
{t("side.cancel")}
</Button>
<Button disabled={creating} ref={buttonRef} onClick={handleCreate}>
{creating ? t("side.creating") : t("side.create")}
</Button>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
)
}