mukaddamzaid commited on
Commit
264f96c
·
1 Parent(s): 0d06a18

feat: enhance ChatSidebar with user ID editing functionality

Browse files

- Added a dialog for editing the user ID, allowing users to update their ID for chat synchronization.
- Implemented validation to ensure the user ID is not empty before updating.
- Introduced a new Pencil icon in the dropdown menu for accessing the edit user ID feature.
- Updated the Messages component layout for improved responsiveness.
- Refactored ToolInvocation component for better icon usage and layout adjustments.

components/chat-sidebar.tsx CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import { useState, useEffect, useRef } from "react";
4
  import { useRouter, usePathname } from "next/navigation";
5
- import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Loader2, Sparkles, ChevronsUpDown, UserIcon, Copy } from "lucide-react";
6
  import {
7
  Sidebar,
8
  SidebarContent,
@@ -24,7 +24,7 @@ import { toast } from "sonner";
24
  import Image from "next/image";
25
  import { MCPServerManager, type MCPServer } from "./mcp-server-manager";
26
  import { ThemeToggle } from "./theme-toggle";
27
- import { getUserId } from "@/lib/user-id";
28
  import { useLocalStorage } from "@/lib/hooks/use-local-storage";
29
  import { STORAGE_KEYS } from "@/lib/constants";
30
  import { useChats } from "@/lib/hooks/use-chats";
@@ -40,6 +40,16 @@ import {
40
  DropdownMenuTrigger,
41
  } from "@/components/ui/dropdown-menu";
42
  import { Avatar, AvatarFallback } from "@/components/ui/avatar";
 
 
 
 
 
 
 
 
 
 
43
 
44
  export function ChatSidebar() {
45
  const router = useRouter();
@@ -50,6 +60,8 @@ export function ChatSidebar() {
50
  const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false);
51
  const { state } = useSidebar();
52
  const isCollapsed = state === "collapsed";
 
 
53
 
54
  // Initialize userId
55
  useEffect(() => {
@@ -80,6 +92,22 @@ export function ChatSidebar() {
80
  // Get active MCP servers status
81
  const activeServersCount = selectedMcpServers.length;
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  // Show loading state if user ID is not yet initialized
84
  if (!userId) {
85
  return null; // Or a loading spinner
@@ -307,6 +335,13 @@ export function ChatSidebar() {
307
  <Copy className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
308
  Copy User ID
309
  </DropdownMenuItem>
 
 
 
 
 
 
 
310
  </DropdownMenuGroup>
311
  <DropdownMenuSeparator />
312
  <DropdownMenuGroup>
@@ -340,6 +375,44 @@ export function ChatSidebar() {
340
  onOpenChange={setMcpSettingsOpen}
341
  />
342
  </SidebarFooter>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  </Sidebar>
344
  );
345
  }
 
2
 
3
  import { useState, useEffect, useRef } from "react";
4
  import { useRouter, usePathname } from "next/navigation";
5
+ import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Loader2, Sparkles, ChevronsUpDown, UserIcon, Copy, Pencil } from "lucide-react";
6
  import {
7
  Sidebar,
8
  SidebarContent,
 
24
  import Image from "next/image";
25
  import { MCPServerManager, type MCPServer } from "./mcp-server-manager";
26
  import { ThemeToggle } from "./theme-toggle";
27
+ import { getUserId, updateUserId } from "@/lib/user-id";
28
  import { useLocalStorage } from "@/lib/hooks/use-local-storage";
29
  import { STORAGE_KEYS } from "@/lib/constants";
30
  import { useChats } from "@/lib/hooks/use-chats";
 
40
  DropdownMenuTrigger,
41
  } from "@/components/ui/dropdown-menu";
42
  import { Avatar, AvatarFallback } from "@/components/ui/avatar";
43
+ import {
44
+ Dialog,
45
+ DialogContent,
46
+ DialogDescription,
47
+ DialogFooter,
48
+ DialogHeader,
49
+ DialogTitle,
50
+ } from "@/components/ui/dialog";
51
+ import { Input } from "@/components/ui/input";
52
+ import { Label } from "@/components/ui/label";
53
 
54
  export function ChatSidebar() {
55
  const router = useRouter();
 
60
  const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false);
61
  const { state } = useSidebar();
62
  const isCollapsed = state === "collapsed";
63
+ const [editUserIdOpen, setEditUserIdOpen] = useState(false);
64
+ const [newUserId, setNewUserId] = useState('');
65
 
66
  // Initialize userId
67
  useEffect(() => {
 
92
  // Get active MCP servers status
93
  const activeServersCount = selectedMcpServers.length;
94
 
95
+ // Handle user ID update
96
+ const handleUpdateUserId = () => {
97
+ if (!newUserId.trim()) {
98
+ toast.error("User ID cannot be empty");
99
+ return;
100
+ }
101
+
102
+ updateUserId(newUserId.trim());
103
+ setUserId(newUserId.trim());
104
+ setEditUserIdOpen(false);
105
+ toast.success("User ID updated successfully");
106
+
107
+ // Refresh the page to reload chats with new user ID
108
+ window.location.reload();
109
+ };
110
+
111
  // Show loading state if user ID is not yet initialized
112
  if (!userId) {
113
  return null; // Or a loading spinner
 
335
  <Copy className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
336
  Copy User ID
337
  </DropdownMenuItem>
338
+ <DropdownMenuItem onSelect={(e) => {
339
+ e.preventDefault();
340
+ setEditUserIdOpen(true);
341
+ }}>
342
+ <Pencil className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
343
+ Edit User ID
344
+ </DropdownMenuItem>
345
  </DropdownMenuGroup>
346
  <DropdownMenuSeparator />
347
  <DropdownMenuGroup>
 
375
  onOpenChange={setMcpSettingsOpen}
376
  />
377
  </SidebarFooter>
378
+
379
+ <Dialog open={editUserIdOpen} onOpenChange={(open) => {
380
+ setEditUserIdOpen(open);
381
+ if (open) {
382
+ setNewUserId(userId);
383
+ }
384
+ }}>
385
+ <DialogContent className="sm:max-w-[400px]">
386
+ <DialogHeader>
387
+ <DialogTitle>Edit User ID</DialogTitle>
388
+ <DialogDescription>
389
+ Update your user ID for chat synchronization. This will affect which chats are visible to you.
390
+ </DialogDescription>
391
+ </DialogHeader>
392
+ <div className="grid gap-4 py-4">
393
+ <div className="grid gap-2">
394
+ <Label htmlFor="userId">User ID</Label>
395
+ <Input
396
+ id="userId"
397
+ value={newUserId}
398
+ onChange={(e) => setNewUserId(e.target.value)}
399
+ placeholder="Enter your user ID"
400
+ />
401
+ </div>
402
+ </div>
403
+ <DialogFooter>
404
+ <Button
405
+ variant="outline"
406
+ onClick={() => setEditUserIdOpen(false)}
407
+ >
408
+ Cancel
409
+ </Button>
410
+ <Button onClick={handleUpdateUserId}>
411
+ Save Changes
412
+ </Button>
413
+ </DialogFooter>
414
+ </DialogContent>
415
+ </Dialog>
416
  </Sidebar>
417
  );
418
  }
components/messages.tsx CHANGED
@@ -11,13 +11,13 @@ export const Messages = ({
11
  isLoading: boolean;
12
  status: "error" | "submitted" | "streaming" | "ready";
13
  }) => {
14
- const [containerRef, endRef] = useScrollToBottom();
15
  return (
16
  <div
17
  className="h-full overflow-y-auto no-scrollbar"
18
- ref={containerRef}
19
  >
20
- <div className="max-w-xl mx-auto py-4">
21
  {messages.map((m, i) => (
22
  <Message
23
  key={i}
@@ -27,7 +27,7 @@ export const Messages = ({
27
  status={status}
28
  />
29
  ))}
30
- <div className="h-1" ref={endRef} />
31
  </div>
32
  </div>
33
  );
 
11
  isLoading: boolean;
12
  status: "error" | "submitted" | "streaming" | "ready";
13
  }) => {
14
+ // const [containerRef, endRef] = useScrollToBottom();
15
  return (
16
  <div
17
  className="h-full overflow-y-auto no-scrollbar"
18
+ // ref={containerRef}
19
  >
20
+ <div className="max-w-lg sm:max-w-3xl mx-auto py-4">
21
  {messages.map((m, i) => (
22
  <Message
23
  key={i}
 
27
  status={status}
28
  />
29
  ))}
30
+ {/* <div className="h-1" ref={endRef} /> */}
31
  </div>
32
  </div>
33
  );
components/model-picker.tsx CHANGED
@@ -109,20 +109,20 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
109
  };
110
 
111
  return (
112
- <div className="absolute bottom-2 left-2">
113
  <Select
114
  value={validModelId}
115
  onValueChange={handleModelChange}
116
  defaultValue={validModelId}
117
  >
118
  <SelectTrigger
119
- className="w-48 px-3 h-9 rounded-full group border-border/80 bg-background/80 backdrop-blur-sm hover:bg-muted/30 transition-all duration-200"
120
  >
121
  <SelectValue
122
  placeholder="Select model"
123
- className="text-xs font-medium flex items-center gap-2"
124
  >
125
- <div className="flex items-center gap-2">
126
  {getProviderIcon(modelDetails[validModelId].provider)}
127
  <TextMorph>{modelDetails[validModelId].name}</TextMorph>
128
  </div>
@@ -130,12 +130,11 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
130
  </SelectTrigger>
131
  <SelectContent
132
  align="start"
133
- className="bg-background/95 dark:bg-muted/95 backdrop-blur-sm border-border/80 rounded-lg overflow-hidden p-0"
134
- style={{ width: "485px" }}
135
  >
136
- <div className="grid grid-cols-[170px_1fr] items-start gap-x-4">
137
  {/* Model selector column */}
138
- <div className="border-r border-border/40 bg-muted/20 p-2">
139
  <SelectGroup className="space-y-1">
140
  {MODELS.map((id) => {
141
  const modelId = id as modelID;
@@ -146,7 +145,7 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
146
  onMouseEnter={() => setHoveredModel(modelId)}
147
  onMouseLeave={() => setHoveredModel(null)}
148
  className={cn(
149
- "!px-3 py-2 cursor-pointer rounded-md text-xs transition-colors duration-150",
150
  "hover:bg-primary/5 hover:text-primary-foreground",
151
  "focus:bg-primary/10 focus:text-primary focus:outline-none",
152
  "data-[highlighted]:bg-primary/10 data-[highlighted]:text-primary",
@@ -154,11 +153,11 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
154
  )}
155
  >
156
  <div className="flex flex-col gap-0.5">
157
- <div className="flex items-center gap-2">
158
  {getProviderIcon(modelDetails[modelId].provider)}
159
  <span className="font-medium truncate">{modelDetails[modelId].name}</span>
160
  </div>
161
- <span className="text-xs text-muted-foreground">
162
  {modelDetails[modelId].provider}
163
  </span>
164
  </div>
@@ -168,8 +167,8 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
168
  </SelectGroup>
169
  </div>
170
 
171
- {/* Model details column */}
172
- <div className="p-4 flex flex-col">
173
  <div>
174
  <div className="flex items-center gap-2 mb-1">
175
  {getProviderIcon(currentModelDetails.provider)}
@@ -195,12 +194,12 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
195
  ))}
196
  </div>
197
 
198
- <div className="text-xs text-foreground/90 leading-relaxed mb-3">
199
  {currentModelDetails.description}
200
  </div>
201
  </div>
202
 
203
- <div className="bg-muted/40 rounded-md p-2">
204
  <div className="text-[10px] text-muted-foreground flex justify-between items-center">
205
  <span>API Version:</span>
206
  <code className="bg-background/80 px-2 py-0.5 rounded text-[10px] font-mono">
@@ -209,6 +208,27 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
209
  </div>
210
  </div>
211
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  </div>
213
  </SelectContent>
214
  </Select>
 
109
  };
110
 
111
  return (
112
+ <div className="absolute bottom-2 left-2 z-10">
113
  <Select
114
  value={validModelId}
115
  onValueChange={handleModelChange}
116
  defaultValue={validModelId}
117
  >
118
  <SelectTrigger
119
+ className="max-w-[150px] sm:max-w-none sm:w-48 px-2 sm:px-3 h-8 sm:h-9 rounded-full group border-border/80 bg-background/80 backdrop-blur-sm hover:bg-muted/30 transition-all duration-200"
120
  >
121
  <SelectValue
122
  placeholder="Select model"
123
+ className="text-xs font-medium flex items-center gap-1 sm:gap-2"
124
  >
125
+ <div className="flex items-center gap-1 sm:gap-2">
126
  {getProviderIcon(modelDetails[validModelId].provider)}
127
  <TextMorph>{modelDetails[validModelId].name}</TextMorph>
128
  </div>
 
130
  </SelectTrigger>
131
  <SelectContent
132
  align="start"
133
+ className="bg-background/95 dark:bg-muted/95 backdrop-blur-sm border-border/80 rounded-lg overflow-hidden p-0 w-[280px] sm:w-[350px] md:w-[485px]"
 
134
  >
135
+ <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] md:grid-cols-[170px_1fr] items-start">
136
  {/* Model selector column */}
137
+ <div className="sm:border-r border-border/40 bg-muted/20 p-2">
138
  <SelectGroup className="space-y-1">
139
  {MODELS.map((id) => {
140
  const modelId = id as modelID;
 
145
  onMouseEnter={() => setHoveredModel(modelId)}
146
  onMouseLeave={() => setHoveredModel(null)}
147
  className={cn(
148
+ "!px-2 sm:!px-3 py-1.5 sm:py-2 cursor-pointer rounded-md text-xs transition-colors duration-150",
149
  "hover:bg-primary/5 hover:text-primary-foreground",
150
  "focus:bg-primary/10 focus:text-primary focus:outline-none",
151
  "data-[highlighted]:bg-primary/10 data-[highlighted]:text-primary",
 
153
  )}
154
  >
155
  <div className="flex flex-col gap-0.5">
156
+ <div className="flex items-center gap-1.5">
157
  {getProviderIcon(modelDetails[modelId].provider)}
158
  <span className="font-medium truncate">{modelDetails[modelId].name}</span>
159
  </div>
160
+ <span className="text-[10px] sm:text-xs text-muted-foreground">
161
  {modelDetails[modelId].provider}
162
  </span>
163
  </div>
 
167
  </SelectGroup>
168
  </div>
169
 
170
+ {/* Model details column - hidden on smallest screens, visible on sm+ */}
171
+ <div className="p-2 sm:p-3 md:p-4 flex flex-col sm:block hidden">
172
  <div>
173
  <div className="flex items-center gap-2 mb-1">
174
  {getProviderIcon(currentModelDetails.provider)}
 
194
  ))}
195
  </div>
196
 
197
+ <div className="text-xs text-foreground/90 leading-relaxed mb-3 hidden md:block">
198
  {currentModelDetails.description}
199
  </div>
200
  </div>
201
 
202
+ <div className="bg-muted/40 rounded-md p-2 hidden md:block">
203
  <div className="text-[10px] text-muted-foreground flex justify-between items-center">
204
  <span>API Version:</span>
205
  <code className="bg-background/80 px-2 py-0.5 rounded text-[10px] font-mono">
 
208
  </div>
209
  </div>
210
  </div>
211
+
212
+ {/* Condensed model details for mobile only */}
213
+ <div className="p-3 sm:hidden border-t border-border/30">
214
+ <div className="flex flex-wrap gap-1 mb-2">
215
+ {currentModelDetails.capabilities.slice(0, 4).map((capability) => (
216
+ <span
217
+ key={capability}
218
+ className={cn(
219
+ "inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded-full font-medium",
220
+ getCapabilityColor(capability)
221
+ )}
222
+ >
223
+ {getCapabilityIcon(capability)}
224
+ <span>{capability}</span>
225
+ </span>
226
+ ))}
227
+ {currentModelDetails.capabilities.length > 4 && (
228
+ <span className="text-[10px] text-muted-foreground">+{currentModelDetails.capabilities.length - 4} more</span>
229
+ )}
230
+ </div>
231
+ </div>
232
  </div>
233
  </SelectContent>
234
  </Select>
components/tool-invocation.tsx CHANGED
@@ -6,11 +6,11 @@ import {
6
  ChevronDownIcon,
7
  ChevronUpIcon,
8
  Loader2,
9
- PocketKnife,
10
- CheckCircle,
11
- StopCircle,
12
- Code2,
13
- Terminal,
14
  } from "lucide-react";
15
 
16
  interface ToolInvocationProps {
@@ -46,11 +46,11 @@ export function ToolInvocation({
46
  const getStatusIcon = () => {
47
  if (state === "call") {
48
  if (isLatestMessage && status !== "ready") {
49
- return <Loader2 className="animate-spin h-4 w-4 text-muted-foreground" />;
50
  }
51
- return <StopCircle className="h-4 w-4 text-destructive" />;
52
  }
53
- return <CheckCircle size={14} className="text-success" />;
54
  };
55
 
56
  const formatContent = (content: any): string => {
@@ -70,31 +70,26 @@ export function ToolInvocation({
70
  };
71
 
72
  return (
73
- <div className="flex flex-col gap-2 p-4 mb-4 bg-muted/50 rounded-xl border border-border backdrop-blur-sm">
74
- <div className="flex items-center gap-3">
75
- <div className="flex items-center justify-center w-8 h-8 bg-muted rounded-lg">
76
- <PocketKnife className="h-4 w-4" />
 
 
 
77
  </div>
78
- <div className="flex-1 flex items-center gap-2">
79
- <span className="text-sm font-medium">
80
- {state === "call" ? "Calling" : "Called"}
81
- </span>
82
- <code className="px-2 py-1 text-xs font-mono rounded-md bg-muted text-muted-foreground">
83
- {toolName}
84
- </code>
85
  </div>
86
  <div className="flex items-center gap-2">
87
  {getStatusIcon()}
88
- <button
89
- onClick={() => setIsExpanded(!isExpanded)}
90
- className="p-1 hover:bg-muted rounded-md transition-colors"
91
- >
92
- {isExpanded ? (
93
- <ChevronUpIcon className="h-4 w-4" />
94
- ) : (
95
- <ChevronDownIcon className="h-4 w-4" />
96
- )}
97
- </button>
98
  </div>
99
  </div>
100
 
@@ -106,27 +101,27 @@ export function ToolInvocation({
106
  exit="collapsed"
107
  variants={variants}
108
  transition={{ duration: 0.2 }}
109
- className="space-y-3 pt-2"
110
  >
111
  {!!args && (
112
- <div className="space-y-1.5">
113
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
114
- <Terminal className="h-3.5 w-3.5" />
115
  <span>Arguments</span>
116
  </div>
117
- <pre className="text-xs font-mono bg-muted p-3 rounded-lg overflow-x-auto">
118
  {formatContent(args)}
119
  </pre>
120
  </div>
121
  )}
122
 
123
  {!!result && (
124
- <div className="space-y-1.5">
125
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
126
- <Code2 className="h-3.5 w-3.5" />
127
  <span>Result</span>
128
  </div>
129
- <pre className="text-xs font-mono bg-muted p-3 rounded-lg overflow-x-auto max-h-[300px] overflow-y-auto">
130
  {formatContent(result)}
131
  </pre>
132
  </div>
 
6
  ChevronDownIcon,
7
  ChevronUpIcon,
8
  Loader2,
9
+ CheckCircle2,
10
+ TerminalSquare,
11
+ Code,
12
+ ArrowRight,
13
+ Circle,
14
  } from "lucide-react";
15
 
16
  interface ToolInvocationProps {
 
46
  const getStatusIcon = () => {
47
  if (state === "call") {
48
  if (isLatestMessage && status !== "ready") {
49
+ return <Loader2 className="animate-spin h-3.5 w-3.5 text-muted-foreground/70" />;
50
  }
51
+ return <Circle className="h-3.5 w-3.5 fill-destructive/20 text-destructive/70" />;
52
  }
53
+ return <CheckCircle2 size={14} className="text-success/90" />;
54
  };
55
 
56
  const formatContent = (content: any): string => {
 
70
  };
71
 
72
  return (
73
+ <div className="flex flex-col mb-3 bg-muted/30 rounded-lg border border-border/40 overflow-hidden">
74
+ <div
75
+ className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
76
+ onClick={() => setIsExpanded(!isExpanded)}
77
+ >
78
+ <div className="flex items-center justify-center w-5 h-5">
79
+ <TerminalSquare className="h-3.5 w-3.5 text-primary/70" />
80
  </div>
81
+ <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground flex-1">
82
+ <span className="text-foreground/80">{toolName}</span>
83
+ <ArrowRight className="h-3 w-3 text-muted-foreground/50" />
84
+ <span className="text-foreground/60">{state === "call" ? "Running" : "Completed"}</span>
 
 
 
85
  </div>
86
  <div className="flex items-center gap-2">
87
  {getStatusIcon()}
88
+ {isExpanded ? (
89
+ <ChevronUpIcon className="h-3.5 w-3.5 text-muted-foreground/70" />
90
+ ) : (
91
+ <ChevronDownIcon className="h-3.5 w-3.5 text-muted-foreground/70" />
92
+ )}
 
 
 
 
 
93
  </div>
94
  </div>
95
 
 
101
  exit="collapsed"
102
  variants={variants}
103
  transition={{ duration: 0.2 }}
104
+ className="space-y-2 p-3 pt-1 border-t border-border/30"
105
  >
106
  {!!args && (
107
+ <div className="space-y-1">
108
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
109
+ <Code className="h-3 w-3" />
110
  <span>Arguments</span>
111
  </div>
112
+ <pre className="text-xs font-mono bg-background/50 p-3 rounded-md overflow-x-auto">
113
  {formatContent(args)}
114
  </pre>
115
  </div>
116
  )}
117
 
118
  {!!result && (
119
+ <div className="space-y-1">
120
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
121
+ <ArrowRight className="h-3 w-3" />
122
  <span>Result</span>
123
  </div>
124
+ <pre className="text-xs font-mono bg-background/50 p-3 rounded-md overflow-x-auto max-h-[300px] overflow-y-auto">
125
  {formatContent(result)}
126
  </pre>
127
  </div>
lib/user-id.ts CHANGED
@@ -15,4 +15,9 @@ export function getUserId(): string {
15
  }
16
 
17
  return userId;
 
 
 
 
 
18
  }
 
15
  }
16
 
17
  return userId;
18
+ }
19
+
20
+ export function updateUserId(newUserId: string): void {
21
+ if (typeof window === 'undefined') return;
22
+ localStorage.setItem(USER_ID_KEY, newUserId);
23
  }