Skip to content

Commit

Permalink
Create /react sub-package (#38)
Browse files Browse the repository at this point in the history
Co-authored-by: Jared Palmer <jared@jaredpalmer.com>
  • Loading branch information
shuding and jaredpalmer committed Jun 11, 2023
1 parent 6e28253 commit 78477d3
Show file tree
Hide file tree
Showing 21 changed files with 195 additions and 1,967 deletions.
8 changes: 8 additions & 0 deletions .changeset/dull-rabbits-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"ai-connector": patch
---

- Create `/react` sub-package.
- Create `import { useChat, useCompletion } from 'ai-connector/react'` and mark React as an optional peer dependency so we can add more framework support in the future.
- Also renamed `set` to `setMessages` and `setCompletion` to unify the API naming as we have `setInput` too.
- Added an `sendExtraMessageFields` field to `useChat` that defaults to `false`, to prevent OpenAI errors when `id` is not filtered out.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function POST() {
// ./app/page.tsx
'use client'

import { useChat } from 'ai-connector'

This comment has been minimized.

Copy link
@WhiteAlexPC

WhiteAlexPC Jul 31, 2023

README.md

import { useChat } from 'ai-connector/react'

export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat()
Expand Down
2 changes: 1 addition & 1 deletion examples/with-langchain/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useChat } from 'ai-connector'
import { useChat } from 'ai-connector/react'

export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat()
Expand Down
2 changes: 1 addition & 1 deletion examples/with-openai/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useChat } from 'ai-connector'
import { useChat } from 'ai-connector/react'

export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat()
Expand Down
27 changes: 24 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,33 @@
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**"
"dist/**/*",
"dist/react/**/*"
],
"scripts": {
"build": "tsup",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"dev": "tsup --watch",
"lint": "eslint \"src/**/*.ts*\"",
"lint": "eslint \"./**/*.ts*\"",
"type-check": "tsc --noEmit",
"prettier-check": "prettier --check \"src/**/*.ts*\"",
"prettier-check": "prettier --check \"./**/*.ts*\"",
"test": "jest --env @edge-runtime/jest-environment .test.ts && jest --env node .test.ts"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"module": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./react": {
"types": "./react/dist/index.d.ts",
"import": "./react/dist/index.mjs",
"module": "./react/dist/index.mjs",
"require": "./react/dist/index.js"
}
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node"
Expand Down Expand Up @@ -45,6 +61,11 @@
"peerDependencies": {
"react": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
},
"engines": {
"node": ">=14.6"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/core/react/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './use-chat'
export * from './use-completion'
10 changes: 10 additions & 0 deletions packages/core/react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": "./dist/index.mjs",
"private": true,
"peerDependencies": {
"react": "*"
}
}
115 changes: 51 additions & 64 deletions packages/core/src/use-chat.ts → packages/core/react/use-chat.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,10 @@
import { useCallback, useId, useRef, useEffect, useState } from 'react'
import useSWRMutation from 'swr/mutation'
import useSWR from 'swr'
import { customAlphabet } from 'nanoid'

// 7-character random string
const nanoid = customAlphabet(
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
7
)

export type Message = {
id: string
createdAt?: Date
content: string
role: 'system' | 'user' | 'assistant'
}

export type CreateMessage = {
id?: string
createdAt?: Date
content: string
role: 'system' | 'user' | 'assistant'
}
import { nanoid, decodeAIStreamChunk } from './utils'

const decoder = new TextDecoder()
function decodeAIStreamChunk(chunk: Uint8Array): string {
return decoder.decode(chunk)
}
import type { Message, CreateMessage } from '../shared/types'
export type { Message, CreateMessage }

export type UseChatOptions = {
/**
Expand Down Expand Up @@ -71,27 +49,41 @@ export type UseChatOptions = {
* Extra body to be sent with the API request.
*/
body?: any

/**
* Whether to send extra message fields such as `message.id` and `message.createdAt` to the API.
* Defaults to `false`. When set to `true`, the API endpoint might need to
* handle the extra fields before forwarding the request to the AI service.
*/
sendExtraMessageFields?: boolean
}

