mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-28 22:47:04 +00:00
616 lines
22 KiB
Plaintext
616 lines
22 KiB
Plaintext
/*
|
|
* CPLanguageModel.j
|
|
* Foundation
|
|
*
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*/
|
|
|
|
@import "CPObject.j"
|
|
@import "CPString.j"
|
|
@import "CPError.j"
|
|
@import "CPDictionary.j"
|
|
@import "CPBundle.j"
|
|
@import "CPUserDefaults.j"
|
|
|
|
// File-scoped fallback configuration parameters
|
|
var CPLanguageModelSessionFallbackServiceType = @"ollama",
|
|
CPLanguageModelSessionFallbackEndpoint = @"http://localhost:11434/api/generate",
|
|
CPLanguageModelSessionFallbackModel = @"gemma4:e4b",
|
|
CPLanguageModelSessionFallbackAPIKey = @"",
|
|
CPLanguageModelSessionFallbackAPIKeyUserDefaultKey = @"",
|
|
CPLanguageModelSessionEndorsesFallback = NO;
|
|
|
|
|
|
/*!
|
|
@ingroup foundation
|
|
@class CPSystemLanguageModel
|
|
|
|
CPSystemLanguageModel provides a standard query interface to inspect the
|
|
availability of client-side, on-device large language models (such as Gemma Nano on Chrome)
|
|
in the active web browser runtime.
|
|
*/
|
|
@implementation CPSystemLanguageModel : CPObject
|
|
|
|
var sharedInstance = nil;
|
|
|
|
/*!
|
|
Returns the singleton system language model monitor.
|
|
@return the default CPSystemLanguageModel instance
|
|
*/
|
|
+ (id)defaultModel
|
|
{
|
|
|
|
if (!sharedInstance)
|
|
sharedInstance = [[CPSystemLanguageModel alloc] init];
|
|
|
|
return sharedInstance;
|
|
}
|
|
|
|
/*!
|
|
Asynchronously queries the active browser environment to determine if on-device
|
|
language models are supported and readily available to execute prompts.
|
|
@param completionHandler a callback block executed with a boolean parameter (supported)
|
|
*/
|
|
- (void)supportsLocaleWithCompletionHandler:(Function)completionHandler
|
|
{
|
|
if (typeof window === "undefined" || !completionHandler)
|
|
{
|
|
if (completionHandler)
|
|
completionHandler(NO);
|
|
|
|
return;
|
|
}
|
|
|
|
(async function() {
|
|
var supported = false;
|
|
|
|
try {
|
|
if (window.ai && window.ai.languageModel) {
|
|
// Pass language options to align with the creation options
|
|
var options = {
|
|
expectedInputs: [{ type: "text", languages: ["en"] }],
|
|
expectedOutputs: [{ type: "text", languages: ["en"] }]
|
|
};
|
|
|
|
if (typeof window.ai.languageModel.availability === 'function') {
|
|
var avail = await window.ai.languageModel.availability(options);
|
|
supported = (avail === "readily" || avail === "available" || avail === "after-download");
|
|
} else if (typeof window.ai.languageModel.capabilities === 'function') {
|
|
var caps = await window.ai.languageModel.capabilities(options);
|
|
supported = (caps.available === "readily" || caps.available === "after-download");
|
|
} else {
|
|
supported = true;
|
|
}
|
|
}
|
|
else if (window.LanguageModel) {
|
|
supported = true;
|
|
}
|
|
} catch (e) {
|
|
supported = false;
|
|
}
|
|
|
|
completionHandler(supported);
|
|
})();
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
/*!
|
|
@ingroup foundation
|
|
@class CPLanguageModelSession
|
|
|
|
CPLanguageModelSession manages an active session with a local on-device
|
|
language model. If the active browser does not support on-device models, the session
|
|
gracefully and transparently falls back to configured remote server endpoints.
|
|
|
|
@discussion
|
|
CPLanguageModelSession handles text generation prompts. If on-device AI
|
|
(like Gemma Nano) is supported by the browser, it is utilized directly.
|
|
Otherwise, or if CPLanguageModelSessionEndorsesFallback is configured to YES,
|
|
the session automatically falls back to configured network-based providers
|
|
(such as local Ollama, Groq, or OpenRouter).
|
|
|
|
Fallback configurations can be populated globally using the application's Info.plist
|
|
via the following keys:
|
|
<pre>
|
|
CPEndorseLanguageModelFallback - YES to bypass on-device models and force fallback
|
|
CPDefaultLanguageModelService - "ollama" | "groq" | "gemini" | "openrouter"
|
|
CPDefaultLanguageModelEndpoint - API Endpoint (e.g. Ollama URL)
|
|
CPDefaultLanguageModelModel - Model name string
|
|
CPDefaultLanguageModelAPIKeyUserDefaultKey - CPUserDefaults key containing the actual API token
|
|
CPDefaultLanguageModelAPIKey - Authentication token string (Unsecure direct fallback)
|
|
</pre>
|
|
*/
|
|
@implementation CPLanguageModelSession : CPObject
|
|
{
|
|
id _chromeSession @accessors(property=chromeSession);
|
|
CPString _instructions @accessors(property=instructions);
|
|
CPString _fallbackServiceType @accessors(property=fallbackServiceType);
|
|
CPString _fallbackEndpoint @accessors(property=fallbackEndpoint);
|
|
CPString _fallbackModel @accessors(property=fallbackModel);
|
|
CPString _fallbackAPIKey @accessors(property=fallbackAPIKey);
|
|
}
|
|
|
|
/*!
|
|
Initializes fallback defaults and "Endorsement" flags from the application's Info.plist.
|
|
*/
|
|
+ (void)initialize
|
|
{
|
|
if (self === [CPLanguageModelSession class])
|
|
{
|
|
var bundle = [CPBundle mainBundle];
|
|
|
|
CPLanguageModelSessionEndorsesFallback = !![bundle objectForInfoDictionaryKey:@"CPEndorseLanguageModelFallback"];
|
|
CPLanguageModelSessionFallbackServiceType = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelService"] || @"ollama";
|
|
CPLanguageModelSessionFallbackEndpoint = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelEndpoint"] || @"http://localhost:11434/api/generate";
|
|
CPLanguageModelSessionFallbackModel = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelModel"] || @"gemma4:e4b";
|
|
CPLanguageModelSessionFallbackAPIKey = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelAPIKey"] || @"";
|
|
CPLanguageModelSessionFallbackAPIKeyUserDefaultKey = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelAPIKeyUserDefaultKey"] || @"";
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Configures whether the session should bypass native browser AI models and force fallback network routing.
|
|
@param shouldEndorse YES to bypass native AI; NO to prioritize native AI if available
|
|
*/
|
|
+ (void)setEndorsesFallback:(BOOL)shouldEndorse
|
|
{
|
|
CPLanguageModelSessionEndorsesFallback = shouldEndorse;
|
|
}
|
|
|
|
/*!
|
|
Indicates if the session bypasses native browser AI models.
|
|
@return YES if forced fallback is active; NO otherwise
|
|
*/
|
|
+ (BOOL)endorsesFallback
|
|
{
|
|
return CPLanguageModelSessionEndorsesFallback;
|
|
}
|
|
|
|
/*!
|
|
Configures fallback details dynamically, overriding any defaults loaded from Info.plist.
|
|
@param serviceType the service type (e.g. @"ollama", @"groq", @"gemini", @"openrouter")
|
|
@param endpoint the network target URL
|
|
@param model the model identifier
|
|
@param apiKey the API key string
|
|
*/
|
|
+ (void)setFallbackServiceType:(CPString)serviceType endpoint:(CPString)endpoint model:(CPString)model apiKey:(CPString)apiKey
|
|
{
|
|
CPLanguageModelSessionFallbackServiceType = serviceType;
|
|
CPLanguageModelSessionFallbackEndpoint = endpoint;
|
|
CPLanguageModelSessionFallbackModel = model;
|
|
CPLanguageModelSessionFallbackAPIKey = apiKey;
|
|
}
|
|
|
|
/*!
|
|
Configures the CPUserDefaults key used to dynamically look up the API key.
|
|
@param keyName the user defaults key name containing the actual credentials
|
|
*/
|
|
+ (void)setFallbackAPIKeyUserDefaultKey:(CPString)keyName
|
|
{
|
|
CPLanguageModelSessionFallbackAPIKeyUserDefaultKey = keyName;
|
|
}
|
|
|
|
/*!
|
|
Gets the CPUserDefaults key name used to dynamically look up the API key.
|
|
@return the user defaults key name
|
|
*/
|
|
+ (CPString)fallbackAPIKeyUserDefaultKey
|
|
{
|
|
return CPLanguageModelSessionFallbackAPIKeyUserDefaultKey;
|
|
}
|
|
|
|
/*!
|
|
Initializes a language model session with specific system instructions.
|
|
@param instructions the system instructions or context prompt
|
|
@return the initialized session
|
|
*/
|
|
- (id)initWithInstructions:(CPString)instructions
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
_instructions = instructions;
|
|
_chromeSession = nil;
|
|
_fallbackServiceType = nil;
|
|
_fallbackEndpoint = nil;
|
|
_fallbackModel = nil;
|
|
_fallbackAPIKey = nil;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
/*!
|
|
Initializes a language model session with specific system instructions and an explicit programmatic API key.
|
|
@param instructions the system instructions or context prompt
|
|
@param apiKey the fallback API key to use specifically for this session
|
|
@return the initialized session
|
|
*/
|
|
- (id)initWithInstructions:(CPString)instructions apiKey:(CPString)apiKey
|
|
{
|
|
self = [self initWithInstructions:instructions];
|
|
|
|
if (self)
|
|
{
|
|
_fallbackAPIKey = apiKey;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
/*!
|
|
Initializes a language model session with instructions and explicit fallback settings.
|
|
@param instructions the system instructions or context prompt
|
|
@param options dictionary containing custom fallback configuration (e.g. @{ @"serviceType": ..., @"apiKey": ... })
|
|
@return the initialized session
|
|
*/
|
|
- (id)initWithInstructions:(CPString)instructions fallbackOptions:(CPDictionary)options
|
|
{
|
|
self = [self initWithInstructions:instructions];
|
|
|
|
if (self)
|
|
{
|
|
if (options)
|
|
{
|
|
_fallbackServiceType = [options objectForKey:@"serviceType"];
|
|
_fallbackEndpoint = [options objectForKey:@"endpoint"];
|
|
_fallbackModel = [options objectForKey:@"model"];
|
|
_fallbackAPIKey = [options objectForKey:@"apiKey"];
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
/*!
|
|
Sends a query prompt to the language model session.
|
|
@param prompt the query text to analyze
|
|
@param completionHandler a callback receiving the response string or a CPError instance
|
|
*/
|
|
- (void)respondToPrompt:(CPString)prompt options:(id)options completionHandler:(Function)completionHandler
|
|
{
|
|
// If the developer forced fallback, bypass native browser execution
|
|
if (CPLanguageModelSessionEndorsesFallback)
|
|
{
|
|
[self _executeRemoteFallbackWithPrompt:prompt options:options completionHandler:completionHandler];
|
|
return;
|
|
}
|
|
|
|
if (_chromeSession)
|
|
{
|
|
[self _executePrompt:prompt options:options completionHandler:completionHandler];
|
|
return;
|
|
}
|
|
|
|
var instructions = [self instructions];
|
|
|
|
[CPLanguageModelSession _getChromeFactoryWithCompletionHandler:function(factory, error) {
|
|
if (error) {
|
|
[self _executeRemoteFallbackWithPrompt:prompt options:options completionHandler:completionHandler];
|
|
return;
|
|
}
|
|
|
|
// Add the required expected input and output parameters
|
|
var sessionOptions = {
|
|
expectedInputs: [{ type: "text", languages: ["en"] }],
|
|
expectedOutputs: [{ type: "text", languages: ["en"] }]
|
|
};
|
|
if (instructions) {
|
|
sessionOptions.systemPrompt = instructions;
|
|
}
|
|
|
|
factory.create(sessionOptions).then(function(session) {
|
|
[self setChromeSession:session];
|
|
[self _executePrompt:prompt options:options completionHandler:completionHandler];
|
|
}).catch(function(err) {
|
|
[self _executeRemoteFallbackWithPrompt:prompt options:options completionHandler:completionHandler];
|
|
});
|
|
}];
|
|
}
|
|
|
|
/*!
|
|
Sends a query prompt and streams the response chunk-by-chunk for live UI rendering.
|
|
@param prompt the query text to analyze
|
|
@param chunkHandler a callback block executed as text increments are received
|
|
@param completionHandler a final callback block executed when generation completes
|
|
*/
|
|
- (void)respondToPrompt:(CPString)prompt
|
|
onChunkReceived:(Function)chunkHandler
|
|
completed:(Function)completionHandler
|
|
{
|
|
if (CPLanguageModelSessionEndorsesFallback)
|
|
{
|
|
[self _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) {
|
|
if (!err && chunkHandler)
|
|
chunkHandler(res);
|
|
completionHandler(res, err);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
|
|
}];
|
|
return;
|
|
}
|
|
|
|
if (_chromeSession)
|
|
{
|
|
[self _executePromptStreaming:prompt onChunkReceived:chunkHandler completed:completionHandler];
|
|
return;
|
|
}
|
|
|
|
var instructions = [self instructions];
|
|
|
|
[CPLanguageModelSession _getChromeFactoryWithCompletionHandler:function(factory, error) {
|
|
if (error) {
|
|
[self _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) {
|
|
if (!err && chunkHandler)
|
|
chunkHandler(res);
|
|
completionHandler(res, err);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
|
|
}];
|
|
return;
|
|
}
|
|
|
|
// Add the required expected input and output parameters
|
|
var options = {
|
|
expectedInputs: [{ type: "text", languages: ["en"] }],
|
|
expectedOutputs: [{ type: "text", languages: ["en"] }]
|
|
};
|
|
|
|
if (instructions)
|
|
options.systemPrompt = instructions;
|
|
|
|
factory.create(options).then(function(session) {
|
|
[self setChromeSession:session];
|
|
[self _executePromptStreaming:prompt onChunkReceived:chunkHandler completed:completionHandler];
|
|
}).catch(function(err)
|
|
{
|
|
[self _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) {
|
|
if (!err && chunkHandler)
|
|
chunkHandler(res);
|
|
|
|
completionHandler(res, err);
|
|
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
|
|
}];
|
|
});
|
|
}];
|
|
}
|
|
|
|
/*!
|
|
Closes the session and releases associated memory resources on-device.
|
|
*/
|
|
- (void)destroy
|
|
{
|
|
if (_chromeSession && typeof _chromeSession.destroy === "function")
|
|
{
|
|
_chromeSession.destroy();
|
|
_chromeSession = nil;
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - Private Helper Methods
|
|
|
|
+ (void)_getChromeFactoryWithCompletionHandler:(Function)completionHandler
|
|
{
|
|
if (typeof window === "undefined")
|
|
{
|
|
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain" code:-1 userInfo:[CPDictionary dictionaryWithObject:@"Execution environment is not a browser window." forKey:CPLocalizedDescriptionKey]];
|
|
completionHandler(nil, cpError);
|
|
return;
|
|
}
|
|
|
|
if (window.ai && window.ai.languageModel)
|
|
completionHandler(window.ai.languageModel, nil);
|
|
else if (window.LanguageModel)
|
|
completionHandler(window.LanguageModel, nil);
|
|
else
|
|
completionHandler(nil, [CPError errorWithDomain:@"CPLanguageModelErrorDomain" code:0 userInfo:nil]);
|
|
}
|
|
|
|
- (void)_executePrompt:(CPString)prompt options:(id)options completionHandler:(Function)completionHandler
|
|
{
|
|
var promptPromise = options ? _chromeSession.prompt(prompt, options) : _chromeSession.prompt(prompt);
|
|
|
|
promptPromise.then(function(result) {
|
|
completionHandler(result, nil);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
}).catch(function(err) {
|
|
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
|
code:2
|
|
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
|
completionHandler(nil, cpError);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
});
|
|
}
|
|
|
|
- (CPString)_resolvedFallbackServiceType
|
|
{
|
|
return _fallbackServiceType || CPLanguageModelSessionFallbackServiceType;
|
|
}
|
|
|
|
- (CPString)_resolvedFallbackEndpoint
|
|
{
|
|
return _fallbackEndpoint || CPLanguageModelSessionFallbackEndpoint;
|
|
}
|
|
|
|
- (CPString)_resolvedFallbackModel
|
|
{
|
|
return _fallbackModel || CPLanguageModelSessionFallbackModel;
|
|
}
|
|
|
|
- (CPString)_resolvedFallbackAPIKey
|
|
{
|
|
// 1. Session instance explicit key has highest priority
|
|
if (_fallbackAPIKey)
|
|
return _fallbackAPIKey;
|
|
|
|
// 2. Class fallback API key set programmatically takes second priority
|
|
if (CPLanguageModelSessionFallbackAPIKey)
|
|
return CPLanguageModelSessionFallbackAPIKey;
|
|
|
|
// 3. Dynamic lookup from standard user defaults takes final priority
|
|
if (CPLanguageModelSessionFallbackAPIKeyUserDefaultKey)
|
|
{
|
|
var defaults = [CPUserDefaults standardUserDefaults],
|
|
apiKey = [defaults objectForKey:CPLanguageModelSessionFallbackAPIKeyUserDefaultKey];
|
|
if (apiKey)
|
|
return apiKey;
|
|
}
|
|
|
|
return @"";
|
|
}
|
|
|
|
- (void)_executeRemoteFallbackWithPrompt:(CPString)prompt options:(id)options completionHandler:(Function)completionHandler
|
|
{
|
|
var systemPrompt = [self instructions],
|
|
serviceType = [self _resolvedFallbackServiceType],
|
|
endpoint = [self _resolvedFallbackEndpoint],
|
|
model = [self _resolvedFallbackModel],
|
|
apiKey = [self _resolvedFallbackAPIKey];
|
|
|
|
var reqUrl = @"",
|
|
headers = { "Content-Type": "application/json" },
|
|
payload = {};
|
|
|
|
if (serviceType === @"groq")
|
|
{
|
|
reqUrl = "https://api.groq.com/openai/v1/chat/completions";
|
|
headers["Authorization"] = "Bearer " + apiKey;
|
|
payload = {
|
|
"model": model,
|
|
"messages": [
|
|
{ "role": "system", "content": systemPrompt },
|
|
{ "role": "user", "content": prompt }
|
|
],
|
|
"temperature": 0
|
|
};
|
|
}
|
|
else if (serviceType === @"gemini")
|
|
{
|
|
reqUrl = "https://generativelanguage.googleapis.com/v1beta/models/" + model + ":generateContent?key=" + apiKey;
|
|
payload = {
|
|
"contents": [
|
|
{ "parts": [{ "text": systemPrompt + "\n\n" + prompt }] }
|
|
],
|
|
"generationConfig": { "temperature": 0 }
|
|
};
|
|
}
|
|
else if (serviceType === @"openrouter")
|
|
{
|
|
reqUrl = "https://openrouter.ai/api/v1/chat/completions";
|
|
headers["Authorization"] = "Bearer " + apiKey;
|
|
payload = {
|
|
"model": model,
|
|
"messages": [
|
|
{ "role": "system", "content": systemPrompt },
|
|
{ "role": "user", "content": prompt }
|
|
],
|
|
"temperature": 0
|
|
};
|
|
}
|
|
else
|
|
{
|
|
reqUrl = endpoint || "http://localhost:11434/api/generate";
|
|
payload = {
|
|
"model": model,
|
|
"prompt": systemPrompt + "\n\n" + prompt,
|
|
"stream": false,
|
|
"options": { "temperature": 0 }
|
|
};
|
|
}
|
|
|
|
fetch(reqUrl, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(function(response) {
|
|
if (!response.ok) {
|
|
throw new Error("HTTP error! Status: " + response.status);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(function(data) {
|
|
var responseText = "";
|
|
|
|
if (serviceType === "groq" || serviceType === "openrouter") {
|
|
responseText = (data.choices && data.choices[0] && data.choices[0].message) ? data.choices[0].message.content : "";
|
|
} else if (serviceType === "gemini") {
|
|
responseText = (data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts) ? data.candidates[0].content.parts[0].text : "";
|
|
} else {
|
|
responseText = data.response || "";
|
|
}
|
|
|
|
completionHandler(responseText, nil);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
})
|
|
.catch(function(err) {
|
|
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
|
code:4
|
|
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
|
completionHandler(nil, cpError);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
});
|
|
}
|
|
|
|
- (void)_executePromptStreaming:(CPString)prompt
|
|
onChunkReceived:(Function)chunkHandler
|
|
completed:(Function)completionHandler
|
|
{
|
|
var stream;
|
|
|
|
try {
|
|
stream = _chromeSession.promptStreaming(prompt);
|
|
} catch (err) {
|
|
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
|
code:3
|
|
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
|
completionHandler(nil, cpError);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
return;
|
|
}
|
|
|
|
(async function() {
|
|
var lastChunk = "";
|
|
|
|
try {
|
|
for await (const chunk of stream) {
|
|
lastChunk = chunk;
|
|
if (chunkHandler) {
|
|
chunkHandler(chunk);
|
|
}
|
|
}
|
|
if (completionHandler)
|
|
{
|
|
completionHandler(lastChunk, nil);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
|
|
}
|
|
} catch (err) {
|
|
if (completionHandler) {
|
|
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
|
code:2
|
|
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
|
completionHandler(nil, cpError);
|
|
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
|
|
@end
|