capacitor-apple-intelligence 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'CapacitorAppleIntelligence'
3
- s.version = '1.0.0'
3
+ s.version = '1.0.2'
4
4
  s.summary = 'Capacitor plugin for Apple Intelligence with schema-constrained JSON generation'
5
5
  s.license = 'MIT'
6
6
  s.homepage = 'https://github.com/farabiabdelwahe/capacitor-apple-intelligence'
package/README.md CHANGED
@@ -42,6 +42,18 @@ npx cap sync
42
42
 
43
43
  ### Basic Example
44
44
 
45
+ First, check if Apple Intelligence is available:
46
+
47
+ ```typescript
48
+ const availability = await AppleIntelligence.checkAvailability();
49
+ if (!availability.available) {
50
+ console.error("Apple Intelligence not available");
51
+ return;
52
+ }
53
+ ```
54
+
55
+ Then generate JSON:
56
+
45
57
  ```typescript
46
58
  import { AppleIntelligence } from 'capacitor-apple-intelligence';
47
59
 
@@ -177,12 +189,40 @@ const result = await AppleIntelligence.generateTextWithLanguage({
177
189
  language: "es" // or "Spanish"
178
190
  });
179
191
 
192
+ if (result.success) {
193
+ console.log(result.content);
194
+ // "El atardecer pinta el cielo con tonos dorados y rosados..."
195
+ }
180
196
  if (result.success) {
181
197
  console.log(result.content);
182
198
  // "El atardecer pinta el cielo con tonos dorados y rosados..."
183
199
  }
184
200
  ```
185
201
 
202
+ ### Image Generation
203
+
204
+ Generate images from text prompts:
205
+
206
+ ```typescript
207
+ const result = await AppleIntelligence.generateImage({
208
+ prompt: "A futuristic city with flying cars",
209
+ style: "animation", // Optional: "animation" (default), "illustration", "sketch"
210
+ count: 1, // Optional: Defaults to 1
211
+ sourceImage: base64String // Optional: Required for face-based generation
212
+ });
213
+
214
+ if (result.success && result.images) {
215
+ result.images.forEach(base64Image => {
216
+ // Display image
217
+ const img = document.createElement('img');
218
+ img.src = `data:image/png;base64,${base64Image}`;
219
+ document.body.appendChild(img);
220
+ });
221
+ }
222
+ ```
223
+
224
+ **Note**: When your prompt involves generating images of people, you **must** provide a `sourceImage` containing a clearly visible face. This is a requirement of Apple's Image Playground API. The source image should be passed as a base64-encoded string.
225
+
186
226
  Supported language codes: `en`, `es`, `fr`, `de`, `ja`, `zh`, `it`, `pt`, `ru`, `ar`, `ko`
187
227
 
188
228
  ## API
@@ -211,7 +251,8 @@ if (result.available) {
211
251
  }
