@stapel/chat-react 0.1.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/CHANGELOG.md +1 -0
- package/MODULE.md +118 -0
- package/README.md +126 -0
- package/dist/api/chatApi.d.ts +49 -0
- package/dist/api/chatApi.d.ts.map +1 -0
- package/dist/api/chatApi.js +46 -0
- package/dist/api/chatApi.js.map +1 -0
- package/dist/api/extensions.d.ts +19 -0
- package/dist/api/extensions.d.ts.map +1 -0
- package/dist/api/extensions.js +19 -0
- package/dist/api/extensions.js.map +1 -0
- package/dist/api/generated/schema.d.ts +547 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/types.d.ts +75 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/ConversationListPanel.d.ts +22 -0
- package/dist/default/ConversationListPanel.d.ts.map +1 -0
- package/dist/default/ConversationListPanel.js +49 -0
- package/dist/default/ConversationListPanel.js.map +1 -0
- package/dist/default/ConversationThreadPanel.d.ts +21 -0
- package/dist/default/ConversationThreadPanel.d.ts.map +1 -0
- package/dist/default/ConversationThreadPanel.js +51 -0
- package/dist/default/ConversationThreadPanel.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +8 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +26 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/StartChatButton.d.ts +17 -0
- package/dist/default/StartChatButton.d.ts.map +1 -0
- package/dist/default/StartChatButton.js +18 -0
- package/dist/default/StartChatButton.js.map +1 -0
- package/dist/default/index.d.ts +21 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +18 -0
- package/dist/default/index.js.map +1 -0
- package/dist/flows/errors.d.ts +12 -0
- package/dist/flows/errors.d.ts.map +1 -0
- package/dist/flows/errors.js +13 -0
- package/dist/flows/errors.js.map +1 -0
- package/dist/flows/freshness.d.ts +69 -0
- package/dist/flows/freshness.d.ts.map +1 -0
- package/dist/flows/freshness.js +227 -0
- package/dist/flows/freshness.js.map +1 -0
- package/dist/flows/registry.d.ts +26 -0
- package/dist/flows/registry.d.ts.map +1 -0
- package/dist/flows/registry.js +24 -0
- package/dist/flows/registry.js.map +1 -0
- package/dist/headless/ChatProvider.d.ts +22 -0
- package/dist/headless/ChatProvider.d.ts.map +1 -0
- package/dist/headless/ChatProvider.js +18 -0
- package/dist/headless/ChatProvider.js.map +1 -0
- package/dist/headless/ConversationList.d.ts +51 -0
- package/dist/headless/ConversationList.d.ts.map +1 -0
- package/dist/headless/ConversationList.js +47 -0
- package/dist/headless/ConversationList.js.map +1 -0
- package/dist/headless/ConversationThread.d.ts +50 -0
- package/dist/headless/ConversationThread.d.ts.map +1 -0
- package/dist/headless/ConversationThread.js +64 -0
- package/dist/headless/ConversationThread.js.map +1 -0
- package/dist/headless/MessageComposer.d.ts +39 -0
- package/dist/headless/MessageComposer.d.ts.map +1 -0
- package/dist/headless/MessageComposer.js +45 -0
- package/dist/headless/MessageComposer.js.map +1 -0
- package/dist/headless/StartDirectChat.d.ts +40 -0
- package/dist/headless/StartDirectChat.d.ts.map +1 -0
- package/dist/headless/StartDirectChat.js +46 -0
- package/dist/headless/StartDirectChat.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +10 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +20 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +23 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +75 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +359 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +183 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +78 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +130 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +28 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +80 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/limits.d.ts +12 -0
- package/dist/model/limits.d.ts.map +1 -0
- package/dist/model/limits.js +12 -0
- package/dist/model/limits.js.map +1 -0
- package/dist/model/mutations.d.ts +66 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +156 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +33 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +93 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +8 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +26 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/readMarker.d.ts +25 -0
- package/dist/model/readMarker.d.ts.map +1 -0
- package/dist/model/readMarker.js +31 -0
- package/dist/model/readMarker.js.map +1 -0
- package/dist/model/runtime.d.ts +50 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +27 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/model/threadWindow.d.ts +95 -0
- package/dist/model/threadWindow.d.ts.map +1 -0
- package/dist/model/threadWindow.js +100 -0
- package/dist/model/threadWindow.js.map +1 -0
- package/dist/nav/manifest.d.ts +23 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +15 -0
- package/dist/nav/manifest.js.map +1 -0
- package/dist/realtime/chatSocket.d.ts +93 -0
- package/dist/realtime/chatSocket.d.ts.map +1 -0
- package/dist/realtime/chatSocket.js +199 -0
- package/dist/realtime/chatSocket.js.map +1 -0
- package/dist/realtime/frames.d.ts +123 -0
- package/dist/realtime/frames.d.ts.map +1 -0
- package/dist/realtime/frames.js +123 -0
- package/dist/realtime/frames.js.map +1 -0
- package/dist/realtime/streams.d.ts +49 -0
- package/dist/realtime/streams.d.ts.map +1 -0
- package/dist/realtime/streams.js +52 -0
- package/dist/realtime/streams.js.map +1 -0
- package/llms.txt +89 -0
- package/manifest.json +733 -0
- package/nav-manifest.json +25 -0
- package/package.json +114 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/chatApi.ts +142 -0
- package/src/api/extensions.ts +18 -0
- package/src/api/generated/schema.ts +549 -0
- package/src/api/types.ts +87 -0
- package/src/default/ConversationListPanel.tsx +163 -0
- package/src/default/ConversationThreadPanel.tsx +245 -0
- package/src/default/ErrorAlert.tsx +43 -0
- package/src/default/StartChatButton.tsx +78 -0
- package/src/default/index.ts +20 -0
- package/src/flows/errors.ts +16 -0
- package/src/flows/freshness.ts +315 -0
- package/src/flows/registry.ts +36 -0
- package/src/headless/ChatProvider.tsx +23 -0
- package/src/headless/ConversationList.tsx +92 -0
- package/src/headless/ConversationThread.tsx +107 -0
- package/src/headless/MessageComposer.tsx +86 -0
- package/src/headless/StartDirectChat.tsx +75 -0
- package/src/i18n/errorsMap.ts +31 -0
- package/src/i18n/es.ts +95 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +214 -0
- package/src/i18n/generated/errors.json +408 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +152 -0
- package/src/i18n/ru.ts +98 -0
- package/src/index.ts +189 -0
- package/src/model/context.tsx +25 -0
- package/src/model/limits.ts +11 -0
- package/src/model/mutations.ts +208 -0
- package/src/model/queries.ts +120 -0
- package/src/model/queryKeys.ts +32 -0
- package/src/model/readMarker.ts +32 -0
- package/src/model/runtime.ts +90 -0
- package/src/model/threadWindow.ts +173 -0
- package/src/nav/manifest.ts +36 -0
- package/src/realtime/chatSocket.ts +304 -0
- package/src/realtime/frames.ts +229 -0
- package/src/realtime/streams.ts +85 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stapel-chat WebSocket protocol, typed.
|
|
3
|
+
*
|
|
4
|
+
* This is a MIRROR of the module's own contract, not an invention:
|
|
5
|
+
* `stapel_chat/consumers.py` (`ChatConsumer`) and `stapel_chat/realtime.py`
|
|
6
|
+
* (`message_frame`) are the source, and `MODULE.md` §"Realtime protocol"
|
|
7
|
+
* states it in one line:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* client → server: hello{last_seq} / send{body,attachments,reply_to} / ack{seq} / ping
|
|
11
|
+
* server → client: welcome{server_seq} / message{…seq} / replay_done{up_to_seq} / error{code,message} / pong
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The WS surface is invisible to `schema.json` (OpenAPI describes HTTP), so
|
|
15
|
+
* these types cannot be generated today — the realtime spec (§8) parks the
|
|
16
|
+
* frame contract in the `events.json` family for later. Until then this file
|
|
17
|
+
* is the pair's declared mirror and `test/frames.test.ts` pins it against the
|
|
18
|
+
* shapes the consumer actually sends.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here imports React or `@stapel/core`: it is plain protocol, so the
|
|
21
|
+
* future `@stapel/realtime` substrate can subsume it without dragging the UI.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Widest resume gap the server replays inline (`consumers.REPLAY_LIMIT`).
|
|
25
|
+
* Past it the server answers `error{resync}` instead, and the client
|
|
26
|
+
* re-hydrates from the REST history. Mirrored here so the client can SAY that
|
|
27
|
+
* before the server has to.
|
|
28
|
+
*/
|
|
29
|
+
export const CHAT_WS_REPLAY_LIMIT = 500;
|
|
30
|
+
/** `ChatConsumer.connect()` closes with this when `scope["user"]` is unset. */
|
|
31
|
+
export const CHAT_WS_CLOSE_UNAUTHENTICATED = 4401;
|
|
32
|
+
/** …and with this when the caller is not a participant of the conversation. */
|
|
33
|
+
export const CHAT_WS_CLOSE_NOT_PARTICIPANT = 4403;
|
|
34
|
+
/** The one error code that means "stop replaying, re-read the journal". */
|
|
35
|
+
export const CHAT_WS_RESYNC = "resync";
|
|
36
|
+
// ── parsing (the wire is untrusted structured text) ──────────────────────────
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return typeof value === "object" && value !== null;
|
|
39
|
+
}
|
|
40
|
+
function str(value) {
|
|
41
|
+
return typeof value === "string" ? value : null;
|
|
42
|
+
}
|
|
43
|
+
function num(value) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Narrow one decoded server frame, or `null` for anything this build does not
|
|
48
|
+
* understand (an unknown `type`, a missing field, a `seq` that is not a
|
|
49
|
+
* number). Unknown is DROPPED, never coerced: a frame this client cannot read
|
|
50
|
+
* must not advance the seq cursor, or the gap it leaves is invisible.
|
|
51
|
+
*/
|
|
52
|
+
export function parseServerFrame(value) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return null;
|
|
55
|
+
switch (value.type) {
|
|
56
|
+
case "welcome": {
|
|
57
|
+
const conversationId = str(value.conversation_id);
|
|
58
|
+
const serverSeq = num(value.server_seq);
|
|
59
|
+
if (conversationId === null || serverSeq === null)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
type: "welcome",
|
|
63
|
+
conversation_id: conversationId,
|
|
64
|
+
server_seq: serverSeq,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
case "message": {
|
|
68
|
+
const seq = num(value.seq);
|
|
69
|
+
const messageId = str(value.message_id);
|
|
70
|
+
const conversationId = str(value.conversation_id);
|
|
71
|
+
const createdAt = str(value.created_at);
|
|
72
|
+
if (seq === null ||
|
|
73
|
+
messageId === null ||
|
|
74
|
+
conversationId === null ||
|
|
75
|
+
createdAt === null) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const attachments = Array.isArray(value.attachments)
|
|
79
|
+
? value.attachments.filter((a) => typeof a === "string")
|
|
80
|
+
: [];
|
|
81
|
+
return {
|
|
82
|
+
type: "message",
|
|
83
|
+
message_id: messageId,
|
|
84
|
+
conversation_id: conversationId,
|
|
85
|
+
sender_id: str(value.sender_id),
|
|
86
|
+
seq,
|
|
87
|
+
kind: str(value.kind) ?? "text",
|
|
88
|
+
body: str(value.body) ?? "",
|
|
89
|
+
reply_to: str(value.reply_to),
|
|
90
|
+
attachments,
|
|
91
|
+
created_at: createdAt,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case "replay_done": {
|
|
95
|
+
const upTo = num(value.up_to_seq);
|
|
96
|
+
if (upTo === null)
|
|
97
|
+
return null;
|
|
98
|
+
return { type: "replay_done", up_to_seq: upTo };
|
|
99
|
+
}
|
|
100
|
+
case "error": {
|
|
101
|
+
const code = str(value.code);
|
|
102
|
+
if (code === null)
|
|
103
|
+
return null;
|
|
104
|
+
return { type: "error", code, message: str(value.message) ?? "" };
|
|
105
|
+
}
|
|
106
|
+
case "pong":
|
|
107
|
+
return { type: "pong" };
|
|
108
|
+
default:
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Decode a socket payload (JSON text) into a frame, or `null`. */
|
|
113
|
+
export function decodeServerFrame(data) {
|
|
114
|
+
if (typeof data !== "string")
|
|
115
|
+
return null;
|
|
116
|
+
try {
|
|
117
|
+
return parseServerFrame(JSON.parse(data));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=frames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/realtime/frames.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How this pair NAMES the things it can be fresh about (realtime spec §7,
|
|
3
|
+
* "how a pair declares its channels"): typed stream keys plus the one rule
|
|
4
|
+
* that turns a key into a socket URL. Everything that decides *whether* a stream
|
|
5
|
+
* has a socket at all lives here, so the transport seam above can stay a
|
|
6
|
+
* two-line question.
|
|
7
|
+
*/
|
|
8
|
+
/** A conversation's own journal — the module mounts a socket per thread. */
|
|
9
|
+
export interface ChatConversationStream {
|
|
10
|
+
readonly kind: "conversation";
|
|
11
|
+
readonly conversationId: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The caller's inbox (their conversation list).
|
|
15
|
+
*
|
|
16
|
+
* There is NO socket for this one, and that is a fact about the backend, not
|
|
17
|
+
* a gap in this pair: `stapel_chat.routing` mounts exactly one pattern,
|
|
18
|
+
* `ws/chat/<uuid:conversation_id>`, and the Channels group a message fans out
|
|
19
|
+
* to is `chat.conv.<id>` — per thread. Nothing broadcasts "your list moved".
|
|
20
|
+
* So the inbox is kept fresh by the polling half of the seam, and the seam is
|
|
21
|
+
* the reason no screen has to know that.
|
|
22
|
+
*/
|
|
23
|
+
export interface ChatInboxStream {
|
|
24
|
+
readonly kind: "inbox";
|
|
25
|
+
}
|
|
26
|
+
export type ChatStreamKey = ChatConversationStream | ChatInboxStream;
|
|
27
|
+
export declare function chatConversationStream(conversationId: string): ChatConversationStream;
|
|
28
|
+
export declare function chatInboxStream(): ChatInboxStream;
|
|
29
|
+
/** A stable string for a stream key — a React dependency, a log line. */
|
|
30
|
+
export declare function chatStreamId(stream: ChatStreamKey): string;
|
|
31
|
+
/**
|
|
32
|
+
* Derive the socket base from the REST base URL.
|
|
33
|
+
*
|
|
34
|
+
* The mount point is canonical (`stapel_chat/routing.py`, and the fleet
|
|
35
|
+
* convention `/ws/<mod>/...` from the realtime spec §8): the sockets sit at
|
|
36
|
+
* the HOST ROOT, not under the module's API prefix, so only the origin of
|
|
37
|
+
* `baseUrl` is used. `http` → `ws`, `https` → `wss`.
|
|
38
|
+
*
|
|
39
|
+
* Returns `null` when no origin can be resolved — a relative `baseUrl` with
|
|
40
|
+
* no `origin` (server-side rendering, a node test). `null` is not a failure:
|
|
41
|
+
* it means "this build cannot open a socket", and the transport seam polls.
|
|
42
|
+
*/
|
|
43
|
+
export declare function deriveChatSocketBase(baseUrl: string, origin?: string | null): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* The socket URL for a stream, or `null` when the stream has none (the
|
|
46
|
+
* inbox) or no base could be derived.
|
|
47
|
+
*/
|
|
48
|
+
export declare function chatSocketUrl(base: string | null, stream: ChatStreamKey): string | null;
|
|
49
|
+
//# sourceMappingURL=streams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../src/realtime/streams.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,GAAG,sBAAsB,GAAG,eAAe,CAAC;AAErE,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,MAAM,GACrB,sBAAsB,CAExB;AAED,wBAAgB,eAAe,IAAI,eAAe,CAEjD;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAI1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,GAAG,IAAI,CASf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,MAAM,EAAE,aAAa,GACpB,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How this pair NAMES the things it can be fresh about (realtime spec §7,
|
|
3
|
+
* "how a pair declares its channels"): typed stream keys plus the one rule
|
|
4
|
+
* that turns a key into a socket URL. Everything that decides *whether* a stream
|
|
5
|
+
* has a socket at all lives here, so the transport seam above can stay a
|
|
6
|
+
* two-line question.
|
|
7
|
+
*/
|
|
8
|
+
export function chatConversationStream(conversationId) {
|
|
9
|
+
return { kind: "conversation", conversationId };
|
|
10
|
+
}
|
|
11
|
+
export function chatInboxStream() {
|
|
12
|
+
return { kind: "inbox" };
|
|
13
|
+
}
|
|
14
|
+
/** A stable string for a stream key — a React dependency, a log line. */
|
|
15
|
+
export function chatStreamId(stream) {
|
|
16
|
+
return stream.kind === "conversation"
|
|
17
|
+
? `conversation:${stream.conversationId}`
|
|
18
|
+
: "inbox";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive the socket base from the REST base URL.
|
|
22
|
+
*
|
|
23
|
+
* The mount point is canonical (`stapel_chat/routing.py`, and the fleet
|
|
24
|
+
* convention `/ws/<mod>/...` from the realtime spec §8): the sockets sit at
|
|
25
|
+
* the HOST ROOT, not under the module's API prefix, so only the origin of
|
|
26
|
+
* `baseUrl` is used. `http` → `ws`, `https` → `wss`.
|
|
27
|
+
*
|
|
28
|
+
* Returns `null` when no origin can be resolved — a relative `baseUrl` with
|
|
29
|
+
* no `origin` (server-side rendering, a node test). `null` is not a failure:
|
|
30
|
+
* it means "this build cannot open a socket", and the transport seam polls.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveChatSocketBase(baseUrl, origin) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(baseUrl, origin ?? undefined);
|
|
35
|
+
const protocol = url.protocol === "https:" || url.protocol === "wss:" ? "wss:" : "ws:";
|
|
36
|
+
return `${protocol}//${url.host}/ws/chat/`;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The socket URL for a stream, or `null` when the stream has none (the
|
|
44
|
+
* inbox) or no base could be derived.
|
|
45
|
+
*/
|
|
46
|
+
export function chatSocketUrl(base, stream) {
|
|
47
|
+
if (base === null || stream.kind !== "conversation")
|
|
48
|
+
return null;
|
|
49
|
+
const prefix = base.endsWith("/") ? base : `${base}/`;
|
|
50
|
+
return `${prefix}${encodeURIComponent(stream.conversationId)}`;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=streams.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../../src/realtime/streams.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,MAAM,UAAU,sBAAsB,CACpC,cAAsB;IAEtB,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc;QACnC,CAAC,CAAC,gBAAgB,MAAM,CAAC,cAAc,EAAE;QACzC,CAAC,CAAC,OAAO,CAAC;AACd,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,MAAsB;IAEtB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,IAAI,SAAS,CAAC,CAAC;QAClD,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;QACxE,OAAO,GAAG,QAAQ,KAAK,GAAG,CAAC,IAAI,WAAW,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAmB,EACnB,MAAqB;IAErB,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;IACtD,OAAO,GAAG,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;AACjE,CAAC"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @stapel/chat-react 0.1.0
|
|
2
|
+
|
|
3
|
+
Headless React flow pair for stapel-chat (contract >=0.2 <0.3) — business + state, zero visual opinion.
|
|
4
|
+
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
5
|
+
verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
|
|
6
|
+
|
|
7
|
+
## The one right way (do this, the rest is a review/lint smell)
|
|
8
|
+
- No raw fetch/axios. The client is injected via <ChatProvider>/StapelConfigProvider;
|
|
9
|
+
every hook and flow already carries auth, refresh, and the error envelope.
|
|
10
|
+
- Render errors, never try/catch them: a flow's state carries FlowError{code,params};
|
|
11
|
+
render `t(code, params)` and branch on `explainChatError(code)` remediation.
|
|
12
|
+
- Server state = the use* hooks (query layer); keys come only from chatQueryKeys.
|
|
13
|
+
- User strings = i18n keys (registerChatI18n); never string literals.
|
|
14
|
+
- Sign-in UI = a headless flow component; copy it (shadcn-style) to restyle.
|
|
15
|
+
|
|
16
|
+
## Layers
|
|
17
|
+
api (typed client) · model (hooks, session) · flows (machines) · headless · i18n
|
|
18
|
+
|
|
19
|
+
## Documented flows (flows.json — canonical id, steps, endpoints)
|
|
20
|
+
|
|
21
|
+
## Operations (typed; use the named op, never a path string)
|
|
22
|
+
Request/response schema names are in manifest.json + the generated types.
|
|
23
|
+
Paths are relative to `/chat/api/v1/`.
|
|
24
|
+
- chat_api_v1_conversations_create: POST /conversations
|
|
25
|
+
- chat_api_v1_conversations_list: GET /conversations
|
|
26
|
+
- chat_api_v1_conversations_messages_create: POST /conversations/{conversation_id}/messages
|
|
27
|
+
- chat_api_v1_conversations_messages_list: GET /conversations/{conversation_id}/messages
|
|
28
|
+
- chat_api_v1_conversations_read_create: POST /conversations/{conversation_id}/read
|
|
29
|
+
- chat_api_v1_conversations_retrieve: GET /conversations/{conversation_id}
|
|
30
|
+
- chat_api_v1_support_conversations_assign_create: POST /support/conversations/{conversation_id}/assign
|
|
31
|
+
- chat_api_v1_support_conversations_reopen_create: POST /support/conversations/{conversation_id}/reopen
|
|
32
|
+
- chat_api_v1_support_conversations_resolve_create: POST /support/conversations/{conversation_id}/resolve
|
|
33
|
+
- chat_api_v1_support_queue_list: GET /support/queue
|
|
34
|
+
|
|
35
|
+
## Query hooks (server state; keys come only from the key factory)
|
|
36
|
+
- useConversation (query) → conversation
|
|
37
|
+
- useLoadOlderMessages (mutation) → messages
|
|
38
|
+
- useMarkRead (mutation) → markRead
|
|
39
|
+
- useSendMessage (mutation) → sendMessage
|
|
40
|
+
- useStartDirectChat (mutation) → createConversation
|
|
41
|
+
- useThread (query) → messages
|
|
42
|
+
|
|
43
|
+
## Errors (render t(code, params); UX from remediation)
|
|
44
|
+
54 keys (full catalog: manifest.json §errors). By remediation: fix_input 25 · retry 16 · verify 5 · wait_and_retry 5 · contact_support 2 · reauthenticate 1.
|
|
45
|
+
Param-bearing keys (interpolation slots matter):
|
|
46
|
+
- error.400.field.blank [400] → fix_input {field}
|
|
47
|
+
- error.400.field.does_not_exist [400] → fix_input {field}
|
|
48
|
+
- error.400.field.invalid [400] → fix_input {field}
|
|
49
|
+
- error.400.field.invalid_choice [400] → fix_input {field}
|
|
50
|
+
- error.400.field.max_length [400] → fix_input {field,max_length}
|
|
51
|
+
- error.400.field.max_value [400] → fix_input {field,max_value}
|
|
52
|
+
- error.400.field.min_length [400] → fix_input {field,min_length}
|
|
53
|
+
- error.400.field.min_value [400] → fix_input {field,min_value}
|
|
54
|
+
- error.400.field.null [400] → fix_input {field}
|
|
55
|
+
- error.400.field.required [400] → fix_input {field}
|
|
56
|
+
- error.400.field.unique [400] → fix_input {field}
|
|
57
|
+
- error.429.rate_limit [429] → wait_and_retry {retry_after_minutes}
|
|
58
|
+
|
|
59
|
+
## Analytics events (typed; defineEvent → events.json, drift-gated)
|
|
60
|
+
- (no app defineEvent() in this pair — its analytic events are the
|
|
61
|
+
auto-instrumented flow funnels below)
|
|
62
|
+
|
|
63
|
+
## Flow funnels (auto-instrumented: flow.<id>.<step> {phase})
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// Typed event + tracked() click (the one right way; §3.1).
|
|
67
|
+
const planSelected = defineEvent({
|
|
68
|
+
name: "pricing.plan.selected",
|
|
69
|
+
description: "User picked a plan",
|
|
70
|
+
props: { plan: prop.oneOf(["free", "pro", "team"], "Plan code") },
|
|
71
|
+
});
|
|
72
|
+
const { tracked } = useTracked();
|
|
73
|
+
<Button onClick={tracked(planSelected, { plan }, startCheckout)} />
|
|
74
|
+
// A click that STEPS a flow machine is already instrumented — mark it
|
|
75
|
+
// data-analytics="flow" instead; tracked() on top double-counts (§3.2).
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Demos (defineDemo → manifest.demos; compiled, linted, rendered examples)
|
|
79
|
+
- chat.conversations → <ConversationList> [default] demo/ConversationList.demo.tsx
|
|
80
|
+
- chat.start → <StartDirectChat> [default|no-seller|own-listing] demo/StartDirectChat.demo.tsx
|
|
81
|
+
- chat.thread → <ConversationThread> [default] demo/ConversationThread.demo.tsx
|
|
82
|
+
Each source file is the canonical usage snippet (open the default variant).
|
|
83
|
+
|
|
84
|
+
## Snippets
|
|
85
|
+
```tsx
|
|
86
|
+
// Error rendering + remediation branch (one pattern for every pair).
|
|
87
|
+
const r = explainChatError(err.code); // 'wait_and_retry' | 'verify' | ...
|
|
88
|
+
return <Alert action={r}>{t(err.code, err.params)}</Alert>;
|
|
89
|
+
```
|