Skip to content

Commit a497ef9

Browse files
authored
Merge pull request #786 from miurla/chore/next-16-2-upgrade
Upgrade to Next.js 16.2.1
2 parents 42f5d80 + 4666662 commit a497ef9

13 files changed

Lines changed: 395 additions & 389 deletions

.eslintrc.json

Lines changed: 0 additions & 36 deletions
This file was deleted.

bun.lock

Lines changed: 140 additions & 154 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/artifact/chat-artifact-container.tsx

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,45 +42,45 @@ export function ChatArtifactContainer({
4242
children: React.ReactNode
4343
}) {
4444
const { state } = useArtifact()
45-
const containerRef = useRef<HTMLDivElement>(null)
45+
const [containerElement, setContainerElement] =
46+
useState<HTMLDivElement | null>(null)
47+
const hasAppliedSavedWidthRef = useRef(false)
4648
const [width, setWidth] = useState(DEFAULT_WIDTH)
4749
const [isResizing, setIsResizing] = useState(false)
4850
const hasUser = useHasUser()
4951
const { open, isMobile: isMobileSidebar } = useSidebar()
5052

51-
// Load saved width after hydration
52-
useEffect(() => {
53+
const setContainerRef = useCallback((node: HTMLDivElement | null) => {
54+
setContainerElement(node)
55+
56+
if (!node || hasAppliedSavedWidthRef.current) {
57+
return
58+
}
59+
60+
hasAppliedSavedWidthRef.current = true
61+
5362
const savedWidth = localStorage.getItem('artifactPanelWidth')
54-
if (savedWidth) {
55-
const parsedWidth = parseInt(savedWidth, 10)
56-
// Ensure parsedWidth is at least MIN_WIDTH to prevent invalid panel states
57-
if (
58-
!isNaN(parsedWidth) &&
59-
parsedWidth >= MIN_WIDTH &&
60-
parsedWidth <= MAX_WIDTH
61-
) {
62-
// Clamp against available space considering chat minimum width
63-
const containerRect = containerRef.current?.getBoundingClientRect()
64-
if (containerRect) {
65-
const { allowedMin, allowedMax } = getAllowedWidthBounds(
66-
containerRect.width
67-
)
68-
const clamped = Math.min(
69-
Math.max(parsedWidth, allowedMin),
70-
allowedMax
71-
)
72-
setWidth(clamped)
73-
} else {
74-
setWidth(parsedWidth)
75-
}
76-
}
63+
if (!savedWidth) {
64+
return
65+
}
66+
67+
const parsedWidth = parseInt(savedWidth, 10)
68+
if (
69+
isNaN(parsedWidth) ||
70+
parsedWidth < MIN_WIDTH ||
71+
parsedWidth > MAX_WIDTH
72+
) {
73+
return
7774
}
75+
76+
const { allowedMin, allowedMax } = getAllowedWidthBounds(node.clientWidth)
77+
const clampedWidth = Math.min(Math.max(parsedWidth, allowedMin), allowedMax)
78+
setWidth(clampedWidth)
7879
}, [])
7980

8081
// Keep width in bounds when container resizes (e.g., window resize)
8182
useEffect(() => {
82-
const el = containerRef.current
83-
if (!el) return
83+
if (!containerElement) return
8484
const ro = new ResizeObserver(entries => {
8585
for (const entry of entries) {
8686
const { allowedMin, allowedMax } = getAllowedWidthBounds(
@@ -89,9 +89,9 @@ export function ChatArtifactContainer({
8989
setWidth(prev => Math.min(Math.max(prev, allowedMin), allowedMax))
9090
}
9191
})
92-
ro.observe(el)
92+
ro.observe(containerElement)
9393
return () => ro.disconnect()
94-
}, [])
94+
}, [containerElement])
9595

9696
const startResize = useCallback((e: React.MouseEvent) => {
9797
e.preventDefault()
@@ -102,7 +102,7 @@ export function ChatArtifactContainer({
102102
if (!isResizing) return
103103

104104
const handleMouseMove = (e: MouseEvent) => {
105-
const containerRect = containerRef.current?.getBoundingClientRect()
105+
const containerRect = containerElement?.getBoundingClientRect()
106106
if (containerRect) {
107107
const newWidth = containerRect.right - e.clientX
108108
const { allowedMin, allowedMax } = getAllowedWidthBounds(
@@ -128,7 +128,7 @@ export function ChatArtifactContainer({
128128
document.removeEventListener('mousemove', handleMouseMove)
129129
document.removeEventListener('mouseup', handleMouseUp)
130130
}
131-
}, [isResizing])
131+
}, [containerElement, isResizing])
132132

133133
return (
134134
<div className="flex-1 min-h-0 min-w-0 h-screen flex">
@@ -140,7 +140,7 @@ export function ChatArtifactContainer({
140140

141141
{/* Desktop: Independent panels like morphic-studio */}
142142
<div
143-
ref={containerRef}
143+
ref={setContainerRef}
144144
className="hidden md:flex flex-1 min-w-0 overflow-hidden"
145145
>
146146
{/* Chat Panel - Independent container */}

components/chat.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -242,34 +242,37 @@ export function Chat({
242242
const container = scrollContainerRef.current
243243
if (!container) return
244244

245-
const handleScroll = () => {
245+
const updateIsAtBottom = () => {
246246
const { scrollTop, scrollHeight, clientHeight } = container
247247
const threshold = 50 // threshold in pixels
248-
if (scrollHeight - scrollTop - clientHeight < threshold) {
249-
setIsAtBottom(true)
250-
} else {
251-
setIsAtBottom(false)
252-
}
248+
setIsAtBottom(scrollHeight - scrollTop - clientHeight < threshold)
249+
}
250+
251+
const handleScroll = () => {
252+
updateIsAtBottom()
253253
}
254254

255255
container.addEventListener('scroll', handleScroll, { passive: true })
256-
handleScroll() // Set initial state
256+
const frame = requestAnimationFrame(updateIsAtBottom)
257257

258-
return () => container.removeEventListener('scroll', handleScroll)
258+
return () => {
259+
cancelAnimationFrame(frame)
260+
container.removeEventListener('scroll', handleScroll)
261+
}
259262
}, [messages.length])
260263

261264
// Check scroll position when messages change (during generation)
262265
useEffect(() => {
263266
const container = scrollContainerRef.current
264267
if (!container) return
265268

266-
const { scrollTop, scrollHeight, clientHeight } = container
267-
const threshold = 50
268-
if (scrollHeight - scrollTop - clientHeight < threshold) {
269-
setIsAtBottom(true)
270-
} else {
271-
setIsAtBottom(false)
272-
}
269+
const frame = requestAnimationFrame(() => {
270+
const { scrollTop, scrollHeight, clientHeight } = container
271+
const threshold = 50
272+
setIsAtBottom(scrollHeight - scrollTop - clientHeight < threshold)
273+
})
274+
275+
return () => cancelAnimationFrame(frame)
273276
}, [messages])
274277

275278
// Scroll to the section when a new user message is sent

components/model-type-selector.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useEffect, useState, useSyncExternalStore } from 'react'
44

55
import { Check, ChevronDown } from 'lucide-react'
66

77
import { ModelType } from '@/lib/types/model-type'
8-
import { getCookie, setCookie } from '@/lib/utils/cookies'
8+
import {
9+
getCookie,
10+
setCookie,
11+
subscribeToCookieChange
12+
} from '@/lib/utils/cookies'
913

1014
import { Button } from './ui/button'
1115
import {
@@ -25,29 +29,32 @@ export function ModelTypeSelector({
2529
}: {
2630
disabled?: boolean
2731
}) {
28-
const [value, setValue] = useState<ModelType>('speed')
32+
const value = useSyncExternalStore(
33+
subscribeToCookieChange,
34+
() => {
35+
const savedType = getCookie('modelType')
36+
return savedType === 'quality' ? 'quality' : 'speed'
37+
},
38+
() => 'speed'
39+
)
2940
const [dropdownOpen, setDropdownOpen] = useState(false)
3041

3142
useEffect(() => {
3243
if (disabled) {
33-
setValue('speed')
3444
setCookie('modelType', 'speed')
35-
return
36-
}
37-
const savedType = getCookie('modelType')
38-
if (savedType && ['speed', 'quality'].includes(savedType)) {
39-
setValue(savedType as ModelType)
4045
}
4146
}, [disabled])
4247

4348
const handleTypeSelect = (type: ModelType) => {
4449
if (disabled) return
45-
setValue(type)
4650
setCookie('modelType', type)
4751
setDropdownOpen(false)
4852
}
4953

50-
const selectedOption = MODEL_TYPE_OPTIONS.find(opt => opt.value === value)
54+
const selectedValue = disabled ? 'speed' : value
55+
const selectedOption = MODEL_TYPE_OPTIONS.find(
56+
opt => opt.value === selectedValue
57+
)
5158

5259
return (
5360
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
@@ -71,7 +78,7 @@ export function ModelTypeSelector({
7178
sideOffset={5}
7279
>
7380
{MODEL_TYPE_OPTIONS.map(option => {
74-
const isSelected = value === option.value
81+
const isSelected = selectedValue === option.value
7582
return (
7683
<DropdownMenuItem
7784
key={option.value}

components/search-mode-selector.tsx

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
'use client'
22

3-
import { useEffect, useRef, useState } from 'react'
3+
import { useEffect, useState, useSyncExternalStore } from 'react'
44

55
import { Check, ChevronDown } from 'lucide-react'
66

77
import { SEARCH_MODE_CONFIGS } from '@/lib/config/search-modes'
88
import { SearchMode } from '@/lib/types/search'
99
import { cn } from '@/lib/utils'
10-
import { getCookie, setCookie } from '@/lib/utils/cookies'
10+
import {
11+
getCookie,
12+
setCookie,
13+
subscribeToCookieChange
14+
} from '@/lib/utils/cookies'
1115

1216
import { Button } from './ui/button'
1317
import {
@@ -19,42 +23,27 @@ import {
1923
import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card'
2024

2125
export function SearchModeSelector() {
22-
const [value, setValue] = useState<SearchMode>('quick')
23-
const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({})
26+
const value = useSyncExternalStore(
27+
subscribeToCookieChange,
28+
() => {
29+
const savedMode = getCookie('searchMode')
30+
return savedMode === 'adaptive' ? 'adaptive' : 'quick'
31+
},
32+
() => 'quick'
33+
)
2434
const [openHoverCard, setOpenHoverCard] = useState<string | null>(null)
2535
const [justSelected, setJustSelected] = useState(false)
2636
const [dropdownOpen, setDropdownOpen] = useState(false)
27-
const buttonsRef = useRef<(HTMLButtonElement | null)[]>([])
2837

2938
useEffect(() => {
3039
const savedMode = getCookie('searchMode')
31-
if (savedMode && ['quick', 'adaptive'].includes(savedMode)) {
32-
setValue(savedMode as SearchMode)
33-
} else if (savedMode) {
40+
if (savedMode && !['quick', 'adaptive'].includes(savedMode)) {
3441
// Clean up invalid cookie value (e.g., old 'planning' mode)
3542
setCookie('searchMode', 'quick')
36-
setValue('quick')
3743
}
3844
}, [])
3945

40-
useEffect(() => {
41-
// Update indicator position when value changes
42-
const selectedIndex = SEARCH_MODE_CONFIGS.findIndex(
43-
config => config.value === value
44-
)
45-
const selectedButton = buttonsRef.current[selectedIndex]
46-
47-
if (selectedButton) {
48-
const { offsetLeft, offsetWidth } = selectedButton
49-
setIndicatorStyle({
50-
transform: `translateX(${offsetLeft}px)`,
51-
width: `${offsetWidth}px`
52-
})
53-
}
54-
}, [value])
55-
5646
const handleModeSelect = (mode: SearchMode) => {
57-
setValue(mode)
5847
setCookie('searchMode', mode)
5948
setOpenHoverCard(null) // Close hover card on selection
6049
setDropdownOpen(false) // Close dropdown on selection
@@ -70,6 +59,14 @@ export function SearchModeSelector() {
7059
config => config.value === value
7160
)
7261
const SelectedIcon = selectedMode?.icon
62+
const selectedIndex = Math.max(
63+
SEARCH_MODE_CONFIGS.findIndex(config => config.value === value),
64+
0
65+
)
66+
const indicatorStyle = {
67+
width: `${100 / SEARCH_MODE_CONFIGS.length}%`,
68+
transform: `translateX(${selectedIndex * 100}%)`
69+
}
7370

7471
return (
7572
<>
@@ -159,12 +156,9 @@ export function SearchModeSelector() {
159156
<HoverCardTrigger asChild>
160157
<button
161158
type="button"
162-
ref={el => {
163-
buttonsRef.current[index] = el
164-
}}
165159
onClick={() => handleModeSelect(config.value)}
166160
className={cn(
167-
'relative z-10 flex items-center justify-center rounded-full px-3 py-2 transition-colors duration-200',
161+
'relative z-10 flex-1 items-center justify-center rounded-full px-3 py-2 transition-colors duration-200',
168162
isSelected
169163
? 'text-foreground'
170164
: 'text-muted-foreground hover:text-foreground/80'

0 commit comments

Comments
 (0)