diff --git a/packages/ai-jsx/package.json b/packages/ai-jsx/package.json index 4bcc1dae0..0ea866478 100644 --- a/packages/ai-jsx/package.json +++ b/packages/ai-jsx/package.json @@ -4,7 +4,7 @@ "repository": "fixie-ai/ai-jsx", "bugs": "https://github.com/fixie-ai/ai-jsx/issues", "homepage": "https://ai-jsx.com", - "version": "0.7.3", + "version": "0.8.0", "volta": { "extends": "../../package.json" }, diff --git a/packages/ai-jsx/src/react/completion.tsx b/packages/ai-jsx/src/react/completion.tsx index 2c732c396..402c7847a 100644 --- a/packages/ai-jsx/src/react/completion.tsx +++ b/packages/ai-jsx/src/react/completion.tsx @@ -119,5 +119,12 @@ export function collectComponents(node: React.ReactNode | AI.Node) { } } collectComponentsRec(node, true); - return Object.fromEntries(Array.from(reactComponents).map((c) => [reactComponentName(c), c])); + return Object.fromEntries( + Array.from(reactComponents) + /* Filter out symbols, e.g. React.Fragment. + `usageExample` will often be passed as a Fragment, and we don't want to tell the AI to output fragments. + */ + .filter((component) => typeof component !== 'symbol') + .map((c) => [reactComponentName(c), c]) + ); } diff --git a/packages/ai-jsx/src/react/jit-ui/mdx.tsx b/packages/ai-jsx/src/react/jit-ui/mdx.tsx index 67fa051d6..844156e43 100644 --- a/packages/ai-jsx/src/react/jit-ui/mdx.tsx +++ b/packages/ai-jsx/src/react/jit-ui/mdx.tsx @@ -1,16 +1,28 @@ +/** @jsxImportSource ai-jsx/react */ + import * as AI from '../core.js'; -import { ChatCompletion, SystemMessage } from '../../core/completion.js'; +import { SystemMessage } from '../../core/completion.js'; import React from 'react'; import { collectComponents } from '../completion.js'; /** - * Use GPT-4 with this. + * A completion component that emits [MDX](https://mdxjs.com/). + * + * By default, the result streamed out of this component will sometimes be unparsable, as the model emits a partial value. + * (For instance, if the model is emitting the string `foo `, and + * it streams out `foo - + return You are an assistant who can use React components to work with the user. By default, you use markdown. However, if it's useful, you can also mix in the following React components: {Object.keys(components).join(', ')}. All your responses should be in MDX, which is Markdown For the Component Era. Here are instructions for how to use MDX: @@ -26,7 +38,7 @@ export function MdxChatCompletion({ children, usageExamples }: { children: AI.No === Begin example {` - Here is some markdown text + Here is some markdown text # Here is more markdown text @@ -109,7 +121,5 @@ export function MdxChatCompletion({ children, usageExamples }: { children: AI.No === Begin components {usageExamples} === end components - - {children} - ; + ; } diff --git a/packages/docs/docs/changelog.md b/packages/docs/docs/changelog.md index faa3ca3da..54e640dc9 100644 --- a/packages/docs/docs/changelog.md +++ b/packages/docs/docs/changelog.md @@ -1,6 +1,10 @@ # Changelog -## 0.7.3 +## 0.8.0 + +- Move `MdxChatCompletion` to be `MdxSystemMessage`. You can now put this `SystemMessage` in any `ChatCompletion` to prompt the model to give MDX output. + +## [0.7.3](https://github.com/fixie-ai/ai-jsx/commit/670ea52647138052cb116cbc56b6cc4bb49512a0) - Update readme. diff --git a/packages/examples/src/mdx.tsx b/packages/examples/src/mdx.tsx index 5a869f825..f6c2e2914 100644 --- a/packages/examples/src/mdx.tsx +++ b/packages/examples/src/mdx.tsx @@ -1,14 +1,13 @@ /** @jsxImportSource ai-jsx/react */ import * as AI from 'ai-jsx'; -import { SystemMessage, UserMessage } from 'ai-jsx/core/completion'; -// import { showInspector } from 'ai-jsx/core/inspector'; -import { MdxChatCompletion } from 'ai-jsx/react/jit-ui/mdx'; +import { SystemMessage, UserMessage, ChatCompletion } from 'ai-jsx/core/completion'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { showInspector } from 'ai-jsx/core/inspector'; +import { MdxSystemMessage } from 'ai-jsx/react/jit-ui/mdx'; import { JsonChatCompletion } from 'ai-jsx/batteries/constrained-output'; import z from 'zod'; import { OpenAI } from 'ai-jsx/lib/openai'; -import { PinoLogger } from 'ai-jsx/core/log'; -import { pino } from 'pino'; /* eslint-disable @typescript-eslint/no-unused-vars */ function Card({ header, footer, children }: { header?: string; footer?: string; children: string }) { @@ -115,7 +114,11 @@ function QuestionAndAnswer({ children }: { children: AI.Node }, { memo }: AI.Com Q: {question} {'\n'} - A: {question} + A:{' '} + + + {question} + {'\n\n'} @@ -166,19 +169,8 @@ export function App() { // showInspector(); -const logger = pino({ - name: 'ai-jsx', - level: process.env.loglevel ?? 'trace', - transport: { - target: 'pino-pretty', - options: { - colorize: true, - }, - }, -}); - let lastValue = ''; -const rendering = AI.createRenderContext({ logger: new PinoLogger(logger) }).render(, { appendOnly: true }); +const rendering = AI.createRenderContext().render(, { appendOnly: true }); for await (const frame of rendering) { process.stdout.write(frame.slice(lastValue.length)); lastValue = frame; diff --git a/packages/nextjs-demo/package.json b/packages/nextjs-demo/package.json index 86191ed09..0b3feb53b 100644 --- a/packages/nextjs-demo/package.json +++ b/packages/nextjs-demo/package.json @@ -13,6 +13,8 @@ "dependencies": { "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.18", + "@mdx-js/mdx": "^2.3.0", + "@mdx-js/react": "^2.3.0", "@octokit/graphql": "^5.0.6", "@tailwindcss/forms": "^0.5.3", "@types/node": "20.2.5", @@ -29,6 +31,7 @@ "postcss": "8.4.24", "react": "18.2.0", "react-dom": "18.2.0", + "remark-gfm": "^3.0.1", "tailwindcss": "3.3.2", "typescript": "^5.1.3" }, diff --git a/packages/nextjs-demo/src/app/500.tsx b/packages/nextjs-demo/src/app/500.tsx new file mode 100644 index 000000000..7aa29e7a9 --- /dev/null +++ b/packages/nextjs-demo/src/app/500.tsx @@ -0,0 +1,7 @@ +export default function Failure() { + return ( +
+