export type UseChatHelpers = {
/** Current messages in the chat */
messages: Message[]
/** SWR's error object */
error: any
/** The error object of the API request */
error: undefined | Error
/**
* Append a message to the chat list. This trigger the API call
* to fetch to the API endpoint to get the AI response.
* Append a user message to the chat list. This triggers the API call to fetch
* the assistant's response.
*/
append: (message: Message | CreateMessage) => void
/**
* Reload the last AI chat response for the given chat history. If the last
* message isn't from the assistant, this method will do nothing.
* message isn't from the assistant, it will request the API to generate a
* new response.
*/
reload: () => void
/** Abort the current API request. */
/**
* Abort the current request immediately, keep the generated tokens if any.
*/
stop: () => void
/** Update the `messages` state locally. */
set: (messages: Message[]) => void
/**
* Update the `messages` state locally. This is useful when you want to
* edit the messages on the client, and then trigger the `reload` method
* manually to regenerate the AI response.
*/
setMessages: (messages: Message[]) => void
/** The current value of the input */
input: string
/** setState-powered method to update the input value */
Expand All @@ -100,7 +92,7 @@ export type UseChatHelpers = {
handleInputChange: (e: any) => void
/** Form submission handler to automattically reset input and append a user message */
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
/** Whether SWR-fetch is in progress */
/** Whether the API request is in progress */
isLoading: boolean
}

Expand All @@ -109,6 +101,7 @@ export function useChat({
id,
initialMessages = [],
initialInput = '',
sendExtraMessageFields,
onResponse,
onFinish,
headers,
Expand Down Expand Up @@ -166,7 +159,12 @@ export function useChat({
const res = await fetch(api, {
method: 'POST',
body: JSON.stringify({
messages: messagesSnapshot,
messages: sendExtraMessageFields
? messagesSnapshot
: messagesSnapshot.map(({ role, content }) => ({
role,
content
})),
...extraMetadataRef.current.body
}),
headers: extraMetadataRef.current.headers || {},
Expand Down Expand Up @@ -253,22 +251,16 @@ export function useChat({
}
)

/**
* Append a user message to the chat list, and trigger the API call to fetch
* the assistant's response.
*/
const append = useCallback(async (message: Message | CreateMessage) => {
if (!message.id) {
message.id = nanoid()
}
return trigger(messagesRef.current.concat(message as Message))
}, [])
const append = useCallback(
async (message: Message | CreateMessage) => {
if (!message.id) {
message.id = nanoid()
}
return trigger(messagesRef.current.concat(message as Message))
},
[trigger]
)

/**
* Reload the last AI chat response for the given chat history. If the last
* message isn't from the assistant, it will request the API to generate a
* new response.
*/
const reload = useCallback(async () => {
if (messagesRef.current.length === 0) return null

Expand All @@ -277,27 +269,22 @@ export function useChat({
return trigger(messagesRef.current.slice(0, -1))
}
return trigger(messagesRef.current)
}, [])
}, [trigger])

/**
* Abort the current request immediately, keep the generated tokens if any.
*/
const stop = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
abortControllerRef.current = null
}
}, [])

/**
* Update the `messages` state locally. This is useful when you want to
* edit the messages on the client, and then trigger the `reload` method
* manually to regenerate the AI response.
*/
const set = useCallback((messages: Message[]) => {
mutate(messages, false)
messagesRef.current = messages
}, [])
const setMessages = useCallback(
(messages: Message[]) => {
mutate(messages, false)
messagesRef.current = messages
},
[mutate]
)

// Input state and handlers.
const [input, setInput] = useState(initialInput)
Expand Down Expand Up @@ -325,7 +312,7 @@ export function useChat({
append,
reload,
stop,
set,
setMessages,
input,
setInput,
handleInputChange,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { useCallback, useEffect, useId, useRef, useState } from 'react'
import useSWRMutation from 'swr/mutation'
import useSWR from 'swr'

const decoder = new TextDecoder()
function decodeAIStreamChunk(chunk: Uint8Array): string {
return decoder.decode(chunk)
}
import { decodeAIStreamChunk } from './utils'

export type UseCompletionOptions = {
/**
Expand Down Expand Up @@ -51,15 +47,45 @@ export type UseCompletionOptions = {
}

export type UseCompletionHelpers = {
/** The current completion result */
completion: string
/**
* Send a new prompt to the API endpoint and update the completion state.
*/
complete: (prompt: string) => void
error: any
set: (completion: string) => void
/** The error object of the API request */
error: undefined | Error
/**
* Abort the current API request but keep the generated tokens.
*/
stop: () => void
/**
* Update the `completion` state locally.
*/
setCompletion: (completion: string) => void
/** The current value of the input */
input: string
/** setState-powered method to update the input value */
setInput: React.Dispatch<React.SetStateAction<string>>
/**
* An input/textarea-ready onChange handler to control the value of the input
* @example
* ```jsx
* <input onChange={handleInputChange} value={input} />
* ```
*/
handleInputChange: (e: any) => void
/**
* Form submission handler to automattically reset input and append a user message
* @example
* ```jsx
* <form onSubmit={handleSubmit}>
* <input onChange={handleInputChange} value={input} />
* </form>
* ```
*/
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
/** Whether the API request is in progress */
isLoading: boolean
}

Expand Down Expand Up @@ -178,20 +204,14 @@ export function useCompletion({
}
)

/**
* Abort the current API request but keep the generated tokens.
*/
const stop = useCallback(() => {
if (abortController) {
abortController.abort()
setAbortController(null)
}
}, [abortController])

/**
* Update the `completion` state locally.
*/
const set = useCallback(
const setCompletion = useCallback(
(completion: string) => {
mutate(completion, false)
},
Expand All @@ -204,7 +224,7 @@ export function useCompletion({
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!input) return
trigger(input)
trigger(input)
},
[input, trigger]
)
Expand All @@ -224,7 +244,7 @@ export function useCompletion({
completion,
complete,
error,
set,
setCompletion,
stop,
input,
setInput,
Expand Down

0 comments on commit 78477d3

Please sign in to comment.