Vue Storefront is now Alokai! Learn More
First workflow

First workflow

Now that you understand the way Alokai Compass works and have it intalled in your Storefront project, you can start creating engaging AI interactions.

This guide assumes you have installed Alokai Compass module in your project and enabled its features.

Create the Quotes feature

In this chapter, your will create a simple component allowing users to get a random quote generated by a Large Language Model. In order to achieve that, you will:

  • Create the quotes Workflow featuring a single Action responsible for generating a random quote,
  • Display the generated quotes in a dedicated UI component in your Storefront.

quotes component version 1

1

Create the quotes workflow

In the storefront-middleware application, create a new file and use the defineWorkflow() helper to create the quotes workflow configuration. Choose the heavy model and - for a better user experience - allow it to stream responses.

apps/**/storefront-middleware/sf-modules/compass/workflows/quotes/index.ts
import { defineWorkflow, END, START } from '@alokai/compass';

enum ACTIONS {
  QUOTE = 'quote',
}

export const quotesWorkflow = defineWorkflow({
  actions: {
    [ACTIONS.QUOTE]: {
      allowStreaming: true,
      model: 'heavy',
      prompt: 'You are a helpful assistant retrieving and explaining a random quote to the user.',
      type: 'llm',
    },
  },
  toolkits: [],
  transitions: {
    [START]: ACTIONS.QUOTE,
    [ACTIONS.QUOTE]: END,
  },
});

2

Register the quotes workflow

Import the newly-created quotes workflow in the Alokai Compass module configuration file and add it to the configuration.workflows object.

apps/**/storefront-middleware/sf-modules/compass/config.ts
import { quotesWorkflow } from '@/sf-modules/compass/workflows/quotes';

enum WORKFLOWS {
  // ...
  QUOTES = 'quotes',
}

export const config = defineConfig({
  // ...
  configuration: {
    // ...
    workflows: {
      // ...
      [WORKFLOWS.QUOTES]: quotesWorkflow,
    },
  },
});

3

Test the quotes workflow

With the workflow registered and your application running on port 4011, you can send a request to the /invoke endpoint and see the LLM respond with a random quote!

curl --location 'http://localhost:4011/compass/invoke' --header 'Content-Type: application/json' --data '{ "workflowId": "quotes", "messages": [{ "role": "user", "content": "{ \"type\":\"text\", \"text\": \"Get me a quote\" }" }] }'

4

Create the Quotes component

In the storefront-unified-nextjs application, create a new file for the Quotes component.

  1. Call the useWorkflow() hook, passing quotes as the first argument (workflowId). As the second argument, pass the configuration object with the stream property set to true.
  2. Create a handler invoking the quotes workflow with a proper message
  3. Create a helper extracting the last assitant message from the messages returned by useWorkflow() hook
  4. Create a paragraph displaying the LLM-generated quote
  5. Create a button with the Get a quote label and attach the handler created in point #2 to it
apps/**/storefront-unified-nextjs/sf-modules/compass/components/quotes/quotes.tsx
'use client';
import { SfButton, SfIconClose, SfLoaderCircular, SfModal, useDisclosure } from '@storefront-ui/react';

import SidesheetButton from '@/sf-modules/compass/components/ai-assistant/sidesheet-button';
import { useWorkflow } from '@/sf-modules/compass/hooks';
import { AssistantMessage } from '@/sf-modules/compass/types';

