Skip to content

Commit af9fc65

Browse files
committed
chore: clickable responses update
1 parent f47bf53 commit af9fc65

File tree

4 files changed

+309
-92
lines changed

4 files changed

+309
-92
lines changed

apps/masterbots.ai/app/globals.css

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,97 @@
141141
[data-font-size='x-large'] {
142142
font-size: calc(var(--font-size-base) * 1.4);
143143
}
144+
145+
/* Enhanced nested list styles */
146+
.nested-list {
147+
@apply ml-2 space-y-2 text-foreground;
148+
}
149+
150+
/* First level items */
151+
.nested-list > li {
152+
@apply relative ml-4;
153+
padding-left: 1.5rem;
154+
}
155+
156+
/* Second level lists */
157+
.nested-list .nested-list {
158+
@apply mt-2 ml-4 space-y-1.5;
159+
}
160+
161+
/* Second level items */
162+
.nested-list .nested-list > li {
163+
@apply relative ml-4;
164+
padding-left: 1.5rem;
165+
}
166+
167+
/* Third level lists */
168+
.nested-list .nested-list .nested-list {
169+
@apply mt-2 ml-6 space-y-1;
170+
}
171+
172+
/* Third level items */
173+
.nested-list .nested-list .nested-list > li {
174+
@apply relative ml-4;
175+
padding-left: 1.5rem;
176+
}
177+
178+
/* Custom list markers using pseudo-elements */
179+
.nested-list.list-disc > li::before {
180+
content: "•";
181+
@apply absolute left-0 text-muted-foreground;
182+
}
183+
184+
.nested-list.list-decimal > li {
185+
counter-increment: list-counter;
186+
}
187+
188+
.nested-list.list-decimal > li::before {
189+
content: counter(list-counter) ".";
190+
@apply absolute left-0 text-muted-foreground;
191+
}
192+
193+
/* Clickable elements styling */
194+
.nested-list .text-link,
195+
.nested-list .text-blue-500 {
196+
@apply inline-flex items-center transition-colors duration-200 cursor-pointer hover:underline text-accent hover:text-accent/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
197+
}
198+
199+
/* Dark mode adjustments */
200+
.dark .nested-list .text-link,
201+
.dark .nested-list .text-blue-500 {
202+
@apply text-accent hover:text-accent/80;
203+
}
204+
205+
/* Spacing for items with nested content */
206+
.nested-list li:has(> .nested-list) {
207+
@apply mt-2 mb-1;
208+
}
209+
210+
/* Interactive states */
211+
.nested-list li:hover > .text-link,
212+
.nested-list li:hover > .text-blue-500 {
213+
@apply underline;
214+
}
215+
216+
/* Focus styles for accessibility */
217+
.nested-list li:focus-within {
218+
@apply rounded outline-none ring-1 ring-ring ring-offset-1;
219+
}
220+
221+
/* Ensure proper spacing between text and nested lists */
222+
.nested-list li > *:first-child {
223+
@apply inline-block;
224+
}
225+
226+
/* Handle markdown content within list items */
227+
.nested-list li p {
228+
@apply inline;
229+
}
230+
231+
/* Preserve spacing when list items contain multiple paragraphs */
232+
.nested-list li p + p {
233+
@apply block mt-2;
234+
}
144235
}
145236

146237
.scrollbar {
@@ -315,7 +406,7 @@
315406
margin-left: 300px;
316407
}
317408
}
318-
/* // black when it's dark mode and white when it's light mode */
409+
319410
.p-FieldLabel.Label {
320411
color: green !important;
321412
}

