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 (
+
+ );
+}
+
+export function StackedForm({
+ children,
+ cancelLabel,
+ submitLabel,
+}: {
+ children: ReactNode;
+ cancelLabel: ReactNode;
+ submitLabel: ReactNode;
+}) {
+ return (
+
+ );
+}
+
+// TODO: What's the best way to give the UI icons? Maybe stick to emojis for now?
diff --git a/packages/nextjs-demo/src/components/BuildingBlocksGenerator.tsx b/packages/nextjs-demo/src/components/BuildingBlocksGenerator.tsx
new file mode 100644
index 000000000..e66f3ec70
--- /dev/null
+++ b/packages/nextjs-demo/src/components/BuildingBlocksGenerator.tsx
@@ -0,0 +1,98 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import Image from 'next/image';
+import * as BuildingBlocks from './BuildingBlocks';
+import * as runtime from 'react/jsx-runtime';
+import { useChat } from 'ai/react';
+
+// I don't feel like messing with the build system to fix this.
+// We get the error:
+// "The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("@mdx-js/mdx")' call instead."
+// @ts-expect-error
+import { run, compile } from '@mdx-js/mdx';
+// @ts-expect-error
+import remarkGFM from 'remark-gfm';
+
+export default function BuildingBlockGenerator({ topic }: { topic: string }) {
+ const { messages, isLoading, append } = useChat({
+ api: '/building-blocks/api',
+ });
+
+ useEffect(() => {
+ if (messages.length === 0) {
+ append({
+ id: '0',
+ role: 'user',
+ content: topic,
+ });
+ }
+ }, [messages]);
+
+ const [mdx, setMdx] = useState(null);
+
+ function getAIResponse() {
+ if (messages.length <= 1) {
+ return null;
+ }
+ return messages[messages.length - 1].content;
+ }
+
+ useEffect(() => {
+ (async () => {
+ const aiResponse = getAIResponse();
+ if (!aiResponse) {
+ return;
+ }
+ let compiled;
+ try {
+ compiled = String(
+ await compile(aiResponse, {
+ outputFormat: 'function-body',
+ // If we enable this, we get slightly nicer error messages.
+ // But we also get _jsxDev is not a function.
+ // This seems surmountable but also not something I want to attend to now.
+ development: false,
+ remarkPlugins: [remarkGFM],
+ })
+ );
+ } catch {
+ console.log(
+ 'Cannot parse MDX. If the stream is still coming in, this is fine. But if you were expecting this to be parsable, then there may be a bug.',
+ aiResponse
+ );
+ return;
+ }
+ const { default: Content } = await run(compiled, runtime);
+
+ const components = {
+ table: (props: any) =>