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
842 changes: 125 additions & 717 deletions app/actions.tsx

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Chat } from '@/components/chat'
import {nanoid } from 'nanoid'
import { AI } from './actions'

export const maxDuration = 60

Expand All @@ -9,10 +8,8 @@ import { MapDataProvider } from '@/components/map/map-data-context'
export default function Page() {
const id = nanoid()
return (
<AI initialAIState={{ chatId: id, messages: [] }}>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
</AI>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
)
}
30 changes: 5 additions & 25 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 { 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
Expand All @@ -15,8 +14,6 @@ export interface SearchPageProps {

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
return {
Expand All @@ -29,15 +26,12 @@ export default async function SearchPage({ params }: SearchPageProps) {
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();
}

Expand All @@ -48,29 +42,15 @@ export default async function SearchPage({ params }: SearchPageProps) {
const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => {
return {
id: dbMsg.id,
role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities
role: dbMsg.role as AIMessage['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.
}}
>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
</AI>
<MapDataProvider>
<Chat id={id} initialMessages={initialMessages} />
</MapDataProvider>
);
}
}
193 changes: 105 additions & 88 deletions bun.lock

Large diffs are not rendered by default.

157 changes: 105 additions & 52 deletions components/chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,123 @@
'use client'

import { StreamableValue, useUIState } from 'ai/rsc'
import type { AI, UIState } from '@/app/actions'
import { Message } from 'ai'
import { CollapsibleMessage } from './collapsible-message'
import { UserMessage } from './user-message'
import { BotMessage } from './message'
import { Section } from './section'
import SearchRelated from './search-related'
import { FollowupPanel } from './followup-panel'
import { SearchSection } from './search-section'
import RetrieveSection from './retrieve-section'
import { VideoSearchSection } from './video-search-section'
import { MapQueryHandler } from './map/map-query-handler'
import { CopilotDisplay } from './copilot-display'
import { ResolutionSearchSection } from './resolution-search-section'

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

export function ChatMessages({ messages }: ChatMessagesProps) {
if (!messages.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] = {
id: message.id,
components: [],
isCollapsed: message.isCollapsed
return (
<>
{messages.map((message, index) => {
const { role, content, id, toolInvocations, data } = message

if (role === 'user') {
return (
<CollapsibleMessage
key={id}
message={{
id,
component: (
<UserMessage
content={content}
showShare={index === 0}
/>
)
}}
isLastMessage={index === messages.length - 1}
/>
)
}
}
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>
}[]
if (role === 'assistant') {
const extraData = Array.isArray(data) ? data : []

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
}
/>
)
)}
return (
<CollapsibleMessage
key={id}
message={{
id,
component: (
<div className="flex flex-col gap-4">
{content && (
<Section title="response">
<BotMessage content={content} />
</Section>
)}

{toolInvocations?.map((toolInvocation) => {
const { toolName, toolCallId, state } = toolInvocation

if (state === 'result') {
const { result } = toolInvocation

switch (toolName) {
case 'search':
return <SearchSection key={toolCallId} result={JSON.stringify(result)} />
case 'retrieve':
return <RetrieveSection key={toolCallId} data={result} />
case 'videoSearch':
return <VideoSearchSection key={toolCallId} result={JSON.stringify(result)} />
case 'geospatialQueryTool':
if (result.type === 'MAP_QUERY_TRIGGER') {
return <MapQueryHandler key={toolCallId} toolOutput={result} />
}
return null
default:
return null
}
}
return null
})}
Comment on lines +65 to +88

Choose a reason for hiding this comment

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

Tool implementations now return { error: string } objects on failure, but ChatMessages renders tool results by passing result into SearchSection/RetrieveSection/VideoSearchSection without checking for an error shape. This will likely cause runtime errors or confusing empty sections.

At minimum, error payloads need consistent rendering.

Suggestion

Handle tool error results explicitly in ChatMessages:

  • If result?.error, render a Section with an error Card/message.
  • Otherwise render the normal section.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing consistent tool error rendering.


{extraData.map((d: any, i) => {
if (d.type === 'related') {
return (
<Section key={i} title="Related" separator={true}>
<SearchRelated relatedQueries={d.object} />
</Section>
)
}
if (d.type === 'inquiry') {
return <CopilotDisplay key={i} content={d.object.question} />
}
if (d.type === 'resolution_search_result') {
return <ResolutionSearchSection key={i} result={d.object} />
}
return null
})}

{index === messages.length - 1 && role === 'assistant' && (
<Section title="Follow-up" className="pb-8">
<FollowupPanel />
</Section>
)}
</div>
)
}}
isLastMessage={index === messages.length - 1}
/>
)
}
return null
})}
</>
)
}
Loading