@copilotkitnext/react 1.51.5-next.4 → 1.52.0-next.6
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/components/WildcardToolCallRender.cjs +13 -13
- package/dist/components/WildcardToolCallRender.cjs.map +1 -1
- package/dist/components/WildcardToolCallRender.mjs +13 -13
- package/dist/components/WildcardToolCallRender.mjs.map +1 -1
- package/dist/components/chat/CopilotChat.cjs.map +1 -1
- package/dist/components/chat/CopilotChat.mjs.map +1 -1
- package/dist/components/chat/CopilotChatAssistantMessage.cjs +36 -28
- package/dist/components/chat/CopilotChatAssistantMessage.cjs.map +1 -1
- package/dist/components/chat/CopilotChatAssistantMessage.mjs +37 -29
- package/dist/components/chat/CopilotChatAssistantMessage.mjs.map +1 -1
- package/dist/components/chat/CopilotChatAudioRecorder.cjs +2 -2
- package/dist/components/chat/CopilotChatAudioRecorder.cjs.map +1 -1
- package/dist/components/chat/CopilotChatAudioRecorder.mjs +2 -2
- package/dist/components/chat/CopilotChatAudioRecorder.mjs.map +1 -1
- package/dist/components/chat/CopilotChatInput.cjs +59 -51
- package/dist/components/chat/CopilotChatInput.cjs.map +1 -1
- package/dist/components/chat/CopilotChatInput.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatInput.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatInput.mjs +59 -51
- package/dist/components/chat/CopilotChatInput.mjs.map +1 -1
- package/dist/components/chat/CopilotChatMessageView.cjs +12 -7
- package/dist/components/chat/CopilotChatMessageView.cjs.map +1 -1
- package/dist/components/chat/CopilotChatMessageView.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatMessageView.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatMessageView.mjs +12 -7
- package/dist/components/chat/CopilotChatMessageView.mjs.map +1 -1
- package/dist/components/chat/CopilotChatReasoningMessage.cjs +24 -20
- package/dist/components/chat/CopilotChatReasoningMessage.cjs.map +1 -1
- package/dist/components/chat/CopilotChatReasoningMessage.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatReasoningMessage.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatReasoningMessage.mjs +25 -21
- package/dist/components/chat/CopilotChatReasoningMessage.mjs.map +1 -1
- package/dist/components/chat/CopilotChatSuggestionPill.cjs +6 -5
- package/dist/components/chat/CopilotChatSuggestionPill.cjs.map +1 -1
- package/dist/components/chat/CopilotChatSuggestionPill.mjs +6 -5
- package/dist/components/chat/CopilotChatSuggestionPill.mjs.map +1 -1
- package/dist/components/chat/CopilotChatSuggestionView.cjs +24 -12
- package/dist/components/chat/CopilotChatSuggestionView.cjs.map +1 -1
- package/dist/components/chat/CopilotChatSuggestionView.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatSuggestionView.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatSuggestionView.mjs +25 -13
- package/dist/components/chat/CopilotChatSuggestionView.mjs.map +1 -1
- package/dist/components/chat/CopilotChatToggleButton.cjs +7 -6
- package/dist/components/chat/CopilotChatToggleButton.cjs.map +1 -1
- package/dist/components/chat/CopilotChatToggleButton.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatToggleButton.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatToggleButton.mjs +7 -6
- package/dist/components/chat/CopilotChatToggleButton.mjs.map +1 -1
- package/dist/components/chat/CopilotChatUserMessage.cjs +29 -24
- package/dist/components/chat/CopilotChatUserMessage.cjs.map +1 -1
- package/dist/components/chat/CopilotChatUserMessage.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatUserMessage.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatUserMessage.mjs +30 -25
- package/dist/components/chat/CopilotChatUserMessage.mjs.map +1 -1
- package/dist/components/chat/CopilotChatView.cjs +45 -35
- package/dist/components/chat/CopilotChatView.cjs.map +1 -1
- package/dist/components/chat/CopilotChatView.d.cts +1 -1
- package/dist/components/chat/CopilotChatView.d.cts.map +1 -1
- package/dist/components/chat/CopilotChatView.d.mts +1 -1
- package/dist/components/chat/CopilotChatView.d.mts.map +1 -1
- package/dist/components/chat/CopilotChatView.mjs +45 -35
- package/dist/components/chat/CopilotChatView.mjs.map +1 -1
- package/dist/components/chat/CopilotModalHeader.cjs +8 -8
- package/dist/components/chat/CopilotModalHeader.cjs.map +1 -1
- package/dist/components/chat/CopilotModalHeader.d.cts.map +1 -1
- package/dist/components/chat/CopilotModalHeader.d.mts.map +1 -1
- package/dist/components/chat/CopilotModalHeader.mjs +8 -8
- package/dist/components/chat/CopilotModalHeader.mjs.map +1 -1
- package/dist/components/chat/CopilotPopupView.cjs +20 -15
- package/dist/components/chat/CopilotPopupView.cjs.map +1 -1
- package/dist/components/chat/CopilotPopupView.d.cts.map +1 -1
- package/dist/components/chat/CopilotPopupView.d.mts.map +1 -1
- package/dist/components/chat/CopilotPopupView.mjs +20 -15
- package/dist/components/chat/CopilotPopupView.mjs.map +1 -1
- package/dist/components/chat/CopilotSidebarView.cjs +54 -43
- package/dist/components/chat/CopilotSidebarView.cjs.map +1 -1
- package/dist/components/chat/CopilotSidebarView.d.cts.map +1 -1
- package/dist/components/chat/CopilotSidebarView.d.mts.map +1 -1
- package/dist/components/chat/CopilotSidebarView.mjs +55 -44
- package/dist/components/chat/CopilotSidebarView.mjs.map +1 -1
- package/dist/components/ui/button.cjs +42 -42
- package/dist/components/ui/button.cjs.map +1 -1
- package/dist/components/ui/button.mjs +42 -42
- package/dist/components/ui/button.mjs.map +1 -1
- package/dist/components/ui/dropdown-menu.cjs +7 -6
- package/dist/components/ui/dropdown-menu.cjs.map +1 -1
- package/dist/components/ui/dropdown-menu.mjs +7 -6
- package/dist/components/ui/dropdown-menu.mjs.map +1 -1
- package/dist/components/ui/tooltip.cjs +3 -2
- package/dist/components/ui/tooltip.cjs.map +1 -1
- package/dist/components/ui/tooltip.mjs +3 -2
- package/dist/components/ui/tooltip.mjs.map +1 -1
- package/dist/hooks/use-component.cjs +18 -5
- package/dist/hooks/use-component.cjs.map +1 -1
- package/dist/hooks/use-component.d.cts +21 -7
- package/dist/hooks/use-component.d.cts.map +1 -1
- package/dist/hooks/use-component.d.mts +21 -7
- package/dist/hooks/use-component.d.mts.map +1 -1
- package/dist/hooks/use-component.mjs +18 -5
- package/dist/hooks/use-component.mjs.map +1 -1
- package/dist/hooks/use-default-render-tool.cjs +2 -2
- package/dist/hooks/use-default-render-tool.cjs.map +1 -1
- package/dist/hooks/use-default-render-tool.d.cts +3 -3
- package/dist/hooks/use-default-render-tool.d.cts.map +1 -1
- package/dist/hooks/use-default-render-tool.d.mts +3 -3
- package/dist/hooks/use-default-render-tool.d.mts.map +1 -1
- package/dist/hooks/use-default-render-tool.mjs +2 -2
- package/dist/hooks/use-default-render-tool.mjs.map +1 -1
- package/dist/hooks/use-render-tool.cjs +15 -9
- package/dist/hooks/use-render-tool.cjs.map +1 -1
- package/dist/hooks/use-render-tool.d.cts +9 -9
- package/dist/hooks/use-render-tool.d.cts.map +1 -1
- package/dist/hooks/use-render-tool.d.mts +9 -9
- package/dist/hooks/use-render-tool.d.mts.map +1 -1
- package/dist/hooks/use-render-tool.mjs +15 -9
- package/dist/hooks/use-render-tool.mjs.map +1 -1
- package/dist/index.umd.js +429 -339
- package/dist/index.umd.js.map +1 -1
- package/dist/lib/utils.cjs +2 -1
- package/dist/lib/utils.cjs.map +1 -1
- package/dist/lib/utils.mjs +3 -2
- package/dist/lib/utils.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/eslint-rules/README.md +52 -0
- package/eslint-rules/require-cpk-prefix.mjs +375 -0
- package/eslint.config.mjs +17 -0
- package/package.json +8 -7
|
@@ -6,7 +6,7 @@ import CopilotChatToolCallsView from "./CopilotChatToolCallsView.mjs";
|
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { twMerge } from "tailwind-merge";
|
|
8
8
|
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp, Volume2 } from "lucide-react";
|
|
9
|
-
import {
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import "katex/dist/katex.min.css";
|
|
11
11
|
import { Streamdown } from "streamdown";
|
|
12
12
|
|
|
@@ -25,7 +25,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
25
25
|
const boundReadAloudButton = renderSlot(readAloudButton, CopilotChatAssistantMessage.ReadAloudButton, { onClick: onReadAloud });
|
|
26
26
|
const boundRegenerateButton = renderSlot(regenerateButton, CopilotChatAssistantMessage.RegenerateButton, { onClick: onRegenerate });
|
|
27
27
|
const boundToolbar = renderSlot(toolbar, CopilotChatAssistantMessage.Toolbar, { children: /* @__PURE__ */ jsxs("div", {
|
|
28
|
-
className: "flex items-center gap-1",
|
|
28
|
+
className: "cpk:flex cpk:items-center cpk:gap-1",
|
|
29
29
|
children: [
|
|
30
30
|
boundCopyButton,
|
|
31
31
|
(onThumbsUp || thumbsUpButton) && boundThumbsUpButton,
|
|
@@ -42,31 +42,39 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
42
42
|
const hasContent = !!(message.content && message.content.trim().length > 0);
|
|
43
43
|
const isLatestAssistantMessage = message.role === "assistant" && messages?.[messages.length - 1]?.id === message.id;
|
|
44
44
|
const shouldShowToolbar = toolbarVisible && hasContent && !(isRunning && isLatestAssistantMessage);
|
|
45
|
-
if (children) return /* @__PURE__ */ jsx(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
if (children) return /* @__PURE__ */ jsx("div", {
|
|
46
|
+
"data-copilotkit": true,
|
|
47
|
+
style: { display: "contents" },
|
|
48
|
+
children: children({
|
|
49
|
+
markdownRenderer: boundMarkdownRenderer,
|
|
50
|
+
toolbar: boundToolbar,
|
|
51
|
+
toolCallsView: boundToolCallsView,
|
|
52
|
+
copyButton: boundCopyButton,
|
|
53
|
+
thumbsUpButton: boundThumbsUpButton,
|
|
54
|
+
thumbsDownButton: boundThumbsDownButton,
|
|
55
|
+
readAloudButton: boundReadAloudButton,
|
|
56
|
+
regenerateButton: boundRegenerateButton,
|
|
57
|
+
message,
|
|
58
|
+
messages,
|
|
59
|
+
isRunning,
|
|
60
|
+
onThumbsUp,
|
|
61
|
+
onThumbsDown,
|
|
62
|
+
onReadAloud,
|
|
63
|
+
onRegenerate,
|
|
64
|
+
additionalToolbarItems,
|
|
65
|
+
toolbarVisible: shouldShowToolbar
|
|
66
|
+
})
|
|
67
|
+
});
|
|
64
68
|
return /* @__PURE__ */ jsxs("div", {
|
|
65
|
-
|
|
69
|
+
"data-copilotkit": true,
|
|
70
|
+
className: twMerge(className),
|
|
66
71
|
...props,
|
|
67
72
|
"data-message-id": message.id,
|
|
68
73
|
children: [
|
|
69
|
-
|
|
74
|
+
/* @__PURE__ */ jsx("div", {
|
|
75
|
+
className: "cpk:prose cpk:max-w-full cpk:break-words cpk:dark:prose-invert",
|
|
76
|
+
children: boundMarkdownRenderer
|
|
77
|
+
}),
|
|
70
78
|
boundToolCallsView,
|
|
71
79
|
shouldShowToolbar && boundToolbar
|
|
72
80
|
]
|
|
@@ -79,7 +87,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
79
87
|
children: content ?? ""
|
|
80
88
|
});
|
|
81
89
|
_CopilotChatAssistantMessage.Toolbar = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
82
|
-
className: twMerge("w-full bg-transparent flex items-center
|
|
90
|
+
className: twMerge("cpk:w-full cpk:bg-transparent cpk:flex cpk:items-center cpk:-ml-[5px] cpk:-mt-[0px]", className),
|
|
83
91
|
...props
|
|
84
92
|
});
|
|
85
93
|
const ToolbarButton = _CopilotChatAssistantMessage.ToolbarButton = ({ title, children, ...props }) => {
|
|
@@ -110,7 +118,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
110
118
|
onClick: handleClick,
|
|
111
119
|
className,
|
|
112
120
|
...props,
|
|
113
|
-
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-[18px]" }) : /* @__PURE__ */ jsx(Copy, { className: "size-[18px]" })
|
|
121
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "cpk:size-[18px]" }) : /* @__PURE__ */ jsx(Copy, { className: "cpk:size-[18px]" })
|
|
114
122
|
});
|
|
115
123
|
};
|
|
116
124
|
_CopilotChatAssistantMessage.ThumbsUpButton = ({ title, ...props }) => {
|
|
@@ -118,7 +126,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
118
126
|
return /* @__PURE__ */ jsx(ToolbarButton, {
|
|
119
127
|
title: title || labels.assistantMessageToolbarThumbsUpLabel,
|
|
120
128
|
...props,
|
|
121
|
-
children: /* @__PURE__ */ jsx(ThumbsUp, { className: "size-[18px]" })
|
|
129
|
+
children: /* @__PURE__ */ jsx(ThumbsUp, { className: "cpk:size-[18px]" })
|
|
122
130
|
});
|
|
123
131
|
};
|
|
124
132
|
_CopilotChatAssistantMessage.ThumbsDownButton = ({ title, ...props }) => {
|
|
@@ -126,7 +134,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
126
134
|
return /* @__PURE__ */ jsx(ToolbarButton, {
|
|
127
135
|
title: title || labels.assistantMessageToolbarThumbsDownLabel,
|
|
128
136
|
...props,
|
|
129
|
-
children: /* @__PURE__ */ jsx(ThumbsDown, { className: "size-[18px]" })
|
|
137
|
+
children: /* @__PURE__ */ jsx(ThumbsDown, { className: "cpk:size-[18px]" })
|
|
130
138
|
});
|
|
131
139
|
};
|
|
132
140
|
_CopilotChatAssistantMessage.ReadAloudButton = ({ title, ...props }) => {
|
|
@@ -134,7 +142,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
134
142
|
return /* @__PURE__ */ jsx(ToolbarButton, {
|
|
135
143
|
title: title || labels.assistantMessageToolbarReadAloudLabel,
|
|
136
144
|
...props,
|
|
137
|
-
children: /* @__PURE__ */ jsx(Volume2, { className: "size-[20px]" })
|
|
145
|
+
children: /* @__PURE__ */ jsx(Volume2, { className: "cpk:size-[20px]" })
|
|
138
146
|
});
|
|
139
147
|
};
|
|
140
148
|
_CopilotChatAssistantMessage.RegenerateButton = ({ title, ...props }) => {
|
|
@@ -142,7 +150,7 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp,
|
|
|
142
150
|
return /* @__PURE__ */ jsx(ToolbarButton, {
|
|
143
151
|
title: title || labels.assistantMessageToolbarRegenerateLabel,
|
|
144
152
|
...props,
|
|
145
|
-
children: /* @__PURE__ */ jsx(RefreshCw, { className: "size-[18px]" })
|
|
153
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: "cpk:size-[18px]" })
|
|
146
154
|
});
|
|
147
155
|
};
|
|
148
156
|
})(CopilotChatAssistantMessage || (CopilotChatAssistantMessage = {}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopilotChatAssistantMessage.mjs","names":[],"sources":["../../../src/components/chat/CopilotChatAssistantMessage.tsx"],"sourcesContent":["import { AssistantMessage, Message } from \"@ag-ui/core\";\nimport { useState } from \"react\";\nimport {\n Copy,\n Check,\n ThumbsUp,\n ThumbsDown,\n Volume2,\n RefreshCw,\n} from \"lucide-react\";\nimport {\n useCopilotChatConfiguration,\n CopilotChatDefaultLabels,\n} from \"@/providers/CopilotChatConfigurationProvider\";\nimport { twMerge } from \"tailwind-merge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport \"katex/dist/katex.min.css\";\nimport { WithSlots, renderSlot } from \"@/lib/slots\";\nimport { Streamdown } from \"streamdown\";\nimport CopilotChatToolCallsView from \"./CopilotChatToolCallsView\";\n\nexport type CopilotChatAssistantMessageProps = WithSlots<\n {\n markdownRenderer: typeof CopilotChatAssistantMessage.MarkdownRenderer;\n toolbar: typeof CopilotChatAssistantMessage.Toolbar;\n copyButton: typeof CopilotChatAssistantMessage.CopyButton;\n thumbsUpButton: typeof CopilotChatAssistantMessage.ThumbsUpButton;\n thumbsDownButton: typeof CopilotChatAssistantMessage.ThumbsDownButton;\n readAloudButton: typeof CopilotChatAssistantMessage.ReadAloudButton;\n regenerateButton: typeof CopilotChatAssistantMessage.RegenerateButton;\n toolCallsView: typeof CopilotChatToolCallsView;\n },\n {\n onThumbsUp?: (message: AssistantMessage) => void;\n onThumbsDown?: (message: AssistantMessage) => void;\n onReadAloud?: (message: AssistantMessage) => void;\n onRegenerate?: (message: AssistantMessage) => void;\n message: AssistantMessage;\n messages?: Message[];\n isRunning?: boolean;\n additionalToolbarItems?: React.ReactNode;\n toolbarVisible?: boolean;\n } & React.HTMLAttributes<HTMLDivElement>\n>;\n\nexport function CopilotChatAssistantMessage({\n message,\n messages,\n isRunning,\n onThumbsUp,\n onThumbsDown,\n onReadAloud,\n onRegenerate,\n additionalToolbarItems,\n toolbarVisible = true,\n markdownRenderer,\n toolbar,\n copyButton,\n thumbsUpButton,\n thumbsDownButton,\n readAloudButton,\n regenerateButton,\n toolCallsView,\n children,\n className,\n ...props\n}: CopilotChatAssistantMessageProps) {\n const boundMarkdownRenderer = renderSlot(\n markdownRenderer,\n CopilotChatAssistantMessage.MarkdownRenderer,\n {\n content: message.content || \"\",\n },\n );\n\n const boundCopyButton = renderSlot(\n copyButton,\n CopilotChatAssistantMessage.CopyButton,\n {\n onClick: async () => {\n if (message.content) {\n try {\n await navigator.clipboard.writeText(message.content);\n } catch (err) {\n console.error(\"Failed to copy message:\", err);\n }\n }\n },\n },\n );\n\n const boundThumbsUpButton = renderSlot(\n thumbsUpButton,\n CopilotChatAssistantMessage.ThumbsUpButton,\n {\n onClick: onThumbsUp,\n },\n );\n\n const boundThumbsDownButton = renderSlot(\n thumbsDownButton,\n CopilotChatAssistantMessage.ThumbsDownButton,\n {\n onClick: onThumbsDown,\n },\n );\n\n const boundReadAloudButton = renderSlot(\n readAloudButton,\n CopilotChatAssistantMessage.ReadAloudButton,\n {\n onClick: onReadAloud,\n },\n );\n\n const boundRegenerateButton = renderSlot(\n regenerateButton,\n CopilotChatAssistantMessage.RegenerateButton,\n {\n onClick: onRegenerate,\n },\n );\n\n const boundToolbar = renderSlot(\n toolbar,\n CopilotChatAssistantMessage.Toolbar,\n {\n children: (\n <div className=\"flex items-center gap-1\">\n {boundCopyButton}\n {(onThumbsUp || thumbsUpButton) && boundThumbsUpButton}\n {(onThumbsDown || thumbsDownButton) && boundThumbsDownButton}\n {(onReadAloud || readAloudButton) && boundReadAloudButton}\n {(onRegenerate || regenerateButton) && boundRegenerateButton}\n {additionalToolbarItems}\n </div>\n ),\n },\n );\n\n const boundToolCallsView = renderSlot(\n toolCallsView,\n CopilotChatToolCallsView,\n {\n message,\n messages,\n },\n );\n\n // Don't show toolbar if message has no content (only tool calls)\n const hasContent = !!(message.content && message.content.trim().length > 0);\n const isLatestAssistantMessage =\n message.role === \"assistant\" &&\n messages?.[messages.length - 1]?.id === message.id;\n const shouldShowToolbar =\n toolbarVisible && hasContent && !(isRunning && isLatestAssistantMessage);\n\n if (children) {\n return (\n <>\n {children({\n markdownRenderer: boundMarkdownRenderer,\n toolbar: boundToolbar,\n toolCallsView: boundToolCallsView,\n copyButton: boundCopyButton,\n thumbsUpButton: boundThumbsUpButton,\n thumbsDownButton: boundThumbsDownButton,\n readAloudButton: boundReadAloudButton,\n regenerateButton: boundRegenerateButton,\n message,\n messages,\n isRunning,\n onThumbsUp,\n onThumbsDown,\n onReadAloud,\n onRegenerate,\n additionalToolbarItems,\n toolbarVisible: shouldShowToolbar,\n })}\n </>\n );\n }\n\n return (\n <div\n className={twMerge(\n \"prose max-w-full break-words dark:prose-invert\",\n className,\n )}\n {...props}\n data-message-id={message.id}\n >\n {boundMarkdownRenderer}\n {boundToolCallsView}\n {shouldShowToolbar && boundToolbar}\n </div>\n );\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace CopilotChatAssistantMessage {\n export const MarkdownRenderer: React.FC<\n Omit<React.ComponentProps<typeof Streamdown>, \"children\"> & {\n content: string;\n }\n > = ({ content, className, ...props }) => (\n <Streamdown className={className} {...props}>\n {content ?? \"\"}\n </Streamdown>\n );\n\n export const Toolbar: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({\n className,\n ...props\n }) => (\n <div\n className={twMerge(\n \"w-full bg-transparent flex items-center -ml-[5px] -mt-[0px]\",\n className,\n )}\n {...props}\n />\n );\n\n export const ToolbarButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement> & {\n title: string;\n children: React.ReactNode;\n }\n > = ({ title, children, ...props }) => {\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n type=\"button\"\n variant=\"assistantMessageToolbarButton\"\n aria-label={title}\n {...props}\n >\n {children}\n </Button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n <p>{title}</p>\n </TooltipContent>\n </Tooltip>\n );\n };\n\n export const CopyButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ className, title, onClick, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n const [copied, setCopied] = useState(false);\n\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n\n if (onClick) {\n onClick(event);\n }\n };\n\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarCopyMessageLabel}\n onClick={handleClick}\n className={className}\n {...props}\n >\n {copied ? (\n <Check className=\"size-[18px]\" />\n ) : (\n <Copy className=\"size-[18px]\" />\n )}\n </ToolbarButton>\n );\n };\n\n export const ThumbsUpButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarThumbsUpLabel}\n {...props}\n >\n <ThumbsUp className=\"size-[18px]\" />\n </ToolbarButton>\n );\n };\n\n export const ThumbsDownButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarThumbsDownLabel}\n {...props}\n >\n <ThumbsDown className=\"size-[18px]\" />\n </ToolbarButton>\n );\n };\n\n export const ReadAloudButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarReadAloudLabel}\n {...props}\n >\n <Volume2 className=\"size-[20px]\" />\n </ToolbarButton>\n );\n };\n\n export const RegenerateButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarRegenerateLabel}\n {...props}\n >\n <RefreshCw className=\"size-[18px]\" />\n </ToolbarButton>\n );\n };\n}\n\nCopilotChatAssistantMessage.MarkdownRenderer.displayName =\n \"CopilotChatAssistantMessage.MarkdownRenderer\";\nCopilotChatAssistantMessage.Toolbar.displayName =\n \"CopilotChatAssistantMessage.Toolbar\";\nCopilotChatAssistantMessage.CopyButton.displayName =\n \"CopilotChatAssistantMessage.CopyButton\";\nCopilotChatAssistantMessage.ThumbsUpButton.displayName =\n \"CopilotChatAssistantMessage.ThumbsUpButton\";\nCopilotChatAssistantMessage.ThumbsDownButton.displayName =\n \"CopilotChatAssistantMessage.ThumbsDownButton\";\nCopilotChatAssistantMessage.ReadAloudButton.displayName =\n \"CopilotChatAssistantMessage.ReadAloudButton\";\nCopilotChatAssistantMessage.RegenerateButton.displayName =\n \"CopilotChatAssistantMessage.RegenerateButton\";\n\nexport default CopilotChatAssistantMessage;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CopilotChatAssistantMessage.mjs","names":[],"sources":["../../../src/components/chat/CopilotChatAssistantMessage.tsx"],"sourcesContent":["import { AssistantMessage, Message } from \"@ag-ui/core\";\nimport { useState } from \"react\";\nimport {\n Copy,\n Check,\n ThumbsUp,\n ThumbsDown,\n Volume2,\n RefreshCw,\n} from \"lucide-react\";\nimport {\n useCopilotChatConfiguration,\n CopilotChatDefaultLabels,\n} from \"@/providers/CopilotChatConfigurationProvider\";\nimport { twMerge } from \"tailwind-merge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport \"katex/dist/katex.min.css\";\nimport { WithSlots, renderSlot } from \"@/lib/slots\";\nimport { Streamdown } from \"streamdown\";\nimport CopilotChatToolCallsView from \"./CopilotChatToolCallsView\";\n\nexport type CopilotChatAssistantMessageProps = WithSlots<\n {\n markdownRenderer: typeof CopilotChatAssistantMessage.MarkdownRenderer;\n toolbar: typeof CopilotChatAssistantMessage.Toolbar;\n copyButton: typeof CopilotChatAssistantMessage.CopyButton;\n thumbsUpButton: typeof CopilotChatAssistantMessage.ThumbsUpButton;\n thumbsDownButton: typeof CopilotChatAssistantMessage.ThumbsDownButton;\n readAloudButton: typeof CopilotChatAssistantMessage.ReadAloudButton;\n regenerateButton: typeof CopilotChatAssistantMessage.RegenerateButton;\n toolCallsView: typeof CopilotChatToolCallsView;\n },\n {\n onThumbsUp?: (message: AssistantMessage) => void;\n onThumbsDown?: (message: AssistantMessage) => void;\n onReadAloud?: (message: AssistantMessage) => void;\n onRegenerate?: (message: AssistantMessage) => void;\n message: AssistantMessage;\n messages?: Message[];\n isRunning?: boolean;\n additionalToolbarItems?: React.ReactNode;\n toolbarVisible?: boolean;\n } & React.HTMLAttributes<HTMLDivElement>\n>;\n\nexport function CopilotChatAssistantMessage({\n message,\n messages,\n isRunning,\n onThumbsUp,\n onThumbsDown,\n onReadAloud,\n onRegenerate,\n additionalToolbarItems,\n toolbarVisible = true,\n markdownRenderer,\n toolbar,\n copyButton,\n thumbsUpButton,\n thumbsDownButton,\n readAloudButton,\n regenerateButton,\n toolCallsView,\n children,\n className,\n ...props\n}: CopilotChatAssistantMessageProps) {\n const boundMarkdownRenderer = renderSlot(\n markdownRenderer,\n CopilotChatAssistantMessage.MarkdownRenderer,\n {\n content: message.content || \"\",\n },\n );\n\n const boundCopyButton = renderSlot(\n copyButton,\n CopilotChatAssistantMessage.CopyButton,\n {\n onClick: async () => {\n if (message.content) {\n try {\n await navigator.clipboard.writeText(message.content);\n } catch (err) {\n console.error(\"Failed to copy message:\", err);\n }\n }\n },\n },\n );\n\n const boundThumbsUpButton = renderSlot(\n thumbsUpButton,\n CopilotChatAssistantMessage.ThumbsUpButton,\n {\n onClick: onThumbsUp,\n },\n );\n\n const boundThumbsDownButton = renderSlot(\n thumbsDownButton,\n CopilotChatAssistantMessage.ThumbsDownButton,\n {\n onClick: onThumbsDown,\n },\n );\n\n const boundReadAloudButton = renderSlot(\n readAloudButton,\n CopilotChatAssistantMessage.ReadAloudButton,\n {\n onClick: onReadAloud,\n },\n );\n\n const boundRegenerateButton = renderSlot(\n regenerateButton,\n CopilotChatAssistantMessage.RegenerateButton,\n {\n onClick: onRegenerate,\n },\n );\n\n const boundToolbar = renderSlot(\n toolbar,\n CopilotChatAssistantMessage.Toolbar,\n {\n children: (\n <div className=\"cpk:flex cpk:items-center cpk:gap-1\">\n {boundCopyButton}\n {(onThumbsUp || thumbsUpButton) && boundThumbsUpButton}\n {(onThumbsDown || thumbsDownButton) && boundThumbsDownButton}\n {(onReadAloud || readAloudButton) && boundReadAloudButton}\n {(onRegenerate || regenerateButton) && boundRegenerateButton}\n {additionalToolbarItems}\n </div>\n ),\n },\n );\n\n const boundToolCallsView = renderSlot(\n toolCallsView,\n CopilotChatToolCallsView,\n {\n message,\n messages,\n },\n );\n\n // Don't show toolbar if message has no content (only tool calls)\n const hasContent = !!(message.content && message.content.trim().length > 0);\n const isLatestAssistantMessage =\n message.role === \"assistant\" &&\n messages?.[messages.length - 1]?.id === message.id;\n const shouldShowToolbar =\n toolbarVisible && hasContent && !(isRunning && isLatestAssistantMessage);\n\n if (children) {\n return (\n <div data-copilotkit style={{ display: \"contents\" }}>\n {children({\n markdownRenderer: boundMarkdownRenderer,\n toolbar: boundToolbar,\n toolCallsView: boundToolCallsView,\n copyButton: boundCopyButton,\n thumbsUpButton: boundThumbsUpButton,\n thumbsDownButton: boundThumbsDownButton,\n readAloudButton: boundReadAloudButton,\n regenerateButton: boundRegenerateButton,\n message,\n messages,\n isRunning,\n onThumbsUp,\n onThumbsDown,\n onReadAloud,\n onRegenerate,\n additionalToolbarItems,\n toolbarVisible: shouldShowToolbar,\n })}\n </div>\n );\n }\n\n return (\n <div\n data-copilotkit\n className={twMerge(className)}\n {...props}\n data-message-id={message.id}\n >\n <div className=\"cpk:prose cpk:max-w-full cpk:break-words cpk:dark:prose-invert\">\n {boundMarkdownRenderer}\n </div>\n {boundToolCallsView}\n {shouldShowToolbar && boundToolbar}\n </div>\n );\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace CopilotChatAssistantMessage {\n export const MarkdownRenderer: React.FC<\n Omit<React.ComponentProps<typeof Streamdown>, \"children\"> & {\n content: string;\n }\n > = ({ content, className, ...props }) => (\n <Streamdown className={className} {...props}>\n {content ?? \"\"}\n </Streamdown>\n );\n\n export const Toolbar: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({\n className,\n ...props\n }) => (\n <div\n className={twMerge(\n \"cpk:w-full cpk:bg-transparent cpk:flex cpk:items-center cpk:-ml-[5px] cpk:-mt-[0px]\",\n className,\n )}\n {...props}\n />\n );\n\n export const ToolbarButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement> & {\n title: string;\n children: React.ReactNode;\n }\n > = ({ title, children, ...props }) => {\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n type=\"button\"\n variant=\"assistantMessageToolbarButton\"\n aria-label={title}\n {...props}\n >\n {children}\n </Button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n <p>{title}</p>\n </TooltipContent>\n </Tooltip>\n );\n };\n\n export const CopyButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ className, title, onClick, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n const [copied, setCopied] = useState(false);\n\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n\n if (onClick) {\n onClick(event);\n }\n };\n\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarCopyMessageLabel}\n onClick={handleClick}\n className={className}\n {...props}\n >\n {copied ? (\n <Check className=\"cpk:size-[18px]\" />\n ) : (\n <Copy className=\"cpk:size-[18px]\" />\n )}\n </ToolbarButton>\n );\n };\n\n export const ThumbsUpButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarThumbsUpLabel}\n {...props}\n >\n <ThumbsUp className=\"cpk:size-[18px]\" />\n </ToolbarButton>\n );\n };\n\n export const ThumbsDownButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarThumbsDownLabel}\n {...props}\n >\n <ThumbsDown className=\"cpk:size-[18px]\" />\n </ToolbarButton>\n );\n };\n\n export const ReadAloudButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarReadAloudLabel}\n {...props}\n >\n <Volume2 className=\"cpk:size-[20px]\" />\n </ToolbarButton>\n );\n };\n\n export const RegenerateButton: React.FC<\n React.ButtonHTMLAttributes<HTMLButtonElement>\n > = ({ title, ...props }) => {\n const config = useCopilotChatConfiguration();\n const labels = config?.labels ?? CopilotChatDefaultLabels;\n return (\n <ToolbarButton\n title={title || labels.assistantMessageToolbarRegenerateLabel}\n {...props}\n >\n <RefreshCw className=\"cpk:size-[18px]\" />\n </ToolbarButton>\n );\n };\n}\n\nCopilotChatAssistantMessage.MarkdownRenderer.displayName =\n \"CopilotChatAssistantMessage.MarkdownRenderer\";\nCopilotChatAssistantMessage.Toolbar.displayName =\n \"CopilotChatAssistantMessage.Toolbar\";\nCopilotChatAssistantMessage.CopyButton.displayName =\n \"CopilotChatAssistantMessage.CopyButton\";\nCopilotChatAssistantMessage.ThumbsUpButton.displayName =\n \"CopilotChatAssistantMessage.ThumbsUpButton\";\nCopilotChatAssistantMessage.ThumbsDownButton.displayName =\n \"CopilotChatAssistantMessage.ThumbsDownButton\";\nCopilotChatAssistantMessage.ReadAloudButton.displayName =\n \"CopilotChatAssistantMessage.ReadAloudButton\";\nCopilotChatAssistantMessage.RegenerateButton.displayName =\n \"CopilotChatAssistantMessage.RegenerateButton\";\n\nexport default CopilotChatAssistantMessage;\n"],"mappings":"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"}
|
|
@@ -192,11 +192,11 @@ const CopilotChatAudioRecorder = (0, react.forwardRef)((props, ref) => {
|
|
|
192
192
|
cleanup
|
|
193
193
|
]);
|
|
194
194
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
195
|
-
className: (0, tailwind_merge.twMerge)("w-full py-3 px-5", className),
|
|
195
|
+
className: (0, tailwind_merge.twMerge)("cpk:w-full cpk:py-3 cpk:px-5", className),
|
|
196
196
|
...divProps,
|
|
197
197
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
198
198
|
ref: canvasRef,
|
|
199
|
-
className: "block w-full h-[26px]"
|
|
199
|
+
className: "cpk:block cpk:w-full cpk:h-[26px]"
|
|
200
200
|
})
|
|
201
201
|
});
|
|
202
202
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopilotChatAudioRecorder.cjs","names":[],"sources":["../../../src/components/chat/CopilotChatAudioRecorder.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useImperativeHandle,\n forwardRef,\n useCallback,\n useState,\n} from \"react\";\nimport { twMerge } from \"tailwind-merge\";\n\n/** Finite-state machine for every recorder implementation */\nexport type AudioRecorderState = \"idle\" | \"recording\" | \"processing\";\n\n/** Error subclass so callers can `instanceof`-guard recorder failures */\nexport class AudioRecorderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AudioRecorderError\";\n }\n}\n\nexport interface AudioRecorderRef {\n state: AudioRecorderState;\n start: () => Promise<void>;\n stop: () => Promise<Blob>;\n dispose: () => void;\n}\n\nexport const CopilotChatAudioRecorder = forwardRef<\n AudioRecorderRef,\n React.HTMLAttributes<HTMLDivElement>\n>((props, ref) => {\n const { className, ...divProps } = props;\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // Recording state\n const [recorderState, setRecorderState] =\n useState<AudioRecorderState>(\"idle\");\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const streamRef = useRef<MediaStream | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const audioContextRef = useRef<AudioContext | null>(null);\n const animationIdRef = useRef<number | null>(null);\n\n // Amplitude history buffer for scrolling waveform\n const amplitudeHistoryRef = useRef<number[]>([]);\n const frameCountRef = useRef<number>(0);\n const scrollOffsetRef = useRef<number>(0);\n const smoothedAmplitudeRef = useRef<number>(0);\n const fadeOpacityRef = useRef<number>(0);\n\n // Clean up all resources\n const cleanup = useCallback(() => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n if (\n mediaRecorderRef.current &&\n mediaRecorderRef.current.state !== \"inactive\"\n ) {\n try {\n mediaRecorderRef.current.stop();\n } catch {\n // Ignore errors during cleanup\n }\n }\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }\n if (audioContextRef.current && audioContextRef.current.state !== \"closed\") {\n audioContextRef.current.close().catch(() => {\n // Ignore close errors\n });\n audioContextRef.current = null;\n }\n mediaRecorderRef.current = null;\n analyserRef.current = null;\n audioChunksRef.current = [];\n amplitudeHistoryRef.current = [];\n frameCountRef.current = 0;\n scrollOffsetRef.current = 0;\n smoothedAmplitudeRef.current = 0;\n fadeOpacityRef.current = 0;\n }, []);\n\n // Start recording\n const start = useCallback(async () => {\n if (recorderState !== \"idle\") {\n throw new AudioRecorderError(\"Recorder is already active\");\n }\n\n try {\n // Request microphone access\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n\n // Set up audio context for visualization\n const audioContext = new AudioContext();\n audioContextRef.current = audioContext;\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 2048; // Higher resolution for time-domain waveform\n source.connect(analyser);\n analyserRef.current = analyser;\n\n // Determine best MIME type for recording\n const mimeType = MediaRecorder.isTypeSupported(\"audio/webm;codecs=opus\")\n ? \"audio/webm;codecs=opus\"\n : MediaRecorder.isTypeSupported(\"audio/webm\")\n ? \"audio/webm\"\n : MediaRecorder.isTypeSupported(\"audio/mp4\")\n ? \"audio/mp4\"\n : \"\";\n\n const options: MediaRecorderOptions = mimeType ? { mimeType } : {};\n const mediaRecorder = new MediaRecorder(stream, options);\n mediaRecorderRef.current = mediaRecorder;\n audioChunksRef.current = [];\n\n mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n // Start recording with timeslice to collect data periodically\n mediaRecorder.start(100);\n setRecorderState(\"recording\");\n } catch (error) {\n cleanup();\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n throw new AudioRecorderError(\"Microphone permission denied\");\n }\n if (error instanceof Error && error.name === \"NotFoundError\") {\n throw new AudioRecorderError(\"No microphone found\");\n }\n throw new AudioRecorderError(\n error instanceof Error ? error.message : \"Failed to start recording\",\n );\n }\n }, [recorderState, cleanup]);\n\n // Stop recording and return audio blob\n const stop = useCallback((): Promise<Blob> => {\n return new Promise((resolve, reject) => {\n const mediaRecorder = mediaRecorderRef.current;\n if (!mediaRecorder || recorderState !== \"recording\") {\n reject(new AudioRecorderError(\"No active recording\"));\n return;\n }\n\n setRecorderState(\"processing\");\n\n mediaRecorder.onstop = () => {\n const mimeType = mediaRecorder.mimeType || \"audio/webm\";\n const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });\n\n // Clean up but keep the blob\n cleanup();\n setRecorderState(\"idle\");\n resolve(audioBlob);\n };\n\n mediaRecorder.onerror = () => {\n cleanup();\n setRecorderState(\"idle\");\n reject(new AudioRecorderError(\"Recording failed\"));\n };\n\n mediaRecorder.stop();\n });\n }, [recorderState, cleanup]);\n\n // Calculate RMS amplitude from time-domain data\n const calculateAmplitude = (dataArray: Uint8Array): number => {\n let sum = 0;\n for (let i = 0; i < dataArray.length; i++) {\n // Normalize to -1 to 1 range (128 is center/silence)\n const sample = (dataArray[i] ?? 128) / 128 - 1;\n sum += sample * sample;\n }\n return Math.sqrt(sum / dataArray.length);\n };\n\n // Canvas rendering with animation\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n // Configuration\n const barWidth = 2;\n const barGap = 1;\n const barSpacing = barWidth + barGap;\n const scrollSpeed = 1 / 3; // Pixels per frame\n\n const draw = () => {\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n // Update canvas dimensions if container resized\n if (\n canvas.width !== rect.width * dpr ||\n canvas.height !== rect.height * dpr\n ) {\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n ctx.scale(dpr, dpr);\n }\n\n // Calculate how many bars fit in the canvas (plus extra for smooth scrolling)\n const maxBars = Math.floor(rect.width / barSpacing) + 2;\n\n // Get current amplitude if recording\n if (analyserRef.current && recorderState === \"recording\") {\n // Pre-fill history with zeros on first frame so line is visible immediately\n if (amplitudeHistoryRef.current.length === 0) {\n amplitudeHistoryRef.current = new Array(maxBars).fill(0);\n }\n\n // Fade in the waveform smoothly\n if (fadeOpacityRef.current < 1) {\n fadeOpacityRef.current = Math.min(1, fadeOpacityRef.current + 0.03);\n }\n\n // Smooth scrolling - increment offset every frame\n scrollOffsetRef.current += scrollSpeed;\n\n // Sample amplitude every frame for smoothing\n const bufferLength = analyserRef.current.fftSize;\n const dataArray = new Uint8Array(bufferLength);\n analyserRef.current.getByteTimeDomainData(dataArray);\n const rawAmplitude = calculateAmplitude(dataArray);\n\n // Smoothing: gradual attack and decay\n const attackSpeed = 0.12; // Smooth rise\n const decaySpeed = 0.08; // Smooth fade out\n const speed =\n rawAmplitude > smoothedAmplitudeRef.current\n ? attackSpeed\n : decaySpeed;\n smoothedAmplitudeRef.current +=\n (rawAmplitude - smoothedAmplitudeRef.current) * speed;\n\n // When offset reaches a full bar width, add a new sample and reset offset\n if (scrollOffsetRef.current >= barSpacing) {\n scrollOffsetRef.current -= barSpacing;\n amplitudeHistoryRef.current.push(smoothedAmplitudeRef.current);\n\n // Trim history to fit canvas\n if (amplitudeHistoryRef.current.length > maxBars) {\n amplitudeHistoryRef.current =\n amplitudeHistoryRef.current.slice(-maxBars);\n }\n }\n }\n\n // Clear canvas\n ctx.clearRect(0, 0, rect.width, rect.height);\n\n // Get current foreground color\n const computedStyle = getComputedStyle(canvas);\n ctx.fillStyle = computedStyle.color;\n ctx.globalAlpha = fadeOpacityRef.current;\n\n const centerY = rect.height / 2;\n const maxAmplitude = rect.height / 2 - 2; // Leave some padding\n\n const history = amplitudeHistoryRef.current;\n\n // Only draw when recording (history has data)\n if (history.length > 0) {\n const offset = scrollOffsetRef.current;\n const edgeFadeWidth = 12; // Pixels to fade at each edge\n\n for (let i = 0; i < history.length; i++) {\n const amplitude = history[i] ?? 0;\n // Scale amplitude (RMS is typically 0-0.5 for normal speech)\n const scaledAmplitude = Math.min(amplitude * 4, 1);\n const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);\n\n // Position: right-aligned with smooth scroll offset\n const x = rect.width - (history.length - i) * barSpacing - offset;\n const y = centerY - barHeight / 2;\n\n // Only draw if visible\n if (x + barWidth > 0 && x < rect.width) {\n // Calculate edge fade opacity\n let edgeOpacity = 1;\n if (x < edgeFadeWidth) {\n // Fade out on left edge\n edgeOpacity = Math.max(0, x / edgeFadeWidth);\n } else if (x > rect.width - edgeFadeWidth) {\n // Fade in on right edge\n edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);\n }\n\n ctx.globalAlpha = fadeOpacityRef.current * edgeOpacity;\n ctx.fillRect(x, y, barWidth, barHeight);\n }\n }\n }\n\n animationIdRef.current = requestAnimationFrame(draw);\n };\n\n draw();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n };\n }, [recorderState]);\n\n // Cleanup on unmount\n useEffect(() => {\n return cleanup;\n }, [cleanup]);\n\n // Expose AudioRecorder API via ref\n useImperativeHandle(\n ref,\n () => ({\n get state() {\n return recorderState;\n },\n start,\n stop,\n dispose: cleanup,\n }),\n [recorderState, start, stop, cleanup],\n );\n\n return (\n <div className={twMerge(\"w-full py-3 px-5\", className)} {...divProps}>\n <canvas ref={canvasRef} className=\"block w-full h-[26px]\" />\n </div>\n );\n});\n\nCopilotChatAudioRecorder.displayName = \"CopilotChatAudioRecorder\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CopilotChatAudioRecorder.cjs","names":[],"sources":["../../../src/components/chat/CopilotChatAudioRecorder.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useImperativeHandle,\n forwardRef,\n useCallback,\n useState,\n} from \"react\";\nimport { twMerge } from \"tailwind-merge\";\n\n/** Finite-state machine for every recorder implementation */\nexport type AudioRecorderState = \"idle\" | \"recording\" | \"processing\";\n\n/** Error subclass so callers can `instanceof`-guard recorder failures */\nexport class AudioRecorderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AudioRecorderError\";\n }\n}\n\nexport interface AudioRecorderRef {\n state: AudioRecorderState;\n start: () => Promise<void>;\n stop: () => Promise<Blob>;\n dispose: () => void;\n}\n\nexport const CopilotChatAudioRecorder = forwardRef<\n AudioRecorderRef,\n React.HTMLAttributes<HTMLDivElement>\n>((props, ref) => {\n const { className, ...divProps } = props;\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // Recording state\n const [recorderState, setRecorderState] =\n useState<AudioRecorderState>(\"idle\");\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const streamRef = useRef<MediaStream | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const audioContextRef = useRef<AudioContext | null>(null);\n const animationIdRef = useRef<number | null>(null);\n\n // Amplitude history buffer for scrolling waveform\n const amplitudeHistoryRef = useRef<number[]>([]);\n const frameCountRef = useRef<number>(0);\n const scrollOffsetRef = useRef<number>(0);\n const smoothedAmplitudeRef = useRef<number>(0);\n const fadeOpacityRef = useRef<number>(0);\n\n // Clean up all resources\n const cleanup = useCallback(() => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n if (\n mediaRecorderRef.current &&\n mediaRecorderRef.current.state !== \"inactive\"\n ) {\n try {\n mediaRecorderRef.current.stop();\n } catch {\n // Ignore errors during cleanup\n }\n }\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }\n if (audioContextRef.current && audioContextRef.current.state !== \"closed\") {\n audioContextRef.current.close().catch(() => {\n // Ignore close errors\n });\n audioContextRef.current = null;\n }\n mediaRecorderRef.current = null;\n analyserRef.current = null;\n audioChunksRef.current = [];\n amplitudeHistoryRef.current = [];\n frameCountRef.current = 0;\n scrollOffsetRef.current = 0;\n smoothedAmplitudeRef.current = 0;\n fadeOpacityRef.current = 0;\n }, []);\n\n // Start recording\n const start = useCallback(async () => {\n if (recorderState !== \"idle\") {\n throw new AudioRecorderError(\"Recorder is already active\");\n }\n\n try {\n // Request microphone access\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n\n // Set up audio context for visualization\n const audioContext = new AudioContext();\n audioContextRef.current = audioContext;\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 2048; // Higher resolution for time-domain waveform\n source.connect(analyser);\n analyserRef.current = analyser;\n\n // Determine best MIME type for recording\n const mimeType = MediaRecorder.isTypeSupported(\"audio/webm;codecs=opus\")\n ? \"audio/webm;codecs=opus\"\n : MediaRecorder.isTypeSupported(\"audio/webm\")\n ? \"audio/webm\"\n : MediaRecorder.isTypeSupported(\"audio/mp4\")\n ? \"audio/mp4\"\n : \"\";\n\n const options: MediaRecorderOptions = mimeType ? { mimeType } : {};\n const mediaRecorder = new MediaRecorder(stream, options);\n mediaRecorderRef.current = mediaRecorder;\n audioChunksRef.current = [];\n\n mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n // Start recording with timeslice to collect data periodically\n mediaRecorder.start(100);\n setRecorderState(\"recording\");\n } catch (error) {\n cleanup();\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n throw new AudioRecorderError(\"Microphone permission denied\");\n }\n if (error instanceof Error && error.name === \"NotFoundError\") {\n throw new AudioRecorderError(\"No microphone found\");\n }\n throw new AudioRecorderError(\n error instanceof Error ? error.message : \"Failed to start recording\",\n );\n }\n }, [recorderState, cleanup]);\n\n // Stop recording and return audio blob\n const stop = useCallback((): Promise<Blob> => {\n return new Promise((resolve, reject) => {\n const mediaRecorder = mediaRecorderRef.current;\n if (!mediaRecorder || recorderState !== \"recording\") {\n reject(new AudioRecorderError(\"No active recording\"));\n return;\n }\n\n setRecorderState(\"processing\");\n\n mediaRecorder.onstop = () => {\n const mimeType = mediaRecorder.mimeType || \"audio/webm\";\n const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });\n\n // Clean up but keep the blob\n cleanup();\n setRecorderState(\"idle\");\n resolve(audioBlob);\n };\n\n mediaRecorder.onerror = () => {\n cleanup();\n setRecorderState(\"idle\");\n reject(new AudioRecorderError(\"Recording failed\"));\n };\n\n mediaRecorder.stop();\n });\n }, [recorderState, cleanup]);\n\n // Calculate RMS amplitude from time-domain data\n const calculateAmplitude = (dataArray: Uint8Array): number => {\n let sum = 0;\n for (let i = 0; i < dataArray.length; i++) {\n // Normalize to -1 to 1 range (128 is center/silence)\n const sample = (dataArray[i] ?? 128) / 128 - 1;\n sum += sample * sample;\n }\n return Math.sqrt(sum / dataArray.length);\n };\n\n // Canvas rendering with animation\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n // Configuration\n const barWidth = 2;\n const barGap = 1;\n const barSpacing = barWidth + barGap;\n const scrollSpeed = 1 / 3; // Pixels per frame\n\n const draw = () => {\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n // Update canvas dimensions if container resized\n if (\n canvas.width !== rect.width * dpr ||\n canvas.height !== rect.height * dpr\n ) {\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n ctx.scale(dpr, dpr);\n }\n\n // Calculate how many bars fit in the canvas (plus extra for smooth scrolling)\n const maxBars = Math.floor(rect.width / barSpacing) + 2;\n\n // Get current amplitude if recording\n if (analyserRef.current && recorderState === \"recording\") {\n // Pre-fill history with zeros on first frame so line is visible immediately\n if (amplitudeHistoryRef.current.length === 0) {\n amplitudeHistoryRef.current = new Array(maxBars).fill(0);\n }\n\n // Fade in the waveform smoothly\n if (fadeOpacityRef.current < 1) {\n fadeOpacityRef.current = Math.min(1, fadeOpacityRef.current + 0.03);\n }\n\n // Smooth scrolling - increment offset every frame\n scrollOffsetRef.current += scrollSpeed;\n\n // Sample amplitude every frame for smoothing\n const bufferLength = analyserRef.current.fftSize;\n const dataArray = new Uint8Array(bufferLength);\n analyserRef.current.getByteTimeDomainData(dataArray);\n const rawAmplitude = calculateAmplitude(dataArray);\n\n // Smoothing: gradual attack and decay\n const attackSpeed = 0.12; // Smooth rise\n const decaySpeed = 0.08; // Smooth fade out\n const speed =\n rawAmplitude > smoothedAmplitudeRef.current\n ? attackSpeed\n : decaySpeed;\n smoothedAmplitudeRef.current +=\n (rawAmplitude - smoothedAmplitudeRef.current) * speed;\n\n // When offset reaches a full bar width, add a new sample and reset offset\n if (scrollOffsetRef.current >= barSpacing) {\n scrollOffsetRef.current -= barSpacing;\n amplitudeHistoryRef.current.push(smoothedAmplitudeRef.current);\n\n // Trim history to fit canvas\n if (amplitudeHistoryRef.current.length > maxBars) {\n amplitudeHistoryRef.current =\n amplitudeHistoryRef.current.slice(-maxBars);\n }\n }\n }\n\n // Clear canvas\n ctx.clearRect(0, 0, rect.width, rect.height);\n\n // Get current foreground color\n const computedStyle = getComputedStyle(canvas);\n ctx.fillStyle = computedStyle.color;\n ctx.globalAlpha = fadeOpacityRef.current;\n\n const centerY = rect.height / 2;\n const maxAmplitude = rect.height / 2 - 2; // Leave some padding\n\n const history = amplitudeHistoryRef.current;\n\n // Only draw when recording (history has data)\n if (history.length > 0) {\n const offset = scrollOffsetRef.current;\n const edgeFadeWidth = 12; // Pixels to fade at each edge\n\n for (let i = 0; i < history.length; i++) {\n const amplitude = history[i] ?? 0;\n // Scale amplitude (RMS is typically 0-0.5 for normal speech)\n const scaledAmplitude = Math.min(amplitude * 4, 1);\n const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);\n\n // Position: right-aligned with smooth scroll offset\n const x = rect.width - (history.length - i) * barSpacing - offset;\n const y = centerY - barHeight / 2;\n\n // Only draw if visible\n if (x + barWidth > 0 && x < rect.width) {\n // Calculate edge fade opacity\n let edgeOpacity = 1;\n if (x < edgeFadeWidth) {\n // Fade out on left edge\n edgeOpacity = Math.max(0, x / edgeFadeWidth);\n } else if (x > rect.width - edgeFadeWidth) {\n // Fade in on right edge\n edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);\n }\n\n ctx.globalAlpha = fadeOpacityRef.current * edgeOpacity;\n ctx.fillRect(x, y, barWidth, barHeight);\n }\n }\n }\n\n animationIdRef.current = requestAnimationFrame(draw);\n };\n\n draw();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n };\n }, [recorderState]);\n\n // Cleanup on unmount\n useEffect(() => {\n return cleanup;\n }, [cleanup]);\n\n // Expose AudioRecorder API via ref\n useImperativeHandle(\n ref,\n () => ({\n get state() {\n return recorderState;\n },\n start,\n stop,\n dispose: cleanup,\n }),\n [recorderState, start, stop, cleanup],\n );\n\n return (\n <div\n className={twMerge(\"cpk:w-full cpk:py-3 cpk:px-5\", className)}\n {...divProps}\n >\n <canvas ref={canvasRef} className=\"cpk:block cpk:w-full cpk:h-[26px]\" />\n </div>\n );\n});\n\nCopilotChatAudioRecorder.displayName = \"CopilotChatAudioRecorder\";\n"],"mappings":"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"}
|
|
@@ -191,11 +191,11 @@ const CopilotChatAudioRecorder = forwardRef((props, ref) => {
|
|
|
191
191
|
cleanup
|
|
192
192
|
]);
|
|
193
193
|
return /* @__PURE__ */ jsx("div", {
|
|
194
|
-
className: twMerge("w-full py-3 px-5", className),
|
|
194
|
+
className: twMerge("cpk:w-full cpk:py-3 cpk:px-5", className),
|
|
195
195
|
...divProps,
|
|
196
196
|
children: /* @__PURE__ */ jsx("canvas", {
|
|
197
197
|
ref: canvasRef,
|
|
198
|
-
className: "block w-full h-[26px]"
|
|
198
|
+
className: "cpk:block cpk:w-full cpk:h-[26px]"
|
|
199
199
|
})
|
|
200
200
|
});
|
|
201
201
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopilotChatAudioRecorder.mjs","names":[],"sources":["../../../src/components/chat/CopilotChatAudioRecorder.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useImperativeHandle,\n forwardRef,\n useCallback,\n useState,\n} from \"react\";\nimport { twMerge } from \"tailwind-merge\";\n\n/** Finite-state machine for every recorder implementation */\nexport type AudioRecorderState = \"idle\" | \"recording\" | \"processing\";\n\n/** Error subclass so callers can `instanceof`-guard recorder failures */\nexport class AudioRecorderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AudioRecorderError\";\n }\n}\n\nexport interface AudioRecorderRef {\n state: AudioRecorderState;\n start: () => Promise<void>;\n stop: () => Promise<Blob>;\n dispose: () => void;\n}\n\nexport const CopilotChatAudioRecorder = forwardRef<\n AudioRecorderRef,\n React.HTMLAttributes<HTMLDivElement>\n>((props, ref) => {\n const { className, ...divProps } = props;\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // Recording state\n const [recorderState, setRecorderState] =\n useState<AudioRecorderState>(\"idle\");\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const streamRef = useRef<MediaStream | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const audioContextRef = useRef<AudioContext | null>(null);\n const animationIdRef = useRef<number | null>(null);\n\n // Amplitude history buffer for scrolling waveform\n const amplitudeHistoryRef = useRef<number[]>([]);\n const frameCountRef = useRef<number>(0);\n const scrollOffsetRef = useRef<number>(0);\n const smoothedAmplitudeRef = useRef<number>(0);\n const fadeOpacityRef = useRef<number>(0);\n\n // Clean up all resources\n const cleanup = useCallback(() => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n if (\n mediaRecorderRef.current &&\n mediaRecorderRef.current.state !== \"inactive\"\n ) {\n try {\n mediaRecorderRef.current.stop();\n } catch {\n // Ignore errors during cleanup\n }\n }\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }\n if (audioContextRef.current && audioContextRef.current.state !== \"closed\") {\n audioContextRef.current.close().catch(() => {\n // Ignore close errors\n });\n audioContextRef.current = null;\n }\n mediaRecorderRef.current = null;\n analyserRef.current = null;\n audioChunksRef.current = [];\n amplitudeHistoryRef.current = [];\n frameCountRef.current = 0;\n scrollOffsetRef.current = 0;\n smoothedAmplitudeRef.current = 0;\n fadeOpacityRef.current = 0;\n }, []);\n\n // Start recording\n const start = useCallback(async () => {\n if (recorderState !== \"idle\") {\n throw new AudioRecorderError(\"Recorder is already active\");\n }\n\n try {\n // Request microphone access\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n\n // Set up audio context for visualization\n const audioContext = new AudioContext();\n audioContextRef.current = audioContext;\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 2048; // Higher resolution for time-domain waveform\n source.connect(analyser);\n analyserRef.current = analyser;\n\n // Determine best MIME type for recording\n const mimeType = MediaRecorder.isTypeSupported(\"audio/webm;codecs=opus\")\n ? \"audio/webm;codecs=opus\"\n : MediaRecorder.isTypeSupported(\"audio/webm\")\n ? \"audio/webm\"\n : MediaRecorder.isTypeSupported(\"audio/mp4\")\n ? \"audio/mp4\"\n : \"\";\n\n const options: MediaRecorderOptions = mimeType ? { mimeType } : {};\n const mediaRecorder = new MediaRecorder(stream, options);\n mediaRecorderRef.current = mediaRecorder;\n audioChunksRef.current = [];\n\n mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n // Start recording with timeslice to collect data periodically\n mediaRecorder.start(100);\n setRecorderState(\"recording\");\n } catch (error) {\n cleanup();\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n throw new AudioRecorderError(\"Microphone permission denied\");\n }\n if (error instanceof Error && error.name === \"NotFoundError\") {\n throw new AudioRecorderError(\"No microphone found\");\n }\n throw new AudioRecorderError(\n error instanceof Error ? error.message : \"Failed to start recording\",\n );\n }\n }, [recorderState, cleanup]);\n\n // Stop recording and return audio blob\n const stop = useCallback((): Promise<Blob> => {\n return new Promise((resolve, reject) => {\n const mediaRecorder = mediaRecorderRef.current;\n if (!mediaRecorder || recorderState !== \"recording\") {\n reject(new AudioRecorderError(\"No active recording\"));\n return;\n }\n\n setRecorderState(\"processing\");\n\n mediaRecorder.onstop = () => {\n const mimeType = mediaRecorder.mimeType || \"audio/webm\";\n const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });\n\n // Clean up but keep the blob\n cleanup();\n setRecorderState(\"idle\");\n resolve(audioBlob);\n };\n\n mediaRecorder.onerror = () => {\n cleanup();\n setRecorderState(\"idle\");\n reject(new AudioRecorderError(\"Recording failed\"));\n };\n\n mediaRecorder.stop();\n });\n }, [recorderState, cleanup]);\n\n // Calculate RMS amplitude from time-domain data\n const calculateAmplitude = (dataArray: Uint8Array): number => {\n let sum = 0;\n for (let i = 0; i < dataArray.length; i++) {\n // Normalize to -1 to 1 range (128 is center/silence)\n const sample = (dataArray[i] ?? 128) / 128 - 1;\n sum += sample * sample;\n }\n return Math.sqrt(sum / dataArray.length);\n };\n\n // Canvas rendering with animation\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n // Configuration\n const barWidth = 2;\n const barGap = 1;\n const barSpacing = barWidth + barGap;\n const scrollSpeed = 1 / 3; // Pixels per frame\n\n const draw = () => {\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n // Update canvas dimensions if container resized\n if (\n canvas.width !== rect.width * dpr ||\n canvas.height !== rect.height * dpr\n ) {\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n ctx.scale(dpr, dpr);\n }\n\n // Calculate how many bars fit in the canvas (plus extra for smooth scrolling)\n const maxBars = Math.floor(rect.width / barSpacing) + 2;\n\n // Get current amplitude if recording\n if (analyserRef.current && recorderState === \"recording\") {\n // Pre-fill history with zeros on first frame so line is visible immediately\n if (amplitudeHistoryRef.current.length === 0) {\n amplitudeHistoryRef.current = new Array(maxBars).fill(0);\n }\n\n // Fade in the waveform smoothly\n if (fadeOpacityRef.current < 1) {\n fadeOpacityRef.current = Math.min(1, fadeOpacityRef.current + 0.03);\n }\n\n // Smooth scrolling - increment offset every frame\n scrollOffsetRef.current += scrollSpeed;\n\n // Sample amplitude every frame for smoothing\n const bufferLength = analyserRef.current.fftSize;\n const dataArray = new Uint8Array(bufferLength);\n analyserRef.current.getByteTimeDomainData(dataArray);\n const rawAmplitude = calculateAmplitude(dataArray);\n\n // Smoothing: gradual attack and decay\n const attackSpeed = 0.12; // Smooth rise\n const decaySpeed = 0.08; // Smooth fade out\n const speed =\n rawAmplitude > smoothedAmplitudeRef.current\n ? attackSpeed\n : decaySpeed;\n smoothedAmplitudeRef.current +=\n (rawAmplitude - smoothedAmplitudeRef.current) * speed;\n\n // When offset reaches a full bar width, add a new sample and reset offset\n if (scrollOffsetRef.current >= barSpacing) {\n scrollOffsetRef.current -= barSpacing;\n amplitudeHistoryRef.current.push(smoothedAmplitudeRef.current);\n\n // Trim history to fit canvas\n if (amplitudeHistoryRef.current.length > maxBars) {\n amplitudeHistoryRef.current =\n amplitudeHistoryRef.current.slice(-maxBars);\n }\n }\n }\n\n // Clear canvas\n ctx.clearRect(0, 0, rect.width, rect.height);\n\n // Get current foreground color\n const computedStyle = getComputedStyle(canvas);\n ctx.fillStyle = computedStyle.color;\n ctx.globalAlpha = fadeOpacityRef.current;\n\n const centerY = rect.height / 2;\n const maxAmplitude = rect.height / 2 - 2; // Leave some padding\n\n const history = amplitudeHistoryRef.current;\n\n // Only draw when recording (history has data)\n if (history.length > 0) {\n const offset = scrollOffsetRef.current;\n const edgeFadeWidth = 12; // Pixels to fade at each edge\n\n for (let i = 0; i < history.length; i++) {\n const amplitude = history[i] ?? 0;\n // Scale amplitude (RMS is typically 0-0.5 for normal speech)\n const scaledAmplitude = Math.min(amplitude * 4, 1);\n const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);\n\n // Position: right-aligned with smooth scroll offset\n const x = rect.width - (history.length - i) * barSpacing - offset;\n const y = centerY - barHeight / 2;\n\n // Only draw if visible\n if (x + barWidth > 0 && x < rect.width) {\n // Calculate edge fade opacity\n let edgeOpacity = 1;\n if (x < edgeFadeWidth) {\n // Fade out on left edge\n edgeOpacity = Math.max(0, x / edgeFadeWidth);\n } else if (x > rect.width - edgeFadeWidth) {\n // Fade in on right edge\n edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);\n }\n\n ctx.globalAlpha = fadeOpacityRef.current * edgeOpacity;\n ctx.fillRect(x, y, barWidth, barHeight);\n }\n }\n }\n\n animationIdRef.current = requestAnimationFrame(draw);\n };\n\n draw();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n };\n }, [recorderState]);\n\n // Cleanup on unmount\n useEffect(() => {\n return cleanup;\n }, [cleanup]);\n\n // Expose AudioRecorder API via ref\n useImperativeHandle(\n ref,\n () => ({\n get state() {\n return recorderState;\n },\n start,\n stop,\n dispose: cleanup,\n }),\n [recorderState, start, stop, cleanup],\n );\n\n return (\n <div className={twMerge(\"w-full py-3 px-5\", className)} {...divProps}>\n <canvas ref={canvasRef} className=\"block w-full h-[26px]\" />\n </div>\n );\n});\n\nCopilotChatAudioRecorder.displayName = \"CopilotChatAudioRecorder\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CopilotChatAudioRecorder.mjs","names":[],"sources":["../../../src/components/chat/CopilotChatAudioRecorder.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useImperativeHandle,\n forwardRef,\n useCallback,\n useState,\n} from \"react\";\nimport { twMerge } from \"tailwind-merge\";\n\n/** Finite-state machine for every recorder implementation */\nexport type AudioRecorderState = \"idle\" | \"recording\" | \"processing\";\n\n/** Error subclass so callers can `instanceof`-guard recorder failures */\nexport class AudioRecorderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AudioRecorderError\";\n }\n}\n\nexport interface AudioRecorderRef {\n state: AudioRecorderState;\n start: () => Promise<void>;\n stop: () => Promise<Blob>;\n dispose: () => void;\n}\n\nexport const CopilotChatAudioRecorder = forwardRef<\n AudioRecorderRef,\n React.HTMLAttributes<HTMLDivElement>\n>((props, ref) => {\n const { className, ...divProps } = props;\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // Recording state\n const [recorderState, setRecorderState] =\n useState<AudioRecorderState>(\"idle\");\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const streamRef = useRef<MediaStream | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const audioContextRef = useRef<AudioContext | null>(null);\n const animationIdRef = useRef<number | null>(null);\n\n // Amplitude history buffer for scrolling waveform\n const amplitudeHistoryRef = useRef<number[]>([]);\n const frameCountRef = useRef<number>(0);\n const scrollOffsetRef = useRef<number>(0);\n const smoothedAmplitudeRef = useRef<number>(0);\n const fadeOpacityRef = useRef<number>(0);\n\n // Clean up all resources\n const cleanup = useCallback(() => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n if (\n mediaRecorderRef.current &&\n mediaRecorderRef.current.state !== \"inactive\"\n ) {\n try {\n mediaRecorderRef.current.stop();\n } catch {\n // Ignore errors during cleanup\n }\n }\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }\n if (audioContextRef.current && audioContextRef.current.state !== \"closed\") {\n audioContextRef.current.close().catch(() => {\n // Ignore close errors\n });\n audioContextRef.current = null;\n }\n mediaRecorderRef.current = null;\n analyserRef.current = null;\n audioChunksRef.current = [];\n amplitudeHistoryRef.current = [];\n frameCountRef.current = 0;\n scrollOffsetRef.current = 0;\n smoothedAmplitudeRef.current = 0;\n fadeOpacityRef.current = 0;\n }, []);\n\n // Start recording\n const start = useCallback(async () => {\n if (recorderState !== \"idle\") {\n throw new AudioRecorderError(\"Recorder is already active\");\n }\n\n try {\n // Request microphone access\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n\n // Set up audio context for visualization\n const audioContext = new AudioContext();\n audioContextRef.current = audioContext;\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 2048; // Higher resolution for time-domain waveform\n source.connect(analyser);\n analyserRef.current = analyser;\n\n // Determine best MIME type for recording\n const mimeType = MediaRecorder.isTypeSupported(\"audio/webm;codecs=opus\")\n ? \"audio/webm;codecs=opus\"\n : MediaRecorder.isTypeSupported(\"audio/webm\")\n ? \"audio/webm\"\n : MediaRecorder.isTypeSupported(\"audio/mp4\")\n ? \"audio/mp4\"\n : \"\";\n\n const options: MediaRecorderOptions = mimeType ? { mimeType } : {};\n const mediaRecorder = new MediaRecorder(stream, options);\n mediaRecorderRef.current = mediaRecorder;\n audioChunksRef.current = [];\n\n mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n // Start recording with timeslice to collect data periodically\n mediaRecorder.start(100);\n setRecorderState(\"recording\");\n } catch (error) {\n cleanup();\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n throw new AudioRecorderError(\"Microphone permission denied\");\n }\n if (error instanceof Error && error.name === \"NotFoundError\") {\n throw new AudioRecorderError(\"No microphone found\");\n }\n throw new AudioRecorderError(\n error instanceof Error ? error.message : \"Failed to start recording\",\n );\n }\n }, [recorderState, cleanup]);\n\n // Stop recording and return audio blob\n const stop = useCallback((): Promise<Blob> => {\n return new Promise((resolve, reject) => {\n const mediaRecorder = mediaRecorderRef.current;\n if (!mediaRecorder || recorderState !== \"recording\") {\n reject(new AudioRecorderError(\"No active recording\"));\n return;\n }\n\n setRecorderState(\"processing\");\n\n mediaRecorder.onstop = () => {\n const mimeType = mediaRecorder.mimeType || \"audio/webm\";\n const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });\n\n // Clean up but keep the blob\n cleanup();\n setRecorderState(\"idle\");\n resolve(audioBlob);\n };\n\n mediaRecorder.onerror = () => {\n cleanup();\n setRecorderState(\"idle\");\n reject(new AudioRecorderError(\"Recording failed\"));\n };\n\n mediaRecorder.stop();\n });\n }, [recorderState, cleanup]);\n\n // Calculate RMS amplitude from time-domain data\n const calculateAmplitude = (dataArray: Uint8Array): number => {\n let sum = 0;\n for (let i = 0; i < dataArray.length; i++) {\n // Normalize to -1 to 1 range (128 is center/silence)\n const sample = (dataArray[i] ?? 128) / 128 - 1;\n sum += sample * sample;\n }\n return Math.sqrt(sum / dataArray.length);\n };\n\n // Canvas rendering with animation\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n // Configuration\n const barWidth = 2;\n const barGap = 1;\n const barSpacing = barWidth + barGap;\n const scrollSpeed = 1 / 3; // Pixels per frame\n\n const draw = () => {\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n // Update canvas dimensions if container resized\n if (\n canvas.width !== rect.width * dpr ||\n canvas.height !== rect.height * dpr\n ) {\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n ctx.scale(dpr, dpr);\n }\n\n // Calculate how many bars fit in the canvas (plus extra for smooth scrolling)\n const maxBars = Math.floor(rect.width / barSpacing) + 2;\n\n // Get current amplitude if recording\n if (analyserRef.current && recorderState === \"recording\") {\n // Pre-fill history with zeros on first frame so line is visible immediately\n if (amplitudeHistoryRef.current.length === 0) {\n amplitudeHistoryRef.current = new Array(maxBars).fill(0);\n }\n\n // Fade in the waveform smoothly\n if (fadeOpacityRef.current < 1) {\n fadeOpacityRef.current = Math.min(1, fadeOpacityRef.current + 0.03);\n }\n\n // Smooth scrolling - increment offset every frame\n scrollOffsetRef.current += scrollSpeed;\n\n // Sample amplitude every frame for smoothing\n const bufferLength = analyserRef.current.fftSize;\n const dataArray = new Uint8Array(bufferLength);\n analyserRef.current.getByteTimeDomainData(dataArray);\n const rawAmplitude = calculateAmplitude(dataArray);\n\n // Smoothing: gradual attack and decay\n const attackSpeed = 0.12; // Smooth rise\n const decaySpeed = 0.08; // Smooth fade out\n const speed =\n rawAmplitude > smoothedAmplitudeRef.current\n ? attackSpeed\n : decaySpeed;\n smoothedAmplitudeRef.current +=\n (rawAmplitude - smoothedAmplitudeRef.current) * speed;\n\n // When offset reaches a full bar width, add a new sample and reset offset\n if (scrollOffsetRef.current >= barSpacing) {\n scrollOffsetRef.current -= barSpacing;\n amplitudeHistoryRef.current.push(smoothedAmplitudeRef.current);\n\n // Trim history to fit canvas\n if (amplitudeHistoryRef.current.length > maxBars) {\n amplitudeHistoryRef.current =\n amplitudeHistoryRef.current.slice(-maxBars);\n }\n }\n }\n\n // Clear canvas\n ctx.clearRect(0, 0, rect.width, rect.height);\n\n // Get current foreground color\n const computedStyle = getComputedStyle(canvas);\n ctx.fillStyle = computedStyle.color;\n ctx.globalAlpha = fadeOpacityRef.current;\n\n const centerY = rect.height / 2;\n const maxAmplitude = rect.height / 2 - 2; // Leave some padding\n\n const history = amplitudeHistoryRef.current;\n\n // Only draw when recording (history has data)\n if (history.length > 0) {\n const offset = scrollOffsetRef.current;\n const edgeFadeWidth = 12; // Pixels to fade at each edge\n\n for (let i = 0; i < history.length; i++) {\n const amplitude = history[i] ?? 0;\n // Scale amplitude (RMS is typically 0-0.5 for normal speech)\n const scaledAmplitude = Math.min(amplitude * 4, 1);\n const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);\n\n // Position: right-aligned with smooth scroll offset\n const x = rect.width - (history.length - i) * barSpacing - offset;\n const y = centerY - barHeight / 2;\n\n // Only draw if visible\n if (x + barWidth > 0 && x < rect.width) {\n // Calculate edge fade opacity\n let edgeOpacity = 1;\n if (x < edgeFadeWidth) {\n // Fade out on left edge\n edgeOpacity = Math.max(0, x / edgeFadeWidth);\n } else if (x > rect.width - edgeFadeWidth) {\n // Fade in on right edge\n edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);\n }\n\n ctx.globalAlpha = fadeOpacityRef.current * edgeOpacity;\n ctx.fillRect(x, y, barWidth, barHeight);\n }\n }\n }\n\n animationIdRef.current = requestAnimationFrame(draw);\n };\n\n draw();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n };\n }, [recorderState]);\n\n // Cleanup on unmount\n useEffect(() => {\n return cleanup;\n }, [cleanup]);\n\n // Expose AudioRecorder API via ref\n useImperativeHandle(\n ref,\n () => ({\n get state() {\n return recorderState;\n },\n start,\n stop,\n dispose: cleanup,\n }),\n [recorderState, start, stop, cleanup],\n );\n\n return (\n <div\n className={twMerge(\"cpk:w-full cpk:py-3 cpk:px-5\", className)}\n {...divProps}\n >\n <canvas ref={canvasRef} className=\"cpk:block cpk:w-full cpk:h-[26px]\" />\n </div>\n );\n});\n\nCopilotChatAudioRecorder.displayName = \"CopilotChatAudioRecorder\";\n"],"mappings":"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"}
|