mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
215 lines
4.9 KiB
Vue
215 lines
4.9 KiB
Vue
<script setup lang="ts">
|
||
import { useClipboard } from '@/composables/useClipboard';
|
||
import { useI18n } from '@n8n/i18n';
|
||
import { useToast } from '@/composables/useToast';
|
||
import { type ParsedAiContent } from '@/utils/aiUtils';
|
||
import { N8nIconButton } from '@n8n/design-system';
|
||
import { type IDataObject } from 'n8n-workflow';
|
||
import VueMarkdown from 'vue-markdown-render';
|
||
import hljs from 'highlight.js/lib/core';
|
||
|
||
const {
|
||
content,
|
||
compact = false,
|
||
renderType,
|
||
} = defineProps<{
|
||
content: ParsedAiContent;
|
||
compact?: boolean;
|
||
renderType: 'rendered' | 'json';
|
||
}>();
|
||
|
||
const i18n = useI18n();
|
||
const clipboard = useClipboard();
|
||
const { showMessage } = useToast();
|
||
|
||
function isJsonString(text: string) {
|
||
try {
|
||
JSON.parse(text);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const markdownOptions = {
|
||
highlight(str: string, lang: string) {
|
||
if (lang && hljs.getLanguage(lang)) {
|
||
try {
|
||
return hljs.highlight(str, { language: lang }).value;
|
||
} catch {}
|
||
}
|
||
|
||
return ''; // use external default escaping
|
||
},
|
||
};
|
||
|
||
function isMarkdown(jsonMarkdown: JsonMarkdown): boolean {
|
||
if (typeof jsonMarkdown !== 'string') return false;
|
||
const markdownPatterns = [
|
||
/^# .+/gm, // headers
|
||
/\*{1,2}.+\*{1,2}/g, // emphasis and strong
|
||
/\[.+\]\(.+\)/g, // links
|
||
/```[\s\S]+```/g, // code blocks
|
||
];
|
||
|
||
return markdownPatterns.some((pattern) => pattern.test(jsonMarkdown));
|
||
}
|
||
|
||
function formatToJsonMarkdown(data: string): string {
|
||
return '```json\n' + data + '\n```';
|
||
}
|
||
|
||
type JsonMarkdown = string | object | Array<string | object>;
|
||
|
||
function jsonToMarkdown(data: JsonMarkdown): string {
|
||
if (isMarkdown(data)) return data as string;
|
||
|
||
if (Array.isArray(data) && data.length && typeof data[0] !== 'number') {
|
||
const markdownArray = data.map((item: JsonMarkdown) => jsonToMarkdown(item));
|
||
|
||
return markdownArray.join('\n\n').trim();
|
||
}
|
||
|
||
if (typeof data === 'string') {
|
||
// If data is a valid JSON string – format it as JSON markdown
|
||
if (isJsonString(data)) {
|
||
return formatToJsonMarkdown(data);
|
||
}
|
||
|
||
// Return original string otherwise
|
||
return data;
|
||
}
|
||
|
||
return formatToJsonMarkdown(JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
function onCopyToClipboard(object: IDataObject | IDataObject[]) {
|
||
try {
|
||
void clipboard.copy(JSON.stringify(object, undefined, 2));
|
||
showMessage({
|
||
title: i18n.baseText('generic.copiedToClipboard'),
|
||
type: 'success',
|
||
});
|
||
} catch {}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div :class="[$style.component, compact ? $style.compact : '']">
|
||
<div
|
||
v-for="({ parsedContent, raw }, index) in content"
|
||
:key="index"
|
||
:class="$style.contentText"
|
||
:data-content-type="parsedContent?.type"
|
||
>
|
||
<template v-if="parsedContent && renderType === 'rendered'">
|
||
<VueMarkdown
|
||
v-if="parsedContent.type === 'json'"
|
||
:source="jsonToMarkdown(parsedContent.data as JsonMarkdown)"
|
||
:class="$style.markdown"
|
||
:options="markdownOptions"
|
||
/>
|
||
<VueMarkdown
|
||
v-else-if="parsedContent.type === 'markdown'"
|
||
:source="parsedContent.data"
|
||
:class="$style.markdown"
|
||
:options="markdownOptions"
|
||
/>
|
||
<p
|
||
v-else-if="parsedContent.type === 'text'"
|
||
:class="$style.runText"
|
||
v-text="parsedContent.data"
|
||
/>
|
||
</template>
|
||
<!-- We weren't able to parse text or raw switch -->
|
||
<div v-else :class="$style.rawContent">
|
||
<N8nIconButton
|
||
size="small"
|
||
:class="$style.copyToClipboard"
|
||
type="secondary"
|
||
:title="i18n.baseText('nodeErrorView.copyToClipboard')"
|
||
icon="files"
|
||
@click="onCopyToClipboard(raw)"
|
||
/>
|
||
<VueMarkdown :source="jsonToMarkdown(raw as JsonMarkdown)" :class="$style.markdown" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" module>
|
||
.runText {
|
||
line-height: var(--font-line-height-xloose);
|
||
white-space: pre-line;
|
||
}
|
||
|
||
.markdown {
|
||
& {
|
||
white-space: pre-wrap;
|
||
|
||
h1 {
|
||
font-size: var(--font-size-l);
|
||
line-height: var(--font-line-height-xloose);
|
||
}
|
||
|
||
h2 {
|
||
font-size: var(--font-size-m);
|
||
line-height: var(--font-line-height-loose);
|
||
}
|
||
|
||
h3 {
|
||
font-size: var(--font-size-s);
|
||
line-height: var(--font-line-height-regular);
|
||
}
|
||
|
||
pre {
|
||
background: var(--chat--message--pre--background);
|
||
border-radius: var(--border-radius-base);
|
||
line-height: var(--font-line-height-xloose);
|
||
padding: var(--spacing-s);
|
||
font-size: var(--font-size-s);
|
||
white-space: pre-wrap;
|
||
|
||
.compact & {
|
||
padding: var(--spacing-3xs);
|
||
font-size: var(--font-size-xs);
|
||
}
|
||
}
|
||
|
||
p {
|
||
.compact & {
|
||
line-height: var(--font-line-height-xloose);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.copyToClipboard {
|
||
position: absolute;
|
||
right: var(--spacing-s);
|
||
top: var(--spacing-s);
|
||
|
||
.compact & {
|
||
right: var(--spacing-2xs);
|
||
top: var(--spacing-2xs);
|
||
}
|
||
}
|
||
|
||
.rawContent {
|
||
position: relative;
|
||
}
|
||
|
||
.contentText {
|
||
padding-top: var(--spacing-s);
|
||
padding-left: var(--spacing-m);
|
||
padding-right: var(--spacing-m);
|
||
font-size: var(--font-size-s);
|
||
|
||
.compact & {
|
||
padding-top: 0;
|
||
padding-inline: var(--spacing-2xs);
|
||
font-size: var(--font-size-2xs);
|
||
}
|
||
}
|
||
</style>
|