@blocksdiy/react-common 1.48.0 → 1.49.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/components/new-agent-chat.d.ts +11 -28
- package/dist/components/new-agent-chat.d.ts.map +1 -1
- package/dist/components/new-agent-chat.js +79 -82
- package/dist/hooks/agent-conversation-context.d.ts +32 -0
- package/dist/hooks/agent-conversation-context.d.ts.map +1 -0
- package/dist/hooks/agent-conversation-context.js +39 -0
- package/dist/hooks/agent-thread-live-update.d.ts +35 -64
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
- package/dist/hooks/agent-thread-live-update.js +81 -64
- package/dist/hooks/use-agent-conversation.d.ts +105 -0
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -0
- package/dist/hooks/use-agent-conversation.js +204 -0
- package/dist/hooks/use-agent-threads.d.ts +3 -50
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +53 -150
- package/dist/utils/composer-message.d.ts +57 -0
- package/dist/utils/composer-message.d.ts.map +1 -0
- package/dist/utils/composer-message.js +96 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-thread-live-update.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-thread-live-update.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent-thread-live-update.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-thread-live-update.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,21 +1,7 @@
|
|
|
1
|
-
// Wire types and pure merge logic for the agent-threads index
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/** The (chatId, chatThreadId) pair — the only stable identity of a thread row. */
|
|
1
|
+
// Wire types and pure merge logic for the agent-threads index; dependency-free for tests.
|
|
2
|
+
const THREAD_STATUSES = ["idle", "interrupted", "waiting_on_agent", "busy", "error"];
|
|
3
|
+
const THREAD_SOURCES = ["direct_chat", "send_message", "feedback", "self_config_resume", "unknown"];
|
|
5
4
|
export const agentThreadKey = (thread) => `${thread.chatId}::${thread.chatThreadId}`;
|
|
6
|
-
const THREAD_STATUSES = ["idle", "interrupted", "error"];
|
|
7
|
-
const THREAD_SOURCES = [
|
|
8
|
-
"direct_chat",
|
|
9
|
-
"agent_call",
|
|
10
|
-
"send_message",
|
|
11
|
-
"feedback",
|
|
12
|
-
"unknown",
|
|
13
|
-
];
|
|
14
|
-
/**
|
|
15
|
-
* Whether a realtime payload's `thread` is a complete list-item row. The server
|
|
16
|
-
* serializes the exact item shape into every event, so this normally passes; when
|
|
17
|
-
* it doesn't (contract drift), the consumer falls back to the getOne endpoint.
|
|
18
|
-
*/
|
|
19
5
|
export const isAgentThreadItem = (value) => {
|
|
20
6
|
const item = value;
|
|
21
7
|
return (typeof item === "object" &&
|
|
@@ -30,15 +16,11 @@ export const isAgentThreadItem = (value) => {
|
|
|
30
16
|
(typeof item.title === "string" || item.title === null) &&
|
|
31
17
|
typeof item.turnCount === "number" &&
|
|
32
18
|
typeof item.firstSeenAt === "string" &&
|
|
33
|
-
typeof item.lastActivityAt === "string"
|
|
19
|
+
typeof item.lastActivityAt === "string" &&
|
|
20
|
+
(item.runningSince == null || typeof item.runningSince === "string") &&
|
|
21
|
+
(item.runningRunId == null || typeof item.runningRunId === "string"));
|
|
34
22
|
};
|
|
35
|
-
// Matches the server's cap for LLM-generated titles (wf-actions-py TITLE_MAX_CHARS).
|
|
36
23
|
const PROVISIONAL_TITLE_MAX_CHARS = 80;
|
|
37
|
-
/**
|
|
38
|
-
* Provisional row title from the prompt that started a conversation (ChatGPT-style):
|
|
39
|
-
* one line, whitespace collapsed, capped like the server's generated titles. Null when
|
|
40
|
-
* there's nothing usable (attachment-only sends) — the UI keeps its id-based fallback.
|
|
41
|
-
*/
|
|
42
24
|
export const provisionalThreadTitle = (prompt) => {
|
|
43
25
|
const collapsed = prompt?.replace(/\s+/g, " ").trim();
|
|
44
26
|
if (!collapsed) {
|
|
@@ -48,42 +30,46 @@ export const provisionalThreadTitle = (prompt) => {
|
|
|
48
30
|
? `${collapsed.slice(0, PROVISIONAL_TITLE_MAX_CHARS - 1).trimEnd()}…`
|
|
49
31
|
: collapsed;
|
|
50
32
|
};
|
|
51
|
-
|
|
52
|
-
* The optimistic row for a user send on `thread`, replacing `previous` (its current
|
|
53
|
-
* optimistic entry, if any): the same identity, its recency bumped to `now`, and a title
|
|
54
|
-
* that never regresses — the provisional title already shown, else the row's own, else
|
|
55
|
-
* one from this prompt (null when the prompt has nothing usable).
|
|
56
|
-
*/
|
|
33
|
+
// Optimistic send row; never carries a run — the server's row for this send owns busy.
|
|
57
34
|
export const optimisticThreadAfterSend = (thread, previous, prompt, now) => ({
|
|
58
35
|
...thread,
|
|
59
|
-
|
|
36
|
+
status: thread.status === "busy" ? "idle" : thread.status,
|
|
37
|
+
runningSince: null,
|
|
38
|
+
runningRunId: null,
|
|
39
|
+
title: thread.title ?? previous?.title ?? provisionalThreadTitle(prompt),
|
|
60
40
|
lastActivityAt: now,
|
|
61
41
|
});
|
|
42
|
+
// Stands aside when the listed row has turns or a different run in flight.
|
|
43
|
+
const removeListedAgentThread = (threads, removed) => {
|
|
44
|
+
const key = agentThreadKey(removed);
|
|
45
|
+
const listed = threads.find((thread) => agentThreadKey(thread) === key);
|
|
46
|
+
if (listed &&
|
|
47
|
+
(listed.turnCount > 0 || (listed.runningRunId != null && listed.runningRunId !== removed.runningRunId))) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return listed ? threads.filter((thread) => thread !== listed) : threads;
|
|
51
|
+
};
|
|
52
|
+
export const removeAgentThread = (threads, optimistic, removed) => {
|
|
53
|
+
const remaining = removeListedAgentThread(threads, removed);
|
|
54
|
+
if (remaining === null) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const key = agentThreadKey(removed);
|
|
58
|
+
const withoutEntry = new Map(optimistic);
|
|
59
|
+
withoutEntry.delete(key);
|
|
60
|
+
return { threads: remaining, optimistic: optimistic.has(key) ? withoutEntry : optimistic };
|
|
61
|
+
};
|
|
62
62
|
const sortTime = (item, sort) => {
|
|
63
63
|
const parsed = new Date(sort === "firstSeen" ? item.firstSeenAt : item.lastActivityAt).getTime();
|
|
64
64
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
65
65
|
};
|
|
66
|
-
/**
|
|
67
|
-
* Merge one realtime thread update into a loaded list. Pure; returns the input array
|
|
68
|
-
* untouched when the event is stale, filtered out, or out of the loaded window.
|
|
69
|
-
*
|
|
70
|
-
* - Out-of-order delivery can't regress a row: events older than the loaded row (by
|
|
71
|
-
* lastActivityAt) are dropped — checked FIRST, so a stale event can't evict a
|
|
72
|
-
* still-current row through the filter check below either. Renames keep
|
|
73
|
-
* lastActivityAt, so equal passes.
|
|
74
|
-
* - A row that stops matching the active filters is removed; a matching one is upserted
|
|
75
|
-
* and the list re-sorted by the active sort (stable, so ties keep their server order).
|
|
76
|
-
* - A NEW row sorting past every loaded row while more pages exist may belong beyond the
|
|
77
|
-
* loaded window; it's skipped so the list stays contiguous with the pagination cursor
|
|
78
|
-
* (unreachable under the default lastActivity-desc sort, where fresh activity sorts first).
|
|
79
|
-
*/
|
|
80
|
-
/** Whether a row passes the list's active server-side filters. */
|
|
81
66
|
export const matchesAgentThreadFilters = (item, options = {}) => (!options.status || item.status === options.status) &&
|
|
82
67
|
(!options.source || item.source === options.source) &&
|
|
83
68
|
(!options.chatId || item.chatId === options.chatId);
|
|
84
69
|
export const applyAgentThreadUpdate = (threads, incoming, options = {}) => {
|
|
85
70
|
const key = agentThreadKey(incoming);
|
|
86
71
|
const existing = threads.find((thread) => agentThreadKey(thread) === key);
|
|
72
|
+
// Stale by lastActivityAt is dropped; equal passes (renames keep lastActivityAt).
|
|
87
73
|
if (existing && sortTime(incoming, "lastActivity") < sortTime(existing, "lastActivity")) {
|
|
88
74
|
return threads;
|
|
89
75
|
}
|
|
@@ -99,18 +85,16 @@ export const applyAgentThreadUpdate = (threads, incoming, options = {}) => {
|
|
|
99
85
|
}
|
|
100
86
|
return next;
|
|
101
87
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* guard compares it against server timestamps).
|
|
113
|
-
*/
|
|
88
|
+
export const reconcileWalkWithLive = (items, live, options = {}) => {
|
|
89
|
+
let merged = items;
|
|
90
|
+
for (const entry of live.values()) {
|
|
91
|
+
merged =
|
|
92
|
+
"removed" in entry
|
|
93
|
+
? (removeListedAgentThread(merged, entry.removed) ?? merged)
|
|
94
|
+
: applyAgentThreadUpdate(merged, entry, options);
|
|
95
|
+
}
|
|
96
|
+
return merged;
|
|
97
|
+
};
|
|
114
98
|
export const overlayOptimisticThreads = (threads, optimistic, options = {}) => {
|
|
115
99
|
if (optimistic.size === 0) {
|
|
116
100
|
return threads;
|
|
@@ -122,11 +106,44 @@ export const overlayOptimisticThreads = (threads, optimistic, options = {}) => {
|
|
|
122
106
|
merged = applyAgentThreadUpdate(merged, row, options);
|
|
123
107
|
}
|
|
124
108
|
}
|
|
125
|
-
|
|
126
|
-
|
|
109
|
+
return merged;
|
|
110
|
+
};
|
|
111
|
+
// Retires for good once the server row shows a run — a settled error must not be hidden.
|
|
112
|
+
export const retireOptimisticThreadsTakenOver = (optimistic, threads) => {
|
|
113
|
+
const taken = threads.filter((thread) => (thread.status === "busy" || thread.runningRunId != null) && optimistic.has(agentThreadKey(thread)));
|
|
114
|
+
if (taken.length === 0) {
|
|
115
|
+
return optimistic;
|
|
116
|
+
}
|
|
117
|
+
const next = new Map(optimistic);
|
|
118
|
+
for (const thread of taken) {
|
|
119
|
+
next.delete(agentThreadKey(thread));
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
};
|
|
123
|
+
/** The server's cap on a title a person sets (RENAME_TITLE_MAX_CHARS in wf-actions-py). */
|
|
124
|
+
export const RENAME_TITLE_MAX_CHARS = 255;
|
|
125
|
+
/** Conversation-index groups, in display order; a row's group follows its write-once `source`. */
|
|
126
|
+
export const AGENT_THREAD_GROUPS = ["chats", "workflows", "feedback"];
|
|
127
|
+
const GROUP_OF_SOURCE = {
|
|
128
|
+
direct_chat: "chats",
|
|
129
|
+
self_config_resume: "chats",
|
|
130
|
+
unknown: "chats",
|
|
131
|
+
send_message: "workflows",
|
|
132
|
+
feedback: "feedback",
|
|
133
|
+
};
|
|
134
|
+
/** Where a row files; a source this copy predates files under chats, so a row never renders nowhere. */
|
|
135
|
+
export const agentThreadGroup = (source) => GROUP_OF_SOURCE[source] ?? "chats";
|
|
136
|
+
/** Rows by group in group order, each group in input order; empty groups dropped. */
|
|
137
|
+
export const groupAgentThreads = (threads) => {
|
|
138
|
+
const grouped = AGENT_THREAD_GROUPS.map((group) => ({ group, rows: [] }));
|
|
139
|
+
for (const thread of threads) {
|
|
140
|
+
grouped[AGENT_THREAD_GROUPS.indexOf(agentThreadGroup(thread.source))].rows.push(thread);
|
|
127
141
|
}
|
|
128
|
-
return
|
|
129
|
-
const provisional = row.title === null ? optimistic.get(agentThreadKey(row))?.title : null;
|
|
130
|
-
return provisional ? { ...row, title: provisional } : row;
|
|
131
|
-
});
|
|
142
|
+
return grouped.filter(({ rows }) => rows.length > 0);
|
|
132
143
|
};
|
|
144
|
+
/** A thread the agent opened to act on feedback — by `type`, or by `source` for rows from before the column. */
|
|
145
|
+
export const isFeedbackThread = (row) => row.type === "feedback" || row.source === "feedback";
|
|
146
|
+
/** The conversation a feedback thread was opened from, when `threads` lists it (same chat, the row's pointer). */
|
|
147
|
+
export const feedbackSourceThread = (row, threads) => row.fromSourceChatThreadId
|
|
148
|
+
? threads.find((candidate) => candidate.chatId === row.chatId && candidate.chatThreadId === row.fromSourceChatThreadId)
|
|
149
|
+
: undefined;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type AgentThreadItem } from "./agent-thread-live-update.js";
|
|
2
|
+
import type { UseAgentThreadsResult } from "./use-agent-threads.js";
|
|
3
|
+
/** A conversation is the (chatId, chatThreadId) PAIR — rows under a fixed thread id differ by chat. */
|
|
4
|
+
export type AgentConversationRef = Pick<AgentThreadItem, "chatId" | "chatThreadId">;
|
|
5
|
+
/**
|
|
6
|
+
* A new chat is a draft: a thread id minted here, listed nowhere until the first send. "New
|
|
7
|
+
* chat" on an untouched draft keeps it; once sent, the draft is a thread like any other.
|
|
8
|
+
*/
|
|
9
|
+
export interface AgentConversationSelection {
|
|
10
|
+
conversation: AgentConversationRef;
|
|
11
|
+
draftThreadId: string;
|
|
12
|
+
draftSent: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const newDraftSelection: (chatId: string, draftThreadId?: string) => AgentConversationSelection;
|
|
15
|
+
/** A draft stands by even when the URL opens a thread: "New chat" from there must not mint twice. */
|
|
16
|
+
export declare const initialSelection: (ownChatId: string, urlConversation: AgentConversationRef | undefined) => AgentConversationSelection;
|
|
17
|
+
export declare const isDraftSelection: ({ conversation, draftThreadId, draftSent }: AgentConversationSelection) => boolean;
|
|
18
|
+
/** After choosing a thread; the same object when it is already open (no re-render). */
|
|
19
|
+
export declare const selectionWithThread: (prev: AgentConversationSelection, conversation: AgentConversationRef) => AgentConversationSelection;
|
|
20
|
+
/** After "New chat": an untouched draft stays; anything else mints a fresh one. */
|
|
21
|
+
export declare const selectionWithNewChat: (prev: AgentConversationSelection, ownChatId: string) => AgentConversationSelection;
|
|
22
|
+
/** After the first send on the draft; unchanged when the draft is not the open conversation. */
|
|
23
|
+
export declare const selectionAfterDraftSent: (prev: AgentConversationSelection) => AgentConversationSelection;
|
|
24
|
+
/** What the URL names: a thread, the new chat, or no conversation (one of the surface's pages). */
|
|
25
|
+
export type AgentConversationUrlTarget = AgentConversationRef | "new" | null;
|
|
26
|
+
/** Identity of a URL target; the selection follows the URL whenever this changes. */
|
|
27
|
+
export declare const urlSelectionKey: (target: AgentConversationUrlTarget) => string | null;
|
|
28
|
+
export interface UseAgentConversationSelectionOptions {
|
|
29
|
+
/** The chat the surface's own conversations live under (agentInboxChatId). */
|
|
30
|
+
ownChatId: string;
|
|
31
|
+
urlTarget: AgentConversationUrlTarget;
|
|
32
|
+
}
|
|
33
|
+
export interface UseAgentConversationSelectionResult {
|
|
34
|
+
conversation: AgentConversationRef;
|
|
35
|
+
/** The new chat, nothing sent on it yet. */
|
|
36
|
+
isDraft: boolean;
|
|
37
|
+
/** After the first send on the draft — the caller then navigates to the draft's thread URL. */
|
|
38
|
+
markDraftSent: () => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The open conversation follows the URL — the one source of truth, so choosing a thread is a
|
|
42
|
+
* navigation, never a call here; the draft is local state.
|
|
43
|
+
*/
|
|
44
|
+
export declare const useAgentConversationSelection: ({ ownChatId, urlTarget, }: UseAgentConversationSelectionOptions) => UseAgentConversationSelectionResult;
|
|
45
|
+
export declare const RUN_STALE_MS: number;
|
|
46
|
+
/** A row for a conversation the index lists nothing for (yet): the draft, or a thread whose row has not landed. */
|
|
47
|
+
export declare const agentThreadStandIn: (agentId: string, conversation: AgentConversationRef, isDraft: boolean) => AgentThreadItem;
|
|
48
|
+
/** When the oldest run mark among `rows` reaches the stale ceiling (epoch ms); null with no mark. */
|
|
49
|
+
export declare const nextRunStaleAt: (rows: readonly AgentThreadItem[]) => number | null;
|
|
50
|
+
/** The turns this client has accounted for on one conversation (`key`: agentThreadKey). */
|
|
51
|
+
export interface AccountedTurns {
|
|
52
|
+
key: string;
|
|
53
|
+
turns: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The accounting after a row for `key` reports `turnCount` turns. The first row is a baseline —
|
|
57
|
+
* the chat replayed it on mount — not a turn to fetch. `turnCount` is server-monotonic, so more
|
|
58
|
+
* turns than accounted is a turn this client did not make: replayed once nothing is streaming
|
|
59
|
+
* (a run's own stream is the authoritative view; replaying underneath it would fight its events).
|
|
60
|
+
*/
|
|
61
|
+
export declare const accountedTurnsAfterRow: (accounted: AccountedTurns | null, key: string, turnCount: number, isThinking: boolean) => {
|
|
62
|
+
accounted: AccountedTurns | null;
|
|
63
|
+
replay: boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The accounting after this client's own send on `key`: one more turn. A draft's first send
|
|
67
|
+
* starts the count, so the row that lands for it is read as this send's, not a foreign turn.
|
|
68
|
+
*/
|
|
69
|
+
export declare const accountedTurnsAfterOwnSend: (accounted: AccountedTurns | null, key: string, isDraft: boolean) => AccountedTurns | null;
|
|
70
|
+
/** The chat runtime's side of the bridge — a subset of react-common's AgentChatContextValue. */
|
|
71
|
+
export interface AgentConversationChat {
|
|
72
|
+
isThinking: boolean;
|
|
73
|
+
reloadMessages: () => Promise<void>;
|
|
74
|
+
/** Optional as on AgentChatContextValue: a context a host builds by hand may omit it. */
|
|
75
|
+
setRunningElsewhere?: (runId: string | null) => void;
|
|
76
|
+
}
|
|
77
|
+
export interface UseAgentConversationOptions {
|
|
78
|
+
/** The index the conversation is listed in. */
|
|
79
|
+
threads: UseAgentThreadsResult;
|
|
80
|
+
agentId: string;
|
|
81
|
+
conversation: AgentConversationRef;
|
|
82
|
+
isDraft: boolean;
|
|
83
|
+
chat: AgentConversationChat;
|
|
84
|
+
}
|
|
85
|
+
export interface UseAgentConversationResult {
|
|
86
|
+
/** The conversation's row: listed, else the last listed, else a stand-in. */
|
|
87
|
+
row: AgentThreadItem;
|
|
88
|
+
/** `row` is invented locally — the index lists nothing for this conversation. */
|
|
89
|
+
isStandIn: boolean;
|
|
90
|
+
/** `threads.renameThread`, keeping the remembered row's title current too. */
|
|
91
|
+
renameThread: UseAgentThreadsResult["renameThread"];
|
|
92
|
+
/** After this client sends on the conversation (the chat root's onUserMessageSent). */
|
|
93
|
+
notifyMessageSent: (text: string, message?: {
|
|
94
|
+
name?: string;
|
|
95
|
+
}) => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Keeps the index and the transcript agreeing for the open conversation:
|
|
99
|
+
* - a run on it this client is not streaming (the listed row's `runningRunId`) reads as the chat's own;
|
|
100
|
+
* - a turn this client did not send (`turnCount` ahead of what it accounted for) replays the transcript;
|
|
101
|
+
* - a run mark past the stale ceiling asks the server again (the settle publishes no event);
|
|
102
|
+
* - refocus re-walks the list (realtime has no replay), throttled.
|
|
103
|
+
*/
|
|
104
|
+
export declare const useAgentConversation: ({ threads, agentId, conversation, isDraft, chat, }: UseAgentConversationOptions) => UseAgentConversationResult;
|
|
105
|
+
//# sourceMappingURL=use-agent-conversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-agent-conversation.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-conversation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Headless state for the conversation an agent surface has open, over the threads index
|
|
2
|
+
// (useAgentThreads) and the chat runtime (useAgentChat): which conversation is selected and
|
|
3
|
+
// how a draft becomes a thread; the conversation's row; and the bridge that keeps the index
|
|
4
|
+
// and the transcript agreeing. UI-free — the agent-layout registry item renders it.
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { FEEDBACK_MESSAGE_NAME } from "../utils/composer-message.js";
|
|
7
|
+
import { agentThreadKey } from "./agent-thread-live-update.js";
|
|
8
|
+
export const newDraftSelection = (chatId, draftThreadId = crypto.randomUUID()) => ({
|
|
9
|
+
conversation: { chatId, chatThreadId: draftThreadId },
|
|
10
|
+
draftThreadId,
|
|
11
|
+
draftSent: false,
|
|
12
|
+
});
|
|
13
|
+
/** A draft stands by even when the URL opens a thread: "New chat" from there must not mint twice. */
|
|
14
|
+
export const initialSelection = (ownChatId, urlConversation) => {
|
|
15
|
+
const draft = newDraftSelection(ownChatId);
|
|
16
|
+
return urlConversation ? { ...draft, conversation: urlConversation } : draft;
|
|
17
|
+
};
|
|
18
|
+
export const isDraftSelection = ({ conversation, draftThreadId, draftSent }) => conversation.chatThreadId === draftThreadId && !draftSent;
|
|
19
|
+
/** After choosing a thread; the same object when it is already open (no re-render). */
|
|
20
|
+
export const selectionWithThread = (prev, conversation) => agentThreadKey(prev.conversation) === agentThreadKey(conversation) ? prev : { ...prev, conversation };
|
|
21
|
+
/** After "New chat": an untouched draft stays; anything else mints a fresh one. */
|
|
22
|
+
export const selectionWithNewChat = (prev, ownChatId) => (isDraftSelection(prev) ? prev : newDraftSelection(ownChatId));
|
|
23
|
+
/** After the first send on the draft; unchanged when the draft is not the open conversation. */
|
|
24
|
+
export const selectionAfterDraftSent = (prev) => isDraftSelection(prev) ? { ...prev, draftSent: true } : prev;
|
|
25
|
+
/** Identity of a URL target; the selection follows the URL whenever this changes. */
|
|
26
|
+
export const urlSelectionKey = (target) => target === null ? null : target === "new" ? "new" : agentThreadKey(target);
|
|
27
|
+
/**
|
|
28
|
+
* The open conversation follows the URL — the one source of truth, so choosing a thread is a
|
|
29
|
+
* navigation, never a call here; the draft is local state.
|
|
30
|
+
*/
|
|
31
|
+
export const useAgentConversationSelection = ({ ownChatId, urlTarget, }) => {
|
|
32
|
+
const urlConversation = urlTarget !== null && urlTarget !== "new" ? urlTarget : undefined;
|
|
33
|
+
const [selection, setSelection] = useState(() => initialSelection(ownChatId, urlConversation));
|
|
34
|
+
const markDraftSent = useCallback(() => setSelection(selectionAfterDraftSent), []);
|
|
35
|
+
// Synced during render (React's "adjusting state when a prop changes"), so the chat root
|
|
36
|
+
// below never renders a frame on the previous conversation.
|
|
37
|
+
const urlKey = urlSelectionKey(urlTarget);
|
|
38
|
+
const [syncedUrlKey, setSyncedUrlKey] = useState(urlKey);
|
|
39
|
+
if (syncedUrlKey !== urlKey) {
|
|
40
|
+
setSyncedUrlKey(urlKey);
|
|
41
|
+
if (urlConversation) {
|
|
42
|
+
setSelection((prev) => selectionWithThread(prev, urlConversation));
|
|
43
|
+
}
|
|
44
|
+
else if (urlTarget === "new") {
|
|
45
|
+
setSelection((prev) => selectionWithNewChat(prev, ownChatId));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { conversation: selection.conversation, isDraft: isDraftSelection(selection), markDraftSent };
|
|
49
|
+
};
|
|
50
|
+
/* ---- The open conversation's row, and the index ↔ chat bridge ----------------------------- */
|
|
51
|
+
// Mirrors wf-actions-py RUN_STALE_AFTER: the server ends a run still marked this long after it
|
|
52
|
+
// started, ahead of a list read — and publishes nothing for it, so a viewer has to ask.
|
|
53
|
+
export const RUN_STALE_MS = 30 * 60 * 1000;
|
|
54
|
+
// A refresh re-walks every loaded page; refocus is throttled to this.
|
|
55
|
+
const FOCUS_REFRESH_MIN_INTERVAL_MS = 15_000;
|
|
56
|
+
// While a mark stays past the ceiling (this clock may run ahead of the server's), ask again each minute.
|
|
57
|
+
const STALE_RUN_REPOLL_MS = 60_000;
|
|
58
|
+
/** A row for a conversation the index lists nothing for (yet): the draft, or a thread whose row has not landed. */
|
|
59
|
+
export const agentThreadStandIn = (agentId, conversation, isDraft) => {
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
return {
|
|
62
|
+
agentId,
|
|
63
|
+
chatId: conversation.chatId,
|
|
64
|
+
chatThreadId: conversation.chatThreadId,
|
|
65
|
+
source: "direct_chat",
|
|
66
|
+
status: "idle",
|
|
67
|
+
title: null,
|
|
68
|
+
turnCount: isDraft ? 0 : 1,
|
|
69
|
+
firstSeenAt: now,
|
|
70
|
+
lastActivityAt: now,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/** When the oldest run mark among `rows` reaches the stale ceiling (epoch ms); null with no mark. */
|
|
74
|
+
export const nextRunStaleAt = (rows) => {
|
|
75
|
+
let next = null;
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
const startedAt = Date.parse(row.runningSince ?? "");
|
|
78
|
+
if (Number.isFinite(startedAt) && (next === null || startedAt + RUN_STALE_MS < next)) {
|
|
79
|
+
next = startedAt + RUN_STALE_MS;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return next;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The accounting after a row for `key` reports `turnCount` turns. The first row is a baseline —
|
|
86
|
+
* the chat replayed it on mount — not a turn to fetch. `turnCount` is server-monotonic, so more
|
|
87
|
+
* turns than accounted is a turn this client did not make: replayed once nothing is streaming
|
|
88
|
+
* (a run's own stream is the authoritative view; replaying underneath it would fight its events).
|
|
89
|
+
*/
|
|
90
|
+
export const accountedTurnsAfterRow = (accounted, key, turnCount, isThinking) => {
|
|
91
|
+
if (!accounted || accounted.key !== key) {
|
|
92
|
+
return { accounted: { key, turns: turnCount }, replay: false };
|
|
93
|
+
}
|
|
94
|
+
if (turnCount <= accounted.turns || isThinking) {
|
|
95
|
+
return { accounted, replay: false };
|
|
96
|
+
}
|
|
97
|
+
return { accounted: { key, turns: turnCount }, replay: true };
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The accounting after this client's own send on `key`: one more turn. A draft's first send
|
|
101
|
+
* starts the count, so the row that lands for it is read as this send's, not a foreign turn.
|
|
102
|
+
*/
|
|
103
|
+
export const accountedTurnsAfterOwnSend = (accounted, key, isDraft) => {
|
|
104
|
+
if (accounted && accounted.key === key) {
|
|
105
|
+
return { key, turns: accounted.turns + 1 };
|
|
106
|
+
}
|
|
107
|
+
return isDraft ? { key, turns: 1 } : accounted;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Keeps the index and the transcript agreeing for the open conversation:
|
|
111
|
+
* - a run on it this client is not streaming (the listed row's `runningRunId`) reads as the chat's own;
|
|
112
|
+
* - a turn this client did not send (`turnCount` ahead of what it accounted for) replays the transcript;
|
|
113
|
+
* - a run mark past the stale ceiling asks the server again (the settle publishes no event);
|
|
114
|
+
* - refocus re-walks the list (realtime has no replay), throttled.
|
|
115
|
+
*/
|
|
116
|
+
export const useAgentConversation = ({ threads, agentId, conversation, isDraft, chat, }) => {
|
|
117
|
+
const { threads: rows, refresh, notifyThreadActivity, renameThread: renameListedThread } = threads;
|
|
118
|
+
const { isThinking, reloadMessages, setRunningElsewhere } = chat;
|
|
119
|
+
const conversationKey = agentThreadKey(conversation);
|
|
120
|
+
const listedRow = rows.find((row) => agentThreadKey(row) === conversationKey);
|
|
121
|
+
// A row a filter hides is remembered, so the open conversation keeps its title. Stored
|
|
122
|
+
// during render (React's "storing information from previous renders"), not in an effect.
|
|
123
|
+
const [lastListedRow, setLastListedRow] = useState(null);
|
|
124
|
+
if (listedRow && listedRow !== lastListedRow) {
|
|
125
|
+
setLastListedRow(listedRow);
|
|
126
|
+
}
|
|
127
|
+
const knownRow = listedRow ?? (lastListedRow && agentThreadKey(lastListedRow) === conversationKey ? lastListedRow : undefined);
|
|
128
|
+
// Not memoised: a stand-in's stamps are "now", and a send stands its row in with them.
|
|
129
|
+
const row = knownRow ?? agentThreadStandIn(agentId, conversation, isDraft);
|
|
130
|
+
const rowRef = useRef(row);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
rowRef.current = row;
|
|
133
|
+
});
|
|
134
|
+
const renameThread = useCallback(async (thread, title) => {
|
|
135
|
+
await renameListedThread(thread, title);
|
|
136
|
+
const trimmed = title.trim();
|
|
137
|
+
if (!trimmed) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
setLastListedRow((prev) => prev && agentThreadKey(prev) === agentThreadKey(thread) && prev.title !== trimmed
|
|
141
|
+
? { ...prev, title: trimmed }
|
|
142
|
+
: prev);
|
|
143
|
+
}, [renameListedThread]);
|
|
144
|
+
// A run elsewhere on this conversation — from the LISTED row only: a row a filter hides gets
|
|
145
|
+
// no live updates, so a remembered one would outlive its run.
|
|
146
|
+
const runningElsewhereRunId = listedRow?.runningRunId ?? null;
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
setRunningElsewhere?.(runningElsewhereRunId);
|
|
149
|
+
// Nothing else clears it: told once, the chat would stay shut past this surface's lifetime.
|
|
150
|
+
return () => setRunningElsewhere?.(null);
|
|
151
|
+
}, [setRunningElsewhere, runningElsewhereRunId]);
|
|
152
|
+
// The stale ceiling: ask at the oldest mark's ceiling, then each minute while a mark stays.
|
|
153
|
+
const nextStaleAt = nextRunStaleAt(rows);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (nextStaleAt === null) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let timer;
|
|
159
|
+
const ask = () => {
|
|
160
|
+
void refresh();
|
|
161
|
+
timer = setTimeout(ask, STALE_RUN_REPOLL_MS);
|
|
162
|
+
};
|
|
163
|
+
timer = setTimeout(ask, Math.max(nextStaleAt - Date.now(), 0));
|
|
164
|
+
return () => clearTimeout(timer);
|
|
165
|
+
}, [nextStaleAt, refresh]);
|
|
166
|
+
// Realtime has no replay — refocus re-walks the list, throttled.
|
|
167
|
+
const lastFocusRefreshAtRef = useRef(0);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const onFocus = () => {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
if (now - lastFocusRefreshAtRef.current < FOCUS_REFRESH_MIN_INTERVAL_MS) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
lastFocusRefreshAtRef.current = now;
|
|
175
|
+
void refresh();
|
|
176
|
+
};
|
|
177
|
+
window.addEventListener("focus", onFocus);
|
|
178
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
179
|
+
}, [refresh]);
|
|
180
|
+
// Turns this client did not send (see accountedTurnsAfterRow). `isThinking` is a dependency,
|
|
181
|
+
// so this runs again the moment a run on the conversation settles.
|
|
182
|
+
const accountedTurnsRef = useRef(null);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!knownRow) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const next = accountedTurnsAfterRow(accountedTurnsRef.current, conversationKey, knownRow.turnCount, isThinking);
|
|
188
|
+
accountedTurnsRef.current = next.accounted;
|
|
189
|
+
if (next.replay) {
|
|
190
|
+
void reloadMessages();
|
|
191
|
+
}
|
|
192
|
+
}, [knownRow, conversationKey, isThinking, reloadMessages]);
|
|
193
|
+
const notifyMessageSent = useCallback((text, message) => {
|
|
194
|
+
// A feedback send is recorded on the conversation with no reply there: activity moves;
|
|
195
|
+
// no turn is accounted, and the prompt does not name the thread.
|
|
196
|
+
const isFeedbackSend = message?.name === FEEDBACK_MESSAGE_NAME;
|
|
197
|
+
notifyThreadActivity(rowRef.current, isFeedbackSend ? undefined : text);
|
|
198
|
+
if (isFeedbackSend) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
accountedTurnsRef.current = accountedTurnsAfterOwnSend(accountedTurnsRef.current, conversationKey, isDraft);
|
|
202
|
+
}, [notifyThreadActivity, conversationKey, isDraft]);
|
|
203
|
+
return { row, isStandIn: knownRow === undefined, renameThread, notifyMessageSent };
|
|
204
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type AgentThreadItem, type AgentThreadSource, type AgentThreadStatus } from "./agent-thread-live-update.js";
|
|
2
2
|
export * from "./agent-thread-live-update.js";
|
|
3
|
+
export { type AgentConversationChat, type AgentConversationRef, type AgentConversationUrlTarget, agentThreadStandIn, nextRunStaleAt, RUN_STALE_MS, useAgentConversation, type UseAgentConversationOptions, type UseAgentConversationResult, useAgentConversationSelection, type UseAgentConversationSelectionOptions, type UseAgentConversationSelectionResult, } from "./use-agent-conversation.js";
|
|
4
|
+
export { type AgentConversationContextValue, AgentConversationProvider, type AgentConversationProviderProps, type AgentThreadFilters, useAgentConversationContext, } from "./agent-conversation-context.js";
|
|
3
5
|
export interface AgentThreadsPage {
|
|
4
6
|
items: AgentThreadItem[];
|
|
5
7
|
nextCursor: string | null;
|
|
@@ -12,75 +14,26 @@ export interface UseAgentThreadsOptions {
|
|
|
12
14
|
limit?: number;
|
|
13
15
|
sort?: "lastActivity" | "firstSeen";
|
|
14
16
|
order?: "asc" | "desc";
|
|
15
|
-
/** Platform host override; defaults to getApiHost() (same convention as the agent chat). */
|
|
16
17
|
baseUrl?: string;
|
|
17
|
-
/** Bearer token for callers that don't share the platform session cookie. */
|
|
18
18
|
token?: string;
|
|
19
|
-
/** Extra request headers, merged over the defaults. */
|
|
20
19
|
headers?: Record<string, string>;
|
|
21
20
|
}
|
|
22
21
|
export interface UseAgentThreadsResult {
|
|
23
22
|
threads: AgentThreadItem[];
|
|
24
|
-
/**
|
|
25
|
-
* Report a user send on a conversation — ChatGPT-style optimistic listing. Pass the
|
|
26
|
-
* conversation's row (the listed one when the index has it, else one the consumer
|
|
27
|
-
* builds from the (chatId, chatThreadId) pair) and the prompt text: a conversation with
|
|
28
|
-
* no server row yet appears in `threads` immediately, titled by the truncated prompt,
|
|
29
|
-
* and every send bumps the row's recency; server rows take over under the same identity
|
|
30
|
-
* as events land. Identity-stable — safe to hand to memoized chat surfaces.
|
|
31
|
-
*/
|
|
32
23
|
notifyThreadActivity: (thread: AgentThreadItem, prompt?: string) => void;
|
|
33
|
-
/**
|
|
34
|
-
* Rename a conversation. Optimistic — the list shows the new title immediately and
|
|
35
|
-
* rolls back on failure (the promise rejects so callers can surface an error).
|
|
36
|
-
*/
|
|
37
24
|
renameThread: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">, title: string) => Promise<void>;
|
|
38
|
-
/** First page (or refresh) in flight. */
|
|
39
25
|
isLoading: boolean;
|
|
40
|
-
/** A cursor page is being appended. */
|
|
41
26
|
isLoadingMore: boolean;
|
|
42
27
|
error: Error | null;
|
|
43
28
|
hasMore: boolean;
|
|
44
29
|
loadMore: () => Promise<void>;
|
|
45
|
-
/**
|
|
46
|
-
* Re-walk every loaded page. Freshness is otherwise live (the hook subscribes to
|
|
47
|
-
* AGENT_THREAD_UPDATED itself) — keep refresh for recovery, e.g. a throttled
|
|
48
|
-
* window-focus refresh after a dropped connection.
|
|
49
|
-
*/
|
|
50
30
|
refresh: () => Promise<void>;
|
|
51
31
|
}
|
|
52
|
-
/**
|
|
53
|
-
* The platform inbox's fixed chatId for an agent: one shared value per agent block, so
|
|
54
|
-
* index rows are recognizable as "direct chats with this agent on the platform" at a
|
|
55
|
-
* glance (and filterable via the endpoint's chatId param). Conversations under it are
|
|
56
|
-
* distinguished by their unique chatThreadId.
|
|
57
|
-
*/
|
|
58
32
|
export declare const AGENT_INBOX_CHAT_PREFIX = "agent-inbox-";
|
|
59
33
|
export declare const agentInboxChatId: (agentId: string) => string;
|
|
60
|
-
/**
|
|
61
|
-
* Short human label for a row with no generated title yet: the half of the identity
|
|
62
|
-
* pair that varies for the row. Inbox rows share one fixed chatId (above) and vary by
|
|
63
|
-
* thread id; rows from other surfaces (published-app chats) are the reverse. One-sided
|
|
64
|
-
* writers (agent calls) index a "_" placeholder for the missing half — fall back to
|
|
65
|
-
* the real half rather than labeling every such row "_".
|
|
66
|
-
*/
|
|
67
34
|
export declare const agentThreadShortLabel: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => string;
|
|
68
|
-
/** How a row is labelled: its title, or the id-based short label while it has none. */
|
|
69
35
|
export declare const agentThreadDisplayTitle: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId" | "title">) => string;
|
|
70
|
-
/**
|
|
71
|
-
* Headless agent-threads index: fetches an agent's conversation threads from the platform
|
|
72
|
-
* endpoint with keyset-cursor pagination, then keeps them live — every threads-table write
|
|
73
|
-
* publishes AGENT_THREAD_UPDATED on the app's Pusher channel and the hook upserts the row,
|
|
74
|
-
* falling back to the getOne endpoint when a payload lacks the full row. New conversations
|
|
75
|
-
* list optimistically the moment the first prompt is sent (wire notifyThreadActivity to the
|
|
76
|
-
* chat's send seam). The LIST only — which conversation is open is the consumer's (a
|
|
77
|
-
* conversation is addressed by its (chatId, chatThreadId) pair; a new one is a fresh UUID
|
|
78
|
-
* thread id under the surface's chatId — CopilotKit replaces non-UUID ids per load).
|
|
79
|
-
* Pair it with whatever list UI the host app uses. No per-host wiring: both hosts already
|
|
80
|
-
* run the shared websocketsService singleton and subscribe the app channel (the platform
|
|
81
|
-
* via its layouts' useWebsockets hooks, a boilerplate app via the client SDK's
|
|
82
|
-
* ClientProvider), and listening before the socket connects just parks the handler.
|
|
83
|
-
*/
|
|
36
|
+
/** Agent-threads index with pagination, realtime upserts, and optimistic send listing. */
|
|
84
37
|
export declare const useAgentThreads: (options: UseAgentThreadsOptions) => UseAgentThreadsResult;
|
|
85
38
|
export default useAgentThreads;
|
|
86
39
|
//# sourceMappingURL=use-agent-threads.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-threads.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-threads.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,
|
|
1
|
+
{"version":3,"file":"use-agent-threads.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-threads.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EAUvB,MAAM,4BAA4B,CAAC;AAEpC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,6BAA6B,EAC7B,KAAK,oCAAoC,EACzC,KAAK,mCAAmC,GACzC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,6BAA6B,EAClC,yBAAyB,EACzB,KAAK,8BAA8B,EACnC,KAAK,kBAAkB,EACvB,2BAA2B,GAC5B,MAAM,8BAA8B,CAAC;AAItC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,GAAG,WAAW,CAAC;IACpC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,oBAAoB,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,YAAY,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzG,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAAgD,CAAC;AAEpG,eAAO,MAAM,qBAAqB,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,KAAG,MAIhG,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,GAAG,OAAO,CAAC,KAAG,MACpD,CAAC;AAuC1D,0FAA0F;AAC1F,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,qBA6UjE,CAAC;AAEF,eAAe,eAAe,CAAC"}
|