@pmate/chat-sdk 0.1.10 → 0.1.12

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.
@@ -30,6 +30,7 @@ function DesktopInner(props) {
30
30
  const fileInputRef = useRef(null);
31
31
  const voice = useVoiceInputController({
32
32
  ...props.voice,
33
+ onVoiceTranscript: props.voice?.onVoiceTranscript,
33
34
  onVoiceSend: async (blob) => {
34
35
  await props.voice?.onVoiceSend?.(blob);
35
36
  await props.onSend?.({
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Provider, createStore } from "jotai";
4
4
  import { useEffect, useRef, useState } from "react";
@@ -35,6 +35,7 @@ function MobileInner(props) {
35
35
  const fileInputRef = useRef(null);
36
36
  const voice = useVoiceInputController({
37
37
  ...props.voice,
38
+ onVoiceTranscript: props.voice?.onVoiceTranscript,
38
39
  onVoiceSend: async (blob) => {
39
40
  await props.onSend?.({
40
41
  text: "",
@@ -196,14 +197,16 @@ function MobileInner(props) {
196
197
  boxShadow: "none",
197
198
  overflow: "visible",
198
199
  }, children: [isRecordingOverlayVisible ? (_jsxs("div", { style: {
199
- position: "absolute",
200
- left: 0,
201
- right: 0,
202
- bottom: 72,
200
+ position: "fixed",
201
+ left: "50%",
202
+ bottom: "calc(72px + env(safe-area-inset-bottom, 0px))",
203
+ width: "min(420px, calc(100vw - 16px))",
204
+ transform: "translateX(-50%)",
203
205
  height: 250,
204
206
  pointerEvents: "none",
205
207
  background: "linear-gradient(180deg, rgba(22, 16, 45, 0) 0%, rgba(22, 16, 45, 0.24) 34%, rgba(22, 16, 45, 0.6) 100%)",
206
208
  borderRadius: 28,
209
+ zIndex: 1000,
207
210
  }, children: [_jsxs("div", { style: {
208
211
  position: "absolute",
209
212
  left: "50%",
@@ -313,7 +316,7 @@ function MobileInner(props) {
313
316
  gridTemplateColumns: "30px minmax(0, 1fr) 30px 30px",
314
317
  gap: 4,
315
318
  alignItems: "center",
316
- padding: "0 0 env(safe-area-inset-bottom, 0px)",
319
+ padding: 0,
317
320
  }, children: [_jsx(EmojiPicker, { open: isEmojiOpen, isMobile: true, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx("button", { type: "button", "aria-label": state.mode === "voice" ? "切换到文字输入" : "切换到语音输入", disabled: props.disabled || props.enableVoice === false, onClick: () => {
318
321
  const next = state.mode === "voice" ? "text" : "voice";
319
322
  state.setMode(next);
@@ -325,6 +328,7 @@ function MobileInner(props) {
325
328
  : tokens.color.iconDefault,
326
329
  opacity: props.disabled ? 0.45 : 1,
327
330
  }, children: state.mode === "voice" ? (_jsx(KeyboardOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
331
+ gridColumn: state.mode === "voice" ? "2 / 5" : undefined,
328
332
  borderRadius: tokens.radius.inputSurface,
329
333
  background: state.status === "error"
330
334
  ? tokens.color.surfaceInputError
@@ -342,8 +346,8 @@ function MobileInner(props) {
342
346
  minHeight: 48,
343
347
  display: "flex",
344
348
  alignItems: "center",
345
- padding: state.mode === "voice" ? "0" : "0 12px",
346
- }, children: state.mode === "voice" ? (_jsx("div", { style: { minHeight: 48 }, children: _jsx(HoldToTalkButton, { tokens: tokens, active: isRecordingOverlayVisible, disabled: props.disabled, text: "\u6309\u4F4F \u8BF4\u8BDD", activeText: "\u677E\u5F00 \u53D1\u9001", onPressStart: () => {
349
+ padding: state.mode === "voice" ? 0 : "0 12px",
350
+ }, children: state.mode === "voice" ? (_jsx("div", { style: { minHeight: 48, width: "100%" }, children: _jsx(HoldToTalkButton, { tokens: tokens, active: isRecordingOverlayVisible, disabled: props.disabled, text: "\u6309\u4F4F \u8BF4\u8BDD", activeText: "\u677E\u5F00 \u53D1\u9001", onPressStart: () => {
347
351
  setRecordingChoice("send");
348
352
  voice.startRecording();
349
353
  }, onPressMove: (event) => {
@@ -386,23 +390,23 @@ function MobileInner(props) {
386
390
  fontSize: tokens.typography.inputText.size,
387
391
  lineHeight: `${tokens.typography.inputText.lineHeight}px`,
388
392
  caretColor: tokens.color.brandPrimary,
389
- } })) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, onClick: () => {
390
- setIsMoreOpen(false);
391
- setIsEmojiOpen((prev) => !prev);
392
- }, style: {
393
- ...sideActionStyle,
394
- width: 30,
395
- height: 30,
396
- } }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
397
- setIsEmojiOpen(false);
398
- setIsMoreOpen((prev) => !prev);
399
- }, disabled: props.disabled, style: {
400
- ...sideActionStyle,
401
- color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
402
- width: 30,
403
- height: 30,
404
- opacity: props.disabled ? 0.4 : 1,
405
- } })] }), _jsx(StatusHint, { tokens: tokens, text: state.status === "recording" || state.status === "transcribing" ? undefined : statusText, tone: state.status === "error" ? "error" : "secondary" })] }));
393
+ } })) }), state.mode === "voice" ? null : (_jsxs(_Fragment, { children: [_jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, onClick: () => {
394
+ setIsMoreOpen(false);
395
+ setIsEmojiOpen((prev) => !prev);
396
+ }, style: {
397
+ ...sideActionStyle,
398
+ width: 30,
399
+ height: 30,
400
+ } }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
401
+ setIsEmojiOpen(false);
402
+ setIsMoreOpen((prev) => !prev);
403
+ }, disabled: props.disabled, style: {
404
+ ...sideActionStyle,
405
+ color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
406
+ width: 30,
407
+ height: 30,
408
+ opacity: props.disabled ? 0.4 : 1,
409
+ } })] }))] }), _jsx(StatusHint, { tokens: tokens, text: state.status === "recording" || state.status === "transcribing" ? undefined : statusText, tone: state.status === "error" ? "error" : "secondary" })] }));
406
410
  }
407
411
  export function ChatInputMobile(props) {
408
412
  const stateKey = JSON.stringify({
@@ -24,6 +24,10 @@ export function HoldToTalkButton({ tokens, active, disabled, text = "按住 说
24
24
  boxShadow: "none",
25
25
  padding: "0 16px",
26
26
  minHeight: 48,
27
+ height: "100%",
28
+ display: "flex",
29
+ alignItems: "center",
30
+ justifyContent: "center",
27
31
  fontFamily: tokens.typography.familyBase,
28
32
  fontSize: tokens.typography.voiceHoldToTalk.size,
29
33
  lineHeight: `${tokens.typography.voiceHoldToTalk.lineHeight}px`,
@@ -8,7 +8,6 @@ export function WaveformFeedback({ levels, tokens, compact = false, }) {
8
8
  justifyContent: "center",
9
9
  gap: compact ? 2 : 3,
10
10
  minHeight: compact ? 24 : 30,
11
- filter: compact ? "blur(0.15px)" : "blur(0.1px)",
12
11
  }, children: normalizedLevels.map((level, index) => {
13
12
  const distanceFromCenter = Math.abs(index - (normalizedLevels.length - 1) / 2);
14
13
  const centerWeight = 1 - distanceFromCenter / ((normalizedLevels.length + 1) / 2);
@@ -36,13 +36,15 @@ export function useVoiceInputController(config) {
36
36
  setDurationMs(0);
37
37
  if (stopActionRef.current === "transcribe") {
38
38
  setStatus("transcribing");
39
- void transcribeBlob(blob);
39
+ void transcribeBlob(blob, "transcribe");
40
40
  return;
41
41
  }
42
- setTranscript("");
43
- setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
44
- setError(null);
45
- void configRef.current?.onVoiceSend?.(blob);
42
+ if (configRef.current?.asrClient || configRef.current?.simulatedTranscript) {
43
+ setStatus("transcribing");
44
+ void transcribeBlob(blob, "send");
45
+ return;
46
+ }
47
+ finishVoiceSend(blob);
46
48
  });
47
49
  const unsubAbort = manager.on(MicEvents.Abort, () => {
48
50
  setLevels(DEFAULT_WAVEFORM);
@@ -67,7 +69,7 @@ export function useVoiceInputController(config) {
67
69
  managerRef.current = null;
68
70
  };
69
71
  }, []);
70
- async function transcribeBlob(blob) {
72
+ async function transcribeBlob(blob, action) {
71
73
  try {
72
74
  const transcribeTimeoutMs = configRef.current?.transcribeTimeoutMs ?? DEFAULT_TRANSCRIBE_TIMEOUT_MS;
73
75
  const nextTranscript = configRef.current?.asrClient
@@ -80,6 +82,17 @@ export function useVoiceInputController(config) {
80
82
  setError("No speech was recognized.");
81
83
  return;
82
84
  }
85
+ if (action === "send") {
86
+ setTranscript("");
87
+ setDurationMs(0);
88
+ setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
89
+ setError(null);
90
+ await configRef.current?.onVoiceTranscript?.(nextTranscript, {
91
+ blob,
92
+ action,
93
+ });
94
+ return;
95
+ }
83
96
  setTranscript(nextTranscript);
84
97
  setText(insertTranscriptText(textRef.current, nextTranscript));
85
98
  setDurationMs(0);
@@ -91,6 +104,12 @@ export function useVoiceInputController(config) {
91
104
  setError(error instanceof Error ? error.message : String(error));
92
105
  }
93
106
  }
107
+ function finishVoiceSend(blob) {
108
+ setTranscript("");
109
+ setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
110
+ setError(null);
111
+ void configRef.current?.onVoiceSend?.(blob);
112
+ }
94
113
  function beginVoiceMode() {
95
114
  setMode("voice");
96
115
  setStatus("voice-mode-ready");
package/lib/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from "./services/authTokenResolver";
19
19
  export * from "./runtime/botAuth";
20
20
  export * from "./runtime/botKey";
21
21
  export * from "./runtime/chatClient";
22
+ export * from "./runtime/readerClient";
22
23
  export * from "./runtime/thread";
23
24
  export * from "./tokens/defaultTokens";
24
25
  export * from "./tokens/chatInputThemes";
package/lib/index.js CHANGED
@@ -19,6 +19,7 @@ export * from "./services/authTokenResolver";
19
19
  export * from "./runtime/botAuth";
20
20
  export * from "./runtime/botKey";
21
21
  export * from "./runtime/chatClient";
22
+ export * from "./runtime/readerClient";
22
23
  export * from "./runtime/thread";
23
24
  export * from "./tokens/defaultTokens";
24
25
  export * from "./tokens/chatInputThemes";
@@ -2,4 +2,5 @@ export * from "./botAuth.js";
2
2
  export * from "./botKey.js";
3
3
  export * from "./chatClient.js";
4
4
  export * from "./protocol.js";
5
+ export * from "./readerClient.js";
5
6
  export * from "./thread.js";
@@ -2,4 +2,5 @@ export * from "./botAuth.js";
2
2
  export * from "./botKey.js";
3
3
  export * from "./chatClient.js";
4
4
  export * from "./protocol.js";
5
+ export * from "./readerClient.js";
5
6
  export * from "./thread.js";
@@ -0,0 +1,97 @@
1
+ export type ChatReaderClientOptions = {
2
+ baseUrl?: string;
3
+ appId: string;
4
+ chainId?: string;
5
+ socialChainId?: string;
6
+ fetchImpl?: typeof fetch;
7
+ };
8
+ export type ChatReaderEnvelope<T> = {
9
+ success?: boolean;
10
+ data?: T;
11
+ message?: string;
12
+ };
13
+ export type ChatReaderMessage = {
14
+ hash: string;
15
+ t: number;
16
+ from: string;
17
+ to: string;
18
+ opcode: number;
19
+ kind: number;
20
+ body: unknown;
21
+ recipients?: string[];
22
+ };
23
+ export type ChatReaderThread = {
24
+ threadHash: string;
25
+ type: "dm" | "group";
26
+ associatedId: string;
27
+ lastUpdateAt: number;
28
+ name: string;
29
+ avatar: string;
30
+ lastMessage: ChatReaderMessage;
31
+ total: number;
32
+ unread: number;
33
+ };
34
+ export type ChatReaderContact = {
35
+ id?: string;
36
+ accountId?: string;
37
+ peerId?: string;
38
+ type?: "dm" | "group" | "user";
39
+ name?: string;
40
+ nickName?: string;
41
+ title?: string;
42
+ avatar?: string;
43
+ relationship?: string;
44
+ threadId?: string;
45
+ updatedAt?: number;
46
+ members?: string[];
47
+ };
48
+ export type ChatReaderSocialMessage = {
49
+ appId: string;
50
+ threadId: string;
51
+ threadKind: "dm" | "group";
52
+ messageId: string;
53
+ from: string;
54
+ to: string;
55
+ recipients?: string[];
56
+ body: unknown;
57
+ clientCreatedAt?: number;
58
+ serverCreatedAt: number;
59
+ };
60
+ export type ChatReaderMessagePage = {
61
+ page: number;
62
+ totalPage: number;
63
+ pageSize: number;
64
+ data: ChatReaderMessage[];
65
+ };
66
+ export type ChatReaderUnread = {
67
+ appId: string;
68
+ accountId: string;
69
+ threadId: string;
70
+ unreadCount: number;
71
+ };
72
+ export declare class ChatReaderClient {
73
+ private readonly options;
74
+ private readonly baseUrl;
75
+ private readonly chainId;
76
+ private readonly socialChainId;
77
+ private readonly fetchImpl;
78
+ constructor(options: ChatReaderClientOptions);
79
+ listThreads(userId: string): Promise<ChatReaderThread[]>;
80
+ getThread(userId: string, threadHash: string): Promise<ChatReaderThread | null>;
81
+ listSocialContacts(accountId: string): Promise<ChatReaderContact[]>;
82
+ listSocialUnread(accountId: string): Promise<ChatReaderUnread[]>;
83
+ listSocialMessages(threadId: string): Promise<ChatReaderSocialMessage[]>;
84
+ listThreadMessages(input: {
85
+ userId: string;
86
+ threadHash: string;
87
+ page?: number;
88
+ }): Promise<ChatReaderMessagePage | null>;
89
+ markRead(input: {
90
+ userId: string;
91
+ threadHash: string;
92
+ hashes: string[];
93
+ }): Promise<unknown>;
94
+ private indexerRequest;
95
+ private request;
96
+ }
97
+ export declare function createChatReaderClient(options: ChatReaderClientOptions): ChatReaderClient;
@@ -0,0 +1,91 @@
1
+ const DEFAULT_INDEXER_URL = "https://indexer.pmate.chat";
2
+ const DEFAULT_CHAIN_ID = "pmate";
3
+ const DEFAULT_SOCIAL_CHAIN_ID = "social";
4
+ export class ChatReaderClient {
5
+ options;
6
+ baseUrl;
7
+ chainId;
8
+ socialChainId;
9
+ fetchImpl;
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_INDEXER_URL);
13
+ this.chainId = options.chainId ?? DEFAULT_CHAIN_ID;
14
+ this.socialChainId = options.socialChainId ?? DEFAULT_SOCIAL_CHAIN_ID;
15
+ this.fetchImpl = options.fetchImpl ?? fetch;
16
+ }
17
+ async listThreads(userId) {
18
+ return this.indexerRequest("thread", "threads", { user: userId });
19
+ }
20
+ async getThread(userId, threadHash) {
21
+ return this.indexerRequest("thread", "thread", { user: userId, threadHash });
22
+ }
23
+ async listSocialContacts(accountId) {
24
+ return this.indexerRequest("social_indexer", "contacts", {
25
+ appId: this.options.appId,
26
+ accountId,
27
+ }, undefined, { chainId: this.socialChainId });
28
+ }
29
+ async listSocialUnread(accountId) {
30
+ return this.indexerRequest("social_indexer", "unread", {
31
+ appId: this.options.appId,
32
+ accountId,
33
+ }, undefined, { chainId: this.socialChainId });
34
+ }
35
+ async listSocialMessages(threadId) {
36
+ return this.indexerRequest("social_indexer", "messages", {
37
+ appId: this.options.appId,
38
+ threadId,
39
+ }, undefined, { chainId: this.socialChainId });
40
+ }
41
+ async listThreadMessages(input) {
42
+ return this.indexerRequest("thread", "messages", {
43
+ user: input.userId,
44
+ threadHash: input.threadHash,
45
+ ...(input.page !== undefined ? { page: String(input.page) } : {}),
46
+ });
47
+ }
48
+ async markRead(input) {
49
+ return this.indexerRequest("thread", "read", {
50
+ user: input.userId,
51
+ threadHash: input.threadHash,
52
+ hashes: JSON.stringify(input.hashes),
53
+ }, {
54
+ method: "POST",
55
+ });
56
+ }
57
+ async indexerRequest(indexer, action, query, init, options) {
58
+ const method = init?.method ?? "GET";
59
+ const chainId = options?.chainId ?? this.chainId;
60
+ const path = `/${encodeURIComponent(chainId)}/${encodeURIComponent(indexer)}/${encodeURIComponent(action)}`;
61
+ if (method.toUpperCase() === "POST") {
62
+ return this.request(path, {
63
+ ...init,
64
+ method,
65
+ body: JSON.stringify(query),
66
+ });
67
+ }
68
+ const search = new URLSearchParams(query);
69
+ return this.request(`${path}?${search.toString()}`, init);
70
+ }
71
+ async request(path, init) {
72
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
73
+ ...init,
74
+ headers: {
75
+ "content-type": "application/json",
76
+ ...(init?.headers ?? {}),
77
+ },
78
+ });
79
+ const json = (await response.json().catch(() => ({})));
80
+ if (!response.ok || json.success === false) {
81
+ throw new Error(json.message || `chat-rest ${response.status}`);
82
+ }
83
+ return (json.data ?? json);
84
+ }
85
+ }
86
+ export function createChatReaderClient(options) {
87
+ return new ChatReaderClient(options);
88
+ }
89
+ function normalizeBaseUrl(baseUrl) {
90
+ return baseUrl.replace(/\/+$/, "");
91
+ }
@@ -15,4 +15,8 @@ export type ChatVoiceConfig = {
15
15
  simulatedTranscript?: string;
16
16
  shortRecordingMessage?: string;
17
17
  onVoiceSend?: (blob: Blob) => Promise<void> | void;
18
+ onVoiceTranscript?: (text: string, context: {
19
+ blob: Blob;
20
+ action: "send" | "transcribe";
21
+ }) => Promise<void> | void;
18
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",