@copilotkit/react-core 1.55.0-next.9 → 1.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -6
- package/dist/{copilotkit-DeOzjPsb.mjs → copilotkit-BY5S1-0P.mjs} +2402 -552
- package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
- package/dist/{copilotkit-BqcyhQjT.d.mts → copilotkit-BuhSUZHb.d.mts} +228 -17
- package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
- package/dist/{copilotkit-BDNjFNmk.cjs → copilotkit-Bz5-ImDl.cjs} +2421 -541
- package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
- package/dist/{copilotkit-l-IBF4Xp.d.cts → copilotkit-dwDWYpya.d.cts} +228 -17
- package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1400 -238
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +13 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- package/dist/v2/index.mjs +3 -2
- package/dist/v2/index.umd.js +2442 -552
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +62 -54
- package/scripts/scope-preflight.mjs +1 -2
- package/src/components/CopilotListeners.tsx +41 -8
- package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
- package/src/components/toast/toast-provider.tsx +269 -194
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
- package/src/v2/__tests__/utils/test-helpers.tsx +67 -0
- package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
- package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
- package/src/v2/components/CopilotKitInspector.tsx +2 -0
- package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
- package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
- package/src/v2/components/chat/CopilotChat.tsx +193 -50
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
- package/src/v2/components/chat/CopilotChatMessageView.tsx +253 -149
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
- package/src/v2/components/chat/CopilotChatView.tsx +179 -66
- package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
- package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
- package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +43 -2
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
- package/src/v2/components/chat/index.ts +9 -0
- package/src/v2/components/chat/scroll-element-context.ts +13 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
- package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
- package/src/v2/hooks/index.ts +5 -0
- package/src/v2/hooks/use-agent.tsx +95 -10
- package/src/v2/hooks/use-attachments.tsx +269 -0
- package/src/v2/hooks/use-frontend-tool.tsx +5 -2
- package/src/v2/hooks/use-render-activity-message.tsx +9 -2
- package/src/v2/hooks/use-threads.tsx +35 -15
- package/src/v2/index.ts +5 -1
- package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
- package/src/v2/lib/__tests__/slots.test.ts +56 -0
- package/src/v2/lib/processPartialHtml.ts +45 -0
- package/src/v2/lib/slots.tsx +42 -1
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
- package/src/v2/providers/CopilotKitProvider.tsx +268 -32
- package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
- package/src/v2/providers/index.ts +7 -0
- package/src/v2/styles/globals.css +2 -1
- package/src/v2/types/index.ts +1 -0
- package/src/v2/types/sandbox-function.ts +11 -0
- package/dist/copilotkit-BDNjFNmk.cjs.map +0 -1
- package/dist/copilotkit-BqcyhQjT.d.mts.map +0 -1
- package/dist/copilotkit-DeOzjPsb.mjs.map +0 -1
- package/dist/copilotkit-l-IBF4Xp.d.cts.map +0 -1
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useAgent } from "../../hooks/use-agent";
|
|
2
|
+
import { useAttachments } from "../../hooks/use-attachments";
|
|
2
3
|
import { useSuggestions } from "../../hooks/use-suggestions";
|
|
3
4
|
import { CopilotChatView, CopilotChatViewProps } from "./CopilotChatView";
|
|
4
5
|
import { CopilotChatInputMode } from "./CopilotChatInput";
|
|
@@ -12,16 +13,22 @@ import {
|
|
|
12
13
|
randomUUID,
|
|
13
14
|
TranscriptionErrorCode,
|
|
14
15
|
} from "@copilotkit/shared";
|
|
16
|
+
import type { AttachmentsConfig, InputContent } from "@copilotkit/shared";
|
|
15
17
|
import { Suggestion, CopilotKitCoreErrorCode } from "@copilotkit/core";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
+
import React, {
|
|
19
|
+
useCallback,
|
|
20
|
+
useEffect,
|
|
21
|
+
useMemo,
|
|
22
|
+
useRef,
|
|
23
|
+
useState,
|
|
24
|
+
} from "react";
|
|
18
25
|
import {
|
|
19
26
|
useCopilotKit,
|
|
20
27
|
useLicenseContext,
|
|
21
28
|
} from "../../providers/CopilotKitProvider";
|
|
22
29
|
import { InlineFeatureWarning } from "../../components/license-warning-banner";
|
|
23
30
|
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
|
|
24
|
-
import { renderSlot, SlotValue } from "../../lib/slots";
|
|
31
|
+
import { renderSlot, useShallowStableRef, SlotValue } from "../../lib/slots";
|
|
25
32
|
import {
|
|
26
33
|
transcribeAudio,
|
|
27
34
|
TranscriptionError,
|
|
@@ -34,12 +41,22 @@ export type CopilotChatProps = Omit<
|
|
|
34
41
|
| "suggestions"
|
|
35
42
|
| "suggestionLoadingIndexes"
|
|
36
43
|
| "onSelectSuggestion"
|
|
44
|
+
// Attachment state props — managed internally based on `attachments` config
|
|
45
|
+
| "attachments"
|
|
46
|
+
| "onRemoveAttachment"
|
|
47
|
+
| "onAddFile"
|
|
48
|
+
| "dragOver"
|
|
49
|
+
| "onDragOver"
|
|
50
|
+
| "onDragLeave"
|
|
51
|
+
| "onDrop"
|
|
37
52
|
> & {
|
|
38
53
|
agentId?: string;
|
|
39
54
|
threadId?: string;
|
|
40
55
|
labels?: Partial<CopilotChatLabels>;
|
|
41
56
|
chatView?: SlotValue<typeof CopilotChatView>;
|
|
42
57
|
isModalDefaultOpen?: boolean;
|
|
58
|
+
/** Enable multimodal file attachments (images, audio, video, documents). */
|
|
59
|
+
attachments?: AttachmentsConfig;
|
|
43
60
|
/**
|
|
44
61
|
* Error handler scoped to this chat's agent. Fires in addition to the
|
|
45
62
|
* provider-level onError (does not suppress it). Receives only errors
|
|
@@ -50,6 +67,18 @@ export type CopilotChatProps = Omit<
|
|
|
50
67
|
code: CopilotKitCoreErrorCode;
|
|
51
68
|
context: Record<string, any>;
|
|
52
69
|
}) => void | Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Throttle interval (in milliseconds) for re-renders triggered by message
|
|
72
|
+
* change notifications. Overrides the provider-level `defaultThrottleMs`
|
|
73
|
+
* for this chat instance. Forwarded to the internal `useAgent()` hook,
|
|
74
|
+
* which resolves the effective throttle value.
|
|
75
|
+
*
|
|
76
|
+
* @default undefined — inherits from provider `defaultThrottleMs`;
|
|
77
|
+
* if that is also unset, re-renders are unthrottled. Note: passing
|
|
78
|
+
* `throttleMs={0}` explicitly disables throttling for this instance
|
|
79
|
+
* even when the provider specifies a non-zero `defaultThrottleMs`.
|
|
80
|
+
*/
|
|
81
|
+
throttleMs?: number;
|
|
53
82
|
};
|
|
54
83
|
export function CopilotChat({
|
|
55
84
|
agentId,
|
|
@@ -57,7 +86,9 @@ export function CopilotChat({
|
|
|
57
86
|
labels,
|
|
58
87
|
chatView,
|
|
59
88
|
isModalDefaultOpen,
|
|
89
|
+
attachments: attachmentsConfig,
|
|
60
90
|
onError,
|
|
91
|
+
throttleMs,
|
|
61
92
|
...props
|
|
62
93
|
}: CopilotChatProps) {
|
|
63
94
|
// Check for existing configuration provider
|
|
@@ -74,6 +105,7 @@ export function CopilotChat({
|
|
|
74
105
|
const { agent } = useAgent({
|
|
75
106
|
agentId: resolvedAgentId,
|
|
76
107
|
threadId: resolvedThreadId,
|
|
108
|
+
throttleMs,
|
|
77
109
|
});
|
|
78
110
|
const { copilotkit } = useCopilotKit();
|
|
79
111
|
const { suggestions: autoSuggestions } = useSuggestions({
|
|
@@ -130,6 +162,21 @@ export function CopilotChat({
|
|
|
130
162
|
);
|
|
131
163
|
const [isTranscribing, setIsTranscribing] = useState(false);
|
|
132
164
|
|
|
165
|
+
// Attachments
|
|
166
|
+
const {
|
|
167
|
+
attachments: selectedAttachments,
|
|
168
|
+
enabled: attachmentsEnabled,
|
|
169
|
+
dragOver,
|
|
170
|
+
fileInputRef,
|
|
171
|
+
containerRef: chatContainerRef,
|
|
172
|
+
handleFileUpload,
|
|
173
|
+
handleDragOver,
|
|
174
|
+
handleDragLeave,
|
|
175
|
+
handleDrop,
|
|
176
|
+
removeAttachment,
|
|
177
|
+
consumeAttachments,
|
|
178
|
+
} = useAttachments({ config: attachmentsConfig });
|
|
179
|
+
|
|
133
180
|
// Check if transcription is enabled
|
|
134
181
|
const isTranscriptionEnabled = copilotkit.audioFileTranscriptionEnabled;
|
|
135
182
|
|
|
@@ -186,11 +233,47 @@ export function CopilotChat({
|
|
|
186
233
|
|
|
187
234
|
const onSubmitInput = useCallback(
|
|
188
235
|
async (value: string) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
236
|
+
// Block if uploads in progress
|
|
237
|
+
const hasUploading = selectedAttachments.some(
|
|
238
|
+
(a) => a.status === "uploading",
|
|
239
|
+
);
|
|
240
|
+
if (hasUploading) {
|
|
241
|
+
console.error(
|
|
242
|
+
"[CopilotKit] Cannot send while attachments are uploading",
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const readyAttachments = consumeAttachments();
|
|
248
|
+
|
|
249
|
+
if (readyAttachments.length > 0) {
|
|
250
|
+
const contentParts: InputContent[] = [];
|
|
251
|
+
if (value.trim()) {
|
|
252
|
+
contentParts.push({ type: "text", text: value });
|
|
253
|
+
}
|
|
254
|
+
for (const att of readyAttachments) {
|
|
255
|
+
contentParts.push({
|
|
256
|
+
type: att.type,
|
|
257
|
+
source: att.source,
|
|
258
|
+
metadata: {
|
|
259
|
+
...(att.filename ? { filename: att.filename } : {}),
|
|
260
|
+
...att.metadata,
|
|
261
|
+
},
|
|
262
|
+
} as InputContent);
|
|
263
|
+
}
|
|
264
|
+
agent.addMessage({
|
|
265
|
+
id: randomUUID(),
|
|
266
|
+
role: "user",
|
|
267
|
+
content: contentParts,
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
agent.addMessage({
|
|
271
|
+
id: randomUUID(),
|
|
272
|
+
role: "user",
|
|
273
|
+
content: value,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
194
277
|
// Clear input after submitting
|
|
195
278
|
setInputValue("");
|
|
196
279
|
try {
|
|
@@ -201,7 +284,7 @@ export function CopilotChat({
|
|
|
201
284
|
},
|
|
202
285
|
// copilotkit is intentionally excluded — it is a stable ref that never changes.
|
|
203
286
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
-
[agent],
|
|
287
|
+
[agent, selectedAttachments, consumeAttachments],
|
|
205
288
|
);
|
|
206
289
|
|
|
207
290
|
const handleSelectSuggestion = useCallback(
|
|
@@ -336,22 +419,38 @@ export function CopilotChat({
|
|
|
336
419
|
}
|
|
337
420
|
}, [transcriptionError]);
|
|
338
421
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
{
|
|
347
|
-
...restProps,
|
|
348
|
-
...(typeof providedMessageView === "string"
|
|
349
|
-
? { messageView: { className: providedMessageView } }
|
|
350
|
-
: providedMessageView !== undefined
|
|
351
|
-
? { messageView: providedMessageView }
|
|
352
|
-
: {}),
|
|
353
|
-
},
|
|
422
|
+
// Stabilize slot object references so inline props (new object reference on
|
|
423
|
+
// every parent render) don't defeat MemoizedSlotWrapper's shallow equality
|
|
424
|
+
// check and cause unnecessary re-renders of the message list on each keystroke.
|
|
425
|
+
const stableMessageView = useShallowStableRef(
|
|
426
|
+
typeof providedMessageView === "string"
|
|
427
|
+
? { className: providedMessageView }
|
|
428
|
+
: providedMessageView,
|
|
354
429
|
);
|
|
430
|
+
const stableSuggestionView = useShallowStableRef(providedSuggestionView);
|
|
431
|
+
|
|
432
|
+
// Stabilize the `onAddFile` handler. Without useCallback, a new arrow
|
|
433
|
+
// function is created inline on every render, causing CopilotChatView to
|
|
434
|
+
// re-render on every keystroke even when nothing else changed.
|
|
435
|
+
const handleAddFile = useCallback(() => {
|
|
436
|
+
// Delay to let Radix dropdown menu close before triggering file input
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
fileInputRef.current?.click();
|
|
439
|
+
}, 100);
|
|
440
|
+
}, []);
|
|
441
|
+
|
|
442
|
+
// Use shallow spread instead of ts-deepmerge. ts-deepmerge deep-clones plain
|
|
443
|
+
// objects even from a single source, which would defeat the reference
|
|
444
|
+
// stability we just established for stableMessageView and other slot values.
|
|
445
|
+
const mergedProps: Partial<CopilotChatViewProps> = {
|
|
446
|
+
isRunning: agent.isRunning,
|
|
447
|
+
suggestions: autoSuggestions,
|
|
448
|
+
onSelectSuggestion: handleSelectSuggestion,
|
|
449
|
+
suggestionView: stableSuggestionView,
|
|
450
|
+
...restProps,
|
|
451
|
+
};
|
|
452
|
+
if (stableMessageView !== undefined)
|
|
453
|
+
mergedProps.messageView = stableMessageView;
|
|
355
454
|
|
|
356
455
|
const hasMessages = agent.messages.length > 0;
|
|
357
456
|
const shouldAllowStop = agent.isRunning && hasMessages;
|
|
@@ -367,15 +466,39 @@ export function CopilotChat({
|
|
|
367
466
|
? "processing"
|
|
368
467
|
: transcribeMode;
|
|
369
468
|
|
|
370
|
-
// Memoize messages array
|
|
371
|
-
//
|
|
372
|
-
|
|
469
|
+
// Memoize messages array — only create a new reference when content changes.
|
|
470
|
+
// We build a lightweight fingerprint instead of JSON.stringify to avoid
|
|
471
|
+
// serializing large base64 attachment data on every render. The key captures:
|
|
472
|
+
// - message id, role, content length (text streaming)
|
|
473
|
+
// - content part count (multimodal additions)
|
|
474
|
+
// - tool call ids + argument lengths (tool call streaming)
|
|
475
|
+
const messagesMemoKey = agent.messages
|
|
476
|
+
.map((m) => {
|
|
477
|
+
const contentKey =
|
|
478
|
+
typeof m.content === "string"
|
|
479
|
+
? m.content.length
|
|
480
|
+
: Array.isArray(m.content)
|
|
481
|
+
? m.content.length
|
|
482
|
+
: 0;
|
|
483
|
+
const toolCallsKey =
|
|
484
|
+
"toolCalls" in m && Array.isArray(m.toolCalls)
|
|
485
|
+
? m.toolCalls
|
|
486
|
+
.map(
|
|
487
|
+
(tc: any) => `${tc.id}:${tc.function?.arguments?.length ?? 0}`,
|
|
488
|
+
)
|
|
489
|
+
.join(";")
|
|
490
|
+
: "";
|
|
491
|
+
return `${m.id}:${m.role}:${contentKey}:${toolCallsKey}`;
|
|
492
|
+
})
|
|
493
|
+
.join(",");
|
|
373
494
|
const messages = useMemo(
|
|
374
495
|
() => [...agent.messages],
|
|
375
|
-
|
|
496
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
497
|
+
[messagesMemoKey],
|
|
376
498
|
);
|
|
377
499
|
|
|
378
|
-
const finalProps =
|
|
500
|
+
const finalProps: CopilotChatViewProps = {
|
|
501
|
+
...mergedProps,
|
|
379
502
|
messages,
|
|
380
503
|
// Input behavior props
|
|
381
504
|
onSubmitMessage: onSubmitInput,
|
|
@@ -390,7 +513,15 @@ export function CopilotChat({
|
|
|
390
513
|
onFinishTranscribeWithAudio: showTranscription
|
|
391
514
|
? handleFinishTranscribeWithAudio
|
|
392
515
|
: undefined,
|
|
393
|
-
|
|
516
|
+
// Attachment props
|
|
517
|
+
attachments: selectedAttachments,
|
|
518
|
+
onRemoveAttachment: removeAttachment,
|
|
519
|
+
onAddFile: attachmentsEnabled ? handleAddFile : undefined,
|
|
520
|
+
dragOver,
|
|
521
|
+
onDragOver: handleDragOver,
|
|
522
|
+
onDragLeave: handleDragLeave,
|
|
523
|
+
onDrop: handleDrop,
|
|
524
|
+
};
|
|
394
525
|
|
|
395
526
|
// Always create a provider with merged values
|
|
396
527
|
// This ensures priority: props > existing config > defaults
|
|
@@ -403,26 +534,38 @@ export function CopilotChat({
|
|
|
403
534
|
labels={labels}
|
|
404
535
|
isModalDefaultOpen={isModalDefaultOpen}
|
|
405
536
|
>
|
|
406
|
-
{
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
537
|
+
<div ref={chatContainerRef} style={{ display: "contents" }}>
|
|
538
|
+
{attachmentsEnabled && (
|
|
539
|
+
<input
|
|
540
|
+
type="file"
|
|
541
|
+
multiple
|
|
542
|
+
ref={fileInputRef}
|
|
543
|
+
onChange={handleFileUpload}
|
|
544
|
+
accept={attachmentsConfig?.accept ?? "*/*"}
|
|
545
|
+
style={{ display: "none" }}
|
|
546
|
+
/>
|
|
547
|
+
)}
|
|
548
|
+
{!isChatLicensed && <InlineFeatureWarning featureName="Chat" />}
|
|
549
|
+
{transcriptionError && (
|
|
550
|
+
<div
|
|
551
|
+
style={{
|
|
552
|
+
position: "absolute",
|
|
553
|
+
bottom: "100px",
|
|
554
|
+
left: "50%",
|
|
555
|
+
transform: "translateX(-50%)",
|
|
556
|
+
backgroundColor: "#ef4444",
|
|
557
|
+
color: "white",
|
|
558
|
+
padding: "8px 16px",
|
|
559
|
+
borderRadius: "8px",
|
|
560
|
+
fontSize: "14px",
|
|
561
|
+
zIndex: 50,
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
{transcriptionError}
|
|
565
|
+
</div>
|
|
566
|
+
)}
|
|
567
|
+
{RenderedChatView}
|
|
568
|
+
</div>
|
|
426
569
|
</CopilotChatConfigurationProvider>
|
|
427
570
|
);
|
|
428
571
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AssistantMessage, Message } from "@ag-ui/core";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
Copy,
|
|
5
5
|
Check,
|
|
@@ -265,10 +265,25 @@ export namespace CopilotChatAssistantMessage {
|
|
|
265
265
|
const config = useCopilotChatConfiguration();
|
|
266
266
|
const labels = config?.labels ?? CopilotChatDefaultLabels;
|
|
267
267
|
const [copied, setCopied] = useState(false);
|
|
268
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
return () => {
|
|
272
|
+
if (timerRef.current !== null) {
|
|
273
|
+
clearTimeout(timerRef.current);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}, []);
|
|
268
277
|
|
|
269
278
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
270
279
|
setCopied(true);
|
|
271
|
-
|
|
280
|
+
if (timerRef.current !== null) {
|
|
281
|
+
clearTimeout(timerRef.current);
|
|
282
|
+
}
|
|
283
|
+
timerRef.current = setTimeout(() => {
|
|
284
|
+
timerRef.current = null;
|
|
285
|
+
setCopied(false);
|
|
286
|
+
}, 2000);
|
|
272
287
|
|
|
273
288
|
if (onClick) {
|
|
274
289
|
onClick(event);
|