From b2f4b91979ce96067dbf01e7db1263570a208be5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 11:53:13 +0800 Subject: [PATCH] perf: replace copy button toast notifications with checkmark feedback (#1898) * Initial plan * Replace copy button toast notifications with checkmark visual feedback Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Complete copy button checkmark feedback implementation Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * revert pnpm-lock.yaml --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin --- .../home/bots/components/bot-form/BotForm.tsx | 22 ++++--- .../components/bot-log/view/BotLogCard.tsx | 63 ++++++++++--------- .../ApiIntegrationDialog.tsx | 24 +++++-- web/src/app/home/plugins/page.tsx | 28 +++++++-- 4 files changed, 90 insertions(+), 47 deletions(-) diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index bef21443..5e030021 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -19,6 +19,7 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; +import { Copy, Check } from 'lucide-react'; import { Dialog, @@ -116,6 +117,7 @@ export default function BotForm({ const [, setIsLoading] = useState(false); const [webhookUrl, setWebhookUrl] = useState(''); const webhookInputRef = React.useRef(null); + const [copied, setCopied] = useState(false); // Watch adapter and adapter_config for filtering const currentAdapter = form.watch('adapter'); @@ -153,7 +155,6 @@ export default function BotForm({ const inputElement = webhookInputRef.current; if (!inputElement) { console.error('[Copy] Input element not found'); - toast.error(t('common.copyFailed')); return; } @@ -178,7 +179,8 @@ export default function BotForm({ console.log('[Copy] Clipboard API success'); inputElement.blur(); // 取消选中 inputElement.readOnly = true; - toast.success(t('bots.webhookUrlCopied')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); }) .catch((err) => { console.error( @@ -191,9 +193,8 @@ export default function BotForm({ inputElement.blur(); inputElement.readOnly = true; if (successful) { - toast.success(t('bots.webhookUrlCopied')); - } else { - toast.error(t('common.copyFailed')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); } }); } else { @@ -207,15 +208,13 @@ export default function BotForm({ inputElement.blur(); inputElement.readOnly = true; if (successful) { - toast.success(t('bots.webhookUrlCopied')); - } else { - toast.error(t('common.copyFailed')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); } } } catch (err) { console.error('[Copy] Copy failed:', err); inputElement.readOnly = true; - toast.error(t('common.copyFailed')); } }; @@ -548,6 +547,11 @@ export default function BotForm({ size="sm" onClick={copyToClipboard} > + {copied ? ( + + ) : ( + + )} {t('common.copy')} diff --git a/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx index f916af2f..7cc380ec 100644 --- a/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx +++ b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx @@ -1,15 +1,17 @@ 'use client'; +import { useState } from 'react'; import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; import styles from './botLog.module.css'; import { httpClient } from '@/app/infra/http/HttpClient'; import { PhotoProvider } from 'react-photo-view'; import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; +import { Check } from 'lucide-react'; export function BotLogCard({ botLog }: { botLog: BotLog }) { const { t } = useTranslation(); const baseURL = httpClient.getBaseUrl(); + const [copied, setCopied] = useState(false); function formatTime(timestamp: number) { const now = new Date(); @@ -75,42 +77,47 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) { {botLog.message_session_id && (
{ navigator.clipboard .writeText(botLog.message_session_id) .then(() => { - toast.success(t('common.copySuccess')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); }); }} title={t('common.clickToCopy')} > - - + ) : ( + - - - + > + + + + + )} {getSubChatId(botLog.message_session_id)} diff --git a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx b/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx index 68b36417..cb0996be 100644 --- a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx +++ b/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; -import { Copy, Trash2, Plus } from 'lucide-react'; +import { Copy, Check, Trash2, Plus } from 'lucide-react'; import { Dialog, DialogContent, @@ -83,6 +83,7 @@ export default function ApiIntegrationDialog({ const [newWebhookDescription, setNewWebhookDescription] = useState(''); const [newWebhookEnabled, setNewWebhookEnabled] = useState(true); const [deleteWebhookId, setDeleteWebhookId] = useState(null); + const [copiedKey, setCopiedKey] = useState(null); // 清理 body 样式,防止对话框关闭后页面无法交互 useEffect(() => { @@ -154,7 +155,8 @@ export default function ApiIntegrationDialog({ const handleCopyKey = (key: string) => { navigator.clipboard.writeText(key); - toast.success(t('common.apiKeyCopied')); + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 2000); }; const maskApiKey = (key: string) => { @@ -333,7 +335,11 @@ export default function ApiIntegrationDialog({ onClick={() => handleCopyKey(key.key)} title={t('common.copyApiKey')} > - + {copiedKey === key.key ? ( + + ) : ( + + )}
@@ -621,7 +631,11 @@ export default function ApiIntegrationDialog({ variant="outline" size="icon" > - + {copiedKey === createdKey?.key ? ( + + ) : ( + + )} diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 3444fa7f..822364d6 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -26,6 +26,7 @@ import { ChevronLeft, Code, Copy, + Check, Bug, } from 'lucide-react'; import { @@ -118,6 +119,8 @@ export default function PluginConfigPage() { plugin_debug_key: string; } | null>(null); const [debugPopoverOpen, setDebugPopoverOpen] = useState(false); + const [copiedDebugUrl, setCopiedDebugUrl] = useState(false); + const [copiedDebugKey, setCopiedDebugKey] = useState(false); useEffect(() => { const fetchPluginSystemStatus = async () => { @@ -398,9 +401,15 @@ export default function PluginConfigPage() { } }; - const handleCopyDebugInfo = (text: string) => { + const handleCopyDebugInfo = (text: string, type: 'url' | 'key') => { navigator.clipboard.writeText(text); - toast.success(t('plugins.copiedToClipboard')); + if (type === 'url') { + setCopiedDebugUrl(true); + setTimeout(() => setCopiedDebugUrl(false), 2000); + } else { + setCopiedDebugKey(true); + setTimeout(() => setCopiedDebugKey(false), 2000); + } }; const renderPluginDisabledState = () => ( @@ -536,10 +545,14 @@ export default function PluginConfigPage() { size="icon" className="h-8 w-8 shrink-0" onClick={() => - handleCopyDebugInfo(debugInfo?.debug_url || '') + handleCopyDebugInfo(debugInfo?.debug_url || '', 'url') } > - + {copiedDebugUrl ? ( + + ) : ( + + )} @@ -564,11 +577,16 @@ export default function PluginConfigPage() { onClick={() => handleCopyDebugInfo( debugInfo?.plugin_debug_key || '', + 'key', ) } disabled={!debugInfo?.plugin_debug_key} > - + {copiedDebugKey ? ( + + ) : ( + + )} {!debugInfo?.plugin_debug_key && (