@flamingo-stack/openframe-frontend-core 0.0.678 → 0.0.679-2445.7368.1
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/{chunk-EXQPK67M.cjs → chunk-25Y3UR3R.cjs} +10 -10
- package/dist/{chunk-EXQPK67M.cjs.map → chunk-25Y3UR3R.cjs.map} +1 -1
- package/dist/{chunk-EPH3FGIF.js → chunk-2YMFBKYR.js} +3 -3
- package/dist/{chunk-RI44VAGW.cjs → chunk-3LW6AJMJ.cjs} +338 -338
- package/dist/{chunk-RI44VAGW.cjs.map → chunk-3LW6AJMJ.cjs.map} +1 -1
- package/dist/{chunk-MB5JXDJK.js → chunk-4ZKZSWX4.js} +3 -3
- package/dist/{chunk-33HTSSCG.js → chunk-6LN7ZNHT.js} +3 -3
- package/dist/{chunk-S3NK47ZJ.js → chunk-7WGUMITJ.js} +2 -2
- package/dist/{chunk-UBAXZQKE.js → chunk-CDMZOXS7.js} +3 -3
- package/dist/{chunk-XZG2NZLL.cjs → chunk-CGYFCEHM.cjs} +10 -10
- package/dist/{chunk-XZG2NZLL.cjs.map → chunk-CGYFCEHM.cjs.map} +1 -1
- package/dist/{chunk-RBBFGN6B.cjs → chunk-CY3XHQM3.cjs} +5 -5
- package/dist/{chunk-RBBFGN6B.cjs.map → chunk-CY3XHQM3.cjs.map} +1 -1
- package/dist/{chunk-STVG6WV4.js → chunk-EEL4RGHX.js} +2 -2
- package/dist/{chunk-HEPOYA2M.js → chunk-FQDHQNSU.js} +31 -10
- package/dist/{chunk-HEPOYA2M.js.map → chunk-FQDHQNSU.js.map} +1 -1
- package/dist/{chunk-D5FLAQQH.js → chunk-FUBOKAYO.js} +5 -5
- package/dist/{chunk-L3O2HPOO.cjs → chunk-GZIYE35W.cjs} +15 -15
- package/dist/{chunk-L3O2HPOO.cjs.map → chunk-GZIYE35W.cjs.map} +1 -1
- package/dist/{chunk-GPSL3KFX.cjs → chunk-H27EHTLX.cjs} +3 -3
- package/dist/{chunk-GPSL3KFX.cjs.map → chunk-H27EHTLX.cjs.map} +1 -1
- package/dist/{chunk-CPJ7QBIV.cjs → chunk-HA3SBMR2.cjs} +4 -4
- package/dist/{chunk-CPJ7QBIV.cjs.map → chunk-HA3SBMR2.cjs.map} +1 -1
- package/dist/{chunk-7IYDTCKE.js → chunk-HLMMG6Q7.js} +5 -5
- package/dist/{chunk-OAS4JLF4.cjs → chunk-HTNUTEXN.cjs} +22 -22
- package/dist/{chunk-OAS4JLF4.cjs.map → chunk-HTNUTEXN.cjs.map} +1 -1
- package/dist/{chunk-GNOCB662.js → chunk-ICGGFQQU.js} +4 -4
- package/dist/{chunk-X2ORWRBO.js → chunk-IPZCEAAH.js} +2 -2
- package/dist/{chunk-DZR47TRO.cjs → chunk-KNPUHPIF.cjs} +33 -33
- package/dist/{chunk-DZR47TRO.cjs.map → chunk-KNPUHPIF.cjs.map} +1 -1
- package/dist/{chunk-N4HJDYMG.js → chunk-L4NLXAET.js} +2 -2
- package/dist/{chunk-THWEZULI.cjs → chunk-MGSWL3L3.cjs} +26 -26
- package/dist/{chunk-THWEZULI.cjs.map → chunk-MGSWL3L3.cjs.map} +1 -1
- package/dist/{chunk-J7VFEMTJ.js → chunk-NQCID4VF.js} +6 -6
- package/dist/{chunk-FBQSQRUY.cjs → chunk-QU7SDFKO.cjs} +71 -71
- package/dist/{chunk-FBQSQRUY.cjs.map → chunk-QU7SDFKO.cjs.map} +1 -1
- package/dist/{chunk-R5B4T26B.cjs → chunk-RJYYGWJL.cjs} +22 -22
- package/dist/{chunk-R5B4T26B.cjs.map → chunk-RJYYGWJL.cjs.map} +1 -1
- package/dist/{chunk-JHSDZI3D.cjs → chunk-T7GIFC5Y.cjs} +31 -10
- package/dist/chunk-T7GIFC5Y.cjs.map +1 -0
- package/dist/{chunk-K7YEA2LO.cjs → chunk-TKL5JKZS.cjs} +10 -10
- package/dist/{chunk-K7YEA2LO.cjs.map → chunk-TKL5JKZS.cjs.map} +1 -1
- package/dist/{chunk-2752B3YY.cjs → chunk-UAPSU6YW.cjs} +3 -3
- package/dist/{chunk-2752B3YY.cjs.map → chunk-UAPSU6YW.cjs.map} +1 -1
- package/dist/{chunk-CUGM36ZU.js → chunk-UKZAW4FE.js} +3 -3
- package/dist/{chunk-MP2UTOLN.js → chunk-XSSYG7KU.js} +5 -5
- package/dist/{chunk-G4EXCWR4.cjs → chunk-ZB34GLOZ.cjs} +45 -45
- package/dist/{chunk-G4EXCWR4.cjs.map → chunk-ZB34GLOZ.cjs.map} +1 -1
- package/dist/{chunk-EWNKYH2D.js → chunk-ZRN244JV.js} +2 -2
- package/dist/components/case-studies/index.cjs +14 -14
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-attachments.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +73 -73
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +39 -39
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +25 -25
- package/dist/components/meeting-scheduler/index.js +2 -2
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/index.cjs +7 -7
- package/dist/index.js +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/embeddable-chat-composer-scope.test.tsx +234 -0
- package/src/components/chat/embeddable-chat.tsx +63 -18
- package/src/components/chat/hooks/use-chat-attachments.ts +4 -1
- package/dist/chunk-JHSDZI3D.cjs.map +0 -1
- /package/dist/{chunk-EPH3FGIF.js.map → chunk-2YMFBKYR.js.map} +0 -0
- /package/dist/{chunk-MB5JXDJK.js.map → chunk-4ZKZSWX4.js.map} +0 -0
- /package/dist/{chunk-33HTSSCG.js.map → chunk-6LN7ZNHT.js.map} +0 -0
- /package/dist/{chunk-S3NK47ZJ.js.map → chunk-7WGUMITJ.js.map} +0 -0
- /package/dist/{chunk-UBAXZQKE.js.map → chunk-CDMZOXS7.js.map} +0 -0
- /package/dist/{chunk-STVG6WV4.js.map → chunk-EEL4RGHX.js.map} +0 -0
- /package/dist/{chunk-D5FLAQQH.js.map → chunk-FUBOKAYO.js.map} +0 -0
- /package/dist/{chunk-7IYDTCKE.js.map → chunk-HLMMG6Q7.js.map} +0 -0
- /package/dist/{chunk-GNOCB662.js.map → chunk-ICGGFQQU.js.map} +0 -0
- /package/dist/{chunk-X2ORWRBO.js.map → chunk-IPZCEAAH.js.map} +0 -0
- /package/dist/{chunk-N4HJDYMG.js.map → chunk-L4NLXAET.js.map} +0 -0
- /package/dist/{chunk-J7VFEMTJ.js.map → chunk-NQCID4VF.js.map} +0 -0
- /package/dist/{chunk-CUGM36ZU.js.map → chunk-UKZAW4FE.js.map} +0 -0
- /package/dist/{chunk-MP2UTOLN.js.map → chunk-XSSYG7KU.js.map} +0 -0
- /package/dist/{chunk-EWNKYH2D.js.map → chunk-ZRN244JV.js.map} +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer belongs to the conversation on screen — pinned.
|
|
3
|
+
*
|
|
4
|
+
* `ChatInput` is an uncontrolled contenteditable that owns its draft, and the
|
|
5
|
+
* slash-command menu is derived from that draft (`/` at the start keeps it
|
|
6
|
+
* open). The panel already dropped the staged context chips on a dialog
|
|
7
|
+
* change, but the text stayed, so opening another conversation — from the
|
|
8
|
+
* list, or from a notification the host resolves straight into the store —
|
|
9
|
+
* landed with the previous one's half-typed `/` and its menu still open over
|
|
10
|
+
* the new thread. The composer is now keyed on the conversation: leaving one
|
|
11
|
+
* remounts it fresh in the same commit.
|
|
12
|
+
*
|
|
13
|
+
* The one id change that must NOT reset it: a send from the new-chat surface
|
|
14
|
+
* creates the dialog, and the id the panel then lands on is this conversation
|
|
15
|
+
* being named. The editor stays typable while that first turn streams, so a
|
|
16
|
+
* follow-up already being typed has to survive.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
20
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
21
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
22
|
+
import { createRef } from 'react';
|
|
23
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
24
|
+
|
|
25
|
+
// Identity + every other authed fetch: a promise that never settles keeps the
|
|
26
|
+
// panel offline and free of state updates landing outside `act`.
|
|
27
|
+
vi.mock('../../../utils/embed-authed-fetch', () => ({
|
|
28
|
+
embedAuthedFetch: vi.fn(() => new Promise(() => {})),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// One canned command, so `/` opens a real suggestions menu.
|
|
32
|
+
vi.mock('../hooks/use-slash-commands', async importOriginal => {
|
|
33
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
fetchSlashCommands: vi.fn(() =>
|
|
37
|
+
Promise.resolve([
|
|
38
|
+
{
|
|
39
|
+
id: 'roadmap',
|
|
40
|
+
label: 'ClickUp Roadmap',
|
|
41
|
+
description: 'Browse the public ClickUp roadmap',
|
|
42
|
+
argumentHint: '[task id or name]',
|
|
43
|
+
actions: [{ id: 'recent', label: 'Recent' }],
|
|
44
|
+
},
|
|
45
|
+
]),
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
import { ChatRuntimeContext, type ChatRuntime } from '../../../contexts/chat-runtime-context';
|
|
51
|
+
import { EmbeddableChat, type EmbeddableChatHandle } from '../embeddable-chat';
|
|
52
|
+
import type { UnifiedChatState } from '../types/unified-chat-state.types';
|
|
53
|
+
|
|
54
|
+
const runtime: ChatRuntime = {
|
|
55
|
+
endpoints: {
|
|
56
|
+
chatStreamUrl: '/api/docs/chat',
|
|
57
|
+
approvalToolUrl: '/api/chat/agent/confirm-tool',
|
|
58
|
+
commandsUrl: '/api/docs/commands',
|
|
59
|
+
buildListUrl: () => null,
|
|
60
|
+
attachmentUploadUrl: '/api/storage/generate-upload-url',
|
|
61
|
+
attachmentViewUrlPrefix: '/api/storage/view/chat-attachments/',
|
|
62
|
+
identityUrl: '/api/chat/identity',
|
|
63
|
+
},
|
|
64
|
+
navigation: { mode: 'host' },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const MESSAGE: UnifiedChatState['messages'][number] = {
|
|
68
|
+
id: 'm-1',
|
|
69
|
+
role: 'user',
|
|
70
|
+
content: 'hello',
|
|
71
|
+
timestamp: new Date('2026-01-01T00:00:00Z'),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The injected-state path (`mingoState`): the panel renders this object and
|
|
75
|
+
* opens no transport, so the test drives `activeDialogId` by re-rendering. */
|
|
76
|
+
function createState(overrides: Partial<UnifiedChatState>): UnifiedChatState {
|
|
77
|
+
const noop = () => {};
|
|
78
|
+
const asyncNoop = async () => {};
|
|
79
|
+
return {
|
|
80
|
+
messages: [],
|
|
81
|
+
isLoading: false,
|
|
82
|
+
streamingPhase: 'idle',
|
|
83
|
+
sendMessage: asyncNoop,
|
|
84
|
+
stopMessage: noop,
|
|
85
|
+
clearMessages: noop,
|
|
86
|
+
discussRef: noop,
|
|
87
|
+
displayRef: noop,
|
|
88
|
+
currentProvider: 'anthropic',
|
|
89
|
+
currentModelLabel: 'Claude',
|
|
90
|
+
currentContextWindowMaxTokens: null,
|
|
91
|
+
currentInputTokens: null,
|
|
92
|
+
currentOutputTokens: null,
|
|
93
|
+
currentCacheHitRatePct: null,
|
|
94
|
+
currentUsageBreakdown: null,
|
|
95
|
+
dialogs: [
|
|
96
|
+
{ id: 'd-1', title: 'First' },
|
|
97
|
+
{ id: 'd-2', title: 'Second' },
|
|
98
|
+
],
|
|
99
|
+
activeDialogId: null,
|
|
100
|
+
selectDialog: noop,
|
|
101
|
+
startNewDialog: () => Promise.resolve(null),
|
|
102
|
+
deleteDialog: asyncNoop,
|
|
103
|
+
renameDialog: asyncNoop,
|
|
104
|
+
archiveDialog: asyncNoop,
|
|
105
|
+
isDialogsLoading: false,
|
|
106
|
+
dialogsError: false,
|
|
107
|
+
reloadDialogs: noop,
|
|
108
|
+
isMessagesLoading: false,
|
|
109
|
+
hasMoreDialogs: false,
|
|
110
|
+
loadMoreDialogs: asyncNoop,
|
|
111
|
+
hasMoreMessages: false,
|
|
112
|
+
loadMoreMessages: asyncNoop,
|
|
113
|
+
approveRequest: asyncNoop,
|
|
114
|
+
rejectRequest: asyncNoop,
|
|
115
|
+
dialogTokenUsage: null,
|
|
116
|
+
connectionState: 'connected',
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Harness({ state, handleRef }: { state: UnifiedChatState; handleRef?: React.Ref<EmbeddableChatHandle> }) {
|
|
122
|
+
return (
|
|
123
|
+
<ChatRuntimeContext.Provider value={runtime}>
|
|
124
|
+
<DialogPrimitive.Root open>
|
|
125
|
+
<EmbeddableChat
|
|
126
|
+
ref={handleRef}
|
|
127
|
+
shell="none"
|
|
128
|
+
defaultOpen
|
|
129
|
+
defaultActiveMode="mingo"
|
|
130
|
+
showInternalTrigger={false}
|
|
131
|
+
mingoState={state}
|
|
132
|
+
mingoDialogCapabilities={{ canRename: true, canArchive: true }}
|
|
133
|
+
/>
|
|
134
|
+
</DialogPrimitive.Root>
|
|
135
|
+
</ChatRuntimeContext.Provider>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderPanel(state: UnifiedChatState, handleRef?: React.Ref<EmbeddableChatHandle>) {
|
|
140
|
+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
141
|
+
const view = render(
|
|
142
|
+
<QueryClientProvider client={client}>
|
|
143
|
+
<Harness state={state} handleRef={handleRef} />
|
|
144
|
+
</QueryClientProvider>,
|
|
145
|
+
);
|
|
146
|
+
return {
|
|
147
|
+
...view,
|
|
148
|
+
rerender: (next: UnifiedChatState) =>
|
|
149
|
+
view.rerender(
|
|
150
|
+
<QueryClientProvider client={client}>
|
|
151
|
+
<Harness state={next} handleRef={handleRef} />
|
|
152
|
+
</QueryClientProvider>,
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const editor = () => screen.getByRole('textbox', { name: 'Ask a question...' });
|
|
158
|
+
|
|
159
|
+
/** Type into the uncontrolled editor the way the browser does: mutate the DOM,
|
|
160
|
+
* then fire `input` so the component reads it back. */
|
|
161
|
+
function type(text: string) {
|
|
162
|
+
const el = editor();
|
|
163
|
+
el.textContent = `${el.textContent ?? ''}${text}`;
|
|
164
|
+
fireEvent.input(el);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const menu = () => screen.queryByRole('menu', { name: 'Slash command suggestions' });
|
|
168
|
+
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
vi.useRealTimers();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('EmbeddableChat — the composer belongs to the conversation on screen', () => {
|
|
174
|
+
it('opening another dialog drops the draft and the slash menu it kept open', async () => {
|
|
175
|
+
const { rerender } = renderPanel(createState({ activeDialogId: 'd-1', messages: [MESSAGE] }));
|
|
176
|
+
|
|
177
|
+
type('/');
|
|
178
|
+
expect(await screen.findByRole('menu', { name: 'Slash command suggestions' })).toBeInTheDocument();
|
|
179
|
+
expect(editor()).toHaveTextContent('/');
|
|
180
|
+
|
|
181
|
+
rerender(createState({ activeDialogId: 'd-2', messages: [MESSAGE] }));
|
|
182
|
+
|
|
183
|
+
expect(menu()).not.toBeInTheDocument();
|
|
184
|
+
expect(editor()).toHaveTextContent('');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('opening a dialog from the new-chat surface drops the draft typed there', async () => {
|
|
188
|
+
const handle = createRef<EmbeddableChatHandle>();
|
|
189
|
+
const { rerender } = renderPanel(createState({ activeDialogId: null }), handle);
|
|
190
|
+
// Narrow layout: the list has no composer; the compose view does.
|
|
191
|
+
act(() => handle.current?.startNewChat());
|
|
192
|
+
|
|
193
|
+
type('/');
|
|
194
|
+
expect(await screen.findByRole('menu', { name: 'Slash command suggestions' })).toBeInTheDocument();
|
|
195
|
+
|
|
196
|
+
rerender(createState({ activeDialogId: 'd-1', messages: [MESSAGE] }));
|
|
197
|
+
|
|
198
|
+
expect(menu()).not.toBeInTheDocument();
|
|
199
|
+
expect(editor()).toHaveTextContent('');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('a new chat being named by its first send keeps the follow-up already being typed', async () => {
|
|
203
|
+
let settleSend: () => void = () => {};
|
|
204
|
+
const sendMessage = vi.fn<UnifiedChatState['sendMessage']>(
|
|
205
|
+
() =>
|
|
206
|
+
new Promise<void>(resolve => {
|
|
207
|
+
settleSend = resolve;
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
const handle = createRef<EmbeddableChatHandle>();
|
|
211
|
+
const { rerender } = renderPanel(createState({ activeDialogId: null, sendMessage }), handle);
|
|
212
|
+
act(() => handle.current?.startNewChat());
|
|
213
|
+
|
|
214
|
+
type('first question');
|
|
215
|
+
fireEvent.keyDown(editor(), { key: 'Enter' });
|
|
216
|
+
expect(sendMessage.mock.calls[0]?.[0]).toBe('first question');
|
|
217
|
+
expect(editor()).toHaveTextContent('');
|
|
218
|
+
|
|
219
|
+
// The host is creating the dialog; the user is already typing the next one.
|
|
220
|
+
type('and also');
|
|
221
|
+
rerender(createState({ activeDialogId: 'd-new', messages: [MESSAGE], sendMessage }));
|
|
222
|
+
expect(editor()).toHaveTextContent('and also');
|
|
223
|
+
|
|
224
|
+
await act(async () => {
|
|
225
|
+
settleSend();
|
|
226
|
+
await Promise.resolve();
|
|
227
|
+
});
|
|
228
|
+
expect(editor()).toHaveTextContent('and also');
|
|
229
|
+
|
|
230
|
+
// A real switch afterwards still resets.
|
|
231
|
+
rerender(createState({ activeDialogId: 'd-1', messages: [MESSAGE], sendMessage }));
|
|
232
|
+
expect(editor()).toHaveTextContent('');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -95,7 +95,11 @@ import { SourceActionButton } from './source-action-button';
|
|
|
95
95
|
import type { ChatInputRef, SlashCommandActionId } from './types/component.types';
|
|
96
96
|
import type { ChatContextItem, ChatContextPickerConfig } from './types/context-item.types';
|
|
97
97
|
import type { MessageSegment, Message } from './types/message.types';
|
|
98
|
-
import type {
|
|
98
|
+
import type {
|
|
99
|
+
ChatDialogCapabilities,
|
|
100
|
+
UnifiedChatState,
|
|
101
|
+
UnifiedSendMessageOptions,
|
|
102
|
+
} from './types/unified-chat-state.types';
|
|
99
103
|
import { formatChatAttachmentMarkdownForBubble } from './utils/chat-attachment-markdown';
|
|
100
104
|
import { resolveHrefForRuntime } from './utils/chat-nav-resolution';
|
|
101
105
|
import { chatChipClass } from './utils/chip-styles';
|
|
@@ -1215,7 +1219,7 @@ function EmbeddableChatInner({
|
|
|
1215
1219
|
const {
|
|
1216
1220
|
messages: rawMessages,
|
|
1217
1221
|
isLoading: chatLoading,
|
|
1218
|
-
sendMessage,
|
|
1222
|
+
sendMessage: sendMessageRaw,
|
|
1219
1223
|
discussRef,
|
|
1220
1224
|
stopMessage,
|
|
1221
1225
|
clearMessages,
|
|
@@ -1257,6 +1261,29 @@ function EmbeddableChatInner({
|
|
|
1257
1261
|
// adapters already know whether they own a list. Absent = single-thread.
|
|
1258
1262
|
const historyListMode = dialogCapabilities !== undefined;
|
|
1259
1263
|
|
|
1264
|
+
// A send from the new-chat surface asks the host to create the dialog, and
|
|
1265
|
+
// the id the panel then lands on is THIS conversation being named — not the
|
|
1266
|
+
// user leaving it. The editor stays typable while the first turn streams, so
|
|
1267
|
+
// a follow-up already being typed has to survive that transition; the
|
|
1268
|
+
// composer-scope reset below reads this flag to tell the two apart. Armed in
|
|
1269
|
+
// the send's own event (one batch with the composer's own clear, so it is
|
|
1270
|
+
// set by the render the id lands in) and dropped once the send settles either
|
|
1271
|
+
// way: a create that failed would otherwise leave it armed for the next real
|
|
1272
|
+
// switch. Every send path goes through here — the composer, a clarification
|
|
1273
|
+
// card, a quick-action chip, the Guide launcher prompt.
|
|
1274
|
+
const [namingNewChat, setNamingNewChat] = useState(false);
|
|
1275
|
+
const sendMessage = useCallback(
|
|
1276
|
+
async (text: string, options?: UnifiedSendMessageOptions) => {
|
|
1277
|
+
if (activeDialogId == null) setNamingNewChat(true);
|
|
1278
|
+
try {
|
|
1279
|
+
return await sendMessageRaw(text, options);
|
|
1280
|
+
} finally {
|
|
1281
|
+
setNamingNewChat(false);
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
[activeDialogId, sendMessageRaw],
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1260
1287
|
// Non-optional view of the same object for the JSX below. `canRename` /
|
|
1261
1288
|
// `canArchive` default OFF: the ⋯ menu must never advertise an action
|
|
1262
1289
|
// nothing has wired.
|
|
@@ -1333,25 +1360,42 @@ function EmbeddableChatInner({
|
|
|
1333
1360
|
// Source of truth = the input text — deleting a token's chip drops its item.
|
|
1334
1361
|
const mentionKeysRef = useRef<Set<string>>(new Set());
|
|
1335
1362
|
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1363
|
+
// The composer belongs to the conversation on screen. Leaving it — another
|
|
1364
|
+
// dialog opened from the list, a notification or a link, back to the list, a
|
|
1365
|
+
// new chat, the Mingo ⇄ Guide toggle — drops everything unsent: the draft
|
|
1366
|
+
// text (and with it the slash-command menu that text keeps open), the staged
|
|
1367
|
+
// context chips and the `@` / `+` picker, and the staged uploads. Nothing
|
|
1368
|
+
// typed for one conversation may bleed into another — or into a Guide send,
|
|
1369
|
+
// where the SSE transport silently drops `contextItems`. Mirrors the
|
|
1370
|
+
// post-send reset in `handleSend`.
|
|
1371
|
+
// The draft lives in an UNCONTROLLED contenteditable, so it is dropped the
|
|
1372
|
+
// React way: `composerKey` remounts the composer subtree, in the same commit
|
|
1373
|
+
// the new conversation renders in. An effect-time `clear()` painted one frame
|
|
1374
|
+
// of the old draft — menu open — under the new conversation, and so did
|
|
1375
|
+
// clearing the chips from an effect. So the STATE halves are adjusted while
|
|
1376
|
+
// rendering (React's documented pattern for a reset the render already has
|
|
1377
|
+
// the answer to); the ref and attachment bookkeeping stays in an effect: a ref
|
|
1378
|
+
// must not be written during render, and aborting uploads is a side effect.
|
|
1379
|
+
// The one id change that is NOT a switch: `null → id` while a send from the
|
|
1380
|
+
// new-chat surface is naming it (`namingNewChat` above).
|
|
1381
|
+
const scopeDialogId = activeDialogId ?? null;
|
|
1382
|
+
const [composerScope, setComposerScope] = useState({ dialogId: scopeDialogId, mode: activeMode });
|
|
1383
|
+
const [composerKey, setComposerKey] = useState(0);
|
|
1384
|
+
if (composerScope.dialogId !== scopeDialogId || composerScope.mode !== activeMode) {
|
|
1385
|
+
const named =
|
|
1386
|
+
namingNewChat && composerScope.dialogId == null && scopeDialogId != null && composerScope.mode === activeMode;
|
|
1387
|
+
setComposerScope({ dialogId: scopeDialogId, mode: activeMode });
|
|
1388
|
+
if (!named) {
|
|
1389
|
+
setComposerKey(k => k + 1);
|
|
1390
|
+
setContextItems(NO_CONTEXT_ITEMS);
|
|
1391
|
+
setContextPickerOpen(false);
|
|
1392
|
+
setMentionQuery(null);
|
|
1393
|
+
}
|
|
1351
1394
|
}
|
|
1352
1395
|
useEffect(() => {
|
|
1353
1396
|
mentionKeysRef.current.clear();
|
|
1354
|
-
|
|
1397
|
+
clearAttachments();
|
|
1398
|
+
}, [composerKey, clearAttachments]);
|
|
1355
1399
|
|
|
1356
1400
|
// Map each entity type to its backend mention marker (host-declared on the
|
|
1357
1401
|
// entity type). Drives the committed `@marker:id` token; missing markers fall
|
|
@@ -2661,6 +2705,7 @@ function EmbeddableChatInner({
|
|
|
2661
2705
|
/>
|
|
2662
2706
|
|
|
2663
2707
|
<ChatComposer
|
|
2708
|
+
key={composerKey}
|
|
2664
2709
|
archived={isViewingArchived}
|
|
2665
2710
|
lock={composerLock}
|
|
2666
2711
|
inputRef={chatInputRef}
|
|
@@ -204,7 +204,10 @@ export function useChatAttachments(): UseChatAttachmentsApi {
|
|
|
204
204
|
const clear = useCallback(() => {
|
|
205
205
|
for (const ctrl of controllersRef.current.values()) ctrl.abort();
|
|
206
206
|
controllersRef.current.clear();
|
|
207
|
-
|
|
207
|
+
// Same array back when there is nothing staged, so React bails out of the
|
|
208
|
+
// update: the composer-scope effect calls this on mount and on every
|
|
209
|
+
// conversation switch, and an empty strip must not cost a render.
|
|
210
|
+
setAttachments(prev => (prev.length === 0 ? prev : []));
|
|
208
211
|
}, []);
|
|
209
212
|
|
|
210
213
|
const uploadOne = useCallback(
|