@aj-archipelago/cortex 1.3.5 → 1.3.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.
Files changed (94) hide show
  1. package/helper-apps/cortex-autogen/agents.py +31 -2
  2. package/helper-apps/cortex-realtime-voice-server/.env.sample +6 -0
  3. package/helper-apps/cortex-realtime-voice-server/README.md +22 -0
  4. package/helper-apps/cortex-realtime-voice-server/bun.lockb +0 -0
  5. package/helper-apps/cortex-realtime-voice-server/client/bun.lockb +0 -0
  6. package/helper-apps/cortex-realtime-voice-server/client/index.html +12 -0
  7. package/helper-apps/cortex-realtime-voice-server/client/package.json +65 -0
  8. package/helper-apps/cortex-realtime-voice-server/client/postcss.config.js +6 -0
  9. package/helper-apps/cortex-realtime-voice-server/client/public/favicon.ico +0 -0
  10. package/helper-apps/cortex-realtime-voice-server/client/public/index.html +43 -0
  11. package/helper-apps/cortex-realtime-voice-server/client/public/logo192.png +0 -0
  12. package/helper-apps/cortex-realtime-voice-server/client/public/logo512.png +0 -0
  13. package/helper-apps/cortex-realtime-voice-server/client/public/manifest.json +25 -0
  14. package/helper-apps/cortex-realtime-voice-server/client/public/robots.txt +3 -0
  15. package/helper-apps/cortex-realtime-voice-server/client/public/sounds/connect.mp3 +0 -0
  16. package/helper-apps/cortex-realtime-voice-server/client/public/sounds/disconnect.mp3 +0 -0
  17. package/helper-apps/cortex-realtime-voice-server/client/src/App.test.tsx +9 -0
  18. package/helper-apps/cortex-realtime-voice-server/client/src/App.tsx +126 -0
  19. package/helper-apps/cortex-realtime-voice-server/client/src/SettingsModal.tsx +207 -0
  20. package/helper-apps/cortex-realtime-voice-server/client/src/chat/Chat.tsx +553 -0
  21. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubble.tsx +22 -0
  22. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleLeft.tsx +22 -0
  23. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleRight.tsx +21 -0
  24. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessage.tsx +27 -0
  25. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessageInput.tsx +74 -0
  26. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatTile.tsx +211 -0
  27. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/SoundEffects.ts +56 -0
  28. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavPacker.ts +112 -0
  29. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavRecorder.ts +571 -0
  30. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavStreamPlayer.ts +290 -0
  31. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/AudioAnalysis.ts +186 -0
  32. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/constants.ts +59 -0
  33. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts +214 -0
  34. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts +183 -0
  35. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx +151 -0
  36. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/CopyButton.tsx +32 -0
  37. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ImageOverlay.tsx +166 -0
  38. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx +95 -0
  39. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx +116 -0
  40. package/helper-apps/cortex-realtime-voice-server/client/src/chat/hooks/useWindowResize.ts +27 -0
  41. package/helper-apps/cortex-realtime-voice-server/client/src/chat/utils/audio.ts +33 -0
  42. package/helper-apps/cortex-realtime-voice-server/client/src/index.css +20 -0
  43. package/helper-apps/cortex-realtime-voice-server/client/src/index.tsx +19 -0
  44. package/helper-apps/cortex-realtime-voice-server/client/src/logo.svg +1 -0
  45. package/helper-apps/cortex-realtime-voice-server/client/src/react-app-env.d.ts +1 -0
  46. package/helper-apps/cortex-realtime-voice-server/client/src/reportWebVitals.ts +15 -0
  47. package/helper-apps/cortex-realtime-voice-server/client/src/setupTests.ts +5 -0
  48. package/helper-apps/cortex-realtime-voice-server/client/src/utils/logger.ts +45 -0
  49. package/helper-apps/cortex-realtime-voice-server/client/tailwind.config.js +14 -0
  50. package/helper-apps/cortex-realtime-voice-server/client/tsconfig.json +30 -0
  51. package/helper-apps/cortex-realtime-voice-server/client/vite.config.ts +22 -0
  52. package/helper-apps/cortex-realtime-voice-server/index.ts +19 -0
  53. package/helper-apps/cortex-realtime-voice-server/package.json +28 -0
  54. package/helper-apps/cortex-realtime-voice-server/src/ApiServer.ts +35 -0
  55. package/helper-apps/cortex-realtime-voice-server/src/SocketServer.ts +737 -0
  56. package/helper-apps/cortex-realtime-voice-server/src/Tools.ts +520 -0
  57. package/helper-apps/cortex-realtime-voice-server/src/cortex/expert.ts +29 -0
  58. package/helper-apps/cortex-realtime-voice-server/src/cortex/image.ts +29 -0
  59. package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +91 -0
  60. package/helper-apps/cortex-realtime-voice-server/src/cortex/reason.ts +29 -0
  61. package/helper-apps/cortex-realtime-voice-server/src/cortex/search.ts +30 -0
  62. package/helper-apps/cortex-realtime-voice-server/src/cortex/style.ts +31 -0
  63. package/helper-apps/cortex-realtime-voice-server/src/cortex/utils.ts +95 -0
  64. package/helper-apps/cortex-realtime-voice-server/src/cortex/vision.ts +34 -0
  65. package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +499 -0
  66. package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +279 -0
  67. package/helper-apps/cortex-realtime-voice-server/src/realtime/socket.ts +27 -0
  68. package/helper-apps/cortex-realtime-voice-server/src/realtime/transcription.ts +75 -0
  69. package/helper-apps/cortex-realtime-voice-server/src/realtime/utils.ts +33 -0
  70. package/helper-apps/cortex-realtime-voice-server/src/utils/logger.ts +45 -0
  71. package/helper-apps/cortex-realtime-voice-server/src/utils/prompt.ts +81 -0
  72. package/helper-apps/cortex-realtime-voice-server/tsconfig.json +28 -0
  73. package/package.json +1 -1
  74. package/pathways/basePathway.js +3 -1
  75. package/pathways/system/entity/memory/sys_memory_manager.js +3 -0
  76. package/pathways/system/entity/memory/sys_memory_update.js +44 -45
  77. package/pathways/system/entity/memory/sys_read_memory.js +86 -6
  78. package/pathways/system/entity/memory/sys_search_memory.js +66 -0
  79. package/pathways/system/entity/shared/sys_entity_constants.js +2 -2
  80. package/pathways/system/entity/sys_entity_continue.js +2 -1
  81. package/pathways/system/entity/sys_entity_start.js +10 -0
  82. package/pathways/system/entity/sys_generator_expert.js +0 -2
  83. package/pathways/system/entity/sys_generator_memory.js +31 -0
  84. package/pathways/system/entity/sys_generator_voice_sample.js +36 -0
  85. package/pathways/system/entity/sys_router_tool.js +13 -10
  86. package/pathways/system/sys_parse_numbered_object_list.js +1 -1
  87. package/server/pathwayResolver.js +41 -31
  88. package/server/plugins/azureVideoTranslatePlugin.js +28 -16
  89. package/server/plugins/claude3VertexPlugin.js +0 -9
  90. package/server/plugins/gemini15ChatPlugin.js +18 -5
  91. package/server/plugins/modelPlugin.js +27 -6
  92. package/server/plugins/openAiChatPlugin.js +10 -8
  93. package/server/plugins/openAiVisionPlugin.js +56 -0
  94. package/tests/memoryfunction.test.js +73 -1