export default function Quotes() {
  const { close, isOpen, open } = useDisclosure({ initialValue: false });
  const { isLoading, messages, send } = useWorkflow('quotes', { stream: true }); // #1

  const handleGetQuote = async () => {
    const message = {
      content: [
        {
          text: 'Get me a quote',
          type: 'text' as const,
        },
      ],
      role: 'user',
    };
    await send([message]); // #2
  };

  const aiMessage = getAiMessage(messages.at(-1));

  function getAiMessage(message?: AssistantMessage) {
    if (
      !message ||
      message.role === 'user' ||
      typeof message.content[0] === 'string' ||
      message.content[0]?.type !== 'text'
    ) {
      return null;
    }

    return message.content[0]?.text; // #3
  }

  return (
    <>
      <SidesheetButton
        className="left-0 !right-auto rounded-bl-none rounded-br-[6px] rounded-tl-none rounded-tr-[6px] bg-purple-200"
        onClick={open}
      />
      <SfModal as="section" className="z-50 max-w-[90%] md:max-w-lg" onClose={close} open={isOpen} role="alertdialog">
        <header>
          <SfButton className="absolute right-2 top-2" onClick={close} square variant="tertiary">
            <SfIconClose />
          </SfButton>
          <h3 className="text-center font-bold typography-headline-4 md:typography-headline-3" id="promoModalTitle">
            What's your quote for today?
          </h3>
        </header>
        {aiMessage && (
          <p className="mt-2 text-center" id="promoModalDesc">
            {aiMessage} {/* #4 */}
          </p>
        )}
        {isLoading && (
          <div className="flex items-center justify-center p-4">
            <SfLoaderCircular className="" size="sm" />
          </div>
        )}
        <footer className="mt-4 flex justify-center gap-4">
          <SfButton disabled={isLoading} onClick={handleGetQuote}>Get a quote</SfButton> {/* #5 */}
        </footer>
      </SfModal>
    </>
  );
}

5

Render the Quotes component

Import and render the newly-created Quotes component in the root layout within the CompassProvider and AiAssistant.

import Quotes from '@/sf-modules/compass/components/quotes/quotes';

export default async function RootLayout() {
  // ...
  return (
    <html>
      <body className={classNames(fontHeadings.variable, fontBody.variable, 'font-body')}>
        {/* ... */}
          <CompassProvider>
            <AiAssistant>
              <Quotes />
              {/* ... */}
            </AiAssistant>
          </CompassProvider>
        {/* ... */}
      </body>
    </html>
  );
}

With all of the steps completed, go to your Storefront's Home page. Click the purple button on the left-hand-side of the screen to toggle the Quotes modal. Click the Get a quote button and see the LLM-generated text appear on the screen. Congratulations! You have just created your first Alokai Compass Workflow. Follow the other chapters of this guide to make it even better.

Fetch quotes from an API

So far your feature has been using LLM-generated quotes only. However, it would be more reliable if they were obtained from an verified external API instead of being - at least sometimes - hallucinated by the agent. To achieve that, you will:

  • Create a simple getQuote tool fetching random quote from the DummyJSON API,
  • Ask the LLM to give their opinion on the quote.

quotes component version 2

1

Create the getQuote tool

In the storefront-middleware application, create a new file and use the defineTool() helper to create the getQuote tool. Define its name, description and callback that:

  1. Fetches random quote from the DummyJSON API,
  2. Sends the fetched quote to the browser using the sendToBrowser() utility,
  3. Returns a prompt for the LLM.
apps/**/storefront-middleware/sf-modules/compass/tools/custom/getQuote.ts
import { defineTool, sendToBrowser } from '@alokai/compass';

export const getQuoteTool = defineTool({
  name: 'getQuote',
  description: 'Retrieve a random quote from an API',
  callback: async (context, args) => {
    const response = await fetch(`https://dummyjson.com/quotes/random`); // #1
    const quote = await response.json();

    sendToBrowser({ text: `${quote.quote} - ${quote.author}`, type: 'text' }); // #2

    return `Explain the following quote in max 3 sentences: ${JSON.stringify(quote)}. In your response, do not repeat the quote.`; // #3
  },
});

2

Register the getQuote tool

To register the newly-created tool in your quotes workflow:

  1. Import and add the newly-created tool to the toolkits array,
  2. Add the tool's name to the getQuote action's toolkit array,
  3. Ensure the LLM used by the action reacts to the prompt returned by the tool.

Setting shouldReactToToolsResponses to false would result in terminating the workflow right after the getQuote is called by the LLM.

apps/**/storefront-middleware/sf-modules/compass/workflows/quotes/index.ts
import { getQuoteTool } from '@/sf-modules/compass/tools/custom/getQuote';

export const quotesWorkflow = defineWorkflow({
  actions: {
    [ACTIONS.QUOTE]: {
      shouldReactToToolsResponses: true, // #3
      toolkit: ['getQuote'], // #2
    },
  },
  toolkits: [
    getQuoteTool // #1
  ],
});

3

Render the quote and explanation

In the storefront-unified-nextjs application, make the following changes to the Quotes component:

  1. Extract the quote from the workflow messsages
  2. Render the quote above the AI message with the quote's explanation
'use client';
export default function Quotes() {
  // ... 
  const quote = getAiMessage(messages.at(-2)); // #1
  const aiMessage = getAiMessage(messages.at(-1));

  function getAiMessage(message?: AssistantMessage) {/*...*/}

  return (
    <>
      <SfModal /*...*/>
        {quote && (
          <p className="mt-2 text-center" id="promoModalDesc">
            {quote} {/* #2 */}
          </p>
        )}
        {aiMessage && (
          <p className="mt-2 text-center" id="promoModalDesc">
            {aiMessage}
          </p>
        )}
      </SfModal>
    </>
  );
}

Add generative UI Quote component

The previous examples showed you how to display text-based responses from the LLM. However, Alokai Compass also supports Generative UI - a powerful feature that allows the LLM to render custom React components directly in the chat interface. In this chapter, you will:

  • Create a custom Quote UI component that displays quotes in a styled format,
  • Update the getQuote tool to use the createGenrativeUiComponent() helper,
  • Modify the Quotes component to render the generative UI component.

quotes component with generative UI

1

Define the component schema

In the storefront-middleware application, add the quote component schema to the UI_COMPONENTS object. This schema defines the structure of data that will be passed to your UI component.

apps/**/storefront-middleware/sf-modules/compass/responses/dynamic-ui/index.ts
import { z } from 'zod';

export const UI_COMPONENTS = {
  // ...
  ['quote']: z.object({
    author: z.string(),
    id: z.number(),
    quote: z.string(),
  }),
};

2

Update the getQuote tool

Modify the existing getQuote tool to use the createGenrativeUiComponent() helper instead of sendToBrowser(). This will send a structured UI component to the browser instead of plain text.

apps/**/storefront-middleware/sf-modules/compass/tools/custom/getQuote.ts
import {
  createGenrativeUiComponent,   defineTool,
  sendToBrowser
} from '@alokai/compass';
import { UI_COMPONENTS } from '@/sf-modules/compass/responses/dynamic-ui';

export const getQuoteTool = defineTool({
  // ...
  callback: async () => {
    const response = await fetch(`https://dummyjson.com/quotes/random`);
    const quote = await response.json();

    sendToBrowser({ text: `${quote.quote} - ${quote.author}`, type: 'text' });     sendToBrowser(createGenrativeUiComponent(UI_COMPONENTS, 'quote', quote)); 
    return `Explain the following quote in max 3 sentences: ${JSON.stringify(quote)}. In your response, do not repeat the quote.`;
  },
});

3

Create the component

In the storefront-unified-nextjs application, create a new Quote component that will be rendered as part of the generative UI. This component receives the quote data as props and renders it in a styled format.

apps/**/storefront-unified-nextjs/sf-modules/compass/components/generative/components/quote.tsx
import { AI_RESPONSE_TYPES } from 'storefront-middleware/shared';
import { z } from 'zod';

type Params = z.infer<(typeof AI_RESPONSE_TYPES)['UI_COMPONENTS']['quote']>;

export default function Quote({ author, quote }: Params) {
  return (
    <div className="my-4 rounded-lg border bg-amber-50 p-4 text-center">
      <p>{quote}</p>
      <p className="text-sm font-semibold text-gray-500">{author}</p>
    </div>
  );
}

4

Update the Quotes component

Modify the Quotes component to handle and render the generative UI component. You'll need to:

  1. Import the necessary types and components for handling UI components,
  2. Create a helper function to extract quote data from AI message,
  3. Replace the quote text rendering with the generative UI component.
