XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

Add chat share function

ae1fae7e
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

14 个文件 +239 -88
Modified g4f/Provider/ARTA.py +6 -4
@@ -176,6 +176,7 @@ class ARTA(AsyncGeneratorProvider, ProviderModelMixin):
176 176 # Step 3: Check Generation Status
177 177 status_url = cls.status_check_url.format(record_id=record_id)
178 178 counter = 0
179 start_time = time.time()
179 180 while True:
180 181 async with session.get(status_url, headers=headers, proxy=proxy) as status_response:
181 182 status_data = await status_response.json()
@@ -183,14 +184,15 @@ class ARTA(AsyncGeneratorProvider, ProviderModelMixin):
183 184
184 185 if status == "DONE":
185 186 image_urls = [image["url"] for image in status_data.get("response", [])]
186 yield Reasoning(status="Finished")
187 duration = time.time() - start_time
188 yield Reasoning(label="Generated", status=f"{n} image(s) in {duration:.2f}s")
187 189 yield ImageResponse(images=image_urls, alt=prompt)
188 190 return
189 191 elif status in ("IN_QUEUE", "IN_PROGRESS"):
190 yield Reasoning(status=("Waiting" if status == "IN_QUEUE" else "Generating") + "." * counter)
191 await asyncio.sleep(2) # Poll every 5 seconds
192 yield Reasoning(label=("Waiting" if status == "IN_QUEUE" else "Generating"), status="." * counter)
193 await asyncio.sleep(2) # Poll every 2 seconds
192 194 counter += 1
193 195 if counter > 3:
194 counter = 0
196 counter = 1
195 197 else:
196 198 raise ResponseError(f"Image generation failed with status: {status}")
Modified g4f/Provider/You.py +2 -2
@@ -7,7 +7,7 @@ import uuid
7 7 from ..typing import AsyncResult, Messages, ImageType, Cookies
8 8 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
9 9 from .helper import format_prompt
10 from ..image import EXTENSIONS_MAP, to_bytes, is_accepted_format
10 from ..image import MEDIA_TYPE_MAP, to_bytes, is_accepted_format
11 11 from ..requests import StreamSession, FormData, raise_for_status, get_nodriver
12 12 from ..providers.response import ImagePreview, ImageResponse
13 13 from ..cookies import get_cookies
@@ -159,7 +159,7 @@ class You(AsyncGeneratorProvider, ProviderModelMixin):
159 159 upload_nonce = await response.text()
160 160 data = FormData()
161 161 content_type = is_accepted_format(file)
162 filename = f"image.{EXTENSIONS_MAP[content_type]}" if filename is None else filename
162 filename = f"image.{MEDIA_TYPE_MAP[content_type]}" if filename is None else filename
163 163 data.add_field('file', file, content_type=content_type, filename=filename)
164 164 async with client.post(
165 165 f"{cls.url}/api/upload",
Modified g4f/Provider/hf/HuggingFaceMedia.py +3 -1
@@ -202,7 +202,9 @@ class HuggingFaceMedia(AsyncGeneratorProvider, ProviderModelMixin):
202 202 background_tasks.add(task)
203 203 task.add_done_callback(background_tasks.discard)
204 204 while background_tasks:
205 yield Reasoning(label="Generating", status=f"{time.time() - started:.2f}s")
205 diff = time.time() - started
206 if diff > 1:
207 yield Reasoning(label="Generating", status=f"{diff:.2f}s")
206 208 await asyncio.sleep(0.2)
207 209 provider_info, media_response = await task
208 210 yield Reasoning(label="Finished", status=f"{time.time() - started:.2f}s")
Modified g4f/Provider/needs_auth/DeepSeekAPI.py +2 -2
@@ -72,12 +72,12 @@ class DeepSeekAPI(AsyncAuthedProvider, ProviderModelMixin):
72 72 ):
73 73 if chunk['type'] == 'thinking':
74 74 if not is_thinking:
75 yield Reasoning(None, "Is thinking...")
75 yield Reasoning(status="Is thinking...")
76 76 is_thinking = time.time()
77 77 yield Reasoning(chunk['content'])
78 78 elif chunk['type'] == 'text':
79 79 if is_thinking:
80 yield Reasoning(None, f"Thought for {time.time() - is_thinking:.2f}s")
80 yield Reasoning(status=f"Thought for {time.time() - is_thinking:.2f}s")
81 81 is_thinking = 0
82 82 if chunk['content']:
83 83 yield chunk['content']
Modified g4f/api/__init__.py +18 -10
@@ -38,7 +38,7 @@ import g4f.debug
38 38 from g4f.client import AsyncClient, ChatCompletion, ImagesResponse, convert_to_provider
39 39 from g4f.providers.response import BaseConversation, JsonConversation
40 40 from g4f.client.helper import filter_none
41 from g4f.image import is_data_an_media
41 from g4f.image import is_data_an_media, EXTENSIONS_MAP
42 42 from g4f.image.copy_images import images_dir, copy_media, get_source_url
43 43 from g4f.errors import ProviderNotFoundError, ModelNotFoundError, MissingAuthError, NoValidHarFileError
44 44 from g4f.cookies import read_cookie_files, get_cookies_dir
@@ -179,7 +179,7 @@ class Api:
179 179 return ErrorResponse.from_message("G4F API key required", HTTP_401_UNAUTHORIZED)
180 180 if AppConfig.g4f_api_key is None or not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
181 181 return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
182 elif not AppConfig.demo and not path.startswith("/images/"):
182 elif not AppConfig.demo and not path.startswith("/images/") and not path.startswith("/media/"):
183 183 if user_g4f_api_key is not None:
184 184 if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
185 185 return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
@@ -189,7 +189,7 @@ class Api:
189 189 except HTTPException as e:
190 190 return ErrorResponse.from_message(e.detail, e.status_code, e.headers)
191 191 response = await call_next(request)
192 response.headers["X-Username"] = username
192 response.headers["x-user"] = username
193 193 return response
194 194 return await call_next(request)
195 195
@@ -220,7 +220,7 @@ class Api:
220 220 return HTMLResponse('g4f API: Go to '
221 221 '<a href="/v1/models">models</a>, '
222 222 '<a href="/v1/chat/completions">chat/completions</a>, or '
223 '<a href="/v1/images/generate">images/generate</a> <br><br>'
223 '<a href="/v1/media/generate">media/generate</a> <br><br>'
224 224 'Open Swagger UI at: '
225 225 '<a href="/docs">/docs</a>')
226 226
@@ -259,7 +259,7 @@ class Api:
259 259 provider: ProviderType = ProviderUtils.convert[provider]
260 260 if not hasattr(provider, "get_models"):
261 261 models = []
262 elif credentials is not None:
262 elif credentials is not None and credentials.credentials != "secret":
263 263 models = provider.get_models(api_key=credentials.credentials)
264 264 else:
265 265 models = provider.get_models()
@@ -404,6 +404,7 @@ class Api:
404 404 HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
405 405 HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
406 406 }
407 @self.app.post("/v1/media/generate", responses=responses)
407 408 @self.app.post("/v1/images/generate", responses=responses)
408 409 @self.app.post("/v1/images/generations", responses=responses)
409 410 async def generate_image(
@@ -555,9 +556,14 @@ class Api:
555 556 HTTP_200_OK: {"content": {"image/*": {}}},
556 557 HTTP_404_NOT_FOUND: {}
557 558 })
558 async def get_image(filename, request: Request):
559 @self.app.get("/media/{filename}", responses={
560 HTTP_200_OK: {"content": {"image/*": {}, "audio/*": {}}, "video/*": {}},
561 HTTP_404_NOT_FOUND: {}
562 })
563 async def get_media(filename, request: Request):
559 564 target = os.path.join(images_dir, os.path.basename(filename))
560 565 ext = os.path.splitext(filename)[1][1:]
566 mime_type = EXTENSIONS_MAP.get(ext)
561 567 stat_result = SimpleNamespace()
562 568 stat_result.st_size = 0
563 569 if os.path.isfile(target):
@@ -565,12 +571,14 @@ class Api:
565 571 stat_result.st_mtime = int(f"{filename.split('_')[0]}") if filename.startswith("1") else 0
566 572 headers = {
567 573 "cache-control": "public, max-age=31536000",
568 "content-type": f"image/{ext.replace('jpg', 'jpeg') or 'jpeg'}",
569 574 "last-modified": formatdate(stat_result.st_mtime, usegmt=True),
570 575 "etag": f'"{hashlib.md5(filename.encode()).hexdigest()}"',
571 576 **({
572 577 "content-length": str(stat_result.st_size),
573 } if stat_result.st_size else {})
578 } if stat_result.st_size else {}),
579 **({} if mime_type is None else {
580 "content-type": mime_type,
581 })
574 582 }
575 583 response = FileResponse(
576 584 target,
@@ -584,13 +592,13 @@ class Api:
584 592 return NotModifiedResponse(response.headers)
585 593 except KeyError:
586 594 pass
587 if not os.path.isfile(target):
595 if not os.path.isfile(target) and mime_type is not None:
588 596 source_url = get_source_url(str(request.query_params))
589 597 ssl = None
590 598 if source_url is None:
591 599 backend_url = os.environ.get("G4F_BACKEND_URL")
592 600 if backend_url:
593 source_url = f"{backend_url}/images/{filename}"
601 source_url = f"{backend_url}/media/{filename}"
594 602 ssl = False
595 603 if source_url is not None:
596 604 try:
Modified g4f/client/__init__.py +14 -12
@@ -13,7 +13,7 @@ from ..image.copy_images import copy_media
13 13 from ..typing import Messages, ImageType
14 14 from ..providers.types import ProviderType, BaseRetryProvider
15 15 from ..providers.response import *
16 from ..errors import NoImageResponseError
16 from ..errors import NoMediaResponseError
17 17 from ..providers.retry_provider import IterListProvider
18 18 from ..providers.asyncio import to_sync_generator
19 19 from ..Provider.needs_auth import BingCreateImages, OpenaiAccount
@@ -268,6 +268,7 @@ class Client(BaseClient):
268 268 super().__init__(**kwargs)
269 269 self.chat: Chat = Chat(self, provider)
270 270 self.images: Images = Images(self, image_provider)
271 self.media: Images = Images(self, image_provider)
271 272
272 273 class Completions:
273 274 def __init__(self, client: Client, provider: Optional[ProviderType] = None):
@@ -406,7 +407,7 @@ class Images:
406 407 else:
407 408 response = await self._generate_image_response(provider_handler, provider_name, model, prompt, **kwargs)
408 409
409 if isinstance(response, ImageResponse):
410 if isinstance(response, MediaResponse):
410 411 return await self._process_image_response(
411 412 response,
412 413 model,
@@ -417,8 +418,8 @@ class Images:
417 418 if response is None:
418 419 if error is not None:
419 420 raise error
420 raise NoImageResponseError(f"No image response from {provider_name}")
421 raise NoImageResponseError(f"Unexpected response type: {type(response)}")
421 raise NoMediaResponseError(f"No image response from {provider_name}")
422 raise NoMediaResponseError(f"Unexpected response type: {type(response)}")
422 423
423 424 async def _generate_image_response(
424 425 self,
@@ -428,7 +429,7 @@ class Images:
428 429 prompt: str,
429 430 prompt_prefix: str = "Generate a image: ",
430 431 **kwargs
431 ) -> ImageResponse:
432 ) -> MediaResponse:
432 433 messages = [{"role": "user", "content": f"{prompt_prefix}{prompt}"}]
433 434 response = None
434 435 if hasattr(provider_handler, "create_async_generator"):
@@ -439,7 +440,7 @@ class Images:
439 440 prompt=prompt,
440 441 **kwargs
441 442 ):
442 if isinstance(item, ImageResponse):
443 if isinstance(item, MediaResponse):
443 444 response = item
444 445 break
445 446 elif hasattr(provider_handler, "create_completion"):
@@ -450,7 +451,7 @@ class Images:
450 451 prompt=prompt,
451 452 **kwargs
452 453 ):
453 if isinstance(item, ImageResponse):
454 if isinstance(item, MediaResponse):
454 455 response = item
455 456 break
456 457 else:
@@ -501,17 +502,17 @@ class Images:
501 502 else:
502 503 response = await self._generate_image_response(provider_handler, provider_name, model, prompt, **kwargs)
503 504
504 if isinstance(response, ImageResponse):
505 if isinstance(response, MediaResponse):
505 506 return await self._process_image_response(response, model, provider_name, response_format, proxy)
506 507 if response is None:
507 508 if error is not None:
508 509 raise error
509 raise NoImageResponseError(f"No image response from {provider_name}")
510 raise NoImageResponseError(f"Unexpected response type: {type(response)}")
510 raise NoMediaResponseError(f"No image response from {provider_name}")
511 raise NoMediaResponseError(f"Unexpected response type: {type(response)}")
511 512
512 513 async def _process_image_response(
513 514 self,
514 response: ImageResponse,
515 response: MediaResponse,
515 516 model: str,
516 517 provider: str,
517 518 response_format: Optional[str] = None,
@@ -533,7 +534,7 @@ class Images:
533 534 else:
534 535 # Save locally for None (default) case
535 536 images = await copy_media(response.get_list(), response.get("cookies"), proxy)
536 images = [Image.model_construct(url=f"/images/{os.path.basename(image)}", revised_prompt=response.alt) for image in images]
537 images = [Image.model_construct(url=f"/media/{os.path.basename(image)}", revised_prompt=response.alt) for image in images]
537 538
538 539 return ImagesResponse.model_construct(
539 540 created=int(time.time()),
@@ -552,6 +553,7 @@ class AsyncClient(BaseClient):
552 553 super().__init__(**kwargs)
553 554 self.chat: AsyncChat = AsyncChat(self, provider)
554 555 self.images: AsyncImages = AsyncImages(self, image_provider)
556 self.media: AsyncImages = self.images
555 557
556 558 class AsyncChat:
557 559 completions: AsyncCompletions
Modified g4f/errors.py +1 -1
@@ -34,7 +34,7 @@ class NestAsyncioError(MissingRequirementsError):
34 34 class MissingAuthError(Exception):
35 35 ...
36 36
37 class NoImageResponseError(Exception):
37 class NoMediaResponseError(Exception):
38 38 ...
39 39
40 40 class ResponseError(Exception):
Modified g4f/gui/client/index.html +8 -4
@@ -49,7 +49,7 @@
49 49 <script src="https://cdn.jsdelivr.net/npm/gpt-tokenizer/dist/cl100k_base.js" async></script>
50 50 <script src="https://cdn.jsdelivr.net/npm/gpt-tokenizer/dist/o200k_base.js" async></script>
51 51 </template>
52 <script>
52 <script async>
53 53 if (localStorage.getItem("countTokens") != "false") {
54 54 const template = document.head.querySelector('template');
55 55 document.head.appendChild(template.content);
@@ -61,10 +61,14 @@
61 61 const gpt_image = '<img src="/static/img/gpt.png" alt="your avatar">';
62 62 </script>
63 63 <script src="/static/js/highlight.min.js" async></script>
64 <script>window.conversation_id = "{{chat_id}}"</script>
64 <script>window.conversation_id = "{{conversation_id}}"</script>
65 <script>window.chat_id = "{{chat_id}}"</script>
65 66 <title>G4F Chat</title>
66 67 </head>
67 68 <body>
69 <script async>
70 localStorage.getItem("darkMode") == "false" ? document.body.classList.add("white") : null;
71 </script>
68 72 <div class="gradient"></div>
69 73 <div class="sidebar shown">
70 74 <div class="top">
@@ -118,7 +122,7 @@
118 122 <label for="hide-systemPrompt" class="toogle" title="For more space on phones"></label>
119 123 </div>
120 124 <div class="field">
121 <span class="label">Download generated images</span>
125 <span class="label">Download generated images, audios and videos</span>
122 126 <input type="checkbox" id="download_media" checked/>
123 127 <label for="download_media" class="toogle" title="Download and save generated images, audios and videos"></label>
124 128 </div>
@@ -212,7 +216,7 @@
212 216 G4F Chat
213 217 </div>
214 218 <textarea id="chatPrompt" class="box" placeholder="System prompt"></textarea>
215 <button class="slide-systemPrompt">
219 <button class="slide-header">
216 220 <i class="fa-solid fa-angles-up"></i>
217 221 </button>
218 222 <div class="chat-body" id="chatBody"></div>
Modified g4f/gui/client/static/css/style.css +19 -14
@@ -321,8 +321,9 @@ body:not(.white) a:visited{
321 321 white-space: pre-wrap;
322 322 }
323 323
324 .message .content img{
324 .message .content img, .message .content video{
325 325 max-width: 400px;
326 max-height: 400px;
326 327 }
327 328
328 329 .message .content .audio{
@@ -667,8 +668,9 @@ input-count .text {
667 668 .micro-label {
668 669 cursor: pointer;
669 670 position: absolute;
670 top: 10px;
671 left: 10px;
671 top: 8px;
672 left: 8px;
673 padding: 2px;
672 674 }
673 675
674 676 .file-label:has(> input:valid),
@@ -683,11 +685,12 @@ input-count .text {
683 685 }
684 686
685 687 label.image-label {
686 top: 32px;
688 top: 30px;
687 689 }
688 690
689 691 label[for="micro"] {
690 top: 54px;
692 top: 50px;
693 padding: 4px;
691 694 }
692 695
693 696 @media (pointer:none), (pointer:coarse) {
@@ -872,7 +875,7 @@ input.model:hover
872 875 min-height: 59px;
873 876 height: 59px;
874 877 resize: vertical;
875 padding: var(--inner-gap) var(--section-gap);
878 padding: var(--inner-gap) 28px;
876 879 }
877 880
878 881 #systemPrompt, #chatPrompt, .settings textarea, form textarea {
@@ -935,15 +938,15 @@ input.model:hover
935 938 top: auto !important;
936 939 }
937 940
938 .slide-systemPrompt {
941 .slide-header {
939 942 position: absolute;
940 top: 42px;
943 top: 0;
941 944 z-index: 1;
942 padding: var(--inner-gap) 10px;
945 padding: 10px;
943 946 border: none;
944 947 background: transparent;
945 948 cursor: pointer;
946 height: 49px;
949 height: 40px;
947 950 color: var(--colour-3);
948 951 }
949 952
@@ -1159,7 +1162,7 @@ ul {
1159 1162 }
1160 1163
1161 1164 .sidebar.shown {
1162 width: 300px;
1165 width: 400px;
1163 1166 padding: 15px;
1164 1167 margin-right: 10px;
1165 1168 }
@@ -1475,7 +1478,7 @@ form .field.saved .fa-xmark {
1475 1478 .conversation .user-input,
1476 1479 .conversation .chat-buttons,
1477 1480 .conversation .chat-toolbar,
1478 .conversation .slide-systemPrompt,
1481 .conversation .slide-header,
1479 1482 .message .count i,
1480 1483 .message .assistant,
1481 1484 .message .user {
@@ -1509,7 +1512,7 @@ form .field.saved .fa-xmark {
1509 1512 overflow: hidden;
1510 1513 }
1511 1514 .chat-header {
1512 padding: 10px;
1515 padding: 10px 28px;
1513 1516 font-weight: 500;
1514 1517 white-space: nowrap;
1515 1518 text-overflow: ellipsis;
@@ -1549,10 +1552,12 @@ form .field.saved .fa-xmark {
1549 1552 .chat-footer .send-buttons button {
1550 1553 background: var(--blur-bg);
1551 1554 color: white;
1552 border: none;
1553 1555 padding: 12px 15px;
1554 1556 margin: 0 10px;
1555 1557 border-radius: 5px;
1556 1558 cursor: pointer;
1557 1559 border: 1px dashed #e4d4ffa6;
1560 }
1561 .chat-footer .send-buttons button:hover {
1562 border-style: solid;
1558 1563 }
Modified g4f/gui/client/static/js/chat.v1.js +89 -28
@@ -63,8 +63,6 @@ appStorage = window.localStorage || {
63 63 length: 0
64 64 }
65 65
66 appStorage.getItem("darkMode") == "false" ? document.body.classList.add("white") : null;
67
68 66 let markdown_render = (content) => escapeHtml(content);
69 67 if (window.markdownit) {
70 68 const markdown = window.markdownit();
@@ -81,7 +79,7 @@ if (window.markdownit) {
81 79 .replaceAll('<code>', '<code class="language-plaintext">')
82 80 .replaceAll('&lt;i class=&quot;', '<i class="')
83 81 .replaceAll('&quot;&gt;&lt;/i&gt;', '"></i>')
84 .replaceAll('&lt;video controls src=&quot;', '<video controls width="400" src="')
82 .replaceAll('&lt;video controls src=&quot;', '<video controls loop src="')
85 83 .replaceAll('&quot;&gt;&lt;/video&gt;', '"></video>')
86 84 .replaceAll('&lt;audio controls src=&quot;', '<audio controls src="')
87 85 .replaceAll('&quot;&gt;&lt;/audio&gt;', '"></audio>')
@@ -581,7 +579,7 @@ stop_generating.addEventListener("click", async () => {
581 579 }
582 580 }
583 581 }
584 await load_conversation(window.conversation_id, false);
582 await safe_load_conversation(window.conversation_id, false);
585 583 });
586 584
587 585 document.querySelector(".media-player .fa-x").addEventListener("click", ()=>{
@@ -831,7 +829,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
831 829 for (const [key, value] of Object.entries(message.conversation)) {
832 830 conversation.data[key] = value;
833 831 }
834 await save_conversation(conversation_id, conversation);
832 await save_conversation(conversation_id, conversation, false);
835 833 } else if (message.type == "provider") {
836 834 provider_storage[message_id] = message.provider;
837 835 let provider_el = content_map.content.querySelector('.provider');
@@ -1289,6 +1287,7 @@ const delete_conversation = async (conversation_id) => {
1289 1287 };
1290 1288
1291 1289 const set_conversation = async (conversation_id) => {
1290 window.chat_id = null;
1292 1291 if (title_ids_storage[conversation_id]) {
1293 1292 conversation_id = title_ids_storage[conversation_id];
1294 1293 }
@@ -1300,7 +1299,7 @@ const set_conversation = async (conversation_id) => {
1300 1299 window.conversation_id = conversation_id;
1301 1300
1302 1301 await clear_conversation();
1303 await load_conversation(conversation_id);
1302 await load_conversation(await get_conversation(conversation_id));
1304 1303 load_conversations();
1305 1304 hide_sidebar(true);
1306 1305 };
@@ -1364,15 +1363,14 @@ function merge_messages(message1, message2) {
1364 1363 // console.log(merge_messages("1 != 2", "```python\n1 != 2;"));
1365 1364 // console.log(merge_messages("1 != 2;\n1 != 3;\n", "1 != 2;\n1 != 3;\n"));
1366 1365
1367 const load_conversation = async (conversation_id, scroll=true) => {
1368 let conversation = await get_conversation(conversation_id);
1369 let messages = conversation?.items || [];
1370 console.debug("Conversation:", conversation)
1371
1366 const load_conversation = async (conversation, scroll=true) => {
1372 1367 if (!conversation) {
1373 1368 return;
1374 1369 }
1375 let title = conversation.title || conversation.new_title;
1370 let messages = conversation?.items || [];
1371 console.debug("Conversation:", conversation.id)
1372
1373 let title = conversation.new_title || conversation.title;
1376 1374 title = title ? `${title} - G4F` : window.title;
1377 1375 if (title) {
1378 1376 document.title = title;
@@ -1550,7 +1548,8 @@ async function safe_load_conversation(conversation_id, scroll=true) {
1550 1548 }
1551 1549 }
1552 1550 if (!is_running) {
1553 return await load_conversation(conversation_id, scroll);
1551 let conversation = await get_conversation(conversation_id);
1552 return await load_conversation(conversation, scroll);
1554 1553 }
1555 1554 }
1556 1555
@@ -1563,9 +1562,10 @@ async function get_conversation(conversation_id) {
1563 1562
1564 1563 async function save_conversation(conversation_id, conversation) {
1565 1564 conversation.updated = Date.now();
1565 const data = JSON.stringify(conversation)
1566 1566 appStorage.setItem(
1567 1567 `conversation:${conversation_id}`,
1568 JSON.stringify(conversation)
1568 data
1569 1569 );
1570 1570 }
1571 1571
@@ -1617,6 +1617,14 @@ const remove_message = async (conversation_id, index) => {
1617 1617 }
1618 1618 conversation.items = new_items;
1619 1619 await save_conversation(conversation_id, conversation);
1620 if (window.chat_id) {
1621 const url = `/backend-api/v2/chat/${window.chat_id}`;
1622 response = await fetch(url, {
1623 method: 'POST',
1624 headers: {'content-type': 'application/json'},
1625 body: data,
1626 });
1627 }
1620 1628 };
1621 1629
1622 1630 const get_message = async (conversation_id, index) => {
@@ -1685,6 +1693,14 @@ const add_message = async (
1685 1693 conversation.items = new_messages;
1686 1694 }
1687 1695 await save_conversation(conversation_id, conversation);
1696 if (window.chat_id) {
1697 const url = `/backend-api/v2/chat/${window.chat_id}`;
1698 fetch(url, {
1699 method: 'POST',
1700 headers: {'content-type': 'application/json'},
1701 body: JSON.stringify(conversation),
1702 });
1703 }
1688 1704 return conversation.items.length - 1;
1689 1705 };
1690 1706
@@ -2021,12 +2037,56 @@ chatPrompt.addEventListener("input", function() {
2021 2037 });
2022 2038
2023 2039 window.addEventListener('load', async function() {
2040 if (!window.conversation_id) {
2041 window.conversation_id = window.chat_id;
2042 }
2043 const response = await fetch(`/backend-api/v2/chat/${window.chat_id ? window.chat_id : window.conversation_id}`, {
2044 headers: {'accept': 'application/json'},
2045 });
2046 if (response.ok) {
2047 let conversation = await response.json();
2048 if (window.chat_id && (!window.conversation_id || conversation.id == window.conversation_id)) {
2049 window.conversation_id = conversation.id;
2050 await load_conversation(conversation);
2051 appStorage.setItem(
2052 `conversation:${conversation.id}`,
2053 JSON.stringify(conversation)
2054 );
2055 let refreshOnHide = true;
2056 document.addEventListener("visibilitychange", () => {
2057 if (document.hidden) {
2058 refreshOnHide = false;
2059 } else {
2060 refreshOnHide = true;
2061 }
2062 });
2063 return setInterval(async () => {
2064 if (!refreshOnHide || !window.chat_id) {
2065 return;
2066 }
2067 const response = await fetch(`/backend-api/v2/chat/${window.chat_id}`, {
2068 headers: {'accept': 'application/json', 'if-none-match': conversation.updated},
2069 });
2070 if (response.status == 200) {
2071 const new_conversation = await response.json();
2072 if (conversation.id == window.conversation_id && new_conversation.updated != conversation.updated) {
2073 conversation = new_conversation;
2074 appStorage.setItem(
2075 `conversation:${conversation.id}`,
2076 JSON.stringify(conversation)
2077 );
2078 await load_conversation(conversation);
2079 }
2080 }
2081 }, 5000);
2082 }
2083 }
2024 2084 await safe_load_conversation(window.conversation_id, false);
2025 2085 });
2026 2086
2027 2087 window.addEventListener('DOMContentLoaded', async function() {
2028 2088 await on_load();
2029 if (window.conversation_id == "{{chat_id}}") {
2089 if (!window.conversation_id == "{{chat_id}}") {
2030 2090 window.conversation_id = uuid();
2031 2091 } else {
2032 2092 await on_api();
@@ -2289,11 +2349,9 @@ async function on_api() {
2289 2349 );
2290 2350
2291 2351 const hide_systemPrompt = document.getElementById("hide-systemPrompt")
2292 const slide_systemPrompt_icon = document.querySelector(".slide-systemPrompt i");
2352 const slide_systemPrompt_icon = document.querySelector(".slide-header i");
2293 2353 if (hide_systemPrompt.checked) {
2294 2354 chatPrompt.classList.add("hidden");
2295 slide_systemPrompt_icon.classList.remove("fa-angles-up");
2296 slide_systemPrompt_icon.classList.add("fa-angles-down");
2297 2355 }
2298 2356 hide_systemPrompt.addEventListener('change', async (event) => {
2299 2357 if (event.target.checked) {
@@ -2302,10 +2360,10 @@ async function on_api() {
2302 2360 chatPrompt.classList.remove("hidden");
2303 2361 }
2304 2362 });
2305 document.querySelector(".slide-systemPrompt")?.addEventListener("click", () => {
2306 hide_systemPrompt.click();
2307 const checked = hide_systemPrompt.checked;
2308 chatPrompt.classList[checked ? "add": "remove"]("hidden");
2363 document.querySelector(".slide-header")?.addEventListener("click", () => {
2364 const checked = slide_systemPrompt_icon.classList.contains("fa-angles-up");
2365 document.querySelector(".chat-header").classList[checked ? "add": "remove"]("hidden");
2366 chatPrompt.classList[checked || hide_systemPrompt.checked ? "add": "remove"]("hidden");
2309 2367 slide_systemPrompt_icon.classList[checked ? "remove": "add"]("fa-angles-up");
2310 2368 slide_systemPrompt_icon.classList[checked ? "add": "remove"]("fa-angles-down");
2311 2369 });
@@ -2361,7 +2419,6 @@ async function load_version() {
2361 2419 }
2362 2420
2363 2421 function renderMediaSelect() {
2364 mediaSelect.classList.remove("hidden");
2365 2422 const oldImages = mediaSelect.querySelectorAll("a:has(img)");
2366 2423 oldImages.forEach((el)=>el.remove());
2367 2424 Object.entries(image_storage).forEach(([object_url, file]) => {
@@ -2472,10 +2529,15 @@ function connectToSSE(url, do_refine, bucket_id) {
2472 2529 inputCount.innerText = `Download: ${data.count} files`;
2473 2530 } else if (data.action == "done") {
2474 2531 if (do_refine) {
2475 do_refine = false;
2476 connectToSSE(`/backend-api/v2/files/${bucket_id}?refine_chunks_with_spacy=true`, do_refine, bucket_id);
2532 connectToSSE(`/backend-api/v2/files/${bucket_id}?refine_chunks_with_spacy=true`, false, bucket_id);
2477 2533 return;
2478 2534 }
2535 fileInput.value = "";
2536 paperclip.classList.remove("blink");
2537 if (!data.size) {
2538 inputCount.innerText = "No content found";
2539 return
2540 }
2479 2541 appStorage.setItem(`bucket:${bucket_id}`, data.size);
2480 2542 inputCount.innerText = "Files are loaded successfully";
2481 2543 if (!userInput.value) {
@@ -2483,8 +2545,6 @@ function connectToSSE(url, do_refine, bucket_id) {
2483 2545 handle_ask(false);
2484 2546 } else {
2485 2547 userInput.value += (userInput.value ? "\n" : "") + JSON.stringify({bucket_id: bucket_id}) + "\n";
2486 paperclip.classList.remove("blink");
2487 fileInput.value = "";
2488 2548 }
2489 2549 }
2490 2550 };
@@ -2518,9 +2578,10 @@ async function upload_files(fileInput) {
2518 2578 }
2519 2579 if (result.media) {
2520 2580 result.media.forEach((filename)=> {
2521 const url = `/backend-api/v2/files/${bucket_id}/media/${filename}`;
2581 const url = `/files/${bucket_id}/media/${filename}`;
2522 2582 image_storage[url] = {bucket_id: bucket_id, name: filename};
2523 2583 });
2584 mediaSelect.classList.remove("hidden");
2524 2585 renderMediaSelect();
2525 2586 }
2526 2587 }
Modified g4f/gui/server/backend_api.py +32 -0
@@ -58,6 +58,7 @@ class Backend_Api(Api):
58 58 app (Flask): Flask application instance to attach routes to.
59 59 """
60 60 self.app: Flask = app
61 self.chat_cache = {}
61 62
62 63 if app.demo:
63 64 @app.route('/', methods=['GET'])
@@ -210,6 +211,10 @@ class Backend_Api(Api):
210 211 '/images/<path:name>': {
211 212 'function': self.serve_images,
212 213 'methods': ['GET']
214 },
215 '/media/<path:name>': {
216 'function': self.serve_images,
217 'methods': ['GET']
213 218 }
214 219 }
215 220
@@ -359,6 +364,33 @@ class Backend_Api(Api):
359 364 return "File saved", 200
360 365 return 'Not supported file', 400
361 366
367 @self.app.route('/backend-api/v2/chat/<chat_id>', methods=['GET'])
368 def get_chat(chat_id: str) -> str:
369 chat_id = secure_filename(chat_id)
370 if int(self.chat_cache.get(chat_id, -1)) == int(request.headers.get("if-none-match", 0)):
371 return jsonify({"error": {"message": "Not modified"}}), 304
372 bucket_dir = get_bucket_dir(chat_id)
373 file = os.path.join(bucket_dir, "chat.json")
374 if not os.path.isfile(file):
375 return jsonify({"error": {"message": "Not found"}}), 404
376 with open(file, 'r') as f:
377 chat_data = json.load(f)
378 if int(chat_data.get("updated", 0)) == int(request.headers.get("if-none-match", 0)):
379 return jsonify({"error": {"message": "Not modified"}}), 304
380 self.chat_cache[chat_id] = chat_data.get("updated", 0)
381 return jsonify(chat_data), 200
382
383 @self.app.route('/backend-api/v2/chat/<chat_id>', methods=['POST'])
384 def upload_chat(chat_id: str) -> dict:
385 chat_data = {**request.json}
386 chat_id = secure_filename(chat_id)
387 bucket_dir = get_bucket_dir(chat_id)
388 os.makedirs(bucket_dir, exist_ok=True)
389 with open(os.path.join(bucket_dir, "chat.json"), 'w') as f:
390 json.dump(chat_data, f)
391 self.chat_cache[chat_id] = chat_data.get("updated", 0)
392 return {"chat_id": chat_id}
393
362 394 def handle_synthesize(self, provider: str):
363 395 try:
364 396 provider_handler = convert_to_provider(provider)
Modified g4f/gui/server/website.py +15 -4
@@ -16,6 +16,14 @@ class Website:
16 16 'function': self._chat,
17 17 'methods': ['GET', 'POST']
18 18 },
19 '/chat/<chat_id>/': {
20 'function': self._chat_id,
21 'methods': ['GET', 'POST']
22 },
23 '/chat/<chat_id>/<conversation_id>': {
24 'function': self._chat_id,
25 'methods': ['GET', 'POST']
26 },
19 27 '/chat/menu/': {
20 28 'function': redirect_home,
21 29 'methods': ['GET', 'POST']
@@ -32,11 +40,14 @@ class Website:
32 40
33 41 def _chat(self, conversation_id):
34 42 if conversation_id == "share":
35 return render_template('index.html', chat_id=str(uuid.uuid4()))
36 return render_template('index.html', chat_id=conversation_id)
43 return render_template('index.html', conversation_id=str(uuid.uuid4()))
44 return render_template('index.html', conversation_id=conversation_id)
45
46 def _chat_id(self, chat_id, conversation_id: str = ""):
47 return render_template('index.html', chat_id=chat_id, conversation_id=conversation_id)
37 48
38 49 def _index(self):
39 return render_template('index.html', chat_id=str(uuid.uuid4()))
50 return render_template('index.html', conversation_id=str(uuid.uuid4()))
40 51
41 52 def _settings(self):
42 return render_template('index.html', chat_id=str(uuid.uuid4()))
53 return render_template('index.html', conversation_id=str(uuid.uuid4()))
Modified g4f/image/__init__.py +20 -1
Modified g4f/image/copy_images.py +10 -5