Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/plugins/page/src/mcp/tools/editSpecificPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const inputSchema = z.object({
})

export const editSpecificPage = {
name: 'Edit_page_in_canvas.',
name: 'edit_page_in_canvas',
title: '在画布中编辑页面',
order: 9,
description: 'Edit a specific page in canvas. Use this tool when you need to edit some page in canvas.',
Expand Down
Binary file added packages/plugins/robot/assets/loading.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 107 additions & 30 deletions packages/plugins/robot/src/Main.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,27 @@
<icon-new-session />
</button>
</template>
<div v-if="activeMessages.length === 0">
<tr-welcome title="AI助手" description="您好,我是您的开发小助手" :icon="welcomeIcon" class="robot-welcome">
</tr-welcome>
<tr-prompts
:items="promptItems"
:wrap="true"
item-class="prompt-item"
class="tiny-prompts"
@item-click="handlePromptItemClick"
></tr-prompts>
<div class="robot-chat-container-content" ref="chatContainerRef">
<div v-if="activeMessages.length === 0">
<tr-welcome
title="AI助手"
description="您好,我是您的开发小助手"
:icon="welcomeIcon"
class="robot-welcome"
>
</tr-welcome>
<tr-prompts
:items="promptItems"
:wrap="true"
item-class="prompt-item"
class="tiny-prompts"
@item-click="handlePromptItemClick"
></tr-prompts>
</div>
<tr-bubble-provider :content-renderers="contentRenderers" v-else>
<tr-bubble-list :items="activeMessages" :roles="roles" autoScroll></tr-bubble-list>
</tr-bubble-provider>
</div>
<tr-bubble-provider :message-renderers="{ markdown: MarkdownRenderer }" v-else>
<tr-bubble-list :items="activeMessages" :roles="roles" autoScroll></tr-bubble-list>
</tr-bubble-provider>
<template #footer>
<tr-sender
:maxlength="4000"
Expand All @@ -61,7 +68,7 @@
@submit="sendContent(inputContent, false)"
>
<template #footer-left>
<mcp-server></mcp-server>
<mcp-server :position="mcpDrawerPosition"></mcp-server>
</template>
</tr-sender>
</template>
Expand All @@ -73,7 +80,18 @@

<script lang="ts">
/* metaService: engine.plugins.robot.Main */
import { ref, onMounted, watchEffect, type CSSProperties, h, resolveComponent } from 'vue'
import {
ref,
onMounted,
watchEffect,
type CSSProperties,
h,
resolveComponent,
computed,
watch,
nextTick,
type Component
} from 'vue'
import { Notify, Loading, TinyPopover } from '@opentiny/vue'
import { useCanvas, useHistory, usePage, useModal, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { extend } from '@opentiny/vue-renderless/common/object'
Expand All @@ -85,6 +103,7 @@ import { getBlockContent, initBlockList, getAIModelOptions } from './js/robotSet
import McpServer from './mcp/McpServer.vue'
import useMcpServer from './mcp/useMcp'
import MarkdownRenderer from './mcp/MarkdownRenderer.vue'
import LoadingRenderer from './mcp/LoadingRenderer.vue'
import { sendMcpRequest } from './mcp/utils'

export default {
Expand Down Expand Up @@ -118,6 +137,8 @@ export default {
const tokenValue = ref('')
const showPopover = ref(false)

const chatContainerRef = ref(null)

const { pageSettingState, getDefaultPage } = usePage()
const ROOT_ID = pageSettingState.ROOT_ID
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
Expand All @@ -142,6 +163,14 @@ export default {
)
}

const scrollContent = async () => {
await nextTick()
const el = chatContainerRef.value as HTMLElement | null
if (el) {
el.scrollTop = el.scrollHeight
}
}

const createNewPage = (schema) => {
if (!(pageSettingState.isNew && pageSettingState.isAIPage)) {
pageSettingState.isNew = true
Expand Down Expand Up @@ -188,8 +217,7 @@ export default {

const getAiRespMessage = (role = 'assistant', content) => ({
role,
content,
name: 'AI'
content
})

const requestLoading = ref(false)
Expand All @@ -198,6 +226,7 @@ export default {
if (useMcpServer().isToolsEnabled) {
try {
requestLoading.value = true
await scrollContent()
await sendMcpRequest(messages.value, {
model: selectedModel.value.value,
headers: {
Expand All @@ -209,6 +238,7 @@ export default {
} finally {
inProcesing.value = false
requestLoading.value = false
await scrollContent()
}
return
}
Expand Down Expand Up @@ -236,14 +266,6 @@ export default {
})
}

const scrollContent = async () => {
await sleep(100)
const scrollElement = document.getElementById('chatgpt-window')
if (scrollElement) {
scrollElement.scrollTop = scrollElement.scrollHeight
}
}

const resetContent = async () => {
activeMessages.value = messages.value
await scrollContent()
Expand All @@ -257,8 +279,7 @@ export default {

const getMessage = (content) => ({
role: 'user',
content,
name: 'John'
content
})

const sendContent = async (content, isModel) => {
Expand Down Expand Up @@ -412,11 +433,38 @@ export default {

// 对话角色配置
const roles: Record<string, BubbleRoleConfig> = {
assistant: { placement: 'start', avatar: aiAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer },
assistant: {
placement: 'start',
avatar: aiAvatar,
maxWidth: '90%',
contentRenderer: MarkdownRenderer,
customContentField: 'renderContent'
},
user: { placement: 'end', avatar: userAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer }
Comment thread
hexqi marked this conversation as resolved.
}

watch([() => activeMessages.value.length, () => activeMessages.value.at(-1)?.renderContent?.length ?? 0], () => {
scrollContent()
})

const contentRenderers: Record<string, Component> = {
markdown: MarkdownRenderer,
loading: LoadingRenderer
}

const mcpDrawerPosition = computed(() => {
return {
type: 'fixed',
position: {
top: 'var(--base-top-panel-height)',
bottom: 0,
...(fullscreen.value ? { left: 0 } : { right: 'var(--tr-container-width)' })
}
}
})

return {
chatContainerRef,
robotVisible,
avatarUrl,
chatWindowOpened,
Expand All @@ -439,8 +487,9 @@ export default {
handlePromptItemClick,
welcomeIcon,
roles,
MarkdownRenderer,
requestLoading
contentRenderers,
requestLoading,
mcpDrawerPosition
}
}
}
Expand Down Expand Up @@ -486,10 +535,38 @@ export default {
}

.tr-bubble-list {
font-size: 14px;
flex: 1;
.tr-bubble {
word-break: break-word;
}
ul,
ol {
padding-left: 10px;
}
ul > li {
list-style: disc;
}
ol > li {
list-style: decimal;
}
table {
border-collapse: collapse; // 合并边框
border: 1px solid #ccc;
width: 100%;
margin: 1rem 0;
th,
td {
border: 1px solid #ccc; /* 单元格边框 */
padding: 8px;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
tr:hover {
background-color: #e6f7ff;
Comment thread
hexqi marked this conversation as resolved.
}
}
}

.robot-welcome > div {
Expand Down
3 changes: 3 additions & 0 deletions packages/plugins/robot/src/mcp/LoadingRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<img src="../../assets/loading.webp" alt="loading" style="width: 24px; height: 24px" />
</template>
118 changes: 85 additions & 33 deletions packages/plugins/robot/src/mcp/MarkdownRenderer.vue
Original file line number Diff line number Diff line change
@@ -1,57 +1,109 @@
<template>
<div v-html="renderContent" class="robot-markdown-renderer"></div>
<div v-html="renderContent" class="markdown-renderer" :class="themeClass"></div>
</template>

<script setup lang="ts">
import DOMPurify from 'dompurify'
import MarkdownIt, { type Options as MarkdownItOptions } from 'markdown-it'
import hljs from 'highlight.js'
import { computed } from 'vue'
import 'highlight.js/styles/github.min.css'
import DOMPurify from 'dompurify'
import MarkdownIt, { type Options } from 'markdown-it'
import hljs from 'highlight.js/lib/core'
import 'highlight.js/styles/github.css'

// 按需加载语言
import bash from 'highlight.js/lib/languages/bash'
import javascript from 'highlight.js/lib/languages/javascript'
import typescript from 'highlight.js/lib/languages/typescript'
import json from 'highlight.js/lib/languages/json'
import yaml from 'highlight.js/lib/languages/yaml'
import xml from 'highlight.js/lib/languages/xml'
import shell from 'highlight.js/lib/languages/shell'

// 注册语言
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('json', json)
hljs.registerLanguage('yaml', yaml)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('shell', shell)

const props = defineProps({
content: {
type: String,
required: true
},
theme: {
type: String as () => 'light' | 'dark',
default: 'light'
},
options: {
type: Object as () => Options,
default: () => ({})
}
})

const defaultMarkdownItOptions = {
const themeClass = computed(() => `hljs-theme-${props.theme}`)

const escapeHtml = (s: string) =>
s.replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]!))

const markdownIt = new MarkdownIt({
html: true,
breaks: true,
typographer: true,
highlight: function (str: string, lang: string) {
highlight: (str: string, lang: string) => {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlightAuto(str).value
} catch (__) {
const { value } = hljs.highlight(str, { language: lang, ignoreIllegals: true })
return `<pre class="hljs"><code class="language-${lang}">${value}</code></pre>`
} catch (e) {
/* ignore */
}
}

return str
}
}

const props = defineProps<{
type?: 'markdown' | 'text'
content: string
options?: MarkdownItOptions
}>()

const markdownIt = computed(
() =>
new MarkdownIt({
...defaultMarkdownItOptions,
...props.options
})
)
return `<pre class="hljs"><code>${escapeHtml(str)}</code></pre>`
},
...props.options
})

const renderContent = computed(() => {
const htmlContent = markdownIt.value.render(props.content)
return DOMPurify.sanitize(htmlContent)
return DOMPurify.sanitize(markdownIt.render(props.content))
})
</script>

<style lang="less" scoped>
.robot-markdown-renderer {
<style lang="less">
.markdown-renderer {
word-break: break-word;
& > *:first-child {

pre {
border-radius: 6px;
padding: 1em;
overflow: auto;
line-height: 1.45;
> code {
font-size: 12px;
}
}

// TODO: 适配TInyEngine主题,实现跟随主题自动切换
/* 亮色主题 */
&.hljs-theme-light {
pre {
background-color: #f6f8fa;
}
}

/* 暗色主题 */
&.hljs-theme-dark {
pre {
background-color: #0d1117;
}
}

> *:first-child {
margin-top: 0 !important;
}
& > *:last-child {

> *:last-child {
margin-bottom: 0 !important;
}
}
Expand Down
Loading