feat: add rate limiting with countdown timer for docs chat
- Add in-memory rate limiter (10 requests/minute per user) - Show informative message when limit reached explaining demo context - Display countdown timer showing when chat becomes available again - Auto-reset rate limit when timer expires - Update documentation with rate limit FAQ and category selection flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils'
|
||||
import { ChatInput } from './chat-input'
|
||||
import { ChatMessages } from './chat-messages'
|
||||
import { ChatSuggestions } from './chat-suggestions'
|
||||
import { RateLimitMessage } from './rate-limit-message'
|
||||
import { useDocsChat } from './use-docs-chat'
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,7 @@ import { useDocsChat } from './use-docs-chat'
|
||||
*/
|
||||
export function DocsChatWidget() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { messages, isLoading, isStreaming, error, sendMessage, clearError } = useDocsChat()
|
||||
const { messages, isLoading, isStreaming, error, isRateLimited, rateLimitResetTime, sendMessage, clearError, clearRateLimit } = useDocsChat()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -106,8 +107,16 @@ export function DocsChatWidget() {
|
||||
{/* Messages */}
|
||||
<ChatMessages messages={messages} isStreaming={isStreaming} />
|
||||
|
||||
{/* Suggestions - show only when just welcome message */}
|
||||
{messages.length === 1 && messages[0].id === 'welcome' && (
|
||||
{/* Rate limit message */}
|
||||
{isRateLimited && (
|
||||
<RateLimitMessage
|
||||
resetTime={rateLimitResetTime}
|
||||
onReset={clearRateLimit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Suggestions - show only when just welcome message and not rate limited */}
|
||||
{!isRateLimited && messages.length === 1 && messages[0].id === 'welcome' && (
|
||||
<ChatSuggestions
|
||||
onSelect={sendMessage}
|
||||
disabled={isLoading || isStreaming}
|
||||
@@ -117,7 +126,7 @@ export function DocsChatWidget() {
|
||||
{/* Input */}
|
||||
<ChatInput
|
||||
onSend={sendMessage}
|
||||
disabled={isLoading || isStreaming}
|
||||
disabled={isLoading || isStreaming || isRateLimited}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -2,5 +2,6 @@ export { DocsChatWidget } from './docs-chat-widget'
|
||||
export { ChatMessages } from './chat-messages'
|
||||
export { ChatInput } from './chat-input'
|
||||
export { ChatSuggestions } from './chat-suggestions'
|
||||
export { RateLimitMessage } from './rate-limit-message'
|
||||
export { useDocsChat } from './use-docs-chat'
|
||||
export type { ChatMessage } from './use-docs-chat'
|
||||
|
||||
78
components/docs-chat/rate-limit-message.tsx
Normal file
78
components/docs-chat/rate-limit-message.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle, Clock } from 'lucide-react'
|
||||
|
||||
interface RateLimitMessageProps {
|
||||
resetTime: number | null
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Informative message shown when rate limit is reached
|
||||
* Includes countdown timer and explains what the chat can be used for
|
||||
*/
|
||||
export function RateLimitMessage({ resetTime, onReset }: RateLimitMessageProps) {
|
||||
const [secondsLeft, setSecondsLeft] = useState(() => {
|
||||
if (!resetTime) return 0
|
||||
return Math.max(0, Math.ceil((resetTime - Date.now()) / 1000))
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!resetTime) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const remaining = Math.max(0, Math.ceil((resetTime - Date.now()) / 1000))
|
||||
setSecondsLeft(remaining)
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval)
|
||||
onReset()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [resetTime, onReset])
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-4">
|
||||
<div className="rounded-xl bg-amber-50 border border-amber-200 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-amber-900 text-sm mb-1">
|
||||
Even pauze
|
||||
</h3>
|
||||
<p className="text-xs text-amber-800 mb-3">
|
||||
Je hebt het maximum aantal vragen voor deze minuut bereikt.
|
||||
Dit is een demo-omgeving met beperkte capaciteit.
|
||||
</p>
|
||||
|
||||
<div className="bg-white/60 rounded-lg p-3 mb-3">
|
||||
<p className="text-xs font-medium text-slate-700 mb-2">
|
||||
Waar kan deze assistent bij helpen?
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• Uitleg over EPD-functies en navigatie</li>
|
||||
<li>• Hoe je intakes, screenings en rapportages maakt</li>
|
||||
<li>• Hulp bij spraakherkenning en dicteren</li>
|
||||
<li>• Vragen over cliëntbeheer en dossiers</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{secondsLeft > 0 && (
|
||||
<div className="flex items-center gap-2 text-xs text-amber-700">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
Je kunt weer vragen stellen over <strong>{secondsLeft}</strong> {secondsLeft === 1 ? 'seconde' : 'seconden'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,8 @@ interface UseDocsChatState {
|
||||
isLoading: boolean
|
||||
isStreaming: boolean
|
||||
error: string | null
|
||||
isRateLimited: boolean
|
||||
rateLimitResetTime: number | null // timestamp when rate limit resets
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +30,7 @@ interface UseDocsChatReturn extends UseDocsChatState {
|
||||
sendMessage: (content: string) => Promise<void>
|
||||
clearMessages: () => void
|
||||
clearError: () => void
|
||||
clearRateLimit: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +69,8 @@ export function useDocsChat(): UseDocsChatReturn {
|
||||
isLoading: false,
|
||||
isStreaming: false,
|
||||
error: null,
|
||||
isRateLimited: false,
|
||||
rateLimitResetTime: null,
|
||||
})
|
||||
|
||||
const sendMessage = useCallback(async (content: string) => {
|
||||
@@ -111,6 +116,20 @@ export function useDocsChat(): UseDocsChatReturn {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
|
||||
// Handle rate limit specifically
|
||||
if (response.status === 429) {
|
||||
const resetIn = errorData.resetIn ?? 60 // default 60 seconds
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
isRateLimited: true,
|
||||
rateLimitResetTime: Date.now() + (resetIn * 1000),
|
||||
messages: prev.messages.filter((m) => m.id !== assistantMessage.id),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(errorData.error || `Fout: ${response.status}`)
|
||||
}
|
||||
|
||||
@@ -192,6 +211,8 @@ export function useDocsChat(): UseDocsChatReturn {
|
||||
isLoading: false,
|
||||
isStreaming: false,
|
||||
error: null,
|
||||
isRateLimited: false,
|
||||
rateLimitResetTime: null,
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -199,10 +220,15 @@ export function useDocsChat(): UseDocsChatReturn {
|
||||
setState((prev) => ({ ...prev, error: null }))
|
||||
}, [])
|
||||
|
||||
const clearRateLimit = useCallback(() => {
|
||||
setState((prev) => ({ ...prev, isRateLimited: false, rateLimitResetTime: null }))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
...state,
|
||||
sendMessage,
|
||||
clearMessages,
|
||||
clearError,
|
||||
clearRateLimit,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user