@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 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:
@@ -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, which the server then echoes into the user-message
29
- * context prefix. Same value should drive UI strings on the SDK side.
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(() => null);
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(() => null);
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
- this.status = 'ready';
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('ready');
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, which the server then echoes into the user-message
150
- * context prefix. Same value should drive UI strings on the SDK side.
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
- 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, }) {
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
- if (displayMessages.length > 0) {
67
- flatListRef.current?.scrollToEnd({ animated: true });
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
- so it sits directly under the last message and scrolls with it. */}
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
- that would be rejected by an un-attested send. TypingIndicator in the
81
- empty state carries the "still preparing" signal. */}
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
  }
@@ -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
- * Drives UI strings (placeholder, buttons, dashboard-supplied multi-locale
287
- * greeting) and is forwarded to the backend as part of `sdkContext.locale`.
288
- * Pair with the server-side language priority rule: when set, the AI
289
- * answers in this locale unless project instructions explicitly demand
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
- * Leave undefined to fall back to whatever language the project
293
- * instructions are written in.
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
- * Context object for Tool Registry.
368
- * Provides runtime context to match and execute tools.
369
- * @example { userId: '456', selectedItemId: '123' }
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
- * Context description for the AI.
374
- * Helps the AI understand what the context keys mean.
375
- * @example "User is on the settings screen"
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
@@ -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;
@@ -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="..." />. Drives both UI strings and the
29
- // user-message context prefix on the server.
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(), this.timeout);
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 queryParams = this.subProjectId
403
- ? `?subProjectId=${encodeURIComponent(this.subProjectId)}`
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 serverMessage;
674
+ let parsedBody;
605
675
  try {
606
- serverMessage = JSON.parse(xhr.responseText)?.message;
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[];
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Image generation types for QafkaSDK
4
+ * Parameters and results for generateImage() method
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -7,3 +7,4 @@ export * from './chat';
7
7
  export * from './sdk';
8
8
  export * from './components';
9
9
  export * from './external-navigation';
10
+ export * from './image';
@@ -23,3 +23,4 @@ __exportStar(require("./chat"), exports);
23
23
  __exportStar(require("./sdk"), exports);
24
24
  __exportStar(require("./components"), exports);
25
25
  __exportStar(require("./external-navigation"), exports);
26
+ __exportStar(require("./image"), exports);
@@ -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
- * forwards it as `sdkContext.locale` on every chat request and uses it
28
- * for UI strings.
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`** + **`contextDescription`** — a runtime data bag passed to the AI, with a
282
- description of what its keys mean. (Tool parameters with `source: 'context'` read from here.)
283
- - **`endUserData`** — non-LLM metadata about the user (≤8KB).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qafka/react-native",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "description": "Drop-in AI assistant for React Native: chat, voice, and tool execution with screen-aware navigation.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",