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,编写内容并发布:
<details><summary>Worker JS</summary>
其中 `<your openai api key>` 填写你的 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 <your openai api key>');
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;
}
```
</details>
================================================
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 += `<div class="history-item">
<div style="display: flex; align-items: center;">
<div id="name_${key}" style="flex: 1;" onclick='switchConv("${key}"); showHistory(false);'>${itemName} (${itemData.length}+)</div>
<input id="input_${key}" type="text" placeholder="会话名称" hidden />
<button id="update_${key}" onclick='updateConvName("${key}");' class="icon" title="Save conversation name">🔧</button>
<button id="delete_${key}" onclick='deleteConv("${key}"); showHistory(true);' class="icon" title="Delete">❌</button>
</div></div>`
} else {
historyList.innerHTML += `<div class="history-item">
<div style="display: flex; align-items: center; margin-bottom: 4px;">
<input id="input_${key}" type="text" placeholder="会话名称" />
<button onclick='saveConvName("${key}"); showHistory(true);' class="icon" title="Save conversation name">📝</button>
</div>
<div style="display: flex; align-items: center;">
<div style="flex: 1;" onclick='switchConv("${key}"); showHistory(false);'>
<div>SYST: ${itemData[0].content.replace(/<[^>]+>/g, '')}</div>
<div>USER: ${itemData[1].content.replace(/<[^>]+>/g, '')} (${itemData.length}+)</div>
</div>
<button onclick='deleteConv("${key}"); showHistory(true);' class="icon" title="Delete">❌</button>
</div></div>`
}
}
if (0 == localStorage.length) {
historyList.innerHTML = `<h4>There are no past conversations yet.</h4>`
} else {
}
} else {
historyModal.style.display = 'none'
}
}
function showSettings(ok = true) {
if (ok) {
settingsModal.style.display = ''
setSettingInput(config)
} else {
settingsModal.style.display = 'none'
}
}
function setSettingInput(config) {
domainInput.placeholder = "https://api.openai.com"
maxTokensInput.placeholder = config.maxTokens
systemPromptInput.placeholder = "You are a helpful assistant."
temperatureInput.placeholder = config.temperature
apiKeyInput.value = config.apiKey
if (!config.domain) {
config.domain = domainInput.placeholder
} else {
domainInput.value = config.domain
}
if (!config.maxTokens) {
config.maxTokens = parseInt(maxTokensInput.placeholder)
} else {
maxTokensInput.value = config.maxTokens
}
if (!config.temperature) {
config.temperature = parseInt(temperatureInput.placeholder)
} else {
temperatureInput.value = config.temperature
}
if (!config.model) {
config.model = "gpt-3.5-turbo"
}
modelInput.value = config.model
if (!config.firstPrompt) {
config.firstPrompt = { role: "system", content: systemPromptInput.placeholder }
} else {
systemPromptInput.value = config.firstPrompt.content
}
multiConvInput.checked = config.multi
ttsInput.checked = config.tts
whisperInput.checked = config.onlyWhisper
}
var config = {
domain: "",
apiKey: "",
maxTokens: 500,
model: "",
firstPrompt: null,
multi: true,
stream: true,
prompts: [],
temperature: 0.5,
tts: false,
onlyWhisper: false,
}
function saveSettings() {
if (!apiKeyInput.value) {
alert('OpenAI API key can not empty')
return
}
config.domain = domainInput.value || domainInput.placeholder
config.apiKey = apiKeyInput.value
config.maxTokens = parseInt(maxTokensInput.value || maxTokensInput.placeholder)
config.temperature = parseInt(temperatureInput.value || temperatureInput.placeholder)
config.model = modelInput.value
if (systemPromptInput.value) {
config.firstPrompt = {
role: "system",
content: (systemPromptInput.value || systemPromptInput.placeholder)
}
}
messages[0] = config.firstPrompt
config.multi = multiConvInput.checked
config.tts = ttsInput.checked
config.onlyWhisper = whisperInput.checked
box.firstChild.innerHTML = config.firstPrompt.content
localStorage.setItem("conversation_config", JSON.stringify(config))
showSettings(false)
addItem('system', 'Update successed')
}
function onSelectPrompt(index) {
let prompt = config.prompts[index]
systemPromptInput.value = prompt.content
multiConvInput.checked = prompt.multi
promptDetails.open = false
}
function init() {
let configJson = localStorage.getItem("conversation_config")
let _config = JSON.parse(configJson)
if (_config) {
let ck = Object.keys(config)
ck.forEach(key => {
config[key] = _config[key] || config[key]
});
setSettingInput(config)
} else {
showSettings(true)
}
recogLangInput.value = navigator.language
if (!('speechSynthesis' in window)) {
ttsInput.disabled = false
ttsInput.onclick = () => {
alert("The current browser does not support text-to-speech");
}
}
fetch("./prompts.json").then(resp => {
if (!resp.ok) {
throw new Error(resp.statusText)
}
return resp.json()
}).then(data => {
config.prompts = data
for (let index = 0; index < data.length; index++) {
const prompt = data[index];
promptList.innerHTML += promptDiv(index, prompt)
}
})
reset()
}
window.scrollTo(0, document.body.clientHeight)
init()
const promptDiv = (index, prompt) => {
return `<div style="margin-top: 15px; cursor: pointer;" onclick="onSelectPrompt(${index})">
<div style="display: flex;">
<strong style="flex: 1;">${prompt.title}</strong>
<label style="display: ${prompt.multi ? "" : "none"}; align-items: center; margin: 0">
<span style="white-space: nowrap;">Long conversation</span>
<input type="checkbox" style="width: 1.1rem; height: 1.1rem;" checked disabled/>
</label>
</div>
<div style="margin-top: 2px;">${prompt.content}</div>
</div>`
}
const textToSpeech = async (text, options = {}) => {
loader.hidden = false
const synth = window.speechSynthesis;
// Check if Web Speech API is available
if (!('speechSynthesis' in window)) {
loader.hidden = true
alert("The current browser does not support text-to-speech");
return;
}
// Detect language using franc library
const { franc } = await import("https://cdn.jsdelivr.net/npm/franc@6.1.0/+esm");
let lang = franc(text);
if (lang === "" || lang === "und") {
lang = navigator.language
}
if (lang === "cmn") {
lang = "zh-CN"
}
// Get available voices and find the one that matches the detected language
const voices = await new Promise(resolve => {
const voices = synth.getVoices();
resolve(voices);
});
const voice = voices.find(v => langEq(v.lang, lang) && !v.localService);
if (!voice) {
voice = voices.find(v => langEq(v.lang, navigator.language) && !v.localService);
}
// Create a new SpeechSynthesisUtterance object and set its parameters
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = voice;
utterance.rate = options.rate || 1.0;
utterance.pitch = options.pitch || 1.0;
utterance.volume = options.volume || 1.0;
// Speak the text
synth.speak(utterance);
utterance.addEventListener('boundary', (event) => {
const { charIndex, elapsedTime } = event;
const progress = charIndex / utterance.text.length;
// console.log(`当前朗读进度:${progress * 100}%, 时间:${elapsedTime}`);
loader.hidden = true
});
};
const regionNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' });
const langEq = (lang1, lang2) => {
let langStr1 = regionNamesInEnglish.of(lang1)
let langStr2 = regionNamesInEnglish.of(lang2)
if (langStr1.indexOf(langStr2) !== -1) return true
if (langStr2.indexOf(langStr1) !== -1) return true
return langStr1 === langStr2
}
const getVoices = () => {
return new Promise(resolve => {
synth.onvoiceschanged = () => {
const voices = synth.getVoices();
resolve(voices);
};
});
}
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition
// var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList
// var SpeechRecognitionEvent = SpeechRecognitionEvent || webkitSpeechRecognitionEvent
var recognition = null;
const _speechToText = () => {
loader.hidden = false
// const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition)();
if (!recognition) {
recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.lang = recogLangInput.value;
recognition.interimResults = false;
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
loader.hidden = true
try {
const speechResult = event.results[0][0].transcript;
line.innerText = speechResult;
// onSend()
} catch (error) {
addItem('system', `Speech recogniion result failed: ${error.message}`)
}
};
recognition.onspeechend = function () {
loader.hidden = true
recognition.stop();
};
recognition.onnomatch = function (event) {
loader.hidden = true
addItem('system', `Speech recogniion match failed: ${event.error}`)
}
recognition.onerror = (event) => {
loader.hidden = true
addItem('system', `Speech recogniion error: ${event.error}, ${event}`)
};
}
try {
recognition.start();
} catch (error) {
onError(`Speech error: ${error}`)
}
}
function _speechToText1() {
loader.hidden = false
// 获取音频流
navigator.mediaDevices.getUserMedia({ audio: true })
.then(function (stream) {
// 创建 MediaRecorder 对象
const mediaRecorder = new MediaRecorder(stream);
// 创建 AudioContext 对象
const audioContext = new AudioContext();
// 创建 MediaStreamAudioSourceNode 对象
const source = audioContext.createMediaStreamSource(stream);
// 创建 MediaStreamAudioDestinationNode 对象
const destination = audioContext.createMediaStreamDestination();
// 将 MediaStreamAudioDestinationNode 对象连接到 MediaStreamAudioSourceNode 对象
source.connect(destination);
// 将 MediaStreamAudioDestinationNode 对象的 MediaStream 传递给 MediaRecorder 对象
mediaRecorder.stream = destination.stream;
// 创建一个空的音频缓冲区
let chunks = [];
// 开始录音
mediaRecorder.start();
// 监听录音数据
mediaRecorder.addEventListener('dataavailable', function (event) {
chunks.push(event.data);
});
// 停止录音
mediaRecorder.addEventListener('stop', function () {
// 将录音数据合并为一个 Blob 对象
const blob = new Blob(chunks, { type: 'audio/mp3' });
// 创建一个 Audio 对象
const audio = new Audio();
// 将 Blob 对象转换为 URL
const url = URL.createObjectURL(blob);
// 设置 Audio 对象的 src 属性为 URL
audio.src = url;
// 播放录音
audio.play();
// asr
transcriptions(getRecordFile(chunks, mediaRecorder.mimeType))
});
// 5 秒后停止录音
setTimeout(function () {
mediaRecorder.stop();
stream.getTracks().forEach(track => track.stop());
}, 5000);
})
.catch(function (error) {
console.error(error);
});
}
const transcriptions = (file) => {
const formData = new FormData();
formData.append("model", "whisper-1");
formData.append("file", file);
formData.append("response_format", "json");
fetch(`${config.domain}/v1/audio/transcriptions`, {
method: "POST",
headers: {
"Authorization": "Bearer " + config.apiKey,
},
body: formData,
}).then((resp) => {
return resp.json()
}).then((data) => {
loader.hidden = true
if (data.error) {
throw new Error(`${data.error.code}: ${data.error.message}`)
}
line.innerText = data.text
line.focus()
}).catch(e => {
loader.hidden = true
addItem("system", e)
})
}
const getRecordFile = (chunks, mimeType) => {
const dataType = mimeType.split(';')[0];
const fileType = dataType.split('/')[1];
const blob = new Blob(chunks, { type: dataType });
const name = `input.${fileType}`
return new File([blob], name, { type: dataType })
}
const speechToText = () => {
loader.hidden = false
// 获取音频流
navigator.mediaDevices.getUserMedia({ audio: true })
.then(function (stream) {
// 创建 MediaRecorder 对象
const mediaRecorder = new MediaRecorder(stream);
// 创建 AudioContext 对象
const audioContext = new AudioContext();
// 创建 MediaStreamAudioSourceNode 对象
const source = audioContext.createMediaStreamSource(stream);
// 创建 MediaStreamAudioDestinationNode 对象
const destination = audioContext.createMediaStreamDestination();
// 将 MediaStreamAudioDestinationNode 对象连接到 MediaStreamAudioSourceNode 对象
source.connect(destination);
// 将 MediaStreamAudioDestinationNode 对象的 MediaStream 传递给 MediaRecorder 对象
mediaRecorder.stream = destination.stream;
// 创建一个空的音频缓冲区
let chunks = [];
// 开始录音
mediaRecorder.start();
// 监听录音数据
mediaRecorder.addEventListener('dataavailable', function (event) {
chunks.push(event.data);
});
// 停止录音
mediaRecorder.addEventListener('stop', function () {
console.log("stop record");
const audiofile = getRecordFile(chunks, mediaRecorder.mimeType)
// 将录音数据合并为一个 Blob 对象
// const blob = new Blob(chunks, { type: 'audio/mp3' });
// 创建一个 Audio 对象
const audio = new Audio();
// 将 Blob 对象转换为 URL
const url = URL.createObjectURL(audiofile);
// 设置 Audio 对象的 src 属性为 URL
audio.src = url;
// 播放录音
audio.play();
// 如果仅使用 Whisper 识别,则直接调用
if (config.onlyWhisper) {
transcriptions(audiofile)
}
});
if (config.onlyWhisper) {
detectStopRecording(stream, 0.38, () => {
if (mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
stream.getTracks().forEach(track => track.stop());
})
} else {
asr(
onstop = () => {
addItem("system", `Stoped record: read ${chunks.length} "${mediaRecorder.mimeType}" blob, and start recognition`);
if (mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
stream.getTracks().forEach(track => track.stop());
},
onnomatch = () => {
transcriptions(getRecordFile(chunks, mediaRecorder.mimeType))
},
onerror = () => {
transcriptions(getRecordFile(chunks, mediaRecorder.mimeType))
})
}
})
.catch(function (error) {
console.error(error);
addItem("system", error);
});
}
const asr = (onstop, onnomatch, onerror) => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
const recognition = new SpeechRecognition()
recognition.continuous = false;
recognition.lang = recogLangInput.value;
recognition.interimResults = false;
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
loader.hidden = true
try {
const speechResult = event.results[0][0].transcript;
line.innerText = speechResult;
// onSend()
} catch (error) {
addItem('system', `Speech recogniion result failed: ${error.message}`)
}
};
recognition.onspeechend = function () {
recognition.stop();
onstop();
};
recognition.onnomatch = onnomatch
recognition.onerror = onerror
try {
recognition.start();
} catch (error) {
onerror()
}
}
function detectStopRecording(stream, maxThreshold, callback) {
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(stream);
const analyzerNode = audioContext.createAnalyser();
analyzerNode.fftSize = 2048;
analyzerNode.smoothingTimeConstant = 0.8;
sourceNode.connect(analyzerNode);
const frequencyData = new Uint8Array(analyzerNode.frequencyBinCount);
var startTime = null;
const check = () => {
analyzerNode.getByteFrequencyData(frequencyData);
const amplitude = Math.max(...frequencyData) / 255;
console.log(`amplitude: ${amplitude}`);
if (amplitude >= maxThreshold) {
console.log("speeching");
startTime = new Date().getTime();
requestAnimationFrame(check);
} else if (startTime && (new Date().getTime() - startTime) > 1000) {
callback('stop');
} else {
console.log("no speech");
requestAnimationFrame(check);
}
};
requestAnimationFrame(check);
}
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('./sw.js').then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function (err) {
console.error('ServiceWorker registration failed: ', err);
});
});
}
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">
<link rel="icon" type="image/x-icon" href="./icon-192x192.png">
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Chat GPT Simple</title>
<link rel="stylesheet" href="./main.css">
</head>
<div style="display: flex; align-items: center;">
<h2 style="display: inline-block;">Chat GPT Simple <span style="font-size: 70%; color: #333">(v0.1.1)</span></h2>
<div style="flex: 1;"></div>
<input onclick="reset()" type="submit" title="New (ctrl + i)" value="🥚" />
<input onclick="showHistory(true)" type="submit" title="History (ctrl + h)" value="🥑" />
<input onclick="showSettings(true)" type="submit" title="Setting (ctrl + ,)" value="🍪" />
</div>
<div id="box"></div>
<div id="input">
<input onclick="speechToText()" type="submit" title="Speech to text (ctrl + b)" value="🎄" />
<div id="line" type="text" contenteditable="true"></div>
<div id="loader" class="loader" hidden></div>
<input onclick="onSend()" type="submit" title="按 'ctrl+enter' 或 'alt+enter' 快捷键发送" value="▶" />
</div>
<div style="text-align: right; font-size: 90%; margin-bottom: 4px;">
<a href="#top">TOP</a>
ctrl(alt)+enter
</div>
<div id="settingsModal" class="modal" style="display: none;">
<div id="settings" class="model-body">
<div style="display: flex; align-items: center;">
<h2 style="display: inline-block">Setting</h2>
<div style="flex: 1;"></div>
<input style="width: auto; font-size: 1.5rem;" onclick="showSettings(false)" type="submit" title="Close (Esc)"
value="☕" />
</div>
<label class="label">Domain</label>
<input type="text" id="domainInput" />
<label class="label">API Key <span style="color: red">*</span></label>
<input placeholder='OpenAI API Key' type="text" id="apiKeyInput" />
<label class="label">Model <span style="color: red">*</span></label>
<select id="modelInput" style="font-size: 1.1rem; padding: 3px">
<option value="gpt-3.5-turbo">ChatGPT ($0.002 / 1K tokens) *</option>
<option value="text-ada-001">Ada ($0.0004 / 1K tokens)</option>
<option value="text-babbage-001">Babbage ($0.0005 / 1K tokens)</option>
<option value="text-curie-001">Curie ($0.0020 / 1K tokens)</option>
<option value="text-davinci-003">Davinci ($0.0200 / 1K tokens)</option>
</select>
<label class="label">Max tokens</label>
<input type="text" id="maxTokensInput" />
<label class="label">Temperature</label>
<input type="text" id="temperatureInput" />
<label class="label" style="display: flex; align-items: center;" title="Text to speech">
Text to speech
<input id="ttsInput" type="checkbox" style="width: 1.2rem; height: 1.2rem; margin-left: 10px;" />
</label>
<label class="label" style="display: flex; align-items: center;" title="Only use whisper for speech recognition">
Only use Whisper
<input id="whisperInput" type="checkbox" style="width: 1.2rem; height: 1.2rem; margin-left: 10px;" />
</label>
<label class="label">Recognition language</span></label>
<select id="recogLangInput" style="font-size: 1.1rem; padding: 3px">
<option value="zh">中文</option>
<option value="zh-CN">中文(简体)</option>
<option value="zh-TW">中文(繁体)</option>
<option value="zh-HK">中文(粤语)</option>
<option value="en">英文</option>
<option value="en-US">英文(美式)</option>
<option value="en-UK">英文(英式)</option>
</select>
<label class="label">What is your assistant</label>
<textarea id="systemPromptInput"></textarea>
<label style="display: flex; align-items: center;" title="In a thread, each session is a new session (ctrl + ;)">
Long conversation
<input id="multiConvInput" type="checkbox" style="width: 1.2rem; height: 1.2rem; margin-left: 10px;" />
</label>
<details id="promptDetails" style="margin: 20px 0;">
<summary>Assistant Public Prompts</summary>
<div id="promptList"></div>
</details>
<div style="display: flex; align-items: center; margin: 30px 0;">
<div style="flex: 1;"></div>
<button onclick="saveSettings()">Update</button>
<div style="flex: 1;"></div>
<a href="https://github.com/excing/chatgpt" target="_blank" style="padding: 10px 0;"><svg
class="octicon octicon-mark-github v-align-middle" height="32" viewBox="0 0 16 16" version="1.1" width="32"
aria-hidden="true">
<path fill-rule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
</path>
</svg></a>
</div>
</div>
</div>
<div id="historyModal" class="modal" style="display: none;">
<div id="history" class="model-body">
<div style="display: flex; align-items: center;">
<h2 style="display: inline-block">History</h2>
<div style="flex: 1;"></div>
<input style="width: auto; font-size: 1.5rem;" onclick="showHistory(false)" type="submit" title="Close (Esc)"
value="☕" />
</div>
<input style="width: auto" onclick="deleteAllHistory()" type="submit" title="Delete all history"
value="Delete All" />
<div id="historyList"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sse.js@0.6.1/lib/sse.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uuid@8.3.2/dist/umd/uuidv4.min.js"></script>
<script src="./app.js"></script>
<!-- Google tag (gtag.js) -->
<script async src='https://www.googletagmanager.com/gtag/js?id=G-RBJJWTTD37'></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-RBJJWTTD37');
</script>
================================================
FILE: main.css
================================================
html {
height: 100%;
}
body {
min-height: 100%;
max-width : 720px;
margin : auto;
padding : 0 8px;
display : flex;
flex-flow : column;
}
#input {
width : 100%;
display : flex;
align-items : end;
border-radius: 8px;
border : crimson outset 1px;
margin-bottom: 5px;
}
#input:focus-within {
border-width: 2px;
}
#line {
flex : 1;
padding : 8px 0 8px 8px;
outline : medium;
font-size : 1.2rem;
border-radius: 8px;
border : none;
background : none;
white-space : pre-wrap;
word-wrap : break-word;
word-break : break-word;
}
input[type="submit"],
button.icon {
padding : 4px 8px;
color : green;
border-radius: 8px;
background : none;
border : none;
font-size : 1.5rem;
cursor : pointer;
}
/* CSS LOADER */
.loader {
border : 6px solid #efefef;
border-top : 6px solid #111;
border-radius: 50%;
animation : spin 1.2s linear infinite;
margin : 4px auto;
width : 20px;
height : 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#box {
width : 100%;
flex : auto;
display : flex;
flex-direction: column;
}
.modal {
background : rgba(0, 0, 0, 0.30);
position : absolute;
top : 0;
left : 0;
right : 0;
bottom : 0;
display : flex;
justify-content: center;
align-items : center;
}
.model-body {
width : 100%;
max-width : 720px;
max-height : 85vh;
background-color: white;
border-radius : 20px;
padding : 20px 30px;
margin : 20px;
overflow : auto;
}
.model-body input,
.model-body textarea {
width : calc(100% - 20px);
padding : 10px;
font-size : 1.0rem;
border-radius: 4px;
background : none;
white-space : pre-wrap;
word-wrap : break-word;
word-break : break-word;
}
.model-body label {
font-size: 1.2rem;
}
.model-body label.label {
display: block;
margin : 20px 0 5px 0;
}
.model-body button {
font-size: 1.1rem;
padding : 10px 30px;
}
.system,
.assistant,
.user {
max-width : 80%;
width : fit-content;
padding : 12px 18px;
margin : 8px 0;
font-size : 1.1rem;
white-space: pre-wrap;
}
.system {
align-self: center;
color : #555;
font-size : 1.0rem;
}
.assistant {
background-color: bisque;
border-radius : 36px 8px 8px 0;
}
.user {
background-color: darkcyan;
color : aliceblue;
border-radius : 8px 36px 0 8px;
align-self : end;
margin-left : auto;
}
.history-item {
max-width : calc(100% - 20px);
width : calc(100% - 20px);
padding : 12px 18px;
font-size : 1.0rem;
cursor : pointer;
}
.history-item:nth-child(even) {
background-color: antiquewhite;
}
.history-item:nth-child(odd) {
background-color: aliceblue;
}
================================================
FILE: manifest.json
================================================
{
"name": "Chat GPT Simple APP",
"short_name": "Chat GPT",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "180x180",
"type": "image/png"
}
],
"start_url": "./index.html",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
================================================
FILE: prompts.json
================================================
[
{
"title": "友好",
"content": "你是一个友善的助手。",
"multi": true
},
{
"title": "润色",
"content": "你是一个方案助手,润色下面这段话,修正其中的语法错误,使用正式的文风。",
"multi": false
},
{
"title": "文本翻译",
"content": "你是一个翻译助手,翻译下面这段话,如果这段话非中文则翻译为中文,如果这段话为中文,则翻译为英文。",
"multi": false
},
{
"title": "输出图像",
"content": "你是一个 ASCII art 画像生成助手,为下面这段话生成一个 ASCII art 画像。",
"multi": false
},
{
"title": "夸夸我",
"content": "你是一个夸夸助手。",
"multi": true
},
{
"title": "中英词典",
"content": "你是一个中英词典助手,将英文单词转换为包括中文翻译、英文释义和一个例句的完整解释。请检查所有信息是否准确,并在回答时保持简洁,不需要任何其他反馈。",
"multi": false
},
{
"title": "写小说",
"content": "你是一个小说作者,请根据我的要求和描述写一个小说。",
"multi": true
},
{
"title": "前端智能助手",
"content": "你是一个前端开发专家,用户将提供一些关于 Js、Node、CSS、HTML 等前端代码问题的具体信息,而你的工作就是想出为用户解决问题的策略。这可能包括建议代码、代码逻辑思路策略。",
"multi": true
},
{
"title": "面试官",
"content": "你是一个专业的面试官,具体的职业用户会告诉你。用户将成为候选人,您将向用户询问该职位的面试问题。我希望你只作为面试官回答。不要一次写出所有的问题。我希望你只对用户进行采访。问用户问题,等待用户的回答。不要写解释。像面试官一样一个一个问用户,等用户回答。",
"multi": true
},
{
"title": "JavaScript 控制台",
"content": "你是一个 JavaScript 控制台。用户将键入命令,您将回复 javascript 控制台应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非用户指示您这样做。否则不要键入命令。当用户需要告诉你一些事情时,用户会把文字放在中括号内[就像这样]。",
"multi": false
},
{
"title": "终端",
"content": "你是一个 Linux 终端助手,用户将输入命令,您将回复终端应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非用户指示您这样做,否则不要键入命令。当用户需要告诉你一些事情时,用户会把文字放在中括号内[就像这样]。",
"multi": false
},
{
"title": "终端",
"content": "你是一个 Linux 终端助手,用户将输入命令,您将回复终端应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非用户指示您这样做,否则不要键入命令。当用户需要告诉你一些事情时,用户会把文字放在中括号内[就像这样]。",
"multi": false
},
{
"title": "写作导师",
"content": "我希望你能充当一个AI写作导师。我会提供一个需要帮助提高写作能力的学生,你的任务是使用人工智能工具,如自然语言处理,为学生提供反馈,告诉他们如何改进他们的作文。你还应该利用你的修辞知识和经验,提出学生可以更好地表达他们的思想和想法的写作技巧建议。",
"multi": false
},
{
"title": "网络安全专家",
"content": "我希望你扮演一个网络安全专家的角色。我会提供一些关于数据存储和共享的具体信息,你的工作就是制定保护这些数据免受恶意行为者攻击的策略。这可能包括建议加密方法、创建防火墙或实施将某些活动标记为可疑的政策。",
"multi": false
},
{
"title": "评论员",
"content": "我希望你能担任评论员的角色。我会提供与新闻相关的故事或话题,你将撰写一篇见解深刻的评论文章。你应该运用自己的经验,深入解释为什么某件事很重要,用事实支持论点,并讨论任何故事中出现的问题的潜在解决方案。",
"multi": false
},
{
"title": "魔术师",
"content": "我要你扮演魔术师。我将为您提供观众和一些可以执行的技巧建议。您的目标是以最有趣的方式表演这些技巧,利用您的欺骗和误导技巧让观众惊叹不已。",
"multi": false
},
{
"title": "英语发音助手",
"content": "我希望你能成为汉语使用者的英语发音助手。我会给你写句子,你只需要回答它们的发音,不需要其他任何内容。回复不能是我的句子的翻译,只能是发音。发音应该使用汉语拼音表示音标。回复中不要写解释。",
"multi": false
},
{
"title": "英语口语教师",
"content": "我希望你能充当口语英语教师和提高者。我会用英语和你交谈,你会用英语回答我以练习我的口语英语。我希望你的回答简洁明了,限制回答在100个单词以内。我希望你严格纠正我的语法错误、打字错误和事实错误。我希望你在回答中问我一个问题。现在让我们开始练习,你可以先问我一个问题。记住,我希望你严格纠正我的语法错误、打字错误和事实错误。",
"multi": false
},
{
"title": "旅游指南",
"content": "我希望你能充当一名旅游指南。我会告诉你我的位置,然后你会建议我附近可以参观的地方。在某些情况下,我还会告诉你我想参观的地方类型。你还会向我推荐与我第一个位置相似类型的地方。",
"multi": false
},
{
"title": "抄袭检查员",
"content": "我希望你能充当一名抄袭检查员。我会给你写句子,你只需要用给定语言回复不被检测到抄袭,仅此而已。不要在回复中写解释。",
"multi": false
},
{
"title": "广告商",
"content": "我希望你扮演广告商的角色。你将创建一个宣传活动,以推广你选择的产品或服务。你将选择目标受众,制定关键信息和口号,选择宣传的媒体渠道,并决定需要采取的任何其他活动以达到你的目标。",
"multi": false
},
{
"title": "数学老师",
"content": "我希望你扮演一位数学老师的角色。我会提供一些数学方程或概念,你的工作就是用易于理解的术语来解释它们。这可能包括提供解决问题的逐步说明,用视觉展示各种技巧或建议在线资源进行进一步学习。",
"multi": false
}
]
================================================
FILE: sw.js
================================================
const CACHE_NAME = 'my-pwa-app-cache-v1';
const urlsToCache = [
'./',
'./index.html',
'./app.js',
'./main.css',
'./prompts.json'
];
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
gitextract_5gvqwd5s/ ├── .gitignore ├── LICENSE ├── README.en.md ├── README.md ├── app.js ├── index.html ├── main.css ├── manifest.json ├── prompts.json └── sw.js
SYMBOL INDEX (23 symbols across 2 files)
FILE: app.js
function onSend (line 55) | function onSend() {
function addItem (line 67) | function addItem(type, content) {
function postLine (line 81) | function postLine(line) {
function chat (line 99) | function chat(reqMsgs) {
function completions (line 117) | function completions(reqMsgs) {
function onSuccessed (line 143) | function onSuccessed(assistantElem) {
function send (line 150) | function send(reqUrl, body, onMessage, scussionCall) {
function reset (line 218) | function reset() {
function saveConv (line 227) | function saveConv(message) {
function switchConv (line 232) | function switchConv(key) {
function deleteConv (line 247) | function deleteConv(key) {
function deleteAllHistory (line 251) | function deleteAllHistory() {
function saveConvName (line 260) | function saveConvName(key) {
function updateConvName (line 266) | function updateConvName(key) {
function showHistory (line 280) | function showHistory(ok = true) {
function showSettings (line 327) | function showSettings(ok = true) {
function setSettingInput (line 336) | function setSettingInput(config) {
function saveSettings (line 386) | function saveSettings() {
function onSelectPrompt (line 412) | function onSelectPrompt(index) {
function init (line 419) | function init() {
function _speechToText1 (line 586) | function _speechToText1() {
function detectStopRecording (line 781) | function detectStopRecording(stream, maxThreshold, callback) {
FILE: sw.js
constant CACHE_NAME (line 1) | const CACHE_NAME = 'my-pwa-app-cache-v1';
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (51K chars).
[
{
"path": ".gitignore",
"chars": 10,
"preview": ".DS_Store\n"
},
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2023 Qiang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "README.en.md",
"chars": 1282,
"preview": "[中文](README.md) | [English](README.en.md)\n\n# chatgpt\n\nBuild your OpenAI ChatGPT web site.\n\nThe voice recognition functio"
},
{
"path": "README.md",
"chars": 3100,
"preview": "[中文](README.md) | [English](README.en.md)\n\n# chatgpt\n构建你的 OpenAI ChatGPT Web 站点\n\n语音识别功能,默认使用本地语音识别模式,当本地语音识别失败,会自动使用 `Op"
},
{
"path": "app.js",
"chars": 24152,
"preview": "window.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Escape\") {\n showSettings(false)\n showHistory(false)\n "
},
{
"path": "index.html",
"chars": 6477,
"preview": "<!DOCTYPE html>\n\n<head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n <meta name=\"viewport\" "
},
{
"path": "main.css",
"chars": 3001,
"preview": "html {\n height: 100%;\n}\n\nbody {\n min-height: 100%;\n max-width : 720px;\n margin : auto;\n padding : 0 8px;\n dis"
},
{
"path": "manifest.json",
"chars": 339,
"preview": "{\n \"name\": \"Chat GPT Simple APP\",\n \"short_name\": \"Chat GPT\",\n \"icons\": [\n {\n \"src\": \"icon-192"
},
{
"path": "prompts.json",
"chars": 3328,
"preview": "[\n {\n \"title\": \"友好\",\n \"content\": \"你是一个友善的助手。\",\n \"multi\": true\n },\n {\n \"title\": \"润色\",\n \"content\": \"你是一个"
},
{
"path": "sw.js",
"chars": 723,
"preview": "const CACHE_NAME = 'my-pwa-app-cache-v1';\nconst urlsToCache = [\n './',\n './index.html',\n './app.js',\n './mai"
}
]
About this extraction
This page contains the full source code of the excing/chatgpt GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (42.5 KB), approximately 12.9k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.