Skip to content

Commit

Permalink
Merge pull request #1248 from solliancenet/sc-user-portal-copy-code-b…
Browse files Browse the repository at this point in the history
…utton

User portal code header with language and copy code button
  • Loading branch information
ciprianjichici authored Jul 22, 2024
2 parents 162bc1c + 89f1bc1 commit 8dd61b8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 10 deletions.
57 changes: 47 additions & 10 deletions src/ui/UserPortal/components/ChatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
<template v-if="message.sender === 'Assistant' && message.type === 'LoadingMessage'">
<i class="pi pi-spin pi-spinner"></i>
</template>
<div v-html="displayHtml"></div>

<!-- Render the html content and any vue components within -->
<component :is="compiledMarkdownComponent"></component>
</div>

<div v-if="message.sender !== 'User'" class="message__footer">
Expand Down Expand Up @@ -127,6 +129,7 @@
import type { PropType } from 'vue';
import type { Message, CompletionPrompt } from '@/js/types';
import api from '@/js/api';
import CodeBlockHeader from '@/components/CodeBlockHeader.vue';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark-dimmed.css';
Expand All @@ -138,12 +141,36 @@ import DOMPurify from 'dompurify';
const renderer = new marked.Renderer();
renderer.code = (code, language) => {
const validLanguage = !!(language && hljs.getLanguage(language));
const highlighted = validLanguage ? hljs.highlight(code, { language }).value : hljs.highlightAuto(code).value;
const highlighted = validLanguage ? hljs.highlight(code, { language }) : hljs.highlightAuto(code);
const languageClass = validLanguage ? `hljs language-${language}` : 'hljs';
return `<pre><code class="${languageClass}">${highlighted}</code></pre>`;
const encodedCode = encodeURIComponent(code);
return `<pre><code class="${languageClass}" data-code="${encodedCode}" data-language="${highlighted.language}">${highlighted.value}</code></pre>`;
};
marked.use({ renderer });
function addCodeHeaderComponents(htmlString) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
doc.querySelectorAll('pre code').forEach((element) => {
const languageClass = element.getAttribute('class');
const encodedCode = element.getAttribute('data-code');
// const autoDetectLanguage = element.getAttribute('data-language');
const languageMatch = languageClass.match(/language-(\w+)/);
const language = languageMatch ? languageMatch[1] : 'plaintext';
const header = document.createElement('div');
header.innerHTML = `<code-block-header language="${language}" codecontent="${encodedCode}"></code-block-header>`;
element.parentNode.insertBefore(header.firstChild, element);
});
const html = doc.body.innerHTML;
const withVueCurlyBracesSanitized = html.replace(/{{/g, '&#123;&#123;').replace(/}}/g, '&#125;&#125;');
return withVueCurlyBracesSanitized;
}
export default {
name: 'ChatMessage',
Expand All @@ -165,7 +192,7 @@ export default {
return {
prompt: {} as CompletionPrompt,
viewPrompt: false,
displayHtml: '',
compiledVueTemplate: '',
currentWordIndex: 0,
primaryButtonBg: this.$appConfigStore.primaryButtonBg,
primaryButtonText: this.$appConfigStore.primaryButtonText,
Expand All @@ -175,14 +202,23 @@ export default {
computed: {
compiledMarkdown() {
return DOMPurify.sanitize(marked(this.message.text));
}
},
compiledMarkdownComponent() {
return {
template: `<div>${this.compiledVueTemplate}</div>`,
components: {
CodeBlockHeader,
},
};
},
},
created() {
if (this.showWordAnimation) {
this.displayWordByWord();
} else {
this.displayHtml = this.compiledMarkdown;
this.compiledVueTemplate = addCodeHeaderComponents(this.compiledMarkdown);
}
},
Expand All @@ -191,18 +227,19 @@ export default {
if (this.currentWordIndex >= this.compiledMarkdown.split(/\s+/).length) return;
this.currentWordIndex += 1;
this.displayHtml = truncate(this.compiledMarkdown, this.currentWordIndex, {
const htmlString = truncate(this.compiledMarkdown, this.currentWordIndex, {
byWords: true,
stripTags: false,
ellipsis: '',
decodeEntities: false,
keepWhitespaces: true,
excludes: '',
reserveLastWord: false,
keepWhitespaces: true
keepWhitespaces: true,
});
setTimeout(this.displayWordByWord, 10);
this.compiledVueTemplate = addCodeHeaderComponents(htmlString);
setTimeout(() => this.displayWordByWord(), 10);
},
formatTimeStamp(timeStamp: string) {
Expand Down
65 changes: 65 additions & 0 deletions src/ui/UserPortal/components/CodeBlockHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<div class="header">
<!-- Language name -->
<span>{{ language }}</span>

<!-- Copy button -->
<Button
text
size="small"
label="Copy"
class="copy-button"
@click="copyToClipboard"></Button>
</div>

<!-- Highlighted code templating -->
<slot />
</template>

<script>
export default {
props: {
language: {
type: String,
required: false,
default: 'plaintext'
},
codecontent: {
type: String,
required: true,
},
},
methods: {
copyToClipboard() {
const textarea = document.createElement('textarea');
textarea.value = decodeURIComponent(this.codecontent);
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
this.$toast.add({
severity: 'success',
detail: 'Copied to clipboard!',
life: 5000,
});
}
}
}
</script>

<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--secondary-color);
padding-left: 8px;
}
.copy-button {
color: var(--secondary-button-text) !important;
}
</style>
7 changes: 7 additions & 0 deletions src/ui/UserPortal/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export default defineNuxtConfig({
build: {
transpile: ['primevue'],
},
hooks: {
'vite:extendConfig': (config, { isClient, isServer }) => {
if (isClient) {
config.resolve.alias.vue = 'vue/dist/vue.esm-bundler.js';
}
},
},
devServer: {
...(buildLoadingTemplate ? { loadingTemplate: () => buildLoadingTemplate } : {}),
port: 3000,
Expand Down

0 comments on commit 8dd61b8

Please sign in to comment.