mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-06 08:16:03 +00:00
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useState } from "react"
|
|
import ChatInput from "./input"
|
|
import ChatModelSelector from "./model-selector"
|
|
import { useProject } from "@/stores"
|
|
import { cn } from "@lib/utils"
|
|
import type { ModelId } from "@/lib/models"
|
|
import { SpaceSelector } from "@/components/space-selector"
|
|
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
|
|
import { toast } from "sonner"
|
|
import {
|
|
chatAttachmentKey,
|
|
CHAT_ATTACHMENT_ACCEPT,
|
|
createChatAttachmentDraft,
|
|
type ChatAttachmentDraft,
|
|
isAcceptedChatAttachment,
|
|
} from "./attachments"
|
|
import { ReasoningSelector } from "./reasoning-selector"
|
|
import { getDefaultReasoningEffort, type ReasoningEffort } from "@/lib/models"
|
|
|
|
export function HomeChatComposer({
|
|
onStartChat,
|
|
className,
|
|
}: {
|
|
onStartChat: (
|
|
message: string,
|
|
model: ModelId,
|
|
projectId: string,
|
|
reasoningEffort: ReasoningEffort,
|
|
attachments?: ChatAttachmentDraft[],
|
|
) => void
|
|
className?: string
|
|
}) {
|
|
const [input, setInput] = useState("")
|
|
const [attachmentDrafts, setAttachmentDrafts] = useState<
|
|
ChatAttachmentDraft[]
|
|
>([])
|
|
const [selectedModel, setSelectedModel] = useState<ModelId>("grok-4.5")
|
|
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort>(
|
|
getDefaultReasoningEffort("grok-4.5"),
|
|
)
|
|
const { selectedProject } = useProject()
|
|
const [chatSpaceProjects, setChatSpaceProjects] = useState<string[]>([
|
|
AUTO_CHAT_SPACE_ID,
|
|
])
|
|
|
|
const handleModelChange = useCallback((model: ModelId) => {
|
|
setSelectedModel(model)
|
|
setReasoningEffort(getDefaultReasoningEffort(model))
|
|
}, [])
|
|
|
|
const send = useCallback(() => {
|
|
const t = input.trim()
|
|
if (!t && attachmentDrafts.length === 0) return
|
|
onStartChat(
|
|
t,
|
|
selectedModel,
|
|
chatSpaceProjects[0] ?? selectedProject,
|
|
reasoningEffort,
|
|
attachmentDrafts,
|
|
)
|
|
setInput("")
|
|
setAttachmentDrafts([])
|
|
}, [
|
|
attachmentDrafts,
|
|
chatSpaceProjects,
|
|
input,
|
|
onStartChat,
|
|
reasoningEffort,
|
|
selectedModel,
|
|
selectedProject,
|
|
])
|
|
|
|
const handleAddAttachmentFiles = useCallback(
|
|
(files: FileList | File[]) => {
|
|
const incoming = Array.from(files)
|
|
const accepted = incoming.filter(isAcceptedChatAttachment)
|
|
const rejected = incoming.length - accepted.length
|
|
if (rejected > 0) {
|
|
toast.error(
|
|
rejected === 1
|
|
? "One attachment is not supported or is over 50MB"
|
|
: `${rejected} attachments are not supported or are over 50MB`,
|
|
)
|
|
}
|
|
if (accepted.length === 0) return
|
|
|
|
const existingKeys = new Set(
|
|
attachmentDrafts.map((item) => chatAttachmentKey(item.file)),
|
|
)
|
|
const nextItems: ChatAttachmentDraft[] = []
|
|
let duplicateCount = 0
|
|
for (const file of accepted) {
|
|
const key = chatAttachmentKey(file)
|
|
if (existingKeys.has(key)) {
|
|
duplicateCount++
|
|
continue
|
|
}
|
|
existingKeys.add(key)
|
|
nextItems.push(createChatAttachmentDraft(file))
|
|
}
|
|
if (duplicateCount > 0) {
|
|
toast.message(
|
|
duplicateCount === 1
|
|
? "Skipped duplicate attachment"
|
|
: `Skipped ${duplicateCount} duplicate attachments`,
|
|
)
|
|
}
|
|
if (nextItems.length === 0) return
|
|
setAttachmentDrafts((prev) => [...prev, ...nextItems])
|
|
},
|
|
[attachmentDrafts],
|
|
)
|
|
|
|
const handleRemoveAttachment = useCallback((id: string) => {
|
|
setAttachmentDrafts((prev) => prev.filter((item) => item.id !== id))
|
|
}, [])
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault()
|
|
send()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={cn(className)}>
|
|
<div className="mx-auto w-full max-w-[720px] px-4 pt-1 pb-2 md:pb-6">
|
|
<ChatInput
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onSend={send}
|
|
onStop={() => {}}
|
|
onKeyDown={handleKeyDown}
|
|
isResponding={false}
|
|
attachments={attachmentDrafts}
|
|
onAddAttachmentFiles={handleAddAttachmentFiles}
|
|
onRemoveAttachment={handleRemoveAttachment}
|
|
canSend={input.trim().length > 0 || attachmentDrafts.length > 0}
|
|
attachmentAccept={CHAT_ATTACHMENT_ACCEPT}
|
|
showStatusStrip={false}
|
|
stackedToolbar={
|
|
<ChatModelSelector
|
|
selectedModel={selectedModel}
|
|
onModelChange={handleModelChange}
|
|
minimal
|
|
/>
|
|
}
|
|
toolbarTrailing={
|
|
<ReasoningSelector
|
|
value={reasoningEffort}
|
|
onChange={setReasoningEffort}
|
|
/>
|
|
}
|
|
toolbarEnd={
|
|
<SpaceSelector
|
|
selectedProjects={chatSpaceProjects}
|
|
onValueChange={setChatSpaceProjects}
|
|
variant="insideOut"
|
|
includeAuto
|
|
hideCount
|
|
triggerClassName="h-auto min-h-0 max-w-[min(160px,35vw)] gap-1.5 rounded-md border-0 bg-transparent px-2 py-1 shadow-none hover:bg-white/5"
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|