Skip to content
Open
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
425 changes: 202 additions & 223 deletions app/actions.tsx

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion app/api/chats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { searchParams } = new URL(request.url);
const url = new URL(request.url);
const { searchParams } = url;

const DEFAULT_LIMIT = 20;
const MAX_LIMIT = 100;
Expand Down
3 changes: 2 additions & 1 deletion app/api/embeddings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ function latLonToUTM(lat: number, lon: number, epsgCode: string): { x: number; y
*/
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const url = new URL(req.url);
const { searchParams } = url;
const latParam = searchParams.get('lat');
const lonParam = searchParams.get('lon');
const yearParam = searchParams.get('year');
Expand Down
21 changes: 16 additions & 5 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ChatPanelProps {

export interface ChatPanelRef {
handleAttachmentClick: () => void
submitForm: () => void
submitForm: (message?: string) => void
}

export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, input, setInput, onSuggestionsChange }, ref) => {
Expand All @@ -43,12 +43,16 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
const inputRef = useRef<HTMLTextAreaElement>(null)
const formRef = useRef<HTMLFormElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const pendingMessageRef = useRef<string | null>(null)

useImperativeHandle(ref, () => ({
handleAttachmentClick() {
fileInputRef.current?.click()
},
submitForm() {
submitForm(message?: string) {
if (message) {
pendingMessageRef.current = message
}
formRef.current?.requestSubmit()
}
}));
Expand Down Expand Up @@ -87,13 +91,17 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!input.trim() && !selectedFile) {

const messageToSubmit = pendingMessageRef.current || input
pendingMessageRef.current = null

if (!messageToSubmit.trim() && !selectedFile) {
return
}

const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = []
if (input) {
content.push({ type: 'text', text: input })
if (messageToSubmit) {
content.push({ type: 'text', text: messageToSubmit })
}
if (selectedFile && selectedFile.type.startsWith('image/')) {
content.push({
Expand All @@ -111,6 +119,9 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
])

const formData = new FormData(e.currentTarget)
if (messageToSubmit) {
formData.set('input', messageToSubmit)
}
if (selectedFile) {
formData.append('file', selectedFile)
}
Expand Down
21 changes: 5 additions & 16 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ export function Chat({ id }: ChatProps) {
const { isUsageOpen } = useUsageToggle();
const { isCalendarOpen } = useCalendarToggle()
const [input, setInput] = useState('')
const [showEmptyScreen, setShowEmptyScreen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [showEmptyScreen, setShowEmptyScreen] = useState(messages.length === 0)
const [suggestions, setSuggestions] = useState<PartialRelated | null>(null)
const chatPanelRef = useRef<ChatPanelRef>(null);

Expand Down Expand Up @@ -85,13 +84,6 @@ export function Chat({ id }: ChatProps) {
// Get mapData to access drawnFeatures
const { mapData } = useMapData();

useEffect(() => {
if (isSubmitting) {
chatPanelRef.current?.submitForm()
setIsSubmitting(false)
}
}, [isSubmitting])

// useEffect to call the server action when drawnFeatures changes
useEffect(() => {
if (id && mapData.drawnFeatures && mapData.cameraState) {
Expand All @@ -110,10 +102,8 @@ export function Chat({ id }: ChatProps) {
<SuggestionsDropdown
suggestions={suggestions}
onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setIsSubmitting(true)
chatPanelRef.current?.submitForm(query)
}}
onClose={() => setSuggestions(null)}
className="relative bottom-auto mb-0 w-full shadow-none border-none bg-transparent"
Expand Down Expand Up @@ -152,8 +142,7 @@ export function Chat({ id }: ChatProps) {
{showEmptyScreen ? (
<EmptyScreen
submitMessage={message => {
setInput(message)
setIsSubmitting(true)
chatPanelRef.current?.submitForm(message)
}}
/>
) : (
Expand Down Expand Up @@ -181,6 +170,7 @@ export function Chat({ id }: ChatProps) {
) : (
<>
<ChatPanel
ref={chatPanelRef}
messages={messages}
input={input}
setInput={setInput}
Expand All @@ -191,8 +181,7 @@ export function Chat({ id }: ChatProps) {
{showEmptyScreen ? (
<EmptyScreen
submitMessage={message => {
setInput(message)
setIsSubmitting(true)
chatPanelRef.current?.submitForm(message)
}}
/>
) : (
Expand Down
1 change: 0 additions & 1 deletion components/followup-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function FollowupPanel() {
event.preventDefault()
const formData = new FormData()
formData.append("input", input)
formData.append("action", "resolution_search")

const userMessage = {
id: nanoid(),
Expand Down
10 changes: 6 additions & 4 deletions components/search-related.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import React from 'react'
import React, { memo } from 'react'
import { Button } from './ui/button'
import { ArrowRight } from 'lucide-react'
import {
Expand All @@ -18,7 +18,7 @@ export interface SearchRelatedProps {
relatedQueries: StreamableValue<PartialRelated, any>
}

export const SearchRelated: React.FC<SearchRelatedProps> = ({
export const SearchRelated: React.FC<SearchRelatedProps> = memo(({
relatedQueries
}) => {
const { submit } = useActions()
Expand Down Expand Up @@ -47,7 +47,7 @@ export const SearchRelated: React.FC<SearchRelatedProps> = ({
{data?.items
?.filter(item => item?.query !== '')
.map((item, index) => (
<div className="flex items-start w-full animate-in fade-in slide-in-from-bottom-2 duration-300" key={index}>
<div className="flex items-start w-full animate-in fade-in slide-in-from-bottom-2 duration-300" key={item?.query || index}>
<ArrowRight className="h-4 w-4 mr-2 mt-1 flex-shrink-0 text-accent-foreground/50" />
<Button
variant="link"
Expand All @@ -60,6 +60,8 @@ export const SearchRelated: React.FC<SearchRelatedProps> = ({
))}
</div>
)
}
})

SearchRelated.displayName = 'SearchRelated'

export default SearchRelated
37 changes: 25 additions & 12 deletions lib/actions/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ import { users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'

// Helper to validate UUID format
function isValidUUID(id: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
// Loosened regex for internal mock UUIDs or different versions
const looseUuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return looseUuidRegex.test(id);
}

export async function getChats(userId?: string | null): Promise<DrizzleChat[]> {
if (!userId) {
console.warn('getChats called without userId, returning empty array.')
if (!userId || !isValidUUID(userId)) {
console.warn('getChats called with invalid userId:', userId)
return []
}

Expand All @@ -36,8 +44,8 @@ export async function getChats(userId?: string | null): Promise<DrizzleChat[]> {
}

export async function getChat(id: string, userId: string): Promise<DrizzleChat | null> {
if (!userId) {
console.warn('getChat called without userId.')
if (!userId || !isValidUUID(userId)) {
console.warn('getChat called with invalid userId:', userId)
return null;
}
try {
Expand Down Expand Up @@ -69,9 +77,9 @@ export async function clearChats(
userId?: string | null
): Promise<{ error?: string } | void> {
const currentUserId = userId || (await getCurrentUserIdOnServer())
if (!currentUserId) {
console.error('clearChats: No user ID provided or found.')
return { error: 'User ID is required to clear chats' }
if (!currentUserId || !isValidUUID(currentUserId)) {
console.error('clearChats: Invalid user ID:', currentUserId)
return { error: 'Valid User ID is required to clear chats' }
}

try {
Expand All @@ -94,6 +102,11 @@ export async function saveChat(chat: OldChatType, userId: string): Promise<strin
}
const effectiveUserId = userId || chat.userId;

if (!isValidUUID(effectiveUserId)) {
console.error('saveChat: Invalid user ID:', effectiveUserId);
return null;
}

const newChatData: DbNewChat = {
id: chat.id,
userId: effectiveUserId,
Expand Down Expand Up @@ -122,9 +135,9 @@ export async function saveChat(chat: OldChatType, userId: string): Promise<strin

export async function updateDrawingContext(chatId: string, contextData: { drawnFeatures: any[], cameraState: any }) {
const userId = await getCurrentUserIdOnServer();
if (!userId) {
console.error('updateDrawingContext: Could not get current user ID.');
return { error: 'User not authenticated' };
if (!userId || !isValidUUID(userId)) {
console.error('updateDrawingContext: Invalid user ID:', userId);
return { error: 'Valid user authentication required' };
}

const newDrawingMessage: DbNewMessage = {
Expand All @@ -151,7 +164,7 @@ export async function saveSystemPrompt(
userId: string,
prompt: string
): Promise<{ success?: boolean; error?: string }> {
if (!userId) return { error: 'User ID is required' }
if (!userId || !isValidUUID(userId)) return { error: 'Valid User ID is required' }
if (!prompt) return { error: 'Prompt is required' }

try {
Expand All @@ -169,7 +182,7 @@ export async function saveSystemPrompt(
export async function getSystemPrompt(
userId: string
): Promise<string | null> {
if (!userId) return null
if (!userId || !isValidUUID(userId)) return null

try {
const result = await db.select({ systemPrompt: users.systemPrompt })
Expand Down
2 changes: 1 addition & 1 deletion lib/agents/query-suggestor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function querySuggestor(

let finalRelatedQueries: PartialRelated = {}
const result = await streamObject({
model: (await getModel()) as LanguageModel,
model: (await getModel(false, "auxiliary")) as LanguageModel,
system: `As a professional web researcher, your task is to generate a set of three queries that explore the subject matter more deeply, building upon the initial query and the information uncovered in its search results.

For instance, if the original query was "Starship's third test flight key milestones", your output should follow this format:
Expand Down
22 changes: 10 additions & 12 deletions lib/auth/get-current-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const AUTH_DISABLED_FLAG =
process.env.AUTH_DISABLED_FOR_DEV === 'true' &&
process.env.NODE_ENV !== 'production';
const MOCK_USER_ID = 'dev-user-001'; // A consistent mock user ID for dev mode
const MOCK_USER_ID = '00000000-0000-0000-0000-000000000001'; // A valid UUID for dev mode

/**
* Retrieves the Supabase user and session object in server-side contexts
Expand Down Expand Up @@ -58,25 +58,23 @@ export async function getSupabaseUserAndSessionOnServer(): Promise<{
return { user: null, session: null, error: new Error('Missing Supabase environment variables') };
}

const cookieStore = cookies();
const cookieStore = await cookies();
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
cookies: {
async get(name: string): Promise<string | undefined> {
const cookie = (await cookieStore).get(name); // Use the correct get method
return cookie?.value; // Return the value or undefined
get(name: string): string | undefined {
const cookie = cookieStore.get(name);
return cookie?.value;
},
async set(name: string, value: string, options: CookieOptions): Promise<void> {
set(name: string, value: string, options: CookieOptions): void {
try {
const store = await cookieStore;
store.set({ name, value, ...options }); // Set cookie with options
cookieStore.set({ name, value, ...options });
} catch (error) {
// console.warn(`[Auth] Failed to set cookie ${name}:`, error);
}
},
async remove(name: string, options: CookieOptions): Promise<void> {
remove(name: string, options: CookieOptions): void {
try {
const store = await cookieStore;
store.set({ name, value: '', ...options, maxAge: 0 }); // Delete cookie by setting maxAge to 0
cookieStore.set({ name, value: '', ...options, maxAge: 0 });
} catch (error) {
// console.warn(`[Auth] Failed to delete cookie ${name}:`, error);
}
Expand Down Expand Up @@ -124,4 +122,4 @@ export async function getCurrentUserIdOnServer(): Promise<string | null> {
return null;
}
return user?.id || null;
}
}
2 changes: 1 addition & 1 deletion lib/schema/related.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const relatedSchema = z.object({
query: z.string()
})
)
.length(3)
.min(1).max(3)
})
export type PartialRelated = DeepPartial<typeof relatedSchema>

Expand Down
Loading