mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-12 13:19:43 +00:00
Added stared project and ux enahncements
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-ui",
|
"name": "claude-code-ui",
|
||||||
"version": "1.1.4",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-code-ui",
|
"name": "claude-code-ui",
|
||||||
"version": "1.1.4",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-code": "^1.0.24",
|
"@anthropic-ai/claude-code": "^1.0.24",
|
||||||
|
|||||||
@@ -1597,6 +1597,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
}
|
}
|
||||||
}, []); // Only run once on mount
|
}, []); // Only run once on mount
|
||||||
|
|
||||||
|
// Reset textarea height when input is cleared programmatically
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current && !input.trim()) {
|
||||||
|
textareaRef.current.style.height = 'auto';
|
||||||
|
setIsTextareaExpanded(false);
|
||||||
|
}
|
||||||
|
}, [input]);
|
||||||
|
|
||||||
const handleTranscript = useCallback((text) => {
|
const handleTranscript = useCallback((text) => {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
setInput(prevInput => {
|
setInput(prevInput => {
|
||||||
@@ -1689,6 +1697,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
setInput('');
|
setInput('');
|
||||||
setIsTextareaExpanded(false);
|
setIsTextareaExpanded(false);
|
||||||
|
|
||||||
|
// Reset textarea height to minimal state
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the saved draft since message was sent
|
// Clear the saved draft since message was sent
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
localStorage.removeItem(`draft_input_${selectedProject.name}`);
|
localStorage.removeItem(`draft_input_${selectedProject.name}`);
|
||||||
@@ -1776,8 +1790,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
setInput(e.target.value);
|
const newValue = e.target.value;
|
||||||
|
setInput(newValue);
|
||||||
setCursorPosition(e.target.selectionStart);
|
setCursorPosition(e.target.selectionStart);
|
||||||
|
|
||||||
|
// Handle height reset when input becomes empty
|
||||||
|
if (!newValue.trim()) {
|
||||||
|
e.target.style.height = 'auto';
|
||||||
|
setIsTextareaExpanded(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTextareaClick = (e) => {
|
const handleTextareaClick = (e) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ScrollArea } from './ui/scroll-area';
|
|||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2 } from 'lucide-react';
|
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import ClaudeLogo from './ClaudeLogo';
|
import ClaudeLogo from './ClaudeLogo';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
@@ -66,6 +66,17 @@ function Sidebar({
|
|||||||
const [editingSessionName, setEditingSessionName] = useState('');
|
const [editingSessionName, setEditingSessionName] = useState('');
|
||||||
const [generatingSummary, setGeneratingSummary] = useState({});
|
const [generatingSummary, setGeneratingSummary] = useState({});
|
||||||
|
|
||||||
|
// Starred projects state - persisted in localStorage
|
||||||
|
const [starredProjects, setStarredProjects] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('starredProjects');
|
||||||
|
return saved ? new Set(JSON.parse(saved)) : new Set();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading starred projects:', error);
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Touch handler to prevent double-tap issues on iPad (only for buttons, not scroll areas)
|
// Touch handler to prevent double-tap issues on iPad (only for buttons, not scroll areas)
|
||||||
const handleTouchClick = (callback) => {
|
const handleTouchClick = (callback) => {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
@@ -124,6 +135,37 @@ function Sidebar({
|
|||||||
setExpandedProjects(newExpanded);
|
setExpandedProjects(newExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Starred projects utility functions
|
||||||
|
const toggleStarProject = (projectName) => {
|
||||||
|
const newStarred = new Set(starredProjects);
|
||||||
|
if (newStarred.has(projectName)) {
|
||||||
|
newStarred.delete(projectName);
|
||||||
|
} else {
|
||||||
|
newStarred.add(projectName);
|
||||||
|
}
|
||||||
|
setStarredProjects(newStarred);
|
||||||
|
|
||||||
|
// Persist to localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem('starredProjects', JSON.stringify([...newStarred]));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving starred projects:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isProjectStarred = (projectName) => {
|
||||||
|
return starredProjects.has(projectName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort projects to show starred ones first
|
||||||
|
const sortedProjects = [...projects].sort((a, b) => {
|
||||||
|
const aStarred = isProjectStarred(a.name);
|
||||||
|
const bStarred = isProjectStarred(b.name);
|
||||||
|
|
||||||
|
if (aStarred && !bStarred) return -1;
|
||||||
|
if (!aStarred && bStarred) return 1;
|
||||||
|
return 0; // Keep original order for same star status
|
||||||
|
});
|
||||||
|
|
||||||
const startEditing = (project) => {
|
const startEditing = (project) => {
|
||||||
setEditingProject(project.name);
|
setEditingProject(project.name);
|
||||||
@@ -499,9 +541,10 @@ function Sidebar({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map((project) => {
|
sortedProjects.map((project) => {
|
||||||
const isExpanded = expandedProjects.has(project.name);
|
const isExpanded = expandedProjects.has(project.name);
|
||||||
const isSelected = selectedProject?.name === project.name;
|
const isSelected = selectedProject?.name === project.name;
|
||||||
|
const isStarred = isProjectStarred(project.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.name} className="md:space-y-1">
|
<div key={project.name} className="md:space-y-1">
|
||||||
@@ -512,7 +555,8 @@ function Sidebar({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-3 mx-3 my-1 rounded-lg bg-card border border-border/50 active:scale-[0.98] transition-all duration-150",
|
"p-3 mx-3 my-1 rounded-lg bg-card border border-border/50 active:scale-[0.98] transition-all duration-150",
|
||||||
isSelected && "bg-primary/5 border-primary/20"
|
isSelected && "bg-primary/5 border-primary/20",
|
||||||
|
isStarred && !isSelected && "bg-yellow-50/50 dark:bg-yellow-900/5 border-yellow-200/30 dark:border-yellow-800/30"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// On mobile, just toggle the folder - don't select the project
|
// On mobile, just toggle the folder - don't select the project
|
||||||
@@ -594,6 +638,28 @@ function Sidebar({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Star button */}
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"w-8 h-8 rounded-lg flex items-center justify-center active:scale-90 transition-all duration-150 border",
|
||||||
|
isStarred
|
||||||
|
? "bg-yellow-500/10 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800"
|
||||||
|
: "bg-gray-500/10 dark:bg-gray-900/30 border-gray-200 dark:border-gray-800"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleStarProject(project.name);
|
||||||
|
}}
|
||||||
|
onTouchEnd={handleTouchClick(() => toggleStarProject(project.name))}
|
||||||
|
title={isStarred ? "Remove from favorites" : "Add to favorites"}
|
||||||
|
>
|
||||||
|
<Star className={cn(
|
||||||
|
"w-4 h-4 transition-colors",
|
||||||
|
isStarred
|
||||||
|
? "text-yellow-600 dark:text-yellow-400 fill-current"
|
||||||
|
: "text-gray-600 dark:text-gray-400"
|
||||||
|
)} />
|
||||||
|
</button>
|
||||||
{getAllSessions(project).length === 0 && (
|
{getAllSessions(project).length === 0 && (
|
||||||
<button
|
<button
|
||||||
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
|
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
|
||||||
@@ -635,7 +701,8 @@ function Sidebar({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden md:flex w-full justify-between p-2 h-auto font-normal hover:bg-accent/50",
|
"hidden md:flex w-full justify-between p-2 h-auto font-normal hover:bg-accent/50",
|
||||||
isSelected && "bg-accent text-accent-foreground"
|
isSelected && "bg-accent text-accent-foreground",
|
||||||
|
isStarred && !isSelected && "bg-yellow-50/50 dark:bg-yellow-900/10 hover:bg-yellow-100/50 dark:hover:bg-yellow-900/20"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Desktop behavior: select project and toggle
|
// Desktop behavior: select project and toggle
|
||||||
@@ -722,6 +789,27 @@ function Sidebar({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Star button */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 flex items-center justify-center rounded cursor-pointer touch:opacity-100",
|
||||||
|
isStarred
|
||||||
|
? "hover:bg-yellow-50 dark:hover:bg-yellow-900/20 opacity-100"
|
||||||
|
: "hover:bg-accent"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleStarProject(project.name);
|
||||||
|
}}
|
||||||
|
title={isStarred ? "Remove from favorites" : "Add to favorites"}
|
||||||
|
>
|
||||||
|
<Star className={cn(
|
||||||
|
"w-3 h-3 transition-colors",
|
||||||
|
isStarred
|
||||||
|
? "text-yellow-600 dark:text-yellow-400 fill-current"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-accent flex items-center justify-center rounded cursor-pointer touch:opacity-100"
|
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-accent flex items-center justify-center rounded cursor-pointer touch:opacity-100"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user