From c4809710c4122b15c432b10c348437feee6fd795 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:22:05 +0000 Subject: [PATCH 01/10] feat: synchronize main with auth/backend logic Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .env | 6 +- .gitignore | 66 ++- .vscode/settings.json | 2 +- README.md | 2 +- app/actions.tsx | 478 ++++++++++------- app/api/chat/route.ts | 64 +-- app/layout.tsx | 72 ++- app/page.tsx | 5 +- app/search/[id]/page.tsx | 7 +- bun.lock | 571 +++++++++++---------- components/calendar-notepad.tsx | 3 + components/chat-panel.tsx | 14 +- components/chat.tsx | 223 +++----- components/clear-history.tsx | 3 - components/conditional-lottie.tsx | 4 +- components/followup-panel.tsx | 8 +- components/header-search-button.tsx | 6 +- components/header.tsx | 74 +-- components/history-sidebar.tsx | 35 ++ components/history-toggle-context.tsx | 30 ++ components/map/map-3d.tsx | 4 + components/map/map-data-context.tsx | 1 + components/map/mapbox-map.tsx | 238 ++++----- components/mobile-icons-bar.tsx | 21 +- components/profile-toggle-context.tsx | 11 +- components/profile-toggle.tsx | 18 +- components/purchase-credits-popup.tsx | 73 +++ components/resolution-image.tsx | 60 +++ components/search-related.tsx | 35 +- components/sidebar.tsx | 2 +- components/sidebar/chat-history-client.tsx | 43 +- components/timezone-clock.tsx | 55 ++ components/usage-toggle-context.tsx | 30 ++ components/usage-view.tsx | 96 ++++ config/pricing.json | 4 +- drizzle.config.ts | 19 + lib/actions/calendar.ts | 20 +- lib/actions/chat.ts | 117 ++--- lib/actions/users.ts | 125 ++++- lib/agents/researcher.tsx | 12 +- lib/agents/resolution-search.tsx | 37 +- lib/agents/tools/geospatial.tsx | 21 +- lib/auth/get-current-user.ts | 2 +- lib/db/index.ts | 9 +- lib/supabase/persistence.ts | 56 ++ lib/utils/index.ts | 2 - proxy.ts => middleware.ts | 19 +- next-env.d.ts | 2 +- next.config.mjs | 2 +- package.json | 2 + tests/map.spec.ts | 5 + 51 files changed, 1747 insertions(+), 1067 deletions(-) create mode 100644 components/history-sidebar.tsx create mode 100644 components/history-toggle-context.tsx create mode 100644 components/purchase-credits-popup.tsx create mode 100644 components/resolution-image.tsx create mode 100644 components/timezone-clock.tsx create mode 100644 components/usage-toggle-context.tsx create mode 100644 components/usage-view.tsx create mode 100644 drizzle.config.ts rename proxy.ts => middleware.ts (54%) diff --git a/.env b/.env index 0ae69d85..967be3d1 100644 --- a/.env +++ b/.env @@ -2,6 +2,6 @@ AUTH_DISABLED_FOR_DEV=false DATABASE_URL="postgresql://user:password@host:port/db" SERVER_ACTIONS_ALLOWED_ORIGINS=* STANDARD_TIER_BILLING_CYCLE="yearly" -STANDARD_TIER_CREDITS=8000 -STANDARD_TIER_MONTHLY_PRICE=41 -STANDARD_TIER_PRICE_ID="price_standard_41_yearly" +STANDARD_TIER_CREDITS=500 +STANDARD_TIER_MONTHLY_PRICE=500 +STANDARD_TIER_PRICE_ID="price_standard_500_yearly" diff --git a/.gitignore b/.gitignore index 6ef8344c..3401bf59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,39 @@ -# Dependency directories -node_modules/ +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz .bun/ -# Build outputs -.next/ -dist/ -build/ -out/ +# testing +/coverage +/playwright-report/ +/test-results/ -# Environment variables +# next.js +/.next/ +/out/ + +# production +/build +/dist + +# misc +.DS_Store +*.pem +*.swp +*.swo + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +dev_server.log +server.log +dev.log +*.log + +# local env files .env .env.local .env.development.local @@ -19,26 +44,21 @@ out/ # IDE/Editor .vscode/ .idea/ -*.swp -*.swo -.DS_Store - -# Logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* # Lock files bun.lockb -# Testing -playwright-report/ -test-results/ -coverage/ - # Supabase local CLI state supabase/.temp/ -# Misc -.vercel/ +# vercel +.vercel + +# typescript *.tsbuildinfo +next-env.d.ts + +# AlphaEarth Embeddings - Sensitive Files +gcp_credentials.json +**/gcp_credentials.json +aef_index.csv diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b736623..89d1965f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" -} +} \ No newline at end of file diff --git a/README.md b/README.md index 3eb5e187..d090ea1f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ -[**Pricing**]  |  [**Land**](https://wwww.queue.cx)  |  [**X**](https://x.com/tryqcx) +[**Pricing**](https://buy.stripe.com/14A3cv7K72TR3go14Nasg02)  |  [**Land**](https://wwww.queue.cx)  |  [**X**](https://x.com/tryqcx) QCX - Artificial General Intelligence. | Product Hunt diff --git a/app/actions.tsx b/app/actions.tsx index 929644ba..62b5fefe 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -12,11 +12,9 @@ import type { FeatureCollection } from 'geojson' import { Spinner } from '@/components/ui/spinner' import { Section } from '@/components/section' import { FollowupPanel } from '@/components/followup-panel' -import { inquire, researcher, taskManager, querySuggestor, resolutionSearch } from '@/lib/agents' -// Removed import of useGeospatialToolMcp as it no longer exists and was incorrectly used here. -// The geospatialTool (if used by agents like researcher) now manages its own MCP client. +import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents' import { writer } from '@/lib/agents/writer' -import { saveChat, getSystemPrompt } from '@/lib/actions/chat' // Added getSystemPrompt +import { saveChat, getSystemPrompt } from '@/lib/actions/chat' import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user' import { retrieveContext } from '@/lib/actions/rag' import { Chat, AIMessage } from '@/lib/types' @@ -25,17 +23,17 @@ import { BotMessage } from '@/components/message' import { SearchSection } from '@/components/search-section' import SearchRelated from '@/components/search-related' import { GeoJsonLayer } from '@/components/map/geojson-layer' +import { ResolutionImage } from '@/components/resolution-image' import { CopilotDisplay } from '@/components/copilot-display' import RetrieveSection from '@/components/retrieve-section' import { VideoSearchSection } from '@/components/video-search-section' -import { MapQueryHandler } from '@/components/map/map-query-handler' // Add this import +import { MapQueryHandler } from '@/components/map/map-query-handler' // Define the type for related queries type RelatedQueries = { items: { query: string }[] } -// Removed mcp parameter from submit, as geospatialTool now handles its client. async function submit(formData?: FormData, skip?: boolean) { 'use server' @@ -45,8 +43,18 @@ async function submit(formData?: FormData, skip?: boolean) { const isCollapsed = createStreamableValue(false) const action = formData?.get('action') as string; + const drawnFeaturesString = formData?.get('drawnFeatures') as string; + let drawnFeatures: DrawnFeature[] = []; + try { + drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; + } catch (e) { + console.error('Failed to parse drawnFeatures:', e); + } + if (action === 'resolution_search') { const file = formData?.get('file') as File; + const timezone = (formData?.get('timezone') as string) || 'UTC'; + if (!file) { throw new Error('No file provided for resolution search.'); } @@ -54,102 +62,141 @@ async function submit(formData?: FormData, skip?: boolean) { const buffer = await file.arrayBuffer(); const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; - // Get the current messages, excluding tool-related ones. const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( message => message.role !== 'tool' && message.type !== 'followup' && message.type !== 'related' && - message.type !== 'end' + message.type !== 'end' && + message.type !== 'resolution_search_result' ); - // The user's prompt for this action is static. - const userInput = 'Analyze this map view.'; - - // Construct the multimodal content for the user message. + const userInputRes = 'Analyze this map view.'; const content: CoreMessage['content'] = [ - { type: 'text', text: userInput }, + { type: 'text', text: userInputRes }, { type: 'image', image: dataUrl, mimeType: file.type } ]; - // Add the new user message to the AI state. aiState.update({ ...aiState.get(), messages: [ ...aiState.get().messages, - { id: nanoid(), role: 'user', content: JSON.stringify(content) } + { id: nanoid(), role: 'user', content, type: 'input' } ] }); messages.push({ role: 'user', content }); - // Call the simplified agent, which now returns data directly. - const analysisResult = await resolutionSearch(messages) as any; + const summaryStream = createStreamableValue('Analyzing map view...'); + const groupeId = nanoid(); - // Create a streamable value for the summary and mark it as done. - const summaryStream = createStreamableValue(); - summaryStream.done(analysisResult.summary || 'Analysis complete.'); + async function processResolutionSearch() { + try { + const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); - // Update the UI stream with the BotMessage component. - uiStream.update( - - ); + let fullSummary = ''; + for await (const partialObject of streamResult.partialObjectStream) { + if (partialObject.summary) { + fullSummary = partialObject.summary; + summaryStream.update(fullSummary); + } + } - messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); + const analysisResult = await streamResult.object; + summaryStream.done(analysisResult.summary || 'Analysis complete.'); - const sanitizedMessages: CoreMessage[] = messages.map(m => { - if (Array.isArray(m.content)) { - return { - ...m, - content: m.content.filter(part => part.type !== 'image') - } as CoreMessage - } - return m - }) + if (analysisResult.geoJson) { + uiStream.append( + + ); + } - const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); - uiStream.append( -
- -
- ); + messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); - await new Promise(resolve => setTimeout(resolve, 500)); + const sanitizedMessages: CoreMessage[] = messages.map(m => { + if (Array.isArray(m.content)) { + return { + ...m, + content: m.content.filter((part: any) => part.type !== 'image') + } as CoreMessage + } + return m + }) - const groupeId = nanoid(); + const currentMessages = aiState.get().messages; + const sanitizedHistory = currentMessages.map(m => { + if (m.role === "user" && Array.isArray(m.content)) { + return { + ...m, + content: m.content.map((part: any) => + part.type === "image" ? { ...part, image: "IMAGE_PROCESSED" } : part + ) + } + } + return m + }); + const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); + uiStream.append( +
+ +
+ ); - aiState.done({ - ...aiState.get(), - messages: [ + await new Promise(resolve => setTimeout(resolve, 500)); + + aiState.done({ + ...aiState.get(), + messages: [ ...aiState.get().messages, { - id: groupeId, - role: 'assistant', - content: analysisResult.summary || 'Analysis complete.', - type: 'response' + id: groupeId, + role: 'assistant', + content: analysisResult.summary || 'Analysis complete.', + type: 'response' }, { - id: groupeId, - role: 'assistant', - content: JSON.stringify(analysisResult), - type: 'resolution_search_result' + id: groupeId, + role: 'assistant', + content: JSON.stringify({ + ...analysisResult, + image: dataUrl + }), + type: 'resolution_search_result' }, { - id: groupeId, - role: 'assistant', - content: JSON.stringify(relatedQueries), - type: 'related' + id: groupeId, + role: 'assistant', + content: JSON.stringify(relatedQueries), + type: 'related' }, { - id: groupeId, - role: 'assistant', - content: 'followup', - type: 'followup' + id: groupeId, + role: 'assistant', + content: 'followup', + type: 'followup' } - ] - }); + ] + }); + } catch (error) { + console.error('Error in resolution search:', error); + summaryStream.error(error); + } finally { + isGenerating.done(false); + uiStream.done(); + } + } + + processResolutionSearch(); + + uiStream.update( +
+ + +
+ ); - isGenerating.done(false); - uiStream.done(); return { id: nanoid(), isGenerating: isGenerating.value, @@ -163,8 +210,19 @@ async function submit(formData?: FormData, skip?: boolean) { message.role !== 'tool' && message.type !== 'followup' && message.type !== 'related' && - message.type !== 'end' - ) + message.type !== 'end' && + message.type !== 'resolution_search_result' + ).map(m => { + if (Array.isArray(m.content)) { + return { + ...m, + content: m.content.filter((part: any) => + part.type !== "image" || (typeof part.image === "string" && part.image.startsWith("data:")) + ) + } as any + } + return m + }) const groupeId = nanoid() const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true' @@ -207,9 +265,8 @@ async function submit(formData?: FormData, skip?: boolean) { ); - uiStream.append(answerSection); + uiStream.update(answerSection); - const groupeId = nanoid(); const relatedQueries = { items: [] }; aiState.done({ @@ -293,7 +350,6 @@ async function submit(formData?: FormData, skip?: boolean) { } const hasImage = messageParts.some(part => part.type === 'image') - // Properly type the content based on whether it contains images const content: CoreMessage['content'] = hasImage ? messageParts as CoreMessage['content'] : messageParts.map(part => part.text).join('\n') @@ -314,7 +370,7 @@ async function submit(formData?: FormData, skip?: boolean) { { id: nanoid(), role: 'user', - content: typeof content === 'string' ? content : JSON.stringify(content), + content, type } ] @@ -326,17 +382,11 @@ async function submit(formData?: FormData, skip?: boolean) { } const userId = await getCurrentUserIdOnServer() - if (!userId) { - throw new Error('Unauthorized') - } - const currentSystemPrompt = (await getSystemPrompt(userId)) || '' - - const retrievedContext = userInput - ? await retrieveContext(userInput, aiState.get().chatId) - : [] - const augmentedSystemPrompt = retrievedContext.length > 0 - ? `Context: ${retrievedContext.join('\n')}\n${currentSystemPrompt}` - : currentSystemPrompt + if (!userId) throw new Error('Unauthorized') + const userInputAction = formData?.get('input') as string; + const currentSystemPrompt = (await getSystemPrompt(userId)) || ''; + const retrievedContext = userInputAction ? await retrieveContext(userInput, aiState.get().chatId) : []; + const augmentedSystemPrompt = retrievedContext.length > 0 ? `Context: ${retrievedContext.join('\n')}\n${currentSystemPrompt}` : currentSystemPrompt; const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' async function processEvents() { @@ -380,12 +430,13 @@ async function submit(formData?: FormData, skip?: boolean) { : answer.length === 0 && !errorOccurred ) { const { fullResponse, hasError, toolResponses } = await researcher( - augmentedSystemPrompt, + currentSystemPrompt, uiStream, streamText, messages, mapProvider, - useSpecificAPI + useSpecificAPI, + drawnFeatures ) answer = fullResponse toolOutputs = toolResponses @@ -442,6 +493,8 @@ async function submit(formData?: FormData, skip?: boolean) { ) + await new Promise(resolve => setTimeout(resolve, 500)) + aiState.done({ ...aiState.get(), messages: [ @@ -466,8 +519,6 @@ async function submit(formData?: FormData, skip?: boolean) { } ] }) - } else { - aiState.done(aiState.get()) } isGenerating.done(false) @@ -486,35 +537,19 @@ async function submit(formData?: FormData, skip?: boolean) { async function clearChat() { 'use server' + const aiState = getMutableAIState() + aiState.done({ chatId: nanoid(), messages: [] }) } -export type Message = { - id: string - role: 'user' | 'assistant' | 'system' | 'tool' | 'function' | 'data' - content: string | any[] - name?: string - type?: - | 'response' - | 'inquiry' - | 'related' - | 'followup' - | 'input' - | 'input_related' - | 'tool' - | 'resolution_search_result' - | 'skip' - | 'end' - | 'drawing_context' -} - export type AIState = { + messages: AIMessage[] chatId: string - messages: Message[] + isSharePage?: boolean } export type UIState = { @@ -524,68 +559,100 @@ export type UIState = { isCollapsed?: StreamableValue }[] +const initialAIState: AIState = { + chatId: nanoid(), + messages: [] +} + +const initialUIState: UIState = [] + export const AI = createAI({ actions: { submit, clearChat }, - initialUIState: [], - initialAIState: { chatId: nanoid(), messages: [] }, + initialUIState, + initialAIState, onGetUIState: async () => { 'use server' - const aiState = getAIState() - + const aiState = getAIState() as AIState if (aiState) { - const uiState = getUIStateFromAIState(aiState as Chat) + const uiState = getUIStateFromAIState(aiState) return uiState - } else { - return } + return initialUIState }, - onSetAIState: async ({ state, done }) => { + onSetAIState: async ({ state }) => { 'use server' + if (!state.messages.some(e => e.type === 'response')) { + return + } + const { chatId, messages } = state + const createdAt = new Date() + const path = `/search/${chatId}` + + let title = 'Untitled Chat' + if (messages.length > 0) { + const firstMessageContent = messages[0].content + if (typeof firstMessageContent === 'string') { + try { + const parsedContent = JSON.parse(firstMessageContent) + title = parsedContent.input?.substring(0, 100) || 'Untitled Chat' + } catch (e) { + title = firstMessageContent.substring(0, 100) + } + } else if (Array.isArray(firstMessageContent)) { + const textPart = ( + firstMessageContent as { type: string; text?: string }[] + ).find(p => p.type === 'text') + title = + textPart && textPart.text + ? textPart.text.substring(0, 100) + : 'Image Message' + } + } - const userId = await getCurrentUserIdOnServer() + const updatedMessages: AIMessage[] = [ + ...messages, + { + id: nanoid(), + role: 'assistant', + content: `end`, + type: 'end' + } + ] - if (!userId) { + const { getCurrentUserIdOnServer } = await import( + '@/lib/auth/get-current-user' + ) + const actualUserId = await getCurrentUserIdOnServer() + + if (!actualUserId) { + console.error('onSetAIState: User not authenticated. Chat not saved.') return } - const lastMessage = messages[messages.length - 1] - if (lastMessage && lastMessage.role === 'assistant' && done) { - const chat: Chat = { - id: chatId, - title: typeof messages[0].content === 'string' - ? messages[0].content.substring(0, 100) - : 'New Chat', - userId, - createdAt: new Date(), - messages: messages as any, // Cast to any to avoid type conflict with Chat interface - path: `/search/${chatId}` - } - - await saveChat(chat, userId) + const chat: Chat = { + id: chatId, + createdAt, + userId: actualUserId, + path, + title, + messages: updatedMessages } + await saveChat(chat, actualUserId) } }) -export const getUIStateFromAIState = (aiState: Chat) => { - const chatId = aiState.id - const isSharePage = false // Defaulting to false as it's not defined - - const messages = aiState.messages - .filter( - message => - message.role !== 'system' && - message.role !== 'tool' && - message.type !== 'followup' && - message.type !== 'related' - ) +export const getUIStateFromAIState = (aiState: AIState): UIState => { + const chatId = aiState.chatId + const isSharePage = aiState.isSharePage + return aiState.messages .map((message, index) => { - const { role, content, id, type } = message + const { role, content, id, type, name } = message if ( !type || @@ -602,17 +669,10 @@ export const getUIStateFromAIState = (aiState: Chat) => { case 'input_related': let messageContent: string | any[] try { - // For backward compatibility with old messages that stored a JSON string - const parsed = JSON.parse(content as string) - if (Array.isArray(parsed)) { - messageContent = parsed - } else if (typeof parsed === 'object' && parsed !== null) { - messageContent = type === 'input' ? parsed.input : parsed.related_query - } else { - messageContent = parsed - } + const json = JSON.parse(content as string) + messageContent = + type === 'input' ? json.input : json.related_query } catch (e) { - // New messages will store the content array or string directly messageContent = content } return { @@ -633,10 +693,10 @@ export const getUIStateFromAIState = (aiState: Chat) => { } break case 'assistant': + const answer = createStreamableValue(content as string) + answer.done(content as string) switch (type) { case 'response': - const answer = createStreamableValue() - answer.done(content) return { id, component: ( @@ -646,7 +706,9 @@ export const getUIStateFromAIState = (aiState: Chat) => { ) } case 'related': - const relatedQueries = createStreamableValue() + const relatedQueries = createStreamableValue({ + items: [] + }) relatedQueries.done(JSON.parse(content as string)) return { id, @@ -666,21 +728,15 @@ export const getUIStateFromAIState = (aiState: Chat) => { ) } case 'resolution_search_result': { - let analysisResult: any = {} - try { - analysisResult = JSON.parse(content as string); - } catch (e) { - // Not JSON - } + const analysisResult = JSON.parse(content as string); const geoJson = analysisResult.geoJson as FeatureCollection; - const summaryStream = createStreamableValue() - summaryStream.done(analysisResult.summary || 'Analysis complete.') + const image = analysisResult.image as string; return { id, component: ( <> - + {image && } {geoJson && ( )} @@ -688,35 +744,87 @@ export const getUIStateFromAIState = (aiState: Chat) => { ) } } - default: { - // Handle generic assistant messages that might not have a specific type or are 'answer' type - // Handle content that is not a string (e.g., array of parts) - let displayContent: string = '' - if (typeof content === 'string') { - displayContent = content - } else if (Array.isArray(content)) { - // Convert array content to string representation or extract text - displayContent = content.map(part => { - if ('text' in part) return part.text - return '' - }).join('\n') - } - - const contentStream = createStreamableValue() - contentStream.done(displayContent) + } + break + case 'tool': + try { + const toolOutput = JSON.parse(content as string) + const isCollapsed = createStreamableValue(true) + isCollapsed.done(true) + + if ( + toolOutput.type === 'MAP_QUERY_TRIGGER' && + name === 'geospatialQueryTool' + ) { + const mapUrl = toolOutput.mcp_response?.mapUrl; + const placeName = toolOutput.mcp_response?.location?.place_name; + + return { + id, + component: ( + <> + {mapUrl && ( + + )} + + + ), + isCollapsed: false + } + } + const searchResults = createStreamableValue( + JSON.stringify(toolOutput) + ) + searchResults.done(JSON.stringify(toolOutput)) + switch (name) { + case 'search': return { id, - component: + component: , + isCollapsed: isCollapsed.value } + case 'retrieve': + return { + id, + component: , + isCollapsed: isCollapsed.value + } + case 'videoSearch': + return { + id, + component: ( + + ), + isCollapsed: isCollapsed.value + } + default: + console.warn( + `Unhandled tool result in getUIStateFromAIState: ${name}` + ) + return { id, component: null } + } + } catch (error) { + console.error( + 'Error parsing tool content in getUIStateFromAIState:', + error + ) + return { + id, + component: null } } break default: - return null + return { + id, + component: null + } } }) .filter(message => message !== null) as UIState - - return messages } diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 01aa7470..6d9ec11a 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,55 +1,21 @@ -import { NextResponse, NextRequest } from 'next/server'; -import { saveChat } from '@/lib/actions/chat'; -import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; -import { type Chat } from '@/lib/types'; -import { v4 as uuidv4 } from 'uuid'; +import { getModel } from '@/lib/utils' +import { LanguageModel, streamText } from 'ai' +import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user' -export async function POST(request: NextRequest) { - try { - const userId = await getCurrentUserIdOnServer(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } +export const maxDuration = 60 - const body = await request.json(); - const { title, initialMessageContent, role = 'user' } - = body; +export async function POST(req: Request) { + const { messages } = await req.json() + const userId = await getCurrentUserIdOnServer() - if (!initialMessageContent) { - return NextResponse.json({ error: 'Initial message content is required' }, { status: 400 }); - } - - const chatId = uuidv4(); - const newChat: Chat = { - id: chatId, - userId: userId, - title: title || 'New Chat', - createdAt: new Date(), - path: `/search/${chatId}`, - messages: [ - { - id: uuidv4(), - role: role, - content: initialMessageContent, - createdAt: new Date(), - } - ] - }; - - const savedChatId = await saveChat(newChat, userId); - - if (!savedChatId) { - return NextResponse.json({ error: 'Failed to save chat' }, { status: 500 }); - } + if (!userId) { + return new Response('Unauthorized', { status: 401 }) + } - return NextResponse.json({ message: 'Chat created successfully', chatId: savedChatId }, { status: 201 }); + const result = await streamText({ + model: (await getModel()) as LanguageModel, + messages, + }) - } catch (error) { - console.error('Error in POST /api/chat:', error); - let errorMessage = 'Internal Server Error'; - if (error instanceof Error) { - errorMessage = error.message; - } - return NextResponse.json({ error: errorMessage }, { status: 500 }); - } + return result.toDataStreamResponse() } diff --git a/app/layout.tsx b/app/layout.tsx index 766cd265..b1c203d8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,10 +12,13 @@ import { SpeedInsights } from "@vercel/speed-insights/next" import { Toaster } from '@/components/ui/sonner' import { MapToggleProvider } from '@/components/map-toggle-context' import { ProfileToggleProvider } from '@/components/profile-toggle-context' +import { UsageToggleProvider } from '@/components/usage-toggle-context' import { CalendarToggleProvider } from '@/components/calendar-toggle-context' +import { HistoryToggleProvider } from '@/components/history-toggle-context' +import { HistorySidebar } from '@/components/history-sidebar' import { MapLoadingProvider } from '@/components/map-loading-context'; import ConditionalLottie from '@/components/conditional-lottie'; -import { MapProvider } from '@/components/map/map-context' +import { MapProvider as MapContextProvider } from '@/components/map/map-context' import { getSupabaseUserAndSessionOnServer } from '@/lib/auth/get-current-user' import { PurchaseCreditsPopup } from '@/components/credits/purchase-credits-popup'; import { CreditsProvider } from '@/components/credits/credits-provider'; @@ -34,9 +37,8 @@ const fontPoppins = Poppins({ weight: ['400', '500', '600', '700'] }) -const title = '' -const description = - 'language to Maps' +const title = 'QCX' +const description = 'Language to Maps' export const metadata: Metadata = { metadataBase: new URL('https://www.qcx.world'), @@ -71,6 +73,29 @@ export default async function RootLayout({ return ( + +