@@ -0,0 +1,214 @@
1
+ const AudioProcessorWorklet = `
2
+ class AudioProcessor extends AudioWorkletProcessor {
3
+
4
+ constructor() {
5
+ super();
6
+ this.port.onmessage = this.receive.bind(this);
7
+ this.initialize();
8
+ }
9
+
10
+ initialize() {
11
+ this.foundAudio = false;
12
+ this.recording = false;
13
+ this.chunks = [];
14
+ }
15
+
16
+ /**
17
+ * Concatenates sampled chunks into channels
18
+ * Format is chunk[Left[], Right[]]
19
+ */
20
+ readChannelData(chunks, channel = -1, maxChannels = 9) {
21
+ let channelLimit;
22
+ if (channel !== -1) {
23
+ if (chunks[0] && chunks[0].length - 1 < channel) {
24
+ throw new Error(
25
+ \`Channel \${channel} out of range: max \${chunks[0].length}\`
26
+ );
27
+ }
28
+ channelLimit = channel + 1;
29
+ } else {
30
+ channel = 0;
31
+ channelLimit = Math.min(chunks[0] ? chunks[0].length : 1, maxChannels);
32
+ }
33
+ const channels = [];
34
+ for (let n = channel; n < channelLimit; n++) {
35
+ const length = chunks.reduce((sum, chunk) => {
36
+ return sum + chunk[n].length;
37
+ }, 0);
38
+ const buffers = chunks.map((chunk) => chunk[n]);
39
+ const result = new Float32Array(length);
40
+ let offset = 0;
41
+ for (let i = 0; i < buffers.length; i++) {
42
+ result.set(buffers[i], offset);
43
+ offset += buffers[i].length;
44
+ }
45
+ channels[n] = result;
46
+ }
47
+ return channels;
48
+ }
49
+
50
+ /**
51
+ * Combines parallel audio data into correct format,
52
+ * channels[Left[], Right[]] to float32Array[LRLRLRLR...]
53
+ */
54
+ formatAudioData(channels) {
55
+ if (channels.length === 1) {
56
+ // Simple case is only one channel
57
+ const float32Array = channels[0].slice();
58
+ const meanValues = channels[0].slice();
59
+ return { float32Array, meanValues };
60
+ } else {
61
+ const float32Array = new Float32Array(
62
+ channels[0].length * channels.length
63
+ );
64
+ const meanValues = new Float32Array(channels[0].length);
65
+ for (let i = 0; i < channels[0].length; i++) {
66
+ const offset = i * channels.length;
67
+ let meanValue = 0;
68
+ for (let n = 0; n < channels.length; n++) {
69
+ float32Array[offset + n] = channels[n][i];
70
+ meanValue += channels[n][i];
71
+ }
72
+ meanValues[i] = meanValue / channels.length;
73
+ }
74
+ return { float32Array, meanValues };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Converts 32-bit float data to 16-bit integers
80
+ */
81
+ floatTo16BitPCM(float32Array) {
82
+ const buffer = new ArrayBuffer(float32Array.length * 2);
83
+ const view = new DataView(buffer);
84
+ let offset = 0;
85
+ for (let i = 0; i < float32Array.length; i++, offset += 2) {
86
+ let s = Math.max(-1, Math.min(1, float32Array[i]));
87
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
88
+ }
89
+ return buffer;
90
+ }
91
+
92
+ /**
93
+ * Retrieves the most recent amplitude values from the audio stream
94
+ * @param {number} channel
95
+ */
96
+ getValues(channel = -1) {
97
+ const channels = this.readChannelData(this.chunks, channel);
98
+ const { meanValues } = this.formatAudioData(channels);
99
+ return { meanValues, channels };
100
+ }
101
+
102
+ /**
103
+ * Exports chunks as an audio/wav file
104
+ */
105
+ export() {
106
+ const channels = this.readChannelData(this.chunks);
107
+ const { float32Array, meanValues } = this.formatAudioData(channels);
108
+ const audioData = this.floatTo16BitPCM(float32Array);
109
+ return {
110
+ meanValues: meanValues,
111
+ audio: {
112
+ bitsPerSample: 16,
113
+ channels: channels,
114
+ data: audioData,
115
+ },
116
+ };
117
+ }
118
+
119
+ receive(e) {
120
+ const { event, id } = e.data;
121
+ let receiptData = {};
122
+ switch (event) {
123
+ case 'start':
124
+ this.recording = true;
125
+ break;
126
+ case 'stop':
127
+ this.recording = false;
128
+ break;
129
+ case 'clear':
130
+ this.initialize();
131
+ break;
132
+ case 'export':
133
+ receiptData = this.export();
134
+ break;
135
+ case 'read':
136
+ receiptData = this.getValues();
137
+ break;
138
+ default:
139
+ break;
140
+ }
141
+ // Always send back receipt
142
+ this.port.postMessage({ event: 'receipt', id, data: receiptData });
143
+ }
144
+
145
+ sendChunk(chunk) {
146
+ const channels = this.readChannelData([chunk]);
147
+ const { float32Array, meanValues } = this.formatAudioData(channels);
148
+ const rawAudioData = this.floatTo16BitPCM(float32Array);
149
+ const monoAudioData = this.floatTo16BitPCM(meanValues);
150
+ this.port.postMessage({
151
+ event: 'chunk',
152
+ data: {
153
+ mono: monoAudioData,
154
+ raw: rawAudioData,
155
+ },
156
+ });
157
+ }
158
+
159
+ process(inputList, outputList, parameters) {
160
+ // Copy input to output (e.g. speakers)
161
+ // Note that this creates choppy sounds with Mac products
162
+ const sourceLimit = Math.min(inputList.length, outputList.length);
163
+ for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {
164
+ const input = inputList[inputNum];
165
+ const output = outputList[inputNum];
166
+ const channelCount = Math.min(input.length, output.length);
167
+ for (let channelNum = 0; channelNum < channelCount; channelNum++) {
168
+ input[channelNum].forEach((sample, i) => {
169
+ output[channelNum][i] = sample;
170
+ });
171
+ }
172
+ }
173
+ const inputs = inputList[0];
174
+ // There's latency at the beginning of a stream before recording starts
175
+ // Make sure we actually receive audio data before we start storing chunks
176
+ let sliceIndex = 0;
177
+ if (!this.foundAudio) {
178
+ for (const channel of inputs) {
179
+ sliceIndex = 0; // reset for each channel
180
+ if (this.foundAudio) {
181
+ break;
182
+ }
183
+ if (channel) {
184
+ for (const value of channel) {
185
+ if (value !== 0) {
186
+ // find only one non-zero entry in any channel
187
+ this.foundAudio = true;
188
+ break;
189
+ } else {
190
+ sliceIndex++;
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ if (inputs && inputs[0] && this.foundAudio && this.recording) {
197
+ // We need to copy the TypedArray, because the \`process\`
198
+ // internals will reuse the same buffer to hold each input
199
+ const chunk = inputs.map((input) => input.slice(sliceIndex));
200
+ this.chunks.push(chunk);
201
+ this.sendChunk(chunk);
202
+ }
203
+ return true;
204
+ }
205
+ }
206
+
207
+ registerProcessor('audio_processor', AudioProcessor);
208
+ `;
209
+
210
+ const script = new Blob([AudioProcessorWorklet], {
211
+ type: 'application/javascript',
212
+ });
213
+ const src = URL.createObjectURL(script);
214
+ export const AudioProcessorSrc: string = src;
@@ -0,0 +1,183 @@
1
+ export const StreamProcessorWorklet = `
2
+ class StreamProcessor extends AudioWorkletProcessor {
3
+ constructor() {
4
+ super();
5
+ this.hasStarted = false;
6
+ this.hasInterrupted = false;
7
+ this.outputBuffers = [];
8
+ this.bufferLength = 128;
9
+ this.minBufferSize = 3;
10
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId: null };
11
+ this.writeOffset = 0;
12
+ this.trackSampleOffsets = {};
13
+ this.lastErrorTime = 0;
14
+ this.errorCount = 0;
15
+ this.noBufferCount = 0;
16
+ this.maxNoBufferFrames = 100;
17
+ this.lastUnderrunLog = 0;
18
+
19
+ this.port.onmessage = (event) => {
20
+ try {
21
+ if (event.data) {
22
+ const payload = event.data;
23
+ if (payload.event === 'write') {
24
+ const int16Array = payload.buffer;
25
+ const float32Array = new Float32Array(int16Array.length);
26
+ for (let i = 0; i < int16Array.length; i++) {
27
+ float32Array[i] = int16Array[i] / 0x8000;
28
+ }
29
+ this.writeData(float32Array, payload.trackId);
30
+ } else if (payload.event === 'config') {
31
+ this.minBufferSize = payload.minBufferSize;
32
+ } else if (
33
+ payload.event === 'offset' ||
34
+ payload.event === 'interrupt'
35
+ ) {
36
+ const requestId = payload.requestId;
37
+ const trackId = this.write.trackId;
38
+ const offset = this.trackSampleOffsets[trackId] || 0;
39
+ this.port.postMessage({
40
+ event: 'offset',
41
+ requestId,
42
+ trackId,
43
+ offset,
44
+ });
45
+ if (payload.event === 'interrupt') {
46
+ this.hasInterrupted = true;
47
+ }
48
+ } else {
49
+ throw new Error(\`Unhandled event "\${payload.event}"\`);
50
+ }
51
+ }
52
+ } catch (error) {
53
+ this.handleError(error);
54
+ }
55
+ };
56
+ }
57
+
58
+ handleError(error) {
59
+ const now = currentTime;
60
+ if (now - this.lastErrorTime > 5) {
61
+ // Reset error count if more than 5 seconds have passed
62
+ this.errorCount = 0;
63
+ }
64
+ this.lastErrorTime = now;
65
+ this.errorCount++;
66
+
67
+ if (this.errorCount <= 3) {
68
+ this.port.postMessage({
69
+ event: 'error',
70
+ error: error.message || 'Unknown error in stream processor'
71
+ });
72
+ }
73
+ }
74
+
75
+ writeData(float32Array, trackId = null) {
76
+ try {
77
+ let { buffer } = this.write;
78
+ let offset = this.writeOffset;
79
+
80
+ for (let i = 0; i < float32Array.length; i++) {
81
+ buffer[offset++] = float32Array[i];
82
+ if (offset >= buffer.length) {
83
+ this.outputBuffers.push(this.write);
84
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId };
85
+ buffer = this.write.buffer;
86
+ offset = 0;
87
+ }
88
+ }
89
+
90
+ // If we have a partial buffer at the end, push it too
91
+ if (offset > 0) {
92
+ const finalBuffer = new Float32Array(this.bufferLength);
93
+ finalBuffer.set(buffer.subarray(0, offset));
94
+ this.outputBuffers.push({ buffer: finalBuffer, trackId });
95
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId };
96
+ this.writeOffset = 0;
97
+ } else {
98
+ this.writeOffset = offset;
99
+ }
100
+
101
+ this.noBufferCount = 0;
102
+ return true;
103
+ } catch (error) {
104
+ this.handleError(error);
105
+ return false;
106
+ }
107
+ }
108
+
109
+ process(inputs, outputs, parameters) {
110
+ try {
111
+ const output = outputs[0];
112
+ const outputChannelData = output[0];
113
+ const outputBuffers = this.outputBuffers;
114
+
115
+ if (this.hasInterrupted) {
116
+ this.port.postMessage({ event: 'stop' });
117
+ return false;
118
+ } else if (!this.hasStarted && outputBuffers.length < this.minBufferSize) {
119
+ // Wait for more buffers before starting
120
+ outputChannelData.fill(0);
121
+ return true;
122
+ } else if (outputBuffers.length > 0) {
123
+ this.hasStarted = true;
124
+ this.noBufferCount = 0;
125
+ this.lastUnderrunLog = 0;
126
+
127
+ const { buffer, trackId } = outputBuffers.shift();
128
+ outputChannelData.set(buffer);
129
+
130
+ if (trackId) {
131
+ this.trackSampleOffsets[trackId] =
132
+ this.trackSampleOffsets[trackId] || 0;
133
+ this.trackSampleOffsets[trackId] += buffer.length;
134
+
135
+ // If this was the last buffer for this track, notify completion
136
+ if (outputBuffers.length === 0 || outputBuffers[0].trackId !== trackId) {
137
+ this.port.postMessage({
138
+ event: 'track_complete',
139
+ trackId,
140
+ finalOffset: this.trackSampleOffsets[trackId]
141
+ });
142
+ }
143
+ }
144
+ return true;
145
+ } else if (this.hasStarted) {
146
+ this.noBufferCount++;
147
+
148
+ if (this.noBufferCount >= 5 && currentTime - this.lastUnderrunLog > 1) {
149
+ this.port.postMessage({
150
+ event: 'underrun',
151
+ count: this.noBufferCount,
152
+ bufferSize: this.outputBuffers.length,
153
+ maxBuffers: this.maxNoBufferFrames
154
+ });
155
+ this.lastUnderrunLog = currentTime;
156
+ }
157
+
158
+ if (this.noBufferCount >= this.maxNoBufferFrames) {
159
+ this.port.postMessage({
160
+ event: 'stop',
161
+ reason: 'max_underruns_reached',
162
+ finalCount: this.noBufferCount
163
+ });
164
+ return false;
165
+ }
166
+ outputChannelData.fill(0);
167
+ }
168
+ return true;
169
+ } catch (error) {
170
+ this.handleError(error);
171
+ return true;
172
+ }
173
+ }
174
+ }
175
+
176
+ registerProcessor('stream_processor', StreamProcessor);
177
+ `;
178
+
179
+ const script = new Blob([StreamProcessorWorklet], {
180
+ type: 'application/javascript',
181
+ });
182
+ const src = URL.createObjectURL(script);
183
+ export const StreamProcessorSrc = src;
@@ -0,0 +1,151 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ type AudioVisualizerProps = {
4
+ audioContext: AudioContext | null;
5
+ analyserNode: AnalyserNode | null;
6
+ width?: number;
7
+ height?: number;
8
+ };
9
+
10
+ export function AudioVisualizer({
11
+ audioContext,
12
+ analyserNode,
13
+ width = 300,
14
+ height = 300
15
+ }: AudioVisualizerProps) {
16
+ const canvasRef = useRef<HTMLCanvasElement>(null);
17
+ const rotationRef = useRef(0);
18
+ const colorShiftRef = useRef(0);
19
+ const animationFrameRef = useRef<number>();
20
+
21
+ useEffect(() => {
22
+ if (!audioContext || !analyserNode || !canvasRef.current) return;
23
+
24
+ // Update canvas size when width/height props change
25
+ if (canvasRef.current) {
26
+ canvasRef.current.width = width;
27
+ canvasRef.current.height = height;
28
+ }
29
+
30
+ const draw = () => {
31
+ const canvas = canvasRef.current;
32
+ if (!canvas || !analyserNode) return;
33
+
34
+ const ctx = canvas.getContext('2d');
35
+ if (!ctx) return;
36
+
37
+ const bufferLength = analyserNode.frequencyBinCount;
38
+ const dataArray = new Uint8Array(bufferLength);
39
+ analyserNode.getByteFrequencyData(dataArray);
40
+
41
+ // Clear with fade effect
42
+ ctx.fillStyle = 'rgba(17, 24, 39, 0.3)';
43
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
44
+
45
+ const centerX = canvas.width / 2;
46
+ const centerY = canvas.height / 2;
47
+ const maxRadius = Math.min(centerX, centerY) - 10;
48
+
49
+ // Draw outer circle
50
+ ctx.beginPath();
51
+ ctx.strokeStyle = `hsl(${210 + Math.sin(colorShiftRef.current) * 20}, 80%, 60%)`;
52
+ ctx.lineWidth = 2;
53
+ ctx.arc(centerX, centerY, maxRadius, 0, Math.PI * 2);
54
+ ctx.stroke();
55
+
56
+ // Define base colors with shifting hues
57
+ const baseHue = 210 + Math.sin(colorShiftRef.current) * 20;
58
+ const waveforms = [
59
+ {
60
+ baseRadius: maxRadius * 0.4,
61
+ color: `hsl(${baseHue}, 90%, 70%)`,
62
+ gradientColors: [`hsla(${baseHue}, 90%, 70%, 0.3)`, `hsla(${baseHue}, 90%, 50%, 0)`],
63
+ rotation: rotationRef.current
64
+ },
65
+ {
66
+ baseRadius: maxRadius * 0.6,
67
+ color: `hsl(${baseHue + 10}, 85%, 60%)`,
68
+ gradientColors: [`hsla(${baseHue + 10}, 85%, 60%, 0.3)`, `hsla(${baseHue + 10}, 85%, 40%, 0)`],
69
+ rotation: rotationRef.current + (Math.PI * 2 / 3)
70
+ },
71
+ {
72
+ baseRadius: maxRadius * 0.8,
73
+ color: `hsl(${baseHue + 20}, 80%, 50%)`,
74
+ gradientColors: [`hsla(${baseHue + 20}, 80%, 50%, 0.3)`, `hsla(${baseHue + 20}, 80%, 30%, 0)`],
75
+ rotation: rotationRef.current + (Math.PI * 4 / 3)
76
+ }
77
+ ];
78
+
79
+ waveforms.forEach(({ baseRadius, color, gradientColors, rotation }) => {
80
+ const points: [number, number][] = [];
81
+
82
+ for (let i = 0; i <= bufferLength; i++) {
83
+ const amplitude = dataArray[i % bufferLength] / 255.0;
84
+ const angle = (i * 2 * Math.PI) / bufferLength + rotation;
85
+
86
+ const radius = baseRadius + (maxRadius * 0.4 * amplitude);
87
+ const x = centerX + Math.cos(angle) * radius;
88
+ const y = centerY + Math.sin(angle) * radius;
89
+
90
+ points.push([x, y]);
91
+ }
92
+
93
+ // Create gradient for fill
94
+ const gradient = ctx.createRadialGradient(
95
+ centerX, centerY, baseRadius * 0.8,
96
+ centerX, centerY, baseRadius * 1.2
97
+ );
98
+ gradient.addColorStop(0, gradientColors[0]);
99
+ gradient.addColorStop(1, gradientColors[1]);
100
+
101
+ ctx.beginPath();
102
+ ctx.moveTo(centerX, centerY);
103
+ points.forEach(([x, y]) => {
104
+ ctx.lineTo(x, y);
105
+ });
106
+ ctx.closePath();
107
+ ctx.fillStyle = gradient;
108
+ ctx.fill();
109
+
110
+ ctx.beginPath();
111
+ points.forEach(([x, y], i) => {
112
+ if (i === 0) ctx.moveTo(x, y);
113
+ else ctx.lineTo(x, y);
114
+ });
115
+ ctx.strokeStyle = color;
116
+ ctx.lineWidth = 2;
117
+ ctx.closePath();
118
+ ctx.stroke();
119
+
120
+ ctx.shadowBlur = 15;
121
+ ctx.shadowColor = color;
122
+ });
123
+
124
+ rotationRef.current += 0.002;
125
+ colorShiftRef.current += 0.005;
126
+
127
+ animationFrameRef.current = requestAnimationFrame(draw);
128
+ };
129
+
130
+ draw();
131
+
132
+ return () => {
133
+ if (animationFrameRef.current) {
134
+ cancelAnimationFrame(animationFrameRef.current);
135
+ }
136
+ };
137
+ }, [audioContext, analyserNode, width, height]);
138
+
139
+ return (
140
+ <div className="w-full h-full flex items-center justify-center pointer-events-none">
141
+ <div className="aspect-square w-full max-h-full">
142
+ <canvas
143
+ ref={canvasRef}
144
+ width={width}
145
+ height={height}
146
+ className="bg-gray-900 rounded-lg w-full h-full object-contain pointer-events-none"
147
+ />
148
+ </div>
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,32 @@
1
+ import React, { useState } from 'react';
2
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
3
+ import CheckIcon from '@mui/icons-material/Check';
4
+
5
+ interface CopyButtonProps {
6
+ text: string;
7
+ className?: string;
8
+ }
9
+
10
+ export const CopyButton: React.FC<CopyButtonProps> = ({ text, className = '' }) => {
11
+ const [copied, setCopied] = useState(false);
12
+
13
+ const handleCopy = async () => {
14
+ await navigator.clipboard.writeText(text);
15
+ setCopied(true);
16
+ setTimeout(() => setCopied(false), 2000);
17
+ };
18
+
19
+ return (
20
+ <button
21
+ onClick={handleCopy}
22
+ className={`p-1 text-gray-400 hover:text-cyan-400 transition-colors duration-200 ${className}`}
23
+ title="Copy to clipboard"
24
+ >
25
+ {copied ? (
26
+ <CheckIcon sx={{ fontSize: 16 }} />
27
+ ) : (
28
+ <ContentCopyIcon sx={{ fontSize: 16 }} />
29
+ )}
30
+ </button>
31
+ );
32
+ };