-
-
Notifications
You must be signed in to change notification settings - Fork 8
Resolve Auth Loss, Image Pipeline Errors, and Agent Timeouts #691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dfed6ac
64eb3d4
3578c60
54ef40b
69d2c28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<typeof AI>() | ||
| 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()) | ||
| } | ||
|
Comment on lines
+675
to
+680
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Render a terminal error before closing the detached stream. By the time this catch runs, 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| processEvents() | ||
|
|
@@ -688,14 +697,16 @@ async function clearChat() { | |
|
|
||
| aiState.done({ | ||
| chatId: nanoid(), | ||
| messages: [] | ||
| messages: [], | ||
| userId: undefined as string | undefined | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix: restore the |
||
| }) | ||
| } | ||
|
|
||
| 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<AIState, UIState>({ | |
| } | ||
| ] | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof AI>()' 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 | ||
|
Comment on lines
+6
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win This cleanup rewrite is destructive and non-idempotent. Lines 7-14 drop any 🤖 Prompt for AI Agents |
||
| new_lines.append(line) | ||
|
|
||
| with open('app/actions.tsx', 'w') as f: | ||
| f.writelines(new_lines) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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') | ||
|
Comment on lines
+10
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Make the The whole-file guard on Lines 10-11 skips every replacement once any 🤖 Prompt for AI Agents |
||
|
|
||
| # 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<typeof AI>()\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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| 'use client' | ||
| "use client" | ||
| import { toast } from "sonner" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix: move |
||
|
|
||
| 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<ChatPanelRef, ChatPanelProps>(({ 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.") | ||
|
Comment on lines
+125
to
+130
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Rollback the optimistic message when The user bubble is appended and the input/file are cleared before Lines 125-130 await the server action. If that call throws, the UI keeps a message that never entered 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| const handleClear = async () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
| <div className="text-destructive font-medium"> | ||
| An error occurred during analysis. Please try again. | ||
| </div> | ||
| ) | ||
| streamText.error(error) | ||
| return { result: null as any, fullResponse: '', hasError, toolResponses: [] } | ||
|
Comment on lines
+173
to
+199
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Don't return an empty
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return { result, fullResponse, hasError, toolResponses } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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'); | ||||||||||
|
Comment on lines
+4
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major 🧩 Analysis chain🌐 Web query:
💡 Result: In Playwright, the correct way to assert visibility is using the locator-based assertion: await expect(page.locator('body')).toBeVisible. [1][2][3] The syntax await expect(page).toBeVisible('body') is not valid. The expect function in Playwright is designed to take a Locator or a Page object (for specific page-level assertions like toHaveURL or toHaveTitle) as its argument, and then be chained with a matcher that is appropriate for that object type. [1][4][5] The toBeVisible matcher belongs to the LocatorAssertions class, which requires a Locator as the input to expect. [1][2] Using await expect(page.locator('body')).toBeVisible is the standard, recommended approach as it leverages Playwright's auto-waiting and retry mechanisms to handle dynamic web content effectively. [6][3] Citations:
Use a locator assertion for visibility checks.
Suggested fix test('page loads', async ({ page }) => {
await page.goto('http://localhost:3000');
- await expect(page).toBeVisible('body');
+ await expect(page.locator('body')).toBeVisible();
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| }); | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Clear stale
userIdwhen auth disappears.Lines 44-50 only update AI state when
getCurrentUserIdOnServer()returns a truthy value. If the user logs out or the session expires mid-chat, the previousaiState.userIdsurvives andonSetAIStatelater passes that stale id intosaveChat, so new chat rows are written under the wrong account.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents