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
848 changes: 0 additions & 848 deletions app/actions.tsx

This file was deleted.

428 changes: 428 additions & 0 deletions app/api/chat/stream/route.ts

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { Chat } from '@/components/chat'
import { nanoid } from '@/lib/utils'
import { AI } from './actions'
import { ChatProvider } from '@/components/chat-provider'
import { MapDataProvider } from '@/components/map/map-data-context'

export const maxDuration = 60

import { MapDataProvider } from '@/components/map/map-data-context'

export default function Page() {
const id = nanoid()
return (
<AI initialAIState={{ chatId: id, messages: [] }}>
<ChatProvider chatId={id}>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
</AI>
</ChatProvider>
)
}
56 changes: 19 additions & 37 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,58 @@
import { notFound, redirect } from 'next/navigation';
import { Chat } from '@/components/chat';
import { getChat, getChatMessages } from '@/lib/actions/chat'; // Added getChatMessages
import { AI } from '@/app/actions';
import { ChatProvider } from '@/components/chat-provider';
import { getChat, getChatMessages } from '@/lib/actions/chat';
import { MapDataProvider } from '@/components/map/map-data-context';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; // For server-side auth
import type { AIMessage } from '@/lib/types'; // For AIMessage type
import type { Message as DrizzleMessage } from '@/lib/actions/chat-db'; // For DrizzleMessage type
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
import type { Message } from 'ai/react';
import type { Message as DrizzleMessage } from '@/lib/actions/chat-db';

export const maxDuration = 60;

export interface SearchPageProps {
params: Promise<{ id: string }>; // Keep as is for now
params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: SearchPageProps) {
const { id } = await params; // Keep as is for now
// TODO: Metadata generation might need authenticated user if chats are private
// For now, assuming getChat can be called or it handles anon access for metadata appropriately
const userId = await getCurrentUserIdOnServer(); // Attempt to get user for metadata
const chat = await getChat(id, userId || 'anonymous'); // Pass userId or 'anonymous' if none
const { id } = await params;
const userId = await getCurrentUserIdOnServer();
const chat = await getChat(id, userId || 'anonymous');
return {
title: chat?.title?.toString().slice(0, 50) || 'Search',
};
}

export default async function SearchPage({ params }: SearchPageProps) {
const { id } = await params; // Keep as is for now
const { id } = await params;
const userId = await getCurrentUserIdOnServer();

if (!userId) {
// If no user, redirect to login or show appropriate page
// For now, redirecting to home, but a login page would be better.
redirect('/');
}

const chat = await getChat(id, userId);

if (!chat) {
// If chat doesn't exist or user doesn't have access (handled by getChat)
notFound();
}

// Fetch messages for the chat
const dbMessages: DrizzleMessage[] = await getChatMessages(chat.id);

// Transform DrizzleMessages to AIMessages
const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => {
return {
const validRoles = new Set(['user', 'assistant', 'system'])
const initialMessages: Message[] = dbMessages
.filter((dbMsg) => validRoles.has(dbMsg.role))
.map((dbMsg): Message => ({
id: dbMsg.id,
role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities
role: dbMsg.role as Message['role'],
content: dbMsg.content,
createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
// 'type' and 'name' are not in the basic Drizzle 'messages' schema.
// These would be undefined unless specific logic is added to derive them.
// For instance, if a message with role 'tool' should have a 'name',
// or if some messages have a specific 'type' based on content or other flags.
// This mapping assumes standard user/assistant messages primarily.
};
});
}));

return (
<AI
initialAIState={{
chatId: chat.id,
messages: initialMessages, // Use the transformed messages from the database
// isSharePage: true, // This was in PR#533, but share functionality is removed.
// If needed for styling or other logic, it can be set.
}}
>
<ChatProvider chatId={chat.id} initialMessages={initialMessages}>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
</AI>
</ChatProvider>
);
}
}
156 changes: 107 additions & 49 deletions components/chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,128 @@
'use client'

import { StreamableValue, useUIState } from 'ai/rsc'
import type { AI, UIState } from '@/app/actions'
import type { Message } from 'ai/react'
import { CollapsibleMessage } from './collapsible-message'
import { Section } from './section'
import { BotMessage } from './message'
import { UserMessage } from './user-message'
import { ToolResultRenderer } from './tool-result-renderer'
import { useChatContext, type Annotation } from './chat-provider'
import { Copilot } from './copilot'
import SearchRelated from './search-related'

