mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 18:06:06 +00:00
Feat/web UI fixes v2 (#2152)
* fix(web): 修复复制按钮和插件安装对话框UI问题 - 新增 clipboard.ts 工具函数支持 Clipboard API 降级 - 修复添加机器人页面 Webhook URL 复制按钮未生效 - 修复 API 集成对话框 API Key 复制按钮未生效 - 修复 Bot 会话监控用户 ID 复制按钮未生效 - 修复插件安装进度状态框横向溢出和小屏缩放问题 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): improve clipboard copy with Selection API fallback Replace navigator.clipboard.writeText with Selection API + execCommand for reliable copying in non-secure contexts. Remove duplicate dialog. Fix scanProviderModels type signature to accept rerank model type. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): revert package-lock.json to match upstream Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): fix prettier formatting errors Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): unify all clipboard copy to use copyToClipboard utility - Fix embed code copy button not working in non-secure contexts - Add copy animation (check icon) to embed code button via EmbedCodeField component - Replace raw navigator.clipboard calls in plugins/page.tsx and BotLogCard.tsx - Remove duplicated inline fallback implementations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Copy, Check, Trash2, Plus } from 'lucide-react';
|
||||
@@ -86,6 +86,9 @@ export default function ApiIntegrationDialog({
|
||||
const [newWebhookDescription, setNewWebhookDescription] = useState('');
|
||||
const [newWebhookEnabled, setNewWebhookEnabled] = useState(true);
|
||||
const [deleteWebhookId, setDeleteWebhookId] = useState<number | null>(null);
|
||||
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
|
||||
// Sync URL with dialog state
|
||||
@@ -182,10 +185,29 @@ export default function ApiIntegrationDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
const el = document.createElement('span');
|
||||
el.textContent = text;
|
||||
el.style.cssText =
|
||||
'position:fixed;opacity:0;pointer-events:none;white-space:pre;';
|
||||
document.body.appendChild(el);
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
document.execCommand('copy');
|
||||
sel?.removeAllRanges();
|
||||
document.body.removeChild(el);
|
||||
};
|
||||
|
||||
const handleCopyKey = (key: string) => {
|
||||
navigator.clipboard.writeText(key);
|
||||
try {
|
||||
copyToClipboard(key);
|
||||
} catch {}
|
||||
clearTimeout(copiedTimerRef.current);
|
||||
setCopiedKey(key);
|
||||
setTimeout(() => setCopiedKey(null), 2000);
|
||||
copiedTimerRef.current = setTimeout(() => setCopiedKey(null), 2000);
|
||||
};
|
||||
|
||||
const maskApiKey = (key: string) => {
|
||||
@@ -330,21 +352,21 @@ export default function ApiIntegrationDialog({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((key) => (
|
||||
<TableRow key={key.id}>
|
||||
{apiKeys.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{key.name}</div>
|
||||
{key.description && (
|
||||
<div className="font-medium">{item.name}</div>
|
||||
{item.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{key.description}
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded">
|
||||
{maskApiKey(key.key)}
|
||||
{maskApiKey(item.key)}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -352,10 +374,11 @@ export default function ApiIntegrationDialog({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopyKey(key.key)}
|
||||
type="button"
|
||||
onClick={() => handleCopyKey(item.key)}
|
||||
title={t('common.copyApiKey')}
|
||||
>
|
||||
{copiedKey === key.key ? (
|
||||
{copiedKey === item.key ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
@@ -364,7 +387,7 @@ export default function ApiIntegrationDialog({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDeleteKeyId(key.id)}
|
||||
onClick={() => setDeleteKeyId(item.id)}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -630,44 +653,6 @@ export default function ApiIntegrationDialog({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete API Key Confirmation Dialog */}
|
||||
<Dialog open={!!createdKey} onOpenChange={() => setCreatedKey(null)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.apiKeyCreated')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('common.apiKeyCreatedMessage')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
{t('common.apiKeyValue')}
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Input value={createdKey?.key || ''} readOnly />
|
||||
<Button
|
||||
onClick={() => createdKey && handleCopyKey(createdKey.key)}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
{copiedKey === createdKey?.key ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setCreatedKey(null)}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={!!deleteKeyId}>
|
||||
<AlertDialogPortal>
|
||||
|
||||
Reference in New Issue
Block a user