|
| 1 | +<template> |
| 2 | + <div class="robot"> |
| 3 | + <toolbar-base |
| 4 | + content="AI对话框" |
| 5 | + :icon="options.icon?.default || options?.icon" |
| 6 | + :options="options" |
| 7 | + @click-api="openAIRobot" |
| 8 | + > |
| 9 | + </toolbar-base> |
| 10 | + <Teleport v-if="showTeleport" defer :to="fullscreen ? 'body' : '.tiny-engine-right-robot'"> |
| 11 | + <div class="robot-chat-container" :class="{ 'robot-chat-container-fullscreen': fullscreen }"> |
| 12 | + <robot-chat |
| 13 | + :ref="robotChatRef" |
| 14 | + :prompt-items="promptItems" |
| 15 | + :allowFiles="isVisualModel() && aiType === AI_MODES.Agent" |
| 16 | + @fileSelected="handleFileSelected" |
| 17 | + > |
| 18 | + <template #operations> |
| 19 | + <tiny-popover |
| 20 | + width="290" |
| 21 | + trigger="manual" |
| 22 | + v-model="showSettingPopover" |
| 23 | + :visible-arrow="false" |
| 24 | + popper-class="setting-popover" |
| 25 | + > |
| 26 | + <robot-setting-popover |
| 27 | + v-if="showSettingPopover" |
| 28 | + @changeType="changeModel" |
| 29 | + @close="closePanel" |
| 30 | + ></robot-setting-popover> |
| 31 | + <template #reference> |
| 32 | + <span class="chat-title-dropdown" @click.stop="showSettingPopover = true"> |
| 33 | + <svg-icon name="setting" class="operations-setting ml8"> </svg-icon> |
| 34 | + </span> |
| 35 | + </template> |
| 36 | + </tiny-popover> |
| 37 | + </template> |
| 38 | + <template #footer-left> |
| 39 | + <robot-type-select :aiType="aiType" @typeChange="typeChange"></robot-type-select> |
| 40 | + <mcp-server :position="mcpDrawerPosition" v-if="aiType === AI_MODES.Chat"></mcp-server> |
| 41 | + </template> |
| 42 | + </robot-chat> |
| 43 | + <tiny-dialog-box v-model:visible="showPreview" title="当前AI渲染效果" width="80%"> |
| 44 | + <schema-renderer v-if="showPreview" :schema="currentSchema"></schema-renderer> |
| 45 | + </tiny-dialog-box> |
| 46 | + </div> |
| 47 | + </Teleport> |
| 48 | + </div> |
| 49 | +</template> |
| 50 | + |
| 51 | +<script setup lang="ts"> |
| 52 | +import { computed, h, onMounted, ref } from 'vue' |
| 53 | +import { ToolbarBase } from '@opentiny/tiny-engine-common' |
| 54 | +import RobotChat from './components/RobotChat.vue' |
| 55 | +import RobotSettingPopover from './components/RobotSettingPopover.vue' |
| 56 | +import { TinyPopover, TinyDialogBox } from '@opentiny/vue' |
| 57 | +import { useRobot, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' |
| 58 | +import McpIconComponent from './icons/mcp-icon.vue' |
| 59 | +import PageIconComponent from './icons/page-icon.vue' |
| 60 | +import StudyIconComponent from './icons/study-icon.vue' |
| 61 | +import type { PromptProps } from '@opentiny/tiny-robot' |
| 62 | +import SchemaRenderer from '@opentiny/tiny-schema-renderer' |
| 63 | +import RobotTypeSelect from './components/RobotTypeSelect.vue' |
| 64 | +import McpServer from './mcp/McpServer.vue' |
| 65 | +import { updateLLMConfig } from './client' |
| 66 | +
|
| 67 | +const { options } = defineProps({ |
| 68 | + options: { |
| 69 | + type: Object, |
| 70 | + default: () => ({}) |
| 71 | + } |
| 72 | +}) |
| 73 | +
|
| 74 | +const robotChatRef = ref('robotChatRef') |
| 75 | +
|
| 76 | +const fullscreen = computed(() => { |
| 77 | + return robotChatRef.value?.fullscreen |
| 78 | +}) |
| 79 | +
|
| 80 | +const mcpDrawerPosition = computed(() => { |
| 81 | + return { |
| 82 | + type: 'fixed', |
| 83 | + position: { |
| 84 | + top: 'var(--base-top-panel-height)', |
| 85 | + bottom: 0, |
| 86 | + ...(fullscreen.value ? { left: 0 } : { right: 'var(--tr-container-width)' }) |
| 87 | + } |
| 88 | + } |
| 89 | +}) |
| 90 | +
|
| 91 | +const promptItems: PromptProps[] = [ |
| 92 | + { |
| 93 | + label: 'MCP工具', |
| 94 | + description: '帮我查询当前的页面列表', |
| 95 | + icon: h(McpIconComponent), |
| 96 | + badge: 'NEW' |
| 97 | + }, |
| 98 | + { |
| 99 | + label: '页面搭建场景', |
| 100 | + description: '给当前页面中添加一个问卷调查表单', |
| 101 | + icon: h(PageIconComponent) |
| 102 | + }, |
| 103 | + { |
| 104 | + label: '学习/知识型场景', |
| 105 | + description: 'Vue3 和 React 有什么区别?', |
| 106 | + icon: h(StudyIconComponent) |
| 107 | + } |
| 108 | +] |
| 109 | +
|
| 110 | +const showPreview = ref(false) |
| 111 | +const currentSchema = ref(null) |
| 112 | +const showTeleport = ref(false) |
| 113 | +const showSettingPopover = ref(false) |
| 114 | +
|
| 115 | +const { robotSettingState, AI_MODES, AIModelOptions } = useRobot() |
| 116 | +
|
| 117 | +const isVisualModel = () => { |
| 118 | + const platform = AIModelOptions.find((option) => option.value === robotSettingState.selectedModel.baseUrl) |
| 119 | + const modelAbility = platform.model.find((item) => item.value === robotSettingState.selectedModel.model) |
| 120 | + return modelAbility?.ability?.includes('visual') || false |
| 121 | +} |
| 122 | +
|
| 123 | +const aiType = ref(AI_MODES.Agent) |
| 124 | +
|
| 125 | +const typeChange = (type: string) => { |
| 126 | + aiType.value = type |
| 127 | + robotChatRef.value?.createConversation() |
| 128 | + updateLLMConfig({ |
| 129 | + apiUrl: type === AI_MODES.Agent ? '/app-center/api/ai/chat' : '/app-center/api/chat/completions' |
| 130 | + }) |
| 131 | +} |
| 132 | +
|
| 133 | +const changeApiKey = () => { |
| 134 | + localStorage.removeItem('aiChat') |
| 135 | +} |
| 136 | +
|
| 137 | +const changeModel = (model) => { |
| 138 | + robotSettingState.selectedModel = { |
| 139 | + label: model.label || model.model, |
| 140 | + activeName: model.activeName, |
| 141 | + baseUrl: model.baseUrl, |
| 142 | + model: model.model, |
| 143 | + completeModel: model.completeModel, |
| 144 | + apiKey: model.apiKey |
| 145 | + } |
| 146 | + // singleAttachmentItems.value = [] |
| 147 | + // imageUrl.value = '' |
| 148 | + // endContent() |
| 149 | +
|
| 150 | + if ( |
| 151 | + robotSettingState.selectedModel.apiKey !== model.apiKey && |
| 152 | + robotSettingState.selectedModel.baseUrl === model.baseUrl && |
| 153 | + robotSettingState.selectedModel.model === model.model |
| 154 | + ) { |
| 155 | + robotSettingState.selectedModel.apiKey = model.apiKey |
| 156 | + changeApiKey() |
| 157 | + } |
| 158 | +} |
| 159 | +
|
| 160 | +const closePanel = () => { |
| 161 | + showSettingPopover.value = false |
| 162 | +} |
| 163 | +
|
| 164 | +const openAIRobot = () => { |
| 165 | + robotChatRef.value?.openAIRobot() |
| 166 | +} |
| 167 | +
|
| 168 | +const handleFileSelected = (formData: unknown, updateAttachment: (resourceUrl: string) => void) => { |
| 169 | + try { |
| 170 | + getMetaApi(META_SERVICE.Http) |
| 171 | + .post('/material-center/api/resource/upload', formData, { |
| 172 | + headers: { |
| 173 | + 'Content-Type': 'multipart/form-data' |
| 174 | + } |
| 175 | + }) |
| 176 | + .then((res: any) => { |
| 177 | + updateAttachment(res?.resourceUrl) |
| 178 | + }) |
| 179 | + } catch (error) { |
| 180 | + // eslint-disable-next-line no-console |
| 181 | + console.error('上传失败', error) |
| 182 | + updateAttachment('') |
| 183 | + } |
| 184 | +} |
| 185 | +
|
| 186 | +onMounted(async () => { |
| 187 | + setTimeout(() => { |
| 188 | + showTeleport.value = true |
| 189 | + }, 1000) |
| 190 | +}) |
| 191 | +</script> |
| 192 | + |
| 193 | +<style scoped lang="less"> |
| 194 | +.robot { |
| 195 | + margin-right: 8px; |
| 196 | +} |
| 197 | +.robot-chat-container { |
| 198 | + height: 100%; |
| 199 | +} |
| 200 | +.setting-popover { |
| 201 | + .robot-setting .bottom-buttons .tiny-button { |
| 202 | + margin-left: 10px; |
| 203 | + } |
| 204 | +} |
| 205 | +
|
| 206 | +.operations-setting { |
| 207 | + font-size: 28px; |
| 208 | + padding: 4px; |
| 209 | +} |
| 210 | +
|
| 211 | +.robot-chat-container-fullscreen { |
| 212 | + :deep(.tiny-container) { |
| 213 | + container-type: inline-size; |
| 214 | +
|
| 215 | + &.tr-container.tr-container { |
| 216 | + top: var(--base-top-panel-height); |
| 217 | + position: fixed; |
| 218 | + height: auto; |
| 219 | + } |
| 220 | + } |
| 221 | + .operations-setting { |
| 222 | + font-size: 20px; |
| 223 | + } |
| 224 | + @media (min-width: 1280px) { |
| 225 | + :deep(.robot-chat-container-content) { |
| 226 | + width: 1280px; |
| 227 | + margin: 0 auto; |
| 228 | + } |
| 229 | + :deep(.footer-sender) { |
| 230 | + width: 1280px; |
| 231 | + margin: 0 auto; |
| 232 | + padding: 20px 15px; |
| 233 | + } |
| 234 | + } |
| 235 | +} |
| 236 | +</style> |
0 commit comments