Skip to content

Commit 9605bb4

Browse files
committed
Error handling
1 parent c5a66ac commit 9605bb4

10 files changed

Lines changed: 151 additions & 9 deletions

File tree

app/error.test.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { render, screen } from '@testing-library/react'
2+
import GlobalError from './error'
3+
4+
describe('GlobalError', () => {
5+
it('renders the error message', () => {
6+
const error = new Error('Test error')
7+
render(<GlobalError error={error} />)
8+
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
9+
expect(screen.getByText('Test error')).toBeInTheDocument()
10+
})
11+
12+
it('renders fallback text if no error message', () => {
13+
// @ts-expect-error purposely missing message
14+
render(<GlobalError error={{}} />)
15+
expect(screen.getByText('Unknown error')).toBeInTheDocument()
16+
})
17+
})

app/error.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use client'
2+
3+
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
4+
return (
5+
<html>
6+
<body className="flex flex-col items-center justify-center h-screen w-screen bg-[#181a20] text-[#ececf1]">
7+
<h2 className="text-2xl font-bold mb-2">Something went wrong</h2>
8+
<pre className="text-sm text-[#b4bcd0] bg-[#23272f] rounded p-4 max-w-xl overflow-x-auto">
9+
{error?.message || 'Unknown error'}
10+
</pre>
11+
</body>
12+
</html>
13+
)
14+
}

app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Inter } from 'next/font/google'
44
import { ReactNode } from 'react'
55
import QueryClientProviderWrapper from '@/components/providers/query-client-provider'
66
import SessionProvider from '@/components/providers/session-provider'
7+
import { ToastProvider } from '@/components/providers/toast-provider'
78

89
const inter = Inter({ subsets: ['latin'] })
910

