Files
n8n-enterprise-unlocked/packages/frontend/editor-ui/src/components/RunDataParsedAiContent.vue
2025-07-01 09:30:17 +02:00

215 lines
4.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>