@copilotkit/vue 2.0.0-rc.20 → 2.0.0-rc.21

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 (193) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.mjs +13 -13
  3. package/dist/use-render-activity-message-BAhbpSDh.js +16200 -0
  4. package/dist/use-render-activity-message-BAhbpSDh.js.map +1 -0
  5. package/dist/use-render-activity-message-DJ6q1qHV.cjs +114 -0
  6. package/dist/use-render-activity-message-DJ6q1qHV.cjs.map +1 -0
  7. package/dist/v2/components/A2UIMessageRenderer.d.ts +14 -0
  8. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  9. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts +10 -3
  10. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  11. package/dist/v2/components/CopilotKitInspector.vue.d.ts +5 -1
  12. package/dist/v2/components/CopilotKitInspector.vue.d.ts.map +1 -1
  13. package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts +7 -21
  15. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts.map +1 -1
  16. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +0 -5
  17. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  18. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts +10 -15
  19. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts.map +1 -1
  20. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +14 -138
  21. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -1
  22. package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
  23. package/dist/v2/components/a2ui/utils.d.ts +3 -15
  24. package/dist/v2/components/a2ui/utils.d.ts.map +1 -1
  25. package/dist/v2/components/a2ui.d.ts +6 -1
  26. package/dist/v2/components/a2ui.d.ts.map +1 -1
  27. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  28. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  29. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts +4 -8
  30. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
  31. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  32. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -1
  33. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  34. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -1
  35. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  36. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  37. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
  38. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  39. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  40. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +1 -1
  41. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +1 -1
  42. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  43. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -1
  44. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts +1 -0
  45. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
  46. package/dist/v2/components/chat/audioRecorder.d.ts +3 -4
  47. package/dist/v2/components/chat/audioRecorder.d.ts.map +1 -1
  48. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +3 -2
  49. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
  50. package/dist/v2/components/chat/index.d.ts +6 -6
  51. package/dist/v2/components/chat/tool-call-display-context.d.ts +3 -1
  52. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -1
  53. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -1
  54. package/dist/v2/components/chat/types.d.ts +1 -1
  55. package/dist/v2/components/chat/types.d.ts.map +1 -1
  56. package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -1
  57. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +1 -1
  58. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -1
  59. package/dist/v2/hooks/use-agent-context.d.ts +2 -3
  60. package/dist/v2/hooks/use-agent-context.d.ts.map +1 -1
  61. package/dist/v2/hooks/use-agent.d.ts +4 -1
  62. package/dist/v2/hooks/use-agent.d.ts.map +1 -1
  63. package/dist/v2/hooks/use-attachments.d.ts +7 -0
  64. package/dist/v2/hooks/use-attachments.d.ts.map +1 -1
  65. package/dist/v2/hooks/use-capabilities.d.ts.map +1 -1
  66. package/dist/v2/hooks/use-component.d.ts +1 -0
  67. package/dist/v2/hooks/use-component.d.ts.map +1 -1
  68. package/dist/v2/hooks/use-configure-suggestions.d.ts +4 -9
  69. package/dist/v2/hooks/use-configure-suggestions.d.ts.map +1 -1
  70. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  71. package/dist/v2/hooks/use-human-in-the-loop.d.ts +2 -1
  72. package/dist/v2/hooks/use-human-in-the-loop.d.ts.map +1 -1
  73. package/dist/v2/hooks/use-interrupt.d.ts +0 -1
  74. package/dist/v2/hooks/use-interrupt.d.ts.map +1 -1
  75. package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
  76. package/dist/v2/hooks/use-render-custom-messages.d.ts.map +1 -1
  77. package/dist/v2/hooks/use-suggestions.d.ts.map +1 -1
  78. package/dist/v2/hooks/use-threads.d.ts +0 -8
  79. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  80. package/dist/v2/index.cjs +1 -1
  81. package/dist/v2/index.mjs +1 -1
  82. package/dist/v2/lib/vue-core.d.ts +12 -11
  83. package/dist/v2/lib/vue-core.d.ts.map +1 -1
  84. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  85. package/dist/v2/providers/CopilotKitProvider.types.d.ts +9 -124
  86. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  87. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +9 -10
  88. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  89. package/dist/v2/providers/chat-configuration-store-key.d.ts +8 -0
  90. package/dist/v2/providers/chat-configuration-store-key.d.ts.map +1 -0
  91. package/dist/v2/providers/markdown-renderer.d.ts +3 -1
  92. package/dist/v2/providers/markdown-renderer.d.ts.map +1 -1
  93. package/dist/v2/providers/use-inspector-thread-override.d.ts +5 -0
  94. package/dist/v2/providers/use-inspector-thread-override.d.ts.map +1 -1
  95. package/package.json +8 -8
  96. package/src/v2/__tests__/exports.test.ts +10 -0
  97. package/src/v2/__tests__/mocks/web-inspector.ts +6 -0
  98. package/src/v2/__tests__/utils/test-helpers.ts +34 -4
  99. package/src/v2/components/A2UIMessageRenderer.ts +98 -68
  100. package/src/v2/components/A2UISurfaceActivityRenderer.vue +68 -103
  101. package/src/v2/components/CopilotKitInspector.vue +104 -51
  102. package/src/v2/components/MCPAppsActivityRenderer.ts +75 -201
  103. package/src/v2/components/OpenGenerativeUIRenderer.ts +78 -80
  104. package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +171 -8
  105. package/src/v2/components/__tests__/CopilotKitInspector.test.ts +42 -0
  106. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.ts +9 -2
  107. package/src/v2/components/__tests__/OpenGenerativeUIToolRenderer.test.ts +38 -0
  108. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +1 -6
  109. package/src/v2/components/a2ui/A2UICatalogContext.ts +27 -93
  110. package/src/v2/components/a2ui/A2UIRecoveryStates.ts +127 -327
  111. package/src/v2/components/a2ui/catalog.ts +2 -3
  112. package/src/v2/components/a2ui/utils.ts +13 -67
  113. package/src/v2/components/a2ui.ts +14 -21
  114. package/src/v2/components/chat/CopilotChat.vue +136 -324
  115. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +22 -45
  116. package/src/v2/components/chat/CopilotChatAudioRecorder.vue +19 -246
  117. package/src/v2/components/chat/CopilotChatInput.vue +69 -407
  118. package/src/v2/components/chat/CopilotChatLightbox.vue +10 -31
  119. package/src/v2/components/chat/CopilotChatMessageView.vue +52 -112
  120. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +14 -45
  121. package/src/v2/components/chat/CopilotChatToolCallItem.vue +6 -4
  122. package/src/v2/components/chat/CopilotChatToolCallsView.vue +16 -19
  123. package/src/v2/components/chat/CopilotChatView.vue +28 -23
  124. package/src/v2/components/chat/CopilotPopupViewInternal.vue +21 -97
  125. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -25
  126. package/src/v2/components/chat/CopilotThreadsDrawer.vue +37 -102
  127. package/src/v2/components/chat/StreamingMarkdownDefault.vue +13 -26
  128. package/src/v2/components/chat/__tests__/CopilotChat.test.ts +12 -10
  129. package/src/v2/components/chat/__tests__/CopilotChatAttachmentQueue.test.ts +28 -0
  130. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +56 -0
  131. package/src/v2/components/chat/__tests__/CopilotPopupView.test.ts +19 -0
  132. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +32 -8
  133. package/src/v2/components/chat/__tests__/CopilotSidebarView.test.ts +2 -1
  134. package/src/v2/components/chat/__tests__/StreamingMarkdownDefault.test.ts +54 -0
  135. package/src/v2/components/chat/audioRecorder.ts +4 -7
  136. package/src/v2/components/chat/copilotChatToolCallItemRender.ts +22 -71
  137. package/src/v2/components/chat/tool-call-display-context.ts +6 -1
  138. package/src/v2/components/chat/tool-call-group-parts.ts +13 -33
  139. package/src/v2/components/chat/types.ts +1 -1
  140. package/src/v2/components/chat/use-lightbox.ts +15 -33
  141. package/src/v2/hooks/__tests__/use-agent-error-state.test.ts +5 -3
  142. package/src/v2/hooks/__tests__/use-agent-stability.test.ts +25 -4
  143. package/src/v2/hooks/__tests__/use-agent-throttle.test.ts +5 -1
  144. package/src/v2/hooks/__tests__/use-agent.test.ts +6 -3
  145. package/src/v2/hooks/__tests__/use-component.test.ts +21 -0
  146. package/src/v2/hooks/__tests__/use-configure-suggestions.test.ts +30 -1
  147. package/src/v2/hooks/__tests__/use-default-render-tool.test.ts +10 -10
  148. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +4 -0
  149. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +6 -1
  150. package/src/v2/hooks/__tests__/use-human-in-the-loop.test.ts +187 -12
  151. package/src/v2/hooks/__tests__/use-interrupt.test.ts +97 -0
  152. package/src/v2/hooks/__tests__/use-render-activity-message.test.ts +2 -1
  153. package/src/v2/hooks/__tests__/use-threads.test.ts +48 -6
  154. package/src/v2/hooks/use-agent-context.ts +4 -10
  155. package/src/v2/hooks/use-agent.ts +86 -290
  156. package/src/v2/hooks/use-attachments.ts +42 -166
  157. package/src/v2/hooks/use-capabilities.ts +2 -7
  158. package/src/v2/hooks/use-component.ts +7 -4
  159. package/src/v2/hooks/use-configure-suggestions.ts +28 -218
  160. package/src/v2/hooks/use-default-render-tool.ts +34 -94
  161. package/src/v2/hooks/use-human-in-the-loop.ts +11 -80
  162. package/src/v2/hooks/use-interrupt.ts +29 -308
  163. package/src/v2/hooks/use-render-activity-message.ts +17 -20
  164. package/src/v2/hooks/use-render-custom-messages.ts +20 -63
  165. package/src/v2/hooks/use-suggestions.ts +9 -33
  166. package/src/v2/hooks/use-threads.ts +43 -273
  167. package/src/v2/lib/__tests__/vue-core-renderers.test.ts +42 -0
  168. package/src/v2/lib/vue-core.ts +67 -72
  169. package/src/v2/providers/CopilotChatConfigurationProvider.vue +48 -186
  170. package/src/v2/providers/CopilotKitProvider.types.ts +12 -127
  171. package/src/v2/providers/CopilotKitProvider.vue +188 -402
  172. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +22 -2
  173. package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +19 -1
  174. package/src/v2/providers/__tests__/CopilotKitProvider.config.test.ts +124 -0
  175. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.ts +4 -1
  176. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +45 -2
  177. package/src/v2/providers/chat-configuration-store-key.ts +9 -0
  178. package/src/v2/providers/markdown-renderer.ts +7 -1
  179. package/src/v2/providers/use-inspector-thread-override.ts +22 -80
  180. package/vite.config.ts +5 -1
  181. package/vitest.config.ts +12 -7
  182. package/dist/use-render-activity-message-CsWYyd6r.cjs +0 -91
  183. package/dist/use-render-activity-message-CsWYyd6r.cjs.map +0 -1
  184. package/dist/use-render-activity-message-DntKKNdd.js +0 -17418
  185. package/dist/use-render-activity-message-DntKKNdd.js.map +0 -1
  186. package/dist/v2/components/chat/normalize-auto-scroll.d.ts +0 -3
  187. package/dist/v2/components/chat/normalize-auto-scroll.d.ts.map +0 -1
  188. package/dist/v2/lib/is-mobile-viewport.d.ts +0 -2
  189. package/dist/v2/lib/is-mobile-viewport.d.ts.map +0 -1
  190. package/src/v2/components/__tests__/A2UICatalogContext.test.ts +0 -63
  191. package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +0 -175
  192. package/src/v2/components/chat/normalize-auto-scroll.ts +0 -3
  193. package/src/v2/lib/is-mobile-viewport.ts +0 -5