@@ -24,6 +25,7 @@ export default function RootLayout({
2425
<body className={inter.className}>
2526
<SessionProvider>
2627
<QueryClientProviderWrapper>
28+
<ToastProvider />
2729
{children}
2830
</QueryClientProviderWrapper>
2931
</SessionProvider>

components/chat/chat-input.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useRef, useEffect } from 'react'
22
import { Button } from '@/components/ui/button'
3-
import { LucideSend, Globe, Image as ImageIcon, Search, MoreHorizontal, Upload } from 'lucide-react'
3+
import { LucideSend, Search, Upload } from 'lucide-react'
44
import * as Tooltip from '@radix-ui/react-tooltip'
55
import { useActiveConversation } from '@/hooks/use-active-conversation'
66
import { useCreateConversation } from '@/hooks/use-create-conversation'
@@ -14,6 +14,7 @@ import type { Database } from '@/types/supabase';
1414
import { useConversationModelStore } from '@/hooks/use-conversation-model-store'
1515
import { createPortal } from 'react-dom'
1616
import { usePremiumQueryCountStore } from '@/hooks/use-premium-query-count-store'
17+
import { toast } from 'react-hot-toast'
1718

1819
console.log('ChatInput mounted');
1920

@@ -225,6 +226,7 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
225226
})
226227
if (res.status === 403) {
227228
setShowLimitModal(true)
229+
toast.error('You have hit your daily premium query limit. Add your own API key in Settings for unlimited access.')
228230
return
229231
}
230232
decrementPremiumCount()
@@ -316,9 +318,13 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
316318
} catch (err) {
317319
console.error('Fetch to /api/chat failed:', err);
318320
setError('Failed to contact LLM API');
321+
toast.error('Failed to contact Together.AI. Please try again later.')
319322
return;
320323
}
321-
if (!res.body) throw new Error('No response body');
324+
if (!res.body) {
325+
toast.error('No response from Together.AI. Please try again later.')
326+
throw new Error('No response body');
327+
}
322328
if (res.body) {
323329
ChatCompletionStream.fromReadableStream(res.body)
324330
.on('content', (delta, content) => {

components/profile/profile-modal.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
22
import { createPortal } from 'react-dom'
33
import { supabase } from '@/lib/supabase/client'
44
import { useRouter } from 'next/navigation'
5+
import { toast } from 'react-hot-toast'
56

67
interface ProfileModalProps {
78
open: boolean
@@ -25,6 +26,7 @@ export default function ProfileModal({ open, onClose }: ProfileModalProps) {
2526
setError(null)
2627
const { data: { user }, error: userError } = await supabase.auth.getUser()
2728
if (userError || !user) {
29+
toast.error(userError?.message || 'Auth error. Please sign in again.')
2830
router.replace('/sign-in')
2931
return
3032
}
@@ -34,7 +36,10 @@ export default function ProfileModal({ open, onClose }: ProfileModalProps) {
3436
.eq('id', user.id)
3537
.single()
3638
if (!ignore) {
37-
if (profileError) setError(profileError.message)
39+
if (profileError) {
40+
setError(profileError.message)
41+
toast.error(profileError.message)
42+
}
3843
else {
3944
setProfile({
4045
email: data?.email || '',
@@ -58,14 +63,25 @@ export default function ProfileModal({ open, onClose }: ProfileModalProps) {
5863
.from('users')
5964
.update({ full_name: fullName })
6065
.eq('id', user?.id || '')
61-
if (updateError) setError(updateError.message)
66+
if (updateError) {
67+
setError(updateError.message)
68+
toast.error(updateError.message)
69+
}
6270
else setSuccess('Profile updated!')
6371
setLoading(false)
6472
}
6573

6674
const handleLogout = async () => {
67-
await supabase.auth.signOut()
68-
router.replace('/sign-in')
75+
try {
76+
await supabase.auth.signOut()
77+
router.replace('/sign-in')
78+
} catch (err: unknown) {
79+
if (err && typeof err === 'object' && 'message' in err && typeof (err as { message?: unknown }).message === 'string') {
80+
toast.error((err as { message: string }).message)
81+
} else {
82+
toast.error('Logout failed')
83+
}
84+
}
6985
}
7086

7187
const handleDelete = async () => {
@@ -75,7 +91,10 @@ export default function ProfileModal({ open, onClose }: ProfileModalProps) {
7591
const { data: { user } } = await supabase.auth.getUser()
7692
// You should use a secure API route for this in production
7793
const { error: deleteError } = await supabase.from('users').delete().eq('id', user?.id || '')
78-
if (deleteError) setError(deleteError.message)
94+
if (deleteError) {
95+
setError(deleteError.message)
96+
toast.error(deleteError.message)
97+
}
7998
else {
8099
await supabase.auth.signOut()
81100
router.replace('/sign-up')
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Toaster } from 'react-hot-toast'
2+
3+
export function ToastProvider() {
4+
return <Toaster position="top-center" toastOptions={{
5+
style: {
6+
background: '#23272f',
7+
color: '#ececf1',
8+
border: '1px solid #353740',
9+
fontFamily: 'var(--font-sans)',
10+
fontSize: 15,
11+
zIndex: 99999,
12+
},
13+
duration: 4000,
14+
}} />
15+
}

components/ui/error-boundary.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Component, ReactNode } from 'react'
2+
3+
interface ErrorBoundaryProps {
4+
children: ReactNode
5+
fallback?: ReactNode
6+
}
7+
8+
interface ErrorBoundaryState {
9+
hasError: boolean
10+
error: Error | null
11+
}
12+
13+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
14+
constructor(props: ErrorBoundaryProps) {
15+
super(props)
16+
this.state = { hasError: false, error: null }
17+
}
18+
19+
static getDerivedStateFromError(error: Error) {
20+
return { hasError: true, error }
21+
}
22+
23+
componentDidCatch() {
24+
// Optionally log error to an error reporting service
25+
// console.error(error, errorInfo)
26+
}
27+
28+
render() {
29+
if (this.state.hasError) {
30+
return this.props.fallback || (
31+
<div className="flex flex-col items-center justify-center h-full w-full bg-[#181a20] text-[#ececf1]">
32+
<h2 className="text-2xl font-bold mb-2">Something went wrong</h2>
33+
<pre className="text-sm text-[#b4bcd0] bg-[#23272f] rounded p-4 max-w-xl overflow-x-auto">
34+
{this.state.error?.message}
35+
</pre>
36+
</div>
37+
)
38+
}
39+
return this.props.children
40+
}
41+
}

package-lock.json

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

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"react": "^19.0.0",
3434
"react-dom": "^19.0.0",
3535
"react-hook-form": "^7.56.1",
36+
"react-hot-toast": "^2.5.2",
3637
"react-markdown": "^10.1.0",
3738
"rehype-highlight": "^7.0.2",
3839
"remark-gfm": "^4.0.1",
@@ -76,4 +77,4 @@
7677
"react": "19.1.0",
7778
"react-dom": "19.1.0"
7879
}
79-
}
80+
}

vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default defineConfig({
88
environment: 'jsdom',
99
globals: true,
1010
setupFiles: './vitest.setup.ts',
11-
include: ['**/__tests__/**/*.test.{ts,tsx}'],
11+
include: ['**/__tests__/**/*.test.{ts,tsx}', '**/*.test.{ts,tsx}'],
1212
},
1313
resolve: {
1414
alias: {

0 commit comments

Comments
 (0)