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)
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 (
+
+