212
252
  ```
213
253
 
214
- ---
254
+
255
+
215
256
 
216
257
  ### `generate(request)`
217
258
 
@@ -167,6 +167,13 @@ export interface AppleIntelligencePlugin {
167
167
  * @returns Promise resolving to availability status
168
168
  */
169
169
  checkAvailability(): Promise<AvailabilityResponse>;
170
+ /**
171
+ * Generate an image based on the provided prompt using Apple Intelligence.
172
+ *
173
+ * @param request - The generation request containing prompt and options
174
+ * @returns Promise resolving to an image response with success/error and base64 image data
175
+ */
176
+ generateImage(request: GenerateImageRequest): Promise<GenerateImageResponse>;
170
177
  }
171
178
  /**
172
179
  * Request payload for text generation.
@@ -221,3 +228,47 @@ export interface AvailabilityResponse {
221
228
  */
222
229
  error?: GenerateError;
223
230
  }
231
+ /**
232
+ * Request payload for image generation.
233
+ */
234
+ export interface GenerateImageRequest {
235
+ /**
236
+ * The prompt describing the image to generate.
237
+ */
238
+ prompt: string;
239
+ /**
240
+ * Optional style guide for generation.
241
+ * Supported values: "animation" (default), "illustration", "sketch".
242
+ * Note: "photorealistic" is NOT supported by Apple's on-device Image Playground.
243
+ */
244
+ style?: string;
245
+ /**
246
+ * Optional number of images to generate. Defaults to 1. Maximum 10.
247
+ */
248
+ count?: number;
249
+ /**
250
+ * Optional base64-encoded source image for face-based generation.
251
+ * Required when the prompt involves generating images of people/faces.
252
+ * The image should contain a clearly visible person's face.
253
+ */
254
+ sourceImage?: string;
255
+ }
256
+ /**
257
+ * Response for image generation.
258
+ */
259
+ export interface GenerateImageResponse {
260
+ /**
261
+ * Whether the generation was successful.
262
+ */
263
+ success: boolean;
264
+ /**
265
+ * Array of base64 encoded images strings.
266
+ * Only present when success is true.
267
+ */
268
+ images?: string[];
269
+ /**
270
+ * Error details on failure.
271
+ * Only present when success is false.
272
+ */
273
+ error?: GenerateError;
274
+ }
package/dist/esm/web.d.ts CHANGED
@@ -9,4 +9,5 @@ export declare class AppleIntelligenceWeb extends WebPlugin implements AppleInte
9
9
  generateText(_request: unknown): Promise<any>;
10
10
  generateTextWithLanguage(_request: unknown): Promise<any>;
11
11
  checkAvailability(): Promise<any>;
12
+ generateImage(_request: unknown): Promise<any>;
12
13
  }
package/dist/esm/web.js CHANGED
@@ -43,5 +43,15 @@ export class AppleIntelligenceWeb extends WebPlugin {
43
43
  },
44
44
  };
45
45
  }
46
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
47
+ async generateImage(_request) {
48
+ return {
49
+ success: false,
50
+ error: {
51
+ code: 'UNAVAILABLE',
52
+ message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',
53
+ },
54
+ };
55
+ }
46
56
  }
47
57
  //# sourceMappingURL=web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IACjD,6DAA6D;IAC7D,KAAK,CAAC,QAAQ,CAAC,QAAyB;QACtC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,YAAY,CAAC,QAAiB;QAClC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,wBAAwB,CAAC,QAAiB;QAC9C,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;CACF"}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IACjD,6DAA6D;IAC7D,KAAK,CAAC,QAAQ,CAAC,QAAyB;QACtC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,YAAY,CAAC,QAAiB;QAClC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,wBAAwB,CAAC,QAAiB;QAC9C,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,aAAa,CAAC,QAAiB;QACnC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,0FAA0F;aACpG;SACF,CAAC;IACJ,CAAC;CACF"}
@@ -79,6 +79,16 @@ class AppleIntelligenceWeb extends core.WebPlugin {
79
79
  },
80
80
  };
81
81
  }
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
83
+ async generateImage(_request) {
84
+ return {
85
+ success: false,
86
+ error: {
87
+ code: 'UNAVAILABLE',
88
+ message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',
89
+ },
90
+ };
91
+ }
82
92
  }
83
93
 
84
94
  var web = /*#__PURE__*/Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Apple Intelligence Plugin instance.\n *\n * Provides schema-constrained JSON generation using Apple's on-device\n * Foundation Models framework.\n *\n * @example\n * ```typescript\n * import { AppleIntelligence } from 'capacitor-apple-intelligence';\n *\n * const result = await AppleIntelligence.generate({\n * messages: [{ role: \"user\", content: \"Suggest 3 books\" }],\n * response_format: {\n * type: \"json_schema\",\n * schema: {\n * type: \"array\",\n * items: {\n * type: \"object\",\n * properties: {\n * title: { type: \"string\" },\n * author: { type: \"string\" }\n * },\n * required: [\"title\", \"author\"]\n * }\n * }\n * }\n * });\n * ```\n */\nconst AppleIntelligence = registerPlugin('AppleIntelligence', {\n web: () => import('./web').then((m) => new m.AppleIntelligenceWeb()),\n});\nexport * from './definitions';\nexport { AppleIntelligence };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * Web implementation stub for Apple Intelligence.\n * Always returns UNAVAILABLE since Apple Intelligence is iOS-only.\n */\nexport class AppleIntelligenceWeb extends WebPlugin {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generate(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateText(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateTextWithLanguage(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n async checkAvailability() {\n return {\n available: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACK,MAAC,iBAAiB,GAAGA,mBAAc,CAAC,mBAAmB,EAAE;AAC9D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;AACxE,CAAC;;AC/BD;AACA;AACA;AACA;AACO,MAAM,oBAAoB,SAASC,cAAS,CAAC;AACpD;AACA,IAAI,MAAM,QAAQ,CAAC,QAAQ,EAAE;AAC7B,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;AACA,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;AACjC,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;AACA,IAAI,MAAM,wBAAwB,CAAC,QAAQ,EAAE;AAC7C,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,iBAAiB,GAAG;AAC9B,QAAQ,OAAO;AACf,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Apple Intelligence Plugin instance.\n *\n * Provides schema-constrained JSON generation using Apple's on-device\n * Foundation Models framework.\n *\n * @example\n * ```typescript\n * import { AppleIntelligence } from 'capacitor-apple-intelligence';\n *\n * const result = await AppleIntelligence.generate({\n * messages: [{ role: \"user\", content: \"Suggest 3 books\" }],\n * response_format: {\n * type: \"json_schema\",\n * schema: {\n * type: \"array\",\n * items: {\n * type: \"object\",\n * properties: {\n * title: { type: \"string\" },\n * author: { type: \"string\" }\n * },\n * required: [\"title\", \"author\"]\n * }\n * }\n * }\n * });\n * ```\n */\nconst AppleIntelligence = registerPlugin('AppleIntelligence', {\n web: () => import('./web').then((m) => new m.AppleIntelligenceWeb()),\n});\nexport * from './definitions';\nexport { AppleIntelligence };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * Web implementation stub for Apple Intelligence.\n * Always returns UNAVAILABLE since Apple Intelligence is iOS-only.\n */\nexport class AppleIntelligenceWeb extends WebPlugin {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generate(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateText(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateTextWithLanguage(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n async checkAvailability() {\n return {\n available: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateImage(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACK,MAAC,iBAAiB,GAAGA,mBAAc,CAAC,mBAAmB,EAAE;AAC9D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;AACxE,CAAC;;AC/BD;AACA;AACA;AACA;AACO,MAAM,oBAAoB,SAASC,cAAS,CAAC;AACpD;AACA,IAAI,MAAM,QAAQ,CAAC,QAAQ,EAAE;AAC7B,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;AACA,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;AACjC,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;AACA,IAAI,MAAM,wBAAwB,CAAC,QAAQ,EAAE;AAC7C,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,iBAAiB,GAAG;AAC9B,QAAQ,OAAO;AACf,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;AACA,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;AAClC,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,KAAK;AAC1B,YAAY,KAAK,EAAE;AACnB,gBAAgB,IAAI,EAAE,aAAa;AACnC,gBAAgB,OAAO,EAAE,0FAA0F;AACnH,aAAa;AACb,SAAS;AACT,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -78,6 +78,16 @@ var capacitorAppleIntelligence = (function (exports, core) {
78
78
  },
79
79
  };
80
80
  }
81
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
82
+ async generateImage(_request) {
83
+ return {
84
+ success: false,
85
+ error: {
86
+ code: 'UNAVAILABLE',
87
+ message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',
88
+ },
89
+ };
90
+ }
81
91
  }
82
92
 
