Mon Jul 3 08:25:00 UTC 2023 inscode

上级 e64ad6c0
......@@ -7,6 +7,7 @@
"preview": "vite preview --port 4173"
},
"dependencies": {
"fetch-event-source": "^1.0.0-alpha.2",
"guess": "^1.0.2",
"vue": "^3.2.37"
},
......
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="w-full h-screen">
<!-- toolbar -->
<div class="w-full h-14 pt-2 border-b-2 shadow-2xl text-center fixed bg-slate-200">
<a href="#/llm-rep/app/">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-4 h-4 absolute left-4 top-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
</a>
<span class="leading-10 font-bold">{{ name }} {{ config?.mode === 'completions' ? '(无上下文)' : '' }}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-4 h-4 absolute right-4 top-5 cursor-pointer" @click="cleanHistory">
<path stroke-linecap="round" stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</div>
<!-- main content -->
<div class="flex w-full h-full pt-14">
<!-- left -->
<div class="flex-none w-0 md:w-[260px] bg-slate-200 border-r-2 hidden md:block p-8">
<t-form labelAlign="top" label-width="100" :style="{display: showProfileSetting ? 'block':'none'}">
<t-form-item label="机器人头像">
<!-- <t-input placeholder="请输入内容" v-model="config.robot_img"/> -->
<t-space direction="vertical" align="center" v-for="imgItem in robotAvatarList" class="mr-2 cursor-pointer hover:bg-blue-600">
<t-image class="rounded-lg border-2 " :class="{ 'border-blue-800': imgItem.src === config.robot_img }"
@click="changeRobotAvatar(imgItem.src)" :src="imgItem.src" fit="cover"
:style="{ width: '40px', height: '40px' }" />
<!-- <span>{{ imgItem.name }}</span> -->
</t-space>
</t-form-item>
<t-form-item label="你的头像">
<t-space direction="vertical" align="center" v-for="imgItem in userAvatarList" class="mr-2 cursor-pointer hover:bg-blue-600">
<t-image class="rounded-lg border-2 " :class="{ 'border-blue-800': imgItem.src === config.user_img }"
@click="changeUserAvatar(imgItem.src)" :src="imgItem.src" fit="cover"
:style="{ width: '40px', height: '40px' }" />
<!-- <span>{{ imgItem.name }}</span> -->
</t-space>
</t-form-item>
<t-form-item label="机器人对你的称呼" help="多个称呼用逗号隔开">
<t-input v-model="config.user_call_name" />
</t-form-item>
</t-form>
</div>
<!-- right -->
<div class="flex-auto bg-slate-300 md:bg-slate-300 p-2 sm:p-8">
<div class="w-full bg-slate-200 h-full m-auto relative container max-w-6xl rounded-xl">
<div class="w-full h-full pb-12 p-4 overflow-y-auto overflow-x-hidden" ref="messageList">
<div v-for="item in message" class="mb-8">
<div v-if="item.user === 'User'">
<div class="flex flex-row-reverse">
<t-image class="rounded-lg" v-if="config.user_img" :src="config.user_img" fit="cover"
:style="{ width: '40px', height: '40px' }"></t-image>
<div class="bg-green-400 text-gray-700 p-4 mx-2 w-fit max-w-2xl 2xl:max-w-4xl rounded-lg cursor-pointer" @click="copyAsPrompt(item.message)">
{{ item.message }}
</div>
</div>
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
<div v-else class="flex">
<t-image class="rounded-lg" v-if="config.robot_img" :src="config.robot_img" fit="cover"
:style="{ width: '40px', height: '40px' }"></t-image>
<div class="bg-slate-50 text-gray-700 p-4 mx-2 w-fit max-w-2xl 2xl:max-w-4xl rounded-lg text-wrapper">
{{ item.message ? item.message : '...' }}
</div>
</div>
</div>
<div class="text-center">
<div v-if="loading" @click="stop" class="-mt-4 px-5 py-1 m-auto w-fit cursor-pointer hover:bg-slate-300 border-dotted border-[1px] border-slate-400 rounded-md">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="animate-spin w-4 h-4 inline-block">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 019 14.437V9.564z" />
</svg>
<span class="ml-2 text-[12px]">停止</span>
</div>
</div>
</div>
<div class="w-full absolute bottom-0 bg-slate-100 h-10 py-2 px-2 rounded flex">
<input v-model="prompt" class="flex-auto ring-0 outline-0 pl-2 bg-slate-200" v-on:keyup.enter="query" />
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor"
class="w-6 mx-2 ml-4 h-6 flex-none text-cyan-800 font-bold hover:text-cyan-600 cursor-pointer"
@click="query">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</div>
</div>
</div>
</div>
</header>
<main>
<TheWelcome />
</main>
</div>
</template>
<script>
<style scoped>
header {
line-height: 1.5;
}
// import { Configuration, OpenAIApi } from "openai"
import OpenAI from './js/openai.js'
import Config from './js/config.js'
.logo {
display: block;
margin: 0 auto 2rem;
}
// import MarkdownItVue from 'markdown-it-vue'
// import 'markdown-it-vue/dist/markdown-it-vue.css'
// https://github.com/ravenq/markdown-it-vue
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
const DataServe = new Data()
.logo {
margin: 0 2rem 0 0;
}
export default {
name: 'LlmApp',
components: {
// MarkdownItVue
},
data() {
return {
id: 0,
name: '加载中...',
userAvatarList: [],
robotAvatarList: [],
message: [
// {"user": "User", "message": "创建一个用户表,要求分区"},
// {"user": "AI", "message": "在GaussDB数据库中创建一个带有分区的用户表可以使用以下语句:在GaussDB数据库中创建一个带有分区的用户表可以使用以下语句:在GaussDB数据库中创建一个带有分区的用户表可以使用以下语句:在GaussDB数据库中创建一个带有分区的用户表可以使用以下语句:"},
// {"user": "User", "message": "create table user(id int primary key, name varchar)将这个建表语句翻译成GaussDB建表语句create table user(id int primary key, name varchar)将这个建表语句翻译成GaussDB建表语句create table user(id int primary key, name varchar)将这个建表语句翻译成GaussDB建表语句create table user(id int primary key, name varchar)将这个建表语句翻译成GaussDB建表语句create table user(id int primary key, name varchar)将这个建表语句翻译成GaussDB建表语句"},
// {"user": "AI", "message": "在GaussDB数据库中,创建一个带有主键约束的用户表可以使用以下语句:"},
],
client: null,
indexClient: null,
prompt: '',
config: {
user_img: "",
robot_img: "",
user_call_name: "xxx",
// api_url: 'https://inscode-ide.node.inscode.run:8000/v1',
// index_url: 'http://127.0.0.1:8000/search',
// index_name: 'datasea',
// api_prompt_prefix: '',
// max_tokens: 2048,
// prompt_template: "Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n" +
// "{context}\n"+
// "Question: {question}\n"+
// "Helpful Answer:"
},
loading: false,
maxHistory: 200,
speaking: false, // 是否正在输出语音
autoSpeech: false, // 是否自动开始语音
speech: null, // 语音object
showProfileSetting: false, //是否显示角色设置模块
}
},
methods: {
hideLeftMenu() {
},
changeUserAvatar(imgSrc) {
this.config.user_img = imgSrc
},
changeRobotAvatar(imgSrc) {
this.config.robot_img = imgSrc
},
getAvatar() {
let that = this
DataServe.find('t_llm_rep_chat_img', { resoure_sub_type: 'user_avatar' }).then(res => {
if (res && res.status === 200 && res.data && res.data.code === 200 && res.data.data) {
const data = res.data.data
for (let item of data) {
that.userAvatarList.push({
src: JSON.parse(item.ext).img_src,
name: item.name
})
}
}
})
DataServe.find('t_llm_rep_chat_img', { resoure_sub_type: 'robot_avatar' }).then(res => {
if (res && res.status === 200 && res.data && res.data.code === 200 && res.data.data) {
const data = res.data.data
for (let item of data) {
that.robotAvatarList.push({
src: JSON.parse(item.ext).img_src,
name: item.name
})
}
}
})
},
initAIClient () {
this.client = new OpenAI(this.config)
},
initIndexClient () {
if (this.config?.index_url) {
const index_type = this.config?.index_type
//confluence
if (index_type === 'es') {
this.indexClient = new RepEs(this.config)
} else if(index_type === 'confluence') {
this.indexClient = new RepConfluence(this.config)
} else {
this.indexClient = new RepSimple(this.config)
}
}
},
query() {
if (this.loading) {
MessagePlugin.warning({ content: '正在执行中,请稍等!', placement: 'center' })
return
}
if (this.prompt === '') {
MessagePlugin.warning({ content: '提示词不能为空!', placement: 'center' })
console.info('提示词不能为空')
return
}
if (this.indexClient) {
this.indexClient.query(this.prompt).then(res => {
this.getAnswer(res)
}).catch((err) => {
this.getAnswer()
})
} else {
this.getAnswer()
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
},
getAnswer(context) {
this.loading = true
const _this = this
const messages = this.message
const currentMsg = {
"user": "AI", "message": ''
}
messages.push({ "user": "User", "message": this.prompt })
messages.push(currentMsg)
_this.$refs.messageList.scrollTop = _this.$refs.messageList.scrollHeight;
const newPrompt = this.prompt
console.info(this.prompt)
console.info(newPrompt)
this.prompt = ''
this.client.createCompletion(
newPrompt,
this.message,
context,
{
onmessage: (msg, isPart) => {
// _this.set(_this.message, id, currentMsg)
if (isPart) {
currentMsg.message += msg
} else {
currentMsg.message = msg
}
messages.splice(messages.length - 1, 1)
messages.push(currentMsg)
_this.$refs.messageList.scrollTop = _this.$refs.messageList.scrollHeight;
// console.info(currentMsg.message)
},
onclose: () => {
_this.loading = false
_this.saveHistory()
console.info('close')
if(_this.autoSpeech) {
_this.speak()
}
},
onerror: (err) => {
debugger
_this.loading = false
console.info(err)
}
})
},
getAppInfo() {
const data = Config.getData()
this.name = data.name
document.title = this.name
if (data.ext) {
const config = JSON.parse(data.ext)
this.config = config
if (config.default_prompt) {
this.prompt = config.default_prompt
}
if (config?.auto_speech??false) {
this.autoSpeech = true
this.initSpeaker()
}
if (config?.welcome_text && config?.welcome_text.length > 0 && this.message.length === 0) {
this.message.push({
"user": "AI",
"message": config.welcome_text
})
}
this.showProfileSetting = config?.show_profile_setting??false
this.initAIClient()
this.initIndexClient()
console.info(config)
}
},
recoveryHistory() {
const cacheKey = 'history_' + this.id
const data = localStorage.getItem(cacheKey)
if (data) {
this.message = JSON.parse(data)
}
},
saveHistory() {
const cacheKey = 'history_' + this.id
if (this.message.length > this.maxHistory) {
const tmpHistory = []
const start = this.message.length - this.maxHistory
const end = this.message.length
for (let id = start; id < end; id++) {
tmpHistory.push(this.message[id])
}
localStorage.setItem(cacheKey, JSON.stringify(tmpHistory))
} else {
localStorage.setItem(cacheKey, JSON.stringify(this.message))
}
},
cleanHistory() {
this.message = []
this.saveHistory()
},
initSpeaker () {
window.speechSynthesis.cancel()
this.speech = new SpeechSynthesisUtterance();
this.speech.onend = () => {
console.log("语音播报结束")
this.speaking = false
}
},
speak () {
this.speaking = true
const content = this.message[this.message.length - 1].message
if (content && content.length > 1) {
// var utterThis = new SpeechSynthesisUtterance(text);
this.speech.text = content;
console.info('speak ' + content)
// speech.lang = 'zh';//汉语
window.speechSynthesis.speak(this.speech);
}
},
stop () {
this.client.close()
},
copyAsPrompt (message) {
this.prompt = message
}
},
mounted() {
this.id = this.$route?.params?.id
this.recoveryHistory()
this.getAppInfo()
this.getAvatar()
}
};
</script>
<style>
.text-wrapper {
white-space: pre-wrap;
}
.vuepress-markdown-body {
padding: 16px !important;
}
</style>
\ No newline at end of file
export default {
getData () {
return {
"code": 200,
"data": {
"id": 128897,
"name": "小羊驼",
"create_time": 1684920670901,
"app": "llm_rep",
"resource_type": "app",
"ext": "{\"mode\": \"chat\", \"model\": \"chatglm2-6b\", \"api_url\": \"https://gpu-pod647d498393e106496a046e94-8000.node.inscode.run/v1\", \"api_type\": \"openai\", \"robot_img\": null, \"api_max_token\": \"2048\", \"default_prompt\": \"请翻译成英文:你是谁?\", \"max_request_len\": \"10000\", \"prompt_template\": \"\", \"api_prompt_prefix\": \"\", \"show_profile_setting\": false}"
},
"message": "success"
}
}
}
\ No newline at end of file
import { fetchEventSource } from '@microsoft/fetch-event-source';
import Prompt from './prompt.js'
class OpenAI {
constructor(config) {
this.config = config
this.abortController = null
this.callback = null
this.temperature = parseFloat(config?.temperature??0.7)
}
createCompletion (prompt, history, context, callback) {
const config = this.config
const abortController = new AbortController();
const signal = abortController.signal;
this.abortController = abortController
this.callback = callback
const mode = config?.mode??'chat'
const token = config?.token??'empty'
const url = config.api_url + (mode === 'chat' ? '/chat/completions' : '/completions')
const stop = config?.stop??'[DONE]'
const max_tokens = config?.api_max_token??512
const model = config?.model??'vicuna-13b-all-v1.1'
const temperature = this.temperature
const top_p = config?.top_p??1.0
let stop_key = config?.stop_key??null
if (stop_key !== null && stop_key !== '') {
stop_key = stop_key.split(';;')
}
const data = {
model: model,
max_tokens: parseInt(max_tokens),
temperature: parseFloat(temperature),
top_p: parseFloat(top_p),
stream: true,
stop: stop_key
// prefix: prefix
}
if (mode === 'chat') {
data.messages = Prompt.getPromptByChatMode(config, context, history)
} else {
data.prompt = Prompt.getPromptByTemplate(config, context, prompt)
}
// const prefix = config?.prompt_prefix??''
const fetcher = fetchEventSource(url, {
method: 'POST',
signal: signal,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
onmessage(msg) {
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
if (msg && msg?.data) {
if (msg?.data === stop) {
if (callback?.onclose) {
callback?.onclose()
}
abortController.abort();
return
} else {
console.info(msg.data)
const jsonData = JSON.parse(msg.data)
// 和上面重复触发,只留一个
// if (jsonData.choices[0].finish_reason === 'stop') {
// if (callback?.onclose) {
// callback?.onclose()
// }
// return
// }
let message = null
if (mode === 'chat') {
message = jsonData?.choices[0]?.message?.content
if (typeof message === 'undefined') {
message = jsonData?.choices[0]?.delta?.content
}
if (typeof message === 'undefined') {
message = ''
}
} else {
message = jsonData?.choices[0]?.text
}
callback?.onmessage(message, true)
}
}
// if (msg.event === 'FatalError') {
// throw new FatalError(msg.data);
// }
},
onclose() {
if (callback?.onclose) {
callback?.onclose()
}
// if the server closes the connection unexpectedly, retry:
},
onerror(err) {
if (callback?.onerror) {
callback?.onerror(err)
}
}
});
}
close () {
if (this.abortController) {
this.abortController.abort()
if (this.callback && this.callback?.onclose) {
this.callback.onclose()
}
}
}
}
export default OpenAI
\ No newline at end of file
const default_max_token = 1024
const getContextContent = (context, max_token=default_max_token) => {
if (context && context.length > 0) {
let id = 0
let len = context.length
let contextContent = ''
while(contextContent.length < max_token && id < len) {
if (context[id].page_content.length + contextContent.length < max_token) {
contextContent = contextContent + '\n' + context[id].page_content
}
id++
}
return contextContent
} else {
return ''
}
}
export default {
getPromptByTemplate: (config, context, prompt, history) => {
if (config.api_prompt_prefix) {
prompt = config?.api_prompt_prefix + ' ' + prompt
}
if (config?.prompt_template) {
const contextContent = getContextContent(context, config?.max_request_len??1024)
return config?.prompt_template.replace(/\{question\}/ig, prompt).replace(/\{context\}/ig, contextContent)
} else {
return prompt
}
},
getPromptByChatMode (config, context, history) {
const history_length = Math.min(Math.max(parseInt(config?.history_length??4), 4), 10)
let message = []
if (history && history.length >= 2) {
const end = history.length - 2 // 结束位置
const start = Math.max(history.length - 2 - history_length + 1, 0) // 开始位置
for(let id = start; id <= end; id++) {
const item = history[id]
message.push({
"role": item.user === 'AI' ? "system" : "user",
"content": item.message
})
}
}
if (config?.prompt_template) {
const contextContent = getContextContent(context, config?.max_request_len??1024)
message.unshift({
"role": "user",
"content": config.prompt_template.replace(/\{question\}/ig, '').replace(/\{context\}/ig, contextContent).replace(/\{user_call_name\}/ig, config.user_call_name)
})
}
return message
}
}
\ No newline at end of file
......@@ -2,5 +2,6 @@ import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css'
import './style.css'
createApp(App).mount('#app')
@tailwind base;
@tailwind components;
@tailwind utilities;
\ No newline at end of file
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册