summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHeiner Lohaus <hlohaus@users.noreply.github.com>2024-11-19 10:23:24 +0100
committerHeiner Lohaus <hlohaus@users.noreply.github.com>2024-11-19 10:23:24 +0100
commit7c10a036ed63b0a14f69b27552801dde4f220b4e (patch)
treebb63fe93fdad43581c64c23b92eb773b4ec0263e
parentUpdate unittests (diff)
downloadgpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.tar
gpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.tar.gz
gpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.tar.bz2
gpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.tar.lz
gpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.tar.xz
gpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.tar.zst
gpt4free-7c10a036ed63b0a14f69b27552801dde4f220b4e.zip
-rw-r--r--g4f/Provider/Copilot.py17
-rw-r--r--g4f/Provider/needs_auth/CopilotAccount.py3
-rw-r--r--g4f/Provider/needs_auth/OpenaiChat.py2
-rw-r--r--g4f/gui/client/index.html9
-rw-r--r--g4f/gui/client/static/css/style.css32
-rw-r--r--g4f/gui/client/static/js/chat.v1.js232
6 files changed, 160 insertions, 135 deletions
diff --git a/g4f/Provider/Copilot.py b/g4f/Provider/Copilot.py
index f10202bf..3bf6f193 100644
--- a/g4f/Provider/Copilot.py
+++ b/g4f/Provider/Copilot.py
@@ -23,9 +23,10 @@ except ImportError:
from .base_provider import AbstractProvider, BaseConversation
from .helper import format_prompt
-from ..typing import CreateResult, Messages
+from ..typing import CreateResult, Messages, ImageType
from ..errors import MissingRequirementsError
from ..requests.raise_for_status import raise_for_status
+from ..image import to_bytes, is_accepted_format
from .. import debug
class Conversation(BaseConversation):
@@ -43,6 +44,7 @@ class Copilot(AbstractProvider):
url = "https://copilot.microsoft.com"
working = True
supports_stream = True
+ default_model = "Copilot"
websocket_url = "wss://copilot.microsoft.com/c/api/chat?api-version=2"
conversation_url = f"{url}/c/api/conversations"
@@ -55,6 +57,7 @@ class Copilot(AbstractProvider):
stream: bool = False,
proxy: str = None,
timeout: int = 900,
+ image: ImageType = None,
conversation: Conversation = None,
return_conversation: bool = False,
**kwargs
@@ -98,11 +101,21 @@ class Copilot(AbstractProvider):
if debug.logging:
print(f"Copilot: Use conversation: {conversation_id}")
+ images = []
+ if image is not None:
+ data = to_bytes(image)
+ response = session.post(
+ "https://copilot.microsoft.com/c/api/attachments",
+ headers={"content-type": is_accepted_format(data)},
+ data=data
+ )
+ images.append({"type":"image", "url": response.json().get("url")})
+
wss = session.ws_connect(cls.websocket_url)
wss.send(json.dumps({
"event": "send",
"conversationId": conversation_id,
- "content": [{
+ "content": [*images, {
"type": "text",
"text": prompt,
}],
diff --git a/g4f/Provider/needs_auth/CopilotAccount.py b/g4f/Provider/needs_auth/CopilotAccount.py
index fa43867e..76e51278 100644
--- a/g4f/Provider/needs_auth/CopilotAccount.py
+++ b/g4f/Provider/needs_auth/CopilotAccount.py
@@ -5,4 +5,5 @@ from ..Copilot import Copilot
class CopilotAccount(Copilot):
needs_auth = True
parent = "Copilot"
- default_model = "" \ No newline at end of file
+ default_model = "Copilot"
+ default_vision_model = default_model \ No newline at end of file
diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py
index 13e15f1d..22264dd9 100644
--- a/g4f/Provider/needs_auth/OpenaiChat.py
+++ b/g4f/Provider/needs_auth/OpenaiChat.py
@@ -396,7 +396,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
f"{cls.url}/backend-anon/sentinel/chat-requirements"
if cls._api_key is None else
f"{cls.url}/backend-api/sentinel/chat-requirements",
- json={"p": get_requirements_token(RequestConfig.proof_token)},
+ json={"p": get_requirements_token(RequestConfig.proof_token) if RequestConfig.proof_token else None},
headers=cls._headers
) as response:
cls._update_request_args(session)
diff --git a/g4f/gui/client/index.html b/g4f/gui/client/index.html
index 63e47b3f..b256b0be 100644
--- a/g4f/gui/client/index.html
+++ b/g4f/gui/client/index.html
@@ -72,12 +72,12 @@
</button>
<div class="info">
<i class="fa-brands fa-discord"></i>
- <span class="convo-title">discord ~ <a href="https://discord.gghttps://discord.gg/6yrm7H4B">discord.gg/6yrm7H4B</a>
+ <span class="convo-title">discord ~ <a href="https://discord.gg/6yrm7H4B" target="_blank">discord.gg/6yrm7H4B</a>
</span>
</div>
<div class="info">
<i class="fa-brands fa-github"></i>
- <span class="convo-title">github ~ <a href="https://github.com/xtekky/gpt4free">@xtekky/gpt4free</a>
+ <span class="convo-title">github ~ <a href="https://github.com/xtekky/gpt4free" target="_blank">@xtekky/gpt4free</a>
</span>
</div>
<div class="info">
@@ -87,7 +87,6 @@
</div>
</div>
<div class="images hidden">
-
</div>
<div class="settings hidden">
<div class="paper">
@@ -192,7 +191,7 @@
<i class="fa-regular fa-stop"></i>
</button>
</div>
- <div class="regenerate regenerate-hidden">
+ <div class="regenerate">
<button id="regenerateButton">
<span>Regenerate</span>
<i class="fa-solid fa-rotate"></i>
@@ -263,4 +262,4 @@
<i class="fa-solid fa-bars"></i>
</div>
</body>
-</html>
+</html> \ No newline at end of file
diff --git a/g4f/gui/client/static/css/style.css b/g4f/gui/client/static/css/style.css
index 441e2042..3ee033ea 100644
--- a/g4f/gui/client/static/css/style.css
+++ b/g4f/gui/client/static/css/style.css
@@ -472,6 +472,11 @@ body {
padding: 0 4px;
}
+.stop_generating-hidden, .regenerate-hidden {
+ animation: hide_popup 0.4s;
+ display: none;
+}
+
.stop_generating, .toolbar .regenerate {
position: absolute;
z-index: 1000000;
@@ -479,14 +484,6 @@ body {
right: 0;
}
-@media only screen and (min-width: 40em) {
- .stop_generating, .toolbar .regenerate {
- left: 50%;
- transform: translateX(-50%);
- right: auto;
- }
-}
-
.stop_generating button, .toolbar .regenerate button{
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
@@ -503,6 +500,17 @@ body {
animation: show_popup 0.4s;
}
+@media only screen and (min-width: 40em) {
+ .stop_generating {
+ left: 50%;
+ transform: translateX(-50%);
+ right: auto;
+ }
+ .toolbar .regenerate {
+ right: 5px;
+ }
+}
+
.toolbar .hide-input {
background: transparent;
border: none;
@@ -524,11 +532,6 @@ body {
}
}
-.stop_generating-hidden #cancelButton, .regenerate-hidden #regenerateButton {
- animation: hide_popup 0.4s;
- display: none;
-}
-
.typing {
position: absolute;
top: -25px;
@@ -779,8 +782,7 @@ select {
overflow: auto;
}
-
-#cursor {
+.cursor {
line-height: 17px;
margin-left: 3px;
-webkit-animation: blink 0.8s infinite;
diff --git a/g4f/gui/client/static/js/chat.v1.js b/g4f/gui/client/static/js/chat.v1.js
index 580cbf77..9aba4f10 100644
--- a/g4f/gui/client/static/js/chat.v1.js
+++ b/g4f/gui/client/static/js/chat.v1.js
@@ -20,12 +20,13 @@ const settings = document.querySelector(".settings");
const chat = document.querySelector(".conversation");
const album = document.querySelector(".images");
-let prompt_lock = false;
-
-let content, content_inner, content_count = null;
-
const optionElements = document.querySelectorAll(".settings input, .settings textarea, #model, #model2, #provider")
+let provider_storage = {};
+let message_storage = {};
+let controller_storage = {};
+let content_storage = {}
+
messageInput.addEventListener("blur", () => {
window.scrollTo(0, 0);
});
@@ -98,13 +99,10 @@ const register_message_buttons = async () => {
if (!("click" in el.dataset)) {
el.dataset.click = "true";
el.addEventListener("click", async () => {
- if (prompt_lock) {
- return;
- }
const message_el = el.parentElement.parentElement;
await remove_message(window.conversation_id, message_el.dataset.index);
- await load_conversation(window.conversation_id, false);
- })
+ await safe_load_conversation(window.conversation_id, false);
+ });
}
});
@@ -114,18 +112,16 @@ const register_message_buttons = async () => {
el.addEventListener("click", async () => {
const message_el = el.parentElement.parentElement.parentElement;
const copyText = await get_message(window.conversation_id, message_el.dataset.index);
-
- try {
- if (!navigator.clipboard) {
- throw new Error("navigator.clipboard: Clipboard API unavailable.");
+ try {
+ if (!navigator.clipboard) {
+ throw new Error("navigator.clipboard: Clipboard API unavailable.");
+ }
+ await navigator.clipboard.writeText(copyText);
+ } catch (e) {
+ console.error(e);
+ console.error("Clipboard API writeText() failed! Fallback to document.exec(\"copy\")...");
+ fallback_clipboard(copyText);
}
- await navigator.clipboard.writeText(copyText);
- } catch (e) {
- console.error(e);
- console.error("Clipboard API writeText() failed! Fallback to document.exec(\"copy\")...");
- fallback_clipboard(copyText);
- }
-
el.classList.add("clicked");
setTimeout(() => el.classList.remove("clicked"), 1000);
})
@@ -217,10 +213,8 @@ const register_message_buttons = async () => {
const message_el = el.parentElement.parentElement.parentElement;
el.classList.add("clicked");
setTimeout(() => el.classList.remove("clicked"), 1000);
- prompt_lock = true;
await hide_message(window.conversation_id, message_el.dataset.index);
- window.token = message_id();
- await ask_gpt(message_el.dataset.index);
+ await ask_gpt(message_el.dataset.index, get_message_id());
})
}
});
@@ -264,12 +258,11 @@ const handle_ask = async () => {
messageInput.focus();
window.scrollTo(0, 0);
- message = messageInput.value
+ let message = messageInput.value;
if (message.length <= 0) {
return;
}
messageInput.value = "";
- prompt_lock = true;
count_input()
await add_conversation(window.conversation_id, message);
@@ -279,7 +272,7 @@ const handle_ask = async () => {
message += '\n```'
}
let message_index = await add_message(window.conversation_id, "user", message);
- window.token = message_id();
+ let message_id = get_message_id();
if (imageInput.dataset.src) URL.revokeObjectURL(imageInput.dataset.src);
const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput
@@ -293,7 +286,7 @@ const handle_ask = async () => {
<i class="fa-solid fa-xmark"></i>
<i class="fa-regular fa-phone-arrow-up-right"></i>
</div>
- <div class="content" id="user_${token}">
+ <div class="content" id="user_${message_id}">
<div class="content_inner">
${markdown_render(message)}
${imageInput.dataset.src
@@ -312,17 +305,38 @@ const handle_ask = async () => {
</div>
`;
highlight(message_box);
- await ask_gpt();
+ stop_generating.classList.remove("stop_generating-hidden");
+ await ask_gpt(-1, message_id);
};
-const remove_cancel_button = async () => {
- stop_generating.classList.add(`stop_generating-hiding`);
+async function remove_cancel_button() {
+ stop_generating.classList.add("stop_generating-hidden");
+}
+
+regenerate.addEventListener("click", async () => {
+ regenerate.classList.add("regenerate-hidden");
+ stop_generating.classList.remove("stop_generating-hidden");
+ await hide_message(window.conversation_id);
+ await ask_gpt(-1, get_message_id());
+});
- setTimeout(() => {
- stop_generating.classList.remove(`stop_generating-hiding`);
- stop_generating.classList.add(`stop_generating-hidden`);
- }, 300);
-};
+stop_generating.addEventListener("click", async () => {
+ stop_generating.classList.add("stop_generating-hidden");
+ regenerate.classList.remove("regenerate-hidden");
+ let key;
+ for (key in controller_storage) {
+ if (!controller_storage[key].signal.aborted) {
+ controller_storage[key].abort();
+ let message = message_storage[key];
+ if (message) {
+ content_storage[key].inner.innerHTML += " [aborted]";
+ message_storage[key] += " [aborted]";
+ console.log(`aborted ${window.conversation_id} #${key}`);
+ }
+ }
+ }
+ await load_conversation(window.conversation_id);
+});
const prepare_messages = (messages, message_index = -1) => {
// Removes none user messages at end
@@ -367,13 +381,13 @@ const prepare_messages = (messages, message_index = -1) => {
return new_messages;
}
-
-async function add_message_chunk(message) {
+async function add_message_chunk(message, message_index) {
+ content_map = content_storage[message_index];
if (message.type == "conversation") {
console.info("Conversation used:", message.conversation)
} else if (message.type == "provider") {
- window.provider_result = message.provider;
- content.querySelector('.provider').innerHTML = `
+ provider_storage[message_index] = message.provider;
+ content_map.content.querySelector('.provider').innerHTML = `
<a href="${message.provider.url}" target="_blank">
${message.provider.label ? message.provider.label : message.provider.name}
</a>
@@ -384,12 +398,12 @@ async function add_message_chunk(message) {
} else if (message.type == "error") {
window.error = message.error
console.error(message.error);
- content_inner.innerHTML += `<p><strong>An error occured:</strong> ${message.error}</p>`;
+ content_map.inner.innerHTML += `<p><strong>An error occured:</strong> ${message.error}</p>`;
} else if (message.type == "preview") {
- content_inner.innerHTML = markdown_render(message.preview);
+ content_map.inner.innerHTML = markdown_render(message.preview);
} else if (message.type == "content") {
- window.text += message.content;
- html = markdown_render(window.text);
+ message_storage[message_index] += message.content;
+ html = markdown_render(message_storage[message_index]);
let lastElement, lastIndex = null;
for (element of ['</p>', '</code></pre>', '</p>\n</li>\n</ol>', '</li>\n</ol>', '</li>\n</ul>']) {
const index = html.lastIndexOf(element)
@@ -399,11 +413,11 @@ async function add_message_chunk(message) {
}
}
if (lastIndex) {
- html = html.substring(0, lastIndex) + '<span id="cursor"></span>' + lastElement;
+ html = html.substring(0, lastIndex) + '<span class="cursor"></span>' + lastElement;
}
- content_inner.innerHTML = html;
- content_count.innerText = count_words_and_tokens(text, window.provider_result?.model);
- highlight(content_inner);
+ content_map.inner.innerHTML = html;
+ content_map.count.innerText = count_words_and_tokens(message_storage[message_index], provider_storage[message_index]?.model);
+ highlight(content_map.inner);
}
window.scrollTo(0, 0);
if (message_box.scrollTop >= message_box.scrollHeight - message_box.clientHeight - 100) {
@@ -411,13 +425,6 @@ async function add_message_chunk(message) {
}
}
-// fileInput?.addEventListener("click", (e) => {
-// if (window?.pywebview) {
-// e.preventDefault();
-// pywebview.api.choose_file();
-// }
-// });
-
cameraInput?.addEventListener("click", (e) => {
if (window?.pywebview) {
e.preventDefault();
@@ -432,44 +439,44 @@ imageInput?.addEventListener("click", (e) => {
}
});
-const ask_gpt = async (message_index = -1) => {
- regenerate.classList.add(`regenerate-hidden`);
- messages = await get_messages(window.conversation_id);
- total_messages = messages.length;
+const ask_gpt = async (message_index = -1, message_id) => {
+ let messages = await get_messages(window.conversation_id);
+ let total_messages = messages.length;
messages = prepare_messages(messages, message_index);
-
- stop_generating.classList.remove(`stop_generating-hidden`);
+ message_index = total_messages
+ message_storage[message_index] = "";
+ stop_generating.classList.remove(".stop_generating-hidden");
message_box.scrollTop = message_box.scrollHeight;
window.scrollTo(0, 0);
- el = message_box.querySelector('.count_total');
- el ? el.parentElement.removeChild(el) : null;
+ let count_total = message_box.querySelector('.count_total');
+ count_total ? count_total.parentElement.removeChild(count_total) : null;
message_box.innerHTML += `
- <div class="message" data-index="${total_messages}">
+ <div class="message" data-index="${message_index}">
<div class="assistant">
${gpt_image}
<i class="fa-solid fa-xmark"></i>
<i class="fa-regular fa-phone-arrow-down-left"></i>
</div>
- <div class="content" id="gpt_${window.token}">
+ <div class="content" id="gpt_${message_id}">
<div class="provider"></div>
- <div class="content_inner"><span id="cursor"></span></div>
+ <div class="content_inner"><span class="cursor"></span></div>
<div class="count"></div>
</div>
</div>
`;
- window.controller = new AbortController();
- window.text = "";
- window.error = null;
- window.abort = false;
- window.provider_result = null;
+ controller_storage[message_index] = new AbortController();
+ let error = false;
- content = document.getElementById(`gpt_${window.token}`);
- content_inner = content.querySelector('.content_inner');
- content_count = content.querySelector('.count');
+ let content_el = document.getElementById(`gpt_${message_id}`)
+ let content_map = content_storage[message_index] = {
+ content: content_el,
+ inner: content_el.querySelector('.content_inner'),
+ count: content_el.querySelector('.count'),
+ }
message_box.scrollTop = message_box.scrollHeight;
window.scrollTo(0, 0);
@@ -478,8 +485,6 @@ const ask_gpt = async (message_index = -1) => {
const file = input && input.files.length > 0 ? input.files[0] : null;
const provider = providerSelect.options[providerSelect.selectedIndex].value;
const auto_continue = document.getElementById("auto_continue")?.checked;
- if (file && !provider)
- provider = "Bing";
let api_key = null;
if (provider) {
api_key = document.getElementById(`${provider}-api_key`)?.value || null;
@@ -487,7 +492,7 @@ const ask_gpt = async (message_index = -1) => {
api_key = document.querySelector(`.${provider}-api_key`)?.value || null;
}
await api("conversation", {
- id: window.token,
+ id: message_id,
conversation_id: window.conversation_id,
model: get_selected_model(),
web_search: document.getElementById("switch").checked,
@@ -495,11 +500,11 @@ const ask_gpt = async (message_index = -1) => {
messages: messages,
auto_continue: auto_continue,
api_key: api_key
- }, file);
+ }, file, message_index);
if (!error) {
- html = markdown_render(text);
- content_inner.innerHTML = html;
- highlight(content_inner);
+ html = markdown_render(message_storage[message_index]);
+ content_map.inner.innerHTML = html;
+ highlight(content_map.inner);
if (imageInput) imageInput.value = "";
if (cameraInput) cameraInput.value = "";
@@ -509,23 +514,23 @@ const ask_gpt = async (message_index = -1) => {
console.error(e);
if (e.name != "AbortError") {
error = true;
- content_inner.innerHTML += `<p><strong>An error occured:</strong> ${e}</p>`;
+ content_map.inner.innerHTML += `<p><strong>An error occured:</strong> ${e}</p>`;
}
}
- if (!error && text) {
- await add_message(window.conversation_id, "assistant", text, provider_result);
- await load_conversation(window.conversation_id);
+ delete controller_storage[message_index];
+ if (!error && message_storage[message_index]) {
+ const message_provider = message_index in provider_storage ? provider_storage[message_index] : null;
+ await add_message(window.conversation_id, "assistant", message_storage[message_index], message_provider);
+ await safe_load_conversation(window.conversation_id);
} else {
- let cursorDiv = document.getElementById("cursor");
+ let cursorDiv = message_box.querySelector(".cursor");
if (cursorDiv) cursorDiv.parentNode.removeChild(cursorDiv);
}
window.scrollTo(0, 0);
message_box.scrollTop = message_box.scrollHeight;
await remove_cancel_button();
await register_message_buttons();
- prompt_lock = false;
await load_conversations();
- regenerate.classList.remove("regenerate-hidden");
};
const clear_conversations = async () => {
@@ -645,6 +650,10 @@ const load_conversation = async (conversation_id, scroll=true) => {
let conversation = await get_conversation(conversation_id);
let messages = conversation?.items || [];
+ if (!conversation) {
+ return;
+ }
+
if (systemPrompt) {
systemPrompt.value = conversation.system || "";
}
@@ -714,6 +723,19 @@ const load_conversation = async (conversation_id, scroll=true) => {
}
};
+async function safe_load_conversation(conversation_id, scroll=true) {
+ let is_running = false
+ for (const key in controller_storage) {
+ if (!controller_storage[key].signal.aborted) {
+ is_running = true;
+ break
+ }
+ }
+ if (!is_running) {
+ load_conversation(conversation_id, scroll);
+ }
+}
+
async function get_conversation(conversation_id) {
let conversation = await JSON.parse(
appStorage.getItem(`conversation:${conversation_id}`)
@@ -852,23 +874,6 @@ const load_conversations = async () => {
box_conversations.innerHTML += html;
};
-document.getElementById("cancelButton").addEventListener("click", async () => {
- window.controller.abort();
- if (!window.abort) {
- window.abort = true;
- content_inner.innerHTML += " [aborted]";
- if (window.text) window.text += " [aborted]";
- }
- console.log(`aborted ${window.conversation_id}`);
-});
-
-document.getElementById("regenerateButton").addEventListener("click", async () => {
- prompt_lock = true;
- await hide_message(window.conversation_id);
- window.token = message_id();
- await ask_gpt();
-});
-
const hide_input = document.querySelector(".toolbar .hide-input");
hide_input.addEventListener("click", async (e) => {
const icon = hide_input.querySelector("i");
@@ -891,7 +896,7 @@ const uuid = () => {
);
};
-const message_id = () => {
+function get_message_id() {
random_bytes = (Math.floor(Math.random() * 1338377565) + 2956589730).toString(
2
);
@@ -1124,6 +1129,7 @@ async function on_load() {
}
async function on_api() {
+ let prompt_lock = false;
messageInput.addEventListener("keydown", async (evt) => {
if (prompt_lock) return;
@@ -1132,6 +1138,8 @@ async function on_api() {
if (evt.keyCode === 13 && !evt.shiftKey) {
evt.preventDefault();
console.log("pressed enter");
+ prompt_lock = true;
+ setTimeout(()=>prompt_lock=false, 3);
await handle_ask();
} else {
messageInput.style.removeProperty("height");
@@ -1141,6 +1149,8 @@ async function on_api() {
sendButton.addEventListener(`click`, async () => {
console.log("clicked send");
if (prompt_lock) return;
+ prompt_lock = true;
+ setTimeout(()=>prompt_lock=false, 3);
await handle_ask();
});
messageInput.focus();
@@ -1289,7 +1299,7 @@ function get_selected_model() {
}
}
-async function api(ressource, args=null, file=null) {
+async function api(ressource, args=null, file=null, message_index=null) {
if (window?.pywebview) {
if (args !== null) {
if (ressource == "models") {
@@ -1318,17 +1328,17 @@ async function api(ressource, args=null, file=null) {
}
response = await fetch(url, {
method: 'POST',
- signal: window.controller.signal,
+ signal: controller_storage[message_index].signal,
headers: headers,
body: body
});
- return read_response(response);
+ return read_response(response, message_index);
}
response = await fetch(url);
return await response.json();
}
-async function read_response(response) {
+async function read_response(response, message_index) {
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = ""
while (true) {
@@ -1341,7 +1351,7 @@ async function read_response(response) {
continue;
}
try {
- add_message_chunk(JSON.parse(buffer + line))
+ add_message_chunk(JSON.parse(buffer + line), message_index);
buffer = "";
} catch {
buffer += line
@@ -1382,7 +1392,7 @@ providerSelect.addEventListener("change", () => load_provider_models());
function save_storage() {
let filename = `chat ${new Date().toLocaleString()}.json`.replaceAll(":", "-");
let data = {"options": {"g4f": ""}};
- for (let i = 0; i < appStorage.length; i++){label
+ for (let i = 0; i < appStorage.length; i++) {
let key = appStorage.key(i);
let item = appStorage.getItem(key);
if (key.startsWith("conversation:")) {