@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.
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +13 -13
- package/dist/use-render-activity-message-BAhbpSDh.js +16200 -0
- package/dist/use-render-activity-message-BAhbpSDh.js.map +1 -0
- package/dist/use-render-activity-message-DJ6q1qHV.cjs +114 -0
- package/dist/use-render-activity-message-DJ6q1qHV.cjs.map +1 -0
- package/dist/v2/components/A2UIMessageRenderer.d.ts +14 -0
- package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
- package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts +10 -3
- package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
- package/dist/v2/components/CopilotKitInspector.vue.d.ts +5 -1
- package/dist/v2/components/CopilotKitInspector.vue.d.ts.map +1 -1
- package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
- package/dist/v2/components/OpenGenerativeUIRenderer.d.ts +7 -21
- package/dist/v2/components/OpenGenerativeUIRenderer.d.ts.map +1 -1
- package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +0 -5
- package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
- package/dist/v2/components/a2ui/A2UICatalogContext.d.ts +10 -15
- package/dist/v2/components/a2ui/A2UICatalogContext.d.ts.map +1 -1
- package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +14 -138
- package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -1
- package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
- package/dist/v2/components/a2ui/utils.d.ts +3 -15
- package/dist/v2/components/a2ui/utils.d.ts.map +1 -1
- package/dist/v2/components/a2ui.d.ts +6 -1
- package/dist/v2/components/a2ui.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts +4 -8
- package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts +1 -0
- package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/audioRecorder.d.ts +3 -4
- package/dist/v2/components/chat/audioRecorder.d.ts.map +1 -1
- package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +3 -2
- package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
- package/dist/v2/components/chat/index.d.ts +6 -6
- package/dist/v2/components/chat/tool-call-display-context.d.ts +3 -1
- package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -1
- package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -1
- package/dist/v2/components/chat/types.d.ts +1 -1
- package/dist/v2/components/chat/types.d.ts.map +1 -1
- package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -1
- package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +1 -1
- package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -1
- package/dist/v2/hooks/use-agent-context.d.ts +2 -3
- package/dist/v2/hooks/use-agent-context.d.ts.map +1 -1
- package/dist/v2/hooks/use-agent.d.ts +4 -1
- package/dist/v2/hooks/use-agent.d.ts.map +1 -1
- package/dist/v2/hooks/use-attachments.d.ts +7 -0
- package/dist/v2/hooks/use-attachments.d.ts.map +1 -1
- package/dist/v2/hooks/use-capabilities.d.ts.map +1 -1
- package/dist/v2/hooks/use-component.d.ts +1 -0
- package/dist/v2/hooks/use-component.d.ts.map +1 -1
- package/dist/v2/hooks/use-configure-suggestions.d.ts +4 -9
- package/dist/v2/hooks/use-configure-suggestions.d.ts.map +1 -1
- package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
- package/dist/v2/hooks/use-human-in-the-loop.d.ts +2 -1
- package/dist/v2/hooks/use-human-in-the-loop.d.ts.map +1 -1
- package/dist/v2/hooks/use-interrupt.d.ts +0 -1
- package/dist/v2/hooks/use-interrupt.d.ts.map +1 -1
- package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
- package/dist/v2/hooks/use-render-custom-messages.d.ts.map +1 -1
- package/dist/v2/hooks/use-suggestions.d.ts.map +1 -1
- package/dist/v2/hooks/use-threads.d.ts +0 -8
- package/dist/v2/hooks/use-threads.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/lib/vue-core.d.ts +12 -11
- package/dist/v2/lib/vue-core.d.ts.map +1 -1
- package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.types.d.ts +9 -124
- package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts +9 -10
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/chat-configuration-store-key.d.ts +8 -0
- package/dist/v2/providers/chat-configuration-store-key.d.ts.map +1 -0
- package/dist/v2/providers/markdown-renderer.d.ts +3 -1
- package/dist/v2/providers/markdown-renderer.d.ts.map +1 -1
- package/dist/v2/providers/use-inspector-thread-override.d.ts +5 -0
- package/dist/v2/providers/use-inspector-thread-override.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/v2/__tests__/exports.test.ts +10 -0
- package/src/v2/__tests__/mocks/web-inspector.ts +6 -0
- package/src/v2/__tests__/utils/test-helpers.ts +34 -4
- package/src/v2/components/A2UIMessageRenderer.ts +98 -68
- package/src/v2/components/A2UISurfaceActivityRenderer.vue +68 -103
- package/src/v2/components/CopilotKitInspector.vue +104 -51
- package/src/v2/components/MCPAppsActivityRenderer.ts +75 -201
- package/src/v2/components/OpenGenerativeUIRenderer.ts +78 -80
- package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +171 -8
- package/src/v2/components/__tests__/CopilotKitInspector.test.ts +42 -0
- package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.ts +9 -2
- package/src/v2/components/__tests__/OpenGenerativeUIToolRenderer.test.ts +38 -0
- package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +1 -6
- package/src/v2/components/a2ui/A2UICatalogContext.ts +27 -93
- package/src/v2/components/a2ui/A2UIRecoveryStates.ts +127 -327
- package/src/v2/components/a2ui/catalog.ts +2 -3
- package/src/v2/components/a2ui/utils.ts +13 -67
- package/src/v2/components/a2ui.ts +14 -21
- package/src/v2/components/chat/CopilotChat.vue +136 -324
- package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +22 -45
- package/src/v2/components/chat/CopilotChatAudioRecorder.vue +19 -246
- package/src/v2/components/chat/CopilotChatInput.vue +69 -407
- package/src/v2/components/chat/CopilotChatLightbox.vue +10 -31
- package/src/v2/components/chat/CopilotChatMessageView.vue +52 -112
- package/src/v2/components/chat/CopilotChatToolCallGroup.vue +14 -45
- package/src/v2/components/chat/CopilotChatToolCallItem.vue +6 -4
- package/src/v2/components/chat/CopilotChatToolCallsView.vue +16 -19
- package/src/v2/components/chat/CopilotChatView.vue +28 -23
- package/src/v2/components/chat/CopilotPopupViewInternal.vue +21 -97
- package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -25
- package/src/v2/components/chat/CopilotThreadsDrawer.vue +37 -102
- package/src/v2/components/chat/StreamingMarkdownDefault.vue +13 -26
- package/src/v2/components/chat/__tests__/CopilotChat.test.ts +12 -10
- package/src/v2/components/chat/__tests__/CopilotChatAttachmentQueue.test.ts +28 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +56 -0
- package/src/v2/components/chat/__tests__/CopilotPopupView.test.ts +19 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +32 -8
- package/src/v2/components/chat/__tests__/CopilotSidebarView.test.ts +2 -1
- package/src/v2/components/chat/__tests__/StreamingMarkdownDefault.test.ts +54 -0
- package/src/v2/components/chat/audioRecorder.ts +4 -7
- package/src/v2/components/chat/copilotChatToolCallItemRender.ts +22 -71
- package/src/v2/components/chat/tool-call-display-context.ts +6 -1
- package/src/v2/components/chat/tool-call-group-parts.ts +13 -33
- package/src/v2/components/chat/types.ts +1 -1
- package/src/v2/components/chat/use-lightbox.ts +15 -33
- package/src/v2/hooks/__tests__/use-agent-error-state.test.ts +5 -3
- package/src/v2/hooks/__tests__/use-agent-stability.test.ts +25 -4
- package/src/v2/hooks/__tests__/use-agent-throttle.test.ts +5 -1
- package/src/v2/hooks/__tests__/use-agent.test.ts +6 -3
- package/src/v2/hooks/__tests__/use-component.test.ts +21 -0
- package/src/v2/hooks/__tests__/use-configure-suggestions.test.ts +30 -1
- package/src/v2/hooks/__tests__/use-default-render-tool.test.ts +10 -10
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +4 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +6 -1
- package/src/v2/hooks/__tests__/use-human-in-the-loop.test.ts +187 -12
- package/src/v2/hooks/__tests__/use-interrupt.test.ts +97 -0
- package/src/v2/hooks/__tests__/use-render-activity-message.test.ts +2 -1
- package/src/v2/hooks/__tests__/use-threads.test.ts +48 -6
- package/src/v2/hooks/use-agent-context.ts +4 -10
- package/src/v2/hooks/use-agent.ts +86 -290
- package/src/v2/hooks/use-attachments.ts +42 -166
- package/src/v2/hooks/use-capabilities.ts +2 -7
- package/src/v2/hooks/use-component.ts +7 -4
- package/src/v2/hooks/use-configure-suggestions.ts +28 -218
- package/src/v2/hooks/use-default-render-tool.ts +34 -94
- package/src/v2/hooks/use-human-in-the-loop.ts +11 -80
- package/src/v2/hooks/use-interrupt.ts +29 -308
- package/src/v2/hooks/use-render-activity-message.ts +17 -20
- package/src/v2/hooks/use-render-custom-messages.ts +20 -63
- package/src/v2/hooks/use-suggestions.ts +9 -33
- package/src/v2/hooks/use-threads.ts +43 -273
- package/src/v2/lib/__tests__/vue-core-renderers.test.ts +42 -0
- package/src/v2/lib/vue-core.ts +67 -72
- package/src/v2/providers/CopilotChatConfigurationProvider.vue +48 -186
- package/src/v2/providers/CopilotKitProvider.types.ts +12 -127
- package/src/v2/providers/CopilotKitProvider.vue +188 -402
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +22 -2
- package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +19 -1
- package/src/v2/providers/__tests__/CopilotKitProvider.config.test.ts +124 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.ts +4 -1
- package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +45 -2
- package/src/v2/providers/chat-configuration-store-key.ts +9 -0
- package/src/v2/providers/markdown-renderer.ts +7 -1
- package/src/v2/providers/use-inspector-thread-override.ts +22 -80
- package/vite.config.ts +5 -1
- package/vitest.config.ts +12 -7
- package/dist/use-render-activity-message-CsWYyd6r.cjs +0 -91
- package/dist/use-render-activity-message-CsWYyd6r.cjs.map +0 -1
- package/dist/use-render-activity-message-DntKKNdd.js +0 -17418
- package/dist/use-render-activity-message-DntKKNdd.js.map +0 -1
- package/dist/v2/components/chat/normalize-auto-scroll.d.ts +0 -3
- package/dist/v2/components/chat/normalize-auto-scroll.d.ts.map +0 -1
- package/dist/v2/lib/is-mobile-viewport.d.ts +0 -2
- package/dist/v2/lib/is-mobile-viewport.d.ts.map +0 -1
- package/src/v2/components/__tests__/A2UICatalogContext.test.ts +0 -63
- package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +0 -175
- package/src/v2/components/chat/normalize-auto-scroll.ts +0 -3
- 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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
if (!isPdf(mimeType) && !isText(mimeType)) {
|
|
76
|
+
if (ɵcanPreviewDocument(next.source.mimeType) === null) {
|
|
81
77
|
return;
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
const
|
|
85
|
-
documentBlobUrl.value =
|
|
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
|
-
|
|
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)
|
|
127
|
-
return
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
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
|
-
|
|
24
|
+
waveform?.dispose();
|
|
25
|
+
recorder.dispose();
|
|
253
26
|
});
|
|
254
27
|
|
|
255
28
|
defineExpose({
|
|
256
29
|
get state() {
|
|
257
|
-
return
|
|
30
|
+
return recorder.getSnapshot();
|
|
258
31
|
},
|
|
259
|
-
start,
|
|
260
|
-
stop,
|
|
261
|
-
dispose:
|
|
32
|
+
start: () => recorder.start(),
|
|
33
|
+
stop: () => recorder.stop(),
|
|
34
|
+
dispose: () => recorder.dispose(),
|
|
262
35
|
});
|
|
263
36
|
</script>
|
|
264
37
|
|