feat: improve quota toast with ByteDance Doubao sponsorship info and model config button (#447)

- Add 'Use Your API Key' button to open model config dialog
- Add ByteDance Doubao sponsorship message with registration link
- Update quota limit messages to be warmer and friendlier
- Add dev panel button to test quota toast
- Update i18n translations for EN, ZH, JA
This commit is contained in:
Dayuan Jiang
2025-12-29 12:12:22 +09:00
committed by GitHub
parent 6d1e12bb39
commit 27f26d8b26
7 changed files with 61 additions and 15 deletions

View File

@@ -185,6 +185,7 @@ export default function ChatPanel({
dailyRequestLimit, dailyRequestLimit,
dailyTokenLimit, dailyTokenLimit,
tpmLimit, tpmLimit,
onConfigModel: () => setShowModelConfigDialog(true),
}) })
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available) // Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
@@ -1036,6 +1037,9 @@ export default function ChatPanel({
<DevXmlSimulator <DevXmlSimulator
setMessages={setMessages} setMessages={setMessages}
onDisplayChart={onDisplayChart} onDisplayChart={onDisplayChart}
onShowQuotaToast={() =>
quotaManager.showQuotaLimitToast(50, 50)
}
/> />
)} )}

View File

@@ -134,11 +134,13 @@ const DEV_XML_PRESETS: Record<string, string> = {
interface DevXmlSimulatorProps { interface DevXmlSimulatorProps {
setMessages: React.Dispatch<React.SetStateAction<any[]>> setMessages: React.Dispatch<React.SetStateAction<any[]>>
onDisplayChart: (xml: string) => void onDisplayChart: (xml: string) => void
onShowQuotaToast?: () => void
} }
export function DevXmlSimulator({ export function DevXmlSimulator({
setMessages, setMessages,
onDisplayChart, onDisplayChart,
onShowQuotaToast,
}: DevXmlSimulatorProps) { }: DevXmlSimulatorProps) {
const [devXml, setDevXml] = useState("") const [devXml, setDevXml] = useState("")
const [isSimulating, setIsSimulating] = useState(false) const [isSimulating, setIsSimulating] = useState(false)
@@ -342,6 +344,15 @@ export function DevXmlSimulator({
Stop Stop
</button> </button>
)} )}
{onShowQuotaToast && (
<button
type="button"
onClick={onShowQuotaToast}
className="px-3 py-1 text-xs bg-purple-500 text-white rounded hover:bg-purple-600"
>
Test Quota Toast
</button>
)}
</div> </div>
</div> </div>
</details> </details>

View File

@@ -1,7 +1,6 @@
"use client" "use client"
import { Coffee, X } from "lucide-react" import { Coffee, Settings, X } from "lucide-react"
import Link from "next/link"
import type React from "react" import type React from "react"
import { FaGithub } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
@@ -12,6 +11,7 @@ interface QuotaLimitToastProps {
used: number used: number
limit: number limit: number
onDismiss: () => void onDismiss: () => void
onConfigModel?: () => void
} }
export function QuotaLimitToast({ export function QuotaLimitToast({
@@ -19,6 +19,7 @@ export function QuotaLimitToast({
used, used,
limit, limit,
onDismiss, onDismiss,
onConfigModel,
}: QuotaLimitToastProps) { }: QuotaLimitToastProps) {
const dict = useDictionary() const dict = useDictionary()
const isTokenLimit = type === "token" const isTokenLimit = type === "token"
@@ -75,16 +76,36 @@ export function QuotaLimitToast({
? dict.quota.messageToken ? dict.quota.messageToken
: dict.quota.messageApi} : dict.quota.messageApi}
</p> </p>
<p
dangerouslySetInnerHTML={{
__html: formatMessage(dict.quota.doubaoSponsorship, {
link: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project",
}),
}}
/>
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} /> <p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
<p>{dict.quota.reset}</p> <p>{dict.quota.reset}</p>
</div>{" "} </div>{" "}
{/* Action buttons */} {/* Action buttons */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{onConfigModel && (
<button
type="button"
onClick={() => {
onConfigModel()
onDismiss()
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
<Settings className="w-3.5 h-3.5" />
{dict.quota.configModel}
</button>
)}
<a <a
href="https://github.com/DayuanJiang/next-ai-draw-io" href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
> >
<FaGithub className="w-3.5 h-3.5" /> <FaGithub className="w-3.5 h-3.5" />
{dict.quota.selfHost} {dict.quota.selfHost}

View File

@@ -157,10 +157,12 @@
"tpmLimit": "Rate Limit", "tpmLimit": "Rate Limit",
"tpmMessage": "Too many requests. Please wait a moment.", "tpmMessage": "Too many requests. Please wait a moment.",
"tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.", "tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.",
"messageApi": "Oops — you've reached the daily API limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.", "messageApi": "Looks like you've reached today's demo limit. We're thrilled you're enjoying it, and while ByteDance Doubao generously sponsors this demo, we've had to set a few boundaries to keep things fair for everyone.",
"messageToken": "Oops — you've reached the daily token limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.", "messageToken": "Looks like you've reached today's token limit. We're thrilled you're enjoying it, and while ByteDance Doubao generously sponsors this demo, we've had to set a few boundaries to keep things fair for everyone.",
"tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.", "tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.",
"reset": "Your limit resets tomorrow. Thanks for understanding!", "reset": "Your limit resets tomorrow. Thanks for understanding.",
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">Register here</a> to get 500K free tokens per model (including Doubao, DeepSeek and Kimi), then configure your API key in model settings.",
"configModel": "Use Your API Key",
"selfHost": "Self-host", "selfHost": "Self-host",
"sponsor": "Sponsor", "sponsor": "Sponsor",
"learnMore": "Learn more →", "learnMore": "Learn more →",

View File

@@ -157,10 +157,12 @@
"tpmLimit": "レート制限", "tpmLimit": "レート制限",
"tpmMessage": "リクエストが多すぎます。しばらくお待ちください。", "tpmMessage": "リクエストが多すぎます。しばらくお待ちください。",
"tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。", "tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。",
"messageApi": "おっと — このデモの1日の API 制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。", "messageApi": "今日のデモ利用上限に達してしまったようです。楽しんでいただけて本当に嬉しいです。このデモはByteDance Doubaoのご厚意により提供されていますが、皆様に公平にご利用いただくため、少し制限を設けさせていただいております。",
"messageToken": "おっと — このデモの1日のトークン制限に達しました個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。", "messageToken": "今日のトークン利用上限に達してしまったようです。楽しんでいただけて本当に嬉しいです。このデモはByteDance Doubaoのご厚意により提供されていますが、皆様に公平にご利用いただくため、少し制限を設けさせていただいております。",
"tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。", "tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。",
"reset": "制限は明日リセットされます。ご理解ありがとうございます", "reset": "制限は明日リセットされます。ご理解ありがとうございます",
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">こちらから登録</a>すると、各モデルDoubao、DeepSeek、Kimi含むで50万トークンを無料で取得できます。モデル設定でAPIキーを設定してください。",
"configModel": "APIキーを使用",
"selfHost": "セルフホスト", "selfHost": "セルフホスト",
"sponsor": "スポンサー", "sponsor": "スポンサー",
"learnMore": "詳細 →", "learnMore": "詳細 →",

View File

@@ -157,10 +157,12 @@
"tpmLimit": "速率限制", "tpmLimit": "速率限制",
"tpmMessage": "请求过多。请稍等片刻。", "tpmMessage": "请求过多。请稍等片刻。",
"tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。", "tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。",
"messageApi": "糟糕 — 您已达到此演示的每日 API 限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。", "messageApi": "看来您今天的体验次数已达上限。非常高兴您玩得开心,虽然本项目由字节跳动豆包慷慨赞助,但为了确保大家都能公平使用,我们不得不对使用量做一点小小的限制。",
"messageToken": "糟糕 — 您已达到此演示的每日令牌限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。", "messageToken": "看来您今天的 Token 用量已达上限。非常高兴您玩得开心,虽然本项目由字节跳动豆包慷慨赞助,但为了确保大家都能公平使用,我们不得不对使用量做一点小小的限制。",
"tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。", "tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。",
"reset": "您的限制将在明天重置。感谢您的理解", "reset": "您的限制将在明天重置。感谢您的理解",
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">点击此处注册</a>可获得每个模型 50 万免费 Token包括豆包、DeepSeek 和 Kimi然后在模型设置中配置您的 API Key。",
"configModel": "使用您的密钥",
"selfHost": "自托管", "selfHost": "自托管",
"sponsor": "赞助", "sponsor": "赞助",
"learnMore": "了解更多 →", "learnMore": "了解更多 →",

View File

@@ -10,6 +10,7 @@ export interface QuotaConfig {
dailyRequestLimit: number dailyRequestLimit: number
dailyTokenLimit: number dailyTokenLimit: number
tpmLimit: number tpmLimit: number
onConfigModel?: () => void
} }
/** /**
@@ -22,7 +23,8 @@ export function useQuotaManager(config: QuotaConfig): {
showTokenLimitToast: (used?: number, limit?: number) => void showTokenLimitToast: (used?: number, limit?: number) => void
showTPMLimitToast: (limit?: number) => void showTPMLimitToast: (limit?: number) => void
} { } {
const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config const { dailyRequestLimit, dailyTokenLimit, tpmLimit, onConfigModel } =
config
const dict = useDictionary() const dict = useDictionary()
// Show quota limit toast (request-based) // Show quota limit toast (request-based)
@@ -34,12 +36,13 @@ export function useQuotaManager(config: QuotaConfig): {
used={used ?? dailyRequestLimit} used={used ?? dailyRequestLimit}
limit={limit ?? dailyRequestLimit} limit={limit ?? dailyRequestLimit}
onDismiss={() => toast.dismiss(t)} onDismiss={() => toast.dismiss(t)}
onConfigModel={onConfigModel}
/> />
), ),
{ duration: 15000 }, { duration: 15000 },
) )
}, },
[dailyRequestLimit], [dailyRequestLimit, onConfigModel],
) )
// Show token limit toast // Show token limit toast
@@ -52,12 +55,13 @@ export function useQuotaManager(config: QuotaConfig): {
used={used ?? dailyTokenLimit} used={used ?? dailyTokenLimit}
limit={limit ?? dailyTokenLimit} limit={limit ?? dailyTokenLimit}
onDismiss={() => toast.dismiss(t)} onDismiss={() => toast.dismiss(t)}
onConfigModel={onConfigModel}
/> />
), ),
{ duration: 15000 }, { duration: 15000 },
) )
}, },
[dailyTokenLimit], [dailyTokenLimit, onConfigModel],
) )
// Show TPM limit toast // Show TPM limit toast