From 469158ed2f0839a2918993f334acd3e05d06a9a9 Mon Sep 17 00:00:00 2001 From: Yiyun Yao <106117469+nyonyoko@users.noreply.github.com> Date: Sat, 14 Sep 2024 18:29:34 -0400 Subject: [PATCH 01/11] Init --- .env.example | 10 +- .gitignore | 1 + app/layout.tsx | 8 +- components/providers.tsx | 13 +- lib/chat/actions.tsx | 326 +-------------------------------------- middleware.ts | 6 +- package.json | 1 + pnpm-lock.yaml | 170 ++++++++++++++++++++ 8 files changed, 207 insertions(+), 328 deletions(-) diff --git a/.env.example b/.env.example index 098f3f62d..4de4ec6df 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ -# You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview -# Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys -OPENAI_API_KEY=XXXXXXXX + +# Get your Tune Studio API Key here: https://tune.studio/account/api-keys +TUNE_STUDIO_API_KEY=XXXXXXXX + +# Clerk +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=XXXXXXXX +CLERK_SECRET=XXXXXXXX # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` AUTH_SECRET=XXXXXXXX diff --git a/.gitignore b/.gitignore index dd019e403..229eee340 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ yarn-error.log* # local env files .env.local +.env .env.development.local .env.test.local .env.production.local diff --git a/app/layout.tsx b/app/layout.tsx index a32b2230f..26cd02189 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,7 @@ import { TailwindIndicator } from '@/components/tailwind-indicator' import { Providers } from '@/components/providers' import { Header } from '@/components/header' import { Toaster } from '@/components/ui/sonner' +import { ClerkProvider, SignedIn, SignedOut, SignInButton } from '@clerk/nextjs' export const metadata = { metadataBase: process.env.VERCEL_URL @@ -54,7 +55,12 @@ export default function RootLayout({ children }: RootLayoutProps) { >
-
{children}
+
+ {children} + + + +
diff --git a/components/providers.tsx b/components/providers.tsx index 0a8de4a15..4a9ca9915 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -5,13 +5,16 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes' import { ThemeProviderProps } from 'next-themes/dist/types' import { SidebarProvider } from '@/lib/hooks/use-sidebar' import { TooltipProvider } from '@/components/ui/tooltip' +import { ClerkProvider } from '@clerk/nextjs' export function Providers({ children, ...props }: ThemeProviderProps) { return ( - - - {children} - - + + + + {children} + + + ) } diff --git a/lib/chat/actions.tsx b/lib/chat/actions.tsx index ca6de3d81..ab27c59bd 100644 --- a/lib/chat/actions.tsx +++ b/lib/chat/actions.tsx @@ -8,8 +8,7 @@ import { streamUI, createStreamableValue } from 'ai/rsc' -import { openai } from '@ai-sdk/openai' - +import { createOpenAI } from '@ai-sdk/openai' import { spinner, BotCard, @@ -36,6 +35,11 @@ import { SpinnerMessage, UserMessage } from '@/components/stocks/message' import { Chat, Message } from '@/lib/types' import { auth } from '@/auth' +const openai = createOpenAI({ + baseURL: 'https://proxy.tune.app', + apiKey: process.env.TUNE_STUDIO_API_KEY +}) + async function confirmPurchase(symbol: string, price: number, amount: number) { 'use server' @@ -127,23 +131,9 @@ async function submitUserMessage(content: string) { let textNode: undefined | React.ReactNode const result = await streamUI({ - model: openai('gpt-3.5-turbo'), + model: openai('meta/llama-3.1-8b-instruct'), initial: , - 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.`, + system: `Only reply with the letter "a""`, messages: [ ...aiState.get().messages.map((message: any) => ({ role: message.role, @@ -175,306 +165,6 @@ async function submitUserMessage(content: string) { } 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 ( - - - - ) - - 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 ( - - - - ) - } - }, - 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 ( - - - - ) - - 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 ( - - - - ) - } - }, - 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 - } 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 ( - - - - ) - } - } - }, - 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 ( - - - - ) - - 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 ( - - - - ) - } - } } }) diff --git a/middleware.ts b/middleware.ts index e663a3164..d20c6c765 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,7 +1,11 @@ import NextAuth from 'next-auth' import { authConfig } from './auth.config' +import { clerkMiddleware } from '@clerk/nextjs/server' -export default NextAuth(authConfig).auth +// fix this +export default clerkMiddleware(() => { + NextAuth(authConfig).auth +}) export const config = { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'] diff --git a/package.json b/package.json index 9f9fab680..5de065497 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ai-sdk/openai": "^0.0.53", + "@clerk/nextjs": "^5.5.2", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94e78e2e0..109dd1b37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ai-sdk/openai': specifier: ^0.0.53 version: 0.0.53(zod@3.23.8) + '@clerk/nextjs': + specifier: ^5.5.2 + version: 5.5.2(next@14.2.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-alert-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -251,6 +254,41 @@ packages: resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} engines: {node: '>=6.9.0'} + '@clerk/backend@1.11.1': + resolution: {integrity: sha512-g+jk1wxS0j6s1or6e3rf8KK4bHaIxajCMHAASyhfl9a9OVBqtkeMgbQ3+LIFDRAOSQLxLKrIqJgPGPfOoHz17Q==} + engines: {node: '>=18.17.0'} + + '@clerk/clerk-react@5.8.2': + resolution: {integrity: sha512-5wXr02TmxlGBjBTrM5URCk01b0q/Po6xg3SPo/U8HgrQ8qnY82hbnLxZ1dUuqH3MIzUh2VAoISJzF4TEZYqJJA==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: '>=18 || >=19.0.0-beta' + react-dom: '>=18 || >=19.0.0-beta' + + '@clerk/nextjs@5.5.2': + resolution: {integrity: sha512-xBxwKzjvaJOHY8iCD5AwBU39owLUJw2rC6ndlC+R7mlPTXBrGCjLUNPh8vkokM2z3qACM4mDOGGchj2L8jJ7GQ==} + engines: {node: '>=18.17.0'} + peerDependencies: + next: ^13.5.4 || ^14.0.3 || >=15.0.0-rc + react: '>=18 || >=19.0.0-beta' + react-dom: '>=18 || >=19.0.0-beta' + + '@clerk/shared@2.7.2': + resolution: {integrity: sha512-0SymNLqE5oMPf1XwtqNazNcpIoCKUv77f8rHpx4U8mg73uXYfuEQThNgCJyoM4/qxYLL3SBPKAlZl9MAHfSiyA==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: '>=18 || >=19.0.0-beta' + react-dom: '>=18 || >=19.0.0-beta' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/types@4.20.1': + resolution: {integrity: sha512-s2v3wFgLsB+d0Ot5yN+5IjRNKWl63AAeEczTZDZYSWuNkGihvEXYjS2NtnYuhROBRgWEHEsm0JOp0rQkfTMkBw==} + engines: {node: '>=18.17.0'} + '@floating-ui/core@1.6.7': resolution: {integrity: sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==} @@ -1028,6 +1066,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -1048,6 +1090,9 @@ packages: engines: {node: '>=4'} hasBin: true + csstype@3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1087,6 +1132,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1203,6 +1251,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -1295,6 +1346,10 @@ packages: jose@5.7.0: resolution: {integrity: sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1344,6 +1399,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -1353,6 +1411,10 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} @@ -1574,6 +1636,9 @@ packages: sass: optional: true + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -1870,6 +1935,13 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + snakecase-keys@5.4.4: + resolution: {integrity: sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==} + engines: {node: '>=12'} + sonner@1.5.0: resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==} peerDependencies: @@ -1891,6 +1963,9 @@ packages: peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -1993,9 +2068,16 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} @@ -2248,6 +2330,53 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@clerk/backend@1.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 2.7.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.20.1 + cookie: 0.5.0 + snakecase-keys: 5.4.4 + tslib: 2.4.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/clerk-react@5.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 2.7.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.20.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.4.1 + + '@clerk/nextjs@5.5.2(next@14.2.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/backend': 1.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/clerk-react': 5.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/shared': 2.7.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.20.1 + crypto-js: 4.2.0 + next: 14.2.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + server-only: 0.0.1 + tslib: 2.4.1 + + '@clerk/shared@2.7.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/types': 4.20.1 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.7.0 + swr: 2.2.5(react@18.3.1) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@clerk/types@4.20.1': + dependencies: + csstype: 3.1.1 + '@floating-ui/core@1.6.7': dependencies: '@floating-ui/utils': 0.2.7 @@ -3010,6 +3139,8 @@ snapshots: commander@8.3.0: {} + cookie@0.5.0: {} + cookie@0.6.0: {} cross-spawn@7.0.3: @@ -3027,6 +3158,8 @@ snapshots: cssesc@3.0.0: {} + csstype@3.1.1: {} + csstype@3.1.3: {} debug@4.3.6: @@ -3051,6 +3184,11 @@ snapshots: dlv@1.1.3: {} + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.13: {} @@ -3145,6 +3283,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + glob@10.4.5: dependencies: foreground-child: 3.3.0 @@ -3233,6 +3373,8 @@ snapshots: jose@5.7.0: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} json-schema@0.4.0: {} @@ -3269,6 +3411,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lower-case@2.0.2: + dependencies: + tslib: 2.7.0 + lowlight@1.20.0: dependencies: fault: 1.0.4 @@ -3280,6 +3426,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + map-obj@4.3.0: {} + markdown-table@3.0.3: {} mdast-util-definitions@5.1.2: @@ -3670,6 +3818,11 @@ snapshots: - '@babel/core' - babel-plugin-macros + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.7.0 + node-domexception@1.0.0: {} node-fetch@2.7.0: @@ -3980,6 +4133,17 @@ snapshots: signal-exit@4.1.0: {} + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.7.0 + + snakecase-keys@5.4.4: + dependencies: + map-obj: 4.3.0 + snake-case: 3.0.4 + type-fest: 2.19.0 + sonner@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -3996,6 +4160,8 @@ snapshots: svelte: 4.2.19 swrev: 4.0.0 + std-env@3.7.0: {} + streamsearch@1.1.0: {} string-width@4.2.3: @@ -4123,8 +4289,12 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.4.1: {} + tslib@2.7.0: {} + type-fest@2.19.0: {} + typescript@5.5.4: {} undici-types@5.26.5: {} From a9e2761199e30f5a6aae1367af85c456a6a9abef Mon Sep 17 00:00:00 2001 From: Yiyun Yao <106117469+nyonyoko@users.noreply.github.com> Date: Sat, 14 Sep 2024 18:39:01 -0400 Subject: [PATCH 02/11] Fixing key requiring TUNE --- app/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/actions.ts b/app/actions.ts index 1398aca02..675f136d8 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -165,7 +165,7 @@ export async function refreshHistory(path: string) { } export async function getMissingKeys() { - const keysRequired = ['OPENAI_API_KEY'] + const keysRequired = ['TUNE_STUDIO_API_KEY'] return keysRequired .map(key => (process.env[key] ? '' : key)) .filter(key => key !== '') From ac8dc67bb75aeb13ab7e014c1b2efb22ef6adb26 Mon Sep 17 00:00:00 2001 From: Yiyun Yao <106117469+nyonyoko@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:55:51 -0400 Subject: [PATCH 03/11] using clekr now --- app/(chat)/chat/[id]/page.tsx | 60 +------ app/(chat)/layout.tsx | 3 - app/(chat)/page.tsx | 5 +- app/actions.ts | 164 ------------------- app/dashboard/actions/update-heartreate.ts | 19 +++ app/dashboard/components/form.tsx | 63 ++++++++ app/dashboard/page.tsx | 48 ++++++ app/layout.tsx | 27 +++- app/login/actions.ts | 71 -------- app/login/page.tsx | 18 --- app/share/[id]/page.tsx | 58 ------- app/signup/actions.ts | 111 ------------- app/signup/page.tsx | 18 --- auth.config.ts | 42 ----- auth.ts | 45 ------ components/chat-history.tsx | 49 ------ components/chat-list.tsx | 30 +--- components/chat.tsx | 13 +- components/header.tsx | 64 +------- components/sidebar-actions.tsx | 125 --------------- components/sidebar-desktop.tsx | 19 --- components/sidebar-footer.tsx | 16 -- components/sidebar-item.tsx | 124 -------------- components/sidebar-items.tsx | 42 ----- components/sidebar-list.tsx | 43 ----- components/ui/form.tsx | 178 +++++++++++++++++++++ lib/chat/actions.tsx | 104 ++++++------ middleware.ts | 6 +- package.json | 3 + pnpm-lock.yaml | 42 +++++ 30 files changed, 442 insertions(+), 1168 deletions(-) create mode 100644 app/dashboard/actions/update-heartreate.ts create mode 100644 app/dashboard/components/form.tsx create mode 100644 app/dashboard/page.tsx delete mode 100644 app/login/actions.ts delete mode 100644 app/login/page.tsx delete mode 100644 app/share/[id]/page.tsx delete mode 100644 app/signup/actions.ts delete mode 100644 app/signup/page.tsx delete mode 100644 auth.config.ts delete mode 100644 auth.ts delete mode 100644 components/chat-history.tsx delete mode 100644 components/sidebar-actions.tsx delete mode 100644 components/sidebar-desktop.tsx delete mode 100644 components/sidebar-footer.tsx delete mode 100644 components/sidebar-item.tsx delete mode 100644 components/sidebar-items.tsx delete mode 100644 components/sidebar-list.tsx create mode 100644 components/ui/form.tsx diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index bf8775ce7..0611ad6dd 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -1,11 +1,6 @@ -import { type Metadata } from 'next' -import { notFound, redirect } from 'next/navigation' - -import { auth } from '@/auth' -import { getChat, getMissingKeys } from '@/app/actions' +import { getMissingKeys } from '@/app/actions' import { Chat } from '@/components/chat' import { AI } from '@/lib/chat/actions' -import { Session } from '@/lib/types' export interface ChatPageProps { params: { @@ -13,53 +8,12 @@ export interface ChatPageProps { } } -export async function generateMetadata({ - params -}: ChatPageProps): Promise { - const session = await auth() - - if (!session?.user) { - return {} - } - - const chat = await getChat(params.id, session.user.id) - - if (!chat || 'error' in chat) { - redirect('/') - } else { - return { - title: chat?.title.toString().slice(0, 50) ?? 'Chat' - } - } -} - -export default async function ChatPage({ params }: ChatPageProps) { - const session = (await auth()) as Session +export default async function ChatPage() { const missingKeys = await getMissingKeys() - if (!session?.user) { - redirect(`/login?next=/chat/${params.id}`) - } - - const userId = session.user.id as string - const chat = await getChat(params.id, userId) - - if (!chat || 'error' in chat) { - redirect('/') - } else { - if (chat?.userId !== session?.user?.id) { - notFound() - } - - return ( - - - - ) - } + return ( + + + + ) } diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 2825d593c..693101b2e 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -1,5 +1,3 @@ -import { SidebarDesktop } from '@/components/sidebar-desktop' - interface ChatLayoutProps { children: React.ReactNode } @@ -7,7 +5,6 @@ interface ChatLayoutProps { export default async function ChatLayout({ children }: ChatLayoutProps) { return (
- {children}
) diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx index 21b79f91d..20152e1b8 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -1,8 +1,6 @@ import { nanoid } from '@/lib/utils' import { Chat } from '@/components/chat' import { AI } from '@/lib/chat/actions' -import { auth } from '@/auth' -import { Session } from '@/lib/types' import { getMissingKeys } from '@/app/actions' export const metadata = { @@ -11,12 +9,11 @@ export const metadata = { export default async function IndexPage() { const id = nanoid() - const session = (await auth()) as Session const missingKeys = await getMissingKeys() return ( - + ) } diff --git a/app/actions.ts b/app/actions.ts index 675f136d8..98e1529f7 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,169 +1,5 @@ 'use server' -import { revalidatePath } from 'next/cache' -import { redirect } from 'next/navigation' -import { kv } from '@vercel/kv' - -import { auth } from '@/auth' -import { type Chat } from '@/lib/types' - -export async function getChats(userId?: string | null) { - const session = await auth() - - if (!userId) { - return [] - } - - if (userId !== session?.user?.id) { - return { - error: 'Unauthorized' - } - } - - try { - const pipeline = kv.pipeline() - const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { - rev: true - }) - - for (const chat of chats) { - pipeline.hgetall(chat) - } - - const results = await pipeline.exec() - - return results as Chat[] - } catch (error) { - return [] - } -} - -export async function getChat(id: string, userId: string) { - const session = await auth() - - if (userId !== session?.user?.id) { - return { - error: 'Unauthorized' - } - } - - const chat = await kv.hgetall(`chat:${id}`) - - if (!chat || (userId && chat.userId !== userId)) { - return null - } - - return chat -} - -export async function removeChat({ id, path }: { id: string; path: string }) { - const session = await auth() - - if (!session) { - return { - error: 'Unauthorized' - } - } - - // Convert uid to string for consistent comparison with session.user.id - const uid = String(await kv.hget(`chat:${id}`, 'userId')) - - if (uid !== session?.user?.id) { - return { - error: 'Unauthorized' - } - } - - await kv.del(`chat:${id}`) - await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`) - - revalidatePath('/') - return revalidatePath(path) -} - -export async function clearChats() { - const session = await auth() - - if (!session?.user?.id) { - return { - error: 'Unauthorized' - } - } - - const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1) - if (!chats.length) { - return redirect('/') - } - const pipeline = kv.pipeline() - - for (const chat of chats) { - pipeline.del(chat) - pipeline.zrem(`user:chat:${session.user.id}`, chat) - } - - await pipeline.exec() - - revalidatePath('/') - return redirect('/') -} - -export async function getSharedChat(id: string) { - const chat = await kv.hgetall(`chat:${id}`) - - if (!chat || !chat.sharePath) { - return null - } - - return chat -} - -export async function shareChat(id: string) { - const session = await auth() - - if (!session?.user?.id) { - return { - error: 'Unauthorized' - } - } - - const chat = await kv.hgetall(`chat:${id}`) - - if (!chat || chat.userId !== session.user.id) { - return { - error: 'Something went wrong' - } - } - - const payload = { - ...chat, - sharePath: `/share/${chat.id}` - } - - await kv.hmset(`chat:${chat.id}`, payload) - - return payload -} - -export async function saveChat(chat: Chat) { - const session = await auth() - - if (session && session.user) { - const pipeline = kv.pipeline() - pipeline.hmset(`chat:${chat.id}`, chat) - pipeline.zadd(`user:chat:${chat.userId}`, { - score: Date.now(), - member: `chat:${chat.id}` - }) - await pipeline.exec() - } else { - return - } -} - -export async function refreshHistory(path: string) { - redirect(path) -} - export async function getMissingKeys() { const keysRequired = ['TUNE_STUDIO_API_KEY'] return keysRequired diff --git a/app/dashboard/actions/update-heartreate.ts b/app/dashboard/actions/update-heartreate.ts new file mode 100644 index 000000000..c7057f1c6 --- /dev/null +++ b/app/dashboard/actions/update-heartreate.ts @@ -0,0 +1,19 @@ +'use server' + +import { auth } from '@clerk/nextjs/server' +import { clerkClient } from '@clerk/clerk-sdk-node' + +export async function updateHeartRate({ + newHeartRate +}: { + newHeartRate: string +}) { + const session = await auth() + const user_id = session.userId + if (!user_id) { + throw new Error('YOU ARE NOT LOGGED IN') + } + await clerkClient.users.updateUser(user_id, { + publicMetadata: { heartRate: newHeartRate } + }) +} diff --git a/app/dashboard/components/form.tsx b/app/dashboard/components/form.tsx new file mode 100644 index 000000000..f5a97ce70 --- /dev/null +++ b/app/dashboard/components/form.tsx @@ -0,0 +1,63 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { z } from 'zod' + +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { updateHeartRate } from '../actions/update-heartreate' + +const formSchema = z.object({ + heartrate: z.string().min(2, { + message: 'heartrate must be at least 2 characters.' + }) +}) +export function ProfileForm() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + heartrate: 123 + } + }) + + // 2. Define a submit handler. + async function onSubmit(values: z.infer) { + await updateHeartRate({ newHeartRate: values.heartrate }) + + console.log(values) + } + + return ( +
+ + ( + + heartrate + + + + + This is your public display name. + + + + )} + /> + + + + ) +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 000000000..e9ca128ca --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,48 @@ +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { ProfileForm } from './components/form' + +export function DialogDemo() { + return ( + + + + + + + Edit profile + + Make changes to your profile here. Click save when you're done. + + + + + + ) +} + +export default function Page() { + return ( +
+ +
+ ) +} + +function SomeComponent() { + return ( +
+

asdad

+
+ ) +} diff --git a/app/layout.tsx b/app/layout.tsx index 26cd02189..5e0b07fd1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,7 +7,13 @@ import { TailwindIndicator } from '@/components/tailwind-indicator' import { Providers } from '@/components/providers' import { Header } from '@/components/header' import { Toaster } from '@/components/ui/sonner' -import { ClerkProvider, SignedIn, SignedOut, SignInButton } from '@clerk/nextjs' +import { + ClerkProvider, + SignedIn, + SignedOut, + SignIn, + SignInButton +} from '@clerk/nextjs' export const metadata = { metadataBase: process.env.VERCEL_URL @@ -54,13 +60,18 @@ export default function RootLayout({ children }: RootLayoutProps) { disableTransitionOnChange >
-
-
- {children} - - - -
+ +
+
+ {children} +
+ + + // center this +
+ +
+
diff --git a/app/login/actions.ts b/app/login/actions.ts deleted file mode 100644 index f23e22022..000000000 --- a/app/login/actions.ts +++ /dev/null @@ -1,71 +0,0 @@ -'use server' - -import { signIn } from '@/auth' -import { User } from '@/lib/types' -import { AuthError } from 'next-auth' -import { z } from 'zod' -import { kv } from '@vercel/kv' -import { ResultCode } from '@/lib/utils' - -export async function getUser(email: string) { - const user = await kv.hgetall(`user:${email}`) - return user -} - -interface Result { - type: string - resultCode: ResultCode -} - -export async function authenticate( - _prevState: Result | undefined, - formData: FormData -): Promise { - try { - const email = formData.get('email') - const password = formData.get('password') - - const parsedCredentials = z - .object({ - email: z.string().email(), - password: z.string().min(6) - }) - .safeParse({ - email, - password - }) - - if (parsedCredentials.success) { - await signIn('credentials', { - email, - password, - redirect: false - }) - - return { - type: 'success', - resultCode: ResultCode.UserLoggedIn - } - } else { - return { - type: 'error', - resultCode: ResultCode.InvalidCredentials - } - } - } catch (error) { - if (error instanceof AuthError) { - switch (error.type) { - case 'CredentialsSignin': - return { - type: 'error', - resultCode: ResultCode.InvalidCredentials - } - default: - return { - type: 'error', - resultCode: ResultCode.UnknownError - } - } - } - } -} diff --git a/app/login/page.tsx b/app/login/page.tsx deleted file mode 100644 index 1fba27bea..000000000 --- a/app/login/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { auth } from '@/auth' -import LoginForm from '@/components/login-form' -import { Session } from '@/lib/types' -import { redirect } from 'next/navigation' - -export default async function LoginPage() { - const session = (await auth()) as Session - - if (session) { - redirect('/') - } - - return ( -
- -
- ) -} diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx deleted file mode 100644 index d80f0c842..000000000 --- a/app/share/[id]/page.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { type Metadata } from 'next' -import { notFound, redirect } from 'next/navigation' - -import { formatDate } from '@/lib/utils' -import { getSharedChat } from '@/app/actions' -import { ChatList } from '@/components/chat-list' -import { FooterText } from '@/components/footer' -import { AI, UIState, getUIStateFromAIState } from '@/lib/chat/actions' - -export const runtime = 'edge' -export const preferredRegion = 'home' - -interface SharePageProps { - params: { - id: string - } -} - -export async function generateMetadata({ - params -}: SharePageProps): Promise { - const chat = await getSharedChat(params.id) - - return { - title: chat?.title.slice(0, 50) ?? 'Chat' - } -} - -export default async function SharePage({ params }: SharePageProps) { - const chat = await getSharedChat(params.id) - - if (!chat || !chat?.sharePath) { - notFound() - } - - const uiState: UIState = getUIStateFromAIState(chat) - - return ( - <> -
-
-
-
-

{chat.title}

-
- {formatDate(chat.createdAt)} ยท {chat.messages.length} messages -
-
-
-
- - - -
- - - ) -} diff --git a/app/signup/actions.ts b/app/signup/actions.ts deleted file mode 100644 index 492586ae4..000000000 --- a/app/signup/actions.ts +++ /dev/null @@ -1,111 +0,0 @@ -'use server' - -import { signIn } from '@/auth' -import { ResultCode, getStringFromBuffer } from '@/lib/utils' -import { z } from 'zod' -import { kv } from '@vercel/kv' -import { getUser } from '../login/actions' -import { AuthError } from 'next-auth' - -export async function createUser( - email: string, - hashedPassword: string, - salt: string -) { - const existingUser = await getUser(email) - - if (existingUser) { - return { - type: 'error', - resultCode: ResultCode.UserAlreadyExists - } - } else { - const user = { - id: crypto.randomUUID(), - email, - password: hashedPassword, - salt - } - - await kv.hmset(`user:${email}`, user) - - return { - type: 'success', - resultCode: ResultCode.UserCreated - } - } -} - -interface Result { - type: string - resultCode: ResultCode -} - -export async function signup( - _prevState: Result | undefined, - formData: FormData -): Promise { - const email = formData.get('email') as string - const password = formData.get('password') as string - - const parsedCredentials = z - .object({ - email: z.string().email(), - password: z.string().min(6) - }) - .safeParse({ - email, - password - }) - - if (parsedCredentials.success) { - const salt = crypto.randomUUID() - - const encoder = new TextEncoder() - const saltedPassword = encoder.encode(password + salt) - const hashedPasswordBuffer = await crypto.subtle.digest( - 'SHA-256', - saltedPassword - ) - const hashedPassword = getStringFromBuffer(hashedPasswordBuffer) - - try { - const result = await createUser(email, hashedPassword, salt) - - if (result.resultCode === ResultCode.UserCreated) { - await signIn('credentials', { - email, - password, - redirect: false - }) - } - - return result - } catch (error) { - if (error instanceof AuthError) { - switch (error.type) { - case 'CredentialsSignin': - return { - type: 'error', - resultCode: ResultCode.InvalidCredentials - } - default: - return { - type: 'error', - resultCode: ResultCode.UnknownError - } - } - } else { - return { - type: 'error', - resultCode: ResultCode.UnknownError - } - } - } - } else { - return { - type: 'error', - resultCode: ResultCode.InvalidCredentials - } - } -} diff --git a/app/signup/page.tsx b/app/signup/page.tsx deleted file mode 100644 index dbac96428..000000000 --- a/app/signup/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { auth } from '@/auth' -import SignupForm from '@/components/signup-form' -import { Session } from '@/lib/types' -import { redirect } from 'next/navigation' - -export default async function SignupPage() { - const session = (await auth()) as Session - - if (session) { - redirect('/') - } - - return ( -
- -
- ) -} diff --git a/auth.config.ts b/auth.config.ts deleted file mode 100644 index 6e74c18bb..000000000 --- a/auth.config.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { NextAuthConfig } from 'next-auth' - -export const authConfig = { - secret: process.env.AUTH_SECRET, - pages: { - signIn: '/login', - newUser: '/signup' - }, - callbacks: { - async authorized({ auth, request: { nextUrl } }) { - const isLoggedIn = !!auth?.user - const isOnLoginPage = nextUrl.pathname.startsWith('/login') - const isOnSignupPage = nextUrl.pathname.startsWith('/signup') - - if (isLoggedIn) { - if (isOnLoginPage || isOnSignupPage) { - return Response.redirect(new URL('/', nextUrl)) - } - } - - return true - }, - async jwt({ token, user }) { - if (user) { - token = { ...token, id: user.id } - } - - return token - }, - async session({ session, token }) { - if (token) { - const { id } = token as { id: string } - const { user } = session - - session = { ...session, user: { ...user, id } } - } - - return session - } - }, - providers: [] -} satisfies NextAuthConfig diff --git a/auth.ts b/auth.ts deleted file mode 100644 index 7542992bd..000000000 --- a/auth.ts +++ /dev/null @@ -1,45 +0,0 @@ -import NextAuth from 'next-auth' -import Credentials from 'next-auth/providers/credentials' -import { authConfig } from './auth.config' -import { z } from 'zod' -import { getStringFromBuffer } from './lib/utils' -import { getUser } from './app/login/actions' - -export const { auth, signIn, signOut } = NextAuth({ - ...authConfig, - providers: [ - Credentials({ - async authorize(credentials) { - const parsedCredentials = z - .object({ - email: z.string().email(), - password: z.string().min(6) - }) - .safeParse(credentials) - - if (parsedCredentials.success) { - const { email, password } = parsedCredentials.data - const user = await getUser(email) - - if (!user) return null - - const encoder = new TextEncoder() - const saltedPassword = encoder.encode(password + user.salt) - const hashedPasswordBuffer = await crypto.subtle.digest( - 'SHA-256', - saltedPassword - ) - const hashedPassword = getStringFromBuffer(hashedPasswordBuffer) - - if (hashedPassword === user.password) { - return user - } else { - return null - } - } - - return null - } - }) - ] -}) diff --git a/components/chat-history.tsx b/components/chat-history.tsx deleted file mode 100644 index 88b6ff440..000000000 --- a/components/chat-history.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react' - -import Link from 'next/link' - -import { cn } from '@/lib/utils' -import { SidebarList } from '@/components/sidebar-list' -import { buttonVariants } from '@/components/ui/button' -import { IconPlus } from '@/components/ui/icons' - -interface ChatHistoryProps { - userId?: string -} - -export async function ChatHistory({ userId }: ChatHistoryProps) { - return ( -
-
-

Chat History

-
-
- - - New Chat - -
- - {Array.from({ length: 10 }).map((_, i) => ( -
- ))} -
- } - > - {/* @ts-ignore */} - -
-
- ) -} diff --git a/components/chat-list.tsx b/components/chat-list.tsx index 80821a1ea..acce77ef0 100644 --- a/components/chat-list.tsx +++ b/components/chat-list.tsx @@ -1,46 +1,18 @@ import { Separator } from '@/components/ui/separator' import { UIState } from '@/lib/chat/actions' -import { Session } from '@/lib/types' -import Link from 'next/link' -import { ExclamationTriangleIcon } from '@radix-ui/react-icons' export interface ChatList { messages: UIState - session?: Session isShared: boolean } -export function ChatList({ messages, session, isShared }: ChatList) { +export function ChatList({ messages, isShared }: ChatList) { if (!messages.length) { return null } return (
- {!isShared && !session ? ( - <> -
-
- -
-
-

- Please{' '} - - log in - {' '} - or{' '} - - sign up - {' '} - to save and revisit your chat history! -

-
-
- - - ) : null} - {messages.map((message, index) => (
{message.display} diff --git a/components/chat.tsx b/components/chat.tsx index c43d42a11..1e08bd2ae 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -15,11 +15,10 @@ import { toast } from 'sonner' export interface ChatProps extends React.ComponentProps<'div'> { initialMessages?: Message[] id?: string - session?: Session missingKeys: string[] } -export function Chat({ id, className, session, missingKeys }: ChatProps) { +export function Chat({ id, className, missingKeys }: ChatProps) { const router = useRouter() const path = usePathname() const [input, setInput] = useState('') @@ -28,14 +27,6 @@ export function Chat({ id, className, session, missingKeys }: ChatProps) { const [_, setNewChatId] = useLocalStorage('newChatId', id) - useEffect(() => { - if (session?.user) { - if (!path.includes('chat') && messages.length === 1) { - window.history.replaceState({}, '', `/chat/${id}`) - } - } - }, [id, path, session?.user, messages]) - useEffect(() => { const messagesLength = aiState.messages?.length if (messagesLength === 2) { @@ -66,7 +57,7 @@ export function Chat({ id, className, session, missingKeys }: ChatProps) { ref={messagesRef} > {messages.length ? ( - + ) : ( )} diff --git a/components/header.tsx b/components/header.tsx index 3b41a04a4..1af50fd03 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -2,78 +2,20 @@ import * as React from 'react' import Link from 'next/link' import { cn } from '@/lib/utils' -import { auth } from '@/auth' -import { Button, buttonVariants } from '@/components/ui/button' +import { buttonVariants } from '@/components/ui/button' import { IconGitHub, IconNextChat, IconSeparator, IconVercel } from '@/components/ui/icons' -import { UserMenu } from '@/components/user-menu' -import { SidebarMobile } from './sidebar-mobile' -import { SidebarToggle } from './sidebar-toggle' -import { ChatHistory } from './chat-history' -import { Session } from '@/lib/types' - -async function UserOrLogin() { - const session = (await auth()) as Session - return ( - <> - {session?.user ? ( - <> - - - - - - ) : ( - - - - - )} -
- - {session?.user ? ( - - ) : ( - - )} -
- - ) -} +import { UserButton } from '@clerk/nextjs' export function Header() { return (
-
- }> - - -
) diff --git a/components/sidebar-actions.tsx b/components/sidebar-actions.tsx deleted file mode 100644 index 4f90f2d5d..000000000 --- a/components/sidebar-actions.tsx +++ /dev/null @@ -1,125 +0,0 @@ -'use client' - -import { useRouter } from 'next/navigation' -import * as React from 'react' -import { toast } from 'sonner' - -import { ServerActionResult, type Chat } from '@/lib/types' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle -} from '@/components/ui/alert-dialog' -import { Button } from '@/components/ui/button' -import { IconShare, IconSpinner, IconTrash } from '@/components/ui/icons' -import { ChatShareDialog } from '@/components/chat-share-dialog' -import { - Tooltip, - TooltipContent, - TooltipTrigger -} from '@/components/ui/tooltip' - -interface SidebarActionsProps { - chat: Chat - removeChat: (args: { id: string; path: string }) => ServerActionResult - shareChat: (id: string) => ServerActionResult -} - -export function SidebarActions({ - chat, - removeChat, - shareChat -}: SidebarActionsProps) { - const router = useRouter() - const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) - const [shareDialogOpen, setShareDialogOpen] = React.useState(false) - const [isRemovePending, startRemoveTransition] = React.useTransition() - - return ( - <> -
- - - - - Share chat - - - - - - Delete chat - -
- setShareDialogOpen(false)} - /> - - - - Are you absolutely sure? - - This will permanently delete your chat message and remove your - data from our servers. - - - - - Cancel - - { - event.preventDefault() - // @ts-ignore - startRemoveTransition(async () => { - const result = await removeChat({ - id: chat.id, - path: chat.path - }) - - if (result && 'error' in result) { - toast.error(result.error) - return - } - - setDeleteDialogOpen(false) - router.refresh() - router.push('/') - toast.success('Chat deleted') - }) - }} - > - {isRemovePending && } - Delete - - - - - - ) -} diff --git a/components/sidebar-desktop.tsx b/components/sidebar-desktop.tsx deleted file mode 100644 index 7bc0e19c1..000000000 --- a/components/sidebar-desktop.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Sidebar } from '@/components/sidebar' - -import { auth } from '@/auth' -import { ChatHistory } from '@/components/chat-history' - -export async function SidebarDesktop() { - const session = await auth() - - if (!session?.user?.id) { - return null - } - - return ( - - {/* @ts-ignore */} - - - ) -} diff --git a/components/sidebar-footer.tsx b/components/sidebar-footer.tsx deleted file mode 100644 index a2e18ea66..000000000 --- a/components/sidebar-footer.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { cn } from '@/lib/utils' - -export function SidebarFooter({ - children, - className, - ...props -}: React.ComponentProps<'div'>) { - return ( -
- {children} -
- ) -} diff --git a/components/sidebar-item.tsx b/components/sidebar-item.tsx deleted file mode 100644 index fc7020bcc..000000000 --- a/components/sidebar-item.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client' - -import * as React from 'react' - -import Link from 'next/link' -import { usePathname } from 'next/navigation' - -import { motion } from 'framer-motion' - -import { buttonVariants } from '@/components/ui/button' -import { IconMessage, IconUsers } from '@/components/ui/icons' -import { - Tooltip, - TooltipContent, - TooltipTrigger -} from '@/components/ui/tooltip' -import { useLocalStorage } from '@/lib/hooks/use-local-storage' -import { type Chat } from '@/lib/types' -import { cn } from '@/lib/utils' - -interface SidebarItemProps { - index: number - chat: Chat - children: React.ReactNode -} - -export function SidebarItem({ index, chat, children }: SidebarItemProps) { - const pathname = usePathname() - - const isActive = pathname === chat.path - const [newChatId, setNewChatId] = useLocalStorage('newChatId', null) - const shouldAnimate = index === 0 && isActive && newChatId - - if (!chat?.id) return null - - return ( - -
- {chat.sharePath ? ( - - - - - This is a shared chat. - - ) : ( - - )} -
- -
- - {shouldAnimate ? ( - chat.title.split('').map((character, index) => ( - { - if (index === chat.title.length - 1) { - setNewChatId(null) - } - }} - > - {character} - - )) - ) : ( - {chat.title} - )} - -
- - {isActive &&
{children}
} -
- ) -} diff --git a/components/sidebar-items.tsx b/components/sidebar-items.tsx deleted file mode 100644 index 11cc7fc47..000000000 --- a/components/sidebar-items.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' - -import { Chat } from '@/lib/types' -import { AnimatePresence, motion } from 'framer-motion' - -import { removeChat, shareChat } from '@/app/actions' - -import { SidebarActions } from '@/components/sidebar-actions' -import { SidebarItem } from '@/components/sidebar-item' - -interface SidebarItemsProps { - chats?: Chat[] -} - -export function SidebarItems({ chats }: SidebarItemsProps) { - if (!chats?.length) return null - - return ( - - {chats.map( - (chat, index) => - chat && ( - - - - - - ) - )} - - ) -} diff --git a/components/sidebar-list.tsx b/components/sidebar-list.tsx deleted file mode 100644 index 45c2f4ab8..000000000 --- a/components/sidebar-list.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { clearChats, getChats } from '@/app/actions' -import { ClearHistory } from '@/components/clear-history' -import { SidebarItems } from '@/components/sidebar-items' -import { ThemeToggle } from '@/components/theme-toggle' -import { redirect } from 'next/navigation' -import { cache } from 'react' - -interface SidebarListProps { - userId?: string - children?: React.ReactNode -} - -const loadChats = cache(async (userId?: string) => { - return await getChats(userId) -}) - -export async function SidebarList({ userId }: SidebarListProps) { - const chats = await loadChats(userId) - - if (!chats || 'error' in chats) { - redirect('/') - } else { - return ( -
-
- {chats?.length ? ( -
- -
- ) : ( -
-

No chat history

-
- )} -
-
- - 0} /> -
-
- ) - } -} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 000000000..b6daa654a --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
- {messages?.length >= 2 ? ( -
-
- {id && title ? ( - <> - - setShareDialogOpen(false)} - shareChat={shareChat} - chat={{ - id, - title, - messages: aiState.messages - }} - /> - - ) : null} -
-
- ) : null} -
diff --git a/components/login-button.tsx b/components/login-button.tsx deleted file mode 100644 index ae8f84274..000000000 --- a/components/login-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' - -import * as React from 'react' -import { signIn } from 'next-auth/react' - -import { cn } from '@/lib/utils' -import { Button, type ButtonProps } from '@/components/ui/button' -import { IconGitHub, IconSpinner } from '@/components/ui/icons' - -interface LoginButtonProps extends ButtonProps { - showGithubIcon?: boolean - text?: string -} - -export function LoginButton({ - text = 'Login with GitHub', - showGithubIcon = true, - className, - ...props -}: LoginButtonProps) { - const [isLoading, setIsLoading] = React.useState(false) - return ( - - ) -} diff --git a/components/login-form.tsx b/components/login-form.tsx deleted file mode 100644 index 0a6910d63..000000000 --- a/components/login-form.tsx +++ /dev/null @@ -1,97 +0,0 @@ -'use client' - -import { useFormState, useFormStatus } from 'react-dom' -import { authenticate } from '@/app/login/actions' -import Link from 'next/link' -import { useEffect } from 'react' -import { toast } from 'sonner' -import { IconSpinner } from './ui/icons' -import { getMessageFromCode } from '@/lib/utils' -import { useRouter } from 'next/navigation' - -export default function LoginForm() { - const router = useRouter() - const [result, dispatch] = useFormState(authenticate, undefined) - - useEffect(() => { - if (result) { - if (result.type === 'error') { - toast.error(getMessageFromCode(result.resultCode)) - } else { - toast.success(getMessageFromCode(result.resultCode)) - router.refresh() - } - } - }, [result, router]) - - return ( -
-
-

Please log in to continue.

-
-
- -
- -
-
-
- -
- -
-
-
- -
- - - No account yet?
Sign up
- -
- ) -} - -function LoginButton() { - const { pending } = useFormStatus() - - return ( - - ) -} diff --git a/components/signup-form.tsx b/components/signup-form.tsx deleted file mode 100644 index f5b78a966..000000000 --- a/components/signup-form.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client' - -import { useFormState, useFormStatus } from 'react-dom' -import { signup } from '@/app/signup/actions' -import Link from 'next/link' -import { useEffect } from 'react' -import { toast } from 'sonner' -import { IconSpinner } from './ui/icons' -import { getMessageFromCode } from '@/lib/utils' -import { useRouter } from 'next/navigation' - -export default function SignupForm() { - const router = useRouter() - const [result, dispatch] = useFormState(signup, undefined) - - useEffect(() => { - if (result) { - if (result.type === 'error') { - toast.error(getMessageFromCode(result.resultCode)) - } else { - toast.success(getMessageFromCode(result.resultCode)) - router.refresh() - } - } - }, [result, router]) - - return ( -
-
-

Sign up for an account!

-
-
- -
- -
-
-
- -
- -
-
-
- -
- - - Already have an account? -
Log in
- -
- ) -} - -function LoginButton() { - const { pending } = useFormStatus() - - return ( - - ) -} diff --git a/components/user-menu.tsx b/components/user-menu.tsx deleted file mode 100644 index 3ad28f034..000000000 --- a/components/user-menu.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { type Session } from '@/lib/types' - -import { Button } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { signOut } from '@/auth' - -export interface UserMenuProps { - user: Session['user'] -} - -function getUserInitials(name: string) { - const [firstName, lastName] = name.split(' ') - return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2) -} - -export function UserMenu({ user }: UserMenuProps) { - return ( -
- - - - - - -
{user.email}
-
- -
{ - 'use server' - await signOut() - }} - > - -
-
-
-
- ) -} From e53b1e2e16da5db87e99b78032050f5dab3cf077 Mon Sep 17 00:00:00 2001 From: Yiyun Yao <106117469+nyonyoko@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:25:19 -0400 Subject: [PATCH 08/11] udpdate organization --- components/header.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/header.tsx b/components/header.tsx index 6b2df88b4..7df188b17 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -17,7 +17,10 @@ export function Header() {
- +
) From 99a30a371e9daa12a19d174efbf34d378a921a2a Mon Sep 17 00:00:00 2001 From: Yiyun Yao <106117469+nyonyoko@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:29:13 -0400 Subject: [PATCH 09/11] fix --- app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 7f5c051de..5ed83957c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,7 +7,7 @@ export default async function Page() { const org = session.orgSlug - if (!org) return + if (!org) return redirect(`/${org}`) } From 49af760cd5f11cc345fdf4e892d60538dd21cfeb Mon Sep 17 00:00:00 2001 From: mahdi afshari Date: Sun, 15 Sep 2024 03:36:22 -0400 Subject: [PATCH 10/11] added terra widget to the patient page --- app/api/terra/generateWidgetSession/route.ts | 16 ++++ app/api/terra/route.ts | 9 -- app/patient/components/TerraWidget.tsx | 74 +++++++++++++++ app/patient/components/form.tsx | 96 ++------------------ app/patient/page.tsx | 47 +++++----- 5 files changed, 122 insertions(+), 120 deletions(-) create mode 100644 app/api/terra/generateWidgetSession/route.ts delete mode 100644 app/api/terra/route.ts create mode 100644 app/patient/components/TerraWidget.tsx diff --git a/app/api/terra/generateWidgetSession/route.ts b/app/api/terra/generateWidgetSession/route.ts new file mode 100644 index 000000000..d86a29d0f --- /dev/null +++ b/app/api/terra/generateWidgetSession/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import Terra from "terra-api"; + +const terra = new Terra(process.env.TERRA_DEV_ID ?? "", process.env.TERRA_API_KEY ?? "", process.env.TERRA_WEBHOOK_SECRET ?? ""); + +export async function GET(request: NextRequest) { + const resp = await terra.generateWidgetSession({ + referenceID: "HelloHarvard", + language: "en", + authSuccessRedirectUrl: "http://localhost:3000", + authFailureRedirectUrl: "http://localhost:3000" + }) + return NextResponse.json({ url: resp.url }, { status: 200}); +} + + diff --git a/app/api/terra/route.ts b/app/api/terra/route.ts deleted file mode 100644 index 767802c11..000000000 --- a/app/api/terra/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { clerkClient } from '@clerk/clerk-sdk-node' -import { NextResponse } from 'next/server' - -export const dynamic = 'force-dynamic' - -export async function GET() { - // Your API logic here - return NextResponse.json({ message: 'Hello from Terra API' }) -} diff --git a/app/patient/components/TerraWidget.tsx b/app/patient/components/TerraWidget.tsx new file mode 100644 index 000000000..78c33afdd --- /dev/null +++ b/app/patient/components/TerraWidget.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Button } from '@/components/ui/button'; + +const api_key = process.env.NEXT_PUBLIC_TERRA_API_KEY; +const reference_id = process.env.NEXT_PUBLIC_TERRA_DEV_ID; + +/// The getWidgetAsync function is an asynchronous function that fetches the widget session URL from the Terra API +/// and sets the url state using the setUrl function. +/// It takes an object with an onSuccess callback function as a parameter to set state back to the main component. +export const getWidgetAsync = async (props: { onSuccess: any }) => { + try { + const response = await fetch( + 'https://api.tryterra.co/v2/auth/generateWidgetSession', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'dev-id': 'testingTerra', + 'content-type': 'application/json', + 'x-api-key': api_key as string, + }, + body: JSON.stringify({ + reference_id: reference_id, + providers: + 'GARMIN,WITHINGS,FITBIT,GOOGLE,OURA,WAHOO,PELOTON,ZWIFT,TRAININGPEAKS,FREESTYLELIBRE,DEXCOM,COROS,HUAWEI,OMRON,RENPHO,POLAR,SUUNTO,EIGHT,APPLE,CONCEPT2,WHOOP,IFIT,TEMPO,CRONOMETER,FATSECRET,NUTRACHECK,UNDERARMOUR', + language: 'en', + auth_success_redirect_url: 'terraficapp://request', + auth_failure_redirect_url: 'terraficapp://login', + }), + }, + ); + const json = await response.json(); + props.onSuccess(json.url); + } catch (error) { + console.error(error); + } +}; + +export const Widget = () => { + const [url, setUrl] = useState(''); + + // Close the browser when authentication is completed. Authentication completion can be detected by listening to the incoming link. + // The success or fail url is logged into the console. + const _handleURL = (event: MessageEvent) => { + console.log(event.data); + }; + // Open the browser for authentication using the Widget components. + const _handlePressButtonAsync = async () => { + getWidgetAsync({ onSuccess: setUrl }); + // Use window.open instead of WebBrowser.openBrowserAsync + window.open(url, '_blank'); + }; + + // set up an url listener and invoke getWidgetAsync when the component is mounted + useEffect(() => { + // Linking.addEventListener is not available in Next.js + // We can use window.addEventListener for similar functionality + window.addEventListener('message', _handleURL); + getWidgetAsync({ onSuccess: setUrl }); + + // Cleanup function + return () => { + window.removeEventListener('message', _handleURL); + }; + }, []); + + return ( +
+ +
+ ); +}; diff --git a/app/patient/components/form.tsx b/app/patient/components/form.tsx index 11d9c97ec..138e8f266 100644 --- a/app/patient/components/form.tsx +++ b/app/patient/components/form.tsx @@ -1,89 +1,13 @@ -'use client' - -import { zodResolver } from '@hookform/resolvers/zod' -import { useForm } from 'react-hook-form' -import { z } from 'zod' - -import { Button } from '@/components/ui/button' -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from '@/components/ui/form' -import { Input } from '@/components/ui/input' -import { updateHeartRate } from '../actions/update-heartreate' -import { useState } from 'react' - -const formSchema = z.object({ - heartrate: z.string().min(2, { - message: 'Heart rate must be at least 2 characters.' - }), - weight: z.string().min(2, { - message: 'Weight must be at least 2 characters.' - }) -}) - -export function ProfileForm() { - const [submitting, setSubmitting] = useState(false) - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - heartrate: '123', - weight: '123' - } - }) - - async function onSubmit(values: z.infer) { - setSubmitting(true) - await updateHeartRate({ newHeartRate: values.heartrate }) - setSubmitting(false) - - console.log(values) - } +import { Widget } from './TerraWidget' +export function PatientForm() { return ( -
- - ( - - Heart Rate - - - - This is your heart rate value. - - - )} - /> - ( - - Weight - - - - This is your weight value. - - - )} - /> - - - +
+

