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: navigation, slugs, accordion #174

Merged
merged 2 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading