Skip to content

Commit

Permalink
fix: navigation, slugs, accordion
Browse files Browse the repository at this point in the history
  • Loading branch information
gaboesquivel committed Apr 13, 2024
1 parent e8dda27 commit 81d33e0
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 63 deletions.
3 changes: 2 additions & 1 deletion apps/masterbots.ai/app/(browse)/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export default async function CategoryPage({
searchParams
}: CategoryPageProps) {
const categories = await getCategories()
console.log(params.category)
const categoryId = categories.find(
c => toSlug(c.name) === params.category
).categoryId
)?.categoryId
if (!categoryId) throw new Error('Category not foud')

const query = searchParams.query ? decodeQuery(searchParams.query) : null
Expand Down
4 changes: 2 additions & 2 deletions apps/masterbots.ai/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@
}

.scrollbar::-webkit-scrollbar {
width: 1px;
height: 1px;
width: 4px;
height: 4px;
}
.scrollbar::-webkit-scrollbar-track,
.scrollbar::-webkit-scrollbar-corner {
Expand Down
126 changes: 87 additions & 39 deletions apps/masterbots.ai/components/shared/thread-accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Thread } from '@repo/mb-genql'
import { getMessagePairs } from '@/services/hasura'
Expand All @@ -15,80 +15,128 @@ import { cn } from '@/lib/utils'
import { toSlug } from '@/lib/url'
import { ThreadHeading } from './thread-heading'
import { BrowseChatMessage } from './thread-message'
import { usePathname } from 'next/navigation'
import { clone } from 'lodash'