interface ChatMessagesProps {
messages: UIState
messages: Message[]
}

export function ChatMessages({ messages }: ChatMessagesProps) {
if (!messages.length) {
const { annotations, isLoading } = useChatContext()
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 | 🟡 Minor

isLoading is destructured but never used.

Either thread it into CollapsibleMessage/the assistant skeleton (the prior RSC version used a streaming indicator) or drop it.

-  const { annotations, isLoading } = useChatContext()
+  const { annotations } = useChatContext()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/chat-messages.tsx` at line 18, The variable isLoading is currently
unused after destructuring from useChatContext(); either remove it from the
destructuring to eliminate the unused variable, or thread it into the message
rendering so loading state can be shown — e.g., pass isLoading as a prop into
CollapsibleMessage (or the assistant skeleton component used to render assistant
messages) and use it to show the streaming/loading indicator there; update the
useChatContext() destructure in components/chat-messages.tsx and the
CollapsibleMessage/assistant component props and handling accordingly.


if (!messages.length && !annotations.length) {
return null
}

// Group messages based on ID, and if there are multiple messages with the same ID, combine them into one message
const groupedMessages = messages.reduce(
(acc: { [key: string]: any }, message) => {
if (!acc[message.id]) {
acc[message.id] = {
const renderedMessages: {
id: string
component: React.ReactNode
isCollapsed?: boolean
isAssistant?: boolean
}[] = []

// Render tool result annotations first (they come before the text)
const toolAnnotations = annotations.filter((a: Annotation) => a.type === 'tool_result')
for (let i = 0; i < toolAnnotations.length; i++) {
const ann = toolAnnotations[i]
renderedMessages.push({
id: `tool-${ann.toolName}-${i}`,
component: <ToolResultRenderer toolName={ann.toolName} result={ann.result} />,
isCollapsed: true
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +32 to +40
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.

🧹 Nitpick | 🔵 Trivial

Duplicates getToolResults logic from the provider.

components/chat-provider.tsx exposes getToolResults(toolName) (lines 61-65) which already filters tool_result annotations. Here you re-filter annotations locally — both for consistency and to keep the abstraction boundary in the provider, prefer consuming it from context (or dropping the helper and keeping the logic in one place only).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/chat-messages.tsx` around lines 29 - 36, The code duplicates
provider logic by re-filtering annotations into toolAnnotations; instead call
the provider helper getToolResults(...) (from the context exposed in
chat-provider via getToolResults) to obtain tool results, then iterate those
results to push entries into renderedMessages with id, component
(ToolResultRenderer) and isCollapsed. Remove the local annotations.filter(...)
and use getToolResults to keep logic centralized and consistent.

Comment on lines +31 to +40
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how tool_result annotations are emitted (per-turn marker?) and whether the route
# tags them with any message/turn identifier.
rg -nP -C3 "tool_result|allToolOutputs|toolResponses" app/api/chat/stream/route.ts
# Look for any per-message annotation correlation in chat-provider/chat-messages.
rg -nP -C2 "messageId|turnIndex|annotations" components/chat-provider.tsx components/chat-messages.tsx

Repository: QueueLab/QCX

Length of output: 5381


🏁 Script executed:

# View the full rendering logic in chat-messages.tsx around the tool annotations
sed -n '30,85p' components/chat-messages.tsx

Repository: QueueLab/QCX

Length of output: 1771


Tool result annotations render at the top of the message list and accumulate across conversation turns.

useChat().data (source of annotations from the provider) accumulates all stream frames for the entire session. The route emits tool results as {type: 'tool_result', toolName, result} (lines 184–191 in app/api/chat/stream/route.ts) with no turn or message identifier attached.

In chat-messages.tsx, all tool annotations are pushed to renderedMessages first (lines 31–40), before iterating through messages (lines 40+). This causes:

  1. Multi-turn chats: Tool results from previous turns reappear at the top of the current view, above all user messages.
  2. Single turn: Tool results appear before the user query that triggered them, inverting the natural request → tool-output → answer flow.

To fix: emit a messageId or turnIndex with each tool_result annotation from the route, then filter annotations to only render those matching the current assistant message's turn, or move tool-result rendering into the message.toolInvocations loop (lines 63–78) if that source contains the correct per-message data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/chat-messages.tsx` around lines 31 - 40, Tool result annotations
are being rendered globally from annotations (useChat().data) causing old tool
outputs to accumulate; either have the backend include a per-message identifier
(e.g., messageId or turnIndex on tool_result) and then filter annotations by
that id before pushing to renderedMessages, or stop rendering all tool_result
entries up-front and instead render tool outputs inside the per-message loop
(use message.toolInvocations or the current message object) so you only render
tool results that belong to the current assistant message; update the rendering
logic around the annotations variable and the ToolResultRenderer invocations
(and adjust where renderedMessages is populated) to use the per-message
identifier or message.toolInvocations to scope tool results.


// Render chat messages
for (const message of messages) {
if (message.role === 'user') {
renderedMessages.push({
id: message.id,
component: <UserMessage content={message.content} />
})
} else if (message.role === 'assistant') {
if (message.content) {
renderedMessages.push({
id: message.id,
components: [],
isCollapsed: message.isCollapsed
component: (
<Section title="response">
<BotMessage content={message.content} />
</Section>
),
isAssistant: true
})
}

// Render tool invocations
if (message.toolInvocations) {
for (const invocation of message.toolInvocations) {
if (invocation.state === 'result') {
renderedMessages.push({
id: `${message.id}-tool-${invocation.toolCallId}`,
component: (
<ToolResultRenderer
toolName={invocation.toolName}
result={invocation.result}
/>
),
isCollapsed: true
})
}
}
}
acc[message.id].components.push(message.component)
return acc
},
{}
)
}
}

