@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/dist/aitPermissionPropagation.d.ts +16 -0
- package/dist/aitPermissionPropagation.d.ts.map +1 -0
- package/dist/aitPermissionPropagation.js +21 -0
- package/dist/context/ChatBotContext.d.ts.map +1 -1
- package/dist/context/ChatBotContext.js +230 -231
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/piiMessageState.d.ts +16 -0
- package/dist/piiMessageState.d.ts.map +1 -0
- package/dist/piiMessageState.js +18 -0
- package/dist/suggestionState.d.ts +4 -0
- package/dist/suggestionState.d.ts.map +1 -0
- package/dist/suggestionState.js +14 -0
- package/dist/types/ChatBotTypes.d.ts +7 -6
- package/dist/types/ChatBotTypes.d.ts.map +1 -1
- package/dist/ui/ModelSelector.js +2 -2
- package/dist/ui/PermissionForm.d.ts.map +1 -1
- package/dist/ui/PermissionForm.js +14 -28
- package/dist/ui/PresetMessages.d.ts.map +1 -1
- package/dist/ui/PresetMessages.js +7 -4
- package/package.json +4 -4
- package/src/aitPermissionPropagation.ts +40 -0
- package/src/context/ChatBotContext.tsx +309 -266
- package/src/index.ts +0 -1
- package/src/permissionState.ts +313 -0
- package/src/piiMessageState.ts +35 -0
- package/src/suggestionState.ts +20 -0
- package/src/types/ChatBotTypes.ts +7 -7
- package/src/ui/ModelSelector.tsx +5 -5
- package/src/ui/PermissionForm.tsx +19 -30
- package/src/ui/PresetMessages.tsx +9 -6
package/src/index.ts
CHANGED
|
@@ -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;
|
package/src/ui/ModelSelector.tsx
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
{
|
|
28
|
+
{visibleSuggestions.map((preset: PresetMessage) => (
|
|
26
29
|
<button
|
|
27
|
-
key={
|
|
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
|
+
};
|