mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-18 02:16:07 +00:00
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 <rockchinq@gmail.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -116,6 +117,7 @@ export default function BotForm({
|
|||||||
const [, setIsLoading] = useState<boolean>(false);
|
const [, setIsLoading] = useState<boolean>(false);
|
||||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||||
const webhookInputRef = React.useRef<HTMLInputElement>(null);
|
const webhookInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
// Watch adapter and adapter_config for filtering
|
// Watch adapter and adapter_config for filtering
|
||||||
const currentAdapter = form.watch('adapter');
|
const currentAdapter = form.watch('adapter');
|
||||||
@@ -153,7 +155,6 @@ export default function BotForm({
|
|||||||
const inputElement = webhookInputRef.current;
|
const inputElement = webhookInputRef.current;
|
||||||
if (!inputElement) {
|
if (!inputElement) {
|
||||||
console.error('[Copy] Input element not found');
|
console.error('[Copy] Input element not found');
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +179,8 @@ export default function BotForm({
|
|||||||
console.log('[Copy] Clipboard API success');
|
console.log('[Copy] Clipboard API success');
|
||||||
inputElement.blur(); // 取消选中
|
inputElement.blur(); // 取消选中
|
||||||
inputElement.readOnly = true;
|
inputElement.readOnly = true;
|
||||||
toast.success(t('bots.webhookUrlCopied'));
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -191,9 +193,8 @@ export default function BotForm({
|
|||||||
inputElement.blur();
|
inputElement.blur();
|
||||||
inputElement.readOnly = true;
|
inputElement.readOnly = true;
|
||||||
if (successful) {
|
if (successful) {
|
||||||
toast.success(t('bots.webhookUrlCopied'));
|
setCopied(true);
|
||||||
} else {
|
setTimeout(() => setCopied(false), 2000);
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -207,15 +208,13 @@ export default function BotForm({
|
|||||||
inputElement.blur();
|
inputElement.blur();
|
||||||
inputElement.readOnly = true;
|
inputElement.readOnly = true;
|
||||||
if (successful) {
|
if (successful) {
|
||||||
toast.success(t('bots.webhookUrlCopied'));
|
setCopied(true);
|
||||||
} else {
|
setTimeout(() => setCopied(false), 2000);
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Copy] Copy failed:', err);
|
console.error('[Copy] Copy failed:', err);
|
||||||
inputElement.readOnly = true;
|
inputElement.readOnly = true;
|
||||||
toast.error(t('common.copyFailed'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -548,6 +547,11 @@ export default function BotForm({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
>
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600 mr-2" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
{t('common.copy')}
|
{t('common.copy')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
import styles from './botLog.module.css';
|
import styles from './botLog.module.css';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { PhotoProvider } from 'react-photo-view';
|
import { PhotoProvider } from 'react-photo-view';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const baseURL = httpClient.getBaseUrl();
|
const baseURL = httpClient.getBaseUrl();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
function formatTime(timestamp: number) {
|
function formatTime(timestamp: number) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -75,42 +77,47 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
|||||||
</div>
|
</div>
|
||||||
{botLog.message_session_id && (
|
{botLog.message_session_id && (
|
||||||
<div
|
<div
|
||||||
className={`${styles.tag} ${styles.chatTag}`}
|
className={`${styles.tag} ${styles.chatTag} relative`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.writeText(botLog.message_session_id)
|
.writeText(botLog.message_session_id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(t('common.copySuccess'));
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
title={t('common.clickToCopy')}
|
title={t('common.clickToCopy')}
|
||||||
>
|
>
|
||||||
<svg
|
{copied ? (
|
||||||
className="icon"
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
viewBox="0 0 1024 1024"
|
) : (
|
||||||
version="1.1"
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
className="icon"
|
||||||
p-id="1664"
|
viewBox="0 0 1024 1024"
|
||||||
width="16"
|
version="1.1"
|
||||||
height="16"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="currentColor"
|
p-id="1664"
|
||||||
>
|
width="16"
|
||||||
<path
|
height="16"
|
||||||
d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
|
|
||||||
p-id="1665"
|
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
></path>
|
>
|
||||||
<path
|
<path
|
||||||
d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"
|
d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
|
||||||
p-id="1666"
|
p-id="1665"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
></path>
|
></path>
|
||||||
<path
|
<path
|
||||||
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"
|
d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"
|
||||||
p-id="1667"
|
p-id="1666"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
<path
|
||||||
|
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"
|
||||||
|
p-id="1667"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className={`${styles.chatId}`}>
|
<span className={`${styles.chatId}`}>
|
||||||
{getSubChatId(botLog.message_session_id)}
|
{getSubChatId(botLog.message_session_id)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Copy, Trash2, Plus } from 'lucide-react';
|
import { Copy, Check, Trash2, Plus } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -83,6 +83,7 @@ export default function ApiIntegrationDialog({
|
|||||||
const [newWebhookDescription, setNewWebhookDescription] = useState('');
|
const [newWebhookDescription, setNewWebhookDescription] = useState('');
|
||||||
const [newWebhookEnabled, setNewWebhookEnabled] = useState(true);
|
const [newWebhookEnabled, setNewWebhookEnabled] = useState(true);
|
||||||
const [deleteWebhookId, setDeleteWebhookId] = useState<number | null>(null);
|
const [deleteWebhookId, setDeleteWebhookId] = useState<number | null>(null);
|
||||||
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
|
|
||||||
// 清理 body 样式,防止对话框关闭后页面无法交互
|
// 清理 body 样式,防止对话框关闭后页面无法交互
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -154,7 +155,8 @@ export default function ApiIntegrationDialog({
|
|||||||
|
|
||||||
const handleCopyKey = (key: string) => {
|
const handleCopyKey = (key: string) => {
|
||||||
navigator.clipboard.writeText(key);
|
navigator.clipboard.writeText(key);
|
||||||
toast.success(t('common.apiKeyCopied'));
|
setCopiedKey(key);
|
||||||
|
setTimeout(() => setCopiedKey(null), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const maskApiKey = (key: string) => {
|
const maskApiKey = (key: string) => {
|
||||||
@@ -333,7 +335,11 @@ export default function ApiIntegrationDialog({
|
|||||||
onClick={() => handleCopyKey(key.key)}
|
onClick={() => handleCopyKey(key.key)}
|
||||||
title={t('common.copyApiKey')}
|
title={t('common.copyApiKey')}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
{copiedKey === key.key ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -524,7 +530,11 @@ export default function ApiIntegrationDialog({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
{copiedKey === createdKey?.key ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -621,7 +631,11 @@ export default function ApiIntegrationDialog({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
{copiedKey === createdKey?.key ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Code,
|
Code,
|
||||||
Copy,
|
Copy,
|
||||||
|
Check,
|
||||||
Bug,
|
Bug,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
@@ -118,6 +119,8 @@ export default function PluginConfigPage() {
|
|||||||
plugin_debug_key: string;
|
plugin_debug_key: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
|
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
|
||||||
|
const [copiedDebugUrl, setCopiedDebugUrl] = useState(false);
|
||||||
|
const [copiedDebugKey, setCopiedDebugKey] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPluginSystemStatus = async () => {
|
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);
|
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 = () => (
|
const renderPluginDisabledState = () => (
|
||||||
@@ -536,10 +545,14 @@ export default function PluginConfigPage() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCopyDebugInfo(debugInfo?.debug_url || '')
|
handleCopyDebugInfo(debugInfo?.debug_url || '', 'url')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Copy className="w-3.5 h-3.5" />
|
{copiedDebugUrl ? (
|
||||||
|
<Check className="w-3.5 h-3.5 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -564,11 +577,16 @@ export default function PluginConfigPage() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCopyDebugInfo(
|
handleCopyDebugInfo(
|
||||||
debugInfo?.plugin_debug_key || '',
|
debugInfo?.plugin_debug_key || '',
|
||||||
|
'key',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={!debugInfo?.plugin_debug_key}
|
disabled={!debugInfo?.plugin_debug_key}
|
||||||
>
|
>
|
||||||
<Copy className="w-3.5 h-3.5" />
|
{copiedDebugKey ? (
|
||||||
|
<Check className="w-3.5 h-3.5 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!debugInfo?.plugin_debug_key && (
|
{!debugInfo?.plugin_debug_key && (
|
||||||
|
|||||||
Reference in New Issue
Block a user