wt-copilot / wtc.html
Trina-QwQ's picture
Upload wtc.html
bca2e99 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WT - Copilot</title>
<style>
body {
margin: 0;
display: flex;
height: 100vh;
font-family: "PingFang SC", "Microsoft YaHei", serif;
background-color: #f5f5f5;
}
/* 侧边栏 */
#sidebar {
width: 280px;
background: #fff;
border-right: 1px solid #ddd;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
z-index: 10;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.setting-item label {
font-size: 12px;
color: #666;
font-weight: bold;
}
input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
/* 编辑区域 */
#main-container {
flex: 1;
padding: 40px;
display: flex;
justify-content: center;
overflow-y: auto;
}
#editor {
width: 100%;
max-width: 800px;
min-height: 85vh;
background: white;
padding: 40px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
outline: none;
font-size: 18px;
line-height: 1.8;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
/* 确保内容垂直对齐 */
display: block;
}
/* Ghost Text 样式 - 关键:禁止选中,颜色淡化 */
.ghost-text {
color: #aaa;
user-select: none;
pointer-events: none;
font-style: italic;
}
.status-box {
font-size: 12px;
margin-top: auto;
color: #888;
background: #f9f9f9;
padding: 10px;
border-radius: 5px;
}
.kbd {
background: #eee;
padding: 2px 4px;
border-radius: 3px;
border: 1px solid #ccc;
color: #333;
}
</style>
</head>
<body>
<div id="sidebar">
<h3>WTC - WebUI</h3>
<div class="setting-item">
<label>API Endpoint</label>
<input type="text" id="api-url" value="http://localhost:8080/v1/chat/completions">
</div>
<div class="setting-item">
<label>Temperature</label>
<input type="number" id="temp" value="0.7" step="0.1" min="0" max="2">
</div>
<div class="setting-item">
<label>Max Tokens</label>
<input type="number" id="max-tokens" value="150">
</div>
<div class="status-box">
<p><span class="kbd">Tab</span> 触发续写</p>
<p>🤝 <span class="kbd">Tab</span> 接受建议</p>
<p>Powered by WT-Copilot™ | Trina AI Lab</p>
<hr>
<p id="info-display">状态:就绪</p>
</div>
</div>
<div id="main-container">
<div id="editor" contenteditable="true" spellcheck="false">在这里输入故事开头...</div>
</div>
<script>
const editor = document.getElementById('editor');
const infoDisplay = document.getElementById('info-display');
let isFetching = false;
editor.addEventListener('paste', (e) => {
e.preventDefault();
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
});
function periodicSanitize() {
if (isFetching) return;
const ghost = editor.querySelector('.ghost-text');
if (!ghost) {
const hasComplexHTML = Array.from(editor.childNodes).some(node =>
node.nodeType === 1 && node.tagName !== 'SPAN' && node.tagName !== 'BR'
);
if (hasComplexHTML) {
const selection = window.getSelection();
const offset = selection.focusOffset;
editor.innerText = editor.innerText;
}
}
}
setInterval(periodicSanitize, 5000);
function getContext() {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = editor.innerHTML;
const ghost = tempDiv.querySelector('.ghost-text');
if (ghost) ghost.remove();
const fullText = tempDiv.innerText;
return fullText.slice(-1000);
}
function removeGhost() {
const ghost = editor.querySelector('.ghost-text');
if (ghost) ghost.remove();
}
function acceptGhost() {
const ghost = editor.querySelector('.ghost-text');
if (ghost) {
const text = ghost.innerText;
removeGhost();
document.execCommand('insertText', false, text);
}
}
async function fetchCompletion() {
if (isFetching) return;
const context = getContext();
const apiUrl = document.getElementById('api-url').value;
const temp = parseFloat(document.getElementById('temp').value);
const maxTokens = parseInt(document.getElementById('max-tokens').value);
isFetching = true;
infoDisplay.innerText = "状态:AI 正在思考...";
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
"messages": [{"role": "user", "content": `续写:${context}\n/no_think`}],
"temperature": temp,
"max_tokens": maxTokens
})
});
const data = await response.json();
let rawContent = data.choices[0].message.content;
const cleanContent = rawContent.replace(/<think>[\s\S]*?<\/think>/g, '').trimStart();
if (cleanContent) {
showGhost(cleanContent);
infoDisplay.innerText = "状态:已生成 (Tab 接受)";
} else {
infoDisplay.innerText = "状态:未生成有效补全";
}
} catch (error) {
infoDisplay.innerText = "状态:API 连接失败";
} finally {
isFetching = false;
}
}
function showGhost(text) {
removeGhost();
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const ghostSpan = document.createElement('span');
ghostSpan.className = 'ghost-text';
ghostSpan.innerText = text;
range.insertNode(ghostSpan);
range.setStartBefore(ghostSpan);
range.collapse(true);
}
}
editor.addEventListener('keydown', (e) => {
const hasGhost = !!editor.querySelector('.ghost-text');
if (e.key === 'Tab') {
e.preventDefault();
if (hasGhost) {
acceptGhost();
} else {
fetchCompletion();
}
return;
}
if (hasGhost && !['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) {
removeGhost();
infoDisplay.innerText = "状态:就绪";
}
});
editor.addEventListener('focus', function () {
if (this.innerText === '在这里输入故事开头...') {
this.innerText = '';
}
}, {once: true});
</script>
</body>
</html>