hisaruki commited on
Commit
c8e4e29
·
0 Parent(s):

叩きは完成

Browse files
Files changed (3) hide show
  1. app.py +63 -0
  2. gemini.html +220 -0
  3. gemini.js +556 -0
app.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from fastapi import FastAPI, Body, Request, Depends, HTTPException
5
+ from fastapi.responses import HTMLResponse, ORJSONResponse, FileResponse
6
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials
7
+ from starlette.status import HTTP_401_UNAUTHORIZED
8
+ from fastapi.responses import StreamingResponse
9
+ import collections
10
+ if not hasattr(collections, "MutableSet"):
11
+ collections.MutableSet = collections.abc.MutableSet
12
+ if not hasattr(collections, "MutableMapping"):
13
+ collections.MutableMapping = collections.abc.MutableMapping
14
+ import httpx
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ app = FastAPI()
20
+
21
+ from starlette.middleware.cors import CORSMiddleware
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["https://novelai.net", "https://hisaruki.ddns.net"],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"]
28
+ )
29
+
30
+ security = HTTPBasic()
31
+
32
+ def authenticate(credentials: HTTPBasicCredentials = Depends(security)):
33
+ correct_username = "administrator"
34
+ correct_password = "X2fK9pL7mR3qZ8vY"
35
+ if not (credentials.username == correct_username and credentials.password == correct_password):
36
+ raise HTTPException(
37
+ status_code=HTTP_401_UNAUTHORIZED,
38
+ detail="認証に失敗しました",
39
+ headers={"WWW-Authenticate": "Basic"},
40
+ )
41
+ return credentials.username
42
+ @app.get("/authtest", dependencies=[Depends(authenticate)])
43
+ async def eval(request: Request, body: dict = Body):
44
+ return HTMLResponse(content="ok.", media_type="text/plain")
45
+
46
+
47
+ @app.post("/unify/chat/completions")
48
+ async def unify_chat_completions(request: Request, body: dict = Body(...)):
49
+ headers = {
50
+ "Authorization": request.headers["authorization"],
51
+ "Content-Type": "application/json"
52
+ }
53
+ endpoint = "https://api.unify.ai/v0/chat/completions"
54
+ logger.debug(body)
55
+ async def stream_response():
56
+ async with httpx.AsyncClient(timeout=600) as client:
57
+ async with client.stream("POST", endpoint, json=body, headers=headers) as response:
58
+ if response.status_code != 200:
59
+ raise HTTPException(status_code=response.status_code, detail=response.text)
60
+ async for chunk in response.aiter_bytes():
61
+ yield chunk
62
+
63
+ return StreamingResponse(stream_response(), media_type="text/event-stream")
gemini.html ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ textarea.form-control {
13
+ min-height: 50vh;
14
+ }
15
+ .accordion-button:not(.collapsed) {
16
+ background-color: #212529;
17
+ color: #fff;
18
+ }
19
+ .navbar-toggler {
20
+ display: block !important;
21
+ }
22
+ @keyframes redFlashBg {
23
+ 0%, 100% { background-color: initial; }
24
+ 50% { background-color: rgba(255, 0, 0, 0.5); }
25
+ }
26
+
27
+ @keyframes redFlashFg {
28
+ 0%, 100% { color: initial; }
29
+ 50% { color: red; }
30
+ }
31
+
32
+ .red-flash-bg {
33
+ animation: redFlashBg 0.5s infinite;
34
+ }
35
+
36
+ .red-flash-fg {
37
+ animation: redFlashFg 0.5s infinite;
38
+ }
39
+
40
+ @keyframes greenFlashBg {
41
+ 0%, 100% { background-color: initial; }
42
+ 50% { background-color: rgba(0, 255, 0, 0.5); }
43
+ }
44
+
45
+ @keyframes greenFlashFg {
46
+ 0%, 100% { color: initial; }
47
+ 50% { color: green; }
48
+ }
49
+
50
+ .green-flash-bg {
51
+ animation: greenFlashBg 0.5s infinite;
52
+ }
53
+
54
+ .green-flash-fg {
55
+ animation: greenFlashFg 0.5s infinite;
56
+ }
57
+
58
+ /* 新しいスタイルを追加 */
59
+ .navbar {
60
+ position: sticky;
61
+ top: 0;
62
+ z-index: 1000;
63
+ }
64
+
65
+ /* メインコンテンツの上部にパディングを追加 */
66
+ #mainContent {
67
+ padding-top: 1rem;
68
+ }
69
+ </style>
70
+ </head>
71
+
72
+ <body data-bs-theme="dark">
73
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
74
+ <div class="container">
75
+ <div class="row justify-content-center w-100">
76
+ <div class="col-lg-7 d-flex justify-content-between align-items-center">
77
+ <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#settingsOffcanvas" aria-controls="settingsOffcanvas">
78
+ <span class="navbar-toggler-icon"></span>
79
+ </button>
80
+ <button id="prevAccordion" class="btn btn-sm btn-outline-light me-2">
81
+ <i class="fas fa-chevron-left"></i> 前へ
82
+ </button>
83
+ <button id="nextAccordion" class="btn btn-sm btn-outline-light me-2">
84
+ 次へ <i class="fas fa-chevron-right"></i>
85
+ </button>
86
+ <div class="input-group" style="max-width: 5rem;">
87
+ <input type="range" class="form-range" id="encodeLength" placeholder="エンコード長" min="1" max="16" value="4">
88
+ <input type="number" class="form-control" id="encodeLengthInput" placeholder="エンコード長" min="1" max="16" value="4">
89
+ </div>
90
+ <div class="form-check form-switch">
91
+ <input class="form-check-input" type="checkbox" id="partialEncodeToggle">
92
+ <label class="form-check-label" for="partialEncodeToggle">部分エンコード</label>
93
+ </div>
94
+ <button id="requestButton" class="btn btn-sm btn-primary" onclick="Request()">
95
+ 続きを生成
96
+ </button>
97
+ <button id="stopButton" class="btn btn-sm btn-danger d-none" onclick="stopGeneration()">
98
+ 中止
99
+ </button>
100
+ <a class="navbar-brand" href="#">LLM Client</a>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </nav>
105
+
106
+ <div class="container">
107
+ <div class="row justify-content-center">
108
+ <div id="mainContent" class="col-lg-7">
109
+ <div class="mt-3">
110
+ <div class="accordion" id="mainAccordion">
111
+ <div class="accordion-item">
112
+ <h2 class="accordion-header">
113
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#promptsCollapse">
114
+ 基本設定
115
+ </button>
116
+ </h2>
117
+ <div id="promptsCollapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
118
+ <div class="accordion-body">
119
+ <textarea class="form-control mb-2" id="generatePrompt" placeholder="システムプロンプトを入力"></textarea>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ <div class="accordion-item">
124
+ <h2 class="accordion-header">
125
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#content1Collapse">
126
+ 小説内容 (入力)
127
+ </button>
128
+ </h2>
129
+ <div id="content1Collapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
130
+ <div class="accordion-body">
131
+ <textarea class="form-control mb-2" id="novelContent1" placeholder="ここに小説の本文を入力してください"></textarea>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <div class="accordion-item">
136
+ <h2 class="accordion-header">
137
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#nextPromptCollapse">
138
+ 次の展開
139
+ </button>
140
+ </h2>
141
+ <div id="nextPromptCollapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
142
+ <div class="accordion-body">
143
+ <div class="d-flex justify-content-between align-items-center">
144
+ <textarea class="form-control me-2" id="nextPrompt" placeholder="次の展開を指示" rows="1"></textarea>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ <div class="accordion-item">
150
+ <h2 class="accordion-header">
151
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#content2Collapse">
152
+ 小説内容 (出力)
153
+ </button>
154
+ </h2>
155
+ <div id="content2Collapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
156
+ <div class="accordion-body">
157
+ <textarea class="form-control" id="novelContent2" placeholder="ここに続きが表示されます。"></textarea>
158
+ <button id="moveToInputButton" class="btn btn-primary mt-2" onclick="moveToInput()">
159
+ 入力欄へ移動
160
+ </button>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <div class="row mt-3">
167
+ <div class="col-12 d-flex justify-content-end">
168
+ <input type="text" class="form-control d-inline-block w-auto me-2" id="savedTitle" placeholder="タイトル">
169
+ <button id="saveButton" class="btn btn-secondary me-2" onclick="saveToJson()">
170
+ 保存
171
+ </button>
172
+ <button id="loadButton" class="btn btn-secondary" onclick="loadFromJson()">
173
+ 読込
174
+ </button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="offcanvas offcanvas-start" tabindex="-1" id="settingsOffcanvas" aria-labelledby="settingsOffcanvasLabel">
183
+ <div class="offcanvas-header">
184
+ <h5 class="offcanvas-title" id="settingsOffcanvasLabel">設定</h5>
185
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
186
+ </div>
187
+ <div class="offcanvas-body">
188
+ <h5>Gemini API Key</h5>
189
+ <input type="text" class="form-control mb-2" id="geminiApiKey" placeholder="Gemini API Key">
190
+
191
+ <h5>エンドポイント</h5>
192
+ <select class="form-select mb-2" id="endpointSelect">
193
+ <option value="models/gemini-1.5-pro-002">gemini-1.5-pro-002</option>
194
+ <option value="models/gemini-1.5-flash-002">gemini-1.5-flash-002</option>
195
+ <option value="models/gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
196
+ <option value="models/gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
197
+ </select>
198
+
199
+ <div class="mb-2">
200
+ <label for="characterCount" class="form-label">文字数</label>
201
+ <input type="range" class="form-range" id="characterCount" min="64" max="8192" value="4096" step="64">
202
+ <input type="number" class="form-control" id="characterCountInput" value="4096" min="64" max="8192" step="64">
203
+ </div>
204
+ <div class="form-check mb-2">
205
+ <input class="form-check-input" type="checkbox" id="streamToggle" checked>
206
+ <label class="form-check-label" for="streamToggle">Stream</label>
207
+ </div>
208
+ <button id="formatTextButton" class="btn btn-primary mb-2" onclick="formatText()">
209
+ <i class="fa-solid fa-align-left"></i> 改行を整理
210
+ </button>
211
+ <h5 class="mt-4">メモ</h5>
212
+ <textarea class="form-control" id="memo" placeholder="メモを記録する項目" rows="10"></textarea>
213
+ </div>
214
+ </div>
215
+
216
+ <script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
217
+ <script src="gemini.js"></script>
218
+ </body>
219
+
220
+ </html>
gemini.js ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let lastSaveTimestamp = 0;
2
+ let controller;
3
+
4
+ function formatText() {
5
+ const textOrg = document.getElementById('novelContent1').value;
6
+ let text = textOrg.replace(/[」。)]/g, '$&\n');
7
+ while (text.includes('\n\n')) {
8
+ text = text.replace(/\n\n/g, '\n');
9
+ }
10
+ text = text.replace(/「([^」\n]*)\n([^」\n]*)」/g, '「$1$2」');
11
+ text = text.replace(/(([^)\n]*)\n([^)\n]*))/g, '($1$2)');
12
+
13
+ while (text.search(/「[^「\n]*。\n/) >= 0) {
14
+ text = text.replace(/「([^「\n]*。)\n/, '「$1');
15
+ }
16
+
17
+ text = text.replace(/\n/g, "\n\n");
18
+ text = text.replace(/\n#/g, "\n\n#");
19
+
20
+ document.getElementById('novelContent1').value = text;
21
+ }
22
+
23
+ function unmalform(text) {
24
+ let result = null;
25
+ while (!result && text) {
26
+ try {
27
+ result = decodeURI(text);
28
+ } catch (error) {
29
+ text = text.slice(0, -1);
30
+ }
31
+ }
32
+ return result || '';
33
+ }
34
+
35
+ function partialEncodeURI(text) {
36
+ console.log('partialEncodeURI called');
37
+ console.log('partialEncodeToggle checked:', document.getElementById("partialEncodeToggle").checked);
38
+
39
+ if (!document.getElementById("partialEncodeToggle").checked) {
40
+ console.log('Partial encode is disabled, returning original text');
41
+ return text;
42
+ }
43
+
44
+ let length = parseInt(document.getElementById("encodeLength").value);
45
+ console.log('Encode length:', length);
46
+
47
+ const chunks = [];
48
+ for (let i = 0; i < text.length; i += 1) {
49
+ chunks.push(text.slice(i, i + 1));
50
+ }
51
+
52
+ console.log('Original text:', text);
53
+
54
+ const encodedChunks = chunks.map((chunk, index) => {
55
+ if (index % length === 0) {
56
+ console.log('Encoding chunk at index', index, ':', chunk);
57
+ return encodeURI(chunk);
58
+ }
59
+ return chunk;
60
+ });
61
+
62
+ const result = encodedChunks.join('');
63
+ console.log('Encoded text:', result);
64
+
65
+ return result;
66
+ }
67
+
68
+ function saveToJson() {
69
+ const novelContent1 = document.getElementById('novelContent1').value;
70
+ const novelContent2 = document.getElementById('novelContent2').value;
71
+ const generatePrompt = document.getElementById('generatePrompt').value;
72
+ const nextPrompt = document.getElementById('nextPrompt').value;
73
+ const savedTitle = document.getElementById('savedTitle').value;
74
+ const jsonData = JSON.stringify({
75
+ novelContent1: novelContent1,
76
+ novelContent2: novelContent2,
77
+ generatePrompt: generatePrompt,
78
+ nextPrompt: nextPrompt,
79
+ savedTitle: savedTitle
80
+ });
81
+ const blob = new Blob([jsonData], { type: 'application/json' });
82
+ const url = URL.createObjectURL(blob);
83
+ const a = document.createElement('a');
84
+ a.href = url;
85
+ a.download = 'novel_data.json';
86
+ if (savedTitle) {
87
+ a.download = savedTitle + '.json';
88
+ }
89
+ document.body.appendChild(a);
90
+ a.click();
91
+ document.body.removeChild(a);
92
+ URL.revokeObjectURL(url);
93
+ }
94
+
95
+ function loadFromJson() {
96
+ const fileInput = document.createElement('input');
97
+ fileInput.type = 'file';
98
+ fileInput.accept = '.json';
99
+ fileInput.style.display = 'none';
100
+ document.body.appendChild(fileInput);
101
+ fileInput.addEventListener('change', function (event) {
102
+ const file = event.target.files[0];
103
+ if (file) {
104
+ const reader = new FileReader();
105
+ reader.onload = function (e) {
106
+ try {
107
+ const jsonData = JSON.parse(e.target.result);
108
+ if (jsonData.novelContent1) {
109
+ document.getElementById('novelContent1').value = jsonData.novelContent1;
110
+ }
111
+ if (jsonData.novelContent2) {
112
+ document.getElementById('novelContent2').value = jsonData.novelContent2;
113
+ }
114
+ if (jsonData.generatePrompt) {
115
+ document.getElementById('generatePrompt').value = jsonData.generatePrompt;
116
+ }
117
+ if (jsonData.nextPrompt) {
118
+ document.getElementById('nextPrompt').value = jsonData.nextPrompt;
119
+ }
120
+ if (jsonData.savedTitle) {
121
+ document.getElementById('savedTitle').value = jsonData.savedTitle;
122
+ }
123
+ alert('JSONファイルを正常に読み込みました。');
124
+ } catch (error) {
125
+ alert('無効なJSONファイルです。');
126
+ }
127
+ };
128
+ reader.readAsText(file);
129
+ }
130
+ });
131
+ fileInput.click();
132
+ }
133
+
134
+ function saveToUserStorage(force = false) {
135
+ const currentTime = Date.now();
136
+ if (!force && currentTime - lastSaveTimestamp < 5000) {
137
+ return;
138
+ }
139
+
140
+ const geminiClientData = {
141
+ novelContent: document.getElementById('novelContent1').value,
142
+ novelContent2: document.getElementById('novelContent2').value,
143
+ generatePrompt: document.getElementById('generatePrompt').value,
144
+ nextPrompt: document.getElementById('nextPrompt').value,
145
+ savedTitle: document.getElementById('savedTitle').value,
146
+ memo: document.getElementById('memo').value,
147
+ geminiApiKey: document.getElementById('geminiApiKey').value,
148
+ selectedEndpoint: document.getElementById('endpointSelect').value
149
+ };
150
+
151
+ localStorage.setItem('geminiClient', JSON.stringify(geminiClientData));
152
+ lastSaveTimestamp = currentTime;
153
+ }
154
+
155
+ function loadFromUserStorage() {
156
+ const savedData = localStorage.getItem('geminiClient');
157
+ if (savedData) {
158
+ const geminiClientData = JSON.parse(savedData);
159
+ document.getElementById('novelContent1').value = geminiClientData.novelContent || '';
160
+ document.getElementById('novelContent2').value = geminiClientData.novelContent2 || '';
161
+ document.getElementById('generatePrompt').value = geminiClientData.generatePrompt || '';
162
+ document.getElementById('nextPrompt').value = geminiClientData.nextPrompt || '';
163
+ document.getElementById('savedTitle').value = geminiClientData.savedTitle || '';
164
+ document.getElementById('memo').value = geminiClientData.memo || '';
165
+ document.getElementById('geminiApiKey').value = geminiClientData.geminiApiKey || '';
166
+ document.getElementById('endpointSelect').value = geminiClientData.selectedEndpoint || 'models/gemini-1.5-pro-002';
167
+ }
168
+ }
169
+
170
+ function createPayload() {
171
+ const novelContent1 = document.getElementById('novelContent1');
172
+ const text = novelContent1.value;
173
+ const lines = text.split('\n').filter(x => x);
174
+ let lastPart = lines.pop() || '';
175
+
176
+
177
+ let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
178
+ let messages = [
179
+ {
180
+ "role": "user",
181
+ "parts": [{ "text": systemPrompt || "." }]
182
+ }
183
+ ];
184
+
185
+ if (lines.length > 0) {
186
+ messages.push(
187
+ {
188
+ "role": "model",
189
+ "parts": [{ "text": partialEncodeURI(lines.join("\n")) }]
190
+ },
191
+ {
192
+ "role": "user",
193
+ "parts": [{ "text": "続きを書いて。" }]
194
+ }
195
+ );
196
+ }
197
+
198
+ messages.push(
199
+ {
200
+ "role": "model",
201
+ "parts": [{ "text": lastPart }]
202
+ },
203
+ {
204
+ "role": "user",
205
+ "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。` }]
206
+ }
207
+ );
208
+
209
+ return {
210
+ method: 'POST',
211
+ headers: {},
212
+ body: JSON.stringify({
213
+ contents: messages,
214
+ "generationConfig": {
215
+ "temperature": 1.0,
216
+ "max_output_tokens": 4096
217
+ },
218
+ safetySettings: [
219
+ {
220
+ "category": "HARM_CATEGORY_HATE_SPEECH",
221
+ "threshold": "BLOCK_NONE"
222
+ },
223
+ {
224
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
225
+ "threshold": "BLOCK_NONE"
226
+ },
227
+ {
228
+ "category": "HARM_CATEGORY_HARASSMENT",
229
+ "threshold": "BLOCK_NONE"
230
+ },
231
+ {
232
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
233
+ "threshold": "BLOCK_NONE"
234
+ }
235
+ ]
236
+ }),
237
+ mode: 'cors'
238
+ };
239
+ }
240
+
241
+ function fetchStream(ENDPOINT, payload) {
242
+ const novelContent2 = document.getElementById('novelContent2');
243
+ const requestButton = document.getElementById('requestButton');
244
+ controller = new AbortController();
245
+ const signal = controller.signal;
246
+
247
+ fetch(ENDPOINT, { ...payload, signal })
248
+ .then(response => {
249
+ if (!response.ok) {
250
+ throw new Error('ネットワークの応答が正常ではありません');
251
+ }
252
+ const reader = response.body.getReader();
253
+ const decoder = new TextDecoder();
254
+ let buffer = '';
255
+
256
+ function readStream() {
257
+ reader.read().then(({ done, value }) => {
258
+ if (done) {
259
+ console.debug('ストリームが完了しました');
260
+ document.getElementById('stopButton').classList.add('d-none');
261
+ requestButton.disabled = false;
262
+ return;
263
+ }
264
+
265
+ const chunk = decoder.decode(value, { stream: true });
266
+ buffer += chunk;
267
+ console.debug('チャンクを受信しました:', chunk);
268
+
269
+ // バッファから完全なJSONオブジェクトを抽出して処理
270
+ let startIndex = 0;
271
+ while (true) {
272
+ const endIndex = buffer.indexOf('\n', startIndex);
273
+ if (endIndex === -1) break;
274
+
275
+ const line = buffer.slice(startIndex, endIndex).trim();
276
+ startIndex = endIndex + 1;
277
+
278
+ if (line.startsWith('data: ')) {
279
+ const jsonString = line.slice(5);
280
+ if (jsonString === '[DONE]') {
281
+ console.debug('Received [DONE] signal');
282
+ break;
283
+ }
284
+ try {
285
+ const data = JSON.parse(jsonString);
286
+ console.debug('Parsed JSON:', data);
287
+ if (data.candidates && data.candidates[0].content && data.candidates[0].content.parts) {
288
+ data.candidates[0].content.parts.forEach(part => {
289
+ if (part.text) {
290
+ console.debug('Adding text to output:', part.text);
291
+ novelContent2.value += part.text;
292
+ novelContent2.scrollTop = novelContent2.scrollHeight;
293
+ }
294
+ });
295
+ }
296
+ // finishReasonをチェック
297
+ if (data.candidates && data.candidates[0].finishReason) {
298
+ if (data.candidates[0].finishReason === 'STOP') {
299
+ requestButton.classList.add('green-flash-bg');
300
+ setTimeout(() => {
301
+ requestButton.classList.remove('green-flash-bg');
302
+ }, 2000);
303
+ } else {
304
+ requestButton.classList.add('red-flash-bg');
305
+ setTimeout(() => {
306
+ requestButton.classList.remove('red-flash-bg');
307
+ }, 2000);
308
+ }
309
+ }
310
+ } catch (error) {
311
+ console.error('JSONパースエラー:', error);
312
+ }
313
+ }
314
+ }
315
+
316
+ // 処理済みの部分をバッファから削除
317
+ buffer = buffer.slice(startIndex);
318
+
319
+ readStream();
320
+ }).catch(error => {
321
+ if (error.name === 'AbortError') {
322
+ console.log('フェッチがユーザーによって中止されました');
323
+ } else {
324
+ console.error('ストリーム読み取りエラー:', error);
325
+ }
326
+ document.getElementById('stopButton').classList.add('d-none');
327
+ requestButton.disabled = false;
328
+ });
329
+ }
330
+
331
+ readStream();
332
+ })
333
+ .catch(error => {
334
+ if (error.name === 'AbortError') {
335
+ console.log('フェッチがユーザーよって中止されました');
336
+ } else {
337
+ console.error('フェッチエラー:', error);
338
+ }
339
+ requestButton.disabled = false;
340
+ });
341
+ }
342
+
343
+ function fetchNonStream(ENDPOINT, payload) {
344
+ const novelContent2 = document.getElementById('novelContent2');
345
+ fetch(ENDPOINT, payload)
346
+ .then(response => response.json())
347
+ .then(data => {
348
+ if (data && data.candidates && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts[0].text) {
349
+ novelContent2.value += data.candidates[0].content.parts[0].text;
350
+ novelContent2.scrollTop = novelContent2.scrollHeight;
351
+ }
352
+ })
353
+ .catch(error => {
354
+ console.error('エラー:', error);
355
+ })
356
+ .finally(() => {
357
+ document.getElementById('requestButton').disabled = false;
358
+ });
359
+ }
360
+
361
+ function Request() {
362
+ const selectedEndpoint = document.getElementById('endpointSelect').value;
363
+ let ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value;
364
+ try {
365
+ if (!document.getElementById('geminiApiKey').value) {
366
+ throw new Error("Gemini APIキーが設定されていません。");
367
+ }
368
+ } catch (e) {
369
+ console.error(e);
370
+ document.getElementById("geminiApiKey").classList.add("bg-danger");
371
+ document.querySelector('[data-bs-toggle="offcanvas"]').click();
372
+ return;
373
+ }
374
+ document.getElementById("geminiApiKey").classList.remove("bg-danger");
375
+
376
+ document.getElementById('requestButton').disabled = true;
377
+ let stream = document.getElementById('streamToggle').checked;
378
+ document.getElementById('novelContent2').value = ''; // 新しい生成を開始する前に内容をクリア
379
+
380
+ const payload = createPayload();
381
+
382
+ if (stream) {
383
+ // ここでエンドポイントを正しく変更
384
+ ENDPOINT = ENDPOINT.replace(':generateContent', ':streamGenerateContent') + '&alt=sse';
385
+ console.debug('Stream ENDPOINT:', ENDPOINT); // デバッグ用出力を追加
386
+ fetchStream(ENDPOINT, payload);
387
+ document.getElementById('stopButton').classList.remove('d-none');
388
+ } else {
389
+ fetchNonStream(ENDPOINT, payload);
390
+ }
391
+
392
+ // 出力のアコーディオンを開く
393
+ const outputAccordion = document.querySelector('#content2Collapse');
394
+ if (outputAccordion) {
395
+ const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false });
396
+ bsCollapse.show();
397
+ }
398
+ }
399
+
400
+ function stopGeneration() {
401
+ if (controller) {
402
+ controller.abort();
403
+ controller = null;
404
+ }
405
+ document.getElementById('stopButton').classList.add('d-none');
406
+ document.getElementById('requestButton').disabled = false;
407
+ }
408
+
409
+ // 新しい関数を追加
410
+ function handleKeyPress(event) {
411
+ if (event.ctrlKey && event.key === 'Enter') {
412
+ Request();
413
+ }
414
+ }
415
+
416
+ function syncInputs() {
417
+ const inputs = document.querySelectorAll('input[type="range"], input[type="number"]');
418
+ inputs.forEach(input => {
419
+ const baseId = input.id.replace('Input', '');
420
+ const pairedInput = document.getElementById(baseId + (input.type === 'range' ? 'Input' : ''));
421
+
422
+ if (pairedInput) {
423
+ input.addEventListener('input', function () {
424
+ pairedInput.value = this.value;
425
+ });
426
+ }
427
+ });
428
+ }
429
+
430
+ function openNextAccordion() {
431
+ const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
432
+ let currentIndex = -1;
433
+
434
+ // 現在開いているアコーディオンのインデックスを見つける
435
+ for (let i = 0; i < accordions.length; i++) {
436
+ if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
437
+ currentIndex = i;
438
+ break;
439
+ }
440
+ }
441
+
442
+ // 次のアコーディオンを開く
443
+ if (currentIndex < accordions.length - 1) {
444
+ new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
445
+ new bootstrap.Collapse(accordions[currentIndex + 1].querySelector('.accordion-collapse')).show();
446
+ } else {
447
+ // もう次がない場合、ボタンを赤く点滅させる
448
+ const nextButton = document.getElementById('nextAccordion');
449
+ nextButton.classList.add('red-flash-bg');
450
+ setTimeout(() => {
451
+ nextButton.classList.remove('red-flash-bg');
452
+ }, 2000);
453
+ }
454
+ }
455
+
456
+ function openPreviousAccordion() {
457
+ const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
458
+ let currentIndex = -1;
459
+
460
+ // 現在開いているアコーディオンのインデックスを見つける
461
+ for (let i = 0; i < accordions.length; i++) {
462
+ if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
463
+ currentIndex = i;
464
+ break;
465
+ }
466
+ }
467
+
468
+ // 前のアコーディオンを開く
469
+ if (currentIndex > 0) {
470
+ new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
471
+ new bootstrap.Collapse(accordions[currentIndex - 1].querySelector('.accordion-collapse')).show();
472
+ } else {
473
+ // もう前がない場合、ボタンを赤く点滅させる
474
+ const prevButton = document.getElementById('prevAccordion');
475
+ prevButton.classList.add('red-flash-bg');
476
+ setTimeout(() => {
477
+ prevButton.classList.remove('red-flash-bg');
478
+ }, 2000);
479
+ }
480
+ }
481
+
482
+ function moveToInput() {
483
+ const content1 = document.getElementById('novelContent1');
484
+ const content2 = document.getElementById('novelContent2');
485
+
486
+ let content1Lines = content1.value.trim().split('\n');
487
+ let content2Lines = content2.value.trim().split('\n');
488
+
489
+ // content1の最後の行とcontent2の先頭行が完全に一致する場合、content2から削除
490
+ if (content1Lines[content1Lines.length - 1] === content2Lines[0]) {
491
+ content2Lines.shift();
492
+ } else {
493
+ // 部分的な重複を検出して削除
494
+ const lastLine = content1Lines[content1Lines.length - 1];
495
+ const firstLine = content2Lines[0];
496
+ const overlapIndex = firstLine.indexOf(lastLine);
497
+ if (overlapIndex !== -1) {
498
+ content2Lines[0] = firstLine.slice(overlapIndex + lastLine.length).trim();
499
+ if (content2Lines[0] === '') {
500
+ content2Lines.shift();
501
+ }
502
+ }
503
+ }
504
+
505
+ // content2の内容をcontent1の末尾に追加
506
+ content1.value = content1Lines.join('\n') + '\n' + content2Lines.join('\n');
507
+
508
+ // content2を空にする
509
+ content2.value = '';
510
+
511
+ // content1Collapseを開く
512
+ const content1Collapse = new bootstrap.Collapse(document.getElementById('content1Collapse'), {
513
+ show: true
514
+ });
515
+ }
516
+
517
+ document.addEventListener('DOMContentLoaded', function () {
518
+ // ページ読み込み時にデータを復元
519
+ loadFromUserStorage();
520
+
521
+ // イベントリスナーの設定
522
+ ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'memo', 'geminiApiKey', 'endpointSelect'].forEach(id => {
523
+ document.getElementById(id).addEventListener('input', () => {
524
+ saveToUserStorage(true);
525
+ });
526
+ });
527
+ document.getElementById('novelContent1').addEventListener('keydown', handleKeyPress);
528
+
529
+ document.querySelectorAll('[data-modal-text]').forEach(element => {
530
+ element.addEventListener('click', function () {
531
+ document.querySelectorAll(".modal-text").forEach(el => {
532
+ el.classList.add("d-none");
533
+ if (el.classList.contains(this.getAttribute('data-modal-text'))) {
534
+ el.classList.remove("d-none");
535
+ }
536
+ });
537
+ });
538
+ });
539
+
540
+ syncInputs();
541
+
542
+ // 60秒ごとに自動保存実行
543
+ setInterval(() => {
544
+ saveToUserStorage();
545
+ }, 60000);
546
+
547
+ // 基本設定のアコーディオンを開く
548
+ const basicSettingsAccordion = document.querySelector('#promptsCollapse');
549
+ if (basicSettingsAccordion) {
550
+ new bootstrap.Collapse(basicSettingsAccordion).show();
551
+ }
552
+
553
+ // ナビゲーションボタンのイベントリスナーを設定
554
+ document.getElementById('prevAccordion').addEventListener('click', openPreviousAccordion);
555
+ document.getElementById('nextAccordion').addEventListener('click', openNextAccordion);
556
+ });