Skip to content

Commit b5ddf36

Browse files
authored
feat: support markdown when renderAsPills is true (#424)
1 parent d299e47 commit b5ddf36

File tree

3 files changed

+145
-7
lines changed

3 files changed

+145
-7
lines changed

example/src/samples/sample-data.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,11 +2035,13 @@ mkdir -p src/ lalalaaaa sad fbnsafsdaf sdakjfsd sadf asdkljf basdkjfh ksajhf kjs
20352035
details: {
20362036
'src/components/ui': {
20372037
visibleName: 'ui',
2038-
description: 'src/components/ui'
2038+
description: 'src/components/ui',
2039+
clickable: false
20392040
},
20402041
'src/components/forms': {
20412042
visibleName: 'forms',
2042-
description: 'src/components/forms'
2043+
description: 'src/components/forms',
2044+
clickable: false
20432045
},
20442046
},
20452047
renderAsPills: true
@@ -2059,27 +2061,55 @@ mkdir -p src/ lalalaaaa sad fbnsafsdaf sdakjfsd sadf asdkljf basdkjfh ksajhf kjs
20592061
'src/components/ui': {
20602062
visibleName: 'ui',
20612063
description: 'src/components/ui',
2064+
clickable: false
20622065
},
20632066
'src/components/forms': {
20642067
visibleName: 'forms',
2065-
description: 'src/components/forms'
2068+
description: 'src/components/forms',
2069+
clickable: false
20662070
},
20672071
'src/components/layout': {
20682072
visibleName: 'layout',
2069-
description: 'src/components/layout'
2073+
description: 'src/components/layout',
2074+
clickable: false
20702075
},
20712076
'src/utils/helpers': {
20722077
visibleName: 'helpers',
2073-
description: 'src/components/helpers'
2078+
description: 'src/components/helpers',
2079+
clickable: false
20742080
},
20752081
'src/utils/validation': {
20762082
visibleName: 'validation',
2077-
description: 'src/components/validation'
2083+
description: 'src/components/validation',
2084+
clickable: false
20782085
},
20792086
},
20802087
renderAsPills: true
20812088
}
20822089
}
2090+
},
2091+
{
2092+
type: ChatItemType.ANSWER,
2093+
fullWidth: true,
2094+
padding: false,
2095+
header: {
2096+
icon: 'search',
2097+
body: 'Searched for `*.md` in',
2098+
fileList: {
2099+
filePaths: ['src/docs'],
2100+
details: {
2101+
['src/docs']: {
2102+
visibleName: 'docs',
2103+
description: 'src/docs',
2104+
clickable: false
2105+
}
2106+
},
2107+
renderAsPills: true
2108+
},
2109+
status: {
2110+
text: '5 results found'
2111+
}
2112+
},
20832113
}
20842114
];
20852115

src/components/__test__/chat-item/chat-item-card.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChatItemCard } from '../../chat-item/chat-item-card';
22
import { ChatItemType } from '../../../static';
3+
import { MynahUIGlobalEvents } from '../../../helper/events';
34

