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
98 changes: 61 additions & 37 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
getMutableAIState
} from 'ai/rsc'
import { CoreMessage, ToolResultPart } from 'ai'
import { nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import type { FeatureCollection } from 'geojson'
import { Spinner } from '@/components/ui/spinner'
import { Section } from '@/components/section'
Expand All @@ -21,6 +21,7 @@ import { BotMessage } from '@/components/message'
import { SearchSection } from '@/components/search-section'
import SearchRelated from '@/components/search-related'
import { GeoJsonLayer } from '@/components/map/geojson-layer'
import { ResolutionCarousel } from '@/components/resolution-carousel'
import { ResolutionImage } from '@/components/resolution-image'
import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
Expand Down Expand Up @@ -50,18 +51,29 @@ async function submit(formData?: FormData, skip?: boolean) {
}

if (action === 'resolution_search') {
const file = formData?.get('file') as File;
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;
const timezone = (formData?.get('timezone') as string) || 'UTC';
const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined;

if (!file) {
throw new Error('No file provided for resolution search.');
}

const mapboxBuffer = file_mapbox ? await file_mapbox.arrayBuffer() : null;
const mapboxDataUrl = mapboxBuffer ? `data:${file_mapbox.type};base64,${Buffer.from(mapboxBuffer).toString('base64')}` : null;

const googleBuffer = file_google ? await file_google.arrayBuffer() : null;
const googleDataUrl = googleBuffer ? `data:${file_google.type};base64,${Buffer.from(googleBuffer).toString('base64')}` : null;

const buffer = await file.arrayBuffer();
const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`;

const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter(
message =>
(message: any) =>
message.role !== 'tool' &&
message.type !== 'followup' &&
message.type !== 'related' &&
Expand Down Expand Up @@ -89,7 +101,7 @@ async function submit(formData?: FormData, skip?: boolean) {

async function processResolutionSearch() {
try {
const streamResult = await resolutionSearch(messages, timezone, drawnFeatures);
const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location);

let fullSummary = '';
for await (const partialObject of streamResult.partialObjectStream) {
Expand All @@ -113,7 +125,7 @@ async function submit(formData?: FormData, skip?: boolean) {

messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' });

const sanitizedMessages: CoreMessage[] = messages.map(m => {
const sanitizedMessages: CoreMessage[] = messages.map((m: any) => {
if (Array.isArray(m.content)) {
return {
...m,
Expand All @@ -124,7 +136,7 @@ async function submit(formData?: FormData, skip?: boolean) {
})

const currentMessages = aiState.get().messages;
const sanitizedHistory = currentMessages.map(m => {
const sanitizedHistory = currentMessages.map((m: any) => {
if (m.role === "user" && Array.isArray(m.content)) {
return {
...m,
Expand Down Expand Up @@ -159,7 +171,9 @@ async function submit(formData?: FormData, skip?: boolean) {
role: 'assistant',
content: JSON.stringify({
...analysisResult,
image: dataUrl
image: dataUrl,
mapboxImage: mapboxDataUrl,
googleImage: googleDataUrl
}),
type: 'resolution_search_result'
},
Expand Down Expand Up @@ -190,7 +204,11 @@ async function submit(formData?: FormData, skip?: boolean) {

uiStream.update(
<Section title="response">
<ResolutionImage src={dataUrl} />
<ResolutionCarousel
mapboxImage={mapboxDataUrl || undefined}
googleImage={googleDataUrl || undefined}
initialImage={dataUrl}
/>
<BotMessage content={summaryStream.value} />
</Section>
);
Expand All @@ -203,43 +221,20 @@ async function submit(formData?: FormData, skip?: boolean) {
};
}

const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter(
message =>
message.role !== 'tool' &&
message.type !== 'followup' &&
message.type !== 'related' &&
message.type !== 'end' &&
message.type !== 'resolution_search_result'
).map(m => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter((part: any) =>
part.type !== "image" || (typeof part.image === "string" && part.image.startsWith("data:"))
)
} as any
}
return m
})

const groupeId = nanoid()
const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true'
const maxMessages = useSpecificAPI ? 5 : 10
messages.splice(0, Math.max(messages.length - maxMessages, 0))

const file = !skip ? (formData?.get('file') as File) : undefined
const userInput = skip
? `{"action": "skip"}`
: ((formData?.get('related_query') as string) ||
(formData?.get('input') as string))

if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') {
if (userInput && (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?')) {
const definition = userInput.toLowerCase().trim() === 'what is a planet computer?'
? `A planet computer is a proprietary environment aware system that interoperates weather forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)`

: `QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery. Available for our Pro and Enterprise customers. [QCX Pricing] (https://www.queue.cx/#pricing)`;

const content = JSON.stringify(Object.fromEntries(formData!));
const type = 'input';
const groupeId = nanoid();

aiState.update({
...aiState.get(),
Expand Down Expand Up @@ -299,10 +294,9 @@ async function submit(formData?: FormData, skip?: boolean) {
id: nanoid(),
isGenerating: isGenerating.value,
component: uiStream.value,
isCollapsed: isCollapsed.value,
isCollapsed: isCollapsed.value
};
}
const file = !skip ? (formData?.get('file') as File) : undefined

if (!userInput && !file) {
isGenerating.done(false)
Expand All @@ -314,6 +308,30 @@ async function submit(formData?: FormData, skip?: boolean) {
}
}

const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter(
(message: any) =>
message.role !== 'tool' &&
message.type !== 'followup' &&
message.type !== 'related' &&
message.type !== 'end' &&
message.type !== 'resolution_search_result'
).map((m: any) => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter((part: any) =>
part.type !== "image" || (typeof part.image === "string" && part.image.startsWith("data:"))
)
} as any
}
return m
})

