@copilotkit/react-core 1.54.1 → 1.55.0-next.7
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 +117 -116
- package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
- package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
- package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
- package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
- package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
- package/dist/index.cjs +27 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1941 -35
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +77 -7
- package/dist/v2/index.css +1 -2
- package/dist/v2/index.d.cts +6 -4
- package/dist/v2/index.d.mts +6 -4
- package/dist/v2/index.mjs +7 -4
- package/dist/v2/index.umd.js +5725 -24
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +37 -9
- package/scripts/scope-preflight.mjs +101 -0
- package/src/components/CopilotListeners.tsx +2 -6
- package/src/components/copilot-provider/copilot-messages.tsx +1 -1
- package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
- package/src/components/copilot-provider/copilotkit.tsx +4 -4
- package/src/context/copilot-messages-context.tsx +1 -1
- package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
- package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
- package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
- package/src/hooks/use-agent-nodename.ts +1 -1
- package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
- package/src/hooks/use-coagent.ts +1 -1
- package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat_internal.ts +2 -2
- package/src/hooks/use-copilot-readable.ts +1 -1
- package/src/hooks/use-frontend-tool.ts +2 -2
- package/src/hooks/use-human-in-the-loop.ts +2 -2
- package/src/hooks/use-langgraph-interrupt.ts +2 -5
- package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
- package/src/hooks/use-render-tool-call.ts +1 -1
- package/src/lib/copilot-task.ts +1 -1
- package/src/setupTests.ts +18 -14
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
- package/src/v2/__tests__/globalSetup.ts +14 -0
- package/src/v2/__tests__/setup.ts +93 -0
- package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
- package/src/v2/components/CopilotKitInspector.tsx +50 -0
- package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
- package/src/v2/components/WildcardToolCallRender.tsx +86 -0
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
- package/src/v2/components/chat/CopilotChat.tsx +431 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
- package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
- package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
- package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
- package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
- package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
- package/src/v2/components/chat/CopilotChatView.tsx +598 -0
- package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
- package/src/v2/components/chat/CopilotPopup.tsx +81 -0
- package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
- package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
- package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
- package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
- package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
- package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
- package/src/v2/components/chat/__tests__/setup.ts +1 -0
- package/src/v2/components/chat/index.ts +79 -0
- package/src/v2/components/index.ts +7 -0
- package/src/v2/components/license-warning-banner.tsx +198 -0
- package/src/v2/components/ui/button.tsx +123 -0
- package/src/v2/components/ui/dropdown-menu.tsx +258 -0
- package/src/v2/components/ui/tooltip.tsx +60 -0
- package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
- package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
- package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
- package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
- package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
- package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
- package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
- package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
- package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
- package/src/v2/hooks/index.ts +18 -0
- package/src/v2/hooks/use-agent-context.tsx +45 -0
- package/src/v2/hooks/use-agent.tsx +155 -0
- package/src/v2/hooks/use-component.tsx +89 -0
- package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
- package/src/v2/hooks/use-default-render-tool.tsx +254 -0
- package/src/v2/hooks/use-frontend-tool.tsx +43 -0
- package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
- package/src/v2/hooks/use-interrupt.tsx +305 -0
- package/src/v2/hooks/use-keyboard-height.tsx +67 -0
- package/src/v2/hooks/use-render-activity-message.tsx +73 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
- package/src/v2/hooks/use-render-tool-call.tsx +175 -0
- package/src/v2/hooks/use-render-tool.tsx +181 -0
- package/src/v2/hooks/use-suggestions.tsx +91 -0
- package/src/v2/hooks/use-threads.tsx +256 -0
- package/src/v2/hooks/useKatexStyles.ts +27 -0
- package/src/v2/index.css +1 -1
- package/src/v2/index.ts +18 -2
- package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
- package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
- package/src/v2/lib/react-core.ts +156 -0
- package/src/v2/lib/slots.tsx +143 -0
- package/src/v2/lib/transcription-client.ts +184 -0
- package/src/v2/lib/utils.ts +8 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
- package/src/v2/providers/CopilotKitProvider.tsx +600 -0
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
- package/src/v2/providers/index.ts +14 -0
- package/src/v2/styles/globals.css +230 -0
- package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
- package/src/v2/types/defineToolCallRenderer.ts +65 -0
- package/src/v2/types/frontend-tool.ts +8 -0
- package/src/v2/types/human-in-the-loop.ts +33 -0
- package/src/v2/types/index.ts +7 -0
- package/src/v2/types/interrupt.ts +15 -0
- package/src/v2/types/react-activity-message-renderer.ts +27 -0
- package/src/v2/types/react-custom-message-renderer.ts +17 -0
- package/src/v2/types/react-tool-call-renderer.ts +32 -0
- package/tsdown.config.ts +34 -10
- package/vitest.config.mjs +4 -3
- package/LICENSE +0 -21
- package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
- package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
- package/dist/copilotkit-C94ayZbs.cjs +0 -2161
- package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
- package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
- package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
- package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
- package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
- package/dist/v2/index.css.map +0 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { twMerge } from "tailwind-merge";
|
|
10
|
+
|
|
11
|
+
/** Finite-state machine for every recorder implementation */
|
|
12
|
+
export type AudioRecorderState = "idle" | "recording" | "processing";
|
|
13
|
+
|
|
14
|
+
/** Error subclass so callers can `instanceof`-guard recorder failures */
|
|
15
|
+
export class AudioRecorderError extends Error {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "AudioRecorderError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AudioRecorderRef {
|
|
23
|
+
state: AudioRecorderState;
|
|
24
|
+
start: () => Promise<void>;
|
|
25
|
+
stop: () => Promise<Blob>;
|
|
26
|
+
dispose: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CopilotChatAudioRecorder = forwardRef<
|
|
30
|
+
AudioRecorderRef,
|
|
31
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
32
|
+
>((props, ref) => {
|
|
33
|
+
const { className, ...divProps } = props;
|
|
34
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
35
|
+
|
|
36
|
+
// Recording state
|
|
37
|
+
const [recorderState, setRecorderState] =
|
|
38
|
+
useState<AudioRecorderState>("idle");
|
|
39
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
40
|
+
const audioChunksRef = useRef<Blob[]>([]);
|
|
41
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
42
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
43
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
44
|
+
const animationIdRef = useRef<number | null>(null);
|
|
45
|
+
|
|
46
|
+
// Amplitude history buffer for scrolling waveform
|
|
47
|
+
const amplitudeHistoryRef = useRef<number[]>([]);
|
|
48
|
+
const frameCountRef = useRef<number>(0);
|
|
49
|
+
const scrollOffsetRef = useRef<number>(0);
|
|
50
|
+
const smoothedAmplitudeRef = useRef<number>(0);
|
|
51
|
+
const fadeOpacityRef = useRef<number>(0);
|
|
52
|
+
|
|
53
|
+
// Clean up all resources
|
|
54
|
+
const cleanup = useCallback(() => {
|
|
55
|
+
if (animationIdRef.current) {
|
|
56
|
+
cancelAnimationFrame(animationIdRef.current);
|
|
57
|
+
animationIdRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
mediaRecorderRef.current &&
|
|
61
|
+
mediaRecorderRef.current.state !== "inactive"
|
|
62
|
+
) {
|
|
63
|
+
try {
|
|
64
|
+
mediaRecorderRef.current.stop();
|
|
65
|
+
} catch {
|
|
66
|
+
// Ignore errors during cleanup
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (streamRef.current) {
|
|
70
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
71
|
+
streamRef.current = null;
|
|
72
|
+
}
|
|
73
|
+
if (audioContextRef.current && audioContextRef.current.state !== "closed") {
|
|
74
|
+
audioContextRef.current.close().catch(() => {
|
|
75
|
+
// Ignore close errors
|
|
76
|
+
});
|
|
77
|
+
audioContextRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
mediaRecorderRef.current = null;
|
|
80
|
+
analyserRef.current = null;
|
|
81
|
+
audioChunksRef.current = [];
|
|
82
|
+
amplitudeHistoryRef.current = [];
|
|
83
|
+
frameCountRef.current = 0;
|
|
84
|
+
scrollOffsetRef.current = 0;
|
|
85
|
+
smoothedAmplitudeRef.current = 0;
|
|
86
|
+
fadeOpacityRef.current = 0;
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// Start recording
|
|
90
|
+
const start = useCallback(async () => {
|
|
91
|
+
if (recorderState !== "idle") {
|
|
92
|
+
throw new AudioRecorderError("Recorder is already active");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Request microphone access
|
|
97
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
98
|
+
streamRef.current = stream;
|
|
99
|
+
|
|
100
|
+
// Set up audio context for visualization
|
|
101
|
+
const audioContext = new AudioContext();
|
|
102
|
+
audioContextRef.current = audioContext;
|
|
103
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
104
|
+
const analyser = audioContext.createAnalyser();
|
|
105
|
+
analyser.fftSize = 2048; // Higher resolution for time-domain waveform
|
|
106
|
+
source.connect(analyser);
|
|
107
|
+
analyserRef.current = analyser;
|
|
108
|
+
|
|
109
|
+
// Determine best MIME type for recording
|
|
110
|
+
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
|
|
111
|
+
? "audio/webm;codecs=opus"
|
|
112
|
+
: MediaRecorder.isTypeSupported("audio/webm")
|
|
113
|
+
? "audio/webm"
|
|
114
|
+
: MediaRecorder.isTypeSupported("audio/mp4")
|
|
115
|
+
? "audio/mp4"
|
|
116
|
+
: "";
|
|
117
|
+
|
|
118
|
+
const options: MediaRecorderOptions = mimeType ? { mimeType } : {};
|
|
119
|
+
const mediaRecorder = new MediaRecorder(stream, options);
|
|
120
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
121
|
+
audioChunksRef.current = [];
|
|
122
|
+
|
|
123
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
124
|
+
if (event.data.size > 0) {
|
|
125
|
+
audioChunksRef.current.push(event.data);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Start recording with timeslice to collect data periodically
|
|
130
|
+
mediaRecorder.start(100);
|
|
131
|
+
setRecorderState("recording");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
cleanup();
|
|
134
|
+
if (error instanceof Error && error.name === "NotAllowedError") {
|
|
135
|
+
throw new AudioRecorderError("Microphone permission denied");
|
|
136
|
+
}
|
|
137
|
+
if (error instanceof Error && error.name === "NotFoundError") {
|
|
138
|
+
throw new AudioRecorderError("No microphone found");
|
|
139
|
+
}
|
|
140
|
+
throw new AudioRecorderError(
|
|
141
|
+
error instanceof Error ? error.message : "Failed to start recording",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}, [recorderState, cleanup]);
|
|
145
|
+
|
|
146
|
+
// Stop recording and return audio blob
|
|
147
|
+
const stop = useCallback((): Promise<Blob> => {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const mediaRecorder = mediaRecorderRef.current;
|
|
150
|
+
if (!mediaRecorder || recorderState !== "recording") {
|
|
151
|
+
reject(new AudioRecorderError("No active recording"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setRecorderState("processing");
|
|
156
|
+
|
|
157
|
+
mediaRecorder.onstop = () => {
|
|
158
|
+
const mimeType = mediaRecorder.mimeType || "audio/webm";
|
|
159
|
+
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });
|
|
160
|
+
|
|
161
|
+
// Clean up but keep the blob
|
|
162
|
+
cleanup();
|
|
163
|
+
setRecorderState("idle");
|
|
164
|
+
resolve(audioBlob);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
mediaRecorder.onerror = () => {
|
|
168
|
+
cleanup();
|
|
169
|
+
setRecorderState("idle");
|
|
170
|
+
reject(new AudioRecorderError("Recording failed"));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
mediaRecorder.stop();
|
|
174
|
+
});
|
|
175
|
+
}, [recorderState, cleanup]);
|
|
176
|
+
|
|
177
|
+
// Calculate RMS amplitude from time-domain data
|
|
178
|
+
const calculateAmplitude = (dataArray: Uint8Array): number => {
|
|
179
|
+
let sum = 0;
|
|
180
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
181
|
+
// Normalize to -1 to 1 range (128 is center/silence)
|
|
182
|
+
const sample = (dataArray[i] ?? 128) / 128 - 1;
|
|
183
|
+
sum += sample * sample;
|
|
184
|
+
}
|
|
185
|
+
return Math.sqrt(sum / dataArray.length);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Canvas rendering with animation
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const canvas = canvasRef.current;
|
|
191
|
+
if (!canvas) return;
|
|
192
|
+
|
|
193
|
+
const ctx = canvas.getContext("2d");
|
|
194
|
+
if (!ctx) return;
|
|
195
|
+
|
|
196
|
+
// Configuration
|
|
197
|
+
const barWidth = 2;
|
|
198
|
+
const barGap = 1;
|
|
199
|
+
const barSpacing = barWidth + barGap;
|
|
200
|
+
const scrollSpeed = 1 / 3; // Pixels per frame
|
|
201
|
+
|
|
202
|
+
const draw = () => {
|
|
203
|
+
const rect = canvas.getBoundingClientRect();
|
|
204
|
+
const dpr = window.devicePixelRatio || 1;
|
|
205
|
+
|
|
206
|
+
// Update canvas dimensions if container resized
|
|
207
|
+
if (
|
|
208
|
+
canvas.width !== rect.width * dpr ||
|
|
209
|
+
canvas.height !== rect.height * dpr
|
|
210
|
+
) {
|
|
211
|
+
canvas.width = rect.width * dpr;
|
|
212
|
+
canvas.height = rect.height * dpr;
|
|
213
|
+
ctx.scale(dpr, dpr);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Calculate how many bars fit in the canvas (plus extra for smooth scrolling)
|
|
217
|
+
const maxBars = Math.floor(rect.width / barSpacing) + 2;
|
|
218
|
+
|
|
219
|
+
// Get current amplitude if recording
|
|
220
|
+
if (analyserRef.current && recorderState === "recording") {
|
|
221
|
+
// Pre-fill history with zeros on first frame so line is visible immediately
|
|
222
|
+
if (amplitudeHistoryRef.current.length === 0) {
|
|
223
|
+
amplitudeHistoryRef.current = new Array(maxBars).fill(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fade in the waveform smoothly
|
|
227
|
+
if (fadeOpacityRef.current < 1) {
|
|
228
|
+
fadeOpacityRef.current = Math.min(1, fadeOpacityRef.current + 0.03);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Smooth scrolling - increment offset every frame
|
|
232
|
+
scrollOffsetRef.current += scrollSpeed;
|
|
233
|
+
|
|
234
|
+
// Sample amplitude every frame for smoothing
|
|
235
|
+
const bufferLength = analyserRef.current.fftSize;
|
|
236
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
237
|
+
analyserRef.current.getByteTimeDomainData(dataArray);
|
|
238
|
+
const rawAmplitude = calculateAmplitude(dataArray);
|
|
239
|
+
|
|
240
|
+
// Smoothing: gradual attack and decay
|
|
241
|
+
const attackSpeed = 0.12; // Smooth rise
|
|
242
|
+
const decaySpeed = 0.08; // Smooth fade out
|
|
243
|
+
const speed =
|
|
244
|
+
rawAmplitude > smoothedAmplitudeRef.current
|
|
245
|
+
? attackSpeed
|
|
246
|
+
: decaySpeed;
|
|
247
|
+
smoothedAmplitudeRef.current +=
|
|
248
|
+
(rawAmplitude - smoothedAmplitudeRef.current) * speed;
|
|
249
|
+
|
|
250
|
+
// When offset reaches a full bar width, add a new sample and reset offset
|
|
251
|
+
if (scrollOffsetRef.current >= barSpacing) {
|
|
252
|
+
scrollOffsetRef.current -= barSpacing;
|
|
253
|
+
amplitudeHistoryRef.current.push(smoothedAmplitudeRef.current);
|
|
254
|
+
|
|
255
|
+
// Trim history to fit canvas
|
|
256
|
+
if (amplitudeHistoryRef.current.length > maxBars) {
|
|
257
|
+
amplitudeHistoryRef.current =
|
|
258
|
+
amplitudeHistoryRef.current.slice(-maxBars);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Clear canvas
|
|
264
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
265
|
+
|
|
266
|
+
// Get current foreground color
|
|
267
|
+
const computedStyle = getComputedStyle(canvas);
|
|
268
|
+
ctx.fillStyle = computedStyle.color;
|
|
269
|
+
ctx.globalAlpha = fadeOpacityRef.current;
|
|
270
|
+
|
|
271
|
+
const centerY = rect.height / 2;
|
|
272
|
+
const maxAmplitude = rect.height / 2 - 2; // Leave some padding
|
|
273
|
+
|
|
274
|
+
const history = amplitudeHistoryRef.current;
|
|
275
|
+
|
|
276
|
+
// Only draw when recording (history has data)
|
|
277
|
+
if (history.length > 0) {
|
|
278
|
+
const offset = scrollOffsetRef.current;
|
|
279
|
+
const edgeFadeWidth = 12; // Pixels to fade at each edge
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < history.length; i++) {
|
|
282
|
+
const amplitude = history[i] ?? 0;
|
|
283
|
+
// Scale amplitude (RMS is typically 0-0.5 for normal speech)
|
|
284
|
+
const scaledAmplitude = Math.min(amplitude * 4, 1);
|
|
285
|
+
const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);
|
|
286
|
+
|
|
287
|
+
// Position: right-aligned with smooth scroll offset
|
|
288
|
+
const x = rect.width - (history.length - i) * barSpacing - offset;
|
|
289
|
+
const y = centerY - barHeight / 2;
|
|
290
|
+
|
|
291
|
+
// Only draw if visible
|
|
292
|
+
if (x + barWidth > 0 && x < rect.width) {
|
|
293
|
+
// Calculate edge fade opacity
|
|
294
|
+
let edgeOpacity = 1;
|
|
295
|
+
if (x < edgeFadeWidth) {
|
|
296
|
+
// Fade out on left edge
|
|
297
|
+
edgeOpacity = Math.max(0, x / edgeFadeWidth);
|
|
298
|
+
} else if (x > rect.width - edgeFadeWidth) {
|
|
299
|
+
// Fade in on right edge
|
|
300
|
+
edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
ctx.globalAlpha = fadeOpacityRef.current * edgeOpacity;
|
|
304
|
+
ctx.fillRect(x, y, barWidth, barHeight);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
animationIdRef.current = requestAnimationFrame(draw);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
draw();
|
|
313
|
+
|
|
314
|
+
return () => {
|
|
315
|
+
if (animationIdRef.current) {
|
|
316
|
+
cancelAnimationFrame(animationIdRef.current);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}, [recorderState]);
|
|
320
|
+
|
|
321
|
+
// Cleanup on unmount
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
return cleanup;
|
|
324
|
+
}, [cleanup]);
|
|
325
|
+
|
|
326
|
+
// Expose AudioRecorder API via ref
|
|
327
|
+
useImperativeHandle(
|
|
328
|
+
ref,
|
|
329
|
+
() => ({
|
|
330
|
+
get state() {
|
|
331
|
+
return recorderState;
|
|
332
|
+
},
|
|
333
|
+
start,
|
|
334
|
+
stop,
|
|
335
|
+
dispose: cleanup,
|
|
336
|
+
}),
|
|
337
|
+
[recorderState, start, stop, cleanup],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div
|
|
342
|
+
className={twMerge("cpk:w-full cpk:py-3 cpk:px-5", className)}
|
|
343
|
+
{...divProps}
|
|
344
|
+
>
|
|
345
|
+
<canvas ref={canvasRef} className="cpk:block cpk:w-full cpk:h-[26px]" />
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
CopilotChatAudioRecorder.displayName = "CopilotChatAudioRecorder";
|