sfarena / app.py
sixfingerdev's picture
Update app.py
e7d5fe8 verified
# 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
# ═══════════════════════════════════════════════════════════════════════════
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE, models=MODELS, provider_logos=PROVIDER_LOGOS, provider_colors=PROVIDER_COLORS)
import os # En ΓΌste ekle
# ...
@app.route('/api/init', methods=['POST'])
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)})
@app.route('/api/chat', methods=['POST'])
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')
@app.route('/api/clear', methods=['POST'])
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)