Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ type RelatedQueries = {
};

// Removed mcp parameter from submit, as geospatialTool now handles its client.
async function hideMessage(messageId: string) {
'use server'

const aiState = getMutableAIState<typeof AI>()

aiState.update({
...aiState.get(),
messages: aiState.get().messages.map(msg => {
if (msg.id === messageId) {
return {
...msg,
isHidden: true
}
}
return msg
})
})
}
Comment on lines +35 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

hideMessage should be idempotent and avoid no-op updates

Avoid updating state when nothing changes; also read aiState once.

 async function hideMessage(messageId: string) {
   'use server'

-  const aiState = getMutableAIState<typeof AI>()
-
-  aiState.update({
-    ...aiState.get(),
-    messages: aiState.get().messages.map(msg => {
-      if (msg.id === messageId) {
-        return {
-          ...msg,
-          isHidden: true
-        }
-      }
-      return msg
-    })
-  })
+  const aiState = getMutableAIState<typeof AI>()
+  const current = aiState.get()
+  let changed = false
+  const messages = current.messages.map(msg => {
+    if (msg.id === messageId && msg.isHidden !== true) {
+      changed = true
+      return { ...msg, isHidden: true }
+    }
+    return msg
+  })
+  if (!changed) return
+  aiState.update({ ...current, messages })
 }
📝 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
async function hideMessage(messageId: string) {
'use server'
const aiState = getMutableAIState<typeof AI>()
aiState.update({
...aiState.get(),
messages: aiState.get().messages.map(msg => {
if (msg.id === messageId) {
return {
...msg,
isHidden: true
}
}
return msg
})
})
}
async function hideMessage(messageId: string) {
'use server'
const aiState = getMutableAIState<typeof AI>()
const current = aiState.get()
let changed = false
const messages = current.messages.map(msg => {
if (msg.id === messageId && msg.isHidden !== true) {
changed = true
return { ...msg, isHidden: true }
}
return msg
})
if (!changed) return
aiState.update({ ...current, messages })
}
🤖 Prompt for AI Agents
In app/actions.tsx around lines 35 to 52, the hideMessage function repeatedly
reads aiState and always triggers an update even when nothing changes; refactor
to read aiState.get() once into a local variable, find the target message and if
it doesn't exist or already has isHidden true return early (no-op), otherwise
create a new messages array that only changes the matched message's isHidden to
true and call aiState.update once with the new state; this makes the operation
idempotent and avoids unnecessary state updates.


