From 5fff34be914abb09e8305eafe4b97ea3cf8ae7b7 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 14:50:38 +0200 Subject: [PATCH] new: CPLanguageModel and manual test --- Foundation/CPLanguageModel.j | 605 ++++++++++++++++++ .../CPLanguageModelChatbot/AppController.j | 572 +++++++++++++++++ .../Manual/CPLanguageModelChatbot/Info.plist | 12 + Tests/Manual/CPLanguageModelChatbot/Jakefile | 94 +++ .../Resources/spinner.gif | Bin 0 -> 1434 bytes .../CPLanguageModelChatbot/index-debug.html | 103 +++ .../Manual/CPLanguageModelChatbot/index.html | 166 +++++ Tests/Manual/CPLanguageModelChatbot/main.j | 18 + 8 files changed, 1570 insertions(+) create mode 100644 Foundation/CPLanguageModel.j create mode 100644 Tests/Manual/CPLanguageModelChatbot/AppController.j create mode 100644 Tests/Manual/CPLanguageModelChatbot/Info.plist create mode 100644 Tests/Manual/CPLanguageModelChatbot/Jakefile create mode 100644 Tests/Manual/CPLanguageModelChatbot/Resources/spinner.gif create mode 100644 Tests/Manual/CPLanguageModelChatbot/index-debug.html create mode 100644 Tests/Manual/CPLanguageModelChatbot/index.html create mode 100644 Tests/Manual/CPLanguageModelChatbot/main.j diff --git a/Foundation/CPLanguageModel.j b/Foundation/CPLanguageModel.j new file mode 100644 index 000000000..d39254ffd --- /dev/null +++ b/Foundation/CPLanguageModel.j @@ -0,0 +1,605 @@ +/* + * 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 +@import +@import +@import +@import +@import + +// 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 Gemini Nano) + 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 Gemini 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: +
+    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)
+    
+*/ +@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 selfRef = self, + instructions = [self instructions]; + + [CPLanguageModelSession _getChromeFactoryWithCompletionHandler:function(factory, error) { + if (error) { + [selfRef _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) { + [selfRef setChromeSession:session]; + [selfRef _executePrompt:prompt options:options completionHandler:completionHandler]; + }).catch(function(err) { + [selfRef _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 selfRef = self, + instructions = [self instructions]; + + [CPLanguageModelSession _getChromeFactoryWithCompletionHandler:function(factory, error) { + if (error) { + [selfRef _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) { + if (!err && chunkHandler) + chunkHandler(res); + completionHandler(res, err); + }]; + 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) { + [selfRef setChromeSession:session]; + [selfRef _executePromptStreaming:prompt onChunkReceived:chunkHandler completed:completionHandler]; + }).catch(function(err) { + [selfRef _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) { + if (!err && chunkHandler) + chunkHandler(res); + completionHandler(res, err); + }]; + }); + }]; +} + +/*! + 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); + }).catch(function(err) { + var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain" + code:2 + userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]]; + completionHandler(nil, cpError); + }); +} + +- (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); + }) + .catch(function(err) { + var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain" + code:4 + userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]]; + completionHandler(nil, cpError); + }); +} + +- (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); + 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 diff --git a/Tests/Manual/CPLanguageModelChatbot/AppController.j b/Tests/Manual/CPLanguageModelChatbot/AppController.j new file mode 100644 index 000000000..f7553a496 --- /dev/null +++ b/Tests/Manual/CPLanguageModelChatbot/AppController.j @@ -0,0 +1,572 @@ +// AppController.j +// Manual test application for CPLanguageModelSession & CPSystemLanguageModel +// With custom SpeechBubbleBox drawing and editable System Prompt controls. +// + +@import +@import +@import + +// --- SUBCLASS: SPEECH BUBBLE VIEW --- +@implementation SpeechBubbleBox : CPView +{ + BOOL _isUser; + CPColor _bubbleColor; +} + +- (id)initWithFrame:(CGRect)aFrame isUser:(BOOL)isUser fillColor:(CPColor)aColor +{ + self = [super initWithFrame:aFrame]; + if (self) { + _isUser = isUser; + _bubbleColor = aColor; + [self setAutoresizingMask:CPViewWidthSizable]; + } + return self; +} + +- (void)drawRect:(CGRect)aRect +{ + var context = [[CPGraphicsContext currentContext] graphicsPort]; + var bounds = [self bounds]; + var w = CGRectGetWidth(bounds); + var h = CGRectGetHeight(bounds) - 10.0; // 10px spacing for the bottom triangle pointer + var r = 6.0; // Corner radius + var th = 10.0; // Tail height + + // --- 1. FILL PATH --- + CGContextBeginPath(context); + + // Top-left + CGContextMoveToPoint(context, r, 0); + + // Top edge + CGContextAddLineToPoint(context, w - r, 0); + CGContextAddArcToPoint(context, w, 0, w, r, r); + + // Right edge + CGContextAddLineToPoint(context, w, h - r); + CGContextAddArcToPoint(context, w, h, w - r, h, r); + + // Bottom edge with triangular tail (RHS vs LHS) + if (_isUser) { + CGContextAddLineToPoint(context, w - 21, h); + CGContextAddLineToPoint(context, w - 21, h + th); + CGContextAddLineToPoint(context, w - 35, h); + CGContextAddLineToPoint(context, r, h); + } else { + CGContextAddLineToPoint(context, 35, h); + CGContextAddLineToPoint(context, 21, h + th); + CGContextAddLineToPoint(context, 21, h); + CGContextAddLineToPoint(context, r, h); + } + + // Left edge + CGContextAddArcToPoint(context, 0, h, 0, h - r, r); + CGContextAddLineToPoint(context, 0, r); + CGContextAddArcToPoint(context, 0, 0, r, 0, r); + + CGContextClosePath(context); + + // Fill path + CGContextSetFillColor(context, _bubbleColor); + CGContextFillPath(context); + + // --- 2. OUTLINE PATH --- + CGContextBeginPath(context); + CGContextMoveToPoint(context, r, 0); + CGContextAddLineToPoint(context, w - r, 0); + CGContextAddArcToPoint(context, w, 0, w, r, r); + CGContextAddLineToPoint(context, w, h - r); + CGContextAddArcToPoint(context, w, h, w - r, h, r); + + if (_isUser) { + CGContextAddLineToPoint(context, w - 21, h); + CGContextAddLineToPoint(context, w - 21, h + th); + CGContextAddLineToPoint(context, w - 35, h); + CGContextAddLineToPoint(context, r, h); + } else { + CGContextAddLineToPoint(context, 35, h); + CGContextAddLineToPoint(context, 21, h + th); + CGContextAddLineToPoint(context, 21, h); + CGContextAddLineToPoint(context, r, h); + } + + CGContextAddArcToPoint(context, 0, h, 0, h - r, r); + CGContextAddLineToPoint(context, 0, r); + CGContextAddArcToPoint(context, 0, 0, r, 0, r); + CGContextClosePath(context); + + // Stroke outline + CGContextSetStrokeColor(context, [CPColor colorWithWhite:0.8 alpha:1.0]); + CGContextSetLineWidth(context, 1.0); + CGContextStrokePath(context); +} + +@end + + +// --- MAIN CONTROLLER --- +@implementation AppController : CPObject +{ + CPWindow _mainWindow; + CPScrollView _chatScrollView; + CPView _chatDocumentView; + CPTextField _chatInputField; + CPButton _chatSendButton; + CPButton _settingsButton; + CPCheckBox _forceFallbackCheckbox; + CPTextField _statusLabel; + CPTextView _systemPromptTextView; + + CPWindow _settingsWindow; + CPPopUpButton _servicePopUp; + CPTextField _endpointField; + CPTextField _modelField; + CPTextField _apiKeyField; + + CPLanguageModelSession _session; + float _currentChatY; + id _currentStreamingTextView; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + // Initialize default test settings in local user defaults + var defaults = [CPUserDefaults standardUserDefaults]; + var defaultSettings = [CPDictionary dictionaryWithObjects:[ + @"ollama", + @"http://localhost:11434/api/generate", + @"gemma4:e4b", + @"" + ] forKeys:[ + @"LLMTestServiceType", + @"LLMTestEndpoint", + @"LLMTestModel", + @"LLMTestAPIKey" + ]]; + [defaults registerDefaults:defaultSettings]; + + // Read values to apply fallback routing to CPLanguageModelSession + var activeService = [defaults objectForKey:@"LLMTestServiceType"], + endpoint = [defaults objectForKey:@"LLMTestEndpoint"], + model = [defaults objectForKey:@"LLMTestModel"], + apiKey = [defaults objectForKey:@"LLMTestAPIKey"]; + + [CPLanguageModelSession setFallbackServiceType:activeService + endpoint:endpoint + model:model + apiKey:apiKey]; + + // Main manual test window setup + _mainWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 800, 650) + styleMask:CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask]; + [_mainWindow setTitle:@"CPLanguageModel Manual Test Tool"]; + [_mainWindow center]; + + var contentView = [_mainWindow contentView]; + var bounds = [contentView bounds]; + + // --- TOP CONTROL PANEL (Height 135px) --- + var topBar = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(bounds), 135)]; + [topBar setAutoresizingMask:CPViewWidthSizable | CPViewMaxYMargin]; + [topBar setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]]; + [contentView addSubview:topBar]; + + _statusLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 12, 300, 20)]; + [_statusLabel setStringValue:@"Checking capability..."]; + [_statusLabel setFont:[CPFont systemFontOfSize:12.0]]; + [topBar addSubview:_statusLabel]; + + _forceFallbackCheckbox = [[CPCheckBox alloc] initWithFrame:CGRectMake(330, 12, 140, 20)]; + [_forceFallbackCheckbox setTitle:@"Force Fallback"]; + [_forceFallbackCheckbox setTarget:self]; + [_forceFallbackCheckbox setAction:@selector(toggleForceFallback:)]; + [topBar addSubview:_forceFallbackCheckbox]; + + _settingsButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bounds) - 210, 9, 90, 26)]; + [_settingsButton setTitle:@"Settings..."]; + [_settingsButton setAutoresizingMask:CPViewMinXMargin]; + [_settingsButton setTarget:self]; + [_settingsButton setAction:@selector(openSettingsSheet:)]; + [topBar addSubview:_settingsButton]; + + var clearButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bounds) - 110, 9, 95, 26)]; + [clearButton setTitle:@"Clear Chat"]; + [clearButton setAutoresizingMask:CPViewMinXMargin]; + [clearButton setTarget:self]; + [clearButton setAction:@selector(clearChatAction:)]; + [topBar addSubview:clearButton]; + + // System Prompt Header & Field [1] + var promptLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 42, 300, 18)]; + [promptLabel setStringValue:@"System Instructions (Persona):"]; + [promptLabel setFont:[CPFont boldSystemFontOfSize:11.0]]; + [promptLabel setTextColor:[CPColor darkGrayColor]]; + [topBar addSubview:promptLabel]; + + var promptScroll = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 62, CGRectGetWidth(bounds) - 30, 60)]; + [promptScroll setAutoresizingMask:CPViewWidthSizable]; + [promptScroll setAutohidesScrollers:YES]; + + _systemPromptTextView = [[CPTextView alloc] initWithFrame:[promptScroll bounds]]; + [_systemPromptTextView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_systemPromptTextView setEditable:YES]; + [_systemPromptTextView setFont:[CPFont systemFontOfSize:11.0]]; + // Default instruction preset [1] + [_systemPromptTextView setString:@"You are a helpful, concise testing assistant. You structure your explanations clearly using lists where appropriate."]; + + [promptScroll setDocumentView:_systemPromptTextView]; + [topBar addSubview:promptScroll]; + + // --- SCROLLING CHAT CONTAINER --- + var scrollHeight = CGRectGetHeight(bounds) - 195; + _chatScrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 135, CGRectGetWidth(bounds), scrollHeight)]; + [_chatScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_chatScrollView setAutohidesScrollers:YES]; + [_chatScrollView setHasHorizontalScroller:NO]; + [_chatScrollView setBackgroundColor:[CPColor whiteColor]]; + + _chatDocumentView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([_chatScrollView bounds]), scrollHeight)]; + [_chatDocumentView setAutoresizingMask:CPViewWidthSizable]; + [_chatScrollView setDocumentView:_chatDocumentView]; + [contentView addSubview:_chatScrollView]; + + // --- INPUT CONTAINER --- + var bottomBarY = CGRectGetHeight(bounds) - 60; + var bottomBar = [[CPView alloc] initWithFrame:CGRectMake(0, bottomBarY, CGRectGetWidth(bounds), 60)]; + [bottomBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; + [bottomBar setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]]; + [contentView addSubview:bottomBar]; + + _chatInputField = [[CPTextField alloc] initWithFrame:CGRectMake(15, 13, CGRectGetWidth(bounds) - 130, 34)]; + [_chatInputField setAutoresizingMask:CPViewWidthSizable]; + [_chatInputField setEditable:YES]; + [_chatInputField setBezeled:YES]; + [_chatInputField setPlaceholderString:@"Type a prompt and press Enter..."]; + [_chatInputField setTarget:self]; + [_chatInputField setAction:@selector(submitPromptAction:)]; + [bottomBar addSubview:_chatInputField]; + + _chatSendButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bounds) - 105, 13, 90, 34)]; + [_chatSendButton setTitle:@"Send"]; + [_chatSendButton setAutoresizingMask:CPViewMinXMargin]; + [_chatSendButton setTarget:self]; + [_chatSendButton setAction:@selector(submitPromptAction:)]; + [bottomBar addSubview:_chatSendButton]; + + [_mainWindow orderFront:self]; + + [self checkModelSupport]; + [self resetSession]; +} + +- (void)checkModelSupport +{ + var systemModel = [CPSystemLanguageModel defaultModel]; + [_statusLabel setStringValue:@"Checking capability..."]; + + var selfRef = self; + [systemModel supportsLocaleWithCompletionHandler:function(supported) { + if (supported) { + [selfRef._statusLabel setStringValue:@"On-Device LLM: Supported"]; + } else { + [selfRef._statusLabel setStringValue:@"On-Device LLM: Not available. Using fallback."]; + } + }]; +} + +- (void)toggleForceFallback:(id)sender +{ + var force = [sender state] === CPOnState; + [CPLanguageModelSession setEndorsesFallback:force]; + [_statusLabel setStringValue:(force ? @"Fallback forced programmatically." : @"Prioritizing on-device model.")]; +} + +- (void)clearChatAction:(id)sender +{ + [self resetSession]; +} + +- (void)resetSession +{ + _currentChatY = 15; + _currentStreamingTextView = nil; + + if (_session) { + [_session destroy]; + } + + // Read the custom instructions from the text field upon starting a fresh session [1] + var instructions = [_systemPromptTextView string] || @""; + _session = [[CPLanguageModelSession alloc] initWithInstructions:instructions]; + + [[_chatDocumentView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [_chatDocumentView setFrameSize:CGSizeMake(CGRectGetWidth([_chatScrollView bounds]), CGRectGetHeight([_chatScrollView bounds]))]; + + [self appendMessage:@"Session initialized.\n\nEnter a query below to begin." isUser:NO]; +} + +// Append a formatted message using SpeechBubbleBox and sizeToFit calculation +- (void)appendMessage:(CPString)text isUser:(BOOL)isUser +{ + var docWidth = CGRectGetWidth([_chatScrollView bounds]) - 50; + + var textView = [[CPTextView alloc] initWithFrame:CGRectMake(15, 10, docWidth - 30, 20)]; + [textView insertText:text]; + [textView setTextColor:[CPColor blackColor]]; + [textView setFont:[CPFont systemFontOfSize:11.0]]; + [textView setEditable:YES]; + [textView setRichText:NO]; + [textView setBackgroundColor:[CPColor clearColor]]; + [textView setAutoresizingMask:CPViewWidthSizable]; + + [textView sizeToFit]; + var textHeight = CGRectGetHeight([textView frame]); + + var cardHeight = textHeight + 20; // 10px spacing top/bottom + var bubbleHeight = cardHeight + 10; // Extra 10px spacing for the bottom triangle pointer [1] + + var fillColor = isUser ? [CPColor colorWithRed:0.90 green:0.93 blue:1.0 alpha:1.0] : [CPColor colorWithWhite:0.96 alpha:1.0]; + var cardBox = [[SpeechBubbleBox alloc] initWithFrame:CGRectMake(15, _currentChatY, docWidth, bubbleHeight) + isUser:isUser + fillColor:fillColor]; + + [cardBox addSubview:textView]; + [_chatDocumentView addSubview:cardBox]; + + if (!isUser) { + _currentStreamingTextView = textView; + } + + _currentChatY += bubbleHeight + 15; // 15px gap between consecutive messages + [_chatDocumentView setFrameSize:CGSizeMake(CGRectGetWidth([_chatScrollView bounds]), _currentChatY + 20)]; + + var boundsHeight = CGRectGetHeight([_chatScrollView bounds]); + if (_currentChatY > boundsHeight) { + [[_chatScrollView contentView] scrollToPoint:CGPointMake(0, _currentChatY - boundsHeight + 40)]; + } +} + +// Dynamically resize the SpeechBubbleBox to match updated streaming text changes +- (void)updateStreamingMessage:(CPString)newText +{ + if (!_currentStreamingTextView) + return; + + if (newText == "\n\n") + newText = ' '; + + [_currentStreamingTextView setString:[_currentStreamingTextView._textStorage string] + newText]; + //[_currentStreamingTextView sizeToFit]; + + var textHeight = CGRectGetHeight([_currentStreamingTextView frame]); + var cardHeight = textHeight + 20; + var bubbleHeight = cardHeight + 10; + + var container = [_currentStreamingTextView superview]; // Resolves the SpeechBubbleBox + var oldBubbleHeight = CGRectGetHeight([container frame]); + + [container setFrameSize:CGSizeMake(CGRectGetWidth([container frame]), bubbleHeight)]; + [_currentStreamingTextView setFrameSize:CGSizeMake(CGRectGetWidth([_currentStreamingTextView frame]), textHeight)]; + [container setNeedsDisplay:YES]; // Instructs the canvas to clear and redraw paths + [_currentStreamingTextView setNeedsDisplay:YES]; // Instructs the canvas to clear and redraw paths + + var diffHeight = bubbleHeight - oldBubbleHeight; + _currentChatY += diffHeight; + + [_chatDocumentView setFrameSize:CGSizeMake(CGRectGetWidth([_chatScrollView bounds]), _currentChatY + 20)]; + + var boundsHeight = CGRectGetHeight([_chatScrollView bounds]); + if (_currentChatY > boundsHeight) { + [[_chatScrollView contentView] scrollToPoint:CGPointMake(0, _currentChatY - boundsHeight + 40)]; + } +} + +- (void)submitPromptAction:(id)sender +{ + var prompt = [_chatInputField stringValue]; + if (!prompt || [prompt stringByTrimmingWhitespace] === @"") { + return; + } + + [_chatInputField setStringValue:@""]; + [_chatInputField setEnabled:NO]; + [_chatSendButton setEnabled:NO]; + + [self appendMessage:prompt isUser:YES]; + [self appendMessage:@"Generating response..." isUser:NO]; + + var selfRef = self; + + [_session respondToPrompt:prompt + onChunkReceived:function(chunk) { + [selfRef updateStreamingMessage:chunk]; + } + completed:function(finalText, error) { + [selfRef._chatInputField setEnabled:YES]; + [selfRef._chatInputField becomeFirstResponder]; + [selfRef._chatSendButton setEnabled:YES]; + + if (error) { + [selfRef updateStreamingMessage:@"Error: " + [error localizedDescription]]; + } else { + [selfRef updateStreamingMessage:finalText]; + } + }]; +} + +// --- CONFIGURATION POPUP SHEETS --- + +- (void)openSettingsSheet:(id)sender +{ + if (!_settingsWindow) + { + _settingsWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 420, 260) + styleMask:CPTitledWindowMask | CPClosableWindowMask]; + [_settingsWindow setTitle:@"Fallback Settings"]; + + var sheetContentView = [_settingsWindow contentView]; + var sheetBounds = [sheetContentView bounds]; + + var serviceLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 25, 110, 20)]; + [serviceLabel setStringValue:@"Service Type:"]; + [serviceLabel setFont:[CPFont systemFontOfSize:12.0]]; + [serviceLabel setAlignment:CPRightTextAlignment]; + [sheetContentView addSubview:serviceLabel]; + + _servicePopUp = [[CPPopUpButton alloc] initWithFrame:CGRectMake(135, 22, 180, 26) pullsDown:NO]; + [_servicePopUp addItemWithTitle:@"Ollama (Local)"]; + [[_servicePopUp lastItem] setRepresentedObject:@"ollama"]; + [_servicePopUp addItemWithTitle:@"Groq API"]; + [[_servicePopUp lastItem] setRepresentedObject:@"groq"]; + [_servicePopUp addItemWithTitle:@"Google Gemini"]; + [[_servicePopUp lastItem] setRepresentedObject:@"gemini"]; + [_servicePopUp addItemWithTitle:@"OpenRouter"]; + [[_servicePopUp lastItem] setRepresentedObject:@"openrouter"]; + [_servicePopUp setTarget:self]; + [_servicePopUp setAction:@selector(serviceTypeDidChange:)]; + [sheetContentView addSubview:_servicePopUp]; + + var endpointLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 65, 110, 20)]; + [endpointLabel setStringValue:@"Endpoint URL:"]; + [endpointLabel setFont:[CPFont systemFontOfSize:12.0]]; + [endpointLabel setAlignment:CPRightTextAlignment]; + [sheetContentView addSubview:endpointLabel]; + + _endpointField = [[CPTextField alloc] initWithFrame:CGRectMake(135, 62, CGRectGetWidth(sheetBounds) - 155, 24)]; + [_endpointField setEditable:YES]; + [_endpointField setBezeled:YES]; + [_endpointField setFont:[CPFont systemFontOfSize:12.0]]; + [sheetContentView addSubview:_endpointField]; + + var modelLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 105, 110, 20)]; + [modelLabel setStringValue:@"Model Name:"]; + [modelLabel setFont:[CPFont systemFontOfSize:12.0]]; + [modelLabel setAlignment:CPRightTextAlignment]; + [sheetContentView addSubview:modelLabel]; + + _modelField = [[CPTextField alloc] initWithFrame:CGRectMake(135, 102, CGRectGetWidth(sheetBounds) - 155, 24)]; + [_modelField setEditable:YES]; + [_modelField setBezeled:YES]; + [_modelField setFont:[CPFont systemFontOfSize:12.0]]; + [sheetContentView addSubview:_modelField]; + + var apiKeyLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 145, 110, 20)]; + [apiKeyLabel setStringValue:@"API Key:"]; + [apiKeyLabel setFont:[CPFont systemFontOfSize:12.0]]; + [apiKeyLabel setAlignment:CPRightTextAlignment]; + [sheetContentView addSubview:apiKeyLabel]; + + _apiKeyField = [[CPTextField alloc] initWithFrame:CGRectMake(135, 142, CGRectGetWidth(sheetBounds) - 155, 24)]; + [_apiKeyField setEditable:YES]; + [_apiKeyField setBezeled:YES]; + [_apiKeyField setSecure:YES]; + [_apiKeyField setFont:[CPFont systemFontOfSize:12.0]]; + [sheetContentView addSubview:_apiKeyField]; + + var btnY = CGRectGetHeight(sheetBounds) - 45; + + var cancelBtn = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(sheetBounds) - 205, btnY, 90, 26)]; + [cancelBtn setTitle:@"Cancel"]; + [cancelBtn setTarget:self]; + [cancelBtn setAction:@selector(closeSettingsSheet:)]; + [sheetContentView addSubview:cancelBtn]; + + var saveBtn = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(sheetBounds) - 105, btnY, 90, 26)]; + [saveBtn setTitle:@"Save"]; + [saveBtn setTarget:self]; + [saveBtn setAction:@selector(saveSettings:)]; + [sheetContentView addSubview:saveBtn]; + } + + var defaults = [CPUserDefaults standardUserDefaults]; + var activeService = [defaults objectForKey:@"LLMTestServiceType"] || @"ollama"; + + if (activeService === @"ollama") [_servicePopUp selectItemAtIndex:0]; + else if (activeService === @"groq") [_servicePopUp selectItemAtIndex:1]; + else if (activeService === @"gemini") [_servicePopUp selectItemAtIndex:2]; + else if (activeService === @"openrouter") [_servicePopUp selectItemAtIndex:3]; + + [_endpointField setStringValue:[defaults objectForKey:@"LLMTestEndpoint"] || @"http://localhost:11434/api/generate"]; + [_modelField setStringValue:[defaults objectForKey:@"LLMTestModel"] || @"gemma4:e4b"]; + [_apiKeyField setStringValue:[defaults objectForKey:@"LLMTestAPIKey"] || @""]; + + [self updateFieldsForService:activeService]; + + [CPApp beginSheet:_settingsWindow + modalForWindow:_mainWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; +} + +- (void)updateFieldsForService:(CPString)serviceType +{ + if (serviceType === @"ollama") { + [_endpointField setEnabled:YES]; + [_apiKeyField setEnabled:NO]; + [_apiKeyField setPlaceholderString:@"Not required"]; + } else { + [_endpointField setEnabled:NO]; + [_endpointField setPlaceholderString:@"Default platform endpoint used"]; + [_apiKeyField setEnabled:YES]; + [_apiKeyField setPlaceholderString:@"API Token values"]; + } +} + +- (void)serviceTypeDidChange:(id)sender +{ + var newService = [[_servicePopUp selectedItem] representedObject]; + [self updateFieldsForService:newService]; +} + +- (void)closeSettingsSheet:(id)sender +{ + [CPApp endSheet:_settingsWindow]; + [_settingsWindow orderOut:self]; +} + +- (void)saveSettings:(id)sender +{ + var defaults = [CPUserDefaults standardUserDefaults]; + var activeService = [[_servicePopUp selectedItem] representedObject] || @"ollama"; + var endpoint = [_endpointField stringValue]; + var model = [_modelField stringValue]; + var apiKey = [_apiKeyField stringValue]; + + [defaults setObject:activeService forKey:@"LLMTestServiceType"]; + [defaults setObject:endpoint forKey:@"LLMTestEndpoint"]; + [defaults setObject:model forKey:@"LLMTestModel"]; + [defaults setObject:apiKey forKey:@"LLMTestAPIKey"]; + + [CPLanguageModelSession setFallbackServiceType:activeService + endpoint:endpoint + model:model + apiKey:apiKey]; + + [self closeSettingsSheet:sender]; + [_statusLabel setStringValue:@"Fallback settings updated."]; +} + +@end diff --git a/Tests/Manual/CPLanguageModelChatbot/Info.plist b/Tests/Manual/CPLanguageModelChatbot/Info.plist new file mode 100644 index 000000000..ca0ffdd38 --- /dev/null +++ b/Tests/Manual/CPLanguageModelChatbot/Info.plist @@ -0,0 +1,12 @@ + + + + + CPApplicationDelegateClass + AppController + CPBundleName + CPLanguageModelChatbot + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/CPLanguageModelChatbot/Jakefile b/Tests/Manual/CPLanguageModelChatbot/Jakefile new file mode 100644 index 000000000..bd57b7a0d --- /dev/null +++ b/Tests/Manual/CPLanguageModelChatbot/Jakefile @@ -0,0 +1,94 @@ +/* + * Jakefile + * CPLevelIndicator + * + * Created by Alexander Ljungberg on May 28, 2011. + * Copyright 2011, WireLoad All rights reserved. + */ + +var ENV = require("system").env, + FILE = require("file"), + JAKE = require("jake"), + task = JAKE.task, + FileList = JAKE.FileList, + app = require("cappuccino/jake").app, + configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug", + OS = require("os"); + +app ("CPLevelIndicator", function(task) +{ + task.setBuildIntermediatesPath(FILE.join("Build", "CPLevelIndicator.build", configuration)); + task.setBuildPath(FILE.join("Build", configuration)); + + task.setProductName("CPLevelIndicator"); + task.setIdentifier("com.yourcompany.CPLevelIndicator"); + task.setVersion("1.0"); + task.setAuthor("WireLoad"); + task.setEmail("feedback @nospam@ yourcompany.com"); + task.setSummary("CPLevelIndicator"); + task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**"))); + task.setResources(new FileList("Resources/**")); + task.setIndexFilePath("index.html"); + task.setInfoPlistPath("Info.plist"); + task.setNib2CibFlags("-R Resources/"); + + if (configuration === "Debug") + task.setCompilerFlags("-DDEBUG -g"); + else + task.setCompilerFlags("-O"); +}); + +task ("default", ["CPLevelIndicator"], function() +{ + printResults(configuration); +}); + +task ("build", ["default"]); + +task ("debug", function() +{ + ENV["CONFIGURATION"] = "Debug"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("release", function() +{ + ENV["CONFIGURATION"] = "Release"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("run", ["debug"], function() +{ + OS.system(["open", FILE.join("Build", "Debug", "CPLevelIndicator", "index.html")]); +}); + +task ("run-release", ["release"], function() +{ + OS.system(["open", FILE.join("Build", "Release", "CPLevelIndicator", "index.html")]); +}); + +task ("deploy", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Deployment", "CPLevelIndicator")); + OS.system(["press", "-f", FILE.join("Build", "Release", "CPLevelIndicator"), FILE.join("Build", "Deployment", "CPLevelIndicator")]); + printResults("Deployment") +}); + +task ("desktop", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Desktop", "CPLevelIndicator")); + require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPLevelIndicator"), FILE.join("Build", "Desktop", "CPLevelIndicator", "CPLevelIndicator.app")); + printResults("Desktop") +}); + +task ("run-desktop", ["desktop"], function() +{ + OS.system([FILE.join("Build", "Desktop", "CPLevelIndicator", "CPLevelIndicator.app", "Contents", "MacOS", "NativeHost"), "-i"]); +}); + +function printResults(configuration) +{ + print("----------------------------"); + print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPLevelIndicator")); + print("----------------------------"); +} diff --git a/Tests/Manual/CPLanguageModelChatbot/Resources/spinner.gif b/Tests/Manual/CPLanguageModelChatbot/Resources/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..a5e705f6cbdf914e5e714c35a8dfef807f19e3c2 GIT binary patch literal 1434 zcmZvbdrVVj7{t$-UNOu86#VHu^|s&3rWfwHBbPG_8{SrlB{Tv1uvvj4t6zP!)#d!Ogc zP^62J^$0+~?-ZcXXpBaq%jFsw8L`=H2?+@R02Yg-*Xw;gACBV^i3CMahr>Z667S!? zk3LDe>VLEsU;sWo$Py~o!gWuaIAnaG3Sv``&tvc+8DJO!| zt4fcbQ8tW}a&xZfgtQ9BnFYLvGG|PvCNlg&uh@Q^w9Pz}+I^hCj`vu9JQADPqdkyk zR40xycD9geUgJu1e;$L`Fpa)?Wl-2&6hO@U*KrPqK(I1H=1h?2fce}6GhhP1oBZC# z1e@gt-qFa6lZrl%s1>bdxg*W)Ebt__O(4sj6(*2X@ciUClwHH#U(NRM7XAjS z+scDZ32J*PCoslsgS~#ZlCCGo`r>S6F)Ghw5wVlqHioFaw?ucD(XoLJ0j;28oPoPV z9A}dR$0SKn>uExfkl#j09o;v$BZ909R=*p{ATNq8BJW;YduX2QMOU7$a$_L5e!G^g zpbkx5G4&FZ%7m14rTN}5YB(;x7$5XOc_hf3E|Hw$TVg)P#qf~}nOn03uqsp>UG>vi zWThIoO)-1Q#@u#O;fMC#WAf@Xj70-0g9YZ;dA$HH1fW1q=9-c>>_yA0S$7M*5?}vi z)8MU`Q%P!7sEBBnd$DrKgFQ2?O%6LpdaC%F8Pn-)EC2t=j~ zoaLamla$FX!GQqWi!%s>sj-#5SJX0IF!TP=r21Z}s)k#btN0?=I7uD9C)Gb$fZ#sm zaVq7g`LwZ%PfNpN^_N-IGLHj@AV`s#4&va7_{Hw63G6BkSGXHdogTZ%QOiRb bDpZ@qYcJKM>x%b%*HX74c8MnqfHi*u-bC?g literal 0 HcmV?d00001 diff --git a/Tests/Manual/CPLanguageModelChatbot/index-debug.html b/Tests/Manual/CPLanguageModelChatbot/index-debug.html new file mode 100644 index 000000000..1097bcf1b --- /dev/null +++ b/Tests/Manual/CPLanguageModelChatbot/index-debug.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + CPLevelIndicator + + + + + + + + + + + + + + +
+
+ + + +
+
+ + + diff --git a/Tests/Manual/CPLanguageModelChatbot/index.html b/Tests/Manual/CPLanguageModelChatbot/index.html new file mode 100644 index 000000000..5a58b20d8 --- /dev/null +++ b/Tests/Manual/CPLanguageModelChatbot/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPLanguageModelChatbot/main.j b/Tests/Manual/CPLanguageModelChatbot/main.j new file mode 100644 index 000000000..7a956f1f5 --- /dev/null +++ b/Tests/Manual/CPLanguageModelChatbot/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * CPLevelIndicator + * + * Created by Alexander Ljungberg on May 28, 2011. + * Copyright 2011, WireLoad All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +}