Files
k3GPT/main/ui/llm_chat.aps

290 lines
18 KiB
Plaintext
Raw Normal View History

2025-11-19 19:43:12 +08:00
<div x-data="chat" class="mx-auto flex w-full">
<div class="mx-4 flex w-full flex-col h-[90vh]">
<!-- 对话的展示-->
<div id="chat_display" class="flex-grow overflow-hidden overflow-y-auto">
<!-- 对话列表-->
<div class="">
<div class="flex flex-col gap-2 mx-4">
<!-- AI's Response -->
<div class="w-full mt-2 border-zinc-300 bg-zinc-100 p-2 text-left dark:border-zinc-700 dark:bg-zinc-800 rounded border" >
<div class="flex items-center gap-2 text-neutral-900 dark:text-zinc-50">
<span class="flex size-8 items-center justify-center rounded-full bg-sky-700 text-white dark:bg-sky-600 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span class="text-sm font-bold">Yi.AI</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-neutral-600 dark:text-zinc-200">
厂家:<span x-text="llm.title"></span><br>
地址:<span x-text="llm.url"></span><br>
模型:<span x-text="llm.name"></span><br>
现在可以对话测试了
</p>
</div>
<template x-for="(item, index) in Items" :key="index">
<div>
<!-- User's Chat -->
<div :id="'QA'+index" class="w-full ml-auto max-w-xs border-zinc-300 bg-zinc-100 p-2 text-left dark:border-zinc-700 dark:bg-zinc-800 rounded border" >
<div class="flex items-center gap-2 text-neutral-900 dark:text-zinc-50">
<span class="flex size-8 items-center justify-center rounded-full bg-blue-700 text-white dark:bg-sky-600 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span x-text="item.req" class="text-sm">Alice Brown</span>
</div>
</div>
<!-- AI's Response -->
<div x-show="item.rsp_show" class="mt-2 w-full border-zinc-300 bg-zinc-100 p-2 text-left dark:border-zinc-700 dark:bg-zinc-800 rounded border" >
<div class="flex items-center gap-2 text-neutral-900 dark:text-zinc-50">
<span class="flex size-8 items-center justify-center rounded-full bg-sky-700 text-white dark:bg-sky-600 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span x-text="item.rsp" style="overflow-wrap: break-word;" class="text-sm font-bold">Pengu AI</span>
<button @click="show_info(index)" class="rounded-full p-1 text-neutral-600/75 hover:bg-zinc-900/10 hover:text-neutral-600 focus:outline-none focus-visible:text-neutral-600 focus-visible:zinc-300 focus-visible:outline-offset-0 focus-visible:outline-sky-700 active:bg-zinc-900/5 active:-outline-offset-2 dark:text-zinc-200/75 dark:hover:bg-zinc-50/10 dark:hover:text-zinc-200 dark:focus-visible:text-zinc-200 dark:focus-visible:outline-sky-600 dark:active:bg-zinc-50/5" title="上下文信息" aria-label="上下文信息" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
</svg>
</button>
</div>
<p x-bind:id="item.chat_id" :x-ref="item.chat_id" class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-neutral-600 dark:text-zinc-200"></p>
</div>
</div>
</div>
</template>
</ul>
</div>
</div>
</div>
<!-- 输入框 -->
<div class="flex w-full flex-col gap-2 mb-4 mt-2">
<div class="relative w-full">
<label class="sr-only">ai prompt</label>
<!-- 文件上传-->
<input x-ref="fileInput" @input="upload_file" type="file" multiple hidden class="w-full overflow-clip rounded border border-red-700 bg-zinc-100/50 text-sm text-red-700 file:mr-4 file:cursor-pointer file:border-none file:bg-zinc-100 file:px-4 file:py-2 file:font-medium file:text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-zinc-800/50 dark:file:bg-zinc-800 dark:file:text-zinc-50 dark:focus-visible:outline-sky-600" />
<svg @click="$refs.fileInput.click()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" class="absolute left-3 top-1/2 size-6 cursor-pointer -translate-y-1/2 fill-blue-700 dark:fill-sky-600">
<path fill-rule="evenodd" d="M10.5 3.75a6 6 0 0 0-5.98 6.496A5.25 5.25 0 0 0 6.75 20.25H18a4.5 4.5 0 0 0 2.206-8.423 3.75 3.75 0 0 0-4.133-4.303A6.001 6.001 0 0 0 10.5 3.75Zm2.03 5.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72v4.94a.75.75 0 0 0 1.5 0v-4.94l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z" clip-rule="evenodd"/>
</svg>
<input x-show="one" x-model="query" x-on:input.debounce.100ms="query_len" x-on:keydown.enter="gen" type="text" class="w-full border-outline bg-zinc-100 border border-zinc-300 rounded px-2 py-2 pl-10 pr-24 text-sm text-neutral-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700 disabled:cursor-not-allowed disabled:opacity-75 dark:border-zinc-700 dark:bg-zinc-800/50 dark:text-zinc-200 dark:focus-visible:outline-sky-600" value="" name="prompt" placeholder="Ask AI ..." />
<textarea id="aiPromt" x-show="!one" rows="5" x-model="query" x-on:input.debounce.100ms="query_len" type="text" class="w-full border-outline bg-zinc-100 border border-zinc-300 rounded px-2 py-2 pl-10 pr-24 text-sm text-neutral-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700 disabled:cursor-not-allowed disabled:opacity-75 dark:border-zinc-700 dark:bg-zinc-800/50 dark:text-zinc-200 dark:focus-visible:outline-sky-600" value="" name="prompt" placeholder="Ask AI ..." ></textarea>
<button type="button" x-show="is_gen" x-on:click="gen" class="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer bg-sky-700 rounded px-2 py-1 text-xs tracking-wide text-white transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700 active:opacity-100 active:outline-offset-0 dark:bg-sky-600 dark:text-white dark:focus-visible:outline-sky-600">Generate</button>
<button type="button" x-show="!is_gen" x-on:click="stop" class="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer bg-blue-700 rounded px-2 py-1 text-xs tracking-wide text-white transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700 active:opacity-100 active:outline-offset-0 dark:bg-sky-600 dark:text-white dark:focus-visible:outline-sky-600">Stop</button>
</div>
</div>
</div>
</div>
</div>
<script>
function chat(){
return {
query:"",
mode:"FA", //KG-知识库NS-全网搜索,FA-自由回答,DS-深度思考
eventSource: "", //对话流
is_gen: true, //生成模式
Info: false, // 显示上下文
fItems:[], //上下文信息
one:true, //一行文本输入
style: "",
do_chat_files:{}, //对话过程中产生的文件,主要是数据分析
chat_files:[], //保留对话的文件的记录
Items:[], //对话过程所有信息
qItems:[], //对话问题的列表,用于多轮对话
ai_file: {},
llm:{},
init(){
this.llm = this.llm_cfg.find(item => item.id === this.tabSelected);
},
push_item(item){
this.Items.push(item);
},
push_qitem(item){
this.qItems.push(item);
},
update_last_item(chat_id,title,ctx){
var last = this.Items[this.Items.length-1];
last.chat_id=chat_id;
last.rsp=title;
last.rsp_show=true;
last.ctx=ctx;
},
update_last_qitem(answer){
//更新最后一个问题的答案
var last = this.qItems[this.qItems.length-1];
last.rsp=answer;
},
query_len(){
if (this.query.length >45){
this.one=false;
if (this.query.length >300){
const aiPromt = document.getElementById("aiPromt");
aiPromt.rows="10";
}
if (this.query.length >1000){
const aiPromt = document.getElementById("aiPromt");
aiPromt.rows="15";
}
}else{
this.one=true;
}
},
copy_query(query){ //复制查询
this.query = query;
this.query_len()
},
copy_that(chat_id){
const messagesDiv = document.getElementById(chat_id);
let rsp = messagesDiv.innerHTML;
rsp = rsp.replace("<think>","<p>").replace("</think>","</p>");
copyToClipboard(rsp);
},
stop(){
this.eventSource.close();
this.is_gen = true;
this.one=true;
},
gen(){
this.is_gen = false;
this.push_item({chat_id:"",req:this.query,rsp:"",rsp_show:false});
var query=this.query;
if (this.query.length >100){
query = this.query.slice(0,100)+"...";
}
// 数据对象,代表要发送的数据
const newItem = {
question: this.query,
ai_mode: JSON.stringify(this.llm)
};
// 使用 axios 发送 POST 请求
axios.post('/api/chat_llm_test', newItem)
.then(response =>{
rsp = response.data;
this.update_last_item(rsp.chat_id,`正在请求中...`,"");
this.push_qitem({chat_id:rsp.chat_id,req:query});
// 创建一个EventSource实例连接到服务器发送的事件流
this.eventSource = new EventSource('/api/chat_llm/'+rsp.chat_id);
update_chat(this.eventSource,rsp.chat_id,this);
const div = document.getElementById("chat_display");
div.scrollTop = div.scrollHeight;
})
.catch(error => {
console.error('Error post data:', error);
throw error; // 重新抛出错误以便调用者可以处理
});
this.query="";
this.one=true;
const div = document.getElementById("chat_display");
div.scrollTop = div.scrollHeight+20;
},
}
}
//流式更新对话
function update_chat(eventSource,chat_id,fun){
fun.is_gen = false;
var old_think="";
//分块更新
var ResponseDiv = null;
var messagesDiv = document.createElement('div');
var do_files=[];
// 监听消息事件,将接收到的数据显示在网页上
eventSource.onmessage = function(event) {
//首次创建
if (ResponseDiv==null){
ResponseDiv = document.getElementById(chat_id);
ResponseDiv.appendChild(messagesDiv);
}
const message = JSON.parse(event.data);
if (message.rsp.indexOf("<think>")==0){
var end = message.rsp.indexOf("</think>");
if (end==-1){ //思考还未结束
var think_mk = marked.parse(message.rsp.slice(7));
messagesDiv.innerHTML = old_think + think_mk;
//messagesDiv.insertAdjacentHTML("beforeend",think_mk);
}else{
var think_mk = marked.parse(message.rsp.slice(7,end));
var result= marked.parse(message.rsp.slice(end+8));
if (result!=""){
var think_all=`${think_mk}<div style="color: white; background-color: rgb(3 105 161 / 0.7); border-radius: 5px; margin-bottom: 5px; padding:5px;">${result}</div>`;
messagesDiv.innerHTML = old_think+think_all;
fun.update_last_qitem(result);
}
}
}else if(message.rsp.indexOf("<new>")==0){
//新的一个消息,新的一个层
messagesDiv = document.createElement('div');
ResponseDiv.appendChild(messagesDiv);
}else{
var think_all=`<div style="color: white; background-color: rgb(3 105 161 / 0.7); border-radius: 5px; margin-bottom: 5px; padding:5px;">${marked.parse(message.rsp)}</div>`;
messagesDiv.innerHTML = old_think+ think_all;
fun.update_last_qitem(think_all);
}
//自动滚动
const div = document.getElementById("chat_display");
div.scrollTop = div.scrollHeight;
};
// 监听结束
eventSource.onerror = function(error) {
if (eventSource.readyState != EventSource.CLOSED){
eventSource.close(); // 关闭连接
}
fun.is_gen = true;
fun.gen_ppt_ai(chat_id);
console.info(chat_id+" 回答结束");
if (do_files.length >0){
fun.do_chat_files[chat_id]=do_files;
console.info(do_files);
}
//结束后滚动
const div = document.getElementById("chat_display");
div.scrollTop = div.scrollHeight;
};
}
</script>