diff --git a/app/actions.tsx b/app/actions.tsx index 50e985bf..ae83bf79 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -1,3 +1,4 @@ +import { getCurrentUserIdOnServer } from "@/lib/auth/get-current-user" import { StreamableValue, createAI, @@ -37,6 +38,19 @@ async function submit(formData?: FormData, skip?: boolean) { 'use server' const aiState = getMutableAIState() + const currentMessages = aiState.get().messages; + const sanitizedHistory = currentMessages.map((m: any) => { + 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 uiStream = createStreamableUI() const isGenerating = createStreamableValue(true) const isCollapsed = createStreamableValue(false) @@ -51,6 +65,7 @@ async function submit(formData?: FormData, skip?: boolean) { } if (action === 'resolution_search') { + const isQCX = formData?.get('isQCX') === 'true'; const file_mapbox = formData?.get('file_mapbox') as File; const file_google = formData?.get('file_google') as File; const file = (formData?.get('file') as File) || file_mapbox || file_google; @@ -81,7 +96,7 @@ async function submit(formData?: FormData, skip?: boolean) { message.type !== 'resolution_search_result' ); - const userInput = 'Analyze this map view.'; + const userInput = isQCX ? 'Perform QCX-TERRA ANALYSIS on this Google Satellite image.' : 'Analyze this map view.'; const content: CoreMessage['content'] = [ { type: 'text', text: userInput }, { type: 'image', image: dataUrl, mimeType: file.type } @@ -90,7 +105,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.update({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: nanoid(), role: 'user', content, type: 'input' } ] }); @@ -112,6 +127,7 @@ async function submit(formData?: FormData, skip?: boolean) { } const analysisResult = await streamResult.object; + console.log('[ResolutionSearch] Analysis result:', !!analysisResult.summary, !!analysisResult.geoJson); summaryStream.done(analysisResult.summary || 'Analysis complete.'); if (analysisResult.geoJson) { @@ -134,19 +150,6 @@ async function submit(formData?: FormData, skip?: boolean) { } return m }) - - const currentMessages = aiState.get().messages; - const sanitizedHistory = currentMessages.map((m: any) => { - 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(
@@ -159,7 +162,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.done({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: groupeId, role: 'assistant', @@ -239,7 +242,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.update({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: nanoid(), role: 'user', @@ -265,7 +268,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.done({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: groupeId, role: 'assistant', @@ -332,6 +335,7 @@ async function submit(formData?: FormData, skip?: boolean) { const maxMessages = useSpecificAPI ? 5 : 10 messages.splice(0, Math.max(messages.length - maxMessages, 0)) + const messageParts: { type: 'text' | 'image' text?: string @@ -382,7 +386,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.update({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: nanoid(), role: 'user', @@ -418,7 +422,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.done({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: nanoid(), role: 'assistant', @@ -459,7 +463,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.update({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: groupeId, role: 'tool', @@ -510,7 +514,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.done({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: groupeId, role: 'assistant', @@ -552,6 +556,7 @@ async function clearChat() { const aiState = getMutableAIState() + aiState.done({ chatId: nanoid(), messages: [] @@ -578,88 +583,7 @@ const initialAIState: AIState = { const initialUIState: UIState = [] -export const AI = createAI({ - actions: { - submit, - clearChat - }, - initialUIState, - initialAIState, - onGetUIState: async () => { - 'use server' - - const aiState = getAIState() as AIState - if (aiState) { - const uiState = getUIStateFromAIState(aiState) - return uiState - } - return initialUIState - }, - 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 updatedMessages: AIMessage[] = [ - ...messages, - { - id: nanoid(), - role: 'assistant', - content: `end`, - type: 'end' - } - ] - - 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 chat: Chat = { - id: chatId, - createdAt, - userId: actualUserId, - path, - title, - messages: updatedMessages - } - await saveChat(chat, actualUserId) - } -}) - -export const getUIStateFromAIState = (aiState: AIState): UIState => { +export const getUIStateFromAIState = async (aiState: AIState): Promise => { const chatId = aiState.chatId const isSharePage = aiState.isSharePage return aiState.messages @@ -846,3 +770,81 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { }) .filter(message => message !== null) as UIState } +export const AI = createAI({ + actions: { + submit, + clearChat + }, + initialUIState, + initialAIState, + onGetUIState: async () => { + 'use server' + + const aiState = getAIState() as AIState + if (aiState) { + const uiState = await getUIStateFromAIState(aiState) + return uiState + } + return initialUIState + }, + 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 updatedMessages: AIMessage[] = [ + ...messages, + { + id: nanoid(), + role: 'assistant', + content: `end`, + type: 'end' + } + ] + + + const actualUserId = await getCurrentUserIdOnServer() + + if (!actualUserId) { + console.error('onSetAIState: User not authenticated. Chat not saved.') + return + } + + const chat: Chat = { + id: chatId, + createdAt, + userId: actualUserId, + path, + title, + messages: updatedMessages + } + await saveChat(chat, actualUserId) + } +}) diff --git a/components/header.tsx b/components/header.tsx index fd80bc44..e4184e08 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -50,8 +50,8 @@ export const Header = () => { Chat - -
+ +
- +
- + - + - +
{/* Mobile menu buttons */}
- + diff --git a/components/mobile-icons-bar.tsx b/components/mobile-icons-bar.tsx index d0db2cfa..822f2b41 100644 --- a/components/mobile-icons-bar.tsx +++ b/components/mobile-icons-bar.tsx @@ -6,8 +6,6 @@ import { AI } from '@/app/actions' import { Button } from '@/components/ui/button' import { Search, - CircleUserRound, - Map, CalendarDays, TentTree, Paperclip, diff --git a/components/resolution-carousel.tsx b/components/resolution-carousel.tsx index 1b28cbf7..9a0bb2e8 100644 --- a/components/resolution-carousel.tsx +++ b/components/resolution-carousel.tsx @@ -52,6 +52,7 @@ export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: R const formData = new FormData() formData.append('file', blob, 'google_analysis.png') formData.append('action', 'resolution_search') + formData.append('isQCX', 'true') const responseMessage = await actions.submit(formData) setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) diff --git a/lib/actions/chat.ts b/lib/actions/chat.ts index f36f2cf6..a9269d66 100644 --- a/lib/actions/chat.ts +++ b/lib/actions/chat.ts @@ -101,6 +101,8 @@ export async function saveChat(chat: OldChatType, userId: string): Promise[] = chat.messages.map(msg => ({ diff --git a/tests/sidebar.spec.ts b/tests/sidebar.spec.ts index 5b31a1d7..d39b864b 100644 --- a/tests/sidebar.spec.ts +++ b/tests/sidebar.spec.ts @@ -7,7 +7,7 @@ test.describe('Sidebar and Chat History', () => { }); test('should open the history panel', async ({ page }) => { - await page.click('[data-testid="history-button"]'); + await page.click('[data-testid="logo-history-toggle"]'); const historyPanel = page.locator('[data-testid="history-panel"]'); await expect(historyPanel).toBeVisible(); }); @@ -19,7 +19,7 @@ test.describe('Sidebar and Chat History', () => { await page.waitForSelector('[data-testid^="history-item-"]'); // Now, open the history panel and clear the history - await page.click('[data-testid="history-button"]'); + await page.click('[data-testid="logo-history-toggle"]'); await page.click('[data-testid="clear-history-button"]'); await page.click('[data-testid="clear-history-confirm"]'); diff --git a/verify_history_toggle.py b/verify_history_toggle.py new file mode 100644 index 00000000..2eba3161 --- /dev/null +++ b/verify_history_toggle.py @@ -0,0 +1,27 @@ +from playwright.sync_api import sync_playwright +import time + +def run(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = browser.new_context(viewport={'width': 1280, 'height': 800}) + page = context.new_page() + + try: + print("Navigating to http://localhost:3000...") + page.goto("http://localhost:3000", timeout=60000) + time.sleep(5) + print(f"Current URL: {page.url}") + + page.screenshot(path="initial_load.png") + + # If we are on /auth, we can't really test the history toggle easily without logging in. + # But we can at least check if the header is there if it's visible on /auth (unlikely). + + except Exception as e: + print(f"Error: {e}") + finally: + browser.close() + +if __name__ == "__main__": + run()