feat: 小象助手
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# VITE_APP_URL = http://192.168.31.82:8771
|
||||
VITE_APP_URL = http://18.167.251.121:10015
|
||||
# VITE_APP_URL = http://192.168.31.118:8080
|
||||
VITE_APP_URL = http://192.168.31.82:8755
|
||||
# VITE_APP_URL = http://192.168.31.82:8755
|
||||
VITE_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com
|
||||
|
||||
BIN
src/assets/images/assistant-trigger.png
Normal file
BIN
src/assets/images/assistant-trigger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 978 B |
@@ -1,6 +1,27 @@
|
||||
<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="trigger flex flex-center"
|
||||
:class="{ 'has-unread': hasUnread }"
|
||||
ref="triggerRef"
|
||||
@mousedown="handleTriggerMouseDown"
|
||||
@click="handleTriggerClick"
|
||||
>
|
||||
<img
|
||||
src="@/assets/images/assistant-trigger.png"
|
||||
class="trigger-icon"
|
||||
alt=""
|
||||
@dragstart.prevent
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="showAssistant"
|
||||
class="assistant-container flex flex-col space-between"
|
||||
ref="containerRef"
|
||||
>
|
||||
<div
|
||||
class="assistant-header flex space-between align-center"
|
||||
@mousedown="handleContainerMouseDown"
|
||||
>
|
||||
<div class="assistant-header-title">AI Assistant</div>
|
||||
<SvgIcon name="canvas-assistant-menu" class="menu-icon" color="#D58C4D" />
|
||||
</div>
|
||||
@@ -11,74 +32,106 @@
|
||||
<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 { ref, onMounted, watch } from 'vue'
|
||||
import List from './component/List.vue'
|
||||
import { useDraggable } from './component/useDraggable'
|
||||
import MyEvent from '@/utils/myEvent'
|
||||
|
||||
interface Message {
|
||||
nodeType: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const inputMessage = ref('')
|
||||
const containerRef = ref<HTMLElement>()
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
|
||||
// 使用拖拽 hook
|
||||
const { handleMouseDown } = useDraggable(containerRef)
|
||||
const { handleMouseDown: handleContainerMouseDown } = useDraggable(containerRef)
|
||||
const {
|
||||
handleMouseDown: handleTriggerMouseDown,
|
||||
consumeClickSuppression: consumeTriggerClickSuppression
|
||||
} = useDraggable(triggerRef, {
|
||||
boundary: 'parent'
|
||||
})
|
||||
|
||||
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'
|
||||
const showAssistant = ref(false)
|
||||
|
||||
const handleTriggerClick = () => {
|
||||
if (consumeTriggerClickSuppression()) {
|
||||
return
|
||||
}
|
||||
])
|
||||
showAssistant.value = !showAssistant.value
|
||||
}
|
||||
|
||||
const messageList = ref<Message[]>([])
|
||||
const hasUnread = ref(false)
|
||||
watch(
|
||||
() => showAssistant.value,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
hasUnread.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const listenAssistantPushChat = (message: Message) => {
|
||||
// console.log('有新消息--');
|
||||
|
||||
const exist = messageList.value.find((item: Message) => item.nodeType === message.nodeType)
|
||||
if (!exist) {
|
||||
messageList.value.push(message)
|
||||
hasUnread.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
MyEvent.add('assistantPushChat', listenAssistantPushChat)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.trigger {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
bottom: 50%;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
box-shadow: 0px 13.86px 19.43px 0px #0000000d;
|
||||
border: 0.18rem solid #ebebeb;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
&.has-unread {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.4rem;
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
background-color: #ff4747;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
width: 2.7rem;
|
||||
height: 2.7rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.assistant-container {
|
||||
position: absolute;
|
||||
right: 6.2rem;
|
||||
@@ -95,6 +148,7 @@
|
||||
rgba(255, 207, 144, 0.3) 101.01%
|
||||
);
|
||||
border-radius: 1.69rem;
|
||||
|
||||
.assistant-header {
|
||||
height: 5.66rem;
|
||||
font-family: 'Regular';
|
||||
@@ -109,17 +163,21 @@
|
||||
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;
|
||||
@@ -131,11 +189,13 @@
|
||||
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;
|
||||
@@ -143,6 +203,7 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sender-btn {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
@@ -154,6 +215,7 @@
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
}
|
||||
|
||||
.sender-pause {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img src="@/assets/images/assistant-head.png" class="assistant-head" />
|
||||
</div>
|
||||
<div class="list-item-content-text">
|
||||
{{$t('Assistant.greeting')}}
|
||||
{{ $t('Assistant.greeting') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -21,7 +21,13 @@
|
||||
class="assistant-head"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-item-content-text">{{ item.content }}</div>
|
||||
<div class="list-item-content-text">
|
||||
<VueMarkdown
|
||||
:custom-attrs="customAttrs"
|
||||
:markdown="item.content"
|
||||
:rehype-plugins="[rehypeRaw]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +1,55 @@
|
||||
import { ref, onUnmounted, type Ref } from 'vue'
|
||||
|
||||
interface UseDraggableOptions {
|
||||
boundary?: 'none' | 'parent'
|
||||
threshold?: number
|
||||
onDragStart?: () => void
|
||||
onDragEnd?: () => void
|
||||
onDragEnd?: (moved: boolean) => void
|
||||
}
|
||||
|
||||
export function useDraggable(targetRef: Ref<HTMLElement | undefined>, options?: UseDraggableOptions) {
|
||||
export function useDraggable(
|
||||
targetRef: Ref<HTMLElement | undefined>,
|
||||
options: UseDraggableOptions = {}
|
||||
) {
|
||||
const isDragging = ref(false)
|
||||
const hasMoved = ref(false)
|
||||
const suppressClick = 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
|
||||
const boundary = options.boundary ?? 'none'
|
||||
const threshold = options.threshold ?? 3
|
||||
|
||||
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
|
||||
const getParent = (el: HTMLElement) => {
|
||||
return (el.offsetParent as HTMLElement | null) ?? document.body
|
||||
}
|
||||
|
||||
options?.onDragStart?.()
|
||||
const cleanup = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (e.button !== 0 || !targetRef.value) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const target = targetRef.value
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const parentRect = getParent(target).getBoundingClientRect()
|
||||
|
||||
isDragging.value = true
|
||||
hasMoved.value = false
|
||||
suppressClick.value = false
|
||||
startX.value = e.clientX
|
||||
startY.value = e.clientY
|
||||
startLeft.value = targetRect.left - parentRect.left
|
||||
startTop.value = targetRect.top - parentRect.top
|
||||
|
||||
options.onDragStart?.()
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
@@ -35,27 +58,51 @@ export function useDraggable(targetRef: Ref<HTMLElement | undefined>, options?:
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value || !targetRef.value) return
|
||||
|
||||
const target = targetRef.value
|
||||
const parent = getParent(target)
|
||||
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`
|
||||
if (Math.abs(deltaX) > threshold || Math.abs(deltaY) > threshold) {
|
||||
hasMoved.value = true
|
||||
}
|
||||
|
||||
let nextLeft = startLeft.value + deltaX
|
||||
let nextTop = startTop.value + deltaY
|
||||
|
||||
if (boundary === 'parent') {
|
||||
const maxLeft = Math.max(parent.clientWidth - target.offsetWidth, 0)
|
||||
const maxTop = Math.max(parent.clientHeight - target.offsetHeight, 0)
|
||||
nextLeft = Math.min(Math.max(nextLeft, 0), maxLeft)
|
||||
nextTop = Math.min(Math.max(nextTop, 0), maxTop)
|
||||
}
|
||||
|
||||
target.style.left = `${nextLeft}px`
|
||||
target.style.top = `${nextTop}px`
|
||||
target.style.right = 'auto'
|
||||
target.style.bottom = 'auto'
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
options?.onDragEnd?.()
|
||||
suppressClick.value = hasMoved.value
|
||||
cleanup()
|
||||
options.onDragEnd?.(hasMoved.value)
|
||||
}
|
||||
|
||||
const consumeClickSuppression = () => {
|
||||
const value = suppressClick.value
|
||||
suppressClick.value = false
|
||||
return value
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
handleMouseDown
|
||||
hasMoved,
|
||||
handleMouseDown,
|
||||
consumeClickSuppression
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user