From 2be00446ffb3cd40cb1e6fbb06827c4dabb71f1c Mon Sep 17 00:00:00 2001 From: Jimoh sherifdeen <63134009+sheriffjimoh@users.noreply.github.com> Date: Mon, 20 May 2024 16:06:05 +0100 Subject: [PATCH] feat(masterbots.ai): consistent og image style design and dynamic metadata (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: added og api endpoint * feat: design og image for dark mode * fix: file formated * fix: amend og image to pick current theme color and adapt * feat: added custom metadata to thread page * feat: added custom metadata to bot page * fix: clean up * fix: move bg to a component * fix: move og-image design to a component * fix: use variable for URL * fix: to slug func * ⚡️ Move and clean up UrlToSlug * fix(masterbots.ai): zod dependecy * fix: type error * fix: type error for metadata * fix: clean and build fix --------- Co-authored-by: Roberto Lucas --- .../app/b/[id]/[threadId]/page.tsx | 12 + apps/masterbots.ai/app/og/route.tsx | 121 ++---- apps/masterbots.ai/components/og-bg-image.tsx | 392 ++++++++++++++++++ apps/masterbots.ai/components/og-image.tsx | 150 +++++++ apps/masterbots.ai/lib/metadata.ts | 22 +- apps/masterbots.ai/lib/threads.ts | 36 +- apps/masterbots.ai/lib/url.ts | 29 ++ apps/masterbots.ai/package.json | 2 +- .../public/og-background-image.svg | 0 bun.lockb | Bin 393136 -> 393136 bytes 10 files changed, 652 insertions(+), 112 deletions(-) create mode 100644 apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx create mode 100644 apps/masterbots.ai/components/og-bg-image.tsx create mode 100644 apps/masterbots.ai/components/og-image.tsx create mode 100644 apps/masterbots.ai/lib/url.ts create mode 100644 apps/masterbots.ai/public/og-background-image.svg diff --git a/apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx b/apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx new file mode 100644 index 00000000..f75f4276 --- /dev/null +++ b/apps/masterbots.ai/app/b/[id]/[threadId]/page.tsx @@ -0,0 +1,12 @@ +import { BrowseThread } from '@/components/browse-thread' +import { ChatPageProps } from '@/app/chat/[chatbot]/[threadId]/page' +import { getThread } from '@/app/actions' + +export { generateMbMetadata as generateMetadata } from '@/lib/metadata' + +export default async function ChatPage({ params }: ChatPageProps) { + const thread = await getThread({ + threadId: params.threadId, + }) + return +} diff --git a/apps/masterbots.ai/app/og/route.tsx b/apps/masterbots.ai/app/og/route.tsx index 68bcc501..17c1d641 100644 --- a/apps/masterbots.ai/app/og/route.tsx +++ b/apps/masterbots.ai/app/og/route.tsx @@ -1,119 +1,48 @@ -/* eslint-disable @next/next/no-img-element */ - import { ImageResponse } from '@vercel/og' import { NextRequest } from 'next/server' -import { GeistMono } from 'geist/font/mono' // Import the GeistMono font import '@/app/globals.css' -import { getThread } from '../actions' - +import { getThread } from '@/app/actions' +import OgImage from '@/components/og-image' export const runtime = 'edge' export async function GET(req: NextRequest) { try { const { searchParams } = req.nextUrl - const threadId = searchParams.get('threadId') - const thread = await getThread({ threadId }) - - // You may need to convert GeistMono or fetch it as ArrayBuffer if needed - // const font = GeistMono; // Assuming GeistMono can be directly used, modify as needed + const threadId = searchParams.get('threadId'); + const thread = await getThread({ threadId },) + const question = thread.firstMessage.content + const answer = thread.firstAnswer.content + const username = thread.account?.username + const user_avatar = thread.account?.avatar || '' + let theme = 'dark' + if (typeof window !== 'undefined') { + theme = localStorage.getItem('theme') || 'dark' + } + const isLightTheme = theme === 'light' return new ImageResponse( ( -
-
-
-

- {thread.chatbot.name} -

-

- {thread.firstMessage.content} -

-

- {thread.chatbot.categories[0].name} -

-
- {thread.chatbot.avatar ? ( -
- - - - - - - -
- ) : null} -
-
+ ), { width: 1200, height: 627 } ) - } catch (e) { + } catch (e: any) { console.log(`${e.message}`) return new Response(`Failed to generate the image`, { status: 500 }) } } +function useTheme() { + throw new Error('Function not implemented.') +} diff --git a/apps/masterbots.ai/components/og-bg-image.tsx b/apps/masterbots.ai/components/og-bg-image.tsx new file mode 100644 index 00000000..63a45ece --- /dev/null +++ b/apps/masterbots.ai/components/og-bg-image.tsx @@ -0,0 +1,392 @@ +export default function OgBgImage({ isLightTheme }: { isLightTheme: boolean }) { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/apps/masterbots.ai/components/og-image.tsx b/apps/masterbots.ai/components/og-image.tsx new file mode 100644 index 00000000..08929d0d --- /dev/null +++ b/apps/masterbots.ai/components/og-image.tsx @@ -0,0 +1,150 @@ +import OgBgImage from '@/components/og-bg-image' +interface OgImageProps { + thread: any + question: string + answer: string + username: string | undefined + user_avatar: string + isLightTheme: boolean +} + +export default function OgImage({ thread, question, answer, username, user_avatar, isLightTheme }: OgImageProps) { + + return ( +
+ +
+
+

+ {thread.chatbot.name} +

+

+ {' '} + {thread.chatbot.categories[0]?.category.name} +

+

+ {question} +

+

+ {answer} +

+ +
+ +

+ {username} +

+
+
+ {thread.chatbot.avatar ? ( +
+
+ +
+
+ ) : null} +
+
+ ) +} diff --git a/apps/masterbots.ai/lib/metadata.ts b/apps/masterbots.ai/lib/metadata.ts index 72baded9..0f509c5b 100644 --- a/apps/masterbots.ai/lib/metadata.ts +++ b/apps/masterbots.ai/lib/metadata.ts @@ -1,19 +1,27 @@ +import { getThread } from '@/services/hasura' import type { Metadata } from 'next' -import { getThread } from '@/app/actions' import { getThreadLink } from './threads' export async function generateMbMetadata({ params +}: { + params: any; }): Promise { - const thread = await getThread({ threadId: params.threadId }) + const threadId = params?.threadId + const thread = await getThread({ threadId, jwt: '' }) if (!thread) return + const firstQuestion = + thread.messages.find(m => m.role === 'user')?.content || 'not found' + const firstResponse = + thread.messages.find(m => m.role === 'assistant')?.content || 'not found' + const data = { - title: thread.firstMessage.content, + title: firstQuestion, publishedAt: thread.updatedAt, // format(thread.updatedAt, 'MMMM dd, yyyy'), - summary: thread.firstAnswer.content, - image: `https://alpha.masterbots.ai/og?threadId=${thread.threadId}`, - pathname: getThreadLink({ thread, chat: false }) + summary: firstResponse, + image: `${process.env.VERCEL_URL}/og?threadId=${thread.threadId}`, + pathname: getThreadLink({ thread: thread, chat: false }) } return { @@ -25,7 +33,7 @@ export async function generateMbMetadata({ description: data.summary, type: 'article', publishedTime: data.publishedAt, - url: `https://alpha.masterbots.ai/${data.pathname}`, + url: `${process.env.VERCEL_URL}${data.pathname}`, images: [ { url: data.image diff --git a/apps/masterbots.ai/lib/threads.ts b/apps/masterbots.ai/lib/threads.ts index 2472c94e..56e93b6d 100644 --- a/apps/masterbots.ai/lib/threads.ts +++ b/apps/masterbots.ai/lib/threads.ts @@ -1,6 +1,7 @@ import type * as AI from 'ai' import { MB } from '@repo/supabase' import { toSlug } from './url-params' +import { extractBetweenMarkers } from '@/lib/utils' export function createMessagePairs(messages: AI.Message[]): MB.MessagePair[] { const messagePairs: MB.MessagePair[] = [] @@ -41,19 +42,38 @@ export function cleanPrompt(str: string) { return extracted || str } +export interface MessagePair { + userMessage: MB.Message | AI.Message + chatGptMessage: MB.Message[] +} + +export function convertMessage(message: MB.Message) { + return { + id: message.messageId, + content: message.content, + createAt: message.createdAt, + role: message.role + } as AI.Message +} + +export function getAllUserMessagesAsStringArray( + allMessages: MB.Message[] | AI.Message[] +) { + const userMessages = allMessages.filter(m => m.role === 'user') + const cleanMessages = userMessages.map(m => + extractBetweenMarkers(m.content, 'Then answer this question:') + ) + return cleanMessages.join(', ') +} + export function getThreadLink({ chat = false, - param = false, thread }: { chat?: boolean - param?: boolean - thread: MB.ThreadFull + thread: MB.Thread }) { - console.log('getThreadLink', thread.chatbot?.categories) - if (param) - return `/${toSlug(thread.chatbot.categories[0].name)}?threadId=${thread.threadId.trim()}` return chat - ? `/c/${toSlug(thread.chatbot.name)}/${thread.threadId.trim()}` - : `/${toSlug(thread.chatbot.categories[0].name)}/${thread.threadId.trim()}` + ? `/c/${toSlug(thread.chatbot.name)}/${thread.threadId}` + : `/${toSlug(thread.chatbot.categories[0]?.category.name)}/${thread.threadId}}` } diff --git a/apps/masterbots.ai/lib/url.ts b/apps/masterbots.ai/lib/url.ts new file mode 100644 index 00000000..6fca4411 --- /dev/null +++ b/apps/masterbots.ai/lib/url.ts @@ -0,0 +1,29 @@ +import { z, ZodSchema } from 'zod' + +// Zod schema for validating slug strings +export const SlugSchema: ZodSchema = z + .string() + .min(1) + .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 => { + return username + .toLowerCase() + .replace(/ & /g, '_n_') + .replace(/&/g, '_') + .replace(/[^a-z0-9_+-]/g, separator) +} + + + +//Encodes a string for use in a URL, replacing spaces with the '+' character. +export const encodeQuery = (input: string): string => { + return encodeURIComponent(input).replace(/%20/g, '+').replace(/ /g, '+') +} + +//Decodes a URL-encoded string, converting '+' back into spaces. + +export const decodeQuery = (input: string): string => { + return decodeURIComponent(input.replace(/\+/g, ' ')) +} \ No newline at end of file diff --git a/apps/masterbots.ai/package.json b/apps/masterbots.ai/package.json index 8c26c901..afc0f2e1 100644 --- a/apps/masterbots.ai/package.json +++ b/apps/masterbots.ai/package.json @@ -68,7 +68,7 @@ "remark-math": "^5.1.1", "ts-case-convert": "^2.0.7", "use-debounce": "^10.0.0", - "zod": "^3.22.4", + "zod": "^3.23.8", "@repo/supabase": "workspace:*" }, "devDependencies": { diff --git a/apps/masterbots.ai/public/og-background-image.svg b/apps/masterbots.ai/public/og-background-image.svg new file mode 100644 index 00000000..e69de29b diff --git a/bun.lockb b/bun.lockb index 5ab3f7727ccbbce4fb8cd38e5b0a7da927f3bb47..00f71681a1e3713dfcfc969d76d7a2e910e93263 100755 GIT binary patch delta 6717 zcmXBWclfPz0mpHFSEXfGSy)+AR;OXnFi*BAjJq;unty?p#wWsOke@CLlje3f^(>11}m5! zrkFzy?%|3B4B#E1Si%VYk%|>e5FDjggOO;3(13ZgVgxN%$0)|ofqkrE0t=WOr7b@n^gL{!; z0Rwm!E0!>VzfrM*34%hg24j;}2o0E*C`QnNb*W+u9oUyCCa{3n<%%gR!MQ>)gB8rL zRLr3Vw^S@(0PiZr5=QVhD^@T;aJ6C$#x+_YG+#Uz z0%q?~OkoMmdlfTS!Tf!SIrQM(p;*8G-uo3x7{PB8E0`d-Q?Ul)16m<8V17_Bf)=a~ zDaO!&{b9uf7BKsWVhT%eKB}0(3g)e14n4RZQ!HQr@8gOkjNpGlv4RPLPb$`6d`c^X z2Fy<@M$m%w8O0bnusg*B7BKs)VhT%eKBt($3g(|z%%KPO3yK8{;C)fCgc1BNDONB+ z@MXmsjD=PR4VYh1jGzVUtBNsnV1G?9fd$OIu9(6SoNp**u!8wF6?5pp{gz?@19(ft z5=QX9tysYX!FLpEFuto5LIdXa6eDQC`o3Ze9oRomOke@CA1bD>1ZSm~!3yR-Qp}+T z_s5C_4B-7lv4j!)pDI=`LGUxh8jPQ7h0uWc3&jXpuzJN9ICI|+_8jL?^h0uWcN5u$Qu>PbNLkISs6%$y% z>@SKbEW!D!Vg@Ui|4lK69^6r}fC0R}E0!>V{}06qCJ6qiScCB|tq>Y8|E(B73)X)W zW9Y#CuVMlVm`#c)EWx=;F@u%ZIA6Er-*RKFv;Y6O$z2l#4B)L(EMWwHYsCsC2)0qI z!Pr(Sga%AQF@hGXyD7%dfqi$y1Qsy6hhhp#aPFy?!3yU0Qp}+TcRR%b2Jr5!Si%Ut zsaU}T!S;$Z828Z%p#k&0iV?J6-A^%w4(uHi6Ij6P{)#Cq!FhmU1}m6biaGS)?xga*vVC`QnN^;pFiI5`5wu{vMKOjB?6)c=uz=ay6jNA&^LE7yRxp2u zVh%mH`zRJLfcH+t5=QVN#R?_}_EoIG*iS2j2F(2xBWS@oKrx06>;n}OSitNc#T1s{ z9ITkZ3g)q54n4SsC>Ah)cc@|sBlw3YRxm+uxMB^)5n3TMU>>O$K?~MViZOIxCyEIy zV0N@(3QKT~QOsZk^J5ir=)pZsv48=*;}uI7!9PK3)V@B zF?3*`teC(8W~V5oumtB+#SB(3KTR=*9^BIv3mCx56iXPv-=J8*1i=}KH5g}Vh0uU` zmSO}gSZ6E7(1Cr9Vgd`8ovWC_5}aHygB8rrQ_P_U_k6_y2JkLWEMWxyLd6Ou2rg2r z!MIo}ga*uwiV?J66^b!*U~f`PU;(pB6jNA&bE#qmE0|xVm_rZl<%$Ii;9a3u!U+DA ziWN){l!`SNS80XNfVo*Qf)=c+6=UeYzD6;D1&7C{%y={K|{{RndTe$!L delta 6732 zcmY+{SM=m$0fzA{5E}*&4JsN1bu4I9gt1|)#2!Q?tEjPI=%I%mKp;yEb>~kFbwk~V ziUC{fv5(kdAA9U0wkVb$ypK2B{u~@IZO^xOrZnk zV8sj;Fg-*uhaTKR6=$#n?=Zyz2JjD8EMbJ;2*nDFL@R_E%p(;eXuvv3F@_fGqZJdF z!{ivn6gqH@Rm@-k)8iC#=)paH^|~!qty;Bu)wt!jdey33SGK!-&2B4q9>02zd%kNg z)sX@Q@J~=IVT9mB#R`m*v_h!Ce4klJh8 z!97iJ221cxS1e!v{|v5F@ZTu&Q(mI1Lr)& z3>GjwUonRs+zS+EumtZy#R3NKHz<}cLNHUTz*x}=p$79J#RwX(E>?`81^W`k1m-Zg zR567PoXZq5Sitmh#T1sC-S9VuV{|4PLYMhLD_tiZThD})-%YZN1B zz`9m3h8FCNiV4hNQYxmVgUpAHz}4dLU6NU1;!?= z5Na?h#RwX(-me%#3-&FF3Cv;g0mT$La6YJ*!2+fqQp}+T_g2LjEW!J*VgUpAwPFb) z1h*+xVBD@1LJj6e6eDQB`lwGkL6m#go{iNaymf(F# zv48>mPb-!%Lhu>I3XIQcg;0a}ImHMXus*LCLko7Rn7|w+UrCB+xbR6+#W>*A*jZz`8>*h8FB^C?+t6$u|{K=)n1w zVg?JCep@kz9^CIJ&R_{%r&z!M{&y8i7$NwcVg<(cwL+-D{DEQw4Ol-^jG+blM~Vr| zVe(_e6gqGgiWw|m`V++*dT@WLID;j4KT|AV0RQKTC5#aKLa_qlms%mzVE#%mf(EQ! zF@_fGUn?dshskdgQ|Q3?tzrfXnEp;NhaTMDE6!jE-X9bT7{LFdVhJMzOT`L|KWT+f zgL$W71Pxe!R*azq`!9+K%wh6Z#S}Vl{-&700;Ydg%%KN&P@KUMyniSbFo6G0#S%sc z?ozD4_?K1)HJJZajGzJQKZ-H5VEwh*k(Sm|H1E(17(&#TZ(!w_bCfSO2?K zcHeyMZYy_ez2@Fm|H;F2B!v!~hbv~VfT^XJLl5pYiZfV(_Xx!T2Jj!LSi%UwqZBJJ zw$%!u2J_L15j0>uMlpsKY+EsbIZPg_m_i56;}kPkz;rvs9C~oKSDe8TyvHjRFo6FA z#S%sco~T%XG0_U42J=aZ5j0>uSuutd?58LuFo(%g6;tTI*+DUb1x%l&m_rZl(-mj1 z1kX_{U;zIaiY1H?JX5g(<5^lE)L`zY7(oNpvlU}#!QM$RfjLZeR!pG-N53d587yG> z9DOo}9^B_D&R_}N^Armhz<<7C2_pnAP^`e%MJt3F%oi#~(17JC#?XTOBE9F@ZTu z&Q?sJ11DF^U;)!}6m#goJy&rCOYqK9EMNfte8mz*2rf{pz_?H=gc{5ZiV-wm%@kv3 z!Cp~JU=EXu6jSKHxmYoS1xznd%%KPOQpFi8!MjYcfC2o=6-yW)C=@F&uFwjh2J=eA z2pX`iQjDPm`)b7m<}kTNF@+ACYZWtCz;vTx4n4S~;tZDHU8h*U0RHugC5#Z4Y3*J&uieqvX=V3YH`{jo_Iqx2 L+xB~Io^Sjg>t0-Z