Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hydrate MDX #220

Merged
merged 18 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ai-jsx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@
"prepack": "yarn create-entry-points",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --fix",
"demo": "node dist/esm/react/jit-ui/sample.js",
"test": "jest",
"unit": "yarn build && jest",
"typecheck": "tsc -p tsconfig.json --noEmit",
Expand All @@ -338,6 +339,7 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.5.0",
"@mdx-js/mdx": "^2.3.0",
"@nick.heiner/wandb-fork": "^0.5.2-5",
"axios": "^1.4.0",
"cli-highlight": "^2.1.11",
Expand Down
8 changes: 7 additions & 1 deletion packages/ai-jsx/src/react/completion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,11 @@ 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 */
.filter(component => typeof component !== 'symbol')
.map((c) => [reactComponentName(c), c])
);
}
32 changes: 23 additions & 9 deletions packages/ai-jsx/src/react/jit-ui/mdx.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
/** @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 <Bar />`, and
* it streams out `foo <Ba`, that's not parsable.)
*
* To ensure that the result is always parsable, pass the prop `alwaysParsable`. This will buffer up intermediate streaming results until the result accumulated so far is parsable.
*
* You'll get better results with this if you use GPT-4.
*
* Use `usageExamples` to teach the model how to use your components.
*
* @see https://docs.ai-jsx.com/guides/mdx
* @see https://github.com/fixie-ai/ai-jsx/blob/main/packages/examples/src/mdx.tsx
*/
export function MdxChatCompletion({ children, usageExamples }: { children: AI.Node; usageExamples: React.ReactNode }) {
export function MdxSystemMessage(
{ usageExamples }: { usageExamples: React.ReactNode },
) {
const components = collectComponents(usageExamples);
/* prettier-ignore */
return <ChatCompletion>
<SystemMessage>
return <SystemMessage>
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:
Expand All @@ -26,7 +42,7 @@ export function MdxChatCompletion({ children, usageExamples }: { children: AI.No

=== Begin example
{`
Here is some markdown text
Here is some markdown text
<MyComponent id="123" />

# Here is more markdown text
Expand Down Expand Up @@ -109,7 +125,5 @@ export function MdxChatCompletion({ children, usageExamples }: { children: AI.No
=== Begin components
<AI.React>{usageExamples}</AI.React>
=== end components
</SystemMessage>
{children}
</ChatCompletion>;
</SystemMessage>;
}
21 changes: 6 additions & 15 deletions packages/examples/src/mdx.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
/** @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 { showInspector } from 'ai-jsx/core/inspector';
import { MdxChatCompletion } 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 }) {
Expand Down Expand Up @@ -166,20 +164,13 @@ export function App() {

// showInspector(<App />);

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(<App />, { appendOnly: true });
const rendering = AI.createRenderContext().render(<App />, { appendOnly: true });
for await (const frame of rendering) {
// console.log(frame);
// console.log(frame.slice(lastValue.length));
process.stdout.write(frame.slice(lastValue.length));
lastValue = frame;
}

// console.log(await rendering);
2 changes: 2 additions & 0 deletions packages/nextjs-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 64 additions & 0 deletions packages/nextjs-demo/src/app/building-blocks/api/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/** @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';

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:
<Card header="Sam Smith">
**Skills**: React, TypeScript, Node.js **Location**: Seattle, WA **Years of experience**: 5 **Availability**:
Full-time
</Card>
Example 2 of how you might use this component:
<Card header="Your Ferry Booking" footer="Reservation held for 20 minutes">
**Leaves** at 4:15p and **arrives** at 6:20p.
</Card>
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:
<Card header="AutoWorld">$50 for a quick car wash.</Card>
<Card header="Big Joel Big Trucks">$155 for a detailing</Card>
<Card header="Small Joel Small Trucks">$10 for some guy to spray your car with a hose.</Card>
Example 4 of how you might use this component, after writing out a report on economics: ... and that concludes the
report on economics.
<Card header="Primary Points">
* Price is determined by supply and demand * Setting price floors or ceilings cause deadweight loss. *
Interfering with the natural price can also cause shortages.
</Card>
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:
<ButtonGroup labels={['Yes', 'No']} />
Example 2 of how you might use this component (using with surrounding markdown): The system is configured. How
would you like to proceed?
<ButtonGroup labels={['Deploy to prod', 'Deploy to staging', 'Cancel']} />
Use a badge to indicate status:
<Badge color="yellow">In progress</Badge>
<Badge color="green">Complete</Badge>
Use a toggle to let users enable/disable an option:
<Toggle title="Use rocket fuel" subtitle="($7 surcharge)" />
</>
);

return (
<ChatCompletion>
<MdxSystemMessage usageExamples={usageExamples} />
<UserMessage>{query}</UserMessage>
</ChatCompletion>
);
}

export async function POST(request: NextRequest) {
const { messages } = await request.json();
const lastMessage = _.last(messages) as Message;

return new StreamingTextResponse(toTextStream(<BuildingBlocksAI query={lastMessage.content} />));
}
7 changes: 7 additions & 0 deletions packages/nextjs-demo/src/app/building-blocks/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center w-screen h-screen">
<p>Loading...</p>
</div>
);
}
17 changes: 17 additions & 0 deletions packages/nextjs-demo/src/app/building-blocks/page.tsx
Original file line number Diff line number Diff line change
@@ -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"}}';
const query = searchParams.q ?? defaultValue;
return (
<div>
<ResultContainer title="Building Blocks" description="In this demo, the AI can use building block UI components">
<InputPrompt label="Ask me anything..." defaultValue={query} />
</ResultContainer>
<BuildingBlocks topic={query} />
</div>
);
}
Loading