@appthen/sdk-react 0.0.0
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/ChatHubComposer.d.ts +18 -0
- package/dist/ChatHubComposer.js +8 -0
- package/dist/ChatHubDefaultBodyRenderers.d.ts +8 -0
- package/dist/ChatHubDefaultBodyRenderers.js +74 -0
- package/dist/ChatHubMessageActions.d.ts +10 -0
- package/dist/ChatHubMessageActions.js +32 -0
- package/dist/ChatHubMessageBody.d.ts +9 -0
- package/dist/ChatHubMessageBody.js +22 -0
- package/dist/ChatHubMessageTimeline.d.ts +13 -0
- package/dist/ChatHubMessageTimeline.js +18 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +325 -0
- package/dist/useChatHubMessageActions.d.ts +35 -0
- package/dist/useChatHubMessageActions.js +124 -0
- package/dist/useChatHubMessageMutations.d.ts +53 -0
- package/dist/useChatHubMessageMutations.js +293 -0
- package/package.json +33 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ChatHubComposerOption {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ChatHubComposerProps {
|
|
6
|
+
senderOptions: ChatHubComposerOption[];
|
|
7
|
+
selectedSenderId: string;
|
|
8
|
+
onSelectedSenderIdChange: (senderId: string) => void;
|
|
9
|
+
draft: string;
|
|
10
|
+
onDraftChange: (draft: string) => void;
|
|
11
|
+
onSend: () => void | Promise<void>;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
busy?: boolean;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
sendLabel?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function ChatHubComposer(props: ChatHubComposerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function ChatHubComposer(props) {
|
|
3
|
+
const { senderOptions, selectedSenderId, onSelectedSenderIdChange, draft, onDraftChange, onSend, disabled, busy, placeholder, sendLabel, className, } = props;
|
|
4
|
+
const handleDraftChange = (event) => {
|
|
5
|
+
onDraftChange(event.target.value);
|
|
6
|
+
};
|
|
7
|
+
return (_jsxs("div", { className: className || 'grid gap-3 md:grid-cols-[220px_minmax(0,1fr)_auto]', children: [_jsx("select", { className: "flex h-10 w-full rounded-md border border-input bg-[var(--vscode-input-background)] px-3 py-2 text-sm", value: selectedSenderId, onChange: (event) => onSelectedSenderIdChange(event.target.value), disabled: disabled, children: senderOptions.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }), _jsx("input", { className: "flex h-10 w-full rounded-md border border-input bg-[var(--vscode-input-background)] px-3 py-2 text-sm", value: draft, onChange: handleDraftChange, placeholder: placeholder || '输入一条消息', disabled: disabled }), _jsx("button", { type: "button", className: "inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground disabled:cursor-not-allowed disabled:opacity-50", onClick: () => void onSend(), disabled: disabled || !draft.trim(), children: busy ? '发送中...' : sendLabel || '发送' })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ConversationMessageRecord } from '@appthen/sdk-core';
|
|
3
|
+
import { type ChatHubMessageBodyRenderers } from './ChatHubMessageBody';
|
|
4
|
+
export interface ChatHubDefaultBodyRendererOptions {
|
|
5
|
+
renderMarkdown?: (markdown: string, message: ConversationMessageRecord) => ReactNode;
|
|
6
|
+
overrides?: ChatHubMessageBodyRenderers;
|
|
7
|
+
}
|
|
8
|
+
export declare function createChatHubDefaultBodyRenderers(options?: ChatHubDefaultBodyRendererOptions): ChatHubMessageBodyRenderers;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function resolveText(value) {
|
|
3
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
function renderPlainMarkdown(markdown) {
|
|
6
|
+
return _jsx("div", { className: "whitespace-pre-wrap leading-6", children: markdown });
|
|
7
|
+
}
|
|
8
|
+
function renderForwardedMessage(message) {
|
|
9
|
+
const senderSubjectId = resolveText(message.content?.senderSubjectId) ||
|
|
10
|
+
resolveText(message.content?.originalSenderSubjectId) ||
|
|
11
|
+
'未知发送人';
|
|
12
|
+
const previewText = resolveText(message.content?.previewText) ||
|
|
13
|
+
resolveText(message.content?.text) ||
|
|
14
|
+
resolveText(message.content?.summary) ||
|
|
15
|
+
'转发了一条消息';
|
|
16
|
+
const sourceConversationId = resolveText(message.content?.sourceConversationId);
|
|
17
|
+
const sourceConversationTitle = resolveText(message.content?.sourceConversationTitle) ||
|
|
18
|
+
(message.metadata &&
|
|
19
|
+
typeof message.metadata === 'object' &&
|
|
20
|
+
typeof message.metadata.sourceConversationTitle === 'string' &&
|
|
21
|
+
message.metadata.sourceConversationTitle.trim()
|
|
22
|
+
? message.metadata.sourceConversationTitle.trim()
|
|
23
|
+
: undefined);
|
|
24
|
+
const kind = resolveText(message.content?.originalKind);
|
|
25
|
+
return (_jsxs("div", { className: "rounded-2xl border border-border/60 bg-muted/20 px-3 py-3", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "\u8F6C\u53D1\u6D88\u606F" }), _jsxs("div", { className: "mt-2 rounded-xl border border-border/60 bg-background/90 px-3 py-2", children: [_jsx("div", { className: "text-xs font-medium text-foreground", children: senderSubjectId }), _jsx("div", { className: "mt-1 whitespace-pre-wrap text-sm leading-6 text-foreground/90", children: previewText }), sourceConversationId || kind ? (_jsx("div", { className: "mt-2 text-[11px] text-muted-foreground", children: [
|
|
26
|
+
kind ? `类型: ${kind}` : null,
|
|
27
|
+
sourceConversationTitle
|
|
28
|
+
? `来源会话: ${sourceConversationTitle}`
|
|
29
|
+
: sourceConversationId
|
|
30
|
+
? `来源会话: ${sourceConversationId}`
|
|
31
|
+
: null,
|
|
32
|
+
]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join(' · ') })) : null] })] }));
|
|
35
|
+
}
|
|
36
|
+
function renderEventCard(message, tone) {
|
|
37
|
+
const title = resolveText(message.content?.title) ||
|
|
38
|
+
resolveText(message.content?.eventType) ||
|
|
39
|
+
resolveText(message.kind) ||
|
|
40
|
+
'事件';
|
|
41
|
+
const summary = resolveText(message.content?.summary) ||
|
|
42
|
+
resolveText(message.content?.text) ||
|
|
43
|
+
resolveText(message.content?.message);
|
|
44
|
+
const status = resolveText(message.content?.status);
|
|
45
|
+
const details = resolveText(message.content?.details);
|
|
46
|
+
const toneClass = tone === 'success'
|
|
47
|
+
? 'border-emerald-500/30 bg-emerald-500/10'
|
|
48
|
+
: tone === 'warning'
|
|
49
|
+
? 'border-amber-500/30 bg-amber-500/10'
|
|
50
|
+
: 'border-border/60 bg-muted/40';
|
|
51
|
+
return (_jsxs("div", { className: `rounded-md border px-3 py-2 ${toneClass}`, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: title }), status ? (_jsx("span", { className: "rounded border border-border/60 px-1.5 py-0.5 text-[11px] uppercase tracking-wide text-muted-foreground", children: status })) : null] }), summary ? (_jsx("div", { className: "mt-1 whitespace-pre-wrap text-sm text-foreground/90", children: summary })) : null, details ? (_jsx("div", { className: "mt-2 whitespace-pre-wrap text-xs text-muted-foreground", children: details })) : null] }));
|
|
52
|
+
}
|
|
53
|
+
export function createChatHubDefaultBodyRenderers(options) {
|
|
54
|
+
const markdownRenderer = (message) => {
|
|
55
|
+
const markdown = resolveText(message.content?.markdown) ||
|
|
56
|
+
resolveText(message.content?.text) ||
|
|
57
|
+
'';
|
|
58
|
+
if (!markdown) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return options?.renderMarkdown
|
|
62
|
+
? options.renderMarkdown(markdown, message)
|
|
63
|
+
: renderPlainMarkdown(markdown);
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
markdown: markdownRenderer,
|
|
67
|
+
forwarded_message: renderForwardedMessage,
|
|
68
|
+
status_event: (message) => renderEventCard(message, 'success'),
|
|
69
|
+
lifecycle_event: (message) => renderEventCard(message, 'neutral'),
|
|
70
|
+
approval_event: (message) => renderEventCard(message, 'warning'),
|
|
71
|
+
event: (message) => renderEventCard(message, 'neutral'),
|
|
72
|
+
...(options?.overrides || {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConversationMessageRecord } from '@appthen/sdk-core';
|
|
2
|
+
import type { ChatHubActionSurface, ChatHubMessageActionItem } from './useChatHubMessageActions';
|
|
3
|
+
export interface ChatHubMessageActionsProps {
|
|
4
|
+
message: ConversationMessageRecord;
|
|
5
|
+
actions: ChatHubMessageActionItem[];
|
|
6
|
+
surface?: ChatHubActionSurface;
|
|
7
|
+
className?: string;
|
|
8
|
+
onActionRun?: (action: ChatHubMessageActionItem, message: ConversationMessageRecord) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function ChatHubMessageActions(props: ChatHubMessageActionsProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
function resolveSurfaceClassName(surface) {
|
|
3
|
+
if (surface === 'sheet') {
|
|
4
|
+
return 'flex flex-col gap-2';
|
|
5
|
+
}
|
|
6
|
+
if (surface === 'menu') {
|
|
7
|
+
return 'flex flex-col gap-1';
|
|
8
|
+
}
|
|
9
|
+
return 'flex flex-wrap items-center gap-2';
|
|
10
|
+
}
|
|
11
|
+
export function ChatHubMessageActions(props) {
|
|
12
|
+
const { message, actions, surface = 'inline', className, onActionRun, } = props;
|
|
13
|
+
const visibleActions = actions.filter((action) => !action.hidden);
|
|
14
|
+
if (visibleActions.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return (_jsx("div", { className: className || resolveSurfaceClassName(surface), "data-chat-hub-action-surface": surface, role: "toolbar", "aria-label": "\u6D88\u606F\u64CD\u4F5C", children: visibleActions.map((action) => (_jsx("button", { type: "button", className: [
|
|
18
|
+
'inline-flex min-h-8 items-center justify-center rounded-full border px-2.5 text-[11px] font-medium transition-colors',
|
|
19
|
+
surface === 'inline'
|
|
20
|
+
? 'border-border/60 bg-background/80 text-muted-foreground hover:border-border hover:bg-muted/70 hover:text-foreground'
|
|
21
|
+
: 'border-transparent bg-accent/40 text-foreground hover:bg-accent/70',
|
|
22
|
+
action.destructive
|
|
23
|
+
? 'text-destructive hover:border-destructive/20 hover:bg-destructive/10 hover:text-destructive'
|
|
24
|
+
: '',
|
|
25
|
+
action.pending ? 'opacity-80' : '',
|
|
26
|
+
]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join(' '), onClick: () => {
|
|
29
|
+
onActionRun?.(action, message);
|
|
30
|
+
void action.run();
|
|
31
|
+
}, disabled: action.disabled || action.pending, "aria-label": action.description || action.label, "data-pending": action.pending ? 'true' : 'false', "data-action-id": action.id, children: action.pending ? `${action.label}中...` : action.shortLabel || action.label }, action.id))) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ConversationMessageRecord } from '@appthen/sdk-core';
|
|
3
|
+
export type ChatHubMessageBodyRenderer = (message: ConversationMessageRecord) => ReactNode;
|
|
4
|
+
export type ChatHubMessageBodyRenderers = Partial<Record<string, ChatHubMessageBodyRenderer>>;
|
|
5
|
+
export interface ChatHubMessageBodyProps {
|
|
6
|
+
message: ConversationMessageRecord;
|
|
7
|
+
renderers?: ChatHubMessageBodyRenderers;
|
|
8
|
+
}
|
|
9
|
+
export declare function ChatHubMessageBody(props: ChatHubMessageBodyProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
function defaultRenderBody(message) {
|
|
3
|
+
const text = message.content?.text;
|
|
4
|
+
if (typeof text === 'string') {
|
|
5
|
+
// 必须用 whitespace-pre-wrap 容器:React 裸字符串节点会按 HTML 规则折叠连续空白
|
|
6
|
+
// (包括 \n),导致 markdown 表格全部挤成一行。pre-wrap 保留换行和空格。
|
|
7
|
+
return _jsx("div", { className: "whitespace-pre-wrap leading-6", children: text });
|
|
8
|
+
}
|
|
9
|
+
return (_jsx("div", { className: "whitespace-pre-wrap leading-6 text-xs text-muted-foreground", children: JSON.stringify(message.content || {}) }));
|
|
10
|
+
}
|
|
11
|
+
function renderMutedMessageBody(status) {
|
|
12
|
+
const label = status === 'deleted' ? '消息已删除' : '消息已撤回';
|
|
13
|
+
return (_jsx("div", { className: "rounded-2xl border border-dashed border-border/60 bg-muted/20 px-3 py-2 text-sm text-muted-foreground", children: label }));
|
|
14
|
+
}
|
|
15
|
+
export function ChatHubMessageBody(props) {
|
|
16
|
+
const { message, renderers } = props;
|
|
17
|
+
if (message.status === 'deleted' || message.status === 'revoked') {
|
|
18
|
+
return _jsx(_Fragment, { children: renderMutedMessageBody(message.status) });
|
|
19
|
+
}
|
|
20
|
+
const renderer = (message.kind && renderers?.[message.kind]) || renderers?.default;
|
|
21
|
+
return _jsx(_Fragment, { children: renderer ? renderer(message) : defaultRenderBody(message) });
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ConversationMessageRecord } from '@appthen/sdk-core';
|
|
3
|
+
import { type ChatHubMessageBodyRenderers } from './ChatHubMessageBody';
|
|
4
|
+
export interface ChatHubMessageTimelineProps {
|
|
5
|
+
messages: ConversationMessageRecord[];
|
|
6
|
+
emptyState?: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
renderBody?: (message: ConversationMessageRecord) => ReactNode;
|
|
9
|
+
renderTimestamp?: (message: ConversationMessageRecord) => ReactNode;
|
|
10
|
+
renderMeta?: (message: ConversationMessageRecord) => ReactNode;
|
|
11
|
+
bodyRenderers?: ChatHubMessageBodyRenderers;
|
|
12
|
+
}
|
|
13
|
+
export declare function ChatHubMessageTimeline(props: ChatHubMessageTimelineProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChatHubMessageBody, } from './ChatHubMessageBody';
|
|
3
|
+
function defaultResolveTime(message) {
|
|
4
|
+
const createdAt = typeof message.createdAt === 'string' ? message.createdAt : undefined;
|
|
5
|
+
const updatedAt = typeof message.updatedAt === 'string' ? message.updatedAt : undefined;
|
|
6
|
+
const value = createdAt || updatedAt;
|
|
7
|
+
if (!value)
|
|
8
|
+
return '-';
|
|
9
|
+
const date = new Date(value);
|
|
10
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
11
|
+
}
|
|
12
|
+
export function ChatHubMessageTimeline(props) {
|
|
13
|
+
const { messages, emptyState, className, renderBody, renderTimestamp, renderMeta, bodyRenderers, } = props;
|
|
14
|
+
if (messages.length === 0) {
|
|
15
|
+
return (_jsx("div", { className: className, children: emptyState || (_jsx("div", { className: "rounded-lg border border-dashed border-border/60 px-4 py-10 text-center text-sm text-muted-foreground", children: "\u6682\u65E0\u6D88\u606F" })) }));
|
|
16
|
+
}
|
|
17
|
+
return (_jsx("div", { className: className || 'space-y-3', children: messages.map((message) => (_jsxs("div", { className: "rounded-lg border border-border/60 bg-card/70 px-4 py-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("span", { className: "inline-flex rounded border border-border/60 px-2 py-0.5 text-xs", children: message.senderSubjectId }), _jsx("span", { children: message.kind || 'text' })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: renderTimestamp ? renderTimestamp(message) : defaultResolveTime(message) })] }), _jsx("div", { className: "mt-2 whitespace-pre-wrap text-sm leading-6", children: renderBody ? (renderBody(message)) : (_jsx(ChatHubMessageBody, { message: message, renderers: bodyRenderers })) }), _jsx("div", { className: "mt-2 text-xs text-muted-foreground", children: renderMeta ? renderMeta(message) : `messageId: ${message.messageId}` })] }, message.messageId))) }));
|
|
18
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChatHubAppConfig, ChatHubAppRuntime, ChatHubBootstrapResult, ConversationMessageRecord, ConversationRecord, ListChatHubMessagesPageResult, ListChatHubMessagesOptions, SendChatHubMessageOptions, SendTextMessageOptions, UpdateChatHubMessageOptions } from '@appthen/sdk-core';
|
|
2
|
+
export { ChatHubMessageTimeline } from './ChatHubMessageTimeline';
|
|
3
|
+
export { ChatHubComposer } from './ChatHubComposer';
|
|
4
|
+
export { ChatHubMessageActions } from './ChatHubMessageActions';
|
|
5
|
+
export { ChatHubMessageBody, type ChatHubMessageBodyRenderer, type ChatHubMessageBodyRenderers, } from './ChatHubMessageBody';
|
|
6
|
+
export { createChatHubDefaultBodyRenderers, type ChatHubDefaultBodyRendererOptions, } from './ChatHubDefaultBodyRenderers';
|
|
7
|
+
export { extractChatHubMessagePlainText, useChatHubMessageMutations, type ChatHubForwardDraftItem, type ForwardChatHubMessagesOptions, type ChatHubMutationActionState, type UseChatHubMessageMutationsOptions, type UseChatHubMessageMutationsResult, } from './useChatHubMessageMutations';
|
|
8
|
+
export { useChatHubMessageActions, type ChatHubActionSurface, type ChatHubMessageActionItem, type UseChatHubMessageActionsOptions, type UseChatHubMessageActionsResult, } from './useChatHubMessageActions';
|
|
9
|
+
export interface UseChatHubAppOptions {
|
|
10
|
+
runtime: ChatHubAppRuntime;
|
|
11
|
+
config: ChatHubAppConfig;
|
|
12
|
+
autoLoad?: boolean;
|
|
13
|
+
autoLoadMessages?: boolean;
|
|
14
|
+
initialConversationId?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UseChatHubAppResult {
|
|
17
|
+
config: ChatHubAppConfig;
|
|
18
|
+
runtime: ChatHubAppRuntime;
|
|
19
|
+
status: string;
|
|
20
|
+
error: string | null;
|
|
21
|
+
busy: boolean;
|
|
22
|
+
conversations: ConversationRecord[];
|
|
23
|
+
messages: ConversationMessageRecord[];
|
|
24
|
+
selectedConversationId: string;
|
|
25
|
+
setSelectedConversationId: (conversationId: string) => void;
|
|
26
|
+
selectedSenderId: string;
|
|
27
|
+
setSelectedSenderId: (subjectId: string) => void;
|
|
28
|
+
bootstrapConversation: () => Promise<ChatHubBootstrapResult | null>;
|
|
29
|
+
refreshConversations: () => Promise<void>;
|
|
30
|
+
refreshMessages: (conversationIdOrOptions?: string | (Partial<ListChatHubMessagesOptions> & {
|
|
31
|
+
mode?: 'replace' | 'prepend' | 'append';
|
|
32
|
+
})) => Promise<ConversationMessageRecord[]>;
|
|
33
|
+
refreshMessagesPage: (conversationIdOrOptions?: string | (Partial<ListChatHubMessagesOptions> & {
|
|
34
|
+
mode?: 'replace' | 'prepend' | 'append';
|
|
35
|
+
})) => Promise<ListChatHubMessagesPageResult>;
|
|
36
|
+
mergeMessages: (incoming: ConversationMessageRecord[], conversationId: string, mode?: 'replace' | 'prepend' | 'append') => void;
|
|
37
|
+
sendTextMessage: (input: Omit<SendTextMessageOptions, 'conversationId'> & {
|
|
38
|
+
conversationId?: string;
|
|
39
|
+
}) => Promise<ConversationMessageRecord | null>;
|
|
40
|
+
sendMessage: (input: Omit<SendChatHubMessageOptions, 'conversationId'> & {
|
|
41
|
+
conversationId?: string;
|
|
42
|
+
}) => Promise<ConversationMessageRecord | null>;
|
|
43
|
+
updateMessage: (input: UpdateChatHubMessageOptions & {
|
|
44
|
+
conversationId?: string;
|
|
45
|
+
}) => Promise<ConversationMessageRecord | null>;
|
|
46
|
+
}
|
|
47
|
+
export type ChatHubMessageMergeMode = 'replace' | 'prepend' | 'append';
|
|
48
|
+
export declare function areConversationMessagesEqual(left: ConversationMessageRecord, right: ConversationMessageRecord): boolean;
|
|
49
|
+
export declare function mergeConversationMessages(current: ConversationMessageRecord[], incoming: ConversationMessageRecord[], mode?: ChatHubMessageMergeMode): ConversationMessageRecord[];
|
|
50
|
+
export declare function sortConversationMessages(messages: ConversationMessageRecord[]): ConversationMessageRecord[];
|
|
51
|
+
export declare function filterConversationMessages(messages: ConversationMessageRecord[], conversationId: string): ConversationMessageRecord[];
|
|
52
|
+
export declare function areConversationMessageListsEqual(left: ConversationMessageRecord[], right: ConversationMessageRecord[]): boolean;
|
|
53
|
+
export declare function useChatHubApp(options: UseChatHubAppOptions): UseChatHubAppResult;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
export { ChatHubMessageTimeline } from './ChatHubMessageTimeline';
|
|
3
|
+
export { ChatHubComposer } from './ChatHubComposer';
|
|
4
|
+
export { ChatHubMessageActions } from './ChatHubMessageActions';
|
|
5
|
+
export { ChatHubMessageBody, } from './ChatHubMessageBody';
|
|
6
|
+
export { createChatHubDefaultBodyRenderers, } from './ChatHubDefaultBodyRenderers';
|
|
7
|
+
export { extractChatHubMessagePlainText, useChatHubMessageMutations, } from './useChatHubMessageMutations';
|
|
8
|
+
export { useChatHubMessageActions, } from './useChatHubMessageActions';
|
|
9
|
+
export function areConversationMessagesEqual(left, right) {
|
|
10
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
11
|
+
}
|
|
12
|
+
export function mergeConversationMessages(current, incoming, mode = 'replace') {
|
|
13
|
+
if (mode === 'replace') {
|
|
14
|
+
return sortConversationMessages(incoming);
|
|
15
|
+
}
|
|
16
|
+
const byId = new Map(current.map((message) => [message.messageId, message]));
|
|
17
|
+
for (const message of incoming) {
|
|
18
|
+
const existing = byId.get(message.messageId);
|
|
19
|
+
byId.set(message.messageId, existing && areConversationMessagesEqual(existing, message)
|
|
20
|
+
? existing
|
|
21
|
+
: message);
|
|
22
|
+
}
|
|
23
|
+
if (mode === 'prepend') {
|
|
24
|
+
const incomingIds = new Set(incoming.map((message) => message.messageId));
|
|
25
|
+
return sortConversationMessages([
|
|
26
|
+
...incoming.map((message) => byId.get(message.messageId) || message),
|
|
27
|
+
...current.filter((message) => !incomingIds.has(message.messageId)),
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
const currentIds = new Set(current.map((message) => message.messageId));
|
|
31
|
+
return sortConversationMessages([
|
|
32
|
+
...current.map((message) => byId.get(message.messageId) || message),
|
|
33
|
+
...incoming
|
|
34
|
+
.filter((message) => !currentIds.has(message.messageId))
|
|
35
|
+
.map((message) => byId.get(message.messageId) || message),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
function resolveMessageSequence(message) {
|
|
39
|
+
return typeof message.sequence === 'number' && Number.isFinite(message.sequence)
|
|
40
|
+
? message.sequence
|
|
41
|
+
: null;
|
|
42
|
+
}
|
|
43
|
+
function resolveMessageTimestamp(message) {
|
|
44
|
+
const value = typeof message.createdAt === 'string'
|
|
45
|
+
? message.createdAt
|
|
46
|
+
: typeof message.updatedAt === 'string'
|
|
47
|
+
? message.updatedAt
|
|
48
|
+
: '';
|
|
49
|
+
const timestamp = value ? Date.parse(value) : NaN;
|
|
50
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
51
|
+
}
|
|
52
|
+
function compareConversationMessages(left, right) {
|
|
53
|
+
const leftSequence = resolveMessageSequence(left);
|
|
54
|
+
const rightSequence = resolveMessageSequence(right);
|
|
55
|
+
if (leftSequence !== null && rightSequence !== null && leftSequence !== rightSequence) {
|
|
56
|
+
return leftSequence - rightSequence;
|
|
57
|
+
}
|
|
58
|
+
if (leftSequence !== null && rightSequence === null) {
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
if (leftSequence === null && rightSequence !== null) {
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const timeDiff = resolveMessageTimestamp(left) - resolveMessageTimestamp(right);
|
|
65
|
+
if (timeDiff !== 0) {
|
|
66
|
+
return timeDiff;
|
|
67
|
+
}
|
|
68
|
+
return left.messageId.localeCompare(right.messageId);
|
|
69
|
+
}
|
|
70
|
+
export function sortConversationMessages(messages) {
|
|
71
|
+
return [...messages].sort(compareConversationMessages);
|
|
72
|
+
}
|
|
73
|
+
export function filterConversationMessages(messages, conversationId) {
|
|
74
|
+
return messages.filter((message) => message.conversationId === conversationId);
|
|
75
|
+
}
|
|
76
|
+
export function areConversationMessageListsEqual(left, right) {
|
|
77
|
+
if (left.length !== right.length) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return left.every((message, index) => {
|
|
81
|
+
const next = right[index];
|
|
82
|
+
return next ? areConversationMessagesEqual(message, next) : false;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function useChatHubApp(options) {
|
|
86
|
+
const { runtime, config, autoLoad = true, autoLoadMessages = autoLoad, initialConversationId = '', } = options;
|
|
87
|
+
const [status, setStatus] = useState('准备中');
|
|
88
|
+
const [error, setError] = useState(null);
|
|
89
|
+
const [busy, setBusy] = useState(false);
|
|
90
|
+
const [conversations, setConversations] = useState([]);
|
|
91
|
+
const [messages, setMessages] = useState([]);
|
|
92
|
+
const [selectedConversationId, setSelectedConversationIdState] = useState(initialConversationId);
|
|
93
|
+
const selectedConversationIdRef = useRef(initialConversationId);
|
|
94
|
+
const refreshConversationsRequestSeqRef = useRef(0);
|
|
95
|
+
const [selectedSenderId, setSelectedSenderIdState] = useState(config.subjects?.[0]?.subjectId || '');
|
|
96
|
+
// ref 镜像:refreshConversations 等回调依赖里不含 selectedSenderId,闭包会过期,统一从 ref 读
|
|
97
|
+
const selectedSenderIdRef = useRef(selectedSenderId);
|
|
98
|
+
const setSelectedSenderId = useCallback((subjectId) => {
|
|
99
|
+
selectedSenderIdRef.current = subjectId;
|
|
100
|
+
setSelectedSenderIdState(subjectId);
|
|
101
|
+
}, []);
|
|
102
|
+
const setSelectedConversationId = useCallback((conversationId) => {
|
|
103
|
+
selectedConversationIdRef.current = conversationId;
|
|
104
|
+
setSelectedConversationIdState(conversationId);
|
|
105
|
+
}, []);
|
|
106
|
+
const mergeMessages = useCallback((incoming, conversationId, mode = 'replace') => {
|
|
107
|
+
if (conversationId !== selectedConversationIdRef.current) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const scopedIncoming = filterConversationMessages(incoming, conversationId);
|
|
111
|
+
if (mode === 'replace') {
|
|
112
|
+
setMessages((current) => {
|
|
113
|
+
const scopedCurrent = filterConversationMessages(current, conversationId);
|
|
114
|
+
return areConversationMessageListsEqual(scopedCurrent, scopedIncoming)
|
|
115
|
+
? current
|
|
116
|
+
: scopedIncoming;
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setMessages((current) => {
|
|
121
|
+
const scopedCurrent = filterConversationMessages(current, conversationId);
|
|
122
|
+
const merged = mergeConversationMessages(scopedCurrent, scopedIncoming, mode);
|
|
123
|
+
return areConversationMessageListsEqual(scopedCurrent, merged)
|
|
124
|
+
? current
|
|
125
|
+
: merged;
|
|
126
|
+
});
|
|
127
|
+
}, []);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
// config 变化时重置为默认主体;调用方仍可通过 setSelectedSenderId 覆写为真实用户身份
|
|
130
|
+
setSelectedSenderId(config.subjects?.[0]?.subjectId || '');
|
|
131
|
+
}, [config, setSelectedSenderId]);
|
|
132
|
+
const withBusy = useCallback(async (label, task) => {
|
|
133
|
+
setBusy(true);
|
|
134
|
+
setError(null);
|
|
135
|
+
setStatus(label);
|
|
136
|
+
try {
|
|
137
|
+
return await task();
|
|
138
|
+
}
|
|
139
|
+
catch (nextError) {
|
|
140
|
+
setError(nextError instanceof Error ? nextError.message : `${label}失败`);
|
|
141
|
+
setStatus(`${label}失败`);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
setBusy(false);
|
|
146
|
+
}
|
|
147
|
+
}, []);
|
|
148
|
+
const refreshConversations = useCallback(async () => {
|
|
149
|
+
const requestSeq = ++refreshConversationsRequestSeqRef.current;
|
|
150
|
+
const list = await runtime.listConversations(selectedSenderIdRef.current);
|
|
151
|
+
if (requestSeq !== refreshConversationsRequestSeqRef.current) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
setConversations(list);
|
|
155
|
+
setSelectedConversationIdState((current) => {
|
|
156
|
+
const next = current || list[0]?.conversationId || '';
|
|
157
|
+
selectedConversationIdRef.current = next;
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
}, [runtime]);
|
|
161
|
+
const refreshMessages = useCallback(async (conversationIdOrOptions) => {
|
|
162
|
+
const options = typeof conversationIdOrOptions === 'string'
|
|
163
|
+
? {
|
|
164
|
+
conversationId: conversationIdOrOptions,
|
|
165
|
+
mode: 'replace',
|
|
166
|
+
}
|
|
167
|
+
: {
|
|
168
|
+
mode: 'replace',
|
|
169
|
+
...(conversationIdOrOptions || {}),
|
|
170
|
+
};
|
|
171
|
+
const targetConversationId = options.conversationId || selectedConversationIdRef.current;
|
|
172
|
+
if (!targetConversationId) {
|
|
173
|
+
setMessages([]);
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const list = await runtime.listMessages({
|
|
177
|
+
conversationId: targetConversationId,
|
|
178
|
+
limit: options.limit,
|
|
179
|
+
beforeCreatedAt: options.beforeCreatedAt,
|
|
180
|
+
afterSequence: options.afterSequence,
|
|
181
|
+
});
|
|
182
|
+
mergeMessages(list, targetConversationId, options.mode);
|
|
183
|
+
return list;
|
|
184
|
+
}, [mergeMessages, runtime]);
|
|
185
|
+
const refreshMessagesPage = useCallback(async (conversationIdOrOptions) => {
|
|
186
|
+
const options = typeof conversationIdOrOptions === 'string'
|
|
187
|
+
? {
|
|
188
|
+
conversationId: conversationIdOrOptions,
|
|
189
|
+
mode: 'replace',
|
|
190
|
+
}
|
|
191
|
+
: {
|
|
192
|
+
mode: 'replace',
|
|
193
|
+
...(conversationIdOrOptions || {}),
|
|
194
|
+
};
|
|
195
|
+
const targetConversationId = options.conversationId || selectedConversationIdRef.current;
|
|
196
|
+
if (!targetConversationId) {
|
|
197
|
+
setMessages([]);
|
|
198
|
+
return {
|
|
199
|
+
items: [],
|
|
200
|
+
hasMore: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const page = await runtime.listMessagesPage({
|
|
204
|
+
conversationId: targetConversationId,
|
|
205
|
+
limit: options.limit,
|
|
206
|
+
beforeCreatedAt: options.beforeCreatedAt,
|
|
207
|
+
afterSequence: options.afterSequence,
|
|
208
|
+
});
|
|
209
|
+
mergeMessages(page.items, targetConversationId, options.mode);
|
|
210
|
+
return page;
|
|
211
|
+
}, [mergeMessages, runtime]);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (!autoLoad) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
refreshConversations().catch((nextError) => {
|
|
217
|
+
setError(nextError instanceof Error ? nextError.message : '加载会话失败');
|
|
218
|
+
});
|
|
219
|
+
}, [autoLoad, refreshConversations]);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!autoLoad || !autoLoadMessages) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
refreshMessages().catch((nextError) => {
|
|
225
|
+
setError(nextError instanceof Error ? nextError.message : '加载消息失败');
|
|
226
|
+
});
|
|
227
|
+
}, [autoLoad, autoLoadMessages, refreshMessages, selectedConversationId]);
|
|
228
|
+
const bootstrapConversation = useCallback(async () => {
|
|
229
|
+
const result = await withBusy('准备会话', async () => {
|
|
230
|
+
const bootstrap = await runtime.bootstrapConversation();
|
|
231
|
+
const conversationId = bootstrap.conversation?.conversationId || '';
|
|
232
|
+
setSelectedConversationId(conversationId);
|
|
233
|
+
await refreshConversations();
|
|
234
|
+
if (conversationId) {
|
|
235
|
+
await refreshMessages(conversationId);
|
|
236
|
+
}
|
|
237
|
+
setStatus('会话已就绪');
|
|
238
|
+
return bootstrap;
|
|
239
|
+
});
|
|
240
|
+
return result;
|
|
241
|
+
}, [refreshConversations, refreshMessages, runtime, withBusy]);
|
|
242
|
+
const sendTextMessage = useCallback(async (input) => {
|
|
243
|
+
const conversationId = input.conversationId || selectedConversationId;
|
|
244
|
+
if (!conversationId) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const result = await withBusy('发送消息', async () => {
|
|
248
|
+
const message = await runtime.sendTextMessage({
|
|
249
|
+
...input,
|
|
250
|
+
conversationId,
|
|
251
|
+
});
|
|
252
|
+
mergeMessages([message], conversationId, 'append');
|
|
253
|
+
setStatus('消息已发送');
|
|
254
|
+
return message;
|
|
255
|
+
});
|
|
256
|
+
return result;
|
|
257
|
+
}, [mergeMessages, runtime, selectedConversationId, withBusy]);
|
|
258
|
+
const sendMessage = useCallback(async (input) => {
|
|
259
|
+
const conversationId = input.conversationId || selectedConversationId;
|
|
260
|
+
if (!conversationId) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const result = await withBusy('发送消息', async () => {
|
|
264
|
+
const message = await runtime.sendMessage({
|
|
265
|
+
...input,
|
|
266
|
+
conversationId,
|
|
267
|
+
});
|
|
268
|
+
mergeMessages([message], conversationId, 'append');
|
|
269
|
+
setStatus('消息已发送');
|
|
270
|
+
return message;
|
|
271
|
+
});
|
|
272
|
+
return result;
|
|
273
|
+
}, [mergeMessages, runtime, selectedConversationId, withBusy]);
|
|
274
|
+
const updateMessage = useCallback(async (input) => {
|
|
275
|
+
const conversationId = input.conversationId || selectedConversationId;
|
|
276
|
+
const result = await withBusy('更新消息', async () => {
|
|
277
|
+
const message = await runtime.updateMessage(input);
|
|
278
|
+
if (conversationId) {
|
|
279
|
+
mergeMessages([message], conversationId, 'append');
|
|
280
|
+
}
|
|
281
|
+
setStatus('消息已更新');
|
|
282
|
+
return message;
|
|
283
|
+
});
|
|
284
|
+
return result;
|
|
285
|
+
}, [mergeMessages, runtime, selectedConversationId, withBusy]);
|
|
286
|
+
return useMemo(() => ({
|
|
287
|
+
config,
|
|
288
|
+
runtime,
|
|
289
|
+
status,
|
|
290
|
+
error,
|
|
291
|
+
busy,
|
|
292
|
+
conversations,
|
|
293
|
+
messages,
|
|
294
|
+
selectedConversationId,
|
|
295
|
+
setSelectedConversationId,
|
|
296
|
+
selectedSenderId,
|
|
297
|
+
setSelectedSenderId,
|
|
298
|
+
bootstrapConversation,
|
|
299
|
+
refreshConversations,
|
|
300
|
+
refreshMessages,
|
|
301
|
+
refreshMessagesPage,
|
|
302
|
+
mergeMessages,
|
|
303
|
+
sendTextMessage,
|
|
304
|
+
sendMessage,
|
|
305
|
+
updateMessage,
|
|
306
|
+
}), [
|
|
307
|
+
bootstrapConversation,
|
|
308
|
+
busy,
|
|
309
|
+
config,
|
|
310
|
+
conversations,
|
|
311
|
+
error,
|
|
312
|
+
messages,
|
|
313
|
+
refreshConversations,
|
|
314
|
+
refreshMessages,
|
|
315
|
+
refreshMessagesPage,
|
|
316
|
+
mergeMessages,
|
|
317
|
+
runtime,
|
|
318
|
+
sendMessage,
|
|
319
|
+
selectedConversationId,
|
|
320
|
+
selectedSenderId,
|
|
321
|
+
sendTextMessage,
|
|
322
|
+
status,
|
|
323
|
+
updateMessage,
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ConversationMessageRecord } from '@appthen/sdk-core';
|
|
2
|
+
import type { UseChatHubMessageMutationsResult } from './useChatHubMessageMutations';
|
|
3
|
+
export type ChatHubActionSurface = 'inline' | 'menu' | 'sheet';
|
|
4
|
+
export interface ChatHubMessageActionItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
shortLabel?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
hidden?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
pending?: boolean;
|
|
12
|
+
destructive?: boolean;
|
|
13
|
+
preferredSurface?: ChatHubActionSurface;
|
|
14
|
+
run: () => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface UseChatHubMessageActionsOptions {
|
|
17
|
+
message: ConversationMessageRecord;
|
|
18
|
+
mutations?: Pick<UseChatHubMessageMutationsResult, 'copyMessage' | 'deleteMessage' | 'revokeMessage' | 'setReplyTarget' | 'toggleForwardSelection' | 'isForwardSelected' | 'getActionState'>;
|
|
19
|
+
enableCopy?: boolean;
|
|
20
|
+
enableReply?: boolean;
|
|
21
|
+
enableForward?: boolean;
|
|
22
|
+
enableRetry?: boolean;
|
|
23
|
+
enableDelete?: boolean;
|
|
24
|
+
enableRevoke?: boolean;
|
|
25
|
+
onForward?: (message: ConversationMessageRecord) => void | Promise<void>;
|
|
26
|
+
onRetry?: (message: ConversationMessageRecord) => void | Promise<void>;
|
|
27
|
+
onDelete?: (message: ConversationMessageRecord) => void | Promise<void>;
|
|
28
|
+
onRevoke?: (message: ConversationMessageRecord) => void | Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface UseChatHubMessageActionsResult {
|
|
31
|
+
actions: ChatHubMessageActionItem[];
|
|
32
|
+
primaryActions: ChatHubMessageActionItem[];
|
|
33
|
+
overflowActions: ChatHubMessageActionItem[];
|
|
34
|
+
}
|
|
35
|
+
export declare function useChatHubMessageActions(options: UseChatHubMessageActionsOptions): UseChatHubMessageActionsResult;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
function isRetryableMessage(message) {
|
|
3
|
+
const status = typeof message.status === 'string' ? message.status.toLowerCase() : '';
|
|
4
|
+
return status === 'failed' || status === 'error';
|
|
5
|
+
}
|
|
6
|
+
function isMutedMessage(message) {
|
|
7
|
+
const status = typeof message.status === 'string' ? message.status.toLowerCase() : '';
|
|
8
|
+
return status === 'deleted' || status === 'revoked';
|
|
9
|
+
}
|
|
10
|
+
export function useChatHubMessageActions(options) {
|
|
11
|
+
const { message, mutations, enableCopy = true, enableReply = true, enableForward = true, enableRetry = true, enableDelete = false, enableRevoke = false, onForward, onRetry, onDelete, onRevoke, } = options;
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
const actions = [];
|
|
14
|
+
const muted = isMutedMessage(message);
|
|
15
|
+
if (enableCopy) {
|
|
16
|
+
const state = mutations?.getActionState(`copy:${message.messageId}`);
|
|
17
|
+
actions.push({
|
|
18
|
+
id: 'copy',
|
|
19
|
+
label: '复制',
|
|
20
|
+
shortLabel: '复制',
|
|
21
|
+
description: '复制消息文本,桌面端可用于菜单,移动端可用于长按动作面板',
|
|
22
|
+
pending: state?.pending,
|
|
23
|
+
disabled: muted || !mutations?.copyMessage,
|
|
24
|
+
preferredSurface: 'menu',
|
|
25
|
+
run: async () => {
|
|
26
|
+
if (!mutations?.copyMessage) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await mutations.copyMessage(message);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (enableReply) {
|
|
34
|
+
actions.push({
|
|
35
|
+
id: 'reply',
|
|
36
|
+
label: '回复',
|
|
37
|
+
shortLabel: '回复',
|
|
38
|
+
description: '将当前消息挂到 composer 回复态,适合桌面和移动端复用',
|
|
39
|
+
disabled: muted || !mutations?.setReplyTarget,
|
|
40
|
+
preferredSurface: 'inline',
|
|
41
|
+
run: () => {
|
|
42
|
+
mutations?.setReplyTarget?.(message);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (enableForward) {
|
|
47
|
+
const selected = mutations?.isForwardSelected?.(message.messageId) || false;
|
|
48
|
+
actions.push({
|
|
49
|
+
id: 'forward',
|
|
50
|
+
label: selected ? '取消转发' : '加入转发',
|
|
51
|
+
shortLabel: '转发',
|
|
52
|
+
description: '将当前消息加入待转发集合,由宿主决定是否展示目标会话选择并执行正式转发',
|
|
53
|
+
disabled: muted || (!mutations?.toggleForwardSelection && !onForward),
|
|
54
|
+
preferredSurface: 'sheet',
|
|
55
|
+
run: async () => {
|
|
56
|
+
mutations?.toggleForwardSelection?.(message);
|
|
57
|
+
await onForward?.(message);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (enableRetry && isRetryableMessage(message) && onRetry) {
|
|
62
|
+
actions.push({
|
|
63
|
+
id: 'retry',
|
|
64
|
+
label: '重试',
|
|
65
|
+
shortLabel: '重试',
|
|
66
|
+
description: '适用于宿主维护失败队列时的逐条重试动作',
|
|
67
|
+
preferredSurface: 'inline',
|
|
68
|
+
run: async () => {
|
|
69
|
+
await onRetry(message);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (enableDelete && onDelete) {
|
|
74
|
+
const state = mutations?.getActionState(`delete:${message.messageId}`);
|
|
75
|
+
actions.push({
|
|
76
|
+
id: 'delete',
|
|
77
|
+
label: '删除',
|
|
78
|
+
shortLabel: '删除',
|
|
79
|
+
description: '将消息更新为删除状态,保留消息记录但以删除占位展示',
|
|
80
|
+
pending: state?.pending,
|
|
81
|
+
destructive: true,
|
|
82
|
+
preferredSurface: 'menu',
|
|
83
|
+
disabled: muted,
|
|
84
|
+
run: async () => {
|
|
85
|
+
await onDelete(message);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (enableRevoke && onRevoke) {
|
|
90
|
+
const state = mutations?.getActionState(`revoke:${message.messageId}`);
|
|
91
|
+
actions.push({
|
|
92
|
+
id: 'revoke',
|
|
93
|
+
label: '撤回',
|
|
94
|
+
shortLabel: '撤回',
|
|
95
|
+
description: '将消息更新为撤回状态,并向当前会话同步撤回占位',
|
|
96
|
+
pending: state?.pending,
|
|
97
|
+
destructive: true,
|
|
98
|
+
preferredSurface: 'menu',
|
|
99
|
+
disabled: muted,
|
|
100
|
+
run: async () => {
|
|
101
|
+
await onRevoke(message);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
actions,
|
|
107
|
+
primaryActions: actions.filter((action) => !action.hidden && action.preferredSurface === 'inline'),
|
|
108
|
+
overflowActions: actions.filter((action) => !action.hidden && action.preferredSurface !== 'inline'),
|
|
109
|
+
};
|
|
110
|
+
}, [
|
|
111
|
+
enableCopy,
|
|
112
|
+
enableDelete,
|
|
113
|
+
enableForward,
|
|
114
|
+
enableRevoke,
|
|
115
|
+
enableReply,
|
|
116
|
+
enableRetry,
|
|
117
|
+
message,
|
|
118
|
+
mutations,
|
|
119
|
+
onDelete,
|
|
120
|
+
onForward,
|
|
121
|
+
onRevoke,
|
|
122
|
+
onRetry,
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChatHubAppRuntime, ConversationMessageRecord, SendChatHubMessageOptions, SendTextMessageOptions, UpdateChatHubMessageOptions } from '@appthen/sdk-core';
|
|
2
|
+
export interface ChatHubForwardDraftItem {
|
|
3
|
+
messageId: string;
|
|
4
|
+
sourceConversationId: string;
|
|
5
|
+
senderSubjectId: string;
|
|
6
|
+
kind?: string;
|
|
7
|
+
previewText: string;
|
|
8
|
+
content: Record<string, unknown>;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface ForwardChatHubMessagesOptions {
|
|
12
|
+
targetConversationId: string;
|
|
13
|
+
senderSubjectId?: string;
|
|
14
|
+
items?: ChatHubForwardDraftItem[];
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
clearSelection?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface UseChatHubMessageMutationsOptions {
|
|
19
|
+
runtime: ChatHubAppRuntime;
|
|
20
|
+
selectedConversationId?: string;
|
|
21
|
+
selectedSenderId?: string;
|
|
22
|
+
copyToClipboard?: (text: string) => Promise<void>;
|
|
23
|
+
allowMultiForwardSelection?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ChatHubMutationActionState {
|
|
26
|
+
pending: boolean;
|
|
27
|
+
error: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface UseChatHubMessageMutationsResult {
|
|
30
|
+
replyTarget: ConversationMessageRecord | null;
|
|
31
|
+
setReplyTarget: (message: ConversationMessageRecord | null) => void;
|
|
32
|
+
clearReplyTarget: () => void;
|
|
33
|
+
forwardDraftItems: ChatHubForwardDraftItem[];
|
|
34
|
+
toggleForwardSelection: (message: ConversationMessageRecord) => void;
|
|
35
|
+
clearForwardSelection: () => void;
|
|
36
|
+
isForwardSelected: (messageId: string) => boolean;
|
|
37
|
+
getActionState: (actionKey: string) => ChatHubMutationActionState;
|
|
38
|
+
copyMessage: (message: ConversationMessageRecord) => Promise<string>;
|
|
39
|
+
deleteMessage: (message: ConversationMessageRecord, input?: Omit<UpdateChatHubMessageOptions, 'messageId' | 'status'>) => Promise<ConversationMessageRecord>;
|
|
40
|
+
revokeMessage: (message: ConversationMessageRecord, input?: Omit<UpdateChatHubMessageOptions, 'messageId' | 'status'>) => Promise<ConversationMessageRecord>;
|
|
41
|
+
forwardMessages: (input: ForwardChatHubMessagesOptions) => Promise<ConversationMessageRecord[]>;
|
|
42
|
+
forwardSelectedMessages: (input: Omit<ForwardChatHubMessagesOptions, 'items'>) => Promise<ConversationMessageRecord[]>;
|
|
43
|
+
sendTextMessage: (input: Omit<SendTextMessageOptions, 'conversationId' | 'senderSubjectId'> & {
|
|
44
|
+
conversationId?: string;
|
|
45
|
+
senderSubjectId?: string;
|
|
46
|
+
}) => Promise<ConversationMessageRecord>;
|
|
47
|
+
sendMessage: (input: Omit<SendChatHubMessageOptions, 'conversationId' | 'senderSubjectId'> & {
|
|
48
|
+
conversationId?: string;
|
|
49
|
+
senderSubjectId?: string;
|
|
50
|
+
}) => Promise<ConversationMessageRecord>;
|
|
51
|
+
}
|
|
52
|
+
export declare function extractChatHubMessagePlainText(message: ConversationMessageRecord): string;
|
|
53
|
+
export declare function useChatHubMessageMutations(options: UseChatHubMessageMutationsOptions): UseChatHubMessageMutationsResult;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
function resolveMutationError(error, fallback) {
|
|
3
|
+
return error instanceof Error ? error.message : fallback;
|
|
4
|
+
}
|
|
5
|
+
export function extractChatHubMessagePlainText(message) {
|
|
6
|
+
const content = message.content || {};
|
|
7
|
+
const textCandidates = [
|
|
8
|
+
content.text,
|
|
9
|
+
content.markdown,
|
|
10
|
+
content.body,
|
|
11
|
+
content.summary,
|
|
12
|
+
content.label,
|
|
13
|
+
];
|
|
14
|
+
const matchedText = textCandidates.find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
15
|
+
if (typeof matchedText === 'string') {
|
|
16
|
+
return matchedText;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(content, null, 2);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return message.messageId;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function toForwardDraftItem(message) {
|
|
26
|
+
return {
|
|
27
|
+
messageId: message.messageId,
|
|
28
|
+
sourceConversationId: message.conversationId,
|
|
29
|
+
senderSubjectId: message.senderSubjectId,
|
|
30
|
+
kind: message.kind,
|
|
31
|
+
previewText: extractChatHubMessagePlainText(message),
|
|
32
|
+
content: message.content,
|
|
33
|
+
metadata: message.metadata,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function mergeReplyMetadata(metadata, replyTarget) {
|
|
37
|
+
if (!replyTarget) {
|
|
38
|
+
return metadata;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...(metadata || {}),
|
|
42
|
+
replyTo: {
|
|
43
|
+
messageId: replyTarget.messageId,
|
|
44
|
+
senderSubjectId: replyTarget.senderSubjectId,
|
|
45
|
+
kind: replyTarget.kind,
|
|
46
|
+
previewText: extractChatHubMessagePlainText(replyTarget),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function isMutedMessage(message) {
|
|
51
|
+
return message.status === 'deleted' || message.status === 'revoked';
|
|
52
|
+
}
|
|
53
|
+
export function useChatHubMessageMutations(options) {
|
|
54
|
+
const { runtime, selectedConversationId = '', selectedSenderId = '', copyToClipboard, allowMultiForwardSelection = true, } = options;
|
|
55
|
+
const [replyTarget, setReplyTarget] = useState(null);
|
|
56
|
+
const [forwardDraftItems, setForwardDraftItems] = useState([]);
|
|
57
|
+
const [actionStateByKey, setActionStateByKey] = useState({});
|
|
58
|
+
const updateActionState = useCallback((actionKey, nextState) => {
|
|
59
|
+
setActionStateByKey((current) => {
|
|
60
|
+
if (!nextState) {
|
|
61
|
+
const { [actionKey]: _removed, ...rest } = current;
|
|
62
|
+
return rest;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...current,
|
|
66
|
+
[actionKey]: {
|
|
67
|
+
pending: false,
|
|
68
|
+
error: null,
|
|
69
|
+
...(current[actionKey] || {}),
|
|
70
|
+
...nextState,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
const runAction = useCallback(async (actionKey, task, fallbackMessage) => {
|
|
76
|
+
updateActionState(actionKey, {
|
|
77
|
+
pending: true,
|
|
78
|
+
error: null,
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const result = await task();
|
|
82
|
+
updateActionState(actionKey, {
|
|
83
|
+
pending: false,
|
|
84
|
+
error: null,
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
updateActionState(actionKey, {
|
|
90
|
+
pending: false,
|
|
91
|
+
error: resolveMutationError(error, fallbackMessage),
|
|
92
|
+
});
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}, [updateActionState]);
|
|
96
|
+
const resolveClipboardWriter = useCallback(async () => {
|
|
97
|
+
if (copyToClipboard) {
|
|
98
|
+
return copyToClipboard;
|
|
99
|
+
}
|
|
100
|
+
if (typeof navigator !== 'undefined' &&
|
|
101
|
+
navigator.clipboard &&
|
|
102
|
+
typeof navigator.clipboard.writeText === 'function') {
|
|
103
|
+
return async (text) => navigator.clipboard.writeText(text);
|
|
104
|
+
}
|
|
105
|
+
throw new Error('当前环境不支持剪贴板写入');
|
|
106
|
+
}, [copyToClipboard]);
|
|
107
|
+
const clearReplyTarget = useCallback(() => {
|
|
108
|
+
setReplyTarget(null);
|
|
109
|
+
}, []);
|
|
110
|
+
const clearForwardSelection = useCallback(() => {
|
|
111
|
+
setForwardDraftItems([]);
|
|
112
|
+
}, []);
|
|
113
|
+
const toggleForwardSelection = useCallback((message) => {
|
|
114
|
+
setForwardDraftItems((current) => {
|
|
115
|
+
const exists = current.some((item) => item.messageId === message.messageId);
|
|
116
|
+
if (exists) {
|
|
117
|
+
return current.filter((item) => item.messageId !== message.messageId);
|
|
118
|
+
}
|
|
119
|
+
if (!allowMultiForwardSelection) {
|
|
120
|
+
return [toForwardDraftItem(message)];
|
|
121
|
+
}
|
|
122
|
+
return [...current, toForwardDraftItem(message)];
|
|
123
|
+
});
|
|
124
|
+
}, [allowMultiForwardSelection]);
|
|
125
|
+
const isForwardSelected = useCallback((messageId) => forwardDraftItems.some((item) => item.messageId === messageId), [forwardDraftItems]);
|
|
126
|
+
const getActionState = useCallback((actionKey) => actionStateByKey[actionKey] || {
|
|
127
|
+
pending: false,
|
|
128
|
+
error: null,
|
|
129
|
+
}, [actionStateByKey]);
|
|
130
|
+
const copyMessage = useCallback(async (message) => {
|
|
131
|
+
if (isMutedMessage(message)) {
|
|
132
|
+
throw new Error('已删除或撤回的消息不能复制');
|
|
133
|
+
}
|
|
134
|
+
const actionKey = `copy:${message.messageId}`;
|
|
135
|
+
return runAction(actionKey, async () => {
|
|
136
|
+
const text = extractChatHubMessagePlainText(message);
|
|
137
|
+
const writeToClipboard = await resolveClipboardWriter();
|
|
138
|
+
await writeToClipboard(text);
|
|
139
|
+
return text;
|
|
140
|
+
}, '复制消息失败');
|
|
141
|
+
}, [resolveClipboardWriter, runAction]);
|
|
142
|
+
const deleteMessage = useCallback(async (message, input) => {
|
|
143
|
+
const actionKey = `delete:${message.messageId}`;
|
|
144
|
+
return runAction(actionKey, async () => runtime.deleteMessage({
|
|
145
|
+
messageId: message.messageId,
|
|
146
|
+
actorSubjectId: input?.actorSubjectId || selectedSenderId || message.senderSubjectId,
|
|
147
|
+
metadata: input?.metadata,
|
|
148
|
+
}), '删除消息失败');
|
|
149
|
+
}, [runAction, runtime, selectedSenderId]);
|
|
150
|
+
const revokeMessage = useCallback(async (message, input) => {
|
|
151
|
+
const actionKey = `revoke:${message.messageId}`;
|
|
152
|
+
return runAction(actionKey, async () => runtime.revokeMessage({
|
|
153
|
+
messageId: message.messageId,
|
|
154
|
+
actorSubjectId: input?.actorSubjectId || selectedSenderId || message.senderSubjectId,
|
|
155
|
+
metadata: input?.metadata,
|
|
156
|
+
}), '撤回消息失败');
|
|
157
|
+
}, [runAction, runtime, selectedSenderId]);
|
|
158
|
+
const forwardMessages = useCallback(async (input) => {
|
|
159
|
+
const targetConversationId = input.targetConversationId;
|
|
160
|
+
const senderSubjectId = input.senderSubjectId || selectedSenderId;
|
|
161
|
+
const items = input.items || forwardDraftItems;
|
|
162
|
+
if (!targetConversationId) {
|
|
163
|
+
throw new Error('缺少目标 conversationId,无法转发消息');
|
|
164
|
+
}
|
|
165
|
+
if (!senderSubjectId) {
|
|
166
|
+
throw new Error('缺少 senderSubjectId,无法转发消息');
|
|
167
|
+
}
|
|
168
|
+
if (items.length === 0) {
|
|
169
|
+
throw new Error('当前没有可转发的消息');
|
|
170
|
+
}
|
|
171
|
+
const actionKey = `forward:${targetConversationId}:${items
|
|
172
|
+
.map((item) => item.messageId)
|
|
173
|
+
.join(',')}`;
|
|
174
|
+
return runAction(actionKey, async () => {
|
|
175
|
+
const results = await Promise.all(items.map((item) => runtime.sendMessage({
|
|
176
|
+
conversationId: targetConversationId,
|
|
177
|
+
senderSubjectId,
|
|
178
|
+
kind: 'forwarded_message',
|
|
179
|
+
content: {
|
|
180
|
+
text: item.previewText,
|
|
181
|
+
previewText: item.previewText,
|
|
182
|
+
originalMessageId: item.messageId,
|
|
183
|
+
originalKind: item.kind,
|
|
184
|
+
originalSenderSubjectId: item.senderSubjectId,
|
|
185
|
+
senderSubjectId: item.senderSubjectId,
|
|
186
|
+
sourceConversationId: item.sourceConversationId,
|
|
187
|
+
originalContent: item.content,
|
|
188
|
+
},
|
|
189
|
+
metadata: {
|
|
190
|
+
...(input.metadata || {}),
|
|
191
|
+
forwardedFrom: {
|
|
192
|
+
conversationId: item.sourceConversationId,
|
|
193
|
+
messageId: item.messageId,
|
|
194
|
+
senderSubjectId: item.senderSubjectId,
|
|
195
|
+
kind: item.kind,
|
|
196
|
+
},
|
|
197
|
+
originalMetadata: item.metadata,
|
|
198
|
+
},
|
|
199
|
+
})));
|
|
200
|
+
if (input.clearSelection !== false) {
|
|
201
|
+
setForwardDraftItems((current) => {
|
|
202
|
+
const forwardedMessageIds = new Set(items.map((item) => item.messageId));
|
|
203
|
+
return current.filter((item) => !forwardedMessageIds.has(item.messageId));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
}, '转发消息失败');
|
|
208
|
+
}, [forwardDraftItems, runAction, runtime, selectedSenderId]);
|
|
209
|
+
const forwardSelectedMessages = useCallback(async (input) => forwardMessages({
|
|
210
|
+
...input,
|
|
211
|
+
items: forwardDraftItems,
|
|
212
|
+
}), [forwardDraftItems, forwardMessages]);
|
|
213
|
+
const sendTextMessage = useCallback(async (input) => {
|
|
214
|
+
const conversationId = input.conversationId || selectedConversationId;
|
|
215
|
+
const senderSubjectId = input.senderSubjectId || selectedSenderId;
|
|
216
|
+
if (!conversationId) {
|
|
217
|
+
throw new Error('缺少 conversationId,无法发送消息');
|
|
218
|
+
}
|
|
219
|
+
if (!senderSubjectId) {
|
|
220
|
+
throw new Error('缺少 senderSubjectId,无法发送消息');
|
|
221
|
+
}
|
|
222
|
+
const actionKey = `send-text:${conversationId}:${input.messageId || 'draft'}`;
|
|
223
|
+
const currentReplyTarget = replyTarget;
|
|
224
|
+
return runAction(actionKey, async () => {
|
|
225
|
+
const message = await runtime.sendTextMessage({
|
|
226
|
+
...input,
|
|
227
|
+
conversationId,
|
|
228
|
+
senderSubjectId,
|
|
229
|
+
metadata: mergeReplyMetadata(input.metadata, currentReplyTarget),
|
|
230
|
+
});
|
|
231
|
+
if (currentReplyTarget) {
|
|
232
|
+
setReplyTarget((existing) => existing?.messageId === currentReplyTarget.messageId ? null : existing);
|
|
233
|
+
}
|
|
234
|
+
return message;
|
|
235
|
+
}, '发送消息失败');
|
|
236
|
+
}, [replyTarget, runAction, runtime, selectedConversationId, selectedSenderId]);
|
|
237
|
+
const sendMessage = useCallback(async (input) => {
|
|
238
|
+
const conversationId = input.conversationId || selectedConversationId;
|
|
239
|
+
const senderSubjectId = input.senderSubjectId || selectedSenderId;
|
|
240
|
+
if (!conversationId) {
|
|
241
|
+
throw new Error('缺少 conversationId,无法发送消息');
|
|
242
|
+
}
|
|
243
|
+
if (!senderSubjectId) {
|
|
244
|
+
throw new Error('缺少 senderSubjectId,无法发送消息');
|
|
245
|
+
}
|
|
246
|
+
const actionKey = `send:${conversationId}:${input.messageId || input.kind}`;
|
|
247
|
+
const currentReplyTarget = replyTarget;
|
|
248
|
+
return runAction(actionKey, async () => {
|
|
249
|
+
const message = await runtime.sendMessage({
|
|
250
|
+
...input,
|
|
251
|
+
conversationId,
|
|
252
|
+
senderSubjectId,
|
|
253
|
+
metadata: mergeReplyMetadata(input.metadata, currentReplyTarget),
|
|
254
|
+
});
|
|
255
|
+
if (currentReplyTarget) {
|
|
256
|
+
setReplyTarget((existing) => existing?.messageId === currentReplyTarget.messageId ? null : existing);
|
|
257
|
+
}
|
|
258
|
+
return message;
|
|
259
|
+
}, '发送消息失败');
|
|
260
|
+
}, [replyTarget, runAction, runtime, selectedConversationId, selectedSenderId]);
|
|
261
|
+
return useMemo(() => ({
|
|
262
|
+
replyTarget,
|
|
263
|
+
setReplyTarget,
|
|
264
|
+
clearReplyTarget,
|
|
265
|
+
forwardDraftItems,
|
|
266
|
+
toggleForwardSelection,
|
|
267
|
+
clearForwardSelection,
|
|
268
|
+
isForwardSelected,
|
|
269
|
+
getActionState,
|
|
270
|
+
copyMessage,
|
|
271
|
+
deleteMessage,
|
|
272
|
+
revokeMessage,
|
|
273
|
+
forwardMessages,
|
|
274
|
+
forwardSelectedMessages,
|
|
275
|
+
sendTextMessage,
|
|
276
|
+
sendMessage,
|
|
277
|
+
}), [
|
|
278
|
+
clearForwardSelection,
|
|
279
|
+
clearReplyTarget,
|
|
280
|
+
copyMessage,
|
|
281
|
+
deleteMessage,
|
|
282
|
+
forwardMessages,
|
|
283
|
+
forwardSelectedMessages,
|
|
284
|
+
forwardDraftItems,
|
|
285
|
+
getActionState,
|
|
286
|
+
isForwardSelected,
|
|
287
|
+
replyTarget,
|
|
288
|
+
revokeMessage,
|
|
289
|
+
sendMessage,
|
|
290
|
+
sendTextMessage,
|
|
291
|
+
toggleForwardSelection,
|
|
292
|
+
]);
|
|
293
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appthen/sdk-react",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "AppThen Conversation Hub SDK - React headless hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@appthen/sdk-core": "0.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
32
|
+
}
|
|
33
|
+
}
|