From 81d33e0ca6866e3c711cd3ae8aba50dd6979d5b3 Mon Sep 17 00:00:00 2001 From: Gabo Esquivel Date: Sat, 13 Apr 2024 12:42:12 -0600 Subject: [PATCH] fix: navigation, slugs, accordion --- .../app/(browse)/[category]/page.tsx | 3 +- apps/masterbots.ai/app/globals.css | 4 +- .../components/shared/thread-accordion.tsx | 126 ++++++++++++------ .../components/shared/thread-dialog.tsx | 9 +- .../components/shared/thread-heading.tsx | 7 +- ...ccordion.tsx => thread-list-accordion.tsx} | 30 +++-- .../components/shared/thread-list.tsx | 6 +- .../components/shared/thread-message.tsx | 2 +- apps/masterbots.ai/lib/url.ts | 4 +- 9 files changed, 128 insertions(+), 63 deletions(-) rename apps/masterbots.ai/components/shared/{thread-double-accordion.tsx => thread-list-accordion.tsx} (60%) diff --git a/apps/masterbots.ai/app/(browse)/[category]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/page.tsx index 2df7f6e7..8ea7281d 100644 --- a/apps/masterbots.ai/app/(browse)/[category]/page.tsx +++ b/apps/masterbots.ai/app/(browse)/[category]/page.tsx @@ -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 diff --git a/apps/masterbots.ai/app/globals.css b/apps/masterbots.ai/app/globals.css index 6295162f..54f60268 100644 --- a/apps/masterbots.ai/app/globals.css +++ b/apps/masterbots.ai/app/globals.css @@ -104,8 +104,8 @@ } .scrollbar::-webkit-scrollbar { - width: 1px; - height: 1px; + width: 4px; + height: 4px; } .scrollbar::-webkit-scrollbar-track, .scrollbar::-webkit-scrollbar-corner { diff --git a/apps/masterbots.ai/components/shared/thread-accordion.tsx b/apps/masterbots.ai/components/shared/thread-accordion.tsx index b7841ddd..2d317d6c 100644 --- a/apps/masterbots.ai/components/shared/thread-accordion.tsx +++ b/apps/masterbots.ai/components/shared/thread-accordion.tsx @@ -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' @@ -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
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 - return ( {pairs.map((p, key) => { + const isFirst = key === 0 + console.log(key, p) return ( - - {showHeading ? ( - - {key ? ( -
{p.userMessage.content}
- ) : ( + + { + // is not the frist question we return follow question style + !isFirst ? ( + +
+ {p.userMessage.content} +
+
+ ) : 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 ? ( + - )} - - ) : null} - -
- {p.chatGptMessage.map((message, index) => ( - - ))} -
+
+ ) : null + } + + + {p.chatGptMessage.map(message => ( + + ))}
) @@ -99,7 +147,7 @@ export function ThreadAccordion({ interface ThreadAccordionProps { thread: Thread - initialMessagePairs?: MessagePair[] + initialMessagePairs: MessagePair[] clientFetch?: boolean chat?: boolean showHeading?: boolean diff --git a/apps/masterbots.ai/components/shared/thread-dialog.tsx b/apps/masterbots.ai/components/shared/thread-dialog.tsx index 4d5af21f..7a12441c 100644 --- a/apps/masterbots.ai/components/shared/thread-dialog.tsx +++ b/apps/masterbots.ai/components/shared/thread-dialog.tsx @@ -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' @@ -40,7 +40,12 @@ export function ThreadDialog({ 'max-w-[1400px] w-[80%] h-[90%] hide-buttons overflow-auto' )} > - + {chat ? (
@@ -35,7 +36,7 @@ export function ThreadHeading({ <> by diff --git a/apps/masterbots.ai/components/shared/thread-double-accordion.tsx b/apps/masterbots.ai/components/shared/thread-list-accordion.tsx similarity index 60% rename from apps/masterbots.ai/components/shared/thread-double-accordion.tsx rename to apps/masterbots.ai/components/shared/thread-list-accordion.tsx index a66761a4..6f6e5549 100644 --- a/apps/masterbots.ai/components/shared/thread-double-accordion.tsx +++ b/apps/masterbots.ai/components/shared/thread-list-accordion.tsx @@ -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: @@ -28,9 +29,12 @@ export function ThreadDoubleAccordion({ return ( { setState({ isOpen: v[0] === 'pair-1' }); }} + onValueChange={v => { + setState({ isOpen: v[0] === 'pair-1' }) + }} type="multiple" > + {/* Frist level question and excerpt visible on lists */} + {/* TODO: we need to slide down the content */} - + {/* Secod level accordion with follow up questions + showHeading must be false as we already have in screen on AccordionTrigger above */} +
+ +
) } -interface ThreadDoubleAccordionProps extends DialogProps { +interface ThreadListAccordionProps extends DialogProps { thread: Thread chat?: boolean } diff --git a/apps/masterbots.ai/components/shared/thread-list.tsx b/apps/masterbots.ai/components/shared/thread-list.tsx index 9414ea04..342b9272 100644 --- a/apps/masterbots.ai/components/shared/thread-list.tsx +++ b/apps/masterbots.ai/components/shared/thread-list.tsx @@ -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, @@ -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)) diff --git a/apps/masterbots.ai/components/shared/thread-message.tsx b/apps/masterbots.ai/components/shared/thread-message.tsx index 722b7e3b..44b04c7b 100644 --- a/apps/masterbots.ai/components/shared/thread-message.tsx +++ b/apps/masterbots.ai/components/shared/thread-message.tsx @@ -20,7 +20,7 @@ export function BrowseChatMessage({ const cleanMessage = { ...message, content: cleanPrompt(message.content) } return ( -
+
= 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) }