Examples

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[];
      * }
      */
      const bytes = await filesystem.readFile(fileData.path);
      const workbook = await document.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.

export enum ErrorType {
  ExcelSheetValidation = 'Excel Sheet Validation',
  ExcelSheetDataValidation = 'Excel Sheet Data Validation',
}

export type DataError = 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
*/
export const readWorkbook = (workbook: Workbook): [Array<Record<string, string>>, DataError[]] => {
  const rows: Array<Record<string, string>> = [];
  const errors: 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 
   */
  const contentSheets = 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 
      const sheetData = new Sheet(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.

export class Sheet {
  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[]) {
    const headerRow = rows[0].cells;
    const dataRows = rows.slice(2);

    /* 
     * Runs a utility method verifyHeaders that checks the first row of cells in the
     * worksheet
     */
    const headerError = verifyHeaders(headerRow);
    if (headerError) {
      this.errors.push(headerError);
    }

    // dataRows = all the rows in worksheet from row 3 down
    if (dataRows.length) {
      // creates an array of formatted rowObjects, each one to be validated by verifyRequiredData
      You are all set. Is there anything else I 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"}]}
        const rowObject = 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 cell
        this.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
 */
export const handler = async () => {
  if (await vault.exists(vaultKeys.settings)) {
    new DigestWhisper().show();
  } else {
    new SettingsWhisper().show();
  }
};

export default {
  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
 */
const run = async () => {
  openHandler.start();

  const checkDeliveryTime = async () => {
    const now = new Date();
    const dailyDigestLastTriggeredBeforeToday =
      !(await vault.exists(vaultKeys.dailyDigestLastTriggered)) ||
      differenceInCalendarDays(
        new Date(await vault.read(vaultKeys.dailyDigestLastTriggered)),
        now
      ) < 0;
    if ((await vault.exists(vaultKeys.settings)) && dailyDigestLastTriggeredBeforeToday) {
      const deliveryTimeHasPassed = isBefore(
        parse((await getStoredSettings()).deliveryTime, 'h:mm a', now),
        now
      );
      if (deliveryTimeHasPassed) {
        new DigestWhisper().show();
        await vault.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 overnight
  await checkDeliveryTime();
  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
 */
export const getStoredSettings = async () => {
  const storedSettingsExist = await vault.exists(vaultKeys.settings);
  if (!storedSettingsExist) {
    console.log('Stored settings do not exist. Using defaults.');
    return getDefaultSettings();
  }

  const vaultValue = await vault.read(vaultKeys.settings);
  const storedSettings = JSON.parse(vaultValue);
  if (!isSettings(storedSettings)) {
    console.error(
      'Stored settings are invalid. Using defaults. Settings read from Vault: %s',
      vaultValue
    );
    return getDefaultSettings();
  }

  console.log('Stored settings loaded from Vault.');
  return storedSettings as Settings;
};

export const saveSettings = async (settings: Settings) => {
  await vault.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';

export const onClose = (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
 */
export default {
  show: async () => {
    const markdown: whisper.Markdown = {
      type: whisper.WhisperComponentType.Markdown,
      body: stripIndent`
        This is a simple whisper
      `,
    };
    return whisper.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';

interface Props {
  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
 */
export default class UpdatableClassWhisper {
  whisper: whisper.Whisper | undefined;

  label = 'Updatable Class Whisper';

  props: Props = {
    label: '',
  };

  // Runs when .close() method is called at the end of this class
  static onClose(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 whisper
  createComponents = () => {
    const updatableLabelInput: 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 code
  async show() {
    this.whisper = await whisper.create({
      components: this.createComponents(),
      label: this.label,
      onClose: UpdatableClassWhisper.onClose,
    });
  }

  // Can pass in additional props, in case this whisper requires data from other whispers
  update(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 types
  type SettingsComponents = [
    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() as SettingsComponents;
    [pairButton] = (pairButtonBox as whisper.Box).children as whisper.Button[];
  });

  describe('Dropzone', () => {
    it('updates the whisper when dropzone is given a file', () => {
      const mockFile = {} as whisper.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(new Error(), [], MOCK_WHISPER);

      expect(console.error).toBeCalled();
    });
  });

  describe('Name input', () => {
    it('updates the whisper when name input is changed', () => {
      const mockName = '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(new Error(), '', 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 as whisper.Box).children as whisper.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();
     });
   });

Loop Examples

Form Validation Loop

View it Here!

The Form Validation Loop provides an example of how one might structure a Loop to present a fillable form from an end user and validate it.

JavaScript Loop

View it Here!

This JavaScript Loop provides a very simple example that creates a Whisper when an end user copies something to their clipboard.

Self Test Loop

View it Here!

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.

TypeScript Loop

View it Here!

The TypeScript Loop provides a simple example of how to use typescript files. It behaves exactly the same as the JavaScript Loop above.

RxNav Example Loop

View it Here!

This is an Olive Helps Loop used to look up drug information through the National Library of Medicine's publicly available RxNav API.

Universal Example Loop

View it Here!

The Universal Example Loop provides a simple example of creating a form using React.

Last updated