Skip to content

Commit 301163b

Browse files
authored
Merge pull request #55 from aws/dogusata/add-code-diff-to-syntax-highlighter-and-file-description-tooltip
Dogusata/add code diff to syntax highlighter and file description tooltip
2 parents 24443cb + f7f66d8 commit 301163b

File tree

13 files changed

+204
-77
lines changed

13 files changed

+204
-77
lines changed

docs/DATAMODEL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ export interface TreeNodeDetails {
536536
status?: 'info' | 'success' | 'warning' | 'error';
537537
icon?: MynahIcons;
538538
label?: string;
539+
description?: string; // Markdown tooltip
539540
}
540541

541542
export interface SourceLink {
@@ -1348,7 +1349,8 @@ mynahUI.addChatItem(tabId, {
13481349
'src/devfile.yaml': {
13491350
status: 'error',
13501351
label: "Change rejected",
1351-
icon: MynahIcons.REVERT
1352+
icon: MynahIcons.REVERT,
1353+
description: 'Markdown tooltip to show'
13521354
}
13531355
}
13541356
},

example/src/main.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
exampleRichFollowups,
2828
exampleStreamParts,
2929
sampleMarkdownList,
30+
exampleCodeDiff,
3031
} from './samples/sample-data';
3132
import escapeHTML from 'escape-html';
3233
import './styles/styles.scss';
@@ -54,18 +55,13 @@ export const createMynahUI = (initialData?: MynahUIDataModel): MynahUI => {
5455
icon: MynahIcons.ELLIPSIS,
5556
items: [
5657
{
57-
id: 'menu-action-1',
58-
text: 'Menu action 1!',
59-
icon: MynahIcons.CHAT,
60-
},
61-
{
62-
id: 'menu-action-2',
63-
text: 'Menu action 2!',
64-
icon: MynahIcons.COMMENT,
58+
id: 'show-code-diff',
59+
text: 'Show code diff!',
60+
icon: MynahIcons.CODE_BLOCK,
6561
},
6662
{
6763
id: 'insert-code',
68-
icon: MynahIcons.CODE_BLOCK,
64+
icon: MynahIcons.CURSOR_INSERT,
6965
text: 'Insert code!',
7066
},
7167
],
@@ -87,6 +83,11 @@ export const createMynahUI = (initialData?: MynahUIDataModel): MynahUI => {
8783
mynahUI.updateStore(tabId, {
8884
chatItems: [],
8985
});
86+
} else if (buttonId === 'show-code-diff') {
87+
mynahUI.addChatItem(tabId, {
88+
type: ChatItemType.ANSWER,
89+
body: exampleCodeDiff
90+
});
9091
} else if (buttonId === 'insert-code') {
9192
mynahUI.addToUserPrompt(tabId, exampleCodeBlockToInsert, 'code');
9293
}
@@ -197,6 +198,11 @@ export const createMynahUI = (initialData?: MynahUIDataModel): MynahUI => {
197198
case 'reject-change':
198199
mynahUI.updateChatAnswerWithMessageId(tabId, messageId, exampleFileListChatItemForUpdate);
199200
break;
201+
case 'show-diff':
202+
mynahUI.updateChatAnswerWithMessageId(tabId, messageId, {
203+
body: exampleCodeDiff
204+
});
205+
break;
200206
case 'revert-rejection':
201207
mynahUI.updateChatAnswerWithMessageId(tabId, messageId, {fileList: exampleFileListChatItem.fileList});
202208
break;

example/src/samples/sample-data.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import sampleList2 from './sample-list-2.md';
1616
import sampleList3 from './sample-list-3.md';
1717
import sampleList4 from './sample-list-4.md';
1818
import SampleCode from './sample-code.md';
19+
import SampleDiff from './sample-diff.md';
1920
import { Commands } from '../commands';
2021

2122
export const mynahUIQRImageBase64 =
@@ -104,6 +105,7 @@ export const exampleStreamParts: Partial<ChatItem>[] = [
104105
];
105106

106107
export const exampleCodeBlockToInsert = SampleCode;
108+
export const exampleCodeDiff = SampleDiff;
107109

108110
export const exampleRichFollowups: ChatItem = {
109111
type: ChatItemType.SYSTEM_PROMPT,
@@ -199,8 +201,18 @@ export const exampleFileListChatItem: ChatItem = {
199201
name: 'reject-change',
200202
description: 'Reject Change',
201203
},
204+
{
205+
icon: MynahIcons.CODE_BLOCK,
206+
name: 'show-diff',
207+
description: 'Show Diff',
208+
},
202209
],
203210
},
211+
details: {
212+
'src/index.ts': {
213+
description: exampleCodeDiff
214+
},
215+
},
204216
},
205217
};
206218

