Skip to content

Commit 8f17486

Browse files
committed
fix (limits): graceful handling of rate limits, sidebar report bug button
1 parent 95ce79f commit 8f17486

File tree

8 files changed

+251
-36
lines changed

8 files changed

+251
-36
lines changed

src/client/app/api/chat/message/route.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,20 @@ export const POST = withAuth(async function POST(request, { authHeader }) {
3636
})
3737

3838
if (!backendResponse.ok) {
39-
const errorText = await backendResponse.text()
40-
let errorMessage
39+
const errorText = await backendResponse
40+
.text()
41+
.catch(() => "Unknown backend error")
42+
let errorJson = {}
4143
try {
42-
const errorJson = JSON.parse(errorText)
43-
errorMessage =
44-
errorJson.detail ||
45-
errorJson.message ||
46-
"Backend chat endpoint failed"
44+
errorJson = JSON.parse(errorText)
4745
} catch (e) {
48-
errorMessage =
49-
errorText ||
50-
`Backend chat endpoint failed with status ${backendResponse.status}`
46+
// Not a JSON error, use the raw text
5147
}
52-
throw new Error(errorMessage)
48+
// Return the error from the backend with its original status code
49+
return NextResponse.json(
50+
{ detail: errorJson.detail || errorText },
51+
{ status: backendResponse.status }
52+
)
5353
}
5454

5555
// Return the streaming response directly to the client
@@ -67,7 +67,7 @@ export const POST = withAuth(async function POST(request, { authHeader }) {
6767
} catch (error) {
6868
console.error("API Error in /chat/message:", error)
6969
return NextResponse.json(
70-
{ message: "Internal Server Error", error: error.message },
70+
{ detail: "Internal Server Error", error: error.message },
7171
{ status: 500 }
7272
)
7373
}

src/client/app/api/files/upload/route.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ export const POST = withAuth(async function POST(request, { authHeader }) {
2525
const data = await backendResponse.json()
2626

2727
if (!backendResponse.ok) {
28-
throw new Error(data.detail || "Failed to upload file")
28+
return NextResponse.json(
29+
{ error: data.detail || "Failed to upload file" },
30+
{ status: backendResponse.status }
31+
)
2932
}
3033

3134
return NextResponse.json(data)
3235
} catch (error) {
3336
console.error("API Error in /files/upload:", error)
3437
return NextResponse.json(
35-
{ message: "Internal Server Error", error: error.message },
38+
{ error: "Internal Server Error", details: error.message },
3639
{ status: 500 }
3740
)
3841
}

src/client/app/api/memories/route.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export const POST = withAuth(async function POST(request, { authHeader }) {
3939

4040
const data = await response.json()
4141
if (!response.ok) {
42-
throw new Error(data.detail || "Failed to create memory")
42+
return NextResponse.json(
43+
{ error: data.detail || "Failed to create memory" },
44+
{ status: response.status }
45+
)
4346
}
4447
return NextResponse.json(data, { status: response.status })
4548
} catch (error) {

src/client/app/api/tasks/add/route.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ export const POST = withAuth(async function POST(request, { authHeader }) {
1818

1919
const data = await response.json()
2020
if (!response.ok) {
21-
throw new Error(data.error || "Failed to add task")
21+
// Propagate the error and status code from the backend directly
22+
return NextResponse.json(
23+
{ error: data.detail || "Failed to add task" },
24+
{ status: response.status }
25+
)
2226
}
2327
return NextResponse.json(data)
2428
} catch (error) {
2529
console.error("API Error in /tasks/add:", error)
30+
// This catch is for network errors or if JSON parsing fails
2631
return NextResponse.json({ error: error.message }, { status: 500 })
2732
}
2833
})

src/client/app/chat/page.js

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,10 @@ export default function ChatPage() {
538538
})
539539

540540
if (!response.ok) {
541-
const errorData = await response.json()
542-
throw new Error(errorData.error || "File upload failed")
541+
const errorData = await response.json().catch(() => ({}))
542+
const error = new Error(errorData.error || "File upload failed")
543+
error.status = response.status
544+
throw error
543545
}
544546

545547
const result = await response.json()
@@ -548,7 +550,18 @@ export default function ChatPage() {
548550
id: toastId
549551
})
550552
} catch (error) {
551-
toast.error(`Error: ${error.message}`, { id: toastId })
553+
if (error.status === 429) {
554+
toast.error(
555+
error.message ||
556+
"You've reached your daily file upload limit for the free plan.",
557+
{ id: toastId }
558+
)
559+
if (!isPro) {
560+
setUpgradeModalOpen(true)
561+
}
562+
} else {
563+
toast.error(`Error: ${error.message}`, { id: toastId })
564+
}
552565
setSelectedFile(null)
553566
} finally {
554567
setIsUploading(false)
@@ -614,7 +627,14 @@ export default function ChatPage() {
614627
})
615628

