Upload 2 files
Browse files- index.html +164 -19
- script.js +310 -0
index.html
CHANGED
@@ -1,19 +1,164 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ja">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<title>LLM Client</title>
|
8 |
+
<link href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
10 |
+
crossorigin="anonymous">
|
11 |
+
<style>
|
12 |
+
#novelContent1,
|
13 |
+
#novelContent2 {
|
14 |
+
height: 50vh;
|
15 |
+
}
|
16 |
+
|
17 |
+
#generatePrompt,
|
18 |
+
#nextPrompt {
|
19 |
+
height: 20vh;
|
20 |
+
}
|
21 |
+
|
22 |
+
@media (max-width: 767px) {
|
23 |
+
|
24 |
+
#novelContent1,
|
25 |
+
#novelContent2,
|
26 |
+
#modal-body-1 textarea {
|
27 |
+
height: 90vh;
|
28 |
+
}
|
29 |
+
}
|
30 |
+
</style>
|
31 |
+
</head>
|
32 |
+
|
33 |
+
<body data-bs-theme="dark">
|
34 |
+
<div id="my-modal" class="modal fade" tabindex="-1" aria-labelledby="my-modalLabel" aria-hidden="true">
|
35 |
+
<div class="modal-dialog modal-lg modal-dialog-centered">
|
36 |
+
<div class="modal-content">
|
37 |
+
<div class="bg-primary modal-header">
|
38 |
+
<h5 id="modal-title-1" class="modal-title modal-text modal-text-1">API Settings</h5>
|
39 |
+
<h5 id="modal-title-2" class="modal-title modal-text modal-text-2">Utility</h5>
|
40 |
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
41 |
+
aria-label="Close"></button>
|
42 |
+
</div>
|
43 |
+
<div id="modal-body-1" class="modal-body modal-text modal-text-1">
|
44 |
+
<div class="row">
|
45 |
+
<div class="col-12">
|
46 |
+
<input type="text" class="form-control" id="endpoint" placeholder="Endpoint">
|
47 |
+
</div>
|
48 |
+
<div class="col-12">
|
49 |
+
<textarea class="form-control" id="headers" placeholder="Headers" rows="7"></textarea>
|
50 |
+
</div>
|
51 |
+
<div class="col-12">
|
52 |
+
<textarea class="form-control" id="jsonBody" placeholder="Body" rows="7"></textarea>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
<div id="modal-body-2" class="modal-body modal-text modal-text-2">
|
57 |
+
<div class="row">
|
58 |
+
<div class="col-12 mb-3">
|
59 |
+
<button id="formatTextButton" class="btn btn-primary" onclick="formatText()">
|
60 |
+
<i class="fa-solid fa-align-left"></i> 改行を整理
|
61 |
+
</button>
|
62 |
+
</div>
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
<div class="modal-footer">
|
66 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
67 |
+
</div>
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
</div>
|
71 |
+
<div class="container-fluid">
|
72 |
+
<div class="row m-0 border-start border-end border-2">
|
73 |
+
<div class="col-12">
|
74 |
+
<textarea class="form-control" id="generatePrompt" placeholder="システムプロンプトを入力"></textarea>
|
75 |
+
</div>
|
76 |
+
<div class="col-12 col-md-6">
|
77 |
+
<textarea class="form-control" id="novelContent1" placeholder="ここに小説の本文を入力してください"></textarea>
|
78 |
+
</div>
|
79 |
+
<div class="col-12 col-md-6">
|
80 |
+
<textarea class="form-control" id="novelContent2" placeholder="ここに続きが表示されます。"></textarea>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<div class="col-12">
|
84 |
+
<textarea class="form-control" id="nextPrompt" placeholder="次の展開を指示"></textarea>
|
85 |
+
</div>
|
86 |
+
<div class="col-12 col-md-6 small lh-1">
|
87 |
+
<div class="row">
|
88 |
+
<div class="col-12 col-md-2 d-flex align-items-center mb-2 mb-md-0">
|
89 |
+
<button class="btn btn-primary mx-1" data-bs-toggle="modal" data-bs-target="#my-modal"
|
90 |
+
data-modal-text="modal-text-1">
|
91 |
+
<i class="fa-solid fa-cog"></i>
|
92 |
+
</button>
|
93 |
+
<button class="btn btn-primary mx-1" data-bs-toggle="modal" data-bs-target="#my-modal"
|
94 |
+
data-modal-text="modal-text-2">
|
95 |
+
<i class="fa-solid fa-wrench"></i>
|
96 |
+
</button>
|
97 |
+
</div>
|
98 |
+
<div class="col-12 col-md-4 mb-2 mb-md-0 px-5">
|
99 |
+
<label for="characterCount" class="form-label mb-0 me-3">文字数</label>
|
100 |
+
<input data-input-group="characterCount" type="range" class="form-range me-2"
|
101 |
+
id="characterCount" min="1" max="4096" value="3000">
|
102 |
+
<input data-input-group="characterCount" type="number" class="form-control me-2"
|
103 |
+
id="characterCountInput" value="3000" min="1" max="4096">
|
104 |
+
</div>
|
105 |
+
<div class="col-12 col-md-3 mb-2 mb-md-0 px-5">
|
106 |
+
<label for="encodeLength" class="form-label mb-0 me-3">エンコード頻度</label>
|
107 |
+
<input data-input-group="encodeLength" type="range" class="form-range me-2" id="encodeLength"
|
108 |
+
min="1" max="16" value="16">
|
109 |
+
<input data-input-group="encodeLength" type="number" class="form-control me-2"
|
110 |
+
id="encodeLengthInput" min="1" max="16" value="16">
|
111 |
+
</div>
|
112 |
+
<div class="col-12 col-md-3">
|
113 |
+
<div class="form-check mb-0">
|
114 |
+
<label class="form-check-label" for="partialEncodeToggle">エンコード</label>
|
115 |
+
<input class="form-check-input" type="checkbox" id="partialEncodeToggle">
|
116 |
+
</div>
|
117 |
+
<div class="form-check mb-0">
|
118 |
+
<label class="form-check-label" for="streamToggle">stream</label>
|
119 |
+
<input class="form-check-input" type="checkbox" id="streamToggle" checked>
|
120 |
+
</div>
|
121 |
+
</div>
|
122 |
+
</div>
|
123 |
+
</div>
|
124 |
+
<script>
|
125 |
+
const inputGroups = document.querySelectorAll('[data-input-group]');
|
126 |
+
inputGroups.forEach(group => {
|
127 |
+
const inputs = document.querySelectorAll(`[data-input-group="${group.dataset.inputGroup}"]`);
|
128 |
+
|
129 |
+
inputs.forEach(input => {
|
130 |
+
input.addEventListener('input', function () {
|
131 |
+
inputs.forEach(otherInput => {
|
132 |
+
if (otherInput !== input) {
|
133 |
+
otherInput.value = this.value;
|
134 |
+
}
|
135 |
+
});
|
136 |
+
});
|
137 |
+
});
|
138 |
+
});
|
139 |
+
</script>
|
140 |
+
<div class="col-12 col-md-2 d-flex align-items-center mb-2 mb-md-0">
|
141 |
+
<button id="requestButton" class="btn btn-primary me-2" onclick="Request()">
|
142 |
+
続きを生成
|
143 |
+
</button>
|
144 |
+
<button id="stopButton" class="btn btn-danger d-none" onclick="stopGeneration()">
|
145 |
+
中止
|
146 |
+
</button>
|
147 |
+
</div>
|
148 |
+
<div class="col-12 col-md-4 d-flex align-items-center justify-content-end">
|
149 |
+
<input type="text" class="form-control me-2" id="savedTitle" placeholder="タイトル">
|
150 |
+
<button id="saveButton" class="btn btn-secondary me-2" onclick="saveToJson()">
|
151 |
+
保存
|
152 |
+
</button>
|
153 |
+
<button id="loadButton" class="btn btn-secondary" onclick="loadFromJson()">
|
154 |
+
読込
|
155 |
+
</button>
|
156 |
+
</div>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
</div>
|
160 |
+
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
161 |
+
<script src="script.js"></script>
|
162 |
+
</body>
|
163 |
+
|
164 |
+
</html>
|
script.js
ADDED
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let lastSaveTimestamp = 0;
|
2 |
+
|
3 |
+
function formatText(){
|
4 |
+
const textOrg = document.getElementById('novelContent1').value;
|
5 |
+
let text = textOrg.replace(/[」。)]/g, '$&\n');
|
6 |
+
while (text.includes('\n\n')) {
|
7 |
+
text = text.replace(/\n\n/g, '\n');
|
8 |
+
}
|
9 |
+
text = text.replace(/「([^」\n]*)\n([^」\n]*)」/g, '「$1$2」');
|
10 |
+
text = text.replace(/(([^)\n]*)\n([^)\n]*))/g, '($1$2)');
|
11 |
+
|
12 |
+
while (text.search(/「[^「\n]*。\n/) >= 0) {
|
13 |
+
text = text.replace(/「([^「\n]*。)\n/, '「$1');
|
14 |
+
}
|
15 |
+
|
16 |
+
text = text.replace(/\n/g, "\n\n");
|
17 |
+
text = text.replace(/\n#/g, "\n\n#");
|
18 |
+
|
19 |
+
document.getElementById('novelContent1').value = text;
|
20 |
+
}
|
21 |
+
|
22 |
+
function unmalform(text) {
|
23 |
+
let result = null;
|
24 |
+
while (!result && text) {
|
25 |
+
try {
|
26 |
+
result = decodeURI(text);
|
27 |
+
} catch (error) {
|
28 |
+
text = text.slice(0, -1);
|
29 |
+
}
|
30 |
+
}
|
31 |
+
return result || '';
|
32 |
+
}
|
33 |
+
|
34 |
+
function partialEncodeURI(text) {
|
35 |
+
if (!document.getElementById("partialEncodeToggle").checked) {
|
36 |
+
return text;
|
37 |
+
}
|
38 |
+
let length = document.getElementById("encodeLength").value;
|
39 |
+
const chunks = [];
|
40 |
+
for (let i = 0; i < text.length; i += 1) {
|
41 |
+
chunks.push(text.slice(i, i + 1));
|
42 |
+
}
|
43 |
+
const encodedChunks = chunks.map((chunk, index) => {
|
44 |
+
if (index % length === 0) {
|
45 |
+
return encodeURI(chunk);
|
46 |
+
}
|
47 |
+
return chunk;
|
48 |
+
});
|
49 |
+
return encodedChunks.join('');
|
50 |
+
}
|
51 |
+
|
52 |
+
function saveToJson() {
|
53 |
+
const novelContent1 = document.getElementById('novelContent1').value;
|
54 |
+
const novelContent2 = document.getElementById('novelContent2').value;
|
55 |
+
const generatePrompt = document.getElementById('generatePrompt').value;
|
56 |
+
const nextPrompt = document.getElementById('nextPrompt').value;
|
57 |
+
const savedTitle = document.getElementById('savedTitle').value;
|
58 |
+
const jsonData = JSON.stringify({
|
59 |
+
novelContent1: novelContent1,
|
60 |
+
novelContent2: novelContent2,
|
61 |
+
generatePrompt: generatePrompt,
|
62 |
+
nextPrompt: nextPrompt,
|
63 |
+
savedTitle: savedTitle
|
64 |
+
});
|
65 |
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
66 |
+
const url = URL.createObjectURL(blob);
|
67 |
+
const a = document.createElement('a');
|
68 |
+
a.href = url;
|
69 |
+
a.download = 'novel_data.json';
|
70 |
+
if (savedTitle) {
|
71 |
+
a.download = savedTitle + '.json';
|
72 |
+
}
|
73 |
+
document.body.appendChild(a);
|
74 |
+
a.click();
|
75 |
+
document.body.removeChild(a);
|
76 |
+
URL.revokeObjectURL(url);
|
77 |
+
}
|
78 |
+
|
79 |
+
function loadFromJson() {
|
80 |
+
const fileInput = document.createElement('input');
|
81 |
+
fileInput.type = 'file';
|
82 |
+
fileInput.accept = '.json';
|
83 |
+
fileInput.style.display = 'none';
|
84 |
+
document.body.appendChild(fileInput);
|
85 |
+
fileInput.addEventListener('change', function (event) {
|
86 |
+
const file = event.target.files[0];
|
87 |
+
if (file) {
|
88 |
+
const reader = new FileReader();
|
89 |
+
reader.onload = function (e) {
|
90 |
+
try {
|
91 |
+
const jsonData = JSON.parse(e.target.result);
|
92 |
+
if (jsonData.novelContent1) {
|
93 |
+
document.getElementById('novelContent1').value = jsonData.novelContent1;
|
94 |
+
}
|
95 |
+
if (jsonData.novelContent2) {
|
96 |
+
document.getElementById('novelContent2').value = jsonData.novelContent2;
|
97 |
+
}
|
98 |
+
if (jsonData.generatePrompt) {
|
99 |
+
document.getElementById('generatePrompt').value = jsonData.generatePrompt;
|
100 |
+
}
|
101 |
+
if (jsonData.nextPrompt) {
|
102 |
+
document.getElementById('nextPrompt').value = jsonData.nextPrompt;
|
103 |
+
}
|
104 |
+
if (jsonData.savedTitle) {
|
105 |
+
document.getElementById('savedTitle').value = jsonData.savedTitle;
|
106 |
+
}
|
107 |
+
alert('JSONファイルを正常に読み込みました。');
|
108 |
+
} catch (error) {
|
109 |
+
alert('無効なJSONファイルです。');
|
110 |
+
}
|
111 |
+
};
|
112 |
+
reader.readAsText(file);
|
113 |
+
}
|
114 |
+
});
|
115 |
+
fileInput.click();
|
116 |
+
}
|
117 |
+
|
118 |
+
function saveToUserStorage(force = false) {
|
119 |
+
const currentTime = Date.now();
|
120 |
+
if (!force && currentTime - lastSaveTimestamp < 5000) {
|
121 |
+
return;
|
122 |
+
}
|
123 |
+
const content = document.getElementById('novelContent1').value;
|
124 |
+
const prompt = document.getElementById('generatePrompt').value;
|
125 |
+
const nextPrompt = document.getElementById('nextPrompt').value;
|
126 |
+
const savedTitle = document.getElementById('savedTitle').value;
|
127 |
+
localStorage.setItem('savedNovelContent', content);
|
128 |
+
localStorage.setItem('savedGeneratePrompt', prompt);
|
129 |
+
localStorage.setItem('savedNextPrompt', nextPrompt);
|
130 |
+
['endpoint', 'headers', 'jsonBody'].forEach(id => {
|
131 |
+
localStorage.setItem(`saved${id}`, document.getElementById(id).value);
|
132 |
+
});
|
133 |
+
if (savedTitle) {
|
134 |
+
localStorage.setItem('savedTitle', savedTitle);
|
135 |
+
}
|
136 |
+
lastSaveTimestamp = currentTime;
|
137 |
+
}
|
138 |
+
|
139 |
+
// 60秒ごとに自動保存を実行
|
140 |
+
setInterval(() => {
|
141 |
+
saveToUserStorage();
|
142 |
+
}, 60000);
|
143 |
+
const savedContent = localStorage.getItem('savedNovelContent');
|
144 |
+
const savedPrompt = localStorage.getItem('savedGeneratePrompt');
|
145 |
+
const savedNextPrompt = localStorage.getItem('savedNextPrompt');
|
146 |
+
const savedTitle = localStorage.getItem('savedTitle');
|
147 |
+
if (savedContent) {
|
148 |
+
document.getElementById('novelContent1').value = savedContent;
|
149 |
+
}
|
150 |
+
if (savedPrompt) {
|
151 |
+
document.getElementById('generatePrompt').value = savedPrompt;
|
152 |
+
}
|
153 |
+
if (savedNextPrompt) {
|
154 |
+
document.getElementById('nextPrompt').value = savedNextPrompt;
|
155 |
+
}
|
156 |
+
if (savedTitle) {
|
157 |
+
document.getElementById('savedTitle').value = savedTitle;
|
158 |
+
}
|
159 |
+
['endpoint', 'headers', 'jsonBody'].forEach(id => {
|
160 |
+
document.getElementById(id).value = localStorage.getItem(`saved${id}`);
|
161 |
+
});
|
162 |
+
|
163 |
+
|
164 |
+
['novelContent1', 'generatePrompt', 'nextPrompt'].forEach(id => {
|
165 |
+
document.getElementById(id).addEventListener('input', saveToUserStorage);
|
166 |
+
});
|
167 |
+
['endpoint', 'headers', 'jsonBody'].forEach(id => {
|
168 |
+
document.getElementById(id).addEventListener('input', () => {
|
169 |
+
saveToUserStorage(true);
|
170 |
+
});
|
171 |
+
});
|
172 |
+
|
173 |
+
document.querySelectorAll('[data-modal-text]').forEach(element => {
|
174 |
+
element.addEventListener('click', function() {
|
175 |
+
document.querySelectorAll(".modal-text").forEach(el => {
|
176 |
+
el.classList.add("d-none");
|
177 |
+
if(el.classList.contains(this.getAttribute('data-modal-text'))) {
|
178 |
+
el.classList.remove("d-none");
|
179 |
+
}
|
180 |
+
});
|
181 |
+
|
182 |
+
|
183 |
+
});
|
184 |
+
});
|
185 |
+
|
186 |
+
|
187 |
+
let controller;
|
188 |
+
function Request() {
|
189 |
+
let ENDPOINT;
|
190 |
+
let HEADERS;
|
191 |
+
let jsonBody;
|
192 |
+
try {
|
193 |
+
ENDPOINT = document.getElementById("endpoint").value;
|
194 |
+
HEADERS = JSON.parse(document.getElementById("headers").value);
|
195 |
+
jsonBody = JSON.parse(document.getElementById("jsonBody").value);
|
196 |
+
if (!ENDPOINT) {
|
197 |
+
throw new Error("エンドポイントが設定されていません。");
|
198 |
+
}
|
199 |
+
} catch (e) {
|
200 |
+
console.error(e);
|
201 |
+
document.querySelector(".modal-header").classList.add("bg-danger");
|
202 |
+
document.querySelector('[data-modal-text="modal-text-1"]').click();
|
203 |
+
return;
|
204 |
+
}
|
205 |
+
document.querySelector(".modal-header").classList.remove("bg-danger");
|
206 |
+
|
207 |
+
|
208 |
+
const novelContent1 = document.getElementById('novelContent1');
|
209 |
+
const novelContent2 = document.getElementById('novelContent2');
|
210 |
+
const text = novelContent1.value;
|
211 |
+
const lines = text.split('\n').filter(x => x);
|
212 |
+
let lastPart = lines.pop() || '';
|
213 |
+
let removedPart;
|
214 |
+
let messages = [
|
215 |
+
{
|
216 |
+
"content": document.getElementById('generatePrompt').value || ".",
|
217 |
+
"role": "system"
|
218 |
+
},
|
219 |
+
{
|
220 |
+
"content": ".",
|
221 |
+
"role": "user"
|
222 |
+
},
|
223 |
+
{
|
224 |
+
"content": partialEncodeURI(lines.join("\n")) || ".",
|
225 |
+
"role": "assistant"
|
226 |
+
}
|
227 |
+
];
|
228 |
+
messages = messages.concat([
|
229 |
+
{
|
230 |
+
"content": `続きを${document.getElementById('characterCountInput').value}文字程度で書いてください。${partialEncodeURI(nextPrompt.value)}`,
|
231 |
+
"role": "user"
|
232 |
+
},
|
233 |
+
{
|
234 |
+
"content": lastPart,
|
235 |
+
"role": "assistant"
|
236 |
+
}
|
237 |
+
]);
|
238 |
+
document.getElementById('requestButton').disabled = true;
|
239 |
+
let stream = document.getElementById('streamToggle').checked;
|
240 |
+
jsonBody.messages = messages;
|
241 |
+
jsonBody.stream = stream;
|
242 |
+
let payload = {
|
243 |
+
method: 'POST',
|
244 |
+
headers: HEADERS,
|
245 |
+
body: JSON.stringify(jsonBody)
|
246 |
+
}
|
247 |
+
if (stream === true) {
|
248 |
+
controller = new AbortController();
|
249 |
+
payload.signal = controller.signal;
|
250 |
+
}
|
251 |
+
console.debug(messages, JSON.stringify(messages).length);
|
252 |
+
fetch(ENDPOINT, payload)
|
253 |
+
.then(response => {
|
254 |
+
if (stream === true) {
|
255 |
+
const reader = response.body.getReader();
|
256 |
+
const decoder = new TextDecoder();
|
257 |
+
let buffer = '';
|
258 |
+
function readStream() {
|
259 |
+
reader.read().then(({ done, value }) => {
|
260 |
+
if (done) {
|
261 |
+
document.getElementById('stopButton').classList.add('d-none');
|
262 |
+
return;
|
263 |
+
}
|
264 |
+
buffer += decoder.decode(value, { stream: true });
|
265 |
+
const lines = buffer.split('\n');
|
266 |
+
buffer = lines.pop();
|
267 |
+
lines.forEach(line => {
|
268 |
+
if (line.startsWith('data: ')) {
|
269 |
+
const data = JSON.parse(line.slice(6));
|
270 |
+
if (data.choices && data.choices[0].delta.content) {
|
271 |
+
novelContent2.value += data.choices[0].delta.content;
|
272 |
+
novelContent2.scrollTop = novelContent2.scrollHeight;
|
273 |
+
}
|
274 |
+
}
|
275 |
+
});
|
276 |
+
readStream();
|
277 |
+
});
|
278 |
+
}
|
279 |
+
readStream();
|
280 |
+
} else {
|
281 |
+
return response.json();
|
282 |
+
}
|
283 |
+
})
|
284 |
+
.then(data => {
|
285 |
+
if (data) {
|
286 |
+
novelContent2.value += data.choices[0].message.content;
|
287 |
+
}
|
288 |
+
})
|
289 |
+
.catch(error => {
|
290 |
+
if (error.name === 'AbortError') {
|
291 |
+
console.debug('生成が中止されました');
|
292 |
+
} else {
|
293 |
+
console.error('エラー:', error);
|
294 |
+
}
|
295 |
+
})
|
296 |
+
.finally(() => {
|
297 |
+
document.getElementById('requestButton').disabled = false;
|
298 |
+
});
|
299 |
+
if (stream === true) {
|
300 |
+
document.getElementById('stopButton').classList.remove('d-none');
|
301 |
+
}
|
302 |
+
novelContent2.value = '';
|
303 |
+
}
|
304 |
+
function stopGeneration() {
|
305 |
+
if (controller) {
|
306 |
+
controller.abort();
|
307 |
+
controller = null;
|
308 |
+
}
|
309 |
+
document.getElementById('stopButton').classList.add('d-none');
|
310 |
+
}
|