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
445 changes: 242 additions & 203 deletions app/actions.tsx

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { Chat } from '@/components/chat'
import {nanoid } from 'nanoid'
import { AI } from './actions'
import { AI, AIState } from './actions'

export const maxDuration = 60

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

export default function Page() {
const id = nanoid()
const initialAIState: AIState = {
conversations: [
{
id: nanoid(),
chatId: id,
messages: []
}
]
}
return (
<AI initialAIState={{ chatId: id, messages: [] }}>
<AI initialAIState={initialAIState}>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
Expand Down
67 changes: 35 additions & 32 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,79 @@
import { nanoid } from 'nanoid';
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 { getChat, getChatMessages } from '@/lib/actions/chat';
import { AI, AIState } 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
import type { Message as DrizzleMessage } from '@/lib/actions/chat-db'; // For DrizzleMessage type
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
import type { AIMessage } from '@/lib/types';
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 }>;
}

const validRoles: AIMessage['role'][] = ['user', 'assistant', 'system', 'function', 'data', 'tool'];

function safeGetRole(role: string): AIMessage['role'] {
if (validRoles.includes(role as AIMessage['role'])) {
return role as AIMessage['role'];
}
console.warn(`Invalid role "${role}" found in database, defaulting to 'user'.`);
return 'user';
}


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 {
id: dbMsg.id,
role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities
role: safeGetRole(dbMsg.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={{
const initialAIState = {
conversations: [
{
id: nanoid(),
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.
}}
>
messages: initialMessages,
}
]
} satisfies AIState;

return (
<AI initialAIState={initialAIState}>
<MapDataProvider>
<Chat id={id} />
</MapDataProvider>
</AI>
);
}
}
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.

38 changes: 24 additions & 14 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface ChatPanelRef {

export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, input, setInput }, ref) => {
const [, setMessages] = useUIState<typeof AI>()
const { submit, clearChat } = useActions()
const { submit } = useActions()
// Removed mcp instance as it's no longer passed to submit
const [isMobile, setIsMobile] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
Expand Down Expand Up @@ -69,7 +69,10 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
}
}

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>,
newChat?: boolean
) => {
e.preventDefault()
if (!input && !selectedFile) {
return
Expand All @@ -86,18 +89,23 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
})
}

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

const formData = new FormData(e.currentTarget)
if (selectedFile) {
formData.append('file', selectedFile)
}
if (newChat) {
formData.append('newChat', 'true')
}

setInput('')
clearAttachment()
Expand All @@ -106,10 +114,12 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
setMessages(currentMessages => [...currentMessages, responseMessage as any])
}

const handleClear = async () => {
setMessages([])
const handleNewConversation = async () => {
clearAttachment()
await clearChat()
const formData = new FormData()
formData.append('newChat', 'true')
const responseMessage = await submit(formData)
setMessages(currentMessages => [...currentMessages, responseMessage as any])
}
Comment on lines +117 to 123
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Starting a new conversation currently adds a server response to the local UI state even though the server returns no UI content for a pure newChat request. This yields a blank message item in the timeline after clearing, which is confusing and unnecessary.

Suggestion

Avoid appending the server response for a newChat-only submit. You can still notify the server to create the conversation without pushing a UI item:

const handleNewConversation = async () => {
setMessages([]);
clearAttachment();
const formData = new FormData();
formData.append('newChat', 'true');
await submit(formData); // no UI append here
};

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

Comment on lines +117 to 123
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

Avoid clearing local UI state to prevent flicker when starting a new conversation

setMessages([]) wipes the UI until the server-rendered state returns, causing a perceptible flash. Prefer leaving the existing UI and simply appending the server response for the new conversation.

Apply this diff:

-  const handleNewConversation = async () => {
-    setMessages([])
-    clearAttachment()
-    const formData = new FormData()
-    formData.append('newChat', 'true')
-    const responseMessage = await submit(formData)
-    setMessages(currentMessages => [...currentMessages, responseMessage as any])
-  }
+  const handleNewConversation = async () => {
+    clearAttachment()
+    const formData = new FormData()
+    formData.append('newChat', 'true')
+    const responseMessage = await submit(formData)
+    setMessages(currentMessages => [...currentMessages, responseMessage as any])
+  }

If you need a visual separator immediately, the server will add one via onGetUIState; no client wipe needed.

📝 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
const handleNewConversation = async () => {
setMessages([])
clearAttachment()
await clearChat()
const formData = new FormData()
formData.append('newChat', 'true')
const responseMessage = await submit(formData)
setMessages(currentMessages => [...currentMessages, responseMessage as any])
}
const handleNewConversation = async () => {
clearAttachment()
const formData = new FormData()
formData.append('newChat', 'true')
const responseMessage = await submit(formData)
setMessages(currentMessages => [...currentMessages, responseMessage as any])
}
🤖 Prompt for AI Agents
In components/chat-panel.tsx around lines 117 to 124, remove the client-side
wipe that causes UI flicker by deleting the setMessages([]) call; instead, keep
the existing messages and append the server responseMessage to the
currentMessages (as you're already doing), so the UI is not cleared while
waiting for server-rendered state. Leave clearAttachment() only if you still
want attachments cleared, otherwise remove it too; ensure the final behavior is
to call submit(formData) and then setMessages(current => [...current,
responseMessage as any]) without first setting messages to an empty array.


useEffect(() => {
Expand All @@ -129,10 +139,10 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
type="button"
variant={'secondary'}
className="rounded-full bg-secondary/80 group transition-all hover:scale-105 pointer-events-auto"
onClick={() => handleClear()}
onClick={handleNewConversation}
>
<span className="text-sm mr-2 group-hover:block hidden animate-in fade-in duration-300">
New
New Conversation
</span>
<Plus size={18} className="group-hover:rotate-90 transition-all" />
</Button>
Expand Down
17 changes: 13 additions & 4 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,20 @@ export function Chat({ id }: ChatProps) {
}, [id, path, messages])

useEffect(() => {
if (aiState.messages[aiState.messages.length - 1]?.type === 'response') {
// Refresh the page to chat history updates
router.refresh()
if (
aiState.conversations &&
aiState.conversations.length > 0
) {
const lastConversation = aiState.conversations[aiState.conversations.length - 1];
if (lastConversation && lastConversation.messages.length > 0) {
const lastMessage = lastConversation.messages[lastConversation.messages.length - 1];
if (lastMessage?.type === 'response') {
// Refresh the page to chat history updates
router.refresh();
}
}
}
}, [aiState, router])
}, [aiState, router]);

// Get mapData to access drawnFeatures
const { mapData } = useMapData();
Expand Down
2 changes: 1 addition & 1 deletion components/map-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function MapToggle() {
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.FreeMode)}}>
My Maps
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.DrawingMode)}}>
<DropdownMenuItem data-testid="drawing-mode-button" onClick={() => {setMapType(MapToggleEnum.DrawingMode)}}>
<Pencil className="h-[1rem] w-[1rem] mr-2" />
Draw & Measure
</DropdownMenuItem>
Expand Down
Loading