616629
if (!response.ok) {
617-
throw new Error(`HTTP error! status: ${response.status}`)
630+
const errorData = await response.json().catch(() => ({
631+
detail: `Request failed with status ${response.status}`
632+
}))
633+
const error = new Error(
634+
errorData.detail || "An unexpected error occurred."
635+
)
636+
error.status = response.status
637+
throw error
618638
}
619639

620640
const reader = response.body.getReader()
@@ -706,13 +726,21 @@ export default function ChatPage() {
706726
} catch (error) {
707727
if (error.name === "AbortError") {
708728
toast.info("Message generation stopped.")
729+
} else if (error.status === 429) {
730+
toast.error(
731+
error.message ||
732+
"You've reached a usage limit for today on the free plan."
733+
)
734+
if (!isPro) {
735+
setUpgradeModalOpen(true)
736+
}
709737
} else {
710738
toast.error(`Error: ${error.message}`)
711-
console.error("Fetch error:", error)
712-
setDisplayedMessages((prev) =>
713-
prev.filter((m) => m.id !== newUserMessage.id)
714-
)
715739
}
740+
console.error("Fetch error:", error)
741+
setDisplayedMessages((prev) =>
742+
prev.filter((m) => m.id !== newUserMessage.id)
743+
)
716744
} finally {
717745
setThinking(false)
718746
setStatusText("")
@@ -916,8 +944,16 @@ export default function ChatPage() {
916944
}
917945
}
918946
)
919-
if (!rtcTokenResponse.ok)
920-
throw new Error("Could not initiate voice session.")
947+
if (!rtcTokenResponse.ok) {
948+
const errorData = await rtcTokenResponse
949+
.json()
950+
.catch(() => ({}))
951+
const error = new Error(
952+
errorData.detail || "Could not initiate voice session."
953+
)
954+
error.status = rtcTokenResponse.status
955+
throw error
956+
}
921957
const { rtc_token, ice_servers } = await rtcTokenResponse.json()
922958

923959
// Step 3: Create and connect WebRTCClient directly
@@ -960,9 +996,19 @@ export default function ChatPage() {
960996
rtc_token
961997
)
962998
} catch (error) {
963-
toast.error(
964-
`Failed to connect: ${error.message || "Unknown error"}`
965-
)
999+
if (error.status === 429) {
1000+
toast.error(
1001+
error.message ||
1002+
"You've used all your voice minutes for today on the free plan."
1003+
)
1004+
if (!isPro) {
1005+
setUpgradeModalOpen(true)
1006+
}
1007+
} else {
1008+
toast.error(
1009+
`Failed to connect: ${error.message || "Unknown error"}`
1010+
)
1011+
}
9661012
handleStatusChange("disconnected")
9671013
}
9681014
}

src/client/app/memories/page.js

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,109 @@ import { motion, AnimatePresence } from "framer-motion"
2323
import { formatDistanceToNow, parseISO } from "date-fns"
2424
import { cn } from "@utils/cn"
2525
import dynamic from "next/dynamic"
26+
import { usePlan } from "@hooks/usePlan"
2627
import InteractiveNetworkBackground from "@components/ui/InteractiveNetworkBackground"
2728
import ModalDialog from "@components/ModalDialog"
2829

