feat: Claude Web UI POC with streaming and tool visualization
Initial implementation of a web-based Claude Code interface with: Backend (Node.js + Express + WebSocket): - Claude CLI spawning with JSON stream mode - Session management with resume support (--continue flag) - Session history API endpoint - Real-time WebSocket communication - --include-partial-messages for live streaming Frontend (React + Vite + Tailwind): - Modern dark theme UI (Discord/Slack style) - Live text streaming with content_block_delta handling - Markdown rendering with react-markdown + remark-gfm - Syntax highlighting with react-syntax-highlighter (One Dark) - Collapsible high-tech tool cards with: - Tool-specific icons and colors - Compact summaries (Read, Glob, Bash, Edit, etc.) - Expandable JSON details - Session history loading on resume - Project directory selection - Resume session toggle Docker: - Multi-container setup (backend + nginx frontend) - Isolated Claude config directory - Host network mode for backend Built collaboratively by Neko (VPS Claude) and Web-UI Claude, with Web-UI Claude implementing most frontend features while running inside the interface itself (meta-programming!). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
180
frontend/src/components/Sidebar.jsx
Normal file
180
frontend/src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Play, Square, Trash2, FolderOpen, ChevronRight, Settings } from 'lucide-react';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://100.105.142.13:3001';
|
||||
|
||||
export function Sidebar({
|
||||
open,
|
||||
onToggle,
|
||||
selectedProject,
|
||||
onSelectProject,
|
||||
sessionActive,
|
||||
onStartSession,
|
||||
onStopSession,
|
||||
onClearMessages,
|
||||
resumeSession,
|
||||
onToggleResume
|
||||
}) {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [customPath, setCustomPath] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/api/projects`)
|
||||
.then(res => res.json())
|
||||
.then(setProjects)
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const handleCustomPath = () => {
|
||||
if (customPath.trim()) {
|
||||
onSelectProject(customPath.trim());
|
||||
setCustomPath('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`
|
||||
${open ? 'w-72' : 'w-0'}
|
||||
bg-dark-900 border-r border-dark-800 flex flex-col
|
||||
transition-all duration-300 overflow-hidden
|
||||
lg:relative fixed inset-y-0 left-0 z-40
|
||||
`}
|
||||
>
|
||||
<div className="p-4 border-b border-dark-800">
|
||||
<h2 className="font-semibold text-dark-200 flex items-center gap-2">
|
||||
<Settings className="w-4 h-4" />
|
||||
Session Control
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
{/* Project Selection */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-dark-400 uppercase tracking-wide">
|
||||
Working Directory
|
||||
</h3>
|
||||
|
||||
<div className="space-y-1">
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
key={project.path}
|
||||
onClick={() => onSelectProject(project.path)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left
|
||||
transition-colors text-sm
|
||||
${selectedProject === project.path
|
||||
? 'bg-orange-500/20 text-orange-400 border border-orange-500/30'
|
||||
: 'hover:bg-dark-800 text-dark-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<FolderOpen className="w-4 h-4 flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium truncate">{project.name}</div>
|
||||
<div className="text-xs text-dark-500 truncate">{project.path}</div>
|
||||
</div>
|
||||
{selectedProject === project.path && (
|
||||
<ChevronRight className="w-4 h-4 ml-auto flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Custom path input */}
|
||||
<div className="pt-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={customPath}
|
||||
onChange={(e) => setCustomPath(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCustomPath()}
|
||||
placeholder="Custom path..."
|
||||
className="flex-1 bg-dark-800 border border-dark-700 rounded-lg px-3 py-2
|
||||
text-sm text-dark-200 placeholder-dark-500
|
||||
focus:outline-none focus:border-orange-500/50"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCustomPath}
|
||||
className="px-3 py-2 bg-dark-800 hover:bg-dark-700 rounded-lg
|
||||
text-dark-400 hover:text-dark-200 transition-colors"
|
||||
>
|
||||
Set
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume toggle */}
|
||||
<div className="pt-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer group">
|
||||
<div
|
||||
onClick={onToggleResume}
|
||||
className={`
|
||||
relative w-10 h-5 rounded-full transition-colors
|
||||
${resumeSession ? 'bg-orange-600' : 'bg-dark-700'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform
|
||||
${resumeSession ? 'translate-x-5' : 'translate-x-0.5'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-dark-300 group-hover:text-dark-200">
|
||||
Resume previous session
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Session Actions */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-dark-400 uppercase tracking-wide">
|
||||
Actions
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
{!sessionActive ? (
|
||||
<button
|
||||
onClick={onStartSession}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3
|
||||
bg-green-600 hover:bg-green-500 rounded-lg
|
||||
font-medium transition-colors"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
Start Session
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onStopSession}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3
|
||||
bg-red-600 hover:bg-red-500 rounded-lg
|
||||
font-medium transition-colors"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
Stop Session
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClearMessages}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2
|
||||
bg-dark-800 hover:bg-dark-700 rounded-lg
|
||||
text-dark-300 hover:text-dark-100 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Clear Messages
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-dark-800 text-xs text-dark-500">
|
||||
<div>Claude Code Web UI POC</div>
|
||||
<div>JSON Stream Mode</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user