500

+
+ ); +} diff --git a/packages/nextjs-demo/src/app/building-blocks/api/route.tsx b/packages/nextjs-demo/src/app/building-blocks/api/route.tsx new file mode 100644 index 000000000..937757c14 --- /dev/null +++ b/packages/nextjs-demo/src/app/building-blocks/api/route.tsx @@ -0,0 +1,67 @@ +/** @jsxImportSource ai-jsx/react */ +import { NextRequest } from 'next/server'; +import { UserMessage, ChatCompletion } from 'ai-jsx/core/completion'; +import * as BuildingBlocks from '@/components/BuildingBlocks'; +const { Card, ButtonGroup, Badge, Toggle } = BuildingBlocks; +import { MdxSystemMessage } from 'ai-jsx/react/jit-ui/mdx'; +import { toTextStream } from 'ai-jsx/stream'; +import { Message, StreamingTextResponse } from 'ai'; +import _ from 'lodash'; +import { OpenAI } from 'ai-jsx/lib/openai'; + +function BuildingBlocksAI({ query }: { query: string }) { + const usageExamples = ( + <> + Use a Card to display collected information to the user. The children can be markdown. Only use the card if you + have a logically-grouped set of information to show the user, in the context of a larger response. Generally, your + entire response should not be a card. A card takes optional header and footer props. Example 1 of how you might + use this component: Here's the best candidate I found: + + **Skills**: React, TypeScript, Node.js **Location**: Seattle, WA **Years of experience**: 5 **Availability**: + Full-time + + Example 2 of how you might use this component: + + **Leaves** at 4:15p and **arrives** at 6:20p. + + Example 3 of how you might use this component (using with surrounding markdown): Sure, I'd be happy to help you + find a car wash. Here are some options: + $50 for a quick car wash. + $155 for a detailing + $10 for some guy to spray your car with a hose. + Example 4 of how you might use this component, after writing out a report on economics: ... and that concludes the + report on economics. + + * Price is determined by supply and demand * Setting price floors or ceilings cause deadweight loss. * + Interfering with the natural price can also cause shortages. + + Use a button group when the user needs to make a choice. A ButtonGroup requires a labels prop. Example 1 of how + you might use this component: + + Example 2 of how you might use this component (using with surrounding markdown): The system is configured. How + would you like to proceed? + + Use a badge to indicate status: + In progress + Complete + Use a toggle to let users enable/disable an option: + + + ); + + return ( + + + + {query} + + + ); +} + +export async function POST(request: NextRequest) { + const { messages } = await request.json(); + const lastMessage = _.last(messages) as Message; + + return new StreamingTextResponse(toTextStream()); +} diff --git a/packages/nextjs-demo/src/app/building-blocks/loading.tsx b/packages/nextjs-demo/src/app/building-blocks/loading.tsx new file mode 100644 index 000000000..05242d8fc --- /dev/null +++ b/packages/nextjs-demo/src/app/building-blocks/loading.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+