30+
const proPlanFeatures = [
31+
{ name: "Text Chat", limit: "100 messages per day" },
32+
{ name: "Voice Chat", limit: "10 minutes per day" },
33+
{ name: "One-Time Tasks", limit: "20 async tasks per day" },
34+
{ name: "Recurring Tasks", limit: "10 active recurring workflows" },
35+
{ name: "Triggered Tasks", limit: "10 triggered workflows" },
36+
{
37+
name: "Parallel Agents",
38+
limit: "5 complex tasks per day with 50 sub agents"
39+
},
40+
{ name: "File Uploads", limit: "20 files per day" },
41+
{ name: "Memories", limit: "Unlimited memories" },
42+
{
43+
name: "Other Integrations",
44+
limit: "Notion, GitHub, Slack, Discord, Trello"
45+
}
46+
]
47+
48+
const UpgradeToProModal = ({ isOpen, onClose }) => {
49+
if (!isOpen) return null
50+
51+
const handleUpgrade = () => {
52+
const dashboardUrl = process.env.NEXT_PUBLIC_LANDING_PAGE_URL
53+
if (dashboardUrl) {
54+
window.location.href = `${dashboardUrl}/dashboard`
55+
}
56+
onClose()
57+
}
58+
59+
return (
60+
<AnimatePresence>
61+
{isOpen && (
62+
<motion.div
63+
initial={{ opacity: 0 }}
64+
animate={{ opacity: 1 }}
65+
exit={{ opacity: 0 }}
66+
className="fixed inset-0 bg-black/70 backdrop-blur-md z-[100] flex items-center justify-center p-4"
67+
onClick={onClose}
68+
>
69+
<motion.div
70+
initial={{ scale: 0.95, y: 20 }}
71+
animate={{ scale: 1, y: 0 }}
72+
exit={{ scale: 0.95, y: -20 }}
73+
transition={{ duration: 0.2, ease: "easeInOut" }}
74+
onClick={(e) => e.stopPropagation()}
75+
className="relative bg-neutral-900/90 backdrop-blur-xl p-6 rounded-2xl shadow-2xl w-full max-w-lg border border-neutral-700 flex flex-col"
76+
>
77+
<header className="text-center mb-4">
78+
<h2 className="text-2xl font-bold text-white flex items-center justify-center gap-2">
79+
<IconSparkles className="text-brand-orange" />
80+
Upgrade to Pro
81+
</h2>
82+
<p className="text-neutral-400 mt-2">
83+
Unlock unlimited memories and other powerful
84+
features.
85+
</p>
86+
</header>
87+
<main className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4 my-4">
88+
{proPlanFeatures.map((feature) => (
89+
<div
90+
key={feature.name}
91+
className="flex items-start gap-2.5"
92+
>
93+
<IconCheck
94+
size={18}
95+
className="text-green-400 flex-shrink-0 mt-0.5"
96+
/>
97+
<div>
98+
<p className="text-white text-sm font-medium">
99+
{feature.name}
100+
</p>
101+
<p className="text-neutral-400 text-xs">
102+
{feature.limit}
103+
</p>
104+
</div>
105+
</div>
106+
))}
107+
</main>
108+
<footer className="mt-4 flex flex-col gap-2">
109+
<button
110+
onClick={handleUpgrade}
111+
className="w-full py-2.5 px-5 rounded-lg bg-brand-orange hover:bg-brand-orange/90 text-brand-black font-semibold transition-colors"
112+
>
113+
Upgrade to Pro - $9/month
114+
</button>
115+
<button
116+
onClick={onClose}
117+
className="w-full py-2 px-5 rounded-lg hover:bg-neutral-800 text-sm font-medium text-neutral-400"
118+
>
119+
Not now
120+
</button>
121+
</footer>
122+
</motion.div>
123+
</motion.div>
124+
)}
125+
</AnimatePresence>
126+
)
127+
}
128+
29129
const InfoPanel = ({ onClose, title, children }) => (
30130
<motion.div
31131
initial={{ opacity: 0, backdropFilter: "blur(0px)" }}
@@ -463,6 +563,8 @@ export default function MemoriesPage() {
463563
const [selectedMemory, setSelectedMemory] = useState(null)
464564
const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false)
465565
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
566+
const [isUpgradeModalOpen, setUpgradeModalOpen] = useState(false)
567+
const { isPro } = usePlan()
466568
const [userDetails, setUserDetails] = useState(null)
467569

468570
const topics = useMemo(() => {
@@ -536,14 +638,30 @@ export default function MemoriesPage() {
536638
body: JSON.stringify({ content, source: "manual_entry" })
537639
})
538640
if (!res.ok) {
539-
const errorData = await res.json()
540-
throw new Error(errorData.error || "Failed to add memory")
641+
const errorData = await res.json().catch(() => ({}))
642+
const error = new Error(
643+
errorData.error || "Failed to add memory"
644+
)
645+
error.status = res.status
646+
throw error
541647
}
542648
toast.success("Memory added successfully!", { id: toastId })
543649
setIsCreateModalOpen(false)
544650
await fetchData() // Refresh data
545651
} catch (error) {
546-
toast.error(error.message, { id: toastId })
652+
if (error.status === 429) {
653+
toast.error(
654+
error.message ||
655+
"You've reached your memory limit for the free plan.",
656+
{ id: toastId }
657+
)
658+
if (!isPro) {
659+
setUpgradeModalOpen(true)
660+
setIsCreateModalOpen(false) // Close the create modal
661+
}
662+
} else {
663+
toast.error(error.message, { id: toastId })
664+
}
547665
}
548666
}
549667

@@ -614,6 +732,10 @@ export default function MemoriesPage() {
614732

615733
return (
616734
<div className="flex-1 flex h-screen text-white overflow-hidden">
735+
<UpgradeToProModal
736+
isOpen={isUpgradeModalOpen}
737+
onClose={() => setUpgradeModalOpen(false)}
738+
/>
617739
<AnimatePresence>
618740
{isInfoPanelOpen && (
619741
<InfoPanel

0 commit comments

Comments
 (0)