@@ -6,6 +6,11 @@ import {
6
6
  getSourceUrl,
7
7
  } from "@copilotkit/shared";
8
8
  import type { Attachment } from "@copilotkit/shared";
9
+ import {
10
+ ɵcanPreviewDocument,
11
+ ɵcreateObjectUrlFromSource,
12
+ ɵdecodeTextSource,
13
+ } from "@copilotkit/core";
9
14
  import type { CopilotChatAttachmentQueueProps } from "./types";
10
15
  import { IconPlay } from "../icons";
11
16
  import CopilotChatLightbox from "./CopilotChatLightbox.vue";
@@ -24,6 +29,7 @@ const labels = useChatLabels();
24
29
 
25
30
  const activeLightboxAttachment = ref<Attachment | null>(null);
26
31
  const documentBlobUrl = ref<string | null>(null);
32
+ let revokeDocumentBlobUrl = () => {};
27
33
 
28
34
  const lightboxLabel = computed(() => {
29
35
  const attachment = activeLightboxAttachment.value;
@@ -38,19 +44,13 @@ const lightboxLabel = computed(() => {
38
44
  const isPreviewableDocument = computed(() => {
39
45
  const attachment = activeLightboxAttachment.value;
40
46
  if (!attachment || attachment.type !== "document") return false;
41
- const mimeType = attachment.source.mimeType;
42
- return isPdf(mimeType) || isText(mimeType);
47
+ return ɵcanPreviewDocument(attachment.source.mimeType) !== null;
43
48
  });
44
49
  const decodedTextPreview = computed(() => {
45
50
  const attachment = activeLightboxAttachment.value;
46
51
  if (!attachment || attachment.type !== "document") return null;
47
- const mimeType = attachment.source.mimeType;
48
- if (!isText(mimeType) || attachment.source.type !== "data") return null;
49
- try {
50
- return atob(attachment.source.value);
51
- } catch {
52
- return attachment.source.value;
53
- }
52
+ if (ɵcanPreviewDocument(attachment.source.mimeType) !== "text") return null;
53
+ return ɵdecodeTextSource(attachment.source);
54
54
  });
55
55
  const lightboxDocumentSource = computed(() => {
56
56
  const attachment = activeLightboxAttachment.value;
@@ -62,10 +62,7 @@ const lightboxDocumentSource = computed(() => {
62
62
  watch(
63
63
  activeLightboxAttachment,
64
64
  (next, previous) => {
65
- if (documentBlobUrl.value) {
66
- URL.revokeObjectURL(documentBlobUrl.value);
67
- documentBlobUrl.value = null;
68
- }
65
+ revokeDocumentBlobUrl();
69
66
 
70
67
  if (
71
68
  !next ||
@@ -76,22 +73,22 @@ watch(
76
73
  return;
77
74
  }
78
75
 
79
- const mimeType = next.source.mimeType;
80
- if (!isPdf(mimeType) && !isText(mimeType)) {
76
+ if (ɵcanPreviewDocument(next.source.mimeType) === null) {
81
77
  return;
82
78
  }
83
79
 
84
- const blobUrl = createBlobUrl(next.source.value, mimeType);
85
- documentBlobUrl.value = blobUrl;
80
+ const objectUrl = ɵcreateObjectUrlFromSource(next.source);
81
+ documentBlobUrl.value = objectUrl?.url ?? null;
82
+ revokeDocumentBlobUrl = () => {
83
+ objectUrl?.revoke();
84
+ documentBlobUrl.value = null;
85
+ };
86
86
  },
87
87
  { immediate: true },
88
88
  );
89
89
 
90
90
  onBeforeUnmount(() => {
91
- if (documentBlobUrl.value) {
92
- URL.revokeObjectURL(documentBlobUrl.value);
93
- documentBlobUrl.value = null;
94
- }
91
+ revokeDocumentBlobUrl();
95
92
  });
96
93
 
97
94
  function openLightbox(attachment: Attachment) {
@@ -123,32 +120,12 @@ function cardClassName(attachment: Attachment) {
123
120
  return "cpk-attachment-queue-item-document";
124
121
  }
125
122
 
126
- function isPdf(mimeType: string | undefined): boolean {
127
- return !!mimeType && mimeType.includes("pdf");
128
- }
129
-
130
- function isText(mimeType: string | undefined): boolean {
131
- return !!mimeType && mimeType.startsWith("text/");
123
+ function isPdf(mimeType: string | undefined) {
124
+ return ɵcanPreviewDocument(mimeType) === "pdf";
132
125
  }
133
126
 
134
- function createBlobUrl(
135
- base64Value: string,
136
- mimeType: string | undefined,
137
- ): string | null {
138
- try {
139
- const binary = atob(base64Value);
140
- const bytes = new Uint8Array(binary.length);
141
- for (let i = 0; i < binary.length; i += 1) {
142
- bytes[i] = binary.charCodeAt(i);
143
- }
144
- const blob = new Blob([bytes], {
145
- type: mimeType || "application/octet-stream",
146
- });
147
- return URL.createObjectURL(blob);
148
- } catch (error) {
149
- console.error("[CopilotKit] Failed to decode attachment data:", error);
150
- return null;
151
- }
127
+ function isText(mimeType: string | undefined) {
128
+ return ɵcanPreviewDocument(mimeType) === "text";
152
129
  }
153
130
  </script>
154
131
 
@@ -1,264 +1,37 @@
1
1
  <script setup lang="ts">
2
- import { onBeforeUnmount, ref, useAttrs, watch } from "vue";
3
- import { AudioRecorderError } from "./audioRecorder";
4
- import type { AudioRecorderState } from "./audioRecorder";
2
+ /**
3
+ * Records from the microphone and draws the live level as a scrolling
4
+ * waveform. The recording itself is `ɵAudioRecorder` from
5
+ * `@copilotkit/core`; control it through the exposed ref.
6
+ */
7
+ import { onBeforeUnmount, onMounted, ref, useAttrs } from "vue";
8
+ import { ɵAudioRecorder, ɵcreateWaveformRenderer } from "@copilotkit/core";
5
9
 
6
10
  defineOptions({ inheritAttrs: false });
7
11
 
8
12
  const attrs = useAttrs();
9
13
  const canvasRef = ref<HTMLCanvasElement | null>(null);
10
- const recorderState = ref<AudioRecorderState>("idle");
11
- const mediaRecorderRef = ref<MediaRecorder | null>(null);
12
- const streamRef = ref<MediaStream | null>(null);
13
- const analyserRef = ref<AnalyserNode | null>(null);
14
- const audioContextRef = ref<AudioContext | null>(null);
15
- const animationIdRef = ref<number | null>(null);
16
- const audioChunksRef = ref<Blob[]>([]);
17
- const amplitudeHistoryRef = ref<number[]>([]);
18
- const scrollOffsetRef = ref(0);
19
- const smoothedAmplitudeRef = ref(0);
20
- const fadeOpacityRef = ref(0);
14
+ const recorder = new ɵAudioRecorder();
15
+ let waveform: { dispose: () => void } | undefined;
21
16
 
22
- function cleanup() {
23
- if (animationIdRef.value !== null) {
24
- cancelAnimationFrame(animationIdRef.value);
25
- animationIdRef.value = null;
17
+ onMounted(() => {
18
+ if (canvasRef.value) {
19
+ waveform = ɵcreateWaveformRenderer(canvasRef.value, recorder);
26
20
  }
27
-
28
- const recorder = mediaRecorderRef.value;
29
- if (recorder && recorder.state !== "inactive") {
30
- try {
31
- recorder.stop();
32
- } catch {
33
- // ignore cleanup stop failures
34
- }
35
- }
36
-
37
- if (streamRef.value) {
38
- streamRef.value.getTracks().forEach((track) => track.stop());
39
- streamRef.value = null;
40
- }
41
-
42
- const audioContext = audioContextRef.value;
43
- if (audioContext && audioContext.state !== "closed") {
44
- audioContext.close().catch(() => {
45
- // ignore close errors
46
- });
47
- }
48
-
49
- mediaRecorderRef.value = null;
50
- analyserRef.value = null;
51
- audioContextRef.value = null;
52
- audioChunksRef.value = [];
53
- amplitudeHistoryRef.value = [];
54
- scrollOffsetRef.value = 0;
55
- smoothedAmplitudeRef.value = 0;
56
- fadeOpacityRef.value = 0;
57
- }
58
-
59
- async function start() {
60
- if (recorderState.value !== "idle") {
61
- throw new AudioRecorderError("Recorder is already active");
62
- }
63
-
64
- try {
65
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
66
- streamRef.value = stream;
67
-
68
- const audioContext = new AudioContext();
69
- audioContextRef.value = audioContext;
70
- const source = audioContext.createMediaStreamSource(stream);
71
- const analyser = audioContext.createAnalyser();
72
- analyser.fftSize = 2048;
73
- source.connect(analyser);
74
- analyserRef.value = analyser;
75
-
76
- const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
77
- ? "audio/webm;codecs=opus"
78
- : MediaRecorder.isTypeSupported("audio/webm")
79
- ? "audio/webm"
80
- : MediaRecorder.isTypeSupported("audio/mp4")
81
- ? "audio/mp4"
82
- : "";
83
-
84
- const options = mimeType ? { mimeType } : {};
85
- const recorder = new MediaRecorder(stream, options);
86
- mediaRecorderRef.value = recorder;
87
- audioChunksRef.value = [];
88
-
89
- recorder.ondataavailable = (event) => {
90
- if (event.data.size > 0) {
91
- audioChunksRef.value.push(event.data);
92
- }
93
- };
94
-
95
- recorder.start(100);
96
- recorderState.value = "recording";
97
- } catch (error) {
98
- cleanup();
99
- if (error instanceof Error && error.name === "NotAllowedError") {
100
- throw new AudioRecorderError("Microphone permission denied");
101
- }
102
- if (error instanceof Error && error.name === "NotFoundError") {
103
- throw new AudioRecorderError("No microphone found");
104
- }
105
- throw new AudioRecorderError(
106
- error instanceof Error ? error.message : "Failed to start recording",
107
- );
108
- }
109
- }
110
-
111
- function stop() {
112
- return new Promise<Blob>((resolve, reject) => {
113
- const recorder = mediaRecorderRef.value;
114
- if (!recorder || recorderState.value !== "recording") {
115
- reject(new AudioRecorderError("No active recording"));
116
- return;
117
- }
118
-
119
- recorderState.value = "processing";
120
- recorder.onstop = () => {
121
- const audioBlob = new Blob(audioChunksRef.value, {
122
- type: recorder.mimeType || "audio/webm",
123
- });
124
- cleanup();
125
- recorderState.value = "idle";
126
- resolve(audioBlob);
127
- };
128
- recorder.onerror = () => {
129
- cleanup();
130
- recorderState.value = "idle";
131
- reject(new AudioRecorderError("Recording failed"));
132
- };
133
- recorder.stop();
134
- });
135
- }
136
-
137
- function calculateAmplitude(dataArray: Uint8Array) {
138
- let sum = 0;
139
- for (let index = 0; index < dataArray.length; index += 1) {
140
- const sample = (dataArray[index] ?? 128) / 128 - 1;
141
- sum += sample * sample;
142
- }
143
- return Math.sqrt(sum / dataArray.length);
144
- }
145
-
146
- function drawFrame() {
147
- const canvas = canvasRef.value;
148
- if (!canvas) {
149
- return;
150
- }
151
-
152
- const context = canvas.getContext("2d");
153
- if (!context) {
154
- return;
155
- }
156
-
157
- const rect = canvas.getBoundingClientRect();
158
- const dpr = window.devicePixelRatio || 1;
159
- if (
160
- canvas.width !== rect.width * dpr ||
161
- canvas.height !== rect.height * dpr
162
- ) {
163
- canvas.width = rect.width * dpr;
164
- canvas.height = rect.height * dpr;
165
- context.scale(dpr, dpr);
166
- }
167
-
168
- const barWidth = 2;
169
- const barGap = 1;
170
- const barSpacing = barWidth + barGap;
171
- const maxBars = Math.floor(rect.width / barSpacing) + 2;
172
-
173
- if (analyserRef.value && recorderState.value === "recording") {
174
- if (amplitudeHistoryRef.value.length === 0) {
175
- amplitudeHistoryRef.value = Array.from({ length: maxBars }, () => 0);
176
- }
177
-
178
- if (fadeOpacityRef.value < 1) {
179
- fadeOpacityRef.value = Math.min(1, fadeOpacityRef.value + 0.03);
180
- }
181
-
182
- scrollOffsetRef.value += 1 / 3;
183
- const bufferLength = analyserRef.value.fftSize;
184
- const dataArray = new Uint8Array(bufferLength);
185
- analyserRef.value.getByteTimeDomainData(dataArray);
186
- const rawAmplitude = calculateAmplitude(dataArray);
187
- const attackSpeed = 0.12;
188
- const decaySpeed = 0.08;
189
- const speed =
190
- rawAmplitude > smoothedAmplitudeRef.value ? attackSpeed : decaySpeed;
191
- smoothedAmplitudeRef.value +=
192
- (rawAmplitude - smoothedAmplitudeRef.value) * speed;
193
-
194
- if (scrollOffsetRef.value >= barSpacing) {
195
- scrollOffsetRef.value -= barSpacing;
196
- amplitudeHistoryRef.value.push(smoothedAmplitudeRef.value);
197
- if (amplitudeHistoryRef.value.length > maxBars) {
198
- amplitudeHistoryRef.value = amplitudeHistoryRef.value.slice(-maxBars);
199
- }
200
- }
201
- }
202
-
203
- context.clearRect(0, 0, rect.width, rect.height);
204
- const computedStyle = window.getComputedStyle(canvas);
205
- context.fillStyle = computedStyle.color;
206
- context.globalAlpha = fadeOpacityRef.value;
207
-
208
- const centerY = rect.height / 2;
209
- const maxAmplitude = rect.height / 2 - 2;
210
- const edgeFadeWidth = 12;
211
-
212
- for (let index = 0; index < amplitudeHistoryRef.value.length; index += 1) {
213
- const amplitude = amplitudeHistoryRef.value[index] ?? 0;
214
- const scaledAmplitude = Math.min(amplitude * 4, 1);
215
- const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);
216
- const x =
217
- rect.width -
218
- (amplitudeHistoryRef.value.length - index) * barSpacing -
219
- scrollOffsetRef.value;
220
- const y = centerY - barHeight / 2;
221
- if (x + barWidth <= 0 || x >= rect.width) {
222
- continue;
223
- }
224
-
225
- let edgeOpacity = 1;
226
- if (x < edgeFadeWidth) {
227
- edgeOpacity = Math.max(0, x / edgeFadeWidth);
228
- } else if (x > rect.width - edgeFadeWidth) {
229
- edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);
230
- }
231
-
232
- context.globalAlpha = fadeOpacityRef.value * edgeOpacity;
233
- context.fillRect(x, y, barWidth, barHeight);
234
- }
235
-
236
- animationIdRef.value = requestAnimationFrame(drawFrame);
237
- }
238
-
239
- watch(
240
- recorderState,
241
- () => {
242
- if (animationIdRef.value !== null) {
243
- cancelAnimationFrame(animationIdRef.value);
244
- animationIdRef.value = null;
245
- }
246
- animationIdRef.value = requestAnimationFrame(drawFrame);
247
- },
248
- { immediate: true },
249
- );
21
+ });
250
22
 
251
23
  onBeforeUnmount(() => {
252
- cleanup();
24
+ waveform?.dispose();
25
+ recorder.dispose();
253
26
  });
254
27
 
255
28
  defineExpose({
256
29
  get state() {
257
- return recorderState.value;
30
+ return recorder.getSnapshot();
258
31
  },
259
- start,
260
- stop,
261
- dispose: cleanup,
32
+ start: () => recorder.start(),
33
+ stop: () => recorder.stop(),
34
+ dispose: () => recorder.dispose(),
262
35
  });
263
36
  </script>
264
37