Skip to content
Open
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
13 changes: 8 additions & 5 deletions packages/plugins/robot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"access": "public"
},
"scripts": {
"build": "vite build"
"build": "vite build",
"test": "vitest run",
"test:watch": "vitest"
},
"type": "module",
"main": "dist/index.js",
Expand All @@ -29,9 +31,9 @@
"@opentiny/tiny-engine-common": "workspace:*",
"@opentiny/tiny-engine-meta-register": "workspace:*",
"@opentiny/tiny-engine-utils": "workspace:*",
"@opentiny/tiny-robot": "0.3.1",
"@opentiny/tiny-robot-kit": "0.3.1",
"@opentiny/tiny-robot-svgs": "0.3.1",
"@opentiny/tiny-robot": "0.4.0",
"@opentiny/tiny-robot-kit": "0.4.0",
"@opentiny/tiny-robot-svgs": "0.4.0",
"@opentiny/tiny-schema-renderer": "1.0.0-beta.6",
"@vueuse/core": "^9.13.0",
"dompurify": "^3.0.1",
Expand All @@ -45,7 +47,8 @@
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"vite": "^5.4.2"
"vite": "^5.4.2",
"vitest": "^1.6.1"
},
"peerDependencies": {
"@opentiny/vue": "^3.20.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/robot/src/Main.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
v-model:fullscreen="fullscreen"
v-model:show="robotVisible"
v-model:input="inputMessage"
:status="chatStatus"
:status="mappedStatus"
:prompt-items="promptItems"
:bubble-renderers="bubbleRenderers"
:allowFiles="isVisualModel && robotSettingState.chatMode === ChatMode.Agent"
Expand Down Expand Up @@ -147,7 +147,7 @@ const showTeleport = ref(false)
const showSetting = ref(false)

const {
chatStatus,
mappedStatus,
inputMessage,
messages,
changeChatMode,
Expand Down
108 changes: 83 additions & 25 deletions packages/plugins/robot/src/components/chat/RobotChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,18 @@
@item-click="handlePromptItemClick"
></tr-prompts>
</div>
<tr-bubble-provider v-else :content-renderers="contentRenderers">
<tr-bubble-list :items="messages" :roles="roles" auto-scroll class="robot-bubble-list"> </tr-bubble-list>
<tr-bubble-provider v-else :content-renderer-matches="contentRendererMatches">
<tr-bubble-list
:messages="messages"
:role-configs="roleConfigs"
:content-resolver="resolveMessageContent"
auto-scroll
class="robot-bubble-list"
>
<template #content-footer="{ messages }">
<div v-if="messages[0]?.aborted" class="aborted">已中止</div>
</template>
</tr-bubble-list>
</tr-bubble-provider>
</div>

Expand All @@ -39,9 +49,6 @@
:showWordLimit="false"
@submit="handleSendMessage"
@cancel="handleAbortRequest"
:allowFiles="selectedAttachments.length < 1 && props.allowFiles"
uploadTooltip="支持上传1张图片"
@files-selected="handleSingleFilesSelected"
>
<template #header v-if="selectedAttachments.length > 0">
<div>
Expand All @@ -55,9 +62,18 @@
</tr-attachments>
</div>
</template>
<template #footer-left>
<template #footer>
<slot name="footer-left"></slot>
</template>
<template #footer-right>
<VoiceButton :speech-config="{ lang: 'zh-CN', continuous: false }" />
<UploadButton
v-if="selectedAttachments.length < 1 && props.allowFiles"
accept="image/*"
:multiple="false"
@select="handleSingleFilesSelected"
/>
</template>
</tr-sender>
</div>
</template>
Expand All @@ -74,11 +90,17 @@ import {
TrSender,
TrWelcome,
TrAttachments,
UploadButton,
VoiceButton,
BubbleRenderers,
BubbleRendererMatchPriority,
type BubbleRoleConfig,
type PromptProps,
type RawFileAttachment
type RawFileAttachment,
type BubbleContentRendererMatch
} from '@opentiny/tiny-robot'
import { type ChatMessage, GeneratingStatus } from '@opentiny/tiny-robot-kit'
import { type ChatMessage } from '@opentiny/tiny-robot-kit'
import { GeneratingStatus } from '../../constants/status'
import { LoadingRenderer, MarkdownRenderer, ImgRenderer } from '../renderers'
import { useNotify } from '@opentiny/tiny-engine-meta-register'

Expand Down Expand Up @@ -123,6 +145,41 @@ watch(
}
)

const contentRendererMatches = computed<BubbleContentRendererMatch[]>(() => [
{
priority: BubbleRendererMatchPriority.LOADING,
find: (message) => Boolean(message.loading),
renderer: LoadingRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) => content?.type === 'tool' && message.tool_calls?.length,
renderer: BubbleRenderers.Tools
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) =>
content?.type !== 'tool' && typeof message.reasoning_content === 'string' && message.reasoning_content,
renderer: BubbleRenderers.Reasoning
},
...Object.entries(props.bubbleRenderers).map(([type, renderer]) => ({
priority: BubbleRendererMatchPriority.NORMAL,
find: (_message: any, content: any) => content?.type === type,
renderer
})),
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) =>
!message.loading && message.content && (!content?.type || ['markdown', 'text'].includes(content.type)),
renderer: MarkdownRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any) => message?.content?.[0]?.type === 'img' || message?.content?.[0]?.type === 'image',
renderer: ImgRenderer
}
])
Comment thread
lichunn marked this conversation as resolved.

