chatai/web/agent.html

396 lines
12 KiB
HTML
Raw Permalink Normal View History

2024-12-25 17:18:23 +00:00
<!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>
2025-01-11 04:31:04 +00:00
<button onclick="location.href = '/'">×</button>
2024-12-25 17:18:23 +00:00
</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">
2025-01-11 04:31:04 +00:00
{{.FooterHTML}}<br>
以上内容为 AI 生成,请注意辨别。<br>为了安全和审计需要,我们可能记录您与 AI 的对话内容和您的 IP 地址。请勿向 AI 分享敏感信息。
2024-12-25 17:18:23 +00:00
</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>