-
-
Notifications
You must be signed in to change notification settings - Fork 8
Implement System-Wide Concurrency and Performance Improvements #345
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
eb3c708
fb825a1
f039453
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,4 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { nanoid } from 'nanoid'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { notFound, redirect } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Chat } from '@/components/chat'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { getChat, getChatMessages } from '@/lib/actions/chat'; // Added getChatMessages | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,15 +60,18 @@ export default async function SearchPage({ params }: SearchPageProps) { | |||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <AI | ||||||||||||||||||||||||||||||||||||||||||||||||
| initialAIState={{ | ||||||||||||||||||||||||||||||||||||||||||||||||
| const initialAIState = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| conversations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| chatId: chat.id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| messages: initialMessages, // Use the transformed messages from the database | ||||||||||||||||||||||||||||||||||||||||||||||||
| // isSharePage: true, // This was in PR#533, but share functionality is removed. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // If needed for styling or other logic, it can be set. | ||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| messages: initialMessages, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
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. 🧩 Analysis chainType the new conversation-based Enforce structure using - const initialAIState = {
+ const initialAIState = {
conversations: [
{
id: nanoid(),
chatId: chat.id,
messages: initialMessages,
}
]
- };
+ } satisfies { conversations: Array<{ id: string; chatId: string; messages: AIMessage[] }> };Also confirm Also applies to: 73-79 🏁 Script executed: #!/bin/bash
# Find AIMessage type definition and related types
rg -n "AIMessage|dbMsg" app/search/\[id\]/page.tsx -A 2 -B 2Length of output: 989 🏁 Script executed: #!/bin/bash
# Search for AIMessage type in the codebase
rg -n "type AIMessage|interface AIMessage" --type ts --type tsx -A 5Length of output: 27 🏁 Script executed: #!/bin/bash
# Find initialMessages definition
rg -n "initialMessages" app/search/\[id\]/page.tsx -B 5 -A 2Length of output: 464 🏁 Script executed: #!/bin/bash
# View the context around lines 63-79 to understand both locations
sed -n '55,85p' app/search/\[id\]/page.tsxLength of output: 704 🏁 Script executed: #!/bin/bash
# Search for role mapping patterns
rg -n "\.role|AIMessage\['role'\]" app/search/\[id\]/page.tsx -B 2 -A 2Length of output: 273 🏁 Script executed: #!/bin/bash
# Find where dbMsg comes from
rg -n "dbMsg|const.*Msg" app/search/\[id\]/page.tsx -B 3 -A 1Length of output: 558 🏁 Script executed: #!/bin/bash
# Find AIMessage type definition
rg -n "type AIMessage|interface AIMessage" lib/types -A 10Length of output: 529 🏁 Script executed: #!/bin/bash
# Find DrizzleMessage type definition
rg -n "type.*Message|interface.*Message" lib/actions/chat-db -A 5Length of output: 59 🏁 Script executed: #!/bin/bash
# Get lines 70-85 to see the second location mentioned (73-79)
sed -n '70,85p' app/search/\[id\]/page.tsxLength of output: 154 🏁 Script executed: #!/bin/bash
# Check TypeScript version in package.json
cat package.json | grep -A 2 '"typescript"'Length of output: 32 🏁 Script executed: #!/bin/bash
# Search for Message type import in page.tsx
rg -n "Message|import" app/search/\[id\]/page.tsx | head -20Length of output: 968 🏁 Script executed: #!/bin/bash
# Find chat-db file with different paths
fd -n "chat-db" --type fLength of output: 230 🏁 Script executed: #!/bin/bash
# Search for DrizzleMessage type definition broadly
rg -n "type.*Message|interface.*Message|export.*Message" lib/actions -A 3Length of output: 2907 🏁 Script executed: #!/bin/bash
# Search entire page.tsx for all initialAIState occurrences
rg -n "initialAIState" app/search/\[id\]/page.tsx -B 2 -A 5Length of output: 337 🏁 Script executed: #!/bin/bash
# Get entire file line count and check around 73-79
wc -l app/search/\[id\]/page.tsxLength of output: 27 🏁 Script executed: #!/bin/bash
# Find database schema definition for messages table
fd -type f -name "*.ts" -o -name "*.sql" | xargs rg -l "messages.*schema\|messages.*table\|role.*enum" | head -5Length of output: 170 🏁 Script executed: #!/bin/bash
# Search for messages table definition in schema files
rg -n "export const messages|messages = table\|messages: table" --type ts -A 15Length of output: 2095 🏁 Script executed: #!/bin/bash
# Find where role enum/type is defined
rg -n "role.*enum\|role:.*varchar\|role:.*text\|role.*values" --type ts -A 2Length of output: 0 Type The database schema stores - const initialAIState = {
+ const initialAIState = {
conversations: [
{
id: nanoid(),
chatId: chat.id,
messages: initialMessages,
}
]
- };
+ } satisfies { conversations: Array<{ id: string; chatId: string; messages: AIMessage[] }> };Additionally, replace the unsafe cast at line 52 with a validator/mapper that ensures 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <AI initialAIState={initialAIState}> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <MapDataProvider> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Chat id={id} /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| </MapDataProvider> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,7 +23,7 @@ export interface ChatPanelRef { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, input, setInput }, ref) => { | ||||||||||||||||||||||||||||||||||
| const [, setMessages] = useUIState<typeof AI>() | ||||||||||||||||||||||||||||||||||
| const { submit, clearChat } = useActions() | ||||||||||||||||||||||||||||||||||
| const { submit } = useActions() | ||||||||||||||||||||||||||||||||||
| // Removed mcp instance as it's no longer passed to submit | ||||||||||||||||||||||||||||||||||
| const [isMobile, setIsMobile] = useState(false) | ||||||||||||||||||||||||||||||||||
| const [selectedFile, setSelectedFile] = useState<File | null>(null) | ||||||||||||||||||||||||||||||||||
|
|
@@ -69,7 +69,10 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||||||||||||||||||||||||||||||||||
| const handleSubmit = async ( | ||||||||||||||||||||||||||||||||||
| e: React.FormEvent<HTMLFormElement>, | ||||||||||||||||||||||||||||||||||
| newChat?: boolean | ||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||
| e.preventDefault() | ||||||||||||||||||||||||||||||||||
| if (!input && !selectedFile) { | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
|
|
@@ -86,18 +89,23 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| setMessages(currentMessages => [ | ||||||||||||||||||||||||||||||||||
| ...currentMessages, | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||||||||||
| component: <UserMessage content={content} /> | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||
| if (!newChat) { | ||||||||||||||||||||||||||||||||||
| setMessages(currentMessages => [ | ||||||||||||||||||||||||||||||||||
| ...currentMessages, | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||||||||||
| component: <UserMessage content={content} /> | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const formData = new FormData(e.currentTarget) | ||||||||||||||||||||||||||||||||||
| if (selectedFile) { | ||||||||||||||||||||||||||||||||||
| formData.append('file', selectedFile) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (newChat) { | ||||||||||||||||||||||||||||||||||
| formData.append('newChat', 'true') | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| setInput('') | ||||||||||||||||||||||||||||||||||
| clearAttachment() | ||||||||||||||||||||||||||||||||||
|
|
@@ -106,10 +114,13 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||
| setMessages(currentMessages => [...currentMessages, responseMessage as any]) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const handleClear = async () => { | ||||||||||||||||||||||||||||||||||
| const handleNewConversation = async () => { | ||||||||||||||||||||||||||||||||||
| setMessages([]) | ||||||||||||||||||||||||||||||||||
| clearAttachment() | ||||||||||||||||||||||||||||||||||
| await clearChat() | ||||||||||||||||||||||||||||||||||
| const formData = new FormData() | ||||||||||||||||||||||||||||||||||
| formData.append('newChat', 'true') | ||||||||||||||||||||||||||||||||||
| const responseMessage = await submit(formData) | ||||||||||||||||||||||||||||||||||
| setMessages(currentMessages => [...currentMessages, responseMessage as any]) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
123
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. Starting a new conversation currently adds a server response to the local UI state even though the server returns no UI content for a pure SuggestionAvoid appending the server response for a const handleNewConversation = async () => { Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
Comment on lines
+117
to
123
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. Avoid clearing local UI state to prevent flicker when starting a new conversation
Apply this diff: - const handleNewConversation = async () => {
- setMessages([])
- clearAttachment()
- const formData = new FormData()
- formData.append('newChat', 'true')
- const responseMessage = await submit(formData)
- setMessages(currentMessages => [...currentMessages, responseMessage as any])
- }
+ const handleNewConversation = async () => {
+ clearAttachment()
+ const formData = new FormData()
+ formData.append('newChat', 'true')
+ const responseMessage = await submit(formData)
+ setMessages(currentMessages => [...currentMessages, responseMessage as any])
+ }If you need a visual separator immediately, the server will add one via 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||
|
|
@@ -129,10 +140,10 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||
| variant={'secondary'} | ||||||||||||||||||||||||||||||||||
| className="rounded-full bg-secondary/80 group transition-all hover:scale-105 pointer-events-auto" | ||||||||||||||||||||||||||||||||||
| onClick={() => handleClear()} | ||||||||||||||||||||||||||||||||||
| onClick={handleNewConversation} | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| <span className="text-sm mr-2 group-hover:block hidden animate-in fade-in duration-300"> | ||||||||||||||||||||||||||||||||||
| New | ||||||||||||||||||||||||||||||||||
| New Conversation | ||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||
| <Plus size={18} className="group-hover:rotate-90 transition-all" /> | ||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { useMapToggle, MapToggleEnum } from '../map-toggle-context' | |||||
| import { useMapData } from './map-data-context'; // Add this import | ||||||
| import { useMapLoading } from '../map-loading-context'; // Import useMapLoading | ||||||
| import { useMap } from './map-context' | ||||||
| import { useWorker } from '@/hooks/useWorker' | ||||||
|
|
||||||
|
Comment on lines
+14
to
15
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. 🧹 Nitpick | 🔵 Trivial Good move using a worker; remove unused With calcs offloaded, 🤖 Prompt for AI Agents |
||||||
| mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; | ||||||
|
|
||||||
|
|
@@ -38,6 +39,8 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number | |||||
| // Refs for long-press functionality | ||||||
| const longPressTimerRef = useRef<NodeJS.Timeout | null>(null); | ||||||
| const isMouseDownRef = useRef<boolean>(false); | ||||||
| const turfWorker = useWorker<any[]>(new URL('/workers/turf.worker.ts', import.meta.url)); | ||||||
|
|
||||||
|
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. The Web Worker URL is created with a leading slash and without memoization. SuggestionMemoize a correct relative URL and pass the memoized reference into import { useMemo } from 'react'; This ensures a stable identity and a bundler-friendly path. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
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. Worker URL should be relative to this module, not absolute.
- const turfWorker = useWorker<any[]>(new URL('/workers/turf.worker.ts', import.meta.url));
+ const turfWorker = useWorker<any[]>(new URL('../../workers/turf.worker.ts', import.meta.url));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| // const [isMapLoaded, setIsMapLoaded] = useState(false); // Removed local state | ||||||
|
|
||||||
|
|
@@ -71,98 +74,70 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number | |||||
| lineLabelsRef.current = {} | ||||||
|
|
||||||
| const features = drawRef.current.getAll().features | ||||||
| const currentDrawnFeatures: Array<{ id: string; type: 'Polygon' | 'LineString'; measurement: string; geometry: any }> = [] | ||||||
|
|
||||||
| features.forEach(feature => { | ||||||
| const id = feature.id as string | ||||||
| let featureType: 'Polygon' | 'LineString' | null = null; | ||||||
| let measurement = ''; | ||||||
|
|
||||||
| if (feature.geometry.type === 'Polygon') { | ||||||
| featureType = 'Polygon'; | ||||||
| // Calculate area for polygons | ||||||
| const area = turf.area(feature) | ||||||
| const formattedArea = formatMeasurement(area, true) | ||||||
| measurement = formattedArea; | ||||||
|
|
||||||
| // Get centroid for label placement | ||||||
| const centroid = turf.centroid(feature) | ||||||
| const coordinates = centroid.geometry.coordinates | ||||||
|
|
||||||
| // Create a label | ||||||
| const el = document.createElement('div') | ||||||
| el.className = 'area-label' | ||||||
| el.style.background = 'rgba(255, 255, 255, 0.8)' | ||||||
| el.style.padding = '4px 8px' | ||||||
| el.style.borderRadius = '4px' | ||||||
| el.style.fontSize = '12px' | ||||||
| el.style.fontWeight = 'bold' | ||||||
| el.style.color = '#333333' // Added darker color | ||||||
| el.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)' | ||||||
| el.style.pointerEvents = 'none' | ||||||
| el.textContent = formattedArea | ||||||
|
|
||||||
| // Add marker for the label | ||||||
|
|
||||||
| turfWorker.postMessage({ features }); | ||||||
|
|
||||||
| }, [turfWorker]) | ||||||
|
|
||||||
|
Comment on lines
75
to
80
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. 🛠️ Refactor suggestion | 🟠 Major Stabilize event handler dependencies to avoid listener churn. Depending on the whole - const updateMeasurementLabels = useCallback(() => {
+ const updateMeasurementLabels = useCallback(() => {
if (!map.current || !drawRef.current) return
// ...
- turfWorker.postMessage({ features });
- }, [turfWorker])
+ turfWorker.postMessage({ features });
+ }, [turfWorker.postMessage])Requires the
🤖 Prompt for AI Agents |
||||||
| useEffect(() => { | ||||||
| if (turfWorker.data && map.current && drawRef.current) { | ||||||
| const features = drawRef.current.getAll().features; | ||||||
| const currentDrawnFeatures: Array<{ id: string; type: 'Polygon' | 'LineString'; measurement: string; geometry: any }> = []; | ||||||
|
|
||||||
| turfWorker.data.forEach(result => { | ||||||
| const { id, calculation } = result; | ||||||
| if (!calculation) return; | ||||||
|
|
||||||
| const feature = features.find(f => f.id === id); | ||||||
| if (!feature) return; | ||||||
|
|
||||||
| let featureType: 'Polygon' | 'LineString' | null = null; | ||||||
| let measurement = ''; | ||||||
| let coordinates: [number, number] | undefined; | ||||||
|
|
||||||
| if (calculation.type === 'Polygon') { | ||||||
| featureType = 'Polygon'; | ||||||
| measurement = formatMeasurement(calculation.area, true); | ||||||
| coordinates = calculation.center; | ||||||
| } else if (calculation.type === 'LineString') { | ||||||
| featureType = 'LineString'; | ||||||
| measurement = formatMeasurement(calculation.length, false); | ||||||
| coordinates = calculation.center; | ||||||
| } | ||||||
|
|
||||||
| if (featureType && measurement && coordinates && map.current) { | ||||||
| const el = document.createElement('div'); | ||||||
| el.className = `${featureType.toLowerCase()}-label`; | ||||||
| el.style.background = 'rgba(255, 255, 255, 0.8)'; | ||||||
| el.style.padding = '4px 8px'; | ||||||
| el.style.borderRadius = '4px'; | ||||||
| el.style.fontSize = '12px'; | ||||||
| el.style.fontWeight = 'bold'; | ||||||
| el.style.color = '#333333'; | ||||||
| el.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; | ||||||
| el.style.pointerEvents = 'none'; | ||||||
| el.textContent = measurement; | ||||||
|
|
||||||
| if (map.current) { | ||||||
| const marker = new mapboxgl.Marker({ element: el }) | ||||||
| .setLngLat(coordinates as [number, number]) | ||||||
| .addTo(map.current) | ||||||
|
|
||||||
| polygonLabelsRef.current[id] = marker | ||||||
| } | ||||||
| } | ||||||
| else if (feature.geometry.type === 'LineString') { | ||||||
| featureType = 'LineString'; | ||||||
| // Calculate length for lines | ||||||
| const length = turf.length(feature, { units: 'kilometers' }) * 1000 // Convert to meters | ||||||
| const formattedLength = formatMeasurement(length, false) | ||||||
| measurement = formattedLength; | ||||||
|
|
||||||
| // Get midpoint for label placement | ||||||
| const line = feature.geometry.coordinates | ||||||
| const midIndex = Math.floor(line.length / 2) - 1 | ||||||
| const midpoint = midIndex >= 0 ? line[midIndex] : line[0] | ||||||
|
|
||||||
| // Create a label | ||||||
| const el = document.createElement('div') | ||||||
| el.className = 'distance-label' | ||||||
| el.style.background = 'rgba(255, 255, 255, 0.8)' | ||||||
| el.style.padding = '4px 8px' | ||||||
| el.style.borderRadius = '4px' | ||||||
| el.style.fontSize = '12px' | ||||||
| el.style.fontWeight = 'bold' | ||||||
| el.style.color = '#333333' // Added darker color | ||||||
| el.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)' | ||||||
| el.style.pointerEvents = 'none' | ||||||
| el.textContent = formattedLength | ||||||
|
|
||||||
| // Add marker for the label | ||||||
| if (map.current) { | ||||||
| const marker = new mapboxgl.Marker({ element: el }) | ||||||
| .setLngLat(midpoint as [number, number]) | ||||||
| .addTo(map.current) | ||||||
|
|
||||||
| lineLabelsRef.current[id] = marker | ||||||
| } | ||||||
| } | ||||||
| .setLngLat(coordinates) | ||||||
| .addTo(map.current); | ||||||
|
|
||||||
| if (featureType && id && measurement && feature.geometry) { | ||||||
| currentDrawnFeatures.push({ | ||||||
| id, | ||||||
| type: featureType, | ||||||
| measurement, | ||||||
| geometry: feature.geometry, | ||||||
| }); | ||||||
| } | ||||||
| }) | ||||||
| if (featureType === 'Polygon') { | ||||||
| polygonLabelsRef.current[id] = marker; | ||||||
| } else { | ||||||
| lineLabelsRef.current[id] = marker; | ||||||
| } | ||||||
|
|
||||||
| setMapData(prevData => ({ ...prevData, drawnFeatures: currentDrawnFeatures })) | ||||||
| }, [formatMeasurement, setMapData]) | ||||||
| currentDrawnFeatures.push({ | ||||||
| id, | ||||||
| type: featureType, | ||||||
| measurement, | ||||||
| geometry: feature.geometry, | ||||||
| }); | ||||||
| } | ||||||
| }); | ||||||
| setMapData(prevData => ({ ...prevData, drawnFeatures: currentDrawnFeatures })); | ||||||
| } | ||||||
| }, [turfWorker.data, formatMeasurement, setMapData]) | ||||||
|
Comment on lines
+82
to
+140
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. Worker results are processed asynchronously without staleness protection. Rapid edits can cause out-of-order results to repaint labels for older geometries. Although you guard against deleted features, measurements can still be stale relative to the current geometry. SuggestionIntroduce a monotonically increasing
const calcRequestId = useRef(0)
const updateMeasurementLabels = useCallback(() => {
// ... remove existing labels
const requestId = ++calcRequestId.current
const features = drawRef.current!.getAll().features
turfWorker.postMessage({ requestId, features })
}, [turfWorker])
useEffect(() => {
if (!turfWorker.data || !map.current || !drawRef.current) return
const { requestId, results } = turfWorker.data
if (requestId !== calcRequestId.current) return // ignore stale
// process results as before...
}, [turfWorker.data])
Reply with "@CharlieHelps yes please" if you'd like me to add these correlated request changes across the hook, worker, and map component. |
||||||
|
|
||||||
| // Handle map rotation | ||||||
| const rotateMap = useCallback(() => { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||||||||||||||||||||||||
| import { useState, useEffect, useRef } from 'react'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| type UseWorkerReturnType<T> = { | ||||||||||||||||||||||||||||
| postMessage: (data: any) => void; | ||||||||||||||||||||||||||||
| data: T | null; | ||||||||||||||||||||||||||||
| error: string | null; | ||||||||||||||||||||||||||||
| isLoading: boolean; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export function useWorker<T>(workerUrl: URL): UseWorkerReturnType<T> { | ||||||||||||||||||||||||||||
| const [data, setData] = useState<T | null>(null); | ||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||
| const [isLoading, setIsLoading] = useState<boolean>(false); | ||||||||||||||||||||||||||||
| const workerRef = useRef<Worker | null>(null); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||
| // Create a new worker instance | ||||||||||||||||||||||||||||
| const worker = new Worker(workerUrl, { type: 'module' }); | ||||||||||||||||||||||||||||
| workerRef.current = worker; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| worker.onmessage = (event: MessageEvent<T>) => { | ||||||||||||||||||||||||||||
| setData(event.data); | ||||||||||||||||||||||||||||
| setIsLoading(false); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| worker.onerror = (err: ErrorEvent) => { | ||||||||||||||||||||||||||||
| setError(err.message); | ||||||||||||||||||||||||||||
| setIsLoading(false); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+28
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. Handle structured-clone errors too. Add worker.onerror = (err: ErrorEvent) => {
setError(err.message);
setIsLoading(false);
};
+
+ worker.onmessageerror = (err: MessageEvent) => {
+ setError('Worker message deserialization failed');
+ setIsLoading(false);
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Cleanup worker on component unmount | ||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||
| if (workerRef.current) { | ||||||||||||||||||||||||||||
| workerRef.current.terminate(); | ||||||||||||||||||||||||||||
| workerRef.current = null; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| }, [workerUrl]); | ||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+41
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.
SuggestionAugment the hook to allow structured messages with a type WorkerEnvelope<T> = { requestId?: number; results: T }
// onmessage
worker.onmessage = (event: MessageEvent<WorkerEnvelope<T>>) => {
setData(event.data as any)
setIsLoading(false)
}
// postMessage stays generic; caller provides requestIdI can wire this up and update the map/worker to use it. Reply with "@CharlieHelps yes please" to have me implement it. |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+42
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.
SuggestionReduce sensitivity to object identity by depending on the string form of the URL (or change the hook signature to accept a string): useEffect(() => { This prevents unnecessary terminations when callers re-create equivalent |
||||||||||||||||||||||||||||
| const postMessage = (messageData: any) => { | ||||||||||||||||||||||||||||
| if (workerRef.current) { | ||||||||||||||||||||||||||||
| setIsLoading(true); | ||||||||||||||||||||||||||||
| setError(null); | ||||||||||||||||||||||||||||
| setData(null); | ||||||||||||||||||||||||||||
| workerRef.current.postMessage(messageData); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
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. 🛠️ Refactor suggestion | 🟠 Major Stabilize Use - const postMessage = (messageData: any) => {
+ const postMessage = useCallback((messageData: any, transfer?: Transferable[]) => {
if (workerRef.current) {
setIsLoading(true);
setError(null);
setData(null);
- workerRef.current.postMessage(messageData);
+ // pass transferables when provided
+ transfer ? workerRef.current.postMessage(messageData, transfer)
+ : workerRef.current.postMessage(messageData);
}
- };
+ }, []);
- return { postMessage, data, error, isLoading };
+ return useMemo(() => ({ postMessage, data, error, isLoading }), [postMessage, data, error, isLoading]);Also applies to: 49-50 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return { postMessage, data, error, isLoading }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
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.
🛠️ Refactor suggestion | 🟠 Major
Type the initial AI state to prevent schema drift.
Strongly type this object so refactors don’t silently break runtime.
Also applies to: 21-21
🤖 Prompt for AI Agents