Skip to content

Commit 7de389c

Browse files
committed
only render code block after streaming is complete. Also handle inline code blocks
1 parent d8a1cfe commit 7de389c

File tree

4 files changed

+158
-90
lines changed

4 files changed

+158
-90
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { SafeString } from '@ember/template';
2+
import Component from '@glimmer/component';
3+
4+
import CodeBlock from '@cardstack/host/modifiers/code-block';
5+
import { MonacoEditorOptions } from '@cardstack/host/modifiers/monaco';
6+
7+
import { type MonacoSDK } from '@cardstack/host/services/monaco-service';
8+
9+
interface FormattedMessageSignature {
10+
sanitizedHtml: SafeString;
11+
monacoSDK: MonacoSDK;
12+
renderCodeBlocks: boolean;
13+
}
14+
15+
export default class FormattedMessage extends Component<FormattedMessageSignature> {
16+
<template>
17+
{{#if @renderCodeBlocks}}
18+
<div
19+
class='message'
20+
{{CodeBlock
21+
codeBlockSelector='pre[data-codeblock]'
22+
languageAttr='data-codeblock'
23+
monacoSDK=@monacoSDK
24+
editorDisplayOptions=this.editorDisplayOptions
25+
}}
26+
>
27+
{{@sanitizedHtml}}
28+
</div>
29+
{{else}}
30+
<div class='message'>
31+
{{@sanitizedHtml}}
32+
</div>
33+
{{/if}}
34+
35+
<style scoped>
36+
.message {
37+
padding: var(--ai-assistant-message-padding, var(--boxel-sp));
38+
}
39+
40+
/* code blocks can be rendered inline and as blocks,
41+
this is the styling for when it is rendered as a block */
42+
.message > :deep(.preview-code.code-block) {
43+
width: calc(100% + 2 * var(--boxel-sp));
44+
}
45+
46+
:deep(.preview-code) {
47+
--spacing: var(--boxel-sp-sm);
48+
--fill-container-spacing: calc(
49+
-1 * var(--ai-assistant-message-padding)
50+
);
51+
margin: var(--boxel-sp) var(--fill-container-spacing) 0
52+
var(--fill-container-spacing);
53+
padding: var(--spacing) 0;
54+
background-color: var(--boxel-dark);
55+
}
56+
57+
:deep(.preview-code.code-block) {
58+
display: inline-block; /* sometimes the ai bot may place the codeblock within an <li> */
59+
width: 100%;
60+
position: relative;
61+
padding-top: var(--boxel-sp-xxxl);
62+
}
63+
64+
:deep(.monaco-container) {
65+
height: var(--monaco-container-height);
66+
min-height: 7rem;
67+
max-height: 30vh;
68+
}
69+
70+
/*
71+
This filter is a best-effort approximation of a good looking dark theme that is a function of the white theme that
72+
we use for code previews in the AI panel. While Monaco editor does support multiple themes, it does not support
73+
monaco instances with different themes *on the same page*. This is why we are using a filter to approximate the
74+
dark theme. More details here: https://github.com/Microsoft/monaco-editor/issues/338 (monaco uses global style tags
75+
with hardcoded colors; any instance will override the global style tag, making all code editors look the same,
76+
effectively disabling multiple themes to be used on the same page)
77+
*/
78+
:global(.preview-code .monaco-editor) {
79+
filter: invert(1) hue-rotate(151deg) brightness(0.8) grayscale(0.1);
80+
}
81+
82+
/* we are cribbing the boxel-ui style here as we have a rather
83+
awkward way that we insert the copy button */
84+
:deep(.code-copy-button) {
85+
--spacing: calc(1rem / 1.333);
86+
87+
position: absolute;
88+
top: var(--boxel-sp);
89+
left: var(--boxel-sp-lg);
90+
color: var(--boxel-highlight);
91+
background: none;
92+
border: none;
93+
font: 600 var(--boxel-font-xs);
94+
padding: 0;
95+
margin-bottom: var(--spacing);
96+
display: grid;
97+
grid-template-columns: auto 1fr;
98+
gap: var(--spacing);
99+
letter-spacing: var(--boxel-lsp-xs);
100+
justify-content: center;
101+
height: min-content;
102+
align-items: center;
103+
white-space: nowrap;
104+
min-height: var(--boxel-button-min-height);
105+
min-width: var(--boxel-button-min-width, 5rem);
106+
}
107+
:deep(.code-copy-button .copy-text) {
108+
color: transparent;
109+
}
110+
:deep(.code-copy-button .copy-text:hover) {
111+
color: var(--boxel-highlight);
112+
}
113+
</style>
114+
</template>
115+
116+
private editorDisplayOptions: MonacoEditorOptions = {
117+
wordWrap: 'on',
118+
wrappingIndent: 'indent',
119+
fontWeight: 'bold',
120+
scrollbar: {
121+
alwaysConsumeMouseWheel: false,
122+
},
123+
};
124+
}

packages/host/app/components/ai-assistant/message/index.gts

+27-89
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ import { isCardInstance } from '@cardstack/runtime-common';
1818

1919
import CardPill from '@cardstack/host/components/card-pill';
2020
import FilePill from '@cardstack/host/components/file-pill';
21-
import CodeBlock from '@cardstack/host/modifiers/code-block';
22-
import { MonacoEditorOptions } from '@cardstack/host/modifiers/monaco';
2321

2422
import type CardService from '@cardstack/host/services/card-service';
2523
import { type MonacoSDK } from '@cardstack/host/services/monaco-service';
2624

2725
import { type CardDef } from 'https://cardstack.com/base/card-api';
2826
import { type FileDef } from 'https://cardstack.com/base/file-api';
2927

28+
import FormattedMessage from '../formatted-message';
29+
3030
import type { ComponentLike } from '@glint/template';
3131

3232
function findLastTextNodeWithContent(parentNode: Node): Text | null {
@@ -191,13 +191,6 @@ export default class AiAssistantMessage extends Component<Signature> {
191191
is-error=@errorMessage
192192
}}
193193
{{MessageScroller index=@index registerScroller=@registerScroller}}
194-
{{CodeBlock
195-
codeBlockSelector='pre[data-codeblock]'
196-
languageAttr='data-codeblock'
197-
monacoSDK=@monacoSDK
198-
editorDisplayOptions=this.editorDisplayOptions
199-
index=@index
200-
}}
201194
data-test-ai-assistant-message
202195
...attributes
203196
>
@@ -237,11 +230,21 @@ export default class AiAssistantMessage extends Component<Signature> {
237230
{{/if}}
238231

239232
<div class='content' data-test-ai-message-content>
240-
{{if
241-
(and @isFromAssistant @isStreaming)
242-
(wrapLastTextNodeInStreamingTextSpan @formattedMessage)
243-
@formattedMessage
244-
}}
233+
{{#if (and @isFromAssistant @isStreaming)}}
234+
<FormattedMessage
235+
@renderCodeBlocks={{false}}
236+
@monacoSDK={{@monacoSDK}}
237+
@sanitizedHtml={{wrapLastTextNodeInStreamingTextSpan
238+
@formattedMessage
239+
}}
240+
/>
241+
{{else}}
242+
<FormattedMessage
243+
@renderCodeBlocks={{true}}
244+
@monacoSDK={{@monacoSDK}}
245+
@sanitizedHtml={{@formattedMessage}}
246+
/>
247+
{{/if}}
245248

246249
{{yield}}
247250

@@ -344,7 +347,17 @@ export default class AiAssistantMessage extends Component<Signature> {
344347
letter-spacing: var(--boxel-lsp-xs);
345348
padding: var(--ai-assistant-message-padding, var(--boxel-sp));
346349
}
350+
351+
.content :deep(.message) {
352+
padding: 0;
353+
}
354+
355+
.is-from-assistant .content :deep(.message) {
356+
padding: var(--ai-assistant-message-padding, var(--boxel-sp));
357+
}
358+
347359
.is-from-assistant .content {
360+
padding: 0;
348361
background-color: var(--ai-bot-message-background-color);
349362
color: var(--boxel-light);
350363
/* the below font-smoothing options are only recommended for light-colored
@@ -437,84 +450,9 @@ export default class AiAssistantMessage extends Component<Signature> {
437450
flex-wrap: wrap;
438451
gap: var(--boxel-sp-xxs);
439452
}
440-
441-
:deep(.preview-code) {
442-
--spacing: var(--boxel-sp-sm);
443-
--fill-container-spacing: calc(
444-
-1 * var(--ai-assistant-message-padding)
445-
);
446-
margin: var(--boxel-sp) var(--fill-container-spacing) 0
447-
var(--fill-container-spacing);
448-
padding: var(--spacing) 0;
449-
background-color: var(--boxel-dark);
450-
}
451-
452-
:deep(.preview-code.code-block) {
453-
position: relative;
454-
padding-top: var(--boxel-sp-xxxl);
455-
}
456-
457-
:deep(.monaco-container) {
458-
height: var(--monaco-container-height);
459-
min-height: 7rem;
460-
max-height: 30vh;
461-
}
462-
463-
/*
464-
This filter is a best-effort approximation of a good looking dark theme that is a function of the white theme that
465-
we use for code previews in the AI panel. While Monaco editor does support multiple themes, it does not support
466-
monaco instances with different themes *on the same page*. This is why we are using a filter to approximate the
467-
dark theme. More details here: https://github.com/Microsoft/monaco-editor/issues/338 (monaco uses global style tags
468-
with hardcoded colors; any instance will override the global style tag, making all code editors look the same,
469-
effectively disabling multiple themes to be used on the same page)
470-
*/
471-
:global(.preview-code .monaco-editor) {
472-
filter: invert(1) hue-rotate(151deg) brightness(0.8) grayscale(0.1);
473-
}
474-
475-
/* we are cribbing the boxel-ui style here as we have a rather
476-
awkward way that we insert the copy button */
477-
:deep(.code-copy-button) {
478-
--spacing: calc(1rem / 1.333);
479-
480-
position: absolute;
481-
top: var(--boxel-sp);
482-
left: var(--boxel-sp-lg);
483-
color: var(--boxel-highlight);
484-
background: none;
485-
border: none;
486-
font: 600 var(--boxel-font-xs);
487-
padding: 0;
488-
margin-bottom: var(--spacing);
489-
display: grid;
490-
grid-template-columns: auto 1fr;
491-
gap: var(--spacing);
492-
letter-spacing: var(--boxel-lsp-xs);
493-
justify-content: center;
494-
height: min-content;
495-
align-items: center;
496-
white-space: nowrap;
497-
min-height: var(--boxel-button-min-height);
498-
min-width: var(--boxel-button-min-width, 5rem);
499-
}
500-
:deep(.code-copy-button .copy-text) {
501-
color: transparent;
502-
}
503-
:deep(.code-copy-button .copy-text:hover) {
504-
color: var(--boxel-highlight);
505-
}
506453
</style>
507454
</template>
508455

509-
editorDisplayOptions: MonacoEditorOptions = {
510-
wordWrap: 'on',
511-
wrappingIndent: 'indent',
512-
fontWeight: 'bold',
513-
scrollbar: {
514-
alwaysConsumeMouseWheel: false,
515-
},
516-
};
517-
518456
private get isAvatarAnimated() {
519457
return this.args.isStreaming && !this.args.errorMessage;
520458
}

packages/host/app/modifiers/code-block.ts

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ interface Signature {
1717
languageAttr: string;
1818
monacoSDK: typeof MonacoSDK;
1919
editorDisplayOptions?: MonacoEditorOptions;
20-
index: number;
2120
};
2221
};
2322
}

packages/host/app/services/monaco-service.ts

+7
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export default class MonacoService extends Service {
4646
this.extendMonacoLanguage(lang, monaco),
4747
);
4848
monaco.editor.onDidCreateEditor((editor: _MonacoSDK.editor.ICodeEditor) => {
49+
let cssClass = ((editor as any)._domElement as HTMLElement).getAttribute(
50+
'class',
51+
);
52+
// we only care about the code mode monaco editor here
53+
if (cssClass?.includes('code-block')) {
54+
return;
55+
}
4956
this.editor = editor;
5057
this.editor.onDidFocusEditorText(() => {
5158
this.hasFocus = true;

0 commit comments

Comments
 (0)