// 处理文件选择事件
const handleSingleFilesSelected = (files: File[] | null, retry = false) => {
if (!files?.length) return
Expand Down Expand Up @@ -178,32 +235,27 @@ const getSvgIcon = (name: string, style?: CSSProperties) => {
const aiAvatar = getSvgIcon('AI')
const welcomeIcon = getSvgIcon('AI', { fontSize: '44px' })

const contentRenderers = computed(() => ({
markdown: MarkdownRenderer,
loading: LoadingRenderer,
img: ImgRenderer,
...props.bubbleRenderers
}))
const resolveMessageContent = (message: any) => {
if (Array.isArray(message.renderContent) && message.renderContent.length > 0) {
return message.renderContent
}

return message.content
}

const roles: Record<string, BubbleRoleConfig> = {
const roleConfigs: Record<string, BubbleRoleConfig> = {
assistant: {
placement: 'start',
avatar: aiAvatar,
contentRenderer: MarkdownRenderer,
customContentField: 'renderContent'
avatar: aiAvatar
},
user: {
placement: 'end',
contentRenderer: MarkdownRenderer,
customContentField: 'renderContent'
placement: 'end'
},
system: {
hidden: true
}
}

const senderRef = ref<InstanceType<typeof TrSender> | null>(null)

// 发送消息
const handleSendMessage = async (content: string) => {
const messageContent = content || inputMessage.value
Expand Down Expand Up @@ -371,13 +423,13 @@ const handlePromptItemClick = (ev: unknown, item: { description?: string }) => {
}
}
:deep([data-role='user']) {
--tr-bubble-content-bg: var(--tr-color-primary-light);
--tr-bubble-box-bg: var(--tr-color-primary-light);
}
}

&.fullscreen {
:deep([data-role='assistant']) {
--tr-bubble-content-bg: transparent;
--tr-bubble-box-bg: transparent;
.tr-bubble__content {
padding: 8px 0 0;
}
Expand Down Expand Up @@ -491,4 +543,10 @@ const handlePromptItemClick = (ev: unknown, item: { description?: string }) => {
.robot-bubble-list {
height: 100%;
}

.aborted {
margin-top: 6px;
font-size: 12px;
opacity: 0.7;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

<script setup lang="ts">
import { IconHistory, IconClose } from '@opentiny/tiny-robot-svgs'
import type { Conversation } from '@opentiny/tiny-robot-kit'
import type { ConversationInfo } from '@opentiny/tiny-robot-kit'
import { TrHistory, TrIconButton, type HistoryItem, type HistoryMenuItem } from '@opentiny/tiny-robot'
import { computed, ref } from 'vue'

Expand All @@ -37,7 +37,7 @@ const showHistory = ref(false)
interface HistoryProps {
conversationState: {
currentId?: string | null
conversations: Conversation[]
conversations: ConversationInfo[]
}
onItemClick?: (item: HistoryItem) => void
onItemAction?: (action: HistoryMenuItem, item: HistoryItem) => void
Expand All @@ -47,7 +47,7 @@ interface HistoryProps {
const props = defineProps<HistoryProps>()

// 将平铺格式的历史会话数据转换为分组格式(基于createdAt时间戳)
const convertFlatToGrouped = (flatData: Conversation[]): Array<{ group: string; items: Conversation[] }> => {
const convertFlatToGrouped = (flatData: ConversationInfo[]): Array<{ group: string; items: ConversationInfo[] }> => {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const getDate = (days: number) => {
Expand All @@ -64,7 +64,7 @@ const convertFlatToGrouped = (flatData: Conversation[]): Array<{ group: string;
{ group: '更早', threshold: new Date(0) }
]

const groups = groupConfigs.map((config) => ({ ...config, items: [] as Conversation[] }))
const groups = groupConfigs.map((config) => ({ ...config, items: [] as ConversationInfo[] }))

flatData.forEach((item) => {
const itemDate = new Date(item.createdAt)
Expand Down
45 changes: 36 additions & 9 deletions packages/plugins/robot/src/components/renderers/AgentRenderer.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="build-loading-renderer" v-if="!hasReasoningFinished">
<img :src="getIconUrl(statusData.icon)" :alt="status" />
<img :src="getIconUrl(statusData.icon)" :alt="resolvedStatus" />
<div class="build-loading-renderer-content">
<div class="build-loading-renderer-content-header">{{ statusData.title }}</div>
<div class="build-loading-renderer-content-body">{{ statusData.content }}</div>
Expand All @@ -11,35 +11,59 @@
<script lang="ts">
import { computed } from 'vue'

export const resolveAgentRenderState = (props: any) => {
const renderContent = props.message?.renderContent
const contentItem = Array.isArray(renderContent) ? renderContent[props.contentIndex || 0] || {} : {}

return {
status: contentItem.status || props.status || 'loading',
content: contentItem.content ?? props.content,
contentType: contentItem.type || contentItem.contentType || props.contentType
}
}

export default {
props: {
content: {
type: String,
required: true
default: ''
},
status: {
type: String,
default: 'loading'
default: ''
},
contentType: {
type: String
},
message: {
type: Object,
default: () => ({})
},
contentIndex: {
type: Number,
default: 0
}
},
setup(props) {
const getIconUrl = (icon: string) => {
return new URL(`../../../assets/${icon}`, import.meta.url).href
}

const resolvedState = computed(() => resolveAgentRenderState(props))
const resolvedStatus = computed(() => resolvedState.value.status)
const resolvedContent = computed(() => resolvedState.value.content)
const resolvedContentType = computed(() => resolvedState.value.contentType)

const statusDataMap = {
reasoning: {
title: '深度思考中,请稍等片刻',
icon: 'loading.webp',
content: () => props.content?.slice(-30) || '...'
content: '...'
},
loading: {
title: '页面生成中,请稍等片刻',
icon: 'loading.webp',
content: () => props.content?.slice(-30) || '...'
content: '...'
},
fix: {
title: '页面优化中,请稍等片刻',
Expand All @@ -53,16 +77,18 @@ export default {
},
failed: {
title: '页面生成失败',
content: () => props.content?.slice(-30) || '页面生成失败',
content: () => resolvedContent.value?.slice(-30) || '页面生成失败',
icon: 'failed.svg'
}
}

const hasReasoningFinished = computed(() => props.contentType === 'reasoning' && props.status !== 'reasoning')
const hasReasoningFinished = computed(
() => resolvedContentType.value === 'reasoning' && resolvedStatus.value !== 'reasoning'
)

const statusData = computed(() => {
let status = props.status as keyof typeof statusDataMap
if (props.contentType === 'reasoning') {
let status = resolvedStatus.value as keyof typeof statusDataMap
if (resolvedContentType.value === 'reasoning') {
status = 'reasoning'
}
const data = statusDataMap[status] || statusDataMap.loading
Expand All @@ -74,6 +100,7 @@ export default {

return {
statusData,
resolvedStatus,
getIconUrl,
hasReasoningFinished
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ hljs.registerLanguage('xml', xml)
hljs.registerLanguage('shell', shell)

const props = defineProps({
content: {
type: String,
required: true
message: {
type: Object as () => Options,
default: () => ({})
},
theme: {
type: String as () => 'light' | 'dark',
Expand Down Expand Up @@ -66,7 +66,7 @@ const markdownIt = new MarkdownIt({
})

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

Expand Down
Loading
Loading