diff --git a/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx index 1f203e3d..b3b331cb 100644 --- a/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx +++ b/apps/masterbots.ai/app/(browse)/[category]/[threadId]/page.tsx @@ -1,19 +1,25 @@ -import { getThread } from '@/services/hasura' -import { BrowseThread } from '@/components/browse/browse-thread' +import { getCategories, getMessagePairs, getThread } from '@/services/hasura' + import type { ChatPageProps } from '@/app/c/[chatbot]/[threadId]/page' -import Shortlink from '@/components/browse/shortlink-button' +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 ThreadLandingPage({ params }: ChatPageProps) { + const categories = await getCategories() const thread = await getThread({ threadId: params.threadId }) - return ( - <> -
- -
+ const initialMessagePairs = await getMessagePairs(thread.threadId) - - + return ( +
+ + + +
) } diff --git a/apps/masterbots.ai/app/(browse)/[category]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/page.tsx index 05db7811..1d26482d 100644 --- a/apps/masterbots.ai/app/(browse)/[category]/page.tsx +++ b/apps/masterbots.ai/app/(browse)/[category]/page.tsx @@ -1,6 +1,6 @@ -import BrowseList from '@/components/browse/browse-list' -import { BrowseCategoryTabs } from '@/components/browse/browse-category-tabs' -import { BrowseSearchInput } from '@/components/browse/browse-search-input' +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 @@ -13,7 +13,7 @@ export default async function BrowseCategoryPage({ const categories = await getCategories() const categoryId = categories.find( c => - c.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, 'n') === + c.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, '_') === params.category ).categoryId if (!categoryId) throw new Error('Category id not foud') @@ -24,13 +24,10 @@ export default async function BrowseCategoryPage({ }) return ( -
- - - +
+ + +
) } diff --git a/apps/masterbots.ai/app/(browse)/page.tsx b/apps/masterbots.ai/app/(browse)/page.tsx index 719ed596..9c39ccbf 100644 --- a/apps/masterbots.ai/app/(browse)/page.tsx +++ b/apps/masterbots.ai/app/(browse)/page.tsx @@ -1,21 +1,18 @@ -import BrowseList from '@/components/browse/browse-list' -import { BrowseCategoryTabs } from '@/components/browse/browse-category-tabs' -import { BrowseSearchInput } from '@/components/browse/browse-search-input' +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 BrowsePage() { const categories = await getCategories() const threads = await getBrowseThreads({ limit: 50 }) - return ( -
- - - +
+ + +
) } diff --git a/apps/masterbots.ai/app/b/[id]/page.tsx b/apps/masterbots.ai/app/b/[id]/page.tsx index f0f8b3e4..e9fc5ccd 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/browse-chatbot-details' -import BrowseSpecificThreadList from '@/components/browse/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: 50 }) return ( -
- {chatbot ? : ''} - + + + +
+ +
) } diff --git a/apps/masterbots.ai/app/c/[chatbot]/[threadId]/page.tsx b/apps/masterbots.ai/app/c/[chatbot]/[threadId]/page.tsx index 3403606f..c7d48640 100644 --- a/apps/masterbots.ai/app/c/[chatbot]/[threadId]/page.tsx +++ b/apps/masterbots.ai/app/c/[chatbot]/[threadId]/page.tsx @@ -2,7 +2,7 @@ import { redirect } from 'next/navigation' import type { Message } from 'ai/react' import { isTokenExpired } from '@repo/mb-lib' import { cookies } from 'next/headers' -import { Chat } from '@/components/c/chat' +import { Chat } from '@/components/routes/c/chat' import { getThread } from '@/services/hasura' import { getUserProfile } from '@/services/supabase' diff --git a/apps/masterbots.ai/app/c/[chatbot]/page.tsx b/apps/masterbots.ai/app/c/[chatbot]/page.tsx index 970aad53..0141b9e1 100644 --- a/apps/masterbots.ai/app/c/[chatbot]/page.tsx +++ b/apps/masterbots.ai/app/c/[chatbot]/page.tsx @@ -3,8 +3,8 @@ import { isTokenExpired } from '@repo/mb-lib' import { nanoid } from 'nanoid' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' -import { ChatChatbot } from '@/components/c/chat-chatbot' -import ThreadPanel from '@/components/c/thread-panel' +import { ChatChatbot } from '@/components/routes/c/chat-chatbot' +import ThreadPanel from '@/components/routes/c/thread-panel' import { botNames } from '@/lib/bots-names' import { getChatbot, getThreads } from '@/services/hasura' import { getUserProfile } from '@/services/supabase' diff --git a/apps/masterbots.ai/app/c/layout.tsx b/apps/masterbots.ai/app/c/layout.tsx index 86ab7fd8..54821f7b 100644 --- a/apps/masterbots.ai/app/c/layout.tsx +++ b/apps/masterbots.ai/app/c/layout.tsx @@ -1,5 +1,5 @@ -import { ChatLayoutSection } from '@/components/c/chat-layout-section' -import { ResponsiveSidebar } from '@/components/c/sidebar/sidebar-responsive' +import { ChatLayoutSection } from '@/components/routes/c/chat-layout-section' +import { ResponsiveSidebar } from '@/components/routes/c/sidebar/sidebar-responsive' import FooterCT from '@/components/layout/footer-ct' interface ChatLayoutProps { diff --git a/apps/masterbots.ai/app/c/page.tsx b/apps/masterbots.ai/app/c/page.tsx index 1979a52f..d0917350 100644 --- a/apps/masterbots.ai/app/c/page.tsx +++ b/apps/masterbots.ai/app/c/page.tsx @@ -1,8 +1,8 @@ import { isTokenExpired } from '@repo/mb-lib' import { redirect } from 'next/navigation' import { cookies } from 'next/headers' -import ChatThreadListPanel from '@/components/c/chat-thread-list-panel' -import ThreadPanel from '@/components/c/thread-panel' +import ChatThreadListPanel from '@/components/routes/c/chat-thread-list-panel' +import ThreadPanel from '@/components/routes/c/thread-panel' import { getThreads } from '@/services/hasura' import { getUserProfile } from '@/services/supabase' 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 e3441711..6040683b 100644 --- a/apps/masterbots.ai/app/layout.tsx +++ b/apps/masterbots.ai/app/layout.tsx @@ -9,9 +9,18 @@ import { Providers } from '@/components/layout/providers' import { cn } from '@/lib/utils' import { GlobalStoreProvider } from '@/hooks/use-global-store' -export default async function RootLayout({ children }: RootLayoutProps) { +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() return ( diff --git a/apps/masterbots.ai/app/p/page.tsx b/apps/masterbots.ai/app/p/page.tsx index d2325d98..2086576f 100644 --- a/apps/masterbots.ai/app/p/page.tsx +++ b/apps/masterbots.ai/app/p/page.tsx @@ -1,5 +1,5 @@ import { Suspense } from 'react' -import { WorkEarlyAccessForm } from '@/components/p/early-access-from' +import { WorkEarlyAccessForm } from '@/components/routes/p/early-access-from' export default function WorkPage() { return ( diff --git a/apps/masterbots.ai/app/u/[slug]/page.tsx b/apps/masterbots.ai/app/u/[slug]/page.tsx index 729d66ea..b68cbd06 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/browse-user-details' -import BrowseSpecificThreadList from '@/components/browse/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: 50 }) return ( -
- - + + + +
+ +
) } diff --git a/apps/masterbots.ai/components/browse/browse-chat-message-list.tsx b/apps/masterbots.ai/components/browse/browse-chat-message-list.tsx deleted file mode 100644 index efa2afbf..00000000 --- a/apps/masterbots.ai/components/browse/browse-chat-message-list.tsx +++ /dev/null @@ -1,79 +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 type { Chatbot, Message, User } from '@repo/mb-genql' -import Image from 'next/image' -import Link from 'next/link' -import React from 'react' -import { cn, createMessagePairs } from '@/lib/utils' -import { IconUser } from '@/components/ui/icons' -import { ChatAccordion } from '../shared/chat-accordion' -import type { MessagePair } from './browse-chat-messages' -import { convertMessage } from './browse-chat-messages' -import { BrowseChatMessage } from './browse-chat-message' - -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} -
-
- ) : null} - - {/* Thread Description */} - <> - - {/* Thread Content */} -
- {pair.chatGptMessage.length > 0 - ? pair.chatGptMessage.map((message, index) => ( - - )) - : ''} -
-
- ))} -
- ) -} diff --git a/apps/masterbots.ai/components/browse/browse-chat-messages.tsx b/apps/masterbots.ai/components/browse/browse-chat-messages.tsx deleted file mode 100644 index c219f4c5..00000000 --- a/apps/masterbots.ai/components/browse/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 type * as AI from 'ai' -import type { Chatbot, Message, User } from '@repo/mb-genql' -import React from 'react' -import { getMessages } from '@/services/hasura' -import BrowseChatbotDetails from './browse-chatbot-details' -import { BrowseChatMessageList } from './browse-chat-message-list' - -export interface 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 }) - setMessages(messages) - } - } - React.useEffect(() => { - fetchMessages() - }, [threadId]) - - return ( -
- -
- -
-
- ) -} diff --git a/apps/masterbots.ai/components/browse/browse-chatbot-details.tsx b/apps/masterbots.ai/components/browse/browse-chatbot-details.tsx deleted file mode 100644 index 1145d029..00000000 --- a/apps/masterbots.ai/components/browse/browse-chatbot-details.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import type { Chatbot } from '@repo/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/browse-list-item.tsx b/apps/masterbots.ai/components/browse/browse-list-item.tsx deleted file mode 100644 index 1503120e..00000000 --- a/apps/masterbots.ai/components/browse/browse-list-item.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import Image from 'next/image' -import { Message, Thread } from '@repo/mb-genql' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import React from 'react' -import { cn, sleep } from '@/lib/utils' -import { getMessages } from '@/services/hasura' -import { ChatAccordion } from '../shared/chat-accordion' -import { ShortMessage } from '../short-message' -import { IconOpenAI, IconUser } from '../ui/icons' -import { BrowseChatMessageList } from './browse-chat-message-list' - -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/browse-list.tsx b/apps/masterbots.ai/components/browse/browse-list.tsx deleted file mode 100644 index 7d0f9559..00000000 --- a/apps/masterbots.ai/components/browse/browse-list.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import React from 'react' -import { debounce } from 'lodash' -import type { Thread } from '@repo/mb-genql' -import { useBrowse } from '@/hooks/use-browse' -import { getBrowseThreads } from '@/services/hasura' -import BrowseListItem from './browse-list-item' - -const PAGE_SIZE = 50 - -export default function BrowseList({ initialThreads }: BrowseListProps) { - const { keyword, tab } = useBrowse() - const [threads, setThreads] = React.useState(initialThreads) - const [filteredThreads, setFilteredThreads] = - React.useState(initialThreads) - const [loading, setLoading] = React.useState(false) - const [count, setCount] = React.useState(initialThreads.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(() => { - verifyKeyword() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [keyword, threads]) - - return ( -
- {filteredThreads.map((thread: Thread, key) => ( - - ))} -
- ) -} - -type BrowseListProps = { - initialThreads: Thread[] -} diff --git a/apps/masterbots.ai/components/browse/browse-specific-thread-list.tsx b/apps/masterbots.ai/components/browse/browse-specific-thread-list.tsx deleted file mode 100644 index 4cbf613e..00000000 --- a/apps/masterbots.ai/components/browse/browse-specific-thread-list.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client' - -import React from 'react' -import type { Thread } from '@repo/mb-genql' -import { getBrowseThreads } from '@/services/hasura' -import BrowseListItem from './browse-list-item' - -export default function BrowseSpecificThreadList({ - initialThreads, - query, - PAGE_SIZE, - pageType = '' -}: { - query: Record - 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/browse-thread.tsx b/apps/masterbots.ai/components/browse/browse-thread.tsx deleted file mode 100644 index aa010ad7..00000000 --- a/apps/masterbots.ai/components/browse/browse-thread.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client' - -import type { Thread } from '@repo/mb-genql' -import { cn } from '@/lib/utils' -import { BrowseChatMessages } from './browse-chat-messages' - -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/browse-user-details.tsx b/apps/masterbots.ai/components/browse/browse-user-details.tsx deleted file mode 100644 index bb848475..00000000 --- a/apps/masterbots.ai/components/browse/browse-user-details.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import type { User } from '@repo/mb-genql' -import Image from 'next/image' -import { useEffect, useState } from 'react' -import { getBrowseThreads } from '@/services/hasura' -import { Separator } from '../ui/separator' - -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/browse/shortlink-button.tsx b/apps/masterbots.ai/components/browse/shortlink-button.tsx deleted file mode 100644 index 03585e9e..00000000 --- a/apps/masterbots.ai/components/browse/shortlink-button.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client' - -import Link from 'next/link' -import { useFormState, useFormStatus } from 'react-dom' -import { CardContent } from '@/components/ui/card' -import { Label } from '@/components/ui/label' -import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' -import { shorten } from '@/app/actions' - -const initialState = { - shortLink: '' -} - -export default function Shortlink() { - const [state, formAction] = useFormState(shorten, initialState) - - const url = window.location.href.replace( - 'http://localhost:3000', - 'https://dev.masterbots.ai' - ) - - return ( - -
- - - - {state.shortLink ? ( -
-

- - {state.shortLink.replace(/^https?:\/\//, '')} - -

-
- ) : null} -
- ) -} - -function SubmitButton() { - const { pending } = useFormStatus() - - return ( - - ) -} - -function LoadingCircle() { - return ( - - ) -} diff --git a/apps/masterbots.ai/components/c/thread-date-range-picker.tsx b/apps/masterbots.ai/components/c/thread-date-range-picker.tsx deleted file mode 100644 index 26abf56e..00000000 --- a/apps/masterbots.ai/components/c/thread-date-range-picker.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client' - -import * as React from 'react' -import { CalendarIcon } from '@radix-ui/react-icons' -import { addDays, format } from 'date-fns' -import type { DateRange } from 'react-day-picker' -import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import { Calendar } from '@/components/ui/calendar' -import { - Popover, - PopoverContent, - PopoverTrigger -} from '@/components/ui/popover' - -export function DateRangePicker({ - className -}: React.HTMLAttributes) { - const [date, setDate] = React.useState({ - from: new Date(2023, 0, 20), - to: addDays(new Date(2023, 0, 20), 20) - }) - - return ( -
- - - - - - - - -
- ) -} diff --git a/apps/masterbots.ai/components/empty-screen.tsx b/apps/masterbots.ai/components/empty-screen.tsx deleted file mode 100644 index 73e63488..00000000 --- a/apps/masterbots.ai/components/empty-screen.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { 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 e5a1baee..00000000 --- a/apps/masterbots.ai/components/external-link.tsx +++ /dev/null @@ -1,30 +0,0 @@ -export function ExternalLink({ - href, - children -}: { - href: string - children: React.ReactNode -}) { - return ( - - {children} - - - ) -} diff --git a/apps/masterbots.ai/components/layout/header.tsx b/apps/masterbots.ai/components/layout/header.tsx index 513c977a..2ed83d03 100644 --- a/apps/masterbots.ai/components/layout/header.tsx +++ b/apps/masterbots.ai/components/layout/header.tsx @@ -4,9 +4,9 @@ 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/user-menu' +import { UserMenu } from '@/components/layout/user-menu' import { getUserProfile } from '@/services/supabase' -import { SidebarToggle } from '../c/sidebar/sidebar-toggle' +import { SidebarToggle } from '../routes/c/sidebar/sidebar-toggle' // https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating @@ -15,22 +15,24 @@ export async function Header() { const jwt = cookies().get('hasuraJwt')?.value || '' return ( -
-
- - - - - -
-
- {user && !isTokenExpired(jwt) ? ( - - ) : ( - - )} +
+
+
+ + + + + +
+
+ {user && !isTokenExpired(jwt) ? ( + + ) : ( + + )} +
) diff --git a/apps/masterbots.ai/components/layout/providers.tsx b/apps/masterbots.ai/components/layout/providers.tsx index 8361f35d..09e35119 100644 --- a/apps/masterbots.ai/components/layout/providers.tsx +++ b/apps/masterbots.ai/components/layout/providers.tsx @@ -6,15 +6,20 @@ 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} - - + + + + {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 100% rename from apps/masterbots.ai/components/theme-toggle.tsx rename to apps/masterbots.ai/components/layout/theme-toggle.tsx diff --git a/apps/masterbots.ai/components/user-menu.tsx b/apps/masterbots.ai/components/layout/user-menu.tsx similarity index 100% rename from apps/masterbots.ai/components/user-menu.tsx rename to apps/masterbots.ai/components/layout/user-menu.tsx 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 6da04058..00000000 --- a/apps/masterbots.ai/components/modal-coming-soon.tsx +++ /dev/null @@ -1,45 +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/button-scroll-to-bottom.tsx b/apps/masterbots.ai/components/routes/c/button-scroll-to-bottom.tsx similarity index 100% rename from apps/masterbots.ai/components/button-scroll-to-bottom.tsx rename to apps/masterbots.ai/components/routes/c/button-scroll-to-bottom.tsx diff --git a/apps/masterbots.ai/components/shared/chat-accordion.tsx b/apps/masterbots.ai/components/routes/c/chat-accordion.tsx similarity index 100% rename from apps/masterbots.ai/components/shared/chat-accordion.tsx rename to apps/masterbots.ai/components/routes/c/chat-accordion.tsx diff --git a/apps/masterbots.ai/components/c/chat-chatbot-details.tsx b/apps/masterbots.ai/components/routes/c/chat-chatbot-details.tsx similarity index 98% rename from apps/masterbots.ai/components/c/chat-chatbot-details.tsx rename to apps/masterbots.ai/components/routes/c/chat-chatbot-details.tsx index bb71adca..f1c47d38 100644 --- a/apps/masterbots.ai/components/c/chat-chatbot-details.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-chatbot-details.tsx @@ -6,7 +6,7 @@ import { useSidebar } from '@/hooks/use-sidebar' import { getCategory, getThreads } from '@/services/hasura' import { useThread } from '@/hooks/use-thread' import { useGlobalStore } from '@/hooks/use-global-store' -import { Separator } from '../ui/separator' +import { Separator } from '../../ui/separator' export default function ChatChatbotDetails() { const { user, hasuraJwt } = useGlobalStore() diff --git a/apps/masterbots.ai/components/c/chat-chatbot.tsx b/apps/masterbots.ai/components/routes/c/chat-chatbot.tsx similarity index 100% rename from apps/masterbots.ai/components/c/chat-chatbot.tsx rename to apps/masterbots.ai/components/routes/c/chat-chatbot.tsx diff --git a/apps/masterbots.ai/components/c/chat-clickable-text.tsx b/apps/masterbots.ai/components/routes/c/chat-clickable-text.tsx similarity index 100% rename from apps/masterbots.ai/components/c/chat-clickable-text.tsx rename to apps/masterbots.ai/components/routes/c/chat-clickable-text.tsx diff --git a/apps/masterbots.ai/components/c/chat-history.tsx b/apps/masterbots.ai/components/routes/c/chat-history.tsx similarity index 94% rename from apps/masterbots.ai/components/c/chat-history.tsx rename to apps/masterbots.ai/components/routes/c/chat-history.tsx index 5c2a22c7..225dfc34 100644 --- a/apps/masterbots.ai/components/c/chat-history.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-history.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import Link from 'next/link' import { cn } from '@/lib/utils' -import { SidebarList } from '@/components/c/sidebar/sidebar-list' +import { SidebarList } from '@/components/routes/c/sidebar/sidebar-list' import { buttonVariants } from '@/components/ui/button' import { IconPlus } from '@/components/ui/icons' diff --git a/apps/masterbots.ai/components/c/chat-layout-section.tsx b/apps/masterbots.ai/components/routes/c/chat-layout-section.tsx similarity index 92% rename from apps/masterbots.ai/components/c/chat-layout-section.tsx rename to apps/masterbots.ai/components/routes/c/chat-layout-section.tsx index 7abb0225..0b062805 100644 --- a/apps/masterbots.ai/components/c/chat-layout-section.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-layout-section.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useThread } from '@/hooks/use-thread' -import { ThreadPopup } from './thread-popup' +import { ThreadPopup } from './chat-thread-popup' export function ChatLayoutSection({ children }: { children: React.ReactNode }) { const { sectionRef, isOpenPopup } = useThread() diff --git a/apps/masterbots.ai/components/c/chat-list.tsx b/apps/masterbots.ai/components/routes/c/chat-list.tsx similarity index 87% rename from apps/masterbots.ai/components/c/chat-list.tsx rename to apps/masterbots.ai/components/routes/c/chat-list.tsx index bd09ec73..ff94d467 100644 --- a/apps/masterbots.ai/components/c/chat-list.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-list.tsx @@ -1,11 +1,12 @@ import { type Message } from 'ai' import type { Chatbot } from '@repo/mb-genql' import React from 'react' -import { ChatMessage } from '@/components/c/chat-message' -import { cn, createMessagePairs } from '@/lib/utils' +import { ChatMessage } from '@/components/routes/c/chat-message' +import { cn } from '@/lib/utils' import { useThread } from '@/hooks/use-thread' -import { ShortMessage } from '../short-message' -import { ChatAccordion } from '../shared/chat-accordion' +import { ShortMessage } from '../../shared/thread-short-message' +import { ChatAccordion } from './chat-accordion' +import { createMessagePairs } from '@/lib/threads' export interface ChatList { messages: Message[] @@ -36,14 +37,14 @@ export function 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]) + // 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 ( diff --git a/apps/masterbots.ai/components/c/chat-message-actions.tsx b/apps/masterbots.ai/components/routes/c/chat-message-actions.tsx similarity index 100% rename from apps/masterbots.ai/components/c/chat-message-actions.tsx rename to apps/masterbots.ai/components/routes/c/chat-message-actions.tsx diff --git a/apps/masterbots.ai/components/c/chat-message.tsx b/apps/masterbots.ai/components/routes/c/chat-message.tsx similarity index 91% rename from apps/masterbots.ai/components/c/chat-message.tsx rename to apps/masterbots.ai/components/routes/c/chat-message.tsx index 05700d7e..c3e20772 100644 --- a/apps/masterbots.ai/components/c/chat-message.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-message.tsx @@ -5,11 +5,12 @@ 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/c/chat-clickable-text' -import { ChatMessageActions } from '@/components/c/chat-message-actions' -import { MemoizedReactMarkdown } from '@/components/markdown' +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 { cleanPrompt, cn } from '@/lib/utils' +import { cn } from '@/lib/utils' +import { cleanPrompt } from '@/lib/threads' export interface ChatMessageProps { message: Message diff --git a/apps/masterbots.ai/components/c/chat-panel.tsx b/apps/masterbots.ai/components/routes/c/chat-panel.tsx similarity index 89% rename from apps/masterbots.ai/components/c/chat-panel.tsx rename to apps/masterbots.ai/components/routes/c/chat-panel.tsx index cbaeb51d..1861b4b3 100644 --- a/apps/masterbots.ai/components/c/chat-panel.tsx +++ b/apps/masterbots.ai/components/routes/c/chat-panel.tsx @@ -2,11 +2,11 @@ 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/c/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/layout/footer' -import { ChatShareDialog } from '@/components/c/chat-share-dialog' +import { ChatShareDialog } from '@/components/routes/c/chat-share-dialog' import { cn } from '@/lib/utils' import { useThread } from '@/hooks/use-thread' @@ -67,7 +67,9 @@ export function ChatPanel({ {isLoading ? (
{formState.errors.otherText ?

Please specify other interest

: null} - {formState.isSubmitted && formState.errors && !formState.isValid ?

Please select at least one option

: null} + {formState.isSubmitted && formState.errors && !formState.isValid ? ( +

Please select at least one option

+ ) : null}
diff --git a/apps/masterbots.ai/components/shared/account-avatar.tsx b/apps/masterbots.ai/components/shared/account-avatar.tsx new file mode 100644 index 00000000..6378bdda --- /dev/null +++ b/apps/masterbots.ai/components/shared/account-avatar.tsx @@ -0,0 +1,35 @@ +import { cn } from '@/lib/utils' +import Image from 'next/image' +import Link from 'next/link' +import { IconUser } from '../ui/icons' + +export function AccountAvatar({ href, alt, src, size = 32 }: MbAvatarProp) { + return ( + + {src ? ( + {alt} + ) : ( + + )} + + ) +} + +interface MbAvatarProp { + href: string + alt: string + src?: string + size?: number +} diff --git a/apps/masterbots.ai/components/shared/account-details.tsx b/apps/masterbots.ai/components/shared/account-details.tsx new file mode 100644 index 00000000..6e3c6b61 --- /dev/null +++ b/apps/masterbots.ai/components/shared/account-details.tsx @@ -0,0 +1,72 @@ +import Image from 'next/image' +import Link from 'next/link' +import { Separator } from '../ui/separator' + +export default function AccountDetails({ + alt, + avatar, + username, + href, + threadNum = 0, + chatbotName, + description +}: AccountDetailsProps) { + if (!username && !chatbotName) + throw new Error('You must pass username or chatbotName') + + return ( +
+
+
+ {username +
+ +
+
{username || chatbotName}
+ + {/*
+ {chatbot.categories[0].category.name}. +
*/} +
+
+ {description ?
{description}
: ''} +
+
+ +
+
+ Threads: {threadNum ?? 0} +
+
+ {chatbotName ? ( + + Chat with {chatbotName} > + + ) : null} +
+
+
+
+
+ ) +} + +interface AccountDetailsProps { + alt?: string + avatar: string + username?: string + href: string + threadNum?: number + chatbotName?: string + description?: string +} diff --git a/apps/masterbots.ai/components/browse/browse-search-input.tsx b/apps/masterbots.ai/components/shared/browse-input.tsx similarity index 97% rename from apps/masterbots.ai/components/browse/browse-search-input.tsx rename to apps/masterbots.ai/components/shared/browse-input.tsx index 414f3fb1..6bb79d55 100644 --- a/apps/masterbots.ai/components/browse/browse-search-input.tsx +++ b/apps/masterbots.ai/components/shared/browse-input.tsx @@ -6,7 +6,7 @@ import { IconClose } from '@/components/ui/icons' import { Input } from '@/components/ui/input' import { useBrowse } from '@/hooks/use-browse' -export function BrowseSearchInput() { +export function BrowseInput() { const { keyword, changeKeyword } = useBrowse() return (
diff --git a/apps/masterbots.ai/components/browse/browse-category-link.tsx b/apps/masterbots.ai/components/shared/category-tabs/category-link.tsx similarity index 95% rename from apps/masterbots.ai/components/browse/browse-category-link.tsx rename to apps/masterbots.ai/components/shared/category-tabs/category-link.tsx index c4b1b4fa..f5969b4a 100644 --- a/apps/masterbots.ai/components/browse/browse-category-link.tsx +++ b/apps/masterbots.ai/components/shared/category-tabs/category-link.tsx @@ -2,7 +2,7 @@ import { motion } from 'framer-motion' import type { Category } from '@repo/mb-genql' import Link from 'next/link' -export function BrowseCategoryLink({ +export function CategoryLink({ category, activeTab, onClick, @@ -24,7 +24,7 @@ export function BrowseCategoryLink({ href={ category === 'all' ? '/' - : `/${category.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, 'n')}` + : `/${category.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, '_')}` } id={id} onClick={onClick} diff --git a/apps/masterbots.ai/components/browse/browse-category-tabs.tsx b/apps/masterbots.ai/components/shared/category-tabs/category-tabs.tsx similarity index 90% rename from apps/masterbots.ai/components/browse/browse-category-tabs.tsx rename to apps/masterbots.ai/components/shared/category-tabs/category-tabs.tsx index 9f212b6f..0c7c8d6f 100644 --- a/apps/masterbots.ai/components/browse/browse-category-tabs.tsx +++ b/apps/masterbots.ai/components/shared/category-tabs/category-tabs.tsx @@ -3,9 +3,9 @@ import type { Category } from '@repo/mb-genql' import { useEffect } from 'react' import { useBrowse } from '@/hooks/use-browse' -import { BrowseCategoryLink } from './browse-category-link' +import { CategoryLink } from './category-link' -export function BrowseCategoryTabs({ +export function CategoryTabs({ categories, initialCategory = 'all' }: { @@ -36,7 +36,7 @@ export function BrowseCategoryTabs({ setActiveTab( categories.filter( c => - c.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, 'n') === + c.name.toLowerCase().replace(/\s+/g, '_').replace(/\&/g, '_') === initialCategory )[0]?.categoryId ) @@ -46,7 +46,7 @@ export function BrowseCategoryTabs({ return (
- {categories.map((category, key) => ( - { + // Stop propagation to prevent form submission when clicking on the icon + e.preventDefault() + e.stopPropagation() + const formData = new FormData() + formData.set('url', url) + const { shortLink } = await shorten({}, formData) + navigator.clipboard + .writeText(shortLink) + .then(() => console.log('Shortlink copied to clipboard')) + .catch(error => + console.error('Error copying shortlink to clipboard: ', error) + ) + } + + return +} diff --git a/apps/masterbots.ai/components/markdown.tsx b/apps/masterbots.ai/components/shared/markdown.tsx similarity index 100% rename from apps/masterbots.ai/components/markdown.tsx rename to apps/masterbots.ai/components/shared/markdown.tsx diff --git a/apps/masterbots.ai/components/shared/thread-accordion.tsx b/apps/masterbots.ai/components/shared/thread-accordion.tsx new file mode 100644 index 00000000..74d6f0ba --- /dev/null +++ b/apps/masterbots.ai/components/shared/thread-accordion.tsx @@ -0,0 +1,95 @@ +'use client' + +import { useEffect } from 'react' + +import { getMessagePairs } from '@/services/hasura' + +import { useQuery } from '@tanstack/react-query' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger +} from '@/components/ui/accordion' + +import { Thread } from '@repo/mb-genql' +import { ThreadHeading } from './thread-heading' +import { MessagePair, convertMessage } from '@/lib/threads' +import { BrowseChatMessage } from './thread-message' + +export function ThreadAccordion({ + thread, + initialMessagePairs, + clientFetch = false +}: ThreadAccordionProps) { + // initalMessages is coming from server ssr on load. the rest of messages on demand on mount + const { data: pairs, error } = useQuery({ + queryKey: [`messages-${thread.threadId}`], + queryFn: () => getMessagePairs(thread.threadId), + initialData: initialMessagePairs, + refetchOnMount: true, + enabled: clientFetch + }) + + // update url when dialog opens and closes + useEffect(() => { + const initialUrl = location.href + const threadUrl = `/${thread.chatbot.categories[0].category.name.toLowerCase().replaceAll(' ', '_').replaceAll('&', '_')}/${thread.threadId}` + console.log(`Updating URL to ${threadUrl}, initialUrl was ${initialUrl}`) + + window.history.pushState({}, '', threadUrl) + return () => { + window.history.pushState({}, '', initialUrl) + } + }) + + if (error) return
There was an error loading thread messages
+ + // if no initial message and still loading show loading message + // NOTE: its fast and transitions in. testing without this + if (!pairs?.length) return null + + console.log(pairs.map((_p, key) => `pair-${key}`)) + return ( + + {pairs.map((p, key) => { + return ( + + + {key ? ( +
{p.userMessage.content}
+ ) : ( + + )} +
+ +
+ {p.chatGptMessage.map((message, index) => ( + + ))} +
+
+
+ ) + })} +
+ ) +} + +interface ThreadAccordionProps { + thread: Thread + initialMessagePairs?: MessagePair[] + clientFetch?: boolean +} diff --git a/apps/masterbots.ai/components/shared/thread-dialog.tsx b/apps/masterbots.ai/components/shared/thread-dialog.tsx new file mode 100644 index 00000000..cf0a5f6a --- /dev/null +++ b/apps/masterbots.ai/components/shared/thread-dialog.tsx @@ -0,0 +1,38 @@ +'use client' + +import type { Thread } from '@repo/mb-genql' +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog' +import { ThreadAccordion } from './thread-accordion' +import { ThreadHeading } from './thread-heading' +import { cn } from '@/lib/utils' + +export function ThreadDialog({ thread }: ThreadDialogProps) { + const firstQuestion = + thread.messages.find(m => m.role === 'user')?.content || 'not found' + const firstResponse = + thread.messages.find(m => m.role === 'assistant')?.content || 'not found' + + return ( + + + + + + + + + + ) +} + +interface ThreadDialogProps { + thread: Thread +} diff --git a/apps/masterbots.ai/components/shared/thread-heading.tsx b/apps/masterbots.ai/components/shared/thread-heading.tsx new file mode 100644 index 00000000..604a3bc0 --- /dev/null +++ b/apps/masterbots.ai/components/shared/thread-heading.tsx @@ -0,0 +1,64 @@ +import { Thread } from '@repo/mb-genql' +import { ShortMessage } from './thread-short-message' +import { AccountAvatar } from './account-avatar' +import { cn } from '@/lib/utils' +import Shortlink from './copy-shortlink' + +export function ThreadHeading({ + thread, + response, + question, + copy = false +}: ThreadHeadingProps) { + return ( +
+
+
+ + +
+ {question} + + by + + +
+
+ {copy ? : null} +
+ + {response ? ( +
+ +
+ ) : null} +
+ ) +} + +interface ThreadHeadingProps { + thread: Thread + response?: string + question: string + copy?: boolean +} diff --git a/apps/masterbots.ai/components/shared/thread-list.tsx b/apps/masterbots.ai/components/shared/thread-list.tsx new file mode 100644 index 00000000..d5121b58 --- /dev/null +++ b/apps/masterbots.ai/components/shared/thread-list.tsx @@ -0,0 +1,91 @@ +'use client' + +import React, { useEffect, useRef, useState } from 'react' +import { debounce } from 'lodash' +import type { Thread } from '@repo/mb-genql' +import { useBrowse } from '@/hooks/use-browse' +import { getBrowseThreads } from '@/services/hasura' +import { ThreadDialog } from './thread-dialog' + +export default function ThreadList({ + initialThreads, + filter +}: ThreadListProps) { + const { keyword } = useBrowse() + const [threads, setThreads] = useState(initialThreads) + const [filteredThreads, setFilteredThreads] = + useState(initialThreads) + const [loading, setLoading] = useState(false) + const loadMoreRef = useRef(null) + const [hasMore, setHasMore] = useState(true) + + // load more threads for the category + const loadMore = async () => { + console.log('🟡 Loading More Content') + setLoading(true) + + const moreThreads = await getBrowseThreads({ + ...filter, + offset: filteredThreads.length, + limit: 50 + }) + + if (moreThreads.length === 0) setHasMore(false) + setThreads(prevState => [...prevState, ...moreThreads]) + setLoading(false) + } + + const verifyKeyword = () => { + if (!keyword) { + setFilteredThreads(threads) + } else { + debounce(() => { + setFilteredThreads( + threads.filter((thread: Thread) => + thread.messages[0]?.content + .toLowerCase() + .includes(keyword.toLowerCase()) + ) + ) + }, 230)() + } + } + + useEffect(() => { + verifyKeyword() + }, [keyword, threads, verifyKeyword]) + + // load mare item when it gets to the end + useEffect(() => { + if (!loadMoreRef.current) return + const observer = new IntersectionObserver(([entry]) => { + if (hasMore && entry.isIntersecting && !loading) { + setTimeout(() => loadMore(), 150) + observer.unobserve(entry.target) + } + }) + + observer.observe(loadMoreRef.current) + + return () => observer.disconnect() + }, [hasMore, loading, loadMore]) + + return ( +
+ {filteredThreads.map((thread: Thread, key) => ( + + ))} +
+
+ ) +} + +type ThreadListProps = { + initialThreads: Thread[] + filter: { + categoryId?: number + userId?: string + chatbotName?: string + slug?: string + } +} diff --git a/apps/masterbots.ai/components/browse/browse-chat-message.tsx b/apps/masterbots.ai/components/shared/thread-message.tsx similarity index 82% rename from apps/masterbots.ai/components/browse/browse-chat-message.tsx rename to apps/masterbots.ai/components/shared/thread-message.tsx index ce6f45e0..722b7e3b 100644 --- a/apps/masterbots.ai/components/browse/browse-chat-message.tsx +++ b/apps/masterbots.ai/components/shared/thread-message.tsx @@ -1,16 +1,11 @@ -// 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 type { Message } from 'ai' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' -import Image from 'next/image' import type { Chatbot } from '@repo/mb-genql' -import { cleanPrompt, cn } from '@/lib/utils' +import { cn } from '@/lib/utils' import { CodeBlock } from '@/components/ui/codeblock' -import { MemoizedReactMarkdown } from '@/components/markdown' -import { IconOpenAI, IconUser } from '@/components/ui/icons' -import { ChatMessageActions } from '../c/chat-message-actions' +import { MemoizedReactMarkdown } from '@/components/shared/markdown' +import { cleanPrompt } from '@/lib/threads' export interface ChatMessageProps { message: Message diff --git a/apps/masterbots.ai/components/short-message.tsx b/apps/masterbots.ai/components/shared/thread-short-message.tsx similarity index 97% rename from apps/masterbots.ai/components/short-message.tsx rename to apps/masterbots.ai/components/shared/thread-short-message.tsx index 0288cee3..6ff0a501 100644 --- a/apps/masterbots.ai/components/short-message.tsx +++ b/apps/masterbots.ai/components/shared/thread-short-message.tsx @@ -1,7 +1,7 @@ import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import { MemoizedReactMarkdown } from './markdown' -import { CodeBlock } from './ui/codeblock' +import { CodeBlock } from '../ui/codeblock' export function ShortMessage({ content }: { content: string }) { return ( diff --git a/apps/masterbots.ai/hooks/use-thread.tsx b/apps/masterbots.ai/hooks/use-thread.tsx index cc8bb50a..5befd2d4 100644 --- a/apps/masterbots.ai/hooks/use-thread.tsx +++ b/apps/masterbots.ai/hooks/use-thread.tsx @@ -12,7 +12,7 @@ import { Message as AIMessage } from 'ai' import { uniqBy } from 'lodash' import toast from 'react-hot-toast' import { Chatbot, Message, Thread } from '@repo/mb-genql' -import { getAllUserMessagesAsStringArray } from '@/components/c/chat' +import { getAllUserMessagesAsStringArray } from '@/components/routes/c/chat' import { useSidebar } from './use-sidebar' import { useScroll } from 'framer-motion' import { useAtBottom } from './use-at-bottom' diff --git a/apps/masterbots.ai/lib/animation.ts b/apps/masterbots.ai/lib/animation.ts new file mode 100644 index 00000000..3cfcd9df --- /dev/null +++ b/apps/masterbots.ai/lib/animation.ts @@ -0,0 +1,34 @@ +// Easing function for smooth animation +export const easeInOutQuad = (t: number, b: number, c: number, d: number) => { + t /= d / 2 + if (t < 1) return (c / 2) * t * t + b + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} + +let animationFrameId: number +export const scrollToBottomOfElement = (element?: HTMLElement) => { + if (!element) return + const targetScroll = element.scrollHeight - element.clientHeight + const duration = 500 + const startTime = performance.now() + + const animateScroll = (currentTime: number) => { + const elapsed = currentTime - startTime + const position = easeInOutQuad( + elapsed, + element.scrollTop, + targetScroll - element.scrollTop, + duration + ) + element.scrollTop = position + + if (elapsed < duration) { + animationFrameId = requestAnimationFrame(animateScroll) + } else { + cancelAnimationFrame(animationFrameId) + } + } + + animationFrameId = requestAnimationFrame(animateScroll) +} diff --git a/apps/masterbots.ai/lib/threads.ts b/apps/masterbots.ai/lib/threads.ts new file mode 100644 index 00000000..fab3891e --- /dev/null +++ b/apps/masterbots.ai/lib/threads.ts @@ -0,0 +1,71 @@ +import type * as AI from 'ai' +import { Message } from '@repo/mb-genql' +import { type Message as AIMessage } from 'ai/react' + +export function createMessagePairs(messages: Message[] | AIMessage[]) { + const messagePairs: MessagePair[] = [] + + for (let i = 0; i < messages.length; i++) { + const message = messages[i] + + if (message.role === 'user') { + const userMessage = message + const chatGptMessages = [] + for (let j = i + 1; j < messages.length; j++) { + const chatGptMessage = findNextAssistantMessage(messages, j) + if (!chatGptMessage) { + break + } else { + chatGptMessages.push(chatGptMessage) + continue + } + } + messagePairs.push({ + userMessage, + chatGptMessage: chatGptMessages + }) + } + } + + return messagePairs +} + +const findNextAssistantMessage = ( + messages: Message[] | AIMessage[], + startIndex: number +) => { + if (messages[startIndex].role === 'assistant') { + return { + ...messages[startIndex], + content: cleanPrompt(messages[startIndex].content) + } + } + return null +} + +// From chat-message.tsx +export function cleanPrompt(str: string) { + const marker = ']. Then answer this question:' + const index = str.indexOf(marker) + let extracted = '' + + if (index !== -1) { + extracted = str.substring(index + marker.length) + } + // console.log('cleanPrompt', str, extracted, index) + return extracted || str +} + +export interface MessagePair { + userMessage: Message | AI.Message + chatGptMessage: Message[] +} + +export function convertMessage(message: Message) { + return { + id: message.messageId, + content: message.content, + createAt: message.createdAt, + role: message.role + } as AI.Message +} diff --git a/apps/masterbots.ai/lib/utils.ts b/apps/masterbots.ai/lib/utils.ts index 0783e965..ccacf839 100644 --- a/apps/masterbots.ai/lib/utils.ts +++ b/apps/masterbots.ai/lib/utils.ts @@ -1,5 +1,3 @@ -import { type Message as AIMessage } from 'ai/react' -import type { Message } from '@repo/mb-genql' import { clsx, ClassValue } from 'clsx' import { customAlphabet } from 'nanoid' import { twMerge } from 'tailwind-merge' @@ -70,61 +68,6 @@ export function extractBetweenMarkers( return str.substring(startIndex, endIndex).trim() } -// From browse-list.tsx -export function createMessagePairs(messages: Message[] | AIMessage[]) { - const messagePairs = [] - - for (let i = 0; i < messages.length; i++) { - const message = messages[i] - - if (message.role === 'user') { - const userMessage = message - const chatGptMessages = [] - for (let j = i + 1; j < messages.length; j++) { - const chatGptMessage = findNextAssistantMessage(messages, j) - if (!chatGptMessage) { - break - } else { - chatGptMessages.push(chatGptMessage) - continue - } - } - messagePairs.push({ - userMessage, - chatGptMessage: chatGptMessages - }) - } - } - - return messagePairs -} - -const findNextAssistantMessage = ( - messages: Message[] | AIMessage[], - startIndex: number -) => { - if (messages[startIndex].role === 'assistant') { - return { - ...messages[startIndex], - content: cleanPrompt(messages[startIndex].content) - } - } - return null -} - -// From chat-message.tsx -export function cleanPrompt(str: string) { - const marker = ']. Then answer this question:' - const index = str.indexOf(marker) - let extracted = '' - - if (index !== -1) { - extracted = str.substring(index + marker.length) - } - // console.log('cleanPrompt', str, extracted, index) - return extracted || str -} - export const readingTime = (messages: { content: string }[]) => { let contentGroup: any = [] @@ -141,41 +84,6 @@ export const readingTime = (messages: { content: string }[]) => { return time } -// Easing function for smooth animation -export const easeInOutQuad = (t: number, b: number, c: number, d: number) => { - t /= d / 2 - if (t < 1) return (c / 2) * t * t + b - t-- - return (-c / 2) * (t * (t - 2) - 1) + b -} - -let animationFrameId: number -export const scrollToBottomOfElement = (element?: HTMLElement) => { - if (!element) return - const targetScroll = element.scrollHeight - element.clientHeight - const duration = 500 - const startTime = performance.now() - - const animateScroll = (currentTime: number) => { - const elapsed = currentTime - startTime - const position = easeInOutQuad( - elapsed, - element.scrollTop, - targetScroll - element.scrollTop, - duration - ) - element.scrollTop = position - - if (elapsed < duration) { - animationFrameId = requestAnimationFrame(animateScroll) - } else { - cancelAnimationFrame(animationFrameId) - } - } - - animationFrameId = requestAnimationFrame(animateScroll) -} - export async function sleep(time: number) { return new Promise(resolve => setTimeout(resolve, time)) } diff --git a/apps/masterbots.ai/package.json b/apps/masterbots.ai/package.json index 2b68566e..ffd1e0f9 100644 --- a/apps/masterbots.ai/package.json +++ b/apps/masterbots.ai/package.json @@ -36,6 +36,7 @@ "@repo/mb-lib": "workspace:*", "@repo/mb-types": "workspace:*", "@supabase/ssr": "^0.1.0", + "@tanstack/react-query": "^5.29.0", "@vercel/analytics": "^1.1.1", "@vercel/og": "^0.5.20", "ai": "^2.2.25", diff --git a/apps/masterbots.ai/services/hasura/hasura.service.ts b/apps/masterbots.ai/services/hasura/hasura.service.ts index e871f0a2..0c4184a1 100644 --- a/apps/masterbots.ai/services/hasura/hasura.service.ts +++ b/apps/masterbots.ai/services/hasura/hasura.service.ts @@ -20,6 +20,7 @@ import { SaveNewMessageParams, UpsertUserParams } from './hasura.service.type' +import { createMessagePairs } from '@/lib/threads' function getHasuraClient({ jwt, adminSecret }: GetHasuraClientParams) { return createMbClient({ @@ -506,7 +507,6 @@ export async function getUserInfoFromBrowse(slug: string) { user: { username: true, profilePicture: true, - name: true, __args: { where: { slug: { @@ -541,3 +541,8 @@ export async function getUser({ }) return user[0] } + +export async function getMessagePairs(threadId) { + const messages = await getMessages({ threadId }) + return createMessagePairs(messages) +} diff --git a/apps/masterbots.ai/services/supabase/supa-server.service.ts b/apps/masterbots.ai/services/supabase/supa-server.service.ts index 4c61d863..b154df97 100644 --- a/apps/masterbots.ai/services/supabase/supa-server.service.ts +++ b/apps/masterbots.ai/services/supabase/supa-server.service.ts @@ -4,26 +4,27 @@ import { createSupabaseServerClient } from './supa-server-client' export async function getUserProfile(): Promise { try { - const supabase = await createSupabaseServerClient() - const { - data: { user } - } = await supabase.auth.getUser() - if (!user || !user.email) throw new Error('user not found') + return null + // const supabase = await createSupabaseServerClient() + // const { + // data: { user } + // } = await supabase.auth.getUser() + // if (!user || !user.email) throw new Error('user not found') - // TODO: use supabase - const userProfile = await getUser({ - email: user.email, - adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET || '' - }) + // // TODO: use supabase + // const userProfile = await getUser({ + // email: user.email, + // adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET || '' + // }) - if (!userProfile) throw new Error('user not found') - return { - userId: userProfile.userId, - username: userProfile.username, - name: '', - email: userProfile.email, - image: userProfile.profilePicture || '' - } + // if (!userProfile) throw new Error('user not found') + // return { + // userId: userProfile.userId, + // username: userProfile.username, + // name: '', + // email: userProfile.email, + // image: userProfile.profilePicture || '' + // } } catch (error) { console.log('GET USER PROFILE ERROR', error) return null diff --git a/bun.lockb b/bun.lockb index d05fce61..ed82c55c 100755 Binary files a/bun.lockb and b/bun.lockb differ