@@ -215,6 +227,7 @@ export const exampleFileListChatItemForUpdate: Partial<ChatItem> = {
215227
status: 'error',
216228
label: 'File rejected',
217229
icon: MynahIcons.CANCEL_CIRCLE,
230+
description: exampleCodeDiff
218231
},
219232
},
220233
actions: {

example/src/samples/sample-diff.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
### Changes on `index.ts`
2+
3+
4+
> _Between line 42 and 70_
5+
```diff-typescript
6+
const mynahUI = new MynahUI({
7+
tabs: {
8+
'tab-1': {
9+
isSelected: true,
10+
store: {
11+
tabTitle: 'Chat',
12+
chatItems: [
13+
{
14+
type: ChatItemType.ANSWER,
15+
body: 'Welcome to our chat!',
16+
messageId: 'welcome-message'
17+
},
18+
],
19+
- promptInputPlaceholder: 'Write your question',
20+
+ promptInputPlaceholder: 'Type your question',
21+
}
22+
}
23+
},
24+
- onChatPrompt: () => {},
25+
+ onChatPrompt: (tabId: string, prompt: ChatPrompt) => {
26+
+ mynahUI.addChatItem(tabId, {
27+
+ type: ChatItemType.PROMPT,
28+
+ messageId: new Date().getTime().toString(),
29+
+ body: prompt.escapedPrompt
30+
+ });
31+
+ // call your genAI action
32+
+ }
33+
});
34+
```

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@aws/mynah-ui",
33
"displayName": "AWS Mynah UI",
4-
"version": "4.10.1",
4+
"version": "4.11.0",
55
"description": "AWS Toolkit VSCode and Intellij IDE Extension Mynah UI",
66
"publisher": "Amazon Web Services",
77
"license": "Apache License 2.0",

src/components/chat-item/chat-item-tree-file.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { marked } from 'marked';
12
import { DomBuilder, ExtendedHTMLElement } from '../../helper/dom';
23
import { MynahUIGlobalEvents, cancelEvent } from '../../helper/events';
34
import { FileNodeAction, MynahEventNames, TreeNodeDetails } from '../../static';
45
import { Button } from '../button';
6+
import { Card } from '../card/card';
7+
import { CardBody } from '../card/card-body';
58
import { Icon, MynahIcons } from '../icon';
9+
import { Overlay, OverlayHorizontalDirection, OverlayVerticalDirection } from '../overlay';
610

711
export interface ChatItemTreeFileProps {
812
tabId: string;
@@ -15,8 +19,11 @@ export interface ChatItemTreeFileProps {
1519
actions?: FileNodeAction[];
1620
}
1721

22+
const PREVIEW_DELAY = 250;
1823
export class ChatItemTreeFile {
1924
render: ExtendedHTMLElement;
25+
private fileTooltip: Overlay | null;
26+
private fileTooltipTimeout: ReturnType<typeof setTimeout>;
2027
constructor (props: ChatItemTreeFileProps) {
2128
this.render = DomBuilder.getInstance().build({
2229
type: 'div',
@@ -26,13 +33,23 @@ export class ChatItemTreeFile {
2633
],
2734
events: {
2835
click: () => {
36+
this.hideTooltip();
2937
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FILE_CLICK, {
3038
tabId: props.tabId,
3139
messageId: props.messageId,
3240
filePath: props.filePath,
3341
deleted: props.deleted,
3442
});
35-
}
43+
},
44+
...(props.details?.description != null
45+
? {
46+
mouseenter: (e: MouseEvent) => {
47+
const tooltipText = marked(props.details?.description ?? '', { breaks: true }) as string;
48+
this.showTooltip(tooltipText, OverlayVerticalDirection.CENTER, OverlayHorizontalDirection.TO_RIGHT);
49+
},
50+
mouseout: this.hideTooltip
51+
}
52+
: {})
3653
},
3754
children: [
3855
...(props.icon != null
@@ -83,6 +100,7 @@ export class ChatItemTreeFile {
83100
primary: false,
84101
onClick: (e) => {
85102
cancelEvent(e);
103+
this.hideTooltip();
86104
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FILE_ACTION_CLICK, {
87105
tabId: props.tabId,
88106
messageId: props.messageId,
@@ -96,4 +114,39 @@ export class ChatItemTreeFile {
96114
]
97115
});
98116
}
117+
118+
private readonly showTooltip = (content: string, vDir?: OverlayVerticalDirection, hDir?: OverlayHorizontalDirection): void => {
119+
if (content.trim() !== '') {
120+
clearTimeout(this.fileTooltipTimeout);
121+
this.fileTooltipTimeout = setTimeout(() => {
122+
this.fileTooltip = new Overlay({
123+
background: true,
124+
closeOnOutsideClick: false,
125+
referenceElement: this.render,
126+
dimOutside: false,
127+
removeOtherOverlays: true,
128+
verticalDirection: vDir ?? OverlayVerticalDirection.TO_TOP,
129+
horizontalDirection: hDir ?? OverlayHorizontalDirection.CENTER,
130+
children: [
131+
new Card({
132+
border: false,
133+
children: [
134+
new CardBody({
135+
body: content
136+
}).render
137+
]
138+
}).render
139+
],
140+
});
141+
}, PREVIEW_DELAY);
142+
}
143+
};
144+
145+
public readonly hideTooltip = (): void => {
146+
if (this.fileTooltipTimeout != null) {
147+
clearTimeout(this.fileTooltipTimeout);
148+
}
149+
this.fileTooltip?.close();
150+
this.fileTooltip = null;
151+
};
99152
}

src/components/overlay.ts

Lines changed: 18 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -131,65 +131,29 @@ export class Overlay {
131131
'beforeend'
132132
);
133133

134-
const containerRectangle = this.container.getBoundingClientRect();
134+
// Screen edge fixes
135135
const winHeight = Math.max(document.documentElement.clientHeight ?? 0, window.innerHeight ?? 0);
136136
const winWidth = Math.max(document.documentElement.clientWidth ?? 0, window.innerWidth ?? 0);
137+
const lastContainerRect = this.container.getBoundingClientRect();
138+
const effectiveTop = parseFloat(this.container.style.top ?? '0');
139+
const effectiveLeft = parseFloat(this.container.style.left ?? '0');
137140

138-
// if it will open at the center of the reference element or point
139-
// we only need the half of both measurements
140-
const comparingWidth =
141-
horizontalDirection === OverlayHorizontalDirection.CENTER
142-
? containerRectangle.width / 2
143-
: containerRectangle.width;
144-
const comparingHeight =
145-
verticalDirection === OverlayVerticalDirection.CENTER
146-
? containerRectangle.height / 2
147-
: containerRectangle.height;
148-
149-
// if overlay will open to right or at center
150-
// we're checking if it exceeds from the right edge of the window
151-
if (
152-
horizontalDirection !== OverlayHorizontalDirection.TO_LEFT &&
153-
horizontalDirection !== OverlayHorizontalDirection.END_TO_LEFT &&
154-
comparingWidth + OVERLAY_MARGIN + calculatedLeft > winWidth
155-
) {
156-
if (horizontalDirection === OverlayHorizontalDirection.CENTER) {
157-
// Exceed right edge of the window, shift left by the width of exceeding part * 0.5
158-
// to correctly handle the 50% horizontal transform
159-
this.container.style.left =
160-
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
161-
(calculatedLeft - (containerRectangle.width + (OVERLAY_MARGIN * 2) + calculatedLeft - winWidth) * 0.5) + 'px';
162-
} else {
163-
this.container.style.left =
164-
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
165-
calculatedLeft - (containerRectangle.width + OVERLAY_MARGIN + calculatedLeft - winWidth) + 'px';
166-
}
167-
}
168-
// else if the direction is selected as a one that goes to the left,
169-
// we need to check if it is exceeding from the left edge of the window
170-
else if (calculatedLeft + comparingWidth - containerRectangle.width < OVERLAY_MARGIN) {
171-
this.container.style.left =
172-
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
173-
calculatedLeft + (OVERLAY_MARGIN - calculatedLeft + (comparingWidth - containerRectangle.width)) + 'px';
141+
// Vertical edge
142+
// Check top exceeding
143+
if (lastContainerRect.top < OVERLAY_MARGIN) {
144+
this.container.style.top = `${effectiveTop + (OVERLAY_MARGIN - lastContainerRect.top)}px`;
145+
} // Check bottom exceeding
146+
else if (lastContainerRect.top + lastContainerRect.height + OVERLAY_MARGIN > winHeight) {
147+
this.container.style.top = `${effectiveTop - (lastContainerRect.top + lastContainerRect.height + OVERLAY_MARGIN - winHeight)}px`;
174148
}
175149

176-
// if overlay will open to bottom or at center
177-
// we're checking if it exceeds from the bottom edge of the window
178-
if (
179-
verticalDirection !== OverlayVerticalDirection.TO_TOP &&
180-
verticalDirection !== OverlayVerticalDirection.END_TO_TOP &&
181-
comparingHeight + OVERLAY_MARGIN + calculatedTop > winHeight
182-
) {
183-
this.container.style.top =
184-
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
185-
calculatedTop - (comparingHeight + OVERLAY_MARGIN + calculatedTop - winHeight) + 'px';
186-
}
187-
// else if the direction is selected as a one that goes to the top,
188-
// we need to check if it is exceeding from the top edge of the window
189-
else if (calculatedTop + comparingHeight - containerRectangle.height < OVERLAY_MARGIN) {
190-
this.container.style.top =
191-
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
192-
calculatedTop + (OVERLAY_MARGIN - calculatedTop + (comparingHeight - containerRectangle.height)) + 'px';
150+
// Horizontal edge
151+
// Check left exceeding
152+
if (lastContainerRect.left < OVERLAY_MARGIN) {
153+
this.container.style.left = `${effectiveLeft + (OVERLAY_MARGIN - lastContainerRect.left)}px`;
154+
} // Check right exceeding
155+
else if (lastContainerRect.left + lastContainerRect.width + OVERLAY_MARGIN > winWidth) {
156+
this.container.style.left = `${effectiveLeft - (lastContainerRect.left + lastContainerRect.width + OVERLAY_MARGIN - winWidth)}px`;
193157
}
194158

195159
// we need to delay the class toggle

0 commit comments

Comments
 (0)