@blocksdiy/react-common 1.70.2 → 1.71.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/comments/anchor.d.ts +74 -0
- package/dist/comments/anchor.d.ts.map +1 -0
- package/dist/comments/anchor.js +279 -0
- package/dist/comments/api.d.ts +38 -0
- package/dist/comments/api.d.ts.map +1 -0
- package/dist/comments/api.js +62 -0
- package/dist/comments/comment-agents.d.ts +28 -0
- package/dist/comments/comment-agents.d.ts.map +1 -0
- package/dist/comments/comment-agents.js +32 -0
- package/dist/comments/comment-mode.d.ts +97 -0
- package/dist/comments/comment-mode.d.ts.map +1 -0
- package/dist/comments/comment-mode.js +159 -0
- package/dist/comments/composer.d.ts +133 -0
- package/dist/comments/composer.d.ts.map +1 -0
- package/dist/comments/composer.js +273 -0
- package/dist/comments/index.d.ts +13 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +17 -0
- package/dist/comments/layout.d.ts +25 -0
- package/dist/comments/layout.d.ts.map +1 -0
- package/dist/comments/layout.js +83 -0
- package/dist/comments/mentions.d.ts +32 -0
- package/dist/comments/mentions.d.ts.map +1 -0
- package/dist/comments/mentions.js +30 -0
- package/dist/comments/page-pick.d.ts +76 -0
- package/dist/comments/page-pick.d.ts.map +1 -0
- package/dist/comments/page-pick.js +267 -0
- package/dist/comments/pick-message.d.ts +11 -0
- package/dist/comments/pick-message.d.ts.map +1 -0
- package/dist/comments/pick-message.js +33 -0
- package/dist/comments/pins.d.ts +72 -0
- package/dist/comments/pins.d.ts.map +1 -0
- package/dist/comments/pins.js +125 -0
- package/dist/comments/text-pick.d.ts +59 -0
- package/dist/comments/text-pick.d.ts.map +1 -0
- package/dist/comments/text-pick.js +165 -0
- package/dist/comments/threads-store.d.ts +119 -0
- package/dist/comments/threads-store.d.ts.map +1 -0
- package/dist/comments/threads-store.js +286 -0
- package/dist/comments/types.d.ts +232 -0
- package/dist/comments/types.d.ts.map +1 -0
- package/dist/comments/types.js +109 -0
- package/dist/hooks/agent-conversation-context.d.ts +12 -1
- package/dist/hooks/agent-conversation-context.d.ts.map +1 -1
- package/dist/hooks/agent-conversation-context.js +12 -5
- package/dist/hooks/agent-thread-live-update.d.ts +5 -2
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
- package/dist/hooks/agent-thread-live-update.js +14 -0
- package/dist/hooks/use-agent-conversation.d.ts +8 -0
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
- package/dist/hooks/use-agent-conversation.js +10 -0
- package/dist/hooks/use-agent-threads.d.ts +1 -0
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +4 -1
- package/dist/iframe-communication/types.d.ts +24 -0
- package/dist/iframe-communication/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
export const DEFAULT_COMMENT_OPTIONS = { showResolved: false };
|
|
3
|
+
const sameOptions = (a, b) => a.showResolved === b.showResolved && a.mentionableAgents?.join("\n") === b.mentionableAgents?.join("\n");
|
|
4
|
+
/** How long turning comment mode on waits for a comments surface to mount. */
|
|
5
|
+
export const COMMENT_MODE_CLAIM_TIMEOUT_MS = 5000;
|
|
6
|
+
const defaultTimers = {
|
|
7
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
8
|
+
clearTimeout: (id) => clearTimeout(id),
|
|
9
|
+
};
|
|
10
|
+
export const createCommentModeStore = (timers = defaultTimers) => {
|
|
11
|
+
let on = false;
|
|
12
|
+
let surfaces = 0;
|
|
13
|
+
let pending = null;
|
|
14
|
+
const listeners = new Set();
|
|
15
|
+
const exitListeners = new Set();
|
|
16
|
+
let options = DEFAULT_COMMENT_OPTIONS;
|
|
17
|
+
let openThreadHandler = null;
|
|
18
|
+
const requestOnListeners = new Set();
|
|
19
|
+
const emit = () => listeners.forEach((listener) => listener());
|
|
20
|
+
const canOpenThread = () => requestOnListeners.size > 0 && openThreadHandler !== null;
|
|
21
|
+
const settlePending = (claimed) => {
|
|
22
|
+
if (pending) {
|
|
23
|
+
timers.clearTimeout(pending.timer);
|
|
24
|
+
pending.resolve(claimed);
|
|
25
|
+
pending = null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const turnOn = () => {
|
|
29
|
+
if (!on) {
|
|
30
|
+
on = true;
|
|
31
|
+
emit();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const turnOff = () => {
|
|
35
|
+
if (on) {
|
|
36
|
+
on = false;
|
|
37
|
+
emit();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
set(isOn) {
|
|
42
|
+
// A newer request supersedes one still waiting for a surface.
|
|
43
|
+
settlePending(false);
|
|
44
|
+
if (!isOn) {
|
|
45
|
+
turnOff();
|
|
46
|
+
return Promise.resolve(true);
|
|
47
|
+
}
|
|
48
|
+
if (surfaces > 0) {
|
|
49
|
+
turnOn();
|
|
50
|
+
return Promise.resolve(true);
|
|
51
|
+
}
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
pending = {
|
|
54
|
+
resolve,
|
|
55
|
+
timer: timers.setTimeout(() => {
|
|
56
|
+
pending = null;
|
|
57
|
+
resolve(false);
|
|
58
|
+
}, COMMENT_MODE_CLAIM_TIMEOUT_MS),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
isOn: () => on,
|
|
63
|
+
subscribe(listener) {
|
|
64
|
+
listeners.add(listener);
|
|
65
|
+
return () => listeners.delete(listener);
|
|
66
|
+
},
|
|
67
|
+
registerSurface() {
|
|
68
|
+
surfaces += 1;
|
|
69
|
+
if (pending) {
|
|
70
|
+
turnOn();
|
|
71
|
+
settlePending(true);
|
|
72
|
+
}
|
|
73
|
+
let registered = true;
|
|
74
|
+
return () => {
|
|
75
|
+
if (!registered) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
registered = false;
|
|
79
|
+
surfaces -= 1;
|
|
80
|
+
if (surfaces === 0) {
|
|
81
|
+
turnOff();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
exit() {
|
|
86
|
+
if (on) {
|
|
87
|
+
turnOff();
|
|
88
|
+
exitListeners.forEach((listener) => listener());
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
onExit(listener) {
|
|
92
|
+
exitListeners.add(listener);
|
|
93
|
+
return () => exitListeners.delete(listener);
|
|
94
|
+
},
|
|
95
|
+
setOptions(next) {
|
|
96
|
+
if (!sameOptions(next, options)) {
|
|
97
|
+
options = next;
|
|
98
|
+
emit();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
options: () => options,
|
|
102
|
+
handleOpenThread(handler) {
|
|
103
|
+
openThreadHandler = handler;
|
|
104
|
+
return () => {
|
|
105
|
+
if (openThreadHandler === handler) {
|
|
106
|
+
openThreadHandler = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
onRequestOn(listener) {
|
|
111
|
+
requestOnListeners.add(listener);
|
|
112
|
+
return () => requestOnListeners.delete(listener);
|
|
113
|
+
},
|
|
114
|
+
canOpenThread,
|
|
115
|
+
openThread(request) {
|
|
116
|
+
if (!canOpenThread()) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
requestOnListeners.forEach((listener) => listener());
|
|
120
|
+
openThreadHandler?.(request);
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/** The one store the host and the app share. */
|
|
126
|
+
export const commentMode = createCommentModeStore();
|
|
127
|
+
/** The host: turn comment mode on or off (resolves false when the app has no comments surface). */
|
|
128
|
+
export const setCommentMode = (isOn) => commentMode.set(isOn);
|
|
129
|
+
/** The host: hears when the app turned comment mode off itself. Returns the unsubscribe. */
|
|
130
|
+
export const onCommentModeExit = (listener) => commentMode.onExit(listener);
|
|
131
|
+
/** The host: what the app's comments show (the Comment button's options). */
|
|
132
|
+
export const setCommentOptions = (next) => commentMode.setOptions(next);
|
|
133
|
+
/** The host: hears the app ask for comment mode. Returns the unsubscribe. */
|
|
134
|
+
export const onCommentModeRequest = (listener) => commentMode.onRequestOn(listener);
|
|
135
|
+
/**
|
|
136
|
+
* The app (the agent chat's comment message): opens a thread — its page, comment mode on, the
|
|
137
|
+
* thread open. False when it can't here: outside the platform, or without the comments component.
|
|
138
|
+
*/
|
|
139
|
+
export const openCommentThread = (request, store = commentMode) => store.openThread(request);
|
|
140
|
+
/** Whether `openCommentThread` can open a thread here: the link back to it shows only then. */
|
|
141
|
+
export const canOpenCommentThread = (store = commentMode) => store.canOpenThread();
|
|
142
|
+
/**
|
|
143
|
+
* The app's comments surface: registers itself while mounted and reads the mode. `exit` turns it off
|
|
144
|
+
* from inside the app and tells the host. It opens the threads the app asks for (a comment's link in
|
|
145
|
+
* the agent chat) and reads the host's options.
|
|
146
|
+
*/
|
|
147
|
+
export const useCommentsSurface = ({ onOpenThread, store = commentMode } = {}) => {
|
|
148
|
+
useEffect(() => store.registerSurface(), [store]);
|
|
149
|
+
const latestOpenThread = useRef(onOpenThread);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
latestOpenThread.current = onOpenThread;
|
|
152
|
+
});
|
|
153
|
+
const isHandlingOpenThread = !!onOpenThread;
|
|
154
|
+
useEffect(() => (isHandlingOpenThread ? store.handleOpenThread((request) => latestOpenThread.current?.(request)) : undefined), [isHandlingOpenThread, store]);
|
|
155
|
+
const isCommentMode = useSyncExternalStore(store.subscribe, store.isOn, () => false);
|
|
156
|
+
const options = useSyncExternalStore(store.subscribe, store.options, () => DEFAULT_COMMENT_OPTIONS);
|
|
157
|
+
const exit = useCallback(() => store.exit(), [store]);
|
|
158
|
+
return { isCommentMode, options, exit };
|
|
159
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import { type Mention } from "./mentions.js";
|
|
3
|
+
import type { MentionableUserDto } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The comment composer's logic, headless: the text, the mentions in it, the `@` menu, the "Send to"
|
|
6
|
+
* agent line, and the body to store. The registry's composer only draws it.
|
|
7
|
+
*
|
|
8
|
+
* Each mention is a range of the text (`@Name`, exactly) tied to who it names, so two people with
|
|
9
|
+
* the same name stay apart and a typed `@Dana` is not a mention until picked. Every edit moves the
|
|
10
|
+
* ranges: an edit before one shifts it, an edit inside one turns it back into plain text, and
|
|
11
|
+
* Backspace right after one removes it whole.
|
|
12
|
+
*/
|
|
13
|
+
export interface ComposerMention {
|
|
14
|
+
start: number;
|
|
15
|
+
/** Exclusive: `text.slice(start, end)` is `@` + the mention's name. */
|
|
16
|
+
end: number;
|
|
17
|
+
mention: Mention;
|
|
18
|
+
}
|
|
19
|
+
export interface ComposerState {
|
|
20
|
+
text: string;
|
|
21
|
+
/** In text order, never overlapping. */
|
|
22
|
+
mentions: ComposerMention[];
|
|
23
|
+
}
|
|
24
|
+
export type ComposerSegment = {
|
|
25
|
+
kind: "text";
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "mention";
|
|
29
|
+
text: string;
|
|
30
|
+
mention: Mention;
|
|
31
|
+
};
|
|
32
|
+
export declare const EMPTY_COMPOSER: ComposerState;
|
|
33
|
+
/**
|
|
34
|
+
* The text after an edit, with its mentions moved. `caret` is where the caret is after the edit:
|
|
35
|
+
* what follows it is taken as unchanged, which places a typed or pasted run, or a deletion, exactly
|
|
36
|
+
* (a plain diff can't tell which of two equal letters was typed).
|
|
37
|
+
*/
|
|
38
|
+
export declare const applyEdit: (state: ComposerState, text: string, caret?: number) => ComposerState;
|
|
39
|
+
/** Picks a mention for the `@query` typed at `[queryStart, caret)`: `@Name ` in its place. */
|
|
40
|
+
export declare const insertMention: (state: ComposerState, queryStart: number, caret: number, mention: Mention) => {
|
|
41
|
+
state: ComposerState;
|
|
42
|
+
caret: number;
|
|
43
|
+
};
|
|
44
|
+
/** Backspace with the caret right after a mention: the whole mention goes. Null when it isn't one. */
|
|
45
|
+
export declare const deleteMentionBefore: (state: ComposerState, caret: number) => {
|
|
46
|
+
state: ComposerState;
|
|
47
|
+
caret: number;
|
|
48
|
+
} | null;
|
|
49
|
+
/** Whether the text mentions this agent. */
|
|
50
|
+
export declare const mentionsAgent: (state: ComposerState, agentId: string) => boolean;
|
|
51
|
+
/** Mentions an agent at the start of the text (the "Send to" line), unless the text already does. */
|
|
52
|
+
export declare const addAgentMention: (state: ComposerState, agent: {
|
|
53
|
+
agentId: string;
|
|
54
|
+
name: string;
|
|
55
|
+
}) => ComposerState;
|
|
56
|
+
/** Removes every mention of an agent, with the space after each. */
|
|
57
|
+
export declare const removeAgentMention: (state: ComposerState, agentId: string) => ComposerState;
|
|
58
|
+
/** The text in runs — plain text, and mentions — for the highlight drawn behind the text. */
|
|
59
|
+
export declare const composerSegments: (state: ComposerState) => ComposerSegment[];
|
|
60
|
+
/** The body to store: each mention as its token (`@[42]`, `@[agent:Name]`), the rest as typed. */
|
|
61
|
+
export declare const composerBody: (state: ComposerState) => string;
|
|
62
|
+
/** An agent the composer can mention: one of the app's floating-chat agents. */
|
|
63
|
+
export interface ComposerAgent {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
photoUrl?: string | null;
|
|
67
|
+
}
|
|
68
|
+
export type ComposerOption = Mention & {
|
|
69
|
+
photoUrl?: string | null;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* How well a name matches what was typed after `@`: 0 when the name starts with it, 1 when one of
|
|
73
|
+
* its words does, 2 for any other match (inside a word, an email). The menu lists better matches
|
|
74
|
+
* first, so "@ol" puts Olive before Marina Sokolov.
|
|
75
|
+
*/
|
|
76
|
+
export declare const mentionRank: (name: string, query: string) => number;
|
|
77
|
+
/**
|
|
78
|
+
* The `@` menu for what was typed: the app's agents first, then people (never yourself), each group
|
|
79
|
+
* ranked by `mentionRank` and capped (3 agents, 5 people). `separatorBefore` is the index of the
|
|
80
|
+
* first person when both groups have matches, where the menu draws its divider.
|
|
81
|
+
*/
|
|
82
|
+
export declare const mentionOptions: ({ people, agents, currentUserId, query, }: {
|
|
83
|
+
people: readonly MentionableUserDto[];
|
|
84
|
+
agents: readonly ComposerAgent[];
|
|
85
|
+
currentUserId: number | null;
|
|
86
|
+
query: string;
|
|
87
|
+
}) => {
|
|
88
|
+
options: ComposerOption[];
|
|
89
|
+
separatorBefore: number | null;
|
|
90
|
+
};
|
|
91
|
+
export interface UseMentionComposerOptions {
|
|
92
|
+
people: readonly MentionableUserDto[];
|
|
93
|
+
agents: readonly ComposerAgent[];
|
|
94
|
+
/** Left out of the `@` menu: nobody mentions themselves. */
|
|
95
|
+
currentUserId: number | null;
|
|
96
|
+
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
97
|
+
}
|
|
98
|
+
export interface MentionComposer {
|
|
99
|
+
text: string;
|
|
100
|
+
segments: ComposerSegment[];
|
|
101
|
+
/** What to store; empty when there's nothing to send. */
|
|
102
|
+
body: string;
|
|
103
|
+
/** The `@` menu's options (agents, then people), while an `@query` is being typed. */
|
|
104
|
+
options: ComposerOption[];
|
|
105
|
+
/** The index of the first person when agents and people both match: draw a divider before it. */
|
|
106
|
+
separatorBefore: number | null;
|
|
107
|
+
highlighted: number;
|
|
108
|
+
setHighlighted: (index: number) => void;
|
|
109
|
+
/** Arrow keys in the menu. */
|
|
110
|
+
moveHighlight: (step: 1 | -1) => void;
|
|
111
|
+
pick: (option: ComposerOption) => void;
|
|
112
|
+
/** Esc in the menu. */
|
|
113
|
+
closeMenu: () => void;
|
|
114
|
+
isMenuOpen: boolean;
|
|
115
|
+
/** The textarea's change: its new value and caret. */
|
|
116
|
+
onChange: (text: string, caret: number) => void;
|
|
117
|
+
/**
|
|
118
|
+
* Backspace, before the browser acts on it: right after a mention, selects the whole mention so
|
|
119
|
+
* the key deletes it (undoably). The caller lets the key go on.
|
|
120
|
+
*/
|
|
121
|
+
onBackspace: (selectionStart: number, selectionEnd: number) => void;
|
|
122
|
+
/** The "Send to" line: the agent it shows — the first one mentioned, else the first agent. */
|
|
123
|
+
sendToAgent: ComposerAgent | null;
|
|
124
|
+
isSendingToAgent: boolean;
|
|
125
|
+
/** Checks or unchecks "Send to": mentions the agent, or removes its mentions. */
|
|
126
|
+
setSendingToAgent: (isOn: boolean) => void;
|
|
127
|
+
/** Swaps the "Send to" agent; a mention of the old one becomes one of the new one. */
|
|
128
|
+
chooseSendToAgent: (agentId: string) => void;
|
|
129
|
+
reset: () => void;
|
|
130
|
+
}
|
|
131
|
+
/** The composer's state and actions; see the module comment. */
|
|
132
|
+
export declare const useMentionComposer: ({ people, agents, currentUserId, textareaRef, }: UseMentionComposerOptions) => MentionComposer;
|
|
133
|
+
//# sourceMappingURL=composer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/comments/composer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { matchesMentionQuery, mentionQueryAt, mentionToken } from "./mentions.js";
|
|
3
|
+
export const EMPTY_COMPOSER = { text: "", mentions: [] };
|
|
4
|
+
const label = (mention) => `@${mention.name}`;
|
|
5
|
+
/** Keeps only the ranges that still read exactly `@Name`, in order. */
|
|
6
|
+
const valid = (text, mentions) => mentions
|
|
7
|
+
.filter((range) => range.start >= 0 && text.slice(range.start, range.end) === label(range.mention))
|
|
8
|
+
.sort((a, b) => a.start - b.start);
|
|
9
|
+
/**
|
|
10
|
+
* The text after an edit, with its mentions moved. `caret` is where the caret is after the edit:
|
|
11
|
+
* what follows it is taken as unchanged, which places a typed or pasted run, or a deletion, exactly
|
|
12
|
+
* (a plain diff can't tell which of two equal letters was typed).
|
|
13
|
+
*/
|
|
14
|
+
export const applyEdit = (state, text, caret = text.length) => {
|
|
15
|
+
const before = state.text;
|
|
16
|
+
const delta = text.length - before.length;
|
|
17
|
+
// What follows the caret is unchanged: the edited span ends there in the new text. When it isn't
|
|
18
|
+
// (an undo can leave the caret elsewhere), the longest common ending stands in.
|
|
19
|
+
const afterCaret = Math.max(0, Math.min(text.length - caret, before.length));
|
|
20
|
+
let suffix = afterCaret;
|
|
21
|
+
if (!before.endsWith(text.slice(text.length - afterCaret))) {
|
|
22
|
+
suffix = 0;
|
|
23
|
+
const limit = Math.min(before.length, text.length);
|
|
24
|
+
while (suffix < limit && before[before.length - 1 - suffix] === text[text.length - 1 - suffix]) {
|
|
25
|
+
suffix += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let prefix = 0;
|
|
29
|
+
const prefixLimit = Math.min(before.length - suffix, text.length - suffix);
|
|
30
|
+
while (prefix < prefixLimit && before[prefix] === text[prefix]) {
|
|
31
|
+
prefix += 1;
|
|
32
|
+
}
|
|
33
|
+
const oldEnd = before.length - suffix;
|
|
34
|
+
const mentions = state.mentions.flatMap((range) => {
|
|
35
|
+
if (range.end <= prefix) {
|
|
36
|
+
return [range];
|
|
37
|
+
}
|
|
38
|
+
if (range.start >= oldEnd && oldEnd >= prefix) {
|
|
39
|
+
return [{ ...range, start: range.start + delta, end: range.end + delta }];
|
|
40
|
+
}
|
|
41
|
+
// The edit touched it: it's plain text now.
|
|
42
|
+
return [];
|
|
43
|
+
});
|
|
44
|
+
return { text, mentions: valid(text, mentions) };
|
|
45
|
+
};
|
|
46
|
+
/** Replaces `[from, to)` with `insert`, moving the mentions after it. Mentions inside go. */
|
|
47
|
+
const replaceRange = (state, from, to, insert) => {
|
|
48
|
+
const delta = insert.length - (to - from);
|
|
49
|
+
const text = state.text.slice(0, from) + insert + state.text.slice(to);
|
|
50
|
+
const mentions = state.mentions.flatMap((range) => {
|
|
51
|
+
if (range.end <= from) {
|
|
52
|
+
return [range];
|
|
53
|
+
}
|
|
54
|
+
if (range.start >= to) {
|
|
55
|
+
return [{ ...range, start: range.start + delta, end: range.end + delta }];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
});
|
|
59
|
+
return { text, mentions: valid(text, mentions) };
|
|
60
|
+
};
|
|
61
|
+
/** Picks a mention for the `@query` typed at `[queryStart, caret)`: `@Name ` in its place. */
|
|
62
|
+
export const insertMention = (state, queryStart, caret, mention) => {
|
|
63
|
+
const inserted = `${label(mention)} `;
|
|
64
|
+
const next = replaceRange(state, queryStart, caret, inserted);
|
|
65
|
+
return {
|
|
66
|
+
state: {
|
|
67
|
+
text: next.text,
|
|
68
|
+
mentions: valid(next.text, [
|
|
69
|
+
...next.mentions,
|
|
70
|
+
{ start: queryStart, end: queryStart + label(mention).length, mention },
|
|
71
|
+
]),
|
|
72
|
+
},
|
|
73
|
+
caret: queryStart + inserted.length,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
/** Backspace with the caret right after a mention: the whole mention goes. Null when it isn't one. */
|
|
77
|
+
export const deleteMentionBefore = (state, caret) => {
|
|
78
|
+
const range = state.mentions.find((candidate) => candidate.end === caret);
|
|
79
|
+
if (!range) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return { state: replaceRange(state, range.start, range.end, ""), caret: range.start };
|
|
83
|
+
};
|
|
84
|
+
const sameAgent = (mention, agentId) => mention.kind === "agent" && mention.agentId === agentId;
|
|
85
|
+
/** Whether the text mentions this agent. */
|
|
86
|
+
export const mentionsAgent = (state, agentId) => state.mentions.some((range) => sameAgent(range.mention, agentId));
|
|
87
|
+
/** Mentions an agent at the start of the text (the "Send to" line), unless the text already does. */
|
|
88
|
+
export const addAgentMention = (state, agent) => {
|
|
89
|
+
if (mentionsAgent(state, agent.agentId)) {
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
const mention = { kind: "agent", agentId: agent.agentId, name: agent.name };
|
|
93
|
+
const next = replaceRange(state, 0, 0, `${label(mention)} `);
|
|
94
|
+
return {
|
|
95
|
+
text: next.text,
|
|
96
|
+
mentions: valid(next.text, [...next.mentions, { start: 0, end: label(mention).length, mention }]),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
/** Removes every mention of an agent, with the space after each. */
|
|
100
|
+
export const removeAgentMention = (state, agentId) => [...state.mentions]
|
|
101
|
+
.reverse()
|
|
102
|
+
.filter((range) => sameAgent(range.mention, agentId))
|
|
103
|
+
.reduce((current, range) => {
|
|
104
|
+
const end = current.text[range.end] === " " ? range.end + 1 : range.end;
|
|
105
|
+
return replaceRange(current, range.start, end, "");
|
|
106
|
+
}, state);
|
|
107
|
+
/** The text in runs — plain text, and mentions — for the highlight drawn behind the text. */
|
|
108
|
+
export const composerSegments = (state) => {
|
|
109
|
+
const segments = [];
|
|
110
|
+
let last = 0;
|
|
111
|
+
for (const range of state.mentions) {
|
|
112
|
+
if (range.start > last) {
|
|
113
|
+
segments.push({ kind: "text", text: state.text.slice(last, range.start) });
|
|
114
|
+
}
|
|
115
|
+
segments.push({ kind: "mention", text: state.text.slice(range.start, range.end), mention: range.mention });
|
|
116
|
+
last = range.end;
|
|
117
|
+
}
|
|
118
|
+
if (last < state.text.length) {
|
|
119
|
+
segments.push({ kind: "text", text: state.text.slice(last) });
|
|
120
|
+
}
|
|
121
|
+
return segments;
|
|
122
|
+
};
|
|
123
|
+
/** The body to store: each mention as its token (`@[42]`, `@[agent:Name]`), the rest as typed. */
|
|
124
|
+
export const composerBody = (state) => composerSegments(state)
|
|
125
|
+
.map((segment) => (segment.kind === "mention" ? mentionToken(segment.mention) : segment.text))
|
|
126
|
+
.join("")
|
|
127
|
+
.trim();
|
|
128
|
+
/**
|
|
129
|
+
* How well a name matches what was typed after `@`: 0 when the name starts with it, 1 when one of
|
|
130
|
+
* its words does, 2 for any other match (inside a word, an email). The menu lists better matches
|
|
131
|
+
* first, so "@ol" puts Olive before Marina Sokolov.
|
|
132
|
+
*/
|
|
133
|
+
export const mentionRank = (name, query) => {
|
|
134
|
+
const needle = query.trim().toLowerCase();
|
|
135
|
+
const haystack = name.toLowerCase();
|
|
136
|
+
if (!needle || haystack.startsWith(needle)) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
return haystack.split(/\s+/).some((word) => word.startsWith(needle)) ? 1 : 2;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* The `@` menu for what was typed: the app's agents first, then people (never yourself), each group
|
|
143
|
+
* ranked by `mentionRank` and capped (3 agents, 5 people). `separatorBefore` is the index of the
|
|
144
|
+
* first person when both groups have matches, where the menu draws its divider.
|
|
145
|
+
*/
|
|
146
|
+
export const mentionOptions = ({ people, agents, currentUserId, query, }) => {
|
|
147
|
+
const ranked = (items, cap) => items
|
|
148
|
+
.map((item, index) => ({ item, index, rank: mentionRank(item.name, query) }))
|
|
149
|
+
.sort((a, b) => a.rank - b.rank || a.index - b.index)
|
|
150
|
+
.slice(0, cap)
|
|
151
|
+
.map(({ item }) => item);
|
|
152
|
+
const matchingAgents = ranked(agents.filter((agent) => matchesMentionQuery({ name: agent.name }, query)), 3).map((agent) => ({ kind: "agent", agentId: agent.id, name: agent.name, photoUrl: agent.photoUrl }));
|
|
153
|
+
const matchingPeople = ranked(people.filter((person) => person.userId !== currentUserId && matchesMentionQuery(person, query)), 5).map((person) => ({
|
|
154
|
+
kind: "user",
|
|
155
|
+
userId: person.userId,
|
|
156
|
+
name: person.name,
|
|
157
|
+
photoUrl: person.profileImageUrl,
|
|
158
|
+
}));
|
|
159
|
+
return {
|
|
160
|
+
options: [...matchingAgents, ...matchingPeople],
|
|
161
|
+
separatorBefore: matchingAgents.length > 0 && matchingPeople.length > 0 ? matchingAgents.length : null,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
/** The composer's state and actions; see the module comment. */
|
|
165
|
+
export const useMentionComposer = ({ people, agents, currentUserId, textareaRef, }) => {
|
|
166
|
+
const [state, setState] = useState(EMPTY_COMPOSER);
|
|
167
|
+
const [query, setQuery] = useState(null);
|
|
168
|
+
const [highlighted, setHighlighted] = useState(0);
|
|
169
|
+
const [chosenAgentId, setChosenAgentId] = useState(null);
|
|
170
|
+
const latest = useRef(state);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
latest.current = state;
|
|
173
|
+
});
|
|
174
|
+
// After a change the composer made itself (a pick, a whole-mention delete), the caret goes where
|
|
175
|
+
// that change ends, once the textarea shows the new text.
|
|
176
|
+
const placeCaret = useCallback((caret) => requestAnimationFrame(() => {
|
|
177
|
+
const textarea = textareaRef.current;
|
|
178
|
+
if (textarea) {
|
|
179
|
+
textarea.focus();
|
|
180
|
+
textarea.setSelectionRange(caret, caret);
|
|
181
|
+
}
|
|
182
|
+
}), [textareaRef]);
|
|
183
|
+
const { options, separatorBefore } = useMemo(() => query
|
|
184
|
+
? mentionOptions({ people, agents, currentUserId, query: query.query })
|
|
185
|
+
: { options: [], separatorBefore: null }, [agents, currentUserId, people, query]);
|
|
186
|
+
const onChange = useCallback((text, caret) => {
|
|
187
|
+
setState((current) => applyEdit(current, text, caret));
|
|
188
|
+
setQuery(mentionQueryAt(text, caret));
|
|
189
|
+
setHighlighted(0);
|
|
190
|
+
}, []);
|
|
191
|
+
const pick = useCallback((option) => {
|
|
192
|
+
if (!query) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const caret = textareaRef.current?.selectionStart ?? latest.current.text.length;
|
|
196
|
+
const mention = option.kind === "user"
|
|
197
|
+
? { kind: "user", userId: option.userId, name: option.name }
|
|
198
|
+
: { kind: "agent", agentId: option.agentId, name: option.name };
|
|
199
|
+
const next = insertMention(latest.current, query.start, Math.max(caret, query.start), mention);
|
|
200
|
+
setState(next.state);
|
|
201
|
+
setQuery(null);
|
|
202
|
+
placeCaret(next.caret);
|
|
203
|
+
}, [placeCaret, query, textareaRef]);
|
|
204
|
+
const onBackspace = useCallback((selectionStart, selectionEnd) => {
|
|
205
|
+
const textarea = textareaRef.current;
|
|
206
|
+
const current = latest.current;
|
|
207
|
+
const range = current.mentions.find((candidate) => candidate.end === selectionStart);
|
|
208
|
+
if (!textarea || selectionStart !== selectionEnd || !range) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Selects the whole mention (with the space after it, when that would leave two) and lets
|
|
212
|
+
// the key delete it: the browser's own edit, so Undo brings it back.
|
|
213
|
+
const before = current.text[range.start - 1];
|
|
214
|
+
const end = current.text[range.end] === " " && (range.start === 0 || before === " " || before === "\n")
|
|
215
|
+
? range.end + 1
|
|
216
|
+
: range.end;
|
|
217
|
+
textarea.setSelectionRange(range.start, end);
|
|
218
|
+
}, [textareaRef]);
|
|
219
|
+
const firstMentionedAgent = state.mentions.find((range) => range.mention.kind === "agent")?.mention;
|
|
220
|
+
const sendToAgent = (firstMentionedAgent?.kind === "agent"
|
|
221
|
+
? agents.find((agent) => agent.id === firstMentionedAgent.agentId)
|
|
222
|
+
: undefined) ??
|
|
223
|
+
agents.find((agent) => agent.id === chosenAgentId) ??
|
|
224
|
+
agents[0] ??
|
|
225
|
+
null;
|
|
226
|
+
const isSendingToAgent = !!sendToAgent && mentionsAgent(state, sendToAgent.id);
|
|
227
|
+
const setSendingToAgent = useCallback((isOn) => {
|
|
228
|
+
if (!sendToAgent) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const current = latest.current;
|
|
232
|
+
const next = isOn
|
|
233
|
+
? addAgentMention(current, { agentId: sendToAgent.id, name: sendToAgent.name })
|
|
234
|
+
: removeAgentMention(current, sendToAgent.id);
|
|
235
|
+
setState(next);
|
|
236
|
+
// Back to typing: right after the mention and its space when it was added, else the end.
|
|
237
|
+
placeCaret(isOn ? sendToAgent.name.length + 2 : next.text.length);
|
|
238
|
+
}, [placeCaret, sendToAgent]);
|
|
239
|
+
const chooseSendToAgent = useCallback((agentId) => {
|
|
240
|
+
const next = agents.find((agent) => agent.id === agentId);
|
|
241
|
+
if (!next) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
setChosenAgentId(agentId);
|
|
245
|
+
if (sendToAgent && isSendingToAgent && sendToAgent.id !== agentId) {
|
|
246
|
+
setState((current) => addAgentMention(removeAgentMention(current, sendToAgent.id), { agentId: next.id, name: next.name }));
|
|
247
|
+
}
|
|
248
|
+
}, [agents, isSendingToAgent, sendToAgent]);
|
|
249
|
+
const reset = useCallback(() => {
|
|
250
|
+
setState(EMPTY_COMPOSER);
|
|
251
|
+
setQuery(null);
|
|
252
|
+
}, []);
|
|
253
|
+
return {
|
|
254
|
+
text: state.text,
|
|
255
|
+
segments: composerSegments(state),
|
|
256
|
+
body: composerBody(state),
|
|
257
|
+
options,
|
|
258
|
+
highlighted,
|
|
259
|
+
setHighlighted,
|
|
260
|
+
moveHighlight: (step) => setHighlighted((current) => (options.length ? (current + step + options.length) % options.length : 0)),
|
|
261
|
+
pick,
|
|
262
|
+
closeMenu: () => setQuery(null),
|
|
263
|
+
separatorBefore,
|
|
264
|
+
isMenuOpen: options.length > 0,
|
|
265
|
+
onChange,
|
|
266
|
+
onBackspace,
|
|
267
|
+
sendToAgent,
|
|
268
|
+
isSendingToAgent,
|
|
269
|
+
setSendingToAgent,
|
|
270
|
+
chooseSendToAgent,
|
|
271
|
+
reset,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { ITEM_ATTRIBUTE, FIELD_ATTRIBUTE, PICK_IGNORE_ATTRIBUTE, anchorQuote, anchorComponentName, type AnchorState, } from "./anchor.js";
|
|
3
|
+
export { initials, type Mention, type MentionNames } from "./mentions.js";
|
|
4
|
+
export { toRect, type Rect } from "./layout.js";
|
|
5
|
+
export { COMMENTS_LAYER_ATTRIBUTE, pinPoint, groupPins, useCommentPins, type PinPoint, type PinGroup, type ThreadLayout, } from "./pins.js";
|
|
6
|
+
export { usePagePick, type PagePick, type PagePickHover, type UsePagePickOptions } from "./page-pick.js";
|
|
7
|
+
export { createCommentsApi, type CommentsApi, type CreateCommentsApiOptions } from "./api.js";
|
|
8
|
+
export { setCommentMode, onCommentModeExit, setCommentOptions, onCommentModeRequest, openCommentThread, canOpenCommentThread, useCommentsSurface, type CommentThreadOpenRequest, type CommentOptions, type UseCommentsSurfaceOptions, } from "./comment-mode.js";
|
|
9
|
+
export { commentPingsFor, useCommentThreads, type CommentThreadsStore, type CommentThreadsSnapshot, type CommentThreadLoadState, type SubscribeToCommentPings, } from "./threads-store.js";
|
|
10
|
+
export { useCommentAgents, useRegisterCommentAgents, type CommentAgent, type CommentAgentsSnapshot, type OpenCommentAgentThread, } from "./comment-agents.js";
|
|
11
|
+
export { pagePickComposerMeta, parsePagePickPart, pagePickOf } from "./pick-message.js";
|
|
12
|
+
export { useMentionComposer, type ComposerAgent, type ComposerOption, type ComposerSegment, type MentionComposer, type UseMentionComposerOptions, } from "./composer.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/comments/index.ts"],"names":[],"mappings":"AAMA,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,mBAAmB,EACnB,KAAK,WAAW,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,QAAQ,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACL,wBAAwB,EACxB,QAAQ,EACR,SAAS,EACT,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,YAAY,GAClB,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtG,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,KAAK,wBAAwB,EAAE,MAAM,OAAO,CAAC;AAC3F,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,yBAAyB,GAC/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EACL,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,yBAAyB,GAC/B,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// In-app comments, headless: what the registry `comments` item and the agent chat's pointer are
|
|
2
|
+
// built on. Registry files are copied into each app and never update on their own, while this
|
|
3
|
+
// module is shared at runtime (a UMD external app-render provides), so its exports only grow:
|
|
4
|
+
// never remove, rename or change the meaning of one. It exports what the registry items and
|
|
5
|
+
// app-render use (and the types their signatures show); add an export when an item needs one.
|
|
6
|
+
export * from "./types.js";
|
|
7
|
+
export { ITEM_ATTRIBUTE, FIELD_ATTRIBUTE, PICK_IGNORE_ATTRIBUTE, anchorQuote, anchorComponentName, } from "./anchor.js";
|
|
8
|
+
export { initials } from "./mentions.js";
|
|
9
|
+
export { toRect } from "./layout.js";
|
|
10
|
+
export { COMMENTS_LAYER_ATTRIBUTE, pinPoint, groupPins, useCommentPins, } from "./pins.js";
|
|
11
|
+
export { usePagePick } from "./page-pick.js";
|
|
12
|
+
export { createCommentsApi } from "./api.js";
|
|
13
|
+
export { setCommentMode, onCommentModeExit, setCommentOptions, onCommentModeRequest, openCommentThread, canOpenCommentThread, useCommentsSurface, } from "./comment-mode.js";
|
|
14
|
+
export { commentPingsFor, useCommentThreads, } from "./threads-store.js";
|
|
15
|
+
export { useCommentAgents, useRegisterCommentAgents, } from "./comment-agents.js";
|
|
16
|
+
export { pagePickComposerMeta, parsePagePickPart, pagePickOf } from "./pick-message.js";
|
|
17
|
+
export { useMentionComposer, } from "./composer.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A counter that moves at most once per animation frame whenever the page may have moved: a
|
|
3
|
+
* scroll anywhere (capture phase, so scroll containers count), a window resize, a change to the
|
|
4
|
+
* app's DOM, or a reflow of the body (a panel opening beside the app moves everything with no DOM
|
|
5
|
+
* change and no window resize). Changes inside the comments' own UI don't count, so drawing pins
|
|
6
|
+
* never feeds itself.
|
|
7
|
+
*
|
|
8
|
+
* Positions are read from the DOM, which is not a React input: pass the tick to whatever reads them
|
|
9
|
+
* so it recomputes.
|
|
10
|
+
*/
|
|
11
|
+
export declare const usePageLayoutTick: (isActive: boolean) => number;
|
|
12
|
+
export interface Rect {
|
|
13
|
+
top: number;
|
|
14
|
+
left: number;
|
|
15
|
+
right: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const toRect: (rect: Pick<DOMRect, "top" | "left" | "right" | "bottom">) => Rect;
|
|
19
|
+
/** The overlap of two rects, or null when they don't overlap. */
|
|
20
|
+
export declare const intersectRects: (a: Rect, b: Rect) => Rect | null;
|
|
21
|
+
/** The part of `rect` inside every clip (the viewport, each scrolling ancestor), or null. */
|
|
22
|
+
export declare const visiblePart: (rect: Rect, clips: readonly Rect[]) => Rect | null;
|
|
23
|
+
/** The boxes that clip `element`: the viewport and every ancestor that clips its overflow. */
|
|
24
|
+
export declare const clipRectsOf: (element: Element) => Rect[];
|
|
25
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/comments/layout.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,OAAO,KAAG,MAkCrD,CAAC;AAEF,MAAM,WAAW,IAAI;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,MAAM,GAAI,MAAM,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC,KAAG,IAKhF,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,GAAI,GAAG,IAAI,EAAE,GAAG,IAAI,KAAG,IAAI,GAAG,IAMxD,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,WAAW,GAAI,MAAM,IAAI,EAAE,OAAO,SAAS,IAAI,EAAE,KAAG,IAAI,GAAG,IASvE,CAAC;AAKF,8FAA8F;AAC9F,eAAO,MAAM,WAAW,GAAI,SAAS,OAAO,KAAG,IAAI,EAYlD,CAAC"}
|