@ominiflow/sdk-react 0.2.0-rc.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/README.md +99 -0
- package/dist/components.d.ts +15 -0
- package/dist/hooks.d.ts +14 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +471 -0
- package/dist/provider.d.ts +25 -0
- package/dist/query-keys.d.ts +6 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @ominiflow/sdk-react
|
|
2
|
+
|
|
3
|
+
Bindings React oficiais para o SDK TypeScript do OmniFlow. O pacote entrega `OmniFlowProvider`, hooks idiomáticos com TanStack Query e componentes opt-in para inbox rápido.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ominiflow/sdk @ominiflow/sdk-react @tanstack/react-query
|
|
9
|
+
# ou
|
|
10
|
+
bun add @ominiflow/sdk @ominiflow/sdk-react @tanstack/react-query
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { OmniFlowProvider, MessageList, Composer, useSendMessage } from '@ominiflow/sdk-react';
|
|
17
|
+
|
|
18
|
+
function ConversationPanel() {
|
|
19
|
+
const sendMessage = useSendMessage();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ display: 'grid', gap: 12 }}>
|
|
23
|
+
<MessageList conversationId="conv_123" />
|
|
24
|
+
<Composer
|
|
25
|
+
onSend={(text) =>
|
|
26
|
+
sendMessage.mutateAsync({
|
|
27
|
+
to: '+5511999999999',
|
|
28
|
+
channel: 'whatsapp_official',
|
|
29
|
+
content: { type: 'text', text },
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function App() {
|
|
38
|
+
return (
|
|
39
|
+
<OmniFlowProvider config={{ jwt: 'jwt_emitido_pelo_backend', baseUrl: 'https://api.omniflow.com.br' }}>
|
|
40
|
+
<ConversationPanel />
|
|
41
|
+
</OmniFlowProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Provider
|
|
47
|
+
|
|
48
|
+
O `OmniFlowProvider` aceita um `client` já instanciado ou `config` do `@ominiflow/sdk`.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<OmniFlowProvider config={{ jwt: 'jwt_frontend', baseUrl: 'https://api.omniflow.com.br' }}>
|
|
52
|
+
<App />
|
|
53
|
+
</OmniFlowProvider>
|
|
54
|
+
|
|
55
|
+
<OmniFlowProvider config={{ apiKey: 'omf_live_...' }}>
|
|
56
|
+
<App />
|
|
57
|
+
</OmniFlowProvider>
|
|
58
|
+
|
|
59
|
+
<OmniFlowProvider
|
|
60
|
+
config={{
|
|
61
|
+
oauth: { clientId: 'client_123', clientSecret: 'secret_123' },
|
|
62
|
+
baseUrl: 'https://api.omniflow.com.br',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<App />
|
|
66
|
+
</OmniFlowProvider>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Hooks
|
|
70
|
+
|
|
71
|
+
### `useOmniFlow()`
|
|
72
|
+
|
|
73
|
+
Retorna a instância do `OmniFlow` provida pelo contexto.
|
|
74
|
+
|
|
75
|
+
### `useConversations(filters?)`
|
|
76
|
+
|
|
77
|
+
Executa `client.conversations.list(filters)` com query key namespaced.
|
|
78
|
+
|
|
79
|
+
### `useMessages(conversationId, options?)`
|
|
80
|
+
|
|
81
|
+
Executa paginação cursor de mensagens com `useInfiniteQuery`.
|
|
82
|
+
|
|
83
|
+
### `useSendMessage()`
|
|
84
|
+
|
|
85
|
+
Envia mensagens de texto/imagem/template com optimistic update quando `conversationId` é informado.
|
|
86
|
+
|
|
87
|
+
## Componentes
|
|
88
|
+
|
|
89
|
+
### `<MessageList conversationId={...} />`
|
|
90
|
+
|
|
91
|
+
- renderiza mensagens da conversa
|
|
92
|
+
- busca próxima página ao rolar para o topo
|
|
93
|
+
- placeholder de estado vazio
|
|
94
|
+
|
|
95
|
+
### `<Composer onSend={...} />`
|
|
96
|
+
|
|
97
|
+
- textarea com envio por Enter
|
|
98
|
+
- emoji picker simples
|
|
99
|
+
- seletor de anexo opcional
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type MessageListProps = {
|
|
2
|
+
conversationId: string;
|
|
3
|
+
pageSize?: number;
|
|
4
|
+
emptyState?: string;
|
|
5
|
+
height?: number | string;
|
|
6
|
+
};
|
|
7
|
+
export declare function MessageList({ conversationId, pageSize, emptyState, height, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export type ComposerProps = {
|
|
9
|
+
onSend: (text: string) => void | Promise<void>;
|
|
10
|
+
onAttachmentSelect?: (file: File) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
emojiOptions?: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function Composer({ onSend, onAttachmentSelect, disabled, placeholder, emojiOptions, }: ComposerProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConversationFilters, ListMessagesParams, MessageDto, PaginatedResult, SendMessageParams, SendTemplateParams } from '@omniflow/sdk';
|
|
2
|
+
export type UseSendMessageParams = SendMessageParams | SendTemplateParams;
|
|
3
|
+
export type UseSendMessageOptions = {
|
|
4
|
+
conversationId?: string;
|
|
5
|
+
};
|
|
6
|
+
export type UseMessagesOptions = ListMessagesParams;
|
|
7
|
+
type MutationContext = {
|
|
8
|
+
optimisticMessage?: MessageDto;
|
|
9
|
+
};
|
|
10
|
+
export declare function useOmniFlow(): import("@omniflow/sdk").OmniFlow;
|
|
11
|
+
export declare function useConversations(filters?: ConversationFilters): import("@tanstack/react-query").UseQueryResult<PaginatedResult<import("@omniflow/sdk").ConversationDto>, Error>;
|
|
12
|
+
export declare function useMessages(conversationId: string, options?: UseMessagesOptions): import("@tanstack/react-query").UseInfiniteQueryResult<import("@tanstack/react-query").InfiniteData<PaginatedResult<MessageDto>, unknown>, Error>;
|
|
13
|
+
export declare function useSendMessage(options?: UseSendMessageOptions): import("@tanstack/react-query").UseMutationResult<MessageDto, Error, UseSendMessageParams, MutationContext>;
|
|
14
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { OmniFlowProvider, useOmniFlowContext } from './provider';
|
|
2
|
+
export { Composer, MessageList } from './components';
|
|
3
|
+
export { useConversations, useMessages, useOmniFlow, useSendMessage } from './hooks';
|
|
4
|
+
export { conversationsQueryKey, messagesQueryKey, messagesQueryRoot, OMNIFLOW_CONVERSATIONS_QUERY_ROOT, OMNIFLOW_QUERY_ROOT, } from './query-keys';
|
|
5
|
+
export type { OmniFlowProviderProps } from './provider';
|
|
6
|
+
export type { ComposerProps, MessageListProps } from './components';
|
|
7
|
+
export type { UseMessagesOptions, UseSendMessageOptions, UseSendMessageParams } from './hooks';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// src/provider.tsx
|
|
2
|
+
import { OmniFlow } from "@omniflow/sdk";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
// src/query-keys.ts
|
|
13
|
+
var OMNIFLOW_QUERY_ROOT = ["omniflow"];
|
|
14
|
+
var OMNIFLOW_CONVERSATIONS_QUERY_ROOT = [...OMNIFLOW_QUERY_ROOT, "conversations"];
|
|
15
|
+
function conversationsQueryKey(filters = {}) {
|
|
16
|
+
return [...OMNIFLOW_CONVERSATIONS_QUERY_ROOT, filters];
|
|
17
|
+
}
|
|
18
|
+
function messagesQueryRoot(conversationId) {
|
|
19
|
+
return [...OMNIFLOW_QUERY_ROOT, "messages", conversationId];
|
|
20
|
+
}
|
|
21
|
+
function messagesQueryKey(conversationId, params = {}) {
|
|
22
|
+
return [...messagesQueryRoot(conversationId), params];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/provider.tsx
|
|
26
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
27
|
+
var OmniFlowReactContext = createContext(null);
|
|
28
|
+
function resolveClient(props) {
|
|
29
|
+
if ("client" in props) {
|
|
30
|
+
if (!props.client) {
|
|
31
|
+
throw new Error("client is required");
|
|
32
|
+
}
|
|
33
|
+
return props.client;
|
|
34
|
+
}
|
|
35
|
+
return new OmniFlow(props.config);
|
|
36
|
+
}
|
|
37
|
+
function getRealtimeTarget(eventTarget) {
|
|
38
|
+
if (eventTarget)
|
|
39
|
+
return eventTarget;
|
|
40
|
+
if (typeof window !== "undefined")
|
|
41
|
+
return window;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
function OmniFlowProvider({
|
|
45
|
+
children,
|
|
46
|
+
queryClient: providedQueryClient,
|
|
47
|
+
eventTarget,
|
|
48
|
+
eventName = "omniflow:event",
|
|
49
|
+
...props
|
|
50
|
+
}) {
|
|
51
|
+
const clientRef = useRef(null);
|
|
52
|
+
const client = clientRef.current ?? resolveClient(props);
|
|
53
|
+
clientRef.current = client;
|
|
54
|
+
const [queryClient] = useState(() => providedQueryClient ?? new QueryClient({
|
|
55
|
+
defaultOptions: {
|
|
56
|
+
queries: {
|
|
57
|
+
staleTime: 30000
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const target = getRealtimeTarget(eventTarget);
|
|
63
|
+
if (!target)
|
|
64
|
+
return;
|
|
65
|
+
const handleRealtimeEvent = (event) => {
|
|
66
|
+
const detail = event instanceof CustomEvent ? event.detail : null;
|
|
67
|
+
if (!detail) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (detail.type === "message.received") {
|
|
71
|
+
queryClient.invalidateQueries({ queryKey: OMNIFLOW_CONVERSATIONS_QUERY_ROOT });
|
|
72
|
+
if (detail.conversationId) {
|
|
73
|
+
queryClient.invalidateQueries({
|
|
74
|
+
queryKey: messagesQueryRoot(detail.conversationId)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (detail.type === "conversation.created" || detail.type === "conversation.resolved") {
|
|
80
|
+
queryClient.invalidateQueries({ queryKey: OMNIFLOW_CONVERSATIONS_QUERY_ROOT });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
queryClient.invalidateQueries({ queryKey: OMNIFLOW_QUERY_ROOT });
|
|
84
|
+
};
|
|
85
|
+
target.addEventListener(eventName, handleRealtimeEvent);
|
|
86
|
+
return () => {
|
|
87
|
+
target.removeEventListener(eventName, handleRealtimeEvent);
|
|
88
|
+
};
|
|
89
|
+
}, [eventName, eventTarget, queryClient]);
|
|
90
|
+
return /* @__PURE__ */ jsxDEV(OmniFlowReactContext.Provider, {
|
|
91
|
+
value: { client, queryClient },
|
|
92
|
+
children: /* @__PURE__ */ jsxDEV(QueryClientProvider, {
|
|
93
|
+
client: queryClient,
|
|
94
|
+
children
|
|
95
|
+
}, undefined, false, undefined, this)
|
|
96
|
+
}, undefined, false, undefined, this);
|
|
97
|
+
}
|
|
98
|
+
function useOmniFlowContext() {
|
|
99
|
+
const context = useContext(OmniFlowReactContext);
|
|
100
|
+
if (!context) {
|
|
101
|
+
throw new Error("OmniFlowProvider is required");
|
|
102
|
+
}
|
|
103
|
+
return context;
|
|
104
|
+
}
|
|
105
|
+
// src/components.tsx
|
|
106
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
107
|
+
|
|
108
|
+
// src/hooks.ts
|
|
109
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
110
|
+
function isTemplateSend(params) {
|
|
111
|
+
return "templateId" in params;
|
|
112
|
+
}
|
|
113
|
+
function toMessageContent(params) {
|
|
114
|
+
if (isTemplateSend(params)) {
|
|
115
|
+
return {
|
|
116
|
+
type: "template",
|
|
117
|
+
templateId: params.templateId,
|
|
118
|
+
params: params.params
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return params.content;
|
|
122
|
+
}
|
|
123
|
+
function buildOptimisticMessage(conversationId, params) {
|
|
124
|
+
return {
|
|
125
|
+
id: `optimistic-${Date.now()}`,
|
|
126
|
+
conversationId,
|
|
127
|
+
direction: "OUTBOUND",
|
|
128
|
+
content: toMessageContent(params),
|
|
129
|
+
createdAt: new Date().toISOString()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function useOmniFlow() {
|
|
133
|
+
return useOmniFlowContext().client;
|
|
134
|
+
}
|
|
135
|
+
function useConversations(filters = {}) {
|
|
136
|
+
const client = useOmniFlow();
|
|
137
|
+
return useQuery({
|
|
138
|
+
queryKey: conversationsQueryKey(filters),
|
|
139
|
+
queryFn: () => client.conversations.list(filters)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function useMessages(conversationId, options = {}) {
|
|
143
|
+
const client = useOmniFlow();
|
|
144
|
+
return useInfiniteQuery({
|
|
145
|
+
queryKey: messagesQueryKey(conversationId, options),
|
|
146
|
+
enabled: Boolean(conversationId),
|
|
147
|
+
initialPageParam: options.cursor,
|
|
148
|
+
queryFn: ({ pageParam }) => client.messages.list(conversationId, {
|
|
149
|
+
...options,
|
|
150
|
+
...pageParam ? { cursor: pageParam } : {}
|
|
151
|
+
}),
|
|
152
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function useSendMessage(options = {}) {
|
|
156
|
+
const client = useOmniFlow();
|
|
157
|
+
const queryClient = useQueryClient();
|
|
158
|
+
return useMutation({
|
|
159
|
+
mutationFn: (params) => isTemplateSend(params) ? client.templates.send(params) : client.messages.send(params),
|
|
160
|
+
onMutate: async (params) => {
|
|
161
|
+
if (!options.conversationId) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
const key = messagesQueryRoot(options.conversationId);
|
|
165
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
166
|
+
const optimisticMessage = buildOptimisticMessage(options.conversationId, params);
|
|
167
|
+
queryClient.setQueriesData({ queryKey: key }, (current) => {
|
|
168
|
+
if (!current) {
|
|
169
|
+
return current;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...current,
|
|
173
|
+
pages: current.pages.map((page, index) => {
|
|
174
|
+
if (index !== 0) {
|
|
175
|
+
return page;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
...page,
|
|
179
|
+
data: [optimisticMessage, ...page.data]
|
|
180
|
+
};
|
|
181
|
+
})
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
return { optimisticMessage };
|
|
185
|
+
},
|
|
186
|
+
onSuccess: (_message, _params) => {
|
|
187
|
+
queryClient.invalidateQueries({ queryKey: OMNIFLOW_CONVERSATIONS_QUERY_ROOT });
|
|
188
|
+
if (options.conversationId) {
|
|
189
|
+
queryClient.invalidateQueries({ queryKey: messagesQueryRoot(options.conversationId) });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
onError: (_error, _params, context) => {
|
|
193
|
+
if (!options.conversationId || !context?.optimisticMessage) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
queryClient.setQueriesData({ queryKey: messagesQueryRoot(options.conversationId) }, (current) => {
|
|
197
|
+
if (!current) {
|
|
198
|
+
return current;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
...current,
|
|
202
|
+
pages: current.pages.map((page) => ({
|
|
203
|
+
...page,
|
|
204
|
+
data: page.data.filter((message) => message.id !== context.optimisticMessage?.id)
|
|
205
|
+
}))
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/components.tsx
|
|
213
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
214
|
+
var DEFAULT_EMOJIS = ["\uD83D\uDE00", "\uD83D\uDC4D", "\uD83C\uDF89", "❤️"];
|
|
215
|
+
function formatMessageContent(message) {
|
|
216
|
+
if (message.content.type === "text") {
|
|
217
|
+
return message.content.text;
|
|
218
|
+
}
|
|
219
|
+
if (message.content.type === "image") {
|
|
220
|
+
return message.content.caption || message.content.url;
|
|
221
|
+
}
|
|
222
|
+
return `Template: ${message.content.templateId}`;
|
|
223
|
+
}
|
|
224
|
+
function formatTime(iso) {
|
|
225
|
+
try {
|
|
226
|
+
return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
227
|
+
} catch {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function MessageList({
|
|
232
|
+
conversationId,
|
|
233
|
+
pageSize = 20,
|
|
234
|
+
emptyState = "Nenhuma mensagem ainda",
|
|
235
|
+
height = 360
|
|
236
|
+
}) {
|
|
237
|
+
const listRef = useRef2(null);
|
|
238
|
+
const { data, isPending, hasNextPage, fetchNextPage, isFetchingNextPage } = useMessages(conversationId, { limit: pageSize });
|
|
239
|
+
const messages = (data?.pages ?? []).flatMap((page) => page.data);
|
|
240
|
+
const messageCount = messages.length;
|
|
241
|
+
useEffect2(() => {
|
|
242
|
+
const element = listRef.current;
|
|
243
|
+
if (!element || messageCount === 0) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
element.scrollTo?.({ top: element.scrollHeight });
|
|
248
|
+
} catch {}
|
|
249
|
+
}, [messageCount]);
|
|
250
|
+
const handleScroll = () => {
|
|
251
|
+
const element = listRef.current;
|
|
252
|
+
if (!element || !hasNextPage || isFetchingNextPage) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (element.scrollTop <= 32) {
|
|
256
|
+
fetchNextPage();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
if (!conversationId) {
|
|
260
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
261
|
+
role: "log",
|
|
262
|
+
children: emptyState
|
|
263
|
+
}, undefined, false, undefined, this);
|
|
264
|
+
}
|
|
265
|
+
if (isPending) {
|
|
266
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
267
|
+
role: "log",
|
|
268
|
+
children: "Carregando mensagens..."
|
|
269
|
+
}, undefined, false, undefined, this);
|
|
270
|
+
}
|
|
271
|
+
if (messages.length === 0) {
|
|
272
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
273
|
+
role: "log",
|
|
274
|
+
children: emptyState
|
|
275
|
+
}, undefined, false, undefined, this);
|
|
276
|
+
}
|
|
277
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
278
|
+
ref: listRef,
|
|
279
|
+
role: "log",
|
|
280
|
+
"aria-live": "polite",
|
|
281
|
+
"data-omniflow-message-list": true,
|
|
282
|
+
onScroll: handleScroll,
|
|
283
|
+
style: {
|
|
284
|
+
display: "flex",
|
|
285
|
+
flexDirection: "column",
|
|
286
|
+
gap: "8px",
|
|
287
|
+
overflowY: "auto",
|
|
288
|
+
height,
|
|
289
|
+
padding: "12px",
|
|
290
|
+
border: "1px solid #e5e7eb",
|
|
291
|
+
borderRadius: "12px",
|
|
292
|
+
background: "#ffffff"
|
|
293
|
+
},
|
|
294
|
+
children: [
|
|
295
|
+
hasNextPage ? /* @__PURE__ */ jsxDEV2("div", {
|
|
296
|
+
style: { alignSelf: "center", color: "#6b7280", fontSize: "12px" },
|
|
297
|
+
children: isFetchingNextPage ? "Carregando mais..." : "Role para carregar mais"
|
|
298
|
+
}, undefined, false, undefined, this) : null,
|
|
299
|
+
messages.map((message) => {
|
|
300
|
+
const outbound = message.direction === "OUTBOUND";
|
|
301
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
302
|
+
style: {
|
|
303
|
+
display: "flex",
|
|
304
|
+
flexDirection: "column",
|
|
305
|
+
alignItems: outbound ? "flex-end" : "flex-start"
|
|
306
|
+
},
|
|
307
|
+
children: [
|
|
308
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
309
|
+
style: {
|
|
310
|
+
maxWidth: "80%",
|
|
311
|
+
padding: "10px 12px",
|
|
312
|
+
borderRadius: outbound ? "16px 16px 4px 16px" : "16px 16px 16px 4px",
|
|
313
|
+
background: outbound ? "#0f766e" : "#f3f4f6",
|
|
314
|
+
color: outbound ? "#ffffff" : "#111827",
|
|
315
|
+
lineHeight: "1.4",
|
|
316
|
+
wordBreak: "break-word"
|
|
317
|
+
},
|
|
318
|
+
children: formatMessageContent(message)
|
|
319
|
+
}, undefined, false, undefined, this),
|
|
320
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
321
|
+
style: { marginTop: "4px", fontSize: "11px", color: "#6b7280" },
|
|
322
|
+
children: formatTime(message.createdAt)
|
|
323
|
+
}, undefined, false, undefined, this)
|
|
324
|
+
]
|
|
325
|
+
}, message.id, true, undefined, this);
|
|
326
|
+
})
|
|
327
|
+
]
|
|
328
|
+
}, undefined, true, undefined, this);
|
|
329
|
+
}
|
|
330
|
+
function Composer({
|
|
331
|
+
onSend,
|
|
332
|
+
onAttachmentSelect,
|
|
333
|
+
disabled = false,
|
|
334
|
+
placeholder = "Digite uma mensagem",
|
|
335
|
+
emojiOptions = DEFAULT_EMOJIS
|
|
336
|
+
}) {
|
|
337
|
+
const [text, setText] = useState2("");
|
|
338
|
+
const [emojiPickerOpen, setEmojiPickerOpen] = useState2(false);
|
|
339
|
+
const fileInputRef = useRef2(null);
|
|
340
|
+
const handleSubmit = async () => {
|
|
341
|
+
const trimmed = text.trim();
|
|
342
|
+
if (!trimmed || disabled) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
await onSend(trimmed);
|
|
346
|
+
setText("");
|
|
347
|
+
};
|
|
348
|
+
const canSend = text.trim().length > 0 && !disabled;
|
|
349
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
350
|
+
style: {
|
|
351
|
+
display: "flex",
|
|
352
|
+
flexDirection: "column",
|
|
353
|
+
gap: "8px",
|
|
354
|
+
padding: "12px",
|
|
355
|
+
border: "1px solid #e5e7eb",
|
|
356
|
+
borderRadius: "12px",
|
|
357
|
+
background: "#ffffff"
|
|
358
|
+
},
|
|
359
|
+
children: [
|
|
360
|
+
emojiPickerOpen ? /* @__PURE__ */ jsxDEV2("div", {
|
|
361
|
+
style: { display: "flex", gap: "6px", flexWrap: "wrap" },
|
|
362
|
+
children: emojiOptions.map((emoji) => /* @__PURE__ */ jsxDEV2("button", {
|
|
363
|
+
type: "button",
|
|
364
|
+
"data-emoji": emoji,
|
|
365
|
+
onClick: () => {
|
|
366
|
+
setText((current) => `${current}${emoji}`);
|
|
367
|
+
setEmojiPickerOpen(false);
|
|
368
|
+
},
|
|
369
|
+
style: {
|
|
370
|
+
border: "1px solid #d1d5db",
|
|
371
|
+
borderRadius: "8px",
|
|
372
|
+
background: "#fff",
|
|
373
|
+
padding: "6px 8px",
|
|
374
|
+
cursor: "pointer"
|
|
375
|
+
},
|
|
376
|
+
children: emoji
|
|
377
|
+
}, emoji, false, undefined, this))
|
|
378
|
+
}, undefined, false, undefined, this) : null,
|
|
379
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
380
|
+
style: { display: "flex", alignItems: "flex-end", gap: "8px" },
|
|
381
|
+
children: [
|
|
382
|
+
/* @__PURE__ */ jsxDEV2("button", {
|
|
383
|
+
type: "button",
|
|
384
|
+
"aria-label": "Adicionar emoji",
|
|
385
|
+
onClick: () => {
|
|
386
|
+
setEmojiPickerOpen((current) => !current);
|
|
387
|
+
},
|
|
388
|
+
children: "\uD83D\uDE42"
|
|
389
|
+
}, undefined, false, undefined, this),
|
|
390
|
+
/* @__PURE__ */ jsxDEV2("button", {
|
|
391
|
+
type: "button",
|
|
392
|
+
"aria-label": "Adicionar anexo",
|
|
393
|
+
onClick: () => {
|
|
394
|
+
fileInputRef.current?.click();
|
|
395
|
+
},
|
|
396
|
+
children: "+"
|
|
397
|
+
}, undefined, false, undefined, this),
|
|
398
|
+
/* @__PURE__ */ jsxDEV2("input", {
|
|
399
|
+
ref: fileInputRef,
|
|
400
|
+
type: "file",
|
|
401
|
+
style: { display: "none" },
|
|
402
|
+
onChange: (event) => {
|
|
403
|
+
const file = event.target.files?.[0];
|
|
404
|
+
if (!file) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
onAttachmentSelect?.(file);
|
|
408
|
+
event.target.value = "";
|
|
409
|
+
}
|
|
410
|
+
}, undefined, false, undefined, this),
|
|
411
|
+
/* @__PURE__ */ jsxDEV2("textarea", {
|
|
412
|
+
value: text,
|
|
413
|
+
rows: 1,
|
|
414
|
+
placeholder,
|
|
415
|
+
disabled,
|
|
416
|
+
onInput: (event) => {
|
|
417
|
+
setText(event.target.value);
|
|
418
|
+
},
|
|
419
|
+
onKeyDown: (event) => {
|
|
420
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
handleSubmit();
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
style: {
|
|
426
|
+
flex: 1,
|
|
427
|
+
minHeight: "40px",
|
|
428
|
+
maxHeight: "120px",
|
|
429
|
+
resize: "vertical",
|
|
430
|
+
borderRadius: "10px",
|
|
431
|
+
border: "1px solid #d1d5db",
|
|
432
|
+
padding: "10px 12px",
|
|
433
|
+
fontFamily: "inherit"
|
|
434
|
+
}
|
|
435
|
+
}, undefined, false, undefined, this),
|
|
436
|
+
/* @__PURE__ */ jsxDEV2("button", {
|
|
437
|
+
type: "button",
|
|
438
|
+
"aria-label": "Enviar mensagem",
|
|
439
|
+
disabled: !canSend,
|
|
440
|
+
onClick: () => {
|
|
441
|
+
handleSubmit();
|
|
442
|
+
},
|
|
443
|
+
style: {
|
|
444
|
+
border: "none",
|
|
445
|
+
borderRadius: "9999px",
|
|
446
|
+
padding: "10px 14px",
|
|
447
|
+
background: canSend ? "#0f766e" : "#d1d5db",
|
|
448
|
+
color: "#ffffff"
|
|
449
|
+
},
|
|
450
|
+
children: "Enviar"
|
|
451
|
+
}, undefined, false, undefined, this)
|
|
452
|
+
]
|
|
453
|
+
}, undefined, true, undefined, this)
|
|
454
|
+
]
|
|
455
|
+
}, undefined, true, undefined, this);
|
|
456
|
+
}
|
|
457
|
+
export {
|
|
458
|
+
useSendMessage,
|
|
459
|
+
useOmniFlowContext,
|
|
460
|
+
useOmniFlow,
|
|
461
|
+
useMessages,
|
|
462
|
+
useConversations,
|
|
463
|
+
messagesQueryRoot,
|
|
464
|
+
messagesQueryKey,
|
|
465
|
+
conversationsQueryKey,
|
|
466
|
+
OmniFlowProvider,
|
|
467
|
+
OMNIFLOW_QUERY_ROOT,
|
|
468
|
+
OMNIFLOW_CONVERSATIONS_QUERY_ROOT,
|
|
469
|
+
MessageList,
|
|
470
|
+
Composer
|
|
471
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OmniFlow, type OmniFlowConfig } from '@omniflow/sdk';
|
|
2
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { type PropsWithChildren } from 'react';
|
|
4
|
+
type OmniFlowProviderWithConfigProps = {
|
|
5
|
+
config: OmniFlowConfig;
|
|
6
|
+
client?: never;
|
|
7
|
+
queryClient?: QueryClient;
|
|
8
|
+
eventTarget?: EventTarget;
|
|
9
|
+
eventName?: string;
|
|
10
|
+
};
|
|
11
|
+
type OmniFlowProviderWithClientProps = {
|
|
12
|
+
client: OmniFlow;
|
|
13
|
+
config?: never;
|
|
14
|
+
queryClient?: QueryClient;
|
|
15
|
+
eventTarget?: EventTarget;
|
|
16
|
+
eventName?: string;
|
|
17
|
+
};
|
|
18
|
+
export type OmniFlowProviderProps = PropsWithChildren<OmniFlowProviderWithConfigProps | OmniFlowProviderWithClientProps>;
|
|
19
|
+
type OmniFlowReactContextValue = {
|
|
20
|
+
client: OmniFlow;
|
|
21
|
+
queryClient: QueryClient;
|
|
22
|
+
};
|
|
23
|
+
export declare function OmniFlowProvider({ children, queryClient: providedQueryClient, eventTarget, eventName, ...props }: OmniFlowProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function useOmniFlowContext(): OmniFlowReactContextValue;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ConversationFilters, ListMessagesParams } from '@omniflow/sdk';
|
|
2
|
+
export declare const OMNIFLOW_QUERY_ROOT: readonly ["omniflow"];
|
|
3
|
+
export declare const OMNIFLOW_CONVERSATIONS_QUERY_ROOT: readonly ["omniflow", "conversations"];
|
|
4
|
+
export declare function conversationsQueryKey(filters?: ConversationFilters): readonly ["omniflow", "conversations", ConversationFilters];
|
|
5
|
+
export declare function messagesQueryRoot(conversationId: string): readonly ["omniflow", "messages", string];
|
|
6
|
+
export declare function messagesQueryKey(conversationId: string, params?: ListMessagesParams): readonly ["omniflow", "messages", string, ListMessagesParams];
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ominiflow/sdk-react",
|
|
3
|
+
"version": "0.2.0-rc.0",
|
|
4
|
+
"description": "React bindings for the OmniFlow TypeScript SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"pretest": "bun run --cwd ../sdk build",
|
|
18
|
+
"build": "bun build ./src/index.ts --outdir dist --format esm --external react --external react-dom --external react/jsx-runtime --external @tanstack/react-query --external @omniflow/sdk && tsc --emitDeclarationOnly --declaration --declarationMap false --outDir dist",
|
|
19
|
+
"prebuild": "bun run --cwd ../sdk build",
|
|
20
|
+
"prestorybook": "bun run --cwd ../sdk build",
|
|
21
|
+
"storybook": "storybook dev -p 6006",
|
|
22
|
+
"prebuild-storybook": "bun run --cwd ../sdk build",
|
|
23
|
+
"build-storybook": "storybook build --output-dir storybook-static",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"lint": "biome check ."
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@tanstack/react-query": "^5.0.0",
|
|
30
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
31
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@ominiflow/sdk": "workspace:*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@happy-dom/global-registrator": "^14.0.0",
|
|
38
|
+
"@storybook/addon-a11y": "^10.3.5",
|
|
39
|
+
"@storybook/addon-docs": "^10.3.5",
|
|
40
|
+
"@storybook/react-vite": "^10.3.5",
|
|
41
|
+
"@tanstack/react-query": "^5.0.0",
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@types/react": "^19.0.0",
|
|
44
|
+
"@types/react-dom": "^19.0.0",
|
|
45
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
46
|
+
"react": "^19.0.0",
|
|
47
|
+
"react-dom": "^19.0.0",
|
|
48
|
+
"storybook": "^10.3.5",
|
|
49
|
+
"vite": "^6.0.0"
|
|
50
|
+
},
|
|
51
|
+
"sideEffects": false
|
|
52
|
+
}
|