Refactor to use hooks (#436)

This commit is contained in:
Jeremy 2024-10-11 18:00:22 +05:30 committed by GitHub
parent 124efca9a1
commit cb60f8b143
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 8871 additions and 8726 deletions

View file

@ -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
}))
}

View 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 $$;

View 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": {}
}
}

View file

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1728598022383,
"tag": "0000_keen_devos",
"breakpoints": true
}
]
}

View file

@ -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 }
}

View file

@ -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 }
}

View file

@ -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]
}

View file

@ -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
}
}

View file

@ -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>
)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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);
});
}