Skip to content
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

Ospp 2024/feat graphic dialogue #791

Closed
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
3 changes: 2 additions & 1 deletion packages/design-core/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { importmapPlugin } from './scripts/externalDeps'
import visualizer from 'rollup-plugin-visualizer'
import { getBaseUrlFromCli, copyBundleDeps, copyPreviewImportMap, copyLocalImportMap } from './scripts/localCdnFile'

const origin = 'http://localhost:9090/'
const origin = 'http://localhost:7011/'

const config = {
base: './',
Expand All @@ -23,6 +23,7 @@ const config = {
extensions: ['.js', '.jsx', '.vue'],
alias: {}
},

server: {
// 这里保证本地启动服务是localhost,支持js多线程和谷歌浏览器读写本地文件api
port: 8080,
Expand Down
210 changes: 194 additions & 16 deletions packages/plugins/robot/src/Main.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<tiny-dropdown trigger="click" :show-icon="false">
<span>
<span>{{ selectedModel.label }}</span>
<icon-chevron-down class="ml8"></icon-chevron-down>
<icon-chevron-down class="ml8 arrow-down"></icon-chevron-down>
</span>
<template #dropdown>
<tiny-dropdown-menu popper-class="chat-model-popover" placement="bottom" :visible-arrow="false">
Expand All @@ -37,15 +37,16 @@
:key="index"
:flex="true"
:order="item.role === 'user' ? 'des' : 'asc'"
:justify="item.role === 'user' ? 'end' : 'start'"
:justify="item.role === 'assistant' ? 'start' : 'end'"
class="chat-message-row"
>
<tiny-col :span="1" :no="1" class="chat-avatar-wrap">
<tiny-col v-if="item.role !== 'system'" :span="1" :no="1" class="chat-avatar-wrap">
<img v-if="item.role !== 'user'" class="chat-avatar chat-avatar-ai" src="../assets/AI.png" />
<img v-else class="chat-avatar" :src="avatarUrl" />
</tiny-col>
<tiny-col :span="22" :no="2">
<div
v-if="item.role !== 'system'"
:class="[
'chat-content',
chatWindowOpened ? '' : 'hidden-text',
Expand All @@ -56,7 +57,10 @@
: 'chat-content-ai'
]"
>
<dialog-content :markdownContent="item.content" />
<dialog-content :markdown-content="item.content" />
</div>
<div v-else class="chat-message-image">
<img class="image" :src="item.content" alt="" />
</div>
</tiny-col>
</tiny-row>
Expand All @@ -72,6 +76,7 @@
<svg-icon name="chat-message" class="common-svg"></svg-icon>
</template>
<template #suffix>
<icon-picture class="common-svg upload-image" @click="openFilePicker"></icon-picture>
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure Accessibility for Image Upload Icon

The image upload icon (icon-picture) is clickable but lacks accessibility features such as aria-label or role attributes. This could hinder accessibility for users relying on screen readers.

Consider adding aria-label to improve accessibility:

- <icon-picture class="common-svg upload-image" @click="openFilePicker"></icon-picture>
+ <icon-picture class="common-svg upload-image" aria-label="Upload Image" @click="openFilePicker"></icon-picture>
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
<icon-picture class="common-svg upload-image" @click="openFilePicker"></icon-picture>
<icon-picture class="common-svg upload-image" aria-label="Upload Image" @click="openFilePicker"></icon-picture>

<svg-icon
name="chat-microphone"
:class="['common-svg', 'microphone', { 'microphone-svg': speechStatus }]"
Expand All @@ -82,6 +87,11 @@
<tiny-button @click="endContent">重新发起会话</tiny-button>
<tiny-button @click="sendContent(inputContent, false)">发送</tiny-button>
</footer>
<input type="file" ref="fileInput" style="display: none" @change="handleFileChange" />
<div class="preview-image" v-if="imageUrl !== ''" :data-animated="imageDeleting ? 'out' : ''">
<img class="image" :src="imageUrl" alt="" />
<icon-error class="delete-image" @click="handleDelete"></icon-error>
</div>
</div>
<token-dialog
:dialog-visible="tokenDialogVisible"
Expand All @@ -92,7 +102,7 @@
</template>

<script>
import { ref, onMounted, watch, watchEffect } from 'vue'
import { ref, onMounted, watch, unref, watchEffect } from 'vue'
import {
Layout,
Row,
Expand All @@ -106,7 +116,7 @@ import {
DropdownItem as TinyDropdownItem
} from '@opentiny/vue'
import { useCanvas, useHistory, usePage, useModal } from '@opentiny/tiny-engine-controller'
import { iconChevronDown, iconSetting } from '@opentiny/vue-icon'
import { iconChevronDown, iconSetting, iconPicture, iconError } from '@opentiny/vue-icon'
import { extend } from '@opentiny/vue-renderless/common/object'
import { useHttp } from '@opentiny/tiny-engine-http'
import { getBlockContent, initBlockList, AIModelOptions } from './js/robotSetting'
Expand All @@ -124,8 +134,10 @@ export default {
TinyDropdown,
TinyDropdownMenu,
TinyDropdownItem,
IconPicture: iconPicture(),
IconSetting: iconSetting(),
IconChevronDown: iconChevronDown(),
IconError: iconError(),
DialogContent,
TokenDialog
},
Expand Down Expand Up @@ -169,6 +181,8 @@ export default {
)
}

// TODO:返回schema格式的代码
// eslint-disable-next-line no-unused-vars
const createNewPage = (schema) => {
if (!(pageSettingState.isNew && pageSettingState.isAIPage)) {
pageSettingState.isNew = true
Expand Down Expand Up @@ -224,7 +238,7 @@ export default {
http
.post('/app-center/api/ai/chat', getSendSeesionProcess(), { timeout: 600000 })
.then((res) => {
const { originalResponse, schema } = res
const { originalResponse } = res
const responseMessage = getAiRespMessage(
originalResponse.choices?.[0]?.message.role,
originalResponse.choices?.[0]?.message.content
Expand All @@ -237,9 +251,10 @@ export default {
sessionProcess.displayMessages.push(respDisplayMessage)
messages.value[messages.value.length - 1].content = originalResponse.choices?.[0]?.message.content
setContextSession()
if (schema?.schema) {
createNewPage(schema.schema)
}
// TODO:返回schema格式的代码
// if (schema?.schema) {
// createNewPage(schema.schema)
// }
inProcesing.value = false
connectedFailed.value = false
})
Expand Down Expand Up @@ -283,8 +298,85 @@ export default {
tokenDialogVisible.value = true
}

const getMessage = (content) => ({
role: 'user',
/*
文件上传(仅支持图片,后续根据需求可添加上传类型)
*/
const fileInput = ref(null)
const openFilePicker = () => {
if (unref(fileInput)) {
unref(fileInput)?.click()
}
}

const imageUrl = ref('')
const imageContent = ref()
const uploadFile = (file) => {
const formData = new FormData()
const foundationModelData = JSON.stringify({
foundationModel: {
manufacturer: currentModel.manufacturer,
model: currentModel.value,
token: localStorage.getItem(currentModel.modelKey)
}
})
formData.append('foundationModel', foundationModelData)
formData.append('file', file)
http
.post('/app-center/api/ai/files', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 600000
})
.then((res) => {
imageContent.value = res.originalResponse
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
imageUrl.value = reader.result
}
})
.catch(() => {
Notify({
type: 'error',
message: '上传图片失败',
position: 'top-right',
duration: 5000
})
})
}

const handleFileChange = (event) => {
const files = event.target.files
if (!files.length) {
return
}
const file = files[0]
const validImageTypes = ['image/jpeg', 'image/png', 'image/jpg']
if (!validImageTypes.includes(file.type)) {
alert('请上传有效的图片文件(.jpeg, .png, .jpg)!')
event.target.value = ''
return
}
event.target.value = ''
uploadFile(file)
}

const imageDeleting = ref(false)
const handleDelete = () => {
imageDeleting.value = true
setTimeout(() => {
imageUrl.value = ''
imageContent.value = ''
imageDeleting.value = false
if (unref(fileInput)) {
unref(fileInput).value = ''
}
}, 500)
Comment on lines +304 to +375
Copy link
Contributor

Choose a reason for hiding this comment

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

Review Image Upload and Deletion Logic

The methods openFilePicker, handleFileChange, uploadFile, and handleDelete handle the core functionality of image uploading and deletion. Here are some observations and suggestions:

  1. Error Handling in uploadFile: The method catches errors but only displays a notification. It might be beneficial to also reset relevant states or perform additional cleanup.
  2. File Type Validation: The validation for file types is done in handleFileChange. Consider extracting this to a separate method for better modularity and reusability.
  3. Animation Handling in handleDelete: The deletion uses a timeout to manage animations. This could be replaced with more robust animation handling techniques using Vue's transition features.

Refactor the file type validation into a separate method and improve error handling in uploadFile:

+ const isValidImageType = (fileType) => ['image/jpeg', 'image/png', 'image/jpg'].includes(fileType);

- if (!validImageTypes.includes(file.type)) {
+ if (!isValidImageType(file.type)) {
    alert('请上传有效的图片文件(.jpeg, .png, .jpg)!');
    event.target.value = '';
    return;
  }

Consider using Vue's transition for handling animations in handleDelete instead of setTimeout.

Committable suggestion was skipped due to low confidence.

}

const getMessage = (content, role) => ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Enhance getMessage Function with Default Parameters

The getMessage function now requires a role parameter. To ensure backward compatibility and ease of use, consider providing a default value for the role.

Add a default value for the role parameter in getMessage:

- const getMessage = (content, role) => ({
+ const getMessage = (content, role = 'user') => ({
    role,
    content,
    name: 'John'
  })
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 getMessage = (content, role) => ({
const getMessage = (content, role = 'user') => ({

role,
content,
name: 'John'
})
Expand All @@ -308,19 +400,24 @@ export default {
})
return
}
const realContent = content.trim()
const realContent = String(content).trim()
if (realContent) {
if (chatWindowOpened.value === false) {
await resizeChatWindow()
}
const message = getMessage(realContent)
const message = getMessage(realContent, 'user')
inProcesing.value = true

messages.value.push(message)
sessionProcess?.messages.push(message)
sessionProcess?.displayMessages.push(message)
if (imageContent.value) {
messages.value.push(getMessage(imageUrl.value, 'system'))
sessionProcess?.messages.push(getMessage(JSON.stringify(imageContent.value), 'system'))
sessionProcess?.displayMessages.push(getMessage(imageUrl.value, 'system'))
}
if (!isModel) {
inputContent.value = ''
imageUrl.value = ''
}
await scrollContent()
await sleep(1000)
Expand All @@ -330,7 +427,7 @@ export default {
}
}

// 根据localstorage初始化AI大模型
// 根据localstorage初始化AI大模型s
const initCurrentModel = (aiSession) => {
const currentModelValue = JSON.parse(aiSession)?.foundationModel?.model
currentModel = AIModelOptions.find((item) => item.value === currentModelValue)
Expand Down Expand Up @@ -389,6 +486,7 @@ export default {
message: '切换AI大模型将导致当前会话被清空,重新开启新会话,是否继续?',
exec() {
selectedModel.value = model
currentModel = model
endContent()
}
})
Expand Down Expand Up @@ -433,6 +531,12 @@ export default {
endContent,
resizeChatWindow,
setToken,
openFilePicker,
handleFileChange,
imageDeleting,
handleDelete,
fileInput,
imageUrl,
AIModelOptions,
selectedModel,
currentModel,
Expand All @@ -449,6 +553,70 @@ export default {
.common-svg {
color: var(--ti-lowcode-chat-model-common-icon);
}
.chat-message-image {
margin-right: 45px;
margin-top: -10px;
max-width: 100%;
border-radius: 5px;
border: 1px solid var(--ti-lowcode-chat-model-user-text-border);
.image {
width: 100px;
height: 70px;
border-radius: 5px;
}
}

.preview-image {
position: fixed;
bottom: 5px;
transform: translate(-50%, -50%);
z-index: 1000;
border-radius: 5px;
max-width: 100%;
height: auto;
animation: slideDown 0.5s ease-out forwards;
border: 1px solid var(--ti-lowcode-chat-model-user-text-border);
.image {
border-radius: 5px;
width: 100px;
height: 60px;
display: block;
position: relative;
border: #1a1a1a;
}
.delete-image {
color: red;
position: absolute;
top: -4px;
right: -3px;
cursor: pointer;
z-index: 1001;
}
}
.preview-image[data-animated='out'] {
animation: slideUp 0.5s ease-out forwards;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes slideUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-100%);
}
}

.chat-title-icons {
font-size: 16px;
Expand All @@ -467,6 +635,13 @@ export default {
font-size: 14px;
margin-bottom: 20px;
color: var(--ti-lowcode-chat-model-title);

.arrow-down {
margin-left: 5px;
}
}
.tiny-dropdown .tiny-dropdown__trigger:not(.tiny-button) .tiny-svg {
vertical-align: middle;
}
.chat-window {
max-height: 400px;
Expand Down Expand Up @@ -548,6 +723,9 @@ export default {
font-size: 16px;
color: var(--ti-lowcode-chat-model-input-icon);
}
.upload-image {
margin-right: 7px;
}
.microphone {
font-size: 18px;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/robot/src/js/robotSetting.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const { getBlockList } = useBlock()
export const AIModelOptions = [
// 暂时不能使用,预留模型信息
// { label: 'ChatGPT:gpt-3.5-turbo', value: 'gpt-3.5-turbo', manufacturer: 'openai', modelKey: '' },
{ label: '文心一言:ERNIE-Bot-turbo', value: 'ERNIE-Bot-turbo', manufacturer: 'baiduai', modelKey: 'ACCESS_TOKEN' },
// { label: '文心一言:ERNIE-Bot-turbo', value: 'ERNIE-Bot-turbo', manufacturer: 'baiduai', modelKey: 'ACCESS_TOKEN' },
{ label: 'Kimi:moonshot-v1-8k', value: 'moonshot-v1-8k', manufacturer: 'kimi', modelKey: 'MOONSHOT_API_KEY' }
]

Expand Down