Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve clickable text with sentence content #388

Merged
merged 3 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions apps/masterbots.ai/components/routes/chat/chat-clickable-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export function ClickableText({
? extractTextFromReactNodeWeb(children)
: extractTextFromReactNodeNormal(children)

const createClickHandler = (text: string) => () => {
const createClickHandler = (text: string, fullContext: string) => () => {
if (sendMessageFromResponse && text.trim()) {
const cleanedText = cleanClickableText(text)
const contextToUse = fullContext || cleanedText
sendMessageFromResponse(
`Explain more in-depth and in detail about ${cleanedText}`
`Explain more in-depth and in detail about ${contextToUse}`
)
}
}
Expand Down Expand Up @@ -85,14 +86,18 @@ export function ClickableText({
return linkElement
}

const renderClickableContent = (clickableText: string, restText: string) => (
const renderClickableContent = (
clickableText: string,
restText: string,
fullContext: string
) => (
<span className="inline">
<button
className={cn(
'inline-block cursor-pointer hover:underline bg-transparent border-none p-0 m-0 text-left',
isListItem ? 'text-blue-500' : 'text-link'
)}
onClick={createClickHandler(clickableText)}
onClick={createClickHandler(clickableText, fullContext)}
type="button"
>
{clickableText}
Expand All @@ -116,7 +121,7 @@ export function ClickableText({
const strongContent = extractTextFromReactNodeNormal(
(content.props as { children: React.ReactNode }).children
)
const { clickableText, restText } = parseClickableText(
const { clickableText, restText, fullContext } = parseClickableText(
strongContent + ':'
)

Expand All @@ -126,12 +131,15 @@ export function ClickableText({
key={`clickable-${
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
index
}`}
}`}
className={cn(
'cursor-pointer hover:underline',
isListItem ? 'text-blue-500' : 'text-link'
)}
onClick={createClickHandler(clickableText)}
onClick={createClickHandler(
clickableText,
fullContext || strongContent
)}
type="button"
tabIndex={0}
>
Expand All @@ -153,8 +161,8 @@ export function ClickableText({
typeof item === 'string'
? item
: extractTextFromReactNodeNormal(
(item.props as { children: React.ReactNode }).children
)
(item.props as { children: React.ReactNode }).children
)
)
.join(' ')
return transformLink(content, parentContext)
Expand All @@ -163,7 +171,9 @@ export function ClickableText({
return content
}

const { clickableText, restText } = parseClickableText(String(content))
const { clickableText, restText, fullContext } = parseClickableText(
String(content)
)

if (!clickableText.trim()) {
return content
Expand All @@ -174,9 +184,9 @@ export function ClickableText({
key={`clickable-${
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
index
}`}
}`}
>
{renderClickableContent(clickableText, restText)}
{renderClickableContent(clickableText, restText, fullContext)}
</React.Fragment>
)
})
Expand All @@ -186,13 +196,13 @@ export function ClickableText({
return extractedContent
}

const { clickableText, restText } = parseClickableText(
const { clickableText, restText, fullContext } = parseClickableText(
String(extractedContent)
)

if (!clickableText.trim()) {
return <>{extractedContent}</>
}

return renderClickableContent(clickableText, restText)
return renderClickableContent(clickableText, restText, fullContext)
}
38 changes: 26 additions & 12 deletions apps/masterbots.ai/lib/clickable-results.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ParsedText } from '@/types/types'
import type { ReactNode } from 'react'
import React from 'react'

Expand Down Expand Up @@ -25,11 +26,6 @@ export function createUniquePattern(): RegExp {
return new RegExp(`(?:${UNIQUE_PHRASES.join('|')}):\\s*([^.:]+[.])`, 'i')
}

export interface ParsedText {
clickableText: string
restText: string
}

export function extractTextFromReactNodeWeb(node: ReactNode): ReactNode {
if (React.isValidElement(node)) {
if (node.type === 'strong') {
Expand Down Expand Up @@ -137,22 +133,29 @@ export function extractTextFromReactNodeNormal(node: ReactNode): string {
}

export function parseClickableText(fullText: string): ParsedText {
// Skip URLs
if (typeof fullText === 'string' && fullText.match(/https?:\/\/[^\s]+/)) {
return {
clickableText: '',
restText: fullText,
fullContext: fullText,
}
}

// First check for unique phrases
//* First check for unique phrases
for (const phrase of UNIQUE_PHRASES) {
if (fullText.includes(phrase)) {
// Split content after the phrase
const [_, ...rest] = fullText.split(phrase)
//* Split content after the phrase
const parts = fullText.split(phrase)
const restContent = parts.slice(1).join(phrase).trim()

//* Extract first sentence (up to the first period after the phrase)
const firstSentenceMatch = (phrase + restContent).match(/^(.+?\.)(?:\s|$)/)
const firstSentence = firstSentenceMatch ? firstSentenceMatch[1] : phrase + restContent

return {
clickableText: phrase,
restText: rest.join(phrase), // Rejoin in case phrase appears multiple times
restText: restContent,
fullContext: firstSentence, //* Use first sentence instead as context
}
}
}
Expand All @@ -162,27 +165,38 @@ export function parseClickableText(fullText: string): ParsedText {

if (titleMatch) {
const title = titleMatch[1].trim()
const content = titleMatch[2]

if (!title || title.match(/^[.\s]+$/)) {
return {
clickableText: '',
restText: fullText,
fullContext: fullText,
}
}

// * Extract the first sentence of the content (up to the first period)
const firstSentenceMatch = content.match(/^(.+?\.)(?:\s|$)/);
const firstSentence = firstSentenceMatch
? title + ': ' + firstSentenceMatch[1]
: title + ': ' + content;

return {
clickableText: title,
restText: ': ' + titleMatch[2],
restText: ': ' + content,
fullContext: firstSentence, // * Use title + first sentence instead of full context
}
}

return {
clickableText: '',
restText: fullText,
fullContext: fullText,
}
}


export function cleanClickableText(text: string): string {
// Remove trailing punctuation and whitespace
return text.replace(/[,.()[\]]$/, '').trim()
}

Expand Down
5 changes: 0 additions & 5 deletions apps/masterbots.ai/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,6 @@ export function removeSurroundingQuotes(str: string) {
return str
}

export interface ParsedText {
clickableText: string
restText: string
}

// * Converts ReactNode content to string for processing
export function extractTextFromReactNode(node: ReactNode): string {
if (typeof node === 'string') return node
Expand Down
6 changes: 6 additions & 0 deletions apps/masterbots.ai/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,9 @@ export interface ClassifyQuestionParams {
retryCount?: number
domain?: string
}

export interface ParsedText {
clickableText: string; // The text that appears clickable
restText: string; // The text that follows (for visual rendering)
fullContext: string; // The full sentence context for the follow-up question
}