45
// Mock the tabs store
56
jest.mock('../../../helper/tabs-store', () => ({
@@ -13,7 +14,29 @@ jest.mock('../../../helper/tabs-store', () => ({
1314
}
1415
}));
1516

17+
// Mock global events
18+
jest.mock('../../../helper/events', () => ({
19+
MynahUIGlobalEvents: {
20+
getInstance: jest.fn(() => ({
21+
dispatch: jest.fn()
22+
}))
23+
}
24+
}));
25+
1626
describe('ChatItemCard', () => {
27+
let mockDispatch: jest.Mock;
28+
29+
beforeEach(() => {
30+
mockDispatch = jest.fn();
31+
(MynahUIGlobalEvents.getInstance as jest.Mock).mockReturnValue({
32+
dispatch: mockDispatch
33+
});
34+
});
35+
36+
afterEach(() => {
37+
jest.clearAllMocks();
38+
});
39+
1740
it('should render basic chat item card', () => {
1841
const card = new ChatItemCard({
1942
tabId: 'test-tab',
@@ -115,6 +138,32 @@ describe('ChatItemCard', () => {
115138
expect(deletedPill).toBeTruthy();
116139
expect(deletedPill?.textContent).toBe('deleted.ts');
117140
});
141+
142+
it('should not dispatch click events when clickable is false', () => {
143+
const card = new ChatItemCard({
144+
tabId: 'test-tab',
145+
chatItem: {
146+
type: ChatItemType.ANSWER,
147+
header: {
148+
body: 'Files',
149+
fileList: {
150+
filePaths: [ 'test.js' ],
151+
details: {
152+
'test.js': {
153+
clickable: false
154+
}
155+
},
156+
renderAsPills: true
157+
}
158+
}
159+
}
160+
});
161+
162+
const pillElement = card.render.querySelector('.mynah-chat-item-tree-file-pill') as HTMLElement;
163+
pillElement?.click();
164+
165+
expect(mockDispatch).not.toHaveBeenCalled();
166+
});
118167
});
119168

120169
it('should not render pills when renderAsPills is false', () => {
@@ -173,4 +222,23 @@ describe('ChatItemCard', () => {
173222
const pillElements = card.render.querySelectorAll('.mynah-chat-item-tree-file-pill');
174223
expect(pillElements.length).toBe(0);
175224
});
225+
226+
it('should parse markdown in header body for pills', () => {
227+
const card = new ChatItemCard({
228+
tabId: 'test-tab',
229+
chatItem: {
230+
type: ChatItemType.ANSWER,
231+
header: {
232+
body: 'Reading `inline code` text',
233+
fileList: {
234+
filePaths: [ 'test.js' ],
235+
renderAsPills: true
236+
}
237+
}
238+
}
239+
});
240+
241+
const codeElement = card.render.querySelector('.mynah-inline-code');
242+
expect(codeElement).toBeTruthy();
243+
});
176244
});

src/components/chat-item/chat-item-card.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { MoreContentIndicator } from '../more-content-indicator';
2727
import { Button } from '../button';
2828
import { Overlay, OverlayHorizontalDirection, OverlayVerticalDirection } from '../overlay';
2929
import { marked } from 'marked';
30+
import { parseMarkdown } from '../../helper/marked';
3031

3132
const TOOLTIP_DELAY = 350;
3233
export interface ChatItemCardProps {
@@ -258,9 +259,45 @@ export class ChatItemCard {
258259

259260
// Add body text if present
260261
if (header.body != null && header.body !== '') {
262+
// Parse markdown to handle inline code
263+
const parsedHtml = parseMarkdown(header.body, { includeLineBreaks: true });
264+
265+
// Create a temporary div to extract text content while preserving inline code
266+
const tempDiv = document.createElement('div');
267+
tempDiv.innerHTML = parsedHtml;
268+
269+
// Convert to ChatItemBodyRenderer format
270+
const processNode = (node: Node): Array<string | ChatItemBodyRenderer> => {
271+
if (node.nodeType === Node.TEXT_NODE) {
272+
return [ node.textContent ?? '' ];
273+
} else if (node.nodeType === Node.ELEMENT_NODE) {
274+
const element = node as HTMLElement;
275+
if (element.tagName.toLowerCase() === 'code') {
276+
return [ {
277+
type: 'code' as const,
278+
classNames: [ 'mynah-syntax-highlighter', 'mynah-inline-code' ],
279+
children: [ element.textContent ?? '' ]
280+
} ];
281+
} else {
282+
// For other elements, process their children
283+
const children: Array<string | ChatItemBodyRenderer> = [];
284+
Array.from(element.childNodes).forEach(child => {
285+
children.push(...processNode(child));
286+
});
287+
return children;
288+
}
289+
}
290+
return [ '' ];
291+
};
292+
293+
const children: Array<string | ChatItemBodyRenderer> = [];
294+
Array.from(tempDiv.childNodes).forEach(node => {
295+
children.push(...processNode(node));
296+
});
297+
261298
customRenderer.push({
262299
type: 'span' as const,
263-
children: [ header.body ]
300+
children
264301
});
265302
}
266303

@@ -279,6 +316,9 @@ export class ChatItemCard {
279316
children: [ fileName ],
280317
events: {
281318
click: () => {
319+
if (header.fileList?.details?.[filePath]?.clickable === false) {
320+
return;
321+
}
282322
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FILE_CLICK, {
283323
tabId: this.props.tabId,
284324
messageId: this.props.chatItem.messageId,

0 commit comments

Comments
 (0)