Refactor to use hooks (#436)
This commit is contained in:
parent
124efca9a1
commit
cb60f8b143
139 changed files with 8871 additions and 8726 deletions
|
|
@ -1,588 +0,0 @@
|
|||
import 'server-only'
|
||||
|
||||
import {
|
||||
createAI,
|
||||
createStreamableUI,
|
||||
getMutableAIState,
|
||||
getAIState,
|
||||
streamUI,
|
||||
createStreamableValue
|
||||
} from 'ai/rsc'
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
|
||||
import {
|
||||
spinner,
|
||||
BotCard,
|
||||
BotMessage,
|
||||
SystemMessage,
|
||||
Stock,
|
||||
Purchase
|
||||
} from '@/components/stocks'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { EventsSkeleton } from '@/components/stocks/events-skeleton'
|
||||
import { Events } from '@/components/stocks/events'
|
||||
import { StocksSkeleton } from '@/components/stocks/stocks-skeleton'
|
||||
import { Stocks } from '@/components/stocks/stocks'
|
||||
import { StockSkeleton } from '@/components/stocks/stock-skeleton'
|
||||
import {
|
||||
formatNumber,
|
||||
runAsyncFnWithoutBlocking,
|
||||
sleep,
|
||||
nanoid
|
||||
} from '@/lib/utils'
|
||||
import { saveChat } from '@/app/actions'
|
||||
import { SpinnerMessage, UserMessage } from '@/components/stocks/message'
|
||||
import { Chat, Message } from '@/lib/types'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
async function confirmPurchase(symbol: string, price: number, amount: number) {
|
||||
'use server'
|
||||
|
||||
const aiState = getMutableAIState<typeof AI>()
|
||||
|
||||
const purchasing = createStreamableUI(
|
||||
<div className="inline-flex items-start gap-1 md:items-center">
|
||||
{spinner}
|
||||
<p className="mb-2">
|
||||
Purchasing {amount} ${symbol}...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const systemMessage = createStreamableUI(null)
|
||||
|
||||
runAsyncFnWithoutBlocking(async () => {
|
||||
await sleep(1000)
|
||||
|
||||
purchasing.update(
|
||||
<div className="inline-flex items-start gap-1 md:items-center">
|
||||
{spinner}
|
||||
<p className="mb-2">
|
||||
Purchasing {amount} ${symbol}... working on it...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
await sleep(1000)
|
||||
|
||||
purchasing.done(
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
You have successfully purchased {amount} ${symbol}. Total cost:{' '}
|
||||
{formatNumber(amount * price)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
systemMessage.done(
|
||||
<SystemMessage>
|
||||
You have purchased {amount} shares of {symbol} at ${price}. Total cost ={' '}
|
||||
{formatNumber(amount * price)}.
|
||||
</SystemMessage>
|
||||
)
|
||||
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'system',
|
||||
content: `[User has purchased ${amount} shares of ${symbol} at ${price}. Total cost = ${
|
||||
amount * price
|
||||
}]`
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
purchasingUI: purchasing.value,
|
||||
newMessage: {
|
||||
id: nanoid(),
|
||||
display: systemMessage.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitUserMessage(content: string) {
|
||||
'use server'
|
||||
|
||||
const aiState = getMutableAIState<typeof AI>()
|
||||
|
||||
aiState.update({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'user',
|
||||
content
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
let textStream: undefined | ReturnType<typeof createStreamableValue<string>>
|
||||
let textNode: undefined | React.ReactNode
|
||||
|
||||
const result = await streamUI({
|
||||
model: openai('gpt-3.5-turbo'),
|
||||
initial: <SpinnerMessage />,
|
||||
system: `\
|
||||
You are a stock trading conversation bot and you can help users buy stocks, step by step.
|
||||
You and the user can discuss stock prices and the user can adjust the amount of stocks they want to buy, or place an order, in the UI.
|
||||
|
||||
Messages inside [] means that it's a UI element or a user event. For example:
|
||||
- "[Price of AAPL = 100]" means that an interface of the stock price of AAPL is shown to the user.
|
||||
- "[User has changed the amount of AAPL to 10]" means that the user has changed the amount of AAPL to 10 in the UI.
|
||||
|
||||
If the user requests purchasing a stock, call \`show_stock_purchase_ui\` to show the purchase UI.
|
||||
If the user just wants the price, call \`show_stock_price\` to show the price.
|
||||
If you want to show trending stocks, call \`list_stocks\`.
|
||||
If you want to show events, call \`get_events\`.
|
||||
If the user wants to sell stock, or complete another impossible task, respond that you are a demo and cannot do that.
|
||||
|
||||
Besides that, you can also chat with users and do some calculations if needed.`,
|
||||
messages: [
|
||||
...aiState.get().messages.map((message: any) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
name: message.name
|
||||
}))
|
||||
],
|
||||
text: ({ content, done, delta }) => {
|
||||
if (!textStream) {
|
||||
textStream = createStreamableValue('')
|
||||
textNode = <BotMessage content={textStream.value} />
|
||||
}
|
||||
|
||||
if (done) {
|
||||
textStream.done()
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'assistant',
|
||||
content
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
textStream.update(delta)
|
||||
}
|
||||
|
||||
return textNode
|
||||
},
|
||||
tools: {
|
||||
listStocks: {
|
||||
description: 'List three imaginary stocks that are trending.',
|
||||
parameters: z.object({
|
||||
stocks: z.array(
|
||||
z.object({
|
||||
symbol: z.string().describe('The symbol of the stock'),
|
||||
price: z.number().describe('The price of the stock'),
|
||||
delta: z.number().describe('The change in price of the stock')
|
||||
})
|
||||
)
|
||||
}),
|
||||
generate: async function* ({ stocks }) {
|
||||
yield (
|
||||
<BotCard>
|
||||
<StocksSkeleton />
|
||||
</BotCard>
|
||||
)
|
||||
|
||||
await sleep(1000)
|
||||
|
||||
const toolCallId = nanoid()
|
||||
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolName: 'listStocks',
|
||||
toolCallId,
|
||||
args: { stocks }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'listStocks',
|
||||
toolCallId,
|
||||
result: stocks
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<BotCard>
|
||||
<Stocks props={stocks} />
|
||||
</BotCard>
|
||||
)
|
||||
}
|
||||
},
|
||||
showStockPrice: {
|
||||
description:
|
||||
'Get the current stock price of a given stock or currency. Use this to show the price to the user.',
|
||||
parameters: z.object({
|
||||
symbol: z
|
||||
.string()
|
||||
.describe(
|
||||
'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.'
|
||||
),
|
||||
price: z.number().describe('The price of the stock.'),
|
||||
delta: z.number().describe('The change in price of the stock')
|
||||
}),
|
||||
generate: async function* ({ symbol, price, delta }) {
|
||||
yield (
|
||||
<BotCard>
|
||||
<StockSkeleton />
|
||||
</BotCard>
|
||||
)
|
||||
|
||||
await sleep(1000)
|
||||
|
||||
const toolCallId = nanoid()
|
||||
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolName: 'showStockPrice',
|
||||
toolCallId,
|
||||
args: { symbol, price, delta }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'showStockPrice',
|
||||
toolCallId,
|
||||
result: { symbol, price, delta }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<BotCard>
|
||||
<Stock props={{ symbol, price, delta }} />
|
||||
</BotCard>
|
||||
)
|
||||
}
|
||||
},
|
||||
showStockPurchase: {
|
||||
description:
|
||||
'Show price and the UI to purchase a stock or currency. Use this if the user wants to purchase a stock or currency.',
|
||||
parameters: z.object({
|
||||
symbol: z
|
||||
.string()
|
||||
.describe(
|
||||
'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.'
|
||||
),
|
||||
price: z.number().describe('The price of the stock.'),
|
||||
numberOfShares: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
'The **number of shares** for a stock or currency to purchase. Can be optional if the user did not specify it.'
|
||||
)
|
||||
}),
|
||||
generate: async function* ({ symbol, price, numberOfShares = 100 }) {
|
||||
const toolCallId = nanoid()
|
||||
|
||||
if (numberOfShares <= 0 || numberOfShares > 1000) {
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolName: 'showStockPurchase',
|
||||
toolCallId,
|
||||
args: { symbol, price, numberOfShares }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'showStockPurchase',
|
||||
toolCallId,
|
||||
result: {
|
||||
symbol,
|
||||
price,
|
||||
numberOfShares,
|
||||
status: 'expired'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'system',
|
||||
content: `[User has selected an invalid amount]`
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return <BotMessage content={'Invalid amount'} />
|
||||
} else {
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolName: 'showStockPurchase',
|
||||
toolCallId,
|
||||
args: { symbol, price, numberOfShares }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'showStockPurchase',
|
||||
toolCallId,
|
||||
result: {
|
||||
symbol,
|
||||
price,
|
||||
numberOfShares
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<BotCard>
|
||||
<Purchase
|
||||
props={{
|
||||
numberOfShares,
|
||||
symbol,
|
||||
price: +price,
|
||||
status: 'requires_action'
|
||||
}}
|
||||
/>
|
||||
</BotCard>
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
getEvents: {
|
||||
description:
|
||||
'List funny imaginary events between user highlighted dates that describe stock activity.',
|
||||
parameters: z.object({
|
||||
events: z.array(
|
||||
z.object({
|
||||
date: z
|
||||
.string()
|
||||
.describe('The date of the event, in ISO-8601 format'),
|
||||
headline: z.string().describe('The headline of the event'),
|
||||
description: z.string().describe('The description of the event')
|
||||
})
|
||||
)
|
||||
}),
|
||||
generate: async function* ({ events }) {
|
||||
yield (
|
||||
<BotCard>
|
||||
<EventsSkeleton />
|
||||
</BotCard>
|
||||
)
|
||||
|
||||
await sleep(1000)
|
||||
|
||||
const toolCallId = nanoid()
|
||||
|
||||
aiState.done({
|
||||
...aiState.get(),
|
||||
messages: [
|
||||
...aiState.get().messages,
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolName: 'getEvents',
|
||||
toolCallId,
|
||||
args: { events }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'getEvents',
|
||||
toolCallId,
|
||||
result: events
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<BotCard>
|
||||
<Events props={events} />
|
||||
</BotCard>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: nanoid(),
|
||||
display: result.value
|
||||
}
|
||||
}
|
||||
|
||||
export type AIState = {
|
||||
chatId: string
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export type UIState = {
|
||||
id: string
|
||||
display: React.ReactNode
|
||||
}[]
|
||||
|
||||
export const AI = createAI<AIState, UIState>({
|
||||
actions: {
|
||||
submitUserMessage,
|
||||
confirmPurchase
|
||||
},
|
||||
initialUIState: [],
|
||||
initialAIState: { chatId: nanoid(), messages: [] },
|
||||
onGetUIState: async () => {
|
||||
'use server'
|
||||
|
||||
const session = await auth()
|
||||
|
||||
if (session && session.user) {
|
||||
const aiState = getAIState() as Chat
|
||||
|
||||
if (aiState) {
|
||||
const uiState = getUIStateFromAIState(aiState)
|
||||
return uiState
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
},
|
||||
onSetAIState: async ({ state, done }) => {
|
||||
'use server'
|
||||
|
||||
if (!done) return
|
||||
|
||||
const session = await auth()
|
||||
if (!session || !session.user) return
|
||||
|
||||
const { chatId, messages } = state
|
||||
|
||||
const createdAt = new Date()
|
||||
const userId = session.user.id as string
|
||||
const path = `/chat/${chatId}`
|
||||
|
||||
const firstMessageContent = messages[0].content as string
|
||||
const title = firstMessageContent.substring(0, 100)
|
||||
|
||||
const chat: Chat = {
|
||||
id: chatId,
|
||||
title,
|
||||
userId,
|
||||
createdAt,
|
||||
messages,
|
||||
path
|
||||
}
|
||||
|
||||
await saveChat(chat)
|
||||
}
|
||||
})
|
||||
|
||||
export const getUIStateFromAIState = (aiState: Chat) => {
|
||||
return aiState.messages
|
||||
.filter(message => message.role !== 'system')
|
||||
.map((message, index) => ({
|
||||
id: `${aiState.chatId}-${index}`,
|
||||
display:
|
||||
message.role === 'tool' ? (
|
||||
message.content.map(tool => {
|
||||
return tool.toolName === 'listStocks' ? (
|
||||
<BotCard>
|
||||
{/* TODO: Infer types based on the tool result*/}
|
||||
{/* @ts-expect-error */}
|
||||
<Stocks props={tool.result} />
|
||||
</BotCard>
|
||||
) : tool.toolName === 'showStockPrice' ? (
|
||||
<BotCard>
|
||||
{/* @ts-expect-error */}
|
||||
<Stock props={tool.result} />
|
||||
</BotCard>
|
||||
) : tool.toolName === 'showStockPurchase' ? (
|
||||
<BotCard>
|
||||
{/* @ts-expect-error */}
|
||||
<Purchase props={tool.result} />
|
||||
</BotCard>
|
||||
) : tool.toolName === 'getEvents' ? (
|
||||
<BotCard>
|
||||
{/* @ts-expect-error */}
|
||||
<Events props={tool.result} />
|
||||
</BotCard>
|
||||
) : null
|
||||
})
|
||||
) : message.role === 'user' ? (
|
||||
<UserMessage>{message.content as string}</UserMessage>
|
||||
) : message.role === 'assistant' &&
|
||||
typeof message.content === 'string' ? (
|
||||
<BotMessage content={message.content} />
|
||||
) : null
|
||||
}))
|
||||
}
|
||||
18
lib/drizzle/0000_keen_devos.sql
Normal file
18
lib/drizzle/0000_keen_devos.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
CREATE TABLE IF NOT EXISTS "Chat" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"createdAt" timestamp NOT NULL,
|
||||
"messages" json NOT NULL,
|
||||
"userId" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "User" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" varchar(64) NOT NULL,
|
||||
"password" varchar(64)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
94
lib/drizzle/meta/0000_snapshot.json
Normal file
94
lib/drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"id": "715ec9ec-6715-4d0f-9f6c-9b5c7f09827c",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.Chat": {
|
||||
"name": "Chat",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Chat_userId_User_id_fk": {
|
||||
"name": "Chat_userId_User_id_fk",
|
||||
"tableFrom": "Chat",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.User": {
|
||||
"name": "User",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
lib/drizzle/meta/_journal.json
Normal file
13
lib/drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1728598022383,
|
||||
"tag": "0000_keen_devos",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
export interface useCopyToClipboardProps {
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export function useCopyToClipboard({
|
||||
timeout = 2000
|
||||
}: useCopyToClipboardProps) {
|
||||
const [isCopied, setIsCopied] = React.useState<Boolean>(false)
|
||||
|
||||
const copyToClipboard = (value: string) => {
|
||||
if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
return { isCopied, copyToClipboard }
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { useRef, type RefObject } from 'react'
|
||||
|
||||
export function useEnterSubmit(): {
|
||||
formRef: RefObject<HTMLFormElement>
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
} {
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
const handleKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||
): void => {
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
formRef.current?.requestSubmit()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return { formRef, onKeyDown: handleKeyDown }
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const useLocalStorage = <T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T) => void] => {
|
||||
const [storedValue, setStoredValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieve from localStorage
|
||||
const item = window.localStorage.getItem(key)
|
||||
if (item) {
|
||||
setStoredValue(JSON.parse(item))
|
||||
}
|
||||
}, [key])
|
||||
|
||||
const setValue = (value: T) => {
|
||||
// Save state
|
||||
setStoredValue(value)
|
||||
// Save to localStorage
|
||||
window.localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export const useScrollAnchor = () => {
|
||||
const messagesRef = useRef<HTMLDivElement>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const visibilityRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [isAtBottom, setIsAtBottom] = useState(true)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (messagesRef.current) {
|
||||
messagesRef.current.scrollIntoView({
|
||||
block: 'end',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesRef.current) {
|
||||
if (isAtBottom && !isVisible) {
|
||||
messagesRef.current.scrollIntoView({
|
||||
block: 'end'
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [isAtBottom, isVisible])
|
||||
|
||||
useEffect(() => {
|
||||
const { current } = scrollRef
|
||||
|
||||
if (current) {
|
||||
const handleScroll = (event: Event) => {
|
||||
const target = event.target as HTMLDivElement
|
||||
const offset = 25
|
||||
const isAtBottom =
|
||||
target.scrollTop + target.clientHeight >= target.scrollHeight - offset
|
||||
|
||||
setIsAtBottom(isAtBottom)
|
||||
}
|
||||
|
||||
current.addEventListener('scroll', handleScroll, {
|
||||
passive: true
|
||||
})
|
||||
|
||||
return () => {
|
||||
current.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (visibilityRef.current) {
|
||||
let observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true)
|
||||
} else {
|
||||
setIsVisible(false)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: '0px 0px -150px 0px'
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(visibilityRef.current)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
messagesRef,
|
||||
scrollRef,
|
||||
visibilityRef,
|
||||
scrollToBottom,
|
||||
isAtBottom,
|
||||
isVisible
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'sidebar'
|
||||
|
||||
interface SidebarContext {
|
||||
isSidebarOpen: boolean
|
||||
toggleSidebar: () => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContext | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error('useSidebarContext must be used within a SidebarProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface SidebarProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SidebarProvider({ children }: SidebarProviderProps) {
|
||||
const [isSidebarOpen, setSidebarOpen] = React.useState(true)
|
||||
const [isLoading, setLoading] = React.useState(true)
|
||||
|
||||
React.useEffect(() => {
|
||||
const value = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
if (value) {
|
||||
setSidebarOpen(JSON.parse(value))
|
||||
}
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSidebarOpen(value => {
|
||||
const newState = !value
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState))
|
||||
return newState
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
value={{ isSidebarOpen, toggleSidebar, isLoading }}
|
||||
>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { StreamableValue, readStreamableValue } from 'ai/rsc'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const useStreamableText = (
|
||||
content: string | StreamableValue<string>
|
||||
) => {
|
||||
const [rawContent, setRawContent] = useState(
|
||||
typeof content === 'string' ? content : ''
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (typeof content === 'object') {
|
||||
let value = ''
|
||||
for await (const delta of readStreamableValue(content)) {
|
||||
if (typeof delta === 'string') {
|
||||
setRawContent((value = value + delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [content])
|
||||
|
||||
return rawContent
|
||||
}
|
||||
41
lib/types.ts
41
lib/types.ts
|
|
@ -1,41 +0,0 @@
|
|||
import { CoreMessage } from 'ai'
|
||||
|
||||
export type Message = CoreMessage & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface Chat extends Record<string, any> {
|
||||
id: string
|
||||
title: string
|
||||
createdAt: Date
|
||||
userId: string
|
||||
path: string
|
||||
messages: Message[]
|
||||
sharePath?: string
|
||||
}
|
||||
|
||||
export type ServerActionResult<Result> = Promise<
|
||||
| Result
|
||||
| {
|
||||
error: string
|
||||
}
|
||||
>
|
||||
|
||||
export interface Session {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthResult {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface User extends Record<string, any> {
|
||||
id: string
|
||||
email: string
|
||||
password: string
|
||||
salt: string
|
||||
}
|
||||
149
lib/utils.ts
149
lib/utils.ts
|
|
@ -1,134 +1,43 @@
|
|||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
7
|
||||
) // 7-character random string
|
||||
interface ApplicationError extends Error {
|
||||
info: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export async function fetcher<JSON = any>(
|
||||
input: RequestInfo,
|
||||
init?: RequestInit
|
||||
): Promise<JSON> {
|
||||
const res = await fetch(input, init)
|
||||
export const fetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json()
|
||||
if (json.error) {
|
||||
const error = new Error(json.error) as Error & {
|
||||
status: number
|
||||
}
|
||||
error.status = res.status
|
||||
throw error
|
||||
} else {
|
||||
throw new Error('An unexpected error occurred')
|
||||
}
|
||||
const error = new Error(
|
||||
"An error occurred while fetching the data.",
|
||||
) as ApplicationError;
|
||||
|
||||
error.info = await res.json();
|
||||
error.status = res.status;
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export function formatDate(input: string | number | Date): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
export const formatNumber = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(value)
|
||||
|
||||
export const runAsyncFnWithoutBlocking = (
|
||||
fn: (...args: any) => Promise<any>
|
||||
) => {
|
||||
fn()
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export const getStringFromBuffer = (buffer: ArrayBuffer) =>
|
||||
Array.from(new Uint8Array(buffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
export enum ResultCode {
|
||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||
InvalidSubmission = 'INVALID_SUBMISSION',
|
||||
UserAlreadyExists = 'USER_ALREADY_EXISTS',
|
||||
UnknownError = 'UNKNOWN_ERROR',
|
||||
UserCreated = 'USER_CREATED',
|
||||
UserLoggedIn = 'USER_LOGGED_IN'
|
||||
}
|
||||
|
||||
export const getMessageFromCode = (resultCode: string) => {
|
||||
switch (resultCode) {
|
||||
case ResultCode.InvalidCredentials:
|
||||
return 'Invalid credentials!'
|
||||
case ResultCode.InvalidSubmission:
|
||||
return 'Invalid submission, please try again!'
|
||||
case ResultCode.UserAlreadyExists:
|
||||
return 'User already exists, please log in!'
|
||||
case ResultCode.UserCreated:
|
||||
return 'User created, welcome!'
|
||||
case ResultCode.UnknownError:
|
||||
return 'Something went wrong, please try again!'
|
||||
case ResultCode.UserLoggedIn:
|
||||
return 'Logged in!'
|
||||
export function getLocalStorage(key: string) {
|
||||
if (typeof window !== "undefined") {
|
||||
return JSON.parse(localStorage.getItem(key) || "[]");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function format(date: Date, formatString: string) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth()
|
||||
const day = date.getDate()
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
const monthNames = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
|
||||
return formatString
|
||||
.replace('yyyy', year.toString())
|
||||
.replace('yy', String(year).slice(-2))
|
||||
.replace('LLL', monthNames[month])
|
||||
.replace('MM', String(month + 1).padStart(2, '0'))
|
||||
.replace('dd', String(day).padStart(2, '0'))
|
||||
.replace('d', day.toString())
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
export function parseISO(dateString: string) {
|
||||
return new Date(dateString)
|
||||
}
|
||||
|
||||
export function subMonths(date: Date, amount: number) {
|
||||
const newDate: Date = new Date(date)
|
||||
newDate.setMonth(newDate.getMonth() - amount)
|
||||
return newDate
|
||||
export function generateUUID(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue