• Moikas
  • Posts
  • How Build A Chatbot with LangChain and React as a Google Extension - Part 1 - Advanced Functionality and State Management

How Build A Chatbot with LangChain and React as a Google Extension - Part 1 - Advanced Functionality and State Management

ChatGPT x LangChain x Google Chrome Extension | Moikas

How to Create a Browser Agent pt-2

Hey Everyone!

Welcome back to Part 2 of the Guide/Walkthrough on how to create a Browser Agent. This section will add functionality to the Browser Agent we created in Part 1. If you could complete Part 1 successfully, you should have some code ready to be modified.

Read: Notion

Let’s Begin

I had to install a few more libraries to ensure we needed everything.

  • npm install --save axios fast-deep-equal npm-watch

  • Your Package JSON should look like this.

"dependencies": {
    "autoprefixer": "10.4.14",
    "cheerio": "^1.0.0-rc.12",
    "eslint": "8.44.0",
    "eslint-config-next": "13.4.7",
    "fast-deep-equal": "^3.1.3",
    "langchain": "^0.0.102",
    "next": "13.4.7",
    "npm-watch": "^0.11.0",
    "postcss": "8.4.24",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-markdown": "^8.0.7",
    "styled-components": "^6.0.2",
    "tailwindcss": "3.3.2"
  }

Development

If you are ready for development, here are the npm scripts to run the project. This will prepare and build your project files and watch for updates to rebuild so that you can develop the extension in your browser. I will be using Google Chrome.

🚨 This includes your .env, so don’t publish your extension to the public!!

npm run watch:mac # Develop for Mac npm run watch:win # Develop for Windows

public/manifest.json

This will allow Google Chrome or any other Chrome-based browser like Brave or Edge to install the extension.

{ "manifest_version": 3, "name": "browser-gpt", "version": "0.1.0", "action": { "default_popup": "index.html" }, "description": "Digital Assistance powered by Langchain", "permissions": [ "activeTab", "tabs" ], "host_permissions": [ "" ] }

To install your browser, make sure to turn on Developer Mode

An Upload the Out folder produced by the Watch or Build Scripts.

Create the Initial State for the project

src/state/agent_state.js

The initial state for the Browser Agent project was created to define the starting point for the project's state. This includes the active tab, messages, user input, and the number of tokens used. The state keeps track of the application's current state, making it easier to manage state changes throughout development.

const agent_state = {
  active_tab: '',
  messages: [],
  user_input: '',
  total_tokens_used: 0,
  completion_tokens_used: 0,
  prompt_tokens_used: 0,
  tokens_used:0
};

export default agent_state;

Create a Reducer to handle State Changes.

src/reducer/agent_reducer.js

The reducer was created to handle state changes in the Browser Agent application. It updates the application's state based on various actions, such as changing the active tab or adding a new message. Using a reducer makes it easier to manage state changes and keep track of the current state of the application throughout the development process.

export default function agent_reducer(state, action) {
  switch (action.type) {
    case 'active_tab':
      return {...state, active_tab: action.active_tab};
    case 'new_message':
      return {...state, messages: [...state.messages, action.message]};
    case 'user_input':
      return {...state, user_input: action.user_input};
    case 'total_tokens_used':
      return {...state, tokens_used: state.total_tokens_used + state.tokens_used};
    case 'completion_tokens_used':
      return {
        ...state,
        completion_tokens_used: action.completion_tokens_used,
      };
    case 'prompt_tokens_used':
      return {...state, prompt_tokens_used: action.prompt_tokens_used};
    case 'tokens_used':
      return {...state, tokens_used: action.tokens_used};
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

Create usePersistedReducer and usePrevious hook.

  • Data needs to persist between refresh and when the extension is closed

  • Storing reducer in LocalStorage

src/hooks/usePrevious.js

The usePrevious hook was written to keep track of the previous value of a given input. It is used in the usePersistedReducer hook to check if the current state is equal to the previous state before updating the local storage. This is necessary to prevent unnecessary updates to the local storage and ensure that the data only persists when the state changes.

import {useRef, useEffect} from 'react';

// Given any value
// This hook will return the previous value
// Whenever the current value changes

export function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

src/hooks/usePersistedReducer.js

The usePersistedReducer hook was created to store the reducer's state in local storage so that the data would persist between refreshes and when the extension is closed. This was achieved by updating the local storage whenever the state changed. The usePrevious hook was also used to check if the current state was equal to the previous state before updating the local storage to prevent unnecessary updates.

import {useEffect, useReducer} from 'react';
import deepEqual from 'fast-deep-equal/es6';
import {usePrevious} from './usePrevious';

export function usePersistedReducer(reducer, initialState, storageKey) {
  const [state, dispatch] = useReducer(reducer, initialState, initializer);
  const prevState = usePrevious(state);
  function initializer() {
    if (typeof localStorage === 'undefined') return initialState;
    const stringState = localStorage.getItem(storageKey);
    if (stringState) {
      try {
        return JSON.parse(stringState);
      } catch (error) {
        return initialState;
      }
    } else {
      return initialState;
    }
  }
  useEffect(() => {
    const stateEqual = deepEqual(prevState, state);
    if (!stateEqual && typeof localStorage !== 'undefined') {
      const stringifiedState = JSON.stringify(state);
      localStorage.setItem(storageKey, stringifiedState);
    }
  }, [state, storageKey]);

  return [state, prevState, dispatch];
}

Create an Agent Context and AgentProvider

  • imported persisted reducer to pass state and dispatch to child components

src/components/AgentProvider.js

The Agent Provider and Agent Context were created to manage the state of the Browser Agent application. The usePersistedReducer hook was used to store the state in local storage so that the data would persist between refreshes and when the extension is closed. The Agent Context is used to share the state and dispatch across all child components, while the Agent Provider wraps the entire application to provide access to the context.

import {createContext} from 'react';
import {usePersistedReducer} from '@/hooks/usePersistedReducer';
import agent_reducer from '@/reducer/agent_reducer';
import agent_state from '@/state/agent_state';

export const AgentContext = createContext(null);
//
export default function AgentProvider({children}) {
  const [state, dispatch] = usePersistedReducer(
    agent_reducer,
    agent_state,
    'agent_state'
  );

  return (
    <AgentContext.Provider value={{state, dispatch}}>
      {children}
    </AgentContext.Provider>
  );
}

Subscribe to keep reading

This content is free, but you must be subscribed to Moikas to continue reading.

Already a subscriber?Sign In.Not now

Reply

or to participate.