Skip to content
Closed
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
5 changes: 0 additions & 5 deletions bun.lock

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

53 changes: 28 additions & 25 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,42 +69,45 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
}
}

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!input && !selectedFile) {
return
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!input.trim() && !selectedFile) {
return;
}
Comment on lines +72 to 76
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Trimmed validation vs send-button disabled state are inconsistent.

You guard on input.trim() here, but the submit button uses input.length (Line 254). Users can click “Send” with only spaces, then nothing happens. Align the disabled condition to trimmed length.

If you agree, update the button to: disabled={input.trim().length === 0 && !selectedFile}.


I'll verify the review comment by examining the actual code in the chat-panel.tsx file.


🏁 Script executed:

#!/bin/bash
# Find chat-panel.tsx and extract the relevant sections
fd "chat-panel.tsx" --type f

Length of output: 25


🏁 Script executed:

#!/bin/bash
# Extract lines around 72-76 to see handleSubmit
if [ -f "components/chat-panel.tsx" ]; then
  echo "=== Lines 70-80 (handleSubmit context) ==="
  sed -n '70,80p' "components/chat-panel.tsx"
  echo ""
  echo "=== Lines 250-258 (submit button context) ==="
  sed -n '250,258p' "components/chat-panel.tsx"
fi

Length of output: 776


🏁 Script executed:

#!/bin/bash
# Search for all occurrences of disabled attribute on buttons to understand the full context
if [ -f "components/chat-panel.tsx" ]; then
  echo "=== Searching for 'disabled=' pattern ==="
  rg -n "disabled=" "components/chat-panel.tsx" -A 2 -B 2
fi

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Search for input.length and input.trim patterns
if [ -f "components/chat-panel.tsx" ]; then
  echo "=== Searching for input.length or input.trim patterns ==="
  rg -n "input\.(length|trim)" "components/chat-panel.tsx" -B 1 -A 1
fi

Length of output: 409


Fix button disabled state to prevent clicking with only whitespace input.

Line 254's disabled condition uses input.length === 0, but handleSubmit (line 74) validates !input.trim(). This allows users to click "Send" with only spaces, where nothing happens. Also, line 228 validates input.trim().length === 0, indicating consistent intent across handlers.

Update line 254 to: disabled={input.trim().length === 0 && !selectedFile}

🤖 Prompt for AI Agents
In components/chat-panel.tsx around lines 72-76 (handleSubmit) and where the
Send button is rendered (line ~254), the submit handler uses input.trim() but
the button disabled check uses input.length === 0, allowing whitespace-only
clicks; update the button disabled expression to use trimmed input so it matches
validation (disabled={input.trim().length === 0 && !selectedFile}), ensuring the
Send button is disabled when input is only whitespace and no file is selected.


const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = []
// Create the user message content first, while we still have the input and file
const content: ({ type: 'text'; text: string } | { type: 'image'; image: File })[] = [];
if (input) {
content.push({ type: 'text', text: input })
content.push({ type: 'text', text: input });
}
if (selectedFile && selectedFile.type.startsWith('image/')) {
content.push({
type: 'image',
image: URL.createObjectURL(selectedFile)
})
content.push({ type: 'image', image: selectedFile });
}

setMessages(currentMessages => [
...currentMessages,
{
id: nanoid(),
component: <UserMessage content={content} />
}
])

const formData = new FormData(e.currentTarget)
// Prepare the form data for the server action
const formData = new FormData(e.currentTarget);
if (selectedFile) {
formData.append('file', selectedFile)
formData.append('file', selectedFile);
}

setInput('')
clearAttachment()
// Clear the input fields for the user
setInput('');
clearAttachment();

const responseMessage = await submit(formData)
setMessages(currentMessages => [...currentMessages, responseMessage as any])
}
// Call the server action. It will immediately return a streamable UI component.
const responseMessage = submit(formData);

