feat(ui): 创建共享 UI 组件包

将 web 和 desktop 的重复代码抽取到 @ai-assistant/ui 包:

- 添加可配置的 API 客户端 (configureApiClient)
- 迁移共享组件: ChatMessage, ChatInput, Sidebar, FileBrowser, ConfigPanel
- 迁移共享 hook: useChat
- 添加 responsive prop 支持响应式布局
- 更新 web/desktop 依赖并删除重复代码
This commit is contained in:
2025-12-12 15:52:53 +08:00
parent 563224fa73
commit 68ab6a2016
30 changed files with 711 additions and 1388 deletions
+118
View File
@@ -0,0 +1,118 @@
/**
* Chat Input Component
*
* 支持响应式:responsive=true 时适配移动端键盘和触摸操作
*/
import { useState, useRef, useEffect } from 'react';
import { Send, Square } from 'lucide-react';
import clsx from 'clsx';
interface ChatInputProps {
onSend: (content: string) => void;
onCancel: () => void;
isLoading: boolean;
disabled?: boolean;
/** 是否启用响应式布局(移动端适配) */
responsive?: boolean;
}
export function ChatInput({
onSend,
onCancel,
isLoading,
disabled,
responsive = false,
}: ChatInputProps) {
const [input, setInput] = useState('');
const textareaRef = useRef<HTMLTextAreaElement>(null);
// 自动调整高度
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
// 响应式模式下移动端最大高度稍小
const maxHeight = responsive && window.innerWidth < 768 ? 120 : 200;
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
}
}, [input, responsive]);
const handleSubmit = () => {
const trimmed = input.trim();
if (!trimmed || isLoading || disabled) return;
onSend(trimmed);
setInput('');
// 重置高度
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
// Enter 发送,Shift+Enter 换行
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
return (
<div
className={clsx(
'border-t border-gray-700 bg-gray-900',
responsive ? 'p-3 md:p-4 safe-area-pb' : 'p-4'
)}
>
<div className="max-w-4xl mx-auto flex gap-2">
<div className="flex-1 relative">
<textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
responsive
? 'Type a message...'
: 'Type a message... (Shift+Enter for new line)'
}
disabled={disabled}
rows={1}
className={clsx(
'w-full resize-none rounded-lg border border-gray-600 bg-gray-800',
responsive ? 'px-3 py-2.5 md:px-4 md:py-3' : 'px-4 py-3',
responsive ? 'text-base md:text-sm' : 'text-sm', // 移动端使用 16px 防止缩放
'text-gray-100 placeholder-gray-500',
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
/>
</div>
<button
onClick={isLoading ? onCancel : handleSubmit}
disabled={!isLoading && (!input.trim() || disabled)}
className={clsx(
'rounded-lg flex items-center justify-center transition-colors',
responsive
? 'px-3 py-2.5 md:px-4 md:py-3 min-w-[44px] min-h-[44px]' // 最小触摸目标 44x44
: 'px-4 py-3',
isLoading
? 'bg-red-600 hover:bg-red-700 active:bg-red-800 text-white'
: 'bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{isLoading ? <Square size={20} /> : <Send size={20} />}
</button>
</div>
{/* 响应式模式下桌面端显示提示文字 */}
{responsive && (
<p className="hidden md:block text-xs text-gray-500 text-center mt-2">
Press Enter to send, Shift+Enter for new line
</p>
)}
</div>
);
}