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
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"@upstash/redis": "^1.35.0",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"QCX": ".",
"ai": "^4.3.19",
"build": "^0.1.4",
"class-variance-authority": "^0.7.1",
Expand Down Expand Up @@ -1032,8 +1031,6 @@

"@vercel/speed-insights": ["@vercel/speed-insights@1.2.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw=="],

"QCX": ["QCX@file:", { "dependencies": { "@ai-sdk/amazon-bedrock": "^1.1.6", "@ai-sdk/anthropic": "^1.2.12", "@ai-sdk/google": "^1.2.22", "@ai-sdk/openai": "^1.3.24", "@ai-sdk/xai": "^1.2.18", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.0.1", "@mapbox/mapbox-gl-draw": "^1.5.0", "@modelcontextprotocol/sdk": "^1.13.0", "@radix-ui/react-alert-dialog": "^1.1.10", "@radix-ui/react-avatar": "^1.1.6", "@radix-ui/react-checkbox": "^1.2.2", "@radix-ui/react-collapsible": "^1.1.7", "@radix-ui/react-dialog": "^1.1.10", "@radix-ui/react-dropdown-menu": "^2.1.11", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-radio-group": "^1.3.4", "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slider": "^1.3.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-switch": "^1.2.2", "@radix-ui/react-tabs": "^1.1.9", "@radix-ui/react-toast": "^1.2.11", "@radix-ui/react-tooltip": "^1.2.3", "@smithery/cli": "^1.2.5", "@smithery/sdk": "^1.0.4", "@supabase/ssr": "^0.3.0", "@supabase/supabase-js": "^2.0.0", "@tailwindcss/typography": "^0.5.16", "@turf/turf": "^7.2.0", "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", "QCX": ".", "ai": "^4.3.19", "build": "^0.1.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cookie": "^0.6.0", "dotenv": "^16.5.0", "drizzle-kit": "^0.31.1", "drizzle-orm": "^0.29.0", "embla-carousel-react": "^8.6.0", "exa-js": "^1.6.13", "framer-motion": "^12.15.0", "katex": "^0.16.22", "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", "next": "^15.3.3", "next-themes": "^0.3.0", "open-codex": "^0.1.30", "pg": "^8.16.2", "radix-ui": "^1.3.4", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.56.2", "react-icons": "^5.5.0", "react-markdown": "^9.1.0", "react-textarea-autosize": "^8.5.9", "react-toastify": "^10.0.6", "rehype-external-links": "^3.0.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "smithery": "^0.5.2", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "use-mcp": "^0.0.9", "uuid": "^9.0.0", "zod": "^3.23.8" }, "devDependencies": { "@types/cookie": "^0.6.0", "@types/mapbox-gl": "^3.4.1", "@types/node": "^20.17.30", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/uuid": "^9.0.0", "cross-env": "^7.0.3", "eslint": "^8.57.1", "eslint-config-next": "^14.2.28", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.3" } }],

"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],

"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
Expand Down Expand Up @@ -2568,8 +2565,6 @@

"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],

"QCX/QCX": ["QCX@file:.", {}],

"ai/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],

"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
Expand Down
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])
}

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