chatai/web/agent.html
洛天依 644e1339fb
All checks were successful
Build / Build (push) Successful in 1m22s
feat: home page
2025-01-21 19:11:48 +00:00

396 lines
12 KiB
HTML
Raw Permalink 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.

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