Skip to content

Commit

Permalink
feat(webui): support chat-panel component
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 2, 2021
1 parent bc18170 commit 0f95246
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 99 deletions.
40 changes: 9 additions & 31 deletions packages/plugin-chat/client/chat.vue
Original file line number Diff line number Diff line change
@@ -1,48 +1,26 @@
<template>
<k-card class="sandbox">
<div class="history" ref="panel">
<p v-for="({ username, content }, index) in messages" :key="index">
{{ username }}: {{ content }}
<k-chat-panel class="sandbox" :messages="messages">
<template #default="{ username, content }">
<p>
{{ username }}: <k-message :content="content"/>
</p>
</div>
<k-input v-model="text" @enter="onEnter"></k-input>
</k-card>
</template>
</k-chat-panel>
</template>

<script lang="ts" setup>
import { receive, storage, send } from '~/client'
import { ref, nextTick } from 'vue'
import { receive, storage } from '~/client'
interface Message {
username: string
content: string
}
const text = ref('')
const panel = ref<Element>(null)
const messages = storage.create<Message[]>('chat', [])
function addMessage(body: Message) {
receive('chat', (body) => {
messages.value.push(body)
const { scrollTop, clientHeight, scrollHeight } = panel.value
if (Math.abs(scrollTop + clientHeight - scrollHeight) < 1) {
nextTick(scrollToBottom)
}
}
function scrollToBottom() {
panel.value.scrollTop = panel.value.scrollHeight - panel.value.clientHeight
}
receive('message', addMessage)
function onEnter() {
if (!text.value) return
// addMessage('user', text.value)
// const { token, id } = user.value
// send('chat', { token, id, content: text.value })
// text.value = ''
}
})
</script>
81 changes: 81 additions & 0 deletions packages/plugin-webui/client/components/chat-panel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<k-card class="k-chat-panel">
<div class="k-chat-panel-body" ref="body">
<template v-for="(message, index) in messages" :key="index">
<slot v-bind="message"/>
</template>
</div>
<k-input v-model="text" @enter="onEnter" @paste="onPaste"></k-input>
</k-card>
</template>

<script lang="ts" setup>
import { ref, watch, defineProps, onMounted, nextTick, defineEmit } from 'vue'
import { segment } from '~/client'
const emit = defineEmit(['enter'])
const props = defineProps<{ messages: any[], pinned?: boolean }>()
const text = ref('')
const body = ref<Element>(null)
onMounted(scrollToBottom)
function scrollToBottom() {
body.value.scrollTop = body.value.scrollHeight - body.value.clientHeight
}
watch(props.messages, () => {
const { scrollTop, clientHeight, scrollHeight } = body.value
if (!props.pinned || Math.abs(scrollTop + clientHeight - scrollHeight) < 1) {
nextTick(scrollToBottom)
}
})
function onEnter() {
if (!text.value) return
emit('enter', text.value)
text.value = ''
}
async function onPaste(event: ClipboardEvent) {
const item = event.clipboardData.items[0]
if (item.kind === 'file') {
event.preventDefault()
const file = item.getAsFile()
const reader = new FileReader()
reader.addEventListener('load', function () {
emit('enter', segment.image('base64://' + reader.result.slice(22)))
}, false)
reader.readAsDataURL(file)
}
}
</script>

