Skip to content

Commit b7d4bc2

Browse files
committed
feat: add project selector to chat input toolbar, simplify project page
Move project selection from a duplicated chat input on the Project Page into a folder icon dropdown in the main chat toolbar. Simplify the Project Page to show a "New chat" button instead. Collapse project chats in the sidebar by default, expanding only when active. Add date subtitles and separators to the project chat list. Remove Files section.
1 parent 880f663 commit b7d4bc2

4 files changed

Lines changed: 197 additions & 194 deletions

File tree

frontend/src/components/Sidebar.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,18 @@ export function Sidebar({
106106
window.dispatchEvent(new Event("newchat"));
107107
document.getElementById("message")?.focus();
108108
} else if (location.pathname === "/") {
109-
// Already on home with no conversation_id, just focus
109+
// Already on home with no conversation_id — reset state (clears project selector etc.)
110+
window.dispatchEvent(new Event("newchat"));
110111
document.getElementById("message")?.focus();
111112
} else {
112113
try {
113114
// Navigate to home without any query params
114115
await router.navigate({ to: `/` });
115-
// Ensure element is available after navigation
116-
setTimeout(() => document.getElementById("message")?.focus(), 0);
116+
// Reset state after navigation
117+
setTimeout(() => {
118+
window.dispatchEvent(new Event("newchat"));
119+
document.getElementById("message")?.focus();
120+
}, 0);
117121
} catch (error) {
118122
console.error("Navigation failed:", error);
119123
}

frontend/src/components/UnifiedChat.tsx

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ import {
3939
Maximize2,
4040
Minimize2,
4141
Volume2,
42-
Square
42+
Square,
43+
Folder,
44+
ChevronDown
4345
} from "lucide-react";
4446
import RecordRTC from "recordrtc";
4547
import { useQueryClient } from "@tanstack/react-query";
@@ -69,9 +71,11 @@ import {
6971
DropdownMenu,
7072
DropdownMenuContent,
7173
DropdownMenuItem,
74+
DropdownMenuSeparator,
7275
DropdownMenuTrigger
7376
} from "@/components/ui/dropdown-menu";
7477
import { isTauri } from "@/utils/platform";
78+
import { useProjects } from "@/state/useProjects";
7579
import type {
7680
InputTextContent,
7781
OutputTextContent,
@@ -875,6 +879,7 @@ export function UnifiedChat() {
875879
const isTauriEnv = isTauri();
876880
const queryClient = useQueryClient();
877881
const { playbackError, clearPlaybackError } = useTTS();
882+
const { projects } = useProjects();
878883

879884
// Track chatId from URL - use state so we can update it
880885
const [chatId, setChatId] = useState<string | undefined>(() => {
@@ -926,6 +931,16 @@ export function UnifiedChat() {
926931
return localStorage.getItem("webSearchEnabled") === "true";
927932
});
928933

934+
// Project selector for new chats
935+
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(() => {
936+
const params = new URLSearchParams(window.location.search);
937+
return params.get("project_id") || null;
938+
});
939+
const selectedProject = useMemo(
940+
() => projects.find((p) => p.id === selectedProjectId) || null,
941+
[projects, selectedProjectId]
942+
);
943+
929944
// Fullscreen mode for power users - persisted in localStorage
930945
const [isFullscreen, setIsFullscreen] = useState(() => {
931946
return localStorage.getItem("chatFullscreen") === "true";
@@ -1144,6 +1159,16 @@ export function UnifiedChat() {
11441159
clearAllAttachments();
11451160
// Reset scroll tracking
11461161
prevMessageCountRef.current = 0;
1162+
// Check if navigating from a project page
1163+
const projectParam = new URLSearchParams(window.location.search).get("project_id");
1164+
setSelectedProjectId(projectParam || null);
1165+
if (projectParam) {
1166+
// Clean up the URL param
1167+
const cleaned = new URLSearchParams(window.location.search);
1168+
cleaned.delete("project_id");
1169+
const newUrl = cleaned.toString() ? `/?${cleaned.toString()}` : "/";
1170+
window.history.replaceState(null, "", newUrl);
1171+
}
11471172
};
11481173

11491174
// Handle conversation selection from sidebar
@@ -2347,15 +2372,11 @@ export function UnifiedChat() {
23472372
// Trigger sidebar refresh to show the new conversation
23482373
window.dispatchEvent(new Event("conversationcreated"));
23492374

2350-
// Auto-assign to project if project_id param is present
2351-
const projectParam = new URLSearchParams(window.location.search).get("project_id");
2352-
if (projectParam) {
2375+
// Auto-assign to project if one is selected
2376+
if (selectedProjectId) {
23532377
window.dispatchEvent(new CustomEvent("assignchattoproject", {
2354-
detail: { chatId: conversationId, projectId: projectParam }
2378+
detail: { chatId: conversationId, projectId: selectedProjectId }
23552379
}));
2356-
const cleaned = new URLSearchParams(window.location.search);
2357-
cleaned.delete("project_id");
2358-
window.history.replaceState(null, "", `${window.location.pathname}?${cleaned.toString()}`);
23592380
}
23602381
}
23612382

@@ -3011,6 +3032,55 @@ export function UnifiedChat() {
30113032
</DropdownMenuItem>
30123033
</DropdownMenuContent>
30133034
</DropdownMenu>
3035+
3036+
{/* Project selector — only for new chats */}
3037+
{!chatId && (
3038+
<DropdownMenu>
3039+
<DropdownMenuTrigger asChild>
3040+
<Button
3041+
type="button"
3042+
variant="ghost"
3043+
size="sm"
3044+
className={`p-0 ${selectedProject ? "h-8 px-2 gap-1.5" : "h-8 w-8"}`}
3045+
>
3046+
<Folder
3047+
className={`h-4 w-4 flex-shrink-0 ${
3048+
selectedProject ? "text-purple-500" : "text-muted-foreground"
3049+
}`}
3050+
/>
3051+
{selectedProject && (
3052+
<>
3053+
<span className="text-xs truncate max-w-[120px]">
3054+
{selectedProject.name}
3055+
</span>
3056+
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
3057+
</>
3058+
)}
3059+
</Button>
3060+
</DropdownMenuTrigger>
3061+
<DropdownMenuContent align="start">
3062+
<DropdownMenuItem onClick={() => setSelectedProjectId(null)}>
3063+
<Check
3064+
className={`mr-2 h-4 w-4 ${!selectedProjectId ? "opacity-100" : "opacity-0"}`}
3065+
/>
3066+
<span>No project</span>
3067+
</DropdownMenuItem>
3068+
{projects.length > 0 && <DropdownMenuSeparator />}
3069+
{projects.map((project) => (
3070+
<DropdownMenuItem
3071+
key={project.id}
3072+
onClick={() => setSelectedProjectId(project.id)}
3073+
>
3074+
<Check
3075+
className={`mr-2 h-4 w-4 ${selectedProjectId === project.id ? "opacity-100" : "opacity-0"}`}
3076+
/>
3077+
<Folder className="mr-2 h-4 w-4 flex-shrink-0" />
3078+
<span className="truncate">{project.name}</span>
3079+
</DropdownMenuItem>
3080+
))}
3081+
</DropdownMenuContent>
3082+
</DropdownMenu>
3083+
)}
30143084
</div>
30153085

30163086
<div className="flex items-center gap-2">
@@ -3254,6 +3324,55 @@ export function UnifiedChat() {
32543324
</DropdownMenuItem>
32553325
</DropdownMenuContent>
32563326
</DropdownMenu>
3327+
3328+
{/* Project selector — only for new chats */}
3329+
{!chatId && (
3330+
<DropdownMenu>
3331+
<DropdownMenuTrigger asChild>
3332+
<Button
3333+
type="button"
3334+
variant="ghost"
3335+
size="sm"
3336+
className={`p-0 ${selectedProject ? "h-8 px-2 gap-1.5" : "h-8 w-8"}`}
3337+
>
3338+
<Folder
3339+
className={`h-4 w-4 flex-shrink-0 ${
3340+
selectedProject ? "text-purple-500" : "text-muted-foreground"
3341+
}`}
3342+
/>
3343+
{selectedProject && (
3344+
<>
3345+
<span className="text-xs truncate max-w-[120px]">
3346+
{selectedProject.name}
3347+
</span>
3348+
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
3349+
</>
3350+
)}
3351+
</Button>
3352+
</DropdownMenuTrigger>
3353+
<DropdownMenuContent align="start">
3354+
<DropdownMenuItem onClick={() => setSelectedProjectId(null)}>
3355+
<Check
3356+
className={`mr-2 h-4 w-4 ${!selectedProjectId ? "opacity-100" : "opacity-0"}`}
3357+
/>
3358+
<span>No project</span>
3359+
</DropdownMenuItem>
3360+
{projects.length > 0 && <DropdownMenuSeparator />}
3361+
{projects.map((project) => (
3362+
<DropdownMenuItem
3363+
key={project.id}
3364+
onClick={() => setSelectedProjectId(project.id)}
3365+
>
3366+
<Check
3367+
className={`mr-2 h-4 w-4 ${selectedProjectId === project.id ? "opacity-100" : "opacity-0"}`}
3368+
/>
3369+
<Folder className="mr-2 h-4 w-4 flex-shrink-0" />
3370+
<span className="truncate">{project.name}</span>
3371+
</DropdownMenuItem>
3372+
))}
3373+
</DropdownMenuContent>
3374+
</DropdownMenu>
3375+
)}
32573376
</div>
32583377

32593378
<div className="flex items-center gap-2">

0 commit comments

Comments
 (0)