From 81d33e0ca6866e3c711cd3ae8aba50dd6979d5b3 Mon Sep 17 00:00:00 2001 From: Gabo Esquivel Date: Sat, 13 Apr 2024 12:42:12 -0600 Subject: [PATCH 1/2] 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) } From 88cbf3c53f35bc9b3e222b98fa9e2df604b38c58 Mon Sep 17 00:00:00 2001 From: Gabo Esquivel Date: Sat, 13 Apr 2024 13:41:48 -0600 Subject: [PATCH 2/2] fix: thread navigation --- .../app/(browse)/[category]/page.tsx | 10 ++++- .../components/shared/thread-accordion.tsx | 39 ++++++++----------- apps/masterbots.ai/next.config.js | 18 ++++----- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/apps/masterbots.ai/app/(browse)/[category]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/page.tsx index 8ea7281d..214dada6 100644 --- a/apps/masterbots.ai/app/(browse)/[category]/page.tsx +++ b/apps/masterbots.ai/app/(browse)/[category]/page.tsx @@ -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 @@ -11,6 +12,8 @@ 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( @@ -49,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 + } } diff --git a/apps/masterbots.ai/components/shared/thread-accordion.tsx b/apps/masterbots.ai/components/shared/thread-accordion.tsx index 2d317d6c..b01f8fa5 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, useState } from 'react' +import { useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { Thread } from '@repo/mb-genql' import { getMessagePairs } from '@/services/hasura' @@ -15,8 +15,9 @@ 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 { usePathname, useRouter, useSearchParams } from 'next/navigation' import { clone } from 'lodash' +import { threadId } from 'worker_threads' export function ThreadAccordion({ thread, @@ -48,29 +49,23 @@ export function ThreadAccordion({ // 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}` - ) + 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({}, '', initialPathname) + const url = new URL(window.location.href) + url.searchParams.delete('threadId') + window.history.pushState({}, '', url.pathname + url.search) } - }) + }, [thread.threadId, pathname]) if (error) return
There was an error loading thread messages
diff --git a/apps/masterbots.ai/next.config.js b/apps/masterbots.ai/next.config.js index 9dddfdbf..a721a352 100644 --- a/apps/masterbots.ai/next.config.js +++ b/apps/masterbots.ai/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ -const path = require('path') +const path = require('node:path') module.exports = { images: { @@ -27,22 +27,22 @@ module.exports = { protocol: 'https', hostname: 'api.dicebear.com', port: '', - pathname: '**', + pathname: '**' } ] }, async headers() { return [ { - source: "/api/dicebear", // Adjust the source path based on your API route + source: '/api/dicebear', // Adjust the source path based on your API route headers: [ { - key: "Content-Type", - value: "image/svg+xml", - }, - ], - }, - ]; + key: 'Content-Type', + value: 'image/svg+xml' + } + ] + } + ] }, experimental: { ...(process.env.NODE_ENV === 'development'