Repository: excing/chatgpt
Branch: main
Commit: 5a58d88d7fc0
Files: 10
Total size: 42.5 KB
Directory structure:
gitextract_5gvqwd5s/
├── .gitignore
├── LICENSE
├── README.en.md
├── README.md
├── app.js
├── index.html
├── main.css
├── manifest.json
├── prompts.json
└── sw.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Qiang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.en.md
================================================
[中文](README.md) | [English](README.en.md)
# chatgpt
Build your OpenAI ChatGPT web site.
The voice recognition function defaults to using local voice recognition mode. When local voice recognition fails, it will automatically switch to using `OpenAI Whisper` for recognition. It can also be set to use `Only use Whisper` for recognition.
Note: Due to the Android system's characteristics, Android phone users cannot perform local voice recognition and recording services at the same time. Therefore, mobile phone users should enable `Only use Whisper` mode.
- Custom OpenAI domain name, direct connection, no need to worry about key leakage;
- Your own API Key;
- All data is stored locally;
- Model selection;
- Assistant prompt can be set;
- Multiple prompts are pre-installed;
- Session history record (local storage);
- Support for setting the `temperature` parameter;
- Support for "sse", which is the `stream` in the OpenAI API;
- Support for automatic text playback (TTS);
- Support for voice input (ASR).
- Support for `OpenAI Whisper` recognition (default using local voice recognition)
More people are welcome to improve this [prompt list](https://github.com/excing/chatgpt/blob/main/prompts.json).
## Deployment
Fork this project and then enable your GitHub Pages.
================================================
FILE: README.md
================================================
[中文](README.md) | [English](README.en.md)
# chatgpt
构建你的 OpenAI ChatGPT Web 站点
语音识别功能,默认使用本地语音识别模式,当本地语音识别失败,会自动使用 `OpenAI Whisper` 开始识别,也可以设置为 `仅使用 Wishper` 进行识别。
> 注:由于 Android 手机用户无法同时进行本地语音识别和录音服务(和 Android 系统特性有关),所以手机用户请开启 `仅使用 Wishper` 模式。
- 自定义 OpenAI 域名,直连,不经过他人服务器,无需担心 Key 泄露;
- 自己的 API Key;
- 所有数据都在本地存储;
- 模型选择;
- 可设置助手 `prompt`;
- 预置多个 `prompt`;
- 会话历史记录(本地存储);
- 支持设置 `temperature` 参数;
- 支持 `sse`,即 openai api 中的 `stream`;
- 支持自动播放文本(TTS);
- 支持语音录入(ASR)。
- 支持 `OpenAI Whisper` 识别(默认使用本地语音识别)
欢迎更多人来完善这个 [prompt list](https://github.com/excing/chatgpt/blob/main/prompts.json)。
## 部署
Fork 此项目,然后开启你的 GitHub Pages 即可。
如果你的 OpenAI 不可访问,可以尝试使用这个方案:[使用 Cloudflare Workers 让 OpenAI API 绕过 GFW 且避免被封禁](https://github.com/noobnooc/noobnooc/discussions/9)
省流版:创建一个 Cloudflare Workers,编写内容并发布:
Worker JS
其中 `` 填写你的 OpenAI api key 即可实现客户端无 key 使用。
```js
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request));
})
async function fetchAndApply(request) {
let response = null;
let method = request.method;
let url = new URL(request.url);
let url_hostname = url.hostname;
url.protocol = 'https:';
url.host = 'api.openai.com';
let request_headers = request.headers;
let new_request_headers = new Headers(request_headers);
new_request_headers.set('Host', url.host);
new_request_headers.set('Referer', url.protocol + '//' + url_hostname);
new_request_headers.set('Authorization', 'Bearer ');
let original_response = await fetch(url.href, {
method: method,
headers: new_request_headers,
body: request.body
})
// let original_response_clone = original_response.clone();
let original_text = null;
let response_headers = original_response.headers;
let new_response_headers = new Headers(response_headers);
let status = original_response.status;
new_response_headers.set('Cache-Control', 'no-store');
new_response_headers.set('access-control-allow-origin', '*');
new_response_headers.set('access-control-allow-credentials', true);
new_response_headers.delete('content-security-policy');
new_response_headers.delete('content-security-policy-report-only');
new_response_headers.delete('clear-site-data');
original_text = original_response.body
response = new Response(original_text, {
status,
headers: new_response_headers
})
return response
}
async function replace_response_text(response, upstream_domain, host_name) {
let text = await response.text()
var i, j;
for (i in replace_dict) {
j = replace_dict[i]
if (i == '$upstream') {
i = upstream_domain
} else if (i == '$custom_domain') {
i = host_name
}
if (j == '$upstream') {
j = upstream_domain
} else if (j == '$custom_domain') {
j = host_name
}
let re = new RegExp(i, 'g')
text = text.replace(re, j);
}
return text;
}
```
================================================
FILE: app.js
================================================
window.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
showSettings(false)
showHistory(false)
}
if ((e.ctrlKey || e.altKey)) {
// console.log(e.key);
switch (e.key) {
case "i":
e.preventDefault()
reset()
break;
case ",":
e.preventDefault()
showSettings(true)
break;
case "h":
e.preventDefault()
showHistory(true)
break;
case ";":
e.preventDefault()
config.multi = !config.multi
addItem("system", "Long conversation checked: " + config.multi)
break;
case "b":
e.preventDefault()
speechToText()
break;
default:
break;
}
}
}, { passive: false })
line.addEventListener("keydown", (e) => {
if (e.key == "Enter" && (e.ctrlKey || e.altKey)) {
e.preventDefault()
onSend()
}
})
line.addEventListener("paste", (e) => {
e.preventDefault()
let clipboardData = (e.clipboardData || window.clipboardData)
let paste = clipboardData.getData("text/plain")
.toString()
.replaceAll("\r\n", "\n")
line.focus()
document.execCommand("insertText", false, paste)
}, { passive: false })
function onSend() {
var value = (line.value || line.innerText).trim()
if (!value) return
addItem("user", value)
postLine(value)
line.value = ""
line.innerText = ""
}
function addItem(type, content) {
let request = document.createElement("div")
request.className = type
request.innerText = content
box.appendChild(request)
window.scrollTo({
top: document.body.scrollHeight, behavior: "auto",
})
line.focus()
return request
}
function postLine(line) {
saveConv({ role: "user", content: line })
let reqMsgs = []
if (messages.length < 10) {
reqMsgs.push(...messages)
} else {
reqMsgs.push(messages[0])
reqMsgs.push(...messages.slice(messages.length - 7, messages.length))
}
if (config.model === "gpt-3.5-turbo") {
chat(reqMsgs)
} else {
completions(reqMsgs)
}
}
var convId;
var messages = [];
function chat(reqMsgs) {
let assistantElem = addItem('', '')
let _message = reqMsgs
if (!config.multi) {
_message = [reqMsgs[0], reqMsgs[reqMsgs.length - 1]]
}
send(`${config.domain}/v1/chat/completions`, {
"model": "gpt-3.5-turbo",
"messages": _message,
"max_tokens": config.maxTokens,
"stream": config.stream,
"temperature": config.temperature,
}, (data) => {
let msg = data.choices[0].delta || data.choices[0].message || {}
assistantElem.className = 'assistant'
assistantElem.innerText += msg.content || ""
}, () => onSuccessed(assistantElem),)
}
function completions(reqMsgs) {
let assistantElem = addItem('', '')
let _prompt = ""
if (config.multi) {
reqMsgs.forEach(msg => {
_prompt += `${msg.role}: ${msg.content}\n`
});
} else {
_prompt += `${reqMsgs[0].role}: ${reqMsgs[0].content}\n`
let lastMessage = reqMsgs[reqMsgs.length - 1]
_prompt += `${lastMessage.role}: ${lastMessage.content}\n`
}
_prompt += "assistant: "
send(`${config.domain}/v1/completions`, {
"model": config.model,
"prompt": _prompt,
"max_tokens": config.maxTokens,
"temperature": 0,
"stop": ["\nuser: ", "\nassistant: "],
"stream": config.stream,
"temperature": config.temperature,
}, (data) => {
assistantElem.className = 'assistant'
assistantElem.innerText += data.choices[0].text
}, () => onSuccessed(assistantElem),)
}
function onSuccessed(assistantElem) {
let msg = assistantElem.innerText
saveConv({ role: "assistant", content: msg })
if (config.tts) {
textToSpeech(msg)
}
}
function send(reqUrl, body, onMessage, scussionCall) {
loader.hidden = false
let onError = (data) => {
console.error(data);
loader.hidden = true
if (!data) {
addItem("system", `Unable to access OpenAI, please check your network.`)
} else {
try {
let openai = JSON.parse(data)
addItem("system", `${openai.error.message}`)
} catch (error) {
addItem("system", `${data}`)
}
}
}
if (!config.tts) {
body.stream = true
var source = new SSE(
reqUrl, {
headers: {
"Authorization": "Bearer " + config.apiKey,
"Content-Type": "application/json",
},
method: "POST",
payload: JSON.stringify(body),
});
source.addEventListener("message", function (e) {
if (e.data == "[DONE]") {
loader.hidden = true
scussionCall()
} else {
try {
onMessage(JSON.parse(e.data))
} catch (error) {
onError(error)
}
}
});
source.addEventListener("error", function (e) {
onError(e.data)
});
source.stream();
} else {
body.stream = false
fetch(reqUrl, {
method: "POST",
headers: {
"Authorization": "Bearer " + config.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}).then((resp) => {
return resp.json()
}).then((data) => {
loader.hidden = true
if (data.error) {
throw new Error(`${data.error.code}: ${data.error.message}`)
}
onMessage(data)
scussionCall()
}).catch(onError)
}
}
function reset() {
box.innerHTML = ''
convId = uuidv4();
messages = [config.firstPrompt]
addItem(config.firstPrompt.role, config.firstPrompt.content)
}
const convKey = "conversations_"
const convNameKey = "conversationName_"
function saveConv(message) {
messages.push(message)
localStorage.setItem(`${convKey}${convId}`, JSON.stringify(messages))
}
function switchConv(key) {
if (key == null) {
addItem("system", "No conversations")
return
}
box.innerHTML = ''
messages = JSON.parse(localStorage.getItem(key))
messages.forEach(msg => {
addItem(msg.role, msg.content)
});
convId = key.substring(convKey.length);
systemPromptInput.value = messages[0].content;
saveSettings();
}
function deleteConv(key) {
localStorage.removeItem(key)
}
function deleteAllHistory() {
for (let index = 0; index < localStorage.length; index++) {
let key = localStorage.key(index);
if (key.substring(0, convKey.length) != convKey) { continue }
deleteConv(key)
showHistory(true)
}
}
function saveConvName(key) {
let input = document.getElementById(`input_${key}`)
localStorage.setItem(`${convNameKey}${key}`, input.value)
showHistory(true)
}
function updateConvName(key) {
let name = document.getElementById(`name_${key}`)
let input = document.getElementById(`input_${key}`)
let update = document.getElementById(`update_${key}`)
let del = document.getElementById(`delete_${key}`)
input.hidden = false
name.hidden = true
del.hidden = true
update.innerHTML = "📝"
update.onclick = () => {
saveConvName(key)
}
}
function showHistory(ok = true) {
if (ok) {
historyModal.style.display = ''
historyList.innerHTML = ''
for (let index = 0; index < localStorage.length; index++) {
let key = localStorage.key(index);
if (key.substring(0, convKey.length) != convKey) { continue }
let itemJson = localStorage.getItem(key)
let itemData;
try {
itemData = JSON.parse(itemJson)
} catch (error) {
continue
}
let itemName = localStorage.getItem(`${convNameKey}${key}`)
if (itemName) {
historyList.innerHTML += `