KiWA001 commited on
Commit
1048740
·
1 Parent(s): b3fd1aa

feat: complete mobile-first redesign with video streaming and on-screen keyboard

Browse files

Major UI Redesign:
- Complete mobile-first responsive CSS redesign
- Modern card-based layout optimized for touch
- Smooth animations and transitions
- Better visual hierarchy and spacing
- Sticky on-screen keyboard for mobile typing
- Toggle switches instead of checkboxes
- Progress bars for usage statistics
- Connection status indicators with pulse animation
- Responsive tables with horizontal scroll

Video Streaming:
- MJPEG streaming endpoint (/portal/{provider}/stream)
- Quality selector (Low/Medium/High) for bandwidth control
- Frame rate control (fps parameter)
- Compressed JPEG frames for smooth streaming
- Screenshot endpoint with quality parameter
- Real-time video-like experience at 2fps

On-Screen Keyboard:
- Full QWERTY layout optimized for mobile
- Space, backspace, enter keys
- Shift toggle support
- Stays visible while browser active
- Close to actual browser window

Mobile Optimizations:
- Touch-friendly buttons (min 40px)
- Larger tap targets
- Simplified navigation
- Better scrolling on mobile
- Viewport meta tags for proper scaling
- Prevent zoom on inputs

Browser Portal Enhancements:
- Streamlined toolbar with navigation
- Address bar always visible
- Quality toggle integrated in toolbar
- Message input at bottom with keyboard toggle
- Visual click coordinates display
- Loading spinner during navigation

Dependencies:
- Added Pillow for image processing
- Image compression and resizing support
- JPEG/PNG format conversion

Files changed (3) hide show
  1. admin_router.py +99 -4
  2. requirements.txt +2 -1
  3. static/qaz.html +0 -0
admin_router.py CHANGED
@@ -488,10 +488,12 @@ async def start_unified_portal(req: PortalProviderRequest):
488
  raise HTTPException(status_code=500, detail=str(e))
489
 
490
  @router.get("/portal/{provider}/screenshot")
491
- async def get_unified_portal_screenshot(provider: str):
492
- """Get screenshot from any provider portal."""
493
  import os
494
- from fastapi.responses import FileResponse
 
 
495
 
496
  try:
497
  from browser_portal import get_portal_manager, PortalProvider
@@ -507,7 +509,100 @@ async def get_unified_portal_screenshot(provider: str):
507
  if not os.path.exists(portal.screenshot_path):
508
  raise HTTPException(status_code=404, detail="Screenshot not available")
509
 
510
- return FileResponse(portal.screenshot_path, media_type="image/png")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  except HTTPException:
512
  raise
513
  except Exception as e:
 
488
  raise HTTPException(status_code=500, detail=str(e))
489
 
490
  @router.get("/portal/{provider}/screenshot")
491
+ async def get_unified_portal_screenshot(provider: str, quality: float = 1.0, format: str = "png"):
492
+ """Get screenshot from any provider portal with optional quality/compression."""
493
  import os
494
+ from fastapi.responses import FileResponse, StreamingResponse
495
+ from PIL import Image
496
+ import io
497
 
498
  try:
499
  from browser_portal import get_portal_manager, PortalProvider
 
509
  if not os.path.exists(portal.screenshot_path):
510
  raise HTTPException(status_code=404, detail="Screenshot not available")
511
 
