Files
k3GPT/main/ui/llm_chat.aps
2025-11-19 19:43:12 +08:00

290 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>