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

fix markdown request renderer #14211

Merged
merged 6 commits into from
Sep 30, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@
// *****************************************************************************

import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
import { inject, injectable } from '@theia/core/shared/inversify';
import { injectable } from '@theia/core/shared/inversify';
import {
ChatResponseContent,
InformationalChatResponseContent,
MarkdownChatResponseContent,
} from '@theia/ai-chat/lib/common';
import { ReactNode, useEffect, useRef } from '@theia/core/shared/react';
import * as React from '@theia/core/shared/react';
import * as markdownit from '@theia/core/shared/markdown-it';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';

@injectable()
export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownChatResponseContent | InformationalChatResponseContent> {
@inject(MarkdownRenderer) private renderer: MarkdownRenderer;
protected readonly markdownIt = markdownit();
canHandle(response: ChatResponseContent): number {
if (MarkdownChatResponseContent.is(response)) {
return 10;
Expand All @@ -38,34 +39,52 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownCh
}
return -1;
}
private renderMarkdown(md: MarkdownString): HTMLElement {
return this.renderer.render(md).element;
}
render(response: MarkdownChatResponseContent | InformationalChatResponseContent): ReactNode {
// TODO let the user configure whether they want to see informational content
if (InformationalChatResponseContent.is(response)) {
// null is valid in React
// eslint-disable-next-line no-null/no-null
return null;
}
return <MarkdownWrapper data={response.content} renderCallback={this.renderMarkdown.bind(this)}></MarkdownWrapper>;

return <MarkdownRender response={response} />;
}

}

export const MarkdownWrapper = (props: { data: MarkdownString, renderCallback: (md: MarkdownString) => HTMLElement }) => {
// eslint-disable-next-line no-null/no-null
const ref: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | InformationalChatResponseContent }) => {
const ref = useMarkdownRendering(response.content);

useEffect(() => {
const myDomElement = props.renderCallback(props.data);
return <div ref={ref}></div>;
};

/**
* This hook uses markdown-it directly to render markdown.
* The reason to use markdown-it directly is that the MarkdownRenderer is
* overriden by theia with a monaco version. This monaco version strips all html
* tags from the markdown with empty content.
* This leads to unexpected behavior when rendering markdown with html tags.
*
* @param markdown the string to render as markdown
* @returns the ref to use in an element to render the markdown
*/
export const useMarkdownRendering = (markdown: string | MarkdownString) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement | null>(null);
Comment on lines +72 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a benefit of using null over undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ref paramerter wants null, can't be used with undefined

const markdownString = typeof markdown === 'string' ? markdown : markdown.value;
useEffect(() => {
const markdownIt = markdownit();
const host = document.createElement('div');
const html = markdownIt.render(markdownString);
host.innerHTML = DOMPurify.sanitize(html, {
ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs
});
while (ref?.current?.firstChild) {
ref.current.removeChild(ref.current.firstChild);
}

ref?.current?.appendChild(myDomElement);
}, [props.data.value]);
ref?.current?.appendChild(host);
}, [markdownString]);

return <div ref={ref}></div>;
return ref;
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
TreeProps,
TreeWidget,
} from '@theia/core/lib/browser';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
import {
inject,
injectable,
Expand All @@ -44,10 +43,9 @@ import {
} from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';

import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution';
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
import { MarkdownWrapper } from '../chat-response-renderer/markdown-part-renderer';
import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';

// TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model
export interface RequestNode extends TreeNode {
Expand Down Expand Up @@ -76,9 +74,6 @@ export class ChatViewTreeWidget extends TreeWidget {
@inject(ContributionProvider) @named(ChatNodeToolbarActionContribution)
protected readonly chatNodeToolbarActionContributions: ContributionProvider<ChatNodeToolbarActionContribution>;

@inject(MarkdownRenderer)
private renderer: MarkdownRenderer;

@inject(ChatAgentService)
protected chatAgentService: ChatAgentService;

Expand Down Expand Up @@ -336,16 +331,7 @@ export class ChatViewTreeWidget extends TreeWidget {
}

private renderChatRequest(node: RequestNode): React.ReactNode {
const text = node.request.request.displayText ?? node.request.request.text;
const markdownString = new MarkdownStringImpl(text, { supportHtml: true, isTrusted: true });
return (
<div className={'theia-RequestNode'}>
{<MarkdownWrapper
data={markdownString}
renderCallback={() => this.renderer.render(markdownString).element}
></MarkdownWrapper>}
</div>
);
return <ChatRequestRender node={node} />;
}

private renderChatResponse(node: ResponseNode): React.ReactNode {
Expand Down Expand Up @@ -389,6 +375,13 @@ export class ChatViewTreeWidget extends TreeWidget {
}
}

const ChatRequestRender = ({ node }: { node: RequestNode }) => {
const text = node.request.request.displayText ?? node.request.request.text;
const ref = useMarkdownRendering(text);

return <div className={'theia-RequestNode'} ref={ref}></div>;
};

const ProgressMessage = (c: ChatProgressMessage) => (
<div className='theia-ResponseNode-ProgressMessage'>
<Indicator {...c} /> {c.content}
Expand Down
Loading