Skip to content

feat: New MCP comprehensive example added to the official website #3497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 11, 2025
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
1 change: 1 addition & 0 deletions examples/sites/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@vue/shared": "^3.4.31",
"@vueuse/core": "^12.7.0",
"@vueuse/head": "0.7.13",
"crypto-js": "^4.2.0",
"github-markdown-css": "~5.1.0",
"highlight.js": "^11.5.1",
"marked": "^4.3.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/sites/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { iconClose } from '@opentiny/vue-icon'
import { appData } from './tools'
import useTheme from './tools/useTheme'
import { useNextClient } from '@opentiny/next-vue'
import { globalConversation } from './views/components-doc/composition/utils'
import { globalConversation } from './composable/utils'

export default defineComponent({
name: 'AppVue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import { i18nByKey, getWord } from '@/i18n'
import { $split, fetchDemosFile } from '@/tools'
import { Tabs as TinyTabs, TabItem as TinyTabItem, Button as TinyButton } from '@opentiny/vue'
import { AutoTip as vAutoTip } from '@opentiny/vue-directive'
import { languageMap, vueComponents, getWebdocPath, staticDemoPath } from '../cmp-config'
import { languageMap, vueComponents, getWebdocPath, staticDemoPath } from '../views/components-doc/cmp-config'
import { router } from '@/router.js'
import demoConfig from '@demos/config.js'
import { useApiMode, useTemplateMode } from '@/tools'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
<!-- mcp-robot弹窗 -->
<tr-container v-model:show="appData.showTinyRobot" v-model:fullscreen="fullscreen">
<div v-if="messages.length === 0">
<tr-welcome title="智能助手" description="您好,我是Opentiny AI智能助手" :icon="welcomeIcon">
<tr-welcome title="智能助手" description="您好,我是OpenTiny AI智能助手" :icon="welcomeIcon">
<template #footer>
<div class="welcome-footer"></div>
</template>
</tr-welcome>
<tr-prompts
:items="promptItems"
:items="customPromptItems"
:wrap="true"
item-class="prompt-item"
class="tiny-prompts"
Expand All @@ -18,7 +18,7 @@
<tr-bubble-list v-else :items="messages" :roles="roles" auto-scroll></tr-bubble-list>
<template #footer>
<div class="chat-input">
<TrSuggestionPills :items="suggestionPillItems" @item-click="handleSuggestionPillItemClick" /><br />
<TrSuggestionPills :items="customSuggestionPillItems" @item-click="handleSuggestionPillItemClick" /><br />
<tr-sender
ref="senderRef"
mode="single"
Expand All @@ -42,20 +42,21 @@
<script setup lang="ts">
import { TrBubbleList, TrContainer, TrPrompts, TrSender, TrWelcome, TrSuggestionPills } from '@opentiny/tiny-robot'
import { GeneratingStatus } from '@opentiny/tiny-robot-kit'
import { useTinyRobot } from './composition/useTinyRobot'
import { appData } from '../../tools/appData'
import { useTinyRobot } from '../composable/useTinyRobot'
import { appData } from '../tools/appData'

const props = defineProps<{
promptItems: any[]
suggestionPillItems: any[]
}>()

const {
client,
fullscreen,
show,
welcomeIcon,
promptItems,
computedMessages,
promptItems: defaultPromptItems,
messages,
messageState,
inputMessage,
sendMessage,
abortRequest,
roles,
handlePromptItemClick,
Expand All @@ -64,9 +65,12 @@ const {
clearTemplate,
handleSendMessage,
handleMessageKeydown,
suggestionPillItems,
suggestionPillItems: defaultSuggestionPillItems,
handleSuggestionPillItemClick
} = useTinyRobot()

const customPromptItems = props.promptItems || defaultPromptItems
const customSuggestionPillItems = props.suggestionPillItems || defaultSuggestionPillItems
</script>
Comment on lines +72 to 74
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fallback arrays are not reactive – wrap them in computed()

customPromptItems / customSuggestionPillItems are evaluated once.
If parent components update the props later, the UI will not react.
Make them computed so they track prop changes:

-import { computed } from 'vue'
+import { computed } from 'vue'

-const customPromptItems = props.promptItems || defaultPromptItems
-const customSuggestionPillItems = props.suggestionPillItems || defaultSuggestionPillItems
+const customPromptItems = computed(
+  () => props.promptItems?.length ? props.promptItems : defaultPromptItems
+)
+const customSuggestionPillItems = computed(
+  () => props.suggestionPillItems?.length ? props.suggestionPillItems : defaultSuggestionPillItems
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const customPromptItems = props.promptItems || defaultPromptItems
const customSuggestionPillItems = props.suggestionPillItems || defaultSuggestionPillItems
</script>
<script setup lang="ts">
import { computed } from 'vue'
const customPromptItems = computed(
() => props.promptItems?.length ? props.promptItems : defaultPromptItems
)
const customSuggestionPillItems = computed(
() => props.suggestionPillItems?.length ? props.suggestionPillItems : defaultSuggestionPillItems
)
</script>
🤖 Prompt for AI Agents
In examples/sites/src/components/tiny-robot-chat.vue around lines 72 to 74, the
fallback arrays for customPromptItems and customSuggestionPillItems are assigned
directly, making them non-reactive to prop changes. To fix this, wrap these
assignments in computed() so they update reactively when the props change,
ensuring the UI reflects the latest data from parent components.


<style scoped lang="less">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export class DifyModelProvider extends BaseModelProvider {
try {
// 验证请求的messages属性,必须是数组,且每个消息必须有role\content属性
const lastMessage = request.messages[request.messages.length - 1].content

// 模拟异步流式响应
const response = await fetch(`${this.config.apiUrl}/chat-messages`, {
method: 'POST',
Expand Down
71 changes: 71 additions & 0 deletions examples/sites/src/composable/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ref, watch } from 'vue'

function parse(str) {
if (str === null) return undefined
const type = str[0]
const strVal = str.slice(1)
// 对象时,有可能是Date, 否则反解析后统一是对象
if (type === 'o' || type === 'b') {
let val = JSON.parse(strVal)
return typeof val === 'string' ? new Date(val) : val
}
if (type === 'n') return +Number(strVal)
if (type === 's') return strVal
}
Comment on lines +8 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

parse() mis-classifies boolean vs object & drops undefined

  1. if (type === 'o' || type === 'b') conflates booleans with objects.
    JSON.parse('true') works, but treating it as object is misleading.
  2. Prefix 'u' for undefined written by save() is never handled on read.
-  if (type === 'o' || type === 'b') {
+  if (type === 'o') return JSON.parse(strVal)
+  if (type === 'b') return strVal === 'true'
+  if (type === 'u') return undefined

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/sites/src/composable/storage.ts around lines 8 to 14, the parse()
function incorrectly treats booleans as objects by combining 'o' and 'b' types,
and it does not handle the 'u' prefix for undefined values. To fix this,
separate the handling of booleans ('b') from objects ('o') by parsing booleans
directly without JSON.parse, handle the 'u' prefix by returning undefined when
encountered, and ensure objects are parsed correctly with JSON.parse.


// 带前缀的保存值
function save(store, k, v) {
const type = typeof v
store.setItem(k, type[0] + (type === 'object' ? JSON.stringify(v) : v))
}

/**
* 快速的保存值到 sessionStorage, localStorage.
* 支持基本类型,时间,数组,对象,null,不存在的键值返回undefined 。
* 不支持:Map,Set, 以及多级串联赋值,比如:$session.obj.name="abcd"
*/
function handler(storage) {
return {
get(target, propKey, receiver) {
return parse(storage.getItem(propKey))
},
set(target, propKey, value) {
save(storage, propKey, value)
return true
}
}
}

/** * 快速读写sessionStorage 示例: $session.abc="shen" */
const $session = new Proxy({}, handler(sessionStorage))

/** * 快速读写 localStorage 示例: $local.abc="shen" */
const $local = new Proxy({}, handler(localStorage))

/** * 全局共享值,刷新即丢失! 示例: $cache.abc="shen" */
const $cache = {}

const typeMatcher = { session: $session, local: $local, api: null }

/**
* 用于记录用户行为,并保存到session,local 或api接口(api保存的功能还未实现)
* 示例:useAutoStore("session","key1")
* useAutoStore("session","key2",100)
* useAutoStore("session","key2",$session.key2 || 100)
* @param type 自动存储到的目标
* @param key 存储时的key
* @param defaultValue 默认值。
* @returns 响应式ref
*/
const useAutoStore = (type, key, defaultValue) => {
let refVar = ref(typeMatcher[type][key])
watch(refVar, (curr, prev) => {
typeMatcher[type][key] = curr
})

refVar.value = refVar.value ?? defaultValue

return refVar
}

Comment on lines +48 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

api branch is unusable and will throw at runtime

typeMatcher.api is null; useAutoStore('api', ...) therefore
dereferences null[key], throwing a TypeError.

-const typeMatcher = { session: $session, local: $local, api: null }
+const typeMatcher = { session: $session, local: $local, api: {} } // TODO: wire real impl

Also guard against unsupported types:

if (!typeMatcher[type])
  throw new Error(`Unsupported auto-store type: ${type}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const typeMatcher = { session: $session, local: $local, api: null }
/**
* 用于记录用户行为,并保存到session,local 或api接口(api保存的功能还未实现)
* 示例:useAutoStore("session","key1")
* useAutoStore("session","key2",100)
* useAutoStore("session","key2",$session.key2 || 100)
* @param type 自动存储到的目标
* @param key 存储时的key
* @param defaultValue 默认值。
* @returns 响应式ref
*/
const useAutoStore = (type, key, defaultValue) => {
let refVar = ref(typeMatcher[type][key])
watch(refVar, (curr, prev) => {
typeMatcher[type][key] = curr
})
refVar.value = refVar.value ?? defaultValue
return refVar
}
// change api from null to an empty object placeholder
const typeMatcher = { session: $session, local: $local, api: {} } // TODO: wire real impl
/**
* 用于记录用户行为,并保存到session,local 或api接口(api保存的功能还未实现)
* 示例:useAutoStore("session","key1")
* useAutoStore("session","key2",100)
* useAutoStore("session","key2",$session.key2 || 100)
* @param type 自动存储到的目标
* @param key 存储时的key
* @param defaultValue 默认值。
* @returns 响应式ref
*/
const useAutoStore = (type, key, defaultValue) => {
// guard against unsupported storage targets
if (!typeMatcher[type]) {
throw new Error(`Unsupported auto-store type: ${type}`)
}
let refVar = ref(typeMatcher[type][key])
watch(refVar, (curr, prev) => {
typeMatcher[type][key] = curr
})
refVar.value = refVar.value ?? defaultValue
return refVar
}
🤖 Prompt for AI Agents
In examples/sites/src/composable/storage.ts around lines 48 to 70, the
typeMatcher.api is set to null, causing a runtime TypeError when useAutoStore is
called with 'api' because it tries to access null[key]. To fix this, initialize
typeMatcher.api with a valid object or implement a guard clause in useAutoStore
to check if the type exists and is an object before accessing its keys. If the
type is unsupported, throw an informative error or handle it gracefully to
prevent runtime exceptions.

export { $session, $local, $cache, useAutoStore }
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

import type { ChatMessage, ChatCompletionResponse, StreamHandler } from '@opentiny/tiny-robot-kit'
import type { ChatCompletionRequest } from '@opentiny/tiny-robot-kit'
import type { Ref } from 'vue'
import { ref, type Ref } from 'vue'

export { $local } from './storage'

export const showTinyRobot = ref(true)

Comment on lines +10 to 13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate global‐state source: prefer a single store for showTinyRobot

showTinyRobot is already exposed through appData (used in several other files), and now it is re-declared and exported here as a standalone ref. Two independent refs will desynchronise the UI if they are both imported in different places.

Consider re-exporting the existing appData.showTinyRobot instead of creating a new one:

-import { ref, type Ref } from 'vue'
+import { type Ref } from 'vue'
+import { appData } from '../tools/appData'

-export const showTinyRobot = ref(true)
+export const showTinyRobot = appData.showTinyRobot

This keeps a single source of truth and avoids subtle reactivity bugs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export { $local } from './storage'
export const showTinyRobot = ref(true)
// Replace the existing Vue import
-import { ref, type Ref } from 'vue'
+import { type Ref } from 'vue'
+import { appData } from '../tools/appData'
export { $local } from './storage'
-export const showTinyRobot = ref(true)
+export const showTinyRobot = appData.showTinyRobot
🤖 Prompt for AI Agents
In examples/sites/src/composable/utils.ts around lines 10 to 13, avoid declaring
and exporting a new ref named showTinyRobot since it duplicates the existing
global state in appData. Instead, remove the current showTinyRobot ref
declaration and re-export the existing appData.showTinyRobot to maintain a
single source of truth and prevent UI desynchronization issues.

export const globalConversation = {
id: '',
Expand Down
6 changes: 6 additions & 0 deletions examples/sites/src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Components = () => import('@/views/components-doc/index.vue')
const Docs = () => import('@/views/docs/docs.vue')
const Overview = () => import('@/views/overview.vue')
const Features = () => import('@/views/features.vue')
const Comprehensive = () => import('@/views/comprehensive/index.vue')

const context = import.meta.env.VITE_CONTEXT

Expand All @@ -18,6 +19,11 @@ let routes = [
name: 'overview',
children: [{ name: 'Overview', path: '', component: Overview, meta: { title: '组件总览 | TinyVue' } }]
},
{
path: `${context}:all?/zh-CN/:theme/comprehensive`,
component: Comprehensive,
name: 'comprehensive'
},
// 文档
{
path: `${context}:all?/:lang/:theme/docs/:docId`,
Expand Down
16 changes: 8 additions & 8 deletions examples/sites/src/views/components-doc/common.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,16 @@ import { debounce } from '@opentiny/utils'
import { i18nByKey, getWord, $clone, useApiMode } from '@/tools'
import { router } from '@/router.js'
import { getWebdocPath } from './cmp-config'
import DemoBox from './components/demo.vue'
import AsideAnchor from './components/anchor.vue'
import ComponentHeader from './components/header.vue'
import ComponentContributor from './components/contributor.vue'
import ApiDocs from './components/api-docs.vue'
import McpDocs from './components/mcp-docs.vue'
import useTasksFinish from './composition/useTasksFinish'
import DemoBox from '../../components/demo.vue'
import AsideAnchor from '../../components/anchor.vue'
import ComponentHeader from '../../components/header.vue'
import ComponentContributor from '../../components/contributor.vue'
import ApiDocs from '../../components/api-docs.vue'
import McpDocs from '../../components/mcp-docs.vue'
import useTasksFinish from '../../composable/useTasksFinish'
import { appData } from '../../tools/appData'

import robotChat from './tiny-robot-chat.vue'
import robotChat from '../../components/tiny-robot-chat.vue'

const props = defineProps({ loadData: {}, appMode: {}, demoKey: {} })

Expand Down
Loading
Loading