// Update the UI state with both the user's message and the initial assistant response
// in a single operation. This ensures the component tree structure is consistent.
setMessages(currentMessages => [
...currentMessages,
{
id: nanoid(),
component: <UserMessage content={content} />,
},
responseMessage as any,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Relying on as any here hides the real contract between submit and your messages state shape. This makes it easier for future changes to introduce subtle UI bugs. Typing the submit return value (e.g., a ChatMessage) will let you remove as any and keep the code safer and self-documenting.

Suggestion

Define a concrete message type and type the server action accordingly, then remove the as any:

// Example
type ChatMessage = { id: string; component: React.ReactNode };

// In the server action typing
async function submit(formData: FormData): Promise<ChatMessage> { /* ... */ }

// When replacing the placeholder in the previous suggestion
.then((resolved: ChatMessage) => {
  setMessages((curr) => curr.map((m) => (m.id === pendingId ? resolved : m)));
})

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

]);
Comment on lines +98 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This assumes submit(formData) returns a synchronous value. If submit is a Next.js Server Action (or otherwise async), this will create an unhandled Promise and also append a Promise-like object into state, which can cause runtime errors and non-deterministic behavior. It also means rejections become unhandled. You can still preserve atomic UI updates by inserting a placeholder assistant message in the same setMessages call as the user message, then resolve the server action and replace the placeholder when it completes.

Suggestion

Consider adding a placeholder assistant message atomically, then replacing it when the server action resolves. For example:

// Call the server action and immediately stage a placeholder assistant message
const pendingId = nanoid();
const userMsg = {
  id: nanoid(),
  component: <UserMessage content={content} />,
};

setMessages((curr) => [
  ...curr,
  userMsg,
  { id: pendingId, component: <span aria-busy="true">Thinking…</span> },
]);

submit(formData)
  .then((resolved) => {
    setMessages((curr) =>
      curr.map((m) => (m.id === pendingId ? (resolved as any) : m))
    );
  })
  .catch((err) => {
    // Optional: surface an error state in place of the placeholder
    setMessages((curr) =>
      curr.map((m) =>
        m.id === pendingId
          ? { id: pendingId, component: <span role="alert">Something went wrong</span> }
          : m
      )
    );
    console.error('submit(formData) failed', err);
  });

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

};

const handleClear = async () => {
setMessages([])
Expand Down
40 changes: 34 additions & 6 deletions components/user-message.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import Image from 'next/image'
import { ChatShare } from './chat-share'

type UserMessageContentPart =
| { type: 'text'; text: string }
| { type: 'image'; image: string } // data URL
| { type: 'image'; image: string | File } // Can be a data URL or a File object

type UserMessageProps = {
content: string | UserMessageContentPart[]
Expand All @@ -18,6 +18,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
showShare = false
}) => {
const enableShare = process.env.ENABLE_SHARE === 'true'
const [imageUrl, setImageUrl] = useState<string | null>(null)

// Normalize content to an array
const contentArray =
Expand All @@ -27,21 +28,48 @@ export const UserMessage: React.FC<UserMessageProps> = ({
const textPart = contentArray.find(
(part): part is { type: 'text'; text: string } => part.type === 'text'
)?.text
const imagePart = contentArray.find(
(part): part is { type: 'image'; image: string } => part.type === 'image'
const imageContent = contentArray.find(
(part): part is { type: 'image'; image: string | File } =>
part.type === 'image'
)?.image

useEffect(() => {
let objectUrl: string | null = null

if (imageContent instanceof File) {
objectUrl = URL.createObjectURL(imageContent)
setImageUrl(objectUrl)
} else if (typeof imageContent === 'string') {
setImageUrl(imageContent)
}

// Cleanup function to revoke the object URL
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl)
setImageUrl(null) // Reset state on cleanup
}
}
}, [imageContent])

return (
<div className="flex items-start w-full space-x-3 mt-2">
<div className="flex-1 space-y-2">
{imagePart && (
{imageUrl && (
<div className="p-2 border rounded-lg bg-muted w-fit">
<Image
src={imagePart}
src={imageUrl}
alt="attachment"
width={300}
height={300}
className="max-w-xs max-h-64 rounded-md object-contain"
onLoad={() => {
// Optional: Revoke URL after image has loaded to free memory sooner
// if the image is cached by the browser.
// Note: This might cause issues if the component re-renders and
// the browser needs to fetch the image again.
// URL.revokeObjectURL(imageUrl);
}}
/>
</div>
)}
Expand Down
Empty file added dev.log
Empty file.
2 changes: 1 addition & 1 deletion mapbox_mcp/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type Tool = {
name: string;
// Add other properties as needed based on your usage
};
import { getModel } from 'QCX/lib/utils';
import { getModel } from '../lib/utils';

// Types for location and mapping data
interface LocationResult {
Expand Down