diff --git a/.gitignore b/.gitignore index e63e3c47..32251b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules .DS_Store _dev .vercel -__dev \ No newline at end of file +__dev +.vercel diff --git a/apps/hasura/metadata/databases/masterbots/tables/public_user.yaml b/apps/hasura/metadata/databases/masterbots/tables/public_user.yaml index c7e002a5..ac96b81f 100644 --- a/apps/hasura/metadata/databases/masterbots/tables/public_user.yaml +++ b/apps/hasura/metadata/databases/masterbots/tables/public_user.yaml @@ -17,7 +17,6 @@ array_relationships: table: name: social_following schema: public - - name: following using: foreign_key_constraint_on: @@ -25,7 +24,6 @@ array_relationships: table: name: social_following schema: public - - name: preferences using: foreign_key_constraint_on: @@ -33,7 +31,7 @@ array_relationships: table: name: preference schema: public - + - name: prompts using: foreign_key_constraint_on: @@ -41,7 +39,7 @@ array_relationships: table: name: prompt_user schema: public - + - name: referrals using: foreign_key_constraint_on: @@ -49,7 +47,7 @@ array_relationships: table: name: referral schema: public - + - name: referralsByUserId using: foreign_key_constraint_on: @@ -57,7 +55,7 @@ array_relationships: table: name: referral schema: public - + - name: threads using: foreign_key_constraint_on: @@ -65,7 +63,6 @@ array_relationships: table: name: thread schema: public - - name: userTokens using: foreign_key_constraint_on: @@ -73,49 +70,72 @@ array_relationships: table: name: user_token schema: public - insert_permissions: - role: moderator permission: check: {} columns: + - bio + - date_joined + - email + - favourite_topic - get_free_month - is_blocked - - email + - last_login - password - - profile_picture - pro_user_subscription_id - - slug - - username - - date_joined - - last_login + - profile_picture - role + - slug - user_id + - username comment: "" - - role: user permission: check: {} columns: + - bio - email + - favourite_topic - get_free_month - is_blocked - last_login - password - pro_user_subscription_id - profile_picture + - role - slug - username comment: "" select_permissions: - role: anonymous + permission: + columns: + - bio + - date_joined + - email + - favourite_topic + - last_login + - pro_user_subscription_id + - profile_picture + - role + - slug + - user_id + - username + filter: {} + comment: "" + - role: moderator permission: columns: - date_joined - email + - get_free_month + - is_blocked + - is_verified - last_login - pro_user_subscription_id - profile_picture + - role - slug - user_id - username @@ -141,8 +161,10 @@ select_permissions: - role: user permission: columns: + - bio - date_joined - email + - favourite_topic - get_free_month - is_blocked - is_verified @@ -156,26 +178,41 @@ select_permissions: filter: user_id: _eq: X-Hasura-User-Id - comment: "" + comment: "" update_permissions: - role: moderator permission: columns: + - bio + - date_joined + - favourite_topic + - get_free_month - is_blocked + - last_login + - password + - pro_user_subscription_id + - profile_picture + - role + - slug + - username filter: {} check: null - comment: "Allows moderators to block any user" - + comment: Allows moderators to block any user - role: user permission: columns: + - bio + - favourite_topic - get_free_month - last_login - password - pro_user_subscription_id - profile_picture + - role + - slug + - username filter: user_id: _eq: X-Hasura-User-Id check: null - comment: "" \ No newline at end of file + comment: "" diff --git a/apps/hasura/migrations/masterbots/1730936002843_alter_table_public_user_add_column_bio/down.sql b/apps/hasura/migrations/masterbots/1730936002843_alter_table_public_user_add_column_bio/down.sql new file mode 100644 index 00000000..a76ea6cb --- /dev/null +++ b/apps/hasura/migrations/masterbots/1730936002843_alter_table_public_user_add_column_bio/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +alter table "public"."user" add column "bio" text + null; diff --git a/apps/hasura/migrations/masterbots/1730936002843_alter_table_public_user_add_column_bio/up.sql b/apps/hasura/migrations/masterbots/1730936002843_alter_table_public_user_add_column_bio/up.sql new file mode 100644 index 00000000..a28e5a6b --- /dev/null +++ b/apps/hasura/migrations/masterbots/1730936002843_alter_table_public_user_add_column_bio/up.sql @@ -0,0 +1,2 @@ +alter table "public"."user" add column "bio" text + null; diff --git a/apps/hasura/migrations/masterbots/1730936224679_alter_table_public_user_add_column_favourite_topic/down.sql b/apps/hasura/migrations/masterbots/1730936224679_alter_table_public_user_add_column_favourite_topic/down.sql new file mode 100644 index 00000000..07ecb7c8 --- /dev/null +++ b/apps/hasura/migrations/masterbots/1730936224679_alter_table_public_user_add_column_favourite_topic/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."user" add column "favourite_topic" text +-- null; diff --git a/apps/hasura/migrations/masterbots/1730936224679_alter_table_public_user_add_column_favourite_topic/up.sql b/apps/hasura/migrations/masterbots/1730936224679_alter_table_public_user_add_column_favourite_topic/up.sql new file mode 100644 index 00000000..ad98e8fc --- /dev/null +++ b/apps/hasura/migrations/masterbots/1730936224679_alter_table_public_user_add_column_favourite_topic/up.sql @@ -0,0 +1,2 @@ +alter table "public"."user" add column "favourite_topic" text + null; diff --git a/apps/masterbots.ai/.gitignore b/apps/masterbots.ai/.gitignore index dd019e40..43235242 100644 --- a/apps/masterbots.ai/.gitignore +++ b/apps/masterbots.ai/.gitignore @@ -36,3 +36,6 @@ yarn-error.log* .vercel .vscode .env*.local + +.vercel +.env*.local diff --git a/apps/masterbots.ai/actions.ts b/apps/masterbots.ai/actions.ts deleted file mode 100644 index d6167b27..00000000 --- a/apps/masterbots.ai/actions.ts +++ /dev/null @@ -1,54 +0,0 @@ -'use server' -import { cookies } from 'next/headers' -import axios from 'axios' -import { Resend } from 'resend' -import { z } from 'zod' - -// generate dub.co links -export async function generateShortLink(path: string) { - const cookieStorage = cookies() - try { - const resolved: DubShareLinkResponse = await axios - .post( - `https://api.dub.co/links?workspaceId=${process.env.DUB_WORKSPACE_ID}`, - { - domain: 'mbots.to', - url: `https://masterbots.ai${path}` - }, - { - headers: { - Authorization: `Bearer ${process.env.DUB_API_KEY}`, - 'Content-Type': 'application/json' - } - } - ) - .then(res => res.data) - - if (!resolved) throw new Error('Failed to generate short link') - - return { - data: { - key: resolved.key, - shortLink: resolved.shortLink, - qrCode: resolved.qrCode - }, - error: null - } - } catch (error) { - console.log(path+'Failed to generate short link: ==> ', error) - return { - data: null, - } - } -} - -export interface DubShareLinkResponse { - key: string - shortLink: string - qrCode: string -} - -export type ActionState = { - data?: string - error?: string -} diff --git a/apps/masterbots.ai/app/(browse)/[category]/[threadId]/sitemap.ts b/apps/masterbots.ai/app/(browse)/[category]/[threadId]/sitemap.ts index 2c0fb46a..748682ff 100644 --- a/apps/masterbots.ai/app/(browse)/[category]/[threadId]/sitemap.ts +++ b/apps/masterbots.ai/app/(browse)/[category]/[threadId]/sitemap.ts @@ -6,7 +6,7 @@ export default async function sitemap(): Promise { const threads = await getThreadsWithoutJWT() return threads.map(thread => ({ - url: `${process.env.VERCEL_URL}/${toSlug(thread.chatbot.categories[0].category.name)}/${thread.threadId}`, - lastModified: thread.updatedAt + url: `${process.env.VERCEL_URL}/${toSlug(thread?.chatbot?.categories?.[0]?.category?.name)}/${thread?.threadId}`, + lastModified: thread?.updatedAt ?? new Date().toISOString() })) } diff --git a/apps/masterbots.ai/app/(browse)/[category]/page.tsx b/apps/masterbots.ai/app/(browse)/[category]/page.tsx index cda0420a..32629b69 100644 --- a/apps/masterbots.ai/app/(browse)/[category]/page.tsx +++ b/apps/masterbots.ai/app/(browse)/[category]/page.tsx @@ -1,10 +1,10 @@ -import BrowseList from '@/components/routes/browse/browse-list' import { BrowseCategoryTabs } from '@/components/routes/browse/browse-category-tabs' +import BrowseList from '@/components/routes/browse/browse-list' import { BrowseSearchInput } from '@/components/routes/browse/browse-search-input' +import { generateMetadataFromSEO } from '@/lib/metadata' import { getCategories } from '@/services/hasura' import { toSlug } from 'mb-lib' -import { Metadata } from 'next' -import { generateMetadataFromSEO } from '@/lib/metadata' +import type { Metadata } from 'next' export default async function BrowseCategoryPage({ params @@ -37,8 +37,7 @@ export async function generateMetadata({ const seoData = { title: category?.name || '', - description: - 'Browse our collection of chatbots and find the one that suits your needs.', + description: `Browse the threads and find the one that suits your needs, from the ${category?.name} category`, ogType: 'website', ogImageUrl: '', twitterCard: 'summary' diff --git a/apps/masterbots.ai/app/(browse)/layout.tsx b/apps/masterbots.ai/app/(browse)/layout.tsx index 47782a3d..fed9043e 100644 --- a/apps/masterbots.ai/app/(browse)/layout.tsx +++ b/apps/masterbots.ai/app/(browse)/layout.tsx @@ -11,14 +11,14 @@ interface BrowseLayoutProps { export default async function BrowseLayout({ children }: BrowseLayoutProps) { return ( -
+
- {children} -
+ + {children} + -
-
+
) } diff --git a/apps/masterbots.ai/app/(browse)/sitemap.ts b/apps/masterbots.ai/app/(browse)/sitemap.ts index b590b9d8..3efc4fef 100644 --- a/apps/masterbots.ai/app/(browse)/sitemap.ts +++ b/apps/masterbots.ai/app/(browse)/sitemap.ts @@ -1,5 +1,4 @@ import { MetadataRoute } from 'next' - export default function sitemap(): MetadataRoute.Sitemap { return [ { @@ -7,6 +6,18 @@ export default function sitemap(): MetadataRoute.Sitemap { lastModified: new Date(), changeFrequency: 'yearly', priority: 1 + }, + { + url: `${process.env.VERCEL_URL}/c`, + lastModified: new Date(), + changeFrequency: 'yearly', + priority: 0.8 + }, + { + url: `${process.env.VERCEL_URL}/wordware`, + lastModified: new Date(), + changeFrequency: 'yearly', + priority: 0.5 } ] } diff --git a/apps/masterbots.ai/app/actions/ai-executers.ts b/apps/masterbots.ai/app/actions/ai-executers.ts new file mode 100644 index 00000000..435ab222 --- /dev/null +++ b/apps/masterbots.ai/app/actions/ai-executers.ts @@ -0,0 +1,159 @@ +'use server' + +import { wordwareFlows } from '@/lib/constants/wordware-flows' +import type { aiTools } from '@/lib/helpers/ai-schemas' +import type { WordWareDescribeDAtaResponse } from '@/types/wordware-flows.types' +import axios from 'axios' +import type { z } from 'zod' +import { subtractChatbotMetadataLabels } from '.' + +const { WORDWARE_API_KEY } = process.env + +// TODO: Finish ICL implementation. ICL should be called as a tool that Ai will use to generate content. +export async function getChatbotMetadataTool({ + chatbot, + userContent +}: z.infer) { + console.info('Executing Chatbot Metadata Tool... Chatbot: ', { + chatbot, + userContent + }) + + try { + const chatbotMetadata = await subtractChatbotMetadataLabels( + { + domain: chatbot.categoryId, + chatbot: chatbot.chatbotId + }, + userContent, + // ? We will be using OpenAi for a while, at least for these tools + 'OpenAI' + ) + + console.log('chatbotMetadata ==> ', chatbotMetadata) + return JSON.stringify({ + chatbotMetadata + }) + } catch (error) { + console.error('Error fetching chatbot metadata: ', error) + return JSON.stringify({ + error: 'Internal Server Error while fetching chatbot metadata' + }) + } +} + +export async function getWebSearchTool({ + query +}: z.infer) { + console.info('Executing Web Search Tool... Query: ', query) + const webSearchFlow = wordwareFlows.find(flow => flow.path === 'webSearch') + + if (!webSearchFlow) { + throw new Error('Web Search tool is not available') + } + + try { + const appDataResponse = await axios.get( + `https://api.wordware.ai/v1alpha/apps/masterbots/${webSearchFlow.id}`, + { + headers: { + Authorization: `Bearer ${WORDWARE_API_KEY}`, + 'Content-Type': 'application/json' + } + } + ) + + if (appDataResponse.status >= 400) { + console.error('Error fetching app data: ', appDataResponse) + if (appDataResponse.status >= 500) { + throw new Error( + 'Internal Server Error while fetching app data. Please try again later.' + ) + } + throw new Error('Failed to authenticate for the app. Please try again.') + } + + const appData: WordWareDescribeDAtaResponse = await appDataResponse.data + + console.log('appData ==> ', appData) + + const runAppResponse = await fetch( + `https://api.wordware.ai/v1alpha/apps/masterbots/${webSearchFlow.id}/${appData.version}/runs/wait`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${WORDWARE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inputs: { + query + } + }) + } + ) + + if ( + runAppResponse.status >= 400 || + !runAppResponse.ok || + !runAppResponse.body + ) { + console.error('Error running app: ', runAppResponse) + if (runAppResponse.status >= 500) { + throw new Error( + 'Internal Server Error while fetching app data. Please try again later.' + ) + } + throw new Error('Failed to authenticate for the app. Please try again.') + } + + const response = await runAppResponse.json() + // ! error TS1501: This regular expression flag is only available when targeting 'es2018' or later. + // const jsonRegex = /data:\s*({.*?})(?=\s*data:|\s*event:|$)/gs + // ? Changing target not working. + // TODO: Check typescript config... + const jsonRegex = /data:\s*({.*?})(?=\s*data:|\s*event:|$)/g + + console.log( + '[SERVER] Web Search Response web search status --> ', + response.status + ) + console.log( + '[SERVER] Web Search Response web search outputs --> ', + response.outputs['web search'] + ) + + if (response.status !== 'COMPLETE') { + throw new Error('Web Search could not be completed.') + } + + if (!response.outputs['web search']?.output) { + throw new Error('No output given. Web search could not be completed') + } + + return `${response.outputs['web search'].output} + + ## EXAMPLE: + + **Resume:** + Brewers: 9 + Dodgers: 2 + + **Summary** + Yelich, Perkins power Brewers to 9-2 victory over Dodgers and avoid being swept in weekend series. — Christian Yelich and Blake Perkins both homered, had three hits and drove in three runs as the Milwaukee Brewers beat the Los Angeles Dodgers 9-2 Sunday to snap a seven-game losing streak at Dodger Stadium. + + **Homeruns:** + Yelich + + **Winning Pitcher:** + J. Junis + + **Sources**: + + 1. [https://website1.com/](https://website1.com/) + 2. [https://website2.com/](https://website2.com/)` + } catch (error) { + console.error('Error fetching app data: ', error) + throw error + } +} diff --git a/apps/masterbots.ai/app/api/chat/actions/actions.tsx b/apps/masterbots.ai/app/actions/ai-main-call.ts similarity index 57% rename from apps/masterbots.ai/app/api/chat/actions/actions.tsx rename to apps/masterbots.ai/app/actions/ai-main-call.ts index 37f24c08..0aaa9b60 100644 --- a/apps/masterbots.ai/app/api/chat/actions/actions.tsx +++ b/apps/masterbots.ai/app/actions/ai-main-call.ts @@ -1,269 +1,282 @@ -"use server"; +'use server' -import { AIModels } from "@/app/api/chat/models/models"; +import { AIModels } from '@/app/api/chat/models/models' import { createChatbotMetadataPrompt, createImprovementPrompt, - setDefaultPrompt, -} from "@/lib/constants/prompts"; + setDefaultPrompt +} from '@/lib/constants/prompts' import { + cleanResult, convertToCoreMessages, - setStreamerPayload, -} from "@/lib/helpers/ai-helpers"; -import { fetchChatbotMetadata } from "@/services/hasura"; + setStreamerPayload +} from '@/lib/helpers/ai-helpers' +import { aiTools } from '@/lib/helpers/ai-schemas' +import { fetchChatbotMetadata } from '@/services/hasura' import type { AiClientType, ChatbotMetadataHeaders, CleanPromptResult, - JSONResponseStream, -} from "@/types/types"; -import { createAnthropic } from "@ai-sdk/anthropic"; -import { createOpenAI } from "@ai-sdk/openai"; -import { streamText } from "ai"; -import type { ChatCompletionMessageParam } from "openai/resources"; + JSONResponseStream +} from '@/types/types' +import { createAnthropic } from '@ai-sdk/anthropic' +import { createOpenAI } from '@ai-sdk/openai' +import { streamText } from 'ai' +import type { ChatCompletionMessageParam } from 'openai/resources' //* this function is used to create a client for the OpenAI API const initializeOpenAi = createOpenAI({ apiKey: process.env.OPENAI_API_KEY, - compatibility: "strict", -}); + compatibility: 'strict' +}) const initializeAnthropic = createAnthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, -}); + apiKey: process.env.ANTHROPIC_API_KEY +}) //* Perplexity API uses openai-sdk with compatible mode and a different base URL export async function initializePerplexity(apiKey: string) { if (!apiKey) { throw new Error( - "PERPLEXITY_API_KEY is not defined in environment variables", - ); + 'PERPLEXITY_API_KEY is not defined in environment variables' + ) } return await createOpenAI({ apiKey, - baseURL: "https://api.perplexity.ai", - compatibility: "compatible", - }); + baseURL: 'https://api.perplexity.ai', + compatibility: 'compatible' + }) } // * This function improves the message using the AI export async function improveMessage( content: string, clientType: AiClientType, - model: string, + model: string ): Promise { - const messageImprovementPrompt = createImprovementPrompt(content); + const messageImprovementPrompt = createImprovementPrompt(content) try { const result = await processWithAi( messageImprovementPrompt, clientType, - model, - ); - const cleanedResult = cleanResult(result); + model + ) + const cleanedResult = cleanResult(result) if ( - isInvalidResult(cleanedResult.translatedText || cleanedResult.improvedText, content) && + isInvalidResult( + cleanedResult.translatedText || cleanedResult.improvedText, + content + ) && cleanedResult.improved ) { console.warn( - "AI did not modify the text or returned invalid result. Recursively executing improved prompt.", - ); - return await improveMessage(content, clientType, model); + 'AI did not modify the text or returned invalid result. Recursively executing improved prompt.' + ) + return await improveMessage(content, clientType, model) } - return cleanedResult; + return cleanedResult } catch (error) { const originalText = handleImprovementError( error, content, clientType, - model, - ); - return setDefaultPrompt(originalText); + model + ) + return setDefaultPrompt(originalText) } } export async function subtractChatbotMetadataLabels( metadataHeaders: ChatbotMetadataHeaders, userPrompt: string, - clientType: AiClientType, + clientType: AiClientType ) { - const chatbotMetadata = await fetchChatbotMetadata(metadataHeaders); + const chatbotMetadata = await fetchChatbotMetadata(metadataHeaders) if (!chatbotMetadata) { console.error( - "Chatbot metadata not found. Generating response without them.", - ); - return setDefaultPrompt(userPrompt); + 'Chatbot metadata not found. Generating response without them.' + ) + return setDefaultPrompt(userPrompt) } const prompt = createChatbotMetadataPrompt( metadataHeaders, chatbotMetadata, - userPrompt, - ); - const response = await processWithAi(prompt, clientType, AIModels.Default); + userPrompt + ) + const response = await processWithAi(prompt, clientType, AIModels.Default) - return cleanResult(response); + return cleanResult(response) } // * This function process the AI response and return the cleaned result -async function processWithAi( +export async function processWithAi( prompt: string, clientType: AiClientType, - model: string, + model: string ): Promise { try { const messages = [ - { role: "user", content: prompt }, - ] as ChatCompletionMessageParam[]; - const processedMessages = setStreamerPayload(clientType, messages); + { role: 'user', content: prompt } + ] as ChatCompletionMessageParam[] + const processedMessages = setStreamerPayload(clientType, messages) const response = await createResponseStream(clientType, { model: AIModels.Default, - messages: processedMessages, - } as any); + messages: processedMessages + } as any) if (!response.body) { - throw new Error("Response body is null"); + throw new Error('Response body is null') } if (response.status !== 200) { - const errorText = await response.text(); + const errorText = await response.text() throw new Error( - `API responded with status ${response.status}: ${errorText} `, - ); + `API responded with status ${response.status}: ${errorText} ` + ) } - const result = await readStreamResponse(response.body); - return result; + const result = await readStreamResponse(response.body) + return result } catch (error) { - console.error("Error in processWithAI:", error); - throw error; + console.error('Error in processWithAI:', error) + throw error } } // * This function reads the AI response and return the cleaned result async function readStreamResponse(body: ReadableStream): Promise { - const reader = body.getReader(); - let accumulatedResult = ""; + const reader = body.getReader() + let accumulatedResult = '' while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = new TextDecoder().decode(value); - accumulatedResult += chunk; + const { done, value } = await reader.read() + if (done) break + const chunk = new TextDecoder().decode(value) + accumulatedResult += chunk } - let result = ""; - const parts = accumulatedResult.split("\n"); + let result = '' + const parts = accumulatedResult.split('\n') for (const part of parts) { - const match = part.match(/^0:"(.*)"$/); + const match = part.match(/^0:"(.*)"$/) if (match) { - result += match[1]; + result += match[1] } } - return result; -} - -function cleanResult(result: string): CleanPromptResult { - const cleanedResult = result - .trim() - .replace(/\{\n/g, "{") - .replace(/\n\}/g, "}") - .replace(/\\"/g, '"'); - // * Using template string to avoid parsing errors with ' and " special characters... - return JSON.parse(`${cleanedResult} `); + return result } function isInvalidResult(result: string, originalContent: string): boolean { return ( !result || - result.includes("Original message:") || - result.toLowerCase() === originalContent.toLowerCase() - ); + result.includes('Original message:') || + result === originalContent + ) } function handleImprovementError( error: any, originalContent: string, clientType?: AiClientType, - model?: string, + model?: string ): string { - console.error("Error in improvement process:", error); - return originalContent; + console.error('Error in improvement process:', error) + return originalContent } //* Create a response stream based on the client model type export async function createResponseStream( clientType: AiClientType, json: JSONResponseStream, - req?: Request, + req?: Request ) { - const { model, messages: rawMessages, previewToken } = json; - const messages = setStreamerPayload(clientType, rawMessages); + const { model, messages: rawMessages, previewToken, webSearch } = json + const messages = setStreamerPayload(clientType, rawMessages) + + const tools: Partial = { + // ? Temp disabling ICL as tool. Using direct ICL integration to main prompt instead. Might be enabled later. + // chatbotMetadataExamples: aiTools.chatbotMetadataExamples + } + + console.log('[SERVER] webSearch', webSearch) + + if (webSearch) tools.webSearch = aiTools.webSearch try { - let responseStream: ReadableStream; + let responseStream: ReadableStream switch (clientType) { - case "OpenAI": { - const openaiModel = initializeOpenAi(model); + case 'OpenAI': { + const openaiModel = initializeOpenAi(model) const coreMessages = convertToCoreMessages( - messages as ChatCompletionMessageParam[], - ); + messages as ChatCompletionMessageParam[] + ) const response = await streamText({ model: openaiModel, messages: coreMessages, temperature: 0.4, - }); - responseStream = response.toDataStreamResponse().body as ReadableStream; - break; + tools, + maxRetries: 2, + maxToolRoundtrips: 1 + }) + responseStream = response.toDataStreamResponse().body as ReadableStream + break } - case "Anthropic": { + case 'Anthropic': { const anthropicModel = initializeAnthropic(model, { - cacheControl: true, - }); + cacheControl: true + }) const coreMessages = convertToCoreMessages( - messages as ChatCompletionMessageParam[], - ); + messages as ChatCompletionMessageParam[] + ) const response = await streamText({ model: anthropicModel, messages: coreMessages, temperature: 0.3, maxTokens: 300, - }); - responseStream = response.toDataStreamResponse().body as ReadableStream; - break; + tools, + maxRetries: 2, + maxToolRoundtrips: 1 + }) + responseStream = response.toDataStreamResponse().body as ReadableStream + break } - case "Perplexity": { + case 'Perplexity': { const perplexity = await initializePerplexity( - previewToken || (process.env.PERPLEXITY_API_KEY as string), - ); - const perplexityModel = perplexity(model); + previewToken || (process.env.PERPLEXITY_API_KEY as string) + ) + const perplexityModel = perplexity(model) const coreMessages = convertToCoreMessages( - messages as ChatCompletionMessageParam[], - ); + messages as ChatCompletionMessageParam[] + ) const response = await streamText({ model: perplexityModel, messages: coreMessages, temperature: 0.3, maxTokens: 1000, - }); - responseStream = response.toDataStreamResponse().body as ReadableStream; - break; + tools, + maxRetries: 2, + maxToolRoundtrips: 1 + }) + responseStream = response.toDataStreamResponse().body as ReadableStream + break } default: - throw new Error("Unsupported client type"); + throw new Error('Unsupported client type') } return new Response(responseStream, { - headers: { "Content-Type": "text/event-stream" }, - }); + headers: { 'Content-Type': 'text/event-stream' } + }) } catch (error) { - console.error("Error in createResponseStream:", error); - throw error; + console.error('Error in createResponseStream:', error) + throw error } } diff --git a/apps/masterbots.ai/app/actions/dub-co.ts b/apps/masterbots.ai/app/actions/dub-co.ts new file mode 100644 index 00000000..f1072c3b --- /dev/null +++ b/apps/masterbots.ai/app/actions/dub-co.ts @@ -0,0 +1,53 @@ +'use server' + +import axios from 'axios' +import { cookies } from 'next/headers' + +// generate dub.co links +export async function generateShortLink(path: string) { + const cookieStorage = cookies() + try { + const resolved: DubShareLinkResponse = await axios + .post( + `https://api.dub.co/links?workspaceId=${process.env.DUB_WORKSPACE_ID}`, + { + domain: 'mbots.to', + url: `https://masterbots.ai${path}` + }, + { + headers: { + Authorization: `Bearer ${process.env.DUB_API_KEY}`, + 'Content-Type': 'application/json' + } + } + ) + .then(res => res.data) + + if (!resolved) throw new Error('Failed to generate short link') + + return { + data: { + key: resolved.key, + shortLink: resolved.shortLink, + qrCode: resolved.qrCode + }, + error: null + } + } catch (error) { + console.log(path + 'Failed to generate short link: ==> ', error) + return { + data: null + } + } +} + +export interface DubShareLinkResponse { + key: string + shortLink: string + qrCode: string +} + +export type ActionState = { + data?: string + error?: string +} diff --git a/apps/masterbots.ai/app/actions/index.ts b/apps/masterbots.ai/app/actions/index.ts new file mode 100644 index 00000000..84ecc104 --- /dev/null +++ b/apps/masterbots.ai/app/actions/index.ts @@ -0,0 +1,4 @@ +export * from './ai-executers' +export * from './ai-main-call' +export * from './dub-co' +export * from './subscriptions' diff --git a/apps/masterbots.ai/app/actions.ts b/apps/masterbots.ai/app/actions/subscriptions.ts similarity index 94% rename from apps/masterbots.ai/app/actions.ts rename to apps/masterbots.ai/app/actions/subscriptions.ts index 25d60389..b424be75 100644 --- a/apps/masterbots.ai/app/actions.ts +++ b/apps/masterbots.ai/app/actions/subscriptions.ts @@ -1,3 +1,5 @@ +'use server' + import { parseWordwareResponse } from '@/components/shared/wordware-chat' import { Card, @@ -111,9 +113,7 @@ export async function getPromptDetails(promptId: string) { throw new Error('Prompt ID is required') } - const response = await fetch( - `/api/wordware/describe?promptId=${promptId}` - ) + const response = await fetch(`/api/wordware/describe?promptId=${promptId}`) data = await response.json() if (!response.ok) { throw new Error(data.error || 'Failed to fetch prompt details') @@ -133,7 +133,13 @@ export async function getPromptDetails(promptId: string) { } } -export async function runWordWarePrompt({ promptId, inputs }: { promptId: string, inputs: Record }) { +export async function runWordWarePrompt({ + promptId, + inputs +}: { + promptId: string + inputs: Record +}) { let fullResponse = '' let error = null diff --git a/apps/masterbots.ai/app/api/admin/admin-actions.ts b/apps/masterbots.ai/app/api/admin/admin-actions.ts new file mode 100644 index 00000000..a56e3b68 --- /dev/null +++ b/apps/masterbots.ai/app/api/admin/admin-actions.ts @@ -0,0 +1,51 @@ +import { z } from 'zod' +import * as adminService from '@/services/admin/admin.service' + +type ActionHandler = (data: T) => Promise + +interface AdminAction { + schema: z.ZodType + handler: ActionHandler +} + +export const adminActions = { + blockUser: { + schema: z.object({ + userId: z.string().uuid() + }), + handler: ({ userId }: { userId: string }) => adminService.blockUser(userId) + }, + + unblockUser: { + schema: z.object({ + userId: z.string().uuid() + }), + handler: ({ userId }: { userId: string }) => + adminService.unblockUser(userId) + }, + + updateSubscription: { + schema: z.object({ + userId: z.string().uuid(), + subscriptionId: z.string().nullable() + }), + handler: ({ + userId, + subscriptionId + }: { + userId: string + subscriptionId: string | null + }) => adminService.updateSubscription(userId, subscriptionId) + }, + + setFreeMonth: { + schema: z.object({ + userId: z.string().uuid() + }), + handler: ({ userId }: { userId: string }) => + adminService.setFreeMonth(userId) + } + // biome-ignore lint/suspicious/noExplicitAny: +} satisfies Record> + +export type AdminActionType = keyof typeof adminActions diff --git a/apps/masterbots.ai/app/api/admin/route.ts b/apps/masterbots.ai/app/api/admin/route.ts new file mode 100644 index 00000000..1935f8e5 --- /dev/null +++ b/apps/masterbots.ai/app/api/admin/route.ts @@ -0,0 +1,116 @@ +import { adminActions, type AdminActionType } from './admin-actions'; +import { ZodError } from 'zod'; + +type ErrorResponse = { + error: string; + details?: unknown; + code?: string; +}; + +function handleError(error: unknown): Response { + console.error('Admin API Error:', error); + + if (error instanceof ZodError) { + return Response.json( + { + error: 'Invalid request data', + details: error.errors, + code: 'VALIDATION_ERROR' + } satisfies ErrorResponse, + { status: 400 } + ); + } + + if (error instanceof Error) { + // * Handle specific error types + if (error.message.includes('Unauthorized')) { + return Response.json( + { + error: 'Unauthorized access', + code: 'UNAUTHORIZED' + } satisfies ErrorResponse, + { status: 401 } + ); + } + + if (error.message.includes('User not found')) { + return Response.json( + { + error: error.message, + code: 'NOT_FOUND' + } satisfies ErrorResponse, + { status: 404 } + ); + } + + if (error.message.includes('Cannot grant free month')) { + return Response.json( + { + error: error.message, + code: 'BUSINESS_RULE_VIOLATION' + } satisfies ErrorResponse, + { status: 400 } + ); + } + + return Response.json( + { + error: error.message, + code: 'INTERNAL_ERROR' + } satisfies ErrorResponse, + { status: 500 } + ); + } + + return Response.json( + { + error: 'An unexpected error occurred', + code: 'UNKNOWN_ERROR' + } satisfies ErrorResponse, + { status: 500 } + ); +} + +function isValidAction(action: unknown): action is AdminActionType { + return typeof action === 'string' && action in adminActions; +} + +export async function POST(req: Request) { + try { + const { action, payload } = await req.json().catch(() => ({})); + + if (!action || !isValidAction(action)) { + return Response.json( + { + error: 'Invalid or missing action', + code: 'INVALID_ACTION' + } satisfies ErrorResponse, + { status: 400 } + ); + } + + const actionConfig = adminActions[action]; + const validationResult = actionConfig.schema.safeParse(payload); + + if (!validationResult.success) { + return Response.json( + { + error: 'Invalid payload', + details: validationResult.error.errors, + code: 'INVALID_PAYLOAD' + } satisfies ErrorResponse, + { status: 400 } + ); + } + + // biome-ignore lint/suspicious/noExplicitAny: validated by zod schema + const result = await actionConfig.handler(validationResult.data as any); + + return Response.json({ + data: result, + success: true + }); + } catch (error) { + return handleError(error); + } +} \ No newline at end of file diff --git a/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts b/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts index 14a915f8..3569102a 100644 --- a/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts +++ b/apps/masterbots.ai/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,5 @@ +'use server' + import { authOptions } from '@/auth' import NextAuth from 'next-auth' diff --git a/apps/masterbots.ai/app/api/auth/forgot-password/route.ts b/apps/masterbots.ai/app/api/auth/forgot-password/route.ts index 23dfbd2f..a729480c 100644 --- a/apps/masterbots.ai/app/api/auth/forgot-password/route.ts +++ b/apps/masterbots.ai/app/api/auth/forgot-password/route.ts @@ -1,7 +1,12 @@ +'use server' + +import { sendPasswordResetEmail } from '@/lib/email' import { getHasuraClient } from 'mb-lib' import { type NextRequest, NextResponse } from 'next/server' import crypto from 'node:crypto' -import { sendPasswordResetEmail } from '@/lib/email' + +// * Add explicit runtime configuration +// export const runtime = 'edge' export async function POST(req: NextRequest) { const { email } = await req.json() diff --git a/apps/masterbots.ai/app/api/auth/reset-password/route.ts b/apps/masterbots.ai/app/api/auth/reset-password/route.ts index 2e342b8a..678f73be 100644 --- a/apps/masterbots.ai/app/api/auth/reset-password/route.ts +++ b/apps/masterbots.ai/app/api/auth/reset-password/route.ts @@ -1,6 +1,8 @@ -import { getHasuraClient } from 'mb-lib' -import { type NextRequest, NextResponse } from 'next/server' +'use server' + import bcryptjs from 'bcryptjs' +import { getHasuraClient } from 'mb-lib' +import { type NextRequest, NextResponse } from 'next/server' export async function POST(req: NextRequest) { const { token, password } = await req.json() @@ -93,7 +95,7 @@ export async function POST(req: NextRequest) { { message: 'Password reset successful' }, { status: 200 } ) - // biome-ignore lint/style/noUselessElse: + // biome-ignore lint/style/noUselessElse: } else { console.error('Unexpected result') throw new Error('Failed to update user password or delete token') diff --git a/apps/masterbots.ai/app/api/auth/signup/route.ts b/apps/masterbots.ai/app/api/auth/signup/route.ts index 63cd4562..55b3a58b 100644 --- a/apps/masterbots.ai/app/api/auth/signup/route.ts +++ b/apps/masterbots.ai/app/api/auth/signup/route.ts @@ -1,10 +1,15 @@ -import { generateUsername } from '@/lib/username' +'use server' + import { sendEmailVerification } from '@/lib/email' +import { generateUsername } from '@/lib/username' import bcryptjs from 'bcryptjs' import { getHasuraClient, toSlug } from 'mb-lib' import { type NextRequest, NextResponse } from 'next/server' import crypto from 'node:crypto' +// * Add explicit runtime configuration +// export const runtime = 'edge' + export async function POST(req: NextRequest) { const { email, password, username } = await req.json() diff --git a/apps/masterbots.ai/app/api/auth/verify-email/route.ts b/apps/masterbots.ai/app/api/auth/verify-email/route.ts index f68dbfc9..b5eef6d9 100644 --- a/apps/masterbots.ai/app/api/auth/verify-email/route.ts +++ b/apps/masterbots.ai/app/api/auth/verify-email/route.ts @@ -1,8 +1,10 @@ -import { type NextRequest, NextResponse } from 'next/server' +'use server' + import { getHasuraClient } from 'mb-lib' +import { type NextRequest, NextResponse } from 'next/server' // * Add explicit runtime configuration -export const runtime = 'edge' +// export const runtime = 'edge' export async function POST(req: NextRequest) { const { token } = await req.json() diff --git a/apps/masterbots.ai/app/api/chat/route.ts b/apps/masterbots.ai/app/api/chat/route.ts index 79f6f232..d45d641d 100644 --- a/apps/masterbots.ai/app/api/chat/route.ts +++ b/apps/masterbots.ai/app/api/chat/route.ts @@ -1,8 +1,8 @@ -import { createResponseStream } from '@/app/api/chat/actions/actions' +import { createResponseStream } from '@/app/actions' import { getModelClientType } from '@/lib/helpers/ai-helpers' import { NextResponse } from 'next/server' -export const runtime = 'edge' +// export const runtime = 'edge' export async function POST(req: Request) { try { @@ -16,9 +16,8 @@ export async function POST(req: Request) { const clientModel = getModelClientType(model) const stream = await createResponseStream(clientModel, json, req) - - return stream + return stream } catch (error) { console.error('Error in chat API route:', error) return NextResponse.json( @@ -26,4 +25,4 @@ export async function POST(req: Request) { { status: 500 } ) } -} \ No newline at end of file +} diff --git a/apps/masterbots.ai/app/api/cron/unverified-users/route.ts b/apps/masterbots.ai/app/api/cron/unverified-users/route.ts index 7929c21f..1ae82e64 100644 --- a/apps/masterbots.ai/app/api/cron/unverified-users/route.ts +++ b/apps/masterbots.ai/app/api/cron/unverified-users/route.ts @@ -1,6 +1,6 @@ -import { NextResponse } from 'next/server' -import { getHasuraClient } from 'mb-lib' import { sendEmailVerification } from '@/lib/email' +import { getHasuraClient } from 'mb-lib' +import { NextResponse } from 'next/server' import crypto from 'node:crypto' export async function GET() { diff --git a/apps/masterbots.ai/app/api/og/route.tsx b/apps/masterbots.ai/app/api/og/route.tsx index e2c9c3bb..a9312a62 100644 --- a/apps/masterbots.ai/app/api/og/route.tsx +++ b/apps/masterbots.ai/app/api/og/route.tsx @@ -5,7 +5,8 @@ import { NextRequest } from 'next/server' import { getThread } from '@/services/hasura' import '@/app/globals.css' import OGImage from '@/components/shared/og-image' -export const runtime = 'edge' + +// export const runtime = 'edge' export async function GET(req: NextRequest) { try { diff --git a/apps/masterbots.ai/app/api/payment/sumarize/route.tsx b/apps/masterbots.ai/app/api/payment/sumarize/route.tsx index b6716cc0..70d97816 100644 --- a/apps/masterbots.ai/app/api/payment/sumarize/route.tsx +++ b/apps/masterbots.ai/app/api/payment/sumarize/route.tsx @@ -7,7 +7,7 @@ const stripeSecretKey = process.env.STRIPE_SECRET_KEY; } const stripe = require('stripe')(stripeSecretKey); -export const runtime = 'edge' +// export const runtime = 'edge' export async function POST(req: NextRequest) { try { diff --git a/apps/masterbots.ai/app/api/wordware/describe/route.ts b/apps/masterbots.ai/app/api/wordware/describe/route.ts index 267acf98..bdd4d059 100644 --- a/apps/masterbots.ai/app/api/wordware/describe/route.ts +++ b/apps/masterbots.ai/app/api/wordware/describe/route.ts @@ -1,14 +1,15 @@ +import { WordWareDescribeDAtaResponse } from '@/types/wordware-flows.types' import { NextResponse } from 'next/server' export async function GET(request: Request) { const { searchParams } = new URL(request.url) const promptId = searchParams.get('promptId') - const apiKey = process.env.WORDWARE_API_KEY + const API_KEY = process.env.WORDWARE_API_KEY - if (!apiKey) { + if (!API_KEY) { console.error('Wordware API key is not set') return NextResponse.json( - { error: 'Internal server error' }, + { error: 'Internal Server Error' }, { status: 500 } ) } @@ -22,17 +23,18 @@ export async function GET(request: Request) { try { const response = await fetch( - `https://app.wordware.ai/api/prompt/${promptId}/describe`, + `https://api.wordware.ai/v1alpha/apps/masterbots/${promptId}`, { headers: { - Authorization: `Bearer ${apiKey}` + Authorization: `Bearer ${API_KEY}`, + 'Content-Type': 'application/json' } } ) const contentType = response.headers.get('content-type') if (contentType && contentType.indexOf('application/json') !== -1) { - const data = await response.json() + const data: WordWareDescribeDAtaResponse = await response.json() return NextResponse.json(data, { status: response.status }) } else { const text = await response.text() diff --git a/apps/masterbots.ai/app/api/wordware/run/route.ts b/apps/masterbots.ai/app/api/wordware/run/route.ts index 6c626890..71ca30e4 100644 --- a/apps/masterbots.ai/app/api/wordware/run/route.ts +++ b/apps/masterbots.ai/app/api/wordware/run/route.ts @@ -1,22 +1,24 @@ +import { streamAndValidateResponse } from '@/lib/helpers/ai-streams' import { NextResponse } from 'next/server' -export const runtime = 'edge' +// export const runtime = 'edge' export async function POST(req: Request) { const API_KEY = process.env.WORDWARE_API_KEY if (!API_KEY) { + console.error('Wordware API key is not set') return NextResponse.json( - { error: 'Wordware API key is not set' }, + { error: 'Internal Server Error' }, { status: 500 } ) } try { - const { promptId, inputs } = await req.json() + const { promptId, inputs, appVersion } = await req.json() const response = await fetch( - `https://app.wordware.ai/api/prompt/${promptId}/run`, + `https://api.wordware.ai/v1alpha/apps/masterbots/${promptId}/${appVersion}/runs/stream`, { method: 'POST', headers: { @@ -51,98 +53,3 @@ export async function POST(req: Request) { ) } } - -async function streamAndValidateResponse( - readableStream: ReadableStream | null, - writableStream: WritableStream -) { - if (!readableStream) { - throw new Error('No readable stream available') - } - - const reader = readableStream.getReader() - const writer = writableStream.getWriter() - const decoder = new TextDecoder() - const encoder = new TextEncoder() - - let buffer = '' - const jsonRegex = /\{(?:[^{}]|(?:\{(?:[^{}]|(?:\{[^{}]*\}))*\}))*\}/g - - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - - buffer += decoder.decode(value, { stream: true }) - - let match - let lastIndex = 0 - while ((match = jsonRegex.exec(buffer)) !== null) { - const jsonStr = match[0] - const validatedJson = validateAndSanitizeJson(jsonStr) - if (validatedJson) { - await writer.write(encoder.encode(validatedJson + '\n')) - } - lastIndex = jsonRegex.lastIndex - } - - //* Keeping the unmatched part in the buffer - buffer = buffer.slice(lastIndex) - - //? Buffer is getting too large warning - if (buffer.length > 10000) { - console.warn('Buffer overflow, clearing unmatched data') - buffer = '' - } - - await new Promise(resolve => setTimeout(resolve, 10)) - } - - //* Process any remaining data in the buffer - if (buffer.length > 0) { - const validatedJson = validateAndSanitizeJson(buffer) - if (validatedJson) { - await writer.write(encoder.encode(validatedJson + '\n')) - } - } - } finally { - reader.releaseLock() - writer.close() - } -} - -function validateAndSanitizeJson(jsonStr: string): string | null { - try { - const parsed = JSON.parse(jsonStr) - - //* Validate the structure of the parsed JSON - if (typeof parsed === 'object' && parsed !== null) { - if ( - parsed.type === 'chunk' && - typeof parsed.value === 'object' && - parsed.value !== null - ) { - //* Sanitize the 'value' field if it's a string - if (typeof parsed.value.value === 'string') { - parsed.value.value = sanitizeString(parsed.value.value) - } - } else if (parsed.type === 'generation' || parsed.type === 'prompt') { - //* These types are allowed, but we don't modify their content - } else { - return null - } - return JSON.stringify(parsed) - } - } catch (e) { - console.error('Invalid JSON:', e) - return null - } - return null -} - -function sanitizeString(str: string): string { - return str - .replace(/[<>]/g, '') // Remove < and > to prevent HTML injection - .replace(/javascript:/gi, '') // Remove javascript: to prevent JavaScript injection - .replace(/on\w+=/gi, '') // Remove event handlers -} diff --git a/apps/masterbots.ai/app/auth/forgot-password/page.tsx b/apps/masterbots.ai/app/auth/forgot-password/page.tsx index 4f1beebe..8c53ddee 100644 --- a/apps/masterbots.ai/app/auth/forgot-password/page.tsx +++ b/apps/masterbots.ai/app/auth/forgot-password/page.tsx @@ -1,10 +1,43 @@ +'use client' + import ForgotPasswordForm from '@/components/auth/forgot-password-form' +import { motion } from 'framer-motion' +import Image from 'next/image' export default function ForgotPasswordPage() { return ( -
-

Forgot Password

- -
+ + {/* Logo container with animation */} + + Masterbots Logo + + + {/* Form container with animation and enhanced responsiveness */} + +

Forgot Password

+ +
+
) -} \ No newline at end of file +} diff --git a/apps/masterbots.ai/app/auth/reset-password/page.tsx b/apps/masterbots.ai/app/auth/reset-password/page.tsx index c8a08fb5..e6d62f64 100644 --- a/apps/masterbots.ai/app/auth/reset-password/page.tsx +++ b/apps/masterbots.ai/app/auth/reset-password/page.tsx @@ -1,5 +1,9 @@ -import { Suspense } from 'react' +'use client' + import ResetPasswordForm from '@/components/auth/reset-password-form' +import { motion } from 'framer-motion' +import Image from 'next/image' +import { Suspense } from 'react' export default function ResetPasswordPage({ searchParams @@ -7,11 +11,40 @@ export default function ResetPasswordPage({ searchParams: { token: string } }) { return ( -
-

Reset Password

- Loading...
}> - - - + + {/* Logo container with animation */} + + Masterbots Logo + + + {/* Form container with animation and enhanced responsiveness */} + +

Reset Password

+ Loading...}> + + +
+
) -} \ No newline at end of file +} diff --git a/apps/masterbots.ai/app/auth/signin/page.tsx b/apps/masterbots.ai/app/auth/signin/page.tsx index a9fc258e..91b71f24 100644 --- a/apps/masterbots.ai/app/auth/signin/page.tsx +++ b/apps/masterbots.ai/app/auth/signin/page.tsx @@ -1,8 +1,10 @@ +// app/auth/signin/page.tsx + 'use client' -import Link from 'next/link' -import Image from 'next/image' import SignInForm from '@/components/auth/signin-form' +import Image from 'next/image' +import Link from 'next/link' import { useSearchParams } from 'next/navigation' export default function SignInPage() { @@ -10,36 +12,40 @@ export default function SignInPage() { const verified = searchParams.get('verified') return ( -
-
+
+
Masterbots Logo
-
+ +
{verified && (

Email verified successfully!

Please sign in to access your account.

)} +
-

