Skip to content

Commit 92cf88f

Browse files
Layout v2 (#1097)
* wip * remove unused hook * Add collapse sidebar button * refactor settings into sidebar as a override context * chat thread styling * notification dot * improved rendering * fix light mode * imporved api key page * add chats page * not found nit * guest sidebar footer * add tooltip for sidebar collapse button * nit * onboard nit * fix permission sync banner * what's new * profile settings * home view settings * general settings * remove shadow * changelog * feedback
1 parent 251f5b5 commit 92cf88f

File tree

75 files changed

+2949
-2320
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2949
-2320
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- Redesigned the app layout with a new collapsible sidebar navigation, replacing the previous top navigation bar. [#1097](https://github.com/sourcebot-dev/sourcebot/pull/1097)
12+
1013
## [4.16.8] - 2026-04-09
1114

1215
### Added

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ if (!value) { return; }
5757
if (condition) doSomething();
5858
```
5959

60+
## Conditional ClassNames
61+
62+
Use `cn()` from `@/lib/utils` for conditional classNames instead of template literal interpolation:
63+
64+
```tsx
65+
// Correct
66+
className={cn("border-b transition-colors", isActive ? "border-foreground" : "border-transparent")}
67+
68+
// Incorrect
69+
className={`border-b transition-colors ${isActive ? "border-foreground" : "border-transparent"}`}
70+
```
71+
6072
## Tailwind CSS
6173

6274
Use Tailwind color classes directly instead of CSS variable syntax:

packages/db/tools/scriptRunner.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { injectUserData } from "./scripts/inject-user-data";
77
import { confirmAction } from "./utils";
88
import { injectRepoData } from "./scripts/inject-repo-data";
99
import { testRepoQueryPerf } from "./scripts/test-repo-query-perf";
10+
import { injectChatData } from "./scripts/inject-chat-data";
1011

1112
export interface Script {
1213
run: (prisma: PrismaClient) => Promise<void>;
@@ -19,12 +20,13 @@ export const scripts: Record<string, Script> = {
1920
"inject-user-data": injectUserData,
2021
"inject-repo-data": injectRepoData,
2122
"test-repo-query-perf": testRepoQueryPerf,
23+
"inject-chat-data": injectChatData,
2224
}
2325

2426
const parser = new ArgumentParser();
2527
parser.add_argument("--url", { required: true, help: "Database URL" });
2628
parser.add_argument("--script", { required: true, help: "Script to run" });
27-
const args = parser.parse_args();
29+
const [args] = parser.parse_known_args();
2830

2931
(async () => {
3032
if (!(args.script in scripts)) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Script } from "../scriptRunner";
2+
import { PrismaClient } from "../../dist";
3+
import { confirmAction } from "../utils";
4+
5+
const chatNames = [
6+
"How does the auth middleware work?",
7+
"Explain the search indexing pipeline",
8+
"Where are API routes defined?",
9+
"How to add a new database migration",
10+
"What is the repo sync process?",
11+
"Understanding the chat architecture",
12+
"How does SSO integration work?",
13+
"Explain the permission model",
14+
"Where is the webhook handler?",
15+
"How to configure environment variables",
16+
"Understanding the billing system",
17+
"How does the worker process jobs?",
18+
"Explain the caching strategy",
19+
"Where are the shared types defined?",
20+
"How does code search ranking work?",
21+
"Understanding the notification system",
22+
"How to add a new API endpoint",
23+
"Explain the deployment pipeline",
24+
"Where is error handling centralized?",
25+
"How does real-time updates work?",
26+
"Understanding the plugin system",
27+
"How to write integration tests",
28+
"Explain the file indexing process",
29+
"Where are the email templates?",
30+
"How does rate limiting work?",
31+
"Understanding the monorepo structure",
32+
"How to add a new feature flag",
33+
"Explain the logging setup",
34+
"Where is the GraphQL schema?",
35+
"How does the sidebar component work?",
36+
];
37+
38+
export const injectChatData: Script = {
39+
run: async (prisma: PrismaClient) => {
40+
const orgId = 1;
41+
42+
const org = await prisma.org.findUnique({
43+
where: { id: orgId }
44+
});
45+
46+
if (!org) {
47+
console.error(`Organization with id ${orgId} not found.`);
48+
return;
49+
}
50+
51+
const userIdArg = process.argv.find(arg => arg.startsWith("--user-id="))?.split("=")[1];
52+
53+
const user = userIdArg
54+
? await prisma.user.findUnique({ where: { id: userIdArg } })
55+
: await prisma.user.findFirst({
56+
where: {
57+
orgs: {
58+
some: { orgId }
59+
}
60+
}
61+
});
62+
63+
if (!user) {
64+
console.error(userIdArg
65+
? `User with id "${userIdArg}" not found.`
66+
: `No user found in org ${orgId}.`
67+
);
68+
return;
69+
}
70+
71+
await confirmAction(`This will create ${chatNames.length} chats for user "${user.name ?? user.email}" in org ${orgId}.`);
72+
73+
for (const name of chatNames) {
74+
await prisma.chat.create({
75+
data: {
76+
name,
77+
orgId,
78+
createdById: user.id,
79+
messages: [],
80+
}
81+
});
82+
}
83+
84+
console.log(`Created ${chatNames.length} chats.`);
85+
}
86+
};

packages/web/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@radix-ui/react-avatar": "^1.1.2",
7171
"@radix-ui/react-checkbox": "^1.3.2",
7272
"@radix-ui/react-collapsible": "^1.1.11",
73-
"@radix-ui/react-dialog": "^1.1.4",
73+
"@radix-ui/react-dialog": "^1.1.15",
7474
"@radix-ui/react-dropdown-menu": "^2.1.1",
7575
"@radix-ui/react-hover-card": "^1.1.6",
7676
"@radix-ui/react-icons": "^1.3.0",
@@ -79,13 +79,13 @@
7979
"@radix-ui/react-popover": "^1.1.6",
8080
"@radix-ui/react-scroll-area": "^1.1.0",
8181
"@radix-ui/react-select": "^2.1.6",
82-
"@radix-ui/react-separator": "^1.1.0",
83-
"@radix-ui/react-slot": "^1.1.1",
82+
"@radix-ui/react-separator": "^1.1.8",
83+
"@radix-ui/react-slot": "^1.2.4",
8484
"@radix-ui/react-switch": "^1.2.4",
8585
"@radix-ui/react-tabs": "^1.1.2",
8686
"@radix-ui/react-toast": "^1.2.2",
8787
"@radix-ui/react-toggle": "^1.1.0",
88-
"@radix-ui/react-tooltip": "^1.1.4",
88+
"@radix-ui/react-tooltip": "^1.2.8",
8989
"@react-email/components": "^1.0.2",
9090
"@react-email/render": "^2.0.0",
9191
"@replit/codemirror-lang-csharp": "^6.2.0",
@@ -114,7 +114,7 @@
114114
"ai": "^6.0.105",
115115
"ajv": "^8.17.1",
116116
"bcryptjs": "^3.0.2",
117-
"class-variance-authority": "^0.7.0",
117+
"class-variance-authority": "^0.7.1",
118118
"client-only": "^0.0.1",
119119
"clsx": "^2.1.1",
120120
"cm6-graphql": "^0.2.0",
@@ -149,7 +149,7 @@
149149
"langfuse": "^3.38.4",
150150
"langfuse-vercel": "^3.38.4",
151151
"linguist-languages": "^9.3.1",
152-
"lucide-react": "^0.517.0",
152+
"lucide-react": "^1.7.0",
153153
"micromatch": "^4.0.8",
154154
"minidenticons": "^4.2.1",
155155
"next": "16.1.6",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { DefaultSidebar } from "../../../components/defaultSidebar";
2+
3+
export default async function Page() {
4+
return <DefaultSidebar />;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { DefaultSidebar } from "../../components/defaultSidebar";
2+
3+
export default async function Page() {
4+
return <DefaultSidebar />;
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { authenticatedPage } from "@/middleware/authenticatedPage";
2+
import { SettingsSidebar } from "../../../components/settingsSidebar";
3+
4+
export default authenticatedPage(async () => {
5+
return <SettingsSidebar />;
6+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { authenticatedPage } from "@/middleware/authenticatedPage";
2+
import { SettingsSidebar } from "../../components/settingsSidebar";
3+
4+
export default authenticatedPage(async () => {
5+
return <SettingsSidebar />;
6+
});
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"use client";
2+
3+
import { ChatActionsDropdown } from "@/app/(app)/chat/components/chatActionsDropdown";
4+
import { DeleteChatDialog } from "@/app/(app)/chat/components/deleteChatDialog";
5+
import { DuplicateChatDialog } from "@/app/(app)/chat/components/duplicateChatDialog";
6+
import { RenameChatDialog } from "@/app/(app)/chat/components/renameChatDialog";
7+
import { useToast } from "@/components/hooks/use-toast";
8+
import {
9+
SidebarGroup,
10+
SidebarGroupContent,
11+
SidebarGroupLabel,
12+
SidebarMenu,
13+
SidebarMenuAction,
14+
SidebarMenuButton,
15+
SidebarMenuItem,
16+
} from "@/components/ui/sidebar";
17+
import { deleteChat, duplicateChat, updateChatName } from "@/features/chat/actions";
18+
import { captureEvent } from "@/hooks/useCaptureEvent";
19+
import { isServiceError } from "@/lib/utils";
20+
import { EllipsisIcon, MessagesSquareIcon } from "lucide-react";
21+
import Link from "next/link";
22+
import { usePathname, useRouter } from "next/navigation";
23+
import { useCallback, useState } from "react";
24+
25+
export interface ChatHistoryItem {
26+
id: string;
27+
name: string | null;
28+
createdAt: Date;
29+
}
30+
31+
interface ChatHistoryProps {
32+
chatHistory: ChatHistoryItem[];
33+
hasMore?: boolean;
34+
}
35+
36+
export function ChatHistory({ chatHistory, hasMore }: ChatHistoryProps) {
37+
const pathname = usePathname();
38+
const router = useRouter();
39+
const { toast } = useToast();
40+
41+
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
42+
const [chatIdToRename, setChatIdToRename] = useState<string | null>(null);
43+
const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false);
44+
const [chatIdToDuplicate, setChatIdToDuplicate] = useState<string | null>(null);
45+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
46+
const [chatIdToDelete, setChatIdToDelete] = useState<string | null>(null);
47+
48+
const onRenameChat = useCallback(async (name: string, chatId: string): Promise<boolean> => {
49+
const response = await updateChatName({ chatId, name });
50+
if (isServiceError(response)) {
51+
toast({ description: `Failed to rename chat. Reason: ${response.message}` });
52+
return false;
53+
}
54+
toast({ description: "Chat renamed successfully" });
55+
captureEvent('wa_chat_renamed', { chatId });
56+
router.refresh();
57+
return true;
58+
}, [router, toast]);
59+
60+
const onDeleteChat = useCallback(async (chatIdToDelete: string): Promise<boolean> => {
61+
const response = await deleteChat({ chatId: chatIdToDelete });
62+
if (isServiceError(response)) {
63+
toast({ description: `Failed to delete chat. Reason: ${response.message}` });
64+
return false;
65+
}
66+
toast({ description: "Chat deleted successfully" });
67+
captureEvent('wa_chat_deleted', { chatId: chatIdToDelete });
68+
if (pathname === `/chat/${chatIdToDelete}`) {
69+
router.push("/chat");
70+
} else {
71+
router.refresh();
72+
}
73+
return true;
74+
}, [pathname, router, toast]);
75+
76+
const onDuplicateChat = useCallback(async (newName: string, chatIdToDuplicate: string): Promise<string | null> => {
77+
const response = await duplicateChat({ chatId: chatIdToDuplicate, newName });
78+
if (isServiceError(response)) {
79+
toast({ description: `Failed to duplicate chat. Reason: ${response.message}` });
80+
return null;
81+
}
82+
toast({ description: "Chat duplicated successfully" });
83+
captureEvent('wa_chat_duplicated', { chatId: chatIdToDuplicate });
84+
router.push(`/chat/${response.id}`);
85+
return response.id;
86+
}, [router, toast]);
87+
88+
if (chatHistory.length === 0) {
89+
return null;
90+
}
91+
92+
return (
93+
<>
94+
<SidebarGroup className="group-data-[state=collapsed]:hidden">
95+
<SidebarGroupLabel className="text-muted-foreground whitespace-nowrap">Recent Chats</SidebarGroupLabel>
96+
<SidebarGroupContent>
97+
<SidebarMenu>
98+
{chatHistory.map((chat) => (
99+
<SidebarMenuItem key={chat.id} className="group/chat">
100+
<SidebarMenuButton
101+
asChild
102+
isActive={pathname === `/chat/${chat.id}`}
103+
>
104+
<Link href={`/chat/${chat.id}`}>
105+
<span>{chat.name ?? "Untitled chat"}</span>
106+
</Link>
107+
</SidebarMenuButton>
108+
<ChatActionsDropdown
109+
onRenameClick={() => {
110+
setChatIdToRename(chat.id);
111+
setIsRenameDialogOpen(true);
112+
}}
113+
onDuplicateClick={() => {
114+
setChatIdToDuplicate(chat.id);
115+
setIsDuplicateDialogOpen(true);
116+
}}
117+
onDeleteClick={() => {
118+
setChatIdToDelete(chat.id);
119+
setIsDeleteDialogOpen(true);
120+
}}
121+
>
122+
<SidebarMenuAction showOnHover className="transition-opacity">
123+
<EllipsisIcon className="w-4 h-4" />
124+
</SidebarMenuAction>
125+
</ChatActionsDropdown>
126+
</SidebarMenuItem>
127+
))}
128+
{hasMore && (
129+
<SidebarMenuItem>
130+
<SidebarMenuButton asChild>
131+
<Link href="/chats">
132+
<MessagesSquareIcon className="h-4 w-4" />
133+
<span>All chats</span>
134+
</Link>
135+
</SidebarMenuButton>
136+
</SidebarMenuItem>
137+
)}
138+
</SidebarMenu>
139+
</SidebarGroupContent>
140+
</SidebarGroup>
141+
<RenameChatDialog
142+
isOpen={isRenameDialogOpen}
143+
onOpenChange={setIsRenameDialogOpen}
144+
onRename={async (name) => {
145+
if (chatIdToRename) {
146+
return await onRenameChat(name, chatIdToRename);
147+
}
148+
return false;
149+
}}
150+
currentName={chatHistory.find((chat) => chat.id === chatIdToRename)?.name ?? "Untitled chat"}
151+
/>
152+
<DeleteChatDialog
153+
isOpen={isDeleteDialogOpen}
154+
onOpenChange={setIsDeleteDialogOpen}
155+
onDelete={async () => {
156+
if (chatIdToDelete) {
157+
return await onDeleteChat(chatIdToDelete);
158+
}
159+
return false;
160+
}}
161+
/>
162+
<DuplicateChatDialog
163+
isOpen={isDuplicateDialogOpen}
164+
onOpenChange={setIsDuplicateDialogOpen}
165+
onDuplicate={async (newName) => {
166+
if (chatIdToDuplicate) {
167+
return await onDuplicateChat(newName, chatIdToDuplicate);
168+
}
169+
return null;
170+
}}
171+
currentName={chatHistory.find((chat) => chat.id === chatIdToDuplicate)?.name ?? "Untitled chat"}
172+
/>
173+
</>
174+
);
175+
}

0 commit comments

Comments
 (0)