- 移除 ChatInput 和 MessageBubble 中的固定字体大小类 - MarkdownRenderer 标题和内容使用 em 相对单位 - 表格字体大小改为相对单位保持比例 - 聊天输入框添加 z-20 层级避免被代码块遮挡
203 lines
6.7 KiB
TypeScript
203 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef } from 'react';
|
|
import { Plus, ArrowUp, Upload } from 'lucide-react';
|
|
import { ModelSelector } from './ModelSelector';
|
|
import { ToolsDropdown } from './ToolsDropdown';
|
|
import { FilePreviewList } from './FilePreviewList';
|
|
import { useFileUpload } from '@/hooks/useFileUpload';
|
|
import { cn } from '@/lib/utils';
|
|
import type { Model, Tool } from '@/types';
|
|
import type { UploadFile } from '@/types/file-upload';
|
|
|
|
interface ChatInputProps {
|
|
models: Model[];
|
|
selectedModel: Model;
|
|
onModelSelect: (model: Model) => void;
|
|
tools: Tool[];
|
|
onToolToggle: (toolId: string) => void;
|
|
onEnableAllTools: (enabled: boolean) => void;
|
|
onSend: (message: string, files?: UploadFile[]) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function ChatInput({
|
|
models,
|
|
selectedModel,
|
|
onModelSelect,
|
|
tools,
|
|
onToolToggle,
|
|
onEnableAllTools,
|
|
onSend,
|
|
placeholder = 'How can I help you today?',
|
|
className,
|
|
}: ChatInputProps) {
|
|
const [message, setMessage] = useState('');
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// 使用文件上传 Hook
|
|
const {
|
|
files,
|
|
isDragging,
|
|
addFiles,
|
|
removeFile,
|
|
clearFiles,
|
|
handleDragEnter,
|
|
handleDragLeave,
|
|
handleDragOver,
|
|
handleDrop,
|
|
handlePaste,
|
|
} = useFileUpload();
|
|
|
|
const handleSend = () => {
|
|
if (message.trim() || files.length > 0) {
|
|
onSend(message, files.length > 0 ? files : undefined);
|
|
setMessage('');
|
|
clearFiles();
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
// 检查是否正在使用输入法组合(如中文输入法选词时按回车)
|
|
// isComposing 为 true 时,表示用户正在用输入法选词,此时不应发送消息
|
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
e.preventDefault();
|
|
handleSend();
|
|
}
|
|
};
|
|
|
|
// 处理文件选择
|
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const selectedFiles = e.target.files;
|
|
if (selectedFiles && selectedFiles.length > 0) {
|
|
addFiles(selectedFiles);
|
|
}
|
|
// 重置 input 以允许重复选择同一文件
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
// 打开文件选择对话框
|
|
const openFileDialog = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
return (
|
|
<div className={cn('w-full max-w-[var(--input-max-width)] mx-auto', className)}>
|
|
<div
|
|
className={cn(
|
|
'relative flex flex-col bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md p-4 shadow-[var(--shadow-input)]',
|
|
'transition-all duration-150',
|
|
'focus-within:border-[var(--color-border-focus)] focus-within:shadow-[var(--shadow-input-focus)]',
|
|
isDragging && 'border-[var(--color-primary)] border-2 bg-[var(--color-primary)]/5'
|
|
)}
|
|
onDragEnter={handleDragEnter}
|
|
onDragLeave={handleDragLeave}
|
|
onDragOver={handleDragOver}
|
|
onDrop={handleDrop}
|
|
>
|
|
{/* 拖拽覆盖层 */}
|
|
{isDragging && (
|
|
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-[var(--color-bg-primary)]/90 rounded-md border-2 border-dashed border-[var(--color-primary)]">
|
|
<Upload className="w-8 h-8 text-[var(--color-primary)] mb-2" />
|
|
<p className="text-sm font-medium text-[var(--color-primary)]">
|
|
释放以添加文件
|
|
</p>
|
|
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">
|
|
支持图片、PDF、文档等格式
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 文件预览区域 */}
|
|
{files.length > 0 && (
|
|
<div className="mb-3">
|
|
<FilePreviewList files={files} onRemove={removeFile} />
|
|
</div>
|
|
)}
|
|
|
|
{/* 第一行:输入区域 */}
|
|
<div className="w-full mb-3">
|
|
<input
|
|
type="text"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
onPaste={handlePaste}
|
|
placeholder={files.length > 0 ? '添加描述(可选)...' : placeholder}
|
|
className="w-full border-none outline-none text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]"
|
|
/>
|
|
</div>
|
|
|
|
{/* 第二行:功能按钮区域 */}
|
|
<div className="flex items-center justify-between w-full">
|
|
{/* 左侧按钮 */}
|
|
<div className="flex items-center gap-1">
|
|
{/* 添加附件 */}
|
|
<button
|
|
onClick={openFileDialog}
|
|
className={cn(
|
|
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors',
|
|
'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]',
|
|
files.length > 0 && 'text-[var(--color-primary)]'
|
|
)}
|
|
title="添加附件"
|
|
>
|
|
<Plus size={20} />
|
|
</button>
|
|
|
|
{/* 隐藏的文件输入 */}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple
|
|
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.md,.json,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.go,.rs,.html,.css"
|
|
onChange={handleFileSelect}
|
|
className="hidden"
|
|
/>
|
|
|
|
{/* 工具下拉 */}
|
|
<ToolsDropdown
|
|
tools={tools}
|
|
onToolToggle={onToolToggle}
|
|
onEnableAllToggle={onEnableAllTools}
|
|
/>
|
|
</div>
|
|
|
|
{/* 右侧按钮 */}
|
|
<div className="flex items-center gap-3">
|
|
{/* 模型选择器 */}
|
|
<ModelSelector
|
|
models={models}
|
|
selectedModel={selectedModel}
|
|
onSelect={onModelSelect}
|
|
/>
|
|
|
|
{/* 发送按钮 */}
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={!message.trim() && files.length === 0}
|
|
className={cn(
|
|
'w-[38px] h-[38px] flex items-center justify-center bg-[var(--color-primary)] text-white rounded-xl transition-all duration-150',
|
|
message.trim() || files.length > 0
|
|
? 'hover:bg-[var(--color-primary-hover)] hover:-translate-y-0.5'
|
|
: 'opacity-50 cursor-not-allowed'
|
|
)}
|
|
title="Send message"
|
|
>
|
|
<ArrowUp size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 提示文字 */}
|
|
<p className="text-xs text-[var(--color-text-tertiary)] text-center mt-2">
|
|
可拖拽文件到此处或使用 Ctrl+V 粘贴图片
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|