@qafka/react-native 2.4.2 → 2.6.0
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/CHANGELOG.md +24 -0
- package/README.md +72 -0
- package/dist/QafkaSDK.d.ts +18 -3
- package/dist/QafkaSDK.js +59 -6
- package/dist/components/ChatPage.d.ts +7 -1
- package/dist/components/ChatPage.js +19 -8
- package/dist/components/Qafka.js +5 -2
- package/dist/components/Qafka.types.d.ts +36 -15
- package/dist/hooks/useChatMessages.js +8 -0
- package/dist/hooks/useSDK.d.ts +9 -1
- package/dist/hooks/useSDK.js +15 -1
- package/dist/services/BackendService.d.ts +29 -3
- package/dist/services/BackendService.js +89 -11
- package/dist/services/projectUnavailable.d.ts +26 -0
- package/dist/services/projectUnavailable.js +46 -0
- package/dist/types/image.d.ts +23 -0
- package/dist/types/image.js +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/sdk.d.ts +5 -4
- package/docs/qafka-tool-authoring-guide.md +18 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.6.0] — 2026-09-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Unavailable state: when the assistant for a project is temporarily unavailable, the chat screen now shows a neutral, localized message ("The assistant is currently unavailable." / Turkish when `locale` starts with `tr`) instead of an error. The input is hidden and no error bubble or initialization banner is shown.
|
|
15
|
+
- New `onUnavailable` prop on `<Qafka />`, called when the assistant becomes unavailable (at start-up or mid-conversation). Use it to hide or disable your own entry point to the chat.
|
|
16
|
+
- `SDKStatusType` gains `'unavailable'`, delivered through `onStatusChange` and `QafkaSDK.getStatus()`.
|
|
17
|
+
- Localized greeting: when the `locale` prop is set, the project theme request now includes it, so the greeting and initial message are returned in that language when a translation exists. Without `locale` the request is unchanged.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `QafkaSDK.initialize()` now waits for the project theme request to settle before resolving, so the final status (`'ready'` or `'unavailable'`) is known when it returns. `onError` is not called for the unavailable state.
|
|
22
|
+
- If you `switch` exhaustively over `SDKStatusType` in TypeScript, add a case for `'unavailable'`.
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- Clarified how the `locale`, `context` and `contextDescription` props are used, with examples, and documented `onUnavailable` in the tool authoring guide.
|
|
27
|
+
|
|
28
|
+
## [2.5.0] — 2026-07-19
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Image generation: a new `generateImage()` method creates an image from a text description, or reframes one or more reference images to a target aspect ratio or exact pixel size without stretching or distortion. Results can be returned as base64, a signed time-limited URL, or both, and multiple variants can be requested in a single call. Higher-quality `pro` output is available when it is enabled on your plan. See the "Image Generation" section of the README for the full API, response shape, and error handling.
|
|
33
|
+
|
|
10
34
|
## [2.4.2] — 2026-06-22
|
|
11
35
|
|
|
12
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -87,6 +87,78 @@ Pass `projectId` when your app has more than one registered project:
|
|
|
87
87
|
<Qafka projectId="proj_abc123" />
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Image Generation
|
|
91
|
+
|
|
92
|
+
Generate images from text descriptions or reframe reference images to target aspect ratios and sizes without distortion. The SDK must be initialized before calling this method.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { getSDK } from '@qafka/react-native';
|
|
96
|
+
|
|
97
|
+
const result = await getSDK().generateImage({
|
|
98
|
+
description: 'product photo reframed to 16:9 landscape',
|
|
99
|
+
referenceImage: 'https://example.com/photo.jpg',
|
|
100
|
+
aspectRatio: '16:9',
|
|
101
|
+
quality: 'standard',
|
|
102
|
+
n: 1,
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Parameters
|
|
107
|
+
|
|
108
|
+
| Param | Type | Required | Notes |
|
|
109
|
+
|-------|------|----------|-------|
|
|
110
|
+
| `description` | string | Yes | Text description of the desired image or transformation. |
|
|
111
|
+
| `referenceImage` | string \| string[] | No | Base64 data-URI, URL, or array of either. Used to guide or reframe the output. |
|
|
112
|
+
| `aspectRatio` | string | No | Free-form ratio, e.g. `"16:9"`, `"7:3"`, `"1:1"`. Ignored if `size` is set. |
|
|
113
|
+
| `size` | `{ width: number; height: number }` | No | Exact pixel dimensions. Overrides `aspectRatio`. |
|
|
114
|
+
| `quality` | `'standard' \| 'pro'` | No | Default: `'standard'`. `'pro'` requires image generation pro to be enabled on your plan. |
|
|
115
|
+
| `responseType` | `'base64' \| 'url' \| 'both'` | No | Default: `'base64'`. `'url'` returns a signed, time-limited link; `'both'` returns both. |
|
|
116
|
+
| `n` | number | No | Number of variants to generate. Default: `1`. When `n > 1`, result is an array. |
|
|
117
|
+
|
|
118
|
+
### Result
|
|
119
|
+
|
|
120
|
+
Each generated image is an object with:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
{
|
|
124
|
+
base64?: string; // Returned when responseType includes 'base64'
|
|
125
|
+
url?: string; // Signed link, time-limited. Returned when responseType includes 'url'
|
|
126
|
+
expiresAt?: string; // ISO 8601 timestamp. Present when url is returned
|
|
127
|
+
mimeType: string; // e.g. 'image/png'
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When `n > 1`, the result is an array of such objects.
|
|
132
|
+
|
|
133
|
+
### Example
|
|
134
|
+
|
|
135
|
+
Reframe an uploaded product photo to a banner size and fetch the result as a base64 image:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
try {
|
|
139
|
+
const result = await getSDK().generateImage({
|
|
140
|
+
description: 'High-quality product banner, maintaining all product details',
|
|
141
|
+
referenceImage: productPhotoBase64,
|
|
142
|
+
size: { width: 1200, height: 300 },
|
|
143
|
+
responseType: 'base64',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
setGeneratedImage(result.base64);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err.code === 'not_entitled') {
|
|
149
|
+
showAlert('Image generation is not enabled on your plan.');
|
|
150
|
+
} else if (err.code === 'rate_limited') {
|
|
151
|
+
showAlert('Too many requests. Please try again later.');
|
|
152
|
+
} else if (err.code === 'content_filtered') {
|
|
153
|
+
showAlert('The description or reference image was filtered for policy reasons.');
|
|
154
|
+
} else if (err.code === 'invalid_reference') {
|
|
155
|
+
showAlert('The reference image could not be processed.');
|
|
156
|
+
} else {
|
|
157
|
+
showAlert('Image generation failed. Please try again.');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
90
162
|
## Documentation
|
|
91
163
|
|
|
92
164
|
Full guides, API reference, theming, voice mode, tools, and advanced configuration:
|
package/dist/QafkaSDK.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SDKConfig, SDKStatusType, ChatMessage, ChatResponse } from './types';
|
|
1
|
+
import { SDKConfig, SDKStatusType, ChatMessage, ChatResponse, GenerateImageParams, GenerateImageResult } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* QafkaSDK - Main SDK class
|
|
4
4
|
* Provides AI-powered conversational interface for React Native apps
|
|
@@ -13,6 +13,7 @@ export declare class QafkaSDK {
|
|
|
13
13
|
private conversationManager;
|
|
14
14
|
private navigationHandler;
|
|
15
15
|
private themePrefetchPromise;
|
|
16
|
+
private themePrefetchError;
|
|
16
17
|
private constructor();
|
|
17
18
|
/**
|
|
18
19
|
* Get singleton instance
|
|
@@ -25,8 +26,9 @@ export declare class QafkaSDK {
|
|
|
25
26
|
/**
|
|
26
27
|
* developer-driven locale (BCP 47 — e.g. "tr", "en", "tr-TR").
|
|
27
28
|
* Forwarded to BackendService so it lands in `sdkContext.locale` on every
|
|
28
|
-
* outgoing chat request
|
|
29
|
-
*
|
|
29
|
+
* outgoing chat request — the language the assistant answers in. Explicit
|
|
30
|
+
* only: never derived from the device language, and it leaves the SDK's own
|
|
31
|
+
* UI strings untouched.
|
|
30
32
|
*
|
|
31
33
|
* Pass `null` to clear (e.g. when switching back to instructions-default).
|
|
32
34
|
* Calling before initialize() is a no-op; pass `locale` via SDKConfig instead.
|
|
@@ -131,6 +133,10 @@ export declare class QafkaSDK {
|
|
|
131
133
|
type: string;
|
|
132
134
|
}, conversationId?: string): Promise<any>;
|
|
133
135
|
/**
|
|
136
|
+
* Generate an image based on a description
|
|
137
|
+
*/
|
|
138
|
+
generateImage(params: GenerateImageParams): Promise<GenerateImageResult>;
|
|
139
|
+
/**
|
|
134
140
|
* Get project theme configuration
|
|
135
141
|
*/
|
|
136
142
|
getProjectTheme(): Promise<any>;
|
|
@@ -151,6 +157,15 @@ export declare class QafkaSDK {
|
|
|
151
157
|
*/
|
|
152
158
|
getStatus(): SDKStatusType;
|
|
153
159
|
/**
|
|
160
|
+
* Internal — not part of the documented public surface. Flips the SDK to
|
|
161
|
+
* the 'unavailable' status mid-session (e.g. a chat request comes back
|
|
162
|
+
* with `code === 'PROJECT_UNAVAILABLE'` after the project was already
|
|
163
|
+
* initialized successfully). Notifies the same `onStatusChange` listener
|
|
164
|
+
* `initialize()` uses, so `useSDK` reacts identically regardless of
|
|
165
|
+
* whether unavailability was detected at init time or mid-session.
|
|
166
|
+
*/
|
|
167
|
+
markUnavailable(): void;
|
|
168
|
+
/**
|
|
154
169
|
* Get SDK configuration
|
|
155
170
|
*/
|
|
156
171
|
getConfig(): SDKConfig | null;
|
package/dist/QafkaSDK.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getSDK = exports.QafkaSDK = void 0;
|
|
4
4
|
const storage_1 = require("./services/storage");
|
|
5
5
|
const BackendService_1 = require("./services/BackendService");
|
|
6
|
+
const projectUnavailable_1 = require("./services/projectUnavailable");
|
|
6
7
|
const AttestationManager_1 = require("./services/AttestationManager");
|
|
7
8
|
const ConversationManager_1 = require("./services/ConversationManager");
|
|
8
9
|
const NavigationHandler_1 = require("./services/NavigationHandler");
|
|
@@ -24,6 +25,11 @@ class QafkaSDK {
|
|
|
24
25
|
// ChatPage can render the branded greeting without waiting for SDK ready.
|
|
25
26
|
// See `useProjectTheme` which awaits this promise.
|
|
26
27
|
themePrefetchPromise = null;
|
|
28
|
+
// Captured alongside themePrefetchPromise (which swallows its rejection so
|
|
29
|
+
// useProjectTheme always gets a settled promise). initialize() inspects
|
|
30
|
+
// this after the prefetch settles to decide whether the final status is
|
|
31
|
+
// 'ready' or 'unavailable'.
|
|
32
|
+
themePrefetchError = null;
|
|
27
33
|
constructor() { }
|
|
28
34
|
/**
|
|
29
35
|
* Get singleton instance
|
|
@@ -78,7 +84,10 @@ class QafkaSDK {
|
|
|
78
84
|
// needs are not ready yet — firing the prefetch now would fail, blank out
|
|
79
85
|
// the greeting, and need a retry. Deferred to after init below.
|
|
80
86
|
if (apiKey) {
|
|
81
|
-
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch(() =>
|
|
87
|
+
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch((err) => {
|
|
88
|
+
this.themePrefetchError = err;
|
|
89
|
+
return null;
|
|
90
|
+
});
|
|
82
91
|
}
|
|
83
92
|
// Save config to storage
|
|
84
93
|
await storage_1.storage.setItem('@qafka/config', JSON.stringify(config));
|
|
@@ -103,7 +112,10 @@ class QafkaSDK {
|
|
|
103
112
|
// credentials it needs and the prefetch will succeed on first try,
|
|
104
113
|
// hydrating useProjectTheme before the chat surface paints.
|
|
105
114
|
if (!apiKey && !this.themePrefetchPromise) {
|
|
106
|
-
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch(() =>
|
|
115
|
+
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch((err) => {
|
|
116
|
+
this.themePrefetchError = err;
|
|
117
|
+
return null;
|
|
118
|
+
});
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
catch (error) {
|
|
@@ -119,9 +131,19 @@ class QafkaSDK {
|
|
|
119
131
|
throw error;
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
|
-
|
|
134
|
+
// Decide the final status after the theme prefetch settles — a
|
|
135
|
+
// ProjectUnavailableError there means the project is suspended, which
|
|
136
|
+
// takes priority over an otherwise-successful attestation. This never
|
|
137
|
+
// rejects: the prefetch promise already swallows its own rejection
|
|
138
|
+
// (see themePrefetchError above).
|
|
139
|
+
if (this.themePrefetchPromise) {
|
|
140
|
+
await this.themePrefetchPromise;
|
|
141
|
+
}
|
|
142
|
+
this.status = this.themePrefetchError instanceof projectUnavailable_1.ProjectUnavailableError
|
|
143
|
+
? 'unavailable'
|
|
144
|
+
: 'ready';
|
|
123
145
|
if (config.onStatusChange) {
|
|
124
|
-
config.onStatusChange(
|
|
146
|
+
config.onStatusChange(this.status);
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
catch (error) {
|
|
@@ -146,8 +168,9 @@ class QafkaSDK {
|
|
|
146
168
|
/**
|
|
147
169
|
* developer-driven locale (BCP 47 — e.g. "tr", "en", "tr-TR").
|
|
148
170
|
* Forwarded to BackendService so it lands in `sdkContext.locale` on every
|
|
149
|
-
* outgoing chat request
|
|
150
|
-
*
|
|
171
|
+
* outgoing chat request — the language the assistant answers in. Explicit
|
|
172
|
+
* only: never derived from the device language, and it leaves the SDK's own
|
|
173
|
+
* UI strings untouched.
|
|
151
174
|
*
|
|
152
175
|
* Pass `null` to clear (e.g. when switching back to instructions-default).
|
|
153
176
|
* Calling before initialize() is a no-op; pass `locale` via SDKConfig instead.
|
|
@@ -383,6 +406,21 @@ class QafkaSDK {
|
|
|
383
406
|
return this.backendService.uploadFile(toolId, file, conversationId);
|
|
384
407
|
}
|
|
385
408
|
/**
|
|
409
|
+
* Generate an image based on a description
|
|
410
|
+
*/
|
|
411
|
+
async generateImage(params) {
|
|
412
|
+
if (this.status !== 'ready') {
|
|
413
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
414
|
+
}
|
|
415
|
+
if (!params?.description?.trim()) {
|
|
416
|
+
throw new Error('description cannot be empty');
|
|
417
|
+
}
|
|
418
|
+
if (!this.backendService) {
|
|
419
|
+
throw new Error('Backend service not initialized');
|
|
420
|
+
}
|
|
421
|
+
return this.backendService.generateImage(params);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
386
424
|
* Get project theme configuration
|
|
387
425
|
*/
|
|
388
426
|
async getProjectTheme() {
|
|
@@ -429,6 +467,20 @@ class QafkaSDK {
|
|
|
429
467
|
return this.status;
|
|
430
468
|
}
|
|
431
469
|
/**
|
|
470
|
+
* Internal — not part of the documented public surface. Flips the SDK to
|
|
471
|
+
* the 'unavailable' status mid-session (e.g. a chat request comes back
|
|
472
|
+
* with `code === 'PROJECT_UNAVAILABLE'` after the project was already
|
|
473
|
+
* initialized successfully). Notifies the same `onStatusChange` listener
|
|
474
|
+
* `initialize()` uses, so `useSDK` reacts identically regardless of
|
|
475
|
+
* whether unavailability was detected at init time or mid-session.
|
|
476
|
+
*/
|
|
477
|
+
markUnavailable() {
|
|
478
|
+
this.status = 'unavailable';
|
|
479
|
+
if (this.config?.onStatusChange) {
|
|
480
|
+
this.config.onStatusChange('unavailable');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
432
484
|
* Get SDK configuration
|
|
433
485
|
*/
|
|
434
486
|
getConfig() {
|
|
@@ -465,6 +517,7 @@ class QafkaSDK {
|
|
|
465
517
|
this.subProjectId = null;
|
|
466
518
|
this.config = null;
|
|
467
519
|
this.themePrefetchPromise = null;
|
|
520
|
+
this.themePrefetchError = null;
|
|
468
521
|
this.status = 'uninitialized';
|
|
469
522
|
// Only clear the shared singleton slot when WE are still the current
|
|
470
523
|
// instance. An outgoing component unmounting must not null a newer
|
|
@@ -19,6 +19,12 @@ export interface ChatPageProps {
|
|
|
19
19
|
} | null;
|
|
20
20
|
sdkReady: boolean;
|
|
21
21
|
sdkError: string | null;
|
|
22
|
+
/** True when the project is unavailable (suspended). Replaces the whole
|
|
23
|
+
* chat surface (messages, composer, error banner) with a neutral, centered
|
|
24
|
+
* message instead. */
|
|
25
|
+
unavailable?: boolean;
|
|
26
|
+
/** Forwarded to `unavailableMessage()` for the neutral copy above. */
|
|
27
|
+
locale?: string;
|
|
22
28
|
theme: Theme;
|
|
23
29
|
handleSend: (text: string) => void;
|
|
24
30
|
handleActionClick: (action: any) => void;
|
|
@@ -41,4 +47,4 @@ export interface ChatPageProps {
|
|
|
41
47
|
cardHostCallbacks?: import('../cards/cta/types').CardCTAHostCallbacks;
|
|
42
48
|
cardLifecycle?: import('../cards/cta/types').CardCTALifecycle;
|
|
43
49
|
}
|
|
44
|
-
export declare function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, toolStatus, sdkReady, sdkError, theme, handleSend, handleActionClick, handleExternalSuggestionPress, flatListRef, placeholder, maxMessageLength, showTimestamps, greetingMessage, apiGreeting, isInitialMessageEnabled, initialMessageText, componentRegistry, onBack, onClose, BackComponent, CloseComponent, NavigationButtonComponent, cardHostCallbacks, cardLifecycle, }: ChatPageProps): React.JSX.Element;
|
|
50
|
+
export declare function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, toolStatus, sdkReady, sdkError, unavailable, locale, theme, handleSend, handleActionClick, handleExternalSuggestionPress, flatListRef, placeholder, maxMessageLength, showTimestamps, greetingMessage, apiGreeting, isInitialMessageEnabled, initialMessageText, componentRegistry, onBack, onClose, BackComponent, CloseComponent, NavigationButtonComponent, cardHostCallbacks, cardLifecycle, }: ChatPageProps): React.JSX.Element;
|
|
@@ -11,7 +11,8 @@ const InputArea_1 = require("./InputArea");
|
|
|
11
11
|
const TypingIndicator_1 = require("./TypingIndicator");
|
|
12
12
|
const ToolStatusPill_1 = require("./ToolStatusPill");
|
|
13
13
|
const Qafka_styles_1 = require("./Qafka.styles");
|
|
14
|
-
|
|
14
|
+
const projectUnavailable_1 = require("../services/projectUnavailable");
|
|
15
|
+
function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, toolStatus, sdkReady, sdkError, unavailable, locale, theme, handleSend, handleActionClick, handleExternalSuggestionPress, flatListRef, placeholder, maxMessageLength, showTimestamps, greetingMessage, apiGreeting, isInitialMessageEnabled, initialMessageText, componentRegistry, onBack, onClose, BackComponent, CloseComponent, NavigationButtonComponent, cardHostCallbacks, cardLifecycle, }) {
|
|
15
16
|
const styles = (0, Qafka_styles_1.getWidgetStyles)(theme);
|
|
16
17
|
const renderMessage = ({ item }) => (<MessageBubble_1.MessageBubble message={item.text || item.content} role={item.role === 'system' ? 'assistant' : item.role} timestamp={new Date(item.timestamp)} theme={theme} showTimestamp={showTimestamps} actions={item.actions} onActionPress={handleActionClick} componentRegistry={componentRegistry} toolDefinition={item.toolResponse?.tool} toolResultData={item.toolResponse?.data} NavigationButtonComponent={NavigationButtonComponent} actionResults={item.actionResults} completedSteps={item.completedSteps} externalSuggestions={item.externalSuggestions} onExternalPress={handleExternalSuggestionPress} card={item.card} cardHostCallbacks={cardHostCallbacks} cardLifecycle={cardLifecycle}/>);
|
|
17
18
|
const renderEmpty = () => {
|
|
@@ -54,6 +55,15 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
54
55
|
</react_native_1.View>
|
|
55
56
|
</react_native_1.View>)}
|
|
56
57
|
|
|
58
|
+
{unavailable ? (
|
|
59
|
+
/* Project unavailable (suspended): replace the whole chat surface —
|
|
60
|
+
messages, composer, error banner — with a neutral, centered message.
|
|
61
|
+
No "Something went wrong" bubble, no input, no typing indicator. */
|
|
62
|
+
<react_native_1.View style={[styles.emptyStateStyle, { flex: 1 }]}>
|
|
63
|
+
<react_native_1.Text style={styles.emptyStateTextStyle}>
|
|
64
|
+
{(0, projectUnavailable_1.unavailableMessage)(locale)}
|
|
65
|
+
</react_native_1.Text>
|
|
66
|
+
</react_native_1.View>) : (<>
|
|
57
67
|
{/* Error banner */}
|
|
58
68
|
{sdkError && (<react_native_1.View style={styles.errorStyle}>
|
|
59
69
|
<react_native_1.Text style={styles.errorTextStyle}>{sdkError}</react_native_1.Text>
|
|
@@ -63,10 +73,10 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
63
73
|
<react_native_1.FlatList ref={flatListRef} data={displayMessages} renderItem={renderMessage} keyExtractor={(item) => item.id} style={{ flex: 1, flexGrow: 1, flexShrink: 1 }} contentContainerStyle={styles.messageListStyle} ListEmptyComponent={renderEmpty} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" ListFooterComponent={toolStatus ? (<react_native_1.View style={{ paddingHorizontal: 8, paddingTop: 4, alignItems: 'flex-start' }}>
|
|
64
74
|
<ToolStatusPill_1.ToolStatusPill message={toolStatus.message} theme={theme}/>
|
|
65
75
|
</react_native_1.View>) : null} onContentSizeChange={() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
if (displayMessages.length > 0) {
|
|
77
|
+
flatListRef.current?.scrollToEnd({ animated: true });
|
|
78
|
+
}
|
|
79
|
+
}}/>
|
|
70
80
|
|
|
71
81
|
{/* Typing Indicator */}
|
|
72
82
|
{isTyping && !streamingMessage && (<react_native_1.View style={styles.typingIndicatorContainer}>
|
|
@@ -74,11 +84,12 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
74
84
|
</react_native_1.View>)}
|
|
75
85
|
|
|
76
86
|
{/* Tool Status Pill now renders inside the list (ListFooterComponent)
|
|
77
|
-
|
|
87
|
+
so it sits directly under the last message and scrolls with it. */}
|
|
78
88
|
|
|
79
89
|
{/* Input Area — hidden until SDK is ready so users can't type a message
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
that would be rejected by an un-attested send. TypingIndicator in the
|
|
91
|
+
empty state carries the "still preparing" signal. */}
|
|
82
92
|
{sdkReady && (<InputArea_1.InputArea theme={theme} placeholder={placeholder} onSend={handleSend} isSending={isSending} maxLength={maxMessageLength}/>)}
|
|
93
|
+
</>)}
|
|
83
94
|
</>);
|
|
84
95
|
}
|
package/dist/components/Qafka.js
CHANGED
|
@@ -87,10 +87,10 @@ const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
|
|
|
87
87
|
* />
|
|
88
88
|
* ```
|
|
89
89
|
*/
|
|
90
|
-
exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, devConfig, subProjectId, projectId, locale, mode = 'fullscreen', theme: themeName = 'light', customTheme, themeOverride, isAuthenticated, endUserId, endUserData, enableStreaming = true, voiceEnabled: voiceEnabledProp = true, context: userContext, contextDescription, components: customComponents, showTimestamps = true, placeholder = 'Type a message...', maxMessageLength = 500, greetingMessage, onReady, onMessageSent, onResponseReceived, onError, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
|
|
90
|
+
exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, devConfig, subProjectId, projectId, locale, mode = 'fullscreen', theme: themeName = 'light', customTheme, themeOverride, isAuthenticated, endUserId, endUserData, enableStreaming = true, voiceEnabled: voiceEnabledProp = true, context: userContext, contextDescription, components: customComponents, showTimestamps = true, placeholder = 'Type a message...', maxMessageLength = 500, greetingMessage, onReady, onMessageSent, onResponseReceived, onError, onUnavailable, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
|
|
91
91
|
// === ALL HOOKS FIRST (Rules of Hooks compliance) ===
|
|
92
92
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
93
|
-
const { sdkReady, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
|
|
93
|
+
const { sdkReady, unavailable, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
|
|
94
94
|
apiUrl: apiUrlProp,
|
|
95
95
|
devConfig,
|
|
96
96
|
subProjectId,
|
|
@@ -98,6 +98,7 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
98
98
|
locale,
|
|
99
99
|
onReady,
|
|
100
100
|
onError,
|
|
101
|
+
onUnavailable,
|
|
101
102
|
});
|
|
102
103
|
const { theme, isLoadingTheme, greeting: apiGreeting, initialMessage: initialMessageText, isInitialMessageEnabled, voiceEnabled, } = (0, useProjectTheme_1.useProjectTheme)({
|
|
103
104
|
sdkReady,
|
|
@@ -432,6 +433,8 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
432
433
|
toolStatus,
|
|
433
434
|
sdkReady,
|
|
434
435
|
sdkError,
|
|
436
|
+
unavailable,
|
|
437
|
+
locale,
|
|
435
438
|
theme,
|
|
436
439
|
handleSend,
|
|
437
440
|
handleActionClick,
|
|
@@ -283,14 +283,14 @@ export interface QafkaProps {
|
|
|
283
283
|
/**
|
|
284
284
|
* BCP 47 locale (e.g. "tr", "en", "tr-TR").
|
|
285
285
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* another language.
|
|
286
|
+
* The language your app declares the user speaks. Forwarded to the backend
|
|
287
|
+
* as `sdkContext.locale`: when set, the assistant answers in this language
|
|
288
|
+
* unless the project's own instructions demand another one. It does not
|
|
289
|
+
* change the SDK's built-in UI strings.
|
|
291
290
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
291
|
+
* Explicit only — the SDK never reads the device language for this. Leave
|
|
292
|
+
* undefined when your app has no language choice of its own, and the
|
|
293
|
+
* project's configured language decides.
|
|
294
294
|
*
|
|
295
295
|
* @example "tr"
|
|
296
296
|
* @example "en-US"
|
|
@@ -364,16 +364,29 @@ export interface QafkaProps {
|
|
|
364
364
|
*/
|
|
365
365
|
endUserData?: Record<string, unknown>;
|
|
366
366
|
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
* Runtime facts the assistant may use to personalize answers: the signed-in
|
|
368
|
+
* user's first name, their selected store or branch, account state, an
|
|
369
|
+
* active promotion's progress — whatever the AI should know about THIS user
|
|
370
|
+
* right now.
|
|
371
|
+
*
|
|
372
|
+
* Every key is visible to the AI (flattened into the prompt as `key: value`
|
|
373
|
+
* lines) and available to tool parameters with `source: 'context'`. Because
|
|
374
|
+
* these keys DO reach the prompt, identity data the AI should not see
|
|
375
|
+
* belongs in `endUserData` instead — that lane is never sent to the LLM.
|
|
376
|
+
*
|
|
377
|
+
* When a value has no meaningful state, send an explicit "empty" shape
|
|
378
|
+
* rather than omitting the key — an absent key gives the AI nothing to
|
|
379
|
+
* reason about, so it answers "I don't know".
|
|
380
|
+
*
|
|
381
|
+
* @example { firstName: 'Ada', selectedStoreId: '123', activeSession: { active: false } }
|
|
382
|
+
*/
|
|
371
383
|
context?: Record<string, any>;
|
|
372
384
|
/**
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
* One human-readable sentence describing the screen the user is currently
|
|
386
|
+
* on. Kept separate from `context`: it describes the user's LOCATION, not a
|
|
387
|
+
* fact about them, and the prompt places it above the context lines.
|
|
388
|
+
* @example "User is on the settings screen"
|
|
389
|
+
*/
|
|
377
390
|
contextDescription?: string;
|
|
378
391
|
/**
|
|
379
392
|
* Custom component registry for Tool Registry responses.
|
|
@@ -422,6 +435,14 @@ export interface QafkaProps {
|
|
|
422
435
|
*/
|
|
423
436
|
onError?: (error: Error) => void;
|
|
424
437
|
/**
|
|
438
|
+
* Callback when the project becomes unavailable (suspended). The chat
|
|
439
|
+
* screen stays mounted and shows a neutral message instead of the normal
|
|
440
|
+
* chat surface; use this to hide your own entry point (launcher button,
|
|
441
|
+
* nav item, ...) if you don't want users opening it in this state. Not
|
|
442
|
+
* treated as an error — `onError` is not also called for this transition.
|
|
443
|
+
*/
|
|
444
|
+
onUnavailable?: () => void;
|
|
445
|
+
/**
|
|
425
446
|
* Callback when navigation is suggested (always triggered)
|
|
426
447
|
*/
|
|
427
448
|
onNavigationSuggest?: (suggestion: NavigationSuggestion) => void;
|
|
@@ -534,6 +534,14 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
534
534
|
setIsSending(false);
|
|
535
535
|
setToolStatus(null);
|
|
536
536
|
finalChunkBufferRef.current = '';
|
|
537
|
+
// Project suspended mid-session: flip the SDK to 'unavailable' instead of
|
|
538
|
+
// treating this as a normal error. QafkaSDK.markUnavailable() notifies the
|
|
539
|
+
// same onStatusChange listener useSDK subscribes to, so `onUnavailable`
|
|
540
|
+
// fires there — no onError call and no error bubble here.
|
|
541
|
+
if (err?.code === 'PROJECT_UNAVAILABLE') {
|
|
542
|
+
QafkaSDK_1.QafkaSDK.getInstance().markUnavailable();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
537
545
|
const errorMessage = err.message || 'Unknown error';
|
|
538
546
|
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
539
547
|
// Default fallback bubble. Apps that want a localized or custom error
|
package/dist/hooks/useSDK.d.ts
CHANGED
|
@@ -10,10 +10,18 @@ export interface UseSDKOptions {
|
|
|
10
10
|
locale?: string;
|
|
11
11
|
onReady?: () => void;
|
|
12
12
|
onError?: (error: Error) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Fired when the project is unavailable (suspended). Mutually exclusive
|
|
15
|
+
* with `onError`/`onReady` for this transition — the SDK treats
|
|
16
|
+
* unavailability as an expected, recoverable state rather than a failure.
|
|
17
|
+
*/
|
|
18
|
+
onUnavailable?: () => void;
|
|
13
19
|
devConfig?: unknown;
|
|
14
20
|
}
|
|
15
21
|
export interface UseSDKResult {
|
|
16
22
|
sdkReady: boolean;
|
|
23
|
+
/** True once the SDK has determined the project is unavailable (suspended). */
|
|
24
|
+
unavailable: boolean;
|
|
17
25
|
error: string | null;
|
|
18
26
|
/**
|
|
19
27
|
* Dev-only: the resolved developmentKey from the consumer dev config — the
|
|
@@ -31,4 +39,4 @@ export interface UseSDKResult {
|
|
|
31
39
|
/**
|
|
32
40
|
* Custom hook for managing SDK initialization and lifecycle
|
|
33
41
|
*/
|
|
34
|
-
export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }: UseSDKOptions) => UseSDKResult;
|
|
42
|
+
export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, onUnavailable, devConfig, }: UseSDKOptions) => UseSDKResult;
|
package/dist/hooks/useSDK.js
CHANGED
|
@@ -9,15 +9,18 @@ const resolve_runtime_config_source_1 = require("../resolve-runtime-config-sourc
|
|
|
9
9
|
/**
|
|
10
10
|
* Custom hook for managing SDK initialization and lifecycle
|
|
11
11
|
*/
|
|
12
|
-
const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }) => {
|
|
12
|
+
const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, onUnavailable, devConfig, }) => {
|
|
13
13
|
const [sdkReady, setSdkReady] = (0, react_1.useState)(false);
|
|
14
|
+
const [unavailable, setUnavailable] = (0, react_1.useState)(false);
|
|
14
15
|
const [error, setError] = (0, react_1.useState)(null);
|
|
15
16
|
const [resolvedApiKey, setResolvedApiKey] = (0, react_1.useState)(null);
|
|
16
17
|
const [resolvedApiUrl, setResolvedApiUrl] = (0, react_1.useState)(apiUrl);
|
|
17
18
|
const onReadyRef = (0, react_1.useRef)(onReady);
|
|
18
19
|
const onErrorRef = (0, react_1.useRef)(onError);
|
|
20
|
+
const onUnavailableRef = (0, react_1.useRef)(onUnavailable);
|
|
19
21
|
onReadyRef.current = onReady;
|
|
20
22
|
onErrorRef.current = onError;
|
|
23
|
+
onUnavailableRef.current = onUnavailable;
|
|
21
24
|
const initializeSDK = async () => {
|
|
22
25
|
if (!projectId) {
|
|
23
26
|
const err = new Error('Qafka: `projectId` prop is required.');
|
|
@@ -54,9 +57,17 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, dev
|
|
|
54
57
|
locale,
|
|
55
58
|
onStatusChange: (status) => {
|
|
56
59
|
if (status === 'ready') {
|
|
60
|
+
setUnavailable(false);
|
|
57
61
|
setSdkReady(true);
|
|
58
62
|
onReadyRef.current?.();
|
|
59
63
|
}
|
|
64
|
+
else if (status === 'unavailable') {
|
|
65
|
+
// Unavailability is an expected, recoverable state — not a
|
|
66
|
+
// failure — so no onError/sdkError here, only onUnavailable.
|
|
67
|
+
setSdkReady(false);
|
|
68
|
+
setUnavailable(true);
|
|
69
|
+
onUnavailableRef.current?.();
|
|
70
|
+
}
|
|
60
71
|
else if (status === 'error') {
|
|
61
72
|
setError('Failed to initialize SDK');
|
|
62
73
|
onErrorRef.current?.(new Error('SDK initialization failed'));
|
|
@@ -82,10 +93,12 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, dev
|
|
|
82
93
|
(0, react_1.useEffect)(() => {
|
|
83
94
|
const sdk = QafkaSDK_1.QafkaSDK.getInstance();
|
|
84
95
|
setSdkReady(false);
|
|
96
|
+
setUnavailable(false);
|
|
85
97
|
setError(null);
|
|
86
98
|
initializeSDK();
|
|
87
99
|
return () => {
|
|
88
100
|
setSdkReady(false);
|
|
101
|
+
setUnavailable(false);
|
|
89
102
|
sdk.destroy();
|
|
90
103
|
};
|
|
91
104
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -99,6 +112,7 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, dev
|
|
|
99
112
|
}, [locale, sdkReady]);
|
|
100
113
|
return {
|
|
101
114
|
sdkReady,
|
|
115
|
+
unavailable,
|
|
102
116
|
error,
|
|
103
117
|
resolvedApiKey,
|
|
104
118
|
resolvedApiUrl,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatResponse } from '../types';
|
|
1
|
+
import { ChatResponse, GenerateImageParams, GenerateImageResult } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Backend API Service using native fetch
|
|
4
4
|
*/
|
|
@@ -19,6 +19,7 @@ export declare class BackendService {
|
|
|
19
19
|
private onToolDataRequested;
|
|
20
20
|
private readonly toolDataResolvedFor;
|
|
21
21
|
private static readonly RESOLVER_TIMEOUT_MS;
|
|
22
|
+
private static readonly IMAGE_TIMEOUT_MS;
|
|
22
23
|
constructor(apiKey: string | null, apiUrl?: string, subProjectId?: string);
|
|
23
24
|
setSessionTokenGetter(getter: () => Promise<string | null>): void;
|
|
24
25
|
/**
|
|
@@ -80,7 +81,9 @@ export declare class BackendService {
|
|
|
80
81
|
*/
|
|
81
82
|
private postToolData;
|
|
82
83
|
/**
|
|
83
|
-
* Make HTTP request with timeout
|
|
84
|
+
* Make HTTP request with timeout. `timeoutMs` optionally overrides the
|
|
85
|
+
* instance default (`this.timeout`) for calls that are known to run long,
|
|
86
|
+
* e.g. image generation.
|
|
84
87
|
*/
|
|
85
88
|
private fetchWithTimeout;
|
|
86
89
|
/**
|
|
@@ -100,10 +103,33 @@ export declare class BackendService {
|
|
|
100
103
|
*/
|
|
101
104
|
deleteConversation(sessionId: string): Promise<void>;
|
|
102
105
|
/**
|
|
103
|
-
* Get project theme
|
|
106
|
+
* Get project theme.
|
|
107
|
+
*
|
|
108
|
+
* Sends the explicit locale, when the host set one, so the server can answer
|
|
109
|
+
* with its translation of the project's greeting / initial message instead of
|
|
110
|
+
* the language they were authored in. Same source as `sdkContext.locale`: the
|
|
111
|
+
* `<Qafka locale>` prop only, never a device language. Unset ⇒ no param ⇒ the
|
|
112
|
+
* request is identical to the one earlier SDK versions send, and the server
|
|
113
|
+
* reads a missing param as "no signal".
|
|
104
114
|
*/
|
|
105
115
|
getTheme(): Promise<any>;
|
|
106
116
|
/**
|
|
117
|
+
* Generate one or more images via the backend `/images` endpoint.
|
|
118
|
+
* Uses a much longer timeout than the default (image inference is slow).
|
|
119
|
+
* When `params.n` is absent or <= 1, the single result is unwrapped from
|
|
120
|
+
* the `images` array; otherwise the full array is returned.
|
|
121
|
+
*/
|
|
122
|
+
generateImage(params: GenerateImageParams): Promise<GenerateImageResult>;
|
|
123
|
+
/**
|
|
124
|
+
* Response handler for `/images`. Mirrors `handleResponse`'s ok/!ok split
|
|
125
|
+
* but maps the server's `errorData.error` onto `Error.code` (same
|
|
126
|
+
* augmentation pattern as the `DEV_KEY_INVALID` error in
|
|
127
|
+
* `sendMessageStream`) so callers can branch on stable error codes
|
|
128
|
+
* (`not_entitled`, `rate_limited`, `content_filtered`, `invalid_reference`,
|
|
129
|
+
* `generation_failed`, ...) instead of parsing message strings.
|
|
130
|
+
*/
|
|
131
|
+
private handleImageResponse;
|
|
132
|
+
/**
|
|
107
133
|
* Send message with streaming response
|
|
108
134
|
* React Native compatible version using XMLHttpRequest
|
|
109
135
|
*/
|
|
@@ -4,6 +4,7 @@ exports.BackendService = void 0;
|
|
|
4
4
|
const react_native_1 = require("react-native");
|
|
5
5
|
const package_json_1 = require("../../package.json");
|
|
6
6
|
const QafkaAttestation_1 = require("../native/QafkaAttestation");
|
|
7
|
+
const projectUnavailable_1 = require("./projectUnavailable");
|
|
7
8
|
/**
|
|
8
9
|
* Backend API Service using native fetch
|
|
9
10
|
*/
|
|
@@ -25,13 +26,18 @@ class BackendService {
|
|
|
25
26
|
attestationManager = null;
|
|
26
27
|
refreshing = null;
|
|
27
28
|
// locale is only added to sdkContext when explicitly set by the
|
|
28
|
-
// developer via <Qafka locale="..." />.
|
|
29
|
-
//
|
|
29
|
+
// developer via <Qafka locale="..." />. There is no device/system-language
|
|
30
|
+
// fallback on purpose: the server reads this field as the language the USER
|
|
31
|
+
// writes in, and a device default would state that for every host app that
|
|
32
|
+
// never made the choice. The SDK's own UI strings are unaffected by it.
|
|
30
33
|
locale = null;
|
|
31
34
|
// Tool Data Channel
|
|
32
35
|
onToolDataRequested = null;
|
|
33
36
|
toolDataResolvedFor = new Map();
|
|
34
37
|
static RESOLVER_TIMEOUT_MS = 3000;
|
|
38
|
+
// Image generation is slow (model inference); give it a much longer
|
|
39
|
+
// budget than the default 60s used for chat.
|
|
40
|
+
static IMAGE_TIMEOUT_MS = 120_000;
|
|
35
41
|
constructor(apiKey, apiUrl, subProjectId) {
|
|
36
42
|
// If custom apiUrl provided, ensure it has /api/v1 prefix
|
|
37
43
|
if (apiUrl) {
|
|
@@ -233,11 +239,13 @@ class BackendService {
|
|
|
233
239
|
});
|
|
234
240
|
}
|
|
235
241
|
/**
|
|
236
|
-
* Make HTTP request with timeout
|
|
242
|
+
* Make HTTP request with timeout. `timeoutMs` optionally overrides the
|
|
243
|
+
* instance default (`this.timeout`) for calls that are known to run long,
|
|
244
|
+
* e.g. image generation.
|
|
237
245
|
*/
|
|
238
|
-
async fetchWithTimeout(url, options) {
|
|
246
|
+
async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
|
|
239
247
|
const controller = new AbortController();
|
|
240
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
248
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
241
249
|
try {
|
|
242
250
|
const response = await this.fetchWithRefresh(url, {
|
|
243
251
|
...options,
|
|
@@ -268,6 +276,12 @@ class BackendService {
|
|
|
268
276
|
const errorCode = errorData.error || 'UNKNOWN_ERROR';
|
|
269
277
|
const errorMessage = errorData.message || response.statusText;
|
|
270
278
|
const hint = errorData.hint || '';
|
|
279
|
+
// Project suspended — a distinct signal from the other 403 branches
|
|
280
|
+
// below, callers across the SDK branch on `ProjectUnavailableError` /
|
|
281
|
+
// `code === 'PROJECT_UNAVAILABLE'` rather than parsing message strings.
|
|
282
|
+
if ((0, projectUnavailable_1.isProjectUnavailable)(response.status, errorData)) {
|
|
283
|
+
throw new projectUnavailable_1.ProjectUnavailableError(errorMessage);
|
|
284
|
+
}
|
|
271
285
|
// 401: Unauthorized (Invalid/Missing API Key)
|
|
272
286
|
if (response.status === 401) {
|
|
273
287
|
if (errorCode === 'MISSING_API_KEY') {
|
|
@@ -394,22 +408,78 @@ class BackendService {
|
|
|
394
408
|
}
|
|
395
409
|
}
|
|
396
410
|
/**
|
|
397
|
-
* Get project theme
|
|
411
|
+
* Get project theme.
|
|
412
|
+
*
|
|
413
|
+
* Sends the explicit locale, when the host set one, so the server can answer
|
|
414
|
+
* with its translation of the project's greeting / initial message instead of
|
|
415
|
+
* the language they were authored in. Same source as `sdkContext.locale`: the
|
|
416
|
+
* `<Qafka locale>` prop only, never a device language. Unset ⇒ no param ⇒ the
|
|
417
|
+
* request is identical to the one earlier SDK versions send, and the server
|
|
418
|
+
* reads a missing param as "no signal".
|
|
398
419
|
*/
|
|
399
420
|
async getTheme() {
|
|
400
421
|
try {
|
|
401
422
|
const cleanApiUrl = this.baseURL.replace(/\/api\/v1\/?$/, '');
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
423
|
+
const query = [];
|
|
424
|
+
if (this.subProjectId) {
|
|
425
|
+
query.push(`subProjectId=${encodeURIComponent(this.subProjectId)}`);
|
|
426
|
+
}
|
|
427
|
+
if (this.locale) {
|
|
428
|
+
query.push(`locale=${encodeURIComponent(this.locale)}`);
|
|
429
|
+
}
|
|
430
|
+
const queryParams = query.length > 0 ? `?${query.join('&')}` : '';
|
|
405
431
|
const response = await this.fetchWithTimeout(`${cleanApiUrl}/api/v1/public/projects/chat-theme${queryParams}`, { method: 'GET' });
|
|
406
432
|
return this.handleResponse(response);
|
|
407
433
|
}
|
|
408
434
|
catch (error) {
|
|
435
|
+
// Preserve ProjectUnavailableError as-is — callers (QafkaSDK.initialize)
|
|
436
|
+
// branch on `instanceof`, so re-wrapping here would lose that signal.
|
|
437
|
+
if (error instanceof projectUnavailable_1.ProjectUnavailableError) {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
409
440
|
throw new Error(error.message || 'Failed to fetch theme');
|
|
410
441
|
}
|
|
411
442
|
}
|
|
412
443
|
/**
|
|
444
|
+
* Generate one or more images via the backend `/images` endpoint.
|
|
445
|
+
* Uses a much longer timeout than the default (image inference is slow).
|
|
446
|
+
* When `params.n` is absent or <= 1, the single result is unwrapped from
|
|
447
|
+
* the `images` array; otherwise the full array is returned.
|
|
448
|
+
*/
|
|
449
|
+
async generateImage(params) {
|
|
450
|
+
const response = await this.fetchWithTimeout(`${this.baseURL}/images`, {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
body: JSON.stringify(params),
|
|
453
|
+
}, BackendService.IMAGE_TIMEOUT_MS);
|
|
454
|
+
const data = await this.handleImageResponse(response);
|
|
455
|
+
const images = data.images ?? [];
|
|
456
|
+
return (params.n ?? 1) > 1 ? images : images[0];
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Response handler for `/images`. Mirrors `handleResponse`'s ok/!ok split
|
|
460
|
+
* but maps the server's `errorData.error` onto `Error.code` (same
|
|
461
|
+
* augmentation pattern as the `DEV_KEY_INVALID` error in
|
|
462
|
+
* `sendMessageStream`) so callers can branch on stable error codes
|
|
463
|
+
* (`not_entitled`, `rate_limited`, `content_filtered`, `invalid_reference`,
|
|
464
|
+
* `generation_failed`, ...) instead of parsing message strings.
|
|
465
|
+
*/
|
|
466
|
+
async handleImageResponse(response) {
|
|
467
|
+
if (response.ok) {
|
|
468
|
+
return response.json();
|
|
469
|
+
}
|
|
470
|
+
const errorData = await response.json().catch(() => ({}));
|
|
471
|
+
const err = new Error(errorData.message || `❌ Image generation failed (HTTP ${response.status})`);
|
|
472
|
+
err.code = errorData.error;
|
|
473
|
+
err.status = response.status;
|
|
474
|
+
// RateLimitGuard returns 429 with body { error: 'RATE_LIMIT_EXCEEDED' },
|
|
475
|
+
// but the public contract promises 'rate_limited' — normalize so callers
|
|
476
|
+
// can reliably branch on it.
|
|
477
|
+
if (response.status === 429) {
|
|
478
|
+
err.code = 'rate_limited';
|
|
479
|
+
}
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
413
483
|
* Send message with streaming response
|
|
414
484
|
* React Native compatible version using XMLHttpRequest
|
|
415
485
|
*/
|
|
@@ -601,13 +671,21 @@ class BackendService {
|
|
|
601
671
|
});
|
|
602
672
|
}
|
|
603
673
|
else {
|
|
604
|
-
let
|
|
674
|
+
let parsedBody;
|
|
605
675
|
try {
|
|
606
|
-
|
|
676
|
+
parsedBody = JSON.parse(xhr.responseText);
|
|
607
677
|
}
|
|
608
678
|
catch {
|
|
609
679
|
// non-JSON error body; fall through to a status-based message
|
|
610
680
|
}
|
|
681
|
+
const serverMessage = parsedBody?.message;
|
|
682
|
+
if ((0, projectUnavailable_1.isProjectUnavailable)(xhr.status, parsedBody)) {
|
|
683
|
+
const unavailableErr = new Error(serverMessage || 'The assistant is currently unavailable.');
|
|
684
|
+
unavailableErr.code = 'PROJECT_UNAVAILABLE';
|
|
685
|
+
unavailableErr.status = xhr.status;
|
|
686
|
+
onError(unavailableErr);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
611
689
|
// In dev, a 401 with a test key almost always means the rotated dev
|
|
612
690
|
// key is stale. Surface an actionable hint instead of a bare HTTP error.
|
|
613
691
|
if (__DEV__ && xhr.status === 401 && this.apiKey?.startsWith('qafka_test_')) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the "project unavailable" state: the backend answers
|
|
3
|
+
* with HTTP 403 `{ error: 'project_unavailable' }` when the project (or its
|
|
4
|
+
* parent) is suspended. The chat screen stays mounted but shows a neutral
|
|
5
|
+
* message instead of the normal chat surface or an error bubble.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* True when a response is the specific "project unavailable" signal, as
|
|
9
|
+
* opposed to any other 403 (bundle id, platform, permission, ...).
|
|
10
|
+
*/
|
|
11
|
+
export declare function isProjectUnavailable(status: number, body: unknown): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Thrown by the transport layer when a request fails because the project is
|
|
14
|
+
* unavailable. Callers branch on `code`, not `instanceof`, where they only
|
|
15
|
+
* have a plain error (e.g. across the streaming/XHR boundary).
|
|
16
|
+
*/
|
|
17
|
+
export declare class ProjectUnavailableError extends Error {
|
|
18
|
+
code: string;
|
|
19
|
+
constructor(message?: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Neutral, locale-aware copy shown in place of the chat surface when the
|
|
23
|
+
* project is unavailable. Deliberately generic — no mention of payment,
|
|
24
|
+
* plans, or Qafka — so it reads the same regardless of the underlying cause.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unavailableMessage(locale?: string): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared helpers for the "project unavailable" state: the backend answers
|
|
4
|
+
* with HTTP 403 `{ error: 'project_unavailable' }` when the project (or its
|
|
5
|
+
* parent) is suspended. The chat screen stays mounted but shows a neutral
|
|
6
|
+
* message instead of the normal chat surface or an error bubble.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ProjectUnavailableError = void 0;
|
|
10
|
+
exports.isProjectUnavailable = isProjectUnavailable;
|
|
11
|
+
exports.unavailableMessage = unavailableMessage;
|
|
12
|
+
/**
|
|
13
|
+
* True when a response is the specific "project unavailable" signal, as
|
|
14
|
+
* opposed to any other 403 (bundle id, platform, permission, ...).
|
|
15
|
+
*/
|
|
16
|
+
function isProjectUnavailable(status, body) {
|
|
17
|
+
if (status !== 403)
|
|
18
|
+
return false;
|
|
19
|
+
if (!body || typeof body !== 'object')
|
|
20
|
+
return false;
|
|
21
|
+
return body.error === 'project_unavailable';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by the transport layer when a request fails because the project is
|
|
25
|
+
* unavailable. Callers branch on `code`, not `instanceof`, where they only
|
|
26
|
+
* have a plain error (e.g. across the streaming/XHR boundary).
|
|
27
|
+
*/
|
|
28
|
+
class ProjectUnavailableError extends Error {
|
|
29
|
+
code = 'PROJECT_UNAVAILABLE';
|
|
30
|
+
constructor(message = 'The assistant is currently unavailable.') {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'ProjectUnavailableError';
|
|
33
|
+
Object.setPrototypeOf(this, ProjectUnavailableError.prototype);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.ProjectUnavailableError = ProjectUnavailableError;
|
|
37
|
+
/**
|
|
38
|
+
* Neutral, locale-aware copy shown in place of the chat surface when the
|
|
39
|
+
* project is unavailable. Deliberately generic — no mention of payment,
|
|
40
|
+
* plans, or Qafka — so it reads the same regardless of the underlying cause.
|
|
41
|
+
*/
|
|
42
|
+
function unavailableMessage(locale) {
|
|
43
|
+
return locale?.toLowerCase().startsWith('tr')
|
|
44
|
+
? 'Asistan şu anda hizmet verememektedir.'
|
|
45
|
+
: 'The assistant is currently unavailable.';
|
|
46
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image generation types for QafkaSDK
|
|
3
|
+
* Parameters and results for generateImage() method
|
|
4
|
+
*/
|
|
5
|
+
export interface GenerateImageParams {
|
|
6
|
+
description: string;
|
|
7
|
+
referenceImage?: string | string[];
|
|
8
|
+
aspectRatio?: string;
|
|
9
|
+
size?: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
quality?: 'standard' | 'pro';
|
|
14
|
+
responseType?: 'base64' | 'url' | 'both';
|
|
15
|
+
n?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface GeneratedImage {
|
|
18
|
+
base64?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
expiresAt?: string;
|
|
21
|
+
mimeType: string;
|
|
22
|
+
}
|
|
23
|
+
export type GenerateImageResult = GeneratedImage | GeneratedImage[];
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
package/dist/types/sdk.d.ts
CHANGED
|
@@ -23,9 +23,10 @@ export interface SDKConfig {
|
|
|
23
23
|
/** Request timeout in ms. */
|
|
24
24
|
timeout?: number;
|
|
25
25
|
/**
|
|
26
|
-
* BCP 47 locale (e.g. "tr", "en", "tr-TR"). When set, the SDK
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* BCP 47 locale (e.g. "tr", "en", "tr-TR"). When set, the SDK forwards it
|
|
27
|
+
* as `sdkContext.locale` on every chat request — the language the assistant
|
|
28
|
+
* answers in. Explicit only: never derived from the device language, and it
|
|
29
|
+
* does not change the SDK's own UI strings.
|
|
29
30
|
*/
|
|
30
31
|
locale?: string;
|
|
31
32
|
onStatusChange?: (status: SDKStatusType) => void;
|
|
@@ -35,4 +36,4 @@ export interface SDKConfig {
|
|
|
35
36
|
/**
|
|
36
37
|
* SDK status type
|
|
37
38
|
*/
|
|
38
|
-
export type SDKStatusType = 'uninitialized' | 'initializing' | 'ready' | 'error';
|
|
39
|
+
export type SDKStatusType = 'uninitialized' | 'initializing' | 'ready' | 'unavailable' | 'error';
|
|
@@ -278,11 +278,26 @@ to handle in code. It shows a small set of key/value chips (or routes text to th
|
|
|
278
278
|
You may optionally restyle the chips via `voiceComponents.dataChipList` / `voiceComponents.dataChip`.
|
|
279
279
|
|
|
280
280
|
### Useful context props
|
|
281
|
-
- **`context`**
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
- **`context`** — runtime facts the AI may use to personalize answers (first name, selected
|
|
282
|
+
store, account state). Every key reaches the AI prompt; tool parameters with
|
|
283
|
+
`source: 'context'` also read from here. For a value with no meaningful state, send an
|
|
284
|
+
explicit "empty" shape rather than omitting the key.
|
|
285
|
+
- **`contextDescription`** — one sentence describing the screen the user is currently on
|
|
286
|
+
(their location, not a fact about them; shown to the AI above the context lines).
|
|
287
|
+
- **`endUserData`** — non-LLM metadata about the user (≤8KB). Never reaches the AI prompt —
|
|
288
|
+
the inverse of `context`.
|
|
284
289
|
- **`subProjectId`** — multi-tenant routing.
|
|
285
290
|
- **`locale`** — BCP-47 locale.
|
|
291
|
+
- **`onUnavailable`** — fires when the project becomes unavailable (suspended). The chat
|
|
292
|
+
screen stays mounted and shows a neutral message in place of the normal chat surface; not
|
|
293
|
+
treated as an error (`onError` is not also called). Use it to hide your own entry point —
|
|
294
|
+
e.g. a launcher button or nav item — so users can't open a chat screen with nothing to say:
|
|
295
|
+
```tsx
|
|
296
|
+
const [chatAvailable, setChatAvailable] = useState(true);
|
|
297
|
+
|
|
298
|
+
<Qafka onUnavailable={() => setChatAvailable(false)} />
|
|
299
|
+
{chatAvailable && <LauncherButton onPress={openChat} />}
|
|
300
|
+
```
|
|
286
301
|
|
|
287
302
|
---
|
|
288
303
|
|
package/package.json
CHANGED