@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.1 → 0.1.2
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/context/AgentAssistantContext.d.ts.map +1 -1
- package/dist/context/AgentAssistantContext.js +3 -1
- package/dist/legacy/chatbot/ui/ChatBotHeader.d.ts +15 -0
- package/dist/legacy/chatbot/ui/ChatBotHeader.d.ts.map +1 -0
- package/dist/legacy/chatbot/ui/ChatBotHeader.js +62 -0
- package/dist/legacy/chatbot/ui/ChatBotUI.d.ts.map +1 -1
- package/dist/legacy/chatbot/ui/ChatBotUI.js +3 -71
- package/dist/legacy/chatbot/ui/chatBotHeaderParts.d.ts +15 -0
- package/dist/legacy/chatbot/ui/chatBotHeaderParts.d.ts.map +1 -0
- package/dist/legacy/chatbot/ui/chatBotHeaderParts.js +50 -0
- package/dist/voice/useVoiceTranscriptMessages.d.ts +7 -5
- package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -1
- package/dist/voice/useVoiceTranscriptMessages.js +79 -76
- package/dist/voice/voiceUserBubble.d.ts +10 -0
- package/dist/voice/voiceUserBubble.d.ts.map +1 -0
- package/dist/voice/voiceUserBubble.js +52 -0
- package/package.json +5 -5
- package/src/context/AgentAssistantContext.tsx +4 -2
- package/src/legacy/chatbot/ui/ChatBotHeader.tsx +143 -0
- package/src/legacy/chatbot/ui/ChatBotUI.tsx +13 -144
- package/src/legacy/chatbot/ui/chatBotHeaderParts.tsx +115 -0
- package/src/voice/useVoiceTranscriptMessages.ts +87 -72
- package/src/voice/voiceUserBubble.ts +71 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import { css } from '@emotion/react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { ModelSelector } from './ModelSelector';
|
|
5
|
+
import {
|
|
6
|
+
DRAG_CORNER_EXCLUSION_PX,
|
|
7
|
+
HeaderActions,
|
|
8
|
+
HeaderLoadingIndicator,
|
|
9
|
+
PiiBadge,
|
|
10
|
+
} from './chatBotHeaderParts';
|
|
11
|
+
import { chatHeader, headerTitle } from './styles/isolatedStyles';
|
|
12
|
+
|
|
13
|
+
export type ChatBotHeaderProps = {
|
|
14
|
+
mobileLayout: boolean;
|
|
15
|
+
isDragging: boolean;
|
|
16
|
+
onDragStart: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
17
|
+
isVoiceMode: boolean;
|
|
18
|
+
isVoiceConnecting: boolean;
|
|
19
|
+
onVoiceToggle: () => void;
|
|
20
|
+
piiDisplayMode: 'plain' | 'redacted';
|
|
21
|
+
isAITLoading: boolean;
|
|
22
|
+
onSettingsClick: () => void;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ChatBotHeader: React.FC<ChatBotHeaderProps> = ({
|
|
27
|
+
mobileLayout,
|
|
28
|
+
isDragging,
|
|
29
|
+
onDragStart,
|
|
30
|
+
isVoiceMode,
|
|
31
|
+
isVoiceConnecting,
|
|
32
|
+
onVoiceToggle,
|
|
33
|
+
piiDisplayMode,
|
|
34
|
+
isAITLoading,
|
|
35
|
+
onSettingsClick,
|
|
36
|
+
onClose,
|
|
37
|
+
}) => {
|
|
38
|
+
const actions = (
|
|
39
|
+
<HeaderActions
|
|
40
|
+
isVoiceMode={isVoiceMode}
|
|
41
|
+
isVoiceConnecting={isVoiceConnecting}
|
|
42
|
+
onVoiceToggle={onVoiceToggle}
|
|
43
|
+
onSettingsClick={onSettingsClick}
|
|
44
|
+
onClose={onClose}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const metaChips = !isVoiceMode ? (
|
|
49
|
+
<>
|
|
50
|
+
<div css={css`position: relative !important; pointer-events: auto !important;`}>
|
|
51
|
+
<ModelSelector />
|
|
52
|
+
</div>
|
|
53
|
+
{piiDisplayMode === 'redacted' && <PiiBadge />}
|
|
54
|
+
{isAITLoading && <HeaderLoadingIndicator />}
|
|
55
|
+
</>
|
|
56
|
+
) : null;
|
|
57
|
+
|
|
58
|
+
const dragOverlay = (
|
|
59
|
+
<div
|
|
60
|
+
css={css`
|
|
61
|
+
position: absolute !important;
|
|
62
|
+
left: ${DRAG_CORNER_EXCLUSION_PX}px !important;
|
|
63
|
+
right: ${DRAG_CORNER_EXCLUSION_PX}px !important;
|
|
64
|
+
top: 0 !important;
|
|
65
|
+
bottom: 0 !important;
|
|
66
|
+
z-index: 1 !important;
|
|
67
|
+
cursor: ${isDragging ? 'grabbing' : 'grab'} !important;
|
|
68
|
+
user-select: none !important;
|
|
69
|
+
`}
|
|
70
|
+
onPointerDown={onDragStart}
|
|
71
|
+
title="Drag to move"
|
|
72
|
+
aria-hidden
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div css={[chatHeader, css`
|
|
78
|
+
position: relative !important;
|
|
79
|
+
${mobileLayout ? css`
|
|
80
|
+
padding: 10px 12px !important;
|
|
81
|
+
flex-direction: column !important;
|
|
82
|
+
align-items: stretch !important;
|
|
83
|
+
gap: 8px !important;
|
|
84
|
+
` : ''}
|
|
85
|
+
`]}>
|
|
86
|
+
{dragOverlay}
|
|
87
|
+
{mobileLayout ? (
|
|
88
|
+
<div css={css`
|
|
89
|
+
position: relative !important;
|
|
90
|
+
z-index: 2 !important;
|
|
91
|
+
display: flex !important;
|
|
92
|
+
flex-direction: column !important;
|
|
93
|
+
gap: 8px !important;
|
|
94
|
+
width: 100% !important;
|
|
95
|
+
`}>
|
|
96
|
+
<div css={css`
|
|
97
|
+
display: flex !important;
|
|
98
|
+
justify-content: space-between !important;
|
|
99
|
+
align-items: center !important;
|
|
100
|
+
gap: 8px !important;
|
|
101
|
+
pointer-events: none !important;
|
|
102
|
+
`}>
|
|
103
|
+
<h3 css={[headerTitle, css`pointer-events: none !important;`]}>AI Agent</h3>
|
|
104
|
+
{actions}
|
|
105
|
+
</div>
|
|
106
|
+
{metaChips && (
|
|
107
|
+
<div css={css`
|
|
108
|
+
display: flex !important;
|
|
109
|
+
align-items: center !important;
|
|
110
|
+
flex-wrap: wrap !important;
|
|
111
|
+
gap: 8px !important;
|
|
112
|
+
pointer-events: none !important;
|
|
113
|
+
`}>
|
|
114
|
+
{metaChips}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div css={css`
|
|
120
|
+
position: relative !important;
|
|
121
|
+
z-index: 2 !important;
|
|
122
|
+
display: flex !important;
|
|
123
|
+
justify-content: space-between !important;
|
|
124
|
+
align-items: center !important;
|
|
125
|
+
width: 100% !important;
|
|
126
|
+
pointer-events: none !important;
|
|
127
|
+
`}>
|
|
128
|
+
<div css={css`
|
|
129
|
+
display: flex !important;
|
|
130
|
+
align-items: center !important;
|
|
131
|
+
gap: 10px !important;
|
|
132
|
+
min-width: 0 !important;
|
|
133
|
+
pointer-events: none !important;
|
|
134
|
+
`}>
|
|
135
|
+
<h3 css={headerTitle}>AI Agent</h3>
|
|
136
|
+
{metaChips}
|
|
137
|
+
</div>
|
|
138
|
+
{actions}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
@@ -3,28 +3,22 @@ import { css } from '@emotion/react';
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { useDraggable, useLocalStorage, useResizable, walletTextUtils } from '@bytexbyte/nxtlinq-ai-agent-web-development';
|
|
5
5
|
import { useChatBot } from '../context/ChatBotContext';
|
|
6
|
-
import
|
|
6
|
+
import { ChatBotHeader } from './ChatBotHeader';
|
|
7
7
|
import { MessageInput } from './MessageInput';
|
|
8
8
|
import { MessageList } from './MessageList';
|
|
9
|
-
import { ModelSelector } from './ModelSelector';
|
|
10
9
|
import { PermissionForm } from './PermissionForm';
|
|
11
10
|
import { PresetMessages } from './PresetMessages';
|
|
12
11
|
import { VoiceModePanel } from './VoiceModePanel';
|
|
13
12
|
import {
|
|
14
|
-
chatHeader,
|
|
15
13
|
chatWindow,
|
|
16
|
-
closeButton,
|
|
17
14
|
errorToast,
|
|
18
15
|
floatingButton,
|
|
19
|
-
headerButton,
|
|
20
|
-
headerTitle,
|
|
21
16
|
idvBanner,
|
|
22
17
|
idvBannerText,
|
|
23
18
|
idvBannerTitle,
|
|
24
19
|
idvDismissButton,
|
|
25
20
|
idvVerifyButton,
|
|
26
21
|
infoToast,
|
|
27
|
-
loadingSpinner,
|
|
28
22
|
modalOverlay,
|
|
29
23
|
resizeHandleNE,
|
|
30
24
|
resizeHandleNW,
|
|
@@ -36,8 +30,6 @@ import {
|
|
|
36
30
|
warningToast
|
|
37
31
|
} from './styles/isolatedStyles';
|
|
38
32
|
|
|
39
|
-
/** Header drag band inset so it does not overlap corner resize hit areas */
|
|
40
|
-
const DRAG_CORNER_EXCLUSION_PX = 20;
|
|
41
33
|
const MOBILE_BREAKPOINT = 768;
|
|
42
34
|
const MOBILE_EDGE_MARGIN = 12;
|
|
43
35
|
const MOBILE_FAB_POSITION = { right: MOBILE_EDGE_MARGIN, bottom: MOBILE_EDGE_MARGIN } as const;
|
|
@@ -716,141 +708,18 @@ export const ChatBotUI: React.FC = () => {
|
|
|
716
708
|
</>
|
|
717
709
|
)}
|
|
718
710
|
|
|
719
|
-
<
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
z-index: 1 !important;
|
|
732
|
-
cursor: ${isDragging ? 'grabbing' : 'grab'} !important;
|
|
733
|
-
user-select: none !important;
|
|
734
|
-
`}
|
|
735
|
-
onPointerDown={handleDragStart}
|
|
736
|
-
title="Drag to move"
|
|
737
|
-
aria-hidden
|
|
738
|
-
/>
|
|
739
|
-
<div
|
|
740
|
-
css={css`
|
|
741
|
-
position: relative !important;
|
|
742
|
-
z-index: 2 !important;
|
|
743
|
-
display: flex !important;
|
|
744
|
-
justify-content: space-between !important;
|
|
745
|
-
align-items: center !important;
|
|
746
|
-
width: 100% !important;
|
|
747
|
-
pointer-events: none !important;
|
|
748
|
-
`}
|
|
749
|
-
>
|
|
750
|
-
<div css={css`
|
|
751
|
-
display: flex !important;
|
|
752
|
-
align-items: center !important;
|
|
753
|
-
gap: 10px !important;
|
|
754
|
-
pointer-events: none !important;
|
|
755
|
-
`}>
|
|
756
|
-
<h3 css={headerTitle}>
|
|
757
|
-
AI Agent
|
|
758
|
-
</h3>
|
|
759
|
-
{!isVoiceMode && (
|
|
760
|
-
<div css={css`
|
|
761
|
-
position: relative !important;
|
|
762
|
-
pointer-events: auto !important;
|
|
763
|
-
`}>
|
|
764
|
-
<ModelSelector />
|
|
765
|
-
</div>
|
|
766
|
-
)}
|
|
767
|
-
{!isVoiceMode && piiDisplayMode === 'redacted' && (
|
|
768
|
-
<div
|
|
769
|
-
css={css`
|
|
770
|
-
display: inline-flex !important;
|
|
771
|
-
align-items: center !important;
|
|
772
|
-
gap: 4px !important;
|
|
773
|
-
padding: 2px 8px !important;
|
|
774
|
-
background-color: rgba(255, 255, 255, 0.2) !important;
|
|
775
|
-
border: 1px solid rgba(255, 255, 255, 0.4) !important;
|
|
776
|
-
border-radius: 10px !important;
|
|
777
|
-
font-size: 10px !important;
|
|
778
|
-
font-weight: 600 !important;
|
|
779
|
-
color: #ffffff !important;
|
|
780
|
-
white-space: nowrap !important;
|
|
781
|
-
line-height: 1.4 !important;
|
|
782
|
-
user-select: none !important;
|
|
783
|
-
`}
|
|
784
|
-
title="PII Protection is active — sensitive data is automatically anonymized before sending to AI"
|
|
785
|
-
>
|
|
786
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
|
|
787
|
-
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/>
|
|
788
|
-
</svg>
|
|
789
|
-
PII Protected
|
|
790
|
-
</div>
|
|
791
|
-
)}
|
|
792
|
-
{isAITLoading && (
|
|
793
|
-
<div css={css`
|
|
794
|
-
display: flex !important;
|
|
795
|
-
align-items: center !important;
|
|
796
|
-
gap: 5px !important;
|
|
797
|
-
font-size: 12px !important;
|
|
798
|
-
opacity: 0.8 !important;
|
|
799
|
-
`}>
|
|
800
|
-
<div css={loadingSpinner} />
|
|
801
|
-
Loading...
|
|
802
|
-
</div>
|
|
803
|
-
)}
|
|
804
|
-
</div>
|
|
805
|
-
<div css={css`
|
|
806
|
-
display: flex !important;
|
|
807
|
-
align-items: center !important;
|
|
808
|
-
gap: 10px !important;
|
|
809
|
-
pointer-events: auto !important;
|
|
810
|
-
`}>
|
|
811
|
-
<button
|
|
812
|
-
onClick={() => void ((isVoiceMode || isVoiceConnecting) ? exitVoiceMode() : enterVoiceMode())}
|
|
813
|
-
css={[headerButton, css`
|
|
814
|
-
width: auto !important;
|
|
815
|
-
min-width: 82px !important;
|
|
816
|
-
padding: 0 10px !important;
|
|
817
|
-
gap: 6px !important;
|
|
818
|
-
font-size: 12px !important;
|
|
819
|
-
font-weight: 600 !important;
|
|
820
|
-
line-height: 1 !important;
|
|
821
|
-
white-space: nowrap !important;
|
|
822
|
-
`]}
|
|
823
|
-
title={
|
|
824
|
-
isVoiceConnecting
|
|
825
|
-
? 'Cancel voice connection and return to text mode'
|
|
826
|
-
: isVoiceMode
|
|
827
|
-
? 'Switch to text mode'
|
|
828
|
-
: 'Switch to voice mode'
|
|
829
|
-
}
|
|
830
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
831
|
-
>
|
|
832
|
-
<GraphicEqIcon css={css`font-size: 16px !important; color: #fff !important;`} />
|
|
833
|
-
{(isVoiceMode || isVoiceConnecting) ? 'Text Mode' : 'Voice Mode'}
|
|
834
|
-
</button>
|
|
835
|
-
<button
|
|
836
|
-
onClick={handleSettingsClick}
|
|
837
|
-
css={headerButton}
|
|
838
|
-
title="AIT Settings"
|
|
839
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
840
|
-
>
|
|
841
|
-
⚙️
|
|
842
|
-
</button>
|
|
843
|
-
<button
|
|
844
|
-
onClick={handleClose}
|
|
845
|
-
css={closeButton}
|
|
846
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
847
|
-
title="Minimize"
|
|
848
|
-
>
|
|
849
|
-
−
|
|
850
|
-
</button>
|
|
851
|
-
</div>
|
|
852
|
-
</div>
|
|
853
|
-
</div>
|
|
711
|
+
<ChatBotHeader
|
|
712
|
+
mobileLayout={mobileLayout}
|
|
713
|
+
isDragging={isDragging}
|
|
714
|
+
onDragStart={handleDragStart}
|
|
715
|
+
isVoiceMode={isVoiceMode}
|
|
716
|
+
isVoiceConnecting={isVoiceConnecting}
|
|
717
|
+
onVoiceToggle={() => void ((isVoiceMode || isVoiceConnecting) ? exitVoiceMode() : enterVoiceMode())}
|
|
718
|
+
piiDisplayMode={piiDisplayMode}
|
|
719
|
+
isAITLoading={isAITLoading}
|
|
720
|
+
onSettingsClick={handleSettingsClick}
|
|
721
|
+
onClose={handleClose}
|
|
722
|
+
/>
|
|
854
723
|
|
|
855
724
|
{showIDVSuggestion && hitAddress && !props.requireWalletIDVVerification && !hasBerifymeToken && !isWalletVerifiedWithBerifyme && (
|
|
856
725
|
<div
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import { css } from '@emotion/react';
|
|
3
|
+
import GraphicEqIcon from '@mui/icons-material/GraphicEq';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { closeButton, headerButton, loadingSpinner } from './styles/isolatedStyles';
|
|
6
|
+
|
|
7
|
+
export const DRAG_CORNER_EXCLUSION_PX = 20;
|
|
8
|
+
|
|
9
|
+
const voiceButtonStyles = css`
|
|
10
|
+
width: auto !important;
|
|
11
|
+
min-width: 82px !important;
|
|
12
|
+
padding: 0 10px !important;
|
|
13
|
+
gap: 6px !important;
|
|
14
|
+
font-size: 12px !important;
|
|
15
|
+
font-weight: 600 !important;
|
|
16
|
+
line-height: 1 !important;
|
|
17
|
+
white-space: nowrap !important;
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export const piiBadgeStyles = css`
|
|
21
|
+
display: inline-flex !important;
|
|
22
|
+
align-items: center !important;
|
|
23
|
+
gap: 4px !important;
|
|
24
|
+
padding: 2px 8px !important;
|
|
25
|
+
background-color: rgba(255, 255, 255, 0.2) !important;
|
|
26
|
+
border: 1px solid rgba(255, 255, 255, 0.4) !important;
|
|
27
|
+
border-radius: 10px !important;
|
|
28
|
+
font-size: 10px !important;
|
|
29
|
+
font-weight: 600 !important;
|
|
30
|
+
color: #ffffff !important;
|
|
31
|
+
white-space: nowrap !important;
|
|
32
|
+
line-height: 1.4 !important;
|
|
33
|
+
user-select: none !important;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export const PiiBadge: React.FC = () => (
|
|
37
|
+
<div
|
|
38
|
+
css={piiBadgeStyles}
|
|
39
|
+
title="PII Protection is active — sensitive data is automatically anonymized before sending to AI"
|
|
40
|
+
>
|
|
41
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
|
|
42
|
+
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z" />
|
|
43
|
+
</svg>
|
|
44
|
+
PII Protected
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
type HeaderActionsProps = {
|
|
49
|
+
isVoiceMode: boolean;
|
|
50
|
+
isVoiceConnecting: boolean;
|
|
51
|
+
onVoiceToggle: () => void;
|
|
52
|
+
onSettingsClick: () => void;
|
|
53
|
+
onClose: () => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const HeaderActions: React.FC<HeaderActionsProps> = ({
|
|
57
|
+
isVoiceMode,
|
|
58
|
+
isVoiceConnecting,
|
|
59
|
+
onVoiceToggle,
|
|
60
|
+
onSettingsClick,
|
|
61
|
+
onClose,
|
|
62
|
+
}) => (
|
|
63
|
+
<div css={css`
|
|
64
|
+
display: flex !important;
|
|
65
|
+
align-items: center !important;
|
|
66
|
+
gap: 10px !important;
|
|
67
|
+
flex-shrink: 0 !important;
|
|
68
|
+
pointer-events: auto !important;
|
|
69
|
+
`}>
|
|
70
|
+
<button
|
|
71
|
+
onClick={onVoiceToggle}
|
|
72
|
+
css={[headerButton, voiceButtonStyles]}
|
|
73
|
+
title={
|
|
74
|
+
isVoiceConnecting
|
|
75
|
+
? 'Cancel voice connection and return to text mode'
|
|
76
|
+
: isVoiceMode
|
|
77
|
+
? 'Switch to text mode'
|
|
78
|
+
: 'Switch to voice mode'
|
|
79
|
+
}
|
|
80
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
81
|
+
>
|
|
82
|
+
<GraphicEqIcon css={css`font-size: 16px !important; color: #fff !important;`} />
|
|
83
|
+
{(isVoiceMode || isVoiceConnecting) ? 'Text Mode' : 'Voice Mode'}
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
onClick={onSettingsClick}
|
|
87
|
+
css={headerButton}
|
|
88
|
+
title="AIT Settings"
|
|
89
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
90
|
+
>
|
|
91
|
+
⚙️
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
onClick={onClose}
|
|
95
|
+
css={closeButton}
|
|
96
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
97
|
+
title="Minimize"
|
|
98
|
+
>
|
|
99
|
+
−
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
export const HeaderLoadingIndicator: React.FC = () => (
|
|
105
|
+
<div css={css`
|
|
106
|
+
display: flex !important;
|
|
107
|
+
align-items: center !important;
|
|
108
|
+
gap: 5px !important;
|
|
109
|
+
font-size: 12px !important;
|
|
110
|
+
opacity: 0.8 !important;
|
|
111
|
+
`}>
|
|
112
|
+
<div css={loadingSpinner} />
|
|
113
|
+
Loading...
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
} from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
6
6
|
import { mergeStreamingTranscript } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
7
7
|
import { useCallback, useRef } from 'react';
|
|
8
|
-
import
|
|
8
|
+
import { ensureUserBubbleForVoiceTurn } from './voiceUserBubble';
|
|
9
9
|
|
|
10
|
-
type
|
|
10
|
+
type InteractionMode = 'text' | 'voice';
|
|
11
|
+
|
|
12
|
+
export type VoiceTranscriptAgentApi = {
|
|
11
13
|
getMessages: () => Message[];
|
|
12
|
-
|
|
14
|
+
updateMessages: (updater: (prev: Message[]) => Message[]) => void;
|
|
13
15
|
syncVoiceTurnHistory: (options?: { last?: number }) => Promise<void>;
|
|
14
16
|
};
|
|
15
17
|
|
|
@@ -26,6 +28,7 @@ export function useVoiceTranscriptMessages(
|
|
|
26
28
|
api: VoiceTranscriptAgentApi,
|
|
27
29
|
interactionMode: InteractionMode,
|
|
28
30
|
voiceSessionId: string | null,
|
|
31
|
+
getPendingUserText?: () => string,
|
|
29
32
|
) {
|
|
30
33
|
const streamIdRef = useRef<string | null>(null);
|
|
31
34
|
const sessionIdRef = useRef(voiceSessionId);
|
|
@@ -38,42 +41,48 @@ export function useVoiceTranscriptMessages(
|
|
|
38
41
|
|
|
39
42
|
const upsertStreaming = useCallback(
|
|
40
43
|
(text: string) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
api.updateMessages((prev) => {
|
|
45
|
+
let streamId = streamIdRef.current;
|
|
46
|
+
if (!streamId) {
|
|
47
|
+
streamId = `${STREAM_PREFIX}${Date.now()}`;
|
|
48
|
+
streamIdRef.current = streamId;
|
|
49
|
+
}
|
|
50
|
+
const idx = prev.findIndex((m) => m.id === streamId);
|
|
51
|
+
const partialContent =
|
|
52
|
+
idx >= 0
|
|
53
|
+
? mergeStreamingTranscript(prev[idx]?.partialContent ?? '', text)
|
|
54
|
+
: text;
|
|
55
|
+
const meta = voiceMeta(sessionIdRef.current);
|
|
56
|
+
|
|
57
|
+
if (idx >= 0) {
|
|
58
|
+
return prev.map((m, i) =>
|
|
56
59
|
i === idx
|
|
57
60
|
? { ...m, partialContent, isStreaming: true, metadata: { ...m.metadata, ...meta } }
|
|
58
61
|
: m,
|
|
59
|
-
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const withUser = ensureUserBubbleForVoiceTurn(
|
|
66
|
+
prev,
|
|
67
|
+
getPendingUserText?.() ?? '',
|
|
68
|
+
undefined,
|
|
69
|
+
meta,
|
|
60
70
|
);
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
]);
|
|
71
|
+
return [
|
|
72
|
+
...withUser,
|
|
73
|
+
{
|
|
74
|
+
id: streamId,
|
|
75
|
+
role: 'assistant' as const,
|
|
76
|
+
content: '',
|
|
77
|
+
partialContent,
|
|
78
|
+
isStreaming: true,
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
metadata: meta,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
});
|
|
75
84
|
},
|
|
76
|
-
[api],
|
|
85
|
+
[api, getPendingUserText],
|
|
77
86
|
);
|
|
78
87
|
|
|
79
88
|
const finalizeAssistant = useCallback(
|
|
@@ -82,11 +91,10 @@ export function useVoiceTranscriptMessages(
|
|
|
82
91
|
streamIdRef.current = null;
|
|
83
92
|
if (!trimmed) return;
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
messages.map((m, i) =>
|
|
94
|
+
api.updateMessages((prev) => {
|
|
95
|
+
const streamIdx = prev.findIndex((m) => m.isStreaming && m.role === 'assistant');
|
|
96
|
+
if (streamIdx >= 0) {
|
|
97
|
+
return prev.map((m, i) =>
|
|
90
98
|
i === streamIdx
|
|
91
99
|
? {
|
|
92
100
|
...m,
|
|
@@ -97,22 +105,21 @@ export function useVoiceTranscriptMessages(
|
|
|
97
105
|
metadata: { ...m.metadata, ...voiceMeta(sessionIdRef.current) },
|
|
98
106
|
}
|
|
99
107
|
: m,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]);
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const last = prev[prev.length - 1];
|
|
111
|
+
if (last?.role === 'assistant' && last.content === trimmed) return prev;
|
|
112
|
+
return [
|
|
113
|
+
...prev,
|
|
114
|
+
{
|
|
115
|
+
id: messageId ?? `voice-asst-${Date.now()}`,
|
|
116
|
+
role: 'assistant' as const,
|
|
117
|
+
content: trimmed,
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
metadata: voiceMeta(sessionIdRef.current),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
});
|
|
116
123
|
},
|
|
117
124
|
[api],
|
|
118
125
|
);
|
|
@@ -122,31 +129,31 @@ export function useVoiceTranscriptMessages(
|
|
|
122
129
|
if (!isVoiceUiActive()) return;
|
|
123
130
|
const text = event.text?.trim() ?? '';
|
|
124
131
|
if (event.role === 'assistant') {
|
|
125
|
-
// Keep one streaming bubble for the whole turn; finalize only in handleDone.
|
|
126
132
|
if (text) upsertStreaming(text);
|
|
127
133
|
return;
|
|
128
134
|
}
|
|
129
135
|
if (event.role === 'user' && !event.interim && text) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
api.updateMessages((prev) => {
|
|
137
|
+
const last = prev[prev.length - 1];
|
|
138
|
+
if (last?.role === 'user' && last.content === text) return prev;
|
|
139
|
+
return [
|
|
140
|
+
...prev,
|
|
141
|
+
{
|
|
142
|
+
id: `voice-user-${Date.now()}`,
|
|
143
|
+
role: 'user' as const,
|
|
144
|
+
content: text,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
metadata: voiceMeta(sessionIdRef.current),
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
});
|
|
143
150
|
}
|
|
144
151
|
},
|
|
145
|
-
[api,
|
|
152
|
+
[api, isVoiceUiActive, upsertStreaming],
|
|
146
153
|
);
|
|
147
154
|
|
|
148
155
|
const handleDone = useCallback(
|
|
149
|
-
(event: VoiceDoneEvent) => {
|
|
156
|
+
(event: VoiceDoneEvent, options?: { pendingUserText?: string }) => {
|
|
150
157
|
if (!isVoiceUiActive()) return;
|
|
151
158
|
if (event.guardrailsBlocked || event.billingBlocked || event.error) {
|
|
152
159
|
streamIdRef.current = null;
|
|
@@ -154,6 +161,14 @@ export function useVoiceTranscriptMessages(
|
|
|
154
161
|
}
|
|
155
162
|
const reply = event.replyText?.trim() ?? '';
|
|
156
163
|
if (reply) {
|
|
164
|
+
api.updateMessages((prev) =>
|
|
165
|
+
ensureUserBubbleForVoiceTurn(
|
|
166
|
+
prev,
|
|
167
|
+
options?.pendingUserText ?? getPendingUserText?.() ?? '',
|
|
168
|
+
event.userMessageId,
|
|
169
|
+
voiceMeta(sessionIdRef.current),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
157
172
|
finalizeAssistant(reply, event.assistantMessageId ?? undefined);
|
|
158
173
|
} else {
|
|
159
174
|
streamIdRef.current = null;
|
|
@@ -162,7 +177,7 @@ export function useVoiceTranscriptMessages(
|
|
|
162
177
|
console.warn('[nxtlinq] syncVoiceTurnHistory after voice turn failed', err);
|
|
163
178
|
});
|
|
164
179
|
},
|
|
165
|
-
[api, finalizeAssistant, isVoiceUiActive],
|
|
180
|
+
[api, finalizeAssistant, getPendingUserText, isVoiceUiActive],
|
|
166
181
|
);
|
|
167
182
|
|
|
168
183
|
const clearVoiceStream = useCallback(() => {
|