{"id":18983729,"url":"https://github.com/edspencer/inform-ai","last_synced_at":"2025-07-07T15:39:10.195Z","repository":{"id":252847728,"uuid":"840030609","full_name":"edspencer/inform-ai","owner":"edspencer","description":"InformAI allows you to easily retrofit context-aware AI into any React application.","archived":false,"fork":false,"pushed_at":"2025-01-24T19:06:28.000Z","size":525,"stargazers_count":30,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-15T14:56:21.313Z","etag":null,"topics":["ai","nextjs","react","reactjs","vercel"],"latest_commit_sha":null,"homepage":"https://edspencer.net/2024/8/26/introducing-inform-ai","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edspencer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-08T20:31:20.000Z","updated_at":"2025-02-24T09:26:16.000Z","dependencies_parsed_at":null,"dependency_job_id":"053a525c-4481-45af-a8b1-ca72582cde1c","html_url":"https://github.com/edspencer/inform-ai","commit_stats":null,"previous_names":["edspencer/inform-ai"],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edspencer%2Finform-ai","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edspencer%2Finform-ai/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edspencer%2Finform-ai/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edspencer%2Finform-ai/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edspencer","download_url":"https://codeload.github.com/edspencer/inform-ai/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249094937,"owners_count":21211836,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","nextjs","react","reactjs","vercel"],"created_at":"2024-11-08T16:18:14.769Z","updated_at":"2025-04-15T14:56:42.071Z","avatar_url":"https://github.com/edspencer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# InformAI - Context-Aware AI Integration for React Apps\n\n**InformAI** is a tool that enables seamless integration of context-aware AI into any React application. It's designed to work effortlessly with the [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction), but is also compatible with other AI SDK providers.\n\n### Key Features:\n\n- **Contextual AI Integration**: Easily expose the state of your React components to an LLM (Large Language Model) or other AI, providing valuable context with minimal effort.\n- **Event Publishing**: Allow your components to publish events, like user interactions, in an LLM-optimized format.\n- **Flexible Usage**: Works well with both client-side React components and React Server Components. Though it excels with Next.js and React Server Components, these are not required.\n\nInformAI doesn't directly send data to your LLM but simplifies integration with tools like the Vercel AI SDK, making it easy to incorporate AI into your app.\n\nIf ChatGPT and other LLMs can read and write text, and Vercel AI SDK adds the ability to write UI by streaming React components as part of an LLM response, InformAI fills in the missing piece by allowing the LLM to read your UI as well as write to it:\n\n![Where InformAI fits](/docs/magic-square.png)\n\n## Installation\n\nInstall the NPM package:\n\n```sh\nnpm install inform-ai\n```\n\nInclude the stylesheet if you plan to use the included UI components (or don't, if you want to use them but customize their appearance):\n\n```tsx\nimport \"inform-ai/dist/main.css\";\n```\n\n## Installing the Provider\n\nInformAI can be used via either the `\u003cInformAI /\u003e` Component or the `useInformAI` hook. Either way, you need to wrap any components using InformAI inside an `InformAIProvider`:\n\n```tsx\nimport { InformAIProvider } from \"inform-ai\";\n\n//somewhere in your layout.tsx or similar:\n\u003cInformAIProvider\u003e{children}\u003c/InformAIProvider\u003e;\n```\n\n## Exposing Component state\n\nNow, within any React component that will be rendered inside that `InformAIProvider`, you can insert a `\u003cInformAI /\u003e` node:\n\n```tsx\nimport { InformAI } from \"inform-ai\";\n\nconst prompt = \"Shows the life history of a person, including their name, title and age\";\n\nexport function Bio({ name, title, age }) {\n  return (\n    \u003cdiv className=\"my-component\"\u003e\n      \u003cInformAI name={`Biography for ${name}`} props={{ name, title, age }} prompt={prompt} /\u003e\n      //... rest of the component here\n    \u003c/div\u003e\n  );\n}\n```\n\nAdding the `\u003cInformAI /\u003e` tag to our component we were able to tell the LLM 3 things about our component:\n\n- **name** - a meaningful name for this specific component instance\n- **props** - any props we want to pass to the LLM (must be JSON-serializable)\n- **prompt** - a string to help the LLM understand what the component does\n\n### useInformAI\n\nAn alternative to the `\u003cInformAI /\u003e` component is to use the `useInformAI` hook. `useInformAI` is a little more versatile than `\u003cInformAI /\u003e`. Here's a slightly simplified example taken from the [backups table from lansaver](https://github.com/edspencer/lansaver/blob/main/components/backup/table.tsx), showing how to use `useInformAI` instead of `\u003cInformAI /\u003e`:\n\n```tsx\nimport { useInformAI } from \"inform-ai\";\n\nconst prompt =\n  \"This table displays a list of backups taken for various devices. The data will be provided to you in JSON format\";\n\nexport function BackupsTable({\n  name = \"Backups Table\",\n  backups,\n  showDevice = false,\n}: {\n  name?: string;\n  backups: BackupWithDevice[];\n  showDevice?: boolean;\n}) {\n  useInformAI({\n    name,\n    prompt,\n    props: {\n      backups,\n    },\n  });\n\n  if (condensed) {\n    return \u003cCondensedBackupsTable backups={backups} showDevice={showDevice} /\u003e;\n  }\n\n  return \u003ctable\u003e//your table implementation\u003c/table\u003e;\n}\n```\n\nIt was useful to use the hook in this case as we render a different table if `condensed` is set to true, but we wanted to surface the same information either way to InformAI, so by using 'useInformAI' we didn't need to maintain 2 duplicate copies of an `\u003cInformAI /\u003e` tag in our 2 table components.\n\n## Exposing Component events\n\nAnother possibility that is unlocked by using `useInformAI` is telling the LLM about component events like clicks or other user interactions:\n\nHere's an example of a different Table component, which can render arbitrary data and exposes `click` events when the user clicks on a table cell:\n\n```tsx\nconst defaultPrompt = `This component is a table that displays data in a tabular format. \nIt takes two props: data and colHeaders. The data prop is an array of objects, where \neach object represents a row in the table. The colHeaders prop is an optional \narray of strings that represent the column headers of the table. \nIf the colHeaders prop is not provided, the component will use the \nkeys of the first object in the data array as the column headers. \nThe component will render the table with the provided data and column headers.`;\n\nexport function Table({ data, colHeaders, name = \"Table\", informPrompt = defaultPrompt, header }: TableProps) {\n  const { addEvent } = useInformAI({\n    name,\n    prompt: informPrompt,\n    props: {\n      data,\n      colHeaders,\n    },\n  });\n\n  //adds a new hint to the AI\n  const cellClicked = (e: React.MouseEvent\u003cHTMLTableCellElement\u003e) =\u003e {\n    addEvent({\n      type: \"user-click\",\n      description: \"User clicked on a cell with data: \" + (e.target as HTMLElement).innerHTML,\n    });\n  };\n\n  return (\n    \u003cdiv className=\"border border-gray-500 flex-1\"\u003e\n      {header}\n      \u003ctable className=\"min-w-full divide-y divide-gray-300\"\u003e\n        \u003cthead\u003e\n          \u003ctr\u003e\n            {colHeaders\n              ? colHeaders.map((header, index) =\u003e \u003cTableHeaderCell key={index}\u003e{header.label}\u003c/TableHeaderCell\u003e)\n              : Object.keys(data[0]).map((header, index) =\u003e \u003cTableHeaderCell key={index}\u003e{header}\u003c/TableHeaderCell\u003e)}\n          \u003c/tr\u003e\n        \u003c/thead\u003e\n        \u003ctbody className=\"divide-y divide-gray-200\"\u003e\n          {data.map((row, rowIndex) =\u003e (\n            \u003ctr key={rowIndex}\u003e\n              {Object.values(row).map((cell, cellIndex) =\u003e (\n                \u003cTableCell key={cellIndex} onClick={cellClicked}\u003e\n                  {cell as ReactNode}\n                \u003c/TableCell\u003e\n              ))}\n            \u003c/tr\u003e\n          ))}\n        \u003c/tbody\u003e\n      \u003c/table\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nThe `type` and `description` we pass can be any strings we like.\n\n## Viewing Current State\n\nUnder the covers, InformAI collects together all of the component state and event messages that are published by `\u003cInformAI /\u003e` and `useInformAI`. While in development, it's useful to be able to see what InformAI is aware of, and what will be sent with the next user message to the LLM.\n\nInformAI ships with a small React component called `\u003cCurrentState /\u003e` which can be rendered anywhere inside your component tree, and will show you all of the component states and events that InformAI has collected.\n\nDrop this into your layout.tsx like so:\n\n```tsx\nimport \"inform-ai/dist/main.css\";\nimport \"./globals.css\";\n\n//optionally include the CurrentState component for easier InformAI debugging\nimport { InformAIProvider, CurrentState } from \"inform-ai\";\n\nexport default function RootLayout({\n  children,\n}: Readonly\u003c{\n  children: React.ReactNode;\n}\u003e) {\n  return (\n    \u003chtml lang=\"en\"\u003e\n      \u003cbody\u003e\n        \u003cInformAIProvider\u003e\n          {children}\n          \u003cCurrentState className=\"fixed top-20 right-3 max-h-[50vh] overflow-auto w-1/5\" /\u003e\n        \u003c/InformAIProvider\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  );\n}\n```\n\n`\u003cCurrentState /\u003e` accepts a `className` so you can style.position it however you like (this example has it pinned top right). It will collapse/expand when you click the component heading if it's getting in the way.\n\n![CurrentState component](/docs/current-state-example.png)\n\n`\u003cCurrentState /\u003e` is intended to help understand/debug in development, and is not something you'd likely ship to your users. Each time a component registers a state or event update, a row is added to CurrentState with the ability to dig down into a JSON view of all of the information.\n\n## Adding a Chatbot\n\nHow you add your Chatbot UI is completely up to you. InformAI works well alongside the Vercel AI SDK (`npm install ai`), and provides a couple of rudimentary chatbot UI components out of the box that use Vercel AI SDK.\n\nHere's how you can use that to create your own simple `ChatBot` component using the Vercel AI SDK and InformAI:\n\n```tsx\n\"use client\";\n\nimport { ChatWrapper } from \"inform-ai\";\nimport { useActions, useUIState } from \"ai/rsc\";\n\nexport function ChatBot() {\n  const { submitUserMessage } = useActions();\n  const [messages, setMessages] = useUIState();\n\n  return \u003cChatWrapper submitUserMessage={submitUserMessage} messages={messages} setMessages={setMessages} /\u003e;\n}\n```\n\nInformAI exposes `ChatBox` and `Messages` components, along with a `ChatWrapper` that just combines them both into an easy package. `ChatBox` is a fairly simple form with a text input and a button to submit the user's message, and `Messages` just renders the conversation between the user and the LLM assistant.\n\nBecause the Vercel AI SDK is awesome, `Messages` can handle streaming LLM responses as well as streaming React Server Components (if you're using nextjs or similar). Here's an example of a conversation using `ChatWrapper`:\n\n![Example Chat on Schedules page](/docs/inform-ai-chat-example.png)\n\nYou're highly encouraged to check out the [ChatWrapper source](/src/ui/ChatWrapper.tsx) as well as that for [ChatBox](/src/ui/ChatBox.tsx) and [Messages](/src/ui/Messages.tsx) - they're all pretty straightforward components so you can use all, some or none of them in your app.\n\n### Vercel AI backend for this example\n\nTo get that `ChatBot` component to work, we actually need 2 more things:\n\n- A Vercel `\u003cAIProvider\u003e` in our React tree\n- A `submitUserMessage` function\n\nWe can define those both in a single file, something like this:\n\n```tsx\n\"use server\";\n\nimport { CoreMessage, generateId } from \"ai\";\nimport { createAI } from \"ai/rsc\";\nimport { AssistantMessage } from \"inform-ai\";\n\nexport type ClientMessage = CoreMessage \u0026 {\n  id: string;\n};\n\nexport type AIState = {\n  chatId: string;\n  messages: ClientMessage[];\n};\n\nexport type UIState = {\n  id: string;\n  role?: string;\n  content: React.ReactNode;\n}[];\n\nexport async function submitUserMessage(messages: ClientMessage[]) {\n  const aiState = getMutableAIState();\n\n  //add the new messages to the AI State so the user can refresh and not lose the context\n  aiState.update({\n    ...aiState.get(),\n    messages: [...aiState.get().messages, ...messages],\n  });\n\n  //set up our streaming LLM response using Vercel AI SDK\n  const result = await streamUI({\n    model: openai(\"gpt-4o-2024-08-06\"),\n    system: \"You are a helpful assistant who blah blah blah\",\n    messages: aiState.get().messages,\n    text: ({ content, done }) =\u003e {\n      if (done) {\n        //save the LLM's response to our AIState\n        aiState.done({\n          ...aiState.get(),\n          messages: [...aiState.get().messages, { role: \"assistant\", content }],\n        });\n      }\n\n      //AssistantMessage is a simple, styled component that supports streaming text/UI responses\n      return \u003cAssistantMessage content={content} /\u003e;\n    },\n  });\n\n  return {\n    id: generateId(),\n    content: result.value,\n  };\n}\n\nexport const AIProvider = createAI\u003cAIState, UIState\u003e({\n  actions: {\n    submitUserMessage,\n  },\n  initialUIState: [] as UIState,\n  initialAIState: { chatId: generateId(), messages: [] } as AIState,\n});\n```\n\nThis gives us our `submitUserMessage` and `\u003cAIProvider\u003e` exports. All you need to do now is add the `\u003cAIProvider\u003e` into your React component tree, just like we did with `\u003cInformAIProvider\u003e`, and everything should Just Work. The `useActions()` hook we used in our `ChatBot.tsx` will be able to pull out our `submitUserMessage` function and pass it to `ChatWrapper`, which will then call it when the user enters and sends a message.\n\nThe AIState management we do there is to keep a running context of the conversation so far - see the [Vercel AI SDK AIState docs](https://sdk.vercel.ai/examples/next-app/state-management/ai-ui-states) if you're not familiar with that pattern.\n\nThe `text` prop we passed to `streamUI` is doing 2 things - rendering a pretty `\u003cAssistantMessage /\u003e` bubble for the streaming LLM response, and saving the finished LLM response into the AIState history when the LLM has finished its answer. This allows the LLM to see the whole conversation when the user sends follow-up messages, without the client needing to send the entire conversation each time.\n\n### Tips \u0026 Tricks\n\n#### Page-level integration\n\nThe fastest way to add InformAI to your app is by doing so at the page level. Below is an example from the [lansaver application](https://github.com/edspencer/lansaver), which is a nextjs app that backs up configurations for network devices like firewalls and managed switches ([see the full SchedulePage component here](https://github.com/edspencer/lansaver/blob/main/app/schedules/%5Bid%5D/page.tsx)).\n\nThis is a React Server Component, rendered on the server. It imports the `\u003cInformAI\u003e` React component, defines a `prompt` string to help the LLM understand what this component does, and then renders `\u003cInformAI\u003e` with a meaningful component `name`, the `prompt`, and an arbitrary `props` object, which is passed to the LLM in addition to the name and prompt:\n\n```tsx\nimport { InformAI } from \"inform-ai\";\n\nconst prompt = `A page that shows the details of a schedule. It should show the schedule's configuration, the devices in the schedule, and recent jobs for the schedule. It should also have buttons to run the schedule, edit the schedule, and delete the schedule.`;\n\nexport default async function SchedulePage({ params: { id } }: { params: { id: string } }) {\n  const schedule = await getSchedule(parseInt(id, 10));\n\n  if (!schedule) {\n    return notFound();\n  }\n\n  const devices = await getScheduleDevices(schedule.id);\n  const jobs = await recentJobs(schedule.id);\n\n  return (\n    \u003cdiv className=\"flex flex-col gap-8\"\u003e\n      \u003cInformAI name=\"Schedule Detail Page\" prompt={prompt} props={{ schedule, devices, jobs }} /\u003e\n      \u003cdiv className=\"flex w-full flex-wrap items-end justify-between\"\u003e\n        \u003cHeading\u003eSchedule Details\u003c/Heading\u003e\n        \u003cdiv className=\"flex gap-4\"\u003e\n          \u003cRunScheduleForm schedule={schedule} /\u003e\n\n          \u003cButton href={`/schedules/${schedule.id}/edit`}\u003eEdit\u003c/Button\u003e\n          \u003cDeleteScheduleButton schedule={schedule} /\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv className=\"flex flex-col md:flex-row gap-8\"\u003e\n        \u003cScheduleConfiguration schedule={schedule} /\u003e\n        \u003cDevicesList devices={devices} /\u003e\n      \u003c/div\u003e\n      \u003cSubheading className=\"mt-8 mb-2\"\u003eRecent Jobs\u003c/Subheading\u003e\n      \u003cNoContentYet items={jobs} message=\"No recent jobs\" /\u003e\n      {jobs.length ? \u003cJobsTable jobs={jobs} /\u003e : null}\n      {/* \u003cRecentBackups deviceId={schedule.id} /\u003e */}\n    \u003c/div\u003e\n  );\n}\n```\n\nIn this case we passed the `schedule` (a row from the database), `devices` (an array of device database rows) and `jobs` (an array of recent backup jobs) to the LLM, but we could have passed anything into `props`, so long as it is serializable into JSON. Next time the user sends the LLM a message, it will also receive all of the context we just exposed to it about this page, so can answer questions about what the user is looking at.\n\nWhen possible, it is usually better to use InformAI at the component level rather than the page level to take advantage of React's composability, but it's really up to you.\n\n#### Streaming UI using Tools\n\nWe can extend our use of streamUI and other functions like it by providing tools definitions for the LLM to choose from. The streamUI() function and its UI streaming capabilities are 100% Vercel AI SDK functionality and not InformAI itself, but InformAI plays well with it and supports streaming UI instead of/in addition to streaming text responses from the LLM:\n\n```tsx\n//inside our submitUserMessage function\nconst result = await streamUI({\n  model: openai(\"gpt-4o-2024-08-06\"),\n  system: \"You are a helpful assistant who blah blah blah\",\n  messages: aiState.get().messages,\n  text: ({ content, done }) =\u003e {\n    if (done) {\n      //save the LLM's response to our AIState\n      aiState.done({\n        ...aiState.get(),\n        messages: [...aiState.get().messages, { role: \"assistant\", content }],\n      });\n    }\n\n    return \u003cAssistantMessage content={content} /\u003e;\n  },\n  tools: {\n    redirect: RedirectTool,\n    backupsTable: BackupsTableTool,\n  },\n});\n```\n\nIn the code snippet above we defined 2 tools that the LLM can execute if it thinks it makes sense to do so. If the tool has a `generate` function, it can render arbitrary React components that will be streamed to the browser. Tools can be defined inline but they're easier to read, test and swap in/out when extracted into their own files (tool-calling LLMs like those from OpenAI are still not great at picking the right tool when given too many options).\n\nHere's a real-world example of a tool definition used in the [LANsaver](https://github.com/edspencer/lansaver) project ([see the full tool source](https://github.com/edspencer/lansaver/blob/main/app/tools/BackupsTable.tsx)). Most of this file is just textual description telling the LLM what the tool is and how to use it. The important part of the tool is the `generate` function:\n\n```tsx\nimport { z } from \"zod\";\nimport { BackupsTable } from \"@/components/backup/table\";\nimport { getDeviceBackups, getDeviceByHostname } from \"@/models/device\";\nimport { getPaginatedBackups } from \"@/models/backup\";\nimport { Spinner } from \"@/components/common/spinner\";\n\ntype BackupsTableToolParameters = {\n  condensed?: boolean;\n  showDevice?: boolean;\n  deviceId?: number;\n  deviceName?: string;\n  perPage?: number;\n  name?: string;\n};\n\nconst BackupsTableTool = {\n  //tells the LLM what this tool is and when to use it\n  description:\n    \"Renders a table of backups. Optionally, you can show the device column and condense the table. If the user requests to see all backups, do not pass in a deviceId.\",\n\n  //tells the LLM how to invoke the tool, what the arguments are and which are optional\n  parameters: z.object({\n    condensed: z\n      .boolean()\n      .optional()\n      .describe(\"Set to true to condense the table and hide some of the extraneous columns\"),\n    showDevice: z.boolean().optional().describe(\"Set to true to show the device column\"),\n    deviceId: z\n      .number()\n      .optional()\n      .describe(\"The ID of the device to show backups for (do not set to show all backups)\"),\n    deviceName: z\n      .string()\n      .optional()\n      .describe(\n        \"The name of the device to show backups for. Pass this if the user asks for backups for a device by name. The tool will perform a fuzzy search for this device\"\n      ),\n    perPage: z.number().optional().describe(\"The number of backups to show per page (defaults to 5)\"),\n    name: z.string().optional().describe(\"A name to give to this table. For example, 'Recent Backups for Device X'\"),\n  }),\n\n  //if the LLM decides to call this tool, generate will be called with the arguments the LLM decided to use\n  //this function can yield a temporary piece of UI, like a spinner, and then return the permanent UI when ready\n  generate: async function* (config: BackupsTableToolParameters) {\n    const { condensed, showDevice, deviceId, deviceName, perPage = 5, name } = config;\n\n    let backups;\n\n    yield \u003cSpinner /\u003e;\n\n    if (deviceName) {\n      // Perform a fuzzy search for the device\n      const device = await getDeviceByHostname(deviceName);\n\n      if (device) {\n        backups = await getDeviceBackups(device.id, { take: perPage });\n      }\n    } else if (deviceId) {\n      backups = await getDeviceBackups(deviceId, { take: perPage });\n    }\n\n    if (!backups) {\n      backups = (await getPaginatedBackups({ includeDevice: true, page: 1, perPage })).backups;\n    }\n\n    return \u003cBackupsTable name={name} backups={backups} condensed={condensed} showDevice={showDevice} /\u003e;\n  },\n};\n\nexport default BackupsTableTool;\n```\n\nNote that this is all just vanilla Vercel AI SDK functionality, and you can read more about it [in their docs](https://sdk.vercel.ai/examples/next-app/interface/route-components). Basically, though, this function `yield`s a Spinner component while it is loading the data for the real component it will show, then does some basic fuzzy searching, then finally returns the `\u003cBackupsTable\u003e` component, which will be streamed to the UI.\n\nThe [Render Interface During Tool Call](https://sdk.vercel.ai/examples/next-app/tools/render-interface-during-tool-call) documentation in the Vercel AI SDK is a good thing to read if you're not familiar with what that can do already.\n\n### What the LLM sees\n\nHere's an example of a quick conversation with the LLM after just integrating InformAI into the `SchedulePage` component that we showed above. `SchedulePage` is just a Next JS page (therefore a React component) that shows some simple details about a backup schedule for devices on a network:\n\n![Example Chat on Schedules page](/docs/inform-ai-chat-example.png)\n\nBy just adding the `\u003cInformAI /\u003e` tag to our `SchedulePage`, we were able to have this conversation with the LLM about its contents, without having to do any other integration. But because we also defined our `FirewallsTableTool` tool, the LLM knew how to stream our `\u003cBackupsTable\u003e` component back instead of a text response.\n\nBecause `\u003cBackupsTable\u003e` also uses InformAI, via the `useInformAI` hook, the LLM also sees all of the information in the React component that it just streamed back, so it was able to answer questions about that too (\"did all of those complete successfully?\").\n\nAs our project was using the bundled `\u003cCurrentState\u003e` component while we had this conversation, we could easily see what the state sent to the LLM looks like directly in our UI:\n\n![CurrentState after this exchange](/docs/current-state-example-2.png)\n\nHere you can see that 4 `state` messages were published to InformAI - the first 2 were for the `SchedulePage` (which has name=`Schedule Detail Page`), and 2 for the freshly-streamed `\u003cBackupsTable\u003e` that the LLM sent back. Expanding the last message there, we can see that the LLM gave the streamed component a sensible name based on the user's request, and also has the `prompt` and `props` that we supply it in `\u003cBackupsTable\u003e`.\n\nThe 'Last Sent' message at the bottom tells us that all of the messages above were already sent to the LLM, as they were popped off the stack using `popRecentMessages` in our `\u003cChatWrapper\u003e` component. `ChatWrapper` also did some deduping and conversion of the messages into an LLM-friendly format. If we add `console.log(aiState.get().messages)` to our `submitUserMessage` function we will see something like this:\n\n```\n[\n  {\n    id: '492b4wc',\n    content: 'Component adc51d has updated its state\\n' +\n      'Component Name: Schedule Detail Page\\n' +\n      \"Component self-description: A page that shows the details of a schedule. It should show the schedule's configuration, the devices in the schedule, and recent jobs for the schedule. It should also have buttons to run the schedule, edit the schedule, and delete the schedule.\\n\" +\n      'Component props: {\"schedule\":{\"id\":6,\"disabled\":false,\"cron\":\"0 0 * * *\",\"name\":\"Daily\",\"devices\":\"21,19,83\",\"createdAt\":\"2024-06-14T19:55:29.825Z\",\"updatedAt\":\"2024-08-13T21:17:43.936Z\"},\"devices\":[{\"id\":21,\"type\":\"tplink\",\"hostname\":\"masterclosetswitch.local\",\"credentials\":\"fdbec89f1c1bb41b0e55d60f46092da3:7391a495c95411ebc5f087c4b8f5bcfb2b903d370cedd6a819a9e69b15f03999b9fbc4378a5254751ddb038bfec87facd5b642d0aa28b48b9ecf675b0deceb28\",\"config\":\"{}\",\"createdAt\":\"2024-06-14T19:55:29.824Z\",\"updatedAt\":\"2024-06-14T19:55:29.824Z\"},{\"id\":19,\"type\":\"opnsense\",\"hostname\":\"firewall.local\",\"credentials\":\"dd503eaafa2acdae999023a63735b7b8:9af028d461a8b3aea27c6edc013d64e98d33476d8614bdd0ad1cab42601a2517c01cc0342b6946fee8bb5a31ceaa26a659b37051da1584ba163360f9465997154ff7f9344ff5726683fe6183e6e7054f622aeeaaa2402dc416e5ae6edea6cb34ff9d80720bb9942d2ccb90015821f8fa103ec0f116bcc3532b2ff285dad80ec56c90503996b094daf52b5775b224b137a8ba0dc13d29e2e4c37d244ff10bda30bc7ed892390efc3e3ac19dd0845e7cb0e6b3cd88c2f126d2f8d9b7191f85f72f\",\"config\":\"{}\",\"createdAt\":\"2024-06-14T19:55:29.823Z\",\"updatedAt\":\"2024-06-16T16:06:39.091Z\"},{\"id\":83,\"type\":\"hass\",\"hostname\":\"13232pct.duckdns.org\",\"credentials\":\"6628e50a7bd550741dd1963ef98bfb67:107376648f66355e19787eb82036ea506a9cae6152ed98f1f1739640d2a930f30c54683c9bc3eaebd49043434afeed16b7928ba31b44048477b40d68f2a1638d83a9e1aaf83f015ffc53ed5114eb77fd90e06cfe3f52f804d9433056b985a0f00358e42d04733440e7c3c245a926266e3f5d1232022850baa970e38d8a33b032e1ccdadc563574420447cacb8498dbb637dfdfa19272cf226df112730cd8e4282e09ce99c30e0854c7ca5144426ad8f7f349fcffea7da3f7970c3ad5af9b33023ad7f057ad4144817f9df0e4c69e1466\",\"config\":\"{\\\\\"port\\\\\":\\\\\"3000\\\\\"}\",\"createdAt\":\"2024-07-16T17:13:33.455Z\",\"updatedAt\":\"2024-07-16T17:13:33.455Z\"}],\"jobs\":[{\"id\":24,\"createdAt\":\"2024-08-13T21:17:54.387Z\",\"updatedAt\":\"2024-08-13T21:18:31.499Z\",\"startedAt\":\"2024-08-13T21:17:54.400Z\",\"finishedAt\":\"2024-08-13T21:18:31.496Z\",\"status\":\"completed\",\"scheduleId\":6,\"_count\":{\"backups\":3}},{\"id\":23,\"createdAt\":\"2024-08-13T21:15:46.991Z\",\"updatedAt\":\"2024-08-13T21:15:47.571Z\",\"startedAt\":\"2024-08-13T21:15:47.004Z\",\"finishedAt\":\"2024-08-13T21:15:47.570Z\",\"status\":\"completed\",\"scheduleId\":6,\"_count\":{\"backups\":2}},{\"id\":22,\"createdAt\":\"2024-08-13T21:09:42.083Z\",\"updatedAt\":\"2024-08-13T21:09:42.083Z\",\"startedAt\":null,\"finishedAt\":null,\"status\":\"pending\",\"scheduleId\":6,\"_count\":{\"backups\":1}},{\"id\":18,\"createdAt\":\"2024-07-15T15:37:38.955Z\",\"updatedAt\":\"2024-07-15T15:38:14.366Z\",\"startedAt\":\"2024-07-15T15:37:38.967Z\",\"finishedAt\":\"2024-07-15T15:38:14.365Z\",\"status\":\"completed\",\"scheduleId\":6,\"_count\":{\"backups\":2}},{\"id\":17,\"createdAt\":\"2024-07-15T15:36:30.814Z\",\"updatedAt\":\"2024-07-15T15:37:06.483Z\",\"startedAt\":\"2024-07-15T15:36:30.828Z\",\"finishedAt\":\"2024-07-15T15:37:06.482Z\",\"status\":\"completed\",\"scheduleId\":6,\"_count\":{\"backups\":2}}]}',\n    role: 'system'\n  },\n  {\n    id: 'QfhU2Z2',\n    content: 'how many devices does this schedule back up?',\n    role: 'user'\n  },\n  { role: 'assistant', content: 'This schedule backs up 3 devices.' },\n  {\n    id: '3Rytlyw',\n    content: 'Component 2b5e1c has updated its state\\n' +\n      'Component Name: 3 Most Recent Backups for firewall.local\\n' +\n      'Component self-description: This table displays a list of backups taken for various devices. The data will be provided to you in JSON format\\n' +\n      'Component props: {\"backups\":[{\"id\":65,\"jobId\":24,\"deviceId\":19,\"createdAt\":\"2024-08-13T21:17:54.391Z\",\"updatedAt\":\"2024-08-13T21:17:54.626Z\",\"status\":\"completed\",\"bytes\":57117},{\"id\":63,\"jobId\":23,\"deviceId\":19,\"createdAt\":\"2024-08-13T21:15:46.996Z\",\"updatedAt\":\"2024-08-13T21:15:47.571Z\",\"status\":\"completed\",\"bytes\":57117},{\"id\":61,\"jobId\":22,\"deviceId\":19,\"createdAt\":\"2024-08-13T21:09:42.091Z\",\"updatedAt\":\"2024-08-13T21:09:42.091Z\",\"status\":\"pending\",\"bytes\":null}]}',\n    role: 'system'\n  },\n  {\n    id: 'Min6j9V',\n    content: 'did all of those complete successfully?',\n    role: 'user'\n  },\n  {\n    role: 'assistant',\n    content: 'Out of the three most recent backups for the device \"firewall.local,\" two backups completed successfully, and one is still pending:\\n' +\n      '\\n' +\n      '1. Backup ID 65: Completed\\n' +\n      '2. Backup ID 63: Completed\\n' +\n      '3. Backup ID 61: Pending'\n  }\n]\n```\n\nIt's a little dense to the human eye, but here we can see the first message is from the `system` role, and is a string representation of the content that we supplied to `\u003cInformAI\u003e` in our `SchedulePage` React component. After that we see our user message, followed by another `system` message that InformAI injected for us because the `\u003cBackupsTable\u003e` was streamed in as a response and published data to InformAI.\n\nThe internal messages stored by InformAI are converted into LLM-friendly strings via the [mapComponentMessages](https://github.com/edspencer/inform-ai/blob/main/src/utils.tsx) function, but it's easy to swap that out for any function you like. The default `mapComponentMessages` function just delegates to a function that looks like this:\n\n```tsx\nexport function mapStateToContent(state: StateMessage) {\n  const content = [];\n\n  const { name, componentId, prompt, props } = state.content;\n\n  content.push(`Component ${componentId} has updated its state`);\n\n  if (name) {\n    content.push(`Component Name: ${name}`);\n  }\n\n  if (prompt) {\n    content.push(`Component self-description: ${prompt}`);\n  }\n\n  if (props) {\n    content.push(`Component props: ${JSON.stringify(props)}`);\n  }\n\n  return content.join(\"\\n\");\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedspencer%2Finform-ai","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedspencer%2Finform-ai","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedspencer%2Finform-ai/lists"}