The LDK includes several example projects that may be useful to look at to understand how to use various aspects of the SDK.
Below you can find both code snippets and sample Loop code.
You can find all of our examples here. Alternatively, they can also be found in the LDK itself under the /examples/ folder.
Code Snippets
To speed up your Loop development, here are code snippets of functionality that our internal development team uses frequently in our own Loops.
1. Reading and validating data from Excel
The following code snippets are examples from a Loop that reads data from each Excel file as its backend, and displays the content in easily navigable Whispers for its frontend.
Code Snippet Purpose: This Loop must first be configured to read data from an Excel sheet at a user-designated filepath. This snippet reads data from the Excel filepath and returns a workable object for subsequent data validation.
/** * Read the given Excel file and return a valid Workbook * @param fileData - object containing file path of the excel file in users computer */static async readInternal(fileData: KnowledgeAreaConfig): Promise<Result<Workbook>> { try {/* * Uses the filesystem aptitude in @oliveai/ldk to read the file path and convert * the excel contents into an object 'workbook' (e.g. { worksheets: Worksheet[] }) * * export interface Worksheet { * hidden: boolean; * hiddenColumns: number[]; * hiddenRows: number[]; * name: string; * rows: Row[]; * } */constbytes=awaitfilesystem.readFile(fileData.path);constworkbook=awaitdocument.xlsxDecode(bytes);return { isOk:true, data: workbook }; } catch (err) { console.error(err); }return { isOk:false, error:'ReadFileError' }; }
Code Snippet Purpose: To validate the Excel data object and ensure it contains the worksheet of interest that will serve as the Loop’s backend.
exportenumErrorType { ExcelSheetValidation ='Excel Sheet Validation', ExcelSheetDataValidation ='Excel Sheet Data Validation',}exporttypeDataError=Partial<Record<ErrorType,string>>;/*** Reads an Excel workbook and looks for a specific name of worksheet, if it doesn't* contain a matching name it adds to an array of type DataError, otherwise it reads and* parses the worksheet with the custom made Sheet class* @param workbook - Entire Excel file*/exportconstreadWorkbook= (workbook:Workbook): [Array<Record<string,string>>,DataError[]] => {constrows:Array<Record<string,string>> = [];consterrors:DataError[] = [];/* * Looks for any worksheets in the excel file with the tab name 'Content' (CONTENT is * a constant) * workbook.worksheets found in the @oliveai/ldk */constcontentSheets=workbook.worksheets.filter((sheet) =>sheet.name ===CONTENT);if (!contentSheets.length) {errors.push({'Excel Sheet Validation':'Cannot find Content tab. Please ensure the tab is named according to the template file and still exists.', }); } else {contentSheets.forEach((sheet) => {// Sheet class seen in the following gist, takes the worksheet name and all the filled rows constsheetData=newSheet(sheet.name,sheet.rows);rows.push(...sheetData.data);errors.push(...sheetData.errors); }); }};
Code Snippet Purpose: To validate the contents of the worksheet to ensure they match this Loop’s data model and can be translated into the Whisper format.
exportclassSheet {readonly data:Record<string,string>[] = [];readonly errors:DataError[] = [];/** * Reads and parses the content worksheet in the excel file and validates the format * * @param name - readonly * @param rows - Excel content tab Rows */constructor(readonly name:string, rows:Row[]) {constheaderRow= rows[0].cells;constdataRows=rows.slice(2);/* * Runs a utility method verifyHeaders that checks the first row of cells in the * worksheet */constheaderError=verifyHeaders(headerRow);if (headerError) {this.errors.push(headerError); }// dataRows = all the rows in worksheet from row 3 downif (dataRows.length) {// creates an array of formatted rowObjects, each one to be validated by verifyRequiredData You are all set. Is there anything elseI can do for you?\""}]}dataRows.reduce((acc, row, counter) => {// one row in dataRows is formatted like so {"cells":[{"value":"cell1 data"},{"value":"cell2 data"},{"value":"cell3 data"}]}constrowObject=row.cells.reduce((topic:Record<string,string>, { value }, i) => {return { ...topic, [headerRow[i].value]: value }; }, {});// +3 to offset zero index and skip first two rows// You can do some validation here on the contents of the objects in the cellthis.errors.push(...verifyRequiredData(rowObject, counter +3));acc.push(rowObject);return acc; },this.data); } else { this.errors.push({'Excel Sheet Validation':'The [Content] tab of your file is empty. Please return to the file and ensure it is populated with content.' }); } }}
2. Utilizing the Vault Aptitude
The following code snippets are examples from the Healthcare Newsfeed Olive Original Loop that pulls relevant news from an API and surfaces it in Daily Digest Whispers, categorized by healthcare news topic.
Code Snippet Purpose: All Loops need a starting point for you to interact with them. This snippet uses the ui.loopOpenHandler method so that when end users click on this Loop in the dropdown menu, either a DigestionWhisper or SettingsWhisper will appear depending on existing settings in the vault.
import { ui, vault } from'@oliveai/ldk';import vaultKeys from'../vault/keys';import { DigestWhisper, SettingsWhisper } from'../../whispers';/** * openHandler found in the UI aptitude of @oliveai/ldk, Registers a handler function * for the Olive Helps Loop Open Button * In this case depending on settings found in the vault aptitude, will conditionally * open one of these whisper classes */exportconsthandler=async () => {if (awaitvault.exists(vaultKeys.settings)) {newDigestWhisper().show(); } else {newSettingsWhisper().show(); }};exportdefault {start: () =>ui.loopOpenHandler(handler),};
Code Snippet Purpose: End users can configure the Healthcare Newsfeed Loop to deliver the Daily Digest at a preferred time (ie. 12pm for lunch break), this code snippet shows a CRON job to determine if it’s time for the Loop to trigger the Daily Digest Whisper.
/** * Calls the above openHandler() function, is used to conditionally determine what * whisper to show after * refreshing and checking the vault every 5 minutes * An example of how to make a make-shift CRON job */construn=async () => {openHandler.start();constcheckDeliveryTime=async () => {constnow=newDate();constdailyDigestLastTriggeredBeforeToday=!(awaitvault.exists(vaultKeys.dailyDigestLastTriggered)) ||differenceInCalendarDays(newDate(awaitvault.read(vaultKeys.dailyDigestLastTriggered)), now ) <0;if ((awaitvault.exists(vaultKeys.settings)) && dailyDigestLastTriggeredBeforeToday) {constdeliveryTimeHasPassed=isBefore(parse((awaitgetStoredSettings()).deliveryTime,'h:mm a', now), now );if (deliveryTimeHasPassed) {newDigestWhisper().show();awaitvault.write(vaultKeys.dailyDigestLastTriggered,now.toString()); } } };// Check on launch then check every 5 minutes.// This timer should run regardless of what happened on launch in case the user left// OH running overnightawaitcheckDeliveryTime();setInterval(checkDeliveryTime,5*60*1000);};
Code Snippet Purpose: To help end users configure the Healthcare Newsfeed Loop to subscribe to particular news topics of interest (ie. Public Health, Digital Health, etc.). This method stores preferences in the vault.
vaultKeys = { settings:'settings', dailyDigestLastTriggered:'dailyDigestLastTriggered',};/** * The Vault aptitude allows Loops to retrieve and store strings in the system's secure * storage * This example shows loop settings configurations being set retrieved and set, so * users do not * have to re-fill out the options. * * Saves preferences for different new topics, allows users to have a personalized newsfeed */exportconstgetStoredSettings=async () => {conststoredSettingsExist=awaitvault.exists(vaultKeys.settings);if (!storedSettingsExist) {console.log('Stored settings do not exist. Using defaults.');returngetDefaultSettings(); }constvaultValue=awaitvault.read(vaultKeys.settings);conststoredSettings=JSON.parse(vaultValue);if (!isSettings(storedSettings)) {console.error('Stored settings are invalid. Using defaults. Settings read from Vault: %s', vaultValue );returngetDefaultSettings(); }console.log('Stored settings loaded from Vault.');return storedSettings asSettings;};exportconstsaveSettings=async (settings:Settings) => {awaitvault.write(vaultKeys.settings,JSON.stringify(settings));console.log('Settings saved successfully.');};
3. Creating Simple Whispers
The following code snippets are examples of how we create simple Whispers in all our Olive Original Loops.
Code Snippet Purpose: Creating simple default Whispers that don’t need to be updatable.
import { whisper } from'@oliveai/ldk';import { stripIndent } from'common-tags';exportconstonClose= (err?:Error) => {if (err) {console.error('There was an error closing Export Default whisper', err); }console.log('Export Default whisper closed');};/** * + More desired Typescript style * + Allows for a consistent WhisperName.show() format * + Default export can be named however the developer wants * + Default export does not include test-only exports which can be explicitly imported * separately * - Does not support updatable in a clean and easy-to-test way */exportdefault {show:async () => {constmarkdown:whisper.Markdown= { type:whisper.WhisperComponentType.Markdown, body:stripIndent` This is a simple whisper `, };returnwhisper.create({ components: [markdown], label:'Export Default Whisper', onClose, }); },};
4. Updatable Whispers
The following code snippets are examples of how we use the updatable Whisper method to display different Loop content without re-triggering new Whispers in our Olive Originals.
Code Snippet Purpose: Update Whispers with different content stored as created Whisper objects
import { whisper } from'@oliveai/ldk';interfaceProps { label:string;}/** * + Class instances allow more control over updatable props * + Updating whispers requires storing the created whisper object * + Separation of concern for different methods, allows for fine tune control of whisper * + Testing makes more sense without explicit exports solely for testing * - Considered overkill for any whispers that don't need updating */exportdefaultclassUpdatableClassWhisper { whisper:whisper.Whisper|undefined; label ='Updatable Class Whisper'; props:Props= { label:'', };// Runs when .close() method is called at the end of this classstaticonClose(err?:Error) {if (err) {console.error('There was an error closing Updatable Class whisper', err); }console.log('Updatable Class whisper closed'); }// Creates the UI of what is to be shown on the whispercreateComponents= () => {constupdatableLabelInput:whisper.TextInput= { type:whisper.WhisperComponentType.TextInput, label:'Change Whisper Label',onChange: (_error:Error|undefined, val:string) => {console.log('Updating whisper label: ', val);this.update({ label: val }); }, };return [updatableLabelInput]; };// Method is called to show this whisper UI from other whispers/part of the codeasyncshow() {this.whisper =awaitwhisper.create({ components:this.createComponents(), label:this.label, onClose:UpdatableClassWhisper.onClose, }); }// Can pass in additional props, in case this whisper requires data from other whispersupdate(props:Partial<Props>) {this.props = { ...this.props,...props };this.whisper?.update({ label:this.props.label ||this.label, components:this.createComponents(), }); }close() {this.whisper?.close(UpdatableClassWhisper.onClose); }}
5. Testing Component Handlers w/ Jest
The following code snippets are examples of how we test our component handlers with Jest.
settings.test.ts
Code Snippet Purpose: Test Button component handling.
/** * When testing component handlers, Breadcrumbs, Link, Button testing can be a bit * tricky thanks to static analysis, * below is an example of testing for the Dropzone Component and TextInput(nameInput). * */describe('Component Handlers', () => {// Different component typestypeSettingsComponents= [whisper.Breadcrumbs,whisper.Divider,whisper.Markdown,whisper.Divider,whisper.DropZone,whisper.TextInput,whisper.TextInput,whisper.Box ];let breadcrumb:SettingsComponents[0];let div1:SettingsComponents[1];let markdown:SettingsComponents[2];let div2:SettingsComponents[3];let dropzone:SettingsComponents[4];let nameInput:SettingsComponents[5];let descriptionInput:SettingsComponents[6];let pairButtonBox:SettingsComponents[7];let pairButton:whisper.Button;beforeEach(() => {/* settingsWhisper.createComponents() returns an array of components we can *destructure and re typecast as SettingsComponents, which allow us to have * access to the .onDrop() method */ [breadcrumb, div1, markdown, div2, dropzone, nameInput, descriptionInput, pairButtonBox] =settingsWhisper.createComponents() asSettingsComponents; [pairButton] = (pairButtonBox aswhisper.Box).children aswhisper.Button[]; });describe('Dropzone', () => {it('updates the whisper when dropzone is given a file', () => {constmockFile= {} aswhisper.File;// You can dropzone.onDrop(undefined, [mockFile],MOCK_WHISPER);expect(settingsWhisper.file.value).toEqual(mockFile);expect(MOCK_WHISPER.update).toBeCalled(); });it('logs an error', () => {dropzone.onDrop(newError(), [],MOCK_WHISPER);expect(console.error).toBeCalled(); }); });describe('Name input', () => {it('updates the whisper when name input is changed', () => {constmockName='name';nameInput.onChange(undefined, mockName,MOCK_WHISPER);expect(settingsWhisper.name.value).toBe(mockName);expect(MOCK_WHISPER.update).toBeCalled(); });it('clears an error when updated', () => {settingsWhisper.name.error ='test';nameInput.onChange(undefined,'',MOCK_WHISPER);expect(settingsWhisper.name.error).toBeUndefined(); });it('logs an error', () => {nameInput.onChange(newError(),'',MOCK_WHISPER);expect(console.error).toBeCalled(); }); });});
settings.test.ts
Code Snippet Purpose: To test Dropzone and TextInput component handling.
/* * Another smaller example of Button component handling following the same structure as * above */describe('Confirmation Component Handlers', () => {let cancelButton:whisper.Button;let confirmButton:whisper.Button;beforeEach(() => {const [_a,_b,_c,box] =settingsWhisper.createConfirmationComponents(); [cancelButton, confirmButton] = (box aswhisper.Box).children aswhisper.Button[];settingsWhisper.refresh =jest.fn();settingsWhisper.validate =jest.fn(); });it('should go back to the settings whisper if "Cancel" is clicked', () => {cancelButton.onClick(undefined,MOCK_WHISPER);expect(settingsWhisper.refresh).toBeCalled();expect(settingsWhisper.validate).not.toBeCalled(); });it('should go through validation if "Yes, Update" is clicked', () => {confirmButton.onClick(undefined,MOCK_WHISPER);expect(settingsWhisper.refresh).not.toBeCalled();expect(settingsWhisper.validate).toBeCalled(); }); });
The Self Test Loop is an incredibly detailed Loop that covers every single Aptitude and Whisper component. We use this internally for testing and validation.