Skip to content

Commit

Permalink
Add support for markdown display (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
curtisman authored Jan 8, 2025
1 parent ee45ff1 commit 182ce69
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 30 deletions.
2 changes: 1 addition & 1 deletion ts/packages/agentSdk/src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// - text can supports use ANSI escape code to control additional text color and style
// - depending on host support, host is expected to strip ANI escape code if not supported.
// - html and iframe might not be supported by all hosts.
export type DisplayType = "html" | "iframe" | "text";
export type DisplayType = "markdown" | "html" | "iframe" | "text";

export type DynamicDisplay = {
content: DisplayContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export class DisplayCommandHandler implements CommandHandler {
description: "Speak the display for the host that supports TTS",
default: false,
},
type: {
description: "Display type",
default: "text",
},
inline: {
description: "Display inline",
default: false,
},
},
args: {
text: {
Expand All @@ -27,14 +35,22 @@ export class DisplayCommandHandler implements CommandHandler {
) {
const { flags, args } = params;

if (
flags.type !== "text" &&
flags.type !== "html" &&
flags.type !== "markdown" &&
flags.type !== "iframe"
) {
throw new Error(`Invalid display type: ${flags.type}`);
}
for (const content of args.text) {
context.actionIO.appendDisplay(
{
type: "text",
type: flags.type,
content,
speak: flags.speak,
},
"block",
flags.inline ? "inline" : "block",
);
}
}
Expand Down
1 change: 1 addition & 0 deletions ts/packages/shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dompurify": "^3.1.6",
"dotenv": "^16.3.1",
"electron-updater": "^6.3.2",
"markdown-it": "^14.1.0",
"microsoft-cognitiveservices-speech-sdk": "^1.38.0",
"typechat": "^0.1.1",
"ws": "^8.17.1"
Expand Down
8 changes: 4 additions & 4 deletions ts/packages/shell/src/main/shellSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ShellSettings
public multiModalContent: boolean;
public devUI: boolean;
public partialCompletion: boolean;
public allowedDisplayType: DisplayType[];
public disallowedDisplayType: DisplayType[];
public onSettingsChanged: EmptyFunction | null;
public onShowSettingsDialog: ((dialogName: string) => void) | null;
public onRunDemo: ((interactive: boolean) => void) | null;
Expand Down Expand Up @@ -81,7 +81,7 @@ export class ShellSettings
this.multiModalContent = settings.multiModalContent;
this.devUI = settings.devUI;
this.partialCompletion = settings.partialCompletion;
this.allowedDisplayType = settings.allowedDisplayType;
this.disallowedDisplayType = settings.disallowedDisplayType;

this.onSettingsChanged = null;
this.onShowSettingsDialog = null;
Expand Down Expand Up @@ -178,8 +178,8 @@ export class ShellSettings
}

public isDisplayTypeAllowed(displayType: DisplayType): boolean {
for (let i = 0; i < this.allowedDisplayType.length; i++) {
if (this.allowedDisplayType[i] === displayType) {
for (let i = 0; i < this.disallowedDisplayType.length; i++) {
if (this.disallowedDisplayType[i] === displayType) {
return true;
}
}
Expand Down
4 changes: 2 additions & 2 deletions ts/packages/shell/src/main/shellSettingsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type ShellSettingsType = {
multiModalContent: boolean;
devUI: boolean;
partialCompletion: boolean;
allowedDisplayType: DisplayType[];
disallowedDisplayType: DisplayType[];
};

export const defaultSettings: ShellSettingsType = {
Expand All @@ -35,5 +35,5 @@ export const defaultSettings: ShellSettingsType = {
multiModalContent: true,
devUI: false,
partialCompletion: true,
allowedDisplayType: ["html", "iframe", "text"],
disallowedDisplayType: [],
};
2 changes: 1 addition & 1 deletion ts/packages/shell/src/preload/electronTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface ClientSettingsProvider {
set: SetSettingFunction | null;
}

export type DisplayType = "html" | "iframe" | "text";
export type DisplayType = "html" | "iframe" | "text" | "markdown";

export type ClientActions =
| "show-camera"
Expand Down
46 changes: 33 additions & 13 deletions ts/packages/shell/src/renderer/src/setContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@typeagent/agent-sdk";
import DOMPurify from "dompurify";
import { SettingsView } from "./settingsView";
import MarkdownIt from "markdown-it";

const ansi_up = new AnsiUp();
ansi_up.use_classes = true;
Expand All @@ -33,16 +34,30 @@ function encodeTextToHtml(text: string): string {
}

const enableText2Html = true;
function processContent(content: string, type: string): string {
return type === "html"
? DOMPurify.sanitize(content, {
ADD_ATTR: ["target", "onclick", "onerror"],
ADD_DATA_URI_TAGS: ["img"],
ADD_URI_SAFE_ATTR: ["src"],
})
: enableText2Html
? textToHtml(content)
: stripAnsi(encodeTextToHtml(content));
function processContent(
content: string,
type: string,
inline: boolean = false,
): string {
switch (type) {
case "iframe":
return content;
case "html":
return DOMPurify.sanitize(content, {
ADD_ATTR: ["target", "onclick", "onerror"],
ADD_DATA_URI_TAGS: ["img"],
ADD_URI_SAFE_ATTR: ["src"],
});
case "markdown":
const md = new MarkdownIt();
return inline ? md.renderInline(content) : md.render(content);
case "text":
return enableText2Html
? textToHtml(content)
: stripAnsi(encodeTextToHtml(content));
default:
throw new Error(`Invalid content type ${type}`);
}
}

function matchKindStyle(elm: HTMLElement, kindStyle?: string) {
Expand All @@ -60,9 +75,10 @@ function matchKindStyle(elm: HTMLElement, kindStyle?: string) {
function messageContentToHTML(
message: MessageContent,
type: DisplayType,
inline: boolean,
): string {
if (typeof message === "string") {
return processContent(message, type);
return processContent(message, type, inline);
}

if (message.length === 0) {
Expand All @@ -71,7 +87,7 @@ function messageContentToHTML(

if (typeof message[0] === "string") {
return (message as string[])
.map((s) => processContent(s, type))
.map((s) => processContent(s, type, inline))
.join("<br>");
}

Expand Down Expand Up @@ -149,7 +165,11 @@ export function setContent(
}

// Process content according to type
const contentHtml = messageContentToHTML(message, type);
const contentHtml = messageContentToHTML(
message,
type,
appendMode === "inline",
);

// if the agent wants to show script we need to do that in isolation so create an iframe
// and put both the script and supplied HTML into it
Expand Down
10 changes: 3 additions & 7 deletions ts/packages/shell/src/renderer/src/settingsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,8 @@ export class SettingsView {
}

public isDisplayTypeAllowed(displayType: DisplayType): boolean {
for (let i = 0; i < this.shellSettings.allowedDisplayType.length; i++) {
if (this.shellSettings.allowedDisplayType[i] === displayType) {
return true;
}
}

return false;
return !this.shellSettings.disallowedDisplayType.some(
(type) => type === displayType,
);
}
}
3 changes: 3 additions & 0 deletions ts/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 182ce69

Please sign in to comment.