Spaces:
Sleeping
Sleeping
| # app.py - SFArena by sixfingerdev | |
| # Temperature hatasΔ± dΓΌzeltildi | |
| from flask import Flask, render_template_string, request, Response, jsonify | |
| import asyncio | |
| import threading | |
| from queue import Queue, Empty | |
| from putergenai import PuterClient | |
| import json | |
| app = Flask(__name__) | |
| app.secret_key = 'sfarena-secret-key-2024' | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GLOBAL DEΔΔ°ΕKENLER VE ASYNC YΓNETΔ°MΔ° | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| client = None | |
| conversations = {} | |
| _loop = None | |
| _loop_thread = None | |
| _client_lock = threading.Lock() | |
| def _run_event_loop(loop): | |
| asyncio.set_event_loop(loop) | |
| loop.run_forever() | |
| def get_event_loop(): | |
| global _loop, _loop_thread | |
| if _loop is None or not _loop.is_running(): | |
| _loop = asyncio.new_event_loop() | |
| _loop_thread = threading.Thread(target=_run_event_loop, args=(_loop,), daemon=True) | |
| _loop_thread.start() | |
| return _loop | |
| def run_async(coro): | |
| loop = get_event_loop() | |
| future = asyncio.run_coroutine_threadsafe(coro, loop) | |
| return future.result(timeout=120) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MODEL KONFΔ°GΓRASYONU | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Temperature DESTEKLEMΔ°YEN modeller (reasoning modelleri) | |
| NO_TEMPERATURE_MODELS = { | |
| "o1", "o1-mini", "o1-pro", | |
| "o3", "o3-mini", | |
| "o4-mini", | |
| "openrouter:openai/o1", "openrouter:openai/o1-mini", "openrouter:openai/o1-pro", | |
| "openrouter:openai/o3", "openrouter:openai/o3-mini", "openrouter:openai/o4-mini", | |
| } | |
| MODELS = { | |
| # βββ OpenAI GPT-5.x βββ | |
| "gpt-5.2": {"name": "GPT-5.2", "provider": "openai"}, | |
| "gpt-5.2-chat": {"name": "GPT-5.2 Chat", "provider": "openai"}, | |
| "gpt-5.2-pro": {"name": "GPT-5.2 Pro", "provider": "openai"}, | |
| "gpt-5.1": {"name": "GPT-5.1", "provider": "openai"}, | |
| "gpt-5.1-chat-latest": {"name": "GPT-5.1 Chat Latest", "provider": "openai"}, | |
| "gpt-5.1-codex": {"name": "GPT-5.1 Codex", "provider": "openai"}, | |
| "gpt-5.1-codex-max": {"name": "GPT-5.1 Codex Max", "provider": "openai"}, | |
| "gpt-5.1-codex-mini": {"name": "GPT-5.1 Codex Mini", "provider": "openai"}, | |
| "gpt-5": {"name": "GPT-5", "provider": "openai"}, | |
| "gpt-5-mini": {"name": "GPT-5 Mini", "provider": "openai"}, | |
| "gpt-5-nano": {"name": "GPT-5 Nano", "provider": "openai"}, | |
| "gpt-5-chat-latest": {"name": "GPT-5 Chat Latest", "provider": "openai"}, | |
| # βββ OpenAI GPT-4.x βββ | |
| "gpt-4.1": {"name": "GPT-4.1", "provider": "openai"}, | |
| "gpt-4.1-mini": {"name": "GPT-4.1 Mini", "provider": "openai"}, | |
| "gpt-4.1-nano": {"name": "GPT-4.1 Nano", "provider": "openai"}, | |
| "gpt-4.5-preview": {"name": "GPT-4.5 Preview", "provider": "openai"}, | |
| "gpt-4o": {"name": "GPT-4o", "provider": "openai"}, | |
| "gpt-4o-mini": {"name": "GPT-4o Mini", "provider": "openai"}, | |
| # βββ OpenAI o-Series (NO TEMPERATURE) βββ | |
| "o1": {"name": "o1", "provider": "openai"}, | |
| "o1-mini": {"name": "o1 Mini", "provider": "openai"}, | |
| "o1-pro": {"name": "o1 Pro", "provider": "openai"}, | |
| "o3": {"name": "o3", "provider": "openai"}, | |
| "o3-mini": {"name": "o3 Mini", "provider": "openai"}, | |
| "o4-mini": {"name": "o4 Mini", "provider": "openai"}, | |
| # βββ OpenRouter OpenAI βββ | |
| "openrouter:openai/gpt-oss-120b": {"name": "GPT-OSS 120B", "provider": "openai"}, | |
| "openrouter:openai/gpt-oss-120b:exacto": {"name": "GPT-OSS 120B Exacto", "provider": "openai"}, | |
| "openrouter:openai/gpt-oss-20b": {"name": "GPT-OSS 20B", "provider": "openai"}, | |
| "openrouter:openai/gpt-oss-20b:free": {"name": "GPT-OSS 20B Free", "provider": "openai"}, | |
| "openrouter:openai/gpt-oss-safeguard-20b": {"name": "GPT-OSS Safeguard 20B", "provider": "openai"}, | |
| "openrouter:openai/codex-mini": {"name": "Codex Mini", "provider": "openai"}, | |
| "openrouter:openai/gpt-5-codex": {"name": "GPT-5 Codex (OR)", "provider": "openai"}, | |
| "openrouter:openai/gpt-5.1-codex": {"name": "GPT-5.1 Codex (OR)", "provider": "openai"}, | |
| "openrouter:openai/gpt-5.1-codex-max": {"name": "GPT-5.1 Codex Max (OR)", "provider": "openai"}, | |
| "openrouter:openai/gpt-5.1-codex-mini": {"name": "GPT-5.1 Codex Mini (OR)", "provider": "openai"}, | |
| # βββ Anthropic Claude βββ | |
| "claude-sonnet-4-5": {"name": "Claude Sonnet 4.5", "provider": "anthropic"}, | |
| "claude-haiku-4-5": {"name": "Claude Haiku 4.5", "provider": "anthropic"}, | |
| "claude-opus-4-5": {"name": "Claude Opus 4.5", "provider": "anthropic"}, | |
| "claude-sonnet-4": {"name": "Claude Sonnet 4", "provider": "anthropic"}, | |
| "claude-opus-4": {"name": "Claude Opus 4", "provider": "anthropic"}, | |
| "claude-opus-4-1": {"name": "Claude Opus 4.1", "provider": "anthropic"}, | |
| # βββ Google Gemini βββ | |
| "gemini-3-flash-preview": {"name": "Gemini 3 Flash Preview", "provider": "google"}, | |
| "gemini-3-pro-preview": {"name": "Gemini 3 Pro Preview", "provider": "google"}, | |
| "gemini-2.5-pro": {"name": "Gemini 2.5 Pro", "provider": "google"}, | |
| "gemini-2.5-flash": {"name": "Gemini 2.5 Flash", "provider": "google"}, | |
| "gemini-2.5-flash-lite": {"name": "Gemini 2.5 Flash Lite", "provider": "google"}, | |
| "gemini-2.0-flash": {"name": "Gemini 2.0 Flash", "provider": "google"}, | |
| "gemini-2.0-flash-lite": {"name": "Gemini 2.0 Flash Lite", "provider": "google"}, | |
| "gemini-1.5-flash": {"name": "Gemini 1.5 Flash", "provider": "google"}, | |
| # βββ xAI Grok βββ | |
| "x-ai/grok-4.1-fast": {"name": "Grok 4.1 Fast", "provider": "xai"}, | |
| "x-ai/grok-2-1212": {"name": "Grok 2 1212", "provider": "xai"}, | |
| "x-ai/grok-3": {"name": "Grok 3", "provider": "xai"}, | |
| "x-ai/grok-3-beta": {"name": "Grok 3 Beta", "provider": "xai"}, | |
| "x-ai/grok-3-mini": {"name": "Grok 3 Mini", "provider": "xai"}, | |
| "x-ai/grok-3-mini-beta": {"name": "Grok 3 Mini Beta", "provider": "xai"}, | |
| "x-ai/grok-4": {"name": "Grok 4", "provider": "xai"}, | |
| "x-ai/grok-4-fast:free": {"name": "Grok 4 Fast Free", "provider": "xai"}, | |
| "x-ai/grok-code-fast-1": {"name": "Grok Code Fast 1", "provider": "xai"}, | |
| # βββ DeepSeek βββ | |
| "deepseek/deepseek-v3.2": {"name": "DeepSeek V3.2", "provider": "deepseek"}, | |
| "deepseek/deepseek-v3.1": {"name": "DeepSeek V3.1", "provider": "deepseek"}, | |
| "deepseek/deepseek-r1": {"name": "DeepSeek R1", "provider": "deepseek"}, | |
| "deepseek/deepseek-v3": {"name": "DeepSeek V3", "provider": "deepseek"}, | |
| # βββ Amazon Nova βββ | |
| "amazon/nova-micro-v1": {"name": "Nova Micro V1", "provider": "amazon"}, | |
| "amazon/nova-2-lite": {"name": "Nova 2 Lite", "provider": "amazon"}, | |
| "amazon/nova-premier": {"name": "Nova Premier", "provider": "amazon"}, | |
| "amazon/nova-pro": {"name": "Nova Pro", "provider": "amazon"}, | |
| # βββ MiniMax βββ | |
| "openrouter:minimax/minimax-m2.1": {"name": "MiniMax M2.1", "provider": "minimax"}, | |
| "openrouter:minimax/minimax-m2": {"name": "MiniMax M2", "provider": "minimax"}, | |
| "openrouter:minimax/minimax-m1": {"name": "MiniMax M1", "provider": "minimax"}, | |
| "openrouter:minimax/minimax-01": {"name": "MiniMax 01", "provider": "minimax"}, | |
| # βββ Meta Llama βββ | |
| "openrouter:meta-llama/llama-4-maverick": {"name": "Llama 4 Maverick", "provider": "meta"}, | |
| # βββ Essential AI βββ | |
| "openrouter:essentialai/rnj-1-instruct": {"name": "RNJ-1 Instruct", "provider": "essentialai"}, | |
| } | |
| PROVIDER_LOGOS = { | |
| "openai": "https://cdn.worldvectorlogo.com/logos/openai-2.svg", | |
| "anthropic": "https://cdn.worldvectorlogo.com/logos/anthropic-1.svg", | |
| "google": "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg", | |
| "xai": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/X_logo.jpg/600px-X_logo.jpg", | |
| "deepseek": "https://avatars.githubusercontent.com/u/148330875?s=200&v=4", | |
| "amazon": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Amazon_logo.svg/1024px-Amazon_logo.svg.png", | |
| "minimax": "https://avatars.githubusercontent.com/u/122397016?s=200&v=4", | |
| "meta": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7b/Meta_Platforms_Inc._logo.svg/1024px-Meta_Platforms_Inc._logo.svg.png", | |
| "essentialai": "https://avatars.githubusercontent.com/u/167503936?s=200&v=4", | |
| } | |
| PROVIDER_COLORS = { | |
| "openai": {"bg": "#10a37f", "text": "#ffffff"}, | |
| "anthropic": {"bg": "#d4a574", "text": "#1a1a1a"}, | |
| "google": {"bg": "#4285f4", "text": "#ffffff"}, | |
| "xai": {"bg": "#000000", "text": "#ffffff"}, | |
| "deepseek": {"bg": "#0066ff", "text": "#ffffff"}, | |
| "amazon": {"bg": "#ff9900", "text": "#000000"}, | |
| "minimax": {"bg": "#6366f1", "text": "#ffffff"}, | |
| "meta": {"bg": "#0668e1", "text": "#ffffff"}, | |
| "essentialai": {"bg": "#8b5cf6", "text": "#ffffff"}, | |
| } | |
| def get_model_options(model_id): | |
| """Model iΓ§in uygun options dΓΆndΓΌr""" | |
| options = { | |
| "model": model_id, | |
| "stream": True, | |
| "max_tokens": 8192 | |
| } | |
| # Temperature desteklemeyen modeller iΓ§in ekleme | |
| if model_id not in NO_TEMPERATURE_MODELS: | |
| options["temperature"] = 0.7 | |
| return options | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HTML TEMPLATE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HTML_TEMPLATE = r''' | |
| <!DOCTYPE html> | |
| <html lang="tr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SFArena - AI Chat Platform</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --bg-primary: #0a0a0b; | |
| --bg-secondary: #141417; | |
| --bg-tertiary: #1c1c21; | |
| --bg-hover: #242429; | |
| --text-primary: #f5f5f7; | |
| --text-secondary: #a1a1a6; | |
| --text-muted: #6e6e73; | |
| --accent-primary: #6366f1; | |
| --accent-secondary: #8b5cf6; | |
| --accent-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); | |
| --border-color: #2c2c31; | |
| --success: #34c759; | |
| --error: #ff453a; | |
| --shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15); | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow: hidden; | |
| } | |
| .bg-pattern { | |
| position: fixed; top: 0; left: 0; right: 0; bottom: 0; | |
| background: | |
| radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 50%), | |
| radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.08) 0%, transparent 50%); | |
| pointer-events: none; z-index: 0; | |
| } | |
| .header { | |
| position: fixed; top: 0; left: 0; right: 0; height: 72px; | |
| background: rgba(10, 10, 11, 0.85); | |
| backdrop-filter: blur(24px) saturate(180%); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.06); | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 0 32px; z-index: 1000; | |
| } | |
| .logo-section { display: flex; align-items: center; gap: 14px; } | |
| .logo-icon { | |
| width: 46px; height: 46px; | |
| background: var(--accent-gradient); | |
| border-radius: 14px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 18px; font-weight: 800; color: white; | |
| box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4); | |
| position: relative; overflow: hidden; | |
| } | |
| .logo-icon::before { | |
| content: ''; position: absolute; | |
| top: -50%; left: -50%; width: 200%; height: 200%; | |
| background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%); | |
| animation: shimmer 3s infinite; | |
| } | |
| @keyframes shimmer { | |
| 0% { transform: translateX(-100%) rotate(45deg); } | |
| 100% { transform: translateX(100%) rotate(45deg); } | |
| } | |
| .logo-text { display: flex; flex-direction: column; } | |
| .logo-title { | |
| font-size: 24px; font-weight: 800; | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .logo-subtitle { font-size: 11px; color: var(--text-muted); margin-top: -2px; } | |
| .logo-subtitle a { color: var(--accent-primary); text-decoration: none; } | |
| .logo-subtitle a:hover { text-decoration: underline; } | |
| .model-selector-wrapper { position: relative; } | |
| .model-selector-btn { | |
| display: flex; align-items: center; gap: 14px; | |
| padding: 10px 20px; background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); border-radius: 14px; | |
| cursor: pointer; transition: all 0.25s; min-width: 300px; | |
| } | |
| .model-selector-btn:hover { | |
| background: var(--bg-tertiary); border-color: var(--accent-primary); | |
| box-shadow: var(--shadow-glow); transform: translateY(-1px); | |
| } | |
| .model-logo { | |
| width: 32px; height: 32px; border-radius: 8px; | |
| object-fit: contain; background: white; padding: 5px; | |
| } | |
| .model-info { flex: 1; text-align: left; } | |
| .model-name { font-size: 14px; font-weight: 600; } | |
| .model-provider { | |
| font-size: 11px; color: var(--text-muted); | |
| text-transform: uppercase; letter-spacing: 0.8px; margin-top: 2px; | |
| } | |
| .dropdown-arrow { color: var(--text-muted); transition: transform 0.3s; } | |
| .model-selector-wrapper.open .dropdown-arrow { transform: rotate(180deg); } | |
| .model-dropdown { | |
| position: absolute; top: calc(100% + 10px); | |
| left: 50%; transform: translateX(-50%) scale(0.95); | |
| width: 420px; max-height: 520px; | |
| background: var(--bg-secondary); border: 1px solid var(--border-color); | |
| border-radius: 18px; box-shadow: 0 25px 60px rgba(0,0,0,0.5); | |
| overflow: hidden; opacity: 0; visibility: hidden; | |
| transition: all 0.25s; z-index: 1001; | |
| } | |
| .model-dropdown.active { | |
| opacity: 1; visibility: visible; transform: translateX(-50%) scale(1); | |
| } | |
| .model-search { | |
| padding: 18px; border-bottom: 1px solid var(--border-color); | |
| background: rgba(0,0,0,0.2); | |
| } | |
| .model-search input { | |
| width: 100%; padding: 14px 18px; | |
| background: var(--bg-tertiary); border: 1px solid var(--border-color); | |
| border-radius: 12px; color: var(--text-primary); font-size: 14px; outline: none; | |
| } | |
| .model-search input:focus { border-color: var(--accent-primary); } | |
| .model-search input::placeholder { color: var(--text-muted); } | |
| .model-list { max-height: 420px; overflow-y: auto; padding: 10px; } | |
| .model-list::-webkit-scrollbar { width: 6px; } | |
| .model-list::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; } | |
| .provider-group { margin-bottom: 16px; } | |
| .provider-header { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 14px; font-size: 11px; font-weight: 700; | |
| color: var(--text-muted); text-transform: uppercase; | |
| } | |
| .provider-header img { width: 20px; height: 20px; border-radius: 5px; } | |
| .model-item { | |
| display: flex; align-items: center; gap: 14px; | |
| padding: 12px 14px; border-radius: 12px; | |
| cursor: pointer; transition: all 0.15s; margin-bottom: 4px; | |
| } | |
| .model-item:hover { background: var(--bg-hover); } | |
| .model-item.selected { | |
| background: rgba(99, 102, 241, 0.12); | |
| border: 1px solid rgba(99, 102, 241, 0.25); | |
| } | |
| .model-item-logo { | |
| width: 36px; height: 36px; border-radius: 10px; | |
| background: white; padding: 6px; flex-shrink: 0; | |
| } | |
| .model-item-info { flex: 1; min-width: 0; } | |
| .model-item-name { font-size: 14px; font-weight: 500; } | |
| .model-item-id { | |
| font-size: 11px; color: var(--text-muted); | |
| font-family: monospace; white-space: nowrap; | |
| overflow: hidden; text-overflow: ellipsis; | |
| } | |
| .model-item-check { | |
| width: 20px; height: 20px; border-radius: 50%; | |
| background: var(--accent-gradient); display: none; | |
| align-items: center; justify-content: center; | |
| color: white; font-size: 10px; | |
| } | |
| .model-item.selected .model-item-check { display: flex; } | |
| .status-section { display: flex; align-items: center; gap: 16px; } | |
| .status-badge { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 16px; background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); border-radius: 24px; | |
| font-size: 12px; font-weight: 500; | |
| } | |
| .status-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--error); transition: all 0.3s; | |
| } | |
| .status-dot.connected { | |
| background: var(--success); box-shadow: 0 0 12px var(--success); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } | |
| .clear-btn { | |
| padding: 10px 20px; background: transparent; | |
| border: 1px solid var(--border-color); border-radius: 12px; | |
| color: var(--text-secondary); font-size: 13px; font-weight: 500; | |
| cursor: pointer; display: flex; align-items: center; gap: 8px; | |
| } | |
| .clear-btn:hover { | |
| background: rgba(255, 69, 58, 0.1); | |
| border-color: var(--error); color: var(--error); | |
| } | |
| .main-container { | |
| position: relative; padding-top: 72px; height: 100vh; | |
| display: flex; flex-direction: column; z-index: 1; | |
| } | |
| .chat-container { | |
| flex: 1; overflow-y: auto; padding: 32px; scroll-behavior: smooth; | |
| } | |
| .chat-container::-webkit-scrollbar { width: 8px; } | |
| .chat-container::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; } | |
| .welcome-screen { | |
| display: flex; flex-direction: column; align-items: center; | |
| justify-content: center; height: 100%; text-align: center; padding: 40px; | |
| } | |
| .welcome-icon { | |
| width: 100px; height: 100px; background: var(--accent-gradient); | |
| border-radius: 32px; display: flex; align-items: center; justify-content: center; | |
| font-size: 48px; margin-bottom: 28px; | |
| box-shadow: 0 12px 40px rgba(99, 102, 241, 0.35); | |
| animation: float 4s ease-in-out infinite; | |
| } | |
| @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } | |
| .welcome-title { | |
| font-size: 40px; font-weight: 800; margin-bottom: 16px; | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .welcome-subtitle { font-size: 17px; color: var(--text-secondary); max-width: 520px; line-height: 1.7; } | |
| .quick-prompts { | |
| display: flex; flex-wrap: wrap; gap: 12px; | |
| margin-top: 36px; justify-content: center; max-width: 650px; | |
| } | |
| .quick-prompt { | |
| padding: 12px 22px; background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); border-radius: 24px; | |
| font-size: 14px; color: var(--text-secondary); cursor: pointer; | |
| } | |
| .quick-prompt:hover { | |
| background: var(--bg-tertiary); border-color: var(--accent-primary); | |
| color: var(--text-primary); transform: translateY(-2px); | |
| } | |
| .message { | |
| display: flex; gap: 18px; margin-bottom: 28px; | |
| max-width: 920px; margin-left: auto; margin-right: auto; | |
| animation: messageSlideIn 0.4s ease; | |
| } | |
| @keyframes messageSlideIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } | |
| .message.user { flex-direction: row-reverse; } | |
| .message-avatar { | |
| width: 44px; height: 44px; border-radius: 14px; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; font-weight: 600; font-size: 13px; | |
| } | |
| .message.user .message-avatar { | |
| background: var(--accent-gradient); color: white; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); | |
| } | |
| .message.assistant .message-avatar { | |
| background: var(--bg-secondary); border: 1px solid var(--border-color); padding: 8px; | |
| } | |
| .message.assistant .message-avatar img { width: 100%; height: 100%; object-fit: contain; } | |
| .message-content { flex: 1; max-width: 75%; } | |
| .message-bubble { padding: 18px 24px; border-radius: 20px; font-size: 15px; line-height: 1.75; } | |
| .message.user .message-bubble { | |
| background: var(--accent-gradient); color: white; | |
| border-bottom-right-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(99, 102, 241, 0.25); | |
| } | |
| .message.assistant .message-bubble { | |
| background: var(--bg-secondary); border: 1px solid var(--border-color); | |
| border-bottom-left-radius: 8px; | |
| } | |
| .message-bubble pre { | |
| background: var(--bg-primary); padding: 18px; border-radius: 12px; | |
| overflow-x: auto; margin: 14px 0; font-family: monospace; font-size: 13px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .message-bubble code { | |
| background: var(--bg-tertiary); padding: 3px 8px; | |
| border-radius: 6px; font-family: monospace; font-size: 13px; | |
| } | |
| .message-bubble pre code { background: transparent; padding: 0; } | |
| .typing-indicator { display: flex; gap: 5px; padding: 10px 0; } | |
| .typing-indicator span { | |
| width: 9px; height: 9px; background: var(--accent-primary); | |
| border-radius: 50%; animation: typingBounce 1.4s infinite; | |
| } | |
| .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes typingBounce { | |
| 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } | |
| 30% { transform: translateY(-8px); opacity: 1; } | |
| } | |
| .input-container { | |
| padding: 24px 32px 32px; | |
| background: linear-gradient(to top, var(--bg-primary) 85%, transparent); | |
| } | |
| .input-wrapper { max-width: 920px; margin: 0 auto; } | |
| .input-box { | |
| display: flex; align-items: flex-end; gap: 14px; | |
| background: var(--bg-secondary); border: 1px solid var(--border-color); | |
| border-radius: 22px; padding: 14px 18px; | |
| } | |
| .input-box:focus-within { | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); | |
| } | |
| .input-box textarea { | |
| flex: 1; background: transparent; border: none; | |
| color: var(--text-primary); font-size: 15px; font-family: inherit; | |
| resize: none; outline: none; max-height: 160px; line-height: 1.6; | |
| } | |
| .input-box textarea::placeholder { color: var(--text-muted); } | |
| .send-btn { | |
| width: 48px; height: 48px; background: var(--accent-gradient); | |
| border: none; border-radius: 14px; color: white; cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 18px; flex-shrink: 0; | |
| } | |
| .send-btn:hover { transform: scale(1.08); box-shadow: 0 6px 25px rgba(99, 102, 241, 0.5); } | |
| .send-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } | |
| .input-hint { text-align: center; margin-top: 14px; font-size: 12px; color: var(--text-muted); } | |
| .input-hint kbd { background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px; font-size: 11px; } | |
| @media (max-width: 768px) { | |
| .header { padding: 0 16px; height: 64px; } | |
| .logo-title { font-size: 20px; } | |
| .logo-subtitle { display: none; } | |
| .model-selector-btn { min-width: auto; padding: 8px 14px; } | |
| .model-info { display: none; } | |
| .model-dropdown { width: calc(100vw - 32px); left: 16px; transform: scale(0.95); } | |
| .model-dropdown.active { transform: scale(1); } | |
| .status-badge span:not(.status-dot) { display: none; } | |
| .clear-btn span { display: none; } | |
| .main-container { padding-top: 64px; } | |
| .chat-container { padding: 20px 16px; } | |
| .input-container { padding: 16px; } | |
| .message-content { max-width: 85%; } | |
| .welcome-title { font-size: 28px; } | |
| .welcome-icon { width: 70px; height: 70px; font-size: 32px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-pattern"></div> | |
| <header class="header"> | |
| <div class="logo-section"> | |
| <div class="logo-icon">SF</div> | |
| <div class="logo-text"> | |
| <span class="logo-title">SFArena</span> | |
| <span class="logo-subtitle">by <a href="https://github.com/sixfingerdev" target="_blank">sixfingerdev</a></span> | |
| </div> | |
| </div> | |
| <div class="model-selector-wrapper" id="modelSelectorWrapper"> | |
| <div class="model-selector-btn" onclick="toggleModelDropdown()"> | |
| <img src="" alt="" class="model-logo" id="selectedModelLogo"> | |
| <div class="model-info"> | |
| <div class="model-name" id="selectedModelName">Model SeΓ§in</div> | |
| <div class="model-provider" id="selectedModelProvider">-</div> | |
| </div> | |
| <i class="fas fa-chevron-down dropdown-arrow"></i> | |
| </div> | |
| <div class="model-dropdown" id="modelDropdown"> | |
| <div class="model-search"> | |
| <input type="text" placeholder=" Model ara..." id="modelSearchInput" oninput="filterModels()"> | |
| </div> | |
| <div class="model-list" id="modelList"></div> | |
| </div> | |
| </div> | |
| <div class="status-section"> | |
| <div class="status-badge"> | |
| <span class="status-dot" id="statusDot"></span> | |
| <span id="statusText">BaΔlanΔ±yor...</span> | |
| </div> | |
| <button class="clear-btn" onclick="clearChat()"> | |
| <i class="fas fa-trash-alt"></i> | |
| <span>Temizle</span> | |
| </button> | |
| </div> | |
| </header> | |
| <main class="main-container"> | |
| <div class="chat-container" id="chatContainer"> | |
| <div class="welcome-screen" id="welcomeScreen"> | |
| <div class="welcome-icon"></div> | |
| <h1 class="welcome-title">SFArena</h1> | |
| <p class="welcome-subtitle">50+ yapay zeka modeli. GPT-5, Claude Opus, Gemini Pro ...</p> | |
| <div class="quick-prompts"> | |
| <div class="quick-prompt" onclick="sendQuickPrompt('Merhaba! Kimsin?')"> Merhaba</div> | |
| <div class="quick-prompt" onclick="sendQuickPrompt('Python ile REST API yaz')"> Python API</div> | |
| <div class="quick-prompt" onclick="sendQuickPrompt('KΔ±sa bir hikaye yaz')"> Hikaye</div> | |
| <div class="quick-prompt" onclick="sendQuickPrompt('JavaScript async/await aΓ§Δ±kla')"> JS Async</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-container"> | |
| <div class="input-wrapper"> | |
| <div class="input-box"> | |
| <textarea id="messageInput" placeholder="MesajΔ±nΔ±zΔ± yazΔ±n..." rows="1" | |
| onkeydown="handleKeyDown(event)" oninput="autoResize(this)"></textarea> | |
| <button class="send-btn" id="sendBtn" onclick="sendMessage()"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| <div class="input-hint"><kbd>Enter</kbd> gΓΆnder β’ <kbd>Shift+Enter</kbd> yeni satΔ±r</div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| const MODELS = {{ models | tojson }}; | |
| const PROVIDER_LOGOS = {{ provider_logos | tojson }}; | |
| let selectedModel = 'gpt-4o-mini'; | |
| let isConnected = false; | |
| let isStreaming = false; | |
| let sessionId = localStorage.getItem('sf_session_id') || ('sf_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2,9)); | |
| localStorage.setItem('sf_session_id', sessionId); | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initModels(); | |
| selectModel('gpt-4o-mini'); | |
| checkConnection(); | |
| document.getElementById('messageInput').focus(); | |
| }); | |
| function initModels() { | |
| const modelList = document.getElementById('modelList'); | |
| const providers = {}; | |
| for (const [id, data] of Object.entries(MODELS)) { | |
| if (!providers[data.provider]) providers[data.provider] = []; | |
| providers[data.provider].push({ id, ...data }); | |
| } | |
| let html = ''; | |
| for (const [provider, models] of Object.entries(providers)) { | |
| const logo = PROVIDER_LOGOS[provider] || ''; | |
| html += '<div class="provider-group" data-provider="' + provider + '">'; | |
| html += '<div class="provider-header"><img src="' + logo + '" onerror="this.style.display=\'none\'">' + provider.toUpperCase() + ' (' + models.length + ')</div>'; | |
| for (const m of models) { | |
| html += '<div class="model-item" data-model="' + m.id + '" data-name="' + m.name.toLowerCase() + '" onclick="selectModel(\'' + m.id + '\')">'; | |
| html += '<img src="' + logo + '" class="model-item-logo" onerror="this.style.background=\'#6366f1\'">'; | |
| html += '<div class="model-item-info"><div class="model-item-name">' + m.name + '</div>'; | |
| html += '<div class="model-item-id">' + m.id + '</div></div>'; | |
| html += '<div class="model-item-check"><i class="fas fa-check"></i></div></div>'; | |
| } | |
| html += '</div>'; | |
| } | |
| modelList.innerHTML = html; | |
| } | |
| function toggleModelDropdown() { | |
| document.getElementById('modelSelectorWrapper').classList.toggle('open'); | |
| document.getElementById('modelDropdown').classList.toggle('active'); | |
| } | |
| function filterModels() { | |
| const q = document.getElementById('modelSearchInput').value.toLowerCase(); | |
| document.querySelectorAll('.model-item').forEach(item => { | |
| const match = item.dataset.name.includes(q) || item.dataset.model.toLowerCase().includes(q); | |
| item.style.display = match ? 'flex' : 'none'; | |
| }); | |
| document.querySelectorAll('.provider-group').forEach(g => { | |
| g.style.display = Array.from(g.querySelectorAll('.model-item')).some(i => i.style.display !== 'none') ? 'block' : 'none'; | |
| }); | |
| } | |
| function selectModel(id) { | |
| selectedModel = id; | |
| const m = MODELS[id]; | |
| document.getElementById('selectedModelLogo').src = PROVIDER_LOGOS[m.provider]; | |
| document.getElementById('selectedModelName').textContent = m.name; | |
| document.getElementById('selectedModelProvider').textContent = m.provider.toUpperCase(); | |
| document.querySelectorAll('.model-item').forEach(i => i.classList.toggle('selected', i.dataset.model === id)); | |
| document.getElementById('modelDropdown').classList.remove('active'); | |
| document.getElementById('modelSelectorWrapper').classList.remove('open'); | |
| } | |
| document.addEventListener('click', e => { | |
| if (!document.querySelector('.model-selector-wrapper').contains(e.target)) { | |
| document.getElementById('modelDropdown').classList.remove('active'); | |
| document.getElementById('modelSelectorWrapper').classList.remove('open'); | |
| } | |
| }); | |
| async function checkConnection() { | |
| try { | |
| const r = await fetch('/api/init', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({session_id: sessionId}) | |
| }); | |
| const d = await r.json(); | |
| if (d.success) { | |
| isConnected = true; | |
| document.getElementById('statusDot').classList.add('connected'); | |
| document.getElementById('statusText').textContent = 'BaΔlandΔ±'; | |
| } else { | |
| document.getElementById('statusText').textContent = 'Hata: ' + d.error; | |
| } | |
| } catch (e) { | |
| document.getElementById('statusText').textContent = 'BaΔlantΔ± hatasΔ±'; | |
| } | |
| } | |
| function handleKeyDown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } | |
| } | |
| function autoResize(t) { | |
| t.style.height = 'auto'; | |
| t.style.height = Math.min(t.scrollHeight, 160) + 'px'; | |
| } | |
| function sendQuickPrompt(t) { | |
| document.getElementById('messageInput').value = t; | |
| sendMessage(); | |
| } | |
| async function sendMessage() { | |
| const input = document.getElementById('messageInput'); | |
| const msg = input.value.trim(); | |
| if (!msg || isStreaming) return; | |
| document.getElementById('welcomeScreen').style.display = 'none'; | |
| addMessage('user', msg); | |
| input.value = ''; input.style.height = 'auto'; | |
| isStreaming = true; | |
| document.getElementById('sendBtn').disabled = true; | |
| const msgId = addMessage('assistant', '', true); | |
| try { | |
| const r = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({session_id: sessionId, message: msg, model: selectedModel}) | |
| }); | |
| const reader = r.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let full = ''; | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) break; | |
| for (const line of decoder.decode(value).split('\n')) { | |
| if (line.startsWith('data: ')) { | |
| try { | |
| const d = JSON.parse(line.slice(6)); | |
| if (d.chunk) { full += d.chunk; updateMessage(msgId, full); } | |
| if (d.error) updateMessage(msgId, 'β ' + d.error); | |
| if (d.done) break; | |
| } catch {} | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| updateMessage(msgId, 'β Hata: ' + e.message); | |
| } | |
| isStreaming = false; | |
| document.getElementById('sendBtn').disabled = false; | |
| removeTyping(msgId); | |
| } | |
| function addMessage(role, content, typing = false) { | |
| const c = document.getElementById('chatContainer'); | |
| const id = 'msg_' + Date.now(); | |
| const logo = PROVIDER_LOGOS[MODELS[selectedModel]?.provider || 'openai']; | |
| const avatar = role === 'user' ? 'Sen' : '<img src="' + logo + '" onerror="this.parentElement.textContent=\'AI\'">'; | |
| let bubble = content ? formatMsg(content) : ''; | |
| if (typing) bubble += '<div class="typing-indicator"><span></span><span></span><span></span></div>'; | |
| c.insertAdjacentHTML('beforeend', | |
| '<div class="message ' + role + '" id="' + id + '">' + | |
| '<div class="message-avatar">' + avatar + '</div>' + | |
| '<div class="message-content"><div class="message-bubble">' + bubble + '</div></div></div>' | |
| ); | |
| c.scrollTop = c.scrollHeight; | |
| return id; | |
| } | |
| function updateMessage(id, content) { | |
| const b = document.querySelector('#' + id + ' .message-bubble'); | |
| if (b) { | |
| const t = b.querySelector('.typing-indicator'); | |
| b.innerHTML = formatMsg(content) + (t ? t.outerHTML : ''); | |
| document.getElementById('chatContainer').scrollTop = document.getElementById('chatContainer').scrollHeight; | |
| } | |
| } | |
| function removeTyping(id) { | |
| const t = document.querySelector('#' + id + ' .typing-indicator'); | |
| if (t) t.remove(); | |
| } | |
| function formatMsg(t) { | |
| return t | |
| .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>') | |
| .replace(/`([^`]+)`/g, '<code>$1</code>') | |
| .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') | |
| .replace(/\*([^*]+)\*/g, '<em>$1</em>') | |
| .replace(/\n/g, '<br>'); | |
| } | |
| function clearChat() { | |
| if (!confirm('Sohbet silinsin mi?')) return; | |
| document.getElementById('chatContainer').innerHTML = '<div class="welcome-screen" id="welcomeScreen"><div class="welcome-icon">π</div><h1 class="welcome-title">SFArena</h1><p class="welcome-subtitle">Yeni sohbet baΕlatΔ±n!</p></div>'; | |
| fetch('/api/clear', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({session_id:sessionId})}); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FLASK ROUTES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE, models=MODELS, provider_logos=PROVIDER_LOGOS, provider_colors=PROVIDER_COLORS) | |
| import os # En ΓΌste ekle | |
| # ... | |
| def init_client(): | |
| global client | |
| try: | |
| data = request.json | |
| session_id = data.get('session_id', 'default') | |
| if session_id not in conversations: | |
| conversations[session_id] = [] | |
| with _client_lock: | |
| if client is None: | |
| client = PuterClient() | |
| # Εifreyi ortam deΔiΕkeninden al | |
| password = os.getenv('PUTER_PASSWORD') | |
| if not password: | |
| raise Exception("PUTER_PASSWORD ortam deΔiΕkeni tanΔ±mlΔ± deΔil!") | |
| run_async(client.login("sixfinger", password)) | |
| return jsonify({"success": True}) | |
| except Exception as e: | |
| return jsonify({"success": False, "error": str(e)}) | |
| def chat(): | |
| global client, conversations | |
| data = request.json | |
| session_id = data.get('session_id', 'default') | |
| user_message = data.get('message', '') | |
| model = data.get('model', 'gpt-4o-mini') | |
| if session_id not in conversations: | |
| conversations[session_id] = [] | |
| conversations[session_id].append({"role": "user", "content": user_message}) | |
| def generate(): | |
| result_queue = Queue() | |
| loop = get_event_loop() | |
| async def async_stream(): | |
| try: | |
| messages = [ | |
| {"role": "system", "content": "Sen yardΔ±mcΔ± bir asistansΔ±n. TΓΌrkΓ§e yanΔ±t ver."}, | |
| ] + conversations[session_id] | |
| # Model bazlΔ± options | |
| options = get_model_options(model) | |
| stream = await client.ai_chat(messages=messages, options=options) | |
| full_response = "" | |
| async for text_chunk, model_name in stream: | |
| if text_chunk: | |
| full_response += text_chunk | |
| result_queue.put(('chunk', text_chunk)) | |
| conversations[session_id].append({"role": "assistant", "content": full_response}) | |
| result_queue.put(('done', None)) | |
| except Exception as e: | |
| result_queue.put(('error', str(e))) | |
| asyncio.run_coroutine_threadsafe(async_stream(), loop) | |
| while True: | |
| try: | |
| msg_type, data = result_queue.get(timeout=1200) | |
| if msg_type == 'chunk': | |
| yield f"data: {json.dumps({'chunk': data})}\n\n" | |
| elif msg_type == 'done': | |
| yield f"data: {json.dumps({'done': True})}\n\n" | |
| break | |
| elif msg_type == 'error': | |
| yield f"data: {json.dumps({'error': data})}\n\n" | |
| break | |
| except Empty: | |
| yield f"data: {json.dumps({'error': 'Zaman aΕΔ±mΔ±'})}\n\n" | |
| break | |
| return Response(generate(), mimetype='text/event-stream') | |
| def clear_history(): | |
| data = request.json | |
| session_id = data.get('session_id', 'default') | |
| if session_id in conversations: | |
| conversations[session_id] = [] | |
| return jsonify({"success": True}) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BAΕLATMA | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == '__main__': | |
| get_event_loop() | |
| print(""" | |
| ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β SFArena - AI Chat Platform β | |
| β by sixfingerdev β | |
| β github.com/sixfingerdev β | |
| β βββββββββββββββββββββββββββββββββββββββββββββββ£ | |
| β Sunucu: http://localhost:5000 β | |
| ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| """) | |
| app.run(debug=False, host='0.0.0.0', port=5000, threaded=True) |