export function ThreadAccordion({
thread,
initialMessagePairs,
clientFetch = false,
chat = false,
// disable automatic client fetch by default
// ThreadList sets this to true to load the rest of messages inside ThreadDialog or ThreadListAccordion
// ThreadList only receives the first question and answer
showHeading = true
}: ThreadAccordionProps) {
// initalMessages is coming from server ssr on load. the rest of messages on demand on mount
const pathname = usePathname()

const { data: pairs, error } = useQuery({
queryKey: [`messages-${thread.threadId}`],
queryFn: () => getMessagePairs(thread.threadId),
initialData: initialMessagePairs,
networkMode: 'always',
refetchOnMount: true,
enabled: clientFetch
})

// update url when dialog opens and closes
useEffect(() => {
const initialUrl = location.href
const dir =
`c/${
toSlug(
chat ? thread.chatbot.name : thread.chatbot.categories[0].category.name
)}`
console.log({
initialMessagePairs,
pairs
})

const threadUrl = `/${dir}/${thread.threadId}`
console.log(`Updating URL to ${threadUrl}, initialUrl was ${initialUrl}`)
// update url when thread accordion opens and closes
// use cases: when using ThreadDialog and DoubleThreadAccordion
// we want this logic here on central place
useEffect(() => {
// clone pathname instead of ref to keep initialValue
const initialPathname = clone(pathname)
// base path changes based on chat prop
// if chat true redirects to /c/{thredId} for chatting experience
// else defaults to public url /{category}/{threadId}
console.log(toSlug(thread.chatbot.categories[0]?.category?.name))
const dir = chat
? `/c/${toSlug(thread.chatbot.name)}`
: `/${toSlug(thread.chatbot.categories[0]?.category?.name)}`
const threadUrl = `${dir}/${thread.threadId}`
console.log({ threadUrl, initialPathname })
// not necessary to update if already the same a
// eg. in thread landing pages /{category}/{threadId}
if (threadUrl === initialPathname) return
console.log(
`Updating URL to ${threadUrl}, initialUrl was ${initialPathname}`
)

window.history.pushState({}, '', threadUrl)
// window.history.pushState({}, '', threadUrl)
return () => {
window.history.pushState({}, '', initialUrl)
// window.history.pushState({}, '', initialPathname)
}
})

if (error) return <div>There was an error loading thread messages</div>

// if no initial message and still loading show loading message
// NOTE: its fast and transitions in. testing without this
if (!pairs.length) return null

return (
<Accordion
className="w-full"
className={cn('w-full border border-solid border-mirage scroll')}
defaultValue={['pair-0', 'pair-1', 'pair-2']}
type="multiple"
key={`accordion-${JSON.stringify(pairs)}`}
>
{pairs.map((p, key) => {
const isFirst = key === 0
console.log(key, p)
return (
<AccordionItem key={key} value={`pair-${key}`}>
{showHeading ? (
<AccordionTrigger className={cn('bg-mirage')}>
{key ? (
<div className="pl-12">{p.userMessage.content}</div>
) : (
<AccordionItem
key={`accordion-item-${thread.threadId}-pair-${key}`}
value={`pair-${key}`}
>
{
// is not the frist question we return follow question style
!isFirst ? (
<AccordionTrigger
className={cn('px-5 border-y border-solid border-mirage')}
>
<div className="pl-12 md:text-lg">
{p.userMessage.content}
</div>
</AccordionTrigger>
) : null
}

{
// when using ThreadAccordion inside ThreadDialog or ThreadListAccordion we want
// to control heading and hide and ThreadAccordion and ThreadDialog show the ThreadHeading already
// when using ThreadAccordion in thread landing page /{category}/{threadId} showHeading must be true
// ThreadHeading is the the big one with the user avatar, ThreadDialog or ThreadListAccordion is hidden
showHeading && isFirst ? (
<AccordionTrigger
className={cn(
'px-5',
isFirst
? 'bg-mirage'
: 'border-y border-solid border-mirage'
)}
>
<ThreadHeading
chat={chat}
copy
question={p.userMessage.content}
thread={thread}
/>
)}
</AccordionTrigger>
) : null}
<AccordionContent aria-expanded>
<div className="px-7">
{p.chatGptMessage.map((message, index) => (
<BrowseChatMessage
chatbot={thread.chatbot}
key={index}
message={convertMessage(message)}
/>
))}
</div>
</AccordionTrigger>
) : null
}

<AccordionContent
aria-expanded
className={cn('mx-8 border-x border-solid border-mirage')}
>
{p.chatGptMessage.map(message => (
<BrowseChatMessage
chatbot={thread.chatbot}
key={`message-${message.messageId}`}
message={convertMessage(message)}
/>
))}
</AccordionContent>
</AccordionItem>
)
Expand All @@ -99,7 +147,7 @@ export function ThreadAccordion({

interface ThreadAccordionProps {
thread: Thread
initialMessagePairs?: MessagePair[]
initialMessagePairs: MessagePair[]
clientFetch?: boolean
chat?: boolean
showHeading?: boolean
Expand Down
9 changes: 7 additions & 2 deletions apps/masterbots.ai/components/shared/thread-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DialogTrigger
} from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
import { convertMessage } from '@/lib/threads'
import { convertMessage, createMessagePairs } from '@/lib/threads'
import { NewChatInput } from '../routes/c/new-chat'
import { ThreadAccordion } from './thread-accordion'
import { ThreadHeading } from './thread-heading'
Expand Down Expand Up @@ -40,7 +40,12 @@ export function ThreadDialog({
'max-w-[1400px] w-[80%] h-[90%] hide-buttons overflow-auto'
)}
>
<ThreadAccordion chat={chat} clientFetch thread={thread} />
<ThreadAccordion
chat={chat}
clientFetch
thread={thread}
initialMessagePairs={createMessagePairs(thread.messages)}
/>
{chat ? (
<DialogFooter>
<NewChatInput
Expand Down
7 changes: 4 additions & 3 deletions apps/masterbots.ai/components/shared/thread-heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cn } from '@/lib/utils'
import { ShortMessage } from './thread-short-message'
import { AccountAvatar } from './account-avatar'
import Shortlink from './copy-shortlink'
import { toSlug } from '@/lib/url'

export function ThreadHeading({
thread,
Expand All @@ -15,13 +16,13 @@ export function ThreadHeading({
<div className={cn(`flex flex-col font-medium w-full`)}>
<div
className={cn(
'flex items-center font-normal md:text-lg transition-all w-full gap-3 pr-4 justify-between'
'flex items-center font-normal md:text-lg transition-all w-full gap-3 pr-4 justify-between '
)}
>
<div className="flex grow gap-3">
<AccountAvatar
alt={thread.chatbot.name}
href={`/${chat ? 'c' : 'b'}/${thread.chatbot.name.toLowerCase()}`}
href={`/${chat ? 'c' : 'b'}/${toSlug(thread.chatbot.name)}`}
src={thread.chatbot.avatar}
/>

Expand All @@ -35,7 +36,7 @@ export function ThreadHeading({
<>
<span className="opacity-50 text-[0.875rem]">by</span>
<AccountAvatar
alt={thread.user.username.replace('_', ' ')}
alt={thread.user.username}
href={`/u/${thread.user.slug}`}
src={thread.user.profilePicture || ''}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {
} from '@/components/ui/accordion'
import { ThreadAccordion } from './thread-accordion'
import { ThreadHeading } from './thread-heading'
import { createMessagePairs } from '@/lib/threads'

export function ThreadDoubleAccordion({
export function ThreadListAccordion({
thread,
chat = false
}: ThreadDoubleAccordionProps) {
}: ThreadListAccordionProps) {
const [state, setState] = useSetState({
isOpen: false,
firstQuestion:
Expand All @@ -28,9 +29,12 @@ export function ThreadDoubleAccordion({
return (
<Accordion
className="w-full"
onValueChange={v => { setState({ isOpen: v[0] === 'pair-1' }); }}
onValueChange={v => {
setState({ isOpen: v[0] === 'pair-1' })
}}
type="multiple"
>
{/* Frist level question and excerpt visible on lists */}
<AccordionItem value="pair-1">
<AccordionTrigger
className={cn('hover:bg-mirage px-5', state.isOpen && 'bg-mirage')}
Expand All @@ -44,20 +48,26 @@ export function ThreadDoubleAccordion({
/>
</AccordionTrigger>

{/* TODO: we need to slide down the content */}
<AccordionContent className={cn('pl-14')}>
<ThreadAccordion
chat={chat}
clientFetch
showHeading={false}
thread={thread}
/>
{/* Secod level accordion with follow up questions
showHeading must be false as we already have in screen on AccordionTrigger above */}
<div className="overflow-y-scroll scrollbar srcoll-smooth max-h-[500px]">
<ThreadAccordion
chat={chat}
clientFetch
showHeading={false}
thread={thread}
initialMessagePairs={createMessagePairs(thread.messages)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)
}

interface ThreadDoubleAccordionProps extends DialogProps {
interface ThreadListAccordionProps extends DialogProps {
thread: Thread
chat?: boolean
}
6 changes: 3 additions & 3 deletions apps/masterbots.ai/components/shared/thread-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GetBrowseThreadsParams } from '@/services/hasura/hasura.service.type'
import { getBrowseThreads } from '@/services/hasura'
// import { useGlobalStore } from '@/hooks/use-global-store'
import { ThreadDialog } from './thread-dialog'
import { ThreadDoubleAccordion } from './thread-double-accordion'
import { ThreadListAccordion } from './thread-list-accordion'

export function ThreadList({
initialThreads,
Expand Down Expand Up @@ -62,8 +62,8 @@ export function ThreadList({
}
}, [isFetchingNextPage, fetchNextPage])

// ThreadDialog and ThreadDoubleAccordion can be used interchangeably
const ThreadComponent = dialog ? ThreadDialog : ThreadDoubleAccordion
// ThreadDialog and ThreadListAccordion can be used interchangeably
const ThreadComponent = dialog ? ThreadDialog : ThreadListAccordion

const threads = uniq(flatten(data.pages))

Expand Down
2 changes: 1 addition & 1 deletion apps/masterbots.ai/components/shared/thread-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function BrowseChatMessage({
const cleanMessage = { ...message, content: cleanPrompt(message.content) }

return (
<div className={cn('group relative my-4 flex items-start')} {...props}>
<div className={cn('group relative pt-4 flex items-start')} {...props}>
<div className="flex-1 px-1 md:ml-4 space-y-2 overflow-hidden">
<MemoizedReactMarkdown
className="min-w-full prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 !max-w-5xl"
Expand Down
4 changes: 2 additions & 2 deletions apps/masterbots.ai/lib/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export const SlugSchema: ZodSchema<string> = z.string()
.regex(/^[a-z0-9]+[a-z0-9+_-]*[a-z0-9]+$/, "Invalid slug format.")

// Function to convert a username into a slug
export const toSlug = (username: string, separator = "_"): string => {
export const toSlug = (username: string, separator = '_' ): string => {
return username
.toLowerCase()
.replace(/ & /g, '_and_')
.replace(/&/g, '_')
.replace(/ & /g, '_')
.replace(/[^a-z0-9_+-]/g, separator)
}

Expand Down

0 comments on commit 81d33e0

Please sign in to comment.