apps/masterbots.ai/components/routes/chat/chat-clickable-text.tsx

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
extractTextFromReactNodeNormal,
44
extractTextFromReactNodeWeb,
55
parseClickableText,
6-
transformLink,
6+
transformLink
77
} from '@/lib/clickable-results'
88
import { useThread } from '@/lib/hooks/use-thread'
99
import { cn } from '@/lib/utils'
@@ -15,7 +15,7 @@ export function ClickableText({
1515
isListItem,
1616
sendMessageFromResponse,
1717
webSearchResults = [],
18-
onReferenceFound,
18+
onReferenceFound
1919
}: ClickableTextProps) {
2020
const { webSearch } = useThread()
2121

@@ -26,33 +26,77 @@ export function ClickableText({
2626
const createClickHandler = (text: string) => () => {
2727
if (sendMessageFromResponse && text.trim()) {
2828
const cleanedText = cleanClickableText(text)
29-
sendMessageFromResponse(`Explain more in-depth and in detail about ${cleanedText}`)
30-
// sendMessageFromResponse(`Tell me more about ${cleanedText}`)
29+
sendMessageFromResponse(
30+
`Explain more in-depth and in detail about ${cleanedText}`
31+
)
32+
}
33+
}
34+
35+
const processNestedContent = (content: React.ReactNode): React.ReactNode => {
36+
if (React.isValidElement(content)) {
37+
if (content.type === 'ul' || content.type === 'ol') {
38+
return React.cloneElement(content, {
39+
...content.props,
40+
className: cn(
41+
content.props.className,
42+
'mt-2 ml-4',
43+
content.type === 'ul' ? 'list-disc' : 'list-decimal',
44+
'nested-list'
45+
),
46+
children: React.Children.map(content.props.children, child =>
47+
processNestedContent(child)
48+
)
49+
})
50+
}
51+
52+
if (content.type === 'li') {
53+
const hasNestedList = React.Children.toArray(
54+
content.props.children
55+
).some(
56+
child =>
57+
React.isValidElement(child) &&
58+
(child.type === 'ul' || child.type === 'ol')
59+
)
60+
61+
return React.cloneElement(content, {
62+
...content.props,
63+
className: cn(
64+
content.props.className,
65+
'ml-4',
66+
hasNestedList && 'mt-2'
67+
),
68+
children: processNestedContent(content.props.children)
69+
})
70+
}
3171
}
72+
73+
return content
3274
}
3375

3476
const processLink = (linkElement: React.ReactElement) => {
3577
const href = linkElement.props.href
36-
// Buscar la referencia correspondiente
37-
const reference = webSearchResults.find((result) => result.url === href)
78+
const reference = webSearchResults.find(result => result.url === href)
3879

3980
if (reference && onReferenceFound) {
4081
onReferenceFound(reference)
41-
return null // Remover el link inline
82+
return null
4283
}
4384

44-
return linkElement // Mantener links que no son de búsqueda web
85+
return linkElement
4586
}
4687

4788
const renderClickableContent = (clickableText: string, restText: string) => (
4889
<>
4990
<span
50-
className={cn('cursor-pointer hover:underline bg-transparent border-none p-0 m-0', isListItem ? 'text-blue-500' : 'text-link')}
51-
// biome-ignore lint/a11y/useSemanticElements: it is required to have a span to not break the text
91+
className={cn(
92+
'cursor-pointer hover:underline bg-transparent border-none p-0 m-0',
93+
isListItem ? 'text-blue-500' : 'text-link'
94+
)}
95+
// biome-ignore lint/a11y/useSemanticElements: <explanation>
5296
role="button"
5397
tabIndex={0}
5498
onClick={createClickHandler(clickableText)}
55-
onKeyUp={(e) => e.key === 'Enter' && createClickHandler(clickableText)()}
99+
onKeyUp={e => e.key === 'Enter' && createClickHandler(clickableText)()}
56100
>
57101
{clickableText}
58102
</span>
@@ -63,23 +107,32 @@ export function ClickableText({
63107
if (Array.isArray(extractedContent)) {
64108
return extractedContent.map((content, index) => {
65109
if (React.isValidElement(content)) {
110+
if (content.type === 'ul' || content.type === 'ol') {
111+
return processNestedContent(content)
112+
}
113+
66114
if (content.type === 'a' && webSearch) {
67115
return processLink(content)
68116
}
69-
// Manejo de elementos strong
117+
70118
if (content.type === 'strong') {
71119
const strongContent = extractTextFromReactNodeNormal(
72-
(content.props as { children: React.ReactNode }).children,
120+
(content.props as { children: React.ReactNode }).children
121+
)
122+
const { clickableText, restText } = parseClickableText(
123+
strongContent + ':'
73124
)
74-
const { clickableText, restText } = parseClickableText(strongContent + ':')
75125

76126
if (clickableText.trim()) {
77127
return (
78128
<button
79-
key={`clickable-${index}`}
129+
key={`clickable-${
130+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
131+
index
132+
}`}
80133
className={cn(
81134
'cursor-pointer hover:underline',
82-
isListItem ? 'text-blue-500' : 'text-link',
135+
isListItem ? 'text-blue-500' : 'text-link'
83136
)}
84137
onClick={createClickHandler(clickableText)}
85138
type="button"
@@ -92,19 +145,19 @@ export function ClickableText({
92145
return content
93146
}
94147

95-
// Manejo de links cuando webSearch está activo
96148
if (content.type === 'a' && webSearch) {
97149
const parentContext = extractedContent
98150
.filter(
99-
(item) =>
100-
typeof item === 'string' || (React.isValidElement(item) && item.type === 'strong'),
151+
item =>
152+
typeof item === 'string' ||
153+
(React.isValidElement(item) && item.type === 'strong')
101154
)
102-
.map((item) =>
155+
.map(item =>
103156
typeof item === 'string'
104157
? item
105158
: extractTextFromReactNodeNormal(
106-
(item.props as { children: React.ReactNode }).children,
107-
),
159+
(item.props as { children: React.ReactNode }).children
160+
)
108161
)
109162
.join(' ')
110163
return transformLink(content, parentContext)
@@ -120,7 +173,12 @@ export function ClickableText({
120173
}
121174

122175
return (
123-
<React.Fragment key={`clickable-${index}`}>
176+
<React.Fragment
177+
key={`clickable-${
178+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
179+
index
180+
}`}
181+
>
124182
{renderClickableContent(clickableText, restText)}
125183
</React.Fragment>
126184
)
@@ -131,7 +189,9 @@ export function ClickableText({
131189
return extractedContent
132190
}
133191

134-
const { clickableText, restText } = parseClickableText(String(extractedContent))
192+
const { clickableText, restText } = parseClickableText(
193+
String(extractedContent)
194+
)
135195

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

0 commit comments

Comments
 (0)