Skip to content
Merged
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
45 changes: 27 additions & 18 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
})
Comment on lines +44 to +50

Copy link
Copy Markdown
Contributor

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 userId when 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 previous aiState.userId survives and onSetAIState later passes that stale id into saveChat, so new chat rows are written under the wrong account.

Suggested fix
-  const userId = await getCurrentUserIdOnServer();
+  const userId = (await getCurrentUserIdOnServer()) ?? undefined;
   const aiState = getMutableAIState<typeof AI>()
-  if (userId && aiState.get().userId !== userId) {
+  if (aiState.get().userId !== userId) {
     aiState.update({
       ...aiState.get(),
       userId
     })
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userId = await getCurrentUserIdOnServer();
const aiState = getMutableAIState<typeof AI>()
if (userId && aiState.get().userId !== userId) {
aiState.update({
...aiState.get(),
userId
})
const userId = (await getCurrentUserIdOnServer()) ?? undefined;
const aiState = getMutableAIState<typeof AI>()
if (aiState.get().userId !== userId) {
aiState.update({
...aiState.get(),
userId
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/actions.tsx` around lines 44 - 50, The AI state update in
getCurrentUserIdOnServer/getMutableAIState currently only handles truthy user
IDs, so a stale aiState.userId can survive after logout or session expiry.
Update the auth-sync logic in this block to explicitly clear or overwrite
aiState.userId when getCurrentUserIdOnServer() returns no user, and ensure
onSetAIState/saveChat no longer reuses an old userId. Use the existing
getMutableAIState<typeof AI>() and aiState.update path to keep the stored user
identity in sync with the current auth state.

}

const uiStream = createStreamableUI()
const isGenerating = createStreamableValue(true)
const isCollapsed = createStreamableValue(false)
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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, submit() has already returned, so the toast in components/chat-panel.tsx will never fire. Failures from taskManager, writer, or querySuggestor currently just finalize whatever is already in uiStream—often the spinner—so the user gets a silent stuck response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/actions.tsx` around lines 675 - 680, The error handling in processEvents
is only logging and finalizing state, so failures from taskManager, writer, or
querySuggestor never surface to the user before the detached stream is closed.
Update the catch in processEvents to append a terminal error message into
uiStream (using the same streaming path that renders the assistant output)
before calling uiStream.done(), so the chat UI shows a final error state instead
of silently leaving the spinner. Keep the existing cleanup in
isGenerating.done(false) and aiState.done(aiState.get()) after the error is
rendered.

}

processEvents()
Expand All @@ -688,14 +697,16 @@ async function clearChat() {

aiState.done({
chatId: nanoid(),
messages: []
messages: [],
userId: undefined as string | undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearChat now calls aiState.done(...), but the const aiState = getMutableAIState<typeof AI>() initialization was removed in this hunk. That leaves aiState undefined when this action runs.

Suggested fix: restore the getMutableAIState initialization at the top of clearChat before calling aiState.done(...).

})
}

export type AIState = {
messages: AIMessage[]
chatId: string
isSharePage?: boolean
userId?: string
}

export type UIState = {
Expand All @@ -707,7 +718,8 @@ export type UIState = {

const initialAIState: AIState = {
chatId: nanoid(),
messages: []
messages: [],
userId: undefined as string | undefined
}

const initialUIState: UIState = []
Expand Down Expand Up @@ -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
}

Expand Down
22 changes: 22 additions & 0 deletions app_actions_final_cleanup.py
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 const aiState = getMutableAIState<typeof AI>() not immediately preceded by const userId =, which strips clearChat()'s required initializer. Lines 15-18 also inject a new uiStream before every matching isGenerating line even if one already exists. Constrain the transform to submit() or use a syntax-aware codemod.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_actions_final_cleanup.py` around lines 6 - 18, The cleanup transform is
too broad and non-idempotent, causing it to delete valid `const aiState =
getMutableAIState<typeof AI>()` initializers outside `submit()` and to inject
duplicate `uiStream` declarations before every `const isGenerating =
createStreamableValue(true)` match. Update the rewrite logic in this cleanup
block so it targets only the `submit()` path and preserves unrelated
initializers like `clearChat()`, or switch to a syntax-aware codemod that checks
surrounding structure before rewriting.

new_lines.append(line)

with open('app/actions.tsx', 'w') as f:
f.writelines(new_lines)
38 changes: 38 additions & 0 deletions app_actions_userId_fix.py
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the userId insertion check site-specific.

The whole-file guard on Lines 10-11 skips every replacement once any userId: undefined already exists. A partially updated app/actions.tsx will therefore keep the other messages: [] sites stale instead of finishing the rewrite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_actions_userId_fix.py` around lines 10 - 11, The current guard in the
content rewrite is too broad because it stops all `messages: []` replacements as
soon as any `userId: undefined` exists. Update the logic around the replace call
in `app_actions_userId_fix.py` so the `userId` insertion check is scoped per
`messages: []` occurrence in `app/actions.tsx`, allowing remaining sites to
still be rewritten even if one already has `userId: 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<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)
12 changes: 9 additions & 3 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client'
"use client"
import { toast } from "sonner"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'use client' must remain the first statement in this file. With the new import above it, Next.js will treat this module as a Server Component, and the hooks used below (useState, useEffect, etc.) will fail.

Suggested fix: move 'use client' back to line 1 and keep all imports after it.


import { useEffect, useState, useRef, ChangeEvent, forwardRef, useImperativeHandle, useCallback } from 'react'
import type { AI, UIState } from '@/app/actions'
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Rollback the optimistic message when submit() rejects.

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 app/actions.tsx, so the visible conversation diverges from the actual AI state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/chat-panel.tsx` around lines 125 - 130, Rollback the optimistic
chat update when submit() fails so the UI stays in sync with the server. In
chat-panel.tsx, the submit flow that appends responseMessage in the try block
should also restore the previous conversation state if the awaited
submit(formData) rejects; use the existing submit handler state around this path
to remove the optimistic user bubble and reinstate the cleared input/file state
on error, rather than leaving a message that never reached app/actions.tsx.

}
}

const handleClear = async () => {
Expand Down
108 changes: 60 additions & 48 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't return an empty fullResponse on terminal stream errors.

app/actions.tsx retries in specific-model mode while answer.length === 0. With Lines 173-199 no longer appending any fallback text on delta.type === 'error' and the catch returning fullResponse: '', a model failure will keep re-entering researcher() until the request times out.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/agents/researcher.tsx` around lines 173 - 199, The terminal stream error
path in researcher() is returning an empty fullResponse, which causes
app/actions.tsx retry logic to loop when answer.length stays 0. Update the error
handling around the streamText processing in lib/agents/researcher.tsx so the
delta.type === 'error' branch or the catch block preserves any accumulated text
or appends a non-empty fallback before returning. Make sure the returned
fullResponse from researcher() is never empty on a terminal failure, while
keeping hasError set and the existing result/toolResponses structure intact.

}

return { result, fullResponse, hasError, toolResponses }
}
29 changes: 10 additions & 19 deletions server.log
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
6 changes: 6 additions & 0 deletions verify_load.spec.ts
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Playwright 1.60, is await expect(page).toBeVisible('body')valid, or should visibility be asserted withawait expect(page.locator('body')).toBeVisible()?

💡 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.

expect(page).toBeVisible('body') is invalid because toBeVisible operates on Locator objects, not the Page instance directly.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await page.goto('http://localhost:3000');
await expect(page).toBeVisible('body');
await page.goto('http://localhost:3000');
await expect(page.locator('body')).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@verify_load.spec.ts` around lines 4 - 5, The visibility check in the load
test is using the Page object incorrectly; update the assertion in the
verify_load.spec.ts test to use a Locator derived from page instead of calling
toBeVisible on page directly. Keep the page.goto call as-is, and change the body
visibility assertion to use the appropriate locator-based API so the test
targets the body element through page.locator or an equivalent Locator
reference.

});