feat: 画布内聊天组件
This commit is contained in:
3
src/assets/icons/canvas-assistant-menu.svg
Normal file
3
src/assets/icons/canvas-assistant-menu.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="10" viewBox="0 0 18 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 1C18 0.447715 17.5523 0 17 0H1C0.447716 0 0 0.447715 0 1C0 1.55228 0.447716 2 1 2H17C17.5523 2 18 1.55228 18 1ZM18 9C18 8.44772 17.5523 8 17 8H7C6.44771 8 6 8.44772 6 9C6 9.55229 6.44771 10 7 10H17C17.5523 10 18 9.55229 18 9Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 365 B |
165
src/components/Assistant/assistant.vue
Normal file
165
src/components/Assistant/assistant.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="assistant-container flex flex-col space-between" ref="containerRef">
|
||||||
|
<div class="assistant-header flex space-between align-center" @mousedown="handleMouseDown">
|
||||||
|
<div class="assistant-header-title">AI Assistant</div>
|
||||||
|
<SvgIcon name="canvas-assistant-menu" class="menu-icon" color="#D58C4D" />
|
||||||
|
</div>
|
||||||
|
<div class="assistant-body flex-1">
|
||||||
|
<List :messageList="messageList" />
|
||||||
|
</div>
|
||||||
|
<div class="assistant-input flex align-center">
|
||||||
|
<el-input v-model="inputMessage" :placeholder="$t('assistant.inputPlaceholder')" />
|
||||||
|
<div class="sender-btn flex flex-center" @click="handleSendAgent">
|
||||||
|
<img src="@/assets/images/sender.png" class="sender-icon" />
|
||||||
|
<!-- <div class="sender-pause" /> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import List from './component/List.vue'
|
||||||
|
import { useDraggable } from './component/useDraggable'
|
||||||
|
|
||||||
|
const inputMessage = ref('')
|
||||||
|
const containerRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
// 使用拖拽 hook
|
||||||
|
const { handleMouseDown } = useDraggable(containerRef)
|
||||||
|
|
||||||
|
const messageList = ref([
|
||||||
|
{
|
||||||
|
content: 'Hi! How can I help you, today?',
|
||||||
|
id: 1,
|
||||||
|
role: 'assistant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Please recommend some .',
|
||||||
|
id: 2,
|
||||||
|
role: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'I noticed that you inputed a new sketch.',
|
||||||
|
id: 3,
|
||||||
|
role: 'system',
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Hi! How can I help you, today?',
|
||||||
|
id: 4,
|
||||||
|
role: 'assistant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Please recommend some .',
|
||||||
|
id: 5,
|
||||||
|
role: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'I noticed that you inputed a new sketch.',
|
||||||
|
id: 36,
|
||||||
|
role: 'system',
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Hi! How can I help you, today?',
|
||||||
|
id: 31,
|
||||||
|
role: 'assistant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Please recommend some .',
|
||||||
|
id: 22,
|
||||||
|
role: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'I noticed that you inputed a new sketch.',
|
||||||
|
id: 63,
|
||||||
|
role: 'system',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.assistant-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 6.2rem;
|
||||||
|
bottom: 13.2rem;
|
||||||
|
width: 46.69rem;
|
||||||
|
height: 56.6rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
// background-color: #fff;
|
||||||
|
box-shadow: 0px 19.44px 27.22px 0px #0000000d;
|
||||||
|
border: 1px solid;
|
||||||
|
border-image-source: linear-gradient(
|
||||||
|
119.03deg,
|
||||||
|
rgba(233, 121, 60, 0.3) 1.61%,
|
||||||
|
rgba(255, 207, 144, 0.3) 101.01%
|
||||||
|
);
|
||||||
|
border-radius: 1.69rem;
|
||||||
|
.assistant-header {
|
||||||
|
height: 5.66rem;
|
||||||
|
font-family: 'Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
padding: 0 1.59rem 0 2.05rem;
|
||||||
|
background: linear-gradient(
|
||||||
|
119.03deg,
|
||||||
|
rgba(233, 121, 60, 0.3) 1.61%,
|
||||||
|
rgba(255, 207, 144, 0.3) 101.01%
|
||||||
|
);
|
||||||
|
border-top-left-radius: 1.69rem;
|
||||||
|
border-top-right-radius: 1.69rem;
|
||||||
|
user-select: none;
|
||||||
|
cursor: grabbing;
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
.menu-icon {
|
||||||
|
width: 1.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.assistant-body {
|
||||||
|
margin: 1.6rem 0 2.9rem;
|
||||||
|
padding: 0 1.95rem 0 2.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.assistant-input {
|
||||||
|
border: 1px solid #0000001a;
|
||||||
|
margin: 0 2rem 3rem;
|
||||||
|
border-radius: 3.53rem;
|
||||||
|
padding: 0 1rem 0 1.7rem;
|
||||||
|
.el-input {
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sender-btn {
|
||||||
|
width: 2.2rem;
|
||||||
|
height: 2.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
.sender-icon {
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 0.9rem;
|
||||||
|
}
|
||||||
|
.sender-pause {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
43
src/components/Assistant/component/List.vue
Normal file
43
src/components/Assistant/component/List.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list-wrapper flex flex-col">
|
||||||
|
<div
|
||||||
|
class="list-item flex align-center"
|
||||||
|
v-for="item in props.messageList"
|
||||||
|
:key="item.id"
|
||||||
|
:class="item.role"
|
||||||
|
>
|
||||||
|
<div class="list-item-content-text">{{ item.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
messageList: Array<any>
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.list-wrapper {
|
||||||
|
row-gap: 1.6rem;
|
||||||
|
.list-item {
|
||||||
|
min-height: 3.86rem;
|
||||||
|
width: fit-content;
|
||||||
|
&.user {
|
||||||
|
width: 26.2rem;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-family: 'Regular';
|
||||||
|
color: #333;
|
||||||
|
padding: 1.1rem 1.7rem;
|
||||||
|
background: linear-gradient(#fffcf4, #fffcf4) padding-box,
|
||||||
|
linear-gradient(
|
||||||
|
119.03deg,
|
||||||
|
rgba(233, 121, 60, 0.3) 1.61%,
|
||||||
|
rgba(255, 207, 144, 0.3) 101.01%
|
||||||
|
)
|
||||||
|
border-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3.18rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/components/Assistant/component/useDraggable.ts
Normal file
61
src/components/Assistant/component/useDraggable.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ref, onUnmounted, type Ref } from 'vue'
|
||||||
|
|
||||||
|
interface UseDraggableOptions {
|
||||||
|
onDragStart?: () => void
|
||||||
|
onDragEnd?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDraggable(targetRef: Ref<HTMLElement | undefined>, options?: UseDraggableOptions) {
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const startX = ref(0)
|
||||||
|
const startY = ref(0)
|
||||||
|
const startLeft = ref(0)
|
||||||
|
const startTop = ref(0)
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
// 只允许左键拖拽
|
||||||
|
if (e.button !== 0) return
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
startX.value = e.clientX
|
||||||
|
startY.value = e.clientY
|
||||||
|
|
||||||
|
if (targetRef.value) {
|
||||||
|
const rect = targetRef.value.getBoundingClientRect()
|
||||||
|
startLeft.value = rect.left
|
||||||
|
startTop.value = rect.top
|
||||||
|
}
|
||||||
|
|
||||||
|
options?.onDragStart?.()
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging.value || !targetRef.value) return
|
||||||
|
|
||||||
|
const deltaX = e.clientX - startX.value
|
||||||
|
const deltaY = e.clientY - startY.value
|
||||||
|
|
||||||
|
targetRef.value.style.left = `${startLeft.value + deltaX}px`
|
||||||
|
targetRef.value.style.top = `${startTop.value + deltaY}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
options?.onDragEnd?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDragging,
|
||||||
|
handleMouseDown
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,12 +25,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<flow-canvas />
|
<flow-canvas />
|
||||||
|
<Assistant />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import flowCanvas from './FlowCanvas/flow-canvas.vue'
|
import flowCanvas from './FlowCanvas/flow-canvas.vue'
|
||||||
import card from './FlowCanvas/components/nodes/cards/index.vue'
|
import card from './FlowCanvas/components/nodes/cards/index.vue'
|
||||||
|
import Assistant from '../Assistant/assistant.vue'
|
||||||
import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue'
|
import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue'
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
x: 100,
|
x: 100,
|
||||||
|
|||||||
@@ -182,5 +182,8 @@ export default {
|
|||||||
deleteCardConfirm: 'Are you sure you want to delete this function card?',
|
deleteCardConfirm: 'Are you sure you want to delete this function card?',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
|
assistant: {
|
||||||
|
inputPlaceholder: 'Ask anything',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default {
|
|||||||
timesPerHour: '{time} 次/小时',
|
timesPerHour: '{time} 次/小时',
|
||||||
userAgreement: '用户协议',
|
userAgreement: '用户协议',
|
||||||
privacyPolicy: '隐私政策',
|
privacyPolicy: '隐私政策',
|
||||||
view: '查看',
|
view: '查看'
|
||||||
},
|
},
|
||||||
Country: {
|
Country: {
|
||||||
unitedStates: '美国',
|
unitedStates: '美国',
|
||||||
@@ -92,14 +92,14 @@ export default {
|
|||||||
france: '法国',
|
france: '法国',
|
||||||
japan: '日本',
|
japan: '日本',
|
||||||
canada: '加拿大',
|
canada: '加拿大',
|
||||||
germany: '德国',
|
germany: '德国'
|
||||||
},
|
},
|
||||||
Role: {
|
Role: {
|
||||||
designer: '设计师',
|
designer: '设计师',
|
||||||
student: '学生',
|
student: '学生',
|
||||||
teacher: '教师',
|
teacher: '教师',
|
||||||
parent: '家长',
|
parent: '家长',
|
||||||
other: '其他',
|
other: '其他'
|
||||||
},
|
},
|
||||||
Input: {
|
Input: {
|
||||||
placeholder: '描述您想要设计的物品……例如:现代木质椅子,包豪斯风格的家具。',
|
placeholder: '描述您想要设计的物品……例如:现代木质椅子,包豪斯风格的家具。',
|
||||||
@@ -176,6 +176,9 @@ export default {
|
|||||||
flowCanvas: {
|
flowCanvas: {
|
||||||
deleteCardConfirm: '确定要删除该功能卡片吗?',
|
deleteCardConfirm: '确定要删除该功能卡片吗?',
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
cancel: '取消',
|
cancel: '取消'
|
||||||
|
},
|
||||||
|
assistant: {
|
||||||
|
inputPlaceholder: '请输入',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user