@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.4.4 → 0.4.7

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/src/index.ts CHANGED
@@ -14,7 +14,6 @@ export {
14
14
 
15
15
  export type {
16
16
  AIModel,
17
- AITMetadata,
18
17
  ChatBotContextType,
19
18
  ChatBotProps,
20
19
  NovaError,
@@ -0,0 +1,313 @@
1
+ import type { AIT, RuntimeAITResult } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import * as React from 'react';
3
+
4
+ const useCommitPhaseEffect = typeof window === 'undefined'
5
+ ? React.useEffect
6
+ : React.useLayoutEffect;
7
+
8
+ export function commitProviderRuntime<T, E>(
9
+ identityRef: React.MutableRefObject<T>,
10
+ identity: T,
11
+ environment: E,
12
+ publishEnvironment: (environment: E) => void,
13
+ ): void {
14
+ identityRef.current = identity;
15
+ publishEnvironment(environment);
16
+ }
17
+
18
+ export function useProviderRuntimeCommit<T, E>(
19
+ identityRef: React.MutableRefObject<T>,
20
+ identity: T,
21
+ environment: E,
22
+ publishEnvironment: (environment: E) => void,
23
+ ): void {
24
+ useCommitPhaseEffect(() => {
25
+ commitProviderRuntime(
26
+ identityRef,
27
+ identity,
28
+ environment,
29
+ publishEnvironment,
30
+ );
31
+ }, [environment, identity, identityRef, publishEnvironment]);
32
+ }
33
+
34
+ function stableUnique(values: readonly string[]): string[] {
35
+ const seen = new Set<string>();
36
+ const result: string[] = [];
37
+ for (const value of values) {
38
+ if (!seen.has(value)) {
39
+ seen.add(value);
40
+ result.push(value);
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+
46
+ export function replaceCurrentMaximumDenyList(
47
+ persistedDeniedPermissions: readonly string[],
48
+ maximumPermissions: readonly string[],
49
+ checkedPermissions: readonly string[],
50
+ ): string[] {
51
+ const maximum = new Set(maximumPermissions);
52
+ const checked = new Set(checkedPermissions);
53
+ for (const permission of checked) {
54
+ if (!maximum.has(permission)) {
55
+ throw new Error(`Permission is outside the authoritative maximum: ${permission}`);
56
+ }
57
+ }
58
+
59
+ const retainedOutsideMaximum = persistedDeniedPermissions.filter(
60
+ (permission) => !maximum.has(permission),
61
+ );
62
+ const deniedInsideMaximum = maximumPermissions.filter(
63
+ (permission) => !checked.has(permission),
64
+ );
65
+ return stableUnique([...retainedOutsideMaximum, ...deniedInsideMaximum]);
66
+ }
67
+
68
+ export function removeOneDeniedPermission(
69
+ persistedDeniedPermissions: readonly string[],
70
+ permission: string,
71
+ ): string[] {
72
+ return stableUnique(persistedDeniedPermissions).filter((value) => value !== permission);
73
+ }
74
+
75
+ export type RuntimeAITContext = {
76
+ controller: string;
77
+ serviceId: string;
78
+ roles?: string[];
79
+ permissionGroup?: string;
80
+ };
81
+
82
+ export function runtimeAITContextKey(context: RuntimeAITContext): string {
83
+ return JSON.stringify([
84
+ context.controller,
85
+ context.serviceId,
86
+ context.roles === undefined ? null : [...context.roles],
87
+ context.permissionGroup ?? null,
88
+ ]);
89
+ }
90
+
91
+ export function runtimeAITSubjectKey(context: RuntimeAITContext): string {
92
+ return JSON.stringify([context.controller, context.serviceId]);
93
+ }
94
+
95
+ export function runtimeAuthorizationMatchesContext(
96
+ authorizationContextKey: string | null,
97
+ renderedContext: RuntimeAITContext | null,
98
+ ): boolean {
99
+ return authorizationContextKey === (
100
+ renderedContext ? runtimeAITContextKey(renderedContext) : null
101
+ );
102
+ }
103
+
104
+ export class StaleRuntimeAITResponseError extends Error {
105
+ constructor() {
106
+ super('Stale runtime AIT response was discarded');
107
+ this.name = 'StaleRuntimeAITResponseError';
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Owns the authoritative recompute lifecycle. Only the newest request for the
113
+ * active captured context may write state; its failures clear state first.
114
+ */
115
+ export class RuntimeAITRecomputeCoordinator {
116
+ private activeContextKey: string | null = null;
117
+ private activeContext: RuntimeAITContext | null = null;
118
+ private generation = 0;
119
+
120
+ commitContext(
121
+ context: RuntimeAITContext | null,
122
+ invalidate: () => void,
123
+ ): boolean {
124
+ const nextKey = context ? runtimeAITContextKey(context) : null;
125
+ if (nextKey === this.activeContextKey) {
126
+ return false;
127
+ }
128
+ this.activeContextKey = nextKey;
129
+ this.activeContext = context ? {
130
+ controller: context.controller,
131
+ serviceId: context.serviceId,
132
+ ...(context.roles !== undefined ? { roles: [...context.roles] } : {}),
133
+ ...(context.permissionGroup !== undefined
134
+ ? { permissionGroup: context.permissionGroup }
135
+ : {}),
136
+ } : null;
137
+ this.generation += 1;
138
+ invalidate();
139
+ return true;
140
+ }
141
+
142
+ getActiveContext(): RuntimeAITContext | null {
143
+ return this.activeContext ? {
144
+ controller: this.activeContext.controller,
145
+ serviceId: this.activeContext.serviceId,
146
+ ...(this.activeContext.roles !== undefined
147
+ ? { roles: [...this.activeContext.roles] }
148
+ : {}),
149
+ ...(this.activeContext.permissionGroup !== undefined
150
+ ? { permissionGroup: this.activeContext.permissionGroup }
151
+ : {}),
152
+ } : null;
153
+ }
154
+
155
+ isContextActive(context: RuntimeAITContext): boolean {
156
+ return runtimeAITContextKey(context) === this.activeContextKey;
157
+ }
158
+
159
+ assertContextActive(context: RuntimeAITContext): void {
160
+ if (!this.isContextActive(context)) {
161
+ throw new StaleRuntimeAITResponseError();
162
+ }
163
+ }
164
+
165
+ async run<T>(
166
+ context: RuntimeAITContext,
167
+ request: () => Promise<T>,
168
+ apply: (result: T, context: RuntimeAITContext) => void,
169
+ clear: () => void,
170
+ ): Promise<T> {
171
+ this.assertContextActive(context);
172
+ const contextKey = runtimeAITContextKey(context);
173
+ const generation = ++this.generation;
174
+
175
+ try {
176
+ const result = await request();
177
+ if (generation !== this.generation || contextKey !== this.activeContextKey) {
178
+ throw new StaleRuntimeAITResponseError();
179
+ }
180
+ apply(result, context);
181
+ return result;
182
+ } catch (error) {
183
+ if (
184
+ !(error instanceof StaleRuntimeAITResponseError)
185
+ && generation === this.generation
186
+ && contextKey === this.activeContextKey
187
+ ) {
188
+ clear();
189
+ }
190
+ throw error;
191
+ }
192
+ }
193
+ }
194
+
195
+ export function useRuntimeAITContextCommit(
196
+ coordinator: RuntimeAITRecomputeCoordinator,
197
+ context: RuntimeAITContext | null,
198
+ invalidate: () => void,
199
+ ): void {
200
+ const contextKey = context ? runtimeAITContextKey(context) : null;
201
+ useCommitPhaseEffect(() => {
202
+ coordinator.commitContext(context, invalidate);
203
+ }, [contextKey, coordinator, invalidate]);
204
+ }
205
+
206
+ export async function dispatchRuntimeAITCompleteReplacement(
207
+ coordinator: RuntimeAITRecomputeCoordinator,
208
+ context: RuntimeAITContext,
209
+ update: () => Promise<unknown>,
210
+ converge: (subjectKey: string) => Promise<void>,
211
+ clear: () => void,
212
+ ): Promise<void> {
213
+ const subjectKey = runtimeAITSubjectKey(context);
214
+ coordinator.assertContextActive(context);
215
+
216
+ let updateFailed = false;
217
+ let updateError: unknown;
218
+ try {
219
+ await update();
220
+ } catch (error) {
221
+ updateFailed = true;
222
+ updateError = error;
223
+ }
224
+
225
+ try {
226
+ await converge(subjectKey);
227
+ } catch (error) {
228
+ if (!updateFailed) {
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ if (updateFailed) {
234
+ if (!coordinator.isContextActive(context)) {
235
+ throw new StaleRuntimeAITResponseError();
236
+ }
237
+ clear();
238
+ throw updateError;
239
+ }
240
+ coordinator.assertContextActive(context);
241
+ }
242
+
243
+ /** Serializes reads, complete replacements, and convergence for one persistence subject. */
244
+ export class RuntimeAITMutationQueue {
245
+ private readonly tails = new Map<string, Promise<void>>();
246
+
247
+ async run<T>(subjectKey: string, mutation: () => Promise<T>): Promise<T> {
248
+ const previous = this.tails.get(subjectKey) ?? Promise.resolve();
249
+ const result = previous.catch(() => undefined).then(mutation);
250
+ const tail = result.then(() => undefined, () => undefined);
251
+ this.tails.set(subjectKey, tail);
252
+ try {
253
+ return await result;
254
+ } finally {
255
+ if (this.tails.get(subjectKey) === tail) {
256
+ this.tails.delete(subjectKey);
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ export type RuntimeAuthorizationState = {
263
+ ait: AIT | null;
264
+ maximumPermissions: string[];
265
+ effectivePermissions: string[];
266
+ deniedPermissions: string[];
267
+ selectedRuleId: string | null;
268
+ selectedRuleType: RuntimeAITResult['selectedRuleType'];
269
+ expiresAt: string | null;
270
+ grantActive: boolean;
271
+ unavailable: boolean;
272
+ };
273
+
274
+ export function runtimeAuthorizationState(
275
+ result: RuntimeAITResult,
276
+ controller: string,
277
+ serviceId: string,
278
+ ): RuntimeAuthorizationState {
279
+ return {
280
+ ait: result.aitId
281
+ ? {
282
+ aitId: result.aitId,
283
+ controller,
284
+ serviceId,
285
+ metadata: { permissions: [...result.effectivePermissions] },
286
+ }
287
+ : null,
288
+ maximumPermissions: [...result.maximumPermissions],
289
+ effectivePermissions: [...result.effectivePermissions],
290
+ deniedPermissions: [...result.deniedPermissions],
291
+ selectedRuleId: result.selectedRuleId,
292
+ selectedRuleType: result.selectedRuleType,
293
+ expiresAt: result.expiresAt,
294
+ grantActive: result.grantActive,
295
+ unavailable: false,
296
+ };
297
+ }
298
+
299
+ export function unavailableRuntimeAuthorization(
300
+ persistedDeniedPermissions: readonly string[],
301
+ ): RuntimeAuthorizationState {
302
+ return {
303
+ ait: null,
304
+ maximumPermissions: [],
305
+ effectivePermissions: [],
306
+ deniedPermissions: [...persistedDeniedPermissions],
307
+ selectedRuleId: null,
308
+ selectedRuleType: 'none',
309
+ expiresAt: null,
310
+ grantActive: false,
311
+ unavailable: true,
312
+ };
313
+ }
@@ -0,0 +1,35 @@
1
+ type PiiMessage = {
2
+ piiStatus?: 'scanning' | 'complete' | 'none';
3
+ piiStep?: 0 | 1 | 2;
4
+ piiProtection?: {
5
+ anonymizedContent?: string;
6
+ mapping?: Record<string, string>;
7
+ };
8
+ };
9
+
10
+ export type PiiScanCompleteData = {
11
+ entityCount?: number;
12
+ anonymizedUserMessage?: string | null;
13
+ mapping?: Record<string, string> | null;
14
+ };
15
+
16
+ export function completePiiScan<T extends PiiMessage>(message: T, data?: PiiScanCompleteData): T {
17
+ const mapping = data?.mapping || {};
18
+ const entityCount = typeof data?.entityCount === 'number'
19
+ ? data.entityCount
20
+ : Object.keys(mapping).length;
21
+
22
+ if (entityCount <= 0 || !data?.anonymizedUserMessage) {
23
+ return { ...message, piiStatus: 'none', piiStep: 1 } as T;
24
+ }
25
+
26
+ return {
27
+ ...message,
28
+ piiStatus: 'complete',
29
+ piiStep: 1,
30
+ piiProtection: {
31
+ anonymizedContent: data.anonymizedUserMessage,
32
+ mapping,
33
+ },
34
+ } as T;
35
+ }
@@ -0,0 +1,20 @@
1
+ import type { PresetMessage } from './types/ChatBotTypes';
2
+
3
+ export function normalizeSuggestionText(value: string): string {
4
+ return value.trim().replace(/\s+/g, ' ').toLocaleLowerCase();
5
+ }
6
+
7
+ export function filterSuggestions(
8
+ suggestions: readonly PresetMessage[],
9
+ lastUserMessage?: string,
10
+ ): PresetMessage[] {
11
+ const lastUserText = normalizeSuggestionText(lastUserMessage || '');
12
+ const seen = new Set<string>();
13
+
14
+ return suggestions.filter((suggestion) => {
15
+ const normalized = normalizeSuggestionText(suggestion.text || '');
16
+ if (!normalized || normalized === lastUserText || seen.has(normalized)) return false;
17
+ seen.add(normalized);
18
+ return true;
19
+ });
20
+ }
@@ -24,11 +24,6 @@ export interface NovaError {
24
24
  error: string;
25
25
  }
26
26
 
27
- export interface AITMetadata {
28
- permissions: string[];
29
- issuedBy: string;
30
- }
31
-
32
27
  // AI Model related types
33
28
  export interface AIModel {
34
29
  label: string;
@@ -62,6 +57,8 @@ export interface ChatBotProps {
62
57
  token: string;
63
58
  } | undefined>;
64
59
  permissionGroup?: string;
60
+ /** Ordered runtime identity-provider roles. Order is preserved for Group fallback. */
61
+ roles?: string[];
65
62
  children?: React.ReactNode;
66
63
  // AI Model related properties
67
64
  onModelChange?: (model: AIModel) => void;
@@ -103,6 +100,9 @@ export interface ChatBotContextType {
103
100
  hitAddress: string | null;
104
101
  ait: AIT | null;
105
102
  permissions: string[];
103
+ maximumPermissions: string[];
104
+ deniedPermissions: string[];
105
+ runtimePermissionsUnavailable: boolean;
106
106
  availablePermissions: ServicePermission[];
107
107
  showPermissionForm: boolean;
108
108
  isPermissionFormOpen: boolean;
@@ -151,7 +151,6 @@ export interface ChatBotContextType {
151
151
  setIsOpen: (open: boolean) => void;
152
152
  setShowPermissionForm: (show: boolean) => void;
153
153
  setIsPermissionFormOpen: (open: boolean) => void;
154
- setPermissions: (permissions: string[]) => void;
155
154
  setIsDisabled: (disabled: boolean) => void;
156
155
  setIsWalletLoading: (loading: boolean) => void;
157
156
  setNotification: (notification: any) => void;
@@ -167,7 +166,7 @@ export interface ChatBotContextType {
167
166
  retryPendingTextTool: () => Promise<boolean>;
168
167
  sendMessage: (content: string, retryCount?: number, isPresetMessage?: boolean, attachments?: Attachment[]) => Promise<void>;
169
168
  handleSubmit: (e: React.FormEvent, attachments?: Attachment[]) => Promise<void>;
170
- handlePresetMessage: (message: PresetMessage) => void;
169
+ handlePresetMessage: (message: PresetMessage) => Promise<void>;
171
170
  savePermissions: (newPermissions?: string[]) => Promise<void>;
172
171
  enableAIT: (toolName: string) => Promise<boolean>;
173
172
  handleVerifyWalletClick: (method: 'berifyme' | 'custom') => Promise<void>;
@@ -200,6 +199,7 @@ export interface ChatBotContextType {
200
199
  onVerifyWallet: (method: 'berifyme' | 'custom') => Promise<void>;
201
200
  serviceId: string;
202
201
  permissionGroup?: string;
202
+ roles?: string[];
203
203
 
204
204
  // Props
205
205
  props: ChatBotProps;
@@ -15,10 +15,10 @@ export const ModelSelector: React.FC = () => {
15
15
 
16
16
  // Safety check: ensure selectedModelIndex is within bounds
17
17
  const safeSelectedModelIndex = selectedModelIndex >= availableModels.length ? 0 : selectedModelIndex;
18
-
18
+
19
19
  // Check if current service is Adilas
20
20
  const isAdilasService = walletTextUtils.isAdilasService(serviceId);
21
-
21
+
22
22
  // Get fallback model label (first available model or 'Unknown')
23
23
  // For Adilas, we don't show the label, only the image
24
24
  const fallbackModelLabel = availableModels.length > 0
@@ -120,7 +120,7 @@ export const ModelSelector: React.FC = () => {
120
120
  font-size: 12px !important;
121
121
  font-weight: 500 !important;
122
122
  gap: 6px !important;
123
-
123
+
124
124
  &:hover {
125
125
  background-color: rgba(255, 255, 255, 0.2) !important;
126
126
  }
@@ -159,7 +159,7 @@ export const ModelSelector: React.FC = () => {
159
159
  color: ${index === safeSelectedModelIndex ? '#007bff' : '#333'} !important;
160
160
  font-size: 12px !important;
161
161
  border-bottom: ${index < availableModels.length - 1 ? '1px solid #eee' : 'none'} !important;
162
-
162
+
163
163
  &:hover {
164
164
  background-color: ${index === safeSelectedModelIndex ? '#f0f0f0' : '#f8f9fa'} !important;
165
165
  }
@@ -187,4 +187,4 @@ export const ModelSelector: React.FC = () => {
187
187
  )}
188
188
  </div>
189
189
  );
190
- };
190
+ };
@@ -14,7 +14,8 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
14
14
  const {
15
15
  hitAddress,
16
16
  permissions,
17
- setPermissions,
17
+ availablePermissions,
18
+ runtimePermissionsUnavailable,
18
19
  setIsDisabled,
19
20
  onSave,
20
21
  onConnectWallet,
@@ -23,15 +24,12 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
23
24
  walletInfo,
24
25
  onVerifyWallet,
25
26
  serviceId,
26
- nxtlinqApi,
27
- permissionGroup,
28
27
  isAITLoading,
29
28
  isWalletLoading = false,
30
29
  refreshAIT,
31
30
  props
32
31
  } = useChatBot();
33
32
 
34
- const [availablePermissions, setAvailablePermissions] = React.useState<any[]>([]);
35
33
  const [isSaving, setIsSaving] = React.useState(false);
36
34
  const [tempPermissions, setTempPermissions] = React.useState<string[]>(permissions);
37
35
  const [hasUserInteracted, setHasUserInteracted] = React.useState(false);
@@ -64,28 +62,6 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
64
62
  // eslint-disable-next-line react-hooks/exhaustive-deps
65
63
  }, [isAllSelected, availablePermissions.length]);
66
64
 
67
- const fetchAvailablePermissions = async () => {
68
- if (!serviceId) return;
69
-
70
- try {
71
- const result = await nxtlinqApi.permissions.getServicePermissions({
72
- serviceId,
73
- ...(permissionGroup && { groupName: permissionGroup })
74
- });
75
- if ('error' in result) {
76
- console.error('Failed to fetch permissions:', result.error);
77
- return;
78
- }
79
- setAvailablePermissions(result.permissions);
80
- } catch (error) {
81
- console.error('Error fetching permissions:', error);
82
- }
83
- };
84
-
85
- React.useEffect(() => {
86
- fetchAvailablePermissions();
87
- }, [serviceId, nxtlinqApi, permissionGroup]);
88
-
89
65
  // Refresh wallet info when component mounts or when hitAddress changes
90
66
  React.useEffect(() => {
91
67
  if (hitAddress) {
@@ -112,8 +88,6 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
112
88
  const handleSave = async () => {
113
89
  setIsSaving(true);
114
90
  try {
115
- // Update the actual permissions with temp permissions
116
- setPermissions(tempPermissions);
117
91
  await onSave(tempPermissions);
118
92
  setHasUserInteracted(false);
119
93
  } finally {
@@ -476,6 +450,21 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
476
450
  `}>
477
451
  Loading permissions...
478
452
  </div>
453
+ ) : runtimePermissionsUnavailable ? (
454
+ <div css={css`
455
+ background-color: #fff4e5 !important;
456
+ padding: 16px !important;
457
+ border-radius: 8px !important;
458
+ border: 1px solid #ffb74d !important;
459
+ text-align: center !important;
460
+ color: #8a4b00 !important;
461
+ `}>
462
+ Permissions are unavailable. Retry to load the authoritative state.
463
+ <button type="button" onClick={() => refreshAIT()} css={css`
464
+ display: block !important;
465
+ margin: 12px auto 0 !important;
466
+ `}>Retry</button>
467
+ </div>
479
468
  ) : (
480
469
  <div css={css`
481
470
  background-color: #f8f9fa !important;
@@ -543,7 +532,7 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
543
532
  setIsDisabled(false);
544
533
  }
545
534
  }}
546
- disabled={isAITLoading || isOtherPermissionDisabled}
535
+ disabled={isAITLoading || runtimePermissionsUnavailable || isOtherPermissionDisabled}
547
536
  css={css`
548
537
  margin: 0 !important;
549
538
  width: 18px !important;
@@ -595,7 +584,7 @@ export const PermissionForm: React.FC<PermissionFormProps> = ({ onClose }) => {
595
584
  </button>
596
585
  <button
597
586
  onClick={handleSave}
598
- disabled={!hasPermissionChanges() || isSaving || isAITLoading}
587
+ disabled={!hasPermissionChanges() || isSaving || isAITLoading || runtimePermissionsUnavailable}
599
588
  css={css`
600
589
  padding: 10px 20px !important;
601
590
  background-color: ${!hasPermissionChanges() || isSaving || isAITLoading ? '#e9ecef' : '#007bff'} !important;
@@ -4,11 +4,14 @@ import { css } from '@emotion/react';
4
4
  import { useChatBot } from '../context/ChatBotContext';
5
5
  import { PresetMessage } from '../types/ChatBotTypes';
6
6
  import { actionButton } from './styles/isolatedStyles';
7
+ import { filterSuggestions, normalizeSuggestionText } from '../suggestionState';
7
8
 
8
9
  export const PresetMessages: React.FC = () => {
9
- const { suggestions, handlePresetMessage } = useChatBot();
10
+ const { suggestions, messages, handlePresetMessage } = useChatBot();
11
+ const lastUserMessage = [...messages].reverse().find(message => message.role === 'user')?.content;
12
+ const visibleSuggestions = filterSuggestions(suggestions || [], lastUserMessage);
10
13
 
11
- if (!suggestions || suggestions.length === 0) {
14
+ if (visibleSuggestions.length === 0) {
12
15
  return null;
13
16
  }
14
17
 
@@ -22,10 +25,10 @@ export const PresetMessages: React.FC = () => {
22
25
  gap: 10px !important;
23
26
  align-items: center !important;
24
27
  `}>
25
- {suggestions.map((preset: PresetMessage, index: number) => (
28
+ {visibleSuggestions.map((preset: PresetMessage) => (
26
29
  <button
27
- key={index}
28
- onClick={() => handlePresetMessage(preset)}
30
+ key={normalizeSuggestionText(preset.text)}
31
+ onClick={() => { void handlePresetMessage(preset); }}
29
32
  css={css`
30
33
  ${actionButton}
31
34
  padding: 5px 10px !important;
@@ -47,4 +50,4 @@ export const PresetMessages: React.FC = () => {
47
50
  ))}
48
51
  </div>
49
52
  );
50
- };
53
+ };