@pmate/chat-sdk 0.1.1
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/README.md +5 -0
- package/dist/assets/ChatInput.stories-6Z6tmO-P.js +922 -0
- package/dist/assets/Color-AVL7NMMY-CdXTB89n.js +1 -0
- package/dist/assets/DocsRenderer-PQXLIZUC-CtoiPcLN.js +1243 -0
- package/dist/assets/client-BG97-bzh.js +9 -0
- package/dist/assets/iframe-CWturJ6d.css +1 -0
- package/dist/assets/iframe-fcxekwVB.js +1071 -0
- package/dist/assets/index-u9_9PRd-.js +1 -0
- package/dist/assets/preload-helper-PPVm8Dsz.js +1 -0
- package/dist/assets/react-18-DKDc3Hbl.js +1 -0
- package/dist/favicon-wrapper.svg +46 -0
- package/dist/favicon.svg +1 -0
- package/dist/iframe.html +687 -0
- package/dist/index.html +148 -0
- package/dist/index.json +1 -0
- package/dist/nunito-sans-bold-italic.woff2 +0 -0
- package/dist/nunito-sans-bold.woff2 +0 -0
- package/dist/nunito-sans-italic.woff2 +0 -0
- package/dist/nunito-sans-regular.woff2 +0 -0
- package/dist/project.json +1 -0
- package/dist/sb-addons/docs-1/manager-bundle.js +151 -0
- package/dist/sb-addons/docs-1/manager-bundle.js.LEGAL.txt +0 -0
- package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +971 -0
- package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js.LEGAL.txt +0 -0
- package/dist/sb-common-assets/favicon-wrapper.svg +46 -0
- package/dist/sb-common-assets/favicon.svg +1 -0
- package/dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/dist/sb-manager/globals-module-info.js +799 -0
- package/dist/sb-manager/globals-runtime.js +69791 -0
- package/dist/sb-manager/globals.js +34 -0
- package/dist/sb-manager/runtime.js +13198 -0
- package/dist/vite-inject-mocker-entry.js +2 -0
- package/lib/atoms/attachmentAtoms.d.ts +4 -0
- package/lib/atoms/attachmentAtoms.js +2 -0
- package/lib/atoms/chatInputAtoms.d.ts +16 -0
- package/lib/atoms/chatInputAtoms.js +6 -0
- package/lib/atoms/voiceAtoms.d.ts +6 -0
- package/lib/atoms/voiceAtoms.js +3 -0
- package/lib/components/AttachmentPreview.d.ts +6 -0
- package/lib/components/AttachmentPreview.js +13 -0
- package/lib/components/ChatInputDesktop.d.ts +2 -0
- package/lib/components/ChatInputDesktop.js +181 -0
- package/lib/components/ChatInputMobile.d.ts +2 -0
- package/lib/components/ChatInputMobile.js +338 -0
- package/lib/components/EmojiPicker.d.ts +10 -0
- package/lib/components/EmojiPicker.js +62 -0
- package/lib/components/EmojiTrigger.d.ts +2 -0
- package/lib/components/EmojiTrigger.js +18 -0
- package/lib/components/HoldToTalkButton.d.ts +13 -0
- package/lib/components/HoldToTalkButton.js +38 -0
- package/lib/components/ImageTrigger.d.ts +2 -0
- package/lib/components/ImageTrigger.js +18 -0
- package/lib/components/InputSurface.d.ts +7 -0
- package/lib/components/InputSurface.js +26 -0
- package/lib/components/SendAction.d.ts +5 -0
- package/lib/components/SendAction.js +18 -0
- package/lib/components/StatusHint.d.ts +6 -0
- package/lib/components/StatusHint.js +16 -0
- package/lib/components/TextComposer.d.ts +5 -0
- package/lib/components/TextComposer.js +16 -0
- package/lib/components/VoiceModeSwitch.d.ts +7 -0
- package/lib/components/VoiceModeSwitch.js +14 -0
- package/lib/components/WaveformFeedback.d.ts +6 -0
- package/lib/components/WaveformFeedback.js +44 -0
- package/lib/components/icons.d.ts +11 -0
- package/lib/components/icons.js +29 -0
- package/lib/hooks/useAttachmentState.d.ts +6 -0
- package/lib/hooks/useAttachmentState.js +21 -0
- package/lib/hooks/useChatInputState.d.ts +14 -0
- package/lib/hooks/useChatInputState.js +25 -0
- package/lib/hooks/useTranscriptComposer.d.ts +1 -0
- package/lib/hooks/useTranscriptComposer.js +11 -0
- package/lib/hooks/useVoiceInputController.d.ts +14 -0
- package/lib/hooks/useVoiceInputController.js +133 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +23 -0
- package/lib/recorder-processor.js +20 -0
- package/lib/services/agentSdkASRClient.d.ts +28 -0
- package/lib/services/agentSdkASRClient.js +25 -0
- package/lib/services/authTokenResolver.d.ts +7 -0
- package/lib/services/authTokenResolver.js +7 -0
- package/lib/stories/ChatInput.stories.d.ts +15 -0
- package/lib/stories/ChatInput.stories.js +38 -0
- package/lib/tests/agentSdkASRClient.test.d.ts +1 -0
- package/lib/tests/agentSdkASRClient.test.js +39 -0
- package/lib/tests/defaultTokens.test.d.ts +1 -0
- package/lib/tests/defaultTokens.test.js +16 -0
- package/lib/tests/transcriptComposer.test.d.ts +1 -0
- package/lib/tests/transcriptComposer.test.js +10 -0
- package/lib/tokens/defaultTokens.d.ts +3 -0
- package/lib/tokens/defaultTokens.js +91 -0
- package/lib/types/attachment.d.ts +8 -0
- package/lib/types/attachment.js +1 -0
- package/lib/types/chatInput.d.ts +139 -0
- package/lib/types/chatInput.js +1 -0
- package/lib/types/voice.d.ts +17 -0
- package/lib/types/voice.js +1 -0
- package/lib/voice/MicStateManager.d.ts +43 -0
- package/lib/voice/MicStateManager.js +155 -0
- package/lib/voice/Recorder.d.ts +44 -0
- package/lib/voice/Recorder.js +224 -0
- package/package.json +59 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
export function WaveformFeedback({ levels, tokens, compact = false, }) {
|
|
4
|
+
const normalizedLevels = normalizeLevels(levels, compact ? 13 : 17);
|
|
5
|
+
return (_jsx("div", { style: {
|
|
6
|
+
display: "flex",
|
|
7
|
+
alignItems: "flex-end",
|
|
8
|
+
justifyContent: "center",
|
|
9
|
+
gap: compact ? 2 : 3,
|
|
10
|
+
minHeight: compact ? 24 : 30,
|
|
11
|
+
filter: compact ? "blur(0.15px)" : "blur(0.1px)",
|
|
12
|
+
}, children: normalizedLevels.map((level, index) => {
|
|
13
|
+
const distanceFromCenter = Math.abs(index - (normalizedLevels.length - 1) / 2);
|
|
14
|
+
const centerWeight = 1 - distanceFromCenter / ((normalizedLevels.length + 1) / 2);
|
|
15
|
+
const shapedLevel = Math.max(compact ? 0.18 : 0.1, level * (compact ? 0.82 + centerWeight * 0.9 : 0.58 + centerWeight * 0.82));
|
|
16
|
+
const baseHeight = compact ? 6 : 6;
|
|
17
|
+
const dynamicHeight = compact ? 12 : 14;
|
|
18
|
+
const opacity = compact ? 0.5 + centerWeight * 0.4 : 0.36 + centerWeight * 0.52;
|
|
19
|
+
return (_jsx("span", { style: {
|
|
20
|
+
width: compact ? 3 : 4,
|
|
21
|
+
height: `${baseHeight + Math.round(shapedLevel * dynamicHeight)}px`,
|
|
22
|
+
borderRadius: tokens.radius.waveformBar,
|
|
23
|
+
background: tokens.color.voiceWaveformActive,
|
|
24
|
+
opacity,
|
|
25
|
+
boxShadow: `0 0 ${compact ? 7 : 8}px rgba(124, 92, 255, ${compact ? 0.12 + centerWeight * 0.16 : 0.08 + centerWeight * 0.14})`,
|
|
26
|
+
transition: `height ${tokens.motion.waveformPulse}, opacity ${tokens.motion.pressFeedback}`,
|
|
27
|
+
} }, index));
|
|
28
|
+
}) }));
|
|
29
|
+
}
|
|
30
|
+
function normalizeLevels(levels, targetBars) {
|
|
31
|
+
if (levels.length === targetBars) {
|
|
32
|
+
return levels;
|
|
33
|
+
}
|
|
34
|
+
if (levels.length === 0) {
|
|
35
|
+
return Array.from({ length: targetBars }, () => 0.18);
|
|
36
|
+
}
|
|
37
|
+
return Array.from({ length: targetBars }, (_, index) => {
|
|
38
|
+
const mappedIndex = (index / Math.max(1, targetBars - 1)) * Math.max(0, levels.length - 1);
|
|
39
|
+
const left = Math.floor(mappedIndex);
|
|
40
|
+
const right = Math.min(levels.length - 1, Math.ceil(mappedIndex));
|
|
41
|
+
const mix = mappedIndex - left;
|
|
42
|
+
return levels[left] * (1 - mix) + levels[right] * mix;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type IconProps = {
|
|
2
|
+
size?: number;
|
|
3
|
+
stroke?: string;
|
|
4
|
+
strokeWidth?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function MicOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function KeyboardOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function SmileOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function PlusOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function ImageOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
function BaseIcon({ size = 22, children, }) {
|
|
4
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: children }));
|
|
5
|
+
}
|
|
6
|
+
export function MicOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.9, }) {
|
|
7
|
+
return (_jsxs(BaseIcon, { size: size, children: [_jsx("rect", { x: "9", y: "3", width: "6", height: "11", rx: "3", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M6.5 11.5C6.5 14.5376 8.96243 17 12 17C15.0376 17 17.5 14.5376 17.5 11.5", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M12 17V21", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M8.5 21H15.5", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
|
|
8
|
+
}
|
|
9
|
+
export function KeyboardOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
|
|
10
|
+
return (_jsxs(BaseIcon, { size: size, children: [_jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "3", stroke: stroke, strokeWidth: strokeWidth }), [
|
|
11
|
+
[6.5, 9],
|
|
12
|
+
[10, 9],
|
|
13
|
+
[13.5, 9],
|
|
14
|
+
[17, 9],
|
|
15
|
+
[6.5, 12.5],
|
|
16
|
+
[10, 12.5],
|
|
17
|
+
[13.5, 12.5],
|
|
18
|
+
[17, 12.5],
|
|
19
|
+
].map(([cx, cy], index) => (_jsx("circle", { cx: cx, cy: cy, r: "0.8", fill: stroke }, index))), _jsx("path", { d: "M7 16.2H17", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
|
|
20
|
+
}
|
|
21
|
+
export function SmileOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
|
|
22
|
+
return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("circle", { cx: "9", cy: "10", r: "1", fill: stroke }), _jsx("circle", { cx: "15", cy: "10", r: "1", fill: stroke }), _jsx("path", { d: "M8.2 14.2C9.15 15.55 10.42 16.2 12 16.2C13.58 16.2 14.85 15.55 15.8 14.2", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
|
|
23
|
+
}
|
|
24
|
+
export function PlusOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
|
|
25
|
+
return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M12 8V16", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M8 12H16", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
|
|
26
|
+
}
|
|
27
|
+
export function ImageOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
|
|
28
|
+
return (_jsxs(BaseIcon, { size: size, children: [_jsx("rect", { x: "4", y: "5", width: "16", height: "14", rx: "3", stroke: stroke, strokeWidth: strokeWidth }), _jsx("circle", { cx: "9", cy: "10", r: "1.6", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M7 16L11.2 12.1C11.75 11.58 12.63 11.58 13.18 12.1L17 15.6", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useChatInputState } from "./useChatInputState";
|
|
2
|
+
export function useAttachmentState() {
|
|
3
|
+
const { attachments, setAttachments, setStatus } = useChatInputState();
|
|
4
|
+
return {
|
|
5
|
+
attachments,
|
|
6
|
+
addAttachment(attachment) {
|
|
7
|
+
setAttachments([
|
|
8
|
+
...attachments,
|
|
9
|
+
{
|
|
10
|
+
id: crypto.randomUUID(),
|
|
11
|
+
kind: "image",
|
|
12
|
+
...attachment,
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
setStatus("attachment-selected");
|
|
16
|
+
},
|
|
17
|
+
removeAttachment(id) {
|
|
18
|
+
setAttachments(attachments.filter((item) => item.id !== id));
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function useChatInputState(): {
|
|
2
|
+
mode: import("..").ChatInputMode;
|
|
3
|
+
setMode: (args_0: import("..").ChatInputMode | ((prev: import("..").ChatInputMode) => import("..").ChatInputMode)) => void;
|
|
4
|
+
status: import("..").ChatInputStatus;
|
|
5
|
+
setStatus: (args_0: import("..").ChatInputStatus | ((prev: import("..").ChatInputStatus) => import("..").ChatInputStatus)) => void;
|
|
6
|
+
text: string;
|
|
7
|
+
setText: (args_0: string | ((prev: string) => string)) => void;
|
|
8
|
+
transcript: string;
|
|
9
|
+
setTranscript: (args_0: string | ((prev: string) => string)) => void;
|
|
10
|
+
error: string | null;
|
|
11
|
+
setError: (args_0: string | ((prev: string | null) => string | null) | null) => void;
|
|
12
|
+
attachments: import("..").ChatAttachment[];
|
|
13
|
+
setAttachments: (args_0: import("..").ChatAttachment[] | ((prev: import("..").ChatAttachment[]) => import("..").ChatAttachment[])) => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useAtom } from "jotai";
|
|
2
|
+
import { chatInputErrorAtom, chatInputModeAtom, chatInputStatusAtom, chatInputTextAtom, chatInputTranscriptAtom, } from "../atoms/chatInputAtoms";
|
|
3
|
+
import { attachmentListAtom } from "../atoms/attachmentAtoms";
|
|
4
|
+
export function useChatInputState() {
|
|
5
|
+
const [mode, setMode] = useAtom(chatInputModeAtom);
|
|
6
|
+
const [status, setStatus] = useAtom(chatInputStatusAtom);
|
|
7
|
+
const [text, setText] = useAtom(chatInputTextAtom);
|
|
8
|
+
const [transcript, setTranscript] = useAtom(chatInputTranscriptAtom);
|
|
9
|
+
const [error, setError] = useAtom(chatInputErrorAtom);
|
|
10
|
+
const [attachments, setAttachments] = useAtom(attachmentListAtom);
|
|
11
|
+
return {
|
|
12
|
+
mode,
|
|
13
|
+
setMode,
|
|
14
|
+
status,
|
|
15
|
+
setStatus,
|
|
16
|
+
text,
|
|
17
|
+
setText,
|
|
18
|
+
transcript,
|
|
19
|
+
setTranscript,
|
|
20
|
+
error,
|
|
21
|
+
setError,
|
|
22
|
+
attachments,
|
|
23
|
+
setAttachments,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function insertTranscriptText(currentText: string, transcript: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChatVoiceConfig } from "../types/voice";
|
|
2
|
+
type StopAction = "send" | "transcribe";
|
|
3
|
+
export declare function useVoiceInputController(config?: ChatVoiceConfig): {
|
|
4
|
+
mode: import("..").ChatInputMode;
|
|
5
|
+
status: import("..").ChatInputStatus;
|
|
6
|
+
waveformLevels: number[];
|
|
7
|
+
durationMs: number;
|
|
8
|
+
beginVoiceMode: () => void;
|
|
9
|
+
setMode: (args_0: import("..").ChatInputMode | ((prev: import("..").ChatInputMode) => import("..").ChatInputMode)) => void;
|
|
10
|
+
startRecording: () => void;
|
|
11
|
+
cancelRecording: (message?: string | null) => void;
|
|
12
|
+
stopRecording: (action?: StopAction) => void;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useAtom } from "jotai";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { waveformLevelsAtom, recordingDurationMsAtom } from "../atoms/voiceAtoms";
|
|
4
|
+
import { insertTranscriptText } from "./useTranscriptComposer";
|
|
5
|
+
import { useChatInputState } from "./useChatInputState";
|
|
6
|
+
import { MicEvents, MicStateManager } from "../voice/MicStateManager";
|
|
7
|
+
const DEFAULT_WAVEFORM = [
|
|
8
|
+
0.18, 0.28, 0.42, 0.58, 0.74, 0.9, 0.74, 0.58, 0.42, 0.28, 0.18,
|
|
9
|
+
];
|
|
10
|
+
export function useVoiceInputController(config) {
|
|
11
|
+
const managerRef = useRef(null);
|
|
12
|
+
const stopActionRef = useRef("send");
|
|
13
|
+
const abortMessageRef = useRef(null);
|
|
14
|
+
const [levels, setLevels] = useAtom(waveformLevelsAtom);
|
|
15
|
+
const [durationMs, setDurationMs] = useAtom(recordingDurationMsAtom);
|
|
16
|
+
const { mode, setMode, status, setStatus, text, setText, setTranscript, setError, } = useChatInputState();
|
|
17
|
+
const modeRef = useRef(mode);
|
|
18
|
+
const textRef = useRef(text);
|
|
19
|
+
const configRef = useRef(config);
|
|
20
|
+
modeRef.current = mode;
|
|
21
|
+
textRef.current = text;
|
|
22
|
+
configRef.current = config;
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const manager = new MicStateManager();
|
|
25
|
+
manager.init();
|
|
26
|
+
managerRef.current = manager;
|
|
27
|
+
const unsubData = manager.on(MicEvents.Data, () => {
|
|
28
|
+
setDurationMs((value) => value + 120);
|
|
29
|
+
});
|
|
30
|
+
const unsubLevel = manager.on(MicEvents.Level, (nextLevels) => {
|
|
31
|
+
setLevels(nextLevels);
|
|
32
|
+
});
|
|
33
|
+
const unsubFinished = manager.on(MicEvents.Finished, (blob) => {
|
|
34
|
+
setLevels(DEFAULT_WAVEFORM);
|
|
35
|
+
setDurationMs(0);
|
|
36
|
+
if (stopActionRef.current === "transcribe") {
|
|
37
|
+
setStatus("transcribing");
|
|
38
|
+
void transcribeBlob(blob);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setTranscript("");
|
|
42
|
+
setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
|
|
43
|
+
setError(null);
|
|
44
|
+
void configRef.current?.onVoiceSend?.(blob);
|
|
45
|
+
});
|
|
46
|
+
const unsubAbort = manager.on(MicEvents.Abort, () => {
|
|
47
|
+
setLevels(DEFAULT_WAVEFORM);
|
|
48
|
+
setDurationMs(0);
|
|
49
|
+
setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
|
|
50
|
+
setError(abortMessageRef.current);
|
|
51
|
+
abortMessageRef.current = null;
|
|
52
|
+
});
|
|
53
|
+
const unsubError = manager.on(MicEvents.Error, (error) => {
|
|
54
|
+
setLevels(DEFAULT_WAVEFORM);
|
|
55
|
+
setDurationMs(0);
|
|
56
|
+
setStatus("error");
|
|
57
|
+
setError(error.message);
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
unsubData();
|
|
61
|
+
unsubLevel();
|
|
62
|
+
unsubFinished();
|
|
63
|
+
unsubAbort();
|
|
64
|
+
unsubError();
|
|
65
|
+
manager.dispose();
|
|
66
|
+
managerRef.current = null;
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
async function transcribeBlob(blob) {
|
|
70
|
+
try {
|
|
71
|
+
const nextTranscript = configRef.current?.asrClient
|
|
72
|
+
? (await configRef.current.asrClient.transcribe(blob, {
|
|
73
|
+
lang: configRef.current.lang,
|
|
74
|
+
})).text.trim()
|
|
75
|
+
: (configRef.current?.simulatedTranscript ?? "hello from transcript").trim();
|
|
76
|
+
if (!nextTranscript) {
|
|
77
|
+
setStatus("error");
|
|
78
|
+
setError("No speech was recognized.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setTranscript(nextTranscript);
|
|
82
|
+
setText(insertTranscriptText(textRef.current, nextTranscript));
|
|
83
|
+
setDurationMs(0);
|
|
84
|
+
setStatus("transcript-ready");
|
|
85
|
+
setError(null);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
setStatus("error");
|
|
89
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function beginVoiceMode() {
|
|
93
|
+
setMode("voice");
|
|
94
|
+
setStatus("voice-mode-ready");
|
|
95
|
+
setError(null);
|
|
96
|
+
}
|
|
97
|
+
function startRecording() {
|
|
98
|
+
if (status === "recording") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setStatus("recording");
|
|
102
|
+
setError(null);
|
|
103
|
+
setTranscript("");
|
|
104
|
+
setDurationMs(0);
|
|
105
|
+
setLevels(DEFAULT_WAVEFORM);
|
|
106
|
+
abortMessageRef.current = null;
|
|
107
|
+
stopActionRef.current = "send";
|
|
108
|
+
void managerRef.current?.start();
|
|
109
|
+
}
|
|
110
|
+
function cancelRecording(message = null) {
|
|
111
|
+
abortMessageRef.current = message;
|
|
112
|
+
managerRef.current?.abort();
|
|
113
|
+
}
|
|
114
|
+
function stopRecording(action = "send") {
|
|
115
|
+
if (durationMs < (configRef.current?.minDurationMs ?? 450)) {
|
|
116
|
+
cancelRecording(configRef.current?.shortRecordingMessage ?? "Hold a little longer to record.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
stopActionRef.current = action;
|
|
120
|
+
managerRef.current?.stop();
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
mode,
|
|
124
|
+
status,
|
|
125
|
+
waveformLevels: levels,
|
|
126
|
+
durationMs,
|
|
127
|
+
beginVoiceMode,
|
|
128
|
+
setMode,
|
|
129
|
+
startRecording,
|
|
130
|
+
cancelRecording,
|
|
131
|
+
stopRecording,
|
|
132
|
+
};
|
|
133
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from "./components/AttachmentPreview";
|
|
2
|
+
export * from "./components/ChatInputDesktop";
|
|
3
|
+
export * from "./components/ChatInputMobile";
|
|
4
|
+
export * from "./components/EmojiTrigger";
|
|
5
|
+
export * from "./components/HoldToTalkButton";
|
|
6
|
+
export * from "./components/ImageTrigger";
|
|
7
|
+
export * from "./components/InputSurface";
|
|
8
|
+
export * from "./components/SendAction";
|
|
9
|
+
export * from "./components/StatusHint";
|
|
10
|
+
export * from "./components/TextComposer";
|
|
11
|
+
export * from "./components/VoiceModeSwitch";
|
|
12
|
+
export * from "./components/WaveformFeedback";
|
|
13
|
+
export * from "./hooks/useAttachmentState";
|
|
14
|
+
export * from "./hooks/useChatInputState";
|
|
15
|
+
export * from "./hooks/useTranscriptComposer";
|
|
16
|
+
export * from "./hooks/useVoiceInputController";
|
|
17
|
+
export * from "./services/agentSdkASRClient";
|
|
18
|
+
export * from "./services/authTokenResolver";
|
|
19
|
+
export * from "./tokens/defaultTokens";
|
|
20
|
+
export * from "./types/attachment";
|
|
21
|
+
export * from "./types/chatInput";
|
|
22
|
+
export * from "./types/voice";
|
|
23
|
+
export * from "./voice/Recorder";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from "./components/AttachmentPreview";
|
|
2
|
+
export * from "./components/ChatInputDesktop";
|
|
3
|
+
export * from "./components/ChatInputMobile";
|
|
4
|
+
export * from "./components/EmojiTrigger";
|
|
5
|
+
export * from "./components/HoldToTalkButton";
|
|
6
|
+
export * from "./components/ImageTrigger";
|
|
7
|
+
export * from "./components/InputSurface";
|
|
8
|
+
export * from "./components/SendAction";
|
|
9
|
+
export * from "./components/StatusHint";
|
|
10
|
+
export * from "./components/TextComposer";
|
|
11
|
+
export * from "./components/VoiceModeSwitch";
|
|
12
|
+
export * from "./components/WaveformFeedback";
|
|
13
|
+
export * from "./hooks/useAttachmentState";
|
|
14
|
+
export * from "./hooks/useChatInputState";
|
|
15
|
+
export * from "./hooks/useTranscriptComposer";
|
|
16
|
+
export * from "./hooks/useVoiceInputController";
|
|
17
|
+
export * from "./services/agentSdkASRClient";
|
|
18
|
+
export * from "./services/authTokenResolver";
|
|
19
|
+
export * from "./tokens/defaultTokens";
|
|
20
|
+
export * from "./types/attachment";
|
|
21
|
+
export * from "./types/chatInput";
|
|
22
|
+
export * from "./types/voice";
|
|
23
|
+
export * from "./voice/Recorder";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class RecorderProcessor extends AudioWorkletProcessor {
|
|
2
|
+
process(inputs) {
|
|
3
|
+
const input = inputs[0]
|
|
4
|
+
if (!input || !input[0]) {
|
|
5
|
+
return true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const samples = input[0]
|
|
9
|
+
const pcm = new Int16Array(samples.length)
|
|
10
|
+
for (let i = 0; i < samples.length; i += 1) {
|
|
11
|
+
const sample = Math.max(-1, Math.min(1, samples[i]))
|
|
12
|
+
pcm[i] = sample < 0 ? sample * 0x8000 : sample * 0x7fff
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.port.postMessage({ type: "pcm", data: pcm.buffer }, [pcm.buffer])
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
registerProcessor("pmate-mic-recorder-processor", RecorderProcessor)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AsyncStream } from "@pmate/utils";
|
|
2
|
+
export type AgentSdkStreamLike = {
|
|
3
|
+
stream<TFinal = unknown, TChunk = Blob>(request: {
|
|
4
|
+
agentId: string;
|
|
5
|
+
stream: AsyncStream<TChunk>;
|
|
6
|
+
params?: Record<string, unknown>;
|
|
7
|
+
minChunkSizeBytes?: number;
|
|
8
|
+
}): {
|
|
9
|
+
finish(): Promise<TFinal | null>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type AgentSdkASRClientOptions = {
|
|
13
|
+
client: AgentSdkStreamLike;
|
|
14
|
+
agentId: string;
|
|
15
|
+
tokenResolver?: {
|
|
16
|
+
getToken(): string;
|
|
17
|
+
};
|
|
18
|
+
minChunkSizeBytes?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare class AgentSdkASRClient {
|
|
21
|
+
private readonly options;
|
|
22
|
+
constructor(options: AgentSdkASRClientOptions);
|
|
23
|
+
transcribe(blob: Blob, input?: {
|
|
24
|
+
lang?: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
text: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AsyncStream } from "@pmate/utils";
|
|
2
|
+
export class AgentSdkASRClient {
|
|
3
|
+
options;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.options = options;
|
|
6
|
+
}
|
|
7
|
+
async transcribe(blob, input) {
|
|
8
|
+
const token = this.options.tokenResolver?.getToken() ?? "";
|
|
9
|
+
const stream = new AsyncStream();
|
|
10
|
+
const task = this.options.client.stream({
|
|
11
|
+
agentId: this.options.agentId,
|
|
12
|
+
stream,
|
|
13
|
+
params: {
|
|
14
|
+
lang: input?.lang ?? "auto",
|
|
15
|
+
token,
|
|
16
|
+
},
|
|
17
|
+
minChunkSizeBytes: this.options.minChunkSizeBytes ?? 0,
|
|
18
|
+
});
|
|
19
|
+
stream.push(blob);
|
|
20
|
+
stream.end();
|
|
21
|
+
return {
|
|
22
|
+
text: String((await task.finish()) ?? "").trim(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react-vite";
|
|
2
|
+
declare const meta: {
|
|
3
|
+
title: string;
|
|
4
|
+
parameters: {
|
|
5
|
+
layout: string;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj<typeof meta>;
|
|
10
|
+
export declare const MobileIdle: Story;
|
|
11
|
+
export declare const MobileVoiceReady: Story;
|
|
12
|
+
export declare const MobileRecording: Story;
|
|
13
|
+
export declare const DesktopTyping: Story;
|
|
14
|
+
export declare const DesktopAttachment: Story;
|
|
15
|
+
export declare const WaveformActive: Story;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ChatInputDesktop } from "../components/ChatInputDesktop";
|
|
3
|
+
import { ChatInputMobile } from "../components/ChatInputMobile";
|
|
4
|
+
import { WaveformFeedback } from "../components/WaveformFeedback";
|
|
5
|
+
import { mergeChatInputTokens } from "../tokens/defaultTokens";
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "@pmate/chat-sdk/ChatInput",
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export default meta;
|
|
13
|
+
export const MobileIdle = {
|
|
14
|
+
render: () => _jsx("div", { style: { width: 360 }, children: _jsx(ChatInputMobile, {}) }),
|
|
15
|
+
};
|
|
16
|
+
export const MobileVoiceReady = {
|
|
17
|
+
render: () => (_jsx("div", { style: { width: 360 }, children: _jsx(ChatInputMobile, { initialState: { mode: "voice", status: "voice-mode-ready" } }) })),
|
|
18
|
+
};
|
|
19
|
+
export const MobileRecording = {
|
|
20
|
+
render: () => (_jsx("div", { style: { width: 360 }, children: _jsx(ChatInputMobile, { initialState: {
|
|
21
|
+
mode: "voice",
|
|
22
|
+
status: "recording",
|
|
23
|
+
waveformLevels: [0.25, 0.5, 0.88, 0.52, 0.28],
|
|
24
|
+
} }) })),
|
|
25
|
+
};
|
|
26
|
+
export const DesktopTyping = {
|
|
27
|
+
render: () => (_jsx("div", { style: { width: 720 }, children: _jsx(ChatInputDesktop, { initialState: { text: "Ship chat-sdk storybook first", status: "typing" } }) })),
|
|
28
|
+
};
|
|
29
|
+
export const DesktopAttachment = {
|
|
30
|
+
render: () => (_jsx("div", { style: { width: 720 }, children: _jsx(ChatInputDesktop, { initialState: {
|
|
31
|
+
text: "Desktop attachment state",
|
|
32
|
+
status: "attachment-selected",
|
|
33
|
+
attachments: [{ id: "1", kind: "image", name: "spec.png" }],
|
|
34
|
+
} }) })),
|
|
35
|
+
};
|
|
36
|
+
export const WaveformActive = {
|
|
37
|
+
render: () => (_jsx(WaveformFeedback, { levels: [0.2, 0.5, 0.9, 0.55, 0.3], tokens: mergeChatInputTokens() })),
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AgentSdkASRClient, } from "../services/agentSdkASRClient";
|
|
3
|
+
describe("AgentSdkASRClient", () => {
|
|
4
|
+
it("streams audio blobs through agent-sdk and returns final text", async () => {
|
|
5
|
+
const seen = {
|
|
6
|
+
blobSizes: [],
|
|
7
|
+
};
|
|
8
|
+
const client = new AgentSdkASRClient({
|
|
9
|
+
client: {
|
|
10
|
+
stream(request) {
|
|
11
|
+
seen.agentId = request.agentId;
|
|
12
|
+
seen.lang = String(request.params?.lang ?? "");
|
|
13
|
+
seen.token = String(request.params?.token ?? "");
|
|
14
|
+
return {
|
|
15
|
+
async finish() {
|
|
16
|
+
for await (const chunk of request.stream) {
|
|
17
|
+
seen.blobSizes.push(chunk.size);
|
|
18
|
+
}
|
|
19
|
+
return "transcribed text";
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
agentId: "agent:transcribe",
|
|
25
|
+
tokenResolver: {
|
|
26
|
+
getToken: () => "user-token",
|
|
27
|
+
},
|
|
28
|
+
minChunkSizeBytes: 0,
|
|
29
|
+
});
|
|
30
|
+
const result = await client.transcribe(new Blob(["voice-bytes"]), {
|
|
31
|
+
lang: "en",
|
|
32
|
+
});
|
|
33
|
+
expect(result.text).toBe("transcribed text");
|
|
34
|
+
expect(seen.agentId).toBe("agent:transcribe");
|
|
35
|
+
expect(seen.lang).toBe("en");
|
|
36
|
+
expect(seen.token).toBe("user-token");
|
|
37
|
+
expect(seen.blobSizes).toEqual([11]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { defaultChatInputTokens, mergeChatInputTokens } from "../tokens/defaultTokens";
|
|
3
|
+
describe("chat-sdk tokens", () => {
|
|
4
|
+
it("keeps purple brand as default", () => {
|
|
5
|
+
expect(defaultChatInputTokens.color.brandPrimary).toBe("#6D4AFF");
|
|
6
|
+
});
|
|
7
|
+
it("merges nested token overrides", () => {
|
|
8
|
+
const merged = mergeChatInputTokens({
|
|
9
|
+
color: {
|
|
10
|
+
brandPrimary: "#000000",
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
expect(merged.color.brandPrimary).toBe("#000000");
|
|
14
|
+
expect(merged.color.voiceWaveformActive).toBe(defaultChatInputTokens.color.voiceWaveformActive);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { insertTranscriptText } from "../hooks/useTranscriptComposer";
|
|
3
|
+
describe("insertTranscriptText", () => {
|
|
4
|
+
it("returns transcript when current text is empty", () => {
|
|
5
|
+
expect(insertTranscriptText("", "hello")).toBe("hello");
|
|
6
|
+
});
|
|
7
|
+
it("appends transcript to existing text", () => {
|
|
8
|
+
expect(insertTranscriptText("draft", "hello")).toBe("draft hello");
|
|
9
|
+
});
|
|
10
|
+
});
|