136 lines
3.6 KiB
HTML
136 lines
3.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Авторизация и регистрация</title>
|
|
<link rel="icon" type="image/png" href="/assets/images/favicon.png">
|
|
<style>
|
|
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }
|
|
#portal {
|
|
display: block;
|
|
border: 0;
|
|
width: 100%;
|
|
height: calc(100vh - 50px);
|
|
}
|
|
/* chat stays fixed at the bottom 50px of viewport */
|
|
.chat-overlay {
|
|
position: fixed;
|
|
left: 0; right: 0; bottom: 0;
|
|
background: #fff;
|
|
border-top: 4px solid #003571;
|
|
z-index: 9999;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
#chat-output-tiny {
|
|
position: fixed;
|
|
left: 0; right: 0;
|
|
bottom: 50px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
padding: 4px 15px;
|
|
font-size: 10px;
|
|
color: #c4c4c4;
|
|
line-height: 1.5;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
background: linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.92) 30%, #fff 100%);
|
|
pointer-events: none;
|
|
}
|
|
#chat-output-tiny:empty { display: none; }
|
|
.chat-overlay .inner {
|
|
max-width: 970px; margin: 0 auto;
|
|
padding: 8px 15px;
|
|
}
|
|
#chat-input {
|
|
width: 100%;
|
|
border: none;
|
|
background: transparent;
|
|
color: #f2f2f2;
|
|
font-size: 14px;
|
|
outline: none;
|
|
resize: none;
|
|
height: 28px;
|
|
max-height: 28px;
|
|
min-height: 28px;
|
|
line-height: 1.5;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
caret-color: #c4c4c4;
|
|
font-family: inherit;
|
|
}
|
|
#chat-input::placeholder { color: transparent; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<iframe id="portal" src="/proxy/auth.php"></iframe>
|
|
|
|
<div id="chat-output-tiny"></div>
|
|
|
|
<div class="chat-overlay">
|
|
<div class="inner">
|
|
<textarea id="chat-input" autocomplete="off" spellcheck="false" rows="1"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const inputEl = document.getElementById('chat-input');
|
|
const outputEl = document.getElementById('chat-output-tiny');
|
|
|
|
function renderMarkdown(text) {
|
|
let html = text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
html = html.replace(/```[\w]*\n?([\s\S]*?)```/g, (_,c) => `<pre>${c.trim()}</pre>`);
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
return html;
|
|
}
|
|
|
|
let inFlight = false;
|
|
async function send(text) {
|
|
if (inFlight || !text.trim()) return;
|
|
inFlight = true;
|
|
outputEl.innerHTML = '';
|
|
let fullText = '';
|
|
try {
|
|
const resp = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({text, model: 'sonnet'}),
|
|
});
|
|
const reader = resp.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buf = '';
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) break;
|
|
buf += decoder.decode(value, {stream:true});
|
|
const parts = buf.split('\n\n');
|
|
buf = parts.pop();
|
|
for (const part of parts) {
|
|
if (!part.startsWith('data: ')) continue;
|
|
const raw = part.slice(6).trim();
|
|
if (raw === '[DONE]') continue;
|
|
try {
|
|
const obj = JSON.parse(raw);
|
|
if (obj.text) {
|
|
fullText += obj.text;
|
|
outputEl.innerHTML = renderMarkdown(fullText);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
} catch(e) {} finally { inFlight = false; }
|
|
}
|
|
|
|
inputEl.addEventListener('paste', () => {
|
|
setTimeout(() => {
|
|
const t = inputEl.value.trim();
|
|
if (t) send(t);
|
|
}, 50);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|