diff --git a/app/actions.tsx b/app/actions.tsx index 90ebaae3..097888fe 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -18,6 +18,7 @@ import { querySuggestor } from '@/lib/agents/query-suggestor' import { researcher } from '@/lib/agents/researcher' import { resolutionSearch, type DrawnFeature } from '@/lib/agents/resolution-search' import { writer } from '@/lib/agents/writer' +import { getCurrentUserIdOnServer } from "@/lib/auth/get-current-user" import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat' import { Chat, AIMessage } from '@/lib/types' @@ -40,8 +41,15 @@ type RelatedQueries = { async function submit(formData?: FormData, skip?: boolean) { 'use server' - + const userId = await getCurrentUserIdOnServer(); const aiState = getMutableAIState() + if (userId && aiState.get().userId !== userId) { + aiState.update({ + ...aiState.get(), + userId + }) + } + const uiStream = createStreamableUI() const isGenerating = createStreamableValue(true) const isCollapsed = createStreamableValue(false) @@ -87,10 +95,7 @@ async function submit(formData?: FormData, skip?: boolean) { } }); - const { getCurrentUserIdOnServer } = await import( - "@/lib/auth/get-current-user" - ) - const userId = (await getCurrentUserIdOnServer()) || "anonymous"; + if (action === 'generate_report_context') { const messagesString = formData?.get('messages'); @@ -407,10 +412,7 @@ async function submit(formData?: FormData, skip?: boolean) { if (Array.isArray(m.content)) { const filteredContent = m.content.filter((part: any) => { if (part.type === 'image') { - const isValid = - typeof part.image === 'string' && - (part.image.startsWith('data:') || - part.image === 'IMAGE_PROCESSED') + const isValid = typeof part.image === 'string' && part.image.startsWith('data:') if (isValid) { retainedImagesCount++ } else { @@ -532,10 +534,11 @@ async function submit(formData?: FormData, skip?: boolean) { } as CoreMessage) } - const currentSystemPrompt = (await getSystemPrompt(userId)) || '' + const currentSystemPrompt = userId ? (await getSystemPrompt(userId)) ?? '' : '' const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' async function processEvents() { + try { let action: any = { object: { next: 'proceed' } } if (!skip) { const taskManagerResult = await taskManager(messages) @@ -668,7 +671,13 @@ async function submit(formData?: FormData, skip?: boolean) { } isGenerating.done(false) - uiStream.done() + uiStream.done() + } catch (error) { + console.error("Error in processEvents:", error) + isGenerating.done(false) + uiStream.done() + aiState.done(aiState.get()) + } } processEvents() @@ -688,7 +697,8 @@ async function clearChat() { aiState.done({ chatId: nanoid(), - messages: [] + messages: [], + userId: undefined as string | undefined }) } @@ -696,6 +706,7 @@ export type AIState = { messages: AIMessage[] chatId: string isSharePage?: boolean + userId?: string } export type UIState = { @@ -707,7 +718,8 @@ export type UIState = { const initialAIState: AIState = { chatId: nanoid(), - messages: [] + messages: [], + userId: undefined as string | undefined } const initialUIState: UIState = [] @@ -771,13 +783,10 @@ export const AI = createAI({ } ] - const { getCurrentUserIdOnServer } = await import( - "@/lib/auth/get-current-user" - ) - const actualUserId = await getCurrentUserIdOnServer() + const actualUserId = state.userId if (!actualUserId) { - console.error('onSetAIState: User not authenticated. Chat not saved.') + console.info('onSetAIState: User not authenticated. Chat not saved.') return } diff --git a/app_actions_final_cleanup.py b/app_actions_final_cleanup.py new file mode 100644 index 00000000..76a02eae --- /dev/null +++ b/app_actions_final_cleanup.py @@ -0,0 +1,22 @@ +with open('app/actions.tsx', 'r') as f: + lines = f.readlines() + +new_lines = [] +skip_next = False +for i, line in enumerate(lines): + if 'const aiState = getMutableAIState()' in line: + if i > 0 and 'const userId =' in lines[i-1]: + new_lines.append(line) + continue + else: + # This is the second one, let's keep it but maybe it missed uiStream + # Actually let's just rewrite the whole block + continue + if 'const isGenerating = createStreamableValue(true)' in line: + new_lines.append(" const uiStream = createStreamableUI()\n") + new_lines.append(line) + continue + new_lines.append(line) + +with open('app/actions.tsx', 'w') as f: + f.writelines(new_lines) diff --git a/app_actions_userId_fix.py b/app_actions_userId_fix.py new file mode 100644 index 00000000..bdf797c5 --- /dev/null +++ b/app_actions_userId_fix.py @@ -0,0 +1,38 @@ +import re + +with open('app/actions.tsx', 'r') as f: + content = f.read() + +# 1. Update AIState +if 'userId?: string' not in content: + content = content.replace('isSharePage?: boolean', 'isSharePage?: boolean\n userId?: string') + +if 'userId: undefined' not in content: + content = content.replace('messages: []', 'messages: [],\n userId: undefined as string | undefined') + +# 2. Update submit function +# Find the start of submit +submit_start = content.find('async function submit(formData?: FormData, skip?: boolean) {') +if submit_start != -1: + # Look for getCurrentUserIdOnServer call within submit + # Actually, let's just insert it at the beginning of submit body + use_server_pos = content.find("'use server'", submit_start) + if use_server_pos != -1: + insert_pos = content.find('\n', use_server_pos) + 1 + # Check if already inserted + if 'const userId = await getCurrentUserIdOnServer();' not in content[insert_pos:insert_pos+200]: + content = content[:insert_pos] + " const userId = await getCurrentUserIdOnServer();\n const aiState = getMutableAIState()\n if (userId && aiState.get().userId !== userId) {\n aiState.update({\n ...aiState.get(),\n userId\n })\n }\n" + content[insert_pos:] + +# 3. Remove old userId resolution +old_res_pattern = r'const \{ getCurrentUserIdOnServer \} = await import\(\s+"@/lib/auth/get-current-user"\s+\)\s+const userId = \(await getCurrentUserIdOnServer\(\)\) \|\| "anonymous";' +content = re.sub(old_res_pattern, '', content) + +# 4. Update onSetAIState +old_onset_pattern = r'const \{ getCurrentUserIdOnServer \} = await import\(\s+"@/lib/auth/get-current-user"\s+\)\s+const actualUserId = await getCurrentUserIdOnServer\(\)' +content = re.sub(old_onset_pattern, 'const actualUserId = state.userId', content) + +# 5. Update error log in onSetAIState +content = content.replace("console.error('onSetAIState: User not authenticated. Chat not saved.')", "console.info('onSetAIState: User not authenticated. Chat not saved.')") + +with open('app/actions.tsx', 'w') as f: + f.write(content) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index ca2fbc6f..1f7611bf 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -1,4 +1,5 @@ -'use client' +"use client" +import { toast } from "sonner" import { useEffect, useState, useRef, ChangeEvent, forwardRef, useImperativeHandle, useCallback } from 'react' import type { AI, UIState } from '@/app/actions' @@ -121,8 +122,13 @@ export const ChatPanel = forwardRef(({ messages, i setInput('') clearAttachment() - const responseMessage = await submit(formData) - setMessages(currentMessages => [...currentMessages, responseMessage as any]) + try { + const responseMessage = await submit(formData) + setMessages(currentMessages => [...currentMessages, responseMessage as any]) + } catch (error) { + console.error("Failed to submit message:", error) + toast.error("Failed to send message. Please try again.") + } } const handleClear = async () => { diff --git a/lib/agents/researcher.tsx b/lib/agents/researcher.tsx index 2b004f1c..bb6065f2 100644 --- a/lib/agents/researcher.tsx +++ b/lib/agents/researcher.tsx @@ -104,10 +104,10 @@ export async function researcher( ? dynamicSystemPrompt : getDefaultSystemPrompt(currentDate, drawnFeatures) - // Check if any message contains an image + // Check if any message contains an image - Ensure we only count real data URLs const hasImage = messages.some(message => Array.isArray(message.content) && - message.content.some(part => part.type === 'image') + message.content.some(part => part.type === 'image' && typeof part.image === 'string' && part.image.startsWith('data:')) ) const lastUserMessage = [...messages].reverse().find(m => m.role === 'user') @@ -132,58 +132,70 @@ export async function researcher( : 'none' }) - const result = await nonexperimental_streamText({ - model: (await getModel(hasImage)) as LanguageModel, - maxTokens: 4096, - system: systemPromptToUse, - messages, - tools: getTools({ uiStream, fullResponse, mapProvider }), - }) - - uiStream.update(null) // remove spinner + try { + const result = await nonexperimental_streamText({ + model: (await getModel(hasImage)) as LanguageModel, + maxTokens: 4096, + system: systemPromptToUse, + messages, + tools: getTools({ uiStream, fullResponse, mapProvider }), + }) + + uiStream.update(null) // remove spinner + + const toolCalls: ToolCallPart[] = [] + const toolResponses: ToolResultPart[] = [] + + for await (const delta of result.fullStream) { + switch (delta.type) { + case 'text-delta': + if (delta.textDelta) { + if (fullResponse.length === 0 && delta.textDelta.length > 0) { + uiStream.update(answerSection) + } + fullResponse += delta.textDelta + streamText.update(fullResponse) + } + break - const toolCalls: ToolCallPart[] = [] - const toolResponses: ToolResultPart[] = [] + case 'tool-call': + toolCalls.push(delta) + break - for await (const delta of result.fullStream) { - switch (delta.type) { - case 'text-delta': - if (delta.textDelta) { - if (fullResponse.length === 0 && delta.textDelta.length > 0) { - uiStream.update(answerSection) + case 'tool-result': + if (!useSpecificModel && toolResponses.length === 0 && delta.result) { + uiStream.append(answerSection) } - fullResponse += delta.textDelta - streamText.update(fullResponse) - } - break + if (!delta.result) hasError = true + toolResponses.push(delta) + break + + case 'error': + hasError = true + console.error('Researcher: Stream error:', delta.error) + break + } + } - case 'tool-call': - toolCalls.push(delta) - break + messages.push({ + role: 'assistant', + content: [{ type: 'text', text: fullResponse }, ...toolCalls], + }) - case 'tool-result': - if (!useSpecificModel && toolResponses.length === 0 && delta.result) { - uiStream.append(answerSection) - } - if (!delta.result) hasError = true - toolResponses.push(delta) - break - - case 'error': - hasError = true - fullResponse += `\n\nError: Tool execution failed.` - break + if (toolResponses.length > 0) { + messages.push({ role: 'tool', content: toolResponses }) } - } - messages.push({ - role: 'assistant', - content: [{ type: 'text', text: fullResponse }, ...toolCalls], - }) - - if (toolResponses.length > 0) { - messages.push({ role: 'tool', content: toolResponses }) + return { result, fullResponse, hasError, toolResponses } + } catch (error) { + hasError = true + console.error('Researcher: Error:', error) + uiStream.update( +
+ An error occurred during analysis. Please try again. +
+ ) + streamText.error(error) + return { result: null as any, fullResponse: '', hasError, toolResponses: [] } } - - return { result, fullResponse, hasError, toolResponses } } diff --git a/server.log b/server.log index 9d968b8c..93973f1a 100644 --- a/server.log +++ b/server.log @@ -1,23 +1,14 @@ -$ next start - ▲ Next.js 15.3.8 +$ next dev --turbo + ▲ Next.js 15.3.8 (Turbopack) - Local: http://localhost:3000 - Network: http://192.168.0.2:3000 + - Environments: .env ✓ Starting... - ⚠ "next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead. - ✓ Ready in 504ms -Chat DB actions loaded. Ensure getCurrentUserId() is correctly implemented for server-side usage if applicable. -Chat DB actions loaded. Ensure getCurrentUserId() is correctly implemented for server-side usage if applicable. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. -[Auth] Supabase URL or Anon Key is not set for server-side auth. +Attention: Next.js now collects completely anonymous telemetry regarding usage. +This information is used to shape Next.js' roadmap and prioritize features. +You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: +https://nextjs.org/telemetry + + ✓ Compiled middleware in 383ms + ✓ Ready in 1798ms diff --git a/verify_load.spec.ts b/verify_load.spec.ts new file mode 100644 index 00000000..ddefa4c7 --- /dev/null +++ b/verify_load.spec.ts @@ -0,0 +1,6 @@ +import { test, expect } from '@playwright/test'; + +test('page loads', async ({ page }) => { + await page.goto('http://localhost:3000'); + await expect(page).toBeVisible('body'); +});