@fayz-ai/plugin-conversations 0.9.4 → 0.10.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/context.d.ts +4 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +210 -205
- package/dist/index.js.map +1 -1
- package/dist/views/ConversationList.d.ts.map +1 -1
- package/dist/views/InboxView.d.ts.map +1 -1
- package/dist/views/NewConversationPanel.d.ts +13 -0
- package/dist/views/NewConversationPanel.d.ts.map +1 -0
- package/package.json +4 -4
- package/dist/views/NewConversationModal.d.ts +0 -6
- package/dist/views/NewConversationModal.d.ts.map +0 -1
package/dist/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type StoreApi } from 'zustand';
|
|
3
3
|
import type { EntityLookup } from '@fayz-ai/admin';
|
|
4
|
+
import type { EntityDef } from '@fayz-ai/core';
|
|
4
5
|
import type { ConversationsUIState } from './store';
|
|
5
6
|
/**
|
|
6
7
|
* App-tunable knobs the inbox UI reads. Kept tiny on purpose: everything here
|
|
@@ -8,12 +9,12 @@ import type { ConversationsUIState } from './store';
|
|
|
8
9
|
* never product policy.
|
|
9
10
|
*/
|
|
10
11
|
export interface ResolvedConversationsConfig {
|
|
11
|
-
/** `people.kind`
|
|
12
|
+
/** `people.kind` the picker searches. */
|
|
12
13
|
contactKind: string;
|
|
13
|
-
/** Per-vertical extension table linked by `person_id` (skipped when absent). */
|
|
14
|
-
contactExtensionTable?: string;
|
|
15
14
|
/** Search source for the contact picker. Defaults to the person archetype. */
|
|
16
15
|
contactLookup?: EntityLookup;
|
|
16
|
+
/** The app's own contact entity — "Create" opens ITS form. Agenda's twin is `clientEntityDef`. */
|
|
17
|
+
contactEntityDef?: EntityDef;
|
|
17
18
|
}
|
|
18
19
|
export declare const DEFAULT_CONVERSATIONS_CONFIG: ResolvedConversationsConfig;
|
|
19
20
|
export declare function ConversationsContextProvider({ store, config, children, }: {
|
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,kGAAkG;IAClG,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AAED,eAAO,MAAM,4BAA4B,EAAE,2BAE1C,CAAA;AAKD,wBAAgB,4BAA4B,CAAC,EAC3C,KAAK,EACL,MAAqC,EACrC,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAA;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B,qBAMA;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,CAAC,GAAG,CAAC,CAIxF;AAED,wBAAgB,sBAAsB,IAAI,2BAA2B,CAEpE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core';
|
|
1
|
+
import type { EntityDef, PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core';
|
|
2
2
|
import type { EntityLookup } from '@fayz-ai/admin';
|
|
3
3
|
import type { ConversationsProvider } from './data/types';
|
|
4
4
|
export interface ConversationsPluginOptions {
|
|
@@ -14,13 +14,13 @@ export interface ConversationsPluginOptions {
|
|
|
14
14
|
* hence a knob rather than a hardcoded value. Default: 'contact'.
|
|
15
15
|
*/
|
|
16
16
|
contactKind?: string;
|
|
17
|
-
/**
|
|
18
|
-
* Per-vertical extension table linked by `person_id` (e.g. 'clients'). Pools
|
|
19
|
-
* without it live off `public.people` alone — absent is not an error.
|
|
20
|
-
*/
|
|
21
|
-
contactExtensionTable?: string;
|
|
22
17
|
/** Custom search source for the picker. Defaults to the person archetype lookup. */
|
|
23
18
|
contactLookup?: EntityLookup;
|
|
19
|
+
/**
|
|
20
|
+
* The app's contact entity, same definition its registry page uses. "Create"
|
|
21
|
+
* in the picker opens THAT form. Without it the picker searches only.
|
|
22
|
+
*/
|
|
23
|
+
contactEntityDef?: EntityDef;
|
|
24
24
|
}
|
|
25
25
|
export declare function createConversationsPlugin(options?: ConversationsPluginOptions): PluginManifest;
|
|
26
26
|
export type { ConversationsProvider } from './data/types';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAEvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAkBzD,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IAEpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oFAAoF;IACpF,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AA2BD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA8G9F;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AACtF,OAAO,EAAE,+BAA+B,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAC3F,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,GACjC,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React4 from 'react';
|
|
2
2
|
import { registerTranslations, getSupabaseClientOptional, getActiveTenantId, useTranslation } from '@fayz-ai/core';
|
|
3
3
|
import { useStore } from 'zustand';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
-
import { MessageSquare,
|
|
6
|
-
import {
|
|
7
|
-
import { PermissionGate, useLimitGuard, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
5
|
+
import { SquarePen, MessageSquare, Globe, Mail, Instagram, Phone, Search, Inbox, ChevronLeft, Clock, Archive, PanelRight, Send, X, User, MapPin, Tag, StickyNote, Link2 } from 'lucide-react';
|
|
6
|
+
import { PageHeaderActions, Button, cn, Input, Skeleton, toast } from '@fayz-ai/ui';
|
|
7
|
+
import { PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
8
8
|
import { createStore } from 'zustand/vanilla';
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
11
|
var DEFAULT_CONVERSATIONS_CONFIG = {
|
|
12
12
|
contactKind: "contact"
|
|
13
13
|
};
|
|
14
|
-
var StoreContext =
|
|
15
|
-
var ConfigContext =
|
|
14
|
+
var StoreContext = React4.createContext(null);
|
|
15
|
+
var ConfigContext = React4.createContext(DEFAULT_CONVERSATIONS_CONFIG);
|
|
16
16
|
function ConversationsContextProvider({
|
|
17
17
|
store,
|
|
18
18
|
config = DEFAULT_CONVERSATIONS_CONFIG,
|
|
@@ -21,12 +21,12 @@ function ConversationsContextProvider({
|
|
|
21
21
|
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
|
|
22
22
|
}
|
|
23
23
|
function useConversationsStore(selector) {
|
|
24
|
-
const store =
|
|
24
|
+
const store = React4.useContext(StoreContext);
|
|
25
25
|
if (!store) throw new Error("useConversationsStore must be used within ConversationsPage");
|
|
26
26
|
return useStore(store, selector);
|
|
27
27
|
}
|
|
28
28
|
function useConversationsConfig() {
|
|
29
|
-
return
|
|
29
|
+
return React4.useContext(ConfigContext);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// src/types.ts
|
|
@@ -54,10 +54,10 @@ var CHANNEL_ACCENT = {
|
|
|
54
54
|
webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
|
|
55
55
|
};
|
|
56
56
|
function useMediaQuery(query) {
|
|
57
|
-
const [matches, setMatches] =
|
|
57
|
+
const [matches, setMatches] = React4.useState(
|
|
58
58
|
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
59
59
|
);
|
|
60
|
-
|
|
60
|
+
React4.useEffect(() => {
|
|
61
61
|
if (typeof window === "undefined") return;
|
|
62
62
|
const mql = window.matchMedia(query);
|
|
63
63
|
const onChange = () => setMatches(mql.matches);
|
|
@@ -130,144 +130,6 @@ function dayLabel(iso) {
|
|
|
130
130
|
if (dayDiff < 7) return d.toLocaleDateString([], { weekday: "long" });
|
|
131
131
|
return d.toLocaleDateString([], { month: "short", day: "numeric", year: now.getFullYear() === d.getFullYear() ? void 0 : "numeric" });
|
|
132
132
|
}
|
|
133
|
-
var CHANNELS = ["whatsapp", "sms", "instagram", "email", "webchat"];
|
|
134
|
-
function personHandleFor(channel, contact) {
|
|
135
|
-
if (!contact) return "";
|
|
136
|
-
if (channel === "email") return contact.email ?? "";
|
|
137
|
-
if (channel === "sms" || channel === "whatsapp") return contact.phone ?? "";
|
|
138
|
-
return "";
|
|
139
|
-
}
|
|
140
|
-
var HANDLE_LABEL_KEY = {
|
|
141
|
-
whatsapp: "conversations.new.handleLabel.phone",
|
|
142
|
-
sms: "conversations.new.handleLabel.phone",
|
|
143
|
-
email: "conversations.new.handleLabel.email",
|
|
144
|
-
instagram: "conversations.new.handleLabel.instagram",
|
|
145
|
-
webchat: "conversations.new.handleLabel.webchat"
|
|
146
|
-
};
|
|
147
|
-
function NewConversationModal({
|
|
148
|
-
open,
|
|
149
|
-
onOpenChange
|
|
150
|
-
}) {
|
|
151
|
-
const t = useTranslation();
|
|
152
|
-
const create = useConversationsStore((s) => s.create);
|
|
153
|
-
const config = useConversationsConfig();
|
|
154
|
-
const guardConversations = useLimitGuard("conversations_month");
|
|
155
|
-
const [channel, setChannel] = React3.useState("whatsapp");
|
|
156
|
-
const [contact, setContact] = React3.useState(null);
|
|
157
|
-
const [typedHandle, setTypedHandle] = React3.useState("");
|
|
158
|
-
const [creatingContact, setCreatingContact] = React3.useState(false);
|
|
159
|
-
const [firstMessage, setFirstMessage] = React3.useState("");
|
|
160
|
-
const [submitting, setSubmitting] = React3.useState(false);
|
|
161
|
-
const [pickerKey, setPickerKey] = React3.useState(0);
|
|
162
|
-
React3.useEffect(() => {
|
|
163
|
-
if (open) {
|
|
164
|
-
setChannel("whatsapp");
|
|
165
|
-
setContact(null);
|
|
166
|
-
setTypedHandle("");
|
|
167
|
-
setCreatingContact(false);
|
|
168
|
-
setFirstMessage("");
|
|
169
|
-
setSubmitting(false);
|
|
170
|
-
setPickerKey((k) => k + 1);
|
|
171
|
-
}
|
|
172
|
-
}, [open]);
|
|
173
|
-
const derivedHandle = personHandleFor(channel, contact);
|
|
174
|
-
const effectiveHandle = derivedHandle || typedHandle;
|
|
175
|
-
const handleLabel = t(HANDLE_LABEL_KEY[channel]);
|
|
176
|
-
const canSubmit = (contact?.name.trim().length ?? 0) > 0 && !submitting;
|
|
177
|
-
async function handleSubmit(e) {
|
|
178
|
-
e.preventDefault();
|
|
179
|
-
if (!canSubmit) return;
|
|
180
|
-
setSubmitting(true);
|
|
181
|
-
try {
|
|
182
|
-
if (await guardConversations() === "blocked") return;
|
|
183
|
-
await create({
|
|
184
|
-
channel,
|
|
185
|
-
contactName: contact.name.trim(),
|
|
186
|
-
contactPersonId: contact?.id,
|
|
187
|
-
contactHandle: effectiveHandle.trim() || void 0,
|
|
188
|
-
firstMessage: firstMessage.trim() || void 0
|
|
189
|
-
});
|
|
190
|
-
invalidateLimit("conversations_month");
|
|
191
|
-
onOpenChange(false);
|
|
192
|
-
} catch (err) {
|
|
193
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
-
toast.error(t("conversations.new.createFailed"), { description: message });
|
|
195
|
-
} finally {
|
|
196
|
-
setSubmitting(false);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange, children: /* @__PURE__ */ jsx(ModalContent, { size: "md", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [
|
|
200
|
-
/* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-foreground", children: t("conversations.new.title") }),
|
|
201
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
202
|
-
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.channel") }),
|
|
203
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: CHANNELS.map((ch) => {
|
|
204
|
-
const Icon = CHANNEL_ICON[ch];
|
|
205
|
-
const active = channel === ch;
|
|
206
|
-
return /* @__PURE__ */ jsxs(
|
|
207
|
-
"button",
|
|
208
|
-
{
|
|
209
|
-
type: "button",
|
|
210
|
-
onClick: () => setChannel(ch),
|
|
211
|
-
"aria-pressed": active,
|
|
212
|
-
className: cn(
|
|
213
|
-
"inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
|
|
214
|
-
active ? "text-white" : "bg-muted text-muted-foreground hover:bg-muted/70"
|
|
215
|
-
),
|
|
216
|
-
style: active ? { backgroundColor: CHANNEL_ACCENT[ch].color } : void 0,
|
|
217
|
-
children: [
|
|
218
|
-
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
219
|
-
CHANNEL_LABELS[ch]
|
|
220
|
-
]
|
|
221
|
-
},
|
|
222
|
-
ch
|
|
223
|
-
);
|
|
224
|
-
}) })
|
|
225
|
-
] }),
|
|
226
|
-
/* @__PURE__ */ jsx(
|
|
227
|
-
ContactPicker,
|
|
228
|
-
{
|
|
229
|
-
value: contact,
|
|
230
|
-
onChange: setContact,
|
|
231
|
-
kind: config.contactKind,
|
|
232
|
-
extensionTable: config.contactExtensionTable,
|
|
233
|
-
lookup: config.contactLookup,
|
|
234
|
-
allowFreeText: true,
|
|
235
|
-
onCreatingChange: setCreatingContact,
|
|
236
|
-
autoFocus: true,
|
|
237
|
-
label: t("conversations.new.contactName"),
|
|
238
|
-
placeholder: t("conversations.new.contactNamePlaceholder"),
|
|
239
|
-
secondaryText: derivedHandle || void 0,
|
|
240
|
-
handleField: {
|
|
241
|
-
label: handleLabel,
|
|
242
|
-
derived: derivedHandle || void 0,
|
|
243
|
-
value: typedHandle,
|
|
244
|
-
onChange: setTypedHandle,
|
|
245
|
-
fieldLabel: t("conversations.new.handle"),
|
|
246
|
-
placeholder: t("conversations.new.handlePlaceholder")
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
pickerKey
|
|
250
|
-
),
|
|
251
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
252
|
-
/* @__PURE__ */ jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
|
|
253
|
-
/* @__PURE__ */ jsx(
|
|
254
|
-
"textarea",
|
|
255
|
-
{
|
|
256
|
-
id: "conv-first-message",
|
|
257
|
-
value: firstMessage,
|
|
258
|
-
onChange: (e) => setFirstMessage(e.target.value),
|
|
259
|
-
rows: 3,
|
|
260
|
-
placeholder: t("conversations.new.firstMessagePlaceholder"),
|
|
261
|
-
className: "max-h-40 min-h-[64px] w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary"
|
|
262
|
-
}
|
|
263
|
-
)
|
|
264
|
-
] }),
|
|
265
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-end gap-2", children: [
|
|
266
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t("conversations.new.cancel") }),
|
|
267
|
-
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: !canSubmit, children: submitting ? t("conversations.new.creating") : t("conversations.new.create") })
|
|
268
|
-
] })
|
|
269
|
-
] }) }) });
|
|
270
|
-
}
|
|
271
133
|
var FILTERS = ["all", "whatsapp", "sms", "instagram", "email", "webchat"];
|
|
272
134
|
function ConversationList({ className }) {
|
|
273
135
|
const t = useTranslation();
|
|
@@ -281,25 +143,8 @@ function ConversationList({ className }) {
|
|
|
281
143
|
setChannelFilter,
|
|
282
144
|
setSearch
|
|
283
145
|
} = useConversationsStore((s) => s);
|
|
284
|
-
const [newOpen, setNewOpen] = React3.useState(false);
|
|
285
146
|
return /* @__PURE__ */ jsxs("aside", { className: cn("w-full shrink-0 flex-col border-r border-border bg-card lg:w-[320px]", className), children: [
|
|
286
147
|
/* @__PURE__ */ jsxs("div", { className: "border-b border-border px-3 py-3", children: [
|
|
287
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
|
|
288
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: t("conversations.title") }),
|
|
289
|
-
/* @__PURE__ */ jsx(PermissionGate, { feature: "conversations", action: "create", children: /* @__PURE__ */ jsxs(
|
|
290
|
-
Button,
|
|
291
|
-
{
|
|
292
|
-
size: "sm",
|
|
293
|
-
onClick: () => setNewOpen(true),
|
|
294
|
-
"aria-label": t("conversations.list.new"),
|
|
295
|
-
"data-testid": "conversations-new",
|
|
296
|
-
children: [
|
|
297
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 sm:mr-1" }),
|
|
298
|
-
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t("conversations.list.new") })
|
|
299
|
-
]
|
|
300
|
-
}
|
|
301
|
-
) })
|
|
302
|
-
] }),
|
|
303
148
|
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
304
149
|
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }),
|
|
305
150
|
/* @__PURE__ */ jsx(
|
|
@@ -373,8 +218,7 @@ function ConversationList({ className }) {
|
|
|
373
218
|
c.id
|
|
374
219
|
);
|
|
375
220
|
})
|
|
376
|
-
] })
|
|
377
|
-
/* @__PURE__ */ jsx(NewConversationModal, { open: newOpen, onOpenChange: setNewOpen })
|
|
221
|
+
] })
|
|
378
222
|
] });
|
|
379
223
|
}
|
|
380
224
|
function buildRows(messages) {
|
|
@@ -397,9 +241,9 @@ function buildRows(messages) {
|
|
|
397
241
|
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
|
|
398
242
|
const t = useTranslation();
|
|
399
243
|
const { messages, sending, send, setStatus } = useConversationsStore((s) => s);
|
|
400
|
-
const [draft, setDraft] =
|
|
401
|
-
const threadRef =
|
|
402
|
-
|
|
244
|
+
const [draft, setDraft] = React4.useState("");
|
|
245
|
+
const threadRef = React4.useRef(null);
|
|
246
|
+
React4.useEffect(() => {
|
|
403
247
|
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
404
248
|
}, [messages.length, selected.id]);
|
|
405
249
|
async function handleSend() {
|
|
@@ -409,7 +253,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
409
253
|
await send(body);
|
|
410
254
|
}
|
|
411
255
|
const accent = CHANNEL_ACCENT[selected.channel];
|
|
412
|
-
const rows =
|
|
256
|
+
const rows = React4.useMemo(() => buildRows(messages), [messages]);
|
|
413
257
|
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
414
258
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-2.5 md:px-5", children: [
|
|
415
259
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
@@ -556,52 +400,212 @@ function ContactPanel({ contact, onClose, className }) {
|
|
|
556
400
|
/* @__PURE__ */ jsx(Section, { icon: Link2, title: t("conversations.contact.linkedRecords"), children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("conversations.contact.noLinkedRecords") }) })
|
|
557
401
|
] });
|
|
558
402
|
}
|
|
403
|
+
var CHANNELS = ["whatsapp", "sms", "instagram", "email", "webchat"];
|
|
404
|
+
function personHandleFor(channel, contact) {
|
|
405
|
+
if (!contact) return "";
|
|
406
|
+
if (channel === "email") return contact.email ?? "";
|
|
407
|
+
if (channel === "sms" || channel === "whatsapp") return contact.phone ?? "";
|
|
408
|
+
return "";
|
|
409
|
+
}
|
|
410
|
+
var HANDLE_LABEL_KEY = {
|
|
411
|
+
whatsapp: "conversations.new.handleLabel.phone",
|
|
412
|
+
sms: "conversations.new.handleLabel.phone",
|
|
413
|
+
email: "conversations.new.handleLabel.email",
|
|
414
|
+
instagram: "conversations.new.handleLabel.instagram",
|
|
415
|
+
webchat: "conversations.new.handleLabel.webchat"
|
|
416
|
+
};
|
|
417
|
+
function NewConversationPanel({
|
|
418
|
+
open,
|
|
419
|
+
onOpenChange
|
|
420
|
+
}) {
|
|
421
|
+
const t = useTranslation();
|
|
422
|
+
const create = useConversationsStore((s) => s.create);
|
|
423
|
+
const config = useConversationsConfig();
|
|
424
|
+
const guardConversations = useLimitGuard("conversations_month");
|
|
425
|
+
const [channel, setChannel] = React4.useState("whatsapp");
|
|
426
|
+
const [contact, setContact] = React4.useState(null);
|
|
427
|
+
const [typedHandle, setTypedHandle] = React4.useState("");
|
|
428
|
+
const [creatingContact, setCreatingContact] = React4.useState(false);
|
|
429
|
+
const [firstMessage, setFirstMessage] = React4.useState("");
|
|
430
|
+
const [submitting, setSubmitting] = React4.useState(false);
|
|
431
|
+
const [pickerKey, setPickerKey] = React4.useState(0);
|
|
432
|
+
React4.useEffect(() => {
|
|
433
|
+
if (open) {
|
|
434
|
+
setChannel("whatsapp");
|
|
435
|
+
setContact(null);
|
|
436
|
+
setTypedHandle("");
|
|
437
|
+
setCreatingContact(false);
|
|
438
|
+
setFirstMessage("");
|
|
439
|
+
setSubmitting(false);
|
|
440
|
+
setPickerKey((k) => k + 1);
|
|
441
|
+
}
|
|
442
|
+
}, [open]);
|
|
443
|
+
const derivedHandle = personHandleFor(channel, contact);
|
|
444
|
+
const effectiveHandle = derivedHandle || typedHandle;
|
|
445
|
+
const handleLabel = t(HANDLE_LABEL_KEY[channel]);
|
|
446
|
+
const canSubmit = (contact?.name.trim().length ?? 0) > 0 && !submitting;
|
|
447
|
+
async function handleSubmit(e) {
|
|
448
|
+
e.preventDefault();
|
|
449
|
+
if (!canSubmit) return;
|
|
450
|
+
setSubmitting(true);
|
|
451
|
+
try {
|
|
452
|
+
if (await guardConversations() === "blocked") return;
|
|
453
|
+
await create({
|
|
454
|
+
channel,
|
|
455
|
+
contactName: contact.name.trim(),
|
|
456
|
+
contactPersonId: contact?.id,
|
|
457
|
+
contactHandle: effectiveHandle.trim() || void 0,
|
|
458
|
+
firstMessage: firstMessage.trim() || void 0
|
|
459
|
+
});
|
|
460
|
+
invalidateLimit("conversations_month");
|
|
461
|
+
onOpenChange(false);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
464
|
+
toast.error(t("conversations.new.createFailed"), { description: message });
|
|
465
|
+
} finally {
|
|
466
|
+
setSubmitting(false);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return /* @__PURE__ */ jsx(
|
|
470
|
+
RightRailPage,
|
|
471
|
+
{
|
|
472
|
+
title: t("conversations.new.title"),
|
|
473
|
+
open,
|
|
474
|
+
onClose: () => onOpenChange(false),
|
|
475
|
+
width: "max-w-md",
|
|
476
|
+
children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4", children: [
|
|
477
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
478
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.channel") }),
|
|
479
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: CHANNELS.map((ch) => {
|
|
480
|
+
const Icon = CHANNEL_ICON[ch];
|
|
481
|
+
const active = channel === ch;
|
|
482
|
+
return /* @__PURE__ */ jsxs(
|
|
483
|
+
"button",
|
|
484
|
+
{
|
|
485
|
+
type: "button",
|
|
486
|
+
onClick: () => setChannel(ch),
|
|
487
|
+
"aria-pressed": active,
|
|
488
|
+
className: cn(
|
|
489
|
+
"inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
|
|
490
|
+
active ? "text-white" : "bg-muted text-muted-foreground hover:bg-muted/70"
|
|
491
|
+
),
|
|
492
|
+
style: active ? { backgroundColor: CHANNEL_ACCENT[ch].color } : void 0,
|
|
493
|
+
children: [
|
|
494
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
495
|
+
CHANNEL_LABELS[ch]
|
|
496
|
+
]
|
|
497
|
+
},
|
|
498
|
+
ch
|
|
499
|
+
);
|
|
500
|
+
}) })
|
|
501
|
+
] }),
|
|
502
|
+
/* @__PURE__ */ jsx(
|
|
503
|
+
ContactPicker,
|
|
504
|
+
{
|
|
505
|
+
value: contact,
|
|
506
|
+
onChange: setContact,
|
|
507
|
+
kind: config.contactKind,
|
|
508
|
+
lookup: config.contactLookup,
|
|
509
|
+
createEntityDef: config.contactEntityDef,
|
|
510
|
+
allowFreeText: true,
|
|
511
|
+
onCreatingChange: setCreatingContact,
|
|
512
|
+
autoFocus: true,
|
|
513
|
+
label: t("conversations.new.contactName"),
|
|
514
|
+
placeholder: t("conversations.new.contactNamePlaceholder"),
|
|
515
|
+
secondaryText: derivedHandle || void 0,
|
|
516
|
+
handleField: {
|
|
517
|
+
label: handleLabel,
|
|
518
|
+
derived: derivedHandle || void 0,
|
|
519
|
+
value: typedHandle,
|
|
520
|
+
onChange: setTypedHandle,
|
|
521
|
+
fieldLabel: t("conversations.new.handle"),
|
|
522
|
+
placeholder: t("conversations.new.handlePlaceholder")
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
pickerKey
|
|
526
|
+
),
|
|
527
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
528
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
|
|
529
|
+
/* @__PURE__ */ jsx(
|
|
530
|
+
"textarea",
|
|
531
|
+
{
|
|
532
|
+
id: "conv-first-message",
|
|
533
|
+
value: firstMessage,
|
|
534
|
+
onChange: (e) => setFirstMessage(e.target.value),
|
|
535
|
+
rows: 3,
|
|
536
|
+
placeholder: t("conversations.new.firstMessagePlaceholder"),
|
|
537
|
+
className: "max-h-40 min-h-[64px] w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary"
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
] }),
|
|
541
|
+
/* @__PURE__ */ jsx("div", { className: "mt-auto pt-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !canSubmit, className: "w-full", children: submitting ? t("conversations.new.creating") : t("conversations.new.create") }) })
|
|
542
|
+
] })
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
}
|
|
559
546
|
function InboxView() {
|
|
560
547
|
const t = useTranslation();
|
|
561
548
|
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
562
549
|
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
563
|
-
const [panelOpen, setPanelOpen] =
|
|
550
|
+
const [panelOpen, setPanelOpen] = React4.useState(false);
|
|
551
|
+
const [newOpen, setNewOpen] = React4.useState(false);
|
|
564
552
|
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
565
|
-
|
|
553
|
+
React4.useEffect(() => {
|
|
566
554
|
setPanelOpen(isWidePanel);
|
|
567
555
|
}, [isWidePanel]);
|
|
568
|
-
return /* @__PURE__ */ jsxs(
|
|
569
|
-
/* @__PURE__ */ jsx(
|
|
570
|
-
|
|
571
|
-
MessageThread,
|
|
556
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
557
|
+
/* @__PURE__ */ jsx(PermissionGate, { feature: "conversations", action: "create", children: /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsxs(
|
|
558
|
+
Button,
|
|
572
559
|
{
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
560
|
+
size: "sm",
|
|
561
|
+
onClick: () => setNewOpen(true),
|
|
562
|
+
"aria-label": t("conversations.list.new"),
|
|
563
|
+
"data-testid": "conversations-new",
|
|
564
|
+
children: [
|
|
565
|
+
/* @__PURE__ */ jsx(SquarePen, { className: "h-3.5 w-3.5 sm:mr-1.5" }),
|
|
566
|
+
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t("conversations.list.new") })
|
|
567
|
+
]
|
|
578
568
|
}
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
"button",
|
|
586
|
-
{
|
|
587
|
-
className: "absolute inset-0 z-10 bg-black/40 xl:hidden",
|
|
588
|
-
"aria-label": "Close details",
|
|
589
|
-
onClick: () => setPanelOpen(false)
|
|
590
|
-
}
|
|
591
|
-
),
|
|
592
|
-
/* @__PURE__ */ jsx(
|
|
593
|
-
ContactPanel,
|
|
569
|
+
) }) }),
|
|
570
|
+
/* @__PURE__ */ jsx(NewConversationPanel, { open: newOpen, onOpenChange: setNewOpen }),
|
|
571
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex h-full min-h-0 overflow-hidden bg-background", children: [
|
|
572
|
+
/* @__PURE__ */ jsx(ConversationList, { className: cn(selected ? "hidden lg:flex" : "flex") }),
|
|
573
|
+
selected ? /* @__PURE__ */ jsx(
|
|
574
|
+
MessageThread,
|
|
594
575
|
{
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
576
|
+
selected,
|
|
577
|
+
panelOpen,
|
|
578
|
+
onTogglePanel: () => setPanelOpen((v) => !v),
|
|
579
|
+
onBack: deselect,
|
|
580
|
+
className: "flex"
|
|
598
581
|
}
|
|
599
|
-
)
|
|
582
|
+
) : /* @__PURE__ */ jsxs("section", { className: "hidden min-w-0 flex-1 flex-col items-center justify-center bg-muted/20 text-muted-foreground lg:flex", children: [
|
|
583
|
+
/* @__PURE__ */ jsx(MessageSquare, { className: "h-9 w-9" }),
|
|
584
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
|
|
585
|
+
] }),
|
|
586
|
+
selected && panelOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
587
|
+
/* @__PURE__ */ jsx(
|
|
588
|
+
"button",
|
|
589
|
+
{
|
|
590
|
+
className: "absolute inset-0 z-10 bg-black/40 xl:hidden",
|
|
591
|
+
"aria-label": "Close details",
|
|
592
|
+
onClick: () => setPanelOpen(false)
|
|
593
|
+
}
|
|
594
|
+
),
|
|
595
|
+
/* @__PURE__ */ jsx(
|
|
596
|
+
ContactPanel,
|
|
597
|
+
{
|
|
598
|
+
contact: selected,
|
|
599
|
+
onClose: () => setPanelOpen(false),
|
|
600
|
+
className: "absolute inset-y-0 right-0 z-20 w-[340px] max-w-[88%] shadow-2xl xl:static xl:z-auto xl:w-[300px] xl:max-w-none xl:shadow-none"
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
] })
|
|
600
604
|
] })
|
|
601
605
|
] });
|
|
602
606
|
}
|
|
603
607
|
function ConversationsPage({ store, config }) {
|
|
604
|
-
|
|
608
|
+
React4.useEffect(() => {
|
|
605
609
|
void store.getState().load();
|
|
606
610
|
}, []);
|
|
607
611
|
return /* @__PURE__ */ jsx(ConversationsContextProvider, { store, config, children: /* @__PURE__ */ jsx(InboxView, {}) });
|
|
@@ -1384,13 +1388,14 @@ function createConversationsPlugin(options) {
|
|
|
1384
1388
|
const store = createConversationsStore(provider);
|
|
1385
1389
|
const config = {
|
|
1386
1390
|
contactKind: options?.contactKind ?? "contact",
|
|
1387
|
-
|
|
1388
|
-
|
|
1391
|
+
contactLookup: options?.contactLookup,
|
|
1392
|
+
contactEntityDef: options?.contactEntityDef
|
|
1389
1393
|
};
|
|
1390
|
-
const PageComponent = () =>
|
|
1394
|
+
const PageComponent = () => React4.createElement(ConversationsPage, { store, config });
|
|
1391
1395
|
PageComponent.displayName = "ConversationsPage";
|
|
1392
1396
|
return {
|
|
1393
1397
|
id: "conversations",
|
|
1398
|
+
defaultAgentRole: "frontdesk",
|
|
1394
1399
|
name: options?.navLabel ?? "Conversations",
|
|
1395
1400
|
icon: "MessageCircle",
|
|
1396
1401
|
version: "1.0.0",
|