512
+ # If quality is 1.0 and format is png, return as-is
513
+ if quality >= 1.0 and format == "png":
514
+ return FileResponse(portal.screenshot_path, media_type="image/png")
515
+
516
+ # Otherwise, compress/process the image
517
+ img = Image.open(portal.screenshot_path)
518
+
519
+ # Resize if quality < 1.0
520
+ if quality < 1.0:
521
+ new_size = (int(img.width * quality), int(img.height * quality))
522
+ img = img.resize(new_size, Image.Resampling.LANCZOS)
523
+
524
+ # Convert to desired format
525
+ img_io = io.BytesIO()
526
+ if format == "jpeg" or format == "jpg":
527
+ img = img.convert("RGB")
528
+ img.save(img_io, format="JPEG", quality=int(quality * 100) if quality < 1 else 85)
529
+ media_type = "image/jpeg"
530
+ else:
531
+ img.save(img_io, format="PNG")
532
+ media_type = "image/png"
533
+
534
+ img_io.seek(0)
535
+ return StreamingResponse(img_io, media_type=media_type)
536
+
537
+ except HTTPException:
538
+ raise
539
+ except Exception as e:
540
+ raise HTTPException(status_code=500, detail=str(e))
541
+
542
+
543
+ # MJPEG Streaming endpoint for video-like experience
544
+ @router.get("/portal/{provider}/stream")
545
+ async def stream_portal_video(provider: str, quality: float = 0.5, fps: int = 2):
546
+ """Stream the portal as MJPEG for video-like experience."""
547
+ import os
548
+ import asyncio
549
+ from fastapi.responses import StreamingResponse
550
+ from PIL import Image
551
+ import io
552
+
553
+ try:
554
+ from browser_portal import get_portal_manager, PortalProvider
555
+
556
+ prov = PortalProvider(provider.lower())
557
+ portal = get_portal_manager().get_portal(prov)
558
+
559
+ if not portal.is_running():
560
+ raise HTTPException(status_code=400, detail=f"{provider} portal not running")
561
+
562
+ async def generate_frames():
563
+ """Generate MJPEG stream."""
564
+ frame_delay = 1.0 / fps
565
+
566
+ while portal.is_running():
567
+ try:
568
+ # Take screenshot
569
+ await portal.take_screenshot()
570
+
571
+ if os.path.exists(portal.screenshot_path):
572
+ # Process image
573
+ img = Image.open(portal.screenshot_path)
574
+
575
+ if quality < 1.0:
576
+ new_size = (int(img.width * quality), int(img.height * quality))
577
+ img = img.resize(new_size, Image.Resampling.LANCZOS)
578
+
579
+ # Convert to JPEG for smaller size
580
+ img = img.convert("RGB")
581
+ img_io = io.BytesIO()
582
+ img.save(img_io, format="JPEG", quality=70)
583
+ img_io.seek(0)
584
+
585
+ frame_data = img_io.getvalue()
586
+
587
+ # Yield MJPEG frame
588
+ yield (
589
+ b'--frame\r\n'
590
+ b'Content-Type: image/jpeg\r\n'
591
+ b'Content-Length: ' + str(len(frame_data)).encode() + b'\r\n'
592
+ b'\r\n' + frame_data + b'\r\n'
593
+ )
594
+
595
+ await asyncio.sleep(frame_delay)
596
+
597
+ except Exception as e:
598
+ print(f"Stream error: {e}")
599
+ await asyncio.sleep(frame_delay)
600
+
601
+ return StreamingResponse(
602
+ generate_frames(),
603
+ media_type="multipart/x-mixed-replace;boundary=frame"
604
+ )
605
+
606
  except HTTPException:
607
  raise
608
  except Exception as e:
requirements.txt CHANGED
@@ -5,10 +5,11 @@ duckduckgo-ai-chat>=0.0.7
5
  httpx>=0.25.0
6
  pydantic>=2.0
7
  supabase>=2.0.0
 
8
  # Search Engine
9
  requests>=2.31.0
10
  beautifulsoup4>=4.12.0
11
  lxml>=5.1.0
12
  duckduckgo_search>=5.0.0
13
 
14
- # Force Rebuild: v2.0.1
 
5
  httpx>=0.25.0
6
  pydantic>=2.0
7
  supabase>=2.0.0
8
+ Pillow>=10.0.0
9
  # Search Engine
10
  requests>=2.31.0
11
  beautifulsoup4>=4.12.0
12
  lxml>=5.1.0
13
  duckduckgo_search>=5.0.0
14
 
15
+ # Force Rebuild: v2.1.0 - Mobile UI + Video Streaming
static/qaz.html CHANGED
The diff for this file is too large to render. See raw diff