Connect Your Health Device

+

+ Click the button below to connect your health device and start syncing your data. +

+ +
) -} +} \ No newline at end of file diff --git a/app/patient/page.tsx b/app/patient/page.tsx index 44591ca5f..e01bb74fb 100644 --- a/app/patient/page.tsx +++ b/app/patient/page.tsx @@ -1,30 +1,27 @@ -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger -} from '@/components/ui/dialog' +'use client' -import { ProfileForm } from './components/form' +import { useEffect } from 'react' +import { useSearchParams } from 'next/navigation' +import { PatientForm } from './components/form' + +export default function PatientPage() { + const searchParams = useSearchParams() + + useEffect(() => { + const authStatus = searchParams.get('auth') + if (authStatus === 'success') { + console.log('Device connected successfully') + // Handle successful connection (e.g., show a success message, update user state) + } else if (authStatus === 'failure') { + console.log('Device connection failed') + // Handle failed connection (e.g., show an error message) + } + }, [searchParams]) -export default function Page() { return ( - - - - - - - Edit profile - - Make changes to your profile here. Click save when you're done. - - - - - +
+

Patient Health Device Connection

+ +
) } From 7eb9973b6dcccab98a6b0ae1d3440553343113ff Mon Sep 17 00:00:00 2001 From: mahdi afshari Date: Sun, 15 Sep 2024 04:26:59 -0400 Subject: [PATCH 11/11] api update --- app/api/terra-widget/route.ts | 47 ++++++++++ app/api/terra/generateWidgetSession/route.ts | 19 ++-- app/patient/components/TerraWidget.tsx | 99 +++++++++----------- 3 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 app/api/terra-widget/route.ts diff --git a/app/api/terra-widget/route.ts b/app/api/terra-widget/route.ts new file mode 100644 index 000000000..fce162af5 --- /dev/null +++ b/app/api/terra-widget/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from "next/server"; + +const api_key = process.env.TERRA_API_KEY; +const reference_id = process.env.TERRA_DEV_ID; + +export async function POST(request: NextRequest) { + try { + console.log('API Key:', api_key); + console.log('Reference ID:', reference_id); + + if (!api_key || !reference_id) { + console.error('Missing API key or reference ID'); + return NextResponse.json({ error: 'Missing API key or reference ID' }, { status: 400 }); + } + + const response = await fetch( + 'https://api.tryterra.co/v2/auth/generateWidgetSession', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'dev-id': 'testingTerra', + 'content-type': 'application/json', + 'x-api-key': api_key, + }, + body: JSON.stringify({ + reference_id: reference_id, + providers: + 'GARMIN,WITHINGS,FITBIT,GOOGLE,OURA,WAHOO,PELOTON,ZWIFT,TRAININGPEAKS,FREESTYLELIBRE,DEXCOM,COROS,HUAWEI,OMRON,RENPHO,POLAR,SUUNTO,EIGHT,APPLE,CONCEPT2,WHOOP,IFIT,TEMPO,CRONOMETER,FATSECRET,NUTRACHECK,UNDERARMOUR', + language: 'en', + auth_success_redirect_url: 'terraficapp://request', + auth_failure_redirect_url: 'terraficapp://login', + }), + }, + ); + + console.log('Terra API response status:', response.status); + + const data = await response.json(); + console.log('Terra API response data:', data); + + return NextResponse.json(data); + } catch (error) { + console.error('Error in Terra API route:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/terra/generateWidgetSession/route.ts b/app/api/terra/generateWidgetSession/route.ts index d86a29d0f..b2b40511b 100644 --- a/app/api/terra/generateWidgetSession/route.ts +++ b/app/api/terra/generateWidgetSession/route.ts @@ -4,13 +4,18 @@ import Terra from "terra-api"; const terra = new Terra(process.env.TERRA_DEV_ID ?? "", process.env.TERRA_API_KEY ?? "", process.env.TERRA_WEBHOOK_SECRET ?? ""); export async function GET(request: NextRequest) { - const resp = await terra.generateWidgetSession({ - referenceID: "HelloHarvard", - language: "en", - authSuccessRedirectUrl: "http://localhost:3000", - authFailureRedirectUrl: "http://localhost:3000" - }) - return NextResponse.json({ url: resp.url }, { status: 200}); + try { + const resp = await terra.generateWidgetSession({ + referenceID: "HelloMIT", + language: "en", + authSuccessRedirectUrl: "http://localhost:3000", + authFailureRedirectUrl: "http://localhost:3000" + }); + return NextResponse.json({ url: resp.url }, { status: 200 }); + } catch (error) { + console.error('Error generating widget session:', error); + return NextResponse.json({ error: 'Failed to generate widget session' }, { status: 500 }); + } } diff --git a/app/patient/components/TerraWidget.tsx b/app/patient/components/TerraWidget.tsx index 78c33afdd..836c4b71c 100644 --- a/app/patient/components/TerraWidget.tsx +++ b/app/patient/components/TerraWidget.tsx @@ -1,74 +1,61 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { Button } from '@/components/ui/button'; -const api_key = process.env.NEXT_PUBLIC_TERRA_API_KEY; -const reference_id = process.env.NEXT_PUBLIC_TERRA_DEV_ID; - -/// The getWidgetAsync function is an asynchronous function that fetches the widget session URL from the Terra API -/// and sets the url state using the setUrl function. -/// It takes an object with an onSuccess callback function as a parameter to set state back to the main component. -export const getWidgetAsync = async (props: { onSuccess: any }) => { +export const getWidgetAsync = async (props: { onSuccess: (url: string) => void }) => { try { - const response = await fetch( - 'https://api.tryterra.co/v2/auth/generateWidgetSession', - { - method: 'POST', - headers: { - Accept: 'application/json', - 'dev-id': 'testingTerra', - 'content-type': 'application/json', - 'x-api-key': api_key as string, - }, - body: JSON.stringify({ - reference_id: reference_id, - providers: - 'GARMIN,WITHINGS,FITBIT,GOOGLE,OURA,WAHOO,PELOTON,ZWIFT,TRAININGPEAKS,FREESTYLELIBRE,DEXCOM,COROS,HUAWEI,OMRON,RENPHO,POLAR,SUUNTO,EIGHT,APPLE,CONCEPT2,WHOOP,IFIT,TEMPO,CRONOMETER,FATSECRET,NUTRACHECK,UNDERARMOUR', - language: 'en', - auth_success_redirect_url: 'terraficapp://request', - auth_failure_redirect_url: 'terraficapp://login', - }), - }, - ); + console.log('Fetching widget URL...'); + const response = await fetch('/api/terra/generateWidgetSession', { method: 'GET' }); + console.log('Response status:', response.status); const json = await response.json(); - props.onSuccess(json.url); + console.log('API Response:', json); + if (json.url) { + props.onSuccess(json.url); + } else { + console.error('No URL in response:', json); + } } catch (error) { - console.error(error); + console.error('Error in getWidgetAsync:', error); } }; export const Widget = () => { - const [url, setUrl] = useState(''); - - // Close the browser when authentication is completed. Authentication completion can be detected by listening to the incoming link. - // The success or fail url is logged into the console. - const _handleURL = (event: MessageEvent) => { - console.log(event.data); - }; - // Open the browser for authentication using the Widget components. - const _handlePressButtonAsync = async () => { - getWidgetAsync({ onSuccess: setUrl }); - // Use window.open instead of WebBrowser.openBrowserAsync - window.open(url, '_blank'); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handlePressButton = async () => { + try { + setError(null); + setIsLoading(true); + console.log('Button clicked'); + await getWidgetAsync({ + onSuccess: (newUrl: string) => { + console.log('Received URL:', newUrl); + if (newUrl) { + window.open(newUrl, '_blank'); + } else { + setError('Received empty URL from Terra API'); + } + } + }); + } catch (error) { + console.error('Error opening widget:', error); + setError('Failed to open widget'); + } finally { + setIsLoading(false); + } }; - // set up an url listener and invoke getWidgetAsync when the component is mounted - useEffect(() => { - // Linking.addEventListener is not available in Next.js - // We can use window.addEventListener for similar functionality - window.addEventListener('message', _handleURL); - getWidgetAsync({ onSuccess: setUrl }); - - // Cleanup function - return () => { - window.removeEventListener('message', _handleURL); - }; - }, []); - return (
- + + {error &&

{error}

}
); };