Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: continuous thread #386

Merged
merged 5 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 24 additions & 33 deletions apps/masterbots.ai/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
import { GeistMono } from 'geist/font/mono'
import { GeistSans } from 'geist/font/sans'
import '@/app/globals.css'
import { Header } from '@/components/layout/header/header'
import { Providers } from '@/components/layout/providers'
import { Toaster } from '@/components/ui/sonner'
import { cn } from '@/lib/utils'
import { GoogleAnalytics } from '@next/third-parties/google'
import { Metadata } from 'next'
import { GeistMono } from 'geist/font/mono'
import { GeistSans } from 'geist/font/sans'
import type { Metadata } from 'next'
import NextTopLoader from 'nextjs-toploader'
import { Toaster } from '@/components/ui/sonner'

export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(
'font-sans antialiased',
GeistSans.variable,
GeistMono.variable
)}
>
<NextTopLoader color="#1ED761" initialPosition={0.20} />
<Toaster toastOptions={{
className: 'bg-background text-background-foreground',
}} />
<Providers
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<body className={cn('font-sans antialiased', GeistSans.variable, GeistMono.variable)}>
<NextTopLoader color="#1ED761" initialPosition={0.2} />
<Toaster
toastOptions={{
className: 'bg-background text-background-foreground',
}}
/>
<Providers attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<div className="flex flex-col min-h-screen">
<Header />
<main className="relative flex flex-col flex-1 bg-muted/50">{children}</main>
Expand All @@ -45,7 +36,7 @@ export const metadata: Metadata = {
metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
title: {
default: 'Masterbots',
template: `%s - Masterbots`
template: `%s - Masterbots`,
},
description:
'Elevating AI Beyond ChatGPT: Specialized Chatbots, Social Sharing and User-Friendly Innovation',
Expand All @@ -59,9 +50,9 @@ export const metadata: Metadata = {
url: `${process.env.BASE_URL || ''}/api/og`,
width: 1232,
height: 928,
alt: 'Masterbots'
}
]
alt: 'Masterbots',
},
],
},
twitter: {
title: 'Masterbots',
Expand All @@ -74,25 +65,25 @@ export const metadata: Metadata = {
url: `${process.env.BASE_URL || ''}/api/og`,
width: 1232,
height: 928,
alt: 'Masterbots'
}
]
alt: 'Masterbots',
},
],
},
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-300x300.png',
apple: '/apple-touch-icon.png'
apple: '/apple-touch-icon.png',
},
other: {
'google-site-verification': 'By9aM0DbPDDO9qa7Y3zNwDFyYuSPslVzje76EVOCcY0'
}
'google-site-verification': 'By9aM0DbPDDO9qa7Y3zNwDFyYuSPslVzje76EVOCcY0',
},
}

export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' }
]
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}