apps/**/storefront-unified-nextjs/sf-modules/compass/components/quotes/quotes.tsx
'use client';
import { UiComponent } from '@alokai/compass/client'; import { SfButton, SfIconClose, SfLoaderCircular, SfModal, useDisclosure } from '@storefront-ui/react';

import { UI_COMPONENTS } from '@sf-modules-middleware/compass/responses/dynamic-ui'; import { GenerativeUiComponent } from '@/sf-modules/compass/components/generative'; import SidesheetButton from '@/sf-modules/compass/components/ai-assistant/sidesheet-button';
import { useWorkflow } from '@/sf-modules/compass/hooks';
import { AssistantMessage } from '@/sf-modules/compass/types';

export default function Quotes() {
  const { close, isOpen, open } = useDisclosure({ initialValue: false });
  const { isLoading, messages, send } = useWorkflow('quotes', { stream: true });

  const handleGetQuote = async () => {
    const message = {
      content: [
        {
          text: 'Get me a quote',
          type: 'text' as const,
        },
      ],
      role: 'user',
    };
    await send([message]);
  };

  const quote = getAiMessage(messages.at(-2));   const quote = getUiComponent(messages.at(-2));   const aiMessage = getAiMessage(messages.at(-1));

  function getAiMessage(message?: AssistantMessage) {
    if (
      !message ||
      message.role === 'user' ||
      typeof message.content[0] === 'string' ||
      message.content[0]?.type !== 'text'
    ) {
      return null;
    }

    return message.content[0]?.text;
  }

  function getUiComponent(message?: AssistantMessage) { // #2
    if (
      !message || 
      message.role === 'user' || 
      typeof message.content[0] === 'string' || 
      message.content[0]?.type !== 'generative-ui'
    ) {
      return null;
    }

    return message.content[0] as UiComponent<'quote', (typeof UI_COMPONENTS)['quote']>;
  }

  return (
    <>
      <SidesheetButton
        className="left-0 !right-auto rounded-bl-none rounded-br-[6px] rounded-tl-none rounded-tr-[6px] bg-purple-200"
        onClick={open}
      />
      <SfModal as="section" className="z-50 max-w-[90%] md:max-w-lg" onClose={close} open={isOpen} role="alertdialog">
        <header>
          <SfButton className="absolute right-2 top-2" onClick={close} square variant="tertiary">
            <SfIconClose />
          </SfButton>
          <h3 className="text-center font-bold typography-headline-4 md:typography-headline-3" id="promoModalTitle">
            What's your quote for today?
          </h3>
        </header>
        {quote && (
          <p className="mt-2 text-center" id="promoModalDesc">
            {quote}
          </p>
        )}
        {quote && <GenerativeUiComponent payload={quote} />} 
        {aiMessage && (
          <p className="mt-2 text-center" id="promoModalDesc">
            {aiMessage}
          </p>
        )}
        {isLoading && (
          <div className="flex items-center justify-center p-4">
            <SfLoaderCircular className="" size="sm" />
          </div>
        )}
        <footer className="mt-4 flex justify-center gap-4">
          <SfButton disabled={isLoading} onClick={handleGetQuote}>
            Get a quote
          </SfButton>
        </footer>
      </SfModal>
    </>
  );
}

With these changes, your quotes feature now uses Generative UI to display quotes in a beautifully styled component instead of plain text. The LLM can now render custom React components directly in the chat interface, opening up endless possibilities for rich, interactive user experiences.

React to frontend hints

While Generative UI allows the LLM to render custom components, Frontend Hints provide a way for the backend to trigger specific actions or updates in the frontend application. This is particularly useful for updating application state, showing notifications, or triggering side effects when certain events occur. In this chapter, you will:

  • Define a custom frontend hint schema for quote-related events,
  • Update the getQuote tool to send frontend hints alongside the UI component,
  • Handle the frontend hint in the React application to show user notifications.

quotes component with frontend hints

1

In the storefront-middleware application, add a new frontend hint to the FRONTEND_HINTS object. This schema defines the structure of data that will be sent to trigger frontend actions.

apps/**/storefront-middleware/sf-modules/compass/responses/frontend-hints/index.ts
import { z } from 'zod';