<style lang="scss">
.k-chat-panel {
height: 100%;
position: relative;
.k-chat-panel-body {
position: absolute;
top: 2rem;
left: 2rem;
right: 2rem;
bottom: 6rem;
overflow-x: visible;
overflow-y: auto;
}
.k-input {
position: absolute;
bottom: 2rem;
left: 2rem;
right: 2rem;
}
}
</style>
6 changes: 3 additions & 3 deletions packages/plugin-webui/client/components/input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,19 @@ const inputStyle = computed(() => ({
const emit = defineEmit(['update:modelValue', 'paste', 'focus', 'blur', 'enter', 'clickPrefix', 'clickSuffix'])
function onInput (event) {
function onInput(event) {
if (props.validate) {
invalid.value = !props.validate(event.target.value)
}
emit('update:modelValue', event.target.value)
}
function onFocus (event) {
function onFocus(event) {
focused.value = true
emit('focus', event)
}
function onBlur (event) {
function onBlur(event) {
focused.value = false
emit('blur', event)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-webui/client/components/message.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="k-message">
<template v-for="({ type, data }, index) in segment.parse(text)" :key="index">
<template v-for="({ type, data }, index) in segment.parse(content)" :key="index">
<span v-if="type === 'text'">{{ data.content }}</span>
<img v-else-if="type === 'image'" :src="data.url || data.file"/>
</template>
Expand All @@ -13,7 +13,7 @@ import { defineProps } from 'vue'
import { segment } from '~/client'
defineProps<{
text: string
content: string
}>()
</script>
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-webui/client/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ declare module '*.vue' {
}

declare module '~/server' {
export * from 'koishi-plugin-webui/src'
export * from 'koishi-plugin-webui'
}

declare module '~/client' {
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin-webui/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Button from './components/button.vue'
import Input from './components/input.vue'
import Message from './components/message.vue'
import Numeric from './components/numeric.vue'
import ChatPanel from './components/chat-panel.vue'
import App from './views/layout/index.vue'
import { start, user, receive, router } from '~/client'

Expand Down Expand Up @@ -62,12 +63,13 @@ router.addRoute({
})

app.component('k-card', Card)
app.component('k-chart', defineAsyncComponent(() => import('./components/echarts')))
app.component('k-button', Button)
app.component('k-collapse', Collapse)
app.component('k-input', Input)
app.component('k-message', Message)
app.component('k-numeric', Numeric)
app.component('k-chart', defineAsyncComponent(() => import('./components/echarts')))
app.component('k-chat-panel', ChatPanel)

app.provide('ecTheme', 'dark-blue')

Expand Down
71 changes: 10 additions & 61 deletions packages/plugin-webui/client/views/sandbox.vue
Original file line number Diff line number Diff line change
@@ -1,69 +1,38 @@
<template>
<k-card class="sandbox">
<div class="history" ref="panel">
<p v-for="({ from, content }, index) in messages" :key="index" :class="from">
<k-message :text="content"/>
<k-chat-panel class="sandbox" :messages="messages" @enter="sendSandbox" :pinned="pinned">
<template #default="{ from, content }">
<p :class="from">
<k-message :content="content"/>
</p>
</div>
<k-input v-model="text" @enter="onEnter" @paste="onPaste"></k-input>
</k-card>
</template>
</k-chat-panel>
</template>

<script lang="ts" setup>
import { ref, watch, nextTick, onMounted } from 'vue'
import { send, receive, user, storage, segment } from '~/client'
import { ref, watch } from 'vue'
import { send, receive, user, storage } from '~/client'
interface Message {
from: 'user' | 'bot'
content: string
}
const text = ref('')
const panel = ref<Element>(null)
const pinned = ref(true)
const messages = storage.create<Message[]>('sandbox', [])
watch(user, () => messages.value = [])
function addMessage(from: 'user' | 'bot', content: string) {
pinned.value = from === 'bot'
messages.value.push({ from, content })
const { scrollTop, clientHeight, scrollHeight } = panel.value
if (from === 'user' || Math.abs(scrollTop + clientHeight - scrollHeight) < 1) {
nextTick(scrollToBottom)
}
}
onMounted(scrollToBottom)
function scrollToBottom() {
panel.value.scrollTop = panel.value.scrollHeight - panel.value.clientHeight
}
function sendSandbox(content: string) {
const { token, id } = user.value
send('sandbox', { token, id, content })
}
function onEnter() {
if (!text.value) return
sendSandbox(text.value)
text.value = ''
}
async function onPaste(event) {
const item = event.clipboardData.items[0]
if (item.kind === 'file') {
event.preventDefault()
const file = item.getAsFile()
const reader = new FileReader()
reader.addEventListener('load', function () {
const { token, id } = user.value
sendSandbox(segment.image('base64://' + reader.result.slice(22)))
}, false)
reader.readAsDataURL(file)
}
}
receive('sandbox:bot', (data) => {
addMessage('bot', data)
})
Expand All @@ -81,19 +50,6 @@ receive('sandbox:clear', (data) => {
<style lang="scss">
.sandbox {
height: 100%;
position: relative;
.history {
position: absolute;
top: 2rem;
left: 2rem;
right: 2rem;
bottom: 6rem;
overflow-x: visible;
overflow-y: auto;
}
p {
padding-left: 1rem;
white-space: break-spaces;
Expand All @@ -117,13 +73,6 @@ receive('sandbox:clear', (data) => {
position: absolute;
left: -.1rem;
}
.k-input {
position: absolute;
bottom: 2rem;
left: 2rem;
right: 2rem;
}
}
</style>

0 comments on commit 0f95246

Please sign in to comment.