返回提交历史
Modified
g4f/Provider/Blackbox.py
+1
-8
Modified
g4f/Provider/PollinationsAI.py
+14
-8
Modified
g4f/Provider/hf/HuggingFaceAPI.py
+23
-2
Modified
g4f/Provider/hf/HuggingFaceInference.py
+30
-27
Modified
g4f/Provider/needs_auth/OpenaiChat.py
+19
-10
Added
g4f/Provider/openai/models.py
+6
-0
Modified
g4f/api/__init__.py
+18
-11
Modified
g4f/gui/client/index.html
+16
-6
Modified
g4f/gui/client/static/css/style.css
+4
-3
Modified
g4f/gui/client/static/img/site.webmanifest
+1
-1
Modified
g4f/gui/client/static/js/chat.v1.js
+104
-25
Modified
g4f/gui/server/backend_api.py
+1
-1
Modified
g4f/tools/run_tools.py
+20
-8
XFEstudio/gpt4free
Update model list in OpenaiChat (o3-mini, o3-mini-high) Add Reasoning to OpenaiChat provider Check for pipeline_tag in HuggingChat providers Add image preview in PollinationsAI Add input of custom Model in GUI
167ceedd
代码差异
13 个文件
+257
-110
@@ -308,14 +308,7 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
308
308
image_url = image_url_match.group(1)
309
309
yield ImageResponse(image_url, format_image_prompt(messages, prompt))
310
310
else:
311
if "<think>" in text_to_yield and "</think>" in text_to_yield:
312
parts = text_to_yield.split('<think>', 1)
313
yield parts[0]
314
reasoning_parts = parts[1].split('</think>', 1)
315
yield Reasoning(f"<think>{reasoning_parts[0]}</think>")
316
yield reasoning_parts[1]
317
full_response = text_to_yield
318
elif "Generated by BLACKBOX.AI" in text_to_yield:
311
if "Generated by BLACKBOX.AI" in text_to_yield:
319
312
conversation.validated_value = await cls.fetch_validated(force_refresh=True)
320
313
if conversation.validated_value:
321
314
data["validated"] = conversation.validated_value
@@ -13,7 +13,7 @@ from ..typing import AsyncResult, Messages, ImagesType
13
13
from ..image import to_data_uri
14
14
from ..requests.raise_for_status import raise_for_status
15
15
from ..requests.aiohttp import get_connector
16
from ..providers.response import ImageResponse, FinishReason, Usage
16
from ..providers.response import ImageResponse, ImagePreview, FinishReason, Usage
17
17
18
18
DEFAULT_HEADERS = {
19
19
'Accept': '*/*',
@@ -125,7 +125,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
125
125
# Check if models
126
126
# Image generation
127
127
if model in cls.image_models:
128
yield await cls._generate_image(
128
async for chunk in cls._generate_image(
129
129
model=model,
130
130
prompt=format_image_prompt(messages, prompt),
131
131
proxy=proxy,
@@ -136,7 +136,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
136
136
private=private,
137
137
enhance=enhance,
138
138
safe=safe
139
)
139
):
140
yield chunk
140
141
else:
141
142
# Text generation
142
143
async for result in cls._generate_text(
@@ -167,7 +168,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
167
168
private: bool,
168
169
enhance: bool,
169
170
safe: bool
170
) -> ImageResponse:
171
) -> AsyncResult:
171
172
params = {
172
173
"seed": seed,
173
174
"width": width,
@@ -178,11 +179,16 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
178
179
"enhance": enhance,
179
180
"safe": safe
180
181
}
181
params = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in params.items() if v is not None}
182
params = {k: json.dumps(v) if isinstance(v, bool) else str(v) for k, v in params.items() if v is not None}
183
params = "&".join( "%s=%s" % (key, quote_plus(params[key]))
184
for key in params.keys())
185
url = f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}?{params}"
186
yield ImagePreview(url, prompt)
182
187
async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
183
async with session.head(f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}", params=params) as response:
184
await raise_for_status(response)
185
return ImageResponse(str(response.url), prompt)
188
async with session.head(url) as response:
189
if response.status != 500: # Server is busy
190
await raise_for_status(response)
191
yield ImageResponse(str(response.url), prompt)
186
192
187
193
@classmethod
188
194
async def _generate_text(
@@ -1,8 +1,11 @@
1
1
from __future__ import annotations
2
2
3
from ...providers.types import Messages
4
from ...typing import ImagesType
5
from ...requests import StreamSession, raise_for_status
6
from ...errors import ModelNotSupportedError
3
7
from ..template.OpenaiTemplate import OpenaiTemplate
4
8
from .models import model_aliases
5
from ...providers.types import Messages
6
9
from .HuggingChat import HuggingChat
7
10
from ... import debug
8
11
@@ -37,6 +40,10 @@ class HuggingFaceAPI(OpenaiTemplate):
37
40
api_base: str = None,
38
41
max_tokens: int = 2048,
39
42
max_inputs_lenght: int = 10000,
43
impersonate: str = None,
44
proxy: str = None,
45
timeout: int = 300,
46
images: ImagesType = None,
40
47
**kwargs
41
48
):
42
49
if api_base is None:
@@ -44,6 +51,20 @@ class HuggingFaceAPI(OpenaiTemplate):
44
51
if model in cls.model_aliases:
45
52
model_name = cls.model_aliases[model]
46
53
api_base = f"https://api-inference.huggingface.co/models/{model_name}/v1"
54
if images is not None:
55
async with StreamSession(
56
proxy=proxy,
57
timeout=timeout,
58
impersonate=impersonate,
59
) as session:
60
async with session.get(f"https://huggingface.co/api/models/{model}") as response:
61
if response.status == 404:
62
raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__}")
63
await raise_for_status(response)
64
model_data = await response.json()
65
pipeline_tag = model_data.get("pipeline_tag")
66
if pipeline_tag != "image-text-to-text":
67
raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__} pipeline_tag={pipeline_tag}")
47
68
start = calculate_lenght(messages)
48
69
if start > max_inputs_lenght:
49
70
if len(messages) > 6:
@@ -54,7 +75,7 @@ class HuggingFaceAPI(OpenaiTemplate):
54
75
if len(messages) > 1 and calculate_lenght(messages) > max_inputs_lenght:
55
76
messages = [messages[-1]]
56
77
debug.log(f"Messages trimmed from: {start} to: {calculate_lenght(messages)}")
57
async for chunk in super().create_async_generator(model, messages, api_base=api_base, max_tokens=max_tokens, **kwargs):
78
async for chunk in super().create_async_generator(model, messages, api_base=api_base, max_tokens=max_tokens, images=images, **kwargs):
58
79
yield chunk
59
80
60
81
def calculate_lenght(messages: Messages) -> int:
@@ -78,18 +78,13 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
78
78
if api_key is not None:
79
79
headers["Authorization"] = f"Bearer {api_key}"
80
80
payload = None
81
if cls.get_models() and model in cls.image_models:
82
stream = False
83
prompt = format_image_prompt(messages, prompt)
84
payload = {"inputs": prompt, "parameters": {"seed": random.randint(0, 2**32), **extra_data}}
85
else:
86
params = {
87
"return_full_text": False,
88
"max_new_tokens": max_tokens,
89
"temperature": temperature,
90
**extra_data
91
}
92
do_continue = action == "continue"
81
params = {
82
"return_full_text": False,
83
"max_new_tokens": max_tokens,
84
"temperature": temperature,
85
**extra_data
86
}
87
do_continue = action == "continue"
93
88
async with StreamSession(
94
89
headers=headers,
95
90
proxy=proxy,
@@ -101,22 +96,30 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
101
96
raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__}")
102
97
await raise_for_status(response)
103
98
model_data = await response.json()
104
model_type = None
105
if "config" in model_data and "model_type" in model_data["config"]:
106
model_type = model_data["config"]["model_type"]
107
debug.log(f"Model type: {model_type}")
108
inputs = get_inputs(messages, model_data, model_type, do_continue)
109
debug.log(f"Inputs len: {len(inputs)}")
110
if len(inputs) > 4096:
111
if len(messages) > 6:
112
messages = messages[:3] + messages[-3:]
113
else:
114
messages = [m for m in messages if m["role"] == "system"] + [messages[-1]]
99
pipeline_tag = model_data.get("pipeline_tag")
100
if pipeline_tag == "text-to-image":
101
stream = False
102
inputs = format_image_prompt(messages, prompt)
103
payload = {"inputs": inputs, "parameters": {"seed": random.randint(0, 2**32), **extra_data}}
104
elif pipeline_tag in ("text-generation", "image-text-to-text"):
105
model_type = None
106
if "config" in model_data and "model_type" in model_data["config"]:
107
model_type = model_data["config"]["model_type"]
108
debug.log(f"Model type: {model_type}")
115
109
inputs = get_inputs(messages, model_data, model_type, do_continue)
116
debug.log(f"New len: {len(inputs)}")
117
if model_type == "gpt2" and max_tokens >= 1024:
118
params["max_new_tokens"] = 512
119
payload = {"inputs": inputs, "parameters": params, "stream": stream}
110
debug.log(f"Inputs len: {len(inputs)}")
111
if len(inputs) > 4096:
112
if len(messages) > 6:
113
messages = messages[:3] + messages[-3:]
114
else:
115
messages = [m for m in messages if m["role"] == "system"] + [messages[-1]]
116
inputs = get_inputs(messages, model_data, model_type, do_continue)
117
debug.log(f"New len: {len(inputs)}")
118
if model_type == "gpt2" and max_tokens >= 1024:
119
params["max_new_tokens"] = 512
120
payload = {"inputs": inputs, "parameters": params, "stream": stream}
121
else:
122
raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__} pipeline_tag: {pipeline_tag}")
120
123
121
124
async with session.post(f"{api_base.rstrip('/')}/models/{model}", json=payload) as response:
122
125
if response.status == 404:
@@ -25,8 +25,9 @@ from ...requests import get_nodriver
25
25
from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
26
26
from ...errors import MissingAuthError, NoValidHarFileError
27
27
from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult
28
from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters
28
from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters, Reasoning
29
29
from ..helper import format_cookies
30
from ..openai.models import default_model, default_image_model, models, image_models, text_models
30
31
from ..openai.har_file import get_request_config
31
32
from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, backend_anon_url
32
33
from ..openai.proofofwork import generate_proof_token
@@ -95,12 +96,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
95
96
supports_gpt_4 = True
96
97
supports_message_history = True
97
98
supports_system_message = True
98
default_model = "auto"
99
default_image_model = "dall-e-3"
100
image_models = [default_image_model]
101
text_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "o1", "o1-preview", "o1-mini"]
99
default_model = default_model
100
default_image_model = default_image_model
101
image_models = image_models
102
102
vision_models = text_models
103
models = text_models + image_models
103
models = models
104
104
synthesize_content_type = "audio/mpeg"
105
105
106
106
_api_key: str = None
@@ -368,9 +368,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
368
368
)
369
369
[debug.log(text) for text in (
370
370
#f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
371
f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
372
f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
371
#f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
372
#f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
373
373
)]
374
if action == "continue" and conversation.message_id is None:
375
action = "next"
374
376
data = {
375
377
"action": action,
376
378
"parent_message_id": conversation.message_id,
@@ -497,7 +499,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
497
499
v = line.get("v")
498
500
if isinstance(v, str) and fields.is_recipient:
499
501
if "p" not in line or line.get("p") == "/message/content/parts/0":
500
yield v
502
yield Reasoning(token=v) if fields.is_thinking else v
501
503
elif isinstance(v, list):
502
504
for m in v:
503
505
if m.get("p") == "/message/content/parts/0" and fields.is_recipient:
@@ -508,6 +510,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
508
510
sources.add_source(link)
509
511
elif re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
510
512
sources.add_source(m.get("v"))
513
elif m.get("p") == "/message/metadata/finished_text":
514
fields.is_thinking = False
515
yield Reasoning(status=m.get("v"))
511
516
elif m.get("p") == "/message/metadata":
512
517
fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type")
513
518
break
@@ -519,6 +524,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
519
524
fields.is_recipient = m.get("recipient", "all") == "all"
520
525
if fields.is_recipient:
521
526
c = m.get("content", {})
527
if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}):
528
fields.is_thinking = True
529
yield Reasoning(status=c.get("metadata", {}).get("initial_text"))
522
530
if c.get("content_type") == "multimodal_text":
523
531
generated_images = []
524
532
for element in c.get("parts"):
@@ -697,13 +705,14 @@ class Conversation(JsonConversation):
697
705
"""
698
706
Class to encapsulate response fields.
699
707
"""
700
def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None):
708
def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None, is_thinking: bool = False):
701
709
self.conversation_id = conversation_id
702
710
self.message_id = message_id
703
711
self.finish_reason = finish_reason
704
712
self.is_recipient = False
705
713
self.parent_message_id = message_id if parent_message_id is None else parent_message_id
706
714
self.user_id = user_id
715
self.is_thinking = is_thinking
707
716
708
717
def get_cookies(
709
718
urls: Optional[Iterator[str]] = None
@@ -0,0 +1,6 @@
1
default_model = "auto"
2
default_image_model = "dall-e-3"
3
image_models = [default_image_model]
4
text_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "o1", "o1-preview", "o1-mini", "o3-mini", "o3-mini-high"]
5
vision_models = text_models
6
models = text_models + image_models
@@ -10,6 +10,7 @@ from email.utils import formatdate
10
10
import os.path
11
11
import hashlib
12
12
import asyncio
13
from urllib.parse import quote_plus
13
14
from fastapi import FastAPI, Response, Request, UploadFile, Depends
14
15
from fastapi.middleware.wsgi import WSGIMiddleware
15
16
from fastapi.responses import StreamingResponse, RedirectResponse, HTMLResponse, JSONResponse
@@ -176,11 +177,11 @@ class Api:
176
177
return ErrorResponse.from_message("G4F API key required", HTTP_401_UNAUTHORIZED)
177
178
if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
178
179
return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
179
elif not AppConfig.demo:
180
if user_g4f_api_key is not None and path.startswith("/images/"):
180
elif not AppConfig.demo and not path.startswith("/images/"):
181
if user_g4f_api_key is not None:
181
182
if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
182
183
return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
183
elif path.startswith("/backend-api/") or path.startswith("/images/") or path.startswith("/chat/") and path != "/chat/":
184
elif path.startswith("/backend-api/") or path.startswith("/chat/") and path != "/chat/":
184
185
try:
185
186
username = await self.get_username(request)
186
187
except HTTPException as e:
@@ -551,8 +552,8 @@ class Api:
551
552
HTTP_404_NOT_FOUND: {}
552
553
})
553
554
async def get_image(filename, request: Request):
554
target = os.path.join(images_dir, filename)
555
ext = os.path.splitext(filename)[1]
555
target = os.path.join(images_dir, quote_plus(filename))
556
ext = os.path.splitext(filename)[1][1:]
556
557
stat_result = SimpleNamespace()
557
558
stat_result.st_size = 0
558
559
if os.path.isfile(target):
@@ -560,10 +561,12 @@ class Api:
560
561
stat_result.st_mtime = int(f"{filename.split('_')[0]}") if filename.startswith("1") else 0
561
562
headers = {
562
563
"cache-control": "public, max-age=31536000",
563
"content-type": f"image/{ext.replace('jpg', 'jpeg')[1:] or 'jpeg'}",
564
"content-length": str(stat_result.st_size),
564
"content-type": f"image/{ext.replace('jpg', 'jpeg') or 'jpeg'}",
565
565
"last-modified": formatdate(stat_result.st_mtime, usegmt=True),
566
566
"etag": f'"{hashlib.md5(filename.encode()).hexdigest()}"',
567
**({
568
"content-length": str(stat_result.st_size),
569
} if stat_result.st_size else {})
567
570
}
568
571
response = FileResponse(
569
572
target,
@@ -583,10 +586,14 @@ class Api:
583
586
source_url = source_url[1]
584
587
source_url = source_url.replace("%2F", "/").replace("%3A", ":").replace("%3F", "?").replace("%3D", "=")
585
588
if source_url.startswith("https://"):
586
await copy_images(
587
[source_url],
588
target=target)
589
debug.log(f"Image copied from {source_url}")
589
try:
590
await copy_images(
591
[source_url],
592
target=target)
593
debug.log(f"Image copied from {source_url}")
594
except Exception as e:
595
debug.log(f"{type(e).__name__}: Download failed: {source_url}\n{e}")
596
return RedirectResponse(url=source_url)
590
597
if not os.path.isfile(target):
591
598
return ErrorResponse.from_message("File not found", HTTP_404_NOT_FOUND)
592
599
async def stream():
@@ -47,12 +47,13 @@
47
47
gallery: '#messages',
48
48
children: 'a:has(img)',
49
49
secondaryZoomLevel: 2,
50
allowPanToNext: true,
50
51
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe'),
51
52
});
52
53
lightbox.addFilter('itemData', (itemData, index) => {
53
54
const img = itemData.element.querySelector('img');
54
itemData.width = img.naturalWidth;
55
itemData.height = img.naturalHeight;
55
itemData.width = img.naturalWidth || 1024;
56
itemData.height = img.naturalHeight || 1024;
56
57
return itemData;
57
58
});
58
59
lightbox.on('uiRegister', function() {
@@ -66,7 +67,13 @@
66
67
lightbox.pswp.on('change', () => {
67
68
const currSlideElement = lightbox.pswp.currSlide.data.element;
68
69
if (currSlideElement) {
69
el.innerText = currSlideElement.querySelector('img').getAttribute('alt');
70
const img = currSlideElement.querySelector('img');
71
el.innerText = img.getAttribute('alt');
72
const download = document.createElement("a");
73
download.setAttribute("href", img.getAttribute('src'));
74
download.setAttribute("download", `${img.getAttribute('alt')}${lightbox.pswp.currSlide.index}.jpg`);
75
download.innerHTML = '<i class="fa-solid fa-download"></i>';
76
el.appendChild(download);
70
77
}
71
78
});
72
79
}
@@ -157,8 +164,8 @@
157
164
<label for="report_error" class="toogle" title=""></label>
158
165
</div>
159
166
<div class="field box">
160
<label for="systemPrompt" class="label" title="">System prompt</label>
161
<textarea id="systemPrompt" placeholder="You are a helpful assistant."></textarea>
167
<label for="systemPrompt" class="label">System prompt</label>
168
<textarea id="systemPrompt" placeholder="You are a helpful assistant." data-value="If you need to generate images, you can use the following format: . This will enable the use of an image generation tool."></textarea>
162
169
</div>
163
170
<div class="field box">
164
171
<label for="message-input-height" class="label" title="">Input max. height</label>
@@ -269,6 +276,7 @@
269
276
<div id="send-button">
270
277
<i class="fa-solid fa-square-plus"></i>
271
278
<i class="fa-regular fa-paper-plane"></i>
279
<a href="" id="download" class="hidden"></a>
272
280
</div>
273
281
</div>
274
282
</div>
@@ -293,7 +301,8 @@
293
301
<option value="dall-e-3">dall-e-3 (Image Generation)</option>
294
302
<option disabled="disabled">----</option>
295
303
</select>
296
<select name="model2" id="model2" class="hidden"></select>
304
<select name="model2" id="model2" class="hidden model"></select>
305
<input type="text" id="model3" value="" class="hidden model" placeholder="Model:"/>
297
306
</div>
298
307
<div class="field">
299
308
<select name="provider" id="provider">
@@ -303,6 +312,7 @@
303
312
<option value="Gemini">Google Gemini</option>
304
313
<option value="DDG">DuckDuckGo AI Chat</option>
305
314
<option value="Blackbox">Blackbox AI</option>
315
<option value="Custom Model">Custom Model</option>
306
316
<option disabled="disabled">----</option>
307
317
</select>
308
318
</div>
@@ -799,7 +799,7 @@ form input:checked+label:after {
799
799
color: var(--colour-3);
800
800
}
801
801
802
select {
802
select, input.model {
803
803
border-radius: 8px;
804
804
backdrop-filter: blur(20px);
805
805
cursor: pointer;
@@ -871,6 +871,7 @@ button.regenerate_button, button.continue_button, button.options_button {
871
871
}
872
872
873
873
select:hover,
874
input.model:hover
874
875
.buttons button:hover,
875
876
.stop_generating button:hover,
876
877
.toolbar .regenerate button:hover,
@@ -948,7 +949,7 @@ select:hover,
948
949
}
949
950
950
951
@media only screen and (min-width: 40em) {
951
select {
952
select, input.model {
952
953
width: 200px;
953
954
}
954
955
.field {
@@ -1446,7 +1447,7 @@ form .field.saved .fa-xmark {
1446
1447
max-height: 200px;
1447
1448
}
1448
1449
1449
.hidden {
1450
.hidden, input.hidden {
1450
1451
display: none;
1451
1452
}
1452
1453
@@ -17,7 +17,7 @@
17
17
"background_color": "#ffffff",
18
18
"display": "standalone",
19
19
"share_target": {
20
"action": "/chat/",
20
"action": "/chat/share",
21
21
"method": "GET",
22
22
"enctype": "application/x-www-form-urlencoded",
23
23
"params": {
@@ -15,6 +15,7 @@ const inputCount = document.getElementById("input-count").querySelector("
15
15
const providerSelect = document.getElementById("provider");
16
16
const modelSelect = document.getElementById("model");
17
17
const modelProvider = document.getElementById("model2");
18
const custom_model = document.getElementById("model3");
18
19
const chatPrompt = document.getElementById("chatPrompt");
19
20
const settings = document.querySelector(".settings");
20
21
const chat = document.querySelector(".conversation");
@@ -78,18 +79,28 @@ function render_reasoning(reasoning, final = false) {
78
79
</div>` : "";
79
80
return `<div class="reasoning_body">
80
81
<div class="reasoning_title">
81
<strong>Reasoning <i class="fa-solid fa-brain"></i>:</strong> ${escapeHtml(reasoning.status)}
82
<strong>Reasoning <i class="brain">🧠</i>:</strong> ${escapeHtml(reasoning.status)}
82
83
</div>
83
84
${inner_text}
84
85
</div>`;
85
86
}
86
87
88
function render_reasoning_text(reasoning) {
89
return `Reasoning 🧠: ${reasoning.status}\n\n${reasoning.text}\n\n`;
90
}
91
87
92
function filter_message(text) {
88
93
return text.replaceAll(
89
94
/<!-- generated images start -->[\s\S]+<!-- generated images end -->/gm, ""
90
95
).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
91
96
}
92
97
98
function filter_message_content(text) {
99
return text.replaceAll(
100
/\/\]\(\/generate\//gm, "/](/images/"
101
).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "")
102
}
103
93
104
function fallback_clipboard (text) {
94
105
var textBox = document.createElement("textarea");
95
106
textBox.value = text;
@@ -182,6 +193,53 @@ const get_message_el = (el) => {
182
193
return message_el;
183
194
}
184
195
196
function register_message_images() {
197
message_box.querySelectorAll(`.loading-indicator`).forEach((el) => el.remove());
198
message_box.querySelectorAll(`.message img:not([alt="your avatar"])`).forEach(async (el) => {
199
if (!el.complete) {
200
const indicator = document.createElement("span");
201
indicator.classList.add("loading-indicator");
202
indicator.innerHTML = `<i class="fas fa-spinner fa-spin"></i>`;
203
el.parentElement.appendChild(indicator);
204
el.onerror = () => {
205
let indexCommand;
206
if ((indexCommand = el.src.indexOf("/generate/")) >= 0) {
207
indexCommand = indexCommand + "/generate/".length + 1;
208
let newPath = el.src.substring(indexCommand)
209
let filename = newPath.replace(/(?:\?.+?|$)/, "");
210
let seed = Math.floor(Date.now() / 1000);
211
newPath = `https://image.pollinations.ai/prompt/${newPath}?seed=${seed}&nologo=true`;
212
let downloadUrl = newPath;
213
if (document.getElementById("download_images")?.checked) {
214
downloadUrl = `/images/${filename}?url=${escapeHtml(newPath)}`;
215
}
216
const link = document.createElement("a");
217
link.setAttribute("href", newPath);
218
const newImg = document.createElement("img");
219
newImg.src = downloadUrl;
220
newImg.alt = el.alt;
221
newImg.onload = () => {
222
lazy_scroll_to_bottom();
223
indicator.remove();
224
}
225
link.appendChild(newImg);
226
el.parentElement.appendChild(link);
227
} else {
228
const span = document.createElement("span");
229
span.innerHTML = `<i class="fa-solid fa-plug"></i>${escapeHtml(el.alt)}`;
230
el.parentElement.appendChild(span);
231
}
232
el.remove();
233
indicator.remove();
234
}
235
el.onload = () => {
236
indicator.remove();
237
lazy_scroll_to_bottom();
238
}
239
}
240
});
241
}
242
185
243
const register_message_buttons = async () => {
186
244
message_box.querySelectorAll(".message .content .provider").forEach(async (el) => {
187
245
if (!("click" in el.dataset)) {
@@ -243,24 +301,22 @@ const register_message_buttons = async () => {
243
301
message_box.querySelectorAll(".message .fa-file-export").forEach(async (el) => {
244
302
if (!("click" in el.dataset)) {
245
303
el.dataset.click = "true";
304
//
246
305
el.addEventListener("click", async () => {
247
306
const elem = window.document.createElement('a');
248
307
let filename = `chat ${new Date().toLocaleString()}.md`.replaceAll(":", "-");
249
308
const conversation = await get_conversation(window.conversation_id);
250
309
let buffer = "";
251
310
conversation.items.forEach(message => {
311
buffer += render_reasoning_text(message.reasoning);
252
312
buffer += `${message.role == 'user' ? 'User' : 'Assistant'}: ${message.content.trim()}\n\n\n`;
253
313
});
254
const file = new File([buffer.trim()], 'message.md', {type: 'text/plain'});
255
const objectUrl = URL.createObjectURL(file);
256
elem.href = objectUrl;
257
elem.download = filename;
258
document.body.appendChild(elem);
259
elem.click();
260
document.body.removeChild(elem);
314
var download = document.getElementById("download");
315
download.setAttribute("href", "data:text/markdown;charset=utf-8," + encodeURIComponent(buffer.trim()));
316
download.setAttribute("download", filename);
317
download.click();
261
318
el.classList.add("clicked");
262
319
setTimeout(() => el.classList.remove("clicked"), 1000);
263
URL.revokeObjectURL(objectUrl);
264
320
})
265
321
}
266
322
});
@@ -376,7 +432,7 @@ const handle_ask = async (do_ask_gpt = true) => {
376
432
messageInput.focus();
377
433
await scroll_to_bottom();
378
434
379
let message = messageInput.value;
435
let message = messageInput.value.trim();
380
436
if (message.length <= 0) {
381
437
return;
382
438
}
@@ -755,6 +811,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
755
811
if (!img.complete)
756
812
return;
757
813
content_map.inner.innerHTML = markdown_render(message.preview);
814
await register_message_images();
758
815
} else if (message.type == "content") {
759
816
message_storage[message_id] += message.content;
760
817
update_message(content_map, message_id, null, scroll);
@@ -779,7 +836,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
779
836
} else if (message.type == "reasoning") {
780
837
if (!reasoning_storage[message_id]) {
781
838
reasoning_storage[message_id] = message;
782
reasoning_storage[message_id].text = message.token || "";
839
reasoning_storage[message_id].text = "";
783
840
} else if (message.status) {
784
841
reasoning_storage[message_id].status = message.status;
785
842
} else if (message.token) {
@@ -952,6 +1009,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
952
1009
}
953
1010
await safe_remove_cancel_button();
954
1011
await register_message_buttons();
1012
await register_message_images();
955
1013
await load_conversations();
956
1014
regenerate_button.classList.remove("regenerate-hidden");
957
1015
}
@@ -1201,8 +1259,8 @@ const load_conversation = async (conversation_id, scroll=true) => {
1201
1259
} else {
1202
1260
buffer = "";
1203
1261
}
1204
buffer = buffer.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
1205
new_content = item.content.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
1262
buffer = filter_message_content(buffer);
1263
new_content = filter_message_content(item.content);
1206
1264
buffer = merge_messages(buffer, new_content);
1207
1265
last_model = item.provider?.model;
1208
1266
providers.push(item.provider?.name);
@@ -1658,12 +1716,9 @@ const register_settings_storage = async () => {
1658
1716
const load_settings_storage = async () => {
1659
1717
const optionElements = document.querySelectorAll(optionElementsSelector);
1660
1718
optionElements.forEach((element) => {
1661
if (element.name && element.name != element.id && (value = appStorage.getItem(element.name))) {
1662
appStorage.setItem(element.id, value);
1663
appStorage.removeItem(element.name);
1664
}
1665
if (!(value = appStorage.getItem(element.id))) {
1666
return;
1719
value = appStorage.getItem(element.id);
1720
if (value == null && element.dataset.value) {
1721
value = element.dataset.value;
1667
1722
}
1668
1723
if (value) {
1669
1724
switch (element.type) {
@@ -1677,10 +1732,10 @@ const load_settings_storage = async () => {
1677
1732
case "number":
1678
1733
case "textarea":
1679
1734
if (element.id.endsWith("-api_key")) {
1680
element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value.length);
1735
element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value ? value.length : 0);
1681
1736
element.dataset.value = value;
1682
1737
} else {
1683
element.value = value;
1738
element.value = value == null ? element.dataset.value : value;
1684
1739
}
1685
1740
break;
1686
1741
default:
@@ -1834,7 +1889,7 @@ async function on_load() {
1834
1889
let chat_url = new URL(window.location.href)
1835
1890
let chat_params = new URLSearchParams(chat_url.search);
1836
1891
if (chat_params.get("prompt")) {
1837
messageInput.value = `${chat_params.title}\n${chat_params.prompt}\n${chat_params.url}`.trim();
1892
messageInput.value = `${window.location.href}\n`;
1838
1893
messageInput.style.height = messageInput.scrollHeight + "px";
1839
1894
messageInput.focus();
1840
1895
//await handle_ask();
@@ -2255,7 +2310,9 @@ chatPrompt?.addEventListener("input", async () => {
2255
2310
});
2256
2311
2257
2312
function get_selected_model() {
2258
if (modelProvider.selectedIndex >= 0) {
2313
if (custom_model.value) {
2314
return custom_model;
2315
} else if (modelProvider.selectedIndex >= 0) {
2259
2316
return modelProvider.options[modelProvider.selectedIndex];
2260
2317
} else if (modelSelect.selectedIndex >= 0) {
2261
2318
model = modelSelect.options[modelSelect.selectedIndex];
@@ -2401,17 +2458,31 @@ async function load_provider_models(provider=null) {
2401
2458
if (!provider) {
2402
2459
provider = providerSelect.value;
2403
2460
}
2461
if (!custom_model.value) {
2462
custom_model.classList.add("hidden");
2463
}
2464
if (provider == "Custom Model" || custom_model.value) {
2465
modelProvider.classList.add("hidden");
2466
modelSelect.classList.add("hidden");
2467
document.getElementById("model3").classList.remove("hidden");
2468
return;
2469
}
2404
2470
modelProvider.innerHTML = '';
2405
2471
modelProvider.name = `model[${provider}]`;
2406
2472
if (!provider) {
2407
2473
modelProvider.classList.add("hidden");
2408
2474
modelSelect.classList.remove("hidden");
2475
document.getElementById("model3").value = "";
2476
document.getElementById("model3").classList.remove("hidden");
2409
2477
return;
2410
2478
}
2411
2479
const models = await api('models', provider);
2412
2480
if (models && models.length > 0) {
2413
2481
modelSelect.classList.add("hidden");
2414
modelProvider.classList.remove("hidden");
2482
if (!custom_model.value) {
2483
custom_model.classList.add("hidden");
2484
modelProvider.classList.remove("hidden");
2485
}
2415
2486
let defaultIndex = 0;
2416
2487
models.forEach((model, i) => {
2417
2488
let option = document.createElement('option');
@@ -2423,11 +2494,13 @@ async function load_provider_models(provider=null) {
2423
2494
defaultIndex = i;
2424
2495
}
2425
2496
});
2426
modelProvider.selectedIndex = defaultIndex;
2427
2497
let value = appStorage.getItem(modelProvider.name);
2428
2498
if (value) {
2429
2499
modelProvider.value = value;
2430
2500
}
2501
modelProvider.selectedIndex = defaultIndex;
2502
} else if (custom_model.value) {
2503
modelSelect.classList.add("hidden");
2431
2504
} else {
2432
2505
modelProvider.classList.add("hidden");
2433
2506
modelSelect.classList.remove("hidden");
@@ -2439,6 +2512,12 @@ providerSelect.addEventListener("change", () => {
2439
2512
});
2440
2513
modelSelect.addEventListener("change", () => messageInput.focus());
2441
2514
modelProvider.addEventListener("change", () => messageInput.focus());
2515
custom_model.addEventListener("change", () => {
2516
if (!custom_model.value) {
2517
load_provider_models();
2518
}
2519
messageInput.focus();
2520
});
2442
2521
2443
2522
document.getElementById("pin").addEventListener("click", async () => {
2444
2523
const pin_container = document.getElementById("pin_container");
@@ -276,7 +276,7 @@ class Backend_Api(Api):
276
276
response = iter_run_tools(ChatCompletion.create, **parameters)
277
277
278
278
if do_filter_markdown:
279
return Response(filter_markdown(response, do_filter_markdown), mimetype='text/plain')
279
return Response(filter_markdown("".join([str(chunk) for chunk in response]), do_filter_markdown), mimetype='text/plain')
280
280
def cast_str():
281
281
for chunk in response:
282
282
if not isinstance(chunk, Exception):