83
93
  var web = /*#__PURE__*/Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Apple Intelligence Plugin instance.\n *\n * Provides schema-constrained JSON generation using Apple's on-device\n * Foundation Models framework.\n *\n * @example\n * ```typescript\n * import { AppleIntelligence } from 'capacitor-apple-intelligence';\n *\n * const result = await AppleIntelligence.generate({\n * messages: [{ role: \"user\", content: \"Suggest 3 books\" }],\n * response_format: {\n * type: \"json_schema\",\n * schema: {\n * type: \"array\",\n * items: {\n * type: \"object\",\n * properties: {\n * title: { type: \"string\" },\n * author: { type: \"string\" }\n * },\n * required: [\"title\", \"author\"]\n * }\n * }\n * }\n * });\n * ```\n */\nconst AppleIntelligence = registerPlugin('AppleIntelligence', {\n web: () => import('./web').then((m) => new m.AppleIntelligenceWeb()),\n});\nexport * from './definitions';\nexport { AppleIntelligence };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * Web implementation stub for Apple Intelligence.\n * Always returns UNAVAILABLE since Apple Intelligence is iOS-only.\n */\nexport class AppleIntelligenceWeb extends WebPlugin {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generate(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateText(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateTextWithLanguage(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n async checkAvailability() {\n return {\n available: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACK,UAAC,iBAAiB,GAAGA,mBAAc,CAAC,mBAAmB,EAAE;IAC9D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;IACxE,CAAC;;IC/BD;IACA;IACA;IACA;IACO,MAAM,oBAAoB,SAASC,cAAS,CAAC;IACpD;IACA,IAAI,MAAM,QAAQ,CAAC,QAAQ,EAAE;IAC7B,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;IACA,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;IACjC,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;IACA,IAAI,MAAM,wBAAwB,CAAC,QAAQ,EAAE;IAC7C,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Apple Intelligence Plugin instance.\n *\n * Provides schema-constrained JSON generation using Apple's on-device\n * Foundation Models framework.\n *\n * @example\n * ```typescript\n * import { AppleIntelligence } from 'capacitor-apple-intelligence';\n *\n * const result = await AppleIntelligence.generate({\n * messages: [{ role: \"user\", content: \"Suggest 3 books\" }],\n * response_format: {\n * type: \"json_schema\",\n * schema: {\n * type: \"array\",\n * items: {\n * type: \"object\",\n * properties: {\n * title: { type: \"string\" },\n * author: { type: \"string\" }\n * },\n * required: [\"title\", \"author\"]\n * }\n * }\n * }\n * });\n * ```\n */\nconst AppleIntelligence = registerPlugin('AppleIntelligence', {\n web: () => import('./web').then((m) => new m.AppleIntelligenceWeb()),\n});\nexport * from './definitions';\nexport { AppleIntelligence };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * Web implementation stub for Apple Intelligence.\n * Always returns UNAVAILABLE since Apple Intelligence is iOS-only.\n */\nexport class AppleIntelligenceWeb extends WebPlugin {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generate(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateText(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateTextWithLanguage(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n async checkAvailability() {\n return {\n available: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async generateImage(_request) {\n return {\n success: false,\n error: {\n code: 'UNAVAILABLE',\n message: 'Apple Intelligence is only available on iOS 26+ devices with Apple Intelligence enabled.',\n },\n };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACK,UAAC,iBAAiB,GAAGA,mBAAc,CAAC,mBAAmB,EAAE;IAC9D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;IACxE,CAAC;;IC/BD;IACA;IACA;IACA;IACO,MAAM,oBAAoB,SAASC,cAAS,CAAC;IACpD;IACA,IAAI,MAAM,QAAQ,CAAC,QAAQ,EAAE;IAC7B,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;IACA,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;IACjC,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;IACA,IAAI,MAAM,wBAAwB,CAAC,QAAQ,EAAE;IAC7C,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;IACA,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;IAClC,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,KAAK;IAC1B,YAAY,KAAK,EAAE;IACnB,gBAAgB,IAAI,EAAE,aAAa;IACnC,gBAAgB,OAAO,EAAE,0FAA0F;IACnH,aAAa;IACb,SAAS;IACT,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -1,9 +1,14 @@
1
1
  import Foundation
2
+ import UIKit
2
3
 
3
4
  #if canImport(FoundationModels)
4
5
  import FoundationModels
5
6
  #endif
6
7
 
8
+ #if canImport(ImagePlayground)
9
+ import ImagePlayground
10
+ #endif
11
+
7
12
  // MARK: - Error Types
8
13
 
9
14
  /// Error codes for Apple Intelligence plugin
@@ -51,6 +56,35 @@ public struct Message {
51
56
  }
52
57
  }
53
58
 
59
+ // MARK: - Image Generation Options
60
+
61
+ /// Configuration options for image generation
62
+ public struct ImageGenerationOptions {
63
+ /// JPEG compression quality (0.0 to 1.0)
64
+ public let compressionQuality: CGFloat
65
+ /// Maximum dimension for the output image
66
+ public let maxDimension: CGFloat?
67
+ /// Output format
68
+ public let format: ImageFormat
69
+
70
+ public enum ImageFormat {
71
+ case png
72
+ case jpeg
73
+ }
74
+
75
+ public static let `default` = ImageGenerationOptions(
76
+ compressionQuality: 0.8,
77
+ maxDimension: 1024,
78
+ format: .jpeg
79
+ )
80
+
81
+ public init(compressionQuality: CGFloat = 0.8, maxDimension: CGFloat? = 1024, format: ImageFormat = .jpeg) {
82
+ self.compressionQuality = min(1.0, max(0.0, compressionQuality))
83
+ self.maxDimension = maxDimension
84
+ self.format = format
85
+ }
86
+ }
87
+
54
88
  // MARK: - Main Implementation
55
89
 
56
90
  /// Apple Intelligence implementation class
@@ -60,6 +94,121 @@ public struct Message {
60
94
  // MARK: - Constants
61
95
 
62
96
  private let maxRetries = 1
97
+ private let maxImageCount = 10
98
+ private let minImageCount = 1
99
+
100
+ // MARK: - Task Management
101
+
102
+ /// Currently running generation tasks for cancellation support
103
+ private var activeTasks: [UUID: Task<Void, Never>] = [:]
104
+ private let taskLock = NSLock()
105
+
106
+ /// Cancel all active generation tasks
107
+ public func cancelAllTasks() {
108
+ taskLock.lock()
109
+ defer { taskLock.unlock() }
110
+ for (_, task) in activeTasks {
111
+ task.cancel()
112
+ }
113
+ activeTasks.removeAll()
114
+ }
115
+
116
+ /// Register a task for tracking
117
+ private func registerTask(_ task: Task<Void, Never>) -> UUID {
118
+ let id = UUID()
119
+ taskLock.lock()
120
+ activeTasks[id] = task
121
+ taskLock.unlock()
122
+ return id
123
+ }
124
+
125
+ /// Unregister a completed task
126
+ private func unregisterTask(_ id: UUID) {
127
+ taskLock.lock()
128
+ activeTasks.removeValue(forKey: id)
129
+ taskLock.unlock()
130
+ }
131
+
132
+ // MARK: - Helper Methods
133
+
134
+ /// Convert schema dictionary to JSON string
135
+ private func schemaToString(_ schema: [String: Any]) -> String {
136
+ guard let data = try? JSONSerialization.data(withJSONObject: schema, options: [.prettyPrinted, .sortedKeys]),
137
+ let string = String(data: data, encoding: .utf8) else {
138
+ return "{}"
139
+ }
140
+ return string
141
+ }
142
+
143
+ /// Parse message dictionaries into Message objects
144
+ public func parseMessages(_ messages: [[String: String]]) -> [Message]? {
145
+ let parsed = messages.compactMap { dict -> Message? in
146
+ guard let roleStr = dict["role"],
147
+ let content = dict["content"],
148
+ let role = MessageRole(rawValue: roleStr) else { return nil }
149
+ return Message(role: role, content: content)
150
+ }
151
+ return parsed.isEmpty ? nil : parsed
152
+ }
153
+
154
+ /// Safely escape string for JSON embedding
155
+ private func escapeForJson(_ string: String) -> String {
156
+ guard let data = try? JSONSerialization.data(withJSONObject: string, options: []),
157
+ let escaped = String(data: data, encoding: .utf8) else {
158
+ // Fallback: basic escaping
159
+ return string
160
+ .replacingOccurrences(of: "\\", with: "\\\\")
161
+ .replacingOccurrences(of: "\"", with: "\\\"")
162
+ .replacingOccurrences(of: "\n", with: "\\n")
163
+ .replacingOccurrences(of: "\r", with: "\\r")
164
+ .replacingOccurrences(of: "\t", with: "\\t")
165
+ }
166
+ // Remove surrounding quotes from JSON string serialization
167
+ return String(escaped.dropFirst().dropLast())
168
+ }
169
+
170
+ /// Get full language name from locale code
171
+ private func fullLanguageName(from code: String) -> String {
172
+ // First try to get from system locale
173
+ if let name = Locale.current.localizedString(forLanguageCode: code.lowercased()) {
174
+ return name
175
+ }
176
+
177
+ // Fallback map for common codes
178
+ let fallbackMap: [String: String] = [
179
+ "en": "English",
180
+ "es": "Spanish",
181
+ "fr": "French",
182
+ "de": "German",
183
+ "ja": "Japanese",
184
+ "zh": "Chinese",
185
+ "it": "Italian",
186
+ "pt": "Portuguese",
187
+ "ru": "Russian",
188
+ "ar": "Arabic",
189
+ "ko": "Korean",
190
+ "nl": "Dutch",
191
+ "pl": "Polish",
192
+ "tr": "Turkish",
193
+ "vi": "Vietnamese",
194
+ "th": "Thai",
195
+ "hi": "Hindi",
196
+ "he": "Hebrew",
197
+ "id": "Indonesian",
198
+ "ms": "Malay",
199
+ "sv": "Swedish",
200
+ "da": "Danish",
201
+ "no": "Norwegian",
202
+ "fi": "Finnish",
203
+ "cs": "Czech",
204
+ "el": "Greek",
205
+ "hu": "Hungarian",
206
+ "ro": "Romanian",
207
+ "uk": "Ukrainian"
208
+ ]
209
+
210
+ return fallbackMap[code.lowercased()] ?? code
211
+ }
63
212
 
64
213
  // MARK: - JSON Schema Validation
65
214
 
@@ -77,10 +226,9 @@ public struct Message {
77
226
  case "string":
78
227
  return (json is String, json is String ? nil : "Expected string, got \(type(of: json))")
79
228
  case "number", "integer":
80
- return (json is NSNumber && !(json is Bool),
81
- (json is NSNumber && !(json is Bool)) ? nil : "Expected number, got \(type(of: json))")
229
+ return validateNumber(json)
82
230
  case "boolean":
83
- return (json is Bool, json is Bool ? nil : "Expected boolean, got \(type(of: json))")
231
+ return validateBoolean(json)
84
232
  case "null":
85
233
  return (json is NSNull, json is NSNull ? nil : "Expected null, got \(type(of: json))")
86
234
  default:
@@ -88,6 +236,40 @@ public struct Message {
88
236
  }
89
237
  }
90
238
 
239
+ /// Validate a number value (ensuring it's not actually a boolean)
240
+ private func validateNumber(_ json: Any) -> (valid: Bool, error: String?) {
241
+ guard let number = json as? NSNumber else {
242
+ return (false, "Expected number, got \(type(of: json))")
243
+ }
244
+
245
+ // Use CFGetTypeID to properly distinguish booleans from numbers
246
+ let boolID = CFBooleanGetTypeID()
247
+ let typeID = CFGetTypeID(number as CFTypeRef)
248
+
249
+ if typeID == boolID {
250
+ return (false, "Expected number, got boolean")
251
+ }
252
+
253
+ return (true, nil)
254
+ }
255
+
256
+ /// Validate a boolean value
257
+ private func validateBoolean(_ json: Any) -> (valid: Bool, error: String?) {
258
+ guard let number = json as? NSNumber else {
259
+ return (false, "Expected boolean, got \(type(of: json))")
260
+ }
261
+
262
+ // Use CFGetTypeID to properly identify booleans
263
+ let boolID = CFBooleanGetTypeID()
264
+ let typeID = CFGetTypeID(number as CFTypeRef)
265
+
266
+ if typeID == boolID {
267
+ return (true, nil)
268
+ }
269
+
270
+ return (false, "Expected boolean, got number")
271
+ }
272
+
91
273
  /// Validate an object against a schema
92
274
  private func validateObject(_ json: Any, schema: [String: Any]) -> (valid: Bool, error: String?) {
93
275
  guard let jsonObject = json as? [String: Any] else {
@@ -155,13 +337,7 @@ public struct Message {
155
337
 
156
338
  /// Build the system prompt with JSON schema instructions
157
339
  private func buildSystemPrompt(userSystemPrompt: String?, schema: [String: Any]) -> String {
158
- let schemaJson: String
159
- do {
160
- let schemaData = try JSONSerialization.data(withJSONObject: schema, options: [.prettyPrinted, .sortedKeys])
161
- schemaJson = String(data: schemaData, encoding: .utf8) ?? "{}"
162
- } catch {
163
- schemaJson = "{}"
164
- }
340
+ let schemaJson = schemaToString(schema)
165
341
 
166
342
  var prompt = """
167
343
  You are a JSON generator. Your response must be ONLY valid JSON that matches the provided schema.
@@ -189,13 +365,7 @@ public struct Message {
189
365
 
190
366
  /// Build corrective prompt for retry attempts
191
367
  private func buildCorrectivePrompt(previousResponse: String, validationError: String, schema: [String: Any]) -> String {
192
- let schemaJson: String
193
- do {
194
- let schemaData = try JSONSerialization.data(withJSONObject: schema, options: [.prettyPrinted, .sortedKeys])
195
- schemaJson = String(data: schemaData, encoding: .utf8) ?? "{}"
196
- } catch {
197
- schemaJson = "{}"
198
- }
368
+ let schemaJson = schemaToString(schema)
199
369
 
200
370
  return """
201
371
  The previous response was invalid JSON or did not match the required schema.
@@ -278,8 +448,13 @@ public struct Message {
278
448
  messages: [Message],
279
449
  schema: [String: Any]
280
450
  ) async -> Result<Any, AppleIntelligenceError> {
281
- // Import Foundation Models at runtime
282
- // Note: This uses the new Foundation Models framework available in iOS 26+
451
+ // Check for task cancellation
452
+ if Task.isCancelled {
453
+ return .failure(AppleIntelligenceError(
454
+ code: .nativeError,
455
+ message: "Generation was cancelled"
456
+ ))
457
+ }
283
458
 
284
459
  // Extract system and user messages
285
460
  let systemMessages = messages.filter { $0.role == .system }.map { $0.content }
@@ -296,6 +471,14 @@ public struct Message {
296
471
  var lastError = ""
297
472
 
298
473
  for attempt in 0...maxRetries {
474
+ // Check for cancellation between attempts
475
+ if Task.isCancelled {
476
+ return .failure(AppleIntelligenceError(
477
+ code: .nativeError,
478
+ message: "Generation was cancelled"
479
+ ))
480
+ }
481
+
299
482
  do {
300
483
  let response: String
301
484
 
@@ -380,14 +563,24 @@ public struct Message {
380
563
  return response.content
381
564
 
382
565
  #else
383
- // Fallback for development/testing when FoundationModels isn't available
384
- throw AppleIntelligenceError(
385
- code: .unavailable,
386
- message: "FoundationModels framework not available"
387
- )
566
+ // Mock fallback for development/testing
567
+ if systemPrompt.contains("You are a JSON generator") {
568
+ // JSON mock for generate() calls - using proper JSON escaping
569
+ let escapedQuery = escapeForJson(userPrompt)
570
+ return """
571
+ {
572
+ "mock_response": "This is a mocked JSON response from Apple Intelligence (Runtime not available)",
573
+ "original_query": "\(escapedQuery)"
574
+ }
575
+ """
576
+ } else {
577
+ // Return plain text for generateText() calls
578
+ return "This is a mocked response from Apple Intelligence (Runtime not available). User query was: \(userPrompt)"
579
+ }
388
580
  #endif
389
581
  }
390
582
 
583
+ // MARK: - Bridge Helpers
391
584
 
392
585
  /// Generate method that returns a dictionary suitable for Capacitor bridge
393
586
  @available(iOS 26, *)
@@ -395,17 +588,7 @@ public struct Message {
395
588
  messages: [[String: String]],
396
589
  schema: [String: Any]
397
590
  ) async -> [String: Any] {
398
- // Convert raw dictionaries to Message objects
399
- let parsedMessages = messages.compactMap { dict -> Message? in
400
- guard let roleStr = dict["role"],
401
- let content = dict["content"],
402
- let role = MessageRole(rawValue: roleStr) else {
403
- return nil
404
- }
405
- return Message(role: role, content: content)
406
- }
407
-
408
- if parsedMessages.isEmpty {
591
+ guard let parsedMessages = parseMessages(messages) else {
409
592
  return [
410
593
  "success": false,
411
594
  "error": AppleIntelligenceError(
@@ -430,6 +613,7 @@ public struct Message {
430
613
  ]
431
614
  }
432
615
  }
616
+
433
617
  /// Generate plain text output using Apple Intelligence
434
618
  /// - Parameters:
435
619
  /// - messages: Array of conversation messages
@@ -438,6 +622,14 @@ public struct Message {
438
622
  public func generateText(
439
623
  messages: [Message]
440
624
  ) async -> Result<String, AppleIntelligenceError> {
625
+ // Check for task cancellation
626
+ if Task.isCancelled {
627
+ return .failure(AppleIntelligenceError(
628
+ code: .nativeError,
629
+ message: "Generation was cancelled"
630
+ ))
631
+ }
632
+
441
633
  let systemMessages = messages.filter { $0.role == .system }.map { $0.content }
442
634
  let userMessages = messages.filter { $0.role == .user }.map { $0.content }
443
635
 
@@ -468,25 +660,19 @@ public struct Message {
468
660
  messages: [Message],
469
661
  language: String
470
662
  ) async -> Result<String, AppleIntelligenceError> {
663
+ // Check for task cancellation
664
+ if Task.isCancelled {
665
+ return .failure(AppleIntelligenceError(
666
+ code: .nativeError,
667
+ message: "Generation was cancelled"
668
+ ))
669
+ }
670
+
471
671
  let systemMessages = messages.filter { $0.role == .system }.map { $0.content }
472
672
  let userMessages = messages.filter { $0.role == .user }.map { $0.content }
473
673
 
474
- // Convert language code to full language name for better model understanding
475
- let languageMap: [String: String] = [
476
- "en": "English",
477
- "es": "Spanish",
478
- "fr": "French",
479
- "de": "German",
480
- "ja": "Japanese",
481
- "zh": "Chinese",
482
- "it": "Italian",
483
- "pt": "Portuguese",
484
- "ru": "Russian",
485
- "ar": "Arabic",
486
- "ko": "Korean"
487
- ]
488
-
489
- let fullLanguageName = languageMap[language.lowercased()] ?? language
674
+ // Get full language name dynamically
675
+ let fullLanguageName = fullLanguageName(from: language)
490
676
 
491
677
  var systemPrompt = systemMessages.joined(separator: "\n")
492
678
  // Append language instruction
@@ -516,16 +702,7 @@ public struct Message {
516
702
  public func generateTextForBridge(
517
703
  messages: [[String: String]]
518
704
  ) async -> [String: Any] {
519
- let parsedMessages = messages.compactMap { dict -> Message? in
520
- guard let roleStr = dict["role"],
521
- let content = dict["content"],
522
- let role = MessageRole(rawValue: roleStr) else {
523
- return nil
524
- }
525
- return Message(role: role, content: content)
526
- }
527
-
528
- if parsedMessages.isEmpty {
705
+ guard let parsedMessages = parseMessages(messages) else {
529
706
  return [
530
707
  "success": false,
531
708
  "error": AppleIntelligenceError(
@@ -557,16 +734,7 @@ public struct Message {
557
734
  messages: [[String: String]],
558
735
  language: String
559
736
  ) async -> [String: Any] {
560
- let parsedMessages = messages.compactMap { dict -> Message? in
561
- guard let roleStr = dict["role"],
562
- let content = dict["content"],
563
- let role = MessageRole(rawValue: roleStr) else {
564
- return nil
565
- }
566
- return Message(role: role, content: content)
567
- }
568
-
569
- if parsedMessages.isEmpty {
737
+ guard let parsedMessages = parseMessages(messages) else {
570
738
  return [
571
739
  "success": false,
572
740
  "error": AppleIntelligenceError(
@@ -591,4 +759,180 @@ public struct Message {
591
759
  ]
592
760
  }
593
761
  }
762
+
763
+ // MARK: - Image Generation
764
+
765
+ /// Supported image styles for Apple's on-device Image Playground
766
+ /// Note: Photorealistic images are NOT supported by Apple's on-device API
767
+ public static let supportedImageStyles = ["animation", "illustration", "sketch"]
768
+
769
+ /// Generate images using Apple Intelligence
770
+ /// - Parameters:
771
+ /// - prompt: Description of the image
772
+ /// - style: Style for the image. Supported values: "animation" (default), "illustration", "sketch".
773
+ /// Note: "photorealistic" and other styles are NOT supported by Apple's on-device Image Playground API.
774
+ /// - count: Number of images (clamped to 1-10)
775
+ /// - sourceImage: Optional source image for face-based generation. Required when prompt involves generating images of people.
776
+ /// - options: Image generation options for compression and sizing
777
+ /// - Returns: Result containing array of base64 image strings or error
778
+ @available(iOS 26, *)
779
+ public func generateImage(
780
+ prompt: String,
781
+ style: String?,
782
+ count: Int,
783
+ sourceImage: UIImage? = nil,
784
+ options: ImageGenerationOptions = .default
785
+ ) async -> Result<[String], AppleIntelligenceError> {
786
+ // Check for task cancellation
787
+ if Task.isCancelled {
788
+ return .failure(AppleIntelligenceError(
789
+ code: .nativeError,
790
+ message: "Image generation was cancelled"
791
+ ))
792
+ }
793
+
794
+ // Validate style parameter
795
+ if let styleStr = style?.lowercased(), !styleStr.isEmpty {
796
+ if !Self.supportedImageStyles.contains(styleStr) {
797
+ return .failure(AppleIntelligenceError(
798
+ code: .nativeError,
799
+ message: "Unsupported image style: '\(style ?? "")'. Apple's on-device Image Playground only supports: \(Self.supportedImageStyles.joined(separator: ", ")). Note: Photorealistic images are not supported by Apple's native API."
800
+ ))
801
+ }
802
+ }
803
+
804
+ // Validate and clamp count
805
+ let validatedCount = max(minImageCount, min(count, maxImageCount))
806
+
807
+ #if canImport(ImagePlayground)
808
+ do {
809
+ let creator = try await ImageCreator()
810
+
811
+ // Build concepts array
812
+ var concepts: [ImagePlaygroundConcept] = [.text(prompt)]
813
+
814
+ // If source image provided, save to temp URL and add as image concept
815
+ if let source = sourceImage, let jpegData = source.jpegData(compressionQuality: 0.9) {
816
+ let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("source_\(UUID().uuidString).jpg")
817
+ try jpegData.write(to: tempURL)
818
+ if let imageConcept = ImagePlaygroundConcept.image(tempURL) {
819
+ concepts.append(imageConcept)
820
+ }
821
+ }
822
+
823
+ // Map style string to ImagePlaygroundStyle enum
824
+ let imageStyle: ImagePlaygroundStyle
825
+ switch style?.lowercased() {
826
+ case "illustration": imageStyle = .illustration
827
+ case "sketch": imageStyle = .sketch
828
+ default: imageStyle = .animation // Default to animation
829
+ }
830
+
831
+ var base64Images: [String] = []
832
+
833
+ for try await createdImage in creator.images(for: concepts, style: imageStyle, limit: validatedCount) {
834
+ // Check for cancellation during image generation
835
+ if Task.isCancelled {
836
+ return .failure(AppleIntelligenceError(
837
+ code: .nativeError,
838
+ message: "Image generation was cancelled"
839
+ ))
840
+ }
841
+
842
+ var uiImage = UIImage(cgImage: createdImage.cgImage)
843
+
844
+ // Resize if needed
845
+ if let maxDim = options.maxDimension {
846
+ uiImage = resizeImage(uiImage, maxDimension: maxDim)
847
+ }
848
+
849
+ // Compress based on format
850
+ let data: Data?
851
+ switch options.format {
852
+ case .png:
853
+ data = uiImage.pngData()
854
+ case .jpeg:
855
+ data = uiImage.jpegData(compressionQuality: options.compressionQuality)
856
+ }
857
+
858
+ if let imageData = data {
859
+ base64Images.append(imageData.base64EncodedString())
860
+ }
861
+ }
862
+
863
+ if base64Images.isEmpty {
864
+ return .failure(AppleIntelligenceError(code: .nativeError, message: "No images were generated"))
865
+ }
866
+ return .success(base64Images)
867
+ } catch let error as NSError {
868
+ // Provide more helpful error message for face-related errors
869
+ let errorMessage = error.localizedDescription
870
+ if errorMessage.lowercased().contains("face") || errorMessage.lowercased().contains("person") || errorMessage.lowercased().contains("source image") {
871
+ return .failure(AppleIntelligenceError(
872
+ code: .nativeError,
873
+ message: "This prompt requires generating images of people. Please provide a source image containing a face using the 'sourceImage' parameter, or modify your prompt to avoid generating human faces."
874
+ ))
875
+ }
876
+ return .failure(AppleIntelligenceError(code: .nativeError, message: "Image generation failed: \(errorMessage)"))
877
+ }
878
+ #else
879
+ return .failure(AppleIntelligenceError(code: .unavailable, message: "ImagePlayground not available"))
880
+ #endif
881
+ }
882
+
883
+ /// Resize image to fit within maximum dimension while maintaining aspect ratio
884
+ private func resizeImage(_ image: UIImage, maxDimension: CGFloat) -> UIImage {
885
+ let size = image.size
886
+
887
+ // Check if resizing is needed
888
+ guard size.width > maxDimension || size.height > maxDimension else {
889
+ return image
890
+ }
891
+
892
+ let ratio = min(maxDimension / size.width, maxDimension / size.height)
893
+ let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
894
+
895
+ let renderer = UIGraphicsImageRenderer(size: newSize)
896
+ return renderer.image { _ in
897
+ image.draw(in: CGRect(origin: .zero, size: newSize))
898
+ }
899
+ }
900
+
901
+ /// Generate image bridge helper
902
+ @available(iOS 26, *)
903
+ public func generateImageForBridge(
904
+ prompt: String,
905
+ style: String?,
906
+ count: Int,
907
+ sourceImageBase64: String? = nil,
908
+ compressionQuality: CGFloat? = nil
909
+ ) async -> [String: Any] {
910
+ let options = ImageGenerationOptions(
911
+ compressionQuality: compressionQuality ?? 0.8,
912
+ maxDimension: 1024,
913
+ format: .jpeg
914
+ )
915
+
916
+ // Decode source image from base64 if provided
917
+ var sourceImage: UIImage? = nil
918
+ if let base64String = sourceImageBase64,
919
+ let imageData = Data(base64Encoded: base64String) {
920
+ sourceImage = UIImage(data: imageData)
921
+ }
922
+
923
+ let result = await generateImage(prompt: prompt, style: style, count: count, sourceImage: sourceImage, options: options)
924
+
925
+ switch result {
926
+ case .success(let images):
927
+ return [
928
+ "success": true,
929
+ "images": images
930
+ ]
931
+ case .failure(let error):
932
+ return [
933
+ "success": false,
934
+ "error": error.asDictionary
935
+ ]
936
+ }
937
+ }
594
938
  }
@@ -3,6 +3,7 @@ import Capacitor
3
3
 
4
4
  /// Capacitor plugin class for Apple Intelligence
5
5
  /// Bridges JavaScript calls to the native AppleIntelligence implementation
6
+ @MainActor
6
7
  @objc(AppleIntelligencePlugin)
7
8
  public class AppleIntelligencePlugin: CAPPlugin, CAPBridgedPlugin {
8
9
 
@@ -14,13 +15,39 @@ public class AppleIntelligencePlugin: CAPPlugin, CAPBridgedPlugin {
14
15
  CAPPluginMethod(name: "generate", returnType: CAPPluginReturnPromise),
15
16
  CAPPluginMethod(name: "generateText", returnType: CAPPluginReturnPromise),
16
17
  CAPPluginMethod(name: "generateTextWithLanguage", returnType: CAPPluginReturnPromise),
17
- CAPPluginMethod(name: "checkAvailability", returnType: CAPPluginReturnPromise)
18
+ CAPPluginMethod(name: "generateImage", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "checkAvailability", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "cancelAllTasks", returnType: CAPPluginReturnPromise)
18
21
  ]
19
22
 
20
23
  // MARK: - Implementation
21
24
 
22
25
  private let implementation = AppleIntelligence()
23
26
 
27
+ // MARK: - Helper Methods
28
+
29
+ /// Create a standardized error response dictionary
30
+ private func errorResponse(code: AppleIntelligenceErrorCode, message: String) -> [String: Any] {
31
+ return [
32
+ "success": false,
33
+ "error": [
34
+ "code": code.rawValue,
35
+ "message": message
36
+ ]
37
+ ]
38
+ }
39
+
40
+ /// Create an unavailable error response from the availability check
41
+ private func unavailableResponse(from availability: (available: Bool, error: AppleIntelligenceError?)) -> [String: Any] {
42
+ return [
43
+ "success": false,
44
+ "error": availability.error?.asDictionary ?? [
45
+ "code": AppleIntelligenceErrorCode.unavailable.rawValue,
46
+ "message": "Apple Intelligence is not available on this device."
47
+ ]
48
+ ]
49
+ }
50
+
24
51
  // MARK: - Plugin Methods
25
52
 
26
53
  /// Generate structured JSON output using Apple Intelligence
@@ -28,185 +55,160 @@ public class AppleIntelligencePlugin: CAPPlugin, CAPBridgedPlugin {
28
55
  @objc func generate(_ call: CAPPluginCall) {
29
56
  // Validate input
30
57
  guard let messagesArray = call.getArray("messages") as? [[String: String]] else {
31
- call.resolve([
32
- "success": false,
33
- "error": [
34
- "code": "NATIVE_ERROR",
35
- "message": "Invalid or missing 'messages' array. Expected array of { role: string, content: string }."
36
- ]
37
- ])
58
+ call.resolve(errorResponse(
59
+ code: .nativeError,
60
+ message: "Invalid or missing 'messages' array. Expected array of { role: string, content: string }."
61
+ ))
38
62
  return
39
63
  }
40
64
 
41
65
  guard let responseFormat = call.getObject("response_format") else {
42
- call.resolve([
43
- "success": false,
44
- "error": [
45
- "code": "NATIVE_ERROR",
46
- "message": "Missing 'response_format' object."
47
- ]
48
- ])
66
+ call.resolve(errorResponse(
67
+ code: .nativeError,
68
+ message: "Missing 'response_format' object."
69
+ ))
49
70
  return
50
71
  }
51
72
 
52
73
  guard let formatType = responseFormat["type"] as? String, formatType == "json_schema" else {
53
- call.resolve([
54
- "success": false,
55
- "error": [
56
- "code": "NATIVE_ERROR",
57
- "message": "response_format.type must be 'json_schema'."
58
- ]
59
- ])
74
+ call.resolve(errorResponse(
75
+ code: .nativeError,
76
+ message: "response_format.type must be 'json_schema'."
77
+ ))
60
78
  return
61
79
  }
62
80
 
63
81
  guard let schema = responseFormat["schema"] as? [String: Any] else {
64
- call.resolve([
65
- "success": false,
66
- "error": [
67
- "code": "NATIVE_ERROR",
68
- "message": "Missing or invalid 'schema' in response_format."
69
- ]
70
- ])
82
+ call.resolve(errorResponse(
83
+ code: .nativeError,
84
+ message: "Missing or invalid 'schema' in response_format."
85
+ ))
71
86
  return
72
87
  }
73
88
 
74
- // Check availability
89
+ // Check availability (handles both iOS version and Apple Intelligence feature availability)
75
90
  let availability = implementation.checkAvailability()
76
- if !availability.available {
77
- call.resolve([
78
- "success": false,
79
- "error": availability.error?.asDictionary ?? [
80
- "code": "UNAVAILABLE",
81
- "message": "Apple Intelligence is not available on this device."
82
- ]
83
- ])
91
+ guard availability.available else {
92
+ call.resolve(unavailableResponse(from: availability))
84
93
  return
85
94
  }
86
95
 
87
96
  // Execute generation asynchronously
88
97
  if #available(iOS 26, *) {
89
- Task {
90
- let result = await implementation.generateForBridge(
98
+ Task { [weak self] in
99
+ guard let self = self else { return }
100
+
101
+ let result = await self.implementation.generateForBridge(
91
102
  messages: messagesArray,
92
103
  schema: schema
93
104
  )
94
105
 
95
- // Resolve on main thread
96
- await MainActor.run {
97
- call.resolve(result)
98
- }
106
+ call.resolve(result)
99
107
  }
100
- } else {
101
- call.resolve([
102
- "success": false,
103
- "error": [
104
- "code": "UNAVAILABLE",
105
- "message": "Apple Intelligence requires iOS 26 or later."
106
- ]
107
- ])
108
108
  }
109
+ // Note: No else needed - checkAvailability already handles iOS version check
109
110
  }
110
111
 
111
112
  /// Generate plain text output using Apple Intelligence
112
113
  @objc func generateText(_ call: CAPPluginCall) {
113
114
  guard let messagesArray = call.getArray("messages") as? [[String: String]] else {
114
- call.resolve([
115
- "success": false,
116
- "error": [
117
- "code": "NATIVE_ERROR",
118
- "message": "Invalid or missing 'messages' array."
119
- ]
120
- ])
115
+ call.resolve(errorResponse(
116
+ code: .nativeError,
117
+ message: "Invalid or missing 'messages' array."
118
+ ))
121
119
  return
122
120
  }
123
121
 
124
122
  // Check availability
125
123
  let availability = implementation.checkAvailability()
126
- if !availability.available {
127
- call.resolve([
128
- "success": false,
129
- "error": availability.error?.asDictionary ?? [
130
- "code": "UNAVAILABLE",
131
- "message": "Apple Intelligence is not available on this device."
132
- ]
133
- ])
124
+ guard availability.available else {
125
+ call.resolve(unavailableResponse(from: availability))
134
126
  return
135
127
  }
136
128
 
137
129
  if #available(iOS 26, *) {
138
- Task {
139
- let result = await implementation.generateTextForBridge(messages: messagesArray)
140
- await MainActor.run {
141
- call.resolve(result)
142
- }
130
+ Task { [weak self] in
131
+ guard let self = self else { return }
132
+
133
+ let result = await self.implementation.generateTextForBridge(messages: messagesArray)
134
+ call.resolve(result)
143
135
  }
144
- } else {
145
- call.resolve([
146
- "success": false,
147
- "error": [
148
- "code": "UNAVAILABLE",
149
- "message": "Apple Intelligence requires iOS 26 or later."
150
- ]
151
- ])
152
136
  }
153
137
  }
154
138
 
155
139
  /// Generate plain text output with specific language
156
140
  @objc func generateTextWithLanguage(_ call: CAPPluginCall) {
157
141
  guard let messagesArray = call.getArray("messages") as? [[String: String]] else {
158
- call.resolve([
159
- "success": false,
160
- "error": [
161
- "code": "NATIVE_ERROR",
162
- "message": "Invalid or missing 'messages' array."
163
- ]
164
- ])
142
+ call.resolve(errorResponse(
143
+ code: .nativeError,
144
+ message: "Invalid or missing 'messages' array."
145
+ ))
165
146
  return
166
147
  }
167
148
 
168
149
  guard let language = call.getString("language") else {
169
- call.resolve([
170
- "success": false,
171
- "error": [
172
- "code": "NATIVE_ERROR",
173
- "message": "Missing 'language' string."
174
- ]
175
- ])
150
+ call.resolve(errorResponse(
151
+ code: .nativeError,
152
+ message: "Missing 'language' string."
153
+ ))
176
154
  return
177
155
  }
178
156
 
179
157
  // Check availability
180
158
  let availability = implementation.checkAvailability()
181
- if !availability.available {
182
- call.resolve([
183
- "success": false,
184
- "error": availability.error?.asDictionary ?? [
185
- "code": "UNAVAILABLE",
186
- "message": "Apple Intelligence is not available on this device."
187
- ]
188
- ])
159
+ guard availability.available else {
160
+ call.resolve(unavailableResponse(from: availability))
189
161
  return
190
162
  }
191
163
 
192
164
  if #available(iOS 26, *) {
193
- Task {
194
- let result = await implementation.generateTextWithLanguageForBridge(
165
+ Task { [weak self] in
166
+ guard let self = self else { return }
167
+
168
+ let result = await self.implementation.generateTextWithLanguageForBridge(
195
169
  messages: messagesArray,
196
170
  language: language
197
171
  )
198
- await MainActor.run {
199
- call.resolve(result)
200
- }
172
+ call.resolve(result)
173
+ }
174
+ }
175
+ }
176
+
177
+ /// Generate image using Apple Intelligence
178
+ @objc func generateImage(_ call: CAPPluginCall) {
179
+ guard let prompt = call.getString("prompt") else {
180
+ call.resolve(errorResponse(
181
+ code: .nativeError,
182
+ message: "Missing 'prompt' string."
183
+ ))
184
+ return
185
+ }
186
+
187
+ let style = call.getString("style")
188
+ let count = call.getInt("count") ?? 1
189
+ let compressionQuality = call.getDouble("compressionQuality")
190
+ let sourceImage = call.getString("sourceImage")
191
+
192
+ // Check availability
193
+ let availability = implementation.checkAvailability()
194
+ guard availability.available else {
195
+ call.resolve(unavailableResponse(from: availability))
196
+ return
197
+ }
198
+
199
+ if #available(iOS 26, *) {
200
+ Task { [weak self] in
201
+ guard let self = self else { return }
202
+
203
+ let result = await self.implementation.generateImageForBridge(
204
+ prompt: prompt,
205
+ style: style,
206
+ count: count,
207
+ sourceImageBase64: sourceImage,
208
+ compressionQuality: compressionQuality.map { CGFloat($0) }
209
+ )
210
+ call.resolve(result)
201
211
  }
202
- } else {
203
- call.resolve([
204
- "success": false,
205
- "error": [
206
- "code": "UNAVAILABLE",
207
- "message": "Apple Intelligence requires iOS 26 or later."
208
- ]
209
- ])
210
212
  }
211
213
  }
212
214
 
@@ -222,10 +224,18 @@ public class AppleIntelligencePlugin: CAPPlugin, CAPBridgedPlugin {
222
224
  call.resolve([
223
225
  "available": false,
224
226
  "error": availability.error?.asDictionary ?? [
225
- "code": "UNAVAILABLE",
227
+ "code": AppleIntelligenceErrorCode.unavailable.rawValue,
226
228
  "message": "Apple Intelligence is not available on this device."
227
229
  ]
228
230
  ])
229
231
  }
230
232
  }
233
+
234
+ /// Cancel all active generation tasks
235
+ @objc func cancelAllTasks(_ call: CAPPluginCall) {
236
+ implementation.cancelAllTasks()
237
+ call.resolve([
238
+ "success": true
239
+ ])
240
+ }
231
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-apple-intelligence",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Capacitor v8 plugin for Apple Intelligence with schema-constrained JSON generation for structured AI responses",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -77,4 +77,4 @@
77
77
  "src": "ios"
78
78
  }
79
79
  }
80
- }
80
+ }