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

Bot page #514

Merged
merged 6 commits into from
Aug 4, 2024
Merged
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
14 changes: 13 additions & 1 deletion web/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ const routes: RouteRecordRaw[] = [
},
],
},
{
path: '/bot',
name: 'Bot',
component: () => import('@/views/bot/index.vue'),
children: [
{
path: ':uuid?',
name: 'Bot',
component: () => import('@/views/bot/index.vue'),
},
],
},
{
path: '/snapshot_all',
name: 'SnapshotAll',
Expand All @@ -25,7 +37,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/bot_all',
name: 'BotAll',
component: () => import('@/views/snapshot/bot_all.vue'),
component: () => import('@/views/bot/all.vue'),
},
{
path: '/admin',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NModal, useDialog, useMessage } from 'naive-ui'
import Search from './components/Search.vue'
import Search from '../snapshot/components/Search.vue'
import { fetchSnapshotAll, fetchSnapshotDelete } from '@/api'
import { displayLocaleDate, formatYearMonth } from '@/utils/date'
import { HoverButton, SvgIcon } from '@/components/common'
Expand All @@ -17,7 +17,7 @@ interface PostLink {
}

function post_url(uuid: string): string {
return `#/snapshot/${uuid}`
return `#/bot/${uuid}`
}

const postsByYearMonth = ref<Record<string, PostLink[]>>({})
Expand Down
222 changes: 222 additions & 0 deletions web/src/views/bot/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<script lang='ts' setup>
import { computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { useDialog, useMessage, NSpin } from 'naive-ui'
import html2canvas from 'html2canvas'
import Message from '../snapshot/components/Message/index.vue'
import { useCopyCode } from '@/hooks/useCopyCode'
import Header from '../snapshot/components/Header/index.vue'
import { CreateSessionFromSnapshot, fetchChatSnapshot } from '@/api/chat_snapshot'
import { HoverButton, SvgIcon } from '@/components/common'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
import { genTempDownloadLink } from '@/utils/download'
import { getCurrentDate } from '@/utils/date'
import { useAuthStore, useChatStore } from '@/store'
import { useQuery } from '@tanstack/vue-query'

const authStore = useAuthStore()
const chatStore = useChatStore()

const route = useRoute()
const dialog = useDialog()
const nui_msg = useMessage()

useCopyCode()

const { isMobile } = useBasicLayout()
// session uuid
const { uuid } = route.params as { uuid: string }

const { data: snapshot_data, isLoading } = useQuery({
queryKey: ['chatSnapshot', uuid],
queryFn: async () => await fetchChatSnapshot(uuid),
})


function handleExport() {

const dialogBox = dialog.warning({
title: t('chat.exportImage'),
content: t('chat.exportImageConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: async () => {
try {
dialogBox.loading = true
const ele = document.getElementById('image-wrapper')
const canvas = await html2canvas(ele as HTMLDivElement, {
useCORS: true,
})
const imgUrl = canvas.toDataURL('image/png')
const tempLink = genTempDownloadLink(imgUrl)
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(imgUrl)
dialogBox.loading = false
nui_msg.success(t('chat.exportSuccess'))
Promise.resolve()
}
catch (error: any) {
nui_msg.error(t('chat.exportFailed'))
}
finally {
dialogBox.loading = false
}
},
})
}
function format_chat_md(chat: Chat.Message): string {
return `<sup><kbd><var>${chat.dateTime}</var></kbd></sup>:\n ${chat.text}`
}

const chatToMarkdown = () => {
try {
/*
uuid: string,
dateTime: string
text: string
inversion?: boolean
error?: boolean
loading?: boolean
isPrompt?: boolean
*/
const chatData = snapshot_data.value.conversation;
const markdown = chatData.map((chat: Chat.Message) => {
if (chat.isPrompt)
return `**system** ${format_chat_md(chat)}}`
else if (chat.inversion)
return `**user** ${format_chat_md(chat)}`
else
return `**assistant** ${format_chat_md(chat)}`
}).join('\n\n----\n\n')
return markdown
}
catch (error) {
console.error(error)
throw error
}
}

function handleMarkdown() {
const dialogBox = dialog.warning({
title: t('chat.exportMD'),
content: t('chat.exportMDConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: async () => {
try {
dialogBox.loading = true
const markdown = chatToMarkdown()
const ts = getCurrentDate()
const filename = `chat-${ts}.md`
const blob = new Blob([markdown], { type: 'text/plain;charset=utf-8' })
const url: string = URL.createObjectURL(blob)
const link: HTMLAnchorElement = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
dialogBox.loading = false
nui_msg.success(t('chat.exportSuccess'))
Promise.resolve()
}
catch (error: any) {
nui_msg.error(t('chat.exportFailed'))
}
finally {
dialogBox.loading = false
}
},
})
}

async function handleChat() {
if (!authStore.getToken())
nui_msg.error(t('common.ask_user_register'))
window.open(`/`, '_blank')
const { SessionUuid }: { SessionUuid: string } = await CreateSessionFromSnapshot(uuid)
await chatStore.setActiveLocal(SessionUuid)
// open link at static/#/chat/{SessionUuid}
//window.open(`/static/#/chat/${SessionUuid}`, '_blank')
}

const footerClass = computed(() => {
let classes = ['p-4']
if (isMobile.value)
classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden']
return classes
})

function onScrollToTop() {
const scrollRef = document.querySelector('#scrollRef')
if (scrollRef)
nextTick(() => scrollRef.scrollTop = 0)
}
</script>

<template>
<div class="flex flex-col w-full h-full">
<div v-if="isLoading">
<NSpin size="large" />
</div>
<div v-else>
<Header :title="snapshot_data.title" typ="chatbot" />
<main class="flex-1 overflow-hidden">
<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
<div id="image-wrapper" class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
:class="[isMobile ? 'p-2' : 'p-4']">
<Message v-for="(item, index) of snapshot_data.conversation" :key="index" :date-time="item.dateTime"
:model="snapshot_data.model" :text="item.text" :inversion="item.inversion" :error="item.error"
:loading="item.loading" :index="index" />
</div>
</div>
</main>
<div class="floating-button">
<HoverButton testid="create-chat" :tooltip="$t('chat_snapshot.createChat')" @click="handleChat">
<span class="text-xl text-[#4f555e] dark:text-white m-auto mx-10">
<SvgIcon icon="mdi:chat-plus" width="32" height="32" />
</span>
</HoverButton>
</div>
<footer :class="footerClass">
<div class="w-full max-w-screen-xl m-auto">
<div class="flex items-center justify-between space-x-2">
<HoverButton v-if="!isMobile" :tooltip="$t('chat_snapshot.exportImage')" @click="handleExport">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:download-2-line" />
</span>
</HoverButton>
<HoverButton v-if="!isMobile" :tooltip="$t('chat_snapshot.exportMarkdown')" @click="handleMarkdown">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="mdi:language-markdown" />
</span>
</HoverButton>
<HoverButton :tooltip="$t('chat_snapshot.scrollTop')" @click="onScrollToTop">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="material-symbols:vertical-align-top" />
</span>
</HoverButton>
</div>
</div>
</footer>
</div>
</div>
</template>

<style>
/* CSS for the button */
.floating-button {
position: fixed;
bottom: 10vh;
right: 10vmin;
z-index: 99;
padding: 0.5em;
border-radius: 50%;
cursor: pointer;
background-color: #4ff09a;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
</style>
12 changes: 9 additions & 3 deletions web/src/views/snapshot/components/Header/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { useMutation, useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()

defineProps<Props>()
const props = defineProps<Props>()

const route = useRoute()

interface Props {
title: string
typ: string
}

const { uuid } = route.params as { uuid: string }
Expand All @@ -22,11 +23,16 @@ const isEditing = ref<boolean>(false)
const titleRef = ref(null)

function handleHome() {
window.open('#/snapshot_all', '_blank')
const typ = props.typ
if (typ === 'snapshot') {
window.open('#/snapshot_all', '_blank')
} else if (typ === 'chatbot') {
window.open('#/bot_all', '_blank')
}
}

function handleChatHome() {
window.open('static/#/chat/', '_blank')
window.open('/static/#/chat/', '_blank')
}

const { mutate } = useMutation({
Expand Down
8 changes: 4 additions & 4 deletions web/src/views/snapshot/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRoute } from 'vue-router'
import { useDialog, useMessage, NSpin } from 'naive-ui'
import html2canvas from 'html2canvas'
import Message from './components/Message/index.vue'
import { useCopyCode } from './hooks/useCopyCode'
import { useCopyCode } from '@/hooks/useCopyCode'
import Header from './components/Header/index.vue'
import { CreateSessionFromSnapshot, fetchChatSnapshot } from '@/api/chat_snapshot'
import { HoverButton, SvgIcon } from '@/components/common'
Expand Down Expand Up @@ -136,7 +136,7 @@ function handleMarkdown() {
async function handleChat() {
if (!authStore.getToken())
nui_msg.error(t('common.ask_user_register'))
window.open(`/`, '_blank')
window.open(`/`, '_blank')
const { SessionUuid }: { SessionUuid: string } = await CreateSessionFromSnapshot(uuid)
await chatStore.setActiveLocal(SessionUuid)
// open link at static/#/chat/{SessionUuid}
Expand All @@ -160,10 +160,10 @@ function onScrollToTop() {
<template>
<div class="flex flex-col w-full h-full">
<div v-if="isLoading">
<NSpin size="large" />
<NSpin size="large" />
</div>
<div v-else>
<Header :title="snapshot_data.title" />
<Header :title="snapshot_data.title" typ="snapshot" />
<main class="flex-1 overflow-hidden">
<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
<div id="image-wrapper" class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
Expand Down
Loading