const groupeId = nanoid()
const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true'
const maxMessages = useSpecificAPI ? 5 : 10
messages.splice(0, Math.max(messages.length - maxMessages, 0))

const messageParts: {
type: 'text' | 'image'
text?: string
Expand Down Expand Up @@ -725,12 +743,18 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
const analysisResult = JSON.parse(content as string);
const geoJson = analysisResult.geoJson as FeatureCollection;
const image = analysisResult.image as string;
const mapboxImage = analysisResult.mapboxImage as string;
const googleImage = analysisResult.googleImage as string;

return {
id,
component: (
<>
{image && <ResolutionImage src={image} />}
<ResolutionCarousel
mapboxImage={mapboxImage}
googleImage={googleImage}
initialImage={image}
/>
{geoJson && (
<GeoJsonLayer id={id} data={geoJson} />
)}
Expand Down
1 change: 0 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ export default function RootLayout({
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<HistorySidebar />
<Footer />
<Toaster />
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Chat } from '@/components/chat'
import {nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import { AI } from './actions'

export const maxDuration = 60
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions chat-panel.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<<<<<<< SEARCH
// New chat button (appears when there are messages)
if (messages.length > 0 && !isMobile) {
return (
<div
className={cn(
'fixed bottom-2 left-2 flex justify-start items-center pointer-events-none',
isMobile ? 'w-full px-2' : 'md:bottom-8'
)}
>
<Button
type="button"
variant={'secondary'}
className="rounded-full bg-secondary/80 group transition-all hover:scale-105 pointer-events-auto"
onClick={() => handleClear()}
data-testid="new-chat-button"
>
<span className="text-sm mr-2 group-hover:block hidden animate-in fade-in duration-300">
New
</span>
<Plus size={18} className="group-hover:rotate-90 transition-all" />
</Button>
</div>
)
}
=======
// New chat button (appears when there are messages)
if (messages.length > 0 && !isMobile) {
return (
<div
className={cn(
'fixed bottom-4 left-4 flex justify-start items-center pointer-events-none z-50'
)}
>
<Button
type="button"
variant={'ghost'}
size={'icon'}
className="rounded-full transition-all hover:scale-110 pointer-events-auto text-primary"
onClick={() => handleClear()}
data-testid="new-chat-button"
title="New Chat"
>
<Sprout size={28} className="fill-primary/20" />
</Button>
</div>
)
}
>>>>>>> REPLACE
Comment on lines +1 to +49
Copy link

Choose a reason for hiding this comment

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

chat-panel.patch contains unresolved conflict markers (<<<<<<<, =======, >>>>>>>). Keeping this in the repo will trip up tooling and is easy to accidentally ship. If this patch is only for manual reference, it should not be committed; otherwise it must be resolved and applied into the real source file(s).

Suggestion

Remove chat-panel.patch from the repository, or resolve the conflict markers and apply the intended change directly to components/chat-panel.tsx (then delete the patch file). Reply with "@CharlieHelps yes please" if you'd like me to add a commit that deletes chat-panel.patch and layout.patch.

52 changes: 26 additions & 26 deletions components/calendar-notepad.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"use client"


import { Users } from "lucide-react";
import { searchUsers } from "@/lib/actions/users";
import type React from "react"
import { useState, useEffect } from "react"
import { ChevronLeft, ChevronRight, MapPin } from "lucide-react"
import { ChevronLeft, ChevronRight, MapPin, Users } from "lucide-react"
import { cn } from "@/lib/utils"
import { getNotes, saveNote } from "@/lib/actions/calendar"
import { searchUsers } from "@/lib/actions/users"
import { useMapData } from "./map/map-data-context"
import type { CalendarNote, NewCalendarNote } from "@/lib/types"
import { TimezoneClock } from "./timezone-clock"
Expand Down Expand Up @@ -76,21 +74,11 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) {
setNotes([savedNote, ...notes])
setNoteContent("")
setTaggedLocation(null)
setShowSuggestions(false)
}
}
}

const handleTagLocation = () => {
if (mapData.targetPosition) {
setTaggedLocation({
type: 'Point',
coordinates: mapData.targetPosition
});
setNoteContent(prev => `${prev} #location`);
}
};


const handleNoteContentChange = async (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setNoteContent(value);
Expand Down Expand Up @@ -126,6 +114,16 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) {
});
};

