Skip to content

Commit

Permalink
fix: navigation, slugs, accordion (#174)
Browse files Browse the repository at this point in the history
* fix: navigation, slugs, accordion

* fix: thread navigation
  • Loading branch information
gaboesquivel authored Apr 13, 2024
1 parent e8dda27 commit cddb186
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 73 deletions.
13 changes: 11 additions & 2 deletions apps/masterbots.ai/app/(browse)/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CategoryTabs } from '@/components/shared/category-tabs/category-tabs'
import { SearchInput } from '@/components/shared/search-input'
import { getBrowseThreads, getCategories } from '@/services/hasura'
import { decodeQuery, toSlug } from '@/lib/url'
import { permanentRedirect } from 'next/navigation'

// TODO: dicuss caching
// export const revalidate = 3600 // revalidate the data at most every hour
Expand All @@ -11,10 +12,13 @@ export default async function CategoryPage({
params,
searchParams
}: CategoryPageProps) {
if (searchParams.threadId)
permanentRedirect(`${params.category}/${searchParams.threadId}`)
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 Expand Up @@ -48,5 +52,10 @@ export default async function CategoryPage({

interface CategoryPageProps {
params: { category: string }
searchParams?: { query: string; page: string; limit: string }
searchParams?: {
query: string
page: string
limit: string
threadId: string
}
}
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
121 changes: 82 additions & 39 deletions apps/masterbots.ai/components/shared/thread-accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,80 +15,123 @@ import { cn } from '@/lib/utils'
import { toSlug } from '@/lib/url'
import { ThreadHeading } from './thread-heading'
import { BrowseChatMessage } from './thread-message'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { clone } from 'lodash'
import { threadId } from 'worker_threads'

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(() => {
const url = new URL(window.location.href)
url.searchParams.set('threadId', thread.threadId)
window.history.pushState({}, '', url.href)

window.history.pushState({}, '', threadUrl)
// hack to delete threadId after initial render
// TODO: remove on next middleware
if (pathname.includes(thread.threadId)) {
url.searchParams.delete('threadId')
window.history.pushState({}, '', url.pathname + url.search)
}
// Cleanup function to remove the query parameter on unmount
return () => {
window.history.pushState({}, '', initialUrl)
const url = new URL(window.location.href)
url.searchParams.delete('threadId')
window.history.pushState({}, '', url.pathname + url.search)
}
})
}, [thread.threadId, pathname])

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 +142,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
Loading

0 comments on commit cddb186

Please sign in to comment.