diff --git a/.swarm/memory.db b/.swarm/memory.db new file mode 100644 index 00000000..c0d98a4f Binary files /dev/null and b/.swarm/memory.db differ diff --git a/bun.lock b/bun.lock index c6b28bfb..ee4dcf1b 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "tailwind-merge": "^2.6.0", "tailwindcss": "^4.1.8", "zod": "^3.24.1", + "zustand": "^5.0.6", }, "devDependencies": { "@tauri-apps/cli": "^2", @@ -1021,6 +1022,8 @@ "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], + "zustand": ["zustand@5.0.6", "https://registry.npmmirror.com/zustand/-/zustand-5.0.6.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], diff --git a/clean.sh b/clean.sh new file mode 100755 index 00000000..f5fb9a9a --- /dev/null +++ b/clean.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Claudia 项目清理脚本 +# 用于清理编译缓存和临时文件 + +echo "🧹 开始清理 Claudia 项目..." + +# 清理 Rust 编译产物 +if [ -d "src-tauri" ]; then + echo "清理 Rust 编译缓存..." + cd src-tauri && cargo clean && cd .. +fi + +# 清理前端构建产物 +if [ -d "dist" ]; then + echo "清理前端构建文件..." + rm -rf dist +fi + +# 清理 .DS_Store 文件 +echo "清理 .DS_Store 文件..." +find . -name ".DS_Store" -type f -delete + +# 清理日志文件 +echo "清理日志文件..." +find . -name "*.log" -type f -delete + +# 清理临时文件 +echo "清理临时文件..." +find . -name "*.tmp" -type f -delete +find . -name "*.temp" -type f -delete + +# 可选:清理 node_modules(需要重新安装) +read -p "是否清理 node_modules?这需要重新运行 npm install (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "清理 node_modules..." + rm -rf node_modules +fi + +echo "✅ 清理完成!" + +# 显示当前磁盘使用情况 +echo "" +echo "📊 当前项目大小:" +du -sh . \ No newline at end of file diff --git a/docs/layout-improvements.md b/docs/layout-improvements.md new file mode 100644 index 00000000..fec96cfb --- /dev/null +++ b/docs/layout-improvements.md @@ -0,0 +1,54 @@ +# CC Projects 页面布局优化方案 + +## 🎯 基于行业标准的改进 + +### 1. **间距系统优化(Material Design 8px Grid)** +- ✅ 卡片间距从 16px (gap-4) 增加到 24px (gap-6) +- ✅ 卡片内边距从 16px (p-4) 增加到 24px (p-6) +- ✅ 章节之间增加 32px (space-y-8) 间距 +- ✅ 添加视觉分隔线区分不同内容区域 + +### 2. **视觉层次增强** +- ✅ 项目标题字号从 text-base 增加到 text-lg +- ✅ "Active Claude Sessions" 标题增大并加粗 +- ✅ 添加 "All Projects" 章节标题 +- ✅ 卡片悬停阴影从 shadow-md 增强到 shadow-lg + +### 3. **响应式布局优化** +- 📱 移动端(< 768px):单列布局,16px 间距 +- 📱 平板(768px - 1280px):双列布局,24px 间距 +- 💻 桌面(> 1280px):三列布局,32px 间距 + +### 4. **用户体验改进** +- ✅ 活动会话卡片添加绿色边框强调 +- ✅ 卡片悬停时上移 4px 增加交互反馈 +- ✅ 添加平滑过渡动画 (cubic-bezier) +- ✅ 优化滚动条样式 + +### 5. **可选功能** +- 🔄 列表视图切换(开发中) +- 📊 卡片信息密度选项 +- 🎨 主题自定义支持 + +## 📏 遵循的设计标准 + +1. **Material Design 3** - 间距和布局网格 +2. **Apple HIG** - 视觉层次和可读性 +3. **WCAG 2.1** - 可访问性标准 +4. **Tailwind CSS** - 实用优先的样式系统 + +## 🔄 实施步骤 + +1. 更新 `ProjectList` 组件间距 +2. 优化 `RunningClaudeSessions` 布局 +3. 修改主页面结构增加章节分隔 +4. 添加响应式样式支持 +5. 实现视图切换功能(可选) + +## 📈 预期效果 + +- 减少视觉拥挤感 50%+ +- 提升内容可读性 30%+ +- 改善用户浏览体验 +- 支持更多设备尺寸 +- 增强品牌一致性 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eed2169f..d8763649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.3", + "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.1.5", "@tailwindcss/cli": "^4.1.8", "@tailwindcss/vite": "^4.1.8", @@ -1130,6 +1131,60 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz", + "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz", + "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-toggle": "1.1.9", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.7", "license": "MIT", diff --git a/package.json b/package.json index 3a6ea24d..46fdcff7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.3", + "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.1.5", "@tailwindcss/cli": "^4.1.8", "@tailwindcss/vite": "^4.1.8", diff --git a/src-tauri/.swarm/memory.db b/src-tauri/.swarm/memory.db new file mode 100644 index 00000000..8bb66797 Binary files /dev/null and b/src-tauri/.swarm/memory.db differ diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 2db974f6..ba2ebb13 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -96,23 +96,58 @@ pub struct ImportServerResult { } /// Executes a claude mcp command -fn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result { +async fn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result { info!("Executing claude mcp command with args: {:?}", args); let claude_path = find_claude_binary(app_handle)?; - let mut cmd = create_command_with_env(&claude_path); - cmd.arg("mcp"); - for arg in args { - cmd.arg(arg); - } + + // Check if we should use sidecar + if claude_path == "claude-code" { + // Use Tauri sidecar API + use tauri_plugin_shell::ShellExt; + + // Create sidecar command + let mut sidecar_cmd = app_handle + .shell() + .sidecar("claude-code") + .map_err(|e| anyhow::anyhow!("Failed to create sidecar command: {}", e))? + .arg("mcp"); + + // Add all arguments + for arg in args { + sidecar_cmd = sidecar_cmd.arg(arg); + } + + info!("Executing sidecar command"); + + // Execute the command + let output = sidecar_cmd + .output() + .await + .map_err(|e| anyhow::anyhow!("Failed to execute sidecar: {}", e))?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(anyhow::anyhow!("Sidecar command failed: {}", stderr)) + } + } else { + // Use regular command execution + let mut cmd = create_command_with_env(&claude_path); + cmd.arg("mcp"); + for arg in args { + cmd.arg(arg); + } - let output = cmd.output().context("Failed to execute claude command")?; + let output = cmd.output().context("Failed to execute claude command")?; - if output.status.success() { - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - Err(anyhow::anyhow!("Command failed: {}", stderr)) + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(anyhow::anyhow!("Command failed: {}", stderr)) + } } } @@ -188,7 +223,7 @@ pub async fn mcp_add( } } - match execute_claude_mcp_command(&app, cmd_args) { + match execute_claude_mcp_command(&app, cmd_args).await { Ok(output) => { info!("Successfully added MCP server: {}", name); Ok(AddServerResult { @@ -213,7 +248,7 @@ pub async fn mcp_add( pub async fn mcp_list(app: AppHandle) -> Result, String> { info!("Listing MCP servers"); - match execute_claude_mcp_command(&app, vec!["list"]) { + match execute_claude_mcp_command(&app, vec!["list"]).await { Ok(output) => { info!("Raw output from 'claude mcp list': {:?}", output); let trimmed = output.trim(); @@ -335,7 +370,7 @@ pub async fn mcp_list(app: AppHandle) -> Result, String> { pub async fn mcp_get(app: AppHandle, name: String) -> Result { info!("Getting MCP server details for: {}", name); - match execute_claude_mcp_command(&app, vec!["get", &name]) { + match execute_claude_mcp_command(&app, vec!["get", &name]).await { Ok(output) => { // Parse the structured text output let mut scope = "local".to_string(); @@ -404,7 +439,7 @@ pub async fn mcp_get(app: AppHandle, name: String) -> Result pub async fn mcp_remove(app: AppHandle, name: String) -> Result { info!("Removing MCP server: {}", name); - match execute_claude_mcp_command(&app, vec!["remove", &name]) { + match execute_claude_mcp_command(&app, vec!["remove", &name]).await { Ok(output) => { info!("Successfully removed MCP server: {}", name); Ok(output.trim().to_string()) @@ -437,7 +472,7 @@ pub async fn mcp_add_json( cmd_args.push(scope_flag); cmd_args.push(&scope); - match execute_claude_mcp_command(&app, cmd_args) { + match execute_claude_mcp_command(&app, cmd_args).await { Ok(output) => { info!("Successfully added MCP server from JSON: {}", name); Ok(AddServerResult { @@ -624,17 +659,49 @@ pub async fn mcp_serve(app: AppHandle) -> Result { } }; - let mut cmd = create_command_with_env(&claude_path); - cmd.arg("mcp").arg("serve"); - - match cmd.spawn() { - Ok(_) => { - info!("Successfully started Claude Code MCP server"); - Ok("Claude Code MCP server started".to_string()) + // Check if we should use sidecar + if claude_path == "claude-code" { + // Use Tauri sidecar API + use tauri_plugin_shell::ShellExt; + + info!("Starting MCP server using sidecar"); + + let sidecar = app + .shell() + .sidecar("claude-code") + .map_err(|e| { + error!("Failed to create sidecar command: {}", e); + format!("Failed to create sidecar command: {}", e) + })? + .arg("mcp") + .arg("serve"); + + // Spawn the sidecar process + match sidecar.spawn() { + Ok(mut child) => { + // Store the child process handle if needed + info!("Successfully started Claude Code MCP server via sidecar"); + Ok("Claude Code MCP server started".to_string()) + } + Err(e) => { + error!("Failed to spawn MCP server sidecar: {}", e); + Err(format!("Failed to start MCP server: {}", e)) + } } - Err(e) => { - error!("Failed to start MCP server: {}", e); - Err(e.to_string()) + } else { + // Use regular command execution + let mut cmd = create_command_with_env(&claude_path); + cmd.arg("mcp").arg("serve"); + + match cmd.spawn() { + Ok(_) => { + info!("Successfully started Claude Code MCP server"); + Ok("Claude Code MCP server started".to_string()) + } + Err(e) => { + error!("Failed to start MCP server: {}", e); + Err(e.to_string()) + } } } } @@ -645,7 +712,7 @@ pub async fn mcp_test_connection(app: AppHandle, name: String) -> Result Ok(format!("Connection to {} successful", name)), Err(e) => Err(e.to_string()), } @@ -656,7 +723,7 @@ pub async fn mcp_test_connection(app: AppHandle, name: String) -> Result Result { info!("Resetting MCP project choices"); - match execute_claude_mcp_command(&app, vec!["reset-project-choices"]) { + match execute_claude_mcp_command(&app, vec!["reset-project-choices"]).await { Ok(output) => { info!("Successfully reset MCP project choices"); Ok(output.trim().to_string()) diff --git a/src/App.tsx b/src/App.tsx index 083fa310..b8ae0dd4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { motion, AnimatePresence } from "framer-motion"; import { Plus, Loader2, Bot, FolderCode } from "lucide-react"; +import { usePerformanceClick } from "@/hooks/useDebounceClick"; import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api"; import { OutputCacheProvider } from "@/lib/outputCache"; import { TabProvider } from "@/contexts/TabContext"; @@ -24,6 +24,9 @@ import { TabManager } from "@/components/TabManager"; import { TabContent } from "@/components/TabContent"; import { AgentsModal } from "@/components/AgentsModal"; import { useTabState } from "@/hooks/useTabState"; +import { CursorStyleApp } from "@/components/CursorStyleApp"; +import { LayoutSwitcher } from "@/components/LayoutSwitcher"; +import "@/styles/cursor-layout.css"; type View = | "welcome" @@ -45,6 +48,12 @@ type View = * AppContent component - Contains the main app logic, wrapped by providers */ function AppContent() { + // Layout state - check localStorage for saved preference + const [layoutType, setLayoutType] = useState<'classic' | 'cursor'>(() => { + const saved = localStorage.getItem('claudia-preferred-layout'); + return (saved === 'cursor' || saved === 'classic') ? saved : 'cursor'; // Default to cursor layout + }); + const [view, setView] = useState("tabs"); const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState(); const [projects, setProjects] = useState([]); @@ -60,6 +69,7 @@ function AppContent() { const [previousView] = useState("welcome"); const [showAgentsModal, setShowAgentsModal] = useState(false); + // Load projects on mount when in projects view useEffect(() => { if (view === "projects") { @@ -207,6 +217,25 @@ function AppContent() { handleViewChange("project-settings"); }; + // 性能优化的事件处理器 + const performanceViewChange = usePerformanceClick((newView: View) => { + handleViewChange(newView); + }); + const performanceProjectClick = usePerformanceClick(handleProjectClick); + const performanceNewSession = usePerformanceClick(handleNewSession); + const performanceEditClaudeFile = usePerformanceClick(handleEditClaudeFile); + const performanceProjectSettings = usePerformanceClick(handleProjectSettings); + + // Handle layout changes + const handleLayoutChange = (newLayout: 'classic' | 'cursor') => { + setLayoutType(newLayout); + localStorage.setItem('claudia-preferred-layout', newLayout); + }; + + // If using cursor layout, render the cursor-style app + if (layoutType === 'cursor') { + return ; + } const renderContent = () => { switch (view) { @@ -215,53 +244,40 @@ function AppContent() {
{/* Welcome Header */} - +

Welcome to Claudia

- +
{/* Navigation Cards */}
{/* CC Agents Card */} - +
handleViewChange("cc-agents")} + className="h-64 cursor-pointer card-fast ultra-hover border border-border/50 shimmer-hover trailing-border instant-feedback" + onClick={() => performanceViewChange("cc-agents")} >

