-
-
Couldn't load subscription status.
- Fork 6
Feat/add spinning loader #308
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 all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -237,3 +237,16 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .mapboxgl-ctrl-group button { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pointer-events: auto !important; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @keyframes spin-counter-clockwise { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: rotate(360deg); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: rotate(0deg); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .animate-spin-counter-clockwise { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| animation: spin-counter-clockwise 1s linear infinite; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+241
to
+252
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 keyframes use SuggestionConsider changing the keyframes to rotate from @keyframes spin-counter-clockwise {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
.animate-spin-counter-clockwise {
animation: spin-counter-clockwise 1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-spin-counter-clockwise {
animation: none;
}
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
Comment on lines
+241
to
+252
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. Respect prefers-reduced-motion for the new spinner. Add a reduced‑motion override to avoid continuous rotation for users who request less motion. @keyframes spin-counter-clockwise {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
.animate-spin-counter-clockwise {
animation: spin-counter-clockwise 1s linear infinite;
}
+
+@media (prefers-reduced-motion: reduce) {
+ .animate-spin-counter-clockwise {
+ animation: none;
+ }
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,9 @@ import { ChatPanel } from './chat-panel' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ChatMessages } from './chat-messages' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { EmptyScreen } from './empty-screen' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Mapbox } from './map/mapbox-map' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useUIState, useAIState } from 'ai/rsc' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useUIState, useAIState, useStreamableValue, type StreamableValue } from 'ai/rsc' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useIsLoading } from '@/components/is-loading-provider' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { UIState } from '@/app/actions' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import MobileIconsBar from './mobile-icons-bar' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle-context"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import SettingsView from "@/components/settings/settings-view"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,6 +19,19 @@ type ChatProps = { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id?: string // This is the chatId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function LoadingStateUpdater({ messages }: { messages: UIState }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { setIsLoading } = useIsLoading(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lastMessage = messages[messages.length - 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isGenerating = lastMessage?.isGenerating; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading] = useStreamableValue(isGenerating); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsLoading(loading || false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [loading, setIsLoading]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+33
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. When SuggestionConsider adding a cleanup that resets the flag: useEffect(() => {
setIsLoading(Boolean(loading));
return () => setIsLoading(false);
}, [loading, setIsLoading]);Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
Comment on lines
+22
to
+33
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 chainEnsure cleanup so the global loading indicator never gets stuck. When Chat unmounts during generation, isLoading can remain true. Reset on unmount. useEffect(() => {
- setIsLoading(loading || false);
- }, [loading, setIsLoading]);
+ setIsLoading(Boolean(loading));
+ return () => setIsLoading(false);
+ }, [loading, setIsLoading]);If 🌐 Web query: 💡 Result: Short answer: No — useStreamableValue expects a StreamableValue created by createStreamableValue and does not accept undefined safely. The docs state the hook "takes a streamable value created using createStreamableValue" (so pass the streamable.value), and the RSC utilities/type code shows the streamable value shape is required. See the docs and implementation/discussion for details. [1][2] Sources
Guard streamable value and reset loading on unmount function LoadingStateUpdater({ messages }: { messages: UIState }) {
const { setIsLoading } = useIsLoading();
const lastMessage = messages[messages.length - 1];
const isGenerating = lastMessage?.isGenerating;
- const [loading] = useStreamableValue(isGenerating);
+ const [loading] = isGenerating != null
+ ? useStreamableValue(isGenerating)
+ : [false];
useEffect(() => {
- setIsLoading(loading || false);
- }, [loading, setIsLoading]);
+ setIsLoading(loading);
+ return () => setIsLoading(false);
+ }, [loading, setIsLoading]);
return null;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function Chat({ id }: ChatProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = usePathname() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,6 +90,7 @@ export function Chat({ id }: ChatProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isMobile) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <MapDataProvider> {/* Add Provider */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LoadingStateUpdater messages={messages} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mobile-layout-container"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mobile-map-section"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {activeView ? <SettingsView /> : <Mapbox />} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -104,6 +120,7 @@ export function Chat({ id }: ChatProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Desktop layout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <MapDataProvider> {/* Add Provider */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LoadingStateUpdater messages={messages} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex justify-start items-start"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* This is the new div for scrolling */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-1/2 flex flex-col space-y-3 md:space-y-4 px-8 sm:px-12 pt-12 md:pt-14 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| 'use client' | ||
|
|
||
| import React from 'react' | ||
| import Image from 'next/image' | ||
| import { useIsLoading } from './is-loading-provider' | ||
| import { ModeToggle } from './mode-toggle' | ||
| import { cn } from '@/lib/utils' | ||
| import HistoryContainer from './history-container' | ||
|
|
@@ -15,6 +18,8 @@ import { MapToggle } from './map-toggle' | |
| import { ProfileToggle } from './profile-toggle' | ||
|
|
||
| export const Header = () => { | ||
| const { isLoading } = useIsLoading() | ||
|
|
||
| return ( | ||
| <header className="fixed w-full p-1 md:p-2 flex justify-between items-center z-10 backdrop-blur md:backdrop-blur-none bg-background/80 md:bg-transparent"> | ||
|
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. Fix a11y: non-functional Button and expose loading state to ATs.
Apply: @@
- <header className="fixed w-full p-1 md:p-2 flex justify-between items-center z-10 backdrop-blur md:backdrop-blur-none bg-background/80 md:bg-transparent">
+ <header
+ className="fixed w-full p-1 md:p-2 flex justify-between items-center z-10 backdrop-blur md:backdrop-blur-none bg-background/80 md:bg-transparent"
+ aria-busy={isLoading}
+ >
@@
- <Button variant="ghost" size="icon">
- <Image
+ <Button variant="ghost" size="icon" asChild>
+ <a href="/" aria-label="Home">
+ <Image
src="/images/logo.svg"
alt="Logo"
width={24}
height={24}
className={cn('h-6 w-auto', {
'animate-spin-counter-clockwise': isLoading
})}
- />
- </Button>
+ />
+ </a>
+ </Button>Also applies to: 32-42 🤖 Prompt for AI Agents |
||
| <div> | ||
|
|
@@ -25,7 +30,15 @@ export const Header = () => { | |
|
|
||
| <div className="absolute left-1"> | ||
| <Button variant="ghost" size="icon"> | ||
| <Image src="/images/logo.svg" alt="Logo" width={24} height={24} className="h-6 w-auto" /> | ||
| <Image | ||
| src="/images/logo.svg" | ||
| alt="Logo" | ||
| width={24} | ||
| height={24} | ||
| className={cn('h-6 w-auto', { | ||
| 'animate-spin-counter-clockwise': isLoading | ||
| })} | ||
| /> | ||
| </Button> | ||
| </div> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| 'use client' | ||
|
|
||
| import { createContext, useContext, useState, ReactNode } from 'react' | ||
|
|
||
| interface IsLoadingContextType { | ||
| isLoading: boolean | ||
| setIsLoading: (isLoading: boolean) => void | ||
| } | ||
|
Comment on lines
+5
to
+8
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 Broaden setIsLoading type to match React’s setState. Current type blocks functional updates. Use Dispatch<SetStateAction>. -import { createContext, useContext, useState, ReactNode } from 'react'
+import { createContext, useContext, useState, ReactNode, type Dispatch, type SetStateAction } from 'react'
@@
interface IsLoadingContextType {
isLoading: boolean
- setIsLoading: (isLoading: boolean) => void
+ setIsLoading: Dispatch<SetStateAction<boolean>>
}Also applies to: 15-15 🤖 Prompt for AI Agents |
||
|
|
||
| const IsLoadingContext = createContext<IsLoadingContextType | undefined>( | ||
| undefined | ||
| ) | ||
|
|
||
| export function IsLoadingProvider({ children }: { children: ReactNode }) { | ||
| const [isLoading, setIsLoading] = useState(false) | ||
|
|
||
| return ( | ||
| <IsLoadingContext.Provider value={{ isLoading, setIsLoading }}> | ||
| {children} | ||
| </IsLoadingContext.Provider> | ||
| ) | ||
| } | ||
|
|
||
| export function useIsLoading() { | ||
| const context = useContext(IsLoadingContext) | ||
| if (context === undefined) { | ||
| throw new Error('useIsLoading must be used within an IsLoadingProvider') | ||
| } | ||
| return context | ||
| } | ||
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.
To respect users’ reduced-motion preferences and improve accessibility, consider disabling the spin animation when
prefers-reduced-motion: reduceis set.Suggestion
Add a reduced-motion override:
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this CSS.