feat(home): add drag file to chat input, add settings config API

This commit is contained in:
2026-03-14 22:22:35 +08:00
parent cbc1af7348
commit e950484af6
6 changed files with 74 additions and 1 deletions

View File

@@ -51,4 +51,13 @@ router.post(
}),
)
router.get(
'/config',
asyncHandler(async (req: Request, res: Response) => {
successResponse(res, {
notebookRoot: NOTEBOOK_ROOT,
})
}),
)
export default router

View File

@@ -13,6 +13,8 @@ import { Messages } from './Messages'
import { MultimodalInput } from './MultimodalInput'
import { generateUUID } from '@/lib/utils'
import { Eraser } from 'lucide-react'
import { useDragStore } from '@/stores/dragStore'
import { getSettingsConfig } from '@/lib/api'
type Status = 'submitted' | 'streaming' | 'ready' | 'error'
@@ -51,6 +53,49 @@ export function Chat({
const [status, setStatus] = useState<Status>('ready')
const [attachments, setAttachments] = useState<Attachment[]>([])
const [showInputAtBottom, setShowInputAtBottom] = useState(false)
const [notebookRoot, setNotebookRoot] = useState<string>('')
const { state: dragState } = useDragStore()
useEffect(() => {
getSettingsConfig().then(config => {
// 统一 notebookRoot 的斜杠
const normalizedRoot = (config.notebookRoot || '').replace(/\\/g, '/')
setNotebookRoot(normalizedRoot)
}).catch(e => {
console.error('Failed to get notebook root:', e)
})
}, [])
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
// 优先从 dragStore 获取
let { draggedPath, draggedType } = dragState
// 如果 dragStore 为空,尝试从 dataTransfer 获取
if (!draggedPath) {
draggedPath = e.dataTransfer.getData('text/plain')
}
if (draggedPath) {
// 统一路径中的斜杠,并拼接完整路径,用【】包围
const normalizedPath = draggedPath.replace(/\\/g, '/')
const fullPath = notebookRoot ? `${notebookRoot}/${normalizedPath}` : normalizedPath
const pathWithBrackets = `${fullPath}`
setInput(prev => prev + pathWithBrackets)
// 延迟聚焦输入框
setTimeout(() => {
const textarea = document.querySelector('textarea') as HTMLTextAreaElement
textarea?.focus()
}, 0)
}
}, [dragState, notebookRoot])
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
}, [])
const scrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'auto' })
@@ -211,6 +256,8 @@ export function Chat({
setInput={setInput}
status={status}
stop={handleStop}
onDrop={handleDrop}
onDragOver={handleDragOver}
/>
</div>
</div>

View File

@@ -1,6 +1,6 @@
export function Greeting() {
return (
<div className="mx-auto mt-64 max-w-2xl px-4 text-center">
<div className="mx-auto mt-60 max-w-2xl px-4 text-center">
<h1 className="text-3xl font-bold tracking-tight">
</h1>

View File

@@ -30,6 +30,8 @@ interface MultimodalInputProps {
messages: ChatMessage[]
sendMessage: (text: string, attachments: Attachment[]) => Promise<void>
className?: string
onDrop?: (e: React.DragEvent) => void
onDragOver?: (e: React.DragEvent) => void
}
export function MultimodalInput({
@@ -42,6 +44,8 @@ export function MultimodalInput({
setAttachments,
sendMessage,
className,
onDrop,
onDragOver,
}: MultimodalInputProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
@@ -135,6 +139,14 @@ export function MultimodalInput({
value={input}
onChange={handleInput}
onKeyDown={handleKeyDown}
onDrop={(e) => {
e.preventDefault()
onDrop?.(e)
}}
onDragOver={(e) => {
e.preventDefault()
onDragOver?.(e)
}}
placeholder="发送消息..."
rows={1}
/>

View File

@@ -83,6 +83,10 @@ export const getSettings = async (): Promise<Settings> => {
return await fetchApi<Settings>('/api/settings')
}
export const getSettingsConfig = async (): Promise<{ notebookRoot: string }> => {
return await fetchApi<{ notebookRoot: string }>('/api/settings/config')
}
export const saveSettings = async (settings: Settings): Promise<Settings> => {
return await fetchApi<Settings>('/api/settings', {
method: 'POST',

View File

@@ -13,6 +13,7 @@ export {
renameItem,
runAiTask,
getSettings,
getSettingsConfig,
saveSettings,
uploadPdfForParsing,
parseLocalHtml,