const handleTagLocation = () => {
if (mapData.targetPosition) {
setTaggedLocation({
type: 'Point',
coordinates: mapData.targetPosition
});
setNoteContent(prev => `${prev} #location`);
}
};

const handleFlyTo = (location: any) => {
if (location && location.coordinates) {
setMapData(prev => ({ ...prev, targetPosition: location.coordinates }));
Expand Down Expand Up @@ -209,22 +207,24 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) {
notes.map((note) => (
<div key={note.id} className="p-3 bg-muted rounded-md">
<div className="flex justify-between items-start">
<div>
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground mb-1">
{new Date(note.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
<p className="text-sm whitespace-pre-wrap break-words">{renderContent(note.content)}</p>
</div>
{note.locationTags && (
<button onClick={() => handleFlyTo(note.locationTags)} className="text-muted-foreground hover:text-foreground ml-2">
<MapPin className="h-5 w-5" />
</button>
)}
{note.userTags && note.userTags.length > 0 && (
<div className="text-muted-foreground ml-2 flex items-center" title={`${note.userTags.length} user(s) tagged`}>
<Users className="h-4 w-4" />
</div>
)}
<div className="flex items-center space-x-2 flex-shrink-0 ml-2">
{note.locationTags && (
<button onClick={() => handleFlyTo(note.locationTags)} className="text-muted-foreground hover:text-foreground" title="Fly to location">
<MapPin className="h-5 w-5" />
</button>
)}
{note.userTags && note.userTags.length > 0 && (
<div className="text-muted-foreground flex items-center" title={`${note.userTags.length} user(s) tagged`}>
<Users className="h-4 w-4" />
</div>
)}
</div>
</div>
</div>
))
Expand Down
Loading