+

Enter your email and password to access your account.

+ +
Don't have an account?{' '} - + Sign up
diff --git a/apps/masterbots.ai/app/auth/signup/page.tsx b/apps/masterbots.ai/app/auth/signup/page.tsx index bb428f0b..1469dac7 100644 --- a/apps/masterbots.ai/app/auth/signup/page.tsx +++ b/apps/masterbots.ai/app/auth/signup/page.tsx @@ -1,29 +1,60 @@ +'use client' + import Image from 'next/image' import SignUpForm from '@/components/auth/signup-form' +import { motion } from 'framer-motion' export default function SignUpPage() { return ( -
-
+ {/* Logo container with animation */} + Masterbots Logo -
-
+ + + {/* Form container with animation and enhanced responsiveness */} +
-

+

Enter your email and password to create your account.

+ -
-
+ + {/* Optional: Add terms and conditions notice */} +

+ By creating an account, you agree to our{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + +

+ + ) } \ No newline at end of file diff --git a/apps/masterbots.ai/app/b/[id]/[threadId]/sitemap.ts b/apps/masterbots.ai/app/b/[id]/[threadId]/sitemap.ts index 07bb28b7..5bdb5d62 100644 --- a/apps/masterbots.ai/app/b/[id]/[threadId]/sitemap.ts +++ b/apps/masterbots.ai/app/b/[id]/[threadId]/sitemap.ts @@ -1,14 +1,13 @@ -import { botNames } from '@/lib/bots-names' +import { botNames } from '@/lib/constants/bots-names' import { getKeyByValue } from '@/lib/utils' import { getThreadsWithoutJWT } from '@/services/hasura' -import { toSlug } from 'mb-lib' -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' export default async function sitemap(): Promise { const threads = await getThreadsWithoutJWT() return threads.map(thread => ({ - url: `${process.env.VERCEL_URL}/b/${getKeyByValue(botNames, thread.chatbot.name)}/${thread.threadId}`, + url: `${process.env.VERCEL_URL}/b/${getKeyByValue(botNames, thread.chatbot?.name)}/${thread.threadId}`, lastModified: thread.updatedAt })) } diff --git a/apps/masterbots.ai/app/b/[id]/layout.tsx b/apps/masterbots.ai/app/b/[id]/layout.tsx index 2cedc8bc..ca62499b 100644 --- a/apps/masterbots.ai/app/b/[id]/layout.tsx +++ b/apps/masterbots.ai/app/b/[id]/layout.tsx @@ -1,5 +1,5 @@ -import { BrowseProvider } from '@/lib/hooks/use-browse' import FooterCT from '@/components/layout/footer/footer-ct' +import { BrowseProvider } from '@/lib/hooks/use-browse' import NextTopLoader from 'nextjs-toploader' interface BrowseLayoutProps { @@ -10,12 +10,12 @@ export default async function BrowseLayout({ children }: BrowseLayoutProps) { return ( -
-
+
+
{children} -
-
+ +
) } diff --git a/apps/masterbots.ai/app/b/[id]/page.tsx b/apps/masterbots.ai/app/b/[id]/page.tsx index 305b227a..ebc482c7 100644 --- a/apps/masterbots.ai/app/b/[id]/page.tsx +++ b/apps/masterbots.ai/app/b/[id]/page.tsx @@ -1,14 +1,14 @@ -import { getChatbot, getBrowseThreads } from '@/services/hasura' -import { botNames } from '@/lib/bots-names' import BrowseChatbotDetails from '@/components/routes/browse/browse-chatbot-details' import BrowseSpecificThreadList from '@/components/routes/browse/browse-specific-thread-list' -import { Metadata } from 'next' +import { botNames } from '@/lib/constants/bots-names' import { generateMetadataFromSEO } from '@/lib/metadata' +import { getBrowseThreads, getChatbot } from '@/services/hasura' +import type { Metadata } from 'next' const PAGE_SIZE = 50 export default async function BotThreadsPage({ - params + params, }: { params: { id: string } }) { @@ -17,14 +17,14 @@ export default async function BotThreadsPage({ chatbot = await getChatbot({ chatbotName: botNames.get(params.id), jwt: '', - threads: true + threads: true, }) if (!chatbot) throw new Error(`Chatbot ${botNames.get(params.id)} not found`) // session will always be defined threads = await getBrowseThreads({ chatbotName: botNames.get(params.id), - limit: PAGE_SIZE + limit: PAGE_SIZE, }) return ( @@ -34,7 +34,7 @@ export default async function BotThreadsPage({ initialThreads={threads} PAGE_SIZE={PAGE_SIZE} query={{ - chatbotName: botNames.get(params.id) + chatbotName: botNames.get(params.id), }} pageType="bot" /> @@ -43,14 +43,14 @@ export default async function BotThreadsPage({ } export async function generateMetadata({ - params + params, }: { params: { id: string } }): Promise { const chatbot = await getChatbot({ chatbotName: botNames.get(params.id), jwt: '', - threads: true + threads: true, }) const seoData = { @@ -58,7 +58,7 @@ export async function generateMetadata({ description: chatbot?.description || '', ogType: 'website', ogImageUrl: chatbot?.avatar || '', - twitterCard: 'summary_large_image' + twitterCard: 'summary_large_image', } return generateMetadataFromSEO(seoData) diff --git a/apps/masterbots.ai/app/b/[id]/sitemap.ts b/apps/masterbots.ai/app/b/[id]/sitemap.ts index 517b7335..642fb4dc 100644 --- a/apps/masterbots.ai/app/b/[id]/sitemap.ts +++ b/apps/masterbots.ai/app/b/[id]/sitemap.ts @@ -1,8 +1,7 @@ -import { botNames } from '@/lib/bots-names' +import { botNames } from '@/lib/constants/bots-names' import { getKeyByValue } from '@/lib/utils' import { getChatbots } from '@/services/hasura' -import { toSlug } from 'mb-lib' -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' export default async function sitemap(): Promise { const chatbots = await getChatbots({}) diff --git a/apps/masterbots.ai/app/c/[category]/[chatbot]/page.tsx b/apps/masterbots.ai/app/c/[category]/[chatbot]/page.tsx index a17b111b..b2aae66d 100644 --- a/apps/masterbots.ai/app/c/[category]/[chatbot]/page.tsx +++ b/apps/masterbots.ai/app/c/[category]/[chatbot]/page.tsx @@ -1,12 +1,9 @@ import { authOptions } from "@/auth"; import { ChatChatbot } from "@/components/routes/chat/chat-chatbot"; import ThreadPanel from "@/components/routes/thread/thread-panel"; -import { formatSystemPrompts } from "@/lib/actions"; -import { botNames } from "@/lib/bots-names"; -import { setDefaultUserPreferencesPrompt } from "@/lib/constants/prompts"; +import { botNames } from "@/lib/constants/bots-names"; import { generateMetadataFromSEO } from "@/lib/metadata"; import { getChatbot, getThreads } from "@/services/hasura"; -import type { Message } from "ai"; import { isTokenExpired } from "mb-lib"; import type { Metadata } from "next"; import { getServerSession } from "next-auth"; @@ -23,16 +20,16 @@ export default async function BotThreadsPage({ // NOTE: maybe we should use same expiration time const jwt = session ? session.user?.hasuraJwt : null; if (!jwt) { - throw new Error("Session JWT is missing."); + console.error("Session JWT is missing."); } - if (isTokenExpired(jwt)) { + if (isTokenExpired(jwt as string)) { redirect(`/auth/signin`); } const chatbotName = botNames.get(params.chatbot); if (!chatbotName) { throw new Error(`Chatbot name for ${params.chatbot} not found`); } - const chatbot = await getChatbot({ chatbotName, jwt }); + const chatbot = await getChatbot({ chatbotName, jwt: jwt as string }); if (!chatbot) throw new Error(`Chatbot ${botNames.get(params.chatbot)} not found`); @@ -43,19 +40,8 @@ export default async function BotThreadsPage({ if (!userId) { throw new Error("User ID is missing."); } - const threads = await getThreads({ chatbotName, jwt, userId }); + const threads = await getThreads({ chatbotName, jwt: jwt as string, userId }); - // format all chatbot prompts as chatgpt 'system' messages - const chatbotSystemPrompts: Message[] = formatSystemPrompts(chatbot.prompts); - - const userPreferencesPrompts: Message[] = [ - setDefaultUserPreferencesPrompt(chatbot), - ]; - - // concatenate all message to pass it to chat component - const initialMessages: Message[] = chatbotSystemPrompts.concat( - userPreferencesPrompts, - ); return ( <> {" "} - + ); } diff --git a/apps/masterbots.ai/app/c/[category]/[chatbot]/sitemap.ts b/apps/masterbots.ai/app/c/[category]/[chatbot]/sitemap.ts index d5643131..e6b04d7b 100644 --- a/apps/masterbots.ai/app/c/[category]/[chatbot]/sitemap.ts +++ b/apps/masterbots.ai/app/c/[category]/[chatbot]/sitemap.ts @@ -1,11 +1,11 @@ -import { botNames } from '@/lib/bots-names' +import { botNames } from '@/lib/constants/bots-names' import { getKeyByValue } from '@/lib/utils' -import { getChatbots, getThreadsWithoutJWT } from '@/services/hasura' +import { getChatbots } from '@/services/hasura' import { toSlug } from 'mb-lib' -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' export default async function sitemap(): Promise { - const chatbots = await getChatbots({}) + const chatbots = await getChatbots({ limit: botNames.size }) return chatbots.map(chatbot => ({ url: `${process.env.VERCEL_URL}/c/${toSlug(chatbot.categories[0].category.name)}/${getKeyByValue(botNames, chatbot.name)}`, diff --git a/apps/masterbots.ai/app/c/[category]/page.tsx b/apps/masterbots.ai/app/c/[category]/page.tsx index 1512b8dd..1c2aa566 100644 --- a/apps/masterbots.ai/app/c/[category]/page.tsx +++ b/apps/masterbots.ai/app/c/[category]/page.tsx @@ -1,3 +1,4 @@ +import { authOptions } from '@/auth' import ChatThreadListPanel from '@/components/routes/chat/chat-thread-list-panel' import ThreadPanel from '@/components/routes/thread/thread-panel' import { generateMetadataFromSEO } from '@/lib/metadata' @@ -6,7 +7,6 @@ import { isTokenExpired, toSlug } from 'mb-lib' import { Metadata } from 'next' import { getServerSession } from 'next-auth' import { redirect } from 'next/navigation' -import { authOptions } from '@/auth' export default async function ChatCategoryPage({ params @@ -53,8 +53,7 @@ export async function generateMetadata({ const seoData = { title: category?.name || '', - description: - 'Please select one of the categories and a bot on the sidebar to start a conversation.', + description: `Please select a bot from the ${category?.name} category to start the conversation.`, ogType: 'website', ogImageUrl: '', twitterCard: 'summary' diff --git a/apps/masterbots.ai/app/c/layout.tsx b/apps/masterbots.ai/app/c/layout.tsx index 033e2ebc..223abe13 100644 --- a/apps/masterbots.ai/app/c/layout.tsx +++ b/apps/masterbots.ai/app/c/layout.tsx @@ -13,9 +13,7 @@ export default async function ChatLayout({ children }: ChatLayoutProps) { {children} -
- -
+ ) } diff --git a/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/page.tsx b/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/page.tsx index 802b6bb8..26905e2a 100644 --- a/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/page.tsx +++ b/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/page.tsx @@ -1,11 +1,8 @@ import { authOptions } from "@/auth"; import { ChatChatbot } from "@/components/routes/chat/chat-chatbot"; import ThreadPanel from "@/components/routes/thread/thread-panel"; -import { formatSystemPrompts } from "@/lib/actions"; -import { botNames } from "@/lib/bots-names"; -import { setDefaultUserPreferencesPrompt } from "@/lib/constants/prompts"; +import { botNames } from "@/lib/constants/bots-names"; import { getChatbot, getThreads } from "@/services/hasura"; -import type { Message } from "ai"; import { isTokenExpired } from "mb-lib"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; @@ -43,18 +40,6 @@ export default async function BrowseProBotPage({ } const threads = await getThreads({ chatbotName, jwt, userId }); - // format all chatbot prompts as chatgpt 'system' messages - const chatbotSystemPrompts: Message[] = formatSystemPrompts(chatbot.prompts); - - const userPreferencesPrompts: Message[] = [ - setDefaultUserPreferencesPrompt(chatbot), - ]; - - // concatenate all message to pass it to chat component - const initialMessages: Message[] = chatbotSystemPrompts.concat( - userPreferencesPrompts, - ); - return ( <> {" "} - + ); } diff --git a/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/sitemap.ts b/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/sitemap.ts index e1e2f3ff..4ddd95a0 100644 --- a/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/sitemap.ts +++ b/apps/masterbots.ai/app/c/p/[category]/[expertise]/[chatbot]/sitemap.ts @@ -1,8 +1,8 @@ -import { botNames } from '@/lib/bots-names' +import { botNames } from '@/lib/constants/bots-names' import { getKeyByValue } from '@/lib/utils' import { getChatbots } from '@/services/hasura' import { toSlug } from 'mb-lib' -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' export default async function sitemap(): Promise { const chatbots = await getChatbots({}) diff --git a/apps/masterbots.ai/app/c/p/layout.tsx b/apps/masterbots.ai/app/c/p/layout.tsx index f58bee6f..31ac74ec 100644 --- a/apps/masterbots.ai/app/c/p/layout.tsx +++ b/apps/masterbots.ai/app/c/p/layout.tsx @@ -1,5 +1,5 @@ -import { ChatLayoutSection } from '@/components/routes/chat/chat-layout-section' import FooterCT from '@/components/layout/footer/footer-ct' +import { ChatLayoutSection } from '@/components/routes/chat/chat-layout-section' import { appConfig } from 'mb-env' import { redirect } from 'next/navigation' import NextTopLoader from 'nextjs-toploader' @@ -9,7 +9,7 @@ interface ChatLayoutProps { } export default async function ChatLayout({ children }: ChatLayoutProps) { - if (!appConfig.devMode) { + if (!appConfig.features.devMode) { console.error('Navigation to Pro is disabled. No access to this page') redirect('/') } diff --git a/apps/masterbots.ai/app/globals.css b/apps/masterbots.ai/app/globals.css index d800f18f..34a12fe1 100644 --- a/apps/masterbots.ai/app/globals.css +++ b/apps/masterbots.ai/app/globals.css @@ -8,7 +8,7 @@ --foreground: 240 10% 3.9%; --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; + --muted-foreground: 240 3.8% 30%; --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; @@ -25,7 +25,7 @@ --secondary: 240 4.8% 95.9%; --secondary-foreground: 240 5.9% 10%; - --accent: 240 4.8% 95.9%; + --accent: 288 82% 50%; --accent-foreground: 0 0% 98%; --destructive: 0 84.2% 60.2%; @@ -46,18 +46,17 @@ --mirage: 217, 33%, 17%; --iron: 240, 6%, 90%; - --tertiary: 288, 82%, 50%; + --tertiary: 288, 82%, 50%; --tertiary-foreground: 0 0% 98%; - + --font-size-base: 1rem; } - .dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; + --muted-foreground: 240 5% 79%; --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; @@ -74,8 +73,8 @@ --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; - --accent: 240 3.7% 15.9%; - --accent-foreground: ; + --accent: 108 70% 66%; + --accent-foreground: 240 5.9% 10%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 85.7% 97.3%; @@ -105,6 +104,19 @@ body { @apply bg-background text-foreground; } + + /* Font size data attributes */ + [data-font-size='normal'] { + font-size: var(--font-size-base); + } + + [data-font-size='large'] { + font-size: calc(var(--font-size-base) * 1.2); + } + + [data-font-size='x-large'] { + font-size: calc(var(--font-size-base) * 1.4); + } } .scrollbar { @@ -177,14 +189,13 @@ background: transparent; cursor: pointer; border: double 6px transparent; - background-image: - linear-gradient(white, white), - linear-gradient( - 180deg, - rgba(113, 113, 122, 0.1) 0%, - rgba(113, 113, 122, 0.5) 50%, - rgba(113, 113, 122, 0.564706) 100% - ); + background-image: linear-gradient(white, white), + linear-gradient( + 180deg, + rgba(113, 113, 122, 0.1) 0%, + rgba(113, 113, 122, 0.5) 50%, + rgba(113, 113, 122, 0.564706) 100% + ); background-origin: border-box; background-clip: content-box, border-box; display: inline-block; @@ -265,14 +276,10 @@ .layout-footer { width: 100%; - overflow: auto; animation: animate-in 300ms ease-in-out; position: relative; - padding: 1rem; - - @media (min-width: 768px) { - padding: 2.5rem; - } + padding: 1.5rem 2rem; + text-align: center; @media (min-width: 1024px) { width: calc(100% - 250px); @@ -295,7 +302,6 @@ display: none; } - .lucide { stroke-width: 1px; -} \ No newline at end of file +} diff --git a/apps/masterbots.ai/app/layout.tsx b/apps/masterbots.ai/app/layout.tsx index 3f8d7359..2daa17a3 100644 --- a/apps/masterbots.ai/app/layout.tsx +++ b/apps/masterbots.ai/app/layout.tsx @@ -21,7 +21,9 @@ export default function RootLayout({ children }: RootLayoutProps) { )} > - + + + +
+
+ +
+
+ {children} +
+
+ +
+
+
+
+
+ + ) +} diff --git a/apps/masterbots.ai/app/u/[slug]/t/[category]/[chatbot]/[threadId]/page.tsx b/apps/masterbots.ai/app/u/[slug]/t/[category]/[chatbot]/[threadId]/page.tsx new file mode 100644 index 00000000..14731fdd --- /dev/null +++ b/apps/masterbots.ai/app/u/[slug]/t/[category]/[chatbot]/[threadId]/page.tsx @@ -0,0 +1,33 @@ +import { BrowseChatMessageList } from '@/components/routes/browse/browse-chat-message-list' +import { getThread } from '@/services/hasura' +import { User } from 'mb-genql' + +interface ThreadPageProps { + params: { + category: string + threadId: string + chatbot: string + } +} +export default async function ThreadPage({ params }: ThreadPageProps) { + + + const thread = await getThread({ + threadId: params.threadId, + jwt: '' + }) + + if(!thread){ + return
Thread not found
+ } + const { user, chatbot, messages } = thread + + return ( + + ) +} diff --git a/apps/masterbots.ai/app/u/[slug]/t/[category]/[chatbot]/page.tsx b/apps/masterbots.ai/app/u/[slug]/t/[category]/[chatbot]/page.tsx new file mode 100644 index 00000000..14dca116 --- /dev/null +++ b/apps/masterbots.ai/app/u/[slug]/t/[category]/[chatbot]/page.tsx @@ -0,0 +1,63 @@ +import { authOptions } from "@/auth"; +import UserThreadPanel from "@/components/routes/thread/user-thread-panel"; +import { botNames } from "@/lib/constants/bots-names"; +import { getBrowseThreads, getThreads, getUserBySlug} from "@/services/hasura/hasura.service"; +import { getServerSession } from "next-auth"; + + +export default async function ProfileChatBot({ params }: { params: { + slug: string; + category: string; + chatbot: string; +} +}) { + let threads = []; + const { slug, category, chatbot } = params; + const session = await getServerSession(authOptions); + const jwt = session ? session.user?.hasuraJwt : ''; + const { user, error } = await getUserBySlug({ + slug, + isSameUser: session?.user.slug === slug + }); + if (!user) return
No user found
+ + const chatbotName = botNames.get(chatbot); + if (!chatbotName) { + throw new Error(`Chatbot name for ${chatbot} not found`); + } + + const fetchThreads = async () => { + try { + const isOwnProfile = session?.user?.id === user?.userId; + if (!isOwnProfile) { + return await getBrowseThreads({ + userId: user.userId, + chatbotName + }); + } + + if (!session?.user?.hasuraJwt) { + throw new Error('Authentication required'); + } + + return await getThreads({ + jwt, + userId: user.userId, + chatbotName + }); + } catch (error) { + console.error('Failed to fetch threads:', error); + return []; + } + }; + + threads = await fetchThreads(); + + return ( + + ) +} \ No newline at end of file diff --git a/apps/masterbots.ai/app/u/[slug]/t/[category]/[threadId]/page.tsx b/apps/masterbots.ai/app/u/[slug]/t/[category]/[threadId]/page.tsx deleted file mode 100644 index f6eac42a..00000000 --- a/apps/masterbots.ai/app/u/[slug]/t/[category]/[threadId]/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ChatPageProps } from '@/types/types' - -export default async function ChatPage({ params }: ChatPageProps) { - return ( -
- /u/[slug]/t/{params.category}/{params.threadId} -
- ) -} diff --git a/apps/masterbots.ai/app/u/[slug]/t/[category]/page.tsx b/apps/masterbots.ai/app/u/[slug]/t/[category]/page.tsx index 7d5ce119..37a598c8 100644 --- a/apps/masterbots.ai/app/u/[slug]/t/[category]/page.tsx +++ b/apps/masterbots.ai/app/u/[slug]/t/[category]/page.tsx @@ -1,11 +1,64 @@ +import { getBrowseThreads, getCategories, getThreads, getUserBySlug } from "@/services/hasura" +import { toSlug } from "mb-lib" +import { authOptions } from '@/auth' +import { getServerSession } from "next-auth" +import { Thread } from "mb-genql" +import UserThreadPanel from "@/components/routes/thread/user-thread-panel" + export default async function BrowseCategoryPage({ params }: { - params: { category: string } + params: { category: string, slug: string } }) { + + + const session = await getServerSession(authOptions) + let threads: Thread[] = [] + const categories = await getCategories() + const category = categories.find( + category => toSlug(category.name) === params.category + ) + + const slug = params.slug + const { user, error } = await getUserBySlug({ + slug, + isSameUser: session?.user.slug === slug + }); + + if (!category) return
Category {params.category} not found
+ if (error) return
Error loading profile: {error}
+ if (!user) return
User {params.slug} not found
+ + const fetchThreads = async () => { + try { + const isOwnProfile = session?.user?.id === user?.userId; + if (!isOwnProfile) { + return await getBrowseThreads({ + userId: user.userId, + categoryId: category?.categoryId + }); + } + + if (!session?.user?.hasuraJwt) { + throw new Error('Authentication required'); + } + + return await getThreads({ + jwt: session.user.hasuraJwt, + userId: user.userId, + categoryId: category?.categoryId + }); + } catch (error) { + console.error('Failed to fetch threads:', error); + return []; + } + }; + + threads = await fetchThreads(); + return (
- /u/[slug]/t/{params.category} +
) } diff --git a/apps/masterbots.ai/app/u/[slug]/t/page.tsx b/apps/masterbots.ai/app/u/[slug]/t/page.tsx index 2f7189cb..ab6f60e1 100644 --- a/apps/masterbots.ai/app/u/[slug]/t/page.tsx +++ b/apps/masterbots.ai/app/u/[slug]/t/page.tsx @@ -1,52 +1,70 @@ -import { getBrowseThreads, getUserInfoFromBrowse } from '@/services/hasura' -import BrowseUserDetails from '@/components/routes/browse/browse-user-details' -import BrowseSpecificThreadList from '@/components/routes/browse/browse-specific-thread-list' -import { Metadata } from 'next' +import { UserThreadList } from "@/components/routes/profile/user-thread-list"; +import type { Metadata } from 'next' import { generateMetadataFromSEO } from '@/lib/metadata' +import { getUserInfoFromBrowse } from '@/services/hasura' +import { authOptions } from '@/auth' +import { getServerSession } from "next-auth" +import { getBrowseThreads, getThreads, getUserBySlug} from "@/services/hasura"; +import { Thread, User } from "mb-genql"; -const PAGE_SIZE = 50 +export default async function ProfilePage({ params }: { params: { slug: string } }) { -export default async function BotThreadsPage({ - params -}: { - params: { slug: string } -}) { - const user = await getUserInfoFromBrowse(params.slug) - if (!user) return
No user found.
+ let threads: Thread[] = [] + const slug = params.slug - const threads = await getBrowseThreads({ - slug: params.slug, - limit: PAGE_SIZE - }) - return ( -
- - -
- ) -} + const session = await getServerSession(authOptions) + const { user, error } = await getUserBySlug({ + slug, + isSameUser: session?.user.slug === slug + }); -export async function generateMetadata({ - params -}: { - params: { slug: string } -}): Promise { - const user = await getUserInfoFromBrowse(params.slug) - const seoData = { - title: user?.username || '', - description: user?.username || '', - ogType: 'website', - ogImageUrl: user?.profilePicture || '', - twitterCard: 'summary_large_image' - } + if (error) return
Error loading profile: {error}
+ if (!user) return
User {params.slug} not found
+ + const fetchThreads = async () => { + try { + const isOwnProfile = session?.user?.id === user?.userId; + if (!isOwnProfile) { + return await getBrowseThreads({ + userId: user.userId + }); + } + + if (!session?.user?.hasuraJwt) { + throw new Error('Authentication required'); + } + + return await getThreads({ + jwt: session.user.hasuraJwt, + userId: user.userId + }); + } catch (error) { + console.error('Failed to fetch threads:', error); + return []; + } + }; + + threads = await fetchThreads(); + + return + } - return generateMetadataFromSEO(seoData) -} + export async function generateMetadata({ + params + }: { + params: { slug: string } + }): Promise { + const user = await getUserInfoFromBrowse(params.slug) + + const seoData = { + title: user?.username || '', + description: user?.username || '', + ogType: 'website', + ogImageUrl: user?.profilePicture || '', + twitterCard: 'summary_large_image' + } + + return generateMetadataFromSEO(seoData) + } + \ No newline at end of file diff --git a/apps/masterbots.ai/app/u/layout.tsx b/apps/masterbots.ai/app/u/layout.tsx index 54497926..d6a3455d 100644 --- a/apps/masterbots.ai/app/u/layout.tsx +++ b/apps/masterbots.ai/app/u/layout.tsx @@ -13,7 +13,6 @@ export default async function BrowseLayout({ children }: BrowseLayoutProps) {
{children} -
diff --git a/apps/masterbots.ai/app/wordware/layout.tsx b/apps/masterbots.ai/app/wordware/layout.tsx index 8e54ab5b..161decc5 100644 --- a/apps/masterbots.ai/app/wordware/layout.tsx +++ b/apps/masterbots.ai/app/wordware/layout.tsx @@ -10,7 +10,7 @@ interface ChatLayoutProps { } export default async function ChatLayout({ children }: ChatLayoutProps) { - if (!appConfig.devMode) { + if (!appConfig.features.devMode) { console.error('Navigation to WordWare is disabled. No access to this page') redirect('/') } diff --git a/apps/masterbots.ai/auth.ts b/apps/masterbots.ai/auth.ts index 40995a34..fff22a4a 100644 --- a/apps/masterbots.ai/auth.ts +++ b/apps/masterbots.ai/auth.ts @@ -1,17 +1,22 @@ import bcrypt from 'bcryptjs' import { setCookie } from 'cookies-next' -import { decodeToken, getHasuraClient, getToken, toSlug, validateJwtSecret, verify } from 'mb-lib' +import { + getHasuraClient, + getToken, + toSlug, + validateJwtSecret, + verify +} from 'mb-lib' import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next' -import { NextAuthOptions, getServerSession, User } from 'next-auth' +import { getServerSession, NextAuthOptions, User } from 'next-auth' import CredentialsProvider from 'next-auth/providers/credentials' import Google from 'next-auth/providers/google' import { getUserRoleByEmail } from './services/hasura' - //* NextAuth configuration strategy with multiprovider options export const authOptions: NextAuthOptions = { providers: [ @@ -41,7 +46,8 @@ export const authOptions: NextAuthOptions = { password: true, username: true, profilePicture: true, - role: true, + role: true, + slug: true } }) if (!user || user.length === 0) { @@ -62,7 +68,14 @@ export const authOptions: NextAuthOptions = { } console.log('User authenticated successfully') //* Return user details to be attached to the token - return { id: user[0].userId, email: user[0].email, name: user[0].username, image: user[0].profilePicture, role: user[0].role || 'user' } + return { + id: user[0].userId, + email: user[0].email, + name: user[0].username, + image: user[0].profilePicture, + role: user[0].role || 'user', + slug: user[0].slug || toSlug(user[0].username) + } } catch (error) { throw new Error('Authentication failed') } @@ -81,24 +94,27 @@ export const authOptions: NextAuthOptions = { async jwt({ token, user, account }) { if (user) { //* Add user role to the token when signing in with Google - if(account?.provider === 'google'){ - const email = user.email; - const userRoleResult = await getUserRoleByEmail({ email }); + if (account?.provider === 'google') { + const email = user.email + const userRoleResult = await getUserRoleByEmail({ email }) if (userRoleResult.users.length > 0) { - token.role = userRoleResult.users[0]?.role || 'user'; + token.role = userRoleResult.users[0]?.role || 'user' + token.slug = userRoleResult.users[0]?.slug } else { - console.error('Error fetching user role:', userRoleResult.error); - token.role = 'user'; // Default to 'user' if no user found or in case of error + console.error('Error fetching user role:', userRoleResult.error) + token.role = 'user' // Default to 'user' if no user found or in case of error + token.slug = toSlug(user.name as string) } - }else{ - token.role = user.role; // use this for other + } else { + token.role = user.role // use this for other + token.slug = user.slug } - token.id = user.id; - token.email = user.email; - token.name = user.name; - token.image = user.image; - token.provider = account?.provider || 'credentials'; + token.id = user.id + token.email = user.email + token.name = user.name + token.image = user.image + token.provider = account?.provider || 'credentials' //* Validate and prepare the JWT secret for signing tokens const jwtSecret = validateJwtSecret( @@ -107,7 +123,7 @@ export const authOptions: NextAuthOptions = { if (!jwtSecret) { throw new Error('Secret not found') } - + try { //* Generate a JWT for Hasura with custom claims const hasuraJwt = await getToken({ @@ -124,7 +140,6 @@ export const authOptions: NextAuthOptions = { throw new Error('Login Error') } - //* Verify the generated JWT to ensure it's valid await verify(hasuraJwt, jwtSecret.key) @@ -154,6 +169,7 @@ export const authOptions: NextAuthOptions = { session.user.image = token.image as string session.user.hasuraJwt = token.hasuraJwt as string session.user.role = token.role as string + session.user.slug = token.slug as string console.log( 'Session created with Hasura JWT 🗝️: ', @@ -167,9 +183,9 @@ export const authOptions: NextAuthOptions = { }, async signIn({ user, account }) { if (account?.provider === 'google') { - const client = getHasuraClient(); + const client = getHasuraClient() - let signedUser; + let signedUser // Check if user exists, if not, create a new user const { user: currentUser } = await client.query({ @@ -195,7 +211,10 @@ export const authOptions: NextAuthOptions = { username: user.name, profilePicture: user.image, // You might want to generate a random password here - password: bcrypt.hashSync(Math.random().toString(36).slice(-8), 10) + password: bcrypt.hashSync( + Math.random().toString(36).slice(-8), + 10 + ) } }, userId: true @@ -209,7 +228,7 @@ export const authOptions: NextAuthOptions = { } return true - }, + } }, pages: { signIn: '/auth/signin' //* Custom sign-in page @@ -217,7 +236,6 @@ export const authOptions: NextAuthOptions = { debug: process.env.NODE_ENV === 'development' //! Enable detailed logging in development mode } - //* Helper function to retrieve the session in server-side contexts export function auth( ...args: diff --git a/apps/masterbots.ai/components/auth/forgot-password-form.tsx b/apps/masterbots.ai/components/auth/forgot-password-form.tsx index 587f058c..0d866fb8 100644 --- a/apps/masterbots.ai/components/auth/forgot-password-form.tsx +++ b/apps/masterbots.ai/components/auth/forgot-password-form.tsx @@ -1,11 +1,12 @@ 'use client' -import React, { useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { toast } from 'react-hot-toast' import { validateEmail } from '@/lib/utils' +import type React from 'react' +import { useState } from 'react' +import { toast } from 'react-hot-toast' export default function ForgotPasswordForm() { const [email, setEmail] = useState('') diff --git a/apps/masterbots.ai/components/auth/reset-password-form.tsx b/apps/masterbots.ai/components/auth/reset-password-form.tsx index 6c5d2ba6..56f8a1df 100644 --- a/apps/masterbots.ai/components/auth/reset-password-form.tsx +++ b/apps/masterbots.ai/components/auth/reset-password-form.tsx @@ -1,14 +1,14 @@ 'use client' -import React from 'react' +import { PasswordStrengthMeter } from '@/components/shared/password-strength-meter' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { toast } from 'react-hot-toast' -import { useRouter } from 'next/navigation' -import PasswordStrengthMeter from '@/components/shared/password-strength-meter' import { isPasswordStrong } from '@/lib/password' import { Eye, EyeOff } from 'lucide-react' +import { useRouter } from 'next/navigation' +import type React from 'react' +import { toast } from 'react-hot-toast' import { useSetState } from 'react-use' interface FormState { diff --git a/apps/masterbots.ai/components/auth/signin-form.tsx b/apps/masterbots.ai/components/auth/signin-form.tsx index 5792f2e0..4403e7a9 100644 --- a/apps/masterbots.ai/components/auth/signin-form.tsx +++ b/apps/masterbots.ai/components/auth/signin-form.tsx @@ -4,6 +4,7 @@ import { LoginButton } from '@/components/shared/login-button' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Eye, EyeOff } from 'lucide-react' import { appConfig } from 'mb-env' import { signIn } from 'next-auth/react' import Link from 'next/link' @@ -12,8 +13,8 @@ import { useState } from 'react' export default function SignInForm() { const router = useRouter() - const [errorMessage, setErrorMessage] = useState(null) + const [showPassword, setShowPassword] = useState(false) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -54,7 +55,22 @@ export default function SignInForm() { - +
+ + +
+
+
- +
+ + +
- ) -} - -function validatePassword(e: React.FocusEvent) { - const password = e.target.value - - if (password.length < 8) { - e.target.setCustomValidity('Password must be at least 8 characters long') - e.target.reportValidity() - } else { - e.target.setCustomValidity('') - } -} - -function verifyPassword(e: React.FocusEvent) { - const form = new FormData(e.currentTarget.form as HTMLFormElement) - const password = form.get('password') as string - const passwordVerify = e.target.value - - if (passwordVerify && password !== passwordVerify) { - e.target.setCustomValidity('Passwords do not match') - e.target.reportValidity() - } else { - e.target.setCustomValidity('') - } } \ No newline at end of file diff --git a/apps/masterbots.ai/components/auth/user-login.tsx b/apps/masterbots.ai/components/auth/user-login.tsx index af49821f..1049ae3e 100644 --- a/apps/masterbots.ai/components/auth/user-login.tsx +++ b/apps/masterbots.ai/components/auth/user-login.tsx @@ -5,6 +5,9 @@ import { UserMenu } from '@/components/layout/header/user-menu' import { Button } from '@/components/ui/button' import Link from 'next/link' import { isTokenExpired } from 'mb-lib' +import { ProfileSidebar } from '@/components/layout/sidebar/profile-sidebar' +import { Suspense } from 'react' +import { ProfileSidebarSkeleton } from '../shared/skeletons/profile-sidebar-skeleton' export function UserLogin() { const { data: session, status } = useSession() @@ -25,7 +28,14 @@ export function UserLogin() { return } - return + return ( + <> + }> + + + + + ) } return diff --git a/apps/masterbots.ai/components/layout/footer/footer-ct.tsx b/apps/masterbots.ai/components/layout/footer/footer-ct.tsx index 4e0612eb..ec3f0f9e 100644 --- a/apps/masterbots.ai/components/layout/footer/footer-ct.tsx +++ b/apps/masterbots.ai/components/layout/footer/footer-ct.tsx @@ -1,40 +1,42 @@ /* eslint-disable react/no-unescaped-entities */ -import { cn } from "@/lib/utils" -import { ElementType } from "react" +import { cn } from "@/lib/utils"; import Link from 'next/link'; +import { ElementType } from "react"; -export default function FooterCT({ nonFooterTag, fixed }: { nonFooterTag?: boolean, fixed?: boolean }) { +export default function FooterCT({ nonFooterTag, fixed, className }: { nonFooterTag?: boolean, fixed?: boolean, className?: string }) { const Footer: ElementType = ({ children }) => nonFooterTag ? -
+
{children}
: -
+
{children}
return ( -
- - Masterbots isn't infallible; verify crucial facts. Not professional advice. - - - Robot avatars by{' '} - - robohash.org - - {' • '} - - terms & policies - - -
+
+
+ + Masterbots isn't infallible; verify crucial facts. Not professional advice. + + + Robot avatars by{' '} + + robohash.org + + {' • '} + + terms & policies + + +
+
) } \ No newline at end of file diff --git a/apps/masterbots.ai/components/layout/header/header.tsx b/apps/masterbots.ai/components/layout/header/header.tsx index 36ffc0be..7e926359 100644 --- a/apps/masterbots.ai/components/layout/header/header.tsx +++ b/apps/masterbots.ai/components/layout/header/header.tsx @@ -3,7 +3,6 @@ import * as React from 'react' import { UserLogin } from '@/components/auth/user-login' import { SidebarToggle } from '@/components/layout/sidebar/sidebar-toggle' -import { ThemeToggle } from '@/components/shared/theme-toggle' import { Button } from '@/components/ui/button' import { IconSeparator } from '@/components/ui/icons' import { appConfig } from 'mb-env' @@ -16,19 +15,20 @@ export function Header() { - - - - {appConfig.devMode && ( - <> - - - - )} + {/* Navigation links - Hidden on mobile */} +
+ + + {appConfig.features.devMode && ( + <> + + + + )} +
-
- +
}> @@ -43,4 +43,4 @@ function HeaderLink({ href, text }: { href: string; text: string }) { {text} ) -} \ No newline at end of file +} diff --git a/apps/masterbots.ai/components/layout/header/user-menu.tsx b/apps/masterbots.ai/components/layout/header/user-menu.tsx index 3b0ccdf6..76f63009 100644 --- a/apps/masterbots.ai/components/layout/header/user-menu.tsx +++ b/apps/masterbots.ai/components/layout/header/user-menu.tsx @@ -1,6 +1,6 @@ 'use client' -import { type Session } from 'next-auth' +import type { Session } from 'next-auth' import { signOut } from 'next-auth/react' import Image from 'next/image' import { Button } from '@/components/ui/button' @@ -11,6 +11,11 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import Link from 'next/link' +import { toSlugWithUnderScore } from 'mb-lib' +import { ThemeToggle } from '@/components/shared/theme-toggle' +import { useTheme } from 'next-themes' + export interface UserMenuProps { user: Session['user'] @@ -21,9 +26,16 @@ function getUserInitials(name: string) { return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2) } +function truncateUsername(username: string | null | undefined, maxLength = 10) { + if (!username) return ''; + return username.length > maxLength ? `${username.slice(0, maxLength - 4)}` : username; +} + + export function UserMenu({ user }: UserMenuProps) { + const { theme } = useTheme() return ( -
+
)} - {user?.name} + + {user?.name && truncateUsername(user.name)} + -
{user?.name}
-
{user?.email}
+ +
{user?.name}
+
{user?.email}
+ +
+ + +
+ {theme === 'dark' ? 'Light Mode' : 'Dark Mode'} + +
) -} +} \ No newline at end of file diff --git a/apps/masterbots.ai/components/layout/profile/hero.tsx b/apps/masterbots.ai/components/layout/profile/hero.tsx new file mode 100644 index 00000000..1a3c182d --- /dev/null +++ b/apps/masterbots.ai/components/layout/profile/hero.tsx @@ -0,0 +1,57 @@ +'use client' +import { UserCard } from '@/components/routes/profile/user-card' +import { useParams } from 'next/navigation' +import { useProfile } from '@/lib/hooks/use-profile' +import { useEffect, useState } from 'react' +import { User } from 'mb-genql' +import { useSession } from 'next-auth/react' + +export function Hero() { + const { slug } = useParams() + const { getuserInfo } = useProfile() + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(false) + const { data: session } = useSession() + + + useEffect(() => { + let isActive = true + if (slug) { + const fetchData = async () => { + setLoading(true) + try { + if (typeof slug !== 'string') { + throw new Error('Invalid slug parameter') + } + + const { user, error } = await getuserInfo(slug as string) + if (!isActive) return + if (error) { + console.log(error) + console.error('Failed to fetch user info:', error) + setUser(null) + return + } + setUser(user) + } finally { + if (isActive) { + setLoading(false) + } + } + } + fetchData() + } + return () => { + isActive = false + } + }, [slug, session]) + + return ( +
+
+
+ +
+
+ ) +} diff --git a/apps/masterbots.ai/components/layout/profile/profile-page-sidebar.tsx b/apps/masterbots.ai/components/layout/profile/profile-page-sidebar.tsx new file mode 100644 index 00000000..ec04a729 --- /dev/null +++ b/apps/masterbots.ai/components/layout/profile/profile-page-sidebar.tsx @@ -0,0 +1,153 @@ +'use client' + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useLocation } from 'react-use'; +import { cn } from '@/lib/utils' +import { IconCaretRight } from '@/components/ui/icons'; +import { MessagesSquare, Settings, ReceiptIcon } from 'lucide-react'; +import { SidebarCategoryGeneral } from '../sidebar/sidebar-category-general'; +import { useParams, usePathname } from 'next/navigation'; +import { useSidebar } from '@/lib/hooks/use-sidebar'; +import { useProfile } from '@/lib/hooks/use-profile'; +import { useSession } from 'next-auth/react'; +import { useAsync } from 'react-use' + +export const ProfileSidebar = ({ children }: any) => { + const pathname = usePathname() + const openSidebar = pathname.includes('/t'); + const [isThreadsOpen, setIsThreadsOpen] = useState(openSidebar); + const location = useLocation(); + const { slug } = useParams() + const { isSidebarOpen, toggleSidebar, setActiveCategory, + setActiveChatbot, } = useSidebar(); + const { currentUser, isSameUser } = useProfile() + const { data: session } = useSession() + const { value: user } = useAsync(async () => { + if (currentUser === null) return null; + return currentUser; + }, [slug, currentUser]); + + const sameUser = isSameUser(user?.userId) + + const handleToggleThreads = () => { + setIsThreadsOpen(!isThreadsOpen); + setActiveCategory(null); + setActiveChatbot(null); + } + + return ( +
+ {/* Overlay for mobile */} + {isSidebarOpen && ( +
toggleSidebar()} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === 'Space') { + toggleSidebar(); + } + }} + /> + )} + +
+ {/* Sidebar */} +
+ +
+ + {/* Main content */} +
+
+ {children} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/apps/masterbots.ai/components/layout/providers.tsx b/apps/masterbots.ai/components/layout/providers.tsx index 0de9df6e..a9fa9680 100644 --- a/apps/masterbots.ai/components/layout/providers.tsx +++ b/apps/masterbots.ai/components/layout/providers.tsx @@ -1,14 +1,18 @@ 'use client' import { TooltipProvider } from '@/components/ui/tooltip' +import { AccessibilityProvider } from '@/lib/hooks/use-accessibility' +import { ModelProvider } from '@/lib/hooks/use-model' import { PaymentProvider } from '@/lib/hooks/use-payment' +import { PowerUpProvider } from '@/lib/hooks/use-power-up' +import { ProfileProvider } from '@/lib/hooks/use-profile' import { SidebarProvider } from '@/lib/hooks/use-sidebar' import { ThreadProvider } from '@/lib/hooks/use-thread' +import { ThreadSearchProvider } from '@/lib/hooks/use-thread-search' +import { ThreadVisibilityProvider } from '@/lib/hooks/use-thread-visibility' import { SessionProvider } from 'next-auth/react' import { ThemeProvider as NextThemesProvider } from 'next-themes' -import { ThemeProviderProps } from 'next-themes/dist/types' -import { ModelProvider } from '@/lib/hooks/use-model' -import { ThreadVisibilityProvider } from '@/lib/hooks/use-thread-visibility' +import type { ThemeProviderProps } from 'next-themes/dist/types' export function Providers({ children, ...props }: ThemeProviderProps) { return ( @@ -18,9 +22,17 @@ export function Providers({ children, ...props }: ThemeProviderProps) { - - {children} - + + + + + + {children} + + + + + diff --git a/apps/masterbots.ai/components/layout/sidebar/profile-sidebar.tsx b/apps/masterbots.ai/components/layout/sidebar/profile-sidebar.tsx new file mode 100644 index 00000000..7707d1e3 --- /dev/null +++ b/apps/masterbots.ai/components/layout/sidebar/profile-sidebar.tsx @@ -0,0 +1,163 @@ +'use client' + +import { ThemeToggle } from '@/components/shared/theme-toggle' +import { Button } from '@/components/ui/button' +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' +import { LogOut } from 'lucide-react' +import { appConfig } from 'mb-env' +import { toSlugWithUnderScore } from 'mb-lib' +import type { Session } from 'next-auth' +import { signOut } from 'next-auth/react' +import Image from 'next/image' +import { useRouter } from 'next/navigation' +import { useCallback, useState } from 'react' + +interface ProfileSidebarProps { + user: Session['user'] & { + hasuraJwt?: string + } +} + +function getUserInitials(name: string) { + const [firstName, lastName] = name.split(' ') + return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2) +} + +export function ProfileSidebar({ user }: ProfileSidebarProps) { + const [isOpen, setIsOpen] = useState(false) + const router = useRouter() + + const handleNavigation = (path: string) => { + setIsOpen(false) + router.push(path) + } + + const handleLogout = useCallback(async () => { + try { + setIsOpen(false) + await new Promise(resolve => setTimeout(resolve, 100)) + await signOut({ callbackUrl: '/' }) + } catch (error) { + console.error('Logout error:', error) + window.location.href = '/' + } + }, []) + + const goToProfile = useCallback((e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + const userSlug = toSlugWithUnderScore(user.name || '') + if (userSlug) { + setIsOpen(false) + router.push(`/u/${userSlug}/t`) + } + }, [router, user.name]) + + + return ( + + + + + +
+ {/* Profile Header */} +
+ +
+ + {/* Navigation Links - Only visible on mobile */} + + + {/* Logout Button */} +
+ + +
+
+
+
+ ) +} diff --git a/apps/masterbots.ai/components/layout/sidebar/sidebar-actions.tsx b/apps/masterbots.ai/components/layout/sidebar/sidebar-actions.tsx index 27bce963..d89be026 100644 --- a/apps/masterbots.ai/components/layout/sidebar/sidebar-actions.tsx +++ b/apps/masterbots.ai/components/layout/sidebar/sidebar-actions.tsx @@ -1,9 +1,6 @@ 'use client' -import { useRouter } from 'next/navigation' -import * as React from 'react' -import { toast } from 'react-hot-toast' -import { ServerActionResult, type Chat } from '@/types/types' +import { ChatShareDialog } from '@/components/routes/chat/chat-share-dialog' import { AlertDialog, AlertDialogAction, @@ -15,14 +12,17 @@ import { AlertDialogTitle } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' -import { IconShare, IconSpinner, IconTrash, IconCheck } from '@/components/ui/icons' -import { ChatShareDialog } from '@/components/routes/chat/chat-share-dialog' +import { IconCheck, IconShare, IconSpinner, IconTrash } from '@/components/ui/icons' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useSidebar } from '@/lib/hooks/use-sidebar' +import { ServerActionResult, type Chat } from '@/types/types' +import { useRouter } from 'next/navigation' +import * as React from 'react' +import { toast } from 'react-hot-toast' interface SidebarActionsProps { chat: Chat @@ -39,9 +39,9 @@ export function SidebarActions({ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) const [shareDialogOpen, setShareDialogOpen] = React.useState(false) const [isRemovePending, startRemoveTransition] = React.useTransition() - const { - isFilterMode, - selectedChats, + const { + isFilterMode, + selectedChats, setSelectedChats, filterValue } = useSidebar() @@ -58,7 +58,7 @@ export function SidebarActions({ }, [chat.id, isSelected, setSelectedChats]) const handleDelete = React.useCallback(async () => { - startRemoveTransition(async () => { + const removeChatAsync = async () => { const result = await removeChat({ id: chat.id, path: chat.path @@ -78,6 +78,10 @@ export function SidebarActions({ if (isSelected) { setSelectedChats(prev => prev.filter(id => id !== chat.id)) } + } + + startRemoveTransition(() => { + removeChatAsync() }) }, [chat.id, chat.path, removeChat, router, isSelected, setSelectedChats]) diff --git a/apps/masterbots.ai/components/layout/sidebar/sidebar-category-general.tsx b/apps/masterbots.ai/components/layout/sidebar/sidebar-category-general.tsx index 2bcd7856..f97d3355 100644 --- a/apps/masterbots.ai/components/layout/sidebar/sidebar-category-general.tsx +++ b/apps/masterbots.ai/components/layout/sidebar/sidebar-category-general.tsx @@ -3,16 +3,15 @@ import SidebarLink from '@/components/layout/sidebar/sidebar-link' import { useSidebar } from '@/lib/hooks/use-sidebar' -export function SidebarCategoryGeneral() { - const { filteredCategories, selectedCategories, isFilterMode } = useSidebar() +export function SidebarCategoryGeneral({ page }: { page?: string }) { + const { filteredCategories, isFilterMode } = useSidebar() if (!filteredCategories.length) return
No matching categories found
- return (
    {filteredCategories.map((category) => (
  • - +
  • ))}
diff --git a/apps/masterbots.ai/components/layout/sidebar/sidebar-link.tsx b/apps/masterbots.ai/components/layout/sidebar/sidebar-link.tsx index 7de6f73d..63bbae2a 100644 --- a/apps/masterbots.ai/components/layout/sidebar/sidebar-link.tsx +++ b/apps/masterbots.ai/components/layout/sidebar/sidebar-link.tsx @@ -1,6 +1,5 @@ 'use client' -import { useRouter } from 'next/navigation' import { Checkbox } from "@/components/ui/checkbox" import { IconCaretRight } from '@/components/ui/icons' import { useSidebar } from '@/lib/hooks/use-sidebar' @@ -9,19 +8,21 @@ import { Category, Chatbot } from 'mb-genql' import { toSlug } from 'mb-lib' import Image from 'next/image' import Link from 'next/link' -import { usePathname } from "next/navigation" -import React, { useCallback, useState } from 'react' +import { useParams, usePathname, useRouter } from 'next/navigation' +import React, { useCallback } from 'react' interface SidebarLinkProps { category: Category isFilterMode: boolean + page?: string } -export default function SidebarLink({ category, isFilterMode }: SidebarLinkProps) { +export default function SidebarLink({ category, isFilterMode, page }: SidebarLinkProps) { const router = useRouter() const pathname = usePathname() - const isBrowse = !pathname.includes('/c') - + const isBrowse = !pathname.includes('/c') && !pathname.includes('/u') + const { slug } = useParams() + const { activeCategory, setActiveCategory, @@ -33,22 +34,33 @@ export default function SidebarLink({ category, isFilterMode }: SidebarLinkProps setSelectedChatbots, expandedCategories, setExpandedCategories, + navigateTo } = useSidebar() const isExpanded = expandedCategories.includes(category.categoryId) const handleClickCategory = useCallback((e: React.MouseEvent) => { e.stopPropagation() if (!isFilterMode) { - setExpandedCategories(prev => - prev.includes(category.categoryId) - ? [] + setExpandedCategories(prev => + prev.includes(category.categoryId) + ? [] : [category.categoryId] ) setActiveCategory(prev => { const newCategory = prev === category.categoryId ? null : category.categoryId - if (newCategory && isBrowse) { + if (newCategory) { setActiveChatbot(null) - router.push(`/c/${toSlug(category.name)}`) + navigateTo({ + page, + slug: typeof slug === 'string' ? slug : undefined, + categoryName: toSlug(category.name.toLowerCase()) + }) + + } else { + navigateTo({ + page, + slug: typeof slug === 'string' ? slug : undefined, + }) } return newCategory }) @@ -101,6 +113,7 @@ export default function SidebarLink({ category, isFilterMode }: SidebarLinkProps isActive={chatbotCategory.chatbot.chatbotId === activeChatbot?.chatbotId} setActiveChatbot={setActiveChatbot} isFilterMode={isFilterMode} + page={page} /> ))}
@@ -125,16 +138,21 @@ export default function SidebarLink({ category, isFilterMode }: SidebarLinkProps return (
- { + e.preventDefault(); + handleClickCategory(e); + }} > {categoryContent} - + {childrenContent}
) @@ -146,6 +164,7 @@ interface ChatbotComponentProps { isActive: boolean setActiveChatbot: React.Dispatch> isFilterMode: boolean + page?: string } const ChatbotComponent: React.FC = React.memo(function ChatbotComponent({ @@ -153,15 +172,25 @@ const ChatbotComponent: React.FC = React.memo(function Ch category, isActive, setActiveChatbot, - isFilterMode + isFilterMode, + page }) { - const { selectedChatbots, toggleChatbotSelection } = useSidebar() + const { selectedChatbots, toggleChatbotSelection, navigateTo } = useSidebar() const pathname = usePathname() - const isBrowse = !pathname.includes('/c') + const isBrowse = !pathname.includes('/c') && !pathname.includes('/u') + const { slug } = useParams() const handleChatbotClick = useCallback((e: React.MouseEvent) => { - if (isFilterMode) e.preventDefault() - else setActiveChatbot(chatbot) + e.preventDefault() + setActiveChatbot(chatbot) + if (chatbot) { + navigateTo({ + page, + slug: slug as string, + categoryName: toSlug(category.name.toLowerCase()), + chatbotName: chatbot.name.toLowerCase() + }) + } }, [chatbot, setActiveChatbot, isFilterMode]) const isSelected = selectedChatbots.includes(chatbot.chatbotId) @@ -199,7 +228,7 @@ const ChatbotComponent: React.FC = React.memo(function Ch
) : ( - - {/*

Chat history

- */} + {/* */} + {/*

Chat history

*/} + {/* */} ) } diff --git a/apps/masterbots.ai/components/layout/sidebar/sidebar.tsx b/apps/masterbots.ai/components/layout/sidebar/sidebar.tsx index 8763332c..9bd33f40 100644 --- a/apps/masterbots.ai/components/layout/sidebar/sidebar.tsx +++ b/apps/masterbots.ai/components/layout/sidebar/sidebar.tsx @@ -17,7 +17,7 @@ export function Sidebar({ className }: React.ComponentProps<'div'>) { data-state={isSidebarOpen ? 'open' : 'closed'} className={cn( className, - 'h-full flex flex-col dark:bg-zinc-950 z-[5000]' + 'h-full flex flex-col dark:bg-zinc-950 z-40' )} >
diff --git a/apps/masterbots.ai/components/routes/browse/browse-accordion.tsx b/apps/masterbots.ai/components/routes/browse/browse-accordion.tsx new file mode 100644 index 00000000..794e133d --- /dev/null +++ b/apps/masterbots.ai/components/routes/browse/browse-accordion.tsx @@ -0,0 +1,271 @@ +/* eslint-disable tailwindcss/migration-from-tailwind-2 */ +/** + * BrowseAccordion Component + * + * This component implements an accordion UI element that can expand and collapse to show or hide content. + * It is designed to handle threads in a chat application, allowing users to view messages related to a specific thread. + * + * Props: + * - thread: The thread object associated with this accordion. + * - className: Additional CSS classes for styling. + * - children: React nodes to be rendered inside the accordion. + * - onToggle: Callback function triggered when the accordion is toggled. + * - isOpen: Controls the open state of the accordion. + * - defaultState: Initial open state of the accordion. + * - triggerClass: CSS classes for the trigger button. + * - contentClass: CSS classes for the content area. + * - arrowClass: CSS classes for the arrow icon. + * - handleOpen: Callback for handling open state changes. + * - handleTrigger: Callback for handling trigger actions. + * - disabled: Disables the accordion if true. + * - isNestedThread: Indicates if the accordion is part of a nested thread. + * + * Key Features: + * - State Management: Uses local state to manage the open/closed state of the accordion. + * - Thread Management: Integrates with a custom hook to manage active threads and responses. + * - Accessibility: Implements ARIA attributes for better accessibility. + * - Smooth Scrolling: Scrolls to the accordion when opened for better user experience. + * - Dynamic Styling: Applies different styles based on the open state and whether the accordion is nested. + * - Conditional Rendering: Renders different UI elements based on the state and props. + */ + +import { useThread } from '@/lib/hooks/use-thread' +import { cn } from '@/lib/utils' +import { ChevronDown } from 'lucide-react' +import type { Thread } from 'mb-genql' +import { useEffect, useRef, useState } from 'react' + +// Helper function to handle body scroll +const toggleBodyScroll = (disable: boolean) => { + if (typeof window === 'undefined') return + + document.body.style.overflow = disable ? 'hidden' : 'auto' + // Prevent iOS Safari bouncing + document.body.style.position = disable ? 'fixed' : 'static' + document.body.style.width = disable ? '100%' : 'auto' +} + +export function BrowseAccordion({ + thread = null, + className, + children, + onToggle, + isOpen, + defaultState = false, + triggerClass, + contentClass, + arrowClass, + handleOpen, + handleTrigger, + disabled = false, + isNestedThread = false, + ...props +}: { + className?: string + children: React.ReactNode[] + defaultState?: boolean + triggerClass?: string + contentClass?: string + onToggle?: (isOpen: boolean) => void + isOpen?: boolean + arrowClass?: string + handleTrigger?: () => void + handleOpen?: () => void + thread?: Thread | null + disabled?: boolean + isNestedThread?: boolean +}) { + const { + activeThread, + setActiveThread, + setIsNewResponse, + isNewResponse, + isOpenPopup + } = useThread() + const [open, setOpen] = useState( + defaultState || activeThread?.threadId === thread?.threadId + ) + const accordionRef = useRef(null) + + const isAnotherThreadOpen = + !isNestedThread && + activeThread !== null && + thread?.threadId !== activeThread?.threadId + const shouldBeDisabled = disabled || isAnotherThreadOpen + + //? Handle body scroll locking + useEffect(() => { + const isMobile = window.innerWidth < 640 // sm breakpoint + + if (isMobile && open && !isNestedThread) { + toggleBodyScroll(true) + } else { + toggleBodyScroll(false) + } + + // Cleanup on unmount + return () => toggleBodyScroll(false) + }, [open, isNestedThread]) + + useEffect(() => { + if ( + (thread?.threadId && + activeThread !== null && + thread?.threadId !== activeThread?.threadId) || + (activeThread === null && thread?.threadId) + ) { + setOpen(false) + } + }, [activeThread, thread]) + + useEffect(() => { + if (isOpen !== undefined) { + setOpen(isOpen) + } + }, [isOpen]) + + const toggle = () => { + if (shouldBeDisabled) return + + setOpen((prevOpen: boolean) => { + const newState = !prevOpen + if (!newState && handleOpen) { + handleOpen() + } + if (thread?.threadId) { + setActiveThread(newState ? thread : null) + } + if (isNewResponse) setIsNewResponse(false) + if (onToggle) { + onToggle(newState) + } + + if (newState && !isNestedThread && accordionRef.current) { + setTimeout(() => { + accordionRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }) + }, 100) + } + + return newState + }) + } + + useEffect(() => { + if ( + !isOpenPopup && + activeThread && + activeThread.threadId === thread?.threadId && + !open + ) { + toggle() + } + }, [isOpenPopup, activeThread, thread, open, toggle]) + + return ( +
+ {!isNestedThread && open && ( + <> + {/* Background glow effect */} +
+ {/* Additional subtle glow layer */} +
+ {/* Border glow */} +
+ + )} + + {/* biome-ignore lint/a11y/useButtonType: */} + + +
+ {children[2]} +
+ + {!isNestedThread && !open && ( +
+ )} +
+ ) +} diff --git a/apps/masterbots.ai/components/routes/browse/browse-category-button.tsx b/apps/masterbots.ai/components/routes/browse/browse-category-button.tsx index 31ffc9fe..73ca8399 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-category-button.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-category-button.tsx @@ -1,5 +1,24 @@ +/** + * BrowseCategoryButton Component + * + * This component represents a button for selecting a category in the browsing interface. + * It allows users to navigate to different categories or return to the main page. + * + * Props: + * - category: The category object or 'all' to represent all categories. + * - activeTab: The currently active category ID or null if 'all' is active. + * - onClick: Callback function triggered when the button is clicked. + * - id: Unique identifier for the button element. + * + * Key Features: + * - Dynamic Navigation: Navigates to the appropriate category page based on the category selected. + * - Active State Styling: Applies different styles based on whether the button is active or not. + * - Animation: Uses Framer Motion to animate the active state with a bubble effect. + * - Accessibility: Includes focus-visible styles for better accessibility. + */ + import { motion } from 'framer-motion' -import { Category } from 'mb-genql' +import type { Category } from 'mb-genql' import { toSlug } from 'mb-lib' import Link from 'next/link' diff --git a/apps/masterbots.ai/components/routes/browse/browse-category-tabs.tsx b/apps/masterbots.ai/components/routes/browse/browse-category-tabs.tsx index 53a1623f..a457d283 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-category-tabs.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-category-tabs.tsx @@ -1,7 +1,23 @@ 'use client' +/** + * BrowseCategoryTabs Component + * + * This component renders a set of category buttons for browsing different categories. + * It allows users to switch between categories and highlights the active category. + * + * Props: + * - categories: An array of category objects to display as buttons. + * - initialCategory: The category to be selected initially; defaults to 'all'. + * + * Key Features: + * - Active Tab Management: Uses a custom hook to manage the active category tab. + * - Smooth Scrolling: Automatically scrolls to the active category button when changed. + * - Dynamic Category Buttons: Renders a button for each category, including an 'all' option. + */ + import { useBrowse } from '@/lib/hooks/use-browse' -import { Category } from 'mb-genql' +import type { Category } from 'mb-genql' import { toSlug } from 'mb-lib' import { useEffect } from 'react' import { BrowseCategoryButton } from '@/components/routes/browse/browse-category-button' @@ -14,6 +30,7 @@ export function BrowseCategoryTabs({ initialCategory?: string }) { const { tab: activeTab, changeTab: setActiveTab } = useBrowse() + useEffect(() => { if (document) { const element = document.getElementById( diff --git a/apps/masterbots.ai/components/routes/browse/browse-chat-message-list.tsx b/apps/masterbots.ai/components/routes/browse/browse-chat-message-list.tsx index 720d1c4b..acc40c90 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-chat-message-list.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-chat-message-list.tsx @@ -1,12 +1,27 @@ -// Inspired by Chatbot-UI and modified to fit the needs of this project -// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatcleanMessage.tsx - +/** + * BrowseChatMessageList Component + * + * This component displays a list of chat messages exchanged between the user and the chatbot. + * It organizes messages into pairs, allowing for a structured presentation of user and chatbot interactions. + * + * Props: + * - messages: An array of Message objects representing the chat messages. + * - chatbot: An optional Chatbot object containing details about the chatbot. + * - isThread: A boolean indicating if the messages are part of a thread (default is false). + * + * Key Features: + * - Message Pairing: Utilizes the `createMessagePairs` utility to pair user messages with corresponding chatbot responses. + * - Accordion Functionality: Each message pair is displayed within an accordion for better organization and readability. + * - Conditional Rendering: Displays the first message pair differently if it is part of a thread. + * - Responsive Design: Applies Tailwind CSS for styling and layout. + */ +'use client' import { cn, createMessagePairs } from '@/lib/utils' -import { Chatbot, Message, User } from 'mb-genql' +import type { Chatbot, Message, User } from 'mb-genql' import { BrowseChatMessage } from '@/components/routes/browse/browse-chat-message' -import { MessagePair, convertMessage } from '@/components/routes/browse/browse-chat-messages' -import { ChatAccordion } from '@/components/routes/chat/chat-accordion' +import { type MessagePair, convertMessage } from '@/components/routes/browse/browse-chat-messages' import React from 'react' +import {BrowseAccordion} from '@/components/routes/browse/browse-accordion'; export function BrowseChatMessageList({ messages, @@ -32,11 +47,12 @@ export function BrowseChatMessageList({ return (
{pairs.map((pair: MessagePair, key: number) => ( - - + ))}
) diff --git a/apps/masterbots.ai/components/routes/browse/browse-chat-message.tsx b/apps/masterbots.ai/components/routes/browse/browse-chat-message.tsx index 17b6b08c..7b51d228 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-chat-message.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-chat-message.tsx @@ -1,5 +1,27 @@ // Inspired by Chatbot-UI and modified to fit the needs of this project // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatcleanMessage.tsx +/** + * ChatMessageProps Interface + * + * This interface defines the props for the BrowseChatMessage component. + * + * Props: + * - message: The message object containing the content to be displayed. + * - chatbot: Optional chatbot object associated with the message. + * + * BrowseChatMessage Component + * + * This component renders a chat message, including markdown content and code blocks. + * It utilizes MemoizedReactMarkdown for rendering markdown and CodeBlock for code snippets. + * + * Key Features: + * - Content Cleaning: Cleans the message content using the cleanPrompt function. + * - Markdown Rendering: Renders the message content as markdown with support for GFM and math. + * - Code Highlighting: Supports inline and block code rendering with syntax highlighting. + * + * @param {ChatMessageProps} props - The props for the component. + * @returns {JSX.Element} The rendered chat message component. + */ import { MemoizedReactMarkdown } from '@/components/shared/markdown' import { CodeBlock } from '@/components/ui/codeblock' @@ -28,6 +50,12 @@ export function BrowseChatMessage({ message, chatbot, ...props }: ChatMessagePro p({ children }) { return

{children}

}, + ol({ children }) { + return
    {children}
+ }, + ul({ children }) { + return
    {children}
+ }, code({ node, inline, className, children, ...props }) { if (children.length) { if (children[0] == '▍') { diff --git a/apps/masterbots.ai/components/routes/browse/browse-chat-messages.tsx b/apps/masterbots.ai/components/routes/browse/browse-chat-messages.tsx index 4db4ae13..86527087 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-chat-messages.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-chat-messages.tsx @@ -1,8 +1,20 @@ // Inspired by Chatbot-UI and modified to fit the needs of this project // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatcleanMessage.tsx +/** + * BrowseChatMessages Component + * + * This component fetches and displays chat messages for a specific thread. + * It retrieves messages based on the provided thread ID and renders the chatbot details + * and the list of messages. + * + * Props: + * - threadId: The ID of the thread to fetch messages for. + * - user: Optional user object associated with the messages. + * - chatbot: Optional chatbot object associated with the messages. + */ -import * as AI from 'ai' -import { Chatbot, Message, User } from 'mb-genql' +import type * as AI from 'ai' +import type { Chatbot, Message, User } from 'mb-genql' import React from 'react' import BrowseChatbotDetails from '@/components/routes/browse/browse-chatbot-details' import { BrowseChatMessageList } from '@/components/routes/browse/browse-chat-message-list' @@ -32,12 +44,16 @@ export function BrowseChatMessages({ chatbot?: Chatbot }) { const [messages, setMessages] = React.useState([]) + + // Fetch messages for the specified thread ID const fetchMessages = async () => { if (threadId && !messages.length) { const messages = await getMessages({ threadId: threadId }) setMessages(messages) } } + + // Effect to fetch messages when the thread ID changes React.useEffect(() => { fetchMessages() }, [threadId]) diff --git a/apps/masterbots.ai/components/routes/browse/browse-chatbot-details.tsx b/apps/masterbots.ai/components/routes/browse/browse-chatbot-details.tsx index d40a4c03..42ebc4ae 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-chatbot-details.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-chatbot-details.tsx @@ -1,16 +1,29 @@ -import { Chatbot } from 'mb-genql' -import Image from 'next/image' -import Link from 'next/link' +import ShareLink from '@/components/routes/thread/thread-share-link' import { Separator } from '@/components/ui/separator' -import ShareLink from '@/components/routes/thread/thread-user-actions' +import type { Chatbot } from 'mb-genql' import { toSlug } from 'mb-lib' +import Image from 'next/image' +import Link from 'next/link' +/** + * BrowseChatbotDetails Component + * + * This component displays detailed information about a specific chatbot. + * It includes the chatbot's name, primary category, description, and the number of threads associated with it. + * + * Props: + * - chatbot: An optional Chatbot object containing details about the chatbot. + * + * Key Features: + * - Conditional Rendering: Displays a message if no chatbot data is available. + * - Dynamic URL Generation: Creates a URL for chatting with the chatbot based on its category and name. + * - Responsive Design: Utilizes Tailwind CSS for styling and layout. + */ export default function BrowseChatbotDetails({ chatbot }: { chatbot?: Chatbot }) { - if (!chatbot?.categories?.length) { return
No chatbot data available
} @@ -19,8 +32,9 @@ export default function BrowseChatbotDetails({ return (
-
- + -
-
-
{chatbot?.name}
- -
- {chatbot?.categories[0].category.name}. -
-
-
- {chatbot?.description ?
{chatbot?.description}
: ''} + +
+
+
+
{chatbot?.name}
+
-
- Threads:{' '} - - {chatbot?.threads.length ?? 1} - + +
+ {chatbot?.categories[0].category.name}.
-
-
-
-
+
+
+ {chatbot?.description ?
{chatbot?.description}
: ''} +
+
+ Threads: + + {chatbot?.threads.length ?? 1} + +
+
+ Chat with {chatbot?.name} > -
- {/* - 1.2k - - 375 */} - {/* 17 */} -
+
+ +
+ {
-
- {chatbot?.avatar -
-
) -} +} \ No newline at end of file diff --git a/apps/masterbots.ai/components/routes/browse/browse-list-item.tsx b/apps/masterbots.ai/components/routes/browse/browse-list-item.tsx index 92aecd94..f668afb2 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-list-item.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-list-item.tsx @@ -1,19 +1,40 @@ -import Image from 'next/image' - -import { ChatAccordion } from '@/components/routes/chat/chat-accordion' +/** + * BrowseListItem Component + * + * This component represents a single item in a list of threads for browsing chat interactions. + * It displays the thread's title, user information, and options for interacting with the thread. + * + * Props: + * - thread: A Thread object containing details about the chat thread. + * - loadMore: A function to load more threads when needed. + * - loading: A boolean indicating if threads are currently being loaded. + * - isLast: A boolean indicating if this is the last thread in the list. + * - hasMore: A boolean indicating if there are more threads to load. + * - pageType: An optional string to specify the type of page (e.g., 'bot', 'user'). + * + * Key Features: + * - Accordion Functionality: Allows users to expand/collapse the thread to view messages. + * - Dynamic Navigation: Navigates to the bot's page or user profile when clicked. + * - Infinite Scrolling: Uses Intersection Observer to load more threads as the user scrolls. + * - Message Fetching: Fetches messages for the thread when the accordion is opened. + * - Responsive Design: Utilizes Tailwind CSS for styling and layout. + */ + +import { BrowseAccordion } from '@/components/routes/browse/browse-accordion' +import { BrowseChatMessageList } from '@/components/routes/browse/browse-chat-message-list' +import { ChatbotAvatar } from '@/components/shared/chatbot-avatar' +import { ShortMessage } from '@/components/shared/short-message' +import { Button } from '@/components/ui/button' import { useBrowse } from '@/lib/hooks/use-browse' +import { useThreadSearch } from '@/lib/hooks/use-thread-search' +import { searchThreadContent } from '@/lib/search' import { cn, sleep } from '@/lib/utils' import { getMessages } from '@/services/hasura' -import { Message, Thread } from 'mb-genql' +import type { Message, Thread } from 'mb-genql' import { toSlug } from 'mb-lib' -import Link from 'next/link' +import Image from 'next/image' import { useRouter } from 'next/navigation' import React from 'react' -import { BrowseChatMessageList } from '@/components/routes/browse/browse-chat-message-list' -import { ShortMessage } from '@/components/shared/short-message' -import { IconOpenAI, IconUser } from '@/components/ui/icons' -import { Button } from '@/components/ui/button' -import { icons } from 'lucide-react' import { ChatOptions } from '../chat/chat-options' let initialUrl: string | null = null @@ -36,16 +57,29 @@ export default function BrowseListItem({ const threadRef = React.useRef(null) const router = useRouter() const [messages, setMessages] = React.useState([]) - // ? Move to custom hook and add it to the context useThread + useProvider @bran18 + const { searchTerm } = useThreadSearch() const [isAccordionOpen, setIsAccordionOpen] = React.useState(false) + const [isVisible, setIsVisible] = React.useState(true) + const { tab } = useBrowse() + React.useEffect(() => { + if (!searchTerm) { + setIsVisible(true) + return + } + const matches = searchThreadContent(thread, searchTerm) + setIsVisible(matches) + }, [searchTerm, thread]) + + React.useEffect(() => { if (initialUrl) return initialUrl = location.href }) + // biome-ignore lint/correctness/useExhaustiveDependencies: React.useEffect(() => { initialUrl = location.href }, [tab]) @@ -111,7 +145,7 @@ export default function BrowseListItem({ return (
- {/* Thread Title */} @@ -131,86 +165,68 @@ export default function BrowseListItem({ 'relative flex items-center font-normal md:text-lg transition-all w-full gap-3 pr-4' )} > - {pageType !== 'bot' && thread.chatbot?.avatar ? ( - - ) : ( - pageType !== 'bot' && ( - - ) - )} -
-
-
- {thread.messages?.[0]?.content} -
- {pageType !== 'user' && ( - by - )} - -
- {pageType !== 'user' && thread?.user?.profilePicture ? ( - - ) : ( - pageType !== 'user' && ( +
+ {/* Main content area - adjusted width and spacing */} +
+ {pageType !== 'bot' && ( - ) - )} + )} + + {/* Message content - adjusted spacing */} +
+ {thread.messages?.[0]?.content} +
+ + {/* User section with tighter spacing on mobile */} + {pageType !== 'user' && ( +
+ by + +
+ )}
+ {/* Thread Options */} +
+
- {/* Thread Options */} -
- -
- +
- {/* Thread Description */} + {/* */}
{thread.messages?.[1]?.content && @@ -230,7 +246,7 @@ export default function BrowseListItem({ user={thread?.user || undefined} messages={messages} /> - +
) } diff --git a/apps/masterbots.ai/components/routes/browse/browse-list.tsx b/apps/masterbots.ai/components/routes/browse/browse-list.tsx index d94e569c..ddd2b285 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-list.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-list.tsx @@ -1,18 +1,43 @@ 'use client' + +/** + * BrowseList Component + * + * This component displays a list of chat threads for browsing based on user-selected categories and keywords. + * It allows users to filter threads dynamically and load more content as they scroll. + * + * Key Features: + * - Dynamic Filtering: Filters threads based on the user's input keyword and selected categories. + * - Infinite Scrolling: Loads more threads when the user reaches the end of the list. + * - State Management: Manages loading states and thread counts using React hooks. + * - Responsive Design: Utilizes Tailwind CSS for styling and layout. + * - Integration with Custom Hooks: Uses `useBrowse` for browsing context and `useSidebar` for category selection. + * + * State Variables: + * - threads: An array of Thread objects representing the current list of threads. + * - filteredThreads: An array of Thread objects representing the filtered list based on the keyword. + * - loading: A boolean indicating if threads are currently being loaded. + * - count: A number representing the total number of threads fetched. + */ + import BrowseListItem from '@/components/routes/browse/browse-list-item' +import { NoResults } from '@/components/shared/no-results-card' +import { BrowseListSkeleton } from '@/components/shared/skeletons/browse-list-skeleton' +import { ThreadItemSkeleton } from '@/components/shared/skeletons/browse-skeletons' import { useBrowse } from '@/lib/hooks/use-browse' import { useSidebar } from '@/lib/hooks/use-sidebar' +import { searchThreadContent } from '@/lib/search' import { getBrowseThreads } from '@/services/hasura' import { debounce } from 'lodash' -import { Thread } from 'mb-genql' +import type { Thread } from 'mb-genql' import React from 'react' const PAGE_SIZE = 50 export default function BrowseList() { const { keyword, tab } = useBrowse() - const [threads, setThreads] = React.useState([]) + const [hasInitialized, setHasInitialized] = React.useState(false) const [filteredThreads, setFilteredThreads] = React.useState([]) const [loading, setLoading] = React.useState(false) const [count, setCount] = React.useState(0) @@ -27,14 +52,23 @@ export default function BrowseList() { chatbotsId: number[] keyword: string }) => { - const threads = await getBrowseThreads({ - categoriesId, - chatbotsId, - keyword, - limit: PAGE_SIZE - }) - setThreads(threads) - setCount(threads.length) + setLoading(true) // ? Seting loading before fetch + try { + const threads = await getBrowseThreads({ + categoriesId, + chatbotsId, + keyword, + limit: PAGE_SIZE + }) + setThreads(threads) + setFilteredThreads(threads) + setCount(threads.length) + setHasInitialized(true) // ? Setting hasInitialized after fetch preventing NoResults from showing + } catch (error) { + console.error('Error fetching threads:', error) + } finally { + setLoading(false) + } } const verifyKeyword = () => { @@ -42,16 +76,12 @@ export default function BrowseList() { setFilteredThreads(threads) } else { debounce(() => { - // TODO: Improve thread messages architecture to implement dynamic search to show only the thread title (first message on thread) - // fetchThreads(keyword, selectedCategories) + // Use our searchThreadContent function instead of just title search setFilteredThreads( threads.filter((thread: Thread) => - thread.messages[0]?.content - .toLowerCase() - .includes(keyword.toLowerCase()) + searchThreadContent(thread, keyword) ) ) - // ? Average time of human reaction is 230ms }, 230)() } } @@ -71,26 +101,52 @@ export default function BrowseList() { setLoading(false) } + // biome-ignore lint/correctness/useExhaustiveDependencies: React.useEffect(() => { - fetchThreads({ keyword, categoriesId: selectedCategories, chatbotsId: selectedChatbots }) - }, [selectedCategories.length, selectedChatbots.length]) + fetchThreads({ + keyword, + categoriesId: selectedCategories, + chatbotsId: selectedChatbots + }) + + console.log({ + selectedCategories, + selectedChatbots + }) + }, [selectedCategories, selectedChatbots]) + // biome-ignore lint/correctness/useExhaustiveDependencies: React.useEffect(() => { verifyKeyword() // eslint-disable-next-line react-hooks/exhaustive-deps }, [keyword, threads]) + + if (loading && threads.length === 0) { + return + } + return (
- {filteredThreads.map((thread: Thread, key) => ( - - ))} + {filteredThreads.length > 0 ? ( + <> + {filteredThreads.map((thread: Thread, key) => ( + + ))} + {loading && } + + ) : ( + hasInitialized && + !loading && ( + + ) + )}
) } diff --git a/apps/masterbots.ai/components/routes/browse/browse-search-input.tsx b/apps/masterbots.ai/components/routes/browse/browse-search-input.tsx index 38f5d66d..cf4b8dde 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-search-input.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-search-input.tsx @@ -1,34 +1,86 @@ -/* eslint-disable react/no-unescaped-entities */ 'use client' +/** + * BrowseSearchInput Component + * + * This component provides a search input field for users to filter chat threads based on keywords. + * It allows users to type in a search term and clear the input when needed. + * + * Key Features: + * - Controlled Input: Manages the input value using state to reflect the current search keyword. + * - Dynamic Placeholder: Displays a placeholder text guiding users on what to search for. + * - Clear Functionality: Provides a button to clear the search input, enhancing user experience. + * - Responsive Design: Utilizes Tailwind CSS for styling and layout. + * - Integration with Custom Hooks: Uses `useBrowse and useThreadSearch` to manage the searching. + */ + import { Button } from '@/components/ui/button' import { IconClose } from '@/components/ui/icons' import { Input } from '@/components/ui/input' import { useBrowse } from '@/lib/hooks/use-browse' +import { useThreadSearch } from '@/lib/hooks/use-thread-search' +import { cn } from '@/lib/utils' +import { Search } from 'lucide-react' export function BrowseSearchInput() { - const { keyword, changeKeyword } = useBrowse() + const { searchTerm, setSearchTerm } = useThreadSearch() + const { changeKeyword } = useBrowse() + + const handleSearch = (value: string) => { + setSearchTerm(value) + changeKeyword(value) + } + return ( -
- { - changeKeyword(e.target.value) - }} - placeholder="Search any chat with any Bot" - className="w-full py-6 bg-white dark:bg-[#343434] text-sm font-medium rounded-lg shadow-sm" - /> - {keyword && ( - - )} +
+
+
+
+
+ +
+ + handleSearch(e.target.value)} + placeholder="Search in all messages and threads..." + className={cn( + 'w-full px-12 py-6', + 'bg-transparent', + 'placeholder:text-zinc-400', + 'text-base dark:text-zinc-100', + 'border-0 ring-0 focus-visible:ring-0 focus-visible:ring-offset-0', + 'rounded-full' + )} + /> + {searchTerm && ( + + )} +
+
) } diff --git a/apps/masterbots.ai/components/routes/browse/browse-specific-thread-list.tsx b/apps/masterbots.ai/components/routes/browse/browse-specific-thread-list.tsx index 2fae95bf..2a00bc51 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-specific-thread-list.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-specific-thread-list.tsx @@ -1,8 +1,27 @@ 'use client' +/** + * BrowseSpecificThreadList Component + * + * This component displays a list of specific chat threads based on user queries. + * It allows users to view initial threads and load more threads as needed. + * + * Props: + * - initialThreads: An array of Thread objects representing the initial set of threads to display. + * - query: An object containing query parameters for fetching threads. + * - PAGE_SIZE: A number indicating the maximum number of threads to load at once. + * - pageType: An optional string to specify the type of page (e.g., 'bot', 'user'). + * + * Key Features: + * - State Management: Manages the list of threads, loading state, and total count using React hooks. + * - Load More Functionality: Fetches additional threads when the user requests more content. + * - Responsive Design: Utilizes Tailwind CSS for styling and layout. + * - Integration with Services: Uses the `getBrowseThreads` service to fetch threads based on the query. + */ + import React from 'react' import { getBrowseThreads } from '@/services/hasura' -import { Thread } from 'mb-genql' +import type { Thread } from 'mb-genql' import BrowseListItem from '@/components/routes/browse/browse-list-item' export default function BrowseSpecificThreadList({ diff --git a/apps/masterbots.ai/components/routes/browse/browse-thread.tsx b/apps/masterbots.ai/components/routes/browse/browse-thread.tsx index d6bf33a5..279ffaea 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-thread.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-thread.tsx @@ -1,8 +1,24 @@ 'use client' +/** + * BrowseThread Component + * + * This component displays a specific chat thread, including messages exchanged between the user and the chatbot. + * It merges past assistant and user messages for UI presentation while excluding system prompts. + * + * Props: + * - thread: A Thread object containing details about the chat thread, including messages and associated chatbot/user information. + * - className: An optional string for additional CSS classes to customize the component's styling. + * + * Key Features: + * - Conditional Rendering: Displays chat messages only if there are messages available in the thread. + * - Integration with Chat Messages Component: Utilizes the `BrowseChatMessages` component to render the messages. + * - Responsive Design: Applies Tailwind CSS for styling and layout. + */ + import { cn } from '@/lib/utils' import { BrowseChatMessages } from '@/components/routes/browse/browse-chat-messages' -import { Thread } from 'mb-genql' +import type { Thread } from 'mb-genql' export function BrowseThread({ thread, @@ -11,10 +27,6 @@ export function BrowseThread({ thread: Thread className?: string }) { - // we merge past assistant and user messages for ui only - // we remove system prompts from ui - // we extend append function to add our system prompts - return (
{thread.messages?.length ? ( diff --git a/apps/masterbots.ai/components/routes/browse/browse-user-details.tsx b/apps/masterbots.ai/components/routes/browse/browse-user-details.tsx index 1f69ed58..682f967d 100644 --- a/apps/masterbots.ai/components/routes/browse/browse-user-details.tsx +++ b/apps/masterbots.ai/components/routes/browse/browse-user-details.tsx @@ -1,6 +1,25 @@ 'use client' -import { User } from 'mb-genql' + +/** + * BrowseUserDetails Component + * + * This component displays detailed information about a specific user, including their username and the number of threads associated with them. + * It fetches the number of threads for the user from the backend service and presents the user's profile picture. + * + * Props: + * - user: An optional User object containing details about the user, including their username and profile picture. + * + * ss + * Key Features: + * - Dynamic Thread Count: Fetches and displays the number of threads associated with the user. + * - Responsive Design: Utilizes Tailwind CSS for styling and layout. + * - Conditional Rendering: Displays user details only if the user data is available. + * - Image Handling: Displays the user's profile picture with a fallback for missing images. + */ + + +import type { User } from 'mb-genql' import Image from 'next/image' import { Separator } from '@/components/ui/separator' import { useEffect, useState } from 'react' @@ -14,35 +33,56 @@ export default function BrowseChatbotDetails({ user }: { user?: User | null }) { }) setThreadNum(threads.length) } + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { getThreadByUserName() }, []) return (
-
-
-
- {user?.username?.replace('_', ' ')} -
- +
+ + {/* biome-ignore lint/a11y/noSvgWithoutTitle: */} + + + + Back to browse + -
-
- Threads: {threadNum ?? 1} +
+
+
+ {user?.username?.replace('_', ' ')} +
+ +
+
+ Threads: {threadNum ?? 1} +
-
-
- {user?.username + +
+ {user?.username +
diff --git a/apps/masterbots.ai/components/routes/chat/admin-mode-approve.tsx b/apps/masterbots.ai/components/routes/chat/admin-mode-approve.tsx index de9cf64e..ff7c0657 100644 --- a/apps/masterbots.ai/components/routes/chat/admin-mode-approve.tsx +++ b/apps/masterbots.ai/components/routes/chat/admin-mode-approve.tsx @@ -1,33 +1,44 @@ -import React, { useState } from 'react'; -import { ShieldCheck } from 'lucide-react'; + +//? Component for approving threads in admin mode + import { useThreadVisibility } from '@/lib/hooks/use-thread-visibility'; +import { ShieldCheck } from 'lucide-react'; +import { useState } from 'react'; interface AdminModeApproveProps { - threadId: string; - } + threadId: string; +} + +export function AdminModeApprove({ threadId }: AdminModeApproveProps) { + const { adminApproveThread } = useThreadVisibility(); + const [isLoading, setIsLoading] = useState(false); //* Tracks the loading state during approval + const [error, setError] = useState(null); //* Stores any error message from approval -export function AdminModeApprove({threadId }: AdminModeApproveProps) { - const { adminApproveThread } = useThreadVisibility(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + //* Handles the approval of a thread by calling the adminApproveThread function + const approveThread = async () => { + setIsLoading(true); + try { + await adminApproveThread(threadId); + } catch (err) { + setError('Failed to approve thread. Please try again.'); + } finally { + setIsLoading(false); + } + }; - const approveThread = async () => { - setIsLoading(true); - try { - await adminApproveThread(threadId); - } catch (err) { - setError('Failed to approve thread. Please try again.'); - } finally { - setIsLoading(false); - } - }; - return ( -
- - - {error &&

{error}

} -
- ) + return ( +
+ + {/* biome-ignore lint/a11y/useButtonType: */} + + {error &&

{error}

} +
+ ); } \ No newline at end of file diff --git a/apps/masterbots.ai/components/routes/chat/admin-mode-toggle.tsx b/apps/masterbots.ai/components/routes/chat/admin-mode-toggle.tsx index 13dcac33..9e401eaa 100644 --- a/apps/masterbots.ai/components/routes/chat/admin-mode-toggle.tsx +++ b/apps/masterbots.ai/components/routes/chat/admin-mode-toggle.tsx @@ -1,17 +1,19 @@ -'use client' -import React from 'react'; -import { ShieldCheck, ShieldX } from 'lucide-react'; +//? Component for toggling admin mode on and off + +'use client'; import { Button } from '@/components/ui/button'; import { useThreadVisibility } from '@/lib/hooks/use-thread-visibility'; +import { ShieldCheck, ShieldX } from 'lucide-react'; -export function AdminModeToggle(){ -const { isAdminMode, handleToggleAdminMode } = useThreadVisibility(); +export function AdminModeToggle() { + //* Retrieves admin mode state and toggle handler from useThreadVisibility hook + const { isAdminMode, handleToggleAdminMode } = useThreadVisibility(); return ( - ); -}; \ No newline at end of file +} diff --git a/apps/masterbots.ai/components/routes/chat/chat-accordion.tsx b/apps/masterbots.ai/components/routes/chat/chat-accordion.tsx index 36d0e001..08ef6af8 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-accordion.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-accordion.tsx @@ -1,24 +1,24 @@ -'use client' +//* Component for displaying a collapsible chat thread accordion -import { ChevronDown } from 'lucide-react' -import * as React from 'react' import { useThread } from '@/lib/hooks/use-thread' -import { Thread } from 'mb-genql' +import { ChevronDown } from 'lucide-react' +import type { Thread } from 'mb-genql' +import React from 'react' export const ChatAccordion = ({ - thread = null, - className, - children, - onToggle, - isOpen, - defaultState = false, - triggerClass, - contentClass, - arrowClass, - handleOpen, - handleTrigger, - disabled = false, - ...props + className, //* CSS classes for the outer div + children, //* Child elements representing different parts of the accordion + onToggle, //* Callback triggered when accordion is toggled + isOpen, //* Controlled state to determine if accordion is open + defaultState = false, //* Initial open state of the accordion + triggerClass, //* CSS classes for the trigger button + contentClass, //* CSS classes for the accordion content + arrowClass, //* CSS classes for the arrow icon + handleOpen, //* Handler for when the accordion is opened + handleTrigger, //* Handler for when the trigger is activated + thread = null, //* Thread data associated with the accordion + disabled = false, //* Disables the accordion + ...props //* Additional props spread to the outer div }: { className?: string children: React.ReactNode[] @@ -33,106 +33,93 @@ export const ChatAccordion = ({ thread?: Thread | null disabled?: boolean }) => { - const { activeThread, setActiveThread, setIsNewResponse, isNewResponse, isOpenPopup } = - useThread() + //* Retrieves thread state and setters from useThread hook + const { + setActiveThread, + setIsNewResponse, + setIsOpenPopup, + isNewResponse, + isOpenPopup + } = useThread() - // If the thread is the active, we keep the thread open - let initialState - - if (defaultState) { - initialState = defaultState - } else { - const { threadId: activeThreadId } = activeThread || {} - const { threadId } = thread || {} - initialState = activeThreadId && threadId === activeThreadId - } + //* Sets the initial open state based on defaultState prop + const initialState = defaultState const [open, setOpen] = React.useState(initialState) + const isMainThread = !isOpenPopup - React.useEffect(() => { - if ( - (thread?.threadId && - activeThread !== null && - thread?.threadId !== activeThread?.threadId) || - (activeThread === null && thread?.threadId) - ) { - setOpen(false) - } - }, [activeThread, thread]) + //* Handles click events on the accordion header + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() - React.useEffect(() => { - if (isOpen !== undefined) { - setOpen(isOpen) + if (isMainThread && thread) { + //* Main thread click - open modal + setActiveThread(thread) + setIsOpenPopup(true) + } else { + //* Sub-conversation click - toggle accordion + toggle() } - }, [isOpen]) + } - const toggle = React.useCallback(() => { - setOpen((prevOpen: any) => { + //* Toggles the open state of the accordion with associated business logic + const toggle = () => { + setOpen((prevOpen: boolean) => { const newState = !prevOpen if (!newState && handleOpen) { handleOpen() } - if (thread?.threadId) { + if (thread?.threadId && !isMainThread) { setActiveThread(newState ? thread : null) } - if (isNewResponse) setIsNewResponse(false) + if (isNewResponse) { + setIsNewResponse(false) + } if (onToggle) { onToggle(newState) } return newState }) - }, [handleOpen, thread, isNewResponse, setIsNewResponse, onToggle, setActiveThread]) - - React.useEffect(() => { - if ( - !isOpenPopup && - activeThread && - activeThread.threadId === thread?.threadId && - !open // ? This condition to prevent unnecessary toggles - ) { - toggle() - } - }, [isOpenPopup, activeThread, thread, open, toggle]) + } return ( -
+
{!disabled && ( - + } ${triggerClass || ''}`} + > + {children[0]} + {!open && children[1]} + { + e.stopPropagation() + handleTrigger() + } + } + : {})} + className={`${open ? '' : '-rotate-90'} absolute -right-2 size-4 shrink-0 mr-4 transition-transform duration-200 ${arrowClass || ''} ${disabled ? 'hidden' : ''}`} + /> + )} {open && (
{children[2]}
diff --git a/apps/masterbots.ai/components/routes/chat/chat-chatbot-details.tsx b/apps/masterbots.ai/components/routes/chat/chat-chatbot-details.tsx index 98322d4b..bfa788b4 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-chatbot-details.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-chatbot-details.tsx @@ -1,24 +1,41 @@ -'use client' - -import { Separator } from '@/components/ui/separator' +import { ChatChatbotDetailsSkeleton } from '@/components/shared/skeletons/chat-chatbot-details-skeleton' +import { Button } from '@/components/ui/button' import { useSidebar } from '@/lib/hooks/use-sidebar' import { useThread } from '@/lib/hooks/use-thread' +import { cn } from '@/lib/utils' import { getCategory, getThreads } from '@/services/hasura' +import { MessageCircle, MessageSquare, Users } from 'lucide-react' import { useSession } from 'next-auth/react' import Image from 'next/image' +import { useParams } from 'next/navigation' import { useEffect, useState } from 'react' -export default function ChatChatbotDetails() { +/** + * Displays detailed information about a chatbot or welcome message in the Masterbots application. + * It serves as both a welcome screen for new users and a details card for specific chatbots. + * + * @features + * - Displays welcome message or chatbot information + * - Shows chatbot avatar with customizable border colors for light/dark modes + * - Presents thread count and follower statistics + * - Provides quick actions (Follow, New Chat) + * - Fully responsive design with mobile-first approach + * - Supports both light and dark themes + */ + +export default function ChatChatbotDetails({ page }: { page?: string }) { const { data: session } = useSession() const { activeCategory, activeChatbot } = useSidebar() const { randomChatbot } = useThread() const [threadNum, setThreadNum] = useState(0) const [categoryName, setCategoryName] = useState('') + const { slug } = useParams() + + + if (status === "loading") return - // * Get the number of all threads const getThreadNum = async () => { if (!session?.user) return - const threads = await getThreads({ jwt: session?.user?.hasuraJwt as string, categoryId: activeCategory, @@ -27,70 +44,122 @@ export default function ChatChatbotDetails() { setThreadNum(threads?.length ?? 0) } - // * Get active category name const getCategoryName = async () => { const category = await getCategory({ categoryId: activeCategory as number }) setCategoryName(category.name) } + // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - // ! Only when no active category, should get thread number - // * (FYI: We display this welcome message when there is no thread on the category - - // * So when category selected and no activeChatbot selected, thread number should be 0 all the time.) if (!activeCategory) { getThreadNum() } else { - // ?When category is selected, should get category name getCategoryName() } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeCategory, activeChatbot, session?.user]) + const botName = activeChatbot?.name || 'BuildBot' + return ( -
-
-
-
- {activeChatbot ? activeChatbot?.name : 'Welcome to Masterbots!'} +
+
+ {/* Card Header */} +
+
+ Welcome to Masterbots!
- -
-
- {activeChatbot - ? categoryName - : activeCategory - ? `You are on the ${categoryName} category. Please select one of the bots on the sidebar to start a conversation.` - : 'Please select one of the categories and a bot on the sidebar to start a conversation.'} -
-
- {activeChatbot && activeChatbot?.description ? ( -
{activeChatbot.description}
- ) : ( - '' +
+ + {/* Separator Line - Extended to edges */} +
+ {/* Floating Avatar - Responsive sizing */} +
+
- Threads made:{' '} - - {activeChatbot - ? activeChatbot?.threads?.length ?? 0 - : threadNum} + > + {`${botName} +
+
+
+ + {/* Description with right margin to avoid avatar overlap */} +
+

+ Here you can create new threads and share them to your network! + Navigate with the sidebar and pick any bot of your interest. +

+
+ + {/* Card Content */} +
+
+

+ Your Journey Begins Here! +

+

+ Try and start with: {botName} +

+
+ +
+
+
+ + + Threads:{' '} + + {activeChatbot + ? (activeChatbot?.threads?.length ?? 0) + : threadNum} +
+ +
+
+ + + Followers:{' '} + + 3.2k + + +
+ + +
+ +
-
- {activeChatbot?.avatar -
) diff --git a/apps/masterbots.ai/components/routes/chat/chat-chatbot.tsx b/apps/masterbots.ai/components/routes/chat/chat-chatbot.tsx index 12bf5b74..99b2746a 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-chatbot.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-chatbot.tsx @@ -1,32 +1,18 @@ +//* Component for rendering a chatbot interface + 'use client' -import { Chatbot } from 'mb-genql' -import { useEffect, useState } from 'react' import { Chat } from '@/components/routes/chat/chat' -import { Message } from 'ai/react' -import { useThread } from '@/lib/hooks/use-thread' +import type { Chatbot } from 'mb-genql' export const ChatChatbot = ({ - initialMessages, - chatbot + chatbot //* Chatbot data to interact with }: { - initialMessages?: Message[] chatbot?: Chatbot }) => { - const { isOpenPopup } = useThread() - const [newThreadId, setNewThreadId] = useState(crypto.randomUUID()) - - useEffect(() => { - if (isOpenPopup) return - setNewThreadId(crypto.randomUUID()) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpenPopup]) - return ( ) } diff --git a/apps/masterbots.ai/components/routes/chat/chat-clickable-text.tsx b/apps/masterbots.ai/components/routes/chat/chat-clickable-text.tsx index be62d7a1..99d1d0f4 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-clickable-text.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-clickable-text.tsx @@ -1,51 +1,31 @@ -export function ClickableText({ - children, - isListItem, - sendMessageFromResponse -}: { +import { + cleanClickableText, + extractTextFromReactNode, + parseClickableText +} from '@/lib/utils' + +interface ClickableTextProps { children: React.ReactNode isListItem: boolean sendMessageFromResponse?: (message: string) => void -}) { - const fullText: string = extractTextFromReactNode(children) - // ? This regex matches any variation of the unique key phrases followed by a colon and then captures the following sentence. - const uniquePhrases = [ - 'Unique, lesser-known', - 'Unique insight', - 'Unique Tip', - 'Unique, lesser-known solution', - 'Unique Solution', - 'Unique, lesser-known option', - 'Unique Insight: Lesser-Known Solution', - 'Unique Recommendation', - 'Lesser-Known Gem', - 'For a UNIQUE, LESSER-KNOWN phrase', - 'Unique, Lesser-Known Destination' - ] - const uniquePattern = new RegExp( - `(?:${uniquePhrases.join('|')}):\\s*([^.:]+[.])`, - 'i' - ) - const generalPattern = /(.*?)([:.,])(?:\s|$)/g - // First, check for the UNIQUE pattern - const uniqueMatch = fullText.match(uniquePattern) - let clickableText = uniqueMatch ? uniqueMatch[1] : '' - let restText = uniqueMatch - ? fullText.slice(fullText.indexOf(clickableText) + clickableText.length) - : fullText +} - // If the UNIQUE pattern isn't found, use the general pattern - if (!uniqueMatch) { - const match = fullText.match(generalPattern) - clickableText = match ? match[0] : '' - restText = match ? fullText.slice(match[0].length) : '' - } +/** + * ClickableText component + * Renders phrases as clickable links, triggering a message when clicked. + */ +export function ClickableText({ + children, + isListItem, + sendMessageFromResponse +}: ClickableTextProps) { + const fullText = extractTextFromReactNode(children) + const { clickableText, restText } = parseClickableText(fullText) const handleClick = () => { - if (sendMessageFromResponse && clickableText) { - // ? @brandon -- I am not 100% sure if this would be the best place to put it, but I found this is the only use case for this scenario. - sendMessageFromResponse(`Tell me more about ${clickableText.replace(/(:|\.|\,)\s*$/, '')}`) - // sendMessageFromResponse(clickableText.replace(/(:|\.|\,)\s*$/, '')) + if (sendMessageFromResponse && clickableText.trim()) { + const cleanedText = cleanClickableText(clickableText) + sendMessageFromResponse(`Tell me more about ${cleanedText}`) } } @@ -55,33 +35,17 @@ export function ClickableText({ return ( <> + {/* biome-ignore lint/a11y/useKeyWithClickEvents: Click handler is supplementary */} + role="button" + tabIndex={0} > {clickableText} {restText} ) -} - -function extractTextFromReactNode(node: React.ReactNode): string { - if (typeof node === 'string') { - return node - } - - if (typeof node === 'number') { - return node.toString() - } - - if (Array.isArray(node)) { - return node.map(extractTextFromReactNode).join('') - } - - if (typeof node === 'object' && node !== null && 'props' in node) { - return extractTextFromReactNode(node.props.children) - } - - return '' -} +} \ No newline at end of file diff --git a/apps/masterbots.ai/components/routes/chat/chat-combobox.tsx b/apps/masterbots.ai/components/routes/chat/chat-combobox.tsx index de8ac4da..f42d0b2e 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-combobox.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-combobox.tsx @@ -1,5 +1,7 @@ 'use client' +//* ChatCombobox component allows users to select an AI model, with interactive dropdown and visual indicators. + import * as React from 'react' import { CheckIcon } from '@radix-ui/react-icons' import { Button, buttonVariants } from '@/components/ui/button' @@ -25,9 +27,11 @@ import { } from '@/components/ui/popover' import { useModel } from '@/lib/hooks/use-model' import { AIModels } from '@/app/api/chat/models/models' +import { usePowerUp } from '@/lib/hooks/use-power-up' +//* Model options available in the combobox, each with label, value, and logo icon. const models = [ - { label: 'GPT-4o', value: AIModels.Default, logo: }, + { label: 'GPT-4o', value: AIModels.Default, logo: "MB" }, { label: 'GPT-4', value: AIModels.GPT4, logo: }, { label: 'Claude3', value: AIModels.Claude3, logo: }, { label: 'llama3_8', value: AIModels.llama3_8b, logo: }, @@ -35,22 +39,26 @@ const models = [ { label: 'WordWare', value: AIModels.WordWare, logo: } ] +//* ChatCombobox provides a popover for AI model selection and triggers model change based on user choice. export function ChatCombobox() { const { selectedModel, changeModel } = useModel() const [open, setOpen] = React.useState(false) const [value, setValue] = React.useState(selectedModel as string) + const { isPowerUp } = usePowerUp() return ( + {/* Font Size Selector */} + event.preventDefault()} + > + + + + {/* Toggle thread visibility option (only for thread owner) */} {isUser && ( event.preventDefault()} > @@ -143,14 +167,15 @@ const AlertDialogue = ({ deleteDialogOpen} :{ deleteDialogOpen: boolean}) => ( )} + {/* Share thread option */} event.preventDefault()} > - - {/* */} + + {/* Delete thread option (only for thread owner) */} {isUser && ( ( + + + {/* Web Search Checkbox */} + +
+ +
+ + + ), + uncheck: ( + <> + + Web search disabled + + ) + }} + /> - ) : ( - messages?.length >= 2 && ( + )} + + + + {/* Font Size Selector */} +
+ +
+
+ + {/* Right side controls */} +
+ {showReload && + (isLoading || loadingState ? ( <> - - {id && title && ( - <> - - setShareDialogOpen(false)} - chat={{ - id, - title, - messages - }} - /> - + {loadingState && ( +
+
+
+
+
+
+

+ {loadingState} +

+
)} + - ) - )} + ) : ( + messages?.length >= 2 && ( + <> + + {id && title && ( + <> + + setShareDialogOpen(false)} + chat={{ + id, + title, + messages + }} + /> + + )} + + ) + ))}
- )} +
{ - loadingState?: string; - scrollToBottom: () => void; - id?: string; - title?: string; - chatbot?: Chatbot; - showReload?: boolean; - placeholder: string; - isAtBottom?: boolean; - className?: string; + scrollToBottom: () => void // Function to scroll chat to the bottom + id?: string // Chat ID, used in message operations + title?: string // Chat title, displayed in the header + chatbot?: Chatbot // Chatbot configuration for enabling/disabling prompt form + showReload?: boolean // Displays reload button when true + placeholder: string // Placeholder text for the input field + isAtBottom?: boolean // Indicates if the chat is scrolled to the bottom + className?: string // Optional custom class for styling the panel } export function ChatPanel({ id, title, isLoading, - loadingState, stop, append, reload, @@ -45,20 +45,24 @@ export function ChatPanel({ scrollToBottom, className, }: ChatPanelProps) { - const { isOpenPopup } = useThread(); + const { isOpenPopup, loadingState } = useThread() // State to control popup visibility + return (
-
+
{ @@ -77,7 +88,8 @@ export function ChatPanel({ role: "user", }); }} - disabled={!Boolean(chatbot)} + // biome-ignore lint/complexity/noExtraBooleanCast: + disabled={!Boolean(chatbot) || isLoading || Boolean(loadingState)} input={input} setInput={setInput} isLoading={isLoading} @@ -86,5 +98,5 @@ export function ChatPanel({
- ); + ) } diff --git a/apps/masterbots.ai/components/routes/chat/chat-scroll-anchor.tsx b/apps/masterbots.ai/components/routes/chat/chat-scroll-anchor.tsx index 930c8956..d39c5578 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-scroll-anchor.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-scroll-anchor.tsx @@ -1,13 +1,16 @@ 'use client' +//* ChatScrollAnchor tracks chat scroll position to auto-scroll to the bottom when new messages appear. + import * as React from 'react' import { useInView } from 'react-intersection-observer' interface ChatScrollAnchorProps { - trackVisibility?: boolean - isAtBottom: boolean + trackVisibility?: boolean // Enables visibility tracking to trigger scroll into view + isAtBottom: boolean // Indicates if chat is scrolled to the bottom } +// ChatScrollAnchor auto-scrolls to the anchor position when `isAtBottom` and `trackVisibility` are true. export function ChatScrollAnchor({ trackVisibility, isAtBottom @@ -15,7 +18,7 @@ export function ChatScrollAnchor({ const { ref, entry, inView } = useInView({ trackVisibility, delay: 100, - rootMargin: '0px 0px -150px 0px' + rootMargin: '0px 0px -150px 0px' // Trigger when near the bottom }) React.useEffect(() => { @@ -26,5 +29,5 @@ export function ChatScrollAnchor({ } }, [inView, entry, isAtBottom, trackVisibility]) - return
-} + return
// Invisible anchor for scroll tracking. +} \ No newline at end of file diff --git a/apps/masterbots.ai/components/routes/chat/chat-search-input.tsx b/apps/masterbots.ai/components/routes/chat/chat-search-input.tsx index c6b22d4c..b135b2f6 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-search-input.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-search-input.tsx @@ -1,33 +1,65 @@ 'use client' +// * This component provides a search input for filtering chat threads based on user input. +// * The search results are filtered based on the content in each thread. + import { Button } from '@/components/ui/button' import { IconClose } from '@/components/ui/icons' import { Input } from '@/components/ui/input' import { useSidebar } from '@/lib/hooks/use-sidebar' +import { cn } from '@/lib/utils' import { getCategory } from '@/services/hasura' import { debounce } from 'lodash' -import { Thread } from 'mb-genql' +import { Search } from 'lucide-react' +import type { Thread } from 'mb-genql' import { useParams } from 'next/navigation' import React from 'react' -export function ChatSearchInput({ setThreads }: { +interface ChatSearchInputProps { setThreads: React.Dispatch> -}) { + onSearch?: (term: string) => void +} + +export function ChatSearchInput({ + setThreads, + onSearch +}: ChatSearchInputProps) { const { chatbot } = useParams() const { activeCategory } = useSidebar() - const [searchPlaceholder, setSearchPlaceholder] = React.useState(null) + const [searchPlaceholder, setSearchPlaceholder] = React.useState< + string | null + >(null) const [keyword, changeKeyword] = React.useState('') const previousThread = React.useRef([]) const previousCategory = React.useRef(null) + const handleKeywordChange = (value: string) => { + changeKeyword(value) + onSearch?.(value) // Call the onSearch callback if provided + } + + const clearSearch = () => { + handleKeywordChange('') // Use the same handler for clearing to ensure onSearch is called + } + + const searchInThread = (thread: Thread, searchTerm: string): boolean => { + // If no search term, return true to show all threads + if (!searchTerm) return true + + const lowercaseSearch = searchTerm.toLowerCase() + + // Check all messages in the thread for the search term + return thread.messages.some(message => + message?.content?.toLowerCase().includes(lowercaseSearch) + ) + } + const fetchSearchPlaceholder = async () => { if (chatbot) { setSearchPlaceholder(chatbot as string) } else if (activeCategory && activeCategory !== previousCategory.current) { previousCategory.current = activeCategory - const getCategoryLabel = await getCategory({ categoryId: activeCategory }) - setSearchPlaceholder(`${getCategoryLabel.name.toLowerCase()} category`) } } @@ -37,47 +69,76 @@ export function ChatSearchInput({ setThreads }: { }, [chatbot, activeCategory]) React.useEffect(() => { - debounce(() => { + const debouncedSearch = debounce(() => { setThreads(prevState => { - // ? If there is no results on a search, we should keep the previous state - // ? and if not, the threads previous state before the search will be lost. - previousThread.current = !previousThread.current.length ? prevState : previousThread.current + previousThread.current = !previousThread.current.length + ? prevState + : previousThread.current const previousThreadState = previousThread.current - if (!keyword) { return previousThreadState } - - return previousThreadState.filter((thread: Thread) => - thread.messages[0]?.content - .toLowerCase() - .includes(keyword.toLowerCase()) + return previousThreadState.filter(thread => + searchInThread(thread, keyword) ) }) - }, 230)() - }, [keyword]) + }, 230) + + debouncedSearch() + return () => debouncedSearch.cancel() + }, [keyword, setThreads]) return ( -
- { - changeKeyword(e.target.value) - }} - placeholder={`Search any chat with ${searchPlaceholder ? searchPlaceholder : 'any bot category'}`} - className="max-w-[600px] bg-white dark:bg-[#343434] text-sm font-medium rounded-lg shadow-sm w-full py-6" - /> - {keyword && ( - - )} + + handleKeywordChange(e.target.value)} + placeholder={`Search all messages in ${searchPlaceholder ? searchPlaceholder : 'any category' + }...`} + className={cn( + 'w-full px-12 py-6', + 'bg-transparent', + 'placeholder:text-zinc-400', + 'text-base dark:text-zinc-100', + 'border-0 ring-0 focus-visible:ring-0 focus-visible:ring-offset-0', + 'rounded-full' + )} + /> + {keyword && ( + + )} +
+
) } diff --git a/apps/masterbots.ai/components/routes/chat/chat-share-dialog.tsx b/apps/masterbots.ai/components/routes/chat/chat-share-dialog.tsx index 965ef9a1..a33cb7a4 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-share-dialog.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-share-dialog.tsx @@ -1,9 +1,11 @@ 'use client' +//* ChatShareDialog provides a dialog to share a chat link, allowing users to copy the link to the clipboard. + import * as React from 'react' -import { type DialogProps } from '@radix-ui/react-dialog' +import type { DialogProps } from '@radix-ui/react-dialog' import { toast } from 'react-hot-toast' -import { type Chat } from '@/types/types' +import type { Chat } from '@/types/types' import { Button } from '@/components/ui/button' import { Dialog, @@ -17,14 +19,13 @@ import { IconSpinner } from '@/components/ui/icons' import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' interface ChatShareDialogProps extends DialogProps { - chat: Pick + chat: Pick // Chat details including ID, title, and messages for sharing // shareChat: (id: string) => ServerActionResult - onCopy: () => void + onCopy: () => void // Callback for after the link is copied } export function ChatShareDialog({ chat, - // shareChat, onCopy, ...props }: ChatShareDialogProps) { @@ -69,7 +70,7 @@ export function ChatShareDialog({
{chat.title}
- {chat.messages.length} messages + {chat.messages.length}
@@ -78,12 +79,6 @@ export function ChatShareDialog({ onClick={() => { // @ts-ignore startShareTransition(async () => { - // const result = await shareChat(chat.id) - // if (result && 'error' in result) { - // toast.error(result.error) - // return - // } - // copyShareLink(result) }) }} > @@ -100,4 +95,4 @@ export function ChatShareDialog({ ) -} +} \ No newline at end of file diff --git a/apps/masterbots.ai/components/routes/chat/chat-thread-list-panel.tsx b/apps/masterbots.ai/components/routes/chat/chat-thread-list-panel.tsx index 5a149a39..d4527348 100644 --- a/apps/masterbots.ai/components/routes/chat/chat-thread-list-panel.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat-thread-list-panel.tsx @@ -1,15 +1,11 @@ 'use client' -import { useThread } from '@/lib/hooks/use-thread' +//* ChatThreadListPanel initializes and renders the chat interface with the active thread's messages and chatbot details. + import { Chat } from '@/components/routes/chat/chat' export default function ChatThreadListPanel() { - const { initialMessages, activeThread } = useThread() return ( - + ) -} +} \ No newline at end of file diff --git a/apps/masterbots.ai/components/routes/chat/chat.tsx b/apps/masterbots.ai/components/routes/chat/chat.tsx index 91b501b9..5216d2d8 100644 --- a/apps/masterbots.ai/components/routes/chat/chat.tsx +++ b/apps/masterbots.ai/components/routes/chat/chat.tsx @@ -1,93 +1,82 @@ "use client"; -import { - improveMessage, - subtractChatbotMetadataLabels, -} from "@/app/api/chat/actions/actions"; +/** + * Chat Component + * + * A complex chat interface that handles: + * - Message management for new and existing chat threads + * - Integration with AI models for message processing and responses + * - Loading states for message generation and processing + * - Chatbot configuration and metadata handling + * - Chat history and message persistence + * - ICL integration for metadata extraction and labelling + * - Real-time message streaming and history management + * - Chat thread creation and state management + * - Message improvement and metadata extraction using AI + * - Automatic scrolling behavior + * + * Key Features: + * - Supports both popup and inline chat modes + * - Handles message processing states (processing, digesting, generating, etc.) + * - Manages chat thread creation and persistence + * - Integrates with multiple chatbot models + * - Provides real-time message streaming + * - Maintains chat history and system prompts + * + * State Management: + * - Uses useChat for message handling + * - Manages loading states for UI feedback + * - Tracks scroll position and bottom visibility + * - Handles chat thread state and persistence + */ + +//TODO: Refactor and optimize the Chat component into smaller sections for better performance and readability + import { ChatList } from "@/components/routes/chat/chat-list"; import { ChatPanel } from "@/components/routes/chat/chat-panel"; import { ChatScrollAnchor } from "@/components/routes/chat/chat-scroll-anchor"; -import { botNames } from "@/lib/bots-names"; -import { followingQuestionsPrompt, setDefaultPrompt } from '@/lib/constants/prompts'; +import { botNames } from "@/lib/constants/bots-names"; import { useAtBottom } from "@/lib/hooks/use-at-bottom"; -import { useModel } from "@/lib/hooks/use-model"; +import { useMBChat } from "@/lib/hooks/use-mb-chat"; import { useSidebar } from "@/lib/hooks/use-sidebar"; import { useThread } from "@/lib/hooks/use-thread"; import { cn, scrollToBottomOfElement } from "@/lib/utils"; -import { createThread, getThread, saveNewMessage } from "@/services/hasura"; import type { - AiClientType, - ChatLoadingState, - ChatProps, - CleanPromptResult, + ChatProps } from "@/types/types"; -import type { ChatRequestOptions, CreateMessage } from "ai"; -import { type Message, useChat } from "ai/react"; import { useScroll } from "framer-motion"; -import { uniqBy } from "lodash"; -import { useSession } from "next-auth/react"; +import { Chatbot } from "mb-genql"; import { useParams } from "next/navigation"; import React, { useEffect } from "react"; -import { toast } from "react-hot-toast"; export function Chat({ - initialMessages, + chatbot: chatbotProps, className, - chatbot, - threadId, chatPanelClassName, isPopup, scrollToBottom: scrollToBottomOfPopup, isAtBottom: isAtBottomOfPopup, }: ChatProps) { - const { data: session } = useSession(); const { - allMessages: threadAllMessages, - initialMessages: threadInitialMessages, activeThread, - sendMessageFromResponse, - setActiveThread, - setIsNewResponse, - setIsOpenPopup, + loadingState, isOpenPopup, sectionRef, isAtBottom: isAtBottomOfSection, + setActiveThread, + setIsOpenPopup, + setLoadingState, } = useThread(); const { activeChatbot } = useSidebar(); const containerRef = React.useRef(); - const params = useParams<{ chatbot: string; threadId: string }>(); - const isNewChat = Boolean(!params.threadId && !activeThread); - const { selectedModel, clientType } = useModel(); - const [loadingState, setLoadingState] = React.useState(); - - const { messages, append, reload, stop, isLoading, input, setInput } = - useChat({ - initialMessages: - params.threadId || isNewChat - ? initialMessages?.filter((m) => m.role === "system") - : threadInitialMessages.filter((m) => m.role === "system"), - id: params.threadId || isNewChat ? threadId : activeThread?.threadId, - body: { - id: params.threadId || isNewChat ? threadId : activeThread?.threadId, - model: selectedModel, - clientType, - }, - onResponse(response) { - if (response.status === 401) { - toast.error(response.statusText); - } - }, - onFinish(message) { - saveNewMessage({ - role: "assistant", - threadId: - params.threadId || isNewChat ? threadId : activeThread?.threadId, - content: message.content, - jwt: session!.user?.hasuraJwt, - }); - }, - }); + const chatbot = chatbotProps || activeThread?.chatbot || activeChatbot as Chatbot + const [ + { newChatThreadId: threadId, input, isLoading, allMessages, isNewChat }, + { appendWithMbContextPrompts, reload, setInput } + ] = useMBChat({ + chatbot, + }); const { scrollY } = useScroll({ container: containerRef as React.RefObject, @@ -123,151 +112,6 @@ export function Chat({ } }; - // we merge past assistant and user messages for ui only - // we remove system prompts from ui - const allMessages = - params.threadId || isNewChat - ? uniqBy(initialMessages?.concat(messages), "content").filter( - (m) => m.role !== "system", - ) - : uniqBy(threadAllMessages.concat(messages), "content").filter( - (m) => m.role !== "system", - ); - - // we extend append function to add our system prompts - const appendWithMbContextPrompts = async ( - userMessage: Message | CreateMessage, - chatRequestOptions?: ChatRequestOptions, - ) => { - if (!session?.user || !chatbot) { - console.error("User is not logged in or session expired."); - toast.error("Failed to start conversation. Please reload and try again."); - return; - } - - // * Loading: processing your request... 'processing' - setLoadingState("processing"); - - let processedMessage: CleanPromptResult = setDefaultPrompt( - userMessage.content, - ); - - // * Cleaning the user question (thread title) with AI - try { - console.log("Original message: ", processedMessage); - processedMessage = await improveMessage( - userMessage.content, - clientType as AiClientType, - selectedModel, - ); - // * Loading: getting the the information right... 'digesting' - setLoadingState("digesting"); - - if ( - processedMessage.improved || - processedMessage.improvedText === userMessage.content - ) { - console.warn("Message was not improved by AI. Using original message."); - } - } catch (error) { - console.error("Error processing message:", error); - } - - const { language, originalText, improvedText, translatedText } = - processedMessage; - const userContent = translatedText || improvedText || originalText; - - console.log("Processed Message: ", processedMessage); - - // ! Loading: Generating awesome stuff for you... 'generating' - setLoadingState("generating"); - - // * Getting the user labelling the thread (categories, sub-category, etc.) - const chatMetadata = await subtractChatbotMetadataLabels( - { - domain: chatbot?.categories[0].categoryId, - chatbot: chatbot?.chatbotId, - }, - userContent, - clientType as AiClientType, - ); - console.log( - "Full responses from subtractChatbotMetadataLabels:", - chatMetadata, - ); - - // * Loading: Polishing Ai request... 'polishing' - setLoadingState("polishing"); - - // ! Connecting to the ICL to send the user labelling the thread and rawData (examples) to the ICL - // TODO: ... - const postIclResponse = (await new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve({ - parsed: chatMetadata, - question: userContent, - domain: chatbot?.categories[0].category.name as string, - chatbot: chatbot?.name as string, - }); - clearTimeout(timeout); - }, 700); - })) as { parsed: any; question: string; domain: string; chatbot: string }; - // ! Her we do something with the response from the ICL and attach it to the chat context the required fields and values for future ICL usage. - console.log("Full responses from postICLResponse:", postIclResponse); - - // * Loading: Now I have the information you need... 'ready' - setLoadingState("ready"); - - if (isNewChat && chatbot) { - await createThread({ - threadId, - chatbotId: chatbot.chatbotId, - jwt: session.user?.hasuraJwt, - userId: session.user.id, - isPublic: activeChatbot?.name !== "BlankBot", - }); - const thread = await getThread({ - threadId, - jwt: session.user?.hasuraJwt, - }); - setActiveThread(thread); - setIsOpenPopup(true); - } - if (activeThread?.threadId) { - setIsOpenPopup(true); - } - - await saveNewMessage({ - role: "user", - threadId: - params.threadId || isNewChat ? threadId : activeThread?.threadId, - content: userContent, - jwt: session.user?.hasuraJwt, - }); - - setIsNewResponse(true); - - return append( - isNewChat - ? { ...userMessage, content: userContent } - : { - ...userMessage, - content: followingQuestionsPrompt(userContent, allMessages), - }, - ).then((response) => { - // * Loading: Here is the information you need... 'finish' - setLoadingState("finished"); - - const timeout = setTimeout(() => { - scrollToBottom(); - setLoadingState(undefined); - clearTimeout(timeout); - }, 750); - - return response; - }); - }; - useEffect(() => { if ( params.chatbot && @@ -288,7 +132,7 @@ export function Chat({ }, 150); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading, isOpenPopup, scrollToBottomOfPopup]); + }, [isLoading, isOpenPopup]); useEffect(() => { if (!isLoading && loadingState) { @@ -304,11 +148,7 @@ export function Chat({ ref={containerRef as React.Ref} className={cn("pb-[200px] pt-4 md:pt-10 h-full overflow-auto", className)} > - + { @@ -28,7 +55,8 @@ export function PromptForm({ placeholder, disabled }: PromptProps) { - const { isOpenPopup } = useThread() + const { isOpenPopup, activeThread } = useThread() + const { activeChatbot } = useSidebar() const { formRef, onKeyDown } = useEnterSubmit() const inputRef = React.useRef(null) const [isFocused, setIsFocused] = React.useState(false) @@ -60,8 +88,11 @@ export function PromptForm({ ref={formRef} >