diff --git a/src/components/features/ChatInput.tsx b/src/components/features/ChatInput.tsx new file mode 100644 index 0000000..27352a6 --- /dev/null +++ b/src/components/features/ChatInput.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, Clock, ArrowUp } from 'lucide-react'; +import { ModelSelector } from './ModelSelector'; +import { ToolsDropdown } from './ToolsDropdown'; +import { cn } from '@/lib/utils'; +import type { Model, Tool } from '@/types'; + +interface ChatInputProps { + models: Model[]; + selectedModel: Model; + onModelSelect: (model: Model) => void; + tools: Tool[]; + onToolToggle: (toolId: string) => void; + onEnableAllTools: (enabled: boolean) => void; + onSend: (message: string) => 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 handleSend = () => { + if (message.trim()) { + onSend(message); + setMessage(''); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( +
+
+ {/* 第一行:输入区域 */} +
+ setMessage(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + className="w-full border-none outline-none text-base text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]" + /> +
+ + {/* 第二行:功能按钮区域 */} +
+ {/* 左侧按钮 */} +
+ {/* 添加附件 */} + + + {/* 工具下拉 */} + + + {/* 历史记录 */} + +
+ + {/* 右侧按钮 */} +
+ {/* 模型选择器 */} + + + {/* 发送按钮 */} + +
+
+
+
+ ); +} diff --git a/src/components/features/MessageBubble.tsx b/src/components/features/MessageBubble.tsx new file mode 100644 index 0000000..395b8d4 --- /dev/null +++ b/src/components/features/MessageBubble.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { Copy, ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react'; +import { Avatar } from '@/components/ui/Avatar'; +import { AILogo } from '@/components/ui/AILogo'; +import { cn } from '@/lib/utils'; +import type { Message, User } from '@/types'; + +interface MessageBubbleProps { + message: Message; + user?: User; +} + +export function MessageBubble({ message, user }: MessageBubbleProps) { + const isUser = message.role === 'user'; + + // 简单的 Markdown 渲染 + const renderContent = (content: string) => { + // 处理代码块 + const parts = content.split(/(`[^`]+`)/g); + return parts.map((part, index) => { + if (part.startsWith('`') && part.endsWith('`')) { + return ( + + {part.slice(1, -1)} + + ); + } + // 处理粗体 + const boldParts = part.split(/(\*\*[^*]+\*\*)/g); + return boldParts.map((boldPart, boldIndex) => { + if (boldPart.startsWith('**') && boldPart.endsWith('**')) { + return ( + + {boldPart.slice(2, -2)} + + ); + } + return {boldPart}; + }); + }); + }; + + if (isUser) { + return ( +
+
+
+ {message.content} +
+
+ {user && } +
+ ); + } + + return ( +
+ {/* AI 图标 */} +
+ +
+ + {/* 消息内容 */} +
+
+
+ {message.content.split('\n\n').map((paragraph, index) => { + // 处理列表 + if (paragraph.startsWith('- ') || paragraph.startsWith('* ')) { + const items = paragraph.split('\n'); + return ( +
    + {items.map((item, i) => ( +
  • + {renderContent(item.replace(/^[-*]\s/, ''))} +
  • + ))} +
+ ); + } + // 处理有序列表 + if (/^\d+\.\s/.test(paragraph)) { + const items = paragraph.split('\n'); + return ( +
    + {items.map((item, i) => ( +
  1. + {renderContent(item.replace(/^\d+\.\s/, ''))} +
  2. + ))} +
+ ); + } + return ( +

+ {renderContent(paragraph)} +

+ ); + })} +
+ + {/* 操作按钮 */} +
+ + + + +
+ + {/* 免责声明 */} +
+ cchcode can make mistakes +
+
+
+
+ ); +} + +interface ActionButtonProps { + icon: React.ComponentType<{ size?: number }>; + title: string; + onClick?: () => void; +} + +function ActionButton({ icon: Icon, title, onClick }: ActionButtonProps) { + return ( + + ); +} diff --git a/src/components/features/ModelSelector.tsx b/src/components/features/ModelSelector.tsx new file mode 100644 index 0000000..028c6d0 --- /dev/null +++ b/src/components/features/ModelSelector.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { Model } from '@/types'; + +interface ModelSelectorProps { + models: Model[]; + selectedModel: Model; + onSelect: (model: Model) => void; +} + +export function ModelSelector({ models, selectedModel, onSelect }: ModelSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // 点击外部关闭 + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ {/* 触发按钮 */} + + + {/* 下拉菜单 */} +
+ {models.map((model) => { + const isSelected = model.id === selectedModel.id; + return ( + + ); + })} +
+
+ ); +} diff --git a/src/components/features/QuickActions.tsx b/src/components/features/QuickActions.tsx new file mode 100644 index 0000000..4a8b63b --- /dev/null +++ b/src/components/features/QuickActions.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { Code, PenTool, GraduationCap, Home, Sparkles } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { QuickAction } from '@/types'; + +const iconMap: Record> = { + Code, + PenTool, + GraduationCap, + Home, + Sparkles, +}; + +interface QuickActionsProps { + actions: QuickAction[]; + onSelect: (action: QuickAction) => void; + className?: string; +} + +export function QuickActions({ actions, onSelect, className }: QuickActionsProps) { + return ( +
+ {actions.map((action) => { + const Icon = iconMap[action.icon]; + return ( + + ); + })} +
+ ); +} diff --git a/src/components/features/ToolsDropdown.tsx b/src/components/features/ToolsDropdown.tsx new file mode 100644 index 0000000..d9c0296 --- /dev/null +++ b/src/components/features/ToolsDropdown.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { Wrench, Search, Terminal, Globe, Check } from 'lucide-react'; +import { Toggle } from '@/components/ui/Toggle'; +import { cn } from '@/lib/utils'; +import type { Tool } from '@/types'; + +const iconMap: Record> = { + Search, + Terminal, + Globe, +}; + +interface ToolsDropdownProps { + tools: Tool[]; + onToolToggle: (toolId: string) => void; + onEnableAllToggle: (enabled: boolean) => void; +} + +export function ToolsDropdown({ tools, onToolToggle, onEnableAllToggle }: ToolsDropdownProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const enabledCount = tools.filter((t) => t.enabled).length; + const allEnabled = enabledCount > 0; + + // 点击外部关闭 + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ {/* 触发按钮 */} +
+ + {enabledCount > 0 && ( + + {enabledCount} + + )} +
+ + {/* 下拉菜单 */} +
+ {/* Header */} +
+ Enable Tools + +
+ + {/* 工具列表 */} + {tools.map((tool) => { + const Icon = iconMap[tool.icon]; + return ( + + ); + })} +
+
+ ); +} diff --git a/src/components/features/Welcome.tsx b/src/components/features/Welcome.tsx new file mode 100644 index 0000000..e3b2b30 --- /dev/null +++ b/src/components/features/Welcome.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { AILogo } from '@/components/ui/AILogo'; +import { cn } from '@/lib/utils'; + +interface WelcomeProps { + greeting: string; + className?: string; +} + +export function Welcome({ greeting, className }: WelcomeProps) { + return ( +
+ {/* 装饰图标 */} +
+ +
+ + {/* 问候语 */} +

+ {greeting} +

+
+ ); +}