diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..7ddc50a2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@repo/eslint-config/library.js'] +} diff --git a/.github/lighthouse/budget.json b/.github/lighthouse/budget.json new file mode 100644 index 00000000..55c15577 --- /dev/null +++ b/.github/lighthouse/budget.json @@ -0,0 +1,27 @@ +[ + { + "path": "/*", + "timings": [ + { + "metric": "interactive", + "budget": 3000 + }, + { + "metric": "first-contentful-paint", + "budget": 1800 + } + ], + "resourceSizes": [ + { + "resourceType": "script", + "budget": 100 + } + ], + "resourceCounts": [ + { + "resourceType": "third-party", + "budget": 4 + } + ] + } +] \ No newline at end of file diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 00000000..c3de53d4 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,85 @@ +name: Vercel Preview URL Lighthouse Audit + +on: + pull_request + +jobs: + generate_lighthouse_audit: + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + # - name: Add comment to PR + # id: loading_comment_to_pr + # uses: marocchino/sticky-pull-request-comment@v2.9.0 + # with: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # number: ${{ github.event.issue.number }} + # header: lighthouse + # message: | + # Running Lighthouse audit... + - name: Wait for 2 minutes + run: sleep 120 + - name: Capture Vercel preview URL + id: vercel_preview_url + uses: zentered/vercel-preview-url@v1.1.9 + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + with: + vercel_project_id: prj_k5bYwz7n5hAUFdrWE986D91RiMbW + vercel_team_id: team_7PAgeqjbXkY6qdxNIEKLbVSC + - name: Get URL + run: echo "https://${{ steps.vercel_preview_url.outputs.preview_url }}" + - uses: actions/checkout@v4 + - name: Audit preview URL with Lighthouse + id: lighthouse_audit + uses: treosh/lighthouse-ci-action@11.4.0 + with: + urls: | + "https://${{ steps.vercel_preview_url.outputs.preview_url }}" + "https://${{ steps.vercel_preview_url.outputs.preview_url }}/b/moneybot" + "https://${{ steps.vercel_preview_url.outputs.preview_url }}/u/slug-1" + "https://${{ steps.vercel_preview_url.outputs.preview_url }}/p" + "https://${{ steps.vercel_preview_url.outputs.preview_url }}/health" + "https://${{ steps.vercel_preview_url.outputs.preview_url }}/health/a8103808-14a3-48ed-aa5a-9e490cfdcf0e" + # budgetPath: '.github/lighthouse/budget.json' + uploadArtifacts: true + temporaryPublicStorage: true + - name: Format lighthouse score + id: format_lighthouse_score + uses: actions/github-script@v7.0.1 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const manifest = ${{ steps.lighthouse_audit.outputs.manifest }}; + const links = ${{ steps.lighthouse_audit.outputs.links }}; + + const formatResult = (res) => Math.round((res * 100)); + const score = res => res >= 90 ? '🟢' : res >= 50 ? '🟠' : '🔴'; + + let comment = '⚡️ Lighthouse report\n' + + manifest.forEach(result => { + const pageLink = result.url; + comment += `\nPage: ${pageLink}\n`; + comment += `Report ${links[pageLink]}\n`; + comment += '| Category | Score |\n'; + comment += '| --- | --- |\n'; + + Object.keys(result.summary).forEach(key => result.summary[key] = formatResult(result.summary[key])); + comment += `| ${score(result.summary.performance)} Performance | ${result.summary.performance} |\n`; + comment += `| ${score(result.summary.accessibility)} Accessibility | ${result.summary.accessibility} |\n`; + comment += `| ${score(result.summary['best-practices'])} Best practices | ${result.summary['best-practices']} |\n`; + comment += `| ${score(result.summary.seo)} SEO | ${result.summary.seo} |\n`; + comment += `| ${score(result.summary.pwa)} PWA | ${result.summary.pwa} |\n`; + }); + + core.setOutput("comment", comment); + - name: Add comment to PR + id: comment_to_pr + uses: marocchino/sticky-pull-request-comment@v2.9.0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + number: ${{ github.event.issue.number }} + header: lighthouse + message: | + ${{ steps.format_lighthouse_score.outputs.comment }} \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..0b30280a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,18 @@ +name: Lint + +on: + pull_request: + push: + branches: + - master + +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + + # run any `bun` or `bunx` command + - run: bun install + - run: bun run lint diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..7ea6a59d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.11.0 diff --git a/README.md b/README.md index d063bd4b..10c40a29 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,28 @@ **MasterBots is a UX-focused open-source chatbot aggregator platform and alternative to ChatGPT.** -**Problem #1: ChatGPT spaghetti threads are hard to organize and most users are not skilled at prompting. The recent custom GPTs may solve some of these problems.** +**Problem #1: ChatGPT spaghetti threads are hard to organize and most users are not skilled at prompting. The recent custom GPTs may solve some of these problems.** Solution: However, MasterBots is organized by categories & domain-expert bots that will be prompt-tuned and fine-tuned to give better experiences from the beginning. -**Problem #2: ChatGPT and LLMs are focused on inference. Inference is slow and costly.** -Re-using generations is far more efficient. +**Problem #2: ChatGPT and LLMs are focused on inference. Inference is slow and costly.** +Re-using generations is far more efficient. -Solution: MasterBots will make all user generations public so people who want answers can browse for answers rather than ‘chat’ and chat requests can be routed via lexical search to existing generations before using an LLM. (Note: In the future private generations will be an option using blackbox inference and to prevent any one company from amassing user data. Generations will either be public for all or private.) Users will also have an option to Share GPT4 thread links to MasterBots to be shared publicly. +Solution: MasterBots will make all user generations public so people who want answers can browse for answers rather than ‘chat’ and chat requests can be routed via lexical search to existing generations before using an LLM. (Note: In the future private generations will be an option using blackbox inference and to prevent any one company from amassing user data. Generations will either be public for all or private.) Users will also have an option to Share GPT4 thread links to MasterBots to be shared publicly. **Problem #3: ChatGPT & GPT4 are expensive and slow.** -Solution: Fine-tuned open-source small models can be cheap and fast. Zepher/Mistral with only 7B parameters is competitive with GPT3.5. The future is thousands of small fine-tuned expert models, not one model to do everything. +Solution: Fine-tuned open-source small models can be cheap and fast. Zepher/Mistral with only 7B parameters is competitive with GPT3.5. The future is thousands of small fine-tuned expert models, not one model to do everything. -**Problem #4: ChatGPT UX is basic.** +**Problem #4: ChatGPT UX is basic.** -Solution: We plan to improve UX by organizing based 1) on category & domain-expertise 2) easy response customizations on tone/length/type/complexity 3) make responses clickable to facilitate further questions 4) continually focus on easy prompting UX. +Solution: We plan to improve UX by organizing based 1) on category & domain-expertise 2) easy response customizations on tone/length/type/complexity 3) make responses clickable to facilitate further questions 4) continually focus on easy prompting UX. -MasterBots is open-source. We are building in public! +MasterBots is open-source. We are building in public! Contributors will earn platform credits that can be sold for cash. (Credits may get more valuable over time) -Here is our public Trello board: +Here is our public Trello board: https://trello.com/b/GDCm18zN/masterbots-chatbot-aggregator-development-board ## Requirements @@ -42,7 +42,7 @@ In root folder, create an .env file based of .env_sample and the use `task` to e - **reboot**: Shuts down and then restarts the services. - **seed**: Applies seed data to the Hasura project. - **console**: Launches the Hasura console for the specified project. -- **migrate**: Applies database migrations and updates Hasura metadata. +- **migrate**: Applies database migrations and updates Hasura metadata. - **reload**: Restarts the Postgres service, then all services, and tails the Hasura logs. - **up**: Starts all services defined in the Docker Compose file with a build. - **down**: Shuts down all services and removes any orphaned containers. @@ -53,8 +53,6 @@ In apps/masterbots.ai folder (set up .env file - see .env_sample): ``` bun install -turbo run dev --scope="masterbots.ai" +turbo run dev --scope="masterbots.ai" # task app will execute the same command ``` - - diff --git a/apps/masterbots.ai/.env.example b/apps/masterbots.ai/.env.example index 5618bc98..c68aba3c 100644 --- a/apps/masterbots.ai/.env.example +++ b/apps/masterbots.ai/.env.example @@ -7,4 +7,7 @@ OPENAI_API_KEY=XXXXXXXX GOOGLE_CLIENT_ID=XXXXXXXX GOOGLE_CLIENT_SECRET=XXXXXXXX +DUB_API_KEY= +HASURA_GRAPHQL_ADMIN_SECRET= +HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key":"xx"}' diff --git a/apps/masterbots.ai/.env.local b/apps/masterbots.ai/.env.local index 983fdfaa..b3f95feb 100644 --- a/apps/masterbots.ai/.env.local +++ b/apps/masterbots.ai/.env.local @@ -11,6 +11,8 @@ NEXT_PUBLIC_APP_ENV=test NEXTAUTH_URL=http://localhost:3000 AUTH_SECRET=bb755cba466058b2e6a195541468e84c -HASURA_GRAPHQL_ADMIN_SECRET=lfg -HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key":"xx52fa850c02dc222631cca898ed1485821a70912a6e3649c49076912daa3b62182ba013315915d64f402ddfbb8b58eb5bd11ba225136a6af45bbae07ca872f4"}' +JWT_TOKEN_EXPIRATION=2630016 + +NEXT_PUBLIC_SUPABASE_URL=https://ipgwcdekxoxsjojvzrre.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlwZ3djZGVreG94c2pvanZ6cnJlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTIyNzE3NjgsImV4cCI6MjAyNzg0Nzc2OH0.d3iRMKIaHPd-pKckO_uRYKdndGDUSWm2RtEJkEV-sDE diff --git a/apps/masterbots.ai/.eslintrc.js b/apps/masterbots.ai/.eslintrc.js new file mode 100644 index 00000000..0a714584 --- /dev/null +++ b/apps/masterbots.ai/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@repo/eslint-config/next.js'] +} diff --git a/apps/masterbots.ai/.eslintrc.json b/apps/masterbots.ai/.eslintrc.json deleted file mode 100644 index c17b5320..00000000 --- a/apps/masterbots.ai/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/eslintrc", - "root": true, - "extends": [ - "next/core-web-vitals", - "prettier", - "plugin:tailwindcss/recommended" - ], - "plugins": ["tailwindcss"], - "rules": { - "tailwindcss/no-custom-classname": "off", - "tailwindcss/classnames-order": "off" - }, - "settings": { - "tailwindcss": { - "callees": ["cn", "cva"], - "config": "tailwind.config.js" - } - }, - "overrides": [ - { - "files": ["*.ts", "*.tsx"], - "parser": "@typescript-eslint/parser" - } - ] -} diff --git a/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx new file mode 100644 index 00000000..3910c808 --- /dev/null +++ b/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx @@ -0,0 +1,31 @@ +import { getCategories, getMessagePairs, getThread } from '@/services/hasura' + +import { ThreadAccordion } from '@/components/shared/thread-accordion' +import { CategoryTabs } from '@/components/shared/category-tabs/category-tabs' +import { BrowseInput } from '@/components/shared/browse-input' + +export default async function ThreadPage({ params }: ThreadPageProps) { + const categories = await getCategories() + const thread = await getThread({ + threadId: params.threadId + }) + const initialMessagePairs = await getMessagePairs(thread.threadId) + + return ( +
+ + + +
+ ) +} + +export interface ThreadPageProps { + params: { + threadId: string + chatbot: string + } +} diff --git a/apps/masterbots.ai/app/(browse)/[category]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/page.tsx new file mode 100644 index 00000000..d044bcb2 --- /dev/null +++ b/apps/masterbots.ai/app/(browse)/[category]/page.tsx @@ -0,0 +1,33 @@ +import { ThreadList } from '@/components/shared/thread-list' +import { CategoryTabs } from '@/components/shared/category-tabs/category-tabs' +import { BrowseInput } from '@/components/shared/browse-input' +import { getBrowseThreads, getCategories } from '@/services/hasura' + +export const revalidate = 3600 // revalidate the data at most every hour + +export default async function BrowseCategoryPage({ + params +}: { + params: { category: string } +}) { + const categories = await getCategories() + const categoryId = categories.find( + c => + c.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, '_') === + params.category + ).categoryId + if (!categoryId) throw new Error('Category id not foud') + + const threads = await getBrowseThreads({ + limit: 20, + categoryId + }) + + return ( +
+ + + +
+ ) +} diff --git a/apps/masterbots.ai/app/browse/layout.tsx b/apps/masterbots.ai/app/(browse)/layout.tsx similarity index 59% rename from apps/masterbots.ai/app/browse/layout.tsx rename to apps/masterbots.ai/app/(browse)/layout.tsx index f68d7024..da3e3bde 100644 --- a/apps/masterbots.ai/app/browse/layout.tsx +++ b/apps/masterbots.ai/app/(browse)/layout.tsx @@ -1,5 +1,5 @@ -import { BrowseProvider } from '@/lib/hooks/use-browse'; -import FooterCT from '@/components/footer-ct'; +import { BrowseProvider } from '@/hooks/use-browse' +import FooterCT from '@/components/layout/footer-ct' interface BrowseLayoutProps { children: React.ReactNode @@ -8,12 +8,10 @@ interface BrowseLayoutProps { export default async function BrowseLayout({ children }: BrowseLayoutProps) { return ( - { /* TODO: https://github.com/TheSGJ/nextjs-toploader/issues/66 */} - {/* */}
{children} - +
diff --git a/apps/masterbots.ai/app/(browse)/page.tsx b/apps/masterbots.ai/app/(browse)/page.tsx new file mode 100644 index 00000000..810b504d --- /dev/null +++ b/apps/masterbots.ai/app/(browse)/page.tsx @@ -0,0 +1,18 @@ +import { ThreadList } from '@/components/shared/thread-list' +import { CategoryTabs } from '@/components/shared/category-tabs/category-tabs' +import { BrowseInput } from '@/components/shared/browse-input' +import { getBrowseThreads, getCategories } from '@/services/hasura' + +export default async function BrowsePage() { + const categories = await getCategories() + const threads = await getBrowseThreads({ + limit: 20 + }) + return ( +
+ + + +
+ ) +} diff --git a/apps/masterbots.ai/app/(chat)/[chatbot]/[threadId]/page.tsx b/apps/masterbots.ai/app/(chat)/[chatbot]/[threadId]/page.tsx deleted file mode 100644 index c3e6bca9..00000000 --- a/apps/masterbots.ai/app/(chat)/[chatbot]/[threadId]/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { redirect } from 'next/navigation' - -import { auth } from '@/auth' -import { Chat } from '@/components/chat' -import { getThread } from '@/services/hasura' -import { Message } from 'ai/react' -import { isTokenExpired } from 'mb-lib' - -export default async function ChatPage({ params }: ChatPageProps) { - const session = await auth() - // NOTE: maybe we should use same expiration time - const jwt = session ? session.user?.hasuraJwt : null - if (!jwt || isTokenExpired(jwt)) { - redirect(`/sign-in`) - } - const thread = await getThread({ - threadId: params.threadId, - jwt: session!.user?.hasuraJwt - }) - - //TODO: handle threadId not found - - if (!session) redirect(`/sign-in?next=/${params.threadId}/${params.threadId}`) - - // NOTE: maybe this should be on actions.ts - // format all chatbot prompts as chatgpt 'system' messages - const chatbotSystemPrompts: Message[] = thread.chatbot.prompts.map( - ({ prompt }) => ({ - id: prompt.promptId.toString(), - role: 'system', - content: prompt.content, - createdAt: new Date() - }) - ) - - const userPreferencesPrompts: Message[] = [ - { - id: thread.threadId, - role: 'system', - content: - `Your response tone will be ${thread.chatbot.defaultTone}. ` + - `Your response length will be ${thread.chatbot.defaultLength}. ` + - `Your response format will be ${thread.chatbot.defaultType}. ` + - `Your response complexity level will be ${thread.chatbot.defaultComplexity}.` + - 'Your response will be generated in the same language as user input.', - createdAt: new Date() - } - ] - - // format all user prompts and chatgpt 'assistant' messages - const userAndAssistantMessages: Message[] = thread.messages.map(m => ({ - id: m.messageId, - role: m.role as Message['role'], - content: m.content, - createdAt: m.createdAt - })) - - // concatenate all message to pass it to chat component - const initialMessages: Message[] = chatbotSystemPrompts - .concat(userPreferencesPrompts) - .concat(userAndAssistantMessages) - - // we always start a new openai chat that why id=nanoid() - // and we pass our system prompts along with assistant and user messages from our db. - return ( - - ) -} - -export interface ChatPageProps { - params: { - threadId: string - chatbot: string - } -} diff --git a/apps/masterbots.ai/app/(chat)/[chatbot]/page.tsx b/apps/masterbots.ai/app/(chat)/[chatbot]/page.tsx deleted file mode 100644 index 924718f5..00000000 --- a/apps/masterbots.ai/app/(chat)/[chatbot]/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { auth } from '@/auth' -import { ChatChatbot } from '@/components/chat-chatbot' -import ThreadPanel from '@/components/thread-panel' -import { botNames } from '@/lib/bots-names' -import { getChatbot, getThreads } from '@/services/hasura' -import { Message } from 'ai' -import { isTokenExpired } from 'mb-lib' -import { nanoid } from 'nanoid' -import { redirect } from 'next/navigation' - -export default async function BotThreadsPage({ - params, - searchParams -}: { - params: { chatbot: string } - searchParams: { [key: string]: string | string[] | undefined } -}) { - const session = await auth() - // NOTE: maybe we should use same expiration time - const jwt = session ? session.user?.hasuraJwt : null - if (!jwt || isTokenExpired(jwt)) { - redirect(`/sign-in`) - } - const chatbot = await getChatbot({ - chatbotName: botNames.get(params.chatbot), - jwt: session!.user?.hasuraJwt - }) - if (!chatbot) - throw new Error(`Chatbot ${botNames.get(params.chatbot)} not found`) - - // session will always be defined - const threads = await getThreads({ - chatbotName: botNames.get(params.chatbot), - jwt: session!.user?.hasuraJwt, - userId: session!.user.id - }) - - // format all chatbot prompts as chatgpt 'system' messages - const chatbotSystemPrompts: Message[] = chatbot.prompts.map(({ prompt }) => ({ - id: prompt.promptId.toString(), - role: 'system', - content: prompt.content, - createdAt: new Date() - })) - - const userPreferencesPrompts: Message[] = [ - { - id: nanoid(), - role: 'system', - content: - `Your response tone will be ${chatbot.defaultTone}. ` + - `Your response length will be ${chatbot.defaultLength}. ` + - `Your response format will be ${chatbot.defaultType}. ` + - `Your response complexity level will be ${chatbot.defaultComplexity}.` + - 'Your response will be generated in the same language as user input.', - createdAt: new Date() - } - ] - - // concatenate all message to pass it to chat component - const initialMessages: Message[] = chatbotSystemPrompts.concat( - userPreferencesPrompts - ) - - return ( - <> - {' '} - - - ) -} diff --git a/apps/masterbots.ai/app/(chat)/layout.tsx b/apps/masterbots.ai/app/(chat)/layout.tsx deleted file mode 100644 index 8aa278bc..00000000 --- a/apps/masterbots.ai/app/(chat)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ChatLayoutSection } from '@/components/chat-layout-section' -import { ResponsiveSidebar } from '@/components/sidebar-responsive' -import FooterCT from '@/components/footer-ct' - -interface ChatLayoutProps { - children: React.ReactNode -} - -export default async function ChatLayout({ children }: ChatLayoutProps) { - return ( -
- {/* TODO: https://github.com/TheSGJ/nextjs-toploader/issues/66 */} - {/* */} - - - {children} - -
- -
-
- ) -} diff --git a/apps/masterbots.ai/app/(chat)/page.tsx b/apps/masterbots.ai/app/(chat)/page.tsx deleted file mode 100644 index 16a7222c..00000000 --- a/apps/masterbots.ai/app/(chat)/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { auth } from '@/auth' -import ChatThreadListPanel from '@/components/chat-thread-list-panel' -import ThreadPanel from '@/components/thread-panel' -import { getThreads } from '@/services/hasura' -import { isTokenExpired } from 'mb-lib' -import { redirect } from 'next/navigation' - -export default async function IndexPage() { - const session = await auth() - - // NOTE: maybe we should use same expiration time - const jwt = session!.user?.hasuraJwt - if (!jwt || isTokenExpired(jwt)) { - redirect(`/sign-in`) - } - - const threads = await getThreads({ - jwt, - userId: session!.user.id - }) - - return ( - <> - - - - ) -} diff --git a/apps/masterbots.ai/app/actions.ts b/apps/masterbots.ai/app/actions.ts new file mode 100644 index 00000000..fee93710 --- /dev/null +++ b/apps/masterbots.ai/app/actions.ts @@ -0,0 +1,34 @@ +'use server' + +import Dub from 'dub' + +const dub = new Dub({ + projectSlug: 'bitcash' +}) + +export async function shorten(_prevState: any, formData: any) { + try { + const url = formData.get('url') + // Validate form data + if (!url || typeof url !== 'string') { + return { + shortLink: 'Invalid URL' + } + } + + const { shortLink } = await dub.links.create({ + url, + domain: 'mbots.to' + }) + + return { + shortLink + } + } catch (error) { + console.log('ERROR', error) + + return { + shortLink: 'Invalid data provided' + } + } +} diff --git a/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts b/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 883210bb..00000000 --- a/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { GET, POST } from '@/auth' -export const runtime = 'edge' diff --git a/apps/masterbots.ai/app/api/chat/route.ts b/apps/masterbots.ai/app/api/chat/route.ts index f1460942..8ee89870 100644 --- a/apps/masterbots.ai/app/api/chat/route.ts +++ b/apps/masterbots.ai/app/api/chat/route.ts @@ -1,7 +1,5 @@ import { OpenAIStream, StreamingTextResponse } from 'ai' import OpenAI from 'openai' - -import { auth } from '@/auth' import { nanoid } from '@/lib/utils' export const runtime = 'edge' @@ -13,13 +11,6 @@ const openai = new OpenAI({ export async function POST(req: Request) { const json = await req.json() const { messages, previewToken } = json - // const userId = (await auth())?.user.id - - // if (!userId) { - // return new Response('Unauthorized', { - // status: 401 - // }) - // } if (previewToken) { openai.apiKey = previewToken diff --git a/apps/masterbots.ai/app/auth/callback/route.ts b/apps/masterbots.ai/app/auth/callback/route.ts new file mode 100644 index 00000000..dbc040e1 --- /dev/null +++ b/apps/masterbots.ai/app/auth/callback/route.ts @@ -0,0 +1,79 @@ +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' +import { type CookieOptions, createServerClient } from '@supabase/ssr' +import { getToken, validateJwtSecret } from '@repo/mb-lib' +import { upsertUser } from '@/services/hasura' +import { nanoid } from '@/lib/utils' +import { createSupabaseServerClient } from '@/services/supabase' + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url) + const code = searchParams.get('code') + if (!code) return NextResponse.redirect(`${origin}/auth/auth-code-error`) + // if "next" is in param, use it as the redirect URL + const next = searchParams.get('next') ?? '/' + + const supabase = await createSupabaseServerClient() + + const { + data: { user }, + error + } = await supabase.auth.exchangeCodeForSession(code) + if (error || !user.email) throw new Error('Login Error') + + const adminSecret = process.env.HASURA_GRAPHQL_ADMIN_SECRET + if (!adminSecret) throw new Error('Admin Secret not found') + const jwtSecret = process.env.HASURA_GRAPHQL_JWT_SECRET + if (!jwtSecret) throw new Error('JWT Secret not found') + + const identity = user.identities && user.identities[0] + + if (!identity) throw new Error('Login Error') + const userProfile = await upsertUser({ + email: user.email, + profilePicture: identity.identity_data.picture, + username: + identity.identity_data.name.replace(/\s/g, '_').toLowerCase() || nanoid(), + password: nanoid(), + adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET || '' + }) + + if (!userProfile) throw new Error('Login Error') + + const hasuraJwt = await getToken({ + user: { + account: userProfile.userId, + role: 'user' + }, + jwtSecret: validateJwtSecret(jwtSecret), + jwtExpiration: Number(process.env.JWT_TOKEN_EXPIRATION) + }) + + if (!hasuraJwt) throw new Error('Login Error') + + // Set the hasuraJwt as a cookie + const cookieHeaders = cookies() + cookieHeaders.set('hasuraJwt', hasuraJwt, { + httpOnly: true, + maxAge: Number(process.env.JWT_TOKEN_EXPIRATION), + path: '/', + sameSite: 'lax' // sameSite policy + }) + cookieHeaders.set( + 'userProfile', + JSON.stringify({ + userId: userProfile.userId, + username: userProfile.username, + name: identity.identity_data.name || '', + email: userProfile.email + }), + { + httpOnly: true, + maxAge: Number(process.env.JWT_TOKEN_EXPIRATION), + path: '/', + sameSite: 'lax' // sameSite policy + } + ) + + if (!error) return NextResponse.redirect(`${origin}${next}`) +} diff --git a/apps/masterbots.ai/app/sign-in/page.tsx b/apps/masterbots.ai/app/auth/sign-in/page.tsx similarity index 83% rename from apps/masterbots.ai/app/sign-in/page.tsx rename to apps/masterbots.ai/app/auth/sign-in/page.tsx index bf728e19..93f41d7a 100644 --- a/apps/masterbots.ai/app/sign-in/page.tsx +++ b/apps/masterbots.ai/app/auth/sign-in/page.tsx @@ -1,5 +1,5 @@ -import { LoginButton } from '@/components/login-button' import Image from 'next/image' +import { SignInButtons } from '@/components/layout/auth/sign-in-buttons' export default async function SignInPage() { return ( @@ -9,16 +9,16 @@ export default async function SignInPage() { style={{ height: 200 }} > Masterbots Logo
- +
) diff --git a/apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx b/apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx deleted file mode 100644 index 7b07ec90..00000000 --- a/apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { getThread } from '@/services/hasura' -import { BrowseThread } from '@/components/browse-thread' -import { ChatPageProps } from '@/app/(chat)/[chatbot]/[threadId]/page' -import PageProps from 'next/types' - -export default async function ChatPage({ params }: ChatPageProps) { - const thread = await getThread({ - threadId: params.threadId, - jwt: '' - }) - return -} diff --git a/apps/masterbots.ai/app/b/[id]/layout.tsx b/apps/masterbots.ai/app/b/[id]/layout.tsx index f68d7024..da3e3bde 100644 --- a/apps/masterbots.ai/app/b/[id]/layout.tsx +++ b/apps/masterbots.ai/app/b/[id]/layout.tsx @@ -1,5 +1,5 @@ -import { BrowseProvider } from '@/lib/hooks/use-browse'; -import FooterCT from '@/components/footer-ct'; +import { BrowseProvider } from '@/hooks/use-browse' +import FooterCT from '@/components/layout/footer-ct' interface BrowseLayoutProps { children: React.ReactNode @@ -8,12 +8,10 @@ interface BrowseLayoutProps { export default async function BrowseLayout({ children }: BrowseLayoutProps) { return ( - { /* TODO: https://github.com/TheSGJ/nextjs-toploader/issues/66 */} - {/* */}
{children} - +
diff --git a/apps/masterbots.ai/app/b/[id]/page.tsx b/apps/masterbots.ai/app/b/[id]/page.tsx index 382f5aee..551d9860 100644 --- a/apps/masterbots.ai/app/b/[id]/page.tsx +++ b/apps/masterbots.ai/app/b/[id]/page.tsx @@ -1,15 +1,16 @@ -import { getChatbot, getBrowseThreads } from '@/services/hasura' +import { getChatbot, getBrowseThreads, getCategories } from '@/services/hasura' import { botNames } from '@/lib/bots-names' -import BrowseChatbotDetails from '@/components/browse-chatbot-details' -import BrowseSpecificThreadList from '@/components/browse-specific-thread-list' - -const PAGE_SIZE = 50 +import { ThreadList } from '@/components/shared/thread-list' +import AccountDetails from '@/components/shared/account-details' +import { CategoryTabs } from '@/components/shared/category-tabs/category-tabs' +import { BrowseInput } from '@/components/shared/browse-input' export default async function BotThreadsPage({ params }: { params: { id: string } }) { + const categories = await getCategories() let chatbot, threads chatbot = await getChatbot({ @@ -18,24 +19,27 @@ export default async function BotThreadsPage({ threads: true }) if (!chatbot) throw new Error(`Chatbot ${botNames.get(params.id)} not found`) - - // session will always be defined + const chatbotName = botNames.get(params.id) threads = await getBrowseThreads({ - chatbotName: botNames.get(params.id), - limit: PAGE_SIZE + chatbotName, + limit: 20 }) return ( -
- {chatbot ? : ''} - + + + +
+ +
) } diff --git a/apps/masterbots.ai/app/browse/[category]/page.tsx b/apps/masterbots.ai/app/browse/[category]/page.tsx deleted file mode 100644 index aa668304..00000000 --- a/apps/masterbots.ai/app/browse/[category]/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import BrowseList from '@/components/browse-list' -import { BrowseCategoryTabs } from '@/components/browse-category-tabs' -import { BrowseSearchInput } from '@/components/browse-search-input' -import { CategoryMainTabs } from '@/components/category-main-tabs' -import { getCategories } from '@/services/hasura' - -export const revalidate = 3600 // revalidate the data at most every hour - -export default async function BrowseCategoryPage({ - params -}: { - params: { category: string } -}) { - const categories = await getCategories() - - return ( -
- - - - -
- ) -} diff --git a/apps/masterbots.ai/app/browse/page.tsx b/apps/masterbots.ai/app/browse/page.tsx deleted file mode 100644 index 7d210ccb..00000000 --- a/apps/masterbots.ai/app/browse/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import BrowseList from '@/components/browse-list' -import { BrowseCategoryTabs } from '@/components/browse-category-tabs' -import { BrowseSearchInput } from '@/components/browse-search-input' -import { CategoryMainTabs } from '@/components/category-main-tabs' -import { getCategories } from '@/services/hasura' - -export const revalidate = 3600 // revalidate the data at most every hour - -export default async function BrowsePage() { - const categories = await getCategories() - - return ( -
- - - - -
- ) -} diff --git a/apps/masterbots.ai/app/c/[chatbot]/[threadId] /page.tsx b/apps/masterbots.ai/app/c/[chatbot]/[threadId] /page.tsx new file mode 100644 index 00000000..b17ad79a --- /dev/null +++ b/apps/masterbots.ai/app/c/[chatbot]/[threadId] /page.tsx @@ -0,0 +1,100 @@ +import { nanoid, type Message } from 'ai' +import { isTokenExpired } from '@repo/mb-lib' +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' +import { botNames } from '@/lib/bots-names' +import { + getBrowseThreads, + getChatbot, + getThread, + getUser +} from '@/services/hasura' +import { createSupabaseServerClient } from '@/services/supabase' +import { ThreadList } from '@/components/shared/thread-list' +import { NewChatInput } from '@/components/routes/c/new-chat' + +export default async function ChatListPage({ + params, + searchParams +}: { + params: { chatbot: string; threadId: string } + searchParams: Record +}) { + const supabase = await createSupabaseServerClient() + const { + data: { user } + } = await supabase.auth.getUser() + if (!user || !user.email) redirect(`/auth/sign-in?next=/c/${params.chatbot}`) + const userProfile = await getUser({ + email: user.email, + adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET || '' + }) + + if (!userProfile) redirect(`/auth/sign-in?next=/c/${params.chatbot}`) + const jwt = cookies().get('hasuraJwt')?.value || '' + + // NOTE: maybe we should use same expiration time + if (!jwt || isTokenExpired(jwt)) + redirect(`/auth/sign-in?next=/c/${params.chatbot}`) + const chatbot = await getChatbot({ + chatbotName: botNames.get(params.chatbot), + jwt + }) + if (!chatbot) + throw new Error(`Chatbot ${botNames.get(params.chatbot)} not found`) + + // session will always be defined + const threads = await getBrowseThreads({ + chatbotName: botNames.get(params.chatbot), + userId: userProfile.userId + }) + + // format all chatbot prompts as chatgpt 'system' messages + const chatbotSystemPrompts: Message[] = chatbot.prompts.map(({ prompt }) => ({ + id: prompt.promptId.toString(), + role: 'system', + content: prompt.content, + createdAt: new Date() + })) + + console.log({ params }) + const currentThread = params.threadId + ? await getThread({ threadId: params.threadId }) + : null + + const userPreferencesPrompts: Message[] = [ + { + id: nanoid(), + role: 'system', + content: + `Your response tone will be ${chatbot.defaultTone}. ` + + `Your response length will be ${chatbot.defaultLength}. ` + + `Your response format will be ${chatbot.defaultType}. ` + + `Your response complexity level will be ${chatbot.defaultComplexity}.` + + 'Your response will be generated in the same language as user input.', + createdAt: new Date() + } + ] + + // concatenate all message to pass it to chat component + const initialMessages: Message[] = chatbotSystemPrompts.concat( + userPreferencesPrompts + ) + + console.log('currentThread', currentThread) + return ( + <> + + + + ) +} diff --git a/apps/masterbots.ai/app/c/[chatbot]/page.tsx b/apps/masterbots.ai/app/c/[chatbot]/page.tsx new file mode 100644 index 00000000..7026c1e9 --- /dev/null +++ b/apps/masterbots.ai/app/c/[chatbot]/page.tsx @@ -0,0 +1,3 @@ +import ChatListPage from './[threadId] /page' + +export default ChatListPage diff --git a/apps/masterbots.ai/app/c/layout.tsx b/apps/masterbots.ai/app/c/layout.tsx new file mode 100644 index 00000000..f53d67c3 --- /dev/null +++ b/apps/masterbots.ai/app/c/layout.tsx @@ -0,0 +1,23 @@ +import { ResponsiveSidebar } from '@/components/routes/c/sidebar/sidebar-responsive' +import FooterCT from '@/components/layout/footer-ct' +import { BrowseProvider } from '@/hooks/use-browse' + +interface ChatLayoutProps { + children: React.ReactNode +} + +export default async function ChatLayout({ children }: ChatLayoutProps) { + return ( + +
+ +
+ {children} +
+ +
+
+
+
+ ) +} diff --git a/apps/masterbots.ai/app/c/page.tsx b/apps/masterbots.ai/app/c/page.tsx new file mode 100644 index 00000000..340dc66d --- /dev/null +++ b/apps/masterbots.ai/app/c/page.tsx @@ -0,0 +1,41 @@ +import { isTokenExpired } from '@repo/mb-lib' +import { redirect } from 'next/navigation' +import { cookies } from 'next/headers' +import { getBrowseThreads, getUser } from '@/services/hasura' +import { createSupabaseServerClient } from '@/services/supabase' +import { ThreadList } from '@/components/shared/thread-list' + +export default async function IndexPage() { + const supabase = await createSupabaseServerClient() + const { + data: { user } + } = await supabase.auth.getUser() + if (!user || !user.email) redirect(`/auth/sign-in`) + + const dbUserProfile = await getUser({ + email: user.email, + adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET || '' + }) + + if (!dbUserProfile) redirect(`/auth/sign-in`) + + const jwt = cookies().get('hasuraJwt').value || '' + + // NOTE: maybe we should use same expiration time + if (!jwt || isTokenExpired(jwt) || !user) redirect(`/auth/sign-in`) + + const threads = await getBrowseThreads({ + slug: dbUserProfile.slug, + limit: 20 + }) + + return ( + <> + + + ) +} diff --git a/apps/masterbots.ai/app/globals.css b/apps/masterbots.ai/app/globals.css index fe0cea82..741d1bc0 100644 --- a/apps/masterbots.ai/app/globals.css +++ b/apps/masterbots.ai/app/globals.css @@ -100,32 +100,21 @@ } .scrollbar { - overflow: auto; + overflow: auto; } .scrollbar::-webkit-scrollbar { - width: 4px; - height: 4px; + width: 1px; + height: 1px; } .scrollbar::-webkit-scrollbar-track, .scrollbar::-webkit-scrollbar-corner { - background: var(--scrollbar-track) !important; + background: var(--scrollbar-track); } .scrollbar::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 2px; } -/* .scrollbar::-webkit-scrollbar-thumb:hover { - background: var(--scrollbar-thumb-hover); -} */ - -@media screen and (min-width: 1024px) { - .scrollbar::-webkit-scrollbar { - width: 8px; - height: 8px; - } -} - .scrollbar.small-thumb::-webkit-scrollbar-thumb { border-left: 300px solid #f9f9fa; @@ -151,3 +140,7 @@ overflow: visible; text-overflow: clip; } + +.hide-buttons > button { + display: none; +} diff --git a/apps/masterbots.ai/app/layout.tsx b/apps/masterbots.ai/app/layout.tsx index 15ab75c8..6040683b 100644 --- a/apps/masterbots.ai/app/layout.tsx +++ b/apps/masterbots.ai/app/layout.tsx @@ -3,11 +3,25 @@ import { GeistSans } from 'geist/font/sans' import { Toaster } from 'react-hot-toast' import '@/app/globals.css' -import { Header } from '@/components/header' -import { Providers } from '@/components/providers' +import { cookies } from 'next/headers' +import { Header } from '@/components/layout/header' +import { Providers } from '@/components/layout/providers' import { cn } from '@/lib/utils' +import { GlobalStoreProvider } from '@/hooks/use-global-store' + +async function getCookieData(): Promise<{ hasuraJwt; userProfile }> { + const hasuraJwt = cookies().get('hasuraJwt')?.value || '' + const userProfile = cookies().get('userProfile')?.value || null + return new Promise(resolve => + setTimeout(() => { + resolve({ hasuraJwt, userProfile }) + }, 1000) + ) +} + +export default async function RootLayout({ children }: RootLayoutProps) { + const { hasuraJwt, userProfile } = await getCookieData() -export default function RootLayout({ children }: RootLayoutProps) { return ( - {/* TODO: https://github.com/TheSGJ/nextjs-toploader/issues/66 */} - {/* */} - -
-
-
{children}
-
- {/* */} -
+ +
+
+
+ {children} +
+
+
+ ) diff --git a/apps/masterbots.ai/app/p/page.tsx b/apps/masterbots.ai/app/p/page.tsx new file mode 100644 index 00000000..2086576f --- /dev/null +++ b/apps/masterbots.ai/app/p/page.tsx @@ -0,0 +1,18 @@ +import { Suspense } from 'react' +import { WorkEarlyAccessForm } from '@/components/routes/p/early-access-from' + +export default function WorkPage() { + return ( +
+

Masterbots for professional work is coming soon!

+

+ Let us know what areas are you most interested in to obtain early + access. +

+
+ }> + + +
+ ) +} diff --git a/apps/masterbots.ai/app/share/[id]/page.tsx b/apps/masterbots.ai/app/share/[id]/page.tsx deleted file mode 100644 index 9aac7207..00000000 --- a/apps/masterbots.ai/app/share/[id]/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { type Metadata } from 'next' -import { notFound } from 'next/navigation' - -import { formatDate } from '@/lib/utils' -import { ChatList } from '@/components/chat-list' -import { FooterText } from '@/components/footer' - -interface SharePageProps { - params: { - id: string - } -} - -export async function generateMetadata({ - params -}: SharePageProps): Promise { - const chat = { title: '' } //await getSharedChat(params.id) - - return { - title: chat?.title.slice(0, 50) ?? 'Chat' - } -} - -export default async function SharePage({ params }: SharePageProps) { - const chat: any = { sharePath: '', title: '' } //await getSharedChat(params.id) - - if (!chat || !chat?.sharePath) { - notFound() - } - - return ( - <> -
-
-
-
-

{chat.title}

-
- {formatDate(chat.createdAt)} · {chat.messages.length} messages -
-
-
-
- -
- - - ) -} diff --git a/apps/masterbots.ai/app/terms-n-policies/layout.tsx b/apps/masterbots.ai/app/terms-n-policies/layout.tsx deleted file mode 100644 index 1e6230e4..00000000 --- a/apps/masterbots.ai/app/terms-n-policies/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import FooterCT from "@/components/footer-ct" - -interface TermPoliciesLayoutProps { - children: React.ReactNode -} - -export default function TermPoliciesLayout({ children }: TermPoliciesLayoutProps) { - return ( -
-
- {children} - -
-
- ) -} diff --git a/apps/masterbots.ai/app/terms-n-policies/page.tsx b/apps/masterbots.ai/app/terms-n-policies/page.tsx deleted file mode 100644 index 9813688a..00000000 --- a/apps/masterbots.ai/app/terms-n-policies/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ - -export default async function TermPoliciesPage() { - - return ( -
-
-

Masterbots Privacy Policy

- - -

Introduction:

-

Welcome to Masterbots, the premier destination for engaging with domain-specific AI-driven - chatbots. By using our website, you agree to the collection, use, and sharing of your personal information - as described in this privacy policy. Our service involves the aggregation of chatbot interactions, which - are publicized by default.

- -

Information We Collect:

-
    -
  • Personal Information: We may collect personal information such as your name, email address, and any - other information you provide when you register, submit inquiries, or interact with our chatbots.
  • -
  • Chat Data: All interactions with our chatbots are collected and, by default, made public on our - platform. This includes text, images, or any other media shared during these interactions.
  • -
  • Usage Data: We collect information on how our service is accessed and used. This includes clickstream - data, browsing preferences, and other interactions with the service.
  • -
- -

How We Use Your Information:

-
    -
  • To provide, maintain, and improve our services.
  • -
  • To publicize chat interactions, as per our platform's unique feature.
  • -
  • To communicate with you, including sending updates, support, and administrative messages.
  • -
  • For research and analytics to better understand how users interact with our chatbots and services.
  • -
- -

Sharing and Disclosure:

-
    -
  • Publicizing Chats: As an integral feature of our platform, chats are publicized by default. Users should - be aware that any information shared during chat sessions will be publicly accessible.
  • -
  • Legal Requirements: We may disclose your information if required by law, regulation, or legal - process.
  • -
  • Third-Party Service Providers: We may share your information with third-party service providers who - perform services on our behalf, such as hosting, analytics, and customer service, under confidentiality - agreements.
  • -
- -

Your Data Protection Rights

-

Depending on your location, you may have rights including:

-
    -
  • Accessing, correcting, updating, or requesting deletion of your personal information.
  • -
  • Objecting to processing of your personal information, asking us to restrict processing, or requesting - the portability of your personal information.
  • -
- -

Data Security

-

We implement appropriate technical and organizational measures to protect the security of your - personal information against unauthorized or unlawful processing and against accidental loss, destruction, - or damage.

- -

Children's Privacy

-

Our service is not directed to individuals under the age of 13. We do not knowingly collect - personal information from children under 13.

- -

Changes to This Privacy Policy

-

We may update this policy from time to time. The updated version will be indicated by an - updated “Revised” date.

- -

Contact Us

-

For any questions or concerns regarding this privacy policy, please contact us at - contact@masterbots.ai

- - -

Introduction

- -

Welcome to Masterbots.ai ("we,", "us," "our"). By accessing our website, you agree to these Terms of Service - ("Terms"), our Privacy Policy, and all applicable laws and regulations governing the use of our platform. Our - website aggregates AI chatbot conversations and publicizes these chats by default. We provide a platform for - users to interact with various AI chatbots and share their interactions publicly on our site.

- -

Acceptance of Terms

-

By using our website, you acknowledge that you have read, understood, and agreed to be bound by these Terms. If - you do not agree with any part of these Terms, you must not use our website.

- -

Use License

-
    -
  • We grant you a limited, non-exclusive, non-transferable license to access and use our website in accordance - with these Terms.
  • -
  • This license does not allow you to use our website or its content for commercial purposes without obtaining - a license from us.
  • -
- -

User Conduct

-

You agree not to use our website for any unlawful purpose or in any way that interrupts, damages, or impairs - the service. You agree not to attempt to gain unauthorized access to our website or any networks, servers, or - computer systems connected to our website. You are responsible for ensuring that any content you submit or - share through our website complies with all applicable laws and regulations. You must not upload, share, or - otherwise disseminate any content that is unlawful, harmful, threatening, abusive, harassing, defamatory, - vulgar, obscene, or otherwise objectionable.

- -

Publicizing of Chats

-

By using our service, you understand and agree that all chats you engage in with our AI chatbots may be - publicized on our website by default. This includes any text, images, or other content you share during these - interactions. You grant us a non-exclusive, worldwide, royalty-free license to use, reproduce, modify, adapt, - publish, and display such content for the purpose of operating and providing our service.

- -

Intellectual Property Rights

-

The content published on our website, including but not limited to text, graphics, logos, and software, is - the property of Masterbots.ai or its licensors and is protected by copyright and intellectual property laws. You - may not use, copy, modify, or distribute any content from our website without the express permission of the - owner.

- -

Disclaimer of Warranties

-

Our website is provided on an "AS IS" and "AS AVAILABLE" basis without warranties of any kind, either express - or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or - non-infringement.

- -

Limitation of Liability

-

We shall not be liable for any direct, indirect, incidental, special, consequential, or punitive damages - resulting from your access to or use of our website.

- -

Changes to Terms

-

We reserve the right to modify these Terms at any time. We will provide notice of any significant changes by - updating the date at the top of these Terms and, where appropriate, notification via email or our - website.

- -

Governing Law

-

These Terms shall be governed by and construed in accordance with the laws of Delaware, without giving effect - to any principles of conflicts of law.

- -

Contact Us

-

For any questions or concerns regarding these Terms, please contact us at contact@masterbots.ai

- - - {/* */} -
-
- ) -} diff --git a/apps/masterbots.ai/app/terms/layout.tsx b/apps/masterbots.ai/app/terms/layout.tsx new file mode 100644 index 00000000..abe7e878 --- /dev/null +++ b/apps/masterbots.ai/app/terms/layout.tsx @@ -0,0 +1,18 @@ +import FooterCT from '@/components/layout/footer-ct' + +interface TermPoliciesLayoutProps { + children: React.ReactNode +} + +export default function TermPoliciesLayout({ + children +}: TermPoliciesLayoutProps) { + return ( +
+
+ {children} + +
+
+ ) +} diff --git a/apps/masterbots.ai/app/terms/page.tsx b/apps/masterbots.ai/app/terms/page.tsx new file mode 100644 index 00000000..07a6b7f6 --- /dev/null +++ b/apps/masterbots.ai/app/terms/page.tsx @@ -0,0 +1,220 @@ +export default async function TermPoliciesPage() { + return ( +
+
+

Masterbots Privacy Policy

+ +

Introduction:

+

+ Welcome to Masterbots, the premier destination for engaging with + domain-specific AI-driven chatbots. By using our website, you agree to + the collection, use, and sharing of your personal information as + described in this privacy policy. Our service involves the aggregation + of chatbot interactions, which are publicized by default. +

+ +

Information We Collect:

+
    +
  • + Personal Information: We may collect personal information such as + your name, email address, and any other information you provide when + you register, submit inquiries, or interact with our chatbots. +
  • +
  • + Chat Data: All interactions with our chatbots are collected and, by + default, made public on our platform. This includes text, images, or + any other media shared during these interactions. +
  • +
  • + Usage Data: We collect information on how our service is accessed + and used. This includes clickstream data, browsing preferences, and + other interactions with the service. +
  • +
+ +

How We Use Your Information:

+
    +
  • To provide, maintain, and improve our services.
  • +
  • + To publicize chat interactions, as per our platform's unique + feature. +
  • +
  • + To communicate with you, including sending updates, support, and + administrative messages. +
  • +
  • + For research and analytics to better understand how users interact + with our chatbots and services. +
  • +
+ +

Sharing and Disclosure:

+
    +
  • + Publicizing Chats: As an integral feature of our platform, chats are + publicized by default. Users should be aware that any information + shared during chat sessions will be publicly accessible. +
  • +
  • + Legal Requirements: We may disclose your information if required by + law, regulation, or legal process. +
  • +
  • + Third-Party Service Providers: We may share your information with + third-party service providers who perform services on our behalf, + such as hosting, analytics, and customer service, under + confidentiality agreements. +
  • +
+ +

Your Data Protection Rights

+

+ Depending on your location, you may have rights including: +

+
    +
  • + Accessing, correcting, updating, or requesting deletion of your + personal information. +
  • +
  • + Objecting to processing of your personal information, asking us to + restrict processing, or requesting the portability of your personal + information. +
  • +
+ +

Data Security

+

+ We implement appropriate technical and organizational measures to + protect the security of your personal information against unauthorized + or unlawful processing and against accidental loss, destruction, or + damage. +

+ +

Children's Privacy

+

+ Our service is not directed to individuals under the age of 13. We do + not knowingly collect personal information from children under 13. +

+ +

+ Changes to This Privacy Policy +

+

+ We may update this policy from time to time. The updated version will + be indicated by an updated “Revised” date. +

+ +

Contact Us

+

+ For any questions or concerns regarding this privacy policy, please + contact us at contact@masterbots.ai +

+ +

Introduction

+ +

+ Welcome to Masterbots.ai ("we,", "us," + "our"). By accessing our website, you agree to these Terms + of Service ("Terms"), our Privacy Policy, and all applicable + laws and regulations governing the use of our platform. Our website + aggregates AI chatbot conversations and publicizes these chats by + default. We provide a platform for users to interact with various AI + chatbots and share their interactions publicly on our site. +

+ +

Acceptance of Terms

+

+ By using our website, you acknowledge that you have read, understood, + and agreed to be bound by these Terms. If you do not agree with any + part of these Terms, you must not use our website. +

+ +

Use License

+
    +
  • + We grant you a limited, non-exclusive, non-transferable license to + access and use our website in accordance with these Terms. +
  • +
  • + This license does not allow you to use our website or its content + for commercial purposes without obtaining a license from us. +
  • +
+ +

User Conduct

+

+ You agree not to use our website for any unlawful purpose or in any + way that interrupts, damages, or impairs the service. You agree not to + attempt to gain unauthorized access to our website or any networks, + servers, or computer systems connected to our website. You are + responsible for ensuring that any content you submit or share through + our website complies with all applicable laws and regulations. You + must not upload, share, or otherwise disseminate any content that is + unlawful, harmful, threatening, abusive, harassing, defamatory, + vulgar, obscene, or otherwise objectionable. +

+ +

Publicizing of Chats

+

+ By using our service, you understand and agree that all chats you + engage in with our AI chatbots may be publicized on our website by + default. This includes any text, images, or other content you share + during these interactions. You grant us a non-exclusive, worldwide, + royalty-free license to use, reproduce, modify, adapt, publish, and + display such content for the purpose of operating and providing our + service. +

+ +

Intellectual Property Rights

+

+ The content published on our website, including but not limited to + text, graphics, logos, and software, is the property of Masterbots.ai + or its licensors and is protected by copyright and intellectual + property laws. You may not use, copy, modify, or distribute any + content from our website without the express permission of the owner. +

+ +

Disclaimer of Warranties

+

+ Our website is provided on an "AS IS" and "AS + AVAILABLE" basis without warranties of any kind, either express + or implied, including but not limited to warranties of + merchantability, fitness for a particular purpose, or + non-infringement. +

+ +

Limitation of Liability

+

+ We shall not be liable for any direct, indirect, incidental, special, + consequential, or punitive damages resulting from your access to or + use of our website. +

+ +

Changes to Terms

+

+ We reserve the right to modify these Terms at any time. We will + provide notice of any significant changes by updating the date at the + top of these Terms and, where appropriate, notification via email or + our website. +

+ +

Governing Law

+

+ These Terms shall be governed by and construed in accordance with the + laws of Delaware, without giving effect to any principles of conflicts + of law. +

+ +

Contact Us

+

+ For any questions or concerns regarding these Terms, please contact us + at contact@masterbots.ai +

+ + {/* */} +
+
+ ) +} diff --git a/apps/masterbots.ai/app/u/[slug]/page.tsx b/apps/masterbots.ai/app/u/[slug]/page.tsx index f1df673c..ab49fe90 100644 --- a/apps/masterbots.ai/app/u/[slug]/page.tsx +++ b/apps/masterbots.ai/app/u/[slug]/page.tsx @@ -1,31 +1,39 @@ -import { getBrowseThreads, getUserInfoFromBrowse } from '@/services/hasura' -import BrowseUserDetails from '@/components/browse-user-details' -import BrowseSpecificThreadList from '@/components/browse-specific-thread-list' - -const PAGE_SIZE = 50 +import { + getBrowseThreads, + getCategories, + getUserInfoFromBrowse +} from '@/services/hasura' +import { ThreadList } from '@/components/shared/thread-list' +import AccountDetails from '@/components/shared/account-details' +import { CategoryTabs } from '@/components/shared/category-tabs/category-tabs' +import { BrowseInput } from '@/components/shared/browse-input' export default async function BotThreadsPage({ params }: { params: { slug: string } }) { + const categories = await getCategories() const user = await getUserInfoFromBrowse(params.slug) if (!user) return
No user found.
const threads = await getBrowseThreads({ slug: params.slug, - limit: PAGE_SIZE + limit: 20 }) return ( -
- - + + + +
+ +
) } diff --git a/apps/masterbots.ai/app/u/layout.tsx b/apps/masterbots.ai/app/u/layout.tsx index f68d7024..fcd53cd5 100644 --- a/apps/masterbots.ai/app/u/layout.tsx +++ b/apps/masterbots.ai/app/u/layout.tsx @@ -1,5 +1,5 @@ -import { BrowseProvider } from '@/lib/hooks/use-browse'; -import FooterCT from '@/components/footer-ct'; +import { BrowseProvider } from '@/hooks/use-browse' +import FooterCT from '@/components/layout/footer-ct' interface BrowseLayoutProps { children: React.ReactNode @@ -8,12 +8,12 @@ interface BrowseLayoutProps { export default async function BrowseLayout({ children }: BrowseLayoutProps) { return ( - { /* TODO: https://github.com/TheSGJ/nextjs-toploader/issues/66 */} + {/* TODO: https://github.com/TheSGJ/nextjs-toploader/issues/66 */} {/* */}
{children} - +
diff --git a/apps/masterbots.ai/auth.ts b/apps/masterbots.ai/auth.ts deleted file mode 100644 index 5f671463..00000000 --- a/apps/masterbots.ai/auth.ts +++ /dev/null @@ -1,93 +0,0 @@ -import NextAuth, { type DefaultSession } from 'next-auth' -import Google from 'next-auth/providers/google' -import { getToken, validateJwtSecret } from 'mb-lib' -import { upsertUser } from './services/hasura' -import { nanoid } from './lib/utils' - -export const { - handlers: { GET, POST }, - auth -} = NextAuth({ - providers: [ - Google({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET - }) - ], - callbacks: { - signIn: async ({ profile }) => { - if (!profile) return false - return true - }, - jwt: async ({ token, profile }) => { - // console.log('========== >jwt callback', { token, profile }) - - // profile is passed on the first call on login - if (profile) { - const adminSecret = process.env.HASURA_GRAPHQL_ADMIN_SECRET - if (!adminSecret) throw new Error('Admin Secret not found') - const jwtSecret = process.env.HASURA_GRAPHQL_JWT_SECRET - if (!adminSecret) throw new Error('JWT Secret not found') - - const dbUser = await upsertUser({ - email: profile.email!, - profilePicture: profile.picture || '', - username: profile.name?.replace(/\s/g, '_') || nanoid(), - password: nanoid(), - adminSecret: adminSecret as string - }) - - // console.log('dbuser', dbUser) - if (!dbUser) throw new Error('Login Error') - - const hasuraJwt = await getToken({ - user: { - account: dbUser.userId, - role: 'user' - }, - jwtSecret: validateJwtSecret(jwtSecret), - jwtExpiration: Number(process.env.JWT_TOKEN_EXPIRATION) - }) - - if (!hasuraJwt) throw new Error('Login Error') - - token.hasuraJwt = hasuraJwt - token.userId = dbUser.userId - token.email = dbUser.email - token.name = dbUser.username.replace(/_/g, ' ') - } - - return token - }, - session: async ({ session, token }) => { - // console.log('session callback', token) - - session.user = { - id: token.userId as string, - image: token.picture as string, - name: token.name as string, - email: token.email as string, - hasuraJwt: token.hasuraJwt as string - } - - return session - }, - authorized({ auth }) { - return !!auth?.user // this ensures there is a logged in user for -every- request - } - }, - pages: { - signIn: '/sign-in' // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages - } -}) - -declare module 'next-auth' { - interface Session { - user: { - id: string - image: string - name: string - hasuraJwt: string - } & DefaultSession['user'] - } -} diff --git a/apps/masterbots.ai/components/browse-chat-message-list.tsx b/apps/masterbots.ai/components/browse-chat-message-list.tsx deleted file mode 100644 index bcef0439..00000000 --- a/apps/masterbots.ai/components/browse-chat-message-list.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// Inspired by Chatbot-UI and modified to fit the needs of this project -// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatcleanMessage.tsx - -import { IconUser } from '@/components/ui/icons' -import { cn, createMessagePairs } from '@/lib/utils' -import { Chatbot, Message, User } from 'mb-genql' -import Image from 'next/image' -import Link from 'next/link' -import React from 'react' -import { BrowseChatMessage } from './browse-chat-message' -import { MessagePair, convertMessage } from './browse-chat-messages' -import { ChatAccordion } from './chat-accordion' - -export function BrowseChatMessageList({ - messages, - user, - chatbot, - isThread = false -}: { - messages: Message[] - user?: User - chatbot?: Chatbot - isThread?: boolean -}) { - const [pairs, setPairs] = React.useState([]) - - React.useEffect(() => { - if (messages.length) { - const prePairs: MessagePair[] = createMessagePairs( - messages - ) as MessagePair[] - setPairs(prePairs) - } else setPairs([]) - }, [messages]) - - return ( -
- {pairs.map((pair: MessagePair, key: number) => ( - - {/* Thread Title */} - {(key !== 0 || isThread) && ( -
-
- {pair.userMessage.content} -
-
- )} - - {/* Thread Description */} - <> - - {/* Thread Content */} -
- {pair.chatGptMessage.length > 0 - ? pair.chatGptMessage.map((message, index) => ( - - )) - : ''} -
-
- ))} -
- ) -} diff --git a/apps/masterbots.ai/components/browse-chat-messages.tsx b/apps/masterbots.ai/components/browse-chat-messages.tsx deleted file mode 100644 index 974c645d..00000000 --- a/apps/masterbots.ai/components/browse-chat-messages.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// Inspired by Chatbot-UI and modified to fit the needs of this project -// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatcleanMessage.tsx - -import * as AI from 'ai' -import { Chatbot, Message, User } from 'mb-genql' -import React from 'react' -import BrowseChatbotDetails from './browse-chatbot-details' -import { BrowseChatMessageList } from './browse-chat-message-list' -import { getMessages } from '@/services/hasura' - -export type MessagePair = { - userMessage: Message - chatGptMessage: Message[] -} - -export function convertMessage(message: Message) { - return { - id: message.messageId, - content: message.content, - createAt: message.createdAt, - role: message.role - } as AI.Message -} - -export function BrowseChatMessages({ - threadId, - user, - chatbot -}: { - threadId: string - user?: User - chatbot?: Chatbot -}) { - const [messages, setMessages] = React.useState([]) - const fetchMessages = async () => { - if (threadId && !messages.length) { - const messages = await getMessages({ threadId: threadId }) - setMessages(messages) - } - } - React.useEffect(() => { - fetchMessages() - }, [threadId]) - - return ( -
- -
- -
-
- ) -} diff --git a/apps/masterbots.ai/components/browse-chatbot-details.tsx b/apps/masterbots.ai/components/browse-chatbot-details.tsx deleted file mode 100644 index 55804bc0..00000000 --- a/apps/masterbots.ai/components/browse-chatbot-details.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Chatbot } from 'mb-genql' -import Image from 'next/image' -import Link from 'next/link' -import { Separator } from './ui/separator' - -export default function BrowseChatbotDetails({ - chatbot -}: { - chatbot?: Chatbot -}) { - return ( -
-
-
-
{chatbot?.name}
- -
- {chatbot?.categories[0].category.name}. -
-
-
- {chatbot?.description ?
{chatbot?.description}
: ''} -
-
- Threads:{' '} - - {chatbot?.threads.length ?? 1} - - {/*
- Views: 0 -
*/} - {/*
- Read time:{' '} - - {readingTime(messages)} min - -
*/} -
-
-
-
-
- - Chat with {chatbot?.name} > - - {/*
- - 0 - - 0 - - -
*/} -
-
-
- {chatbot?.avatar -
-
-
- ) -} diff --git a/apps/masterbots.ai/components/browse-list-item.tsx b/apps/masterbots.ai/components/browse-list-item.tsx deleted file mode 100644 index 810cf8a8..00000000 --- a/apps/masterbots.ai/components/browse-list-item.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import Image from 'next/image' - -import { cn, sleep } from '@/lib/utils' -import { getMessages } from '@/services/hasura' -import { Message, Thread } from 'mb-genql' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import React, { useEffect } from 'react' -import { BrowseChatMessageList } from './browse-chat-message-list' -import { ChatAccordion } from './chat-accordion' -import { ShortMessage } from './short-message' -import { IconOpenAI, IconUser } from './ui/icons' - -export default function BrowseListItem({ - thread, - loadMore, - loading, - isLast, - hasMore, - pageType = '' -}: { - thread: Thread - loadMore: () => void - loading: boolean - isLast: boolean - hasMore: boolean - pageType?: string -}) { - const threadRef = React.useRef(null) - const router = useRouter() - const [messages, setMessages] = React.useState([]) - // ! Move to custom hook and add it to the context useThread + useProvider @bran18 - const [isAccordionOpen, setIsAccordionOpen] = React.useState(false) - - React.useEffect(() => { - if (!threadRef.current) return - const observer = new IntersectionObserver(([entry]) => { - if (hasMore && isLast && entry.isIntersecting && !loading) { - const timeout = setTimeout(() => { - loadMore() - clearTimeout(timeout) - }, 150) - - observer.unobserve(entry.target) - } - }) - - observer.observe(threadRef.current) - - return () => { - observer.disconnect() - } - }, [isLast, hasMore, loading, loadMore]) - - const fetchMessages = async () => { - const messages = await getMessages({ threadId: thread.threadId }) - setMessages(_prev => messages) - } - - const handleAccordionToggle = async (isOpen: boolean) => { - if (isOpen) { - setMessages(_prev => []) - await fetchMessages() - } - // When toggling accordion, it should scroll - // Use optional chaining to ensure scrollIntoView is called only if current is not null - await sleep(300) // animation time - threadRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) - setIsAccordionOpen(isOpen) - // Should fetch messages only when opening thread. - } - - const goToThread = () => { - router.push( - `/b/${thread.chatbot.name.trim().toLowerCase()}/${thread.threadId}` - ) - router.refresh() - } - - return ( -
- - {/* Thread Title */} -
- {pageType !== 'bot' && thread.chatbot?.avatar ? ( - - {thread.chatbot?.name - - ) : ( - pageType !== 'bot' && ( - - - - ) - )} -
-
- {thread.messages?.[0]?.content} -
- {pageType !== 'user' && ( - by - )} - {pageType !== 'user' && thread.user?.profilePicture ? ( - - {thread.user?.username - - ) : ( - pageType !== 'user' && ( - - - - ) - )} -
-
- - {/* Thread Description */} - -
- {thread.messages?.[1]?.content && - thread.messages?.[1]?.role !== 'user' ? ( -
- -
- ) : ( - '' - )} -
- - {/* Thread Content */} - - -
-
- ) -} diff --git a/apps/masterbots.ai/components/browse-list.tsx b/apps/masterbots.ai/components/browse-list.tsx deleted file mode 100644 index 9774efc4..00000000 --- a/apps/masterbots.ai/components/browse-list.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client' - -import React from 'react' - -import { useBrowse } from '@/lib/hooks/use-browse' -import { getBrowseThreads } from '@/services/hasura' -import { debounce } from 'lodash' -import { Thread } from 'mb-genql' -import BrowseListItem from './browse-list-item' - -const PAGE_SIZE = 50 - -export default function BrowseList() { - const { keyword, tab } = useBrowse() - - const [threads, setThreads] = React.useState([]) - const [filteredThreads, setFilteredThreads] = React.useState([]) - const [loading, setLoading] = React.useState(false) - const [count, setCount] = React.useState(0) - - const fetchThreads = async (keyword: string, tab: number | null) => { - const threads = await getBrowseThreads({ - categoryId: tab, - keyword, - limit: PAGE_SIZE - }) - setThreads(threads) - setCount(threads.length) - } - - const verifyKeyword = () => { - if (!keyword) { - setFilteredThreads(threads) - } else { - debounce(() => { - // TODO: Improve thread messages architecture to implement dynamic search to show only the thread title (first message on thread) - // fetchThreads(keyword, tab) - setFilteredThreads( - threads.filter((thread: Thread) => - thread.messages[0]?.content - .toLowerCase() - .includes(keyword.toLowerCase()) - ) - ) - // ? Average time of human reaction is 230ms - }, 230)() - } - } - - const loadMore = async () => { - console.log('🟡 Loading More Content') - setLoading(true) - - const moreThreads = await getBrowseThreads({ - categoryId: tab, - offset: threads.length, - limit: PAGE_SIZE - }) - - setThreads(prevState => [...prevState, ...moreThreads]) - setCount(moreThreads.length) - setLoading(false) - } - - React.useEffect(() => { - fetchThreads('', tab) - }, [tab]) - - React.useEffect(() => { - verifyKeyword() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [keyword, threads]) - - return ( -
- {filteredThreads.map((thread: Thread, key) => ( - - ))} -
- ) -} diff --git a/apps/masterbots.ai/components/browse-specific-thread-list.tsx b/apps/masterbots.ai/components/browse-specific-thread-list.tsx deleted file mode 100644 index 8c4a07e1..00000000 --- a/apps/masterbots.ai/components/browse-specific-thread-list.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client' - -import React from 'react' - -import { getBrowseThreads } from '@/services/hasura' -import { Thread } from 'mb-genql' -import BrowseListItem from './browse-list-item' - -export default function BrowseSpecificThreadList({ - initialThreads, - query, - PAGE_SIZE, - pageType = '' -}: { - query: { [key: string]: string | undefined } - initialThreads: Thread[] - PAGE_SIZE: number - pageType?: string -}) { - const [threads, setThreads] = React.useState(initialThreads) - const [loading, setLoading] = React.useState(false) - const [count, setCount] = React.useState(initialThreads.length) - - const loadMore = async () => { - console.log('🟡 Loading More Content') - setLoading(true) - - const moreThreads = await getBrowseThreads({ - ...query, - limit: PAGE_SIZE, - offset: threads.length - }) - - setThreads(prevState => [...prevState, ...moreThreads]) - setCount(moreThreads.length) - setLoading(false) - } - - return ( -
- {threads.map((thread: Thread, key) => ( - - ))} -
- ) -} diff --git a/apps/masterbots.ai/components/browse-thread.tsx b/apps/masterbots.ai/components/browse-thread.tsx deleted file mode 100644 index 0a02386a..00000000 --- a/apps/masterbots.ai/components/browse-thread.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' - -import { cn, extractBetweenMarkers } from '@/lib/utils' - -import { BrowseChatMessages } from './browse-chat-messages' -import { Thread } from 'mb-genql' - -export function BrowseThread({ - thread, - className -}: { - thread: Thread - className?: string -}) { - // we merge past assistant and user messages for ui only - // we remove system prompts from ui - // we extend append function to add our system prompts - - return ( -
- {thread.messages?.length ? ( - - ) : ( - '' - )} -
- ) -} diff --git a/apps/masterbots.ai/components/browse-user-details.tsx b/apps/masterbots.ai/components/browse-user-details.tsx deleted file mode 100644 index 18520696..00000000 --- a/apps/masterbots.ai/components/browse-user-details.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import { User } from 'mb-genql' -import Image from 'next/image' -import { Separator } from './ui/separator' -import { useEffect, useState } from 'react' -import { getBrowseThreads } from '@/services/hasura' - -export default function BrowseChatbotDetails({ user }: { user?: User | null }) { - const [threadNum, setThreadNum] = useState(0) - const getThreadByUserName = async () => { - const threads = await getBrowseThreads({ - slug: user?.slug - }) - setThreadNum(threads.length) - } - useEffect(() => { - getThreadByUserName() - }, []) - return ( -
-
-
-
- {user?.username.replace('_', ' ')} -
- - -
-
- Threads: {threadNum ?? 1} -
-
-
-
- {user?.username -
-
-
- ) -} diff --git a/apps/masterbots.ai/components/category-main-tabs.tsx b/apps/masterbots.ai/components/category-main-tabs.tsx deleted file mode 100644 index 09496ba9..00000000 --- a/apps/masterbots.ai/components/category-main-tabs.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client' - -import * as React from 'react' - -import { useSidebar } from '@/lib/hooks/use-sidebar' -import { ModalComingSoon } from './modal-coming-soon' - -export function CategoryMainTabs() { - const { tab, changeTab } = useSidebar() - const [isOpen, setIsOpen] = React.useState(false) - - return ( - <> - { - setIsOpen(false) - }} - /> -
- - -
- - ) -} diff --git a/apps/masterbots.ai/components/chat-accordion.tsx b/apps/masterbots.ai/components/chat-accordion.tsx deleted file mode 100644 index 84833d60..00000000 --- a/apps/masterbots.ai/components/chat-accordion.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client' - -import { ChevronDown } from 'lucide-react' -import * as React from 'react' -import { useThread } from '@/lib/hooks/use-thread' -import { Thread } from 'mb-genql' - -export const ChatAccordion = ({ - thread = null, - className, - children, - onToggle, - isOpen, - defaultState = false, - triggerClass, - contentClass, - arrowClass, - handleOpen, - handleTrigger, - ...props -}: { - className?: string - children: React.ReactNode[] - defaultState?: boolean - triggerClass?: string - contentClass?: string - onToggle?: (isOpen: boolean) => void - isOpen?: boolean - arrowClass?: string - handleTrigger?: () => void - handleOpen?: () => void - thread?: Thread | null -}) => { - const { activeThread, setActiveThread, setIsNewResponse, isNewResponse } = - useThread() - // If the thread is the active, we keep the thread open - let initialState - - if (defaultState) { - initialState = defaultState - } else { - const { threadId: activeThreadId } = activeThread || {} - const { threadId } = thread || {} - initialState = activeThreadId && threadId === activeThreadId - } - - const [open, setOpen] = React.useState(initialState) - - React.useEffect(() => { - if ( - (thread?.threadId && - activeThread !== null && - thread?.threadId !== activeThread?.threadId) || - (activeThread === null && thread?.threadId) - ) { - setOpen(false) - } - }, [activeThread, thread]) - - React.useEffect(() => { - if (isOpen !== undefined) { - setOpen(isOpen) - } - }, [isOpen]) - - const toggle = () => { - const newState = !open - setOpen(newState) - if (!newState && handleOpen) { - handleOpen() - } - if (thread?.threadId) { - setActiveThread(newState ? thread : null) - } - if (isNewResponse) setIsNewResponse(false) - if (onToggle) { - onToggle(newState) - } - } - - return ( -
- -
- {children[2]} -
-
- ) -} diff --git a/apps/masterbots.ai/components/chat-chatbot.tsx b/apps/masterbots.ai/components/chat-chatbot.tsx deleted file mode 100644 index ea45fdb9..00000000 --- a/apps/masterbots.ai/components/chat-chatbot.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' - -import { Chatbot } from 'mb-genql' -import { useEffect, useState } from 'react' -import { Chat } from './chat' -import { Message } from 'ai/react' -import { useThread } from '@/lib/hooks/use-thread' - -export const ChatChatbot = ({ - initialMessages, - chatbot -}: { - initialMessages?: Message[] - chatbot?: Chatbot -}) => { - const { isOpenPopup } = useThread() - const [newThreadId, setNewThreadId] = useState(crypto.randomUUID()) - - useEffect(() => { - if (isOpenPopup) return - setNewThreadId(crypto.randomUUID()) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpenPopup]) - - return ( - - ) -} diff --git a/apps/masterbots.ai/components/chat-layout-section.tsx b/apps/masterbots.ai/components/chat-layout-section.tsx deleted file mode 100644 index 599b27c4..00000000 --- a/apps/masterbots.ai/components/chat-layout-section.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client' - -import * as React from 'react' -import { ThreadPopup } from './thread-popup' -import { useThread } from '@/lib/hooks/use-thread' - -export function ChatLayoutSection({ children }: { children: React.ReactNode }) { - const { sectionRef, isOpenPopup } = useThread() - - return ( -
} - className="h-[calc(100vh-4rem)] group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out lg:ml-[250px] xl:ml-[300px] relative" - > -
- {children} -
- - {isOpenPopup ? : ''} -
- ) -} diff --git a/apps/masterbots.ai/components/chat-list.tsx b/apps/masterbots.ai/components/chat-list.tsx deleted file mode 100644 index 941f4385..00000000 --- a/apps/masterbots.ai/components/chat-list.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { type Message } from 'ai' - -import { ChatMessage } from '@/components/chat-message' -import { cn, createMessagePairs } from '@/lib/utils' -import { Chatbot } from 'mb-genql' -import React from 'react' -import { ChatAccordion } from './chat-accordion' -import { ShortMessage } from './short-message' -import { useThread } from '@/lib/hooks/use-thread' - -export interface ChatList { - messages: Message[] - sendMessageFromResponse?: (message: string) => void - chatbot?: Chatbot - isThread?: boolean - className?: string - chatContentClass?: string - chatTitleClass?: string - chatArrowClass?: string -} - -type MessagePair = { - userMessage: Message - chatGptMessage: Message[] -} - -export function ChatList({ - className, - messages, - sendMessageFromResponse, - chatbot, - isThread = true, - chatContentClass, - chatTitleClass, - chatArrowClass -}: ChatList) { - const [pairs, setPairs] = React.useState([]) - const { isNewResponse } = useThread() - - React.useEffect(() => { - if (messages.length) { - const prePairs: MessagePair[] = createMessagePairs( - messages - ) as MessagePair[] - setPairs(prePairs) - } else setPairs([]) - }, [messages]) - - if (!messages.length) return null - return ( -
- {pairs.map((pair: MessagePair, key: number) => ( -
- - {/* Thread Title */} - {!isThread && key === 0 ? ( - '' - ) : ( - - )} - - {/* Thread Description */} - {isThread ? ( -
- {pair.chatGptMessage[0]?.content ? ( -
- -
- ) : ( - '' - )} -
- ) : ( - <> - )} - - {/* Thread Content */} -
- {pair.chatGptMessage.length > 0 - ? pair.chatGptMessage.map((message, index) => ( - - )) - : ''} -
-
-
- ))} -
- ) -} diff --git a/apps/masterbots.ai/components/chat-thread-list-panel.tsx b/apps/masterbots.ai/components/chat-thread-list-panel.tsx deleted file mode 100644 index 89334a7f..00000000 --- a/apps/masterbots.ai/components/chat-thread-list-panel.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import { useThread } from '@/lib/hooks/use-thread' -import { Chat } from './chat' - -export default function ChatThreadListPanel() { - const { initialMessages, activeThread } = useThread() - return ( - - ) -} diff --git a/apps/masterbots.ai/components/chat.tsx b/apps/masterbots.ai/components/chat.tsx deleted file mode 100644 index d9edabea..00000000 --- a/apps/masterbots.ai/components/chat.tsx +++ /dev/null @@ -1,293 +0,0 @@ -'use client' - -import { CreateMessage, useChat, type Message } from 'ai/react' -import { useScroll } from 'framer-motion' - -import { ChatList } from '@/components/chat-list' -import { ChatPanel } from '@/components/chat-panel' -import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' -import { cn, extractBetweenMarkers, scrollToBottomOfElement } from '@/lib/utils' - -import { useAtBottom } from '@/lib/hooks/use-at-bottom' -import { createThread, getThread, saveNewMessage } from '@/services/hasura' -import { ChatRequestOptions } from 'ai' -import { uniqBy } from 'lodash' -import { Chatbot } from 'mb-genql' -import { useSession } from 'next-auth/react' -import { useParams, useRouter } from 'next/navigation' -import React, { useEffect } from 'react' -import { toast } from 'react-hot-toast' -import { useThread } from '@/lib/hooks/use-thread' -import { botNames } from '@/lib/bots-names' -import { useSidebar } from '@/lib/hooks/use-sidebar' - -export function Chat({ - initialMessages, - className, - chatbot, - threadId, - chatPanelClassName, - isPopup, - scrollToBottom: scrollToBottomOfPopup, - isAtBottom: isAtBottomOfPopup -}: ChatProps) { - const { data: session } = useSession() - const { - allMessages: threadAllMessages, - initialMessages: threadInitialMessages, - activeThread, - setActiveThread, - setIsNewResponse, - setIsOpenPopup, - isOpenPopup, - sectionRef, - isAtBottom: isAtBottomOfSection - } = useThread() - const { activeChatbot } = useSidebar() - const containerRef = React.useRef() - - const params = useParams<{ chatbot: string; threadId: string }>() - const isNewChat = Boolean(!params.threadId && !activeThread) - - const { messages, append, reload, stop, isLoading, input, setInput } = - useChat({ - // we remove previous assistant responses to get better responses thru - // our prompting strategy - initialMessages: - params.threadId || isNewChat - ? initialMessages?.filter(m => m.role === 'system') - : threadInitialMessages.filter(m => m.role === 'system'), - id: params.threadId || isNewChat ? threadId : activeThread?.threadId, - body: { - id: params.threadId || isNewChat ? threadId : activeThread?.threadId - }, - onResponse(response) { - if (response.status === 401) { - toast.error(response.statusText) - } - }, - async onFinish(message: Message) { - await saveNewMessage({ - role: 'assistant', - threadId: - params.threadId || isNewChat ? threadId : activeThread?.threadId, - content: message.content, - jwt: session!.user?.hasuraJwt - }) - } - }) - - const { scrollY } = useScroll({ - container: containerRef as React.RefObject - }) - - const { isAtBottom } = useAtBottom({ - ref: containerRef, - scrollY - }) - - // ? saffer way to debounce scroll to bottom - let timeoutId: any - const debounceScrollToBottom = (element: HTMLElement | undefined) => { - clearTimeout(timeoutId) - timeoutId = setTimeout(() => { - scrollToBottomOfElement(element) - clearTimeout(timeoutId) - }, 150) //? Adjustable delay as necessary - } - - const scrollToBottom = () => { - if ( - (params.threadId && containerRef.current) || - (!params.threadId && sectionRef.current) - ) { - let element: any - if (sectionRef.current) { - element = sectionRef.current - } else { - element = containerRef.current - } - debounceScrollToBottom(element) - } - } - - // we merge past assistant and user messages for ui only - // we remove system prompts from ui - const allMessages = - params.threadId || isNewChat - ? uniqBy(initialMessages?.concat(messages), 'content').filter( - m => m.role !== 'system' - ) - : uniqBy(threadAllMessages.concat(messages), 'content').filter( - m => m.role !== 'system' - ) - - const sendMessageFromResponse = async (bulletContent: string) => { - setIsNewResponse(true) - const fullMessage = `Tell me more about ${bulletContent}` - await saveNewMessage({ - role: 'user', - threadId: - params.threadId || isNewChat ? threadId : activeThread?.threadId, - content: fullMessage, - jwt: session!.user?.hasuraJwt - }) - append({ - role: 'user', - content: `First, think about the following questions and requests: [${getAllUserMessagesAsStringArray( - allMessages - )}]. Then answer this question: ${fullMessage}` - }) - } - - // we extend append function to add our system prompts - const appendWithMbContextPrompts = async ( - userMessage: Message | CreateMessage, - chatRequestOptions?: ChatRequestOptions - ) => { - if (isNewChat && chatbot) { - await createThread({ - threadId, - chatbotId: chatbot.chatbotId, - jwt: session!.user?.hasuraJwt, - userId: session!.user.id, - isPublic: activeChatbot?.name !== 'BlankBot' - }) - const thread = await getThread({ - threadId, - jwt: session!.user?.hasuraJwt - }) - setActiveThread(thread) - setIsOpenPopup(true) - } - if (activeThread?.threadId) { - setIsOpenPopup(true) - } - await saveNewMessage({ - role: 'user', - threadId: - params.threadId || isNewChat ? threadId : activeThread?.threadId, - content: userMessage.content, - jwt: session!.user?.hasuraJwt - }) - - setIsNewResponse(true) - - return append( - isNewChat - ? userMessage - : { - ...userMessage, - content: `First, think about the following questions and requests: [${getAllUserMessagesAsStringArray( - allMessages - )}]. Then answer this question: ${userMessage.content}` - } - ) - } - - useEffect(() => { - if ( - params.chatbot && - activeThread && - botNames.get(params.chatbot) !== activeThread.chatbot.name - ) { - setIsOpenPopup(false) - setActiveThread(null) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useEffect(() => { - if (isLoading && isOpenPopup && scrollToBottomOfPopup) { - const timeout = setTimeout(() => { - scrollToBottomOfPopup() - clearTimeout(timeout) - }, 150) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading, isOpenPopup, scrollToBottomOfPopup]) - - return ( - <> - {params.threadId ? ( -
} - className={cn( - 'pb-[200px] pt-4 md:pt-10 h-full overflow-auto', - className - )} - > - - -
- ) : null} - - {((isOpenPopup && isPopup) || (!isOpenPopup && !isPopup)) && ( - - )} - - ) -} - -export interface ChatProps extends React.ComponentProps<'div'> { - initialMessages?: Message[] - chatbot?: Chatbot - threadId: string - newThread?: boolean - chatPanelClassName?: string - isPopup?: boolean - scrollToBottom?: () => void - isAtBottom?: boolean -} - -export function getAllUserMessagesAsStringArray(allMessages: Message[]) { - const userMessages = allMessages.filter(m => m.role === 'user') - const cleanMessages = userMessages.map(m => - extractBetweenMarkers(m.content, 'Then answer this question:') - ) - return cleanMessages.join(', ') -} diff --git a/apps/masterbots.ai/components/empty-screen.tsx b/apps/masterbots.ai/components/empty-screen.tsx deleted file mode 100644 index 8bf6b27f..00000000 --- a/apps/masterbots.ai/components/empty-screen.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { UseChatHelpers } from 'ai/react' - -import { Button } from '@/components/ui/button' -import { ExternalLink } from '@/components/external-link' -import { IconArrowRight } from '@/components/ui/icons' -import { botNames } from '@/lib/bots-names' - -const exampleMessages = [ - { - heading: 'Explain technical concepts', - message: `What is a "serverless function"?` - }, - { - heading: 'Summarize an article', - message: 'Summarize the following article for a 2nd grader: \n' - }, - { - heading: 'Draft an email', - message: `Draft an email to my boss about the following: \n` - } -] - -export function EmptyScreen({ - setInput, - bot -}: Pick & { bot: string }) { - return ( -
-
-

- Welcome to Masterbots AI Chatbots! I{`'`}m {botNames.get(bot)}. -

-

- You can start a conversation with me here or try the following - examples: -

-
- {exampleMessages.map((message, index) => ( - - ))} -
-
-
- ) -} diff --git a/apps/masterbots.ai/components/external-link.tsx b/apps/masterbots.ai/components/external-link.tsx deleted file mode 100644 index ba6cc016..00000000 --- a/apps/masterbots.ai/components/external-link.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export function ExternalLink({ - href, - children -}: { - href: string - children: React.ReactNode -}) { - return ( - - {children} - - - ) -} diff --git a/apps/masterbots.ai/components/footer-ct.tsx b/apps/masterbots.ai/components/footer-ct.tsx deleted file mode 100644 index e5038cd5..00000000 --- a/apps/masterbots.ai/components/footer-ct.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ElementType } from "react" - -export default function FooterCT({ nonFooterTag }: { nonFooterTag?: boolean }) { - const Footer: ElementType = ({ children }) => nonFooterTag ? -
- {children} -
: -
- {children} -
- - return ( - - ) -} \ No newline at end of file diff --git a/apps/masterbots.ai/components/header.tsx b/apps/masterbots.ai/components/header.tsx deleted file mode 100644 index cc0b56c7..00000000 --- a/apps/masterbots.ai/components/header.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react' -import Link from 'next/link' - -import { auth } from '@/auth' -import { Button } from '@/components/ui/button' -import { IconSeparator } from '@/components/ui/icons' -import { UserMenu } from '@/components/user-menu' -import { SidebarToggle } from './sidebar-toggle' -import { isTokenExpired } from 'mb-lib' - -// https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating - -export async function Header() { - return ( -
-
- - - - - -
-
- }> - - -
-
- ) -} - -function HeaderLink({ href, text }: { href: string; text: string }) { - return ( - - ) -} - -async function UserOrLogin() { - const session = await auth() - return ( - <> -
- {session?.user && !isTokenExpired(session?.user?.hasuraJwt) ? ( - - ) : ( - - )} -
- - ) -} diff --git a/apps/masterbots.ai/components/layout/auth/sign-in-buttons.tsx b/apps/masterbots.ai/components/layout/auth/sign-in-buttons.tsx new file mode 100644 index 00000000..41ebd089 --- /dev/null +++ b/apps/masterbots.ai/components/layout/auth/sign-in-buttons.tsx @@ -0,0 +1,52 @@ +'use client' + +import Image from 'next/image' +import { Button } from '@/components/ui/button' +import { useSupabaseClient } from '@/services/supabase' + +export function SignInButtons() { + const supabase = useSupabaseClient() + + const loginWithGitHub = () => { + supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + redirectTo: `${location.origin}/auth/callback` + } + }) + } + + const loginWithGoogle = () => { + supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${location.origin}/auth/callback` + } + }) + } + + return ( +
+ + +
+ ) +} diff --git a/apps/masterbots.ai/components/layout/auth/sign-out-button.tsx b/apps/masterbots.ai/components/layout/auth/sign-out-button.tsx new file mode 100644 index 00000000..f7917716 --- /dev/null +++ b/apps/masterbots.ai/components/layout/auth/sign-out-button.tsx @@ -0,0 +1,18 @@ +'use client' +import { Button } from '@/components/ui/button' + +export default function SignOut() { + async function handleSignOut() { + // const { error } = await supabase.auth.signOut() + // if (error) { + // // eslint-disable-next-line no-console + // console.error('ERROR:', error) + // } + } + + return ( + + ) +} diff --git a/apps/masterbots.ai/components/layout/footer-ct.tsx b/apps/masterbots.ai/components/layout/footer-ct.tsx new file mode 100644 index 00000000..50cdde07 --- /dev/null +++ b/apps/masterbots.ai/components/layout/footer-ct.tsx @@ -0,0 +1,37 @@ +import type { ElementType } from 'react' + +export default function FooterCT({ nonFooterTag }: { nonFooterTag?: boolean }) { + const Footer: ElementType = ({ children }) => + nonFooterTag ? ( +
+ {children} +
+ ) : ( +
+ {children} +
+ ) + + return ( + + ) +} diff --git a/apps/masterbots.ai/components/footer.tsx b/apps/masterbots.ai/components/layout/footer.tsx similarity index 99% rename from apps/masterbots.ai/components/footer.tsx rename to apps/masterbots.ai/components/layout/footer.tsx index 9ac2bc06..a17df033 100644 --- a/apps/masterbots.ai/components/footer.tsx +++ b/apps/masterbots.ai/components/layout/footer.tsx @@ -1,6 +1,5 @@ /* eslint-disable react/no-unescaped-entities */ import React from 'react' - import { cn } from '@/lib/utils' export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { diff --git a/apps/masterbots.ai/components/layout/header.tsx b/apps/masterbots.ai/components/layout/header.tsx new file mode 100644 index 00000000..1418a600 --- /dev/null +++ b/apps/masterbots.ai/components/layout/header.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import Link from 'next/link' +import { isTokenExpired } from '@repo/mb-lib' +import { cookies } from 'next/headers' +import { Button } from '@/components/ui/button' +import { IconSeparator } from '@/components/ui/icons' +import { UserMenu } from '@/components/layout/user-menu' +import { createSupabaseServerClient } from '@/services/supabase' +import { SidebarToggle } from '../routes/c/sidebar/sidebar-toggle' +import { getUser } from '@/services/hasura' + +// https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating + +export async function Header() { + const supabase = await createSupabaseServerClient() + const { + data: { user } + } = await supabase.auth.getUser() + + const jwt = cookies().get('hasuraJwt')?.value || '' + + return ( +
+
+
+ + + + + +
+
+ {user && !isTokenExpired(jwt) ? ( + + ) : ( + + )} +
+
+
+ ) +} + +function HeaderLink({ href, text }: { href: string; text: string }) { + return ( + + ) +} diff --git a/apps/masterbots.ai/components/layout/providers.tsx b/apps/masterbots.ai/components/layout/providers.tsx new file mode 100644 index 00000000..09e35119 --- /dev/null +++ b/apps/masterbots.ai/components/layout/providers.tsx @@ -0,0 +1,25 @@ +'use client' + +import * as React from 'react' +import { ThemeProvider as NextThemesProvider } from 'next-themes' +import type { ThemeProviderProps } from 'next-themes/dist/types' +import { SidebarProvider } from '@/hooks/use-sidebar' +import { TooltipProvider } from '@/components/ui/tooltip' +import { ThreadProvider } from '@/hooks/use-thread' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function Providers({ children, ...props }: ThemeProviderProps) { + return ( + + + + + {children} + + + + + ) +} diff --git a/apps/masterbots.ai/components/tailwind-indicator.tsx b/apps/masterbots.ai/components/layout/tailwind-indicator.tsx similarity index 100% rename from apps/masterbots.ai/components/tailwind-indicator.tsx rename to apps/masterbots.ai/components/layout/tailwind-indicator.tsx diff --git a/apps/masterbots.ai/components/theme-toggle.tsx b/apps/masterbots.ai/components/layout/theme-toggle.tsx similarity index 99% rename from apps/masterbots.ai/components/theme-toggle.tsx rename to apps/masterbots.ai/components/layout/theme-toggle.tsx index 67d3f1a2..e5955023 100644 --- a/apps/masterbots.ai/components/theme-toggle.tsx +++ b/apps/masterbots.ai/components/layout/theme-toggle.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { useTheme } from 'next-themes' - import { Button } from '@/components/ui/button' import { IconMoon, IconSun } from '@/components/ui/icons' @@ -12,13 +11,13 @@ export function ThemeToggle() { return ( - + -
{user?.name}
-
{user?.email}
+
name
+
email
- - signOut({ - callbackUrl: '/' - }) - } - className="text-xs" - > + Log Out
@@ -65,3 +55,11 @@ export function UserMenu({ user }: UserMenuProps) { ) } + +function getUserInitials(name: string) { + const [firstName, lastName] = name.split(' ') + return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2) +} +export interface UserMenuProps { + user: UserProfile +} diff --git a/apps/masterbots.ai/components/login-button.tsx b/apps/masterbots.ai/components/login-button.tsx deleted file mode 100644 index da1fccd6..00000000 --- a/apps/masterbots.ai/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 Google', - showGithubIcon = true, - className, - ...props -}: LoginButtonProps) { - const [isLoading, setIsLoading] = React.useState(false) - return ( - - ) -} diff --git a/apps/masterbots.ai/components/modal-coming-soon.tsx b/apps/masterbots.ai/components/modal-coming-soon.tsx deleted file mode 100644 index ac79bef4..00000000 --- a/apps/masterbots.ai/components/modal-coming-soon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client' - -import * as React from 'react' - -import { cn } from '@/lib/utils' -import { IconClose } from './ui/icons' - -export interface ModalComingSoonProps extends React.ComponentProps<'div'> { - isOpen: boolean - onClose: () => void -} - -export function ModalComingSoon({ className, children, isOpen, onClose }: ModalComingSoonProps) { - - - return ( -
-
-
-
- -
-
- Coming Soon! -
-
-
- ) -} diff --git a/apps/masterbots.ai/components/providers.tsx b/apps/masterbots.ai/components/providers.tsx deleted file mode 100644 index 99847a85..00000000 --- a/apps/masterbots.ai/components/providers.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client' - -import * as React from 'react' -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 { SessionProvider } from 'next-auth/react' -import { ThreadProvider } from '@/lib/hooks/use-thread' - -export function Providers({ children, ...props }: ThemeProviderProps) { - return ( - - - - - {children} - - - - - ) -} diff --git a/apps/masterbots.ai/components/button-scroll-to-bottom.tsx b/apps/masterbots.ai/components/routes/c/button-scroll-to-bottom.tsx similarity index 91% rename from apps/masterbots.ai/components/button-scroll-to-bottom.tsx rename to apps/masterbots.ai/components/routes/c/button-scroll-to-bottom.tsx index 78b1fe5f..3ec1f639 100644 --- a/apps/masterbots.ai/components/button-scroll-to-bottom.tsx +++ b/apps/masterbots.ai/components/routes/c/button-scroll-to-bottom.tsx @@ -1,9 +1,8 @@ 'use client' import * as React from 'react' - import { cn } from '@/lib/utils' -import { Button, type ButtonProps } from '@/components/ui/button' +import { Button, ButtonProps } from '@/components/ui/button' import { IconArrowDown } from '@/components/ui/icons' export function ButtonScrollToBottom({ @@ -14,8 +13,6 @@ export function ButtonScrollToBottom({ }: ButtonProps & { isAtBottom?: boolean; scrollToBottom: () => void }) { return (
-
+
{activeChatbot?.avatar
diff --git a/apps/masterbots.ai/components/chat-clickable-text.tsx b/apps/masterbots.ai/components/routes/c/chat-clickable-text.tsx similarity index 97% rename from apps/masterbots.ai/components/chat-clickable-text.tsx rename to apps/masterbots.ai/components/routes/c/chat-clickable-text.tsx index 2fd9fc62..44125ba3 100644 --- a/apps/masterbots.ai/components/chat-clickable-text.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-clickable-text.tsx @@ -28,7 +28,7 @@ export function ClickableText({ ) const generalPattern = /(.*?)([:.,])(?:\s|$)/g // First, check for the UNIQUE pattern - const uniqueMatch = fullText.match(uniquePattern) + const uniqueMatch = uniquePattern.exec(fullText) let clickableText = uniqueMatch ? uniqueMatch[1] : '' let restText = uniqueMatch ? fullText.slice(fullText.indexOf(clickableText) + clickableText.length) diff --git a/apps/masterbots.ai/components/chat-history.tsx b/apps/masterbots.ai/components/routes/c/chat-history.tsx similarity index 92% rename from apps/masterbots.ai/components/chat-history.tsx rename to apps/masterbots.ai/components/routes/c/chat-history.tsx index 7450fe8c..225dfc34 100644 --- a/apps/masterbots.ai/components/chat-history.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-history.tsx @@ -1,9 +1,7 @@ import * as React from 'react' - import Link from 'next/link' - import { cn } from '@/lib/utils' -import { SidebarList } from '@/components/sidebar-list' +import { SidebarList } from '@/components/routes/c/sidebar/sidebar-list' import { buttonVariants } from '@/components/ui/button' import { IconPlus } from '@/components/ui/icons' @@ -16,11 +14,11 @@ export async function ChatHistory({ userId }: ChatHistoryProps) {
New Chat @@ -31,14 +29,14 @@ export async function ChatHistory({ userId }: ChatHistoryProps) {
{Array.from({ length: 10 }).map((_, i) => (
))}
} > - {/* @ts-ignore */} + {/* @ ts-expect-error */}
diff --git a/apps/masterbots.ai/components/chat-panel.tsx b/apps/masterbots.ai/components/routes/c/chat-input-new.tsx similarity index 77% rename from apps/masterbots.ai/components/chat-panel.tsx rename to apps/masterbots.ai/components/routes/c/chat-input-new.tsx index d7061e19..c30a2944 100644 --- a/apps/masterbots.ai/components/chat-panel.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-input-new.tsx @@ -1,17 +1,16 @@ import * as React from 'react' import { type UseChatHelpers } from 'ai/react' - +import { Chatbot } from '@repo/mb-genql' import { Button } from '@/components/ui/button' -import { PromptForm } from '@/components/prompt-form' -import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' +import { PromptForm } from '@/components/routes/c/prompt-form' +import { ButtonScrollToBottom } from '@/components/routes/c/button-scroll-to-bottom' import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons' -import { FooterText } from '@/components/footer' -import { ChatShareDialog } from '@/components/chat-share-dialog' -import { Chatbot } from 'mb-genql' +import { FooterText } from '@/components/layout/footer' +import { ChatShareDialog } from '@/components/routes/c/chat-share-dialog' import { cn } from '@/lib/utils' -import { useThread } from '@/lib/hooks/use-thread' +import { useThread } from '@/hooks/use-thread' -export interface ChatPanelProps +export interface ChatInputProps extends Pick< UseChatHelpers, | 'append' @@ -30,9 +29,10 @@ export interface ChatPanelProps isAtBottom?: boolean scrollToBottom: () => void className?: string + dialog?: boolean } -export function ChatPanel({ +export function ChatInputNew({ id, title, isLoading, @@ -47,45 +47,51 @@ export function ChatPanel({ showReload = true, isAtBottom, scrollToBottom, - className -}: ChatPanelProps) { + className, + dialog = false +}: ChatInputProps) { const [shareDialogOpen, setShareDialogOpen] = React.useState(false) const { isOpenPopup } = useThread() return (
{chatbot && showReload ? (
{isLoading ? ( ) : ( - messages?.length >= 2 && ( + messages.length >= 2 && (
- {id && title ? ( <>
diff --git a/apps/masterbots.ai/components/routes/c/chat-list.tsx b/apps/masterbots.ai/components/routes/c/chat-list.tsx new file mode 100644 index 00000000..00e444fb --- /dev/null +++ b/apps/masterbots.ai/components/routes/c/chat-list.tsx @@ -0,0 +1,52 @@ +import { type Message } from 'ai' +import type { Chatbot } from '@repo/mb-genql' +import React from 'react' +import { useThread } from '@/hooks/use-thread' + +export interface ChatList { + messages: Message[] + sendMessageFromResponse?: (message: string) => void + chatbot?: Chatbot + isThread?: boolean + className?: string + chatContentClass?: string + chatTitleClass?: string + chatArrowClass?: string +} + +interface MessagePair { + userMessage: Message + chatGptMessage: Message[] +} + +export function ChatList({ + className, + messages, + sendMessageFromResponse, + chatbot, + isThread = true, + chatContentClass, + chatTitleClass, + chatArrowClass +}: ChatList) { + const [pairs, setPairs] = React.useState([]) + const { isNewResponse } = useThread() + + // React.useEffect(() => { + // if (messages.length) { + // const prePairs: MessagePair[] = createMessagePairs( + // messages as Message[] + // ) as MessagePair[] + // setPairs(prePairs) + // } else setPairs([]) + // }, [messages]) + + if (!messages.length) return null + return ( +
+ Accordion +
+ ) +} diff --git a/apps/masterbots.ai/components/chat-message-actions.tsx b/apps/masterbots.ai/components/routes/c/chat-message-actions.tsx similarity index 87% rename from apps/masterbots.ai/components/chat-message-actions.tsx rename to apps/masterbots.ai/components/routes/c/chat-message-actions.tsx index d4e4b40a..980dbca5 100644 --- a/apps/masterbots.ai/components/chat-message-actions.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-message-actions.tsx @@ -1,10 +1,9 @@ 'use client' import { type Message } from 'ai' - import { Button } from '@/components/ui/button' import { IconCheck, IconCopy } from '@/components/ui/icons' -import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { cn } from '@/lib/utils' interface ChatMessageActionsProps extends React.ComponentProps<'div'> { @@ -31,7 +30,7 @@ export function ChatMessageActions({ )} {...props} > - diff --git a/apps/masterbots.ai/components/chat-message.tsx b/apps/masterbots.ai/components/routes/c/chat-message.tsx similarity index 89% rename from apps/masterbots.ai/components/chat-message.tsx rename to apps/masterbots.ai/components/routes/c/chat-message.tsx index 7c8b25ea..c3e20772 100644 --- a/apps/masterbots.ai/components/chat-message.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-message.tsx @@ -1,16 +1,16 @@ // Inspired by Chatbot-UI and modified to fit the needs of this project // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatcleanMessage.tsx -import { ClickableText } from '@/components/chat-clickable-text' -import { ChatMessageActions } from '@/components/chat-message-actions' -import { MemoizedReactMarkdown } from '@/components/markdown' -import { CodeBlock } from '@/components/ui/codeblock' -import { cleanPrompt, cn } from '@/lib/utils' -import { Message } from 'ai' -import { Chatbot } from 'mb-genql' - +import type { Message } from 'ai' +import type { Chatbot } from '@repo/mb-genql' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import { ClickableText } from '@/components/routes/c/chat-clickable-text' +import { ChatMessageActions } from '@/components/routes/c/chat-message-actions' +import { MemoizedReactMarkdown } from '@/components/shared/markdown' +import { CodeBlock } from '@/components/ui/codeblock' +import { cn } from '@/lib/utils' +import { cleanPrompt } from '@/lib/threads' export interface ChatMessageProps { message: Message @@ -33,7 +33,6 @@ export function ChatMessage({
{cleanMessage.content} diff --git a/apps/masterbots.ai/components/chat-scroll-anchor.tsx b/apps/masterbots.ai/components/routes/c/chat-scroll-anchor.tsx similarity index 86% rename from apps/masterbots.ai/components/chat-scroll-anchor.tsx rename to apps/masterbots.ai/components/routes/c/chat-scroll-anchor.tsx index 930c8956..d24ec175 100644 --- a/apps/masterbots.ai/components/chat-scroll-anchor.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-scroll-anchor.tsx @@ -20,11 +20,11 @@ export function ChatScrollAnchor({ React.useEffect(() => { if (isAtBottom && trackVisibility && !inView) { - entry?.target.scrollIntoView({ + entry.target.scrollIntoView({ block: 'start' }) } }, [inView, entry, isAtBottom, trackVisibility]) - return
+ return
} diff --git a/apps/masterbots.ai/components/chat-search-input.tsx b/apps/masterbots.ai/components/routes/c/chat-search-input.tsx similarity index 82% rename from apps/masterbots.ai/components/chat-search-input.tsx rename to apps/masterbots.ai/components/routes/c/chat-search-input.tsx index bc5e7575..80b67f24 100644 --- a/apps/masterbots.ai/components/chat-search-input.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-search-input.tsx @@ -1,22 +1,26 @@ 'use client' -import FooterCT from '@/components/footer-ct' +import { debounce } from 'lodash' +import type { Thread } from '@repo/mb-genql' +import { useParams } from 'next/navigation' +import React from 'react' +import FooterCT from '@/components/layout/footer-ct' import { Button } from '@/components/ui/button' import { IconClose } from '@/components/ui/icons' import { Input } from '@/components/ui/input' -import { useSidebar } from '@/lib/hooks/use-sidebar' +import { useSidebar } from '@/hooks/use-sidebar' import { getCategory } from '@/services/hasura' -import { debounce } from 'lodash' -import { Thread } from 'mb-genql' -import { useParams } from 'next/navigation' -import React from 'react' -export function ChatSearchInput({ setThreads }: { +export function ChatSearchInput({ + setThreads +}: { setThreads: React.Dispatch> }) { const { chatbot } = useParams() const { activeCategory } = useSidebar() - const [searchPlaceholder, setSearchPlaceholder] = React.useState(null) + const [searchPlaceholder, setSearchPlaceholder] = React.useState< + string | null + >(null) const [keyword, changeKeyword] = React.useState('') const previousThread = React.useRef([]) const previousCategory = React.useRef(null) @@ -42,7 +46,9 @@ export function ChatSearchInput({ setThreads }: { setThreads(prevState => { // ? If there is no results on a search, we should keep the previous state // ? and if not, the threads previous state before the search will be lost. - previousThread.current = !previousThread.current.length ? prevState : previousThread.current + previousThread.current = !previousThread.current.length + ? prevState + : previousThread.current const previousThreadState = previousThread.current if (!keyword) { @@ -61,24 +67,26 @@ export function ChatSearchInput({ setThreads }: { return (
{ changeKeyword(e.target.value) }} placeholder={`Search any chat with ${searchPlaceholder ? searchPlaceholder : 'any bot category'}`} - className="max-w-[600px]" + value={keyword} /> - {keyword && ( + {keyword ? ( - )} + ) : null}
diff --git a/apps/masterbots.ai/components/chat-share-dialog.tsx b/apps/masterbots.ai/components/routes/c/chat-share-dialog.tsx similarity index 94% rename from apps/masterbots.ai/components/chat-share-dialog.tsx rename to apps/masterbots.ai/components/routes/c/chat-share-dialog.tsx index 94e9d421..43aa80ab 100644 --- a/apps/masterbots.ai/components/chat-share-dialog.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-share-dialog.tsx @@ -4,8 +4,7 @@ import * as React from 'react' import Link from 'next/link' import { type DialogProps } from '@radix-ui/react-dialog' import { toast } from 'react-hot-toast' - -import { ServerActionResult, type Chat } from '@/lib/types' +import { ServerActionResult, Chat } from '@/lib/types' import { cn } from '@/lib/utils' import { badgeVariants } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -18,7 +17,7 @@ import { DialogTitle } from '@/components/ui/dialog' import { IconSpinner } from '@/components/ui/icons' -import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' interface ChatShareDialogProps extends DialogProps { chat: Pick @@ -80,7 +79,7 @@ export function ChatShareDialog({