async function submit(formData?: FormData, skip?: boolean) {
'use server';

Expand Down Expand Up @@ -272,6 +291,7 @@ const initialUIState: UIState = [];
export const AI = createAI<AIState, UIState>({
actions: {
submit,
hideMessage,
},
initialUIState,
initialAIState,
Expand Down Expand Up @@ -344,6 +364,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
const chatId = aiState.chatId;
const isSharePage = aiState.isSharePage;
return aiState.messages
.filter(message => !message.isHidden)
.map((message, index) => {
const { role, content, id, type, name } = message;

Expand Down
Binary file modified bun.lockb
Binary file not shown.
32 changes: 19 additions & 13 deletions components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { StreamableValue, useUIState } from 'ai/rsc'
import type { AI, UIState } from '@/app/actions'
import { CollapsibleMessage } from './collapsible-message'
import { SwipeableMessage } from './swipeable-message'

interface ChatMessagesProps {
messages: UIState
Expand Down Expand Up @@ -50,19 +51,24 @@ export function ChatMessages({ messages }: ChatMessagesProps) {
},
index
) => (
<CollapsibleMessage
key={`${groupedMessage.id}`}
message={{
id: groupedMessage.id,
component: groupedMessage.components.map((component, i) => (
<div key={`${groupedMessage.id}-${i}`}>{component}</div>
)),
isCollapsed: groupedMessage.isCollapsed
}}
isLastMessage={
groupedMessage.id === messages[messages.length - 1].id
}
/>
<SwipeableMessage
key={`${groupedMessage.id}-swipeable`}
message={groupedMessage}
>
<CollapsibleMessage
key={`${groupedMessage.id}`}
message={{
id: groupedMessage.id,
component: groupedMessage.components.map((component, i) => (
<div key={`${groupedMessage.id}-${i}`}>{component}</div>
)),
isCollapsed: groupedMessage.isCollapsed
}}
isLastMessage={
groupedMessage.id === messages[messages.length - 1].id
}
/>
</SwipeableMessage>
Comment on lines +54 to +71
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Wrap with SwipeableMessage — good; remove redundant key on single child

CollapsibleMessage isn’t part of a list here; the extra key is unnecessary.

           <SwipeableMessage
             key={`${groupedMessage.id}-swipeable`}
             message={groupedMessage}
           >
-            <CollapsibleMessage
-              key={`${groupedMessage.id}`}
+            <CollapsibleMessage
               message={{
                 id: groupedMessage.id,
                 component: groupedMessage.components.map((component, i) => (
                   <div key={`${groupedMessage.id}-${i}`}>{component}</div>
                 )),
                 isCollapsed: groupedMessage.isCollapsed
               }}
               isLastMessage={
                 groupedMessage.id === messages[messages.length - 1].id
               }
             />
           </SwipeableMessage>
📝 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
<SwipeableMessage
key={`${groupedMessage.id}-swipeable`}
message={groupedMessage}
>
<CollapsibleMessage
key={`${groupedMessage.id}`}
message={{
id: groupedMessage.id,
component: groupedMessage.components.map((component, i) => (
<div key={`${groupedMessage.id}-${i}`}>{component}</div>
)),
isCollapsed: groupedMessage.isCollapsed
}}
isLastMessage={
groupedMessage.id === messages[messages.length - 1].id
}
/>
</SwipeableMessage>
<SwipeableMessage
key={`${groupedMessage.id}-swipeable`}
message={groupedMessage}
>
<CollapsibleMessage
message={{
id: groupedMessage.id,
component: groupedMessage.components.map((component, i) => (
<div key={`${groupedMessage.id}-${i}`}>{component}</div>
)),
isCollapsed: groupedMessage.isCollapsed
}}
isLastMessage={
groupedMessage.id === messages[messages.length - 1].id
}
/>
</SwipeableMessage>
🤖 Prompt for AI Agents
In components/chat-messages.tsx around lines 54 to 71, remove the redundant key
prop from the CollapsibleMessage child since SwipeableMessage already has the
unique key; keep the key only on the SwipeableMessage wrapper, delete the key
attribute on the CollapsibleMessage element, and verify TypeScript/JSX props
remain unchanged so rendering and isLastMessage logic still work.

)
)}
</>
Expand Down
30 changes: 30 additions & 0 deletions components/swipeable-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import { useSwipeable } from 'react-swipeable'
import { StreamableValue, useActions } from 'ai/rsc'

interface SwipeableMessageProps {
message: {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}
children: React.ReactNode
}

export function SwipeableMessage({
message,
children
}: SwipeableMessageProps) {
const { hideMessage } = useActions()

const handlers = useSwipeable({
onSwipedLeft: () => {
console.log(`Swiped left on message ${message.id}`)
hideMessage(message.id)
},
trackMouse: true
})

return <div {...handlers}>{children}</div>
}
Comment on lines +1 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden swipe UX and add keyboard/A11y fallback; drop console.log

Prevents accidental hides, supports keyboards/screen readers, and removes debug logging.

 'use client'

 import { useSwipeable } from 'react-swipeable'
 import { StreamableValue, useActions } from 'ai/rsc'

 interface SwipeableMessageProps {
   message: {
     id: string
     components: React.ReactNode[]
     isCollapsed?: StreamableValue<boolean>
   }
   children: React.ReactNode
 }

 export function SwipeableMessage({
   message,
   children
 }: SwipeableMessageProps) {
   const { hideMessage } = useActions()

   const handlers = useSwipeable({
-    onSwipedLeft: () => {
-      console.log(`Swiped left on message ${message.id}`)
-      hideMessage(message.id)
-    },
-    trackMouse: true
+    onSwipedLeft: () => {
+      void hideMessage(message.id)
+    },
+    trackMouse: true,
+    preventScrollOnSwipe: true,
+    delta: 50
   })

-  return <div {...handlers}>{children}</div>
+  return (
+    <div
+      {...handlers}
+      role="button"
+      tabIndex={0}
+      aria-label="Hide message"
+      onKeyDown={(e) => {
+        if (e.key === 'Backspace' || e.key === 'Delete') {
+          e.preventDefault()
+          void hideMessage(message.id)
+        }
+      }}
+    >
+      {children}
+    </div>
+  )
 }
📝 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
'use client'
import { useSwipeable } from 'react-swipeable'
import { StreamableValue, useActions } from 'ai/rsc'
interface SwipeableMessageProps {
message: {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}
children: React.ReactNode
}
export function SwipeableMessage({
message,
children
}: SwipeableMessageProps) {
const { hideMessage } = useActions()
const handlers = useSwipeable({
onSwipedLeft: () => {
console.log(`Swiped left on message ${message.id}`)
hideMessage(message.id)
},
trackMouse: true
})
return <div {...handlers}>{children}</div>
}
'use client'
import { useSwipeable } from 'react-swipeable'
import { StreamableValue, useActions } from 'ai/rsc'
interface SwipeableMessageProps {
message: {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}
children: React.ReactNode
}
export function SwipeableMessage({
message,
children
}: SwipeableMessageProps) {
const { hideMessage } = useActions()
const handlers = useSwipeable({
onSwipedLeft: () => {
void hideMessage(message.id)
},
trackMouse: true,
preventScrollOnSwipe: true,
delta: 50
})
return (
<div
{...handlers}
role="button"
tabIndex={0}
aria-label="Hide message"
onKeyDown={(e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault()
void hideMessage(message.id)
}
}}
>
{children}
</div>
)
}

1 change: 1 addition & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ export type AIMessage = {
| 'followup'
| 'end'
| 'drawing_context' // Added custom type for drawing context messages
isHidden?: boolean
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"react-hook-form": "^7.56.2",
"react-icons": "^5.5.0",
"react-markdown": "^9.1.0",
"react-swipeable": "^7.0.2",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^10.0.6",
"rehype-external-links": "^3.0.0",
Expand Down