Loading...

+
+ ); +} diff --git a/packages/nextjs-demo/src/app/building-blocks/page.tsx b/packages/nextjs-demo/src/app/building-blocks/page.tsx new file mode 100644 index 000000000..66000d68d --- /dev/null +++ b/packages/nextjs-demo/src/app/building-blocks/page.tsx @@ -0,0 +1,17 @@ +import InputPrompt from '@/components/InputPrompt'; +import ResultContainer from '@/components/ResultContainer'; +import BuildingBlocks from '@/components/BuildingBlocksGenerator'; + +export default function BuildingBlocksPage({ searchParams }: { searchParams: any }) { + const defaultValue = + 'Summarize this JSON blob for me, using a Card: {"reservation":{"reservationId":"1234567890","passengerName":"John Doe","flightNumber":"ABC123","origin":"Los Angeles","destination":"New York","departureDate":"2022-01-01","departureTime":"09:00","arrivalDate":"2022-01-01","arrivalTime":"15:00"}}. Also use a Badge. And give me some other github-flavored markdown, using a table,
, and strikethrough.'; + const query = searchParams.q ?? defaultValue; + return ( +
+ + + + +
+ ); +} diff --git a/packages/nextjs-demo/src/components/BuildingBlocks.tsx b/packages/nextjs-demo/src/components/BuildingBlocks.tsx new file mode 100644 index 000000000..1f5d6e2c6 --- /dev/null +++ b/packages/nextjs-demo/src/components/BuildingBlocks.tsx @@ -0,0 +1,371 @@ +'use client'; + +import React, { ReactNode, Children, useState } from 'react'; +import classNames from 'classnames'; +import _ from 'lodash'; +// @ts-expect-error +import { Switch } from '@headlessui/react'; + +export function Button({ children, primary }: { children: ReactNode; primary?: boolean }) { + return ( + + ); +} + +export function IconButton({ children }: { children: ReactNode }) { + return ( + + ); +} + +type BackgroundColor = 'gray' | 'red' | 'yellow' | 'green' | 'blue' | 'indigo' | 'purple' | 'pink'; +export function Badge({ children, color }: { children: ReactNode; color: BackgroundColor }) { + function getColorClasses() { + switch (color) { + case 'red': + return 'bg-red-100 text-red-800 ring-red-600/20'; + case 'green': + return 'bg-green-100 text-green-800 ring-green-600/20'; + case 'yellow': + return 'bg-yellow-100 text-yellow-800 ring-yellow-600/20'; + case 'blue': + return 'bg-blue-100 text-blue-800 ring-blue-600/20'; + case 'indigo': + return 'bg-indigo-100 text-indigo-800 ring-indigo-600/20'; + case 'purple': + return 'bg-purple-100 text-purple-800 ring-purple-600/20'; + case 'pink': + return 'bg-pink-100 text-pink-800 ring-pink-600/20'; + case 'gray': + default: + return 'bg-gray-100 text-gray-800 ring-gray-600/20'; + // This makes it too sensitive to model failures. It's better + // to gracefully degrade. + // throw new Error(`Unrecognized color: ${color}`); + } + } + return ( + + {children} + + ); +} + +export function ButtonGroup({ labels }: { labels: string[] }) { + return ( + + + {labels.length > 2 && + labels.slice(1, -1).map((label, index) => ( + + ))} + + + + ); +} + +export function Card({ children, header, footer }: { children: ReactNode; header?: ReactNode; footer?: ReactNode }) { + if (header || footer) { + return ( +
+ {header &&
{header}
} + +
{children}
+ {footer &&
{footer}
} +
+ ); + } + return ( +
+
{children}
+
+ ); +} + +export function CardList({ children }: { children: ReactNode[] }) { + return ( +
    + {Children.map(children, (child, index) => ( +
  • + {child} +
  • + ))} +
+ ); +} + +/** + * Example: + * + + + Example 2: + + + */ +export function InputWithLabel({ + label, + type, + id, + exampleValue, + helpText, +}: { + label: string; + type: string; + id: string; + /* An example value. This is NOT the place for help text. */ + exampleValue: string; + + /* Text guiding the user on how to fill out the input. */ + helpText?: string; +}) { + return ( +
+
+ + +
+ {helpText && ( +

+ {helpText} +

+ )} +
+ ); +} + +export function TextAreaInput({ label, id, defaultValue }: { label: string; id: string; defaultValue: string }) { + return ( +
+ +
+