// Convert grouped messages into an array with explicit type
const groupedMessagesArray = Object.values(groupedMessages).map(group => ({
...group,
components: group.components as React.ReactNode[]
})) as {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}[]
// Render inquiry annotation if present
const inquiry = annotations.find((a: Annotation) => a.type === 'inquiry')
if (inquiry) {
renderedMessages.push({
id: 'inquiry',
component: <Copilot inquiry={{ value: inquiry.data }} />
})
}

// Render related queries annotation
const related = annotations.findLast?.((a: Annotation) => a.type === 'related')
if (related && related.relatedQueries?.items?.length > 0) {
renderedMessages.push({
id: 'related',
component: (
<Section title="Related" separator={true}>
<SearchRelated relatedQueries={related.relatedQueries} />
</Section>
)
})
}
Comment on lines +82 to +102
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.

Action required

1. Resolution results not rendered 🐞 Bug ≡ Correctness

/api/chat/stream emits a resolution_search_result data annotation, but the client never renders
that annotation type, so resolution-search UI (carousel/GeoJSON/map preview) will not appear.
Agent Prompt
## Issue description
Resolution search results are sent as `type: 'resolution_search_result'` annotations, but the UI never renders them, so users only see the summary text and lose the map/imagery output.

## Issue Context
The route handler emits a `resolution_search_result` annotation containing `{ image, mapboxImage, googleImage, geoJson?, ... }`. The client currently only handles `tool_result`, `inquiry`, and `related` annotations.

## Fix Focus Areas
- components/chat-messages.tsx[77-100]
- app/api/chat/stream/route.ts[235-260]

## What to implement
- Add a renderer branch for `annotation.type === 'resolution_search_result'`.
- Reuse existing UI components (e.g., `ResolutionCarousel`, `GeoJsonLayer`) to display imagery + optional GeoJSON.
- Ensure rendering order matches expectations (e.g., show carousel/overlay before/alongside summary).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Find last assistant message index for isLastMessage prop
let lastAssistantIndex = -1
for (let i = renderedMessages.length - 1; i >= 0; i--) {
if (renderedMessages[i].isAssistant) {
lastAssistantIndex = i
break
}
}

return (
<>
{groupedMessagesArray.map(
(
groupedMessage: {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
},
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
}
/>
)
)}
{renderedMessages.map((msg, index) => (
<CollapsibleMessage
key={msg.id}
message={{
id: msg.id,
component: <div>{msg.component}</div>,
isCollapsed: msg.isCollapsed
}}
isLastMessage={index === lastAssistantIndex}
/>
))}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</>
)
}
Loading