Spaces:
Sleeping
Sleeping
Update web/src/components/sidebar/LeftSidebar.tsx
Browse files
web/src/components/sidebar/LeftSidebar.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
// web/src/components/sidebar/LeftSidebar.tsx
|
| 2 |
-
import React, {
|
| 3 |
import { Button } from "../ui/button";
|
| 4 |
import { Input } from "../ui/input";
|
| 5 |
import { Badge } from "../ui/badge";
|
|
@@ -7,7 +7,6 @@ import { Badge } from "../ui/badge";
|
|
| 7 |
import { SavedChatSection } from "./SavedChatSection";
|
| 8 |
|
| 9 |
import { MailPlus, Users, GraduationCap } from "lucide-react";
|
| 10 |
-
|
| 11 |
import { toast } from "sonner";
|
| 12 |
|
| 13 |
import {
|
|
@@ -106,9 +105,27 @@ function toIntOrFallback(v: any, fb: number) {
|
|
| 106 |
return fb;
|
| 107 |
}
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
export function LeftSidebar(props: Props) {
|
| 110 |
const {
|
| 111 |
-
user,
|
| 112 |
isLoggedIn,
|
| 113 |
spaceType,
|
| 114 |
groupMembers,
|
|
@@ -120,8 +137,6 @@ export function LeftSidebar(props: Props) {
|
|
| 120 |
workspaces,
|
| 121 |
selectedCourse,
|
| 122 |
availableCourses,
|
| 123 |
-
onRenameGroupName,
|
| 124 |
-
onRenameGroupNo,
|
| 125 |
} = props;
|
| 126 |
|
| 127 |
const currentWorkspace = useMemo(
|
|
@@ -178,7 +193,7 @@ export function LeftSidebar(props: Props) {
|
|
| 178 |
}, [availableCourses, currentWorkspace, selectedCourse]);
|
| 179 |
|
| 180 |
const courseName = useMemo(() => {
|
| 181 |
-
return (courseInfo as any)?.name ?? (selectedCourse || "Course");
|
| 182 |
}, [courseInfo, selectedCourse]);
|
| 183 |
|
| 184 |
// --------- Group fields ---------
|
|
@@ -189,7 +204,8 @@ export function LeftSidebar(props: Props) {
|
|
| 189 |
|
| 190 |
const wsGroupName = useMemo(() => {
|
| 191 |
const ws: any = currentWorkspace as any;
|
| 192 |
-
|
|
|
|
| 193 |
}, [currentWorkspace]);
|
| 194 |
|
| 195 |
// --------- Demo group mapping (My Space only) ---------
|
|
@@ -213,11 +229,9 @@ export function LeftSidebar(props: Props) {
|
|
| 213 |
|
| 214 |
const memberCount = (groupMembers || []).length;
|
| 215 |
|
| 216 |
-
|
| 217 |
-
const groupName = useMemo(() => wsGroupName || "YYY", [wsGroupName]);
|
| 218 |
const groupNo = useMemo(() => toIntOrFallback(wsGroupNo, 1), [wsGroupNo]);
|
| 219 |
|
| 220 |
-
|
| 221 |
// Invite dialog state
|
| 222 |
const [inviteOpen, setInviteOpen] = useState(false);
|
| 223 |
const [inviteEmail, setInviteEmail] = useState("");
|
|
@@ -232,27 +246,24 @@ export function LeftSidebar(props: Props) {
|
|
| 232 |
setInviteOpen(false);
|
| 233 |
};
|
| 234 |
|
| 235 |
-
|
| 236 |
// --------- Contacts ---------
|
| 237 |
-
const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
|
| 238 |
const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
|
| 239 |
|
| 240 |
-
const taName = (courseInfo as any)?.teachingAssistant?.name ?? "N/A";
|
| 241 |
const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim();
|
| 242 |
|
| 243 |
return (
|
| 244 |
<div className="h-full w-full flex flex-col min-h-0 bg-background text-foreground">
|
| 245 |
{/* ================= TOP (non-scroll) ================= */}
|
| 246 |
<div className="flex-shrink-0">
|
| 247 |
-
{/* removed Welcome section entirely */}
|
| 248 |
-
|
| 249 |
<div className="mt-2 mb-4">
|
| 250 |
<Divider />
|
| 251 |
</div>
|
| 252 |
|
| 253 |
{/* Course + Group */}
|
| 254 |
<div className="px-4 pt-5 pb-6 space-y-4">
|
| 255 |
-
{/* Course row with icon
|
| 256 |
<div className="flex items-start gap-3">
|
| 257 |
<div className="w-6 flex items-start justify-center flex-shrink-0 pt-[2px]">
|
| 258 |
<GraduationCap className="w-5 h-5 text-muted-foreground" />
|
|
@@ -260,12 +271,14 @@ export function LeftSidebar(props: Props) {
|
|
| 260 |
<div className="text-[22px] leading-snug font-semibold truncate">{courseName}</div>
|
| 261 |
</div>
|
| 262 |
|
| 263 |
-
{/* ===== My Space
|
| 264 |
{!isTeamSpace ? (
|
| 265 |
<div className="space-y-2">
|
| 266 |
<div className="flex items-center justify-between text-[15px]">
|
| 267 |
<span className="text-muted-foreground">Group Name:</span>
|
| 268 |
-
<span className="font-semibold truncate max-w-[60%] text-right">
|
|
|
|
|
|
|
| 269 |
</div>
|
| 270 |
<div className="flex items-center justify-between text-[15px]">
|
| 271 |
<span className="text-muted-foreground">Group Number:</span>
|
|
@@ -273,19 +286,18 @@ export function LeftSidebar(props: Props) {
|
|
| 273 |
</div>
|
| 274 |
</div>
|
| 275 |
) : (
|
| 276 |
-
/* ===== Team/Group
|
| 277 |
<div className="rounded-2xl border bg-background overflow-hidden">
|
| 278 |
<div className="px-4 pt-4 pb-3 space-y-3">
|
| 279 |
-
{/* Line 1: group name (read-only) */}
|
| 280 |
<div className="flex items-center gap-2">
|
| 281 |
<div className="text-[18px] font-semibold truncate">{groupName}</div>
|
| 282 |
</div>
|
| 283 |
|
| 284 |
-
{/* Line 2: Group {no}
|
| 285 |
<div className="flex items-center justify-between gap-3">
|
| 286 |
<div className="flex items-center gap-2">
|
| 287 |
<Users className="w-4 h-4 text-muted-foreground" />
|
| 288 |
-
|
| 289 |
<div className="text-[16px] font-medium">
|
| 290 |
Group <span className="font-semibold">{groupNo}</span> ({memberCount})
|
| 291 |
</div>
|
|
@@ -306,7 +318,7 @@ export function LeftSidebar(props: Props) {
|
|
| 306 |
<div className="px-3 pb-3 space-y-2">
|
| 307 |
{(groupMembers || []).map((member) => {
|
| 308 |
const isAI = !!(member as any).isAI;
|
| 309 |
-
const name = String((member as any).name || "");
|
| 310 |
const email = String((member as any).email || "");
|
| 311 |
const initials = name
|
| 312 |
? name
|
|
|
|
| 1 |
// web/src/components/sidebar/LeftSidebar.tsx
|
| 2 |
+
import React, { useMemo, useState } from "react";
|
| 3 |
import { Button } from "../ui/button";
|
| 4 |
import { Input } from "../ui/input";
|
| 5 |
import { Badge } from "../ui/badge";
|
|
|
|
| 7 |
import { SavedChatSection } from "./SavedChatSection";
|
| 8 |
|
| 9 |
import { MailPlus, Users, GraduationCap } from "lucide-react";
|
|
|
|
| 10 |
import { toast } from "sonner";
|
| 11 |
|
| 12 |
import {
|
|
|
|
| 105 |
return fb;
|
| 106 |
}
|
| 107 |
|
| 108 |
+
/**
|
| 109 |
+
* Important:
|
| 110 |
+
* - The "pencil" you see near Group Name can be coming from DATA (workspace/group name string),
|
| 111 |
+
* not from lucide icons. This sanitizes display text only.
|
| 112 |
+
*/
|
| 113 |
+
function sanitizeGroupDisplayName(v: any) {
|
| 114 |
+
const s = String(v ?? "");
|
| 115 |
+
// Remove common pencil/pen emoji variants (safe no-op if none)
|
| 116 |
+
return s
|
| 117 |
+
.replaceAll("✏️", "")
|
| 118 |
+
.replaceAll("✏", "")
|
| 119 |
+
.replaceAll("🖊️", "")
|
| 120 |
+
.replaceAll("🖊", "")
|
| 121 |
+
.replaceAll("🖋️", "")
|
| 122 |
+
.replaceAll("🖋", "")
|
| 123 |
+
.replace(/\(\s*✏️?\s*\)/g, "")
|
| 124 |
+
.trim();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
export function LeftSidebar(props: Props) {
|
| 128 |
const {
|
|
|
|
| 129 |
isLoggedIn,
|
| 130 |
spaceType,
|
| 131 |
groupMembers,
|
|
|
|
| 137 |
workspaces,
|
| 138 |
selectedCourse,
|
| 139 |
availableCourses,
|
|
|
|
|
|
|
| 140 |
} = props;
|
| 141 |
|
| 142 |
const currentWorkspace = useMemo(
|
|
|
|
| 193 |
}, [availableCourses, currentWorkspace, selectedCourse]);
|
| 194 |
|
| 195 |
const courseName = useMemo(() => {
|
| 196 |
+
return sanitizeGroupDisplayName((courseInfo as any)?.name ?? (selectedCourse || "Course"));
|
| 197 |
}, [courseInfo, selectedCourse]);
|
| 198 |
|
| 199 |
// --------- Group fields ---------
|
|
|
|
| 204 |
|
| 205 |
const wsGroupName = useMemo(() => {
|
| 206 |
const ws: any = currentWorkspace as any;
|
| 207 |
+
const raw = pickAny(ws, ["groupName", "name", "title"]) || "My Group";
|
| 208 |
+
return sanitizeGroupDisplayName(raw);
|
| 209 |
}, [currentWorkspace]);
|
| 210 |
|
| 211 |
// --------- Demo group mapping (My Space only) ---------
|
|
|
|
| 229 |
|
| 230 |
const memberCount = (groupMembers || []).length;
|
| 231 |
|
| 232 |
+
const groupName = useMemo(() => wsGroupName || "My Group", [wsGroupName]);
|
|
|
|
| 233 |
const groupNo = useMemo(() => toIntOrFallback(wsGroupNo, 1), [wsGroupNo]);
|
| 234 |
|
|
|
|
| 235 |
// Invite dialog state
|
| 236 |
const [inviteOpen, setInviteOpen] = useState(false);
|
| 237 |
const [inviteEmail, setInviteEmail] = useState("");
|
|
|
|
| 246 |
setInviteOpen(false);
|
| 247 |
};
|
| 248 |
|
|
|
|
| 249 |
// --------- Contacts ---------
|
| 250 |
+
const instructorName = sanitizeGroupDisplayName((courseInfo as any)?.instructor?.name ?? "N/A");
|
| 251 |
const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
|
| 252 |
|
| 253 |
+
const taName = sanitizeGroupDisplayName((courseInfo as any)?.teachingAssistant?.name ?? "N/A");
|
| 254 |
const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim();
|
| 255 |
|
| 256 |
return (
|
| 257 |
<div className="h-full w-full flex flex-col min-h-0 bg-background text-foreground">
|
| 258 |
{/* ================= TOP (non-scroll) ================= */}
|
| 259 |
<div className="flex-shrink-0">
|
|
|
|
|
|
|
| 260 |
<div className="mt-2 mb-4">
|
| 261 |
<Divider />
|
| 262 |
</div>
|
| 263 |
|
| 264 |
{/* Course + Group */}
|
| 265 |
<div className="px-4 pt-5 pb-6 space-y-4">
|
| 266 |
+
{/* Course row with icon */}
|
| 267 |
<div className="flex items-start gap-3">
|
| 268 |
<div className="w-6 flex items-start justify-center flex-shrink-0 pt-[2px]">
|
| 269 |
<GraduationCap className="w-5 h-5 text-muted-foreground" />
|
|
|
|
| 271 |
<div className="text-[22px] leading-snug font-semibold truncate">{courseName}</div>
|
| 272 |
</div>
|
| 273 |
|
| 274 |
+
{/* ===== My Space ===== */}
|
| 275 |
{!isTeamSpace ? (
|
| 276 |
<div className="space-y-2">
|
| 277 |
<div className="flex items-center justify-between text-[15px]">
|
| 278 |
<span className="text-muted-foreground">Group Name:</span>
|
| 279 |
+
<span className="font-semibold truncate max-w-[60%] text-right">
|
| 280 |
+
{sanitizeGroupDisplayName(demoGroup.name)}
|
| 281 |
+
</span>
|
| 282 |
</div>
|
| 283 |
<div className="flex items-center justify-between text-[15px]">
|
| 284 |
<span className="text-muted-foreground">Group Number:</span>
|
|
|
|
| 286 |
</div>
|
| 287 |
</div>
|
| 288 |
) : (
|
| 289 |
+
/* ===== Team/Group ===== */
|
| 290 |
<div className="rounded-2xl border bg-background overflow-hidden">
|
| 291 |
<div className="px-4 pt-4 pb-3 space-y-3">
|
| 292 |
+
{/* Line 1: group name (read-only, NO pencil) */}
|
| 293 |
<div className="flex items-center gap-2">
|
| 294 |
<div className="text-[18px] font-semibold truncate">{groupName}</div>
|
| 295 |
</div>
|
| 296 |
|
| 297 |
+
{/* Line 2: Group {no} ({count}) + Invite */}
|
| 298 |
<div className="flex items-center justify-between gap-3">
|
| 299 |
<div className="flex items-center gap-2">
|
| 300 |
<Users className="w-4 h-4 text-muted-foreground" />
|
|
|
|
| 301 |
<div className="text-[16px] font-medium">
|
| 302 |
Group <span className="font-semibold">{groupNo}</span> ({memberCount})
|
| 303 |
</div>
|
|
|
|
| 318 |
<div className="px-3 pb-3 space-y-2">
|
| 319 |
{(groupMembers || []).map((member) => {
|
| 320 |
const isAI = !!(member as any).isAI;
|
| 321 |
+
const name = sanitizeGroupDisplayName(String((member as any).name || ""));
|
| 322 |
const email = String((member as any).email || "");
|
| 323 |
const initials = name
|
| 324 |
? name
|