Skip to content

Commit

Permalink
fix markdown request renderer
Browse files Browse the repository at this point in the history
Change the rendering off all markdown renderers to use markdown-it directly.
Also no MarkdownString class is used anymore.

fixes #14208
  • Loading branch information
eneufeld committed Sep 23, 2024
1 parent dedfe25 commit 0cb013f
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@
// *****************************************************************************

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 { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import * as markdownit from '@theia/core/shared/markdown-it';
import * as DOMPurify from '@theia/core/shared/dompurify';

@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 +38,51 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownCh
}
return -1;
}
private renderMarkdown(md: MarkdownString): HTMLElement {
return this.renderer.render(md).element;
}
render(response: MarkdownChatResponseContent | InformationalChatResponseContent): ReactNode {
// // eslint-disable-next-line no-null/no-null
// const ref: React.MutableRefObject<HTMLDivElement | null> = useRef(null);

// useEffect(() => {
// const host = document.createElement('div');
// const html = this.markdownIt.render(response.content);
// 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(host);
// }, [response.content]);
// 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 <div ref={ref}></div>;
return <MarkdownRender response={response} />;
}

}

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

useEffect(() => {
const myDomElement = props.renderCallback(props.data);

const markdownIt = markdownit();
const host = document.createElement('div');
const html = markdownIt.render(response.content);
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);
}, [response.content]);

return <div ref={ref}></div>;
};
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,11 @@ 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 * as markdownit from '@theia/core/shared/markdown-it';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { useEffect, useRef } from '@theia/core/shared/react';

// 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 +76,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 +333,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 +377,27 @@ export class ChatViewTreeWidget extends TreeWidget {
}
}

const ChatRequestRender = ({ node }: { node: RequestNode }) => {
// eslint-disable-next-line no-null/no-null
const ref: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
useEffect(() => {
const markdownIt = markdownit();
const text = node.request.request.displayText ?? node.request.request.text;
const host = document.createElement('div');
const html = markdownIt.render(text);
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(host);
}, [node.request]);

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

const ProgressMessage = (c: ChatProgressMessage) => (
<div className='theia-ResponseNode-ProgressMessage'>
<Indicator {...c} /> {c.content}
Expand Down
27 changes: 13 additions & 14 deletions packages/ai-chat/src/common/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts

import { Command, Emitter, Event, generateUuid, URI } from '@theia/core';
import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
import { ChatAgentLocation } from './chat-agents';
import { ParsedChatRequest } from './parsed-chat-request';
Expand Down Expand Up @@ -128,7 +127,7 @@ export interface ErrorChatResponseContent extends ChatResponseContent {
export interface MarkdownChatResponseContent
extends Required<ChatResponseContent> {
kind: 'markdownContent';
content: MarkdownString;
content: string;
}

export interface CodeChatResponseContent
Expand Down Expand Up @@ -187,7 +186,7 @@ export interface CommandChatResponseContent extends ChatResponseContent {
*/
export interface InformationalChatResponseContent extends ChatResponseContent {
kind: 'informational';
content: MarkdownString;
content: string;
}

export namespace TextChatResponseContent {
Expand All @@ -207,7 +206,7 @@ export namespace MarkdownChatResponseContent {
ChatResponseContent.is(obj) &&
obj.kind === 'markdownContent' &&
'content' in obj &&
MarkdownString.is((obj as { content: unknown }).content)
typeof (obj as { content: unknown }).content === 'string'
);
}
}
Expand All @@ -218,7 +217,7 @@ export namespace InformationalChatResponseContent {
ChatResponseContent.is(obj) &&
obj.kind === 'informational' &&
'content' in obj &&
MarkdownString.is((obj as { content: unknown }).content)
typeof (obj as { content: unknown }).content === 'string'
);
}
}
Expand Down Expand Up @@ -411,35 +410,35 @@ export class TextChatResponseContentImpl implements TextChatResponseContent {

export class MarkdownChatResponseContentImpl implements MarkdownChatResponseContent {
kind: 'markdownContent' = 'markdownContent';
protected _content: MarkdownStringImpl = new MarkdownStringImpl();
protected _content: string;

constructor(content: string) {
this._content.appendMarkdown(content);
this._content = content;
}

get content(): MarkdownString {
get content(): string {
return this._content;
}

asString(): string {
return this._content.value;
return this._content;
}

merge(nextChatResponseContent: MarkdownChatResponseContent): boolean {
this._content.appendMarkdown(nextChatResponseContent.content.value);
this._content += nextChatResponseContent.content;
return true;
}
}

export class InformationalChatResponseContentImpl implements InformationalChatResponseContent {
kind: 'informational' = 'informational';
protected _content: MarkdownStringImpl;
protected _content: string;

constructor(content: string) {
this._content = new MarkdownStringImpl(content);
this._content = content;
}

get content(): MarkdownString {
get content(): string {
return this._content;
}

Expand All @@ -448,7 +447,7 @@ export class InformationalChatResponseContentImpl implements InformationalChatRe
}

merge(nextChatResponseContent: InformationalChatResponseContent): boolean {
this._content.appendMarkdown(nextChatResponseContent.content.value);
this._content += nextChatResponseContent.content;
return true;
}
}
Expand Down

0 comments on commit 0cb013f

Please sign in to comment.