@sybilion/uilib 1.2.19 → 1.2.21
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/esm/components/ui/Chat/ChatChrome/ChatChrome.js +1 -1
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +4 -3
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +11 -0
- package/dist/esm/components/ui/Chat/ChatMessage/icons/CsvIcon.js +8 -0
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +12 -18
- package/dist/esm/components/ui/Chat/chat-preset-utils.js +12 -3
- package/dist/esm/contexts/chat-context.js +69 -10
- package/dist/esm/index.js +1 -1
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +14 -0
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.d.ts +4 -0
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/icons/CsvIcon.d.ts +3 -0
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
- package/dist/esm/types/src/contexts/chat-context.d.ts +21 -7
- package/dist/esm/types/src/docs/pages/ChatUserCsvAttachmentPage.d.ts +1 -0
- package/dist/esm/types/src/utils/downloadTextFile.d.ts +2 -0
- package/dist/esm/utils/downloadTextFile.js +14 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/Chat.types.ts +16 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +1 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +67 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl.d.ts +6 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +8 -1
- package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +36 -0
- package/src/components/ui/Chat/ChatMessage/icons/CsvIcon.tsx +7 -0
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +15 -15
- package/src/components/ui/Chat/chat-preset-utils.ts +12 -6
- package/src/components/ui/Chat/index.ts +3 -1
- package/src/contexts/chat-context.tsx +124 -13
- package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +171 -0
- package/src/docs/registry.ts +6 -0
- package/src/utils/downloadTextFile.ts +16 -0
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
.role-user
|
|
18
18
|
align-items flex-end
|
|
19
19
|
|
|
20
|
+
.userColumn
|
|
21
|
+
display flex
|
|
22
|
+
flex-direction column
|
|
23
|
+
align-items flex-end
|
|
24
|
+
gap var(--p-2)
|
|
25
|
+
max-width 100%
|
|
26
|
+
|
|
20
27
|
.text
|
|
21
28
|
padding var(--p-3) var(--p-4)
|
|
22
29
|
|
|
@@ -29,6 +36,66 @@
|
|
|
29
36
|
:global(.dark) &
|
|
30
37
|
background-color var(--sb-gray-800)
|
|
31
38
|
|
|
39
|
+
.userCsvCard
|
|
40
|
+
appearance none
|
|
41
|
+
border 0
|
|
42
|
+
margin 0
|
|
43
|
+
font inherit
|
|
44
|
+
display flex
|
|
45
|
+
align-items center
|
|
46
|
+
gap var(--p-4)
|
|
47
|
+
padding var(--p-3)
|
|
48
|
+
padding-right var(--p-4)
|
|
49
|
+
background-color var(--sb-slate-100)
|
|
50
|
+
box-shadow 0 0 0 1px var(--border)
|
|
51
|
+
border-radius var(--p-4)
|
|
52
|
+
border-bottom-right-radius 0
|
|
53
|
+
width fit-content
|
|
54
|
+
max-width 100%
|
|
55
|
+
text-align left
|
|
56
|
+
cursor pointer
|
|
57
|
+
transition background-color 150ms
|
|
58
|
+
color var(--sb-green-600)
|
|
59
|
+
|
|
60
|
+
&:hover
|
|
61
|
+
background-color var(--sb-gray-50)
|
|
62
|
+
|
|
63
|
+
&:focus-visible
|
|
64
|
+
outline 2px solid var(--ring)
|
|
65
|
+
outline-offset 2px
|
|
66
|
+
|
|
67
|
+
:global(.dark) &
|
|
68
|
+
background-color var(--sb-gray-800)
|
|
69
|
+
color var(--sb-green-400)
|
|
70
|
+
|
|
71
|
+
&:hover
|
|
72
|
+
background-color var(--sb-gray-900)
|
|
73
|
+
|
|
74
|
+
.userCsvCardIcon
|
|
75
|
+
display flex
|
|
76
|
+
align-items center
|
|
77
|
+
justify-content center
|
|
78
|
+
width 32px
|
|
79
|
+
height 32px
|
|
80
|
+
flex-shrink 0
|
|
81
|
+
|
|
82
|
+
.userCsvCardContent
|
|
83
|
+
display flex
|
|
84
|
+
flex-direction column
|
|
85
|
+
flex 1
|
|
86
|
+
min-width 0
|
|
87
|
+
|
|
88
|
+
.userCsvCardTitle
|
|
89
|
+
font-size var(--text-base)
|
|
90
|
+
font-weight 600
|
|
91
|
+
line-height 1.4
|
|
92
|
+
color var(--text-secondary)
|
|
93
|
+
|
|
94
|
+
.userCsvCardSubtitle
|
|
95
|
+
font-size var(--text-sm)
|
|
96
|
+
color var(--muted-foreground)
|
|
97
|
+
line-height 1.4
|
|
98
|
+
|
|
32
99
|
.role-system
|
|
33
100
|
align-items center
|
|
34
101
|
|
|
@@ -17,6 +17,12 @@ interface CssExports {
|
|
|
17
17
|
'root': string;
|
|
18
18
|
'scrollHorizontal': string;
|
|
19
19
|
'text': string;
|
|
20
|
+
'userColumn': string;
|
|
21
|
+
'userCsvCard': string;
|
|
22
|
+
'userCsvCardContent': string;
|
|
23
|
+
'userCsvCardIcon': string;
|
|
24
|
+
'userCsvCardSubtitle': string;
|
|
25
|
+
'userCsvCardTitle': string;
|
|
20
26
|
}
|
|
21
27
|
export const cssExports: CssExports;
|
|
22
28
|
export default cssExports;
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
} from '../Chat.types';
|
|
9
9
|
import { AgentMessageContent } from './AgentMessageContent';
|
|
10
10
|
import S from './ChatMessage.styl';
|
|
11
|
+
import { UserCsvAttachmentBubble } from './UserCsvAttachmentBubble';
|
|
11
12
|
|
|
12
13
|
export function ChatMessage({
|
|
13
14
|
role,
|
|
14
15
|
text,
|
|
16
|
+
userCsvAttachment,
|
|
15
17
|
onQuickReply,
|
|
16
18
|
suppressedQuickReplyKeys,
|
|
17
19
|
quickReplyDisabled,
|
|
@@ -45,7 +47,12 @@ export function ChatMessage({
|
|
|
45
47
|
renderMessageChart={renderMessageChart}
|
|
46
48
|
/>
|
|
47
49
|
) : (
|
|
48
|
-
<div className={S.
|
|
50
|
+
<div className={S.userColumn}>
|
|
51
|
+
<div className={S.text}>{text}</div>
|
|
52
|
+
{userCsvAttachment ? (
|
|
53
|
+
<UserCsvAttachmentBubble attachment={userCsvAttachment} />
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
49
56
|
)}
|
|
50
57
|
</div>
|
|
51
58
|
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { UserCsvAttachment } from '../Chat.types';
|
|
2
|
+
import { downloadTextFile } from '#uilib/utils/downloadTextFile';
|
|
3
|
+
|
|
4
|
+
import { CsvIcon } from './icons/CsvIcon';
|
|
5
|
+
import S from './ChatMessage.styl';
|
|
6
|
+
|
|
7
|
+
const CSV_DOWNLOAD_HINT = 'Download .CSV file';
|
|
8
|
+
|
|
9
|
+
export function UserCsvAttachmentBubble({
|
|
10
|
+
attachment,
|
|
11
|
+
}: {
|
|
12
|
+
attachment: UserCsvAttachment;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
className={S.userCsvCard}
|
|
18
|
+
aria-label={`${CSV_DOWNLOAD_HINT}: ${attachment.displayName}`}
|
|
19
|
+
onClick={() =>
|
|
20
|
+
downloadTextFile(
|
|
21
|
+
attachment.content,
|
|
22
|
+
attachment.filename,
|
|
23
|
+
'text/csv;charset=utf-8',
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
<div className={S.userCsvCardIcon}>
|
|
28
|
+
<CsvIcon size={32} />
|
|
29
|
+
</div>
|
|
30
|
+
<div className={S.userCsvCardContent}>
|
|
31
|
+
<div className={S.userCsvCardTitle}>{attachment.displayName}</div>
|
|
32
|
+
<div className={S.userCsvCardSubtitle}>{CSV_DOWNLOAD_HINT}</div>
|
|
33
|
+
</div>
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
isChatEmpty,
|
|
28
28
|
useChat,
|
|
29
|
+
useChatOutboundPending,
|
|
29
30
|
useChatsForScopeId,
|
|
30
31
|
} from '#uilib/contexts/chat-context';
|
|
31
32
|
import useEvent from '#uilib/hooks/useEvent';
|
|
@@ -112,6 +113,7 @@ export function useChatPanelChromeModel({
|
|
|
112
113
|
getShellWidth,
|
|
113
114
|
setChatPanelOpen,
|
|
114
115
|
} = useSidebar();
|
|
116
|
+
const [localUiBusy, setLocalUiBusy] = useState(false);
|
|
115
117
|
const {
|
|
116
118
|
chats,
|
|
117
119
|
currentChatId,
|
|
@@ -122,6 +124,11 @@ export function useChatPanelChromeModel({
|
|
|
122
124
|
removeMessageById,
|
|
123
125
|
} = useChatsForScopeId(effectiveScopeId);
|
|
124
126
|
const chat = useChat(effectiveScopeId, currentChatId);
|
|
127
|
+
const isOutboundPending = useChatOutboundPending(
|
|
128
|
+
effectiveScopeId,
|
|
129
|
+
currentChatId,
|
|
130
|
+
);
|
|
131
|
+
const isLoading = isOutboundPending || localUiBusy;
|
|
125
132
|
|
|
126
133
|
const {
|
|
127
134
|
searchParams,
|
|
@@ -130,7 +137,6 @@ export function useChatPanelChromeModel({
|
|
|
130
137
|
mutateSearchParams,
|
|
131
138
|
} = useQueryParams();
|
|
132
139
|
const chatOpen = searchParams.has(CHAT_QUERY_PARAM);
|
|
133
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
134
140
|
const [isOpen, setIsOpen] = useState(false);
|
|
135
141
|
/** `?prompt=` deep link text for one-shot composer pre-fill (see ChatPrompt). */
|
|
136
142
|
const [promptLinkPrefill, setPromptLinkPrefill] = useState<string | null>(
|
|
@@ -355,7 +361,7 @@ export function useChatPanelChromeModel({
|
|
|
355
361
|
) {
|
|
356
362
|
if (quickReplyLockRef.current) return;
|
|
357
363
|
quickReplyLockRef.current = true;
|
|
358
|
-
|
|
364
|
+
setLocalUiBusy(true);
|
|
359
365
|
setUsedScriptBranchKeysByChat(prev => ({
|
|
360
366
|
...prev,
|
|
361
367
|
[chatId]: [...new Set([...(prev[chatId] || []), branchKey])],
|
|
@@ -413,7 +419,7 @@ export function useChatPanelChromeModel({
|
|
|
413
419
|
} catch (error) {
|
|
414
420
|
logger.error('Error resolving preset quick reply:', error);
|
|
415
421
|
} finally {
|
|
416
|
-
|
|
422
|
+
setLocalUiBusy(false);
|
|
417
423
|
quickReplyLockRef.current = false;
|
|
418
424
|
}
|
|
419
425
|
})();
|
|
@@ -422,14 +428,11 @@ export function useChatPanelChromeModel({
|
|
|
422
428
|
|
|
423
429
|
endLocalDemoFlow(chatId);
|
|
424
430
|
void (async () => {
|
|
425
|
-
setIsLoading(true);
|
|
426
431
|
try {
|
|
427
432
|
await sendMessage(displayLabel);
|
|
428
433
|
onMessage?.(displayLabel);
|
|
429
434
|
} catch (error) {
|
|
430
435
|
logger.error('Error sending chat message:', error);
|
|
431
|
-
} finally {
|
|
432
|
-
setIsLoading(false);
|
|
433
436
|
}
|
|
434
437
|
})();
|
|
435
438
|
},
|
|
@@ -499,7 +502,7 @@ export function useChatPanelChromeModel({
|
|
|
499
502
|
) {
|
|
500
503
|
if (quickReplyLockRef.current) return;
|
|
501
504
|
quickReplyLockRef.current = true;
|
|
502
|
-
|
|
505
|
+
setLocalUiBusy(true);
|
|
503
506
|
const newAnswers = {
|
|
504
507
|
...intake.answers,
|
|
505
508
|
[intake.scriptStepId]: message,
|
|
@@ -544,7 +547,7 @@ export function useChatPanelChromeModel({
|
|
|
544
547
|
} catch (e) {
|
|
545
548
|
logger.error('Error advancing freeform preset script:', e);
|
|
546
549
|
} finally {
|
|
547
|
-
|
|
550
|
+
setLocalUiBusy(false);
|
|
548
551
|
quickReplyLockRef.current = false;
|
|
549
552
|
}
|
|
550
553
|
})();
|
|
@@ -569,15 +572,12 @@ export function useChatPanelChromeModel({
|
|
|
569
572
|
}
|
|
570
573
|
}
|
|
571
574
|
|
|
572
|
-
setIsLoading(true);
|
|
573
575
|
try {
|
|
574
576
|
if (chatId) endLocalDemoFlow(chatId);
|
|
575
577
|
await sendMessage(message);
|
|
576
578
|
onMessage?.(message);
|
|
577
579
|
} catch (error) {
|
|
578
580
|
logger.error('Error sending chat message:', error);
|
|
579
|
-
} finally {
|
|
580
|
-
setIsLoading(false);
|
|
581
581
|
}
|
|
582
582
|
},
|
|
583
583
|
[
|
|
@@ -613,7 +613,7 @@ export function useChatPanelChromeModel({
|
|
|
613
613
|
return;
|
|
614
614
|
}
|
|
615
615
|
|
|
616
|
-
|
|
616
|
+
setLocalUiBusy(true);
|
|
617
617
|
try {
|
|
618
618
|
if (!currentChatId) return;
|
|
619
619
|
setScriptCompleteByChatId(prev => {
|
|
@@ -683,7 +683,7 @@ export function useChatPanelChromeModel({
|
|
|
683
683
|
} catch (error) {
|
|
684
684
|
logger.error('Error sending chat message:', error);
|
|
685
685
|
} finally {
|
|
686
|
-
|
|
686
|
+
setLocalUiBusy(false);
|
|
687
687
|
}
|
|
688
688
|
};
|
|
689
689
|
|
|
@@ -717,7 +717,7 @@ export function useChatPanelChromeModel({
|
|
|
717
717
|
|
|
718
718
|
const chatId = currentChatId;
|
|
719
719
|
scriptAdvanceLockRef.current = true;
|
|
720
|
-
|
|
720
|
+
setLocalUiBusy(true);
|
|
721
721
|
addMessage(chatId, MessageRole.USER, step.buttonLabel);
|
|
722
722
|
|
|
723
723
|
void (async () => {
|
|
@@ -761,7 +761,7 @@ export function useChatPanelChromeModel({
|
|
|
761
761
|
} catch (error) {
|
|
762
762
|
logger.error('Error advancing preset script:', error);
|
|
763
763
|
} finally {
|
|
764
|
-
|
|
764
|
+
setLocalUiBusy(false);
|
|
765
765
|
scriptAdvanceLockRef.current = false;
|
|
766
766
|
}
|
|
767
767
|
})();
|
|
@@ -11,17 +11,23 @@ export function normalizePresetMatchText(s: string): string {
|
|
|
11
11
|
return s.trim().normalize('NFC');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function presetMatchesUserText(presetText: string, userTextNorm: string): boolean {
|
|
15
|
+
const presetNorm = normalizePresetMatchText(presetText);
|
|
16
|
+
if (!presetNorm) return false;
|
|
17
|
+
if (userTextNorm === presetNorm) return true;
|
|
18
|
+
const prefix = `${presetNorm} `;
|
|
19
|
+
return userTextNorm.startsWith(prefix);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export function usedPresetIdsFromMessages(
|
|
15
23
|
messages: Message[] | undefined,
|
|
16
24
|
presets: ChatPreset[] | undefined,
|
|
17
25
|
): string[] {
|
|
18
26
|
if (!messages?.length || !presets?.length) return [];
|
|
19
|
-
const userTexts =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.map(m => normalizePresetMatchText(m.text)),
|
|
23
|
-
);
|
|
27
|
+
const userTexts = messages
|
|
28
|
+
.filter(m => m.role === MessageRole.USER)
|
|
29
|
+
.map(m => normalizePresetMatchText(m.text));
|
|
24
30
|
return presets
|
|
25
|
-
.filter(p => userTexts.
|
|
31
|
+
.filter(p => userTexts.some(ut => presetMatchesUserText(p.text, ut)))
|
|
26
32
|
.map(p => p.id);
|
|
27
33
|
}
|
|
@@ -17,8 +17,10 @@ export { ChatPrompt } from './ChatPrompt';
|
|
|
17
17
|
export { ChatPresets } from './ChatPresets';
|
|
18
18
|
export type {
|
|
19
19
|
Chat as ChatType,
|
|
20
|
-
|
|
20
|
+
ChatSendMessagePayload,
|
|
21
21
|
ChatProps,
|
|
22
22
|
ChatPreset as ChatPresetType,
|
|
23
|
+
Message,
|
|
24
|
+
UserCsvAttachment,
|
|
23
25
|
} from './Chat.types';
|
|
24
26
|
export { MessageRole } from './Chat.types';
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type Chat,
|
|
13
|
+
type ChatSendMessagePayload,
|
|
13
14
|
type Message,
|
|
14
15
|
MessageRole,
|
|
16
|
+
type UserCsvAttachment,
|
|
15
17
|
} from '#uilib/components/ui/Chat/Chat.types';
|
|
16
18
|
import { stripJsonDashboardFences } from '#uilib/lib/dashboard-spec/stripJsonDashboardFences';
|
|
17
19
|
import type { ChatResponse } from '#uilib/types/chat-api.types';
|
|
@@ -22,9 +24,18 @@ export type SendChatMessageFn = (
|
|
|
22
24
|
targetChatId: string,
|
|
23
25
|
) => Promise<ChatResponse>;
|
|
24
26
|
|
|
27
|
+
export type {
|
|
28
|
+
ChatSendMessagePayload,
|
|
29
|
+
UserCsvAttachment,
|
|
30
|
+
} from '#uilib/components/ui/Chat/Chat.types';
|
|
31
|
+
|
|
25
32
|
const CHATS_PREFIX = 'chats-';
|
|
26
33
|
const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
|
|
27
34
|
|
|
35
|
+
export type AddChatMessageOptions = {
|
|
36
|
+
userCsvAttachment?: UserCsvAttachment;
|
|
37
|
+
};
|
|
38
|
+
|
|
28
39
|
export interface ChatContextType {
|
|
29
40
|
/** Returns the new session id, or undefined if no user / not created. */
|
|
30
41
|
newChat: (scopeId: string) => string | undefined;
|
|
@@ -34,6 +45,7 @@ export interface ChatContextType {
|
|
|
34
45
|
chatId: string,
|
|
35
46
|
role: MessageRole,
|
|
36
47
|
text: string,
|
|
48
|
+
options?: AddChatMessageOptions,
|
|
37
49
|
) => string | undefined;
|
|
38
50
|
removeMessageById: (
|
|
39
51
|
scopeId: string,
|
|
@@ -42,16 +54,26 @@ export interface ChatContextType {
|
|
|
42
54
|
) => void;
|
|
43
55
|
sendMessage: (
|
|
44
56
|
scopeId: string,
|
|
45
|
-
message: string,
|
|
57
|
+
message: string | ChatSendMessagePayload,
|
|
46
58
|
chatId?: string,
|
|
47
59
|
) => Promise<string>;
|
|
48
60
|
getChatsForScopeId: (scopeId: string) => Chat[];
|
|
49
61
|
getCurrentChatId: (scopeId: string) => string | null;
|
|
50
62
|
deleteChat: (scopeId: string, sessionId: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Ref-count of in-flight `sendChatMessage` requests keyed by
|
|
65
|
+
* `outboundPendingKey(scopeId, chatSessionId)` (session id at send start).
|
|
66
|
+
*/
|
|
67
|
+
outboundPendingByKey: Readonly<Record<string, number>>;
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
|
54
71
|
|
|
72
|
+
/** Stable composite key; avoids collisions if `scopeId` contains `:`. */
|
|
73
|
+
export function outboundPendingKey(scopeId: string, chatSessionId: string) {
|
|
74
|
+
return `${scopeId}\0${chatSessionId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
function getCurrentChatIdKey(scopeId: string) {
|
|
56
78
|
return `chat-current-id-${scopeId}`;
|
|
57
79
|
}
|
|
@@ -149,6 +171,35 @@ export function ChatProvider({
|
|
|
149
171
|
return loadChatsFromLS(userSwitchKey).currentChatId;
|
|
150
172
|
});
|
|
151
173
|
|
|
174
|
+
const [outboundPendingByKey, setOutboundPendingByKey] = useState<
|
|
175
|
+
Record<string, number>
|
|
176
|
+
>({});
|
|
177
|
+
|
|
178
|
+
const beginOutboundPending = useCallback(
|
|
179
|
+
(scopeId: string, chatSessionId: string) => {
|
|
180
|
+
const key = outboundPendingKey(scopeId, chatSessionId);
|
|
181
|
+
setOutboundPendingByKey(prev => ({
|
|
182
|
+
...prev,
|
|
183
|
+
[key]: (prev[key] ?? 0) + 1,
|
|
184
|
+
}));
|
|
185
|
+
},
|
|
186
|
+
[],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const endOutboundPending = useCallback(
|
|
190
|
+
(scopeId: string, chatSessionId: string) => {
|
|
191
|
+
const key = outboundPendingKey(scopeId, chatSessionId);
|
|
192
|
+
setOutboundPendingByKey(prev => {
|
|
193
|
+
const next = { ...prev };
|
|
194
|
+
const n = (next[key] ?? 0) - 1;
|
|
195
|
+
if (n <= 0) delete next[key];
|
|
196
|
+
else next[key] = n;
|
|
197
|
+
return next;
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
[],
|
|
201
|
+
);
|
|
202
|
+
|
|
152
203
|
const getChatsForScopeId = useCallback(
|
|
153
204
|
(scopeId: string): Chat[] => chats[scopeId] ?? [],
|
|
154
205
|
[chats],
|
|
@@ -239,15 +290,24 @@ export function ChatProvider({
|
|
|
239
290
|
}, []);
|
|
240
291
|
|
|
241
292
|
const addMessage = useCallback(
|
|
242
|
-
(
|
|
293
|
+
(
|
|
294
|
+
scopeId: string,
|
|
295
|
+
chatId: string,
|
|
296
|
+
role: MessageRole,
|
|
297
|
+
text: string,
|
|
298
|
+
options?: AddChatMessageOptions,
|
|
299
|
+
) => {
|
|
243
300
|
if (userSwitchKey === null) return undefined;
|
|
244
301
|
addScopeIdToRegistry(scopeId);
|
|
245
302
|
const storedText = stripJsonDashboardFences(text);
|
|
303
|
+
const attachment =
|
|
304
|
+
role === MessageRole.USER ? options?.userCsvAttachment : undefined;
|
|
246
305
|
const newMessage: Message = {
|
|
247
306
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
248
307
|
role,
|
|
249
308
|
text: storedText,
|
|
250
309
|
timestamp: Date.now(),
|
|
310
|
+
...(attachment ? { userCsvAttachment: attachment } : {}),
|
|
251
311
|
};
|
|
252
312
|
|
|
253
313
|
setChats(prev => {
|
|
@@ -292,7 +352,7 @@ export function ChatProvider({
|
|
|
292
352
|
const sendMessage = useCallback(
|
|
293
353
|
async (
|
|
294
354
|
scopeId: string,
|
|
295
|
-
message: string,
|
|
355
|
+
message: string | ChatSendMessagePayload,
|
|
296
356
|
chatId?: string,
|
|
297
357
|
): Promise<string> => {
|
|
298
358
|
const targetChatId = chatId ?? getCurrentChatId(scopeId);
|
|
@@ -300,16 +360,33 @@ export function ChatProvider({
|
|
|
300
360
|
throw new Error('No chat selected');
|
|
301
361
|
}
|
|
302
362
|
|
|
303
|
-
|
|
363
|
+
const apiPayload =
|
|
364
|
+
typeof message === 'string' ? message : message.apiMessage;
|
|
304
365
|
|
|
366
|
+
if (typeof message === 'string') {
|
|
367
|
+
addMessage(scopeId, targetChatId, MessageRole.USER, message);
|
|
368
|
+
} else {
|
|
369
|
+
addMessage(
|
|
370
|
+
scopeId,
|
|
371
|
+
targetChatId,
|
|
372
|
+
MessageRole.USER,
|
|
373
|
+
message.displayText,
|
|
374
|
+
{
|
|
375
|
+
userCsvAttachment: message.userCsvAttachment,
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const pendingChatSessionId = targetChatId;
|
|
381
|
+
beginOutboundPending(scopeId, pendingChatSessionId);
|
|
305
382
|
try {
|
|
306
|
-
const data = await sendChatMessageFn(
|
|
383
|
+
const data = await sendChatMessageFn(apiPayload, pendingChatSessionId);
|
|
307
384
|
|
|
308
|
-
if (data.session_id && data.session_id !==
|
|
385
|
+
if (data.session_id && data.session_id !== pendingChatSessionId) {
|
|
309
386
|
setChats(prev => {
|
|
310
387
|
const scopeChats = prev[scopeId] ?? [];
|
|
311
388
|
const updatedChats = scopeChats.map(chat =>
|
|
312
|
-
chat.session_id ===
|
|
389
|
+
chat.session_id === pendingChatSessionId
|
|
313
390
|
? { ...chat, session_id: data.session_id! }
|
|
314
391
|
: chat,
|
|
315
392
|
);
|
|
@@ -324,7 +401,7 @@ export function ChatProvider({
|
|
|
324
401
|
|
|
325
402
|
addMessage(
|
|
326
403
|
scopeId,
|
|
327
|
-
data.session_id ? data.session_id :
|
|
404
|
+
data.session_id ? data.session_id : pendingChatSessionId,
|
|
328
405
|
MessageRole.ASSISTANT,
|
|
329
406
|
data.response,
|
|
330
407
|
);
|
|
@@ -336,17 +413,32 @@ export function ChatProvider({
|
|
|
336
413
|
? error.message
|
|
337
414
|
: 'Sorry, I encountered an error processing your message. Please try again.';
|
|
338
415
|
|
|
339
|
-
addMessage(
|
|
416
|
+
addMessage(
|
|
417
|
+
scopeId,
|
|
418
|
+
pendingChatSessionId,
|
|
419
|
+
MessageRole.ASSISTANT,
|
|
420
|
+
errorMessage,
|
|
421
|
+
);
|
|
340
422
|
throw error;
|
|
423
|
+
} finally {
|
|
424
|
+
endOutboundPending(scopeId, pendingChatSessionId);
|
|
341
425
|
}
|
|
342
426
|
},
|
|
343
|
-
[
|
|
427
|
+
[
|
|
428
|
+
addMessage,
|
|
429
|
+
beginOutboundPending,
|
|
430
|
+
endOutboundPending,
|
|
431
|
+
getCurrentChatId,
|
|
432
|
+
sendChatMessageFn,
|
|
433
|
+
setCurrentChatId,
|
|
434
|
+
],
|
|
344
435
|
);
|
|
345
436
|
|
|
346
437
|
useEffect(() => {
|
|
347
438
|
if (userSwitchKey === null) {
|
|
348
439
|
setChats({});
|
|
349
440
|
setCurrentChatIdState({});
|
|
441
|
+
setOutboundPendingByKey({});
|
|
350
442
|
return;
|
|
351
443
|
}
|
|
352
444
|
|
|
@@ -377,6 +469,7 @@ export function ChatProvider({
|
|
|
377
469
|
getChatsForScopeId,
|
|
378
470
|
getCurrentChatId,
|
|
379
471
|
deleteChat,
|
|
472
|
+
outboundPendingByKey,
|
|
380
473
|
}}
|
|
381
474
|
>
|
|
382
475
|
{children}
|
|
@@ -409,6 +502,18 @@ export function useChat(
|
|
|
409
502
|
}, [scopeId, chatId, getChatsForScopeId]);
|
|
410
503
|
}
|
|
411
504
|
|
|
505
|
+
export function useChatOutboundPending(
|
|
506
|
+
scopeId: string | undefined | null,
|
|
507
|
+
chatSessionId: string | null | undefined,
|
|
508
|
+
): boolean {
|
|
509
|
+
const { outboundPendingByKey } = useChats();
|
|
510
|
+
return useMemo(() => {
|
|
511
|
+
if (!scopeId || !chatSessionId) return false;
|
|
512
|
+
const key = outboundPendingKey(scopeId, chatSessionId);
|
|
513
|
+
return (outboundPendingByKey[key] ?? 0) > 0;
|
|
514
|
+
}, [scopeId, chatSessionId, outboundPendingByKey]);
|
|
515
|
+
}
|
|
516
|
+
|
|
412
517
|
export function useChatsForScopeId(scopeId: string) {
|
|
413
518
|
const {
|
|
414
519
|
getChatsForScopeId,
|
|
@@ -423,18 +528,24 @@ export function useChatsForScopeId(scopeId: string) {
|
|
|
423
528
|
const chats = getChatsForScopeId(scopeId);
|
|
424
529
|
const currentChatId = getCurrentChatId(scopeId);
|
|
425
530
|
const currentChat = useChat(scopeId, currentChatId ?? undefined);
|
|
531
|
+
const isOutboundPending = useChatOutboundPending(scopeId, currentChatId);
|
|
426
532
|
|
|
427
533
|
return {
|
|
428
534
|
chats,
|
|
429
535
|
currentChat,
|
|
430
536
|
currentChatId,
|
|
537
|
+
isOutboundPending,
|
|
431
538
|
setCurrentChatId: (targetId: string) => setCurrentChatId(scopeId, targetId),
|
|
432
539
|
newChat: () => newChat(scopeId),
|
|
433
|
-
addMessage: (
|
|
434
|
-
|
|
540
|
+
addMessage: (
|
|
541
|
+
chatId: string,
|
|
542
|
+
role: MessageRole,
|
|
543
|
+
text: string,
|
|
544
|
+
options?: AddChatMessageOptions,
|
|
545
|
+
) => addMessage(scopeId, chatId, role, text, options),
|
|
435
546
|
removeMessageById: (chatId: string, messageId: string) =>
|
|
436
547
|
removeMessageById(scopeId, chatId, messageId),
|
|
437
|
-
sendMessage: (message: string, chatId?: string) =>
|
|
548
|
+
sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) =>
|
|
438
549
|
sendMessage(scopeId, message, chatId),
|
|
439
550
|
deleteChat: (sessionId: string) => deleteChat(scopeId, sessionId),
|
|
440
551
|
};
|