@ykurilov/agent-chat-react 0.0.0-stage → 0.55.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 +1670 -0
- package/LICENSE +21 -0
- package/README.md +128 -2
- package/dist/chunk-JZQ7ZOJI.js +6959 -0
- package/dist/chunk-JZQ7ZOJI.js.map +1 -0
- package/dist/index.js +501 -0
- package/dist/index.js.map +1 -0
- package/dist/panel.js +399 -0
- package/dist/panel.js.map +1 -0
- package/dist/types/agent-chat-extensions.d.ts +76 -0
- package/dist/types/agent-chat.d.ts +103 -0
- package/dist/types/agent-panel/agent-panel-hooks.d.ts +14 -0
- package/dist/types/agent-panel/agent-panel-provider.d.ts +24 -0
- package/dist/types/agent-panel/agent-panel-skin.d.ts +41 -0
- package/dist/types/agent-panel/agent-panel.d.ts +13 -0
- package/dist/types/agent-panel/index.d.ts +8 -0
- package/dist/types/agent-panel/panel-context.d.ts +46 -0
- package/dist/types/agent-panel/panel-state.d.ts +12 -0
- package/dist/types/answer-editing/answer-editing.d.ts +25 -0
- package/dist/types/app-bridge/app-bridge.d.ts +50 -0
- package/dist/types/app-bridge/browser-calls.d.ts +24 -0
- package/dist/types/app-bridge/create-app-bridge.d.ts +35 -0
- package/dist/types/app-bridge/remembered-ids.d.ts +6 -0
- package/dist/types/app-bridge/tab-id.d.ts +10 -0
- package/dist/types/app-events.d.ts +39 -0
- package/dist/types/app-refusal.d.ts +21 -0
- package/dist/types/attachments-contract.d.ts +82 -0
- package/dist/types/background-tasks/use-background-tasks.d.ts +53 -0
- package/dist/types/chat-access.d.ts +33 -0
- package/dist/types/chat-list/active-chat-memory.d.ts +5 -0
- package/dist/types/chat-list/chat-list-client.d.ts +35 -0
- package/dist/types/chat-list/chat-list-skin.d.ts +63 -0
- package/dist/types/chat-list/chat-workspace.d.ts +17 -0
- package/dist/types/chat-list/use-chat-list-view.d.ts +12 -0
- package/dist/types/chat-list/use-chat-list.d.ts +47 -0
- package/dist/types/chat-list/use-workspace-list-sync.d.ts +7 -0
- package/dist/types/chat-state/chat-state.d.ts +27 -0
- package/dist/types/chat-types.d.ts +85 -0
- package/dist/types/chat-watch/chat-watch.d.ts +57 -0
- package/dist/types/close-stopped-tool-calls.d.ts +8 -0
- package/dist/types/composer-actions.d.ts +29 -0
- package/dist/types/diagrams/diagram-renders.d.ts +16 -0
- package/dist/types/diagrams/mermaid-blocks.d.ts +18 -0
- package/dist/types/diagrams/unique-svg-ids.d.ts +6 -0
- package/dist/types/diagrams/use-diagram-blocks.d.ts +25 -0
- package/dist/types/diagrams/use-diagram-colors.d.ts +8 -0
- package/dist/types/dictation-field.d.ts +19 -0
- package/dist/types/expanded-parts.d.ts +11 -0
- package/dist/types/feed-announcement.d.ts +7 -0
- package/dist/types/helpers/helper-call.d.ts +57 -0
- package/dist/types/index.d.ts +53 -0
- package/dist/types/labels.d.ts +468 -0
- package/dist/types/live-demos/answer-fences.d.ts +25 -0
- package/dist/types/live-demos/live-demo-document.d.ts +42 -0
- package/dist/types/live-demos/live-demo-frame.d.ts +14 -0
- package/dist/types/live-demos/use-live-demo-blocks.d.ts +18 -0
- package/dist/types/liveness/connection-errors.d.ts +15 -0
- package/dist/types/liveness/reconnect-attempts.d.ts +14 -0
- package/dist/types/liveness/request-deadline.d.ts +16 -0
- package/dist/types/liveness/silence-watchdog.d.ts +21 -0
- package/dist/types/media-viewer/answer-file-images.d.ts +18 -0
- package/dist/types/media-viewer/collect-viewer-items.d.ts +16 -0
- package/dist/types/media-viewer/generated-images.d.ts +18 -0
- package/dist/types/media-viewer/media-viewer-types.d.ts +17 -0
- package/dist/types/media-viewer/open-attachments.d.ts +9 -0
- package/dist/types/media-viewer/use-media-viewer.d.ts +23 -0
- package/dist/types/memory/agent-memory.d.ts +13 -0
- package/dist/types/memory/memory-client.d.ts +19 -0
- package/dist/types/memory/memory-skin.d.ts +103 -0
- package/dist/types/memory/memory-tree.d.ts +17 -0
- package/dist/types/memory/memory-types.d.ts +77 -0
- package/dist/types/memory/use-memory-notes.d.ts +23 -0
- package/dist/types/message-actions.d.ts +45 -0
- package/dist/types/message-feed.d.ts +27 -0
- package/dist/types/notices/agent-notices.d.ts +40 -0
- package/dist/types/pause.d.ts +2 -0
- package/dist/types/server-refusal.d.ts +16 -0
- package/dist/types/skin-contract.d.ts +673 -0
- package/dist/types/spending-status.d.ts +76 -0
- package/dist/types/steering/corrections.d.ts +19 -0
- package/dist/types/step-reset.d.ts +15 -0
- package/dist/types/subject-chat/subject-chat-loading.d.ts +8 -0
- package/dist/types/subject-chat/use-subject-chat-id.d.ts +21 -0
- package/dist/types/thinking-status/thinking-status.d.ts +33 -0
- package/dist/types/turn-finished.d.ts +14 -0
- package/dist/types/use-agent-chat.d.ts +228 -0
- package/dist/types/use-model-choice.d.ts +31 -0
- package/dist/types/use-voice-dialog.d.ts +18 -0
- package/dist/types/use-voice-dictation.d.ts +17 -0
- package/dist/types/voice-dialog.d.ts +103 -0
- package/dist/types/voice-input.d.ts +47 -0
- package/dist/types/voice-tags.d.ts +18 -0
- package/dist/types/voice-turn.d.ts +11 -0
- package/dist/types/widgets/custom-widget-guard.d.ts +21 -0
- package/dist/types/widgets/form-check.d.ts +29 -0
- package/dist/types/widgets/suggested-replies.d.ts +6 -0
- package/dist/types/widgets/use-widget-answer.d.ts +54 -0
- package/dist/types/widgets/widget-answer-keys.d.ts +14 -0
- package/dist/types/widgets/widget-messages.d.ts +14 -0
- package/dist/types/widgets/widget-part.d.ts +17 -0
- package/dist/types/widgets/widget-view.d.ts +50 -0
- package/dist/types/without-markdown.d.ts +1 -0
- package/dist/types/work-view/step-phrases.d.ts +37 -0
- package/dist/types/work-view/work-layout.d.ts +57 -0
- package/dist/types/work-view/work-steps-part.d.ts +15 -0
- package/package.json +53 -3
package/dist/panel.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentChat,
|
|
3
|
+
activeChatTitle,
|
|
4
|
+
rememberedIds,
|
|
5
|
+
useChatLabels,
|
|
6
|
+
useChatList,
|
|
7
|
+
useChatListView,
|
|
8
|
+
useWorkspaceListSync
|
|
9
|
+
} from "./chunk-JZQ7ZOJI.js";
|
|
10
|
+
|
|
11
|
+
// src/agent-panel/agent-panel-provider.tsx
|
|
12
|
+
import { useCallback as useCallback2, useMemo, useRef, useState, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
13
|
+
|
|
14
|
+
// src/app-bridge/tab-id.ts
|
|
15
|
+
var TAB_ID_KEY = "agent-chat:tab-id";
|
|
16
|
+
var tabOpenKey = (tabId) => `agent-chat:tab-open:${tabId}`;
|
|
17
|
+
var pageTabId = null;
|
|
18
|
+
var listening = false;
|
|
19
|
+
function readTabId() {
|
|
20
|
+
try {
|
|
21
|
+
const stored = sessionStorage.getItem(TAB_ID_KEY);
|
|
22
|
+
if (stored !== null && stored !== "" && (stored === pageTabId || !openElsewhere(stored))) return claim(stored);
|
|
23
|
+
const created = crypto.randomUUID();
|
|
24
|
+
sessionStorage.setItem(TAB_ID_KEY, created);
|
|
25
|
+
return claim(created);
|
|
26
|
+
} catch {
|
|
27
|
+
pageTabId ??= crypto.randomUUID();
|
|
28
|
+
return pageTabId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function openElsewhere(tabId) {
|
|
32
|
+
try {
|
|
33
|
+
return localStorage.getItem(tabOpenKey(tabId)) !== null;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function claim(tabId) {
|
|
39
|
+
if (pageTabId !== tabId) {
|
|
40
|
+
markOpen(false);
|
|
41
|
+
pageTabId = tabId;
|
|
42
|
+
markOpen(true);
|
|
43
|
+
if (!listening && typeof addEventListener === "function") {
|
|
44
|
+
listening = true;
|
|
45
|
+
addEventListener("pagehide", () => markOpen(false));
|
|
46
|
+
addEventListener("pageshow", () => markOpen(true));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tabId;
|
|
50
|
+
}
|
|
51
|
+
function markOpen(open) {
|
|
52
|
+
if (pageTabId === null) return;
|
|
53
|
+
try {
|
|
54
|
+
if (open) localStorage.setItem(tabOpenKey(pageTabId), "1");
|
|
55
|
+
else localStorage.removeItem(tabOpenKey(pageTabId));
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/app-bridge/create-app-bridge.ts
|
|
61
|
+
var APP_CHANGES_DELIVERED_KEY = "agent-chat:app-changes-delivered";
|
|
62
|
+
var deliveredChanges = rememberedIds(APP_CHANGES_DELIVERED_KEY, 500);
|
|
63
|
+
function createAppBridge(readOptions) {
|
|
64
|
+
const places = /* @__PURE__ */ new Map();
|
|
65
|
+
let ownersCreated = 0;
|
|
66
|
+
const subscriptions = /* @__PURE__ */ new Set();
|
|
67
|
+
const currentPlace = () => {
|
|
68
|
+
let current = null;
|
|
69
|
+
for (const [owner, place] of places) if (current === null || owner.order > current.owner.order) current = { owner, place };
|
|
70
|
+
return current?.place ?? null;
|
|
71
|
+
};
|
|
72
|
+
const bridge = {
|
|
73
|
+
tabId: readTabId(),
|
|
74
|
+
currentPlace,
|
|
75
|
+
dataChanged(change) {
|
|
76
|
+
if (!deliveredChanges.claim(change.id)) return;
|
|
77
|
+
const { onDataChanged } = readOptions();
|
|
78
|
+
if (onDataChanged) deliver(() => onDataChanged(change));
|
|
79
|
+
for (const subscription of [...subscriptions]) {
|
|
80
|
+
if (subscription.tags.includes(change.tag)) deliver(() => subscription.handler(change));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async browserCall(call, options) {
|
|
84
|
+
const { browserTools, toolUnavailableMessage } = readOptions();
|
|
85
|
+
const run = browserTools && Object.hasOwn(browserTools, call.toolName) ? browserTools[call.toolName] : void 0;
|
|
86
|
+
if (!run) return { error: toolUnavailableMessage };
|
|
87
|
+
try {
|
|
88
|
+
const output = await run(call.input, { signal: options?.signal ?? new AbortController().signal });
|
|
89
|
+
return { output };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
bridge,
|
|
97
|
+
createPlaceOwner: () => ({ order: ++ownersCreated }),
|
|
98
|
+
setPlace(owner, place) {
|
|
99
|
+
if (place === null) places.delete(owner);
|
|
100
|
+
else places.set(owner, place);
|
|
101
|
+
},
|
|
102
|
+
removePlace: (owner) => void places.delete(owner),
|
|
103
|
+
subscribe(tags, handler) {
|
|
104
|
+
const subscription = { tags, handler };
|
|
105
|
+
subscriptions.add(subscription);
|
|
106
|
+
return () => void subscriptions.delete(subscription);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function deliver(notify) {
|
|
111
|
+
try {
|
|
112
|
+
notify();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
115
|
+
else console.error(error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/agent-panel/panel-context.ts
|
|
120
|
+
import { createContext, useContext } from "react";
|
|
121
|
+
var AppBridgeContext = createContext(null);
|
|
122
|
+
var PanelControlsContext = createContext(null);
|
|
123
|
+
var PanelViewContext = createContext(null);
|
|
124
|
+
function required(value, user) {
|
|
125
|
+
if (value === null) throw new Error(`${user} needs AgentPanelProvider above it (import it from "@ykurilov/agent-chat-react/panel")`);
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
function useAppBridgeControl(user = "useAppBridgeControl") {
|
|
129
|
+
return required(useContext(AppBridgeContext), user);
|
|
130
|
+
}
|
|
131
|
+
function usePanelControls(user) {
|
|
132
|
+
return required(useContext(PanelControlsContext), user);
|
|
133
|
+
}
|
|
134
|
+
function usePanelView(user) {
|
|
135
|
+
return required(useContext(PanelViewContext), user);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/agent-panel/panel-state.ts
|
|
139
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
140
|
+
var panelOpenKey = (api, userKey) => `agent-chat:panel:${api}:${userKey}`;
|
|
141
|
+
var panelChatKey = (api, userKey) => `agent-chat:panel-chat:${api}:${userKey}`;
|
|
142
|
+
var unsaved = /* @__PURE__ */ new Map();
|
|
143
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
144
|
+
var subscribe = (listener) => {
|
|
145
|
+
listeners.add(listener);
|
|
146
|
+
return () => void listeners.delete(listener);
|
|
147
|
+
};
|
|
148
|
+
function readPanelOpen(api, userKey) {
|
|
149
|
+
const key = panelOpenKey(api, userKey);
|
|
150
|
+
const kept = unsaved.get(key);
|
|
151
|
+
if (kept !== void 0) return kept;
|
|
152
|
+
try {
|
|
153
|
+
return parseOpen(localStorage.getItem(key));
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function rememberPanelOpen(api, userKey, open) {
|
|
159
|
+
const key = panelOpenKey(api, userKey);
|
|
160
|
+
try {
|
|
161
|
+
localStorage.setItem(key, JSON.stringify({ open }));
|
|
162
|
+
unsaved.delete(key);
|
|
163
|
+
} catch {
|
|
164
|
+
unsaved.set(key, open);
|
|
165
|
+
}
|
|
166
|
+
for (const listener of [...listeners]) listener();
|
|
167
|
+
}
|
|
168
|
+
function usePanelOpen(api, userKey) {
|
|
169
|
+
const open = useSyncExternalStore(subscribe, () => readPanelOpen(api, userKey), () => false);
|
|
170
|
+
const setOpen = useCallback((next) => rememberPanelOpen(api, userKey, next), [api, userKey]);
|
|
171
|
+
return [open, setOpen];
|
|
172
|
+
}
|
|
173
|
+
function parseOpen(stored) {
|
|
174
|
+
try {
|
|
175
|
+
const value = JSON.parse(stored ?? "null");
|
|
176
|
+
return typeof value === "object" && value !== null && value.open === true;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/agent-panel/agent-panel-provider.tsx
|
|
183
|
+
import { jsx } from "react/jsx-runtime";
|
|
184
|
+
var DEFAULT_WIDTH = 420;
|
|
185
|
+
var NARROW_SCREEN = "(max-width: 640px)";
|
|
186
|
+
function AgentPanelProvider({ api, userKey, browserTools, mode = "overlay", width = DEFAULT_WIDTH, onDataChanged, fetch, children }) {
|
|
187
|
+
const labels = useChatLabels();
|
|
188
|
+
const bridgeOptions = useRef({ toolUnavailableMessage: labels.agentPanelToolUnavailable });
|
|
189
|
+
bridgeOptions.current = {
|
|
190
|
+
toolUnavailableMessage: labels.agentPanelToolUnavailable,
|
|
191
|
+
...browserTools && { browserTools },
|
|
192
|
+
...onDataChanged && { onDataChanged }
|
|
193
|
+
};
|
|
194
|
+
const [bridgeControl] = useState(() => createAppBridge(() => bridgeOptions.current));
|
|
195
|
+
const [open, setOpen] = usePanelOpen(api, userKey);
|
|
196
|
+
const list = useChatList({ api, rememberKey: panelChatKey(api, userKey), ...fetch && { fetch } });
|
|
197
|
+
const [listOpen, setListOpen] = useState(false);
|
|
198
|
+
const [busy, setBusy] = useState(false);
|
|
199
|
+
const [draft, setDraft] = useState(null);
|
|
200
|
+
const draftCount = useRef(0);
|
|
201
|
+
const launcherRef = useRef(null);
|
|
202
|
+
const { activeChatId } = list;
|
|
203
|
+
const [sessionUser, setSessionUser] = useState(userKey);
|
|
204
|
+
if (sessionUser !== userKey) {
|
|
205
|
+
setSessionUser(userKey);
|
|
206
|
+
setListOpen(false);
|
|
207
|
+
setDraft(null);
|
|
208
|
+
setBusy(false);
|
|
209
|
+
}
|
|
210
|
+
const currentUser = useRef(userKey);
|
|
211
|
+
currentUser.current = userKey;
|
|
212
|
+
const userBridge = useMemo(
|
|
213
|
+
() => bridgeForUser(bridgeControl.bridge, () => currentUser.current === userKey, () => bridgeOptions.current.toolUnavailableMessage),
|
|
214
|
+
[bridgeControl, userKey]
|
|
215
|
+
);
|
|
216
|
+
if (draft !== null) {
|
|
217
|
+
if (draft.chatId === null) {
|
|
218
|
+
if (activeChatId !== null) setDraft({ ...draft, chatId: activeChatId });
|
|
219
|
+
} else if (draft.chatId !== activeChatId) {
|
|
220
|
+
setDraft(null);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const boundDraft = draft !== null && draft.chatId !== null && draft.chatId === activeChatId ? draft : null;
|
|
224
|
+
const chatBusy = activeChatId !== null && busy;
|
|
225
|
+
useWorkspaceListSync(list, chatBusy);
|
|
226
|
+
const narrow = useSyncExternalStore2(subscribeToScreen, readNarrowScreen, () => false);
|
|
227
|
+
const pageInset = mode === "push" && open && !narrow ? width : 0;
|
|
228
|
+
const { select, create, deleteRequest, refresh } = list;
|
|
229
|
+
const showList = useCallback2(
|
|
230
|
+
(show) => {
|
|
231
|
+
setListOpen(show);
|
|
232
|
+
if (show) void refresh();
|
|
233
|
+
},
|
|
234
|
+
[refresh]
|
|
235
|
+
);
|
|
236
|
+
const toggle = useCallback2(() => setOpen(!open), [open, setOpen]);
|
|
237
|
+
const openChat = useCallback2(
|
|
238
|
+
(chatId) => {
|
|
239
|
+
setOpen(true);
|
|
240
|
+
setListOpen(false);
|
|
241
|
+
select(chatId);
|
|
242
|
+
},
|
|
243
|
+
[select, setOpen]
|
|
244
|
+
);
|
|
245
|
+
const openNewChat = useCallback2(
|
|
246
|
+
async (options) => {
|
|
247
|
+
setOpen(true);
|
|
248
|
+
setListOpen(false);
|
|
249
|
+
const created = await create();
|
|
250
|
+
if (currentUser.current !== userKey) return false;
|
|
251
|
+
if (!created) {
|
|
252
|
+
showList(true);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (options?.draft !== void 0) {
|
|
256
|
+
draftCount.current += 1;
|
|
257
|
+
setDraft({ text: options.draft, nonce: draftCount.current, chatId: null });
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
},
|
|
261
|
+
[create, setOpen, showList, userKey]
|
|
262
|
+
);
|
|
263
|
+
const closeList = useCallback2(() => setListOpen(false), []);
|
|
264
|
+
const escape = useCallback2(() => {
|
|
265
|
+
if (deleteRequest !== null) return;
|
|
266
|
+
if (listOpen) setListOpen(false);
|
|
267
|
+
else setOpen(false);
|
|
268
|
+
}, [deleteRequest, listOpen, setOpen]);
|
|
269
|
+
const controls = useMemo(
|
|
270
|
+
() => ({ open, setOpen, toggle, openChat, openNewChat, pageInset, mode, width, busy: chatBusy }),
|
|
271
|
+
[chatBusy, mode, open, openChat, openNewChat, pageInset, setOpen, toggle, width]
|
|
272
|
+
);
|
|
273
|
+
const view = {
|
|
274
|
+
api,
|
|
275
|
+
controls,
|
|
276
|
+
list,
|
|
277
|
+
// Список не загрузился и открыть нечего: показать его с ошибкой и «Повторить» сразу.
|
|
278
|
+
listOpen: listOpen || activeChatId === null && list.status === "error",
|
|
279
|
+
setListOpen: showList,
|
|
280
|
+
closeList,
|
|
281
|
+
escape,
|
|
282
|
+
chatKey: boundDraft ? `draft:${boundDraft.nonce}:${activeChatId}` : `chat:${activeChatId}`,
|
|
283
|
+
initialDraft: boundDraft?.text,
|
|
284
|
+
setBusy,
|
|
285
|
+
launcherRef,
|
|
286
|
+
bridge: userBridge
|
|
287
|
+
};
|
|
288
|
+
return /* @__PURE__ */ jsx(AppBridgeContext.Provider, { value: bridgeControl, children: /* @__PURE__ */ jsx(PanelControlsContext.Provider, { value: controls, children: /* @__PURE__ */ jsx(PanelViewContext.Provider, { value: view, children }) }) });
|
|
289
|
+
}
|
|
290
|
+
function bridgeForUser(bridge, isCurrent, unavailableMessage) {
|
|
291
|
+
return {
|
|
292
|
+
get tabId() {
|
|
293
|
+
return bridge.tabId;
|
|
294
|
+
},
|
|
295
|
+
currentPlace: () => bridge.currentPlace(),
|
|
296
|
+
dataChanged(change) {
|
|
297
|
+
if (isCurrent()) bridge.dataChanged(change);
|
|
298
|
+
},
|
|
299
|
+
browserCall: (call, options) => isCurrent() ? bridge.browserCall(call, options) : Promise.resolve({ error: unavailableMessage() })
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function subscribeToScreen(onChange) {
|
|
303
|
+
if (typeof matchMedia !== "function") return () => {
|
|
304
|
+
};
|
|
305
|
+
const query = matchMedia(NARROW_SCREEN);
|
|
306
|
+
query.addEventListener("change", onChange);
|
|
307
|
+
return () => query.removeEventListener("change", onChange);
|
|
308
|
+
}
|
|
309
|
+
function readNarrowScreen() {
|
|
310
|
+
return typeof matchMedia === "function" && matchMedia(NARROW_SCREEN).matches;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/agent-panel/agent-panel.tsx
|
|
314
|
+
import { useEffect, useRef as useRef2 } from "react";
|
|
315
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
316
|
+
function AgentPanel({ skin, ...chatProps }) {
|
|
317
|
+
const view = usePanelView("AgentPanel");
|
|
318
|
+
const labels = useChatLabels();
|
|
319
|
+
const { controls, list, launcherRef, setBusy } = view;
|
|
320
|
+
const listView = useChatListView(list, view.closeList);
|
|
321
|
+
const wasOpen = useRef2(controls.open);
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (wasOpen.current && !controls.open) {
|
|
324
|
+
const focused = document.activeElement;
|
|
325
|
+
if (focused === null || focused === document.body || focused.closest("[hidden], [inert]") !== null) launcherRef.current?.focus();
|
|
326
|
+
}
|
|
327
|
+
wasOpen.current = controls.open;
|
|
328
|
+
}, [controls.open, launcherRef]);
|
|
329
|
+
useEffect(() => () => setBusy(false), [setBusy]);
|
|
330
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
331
|
+
/* @__PURE__ */ jsx2(skin.AgentPanelLauncher, { open: controls.open, busy: controls.busy, onToggle: controls.toggle, buttonRef: launcherRef }),
|
|
332
|
+
/* @__PURE__ */ jsx2(
|
|
333
|
+
skin.AgentPanel,
|
|
334
|
+
{
|
|
335
|
+
open: controls.open,
|
|
336
|
+
mode: controls.mode,
|
|
337
|
+
width: controls.width,
|
|
338
|
+
title: activeChatTitle(list, labels),
|
|
339
|
+
listOpen: view.listOpen,
|
|
340
|
+
onListOpenChange: view.setListOpen,
|
|
341
|
+
onClose: () => controls.setOpen(false),
|
|
342
|
+
onEscape: view.escape,
|
|
343
|
+
list: /* @__PURE__ */ jsx2(skin.ChatList, { ...listView }),
|
|
344
|
+
children: list.activeChatId !== null && /* @__PURE__ */ jsx2(
|
|
345
|
+
AgentChat,
|
|
346
|
+
{
|
|
347
|
+
...chatProps,
|
|
348
|
+
api: view.api,
|
|
349
|
+
chatId: list.activeChatId,
|
|
350
|
+
skin,
|
|
351
|
+
onBusyChange: setBusy,
|
|
352
|
+
appBridge: view.bridge,
|
|
353
|
+
...view.initialDraft !== void 0 && { initialDraft: view.initialDraft }
|
|
354
|
+
},
|
|
355
|
+
view.chatKey
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
] });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/agent-panel/agent-panel-hooks.ts
|
|
363
|
+
import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
|
|
364
|
+
function useAgentPanel() {
|
|
365
|
+
return usePanelControls("useAgentPanel");
|
|
366
|
+
}
|
|
367
|
+
function useAgentPlace(place) {
|
|
368
|
+
const control = useAppBridgeControl("useAgentPlace");
|
|
369
|
+
const [owner] = useState2(() => control.createPlaceOwner());
|
|
370
|
+
const latest = useRef3(place);
|
|
371
|
+
latest.current = place;
|
|
372
|
+
const placeKey = place === null ? null : placeIdentity(place);
|
|
373
|
+
useEffect2(() => {
|
|
374
|
+
control.setPlace(owner, latest.current);
|
|
375
|
+
}, [control, owner, placeKey]);
|
|
376
|
+
useEffect2(() => () => control.removePlace(owner), [control, owner]);
|
|
377
|
+
}
|
|
378
|
+
function placeIdentity(place) {
|
|
379
|
+
try {
|
|
380
|
+
return JSON.stringify(place);
|
|
381
|
+
} catch {
|
|
382
|
+
return JSON.stringify({ description: place.description });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function useAgentDataChanges(tag, handler) {
|
|
386
|
+
const control = useAppBridgeControl("useAgentDataChanges");
|
|
387
|
+
const latest = useRef3(handler);
|
|
388
|
+
latest.current = handler;
|
|
389
|
+
const tagsKey = JSON.stringify(typeof tag === "string" ? [tag] : tag);
|
|
390
|
+
useEffect2(() => control.subscribe(JSON.parse(tagsKey), (change) => latest.current(change)), [control, tagsKey]);
|
|
391
|
+
}
|
|
392
|
+
export {
|
|
393
|
+
AgentPanel,
|
|
394
|
+
AgentPanelProvider,
|
|
395
|
+
useAgentDataChanges,
|
|
396
|
+
useAgentPanel,
|
|
397
|
+
useAgentPlace
|
|
398
|
+
};
|
|
399
|
+
//# sourceMappingURL=panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agent-panel/agent-panel-provider.tsx","../src/app-bridge/tab-id.ts","../src/app-bridge/create-app-bridge.ts","../src/agent-panel/panel-context.ts","../src/agent-panel/panel-state.ts","../src/agent-panel/agent-panel.tsx","../src/agent-panel/agent-panel-hooks.ts"],"sourcesContent":["import { useCallback, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { AgentAppBridge, AppDataChange, BrowserToolFunction } from \"../app-bridge/app-bridge\";\nimport { createAppBridge } from \"../app-bridge/create-app-bridge\";\nimport type { AppBridgeOptions } from \"../app-bridge/create-app-bridge\";\nimport { useChatList } from \"../chat-list/use-chat-list\";\nimport { useWorkspaceListSync } from \"../chat-list/use-workspace-list-sync\";\nimport { useChatLabels } from \"../labels\";\nimport { AppBridgeContext, PanelControlsContext, PanelViewContext } from \"./panel-context\";\nimport type { AgentPanelControls, PanelView } from \"./panel-context\";\nimport { panelChatKey, usePanelOpen } from \"./panel-state\";\n\nexport interface AgentPanelProviderProps {\n /** Адрес обработчика чата, например `/api/chat`. */\n api: string;\n /** Номер человека в приложении: открыта ли панель и какой чат открыт, помнится отдельно для каждого. Смена — состояние другого человека. */\n userKey: string;\n /** Функции браузерных инструментов по именам (описания задаёт сервер в `agent.browserTools`). */\n browserTools?: Record<string, BrowserToolFunction>;\n /** `overlay` — панель поверх страницы, `push` — приложение сдвигает страницу на `pageInset`. По умолчанию `overlay`. */\n mode?: \"overlay\" | \"push\";\n /** Ширина панели на широком экране, px. По умолчанию 420. */\n width?: number;\n /** Каждое сообщение об изменении данных, любой метки. */\n onDataChanged?: (change: AppDataChange) => void;\n /** Свой `fetch` для списка чатов. Сам чат, как и `AgentChat`, ходит через `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n children?: ReactNode;\n}\n\nconst DEFAULT_WIDTH = 420;\nconst NARROW_SCREEN = \"(max-width: 640px)\";\n\n/**\n * Состояние панели агента и связь чата с приложением. Ставится в корневой макет, над маршрутами:\n * панель, открытый чат и идущий ответ живут при переходах по страницам приложения.\n */\nexport function AgentPanelProvider({ api, userKey, browserTools, mode = \"overlay\", width = DEFAULT_WIDTH, onDataChanged, fetch, children }: AgentPanelProviderProps) {\n const labels = useChatLabels();\n const bridgeOptions = useRef<AppBridgeOptions>({ toolUnavailableMessage: labels.agentPanelToolUnavailable });\n bridgeOptions.current = {\n toolUnavailableMessage: labels.agentPanelToolUnavailable,\n ...(browserTools && { browserTools }),\n ...(onDataChanged && { onDataChanged }),\n };\n const [bridgeControl] = useState(() => createAppBridge(() => bridgeOptions.current));\n\n const [open, setOpen] = usePanelOpen(api, userKey);\n const list = useChatList({ api, rememberKey: panelChatKey(api, userKey), ...(fetch && { fetch }) });\n const [listOpen, setListOpen] = useState(false);\n const [busy, setBusy] = useState(false);\n /** Готовый текст для поля ввода; `chatId: null` — привяжется к чату, который откроет `openNewChat`. */\n const [draft, setDraft] = useState<{ text: string; nonce: number; chatId: string | null } | null>(null);\n const draftCount = useRef(0);\n const launcherRef = useRef<HTMLButtonElement | null>(null);\n const { activeChatId } = list;\n\n // Сменился человек: его панель, без списка и текста прежнего.\n const [sessionUser, setSessionUser] = useState(userKey);\n if (sessionUser !== userKey) {\n setSessionUser(userKey);\n setListOpen(false);\n setDraft(null);\n setBusy(false);\n }\n /** Человек на экране сейчас: действие, начатое для прежнего, после ожидания ничего не меняет. */\n const currentUser = useRef(userKey);\n currentUser.current = userKey;\n // Мост чата этого человека: поток чата прежнего, который ещё не оборвался, не раздаёт изменения и не делает действий на странице.\n const userBridge = useMemo(\n () => bridgeForUser(bridgeControl.bridge, () => currentUser.current === userKey, () => bridgeOptions.current.toolUnavailableMessage),\n [bridgeControl, userKey],\n );\n if (draft !== null) {\n if (draft.chatId === null) {\n if (activeChatId !== null) setDraft({ ...draft, chatId: activeChatId });\n } else if (draft.chatId !== activeChatId) {\n setDraft(null);\n }\n }\n const boundDraft = draft !== null && draft.chatId !== null && draft.chatId === activeChatId ? draft : null;\n\n const chatBusy = activeChatId !== null && busy;\n useWorkspaceListSync(list, chatBusy);\n const narrow = useSyncExternalStore(subscribeToScreen, readNarrowScreen, () => false);\n const pageInset = mode === \"push\" && open && !narrow ? width : 0;\n\n const { select, create, deleteRequest, refresh } = list;\n // Список открывается поверх чата: свежий с сервера (чат могли удалить или переименовать в другой вкладке).\n const showList = useCallback(\n (show: boolean) => {\n setListOpen(show);\n if (show) void refresh();\n },\n [refresh],\n );\n const toggle = useCallback(() => setOpen(!open), [open, setOpen]);\n const openChat = useCallback(\n (chatId: string) => {\n setOpen(true);\n setListOpen(false);\n select(chatId);\n },\n [select, setOpen],\n );\n const openNewChat = useCallback(\n async (options?: { draft?: string }) => {\n setOpen(true);\n setListOpen(false);\n const created = await create();\n if (currentUser.current !== userKey) return false;\n if (!created) {\n showList(true);\n return false;\n }\n if (options?.draft !== undefined) {\n draftCount.current += 1;\n setDraft({ text: options.draft, nonce: draftCount.current, chatId: null });\n }\n return true;\n },\n [create, setOpen, showList, userKey],\n );\n const closeList = useCallback(() => setListOpen(false), []);\n // Esc: ждёт подтверждения удаления — ничего; открыт список — назад к чату; иначе закрыть панель.\n const escape = useCallback(() => {\n if (deleteRequest !== null) return;\n if (listOpen) setListOpen(false);\n else setOpen(false);\n }, [deleteRequest, listOpen, setOpen]);\n\n const controls = useMemo<AgentPanelControls>(\n () => ({ open, setOpen, toggle, openChat, openNewChat, pageInset, mode, width, busy: chatBusy }),\n [chatBusy, mode, open, openChat, openNewChat, pageInset, setOpen, toggle, width],\n );\n const view: PanelView = {\n api,\n controls,\n list,\n // Список не загрузился и открыть нечего: показать его с ошибкой и «Повторить» сразу.\n listOpen: listOpen || (activeChatId === null && list.status === \"error\"),\n setListOpen: showList,\n closeList,\n escape,\n chatKey: boundDraft ? `draft:${boundDraft.nonce}:${activeChatId}` : `chat:${activeChatId}`,\n initialDraft: boundDraft?.text,\n setBusy,\n launcherRef,\n bridge: userBridge,\n };\n\n return (\n <AppBridgeContext.Provider value={bridgeControl}>\n <PanelControlsContext.Provider value={controls}>\n <PanelViewContext.Provider value={view}>{children}</PanelViewContext.Provider>\n </PanelControlsContext.Provider>\n </AppBridgeContext.Provider>\n );\n}\n\n/** Мост, который молчит, когда `isCurrent()` ложно: сообщения об изменениях не раздаются, браузерные вызовы не исполняются. */\nfunction bridgeForUser(bridge: AgentAppBridge, isCurrent: () => boolean, unavailableMessage: () => string): AgentAppBridge {\n return {\n get tabId() {\n return bridge.tabId;\n },\n currentPlace: () => bridge.currentPlace(),\n dataChanged(change) {\n if (isCurrent()) bridge.dataChanged(change);\n },\n browserCall: (call, options) => (isCurrent() ? bridge.browserCall(call, options) : Promise.resolve({ error: unavailableMessage() })),\n };\n}\n\nfunction subscribeToScreen(onChange: () => void): () => void {\n if (typeof matchMedia !== \"function\") return () => {};\n const query = matchMedia(NARROW_SCREEN);\n query.addEventListener(\"change\", onChange);\n return () => query.removeEventListener(\"change\", onChange);\n}\n\nfunction readNarrowScreen(): boolean {\n return typeof matchMedia === \"function\" && matchMedia(NARROW_SCREEN).matches;\n}\n","/** Под этим ключом `sessionStorage` хранит номер вкладки: переживает перезагрузку, у новой вкладки свой. */\nexport const TAB_ID_KEY = \"agent-chat:tab-id\";\n\n/**\n * Под ключом `agent-chat:tab-open:{номер}` `localStorage` отмечает, что страница с этим номером открыта; уходя (`pagehide`),\n * страница снимает отметку. «Дублировать вкладку» копирует `sessionStorage` вместе с номером, но исходная вкладка ещё открыта —\n * дубликат видит отметку и берёт себе новый номер. Иначе обе вкладки исполнили бы один браузерный вызов.\n */\nexport const tabOpenKey = (tabId: string) => `agent-chat:tab-open:${tabId}`;\n\n/** Номер, который заняла эта страница; на время жизни страницы. */\nlet pageTabId: string | null = null;\nlet listening = false;\n\n/** Номер этой вкладки: прочитанный из `sessionStorage`, если другая открытая вкладка его не занимает, иначе новый случайный. */\nexport function readTabId(): string {\n try {\n const stored = sessionStorage.getItem(TAB_ID_KEY);\n if (stored !== null && stored !== \"\" && (stored === pageTabId || !openElsewhere(stored))) return claim(stored);\n const created = crypto.randomUUID();\n sessionStorage.setItem(TAB_ID_KEY, created);\n return claim(created);\n } catch {\n // Приватный режим: номер один на страницу, после перезагрузки — новый.\n pageTabId ??= crypto.randomUUID();\n return pageTabId;\n }\n}\n\nfunction openElsewhere(tabId: string): boolean {\n try {\n return localStorage.getItem(tabOpenKey(tabId)) !== null;\n } catch {\n return false;\n }\n}\n\nfunction claim(tabId: string): string {\n if (pageTabId !== tabId) {\n markOpen(false);\n pageTabId = tabId;\n markOpen(true);\n if (!listening && typeof addEventListener === \"function\") {\n listening = true;\n addEventListener(\"pagehide\", () => markOpen(false));\n addEventListener(\"pageshow\", () => markOpen(true));\n }\n }\n return tabId;\n}\n\nfunction markOpen(open: boolean) {\n if (pageTabId === null) return;\n try {\n if (open) localStorage.setItem(tabOpenKey(pageTabId), \"1\");\n else localStorage.removeItem(tabOpenKey(pageTabId));\n } catch {\n // Хранилище недоступно: дубликат не отличить, номер останется общим.\n }\n}\n","import type { AgentAppBridge, AppDataChange, BrowserToolFunction, ScreenPlace } from \"./app-bridge\";\nimport { rememberedIds } from \"./remembered-ids\";\nimport { readTabId } from \"./tab-id\";\n\n/** Что мост берёт у приложения; читается в момент вызова, поэтому новые свойства провайдера видны без нового моста. */\nexport interface AppBridgeOptions {\n /** Функции браузерных инструментов по именам. */\n browserTools?: Record<string, BrowserToolFunction>;\n /** Получает каждое сообщение об изменении данных, любой метки. */\n onDataChanged?: (change: AppDataChange) => void;\n /** Ответ модели, если функции инструмента на этой странице нет. */\n toolUnavailableMessage: string;\n}\n\n/** Владелец места: чем больше `order`, тем позже создан и тем главнее его место. */\nexport interface PlaceOwner {\n readonly order: number;\n}\n\n/** Мост и то, чем его наполняют хуки панели: места компонентов и подписки на изменения данных. */\nexport interface AppBridgeControl {\n bridge: AgentAppBridge;\n /**\n * Владелец места для одного компонента; создаётся при его первой отрисовке. Владелец, созданный позже, главнее:\n * вложенный компонент рисуется после внешнего, поэтому страница всегда главнее макета, в котором стоит.\n */\n createPlaceOwner(): PlaceOwner;\n /** Место компонента `owner`; смена места не меняет, чьё главнее. `null` — снять своё место. */\n setPlace(owner: PlaceOwner, place: ScreenPlace | null): void;\n /** Снимает только место `owner`; места других компонентов остаются. */\n removePlace(owner: PlaceOwner): void;\n /** Подписка на сообщения с одной из меток `tags`; возвращает отписку. */\n subscribe(tags: readonly string[], handler: (change: AppDataChange) => void): () => void;\n}\n\n/**\n * Под этим ключом `sessionStorage` хранит `id` розданных сообщений (последние 500): повтор кусков ответа, в том числе\n * после перезагрузки страницы во время ответа, не раздаёт сообщение второй раз.\n */\nexport const APP_CHANGES_DELIVERED_KEY = \"agent-chat:app-changes-delivered\";\n\nconst deliveredChanges = rememberedIds(APP_CHANGES_DELIVERED_KEY, 500);\n\nexport function createAppBridge(readOptions: () => AppBridgeOptions): AppBridgeControl {\n /** Заданные места; текущее — у владельца с наибольшим `order`. */\n const places = new Map<PlaceOwner, ScreenPlace>();\n let ownersCreated = 0;\n const subscriptions = new Set<{ tags: readonly string[]; handler: (change: AppDataChange) => void }>();\n\n const currentPlace = () => {\n let current: { owner: PlaceOwner; place: ScreenPlace } | null = null;\n for (const [owner, place] of places) if (current === null || owner.order > current.owner.order) current = { owner, place };\n return current?.place ?? null;\n };\n\n const bridge: AgentAppBridge = {\n tabId: readTabId(),\n currentPlace,\n dataChanged(change) {\n if (!deliveredChanges.claim(change.id)) return;\n const { onDataChanged } = readOptions();\n if (onDataChanged) deliver(() => onDataChanged(change));\n for (const subscription of [...subscriptions]) {\n if (subscription.tags.includes(change.tag)) deliver(() => subscription.handler(change));\n }\n },\n async browserCall(call, options) {\n const { browserTools, toolUnavailableMessage } = readOptions();\n const run = browserTools && Object.hasOwn(browserTools, call.toolName) ? browserTools[call.toolName] : undefined;\n if (!run) return { error: toolUnavailableMessage };\n try {\n const output = await run(call.input, { signal: options?.signal ?? new AbortController().signal });\n return { output };\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) };\n }\n },\n };\n\n return {\n bridge,\n createPlaceOwner: () => ({ order: ++ownersCreated }),\n setPlace(owner, place) {\n if (place === null) places.delete(owner);\n else places.set(owner, place);\n },\n removePlace: (owner) => void places.delete(owner),\n subscribe(tags, handler) {\n const subscription = { tags, handler };\n subscriptions.add(subscription);\n return () => void subscriptions.delete(subscription);\n },\n };\n}\n\n/** Ошибка одного подписчика не мешает остальным и не обрывает поток ответа; она уходит в журнал страницы. */\nfunction deliver(notify: () => void) {\n try {\n notify();\n } catch (error) {\n if (typeof globalThis.reportError === \"function\") globalThis.reportError(error);\n else console.error(error);\n }\n}\n","import { createContext, useContext } from \"react\";\nimport type { RefObject } from \"react\";\nimport type { AppBridgeControl } from \"../app-bridge/create-app-bridge\";\nimport type { ChatListState } from \"../chat-list/use-chat-list\";\n\n/** Управление панелью из кода приложения (`useAgentPanel`). */\nexport interface AgentPanelControls {\n open: boolean;\n setOpen(open: boolean): void;\n toggle(): void;\n /** Открыть панель на чате; чужой или удалённый — «Чат не найден», список — по кнопке «Чаты». */\n openChat(chatId: string): void;\n /** Новый (или пустой) чат и готовый текст в поле ввода. `false` — не создался (список открыт с ошибкой). */\n openNewChat(options?: { draft?: string }): Promise<boolean>;\n /** Отступ справа для страницы в режиме `push`; 0 — закрыто, режим `overlay` или экран ≤ 640 px. */\n pageInset: number;\n mode: \"overlay\" | \"push\";\n width: number;\n /** Идёт ответ в открытом чате — для значка на кнопке. */\n busy: boolean;\n}\n\n/** То, что нужно `AgentPanel`, чтобы нарисовать кнопку, рамку, список и открытый чат. */\nexport interface PanelView {\n api: string;\n controls: AgentPanelControls;\n list: ChatListState;\n listOpen: boolean;\n setListOpen(open: boolean): void;\n /** Список выбран или создан: закрыть его. */\n closeList(): void;\n /** Esc внутри панели: открыт список — закрыть его, иначе закрыть панель; при подтверждении удаления — ничего. */\n escape(): void;\n /** Ключ открытого чата: меняется вместе с чатом и с новым готовым текстом. */\n chatKey: string;\n initialDraft: string | undefined;\n setBusy(busy: boolean): void;\n launcherRef: RefObject<HTMLButtonElement | null>;\n bridge: AppBridgeControl[\"bridge\"];\n}\n\nexport const AppBridgeContext = createContext<AppBridgeControl | null>(null);\nexport const PanelControlsContext = createContext<AgentPanelControls | null>(null);\nexport const PanelViewContext = createContext<PanelView | null>(null);\n\nfunction required<T>(value: T | null, user: string): T {\n if (value === null) throw new Error(`${user} needs AgentPanelProvider above it (import it from \"@ykurilov/agent-chat-react/panel\")`);\n return value;\n}\n\n/** Мост и регистрация мест и подписок; для хуков панели. */\nexport function useAppBridgeControl(user = \"useAppBridgeControl\"): AppBridgeControl {\n return required(useContext(AppBridgeContext), user);\n}\n\nexport function usePanelControls(user: string): AgentPanelControls {\n return required(useContext(PanelControlsContext), user);\n}\n\nexport function usePanelView(user: string): PanelView {\n return required(useContext(PanelViewContext), user);\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\n\n/** Под этим ключом `localStorage` помнит, открыта ли панель: `{ \"open\": boolean }`. */\nexport const panelOpenKey = (api: string, userKey: string) => `agent-chat:panel:${api}:${userKey}`;\n\n/** Под этим ключом `useChatList` помнит чат, открытый в панели у этого человека. */\nexport const panelChatKey = (api: string, userKey: string) => `agent-chat:panel-chat:${api}:${userKey}`;\n\n/** Состояние, которое не записалось в хранилище (недоступно или переполнено): живёт в памяти страницы и главнее записанного раньше. */\nconst unsaved = new Map<string, boolean>();\nconst listeners = new Set<() => void>();\nconst subscribe = (listener: () => void) => {\n listeners.add(listener);\n return () => void listeners.delete(listener);\n};\n\n/** Была ли панель открыта у этого человека. Нет записи, битая запись или хранилище недоступно — закрыта. */\nexport function readPanelOpen(api: string, userKey: string): boolean {\n const key = panelOpenKey(api, userKey);\n const kept = unsaved.get(key);\n if (kept !== undefined) return kept;\n try {\n return parseOpen(localStorage.getItem(key));\n } catch {\n return false;\n }\n}\n\nexport function rememberPanelOpen(api: string, userKey: string, open: boolean): void {\n const key = panelOpenKey(api, userKey);\n try {\n localStorage.setItem(key, JSON.stringify({ open }));\n unsaved.delete(key);\n } catch {\n unsaved.set(key, open);\n }\n for (const listener of [...listeners]) listener();\n}\n\n/**\n * Открыта ли панель у этого человека; смена `api` или `userKey` — его запомненное состояние.\n * При отрисовке на сервере и в первом проходе гидратации — закрыта, потом — запомненное: разметка не расходится.\n */\nexport function usePanelOpen(api: string, userKey: string): [boolean, (open: boolean) => void] {\n const open = useSyncExternalStore(subscribe, () => readPanelOpen(api, userKey), () => false);\n const setOpen = useCallback((next: boolean) => rememberPanelOpen(api, userKey, next), [api, userKey]);\n return [open, setOpen];\n}\n\nfunction parseOpen(stored: string | null): boolean {\n try {\n const value: unknown = JSON.parse(stored ?? \"null\");\n return typeof value === \"object\" && value !== null && (value as { open?: unknown }).open === true;\n } catch {\n return false;\n }\n}\n","import { useEffect, useRef } from \"react\";\nimport { AgentChat } from \"../agent-chat\";\nimport type { AgentChatProps } from \"../agent-chat\";\nimport type { ChatListSkin } from \"../chat-list/chat-list-skin\";\nimport { activeChatTitle, useChatListView } from \"../chat-list/use-chat-list-view\";\nimport { useChatLabels } from \"../labels\";\nimport type { ChatSkin } from \"../skin-contract\";\nimport type { AgentPanelSkin } from \"./agent-panel-skin\";\nimport { usePanelView } from \"./panel-context\";\n\nexport interface AgentPanelProps extends Omit<AgentChatProps, \"api\" | \"chatId\" | \"subject\" | \"skin\" | \"onBusyChange\" | \"appBridge\" | \"initialDraft\"> {\n /** Скин чата, списка и панели (например, `tandemSkin`). */\n skin: ChatSkin & ChatListSkin & AgentPanelSkin;\n}\n\n/**\n * Кнопка агента и панель с открытым чатом и списком чатов поверх него. Всё видимое рисует скин.\n * Открытый чат смонтирован и при закрытой панели: ответ идёт, подписчики на изменения данных слышат его.\n */\nexport function AgentPanel({ skin, ...chatProps }: AgentPanelProps) {\n const view = usePanelView(\"AgentPanel\");\n const labels = useChatLabels();\n const { controls, list, launcherRef, setBusy } = view;\n const listView = useChatListView(list, view.closeList);\n\n // Закрылась: фокус был в панели (теперь скрытой) или потерян — вернуть его на кнопку агента. Фокус на странице не трогаем.\n const wasOpen = useRef(controls.open);\n useEffect(() => {\n if (wasOpen.current && !controls.open) {\n const focused = document.activeElement;\n if (focused === null || focused === document.body || focused.closest(\"[hidden], [inert]\") !== null) launcherRef.current?.focus();\n }\n wasOpen.current = controls.open;\n }, [controls.open, launcherRef]);\n\n // Кнопку и панель убрали со страницы: о занятости открытого чата больше некому сообщить.\n useEffect(() => () => setBusy(false), [setBusy]);\n\n return (\n <>\n <skin.AgentPanelLauncher open={controls.open} busy={controls.busy} onToggle={controls.toggle} buttonRef={launcherRef} />\n <skin.AgentPanel\n open={controls.open}\n mode={controls.mode}\n width={controls.width}\n title={activeChatTitle(list, labels)}\n listOpen={view.listOpen}\n onListOpenChange={view.setListOpen}\n onClose={() => controls.setOpen(false)}\n onEscape={view.escape}\n list={<skin.ChatList {...listView} />}\n >\n {list.activeChatId !== null && (\n <AgentChat\n key={view.chatKey}\n {...chatProps}\n api={view.api}\n chatId={list.activeChatId}\n skin={skin}\n onBusyChange={setBusy}\n appBridge={view.bridge}\n {...(view.initialDraft !== undefined && { initialDraft: view.initialDraft })}\n />\n )}\n </skin.AgentPanel>\n </>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type { AppDataChange, ScreenPlace } from \"../app-bridge/app-bridge\";\nimport { useAppBridgeControl, usePanelControls } from \"./panel-context\";\nimport type { AgentPanelControls } from \"./panel-context\";\n\nexport type { AgentPanelControls } from \"./panel-context\";\n\n/** Управление панелью из кода приложения: открыть, закрыть, открыть чат, новый чат с готовым текстом, отступ для `push`. */\nexport function useAgentPanel(): AgentPanelControls {\n return usePanelControls(\"useAgentPanel\");\n}\n\n/**\n * Где сейчас человек: место уходит с каждым запросом хода, пока компонент на экране. Главнее место компонента, который\n * появился на экране позже; вложенный всегда главнее внешнего (страница главнее макета, в котором стоит), и при первом\n * показе, и после переходов. Смена места не меняет, чьё главнее. `null` или размонтирование снимает только это место —\n * снова действует место другого компонента.\n */\nexport function useAgentPlace(place: ScreenPlace | null): void {\n const control = useAppBridgeControl(\"useAgentPlace\");\n const [owner] = useState(() => control.createPlaceOwner());\n const latest = useRef(place);\n latest.current = place;\n const placeKey = place === null ? null : placeIdentity(place);\n useEffect(() => {\n control.setPlace(owner, latest.current);\n }, [control, owner, placeKey]);\n useEffect(() => () => control.removePlace(owner), [control, owner]);\n}\n\n/** Ключ места для зависимостей эффекта. `data` не превращается в JSON (цикл, BigInt) — ключ без неё: при отправке её отбросят. */\nfunction placeIdentity(place: ScreenPlace): string {\n try {\n return JSON.stringify(place);\n } catch {\n return JSON.stringify({ description: place.description });\n }\n}\n\n/** Подписка на изменения данных по метке (или списку меток). Подписчик зовётся один раз на сообщение, всегда свежий. */\nexport function useAgentDataChanges(tag: string | readonly string[], handler: (change: AppDataChange) => void): void {\n const control = useAppBridgeControl(\"useAgentDataChanges\");\n const latest = useRef(handler);\n latest.current = handler;\n const tagsKey = JSON.stringify(typeof tag === \"string\" ? [tag] : tag);\n useEffect(() => control.subscribe(JSON.parse(tagsKey) as string[], (change) => latest.current(change)), [control, tagsKey]);\n}\n"],"mappings":"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","names":["useCallback","useSyncExternalStore","useSyncExternalStore","useCallback","useRef","jsx","useRef","useEffect","useRef","useState","useState","useRef","useEffect"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { DynamicToolUIPart, ToolUIPart } from "ai";
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
import type { AppEventViews } from "./app-events.js";
|
|
4
|
+
import type { AgentVideoView, CustomWidgetProps, DiagramColors, GeneratedImagesView } from "./skin-contract.js";
|
|
5
|
+
import type { StepPhrases, WorkView } from "./work-view/step-phrases.js";
|
|
6
|
+
/** Рисовальщик схем (пакет `@ykurilov/agent-chat-diagrams`, `diagramsClient`): тот же договор, что в пакете схем. */
|
|
7
|
+
export interface DiagramsClient {
|
|
8
|
+
render(source: string, colors: DiagramColors): Promise<{
|
|
9
|
+
svg: string;
|
|
10
|
+
} | {
|
|
11
|
+
error: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Картинки агента на экране (пакет `@ykurilov/agent-chat-images`, `imagesClient`): сообщает слою поведения имя инструмента
|
|
16
|
+
* и превращает его вызов в вид для скина. Нет `describe`-результата — вызов остаётся обычной строкой инструмента.
|
|
17
|
+
*/
|
|
18
|
+
export interface ImagesClient {
|
|
19
|
+
/** Имя инструмента, чьи вызовы рисуются как картинки: `generate_images`. */
|
|
20
|
+
toolName: string;
|
|
21
|
+
describe(call: {
|
|
22
|
+
part: ToolUIPart | DynamicToolUIPart;
|
|
23
|
+
api: string;
|
|
24
|
+
chatId: string;
|
|
25
|
+
/** Открыть картинку в окне просмотра; `key` — id вложения. */
|
|
26
|
+
openImage: (key: string, trigger: HTMLElement) => void;
|
|
27
|
+
}): GeneratedImagesView | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Видео агента на экране (пакет `@ykurilov/agent-chat-videos`, `videosClient`): превращает вызов `show_video` в вид
|
|
31
|
+
* карточки. `loading` — вызов идёт, ничего не показывать; `undefined` — отказ или ошибка, обычная строка инструмента.
|
|
32
|
+
*/
|
|
33
|
+
export interface VideosClient {
|
|
34
|
+
/** Имя инструмента, чьи вызовы рисуются как видео: `show_video`. */
|
|
35
|
+
toolName: string;
|
|
36
|
+
describe(part: ToolUIPart | DynamicToolUIPart): {
|
|
37
|
+
state: "loading";
|
|
38
|
+
} | {
|
|
39
|
+
state: "ready";
|
|
40
|
+
video: AgentVideoView;
|
|
41
|
+
} | undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Свои виджеты страницы: имя инструмента → компонент. Типы входа и ответа у каждого свои, поэтому `any`. */
|
|
44
|
+
export type CustomWidgets = Record<string, ComponentType<CustomWidgetProps<any, any>>>;
|
|
45
|
+
/**
|
|
46
|
+
* Необязательные расширения чата, которые страница подключает свойствами `AgentChat`, `ChatWorkspace` и `AgentPanel`
|
|
47
|
+
* и которые слой поведения несёт вниз до частей ответа. Без них отрисовка и запросы, как в 0.15.
|
|
48
|
+
*/
|
|
49
|
+
export interface AgentChatExtensions {
|
|
50
|
+
/** Свои виджеты: `{ pick_slot: PickSlotWidget }`. Базовые виджеты свойства не требуют. */
|
|
51
|
+
widgets?: CustomWidgets;
|
|
52
|
+
/** Схемы из блоков ```mermaid: `diagramsClient`. */
|
|
53
|
+
diagrams?: DiagramsClient;
|
|
54
|
+
/**
|
|
55
|
+
* Живые демонстрации из блоков ```html-demo (specs/043-live-demos): `true` — блок запускается в изолированной рамке.
|
|
56
|
+
* На сервере — `agent.liveDemos: true`.
|
|
57
|
+
*/
|
|
58
|
+
liveDemos?: boolean;
|
|
59
|
+
/** Картинки агента: `imagesClient`. */
|
|
60
|
+
images?: ImagesClient;
|
|
61
|
+
/** Видео агента: `videosClient`. */
|
|
62
|
+
videos?: VideosClient;
|
|
63
|
+
/** Вид событий приложения в ответе: тип события → компонент (`{ lesson_passed: LessonPassedBadge }`). */
|
|
64
|
+
appEvents?: AppEventViews;
|
|
65
|
+
/**
|
|
66
|
+
* Вид работы агента: `detailed` (по умолчанию) — мысли и строки инструментов, как раньше; `simple` — для новичков:
|
|
67
|
+
* живая строка с текущим шагом, после хода «Сделано N шагов» со списком понятных фраз. Меняется на лету. Это
|
|
68
|
+
* отображение, а не права: что прячется от человека, решают права чата (`toolDetails`, `reasoning`).
|
|
69
|
+
*/
|
|
70
|
+
workView?: WorkView;
|
|
71
|
+
/**
|
|
72
|
+
* Понятные фразы шагов для простого вида: имя инструмента → фразы «идёт» и «готово» (готовые или функция от вызова).
|
|
73
|
+
* Нет фразы — `title` инструмента, иначе общая из `labels.workView`. Таблицу держите постоянной.
|
|
74
|
+
*/
|
|
75
|
+
stepPhrases?: StepPhrases;
|
|
76
|
+
}
|