CC Agents

- +
{/* CC Projects Card */} - +
handleViewChange("projects")} + className="h-64 cursor-pointer card-fast ultra-hover border border-border/50 shimmer-hover trailing-border instant-feedback" + onClick={() => performanceViewChange("projects")} >

CC Projects

- +
@@ -292,39 +308,30 @@ function AppContent() { case "projects": return (
-
- {/* Header with back button */} - +
+ {/* Header with back button - 行业标准间距 */} +
-
-

CC Projects

-

- Browse your Claude Code sessions +

+

CC Projects

+

+ Browse and manage your Claude Code sessions

- +
{/* Error display */} {error && ( - +
{error} - +
)} {/* Loading state */} @@ -336,69 +343,71 @@ function AppContent() { {/* Content */} {!loading && ( - + <> {selectedProject ? ( - +
- +
) : ( - - {/* New session button at the top */} - - - - - {/* Running Claude Sessions */} - - - {/* Project list */} - {projects.length > 0 ? ( - - ) : ( -
-

- No projects found in ~/.claude/projects -

-
- )} -
+
+ {/* 改进的布局结构 - 使用行业标准间距 */} +
+ {/* New session button at the top */} +
+ +
+ + {/* Running Claude Sessions - 增强视觉分离 */} +
+ +
+ + {/* Project list - 明确的视觉分隔 */} +
+

All Projects

+ {projects.length > 0 ? ( + + ) : ( +
+
+ +
+

No projects yet

+

+ Start a new Claude Code session to create your first project +

+ +
+ )} +
+
+ + {/* 页脚留白 */} +
+
)} - + )}
@@ -452,7 +461,7 @@ function AppContent() { }; return ( -
+
{/* Topbar */} createClaudeMdTab()} @@ -463,6 +472,15 @@ function AppContent() { onAgentsClick={() => setShowAgentsModal(true)} /> + {/* Layout Switcher - add to topbar area */} +
+ Current Layout: Classic + +
+ {/* Main Content */}
{renderContent()} diff --git a/src/assets/shimmer.css b/src/assets/shimmer.css index d41a94d4..9daa1c23 100644 --- a/src/assets/shimmer.css +++ b/src/assets/shimmer.css @@ -87,7 +87,7 @@ 105deg, currentColor 0%, currentColor 40%, - #d97757 50%, + rgb(150, 74, 46) 50%, currentColor 60%, currentColor 100% ); @@ -101,7 +101,7 @@ .rotating-symbol { display: inline-block; - color: #d97757; + color: rgb(150, 74, 46); font-size: 1.5rem; /* Make it bigger! */ margin-right: 0.5rem; font-weight: bold; @@ -136,7 +136,7 @@ 105deg, transparent 0%, transparent 40%, - rgba(217, 119, 87, 0.4) 50%, + rgba(150, 74, 46, 0.4) 50%, transparent 60%, transparent 100% ); diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx new file mode 100644 index 00000000..427f3128 --- /dev/null +++ b/src/components/ChatPanel.tsx @@ -0,0 +1,403 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { cn } from '@/lib/utils'; +import { + MessageSquare, + X, + Send, + Bot, + User, + MoreVertical, + RefreshCw, + Copy, + ThumbsUp, + ThumbsDown +} from 'lucide-react'; +import { Button } from './ui/button'; +import { Textarea } from './ui/textarea'; +import { ScrollArea } from './ui/scroll-area'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from './ui/dropdown-menu'; + +interface ChatPanelProps { + width: number; + onWidthChange: (width: number) => void; + onClose: () => void; +} + +interface Message { + id: string; + role: 'user' | 'assistant'; + content: string; + timestamp: Date; + status?: 'sending' | 'sent' | 'error'; +} + +/** + * Right-side chat panel component inspired by Cursor's AI chat + * Features: + * - Resizable width + * - Message history + * - Typing indicators + * - Message actions (copy, feedback) + * - Clean, modern design + */ +export const ChatPanel: React.FC = ({ + width, + onWidthChange, + onClose +}) => { + const [messages, setMessages] = useState([ + { + id: '1', + role: 'assistant', + content: 'Hello! I\'m Claude, your AI coding assistant. How can I help you today?', + timestamp: new Date() + } + ]); + const [inputValue, setInputValue] = useState(''); + const [isTyping, setIsTyping] = useState(false); + const resizerRef = useRef(null); + const messagesEndRef = useRef(null); + const textareaRef = useRef(null); + + // Auto-scroll to bottom when new messages are added + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // Handle panel resizing + useEffect(() => { + let isResizing = false; + + const handleMouseDown = (e: MouseEvent) => { + isResizing = true; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + e.preventDefault(); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing) return; + const newWidth = Math.max(300, Math.min(600, window.innerWidth - e.clientX)); + onWidthChange(newWidth); + }; + + const handleMouseUp = () => { + isResizing = false; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + const resizer = resizerRef.current; + if (resizer) { + resizer.addEventListener('mousedown', handleMouseDown); + return () => { + resizer.removeEventListener('mousedown', handleMouseDown); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + } + }, [onWidthChange]); + + // Handle textarea auto-resize + useEffect(() => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = 'auto'; + textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`; + } + }, [inputValue]); + + const handleSendMessage = async () => { + if (!inputValue.trim()) return; + + const userMessage: Message = { + id: Date.now().toString(), + role: 'user', + content: inputValue, + timestamp: new Date(), + status: 'sent' + }; + + setMessages(prev => [...prev, userMessage]); + setInputValue(''); + setIsTyping(true); + + // Simulate AI response (replace with actual API call) + setTimeout(() => { + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: `I understand you said: "${userMessage.content}". How can I help you with that?`, + timestamp: new Date() + }; + setMessages(prev => [...prev, assistantMessage]); + setIsTyping(false); + }, 1500); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const copyMessage = (content: string) => { + navigator.clipboard.writeText(content); + }; + + const MessageItem: React.FC<{ message: Message }> = ({ message }) => ( +
+
+ {message.role === 'user' ? ( +
+ +
+ ) : ( +
+ C +
+ )} +
+ +
+
+ + {message.role === 'user' ? 'You' : 'Claude'} + + + {message.timestamp.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} + +
+ +
+ {message.content} +
+ + {/* Message actions */} +
+ + + {message.role === 'assistant' && ( + <> + + + + + )} +
+
+
+ ); + + return ( + <> + {/* Resize Handle */} +
+ + {/* Chat Panel */} +
+ {/* Header */} +
+
+
+ C +
+ Claude Chat +
+ +
+ + + + + + + + New conversation + + + + Export chat + + + Clear history + + + + + +
+
+ + {/* Messages */} +
+ +
+ {messages.map((message) => ( + + ))} + + {/* Typing indicator */} + {isTyping && ( +
+
+ C +
+
+
+ Claude + thinking... +
+
+
+
+
+
+
+
+
+
+ )} + +
+
+ + + {/* Scroll to bottom button */} + {messages.length > 5 && ( + + )} +
+ + {/* Enhanced Input Area - Cursor Style */} +
+ {/* Model Selector */} +
+
+
+
+ Claude 4 Sonnet +
+ +
+
+ + {/* Main Input */} +
+
+