@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.1 → 0.1.3

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.
@@ -1,4 +1,4 @@
1
- import type { VoiceStatus } from '@bytexbyte/nxtlinq-ai-agent-core-development';
1
+ import type { Message, VoiceStatus } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
2
  import {
3
3
  useNxtlinqAgent,
4
4
  useNxtlinqVoice,
@@ -93,7 +93,9 @@ export function AgentAssistantProvider({
93
93
  const voiceTranscriptApi = useMemo(
94
94
  () => ({
95
95
  getMessages: () => agent.agent.getSnapshot().messages,
96
- setMessages: agent.setMessages,
96
+ updateMessages: (updater: (prev: Message[]) => Message[]) => {
97
+ agent.setMessages(updater(agent.agent.getSnapshot().messages));
98
+ },
97
99
  syncVoiceTurnHistory: agent.syncVoiceTurnHistory,
98
100
  }),
99
101
  [agent.agent, agent.setMessages, agent.syncVoiceTurnHistory],
@@ -162,6 +164,22 @@ export function AgentAssistantProvider({
162
164
  orchestrationCallbacks,
163
165
  );
164
166
 
167
+ // iOS 切換 App 或相機後返回時,若 WebRTC App Channel 已斷開則自動停止語音 session,
168
+ // 避免麥克風圖示顯示活躍但狀態卡在 Idle 的問題。
169
+ useEffect(() => {
170
+ if (typeof document === 'undefined') return;
171
+ const handleVisibilityChange = () => {
172
+ if (document.visibilityState !== 'visible') return;
173
+ if (voice.voiceSessionId == null) return;
174
+ const session = agent.agent.getVoiceSession();
175
+ if (!session?.isAppChannelOpen()) {
176
+ void wrappedStopVoice();
177
+ }
178
+ };
179
+ document.addEventListener('visibilitychange', handleVisibilityChange);
180
+ return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
181
+ }, [voice.voiceSessionId, agent.agent, wrappedStopVoice]);
182
+
165
183
  const theme = useMemo(
166
184
  () => ({
167
185
  ...defaultAgentAssistantTheme,
@@ -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 GraphicEqIcon from '@mui/icons-material/GraphicEq';
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
- <div
720
- css={[chatHeader, css`
721
- position: relative !important;
722
- `]}
723
- >
724
- <div
725
- css={css`
726
- position: absolute !important;
727
- left: ${DRAG_CORNER_EXCLUSION_PX}px !important;
728
- right: ${DRAG_CORNER_EXCLUSION_PX}px !important;
729
- top: 0 !important;
730
- bottom: 0 !important;
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
+ );