export const FRONTEND_HINTS = {
  // ...
  'quote-has-been-found': z.object({
    author: z.string(),
    done: z.boolean().optional(),
  }),
};

The done property in the hint schema will be used later to prevent duplicate notifications.

2

Modify the existing getQuote tool to send a frontend hint when a quote is retrieved. This hint will carry information about the quote's author and can trigger frontend actions.

apps/**/storefront-middleware/sf-modules/compass/tools/custom/getQuote.ts
import {
  createFrontendHint,   createGenrativeUiComponent,
  defineTool,
  sendToBrowser,
} from '@alokai/compass';

import { UI_COMPONENTS } from '@/sf-modules/compass/responses/dynamic-ui';
import { FRONTEND_HINTS } from '@/sf-modules/compass/responses/frontend-hints'; 
export const getQuoteTool = defineTool({
  // ...
  callback: async () => {
    const response = await fetch(`https://dummyjson.com/quotes/random`);
    const quote = await response.json();

    sendToBrowser(
      createFrontendHint(FRONTEND_HINTS, 'quote-has-been-found', { author: quote.author }),
    );
    sendToBrowser(createGenrativeUiComponent(UI_COMPONENTS, 'quote', quote));

    return `Explain the following quote in max 3 sentences: ${JSON.stringify(quote)}. In your response, do not repeat the quote.`;
  },
});

3

Update the useFrontendActions hook to handle the new quote-has-been-found hint. This hook processes incoming frontend hints and triggers appropriate actions in the React application.

apps/**/storefront-unified-nextjs/sf-modules/compass/hooks/use-frontend-actions/index.ts
import { isFrontendHint } from '@alokai/compass/client';
import type { AI_RESPONSE_TYPES } from 'storefront-middleware/shared';

import {
  useCart,
  useNotification} from '@/hooks';
import type { AssistantMessage } from '@/sf-modules/compass/types';

export const useFrontendActions = () => {
  const { refetch } = useCart();
  const notification = useNotification();   return {
    reactToUdmResponse: (newMsg: AssistantMessage) => {
      const { content } = newMsg;
      if (isFrontendHint<(typeof AI_RESPONSE_TYPES)['FRONTEND_HINTS']>(content[0])) {
        const { 
          hintId: command,
          props         } = content[0];
        if (command === 'quote-has-been-found' && !props.done) {
          notification.success(`Congratulations! You've found a quote from ${props.author}.`);
          props.done = true;
        }
        if (command === 'cart-has-been-updated') {
          refetch();
        }
      }
    },
  };
};

4

Finally, update the Quotes component to use the useFrontendActions hook and process incoming messages to trigger the frontend hints.

apps/**/storefront-unified-nextjs/sf-modules/compass/components/quotes/quotes.tsx
'use client';
import { UiComponent } from '@alokai/compass/client';
import { UI_COMPONENTS } from '@sf-modules-middleware/compass/responses/dynamic-ui';
import { SfButton, SfIconClose, SfLoaderCircular, SfModal, useDisclosure } from '@storefront-ui/react';
import { useEffect } from 'react'; 
import SidesheetButton from '@/sf-modules/compass/components/ai-assistant/sidesheet-button';
import { GenerativeUiComponent } from '@/sf-modules/compass/components/generative';
import {
  useFrontendActions,   useWorkflow
} from '@/sf-modules/compass/hooks';
import { AssistantMessage } from '@/sf-modules/compass/types';

export default function Quotes() {
  const { close, isOpen, open } = useDisclosure({ initialValue: false });
  const { isLoading, messages, send } = useWorkflow('quotes', { stream: true });
  const { reactToUdmResponse } = useFrontendActions(); 
  useEffect(() => {
    messages.forEach(reactToUdmResponse);
  }, [messages, reactToUdmResponse]);

  // ... rest of the component remains the same
}

With these changes, your Quotes feature now uses Frontend Hints to trigger actions in the React application. When a quote is retrieved, the backend sends a hint that triggers a congratulatory message showing the quote's author. This pattern can be extended to handle more complex scenarios like updating user preferences, showing notifications, refreshing data, or triggering animations.