|
|
<!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 {
|
|
|
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> |