Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

148 changes: 105 additions & 43 deletions components/sidebar/intersection-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useMapStore } from "@/stores/map-store";
import { X } from "lucide-react";
Expand All @@ -9,19 +10,27 @@ import { PhotoProvider } from "react-photo-view";

export function IntersectionSidebar() {
const { selectedHotspot, selectHotspot } = useMapStore();
const [activeTab, setActiveTab] = useState("overview");

// Reset to overview tab when hotspot changes
useEffect(() => {
if (selectedHotspot) {
setActiveTab("overview");
}
}, [selectedHotspot?.id]);

return (
<AnimatePresence mode="wait">
{selectedHotspot && (
<motion.aside
key={selectedHotspot.id}
initial={{ x: "100%", opacity: 0 }}
animate={{
x: 0,
animate={{
x: 0,
opacity: 1
}}
exit={{
x: "100%",
exit={{
x: "100%",
opacity: 0
}}
transition={{
Expand All @@ -43,55 +52,108 @@ export function IntersectionSidebar() {
}}
className="flex flex-col h-full"
>
<div className="flex items-center justify-between border-b border-zinc-800 p-4">
<div>
<h2 className="text-lg font-semibold text-white">
{selectedHotspot.intersection}
</h2>
<p className="text-sm text-zinc-400">
{selectedHotspot.total_count} collisions recorded
</p>
</div>
<button
onClick={() => selectHotspot(null)}
className="rounded-lg p-2 hover:bg-zinc-800"
>
<X className="h-5 w-5 text-zinc-400" />
</button>
</div>
<div className="flex items-center justify-between border-b border-zinc-800 p-4">
<div>
<h2 className="text-lg font-semibold text-white">
{selectedHotspot.intersection}
</h2>
<p className="text-sm text-zinc-400">
{selectedHotspot.total_count} collisions recorded
</p>
</div>
<button
onClick={() => selectHotspot(null)}
className="rounded-lg p-2 hover:bg-zinc-800"
>
<X className="h-5 w-5 text-zinc-400" />
</button>
</div>
Comment on lines +93 to +99
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessible label to the icon-only Close button.

Proposed fix
               <button
+                aria-label="Close sidebar"
                 onClick={() => selectHotspot(null)}
                 className="rounded-lg p-2 hover:bg-zinc-800"
               >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => selectHotspot(null)}
className="rounded-lg p-2 hover:bg-zinc-800"
>
<X className="h-5 w-5 text-zinc-400" />
</button>
</div>
<button
aria-label="Close sidebar"
onClick={() => selectHotspot(null)}
className="rounded-lg p-2 hover:bg-zinc-800"
>
<X className="h-5 w-5 text-zinc-400" />
</button>
</div>
🤖 Prompt for AI Agents
In @components/sidebar/intersection-sidebar.tsx around lines 93 - 99, The Close
button is icon-only and lacks an accessible label; update the button that calls
selectHotspot(null) (the element rendering the <X /> icon) to include an
accessible name—e.g., add aria-label="Close" or aria-label="Close hotspot" (or
include a visually hidden <span> with readable text) so screen readers can
announce its purpose; keep the onClick and styling unchanged and ensure the
label conveys the action (Close).


<div className="flex-1 overflow-y-auto p-4">
<PhotoProvider>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-zinc-900">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="audit">Safety Audit</TabsTrigger>
<TabsTrigger value="reimagine">Re-imagine</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-y-auto p-4">
<PhotoProvider>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-zinc-900">
<TabsTrigger
value="overview"
className="transition-all hover:scale-105 active:scale-95"
>
Overview
</TabsTrigger>
<TabsTrigger
value="audit"
className="transition-all hover:scale-105 active:scale-95"
>
Safety Audit
</TabsTrigger>
<TabsTrigger
value="reimagine"
className="transition-all hover:scale-105 active:scale-95"
>
Re-imagine
</TabsTrigger>
</TabsList>

<TabsContent value="overview" className="mt-4">
<OverviewTab hotspot={selectedHotspot} />
</TabsContent>
<div className="relative mt-4 overflow-hidden">
<AnimatePresence mode="wait">
{activeTab === "overview" && (
<motion.div
key="overview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
style={{ willChange: "transform, opacity" }}
>
<OverviewTab hotspot={selectedHotspot} />
</motion.div>
)}

<TabsContent value="audit" className="mt-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
{activeTab === "audit" && (
<motion.div
key="audit"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4"
style={{ willChange: "transform, opacity" }}
>
<p className="text-sm text-zinc-400">
Generate a safety audit to see AI analysis
</p>
</div>
</TabsContent>
</motion.div>
)}

<TabsContent value="reimagine" className="mt-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
{activeTab === "reimagine" && (
<motion.div
key="reimagine"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4"
style={{ willChange: "transform, opacity" }}
>
<p className="text-sm text-zinc-400">
Re-imagine this intersection with AI
</p>
</div>
</TabsContent>
</Tabs>
</PhotoProvider>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</Tabs>
</PhotoProvider>
Comment on lines +103 to +189
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "tabs.tsx" | grep -E "components/ui"

Repository: Lemirq/cision

Length of output: 81


🏁 Script executed:

cat -n ./components/ui/tabs.tsx

Repository: Lemirq/cision

Length of output: 2279


🏁 Script executed:

cat ./package.json | grep -A 2 "@radix-ui/react-tabs"

Repository: Lemirq/cision

Length of output: 171


🏁 Script executed:

head -50 ./components/sidebar/intersection-sidebar.tsx

Repository: Lemirq/cision

Length of output: 1503


🏁 Script executed:

sed -n '103,189p' ./components/sidebar/intersection-sidebar.tsx

Repository: Lemirq/cision

Length of output: 3830


Skip TabsContent breaks ARIA/keyboard semantics; use it to wrap each tab panel instead.

Not using TabsContent removes the role="tabpanel", aria-labelledby, and automatic keyboard navigation that Radix UI provides. The suggested asChild workaround won't work—Radix UI v1.1.13's Content component doesn't support that prop.

Instead, wrap each tab's content directly with TabsContent:

Correct approach
-                  <div className="relative mt-4 overflow-hidden">
-                    <AnimatePresence mode="wait" custom={direction}>
-                      {activeTab === "overview" && (
-                        <motion.div key="overview" ...>
-                          <OverviewTab hotspot={selectedHotspot} />
-                        </motion.div>
-                      )}
-                      ...
-                    </AnimatePresence>
-                  </div>
+                  <AnimatePresence mode="wait" custom={direction}>
+                    <TabsContent value="overview" forceMount className="relative mt-4 overflow-hidden">
+                      {activeTab === "overview" && (
+                        <motion.div key="overview" ...>
+                          <OverviewTab hotspot={selectedHotspot} />
+                        </motion.div>
+                      )}
+                    </TabsContent>
+                    <TabsContent value="audit" forceMount className="relative mt-4 overflow-hidden">
+                      {activeTab === "audit" && (
+                        <motion.div key="audit" ...>
+                          ...
+                        </motion.div>
+                      )}
+                    </TabsContent>
+                    <TabsContent value="reimagine" forceMount className="relative mt-4 overflow-hidden">
+                      {activeTab === "reimagine" && (
+                        <motion.div key="reimagine" ...>
+                          ...
+                        </motion.div>
+                      )}
+                    </TabsContent>
+                  </AnimatePresence>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-zinc-900">
<TabsTrigger
value="overview"
className="transition-all hover:scale-105 active:scale-95"
>
Overview
</TabsTrigger>
<TabsTrigger
value="audit"
className="transition-all hover:scale-105 active:scale-95"
>
Safety Audit
</TabsTrigger>
<TabsTrigger
value="reimagine"
className="transition-all hover:scale-105 active:scale-95"
>
Re-imagine
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4">
<OverviewTab hotspot={selectedHotspot} />
</TabsContent>
<div className="relative mt-4 overflow-hidden">
<AnimatePresence mode="wait" custom={direction}>
{activeTab === "overview" && (
<motion.div
key="overview"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{ willChange: "transform, opacity" }}
>
<OverviewTab hotspot={selectedHotspot} />
</motion.div>
)}
<TabsContent value="audit" className="mt-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
{activeTab === "audit" && (
<motion.div
key="audit"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
}}
className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4"
style={{ willChange: "transform, opacity" }}
>
<p className="text-sm text-zinc-400">
Generate a safety audit to see AI analysis
</p>
</div>
</TabsContent>
</motion.div>
)}
<TabsContent value="reimagine" className="mt-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
{activeTab === "reimagine" && (
<motion.div
key="reimagine"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
}}
className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4"
style={{ willChange: "transform, opacity" }}
>
<p className="text-sm text-zinc-400">
Re-imagine this intersection with AI
</p>
</div>
</TabsContent>
</Tabs>
</PhotoProvider>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</Tabs>
</PhotoProvider>
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-zinc-900">
<TabsTrigger
value="overview"
className="transition-all hover:scale-105 active:scale-95"
>
Overview
</TabsTrigger>
<TabsTrigger
value="audit"
className="transition-all hover:scale-105 active:scale-95"
>
Safety Audit
</TabsTrigger>
<TabsTrigger
value="reimagine"
className="transition-all hover:scale-105 active:scale-95"
>
Re-imagine
</TabsTrigger>
</TabsList>
<AnimatePresence mode="wait" custom={direction}>
<TabsContent value="overview" forceMount className="relative mt-4 overflow-hidden">
{activeTab === "overview" && (
<motion.div
key="overview"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{ willChange: "transform, opacity" }}
>
<OverviewTab hotspot={selectedHotspot} />
</motion.div>
)}
</TabsContent>
<TabsContent value="audit" forceMount className="relative mt-4 overflow-hidden">
{activeTab === "audit" && (
<motion.div
key="audit"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{ willChange: "transform, opacity" }}
>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
<p className="text-sm text-zinc-400">
Generate a safety audit to see AI analysis
</p>
</div>
</motion.div>
)}
</TabsContent>
<TabsContent value="reimagine" forceMount className="relative mt-4 overflow-hidden">
{activeTab === "reimagine" && (
<motion.div
key="reimagine"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{ willChange: "transform, opacity" }}
>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
<p className="text-sm text-zinc-400">
Re-imagine this intersection with AI
</p>
</div>
</motion.div>
)}
</TabsContent>
</AnimatePresence>
</Tabs>
</PhotoProvider>
🤖 Prompt for AI Agents
In @components/sidebar/intersection-sidebar.tsx around lines 103 - 189, The tab
panels are rendered directly with motion.div which bypasses Radix's TabsContent
and breaks ARIA/keyboard semantics; wrap each panel in TabsContent (using the
matching value prop: "overview", "audit", "reimagine") and put your motion.div
(and its props: key, custom={direction}, variants, initial, animate, exit,
transition, style, className) as the child of that TabsContent (do not use
asChild since Radix v1.1.13 doesn't support it), keeping OverviewTab inside the
"overview" TabsContent and the static <p> nodes inside the corresponding "audit"
and "reimagine" TabsContent so role="tabpanel" and aria-labelledby/keyboard
behavior are restored.

</div>
</motion.div>
</motion.aside>
)}
</AnimatePresence>
Expand Down
44 changes: 33 additions & 11 deletions components/sidebar/persona-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,12 @@ export function PersonaSidebar() {
connectionType: "websocket",
dynamicVariables: selectedHotspot
? {
intersection_name: selectedHotspot.intersection || selectedHotspot.address,
collision_count: selectedHotspot.total_count.toString(),
fatal_count: selectedHotspot.fatal_count.toString(),
cyclist_count: selectedHotspot.cyclist_count.toString(),
pedestrian_count: selectedHotspot.pedestrian_count.toString(),
}
intersection_name: selectedHotspot.intersection || selectedHotspot.address,
collision_count: selectedHotspot.total_count.toString(),
fatal_count: selectedHotspot.fatal_count.toString(),
cyclist_count: selectedHotspot.cyclist_count.toString(),
pedestrian_count: selectedHotspot.pedestrian_count.toString(),
}
: undefined,
});
} catch (error) {
Expand Down Expand Up @@ -247,7 +247,7 @@ export function PersonaSidebar() {
<motion.aside
initial={{ x: 0 }}
animate={{ x: 0 }}
className="fixed left-16 top-0 z-40 h-screen w-80 border-r border-zinc-800 bg-zinc-950 flex flex-col"
className="fixed left-16 top-0 z-40 h-screen w-80 border-r border-zinc-800 bg-zinc-950 flex flex-col overflow-hidden"
>
<AnimatePresence mode="wait">
{currentView === "list" ? (
Expand All @@ -256,6 +256,10 @@ export function PersonaSidebar() {
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="flex flex-col h-full"
>
{/* Header */}
Expand All @@ -268,10 +272,17 @@ export function PersonaSidebar() {

{/* Agent List */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{PERSONAS.map((persona) => (
{PERSONAS.map((persona, index) => (
<motion.button
key={persona.id}
onClick={() => selectPersona(persona)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: index * 0.1,
ease: "easeOut",
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 hover:border-zinc-700 rounded-xl p-4 transition-all text-left group"
Expand Down Expand Up @@ -327,17 +338,23 @@ export function PersonaSidebar() {
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="flex flex-col h-full"
>
{/* Header with Back Button */}
<div className="border-b border-zinc-800 p-4">
<div className="flex items-center gap-3">
<button
<motion.button
onClick={goBack}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="p-2 -ml-2 hover:bg-zinc-800 rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5 text-zinc-400" />
</button>
</motion.button>
Comment on lines +350 to +357
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessible label to the icon-only Back button.

Proposed fix
                 <motion.button
                   onClick={goBack}
+                  aria-label="Back"
                   whileHover={{ scale: 1.1 }}
                   whileTap={{ scale: 0.9 }}
                   className="p-2 -ml-2 hover:bg-zinc-800 rounded-lg transition-colors"
                 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<motion.button
onClick={goBack}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="p-2 -ml-2 hover:bg-zinc-800 rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5 text-zinc-400" />
</button>
</motion.button>
<motion.button
onClick={goBack}
aria-label="Back"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="p-2 -ml-2 hover:bg-zinc-800 rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5 text-zinc-400" />
</motion.button>
🤖 Prompt for AI Agents
In @components/sidebar/persona-sidebar.tsx around lines 350 - 357, The Back
button is icon-only (motion.button with onClick={goBack} containing <ArrowLeft
/>) and needs an accessible label; add an aria-label (e.g., aria-label="Go back"
or "Back") to the motion.button or include hidden descriptive text for screen
readers (visually-hidden span) so assistive tech can announce the button purpose
while keeping the icon-only visual; ensure the label is concise and matches the
button action (goBack).

<div className="flex-1">
<h2 className="text-lg font-semibold text-white">
{selectedPersona.name}
Expand Down Expand Up @@ -441,12 +458,17 @@ export function PersonaSidebar() {
)}

<AnimatePresence initial={false}>
{transcriptMessages.map((message) => (
{transcriptMessages.map((message, index) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.3,
delay: index * 0.05,
ease: "easeOut",
Comment on lines +467 to +470
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Transcript animation delay accumulates unboundedly for long conversations.

The delay: index * 0.05 grows linearly with the transcript length. For a conversation with 50+ messages, new messages will appear with noticeable delays (e.g., 2.5s+ for message 50). Since these are real-time messages, users expect immediate feedback.

Consider removing the index-based delay or using a fixed small delay for all new messages:

Suggested fix
                    transition={{
                      duration: 0.3,
-                     delay: index * 0.05,
+                     delay: 0.05,
                      ease: "easeOut",
                    }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
transition={{
duration: 0.3,
delay: index * 0.05,
ease: "easeOut",
transition={{
duration: 0.3,
delay: 0.05,
ease: "easeOut",
🤖 Prompt for AI Agents
In @components/sidebar/persona-sidebar.tsx around lines 467 - 470, The animation
delay currently multiplies the message index (delay: index * 0.05), causing
growing latency for long transcripts; locate the transition prop where delay is
computed (the transition object using duration, delay, ease) and replace the
index-based delay with a fixed small delay or zero (e.g., delay: 0 or delay:
0.05) so new messages render immediately regardless of transcript length while
keeping duration and ease unchanged.

}}
className="space-y-1"
>
<div className="flex items-center gap-2">
Expand Down
6 changes: 1 addition & 5 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
turbopack: {
root: __dirname,
},
};
const nextConfig: NextConfig = {};

export default nextConfig;