395 lines
12 KiB
HTML
395 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="renderer" content="webkit">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<meta http-equiv="Cache-Control" content="no-siteapp">
|
||
<link rel="icon" type="image/webp" href="/{{.AgentID}}/avatar.webp">
|
||
<title>{{.AgentName}}</title>
|
||
<style>
|
||
:root {
|
||
--color-primary: {{.PrimaryColor}};
|
||
--color-secondary: {{.SecondaryColor}};
|
||
--color-accent: {{.AccentColor}};
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans',
|
||
'PingFang SC', 'Hiragino Sans GB', 'Source Han Sans CN', 'Source Han Sans SC', 'Microsoft YaHei', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei',
|
||
'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif;
|
||
margin: 0;
|
||
padding: 0;
|
||
background-color: #f3f3f3;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
main {
|
||
max-width: 480px;
|
||
max-width: 960px;
|
||
width: 60%;
|
||
height: 60vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
border-radius: .5rem;
|
||
box-shadow: 0 0 1rem rgba(0, 0, 0, .1);
|
||
}
|
||
|
||
header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 1rem;
|
||
padding: 1rem;
|
||
color: white;
|
||
background: var(--color-primary);
|
||
border-radius: .5rem .5rem 0 0;
|
||
}
|
||
|
||
header > img {
|
||
width: 3.5rem;
|
||
height: 3.5rem;
|
||
border-radius: .5rem;
|
||
}
|
||
|
||
header > nav > h1 {
|
||
margin: 0;
|
||
font-weight: 600;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
header > nav > b {
|
||
display: block;
|
||
margin-top: .25rem;
|
||
font-weight: 400;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
header > aside {
|
||
margin-left: auto;
|
||
display: flex;
|
||
margin-top: -1em;
|
||
margin-right: -1em;
|
||
}
|
||
|
||
header > aside > button {
|
||
width: 2.5rem;
|
||
height: 1.5rem;
|
||
background: var(--color-secondary);
|
||
border: none;
|
||
outline: none;
|
||
border-right: 1px solid white;
|
||
cursor: pointer;
|
||
color: white;
|
||
}
|
||
|
||
header > aside > button:first-child {
|
||
border-radius: .0 0 0 .5rem;
|
||
}
|
||
|
||
header > aside > button:last-child {
|
||
border-radius: 0 .5rem 0 0;
|
||
border-right: none;
|
||
}
|
||
|
||
header > aside > button:hover {
|
||
background: var(--color-accent);
|
||
}
|
||
|
||
article {
|
||
flex-grow: 1;
|
||
background: white;
|
||
padding: 1rem;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
article > section {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
article > section > img {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border: 1px solid #f3f3f3;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
article > section > div {
|
||
padding: .75rem;
|
||
border-radius: .5rem;
|
||
background: var(--color-primary);
|
||
color: white;
|
||
line-height: 1.2;
|
||
margin-right: 3rem;
|
||
}
|
||
|
||
article > section > div > p {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
min-height: 1rem;
|
||
}
|
||
|
||
article > section[role="user"] {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
article > section[role="user"] > div {
|
||
background: #f3f3f3;
|
||
color: #3c3c3c;
|
||
margin-left: 3rem;
|
||
margin-right: 0;
|
||
}
|
||
|
||
article > section[role="system"] > div {
|
||
background: #ba1919;
|
||
color: white;
|
||
}
|
||
|
||
footer {
|
||
min-height: 150px;
|
||
height: 25%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: white;
|
||
border-bottom: 1px solid #f3f3f3;
|
||
border-radius: 0 0 .5rem .5rem;
|
||
}
|
||
|
||
footer > textarea {
|
||
width: 100%;
|
||
resize: none;
|
||
flex-grow: 1;
|
||
padding: .5rem;
|
||
font-size: 16px;
|
||
border: none;
|
||
border-top: 1px solid #d3d3d3;
|
||
border-bottom: 1px solid #d3d3d3;
|
||
outline: none;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
footer > menu {
|
||
margin: .5rem;
|
||
padding: 0;
|
||
display: flex;
|
||
justify-content: end;
|
||
align-items: center;
|
||
}
|
||
|
||
footer > menu > span {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
footer > menu > div > button {
|
||
width: 5rem;
|
||
height: 1.5rem;
|
||
border-radius: .25rem;
|
||
box-shadow: 0 0 .25rem rgba(0, 0, 0, .1);
|
||
background: var(--color-secondary);
|
||
border: none;
|
||
outline: none;
|
||
border-right: 1px solid white;
|
||
cursor: pointer;
|
||
color: white;
|
||
}
|
||
|
||
footer > menu > div > button:disabled {
|
||
background: #d3d3d3;
|
||
color: #666;
|
||
}
|
||
|
||
.copy {
|
||
margin-top: 1rem;
|
||
text-align: center;
|
||
font-size: .8rem;
|
||
line-height: 1.5;
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.copy a {
|
||
color: #ee82ee;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.copy a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
template {
|
||
display: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<header>
|
||
<img src="/{{.AgentID}}/avatar.webp" alt="头像">
|
||
<nav>
|
||
<h1>{{.AgentName}}</h1>
|
||
<b>{{.AgentDescription}}</b>
|
||
</nav>
|
||
<aside>
|
||
<button>_</button>
|
||
<button>□</button>
|
||
<button>×</button>
|
||
</aside>
|
||
</header>
|
||
<article>
|
||
</article>
|
||
<footer>
|
||
<textarea name="chat-box"></textarea>
|
||
<menu>
|
||
<div>
|
||
<button name="clear">清除 (C)</button>
|
||
<button name="send">发送 (S)</button>
|
||
</div>
|
||
</menu>
|
||
</footer>
|
||
</main>
|
||
<div class="copy">
|
||
以上内容为 AI 生成,请注意辨别。<br>为了安全和审计需要,我们可能记录您与 AI 的对话内容和您的 IP 地址。请勿向模型分享敏感信息。
|
||
</div>
|
||
<template id="chat-record">
|
||
<section>
|
||
<img>
|
||
<div>
|
||
</section>
|
||
</template>
|
||
<script>
|
||
(() => {
|
||
function readMessages() {
|
||
return JSON.parse(localStorage.getItem("messages.{{.AgentID}}") || "[]");
|
||
}
|
||
|
||
function writeMessages(messages) {
|
||
localStorage.setItem("messages.{{.AgentID}}", JSON.stringify(messages));
|
||
}
|
||
|
||
function rendererMessages() {
|
||
const messages = readMessages();
|
||
const html = messages.map(msg => {
|
||
const section = document.getElementById('chat-record').content.firstElementChild.cloneNode(true);
|
||
section.setAttribute('role', msg.role);
|
||
const img = section.querySelector('img');
|
||
img.src = msg.role == "model" ? "/{{.AgentID}}/avatar.webp" : `/assets/avatar-${msg.role}.webp`;
|
||
img.alt = `${msg.role} avatar`;
|
||
const div = section.querySelector('div');
|
||
div.innerHTML = msg.content.split('\n').map(line => {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.textContent = line;
|
||
return paragraph.outerHTML;
|
||
}).join('');
|
||
return section.outerHTML;
|
||
});
|
||
const target = document.querySelector('article');
|
||
target.innerHTML = html.join('');
|
||
target.scrollTo(0, target.scrollHeight);
|
||
}
|
||
|
||
function updateLastMessage(content, renderer = true) {
|
||
const messages = readMessages();
|
||
if (messages.length == 0) return;
|
||
messages[messages.length - 1].content += content;
|
||
writeMessages(messages);
|
||
if (renderer) rendererMessages();
|
||
}
|
||
|
||
async function sendMessage() {
|
||
let messages = readMessages();
|
||
const pendingMessage = messages.pop();
|
||
writeMessages(messages);
|
||
const response = await fetch("/{{.AgentID}}/chat", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify(messages.filter(msg => msg.role != "system")),
|
||
});
|
||
messages.push({ role: "model", content: "" });
|
||
writeMessages(messages);
|
||
|
||
console.log(response);
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder("utf-8");
|
||
let chunks = [];
|
||
while (true) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
const events = decoder.decode(value, { stream: true }).split("\n");
|
||
events.forEach(event => {
|
||
if (!event.startsWith("data: ")) return;
|
||
if (event == "data: [DONE]") return;
|
||
const data = JSON.parse(event.slice(6));
|
||
console.log("EventStream Data", data);
|
||
if (data.content) {
|
||
updateLastMessage(data.content, true);
|
||
chunks.push(data.content);
|
||
}
|
||
});
|
||
}
|
||
const fullText = chunks.join("").trim();
|
||
console.log("Full text", fullText);
|
||
messages.pop();
|
||
messages.push({ role: "model", content: fullText });
|
||
writeMessages(messages);
|
||
rendererMessages();
|
||
}
|
||
|
||
document.querySelector("[name=clear]").addEventListener("click", function() {
|
||
writeMessages([]);
|
||
rendererMessages();
|
||
});
|
||
|
||
document.querySelector("[name=chat-box]").addEventListener("keydown", function(event) {
|
||
if ((event.key === "Enter" && (event.ctrlKey || event.metaKey)) || (event.key === "s" && event.altKey)) {
|
||
event.preventDefault();
|
||
document.querySelector("[name=send]").click();
|
||
}
|
||
});
|
||
|
||
document.querySelector("[name=send]").addEventListener("click", function() {
|
||
const textarea = document.querySelector("[name=chat-box]");
|
||
const content = textarea.value.trim();
|
||
if (!content) return;
|
||
textarea.value = "";
|
||
document.querySelector("[name=send]").disabled = true;
|
||
let messages = readMessages();
|
||
messages.push({ role: "user", content });
|
||
messages.push({ role: "model", content: "对方正在输入..." });
|
||
writeMessages(messages);
|
||
rendererMessages();
|
||
sendMessage().catch(error => {
|
||
console.error(error);
|
||
let messages = readMessages();
|
||
messages.push({ role: "system", content: `网络错误:${error}` });
|
||
writeMessages(messages);
|
||
}).finally(() => {
|
||
rendererMessages();
|
||
document.querySelector("[name=send]").disabled = false;
|
||
});
|
||
});
|
||
|
||
(() => rendererMessages())();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|