@ai-matrx/chat 0.7.26 → 0.7.27
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/CHANGELOG.md +29 -4
- package/dist/action-requests/components/ActionRequestAnswerForm.d.ts +7 -0
- package/dist/action-requests/components/ActionRequestAnswerForm.js +3 -1
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +2 -1
- package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +8 -0
- package/dist/host/admin-seat.js +2 -0
- package/dist/messenger/MessengerAvatar.d.ts +18 -0
- package/dist/messenger/MessengerAvatar.js +31 -0
- package/dist/messenger/MessengerContactList.d.ts +16 -0
- package/dist/messenger/MessengerContactList.js +210 -0
- package/dist/messenger/MessengerDetails.d.ts +20 -0
- package/dist/messenger/MessengerDetails.js +35 -0
- package/dist/messenger/MessengerMenu.d.ts +21 -0
- package/dist/messenger/MessengerMenu.js +58 -0
- package/dist/messenger/MessengerRail.d.ts +11 -0
- package/dist/messenger/MessengerRail.js +184 -0
- package/dist/messenger/MessengerShell.d.ts +6 -0
- package/dist/messenger/MessengerShell.js +115 -0
- package/dist/messenger/MessengerThreadHeader.d.ts +8 -0
- package/dist/messenger/MessengerThreadHeader.js +62 -0
- package/dist/messenger/contract.d.ts +149 -0
- package/dist/messenger/contract.js +28 -0
- package/dist/messenger/people/format-time.d.ts +6 -0
- package/dist/messenger/people/format-time.js +14 -0
- package/dist/messenger/people/usePeopleMessengerSource.d.ts +35 -0
- package/dist/messenger/people/usePeopleMessengerSource.js +213 -0
- package/package.json +31 -5
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { ChevronDown, ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
5
|
+
import { cn } from "@ai-matrx/design-system/cn";
|
|
6
|
+
import {
|
|
7
|
+
isSpaceGroup
|
|
8
|
+
} from "./contract.js";
|
|
9
|
+
import { MessengerAvatar } from "./MessengerAvatar.js";
|
|
10
|
+
import { MessengerContextMenu } from "./MessengerMenu.js";
|
|
11
|
+
const RAIL_COLLAPSED_KEY = "ai-matrx.messenger.rail-collapsed";
|
|
12
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
function readCollapsed() {
|
|
14
|
+
try {
|
|
15
|
+
return globalThis.localStorage?.getItem(RAIL_COLLAPSED_KEY) === "1";
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function subscribe(listener) {
|
|
21
|
+
listeners.add(listener);
|
|
22
|
+
const onStorage = (event) => {
|
|
23
|
+
if (event.key === RAIL_COLLAPSED_KEY) listener();
|
|
24
|
+
};
|
|
25
|
+
globalThis.addEventListener?.("storage", onStorage);
|
|
26
|
+
return () => {
|
|
27
|
+
listeners.delete(listener);
|
|
28
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function useRailCollapsed() {
|
|
32
|
+
const collapsed = useSyncExternalStore(subscribe, readCollapsed, () => false);
|
|
33
|
+
const set = (next) => {
|
|
34
|
+
try {
|
|
35
|
+
globalThis.localStorage?.setItem(RAIL_COLLAPSED_KEY, next ? "1" : "0");
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
for (const listener of listeners) listener();
|
|
39
|
+
};
|
|
40
|
+
return [collapsed, set];
|
|
41
|
+
}
|
|
42
|
+
function formatCount(count) {
|
|
43
|
+
return count > 999 ? "999+" : String(count);
|
|
44
|
+
}
|
|
45
|
+
function SpaceItem({
|
|
46
|
+
space,
|
|
47
|
+
active,
|
|
48
|
+
collapsed,
|
|
49
|
+
source
|
|
50
|
+
}) {
|
|
51
|
+
const Icon = space.icon;
|
|
52
|
+
const count = space.count ?? 0;
|
|
53
|
+
const menu = source.spaceMenu;
|
|
54
|
+
return /* @__PURE__ */ jsx(MessengerContextMenu, { entries: menu ? () => menu(space) : void 0, children: /* @__PURE__ */ jsxs(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
type: "button",
|
|
58
|
+
onClick: () => source.onSelectSpace(space.id),
|
|
59
|
+
"aria-current": active ? "page" : void 0,
|
|
60
|
+
"aria-label": collapsed ? space.label : void 0,
|
|
61
|
+
title: collapsed ? space.label : void 0,
|
|
62
|
+
"data-space-id": space.id,
|
|
63
|
+
className: cn(
|
|
64
|
+
"relative flex w-full min-w-0 items-center rounded-lg text-[14px] text-foreground transition-colors",
|
|
65
|
+
collapsed ? "h-10 justify-center" : "h-8 gap-2.5 px-2",
|
|
66
|
+
active ? "bg-accent font-medium" : "hover:bg-accent/60"
|
|
67
|
+
),
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-5 w-5 shrink-0", strokeWidth: 1.75 }),
|
|
70
|
+
collapsed ? count > 0 ? /* @__PURE__ */ jsx("span", { className: "absolute right-0.5 top-0.5 min-w-4 rounded-full bg-primary px-1 text-center text-[10px] font-semibold leading-4 text-primary-foreground", children: formatCount(count) }) : null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
71
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: space.label }),
|
|
72
|
+
count > 0 ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[13px] tabular-nums text-muted-foreground", children: formatCount(count) }) : null
|
|
73
|
+
] })
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
) });
|
|
77
|
+
}
|
|
78
|
+
function Group({
|
|
79
|
+
entry,
|
|
80
|
+
collapsed,
|
|
81
|
+
source
|
|
82
|
+
}) {
|
|
83
|
+
const [open, setOpen] = useState(entry.defaultOpen ?? true);
|
|
84
|
+
if (collapsed) {
|
|
85
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", "data-space-group": entry.id, children: [
|
|
86
|
+
/* @__PURE__ */ jsx("div", { className: "mx-auto my-1.5 h-px w-6 bg-border" }),
|
|
87
|
+
entry.spaces.map((space) => /* @__PURE__ */ jsx(SpaceItem, { space, active: source.activeSpaceId === space.id, collapsed: true, source }, space.id))
|
|
88
|
+
] });
|
|
89
|
+
}
|
|
90
|
+
const Chevron = open ? ChevronDown : ChevronRight;
|
|
91
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-col gap-0.5", "data-space-group": entry.id, children: [
|
|
92
|
+
/* @__PURE__ */ jsxs(
|
|
93
|
+
"button",
|
|
94
|
+
{
|
|
95
|
+
type: "button",
|
|
96
|
+
onClick: () => setOpen(!open),
|
|
97
|
+
"aria-expanded": open,
|
|
98
|
+
className: "flex h-7 w-full items-center justify-between rounded-md px-2 text-[12px] font-medium text-muted-foreground hover:text-foreground",
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: entry.label }),
|
|
101
|
+
/* @__PURE__ */ jsx(Chevron, { className: "h-3.5 w-3.5 shrink-0" })
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
open ? entry.spaces.map((space) => /* @__PURE__ */ jsx(SpaceItem, { space, active: source.activeSpaceId === space.id, collapsed: false, source }, space.id)) : null
|
|
106
|
+
] });
|
|
107
|
+
}
|
|
108
|
+
function Profile({ profile, collapsed }) {
|
|
109
|
+
return /* @__PURE__ */ jsxs(
|
|
110
|
+
"button",
|
|
111
|
+
{
|
|
112
|
+
type: "button",
|
|
113
|
+
onClick: profile.onSelect,
|
|
114
|
+
disabled: profile.onSelect === void 0,
|
|
115
|
+
title: collapsed ? profile.name : void 0,
|
|
116
|
+
className: cn(
|
|
117
|
+
"flex min-w-0 items-center rounded-lg text-[14px] font-medium text-foreground enabled:hover:bg-accent/60",
|
|
118
|
+
collapsed ? "h-10 w-full justify-center" : "h-10 w-full gap-2.5 px-1.5"
|
|
119
|
+
),
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsx(MessengerAvatar, { name: profile.name, avatar: profile.avatar, size: "rail" }),
|
|
122
|
+
collapsed ? null : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: profile.name })
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
function MessengerRail({
|
|
128
|
+
source,
|
|
129
|
+
className,
|
|
130
|
+
forceCollapsed = false
|
|
131
|
+
}) {
|
|
132
|
+
const [remembered, setCollapsed] = useRailCollapsed();
|
|
133
|
+
const collapsed = forceCollapsed || remembered;
|
|
134
|
+
const Toggle = collapsed ? PanelLeftOpen : PanelLeftClose;
|
|
135
|
+
return /* @__PURE__ */ jsxs(
|
|
136
|
+
"nav",
|
|
137
|
+
{
|
|
138
|
+
"aria-label": "Spaces",
|
|
139
|
+
"data-collapsed": collapsed ? "true" : "false",
|
|
140
|
+
className: cn(
|
|
141
|
+
"flex h-full shrink-0 flex-col border-r border-border bg-muted/40 py-2 transition-[width] duration-150",
|
|
142
|
+
collapsed ? "w-16 px-2" : "w-36 px-2",
|
|
143
|
+
className
|
|
144
|
+
),
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto overflow-x-hidden", children: source.rail.map(
|
|
147
|
+
(entry) => isSpaceGroup(entry) ? /* @__PURE__ */ jsx(Group, { entry, collapsed, source }, entry.id) : /* @__PURE__ */ jsx(
|
|
148
|
+
SpaceItem,
|
|
149
|
+
{
|
|
150
|
+
space: entry,
|
|
151
|
+
active: source.activeSpaceId === entry.id,
|
|
152
|
+
collapsed,
|
|
153
|
+
source
|
|
154
|
+
},
|
|
155
|
+
entry.id
|
|
156
|
+
)
|
|
157
|
+
) }),
|
|
158
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex shrink-0 flex-col gap-1", children: [
|
|
159
|
+
source.profile ? /* @__PURE__ */ jsx(Profile, { profile: source.profile, collapsed }) : null,
|
|
160
|
+
forceCollapsed ? null : /* @__PURE__ */ jsx(
|
|
161
|
+
"button",
|
|
162
|
+
{
|
|
163
|
+
type: "button",
|
|
164
|
+
onClick: () => setCollapsed(!collapsed),
|
|
165
|
+
"aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
166
|
+
title: collapsed ? "Expand" : "Collapse",
|
|
167
|
+
"aria-pressed": collapsed,
|
|
168
|
+
className: cn(
|
|
169
|
+
"flex h-8 items-center rounded-lg text-muted-foreground hover:bg-accent/60 hover:text-foreground",
|
|
170
|
+
collapsed ? "w-full justify-center" : "w-8 justify-center"
|
|
171
|
+
),
|
|
172
|
+
children: /* @__PURE__ */ jsx(Toggle, { className: "h-4 w-4", strokeWidth: 1.75 })
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
] })
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
MessengerRail,
|
|
182
|
+
RAIL_COLLAPSED_KEY,
|
|
183
|
+
useRailCollapsed
|
|
184
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type MessengerSource } from "./contract.js";
|
|
2
|
+
export interface MessengerShellProps {
|
|
3
|
+
readonly source: MessengerSource;
|
|
4
|
+
readonly className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function MessengerShell({ source, className }: MessengerShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { MessageCircle, X } from "lucide-react";
|
|
5
|
+
import { cn } from "@ai-matrx/design-system/cn";
|
|
6
|
+
import { visibleContacts } from "./contract.js";
|
|
7
|
+
import { MessengerContactList } from "./MessengerContactList.js";
|
|
8
|
+
import { MessengerRail } from "./MessengerRail.js";
|
|
9
|
+
import { MessengerThreadHeader } from "./MessengerThreadHeader.js";
|
|
10
|
+
const NARROW = "(max-width: 767px)";
|
|
11
|
+
function useNarrow() {
|
|
12
|
+
return useSyncExternalStore(
|
|
13
|
+
(listener) => {
|
|
14
|
+
const query = globalThis.matchMedia?.(NARROW);
|
|
15
|
+
query?.addEventListener?.("change", listener);
|
|
16
|
+
return () => query?.removeEventListener?.("change", listener);
|
|
17
|
+
},
|
|
18
|
+
() => globalThis.matchMedia?.(NARROW)?.matches ?? false,
|
|
19
|
+
() => false
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function MessengerShell({ source, className }) {
|
|
23
|
+
const [query, setQuery] = useState("");
|
|
24
|
+
const [filterId, setFilterId] = useState(null);
|
|
25
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
26
|
+
const [pane, setPane] = useState(source.activeContactId !== null ? "thread" : "list");
|
|
27
|
+
const narrow = useNarrow();
|
|
28
|
+
const filter = source.filters?.find((f) => f.id === filterId) ?? null;
|
|
29
|
+
const contacts = useMemo(() => visibleContacts(source.contacts, query, filter), [source.contacts, query, filter]);
|
|
30
|
+
const active = source.contacts.find((c) => c.id === source.activeContactId) ?? null;
|
|
31
|
+
const showThread = pane === "thread" && active !== null;
|
|
32
|
+
const shellSource = useMemo(
|
|
33
|
+
() => ({
|
|
34
|
+
...source,
|
|
35
|
+
onSelectContact: (id) => {
|
|
36
|
+
source.onSelectContact(id);
|
|
37
|
+
setPane("thread");
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
[source]
|
|
41
|
+
);
|
|
42
|
+
const renderDetails = source.renderDetails;
|
|
43
|
+
const toggleDetails = renderDetails ? () => setDetailsOpen((open) => !open) : null;
|
|
44
|
+
return /* @__PURE__ */ jsxs(
|
|
45
|
+
"div",
|
|
46
|
+
{
|
|
47
|
+
className: cn("relative flex h-full min-h-0 w-full min-w-0 overflow-hidden bg-background text-foreground", className),
|
|
48
|
+
"data-messenger-shell": true,
|
|
49
|
+
"data-pane": showThread ? "thread" : "list",
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ jsx(MessengerRail, { source: shellSource, forceCollapsed: narrow, className: cn(showThread && "max-md:hidden") }),
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
className: cn(
|
|
56
|
+
"flex min-h-0 w-[320px] min-w-0 shrink-0 border-r border-border max-md:w-auto max-md:flex-1",
|
|
57
|
+
showThread && "max-md:hidden"
|
|
58
|
+
),
|
|
59
|
+
children: /* @__PURE__ */ jsx(
|
|
60
|
+
MessengerContactList,
|
|
61
|
+
{
|
|
62
|
+
source: shellSource,
|
|
63
|
+
contacts,
|
|
64
|
+
query,
|
|
65
|
+
onQueryChange: setQuery,
|
|
66
|
+
activeFilter: filter,
|
|
67
|
+
onFilterChange: setFilterId
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
/* @__PURE__ */ jsx("main", { className: cn("flex min-h-0 min-w-0 flex-1 flex-col", !showThread && "max-md:hidden"), "aria-label": "Conversation", children: active ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
73
|
+
/* @__PURE__ */ jsx(
|
|
74
|
+
MessengerThreadHeader,
|
|
75
|
+
{
|
|
76
|
+
source,
|
|
77
|
+
contact: active,
|
|
78
|
+
detailsOpen,
|
|
79
|
+
onToggleDetails: toggleDetails,
|
|
80
|
+
onBack: () => setPane("list")
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
/* @__PURE__ */ jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", "data-messenger-thread": true, children: source.renderThread(active) }, active.id)
|
|
84
|
+
] }) : source.renderEmpty?.() ?? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 bg-muted/30 text-muted-foreground", children: [
|
|
85
|
+
/* @__PURE__ */ jsx(MessageCircle, { className: "h-10 w-10", strokeWidth: 1.25 }),
|
|
86
|
+
/* @__PURE__ */ jsx("span", { className: "text-[14px]", children: "Pick a conversation" })
|
|
87
|
+
] }) }),
|
|
88
|
+
active && renderDetails && detailsOpen ? /* @__PURE__ */ jsxs(
|
|
89
|
+
"aside",
|
|
90
|
+
{
|
|
91
|
+
"aria-label": "Details",
|
|
92
|
+
className: "relative flex min-h-0 w-80 shrink-0 flex-col border-l border-border bg-card max-lg:absolute max-lg:inset-y-0 max-lg:right-0 max-lg:z-20 max-lg:shadow-xl max-md:w-full",
|
|
93
|
+
"data-messenger-details": true,
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ jsx(
|
|
96
|
+
"button",
|
|
97
|
+
{
|
|
98
|
+
type: "button",
|
|
99
|
+
onClick: () => setDetailsOpen(false),
|
|
100
|
+
"aria-label": "Close details",
|
|
101
|
+
className: "absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground",
|
|
102
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
renderDetails(active)
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
) : null
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
MessengerShell
|
|
115
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MessengerContact, MessengerSource } from "./contract.js";
|
|
2
|
+
export declare function MessengerThreadHeader({ source, contact, detailsOpen, onToggleDetails, onBack, }: {
|
|
3
|
+
source: MessengerSource;
|
|
4
|
+
contact: MessengerContact;
|
|
5
|
+
detailsOpen: boolean;
|
|
6
|
+
onToggleDetails: (() => void) | null;
|
|
7
|
+
onBack: () => void;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown, ChevronLeft, MoreVertical, PanelRight } from "lucide-react";
|
|
4
|
+
import { cn } from "@ai-matrx/design-system/cn";
|
|
5
|
+
import { MessengerAvatar } from "./MessengerAvatar.js";
|
|
6
|
+
import { MessengerContextMenu, MessengerDropdownMenu } from "./MessengerMenu.js";
|
|
7
|
+
const ICON_BUTTON = "flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground";
|
|
8
|
+
function MessengerThreadHeader({
|
|
9
|
+
source,
|
|
10
|
+
contact,
|
|
11
|
+
detailsOpen,
|
|
12
|
+
onToggleDetails,
|
|
13
|
+
onBack
|
|
14
|
+
}) {
|
|
15
|
+
const info = source.header(contact);
|
|
16
|
+
const headerMenu = source.headerMenu;
|
|
17
|
+
const titleMenu = source.titleMenu;
|
|
18
|
+
const titleBlock = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
|
|
20
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-[15px] font-semibold text-foreground", "data-header-title": true, children: info.title }),
|
|
21
|
+
titleMenu ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 shrink-0 text-muted-foreground" }) : null
|
|
22
|
+
] }),
|
|
23
|
+
info.subtitle ? /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] text-muted-foreground", children: info.subtitle }) : null
|
|
24
|
+
] });
|
|
25
|
+
const titleClass = "flex min-w-0 flex-col items-start justify-center rounded-md px-1 py-0.5 text-left leading-tight hover:bg-accent/60";
|
|
26
|
+
return /* @__PURE__ */ jsx(MessengerContextMenu, { entries: headerMenu ? () => headerMenu(contact) : void 0, children: /* @__PURE__ */ jsxs("header", { className: "flex h-14 w-full min-w-0 shrink-0 items-center gap-2 border-b border-border bg-card px-3", children: [
|
|
27
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, "aria-label": "Back", className: cn(ICON_BUTTON, "-ml-1 md:hidden"), children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" }) }),
|
|
28
|
+
/* @__PURE__ */ jsx(MessengerAvatar, { name: info.title, avatar: info.avatar ?? contact.avatar, size: "header" }),
|
|
29
|
+
/* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center", children: titleMenu ? /* @__PURE__ */ jsx(
|
|
30
|
+
MessengerDropdownMenu,
|
|
31
|
+
{
|
|
32
|
+
align: "start",
|
|
33
|
+
entries: () => titleMenu(contact),
|
|
34
|
+
trigger: /* @__PURE__ */ jsx("button", { type: "button", className: titleClass, "aria-label": `${info.title}, switch`, children: titleBlock })
|
|
35
|
+
}
|
|
36
|
+
) : onToggleDetails ? /* @__PURE__ */ jsx("button", { type: "button", className: titleClass, onClick: onToggleDetails, children: titleBlock }) : /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-col justify-center px-1 leading-tight", children: titleBlock }) }),
|
|
37
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
|
|
38
|
+
source.headerActions?.(contact),
|
|
39
|
+
onToggleDetails ? /* @__PURE__ */ jsx(
|
|
40
|
+
"button",
|
|
41
|
+
{
|
|
42
|
+
type: "button",
|
|
43
|
+
onClick: onToggleDetails,
|
|
44
|
+
"aria-label": detailsOpen ? "Hide details" : "Show details",
|
|
45
|
+
"aria-pressed": detailsOpen,
|
|
46
|
+
className: cn(ICON_BUTTON, detailsOpen && "bg-accent text-foreground"),
|
|
47
|
+
children: /* @__PURE__ */ jsx(PanelRight, { className: "h-[18px] w-[18px]", strokeWidth: 1.75 })
|
|
48
|
+
}
|
|
49
|
+
) : null,
|
|
50
|
+
headerMenu ? /* @__PURE__ */ jsx(
|
|
51
|
+
MessengerDropdownMenu,
|
|
52
|
+
{
|
|
53
|
+
entries: () => headerMenu(contact),
|
|
54
|
+
trigger: /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Conversation menu", className: ICON_BUTTON, children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-[18px] w-[18px]" }) })
|
|
55
|
+
}
|
|
56
|
+
) : null
|
|
57
|
+
] })
|
|
58
|
+
] }) });
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
MessengerThreadHeader
|
|
62
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MESSENGER SOURCE CONTRACT — what a data source hands `<MessengerShell>`.
|
|
3
|
+
*
|
|
4
|
+
* The shell is chrome only: a rail of spaces, a contact list, a thread header,
|
|
5
|
+
* an optional details panel and the right-click menus. It never renders a
|
|
6
|
+
* message, a bubble or a composer. Everything a person reads or writes comes
|
|
7
|
+
* from the SOURCE (`renderThread`), so the people source mounts
|
|
8
|
+
* `@ai-matrx/messaging`'s `ConversationView` and the agents source mounts this
|
|
9
|
+
* package's own chat room — one renderer each, never a third.
|
|
10
|
+
*
|
|
11
|
+
* Search and filter chips are applied by the shell over `contacts`
|
|
12
|
+
* (`matchesQuery` / `MessengerFilter.test`), so a source only lists its rows.
|
|
13
|
+
* Every menu is the source's: each use case supplies options about the item
|
|
14
|
+
* that was clicked, and the shell renders them with the design system's
|
|
15
|
+
* context-menu and dropdown-menu primitives.
|
|
16
|
+
*/
|
|
17
|
+
import type { ComponentType, ReactNode } from "react";
|
|
18
|
+
/** Any icon component (lucide-react and @ai-matrx/icons both fit). */
|
|
19
|
+
export type MessengerIcon = ComponentType<{
|
|
20
|
+
className?: string;
|
|
21
|
+
strokeWidth?: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** What a round avatar shows. A monogram with no `text` uses the name's initials. */
|
|
24
|
+
export type MessengerAvatar = {
|
|
25
|
+
readonly kind: "image";
|
|
26
|
+
readonly src: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "emoji";
|
|
29
|
+
readonly emoji: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "icon";
|
|
32
|
+
readonly icon: MessengerIcon;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: "monogram";
|
|
35
|
+
readonly text?: string;
|
|
36
|
+
};
|
|
37
|
+
export interface MessengerMenuAction {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly icon?: MessengerIcon;
|
|
41
|
+
readonly onSelect: () => void;
|
|
42
|
+
readonly destructive?: boolean;
|
|
43
|
+
readonly disabled?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface MessengerMenuSeparator {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly separator: true;
|
|
48
|
+
}
|
|
49
|
+
export type MessengerMenuEntry = MessengerMenuAction | MessengerMenuSeparator;
|
|
50
|
+
export declare function isMenuSeparator(entry: MessengerMenuEntry): entry is MessengerMenuSeparator;
|
|
51
|
+
/** One top-level rail item (WhatsApp's Chats, Calls, Starred…). */
|
|
52
|
+
export interface MessengerSpace {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly label: string;
|
|
55
|
+
readonly icon: MessengerIcon;
|
|
56
|
+
/** Shown beside the label (or as a badge when collapsed). 0 and undefined draw nothing. */
|
|
57
|
+
readonly count?: number;
|
|
58
|
+
}
|
|
59
|
+
/** A collapsible group of spaces (WhatsApp's "More" and "Communities"). */
|
|
60
|
+
export interface MessengerSpaceGroup {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly label: string;
|
|
63
|
+
readonly spaces: readonly MessengerSpace[];
|
|
64
|
+
readonly defaultOpen?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export type MessengerRailEntry = MessengerSpace | MessengerSpaceGroup;
|
|
67
|
+
export declare function isSpaceGroup(entry: MessengerRailEntry): entry is MessengerSpaceGroup;
|
|
68
|
+
/** One row in the contact list. Every field but `id` and `name` is optional chrome. */
|
|
69
|
+
export interface MessengerContact {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly avatar?: MessengerAvatar;
|
|
73
|
+
/** Already formatted for the row ("11:24 AM", "Yesterday"). */
|
|
74
|
+
readonly time?: string;
|
|
75
|
+
readonly preview?: string;
|
|
76
|
+
readonly unread?: number;
|
|
77
|
+
readonly pinned?: boolean;
|
|
78
|
+
readonly muted?: boolean;
|
|
79
|
+
/** An unsent draft; the row shows "Draft:" and this text instead of the preview. */
|
|
80
|
+
readonly draft?: string;
|
|
81
|
+
/** Extra text the search matches (an email, a handle). Never drawn. */
|
|
82
|
+
readonly searchText?: string;
|
|
83
|
+
}
|
|
84
|
+
/** A filter chip under the search ("All", "Unread", "Groups"). */
|
|
85
|
+
export interface MessengerFilter {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly label: string;
|
|
88
|
+
readonly test: (contact: MessengerContact) => boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface MessengerHeaderInfo {
|
|
91
|
+
readonly title: string;
|
|
92
|
+
readonly subtitle?: string;
|
|
93
|
+
readonly avatar?: MessengerAvatar;
|
|
94
|
+
}
|
|
95
|
+
export interface MessengerProfile {
|
|
96
|
+
readonly name: string;
|
|
97
|
+
readonly avatar?: MessengerAvatar;
|
|
98
|
+
readonly onSelect?: () => void;
|
|
99
|
+
}
|
|
100
|
+
export interface MessengerListState {
|
|
101
|
+
readonly loading?: boolean;
|
|
102
|
+
readonly error?: string | null;
|
|
103
|
+
readonly retry?: () => void;
|
|
104
|
+
readonly hasMore?: boolean;
|
|
105
|
+
readonly loadMore?: () => void;
|
|
106
|
+
/** Words for an empty list ("No chats yet"). */
|
|
107
|
+
readonly emptyLabel?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface MessengerSource {
|
|
110
|
+
/** The list title ("Chats"). */
|
|
111
|
+
readonly title: string;
|
|
112
|
+
readonly rail: readonly MessengerRailEntry[];
|
|
113
|
+
readonly activeSpaceId: string | null;
|
|
114
|
+
readonly onSelectSpace: (spaceId: string) => void;
|
|
115
|
+
readonly spaceMenu?: (space: MessengerSpace) => readonly MessengerMenuEntry[];
|
|
116
|
+
readonly profile?: MessengerProfile;
|
|
117
|
+
readonly contacts: readonly MessengerContact[];
|
|
118
|
+
readonly listState?: MessengerListState;
|
|
119
|
+
readonly filters?: readonly MessengerFilter[];
|
|
120
|
+
readonly activeContactId: string | null;
|
|
121
|
+
readonly onSelectContact: (contactId: string) => void;
|
|
122
|
+
readonly contactMenu?: (contact: MessengerContact) => readonly MessengerMenuEntry[];
|
|
123
|
+
/** The title row's "⋯" menu. */
|
|
124
|
+
readonly listMenu?: () => readonly MessengerMenuEntry[];
|
|
125
|
+
/** The title row's "+" button. Absent: no button. */
|
|
126
|
+
readonly onNew?: () => void;
|
|
127
|
+
readonly newLabel?: string;
|
|
128
|
+
readonly header: (contact: MessengerContact) => MessengerHeaderInfo;
|
|
129
|
+
/** Right-click and "⋯" on the thread header. */
|
|
130
|
+
readonly headerMenu?: (contact: MessengerContact) => readonly MessengerMenuEntry[];
|
|
131
|
+
/**
|
|
132
|
+
* A dropdown under the header title (past threads + "New chat" for an agent).
|
|
133
|
+
* Present: the title opens it. Absent: the title opens the details panel.
|
|
134
|
+
*/
|
|
135
|
+
readonly titleMenu?: (contact: MessengerContact) => readonly MessengerMenuEntry[];
|
|
136
|
+
readonly headerActions?: (contact: MessengerContact) => ReactNode;
|
|
137
|
+
/** The thread body. The source's own renderer; the shell draws nothing inside it. */
|
|
138
|
+
readonly renderThread: (contact: MessengerContact) => ReactNode;
|
|
139
|
+
/** The right-hand details panel. Absent: no panel and no toggle. */
|
|
140
|
+
readonly renderDetails?: (contact: MessengerContact) => ReactNode;
|
|
141
|
+
/** What the thread area shows with no contact selected. */
|
|
142
|
+
readonly renderEmpty?: () => ReactNode;
|
|
143
|
+
}
|
|
144
|
+
/** The shell's search: case-insensitive over name, preview and `searchText`. */
|
|
145
|
+
export declare function matchesQuery(contact: MessengerContact, query: string): boolean;
|
|
146
|
+
/** The rows the list draws: the active filter chip, then the search. */
|
|
147
|
+
export declare function visibleContacts(contacts: readonly MessengerContact[], query: string, filter: MessengerFilter | null): readonly MessengerContact[];
|
|
148
|
+
/** Two-letter initials for a monogram avatar. */
|
|
149
|
+
export declare function initialsOf(name: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function isMenuSeparator(entry) {
|
|
2
|
+
return "separator" in entry && entry.separator === true;
|
|
3
|
+
}
|
|
4
|
+
function isSpaceGroup(entry) {
|
|
5
|
+
return "spaces" in entry;
|
|
6
|
+
}
|
|
7
|
+
function matchesQuery(contact, query) {
|
|
8
|
+
const q = query.trim().toLowerCase();
|
|
9
|
+
if (q === "") return true;
|
|
10
|
+
return [contact.name, contact.preview, contact.searchText].some((text) => text !== void 0 && text.toLowerCase().includes(q));
|
|
11
|
+
}
|
|
12
|
+
function visibleContacts(contacts, query, filter) {
|
|
13
|
+
return contacts.filter((c) => (filter === null || filter.test(c)) && matchesQuery(c, query));
|
|
14
|
+
}
|
|
15
|
+
function initialsOf(name) {
|
|
16
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
17
|
+
if (words.length === 0) return "?";
|
|
18
|
+
const first = words[0].charAt(0);
|
|
19
|
+
const last = words.length > 1 ? words[words.length - 1].charAt(0) : "";
|
|
20
|
+
return `${first}${last}`.toUpperCase();
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
initialsOf,
|
|
24
|
+
isMenuSeparator,
|
|
25
|
+
isSpaceGroup,
|
|
26
|
+
matchesQuery,
|
|
27
|
+
visibleContacts
|
|
28
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A row's time, the desktop-messenger way: today → "11:24 AM", yesterday →
|
|
3
|
+
* "Yesterday", this week → the weekday, older → the short date. Pure; `now`
|
|
4
|
+
* is injectable for tests.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatMessengerTime(iso: string | null, now?: Date): string | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function formatMessengerTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
2
|
+
if (iso === null) return void 0;
|
|
3
|
+
const at = new Date(iso);
|
|
4
|
+
if (Number.isNaN(at.getTime())) return void 0;
|
|
5
|
+
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
6
|
+
const days = Math.round((startOfDay(now) - startOfDay(at)) / 864e5);
|
|
7
|
+
if (days <= 0) return at.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
|
|
8
|
+
if (days === 1) return "Yesterday";
|
|
9
|
+
if (days < 7) return at.toLocaleDateString(void 0, { weekday: "long" });
|
|
10
|
+
return at.toLocaleDateString(void 0, { month: "numeric", day: "numeric", year: "2-digit" });
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
formatMessengerTime
|
|
14
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PEOPLE SOURCE — `@ai-matrx/messaging` conversations as a MessengerSource.
|
|
3
|
+
*
|
|
4
|
+
* Everything real is messaging's: the inbox (useConversations), the People /
|
|
5
|
+
* Agents and archive axes (server requests, never client sieves), presence,
|
|
6
|
+
* mark-read, and the thread body, which is messaging's own `ConversationView`
|
|
7
|
+
* (`showHeader={false}`, `appearance="messenger"`) — rich content, references,
|
|
8
|
+
* actions, typing and the composer are never re-implemented here.
|
|
9
|
+
*
|
|
10
|
+
* Needs a `<MessagingProvider>` above it (a host mounts one app-wide).
|
|
11
|
+
* Only this subpath imports `@ai-matrx/messaging`; the build is per-file, so a
|
|
12
|
+
* chat host that never mounts the messenger loads none of it.
|
|
13
|
+
*/
|
|
14
|
+
import { type ReactNode } from "react";
|
|
15
|
+
import { type ConversationId, type ConversationSummary } from "@ai-matrx/messaging/react";
|
|
16
|
+
import type { MessengerMenuEntry, MessengerProfile, MessengerSource } from "../contract.js";
|
|
17
|
+
export interface PeopleMessengerOptions {
|
|
18
|
+
/** The list title. Default "Chats". */
|
|
19
|
+
readonly title?: string;
|
|
20
|
+
readonly profile?: MessengerProfile;
|
|
21
|
+
/** The "+" button (the host's new-conversation dialog). */
|
|
22
|
+
readonly onNewConversation?: () => void;
|
|
23
|
+
/** Conversation ids the host keeps pinned, in order. With `onTogglePin`, rows get Pin/Unpin. */
|
|
24
|
+
readonly pinnedConversationIds?: readonly string[];
|
|
25
|
+
readonly onTogglePin?: (conversationId: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* The thread body. Default: messaging's `ConversationView` with the messenger
|
|
28
|
+
* appearance. A host that wraps that SAME view in its own chrome (its
|
|
29
|
+
* right-click menu, its composer field) passes it here — never a second renderer.
|
|
30
|
+
*/
|
|
31
|
+
readonly renderConversation?: (conversationId: ConversationId) => ReactNode;
|
|
32
|
+
/** Host-specific options appended to every conversation menu. */
|
|
33
|
+
readonly conversationMenuExtras?: (summary: ConversationSummary) => readonly MessengerMenuEntry[];
|
|
34
|
+
}
|
|
35
|
+
export declare function usePeopleMessengerSource(options?: PeopleMessengerOptions): MessengerSource;
|