interface RootLayoutProps {
Expand Down
55 changes: 29 additions & 26 deletions apps/masterbots.ai/components/routes/chat/chat-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function ChatList({
<div
ref={effectiveContainerRef}
className={cn(
'relative max-w-3xl px-4 mx-auto overflow-auto scrollbar-thin',
'relative max-w-3xl px-4 mx-auto',
className,
{ 'flex flex-col gap-3': isThread }
)}
Expand Down Expand Up @@ -168,20 +168,25 @@ function MessagePairs({
{previousPairs.length > 0 && pairs.length > 0 && (
<Separator className="relative mt-6 -bottom-1.5 h-1.5 z-[2] rounded-sm bg-iron dark:bg-mirage" />
)}
{pairs.map((pair: MessagePair, key: number, pairsArray) => (
<MessagePairAccordion
key={`${pair.userMessage.createdAt}-${pair.chatGptMessage[0]?.id ?? 'pending'}`}
pair={pair}
isThread={isThread}
index={key}
arrayLength={pairsArray.length}
isNewResponse={isNewResponse}
type="current"
chatTitleClass={chatTitleClass}
chatContentClass={chatContentClass}
sendMessageFn={sendMessageFn}
/>
))}
{pairs.map((pair: MessagePair, key: number, pairsArray) => pair.chatGptMessage[0] && pair.userMessage ? (
<>
<MessagePairAccordion
key={`${pair.userMessage.createdAt}-${pair.chatGptMessage[0]?.id ?? 'pending'}`}
pair={pair}
isThread={isThread}
index={key}
arrayLength={pairsArray.length}
isNewResponse={isNewResponse}
type="current"
chatTitleClass={chatTitleClass}
chatContentClass={chatContentClass}
sendMessageFn={sendMessageFn}
/>
{pairsArray.length > 1 && key === pairsArray.length - 1 ? (
<ChatLoadingState key="chat-loading-state" />
) : null}
</>
) : null)}
</>
)
}
Expand Down Expand Up @@ -210,11 +215,13 @@ export function MessagePairAccordion({

return (
<SharedAccordion
key={`${pair.userMessage.createdAt}-${pair.chatGptMessage[0]?.id ?? 'pending'}`}
defaultState={
index === 0 ||
index === arrayLength - 1 ||
(index === arrayLength - 2 && isNewResponse)
// ? Case for when there is more than one message and we want to hide the first message
// (!index && arrayLength <= 1)
// ? Case for when we have the first message in the conversation or last and both are not previous
((!index || index === arrayLength - 1) && !isPrevious) ||
// ? Case for when we have the first message in the previous conversation
(!index && isPrevious)
}
className={cn(
{ relative: isThread },
Expand All @@ -230,7 +237,7 @@ export function MessagePairAccordion({
hidden: !isThread && index === 0, // Style differences for previous vs current messages
'dark:bg-[#1d283a9a] bg-iron !border-l-[transparent] [&[data-state=open]]:!bg-gray-400/50 dark:[&[data-state=open]]:!bg-mirage':
!isPrevious,
'bg-accent/15 dark:bg-accent/15 hover:bg-accent/30 hover:dark:bg-accent/30 border-l-accent/20 dark:border-l-accent/20 [&[data-state=open]]:!bg-accent/25 dark:[&[data-state=open]]:!bg-accent/25':
'bg-accent/10 dark:bg-accent/10 hover:bg-accent/30 hover:dark:bg-accent/30 border-l-accent/10 dark:border-l-accent/10 [&[data-state=open]]:!bg-accent/30 dark:[&[data-state=open]]:!bg-accent/30':
isPrevious
},
props.chatTitleClass
Expand All @@ -250,9 +257,7 @@ export function MessagePairAccordion({
''
) : (
<div
className={cn('flex items-start gap-2', {
'[&_div]:text-sm': isPrevious
})}
className={cn('flex items-start gap-2')}
>
<ChatMessage actionRequired={false} message={pair.userMessage} />
</div>
Expand All @@ -278,7 +283,6 @@ export function MessagePairAccordion({
'mx-4 md:mx-[46px] px-1 py-4 h-full',
{
'!border-[transparent]': !isThread && index === 0,
'[&>div>div>div_*]:!text-xs': isPrevious
},
props.chatContentClass
)}
Expand All @@ -288,7 +292,7 @@ export function MessagePairAccordion({
<span className="absolute top-1 -left-5 px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-accent text-accent-foreground">
Previous Thread
</span>
<div className="pb-3 mt-4 overflow-hidden text-sm opacity-50">
<div className="pb-3 mt-4 overflow-hidden opacity-50">
Continued from{' '}
<b>&ldquo;{pair.userMessage.content.trim()}&rdquo;</b> thread
{activeThread?.thread?.user?.username
Expand All @@ -299,7 +303,6 @@ export function MessagePairAccordion({
) : (
''
)}
<ChatLoadingState />
{pair.chatGptMessage.length > 0
? pair.chatGptMessage.map(message => (
<ChatMessage
Expand Down
51 changes: 24 additions & 27 deletions apps/masterbots.ai/components/routes/chat/chat-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function ChatOptions({ threadId, thread, isBrowse }: ChatOptionsProps) {
const title = thread?.messages[0]?.content ?? 'Untitled'
const text =
thread?.messages[1]?.content.substring(0, 100) ?? 'No description found...'
const url = `/${toSlug(thread.chatbot.categories[0].category.name)}/${thread.threadId}`
const url = `/b/${toSlug(thread.chatbot.categories[0].category.name)}/${thread.threadId}`
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const { customSonner } = useSonner()
Expand Down Expand Up @@ -100,36 +100,36 @@ export function ChatOptions({ threadId, thread, isBrowse }: ChatOptionsProps) {
return (
<div className="flex items-center gap-4 sm:gap-3 pt-[3px]">
<AlertDialogue deleteDialogOpen={isDeleteOpen} />
{!isBrowse && (
<div className="flex items-center gap-1 sm:gap-3">
<span className="px-2.5 py-0.5 flex items-center justify-center bg-gray-200 rounded-full dark:bg-gray-700 text-[10px] leading-none sm:text-xs whitespace-nowrap">
{thread?.isPublic ? 'Public' : 'Private'}
</span>
</div>
)}

<DropdownMenu>
<DropdownMenuTrigger asChild className={cn(buttonVariants({
variant: 'ghost',
size: 'sm'
}), 'size-6 p-0 sm:size-8')}>
<MoreVertical className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
sideOffset={8}
align="end"
className="w-[160px] sm:w-[180px] px-0"
<DropdownMenuTrigger
className={cn(
buttonVariants({
variant: 'ghost',
size: 'icon',
radius: 'full',
}),
'p-1',
)}
>
<MoreVertical className="w-4 h-4" />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={8} align="end" className="w-[160px] sm:w-[180px] px-0">
{/* Toggle thread visibility option (only for thread owner) */}
{isUser && (
<DropdownMenuItem
className="flex-col items-start"
onSelect={event => event.preventDefault()}
onSelect={(event) => event.preventDefault()}
>
<Button
onClick={e => {
onClick={async (e) => {
e.stopPropagation()
toggleVisibility(!thread?.isPublic, threadId)
try {
await toggleVisibility(!thread?.isPublic, threadId)
thread.isPublic = !thread?.isPublic
} catch (error) {
console.error('Failed to update thread visibility:', error)
}
}}
variant={'ghost'}
size={'sm'}
Expand All @@ -152,23 +152,20 @@ export function ChatOptions({ threadId, thread, isBrowse }: ChatOptionsProps) {
{/* Share thread option */}
<DropdownMenuItem
className="flex-col items-start"
onSelect={event => event.preventDefault()}
onSelect={(event) => event.preventDefault()}
>
<ShareButton url={url} />
</DropdownMenuItem>
{/* Delete thread option (only for thread owner) */}
{isUser && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-xs"
onSelect={event => event.preventDefault()}
>
<DropdownMenuItem className="text-xs" onSelect={(event) => event.preventDefault()}>
<Button
variant={'ghost'}
size={'sm'}
className="flex justify-between w-full text-red-400"
onClick={e => {
onClick={(e) => {
e.stopPropagation()
setIsDeleteOpen(true)
}}
Expand Down
23 changes: 20 additions & 3 deletions apps/masterbots.ai/components/routes/thread/thread-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { ChatOptions } from '@/components/routes/chat/chat-options'
import { ChatbotAvatar } from '@/components/shared/chatbot-avatar'
import { SharedAccordion } from '@/components/shared/shared-accordion'
import { ShortMessage } from '@/components/shared/short-message'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { useMBScroll } from '@/lib/hooks/use-mb-scroll'
import { useThread } from '@/lib/hooks/use-thread'
import { useThreadVisibility } from '@/lib/hooks/use-thread-visibility'
import { cn } from '@/lib/utils'
import { cn, getRouteType } from '@/lib/utils'
import type { Thread } from 'mb-genql'
import { usePathname } from 'next/navigation'
import { useRef } from 'react'

export default function ThreadComponent({
Expand All @@ -30,7 +32,9 @@ export default function ThreadComponent({
const threadRef = useRef<HTMLLIElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const { isNewResponse } = useThread()
const { isAdminMode } = useThreadVisibility()
const { isPublic, isAdminMode } = useThreadVisibility()
const pathname = usePathname()
const routeType = getRouteType(pathname)

const { scrollToTop } = useMBScroll({
containerRef: contentRef,
Expand Down Expand Up @@ -79,7 +83,20 @@ export default function ThreadComponent({
</span>
</span>
{/* Thread Options */}
<div className="pl-2 pr-4 sm:pl-4 sm:pr-8">
<div className="flex gap-3 items-center justify-center pl-2 pr-4 sm:pl-4 sm:pr-8">
{routeType === 'chat' && (
<Badge
variant="outline"
className={cn({
// Light mode accent color...
'bg-[#BE17E8] text-white': thread.isApproved && thread.isPublic,
// Woodsmoke
'bg-[#09090B] text-white': thread.isApproved && !thread.isPublic,
})}
>
{thread.isPublic ? 'Public' : 'Private'}
</Badge>
)}
<ChatOptions threadId={thread.threadId} thread={thread} isBrowse />
</div>
</div>
Expand Down
Loading