|
|
import gradio as gr |
|
|
import os |
|
|
import json |
|
|
import requests |
|
|
import xml.etree.ElementTree as ET |
|
|
import schedule |
|
|
import time |
|
|
import threading |
|
|
from huggingface_hub import HfApi, create_repo, hf_hub_download |
|
|
import warnings |
|
|
import pandas as pd |
|
|
from docx import Document |
|
|
import spaces |
|
|
from google.oauth2.service_account import Credentials |
|
|
from googleapiclient.discovery import build |
|
|
from googleapiclient.http import MediaIoBaseDownload |
|
|
import io |
|
|
import warnings |
|
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning |
|
|
from datetime import datetime |
|
|
import re |
|
|
from typing import List, Dict, Tuple, Optional |
|
|
import asyncio |
|
|
from dataclasses import dataclass |
|
|
from enum import Enum |
|
|
|
|
|
warnings.simplefilter('ignore', InsecureRequestWarning) |
|
|
warnings.filterwarnings("ignore", category=UserWarning, module="gradio.components.chatbot") |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class Product: |
|
|
name: str |
|
|
full_name: str |
|
|
stock_status: str |
|
|
price: str |
|
|
eft_price: str |
|
|
rebate_price: str |
|
|
rebate_money_order_price: str |
|
|
link: str |
|
|
category: str = "" |
|
|
|
|
|
class ChatMode(Enum): |
|
|
GENERAL = "general" |
|
|
PRODUCT_SEARCH = "product_search" |
|
|
BIKE_FINDER = "bike_finder" |
|
|
SUPPORT = "support" |
|
|
|
|
|
class SessionState: |
|
|
def __init__(self): |
|
|
self.mode = ChatMode.GENERAL |
|
|
self.bike_finder_step = 0 |
|
|
self.user_preferences = {} |
|
|
self.search_history = [] |
|
|
self.favorite_products = [] |
|
|
|
|
|
|
|
|
class TrekAssistant: |
|
|
def __init__(self): |
|
|
self.session_states = {} |
|
|
self.products = [] |
|
|
self.system_messages = [] |
|
|
self.api_key = os.getenv("OPENAI_API_KEY") |
|
|
self.hf_token = os.getenv("hfapi") |
|
|
self.log_file = '/data/chat_logs.txt' if os.path.exists('/data') else 'chat_logs.txt' |
|
|
self.global_chat_history = [] |
|
|
self.history_lock = threading.Lock() |
|
|
self.file_lock = threading.Lock() |
|
|
self.last_logged_index = 0 |
|
|
|
|
|
|
|
|
self.initialize_products() |
|
|
self.initialize_system_messages() |
|
|
self.setup_scheduler() |
|
|
|
|
|
def initialize_products(self): |
|
|
"""Trek ürünlerini API'den çek ve işle""" |
|
|
try: |
|
|
url = 'https://www.trekbisiklet.com.tr/output/8582384479' |
|
|
response = requests.get(url, verify=False) |
|
|
root = ET.fromstring(response.content) |
|
|
|
|
|
for item in root.findall('item'): |
|
|
name_words = item.find('rootlabel').text.lower().split() |
|
|
name = name_words[0] |
|
|
full_name = ' '.join(name_words) |
|
|
|
|
|
stock_amount = "stokta" if item.find('stockAmount').text > '0' else "stokta değil" |
|
|
|
|
|
if stock_amount == "stokta": |
|
|
|
|
|
price_info = self._process_prices(item) |
|
|
product_link = item.find('productLink').text if item.find('productLink') is not None else "" |
|
|
|
|
|
product = Product( |
|
|
name=name, |
|
|
full_name=full_name, |
|
|
stock_status=stock_amount, |
|
|
price=price_info['price'], |
|
|
eft_price=price_info['eft_price'], |
|
|
rebate_price=price_info['rebate_price'], |
|
|
rebate_money_order_price=price_info['rebate_money_order_price'], |
|
|
link=product_link, |
|
|
category=self._determine_category(full_name) |
|
|
) |
|
|
else: |
|
|
product = Product( |
|
|
name=name, |
|
|
full_name=full_name, |
|
|
stock_status=stock_amount, |
|
|
price="", |
|
|
eft_price="", |
|
|
rebate_price="", |
|
|
rebate_money_order_price="", |
|
|
link="", |
|
|
category=self._determine_category(full_name) |
|
|
) |
|
|
|
|
|
self.products.append(product) |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Ürün yükleme hatası: {e}") |
|
|
|
|
|
def _process_prices(self, item): |
|
|
"""Fiyat bilgilerini işle ve yuvarla""" |
|
|
def round_price(price_str): |
|
|
try: |
|
|
price_float = float(price_str) |
|
|
if price_float > 200000: |
|
|
return str(round(price_float / 5000) * 5000) |
|
|
elif price_float > 30000: |
|
|
return str(round(price_float / 1000) * 1000) |
|
|
elif price_float > 10000: |
|
|
return str(round(price_float / 100) * 100) |
|
|
else: |
|
|
return str(round(price_float / 10) * 10) |
|
|
except (ValueError, TypeError): |
|
|
return price_str |
|
|
|
|
|
price_str = item.find('priceTaxWithCur').text if item.find('priceTaxWithCur') is not None else "0" |
|
|
price_eft_str = item.find('priceEft').text if item.find('priceEft') is not None else "" |
|
|
price_rebate_str = item.find('priceRebateWithTax').text if item.find('priceRebateWithTax') is not None else "" |
|
|
price_rebate_money_order_str = item.find('priceRebateWithMoneyOrderWithTax').text if item.find('priceRebateWithMoneyOrderWithTax') is not None else "" |
|
|
|
|
|
|
|
|
if not price_eft_str: |
|
|
try: |
|
|
price_eft_str = str(float(price_str) * 0.975) |
|
|
except: |
|
|
price_eft_str = "" |
|
|
|
|
|
return { |
|
|
'price': round_price(price_str), |
|
|
'eft_price': round_price(price_eft_str), |
|
|
'rebate_price': round_price(price_rebate_str), |
|
|
'rebate_money_order_price': round_price(price_rebate_money_order_str) |
|
|
} |
|
|
|
|
|
def _determine_category(self, full_name): |
|
|
"""Ürün kategorisini belirle""" |
|
|
name_lower = full_name.lower() |
|
|
if any(x in name_lower for x in ['madone', 'emonda', 'domane', 'checkpoint', 'checkmate']): |
|
|
return "Yol Bisikleti" |
|
|
elif any(x in name_lower for x in ['marlin', 'roscoe', 'procaliber', 'supercaliber', 'fuel']): |
|
|
return "Dağ Bisikleti" |
|
|
elif any(x in name_lower for x in ['fx', 'ds', 'dual sport']): |
|
|
return "Şehir Bisikleti" |
|
|
elif any(x in name_lower for x in ['powerfly', 'rail', 'verve+', 'townie+', 'domane+', 'fuel exe']): |
|
|
return "Elektrikli Bisiklet" |
|
|
elif any(x in name_lower for x in ['checkpoint']): |
|
|
return "Gravel Bisiklet" |
|
|
return "Diğer" |
|
|
|
|
|
def initialize_system_messages(self): |
|
|
"""Sistem mesajlarını hazırla""" |
|
|
self.base_system_messages = [ |
|
|
{"role": "system", "content": """Sen Trek Bisiklet'in gelişmiş AI asistanısın. |
|
|
Görevin müşterilere en iyi hizmeti sunmak, doğru bisiklet seçiminde yardımcı olmak ve |
|
|
tüm sorularını profesyonel bir şekilde yanıtlamak. |
|
|
|
|
|
Önemli kurallar: |
|
|
1. Her zaman kibar, yardımsever ve profesyonel ol |
|
|
2. Sadece Trek markası ürünlerini öner |
|
|
3. Stok bilgilerini güncel tut |
|
|
4. Fiyatları doğru ve net bir şekilde belirt |
|
|
5. Müşteri memnuniyetini ön planda tut |
|
|
6. Teknik soruları detaylı ama anlaşılır şekilde yanıtla |
|
|
7. Satış sonrası hizmetler hakkında bilgi ver |
|
|
8. Mağaza bilgilerini doğru aktar"""} |
|
|
] |
|
|
|
|
|
|
|
|
self.store_info = { |
|
|
"istanbul": [ |
|
|
{ |
|
|
"name": "Ortaköy Mağazası", |
|
|
"address": "Dereboyu Cad No:84 Ortaköy Beşiktaş", |
|
|
"phone": "0212 227 10 15", |
|
|
"hours": "10:00 - 19:00", |
|
|
"notes": "Toyota Plaza ve Carrefour yanında" |
|
|
}, |
|
|
{ |
|
|
"name": "Caddebostan Mağazası", |
|
|
"address": "Prof. Dr. Hulusi Behçet 18 Caddebostan, Kadıköy", |
|
|
"phone": "0216 629 24 32", |
|
|
"hours": "10:00 - 19:00", |
|
|
"notes": "Göztepe Parkı karşısında, Bike Fit hizmeti mevcut" |
|
|
}, |
|
|
{ |
|
|
"name": "Sarıyer Mağazası", |
|
|
"address": "Mareşal Fevzi Çakmak Cad. No 54 Kemer-Bahçeköy Mahallesi Sarıyer", |
|
|
"phone": "0542 137 10 80", |
|
|
"hours": "10:00 - 19:00", |
|
|
"notes": "Elektrikli bisiklet merkezi" |
|
|
} |
|
|
], |
|
|
"izmir": [ |
|
|
{ |
|
|
"name": "İzmir Mağazası", |
|
|
"address": "Sezer Doğan Sok. The Kar Suits 14A Alsancak Konak İzmir", |
|
|
"phone": "0543 936 23 35", |
|
|
"hours": "10:00 - 19:00", |
|
|
"notes": "Yeni açıldı!" |
|
|
} |
|
|
] |
|
|
} |
|
|
|
|
|
def get_session_state(self, session_id: str) -> SessionState: |
|
|
"""Oturum durumunu al veya oluştur""" |
|
|
if session_id not in self.session_states: |
|
|
self.session_states[session_id] = SessionState() |
|
|
return self.session_states[session_id] |
|
|
|
|
|
def detect_intent(self, message: str) -> ChatMode: |
|
|
"""Kullanıcı mesajından niyeti anla""" |
|
|
message_lower = message.lower() |
|
|
|
|
|
|
|
|
if any(word in message_lower for word in ['fiyat', 'stok', 'model', 'bisiklet', 'ürün']): |
|
|
return ChatMode.PRODUCT_SEARCH |
|
|
|
|
|
|
|
|
if any(phrase in message_lower for phrase in ['hangi bisiklet', 'öner', 'seçim', 'bana uygun']): |
|
|
return ChatMode.BIKE_FINDER |
|
|
|
|
|
|
|
|
if any(word in message_lower for word in ['servis', 'garanti', 'mağaza', 'adres', 'telefon']): |
|
|
return ChatMode.SUPPORT |
|
|
|
|
|
return ChatMode.GENERAL |
|
|
|
|
|
def search_products(self, query: str) -> List[Product]: |
|
|
"""Ürün ara""" |
|
|
query_lower = query.lower() |
|
|
results = [] |
|
|
|
|
|
for product in self.products: |
|
|
if query_lower in product.full_name.lower() or query_lower in product.name.lower(): |
|
|
results.append(product) |
|
|
|
|
|
return results |
|
|
|
|
|
def format_product_info(self, product: Product) -> str: |
|
|
"""Ürün bilgilerini formatla""" |
|
|
if product.stock_status != "stokta": |
|
|
return f"❌ **{product.full_name}** - Stokta değil" |
|
|
|
|
|
info = f"✅ **{product.full_name}**\n" |
|
|
info += f"📦 Durum: {product.stock_status}\n" |
|
|
|
|
|
|
|
|
has_campaign = product.rebate_price and product.rebate_price != "" |
|
|
|
|
|
if has_campaign: |
|
|
info += f"💰 ~~{product.price} TL~~ → **{product.rebate_price} TL** (Kampanyalı!)\n" |
|
|
|
|
|
|
|
|
try: |
|
|
discount = float(product.price) - float(product.rebate_price) |
|
|
info += f"🎯 İndirim: {discount:.0f} TL\n" |
|
|
except: |
|
|
pass |
|
|
else: |
|
|
info += f"💰 Fiyat: **{product.price} TL**\n" |
|
|
if product.eft_price: |
|
|
info += f"💳 Havale: {product.eft_price} TL (%2.5 indirim)\n" |
|
|
|
|
|
if product.link: |
|
|
info += f"🔗 [Ürün Detayları]({product.link})\n" |
|
|
|
|
|
return info |
|
|
|
|
|
def bike_finder_process(self, session_state: SessionState, user_message: str) -> str: |
|
|
"""Bisiklet bulucu süreci""" |
|
|
steps = [ |
|
|
"Hangi tür bisikletle ilgileniyorsunuz? (Yol, Dağ, Şehir, Elektrikli, Gravel)", |
|
|
"Bisikleti hangi amaçla kullanacaksınız? (Günlük ulaşım, spor, tur, yarış vb.)", |
|
|
"Hangi özellikler sizin için önemli? (Hız, konfor, dayanıklılık, hafiflik)", |
|
|
"Genelde hangi zeminlerde kullanacaksınız? (Asfalt, toprak, karışık)", |
|
|
"Boy ve iç bacak ölçülerinizi paylaşır mısınız?", |
|
|
"Bütçeniz nedir?" |
|
|
] |
|
|
|
|
|
current_step = session_state.bike_finder_step |
|
|
|
|
|
if current_step < len(steps): |
|
|
|
|
|
if current_step > 0: |
|
|
session_state.user_preferences[f"step_{current_step}"] = user_message |
|
|
|
|
|
|
|
|
session_state.bike_finder_step += 1 |
|
|
|
|
|
if session_state.bike_finder_step < len(steps): |
|
|
return f"**Adım {session_state.bike_finder_step}/{len(steps)}**\n\n{steps[session_state.bike_finder_step - 1]}" |
|
|
else: |
|
|
|
|
|
return self.generate_bike_recommendation(session_state.user_preferences) |
|
|
|
|
|
return "Bisiklet bulma sürecini başlatmak ister misiniz?" |
|
|
|
|
|
def generate_bike_recommendation(self, preferences: dict) -> str: |
|
|
"""Bisiklet önerisi oluştur""" |
|
|
recommendation = "🚴 **Sizin için önerilerimiz:**\n\n" |
|
|
|
|
|
|
|
|
|
|
|
bike_type = preferences.get('step_1', '').lower() |
|
|
|
|
|
if 'yol' in bike_type: |
|
|
recommendation += "**1. Trek Madone SL 6 Gen 8**\n" |
|
|
recommendation += "Hafif, hızlı ve aerodinamik yol bisikleti\n\n" |
|
|
recommendation += "**2. Trek Domane SL 5**\n" |
|
|
recommendation += "Konforlu geometri, uzun turlar için ideal\n\n" |
|
|
elif 'dağ' in bike_type: |
|
|
recommendation += "**1. Trek Marlin 7**\n" |
|
|
recommendation += "Giriş seviyesi, sağlam ve güvenilir\n\n" |
|
|
recommendation += "**2. Trek Fuel EX 8**\n" |
|
|
recommendation += "Full süspansiyon, zorlu parkurlar için\n\n" |
|
|
|
|
|
recommendation += "\n📞 Detaylı bilgi için mağazalarımızı ziyaret edebilirsiniz!" |
|
|
|
|
|
return recommendation |
|
|
|
|
|
def format_store_info(self, city: str = None) -> str: |
|
|
"""Mağaza bilgilerini formatla""" |
|
|
info = "🏪 **Trek Mağazaları**\n\n" |
|
|
|
|
|
if city and city.lower() in self.store_info: |
|
|
stores = self.store_info[city.lower()] |
|
|
else: |
|
|
stores = [] |
|
|
for city_stores in self.store_info.values(): |
|
|
stores.extend(city_stores) |
|
|
|
|
|
for store in stores: |
|
|
info += f"**{store['name']}**\n" |
|
|
info += f"📍 {store['address']}\n" |
|
|
info += f"📞 {store['phone']}\n" |
|
|
info += f"🕐 {store['hours']}\n" |
|
|
if store.get('notes'): |
|
|
info += f"ℹ️ {store['notes']}\n" |
|
|
info += "\n" |
|
|
|
|
|
info += "❗ Tüm mağazalarımız Pazar günü kapalıdır." |
|
|
|
|
|
return info |
|
|
|
|
|
@spaces.GPU(duration=1200) |
|
|
async def process_message(self, message: str, history: List, session_id: str): |
|
|
"""Ana mesaj işleme fonksiyonu""" |
|
|
try: |
|
|
session_state = self.get_session_state(session_id) |
|
|
|
|
|
|
|
|
self.log_message("user", message) |
|
|
|
|
|
|
|
|
if session_state.mode == ChatMode.BIKE_FINDER and session_state.bike_finder_step > 0: |
|
|
response = self.bike_finder_process(session_state, message) |
|
|
else: |
|
|
|
|
|
intent = self.detect_intent(message) |
|
|
session_state.mode = intent |
|
|
|
|
|
if intent == ChatMode.PRODUCT_SEARCH: |
|
|
|
|
|
products = self.search_products(message) |
|
|
if products: |
|
|
response = "🔍 **Arama Sonuçları:**\n\n" |
|
|
for product in products[:5]: |
|
|
response += self.format_product_info(product) + "\n" |
|
|
else: |
|
|
response = "❌ Aradığınız kriterlere uygun ürün bulunamadı." |
|
|
|
|
|
elif intent == ChatMode.BIKE_FINDER: |
|
|
session_state.bike_finder_step = 1 |
|
|
response = self.bike_finder_process(session_state, message) |
|
|
|
|
|
elif intent == ChatMode.SUPPORT: |
|
|
if 'mağaza' in message.lower() or 'adres' in message.lower(): |
|
|
response = self.format_store_info() |
|
|
else: |
|
|
response = await self.get_ai_response(message, history) |
|
|
|
|
|
else: |
|
|
response = await self.get_ai_response(message, history) |
|
|
|
|
|
|
|
|
self.log_message("assistant", response) |
|
|
|
|
|
|
|
|
with self.history_lock: |
|
|
self.global_chat_history.append({"role": "user", "content": message}) |
|
|
self.global_chat_history.append({"role": "assistant", "content": response}) |
|
|
|
|
|
return response |
|
|
|
|
|
except Exception as e: |
|
|
print(f"İşlem hatası: {e}") |
|
|
return "❌ Bir hata oluştu. Lütfen tekrar deneyin." |
|
|
|
|
|
async def get_ai_response(self, message: str, history: List) -> str: |
|
|
"""OpenAI API'den yanıt al""" |
|
|
messages = self.base_system_messages + history + [{"role": "user", "content": message}] |
|
|
|
|
|
|
|
|
input_words = message.lower().split() |
|
|
for product in self.products: |
|
|
if product.name in input_words: |
|
|
product_info = self.format_product_info(product) |
|
|
messages.append({"role": "system", "content": f"Ürün bilgisi: {product_info}"}) |
|
|
|
|
|
payload = { |
|
|
"model": "gpt-4.1", |
|
|
"messages": messages, |
|
|
"temperature": 0.7, |
|
|
"max_tokens": 1000, |
|
|
"stream": True |
|
|
} |
|
|
|
|
|
headers = { |
|
|
"Content-Type": "application/json", |
|
|
"Authorization": f"Bearer {self.api_key}" |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.post( |
|
|
"https://api.openai.com/v1/chat/completions", |
|
|
headers=headers, |
|
|
json=payload, |
|
|
stream=True |
|
|
) |
|
|
|
|
|
if response.status_code != 200: |
|
|
return "❌ Bir hata oluştu. Lütfen daha sonra tekrar deneyin." |
|
|
|
|
|
full_response = "" |
|
|
for chunk in response.iter_lines(): |
|
|
if chunk: |
|
|
chunk_str = chunk.decode('utf-8') |
|
|
if chunk_str.startswith("data: ") and chunk_str != "data: [DONE]": |
|
|
try: |
|
|
chunk_data = json.loads(chunk_str[6:]) |
|
|
delta = chunk_data['choices'][0]['delta'] |
|
|
if 'content' in delta: |
|
|
full_response += delta['content'] |
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
|
|
|
return full_response |
|
|
|
|
|
except Exception as e: |
|
|
print(f"API hatası: {e}") |
|
|
return "❌ Bir hata oluştu. Lütfen daha sonra tekrar deneyin." |
|
|
|
|
|
def log_message(self, role: str, content: str): |
|
|
"""Mesajları logla""" |
|
|
try: |
|
|
with self.file_lock: |
|
|
with open(self.log_file, 'a', encoding='utf-8') as f: |
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
|
|
f.write(f"[{timestamp}] {role.upper()}: {content}\n") |
|
|
except Exception as e: |
|
|
print(f"Log hatası: {e}") |
|
|
|
|
|
def setup_scheduler(self): |
|
|
"""Zamanlayıcı kur""" |
|
|
def scheduled_upload(): |
|
|
self.upload_logs_to_hf() |
|
|
|
|
|
schedule.every().day.at("11:32").do(scheduled_upload) |
|
|
schedule.every().day.at("15:30").do(scheduled_upload) |
|
|
schedule.every().day.at("19:30").do(scheduled_upload) |
|
|
schedule.every().day.at("21:30").do(scheduled_upload) |
|
|
|
|
|
def run_schedule(): |
|
|
while True: |
|
|
schedule.run_pending() |
|
|
time.sleep(60) |
|
|
|
|
|
scheduler_thread = threading.Thread(target=run_schedule, daemon=True) |
|
|
scheduler_thread.start() |
|
|
|
|
|
def upload_logs_to_hf(self): |
|
|
"""Logları Hugging Face'e yükle""" |
|
|
try: |
|
|
api = HfApi(token=self.hf_token) |
|
|
api.upload_file( |
|
|
path_or_fileobj=self.log_file, |
|
|
path_in_repo="chat_logs.txt", |
|
|
repo_id="SamiKoen/BF", |
|
|
repo_type="space", |
|
|
commit_message=f"Log güncelleme - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" |
|
|
) |
|
|
print("Loglar HF'ye yüklendi") |
|
|
except Exception as e: |
|
|
print(f"HF yükleme hatası: {e}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_css = """ |
|
|
/* Modern font ayarları */ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
|
|
|
|
|
* { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; |
|
|
} |
|
|
|
|
|
/* Ana container */ |
|
|
.gradio-container { |
|
|
max-width: 1200px !important; |
|
|
margin: 0 auto !important; |
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; |
|
|
min-height: 100vh !important; |
|
|
} |
|
|
|
|
|
/* Başlık alanı */ |
|
|
.main-header { |
|
|
background: linear-gradient(135deg, #e60012 0%, #c70000 100%); |
|
|
color: white; |
|
|
padding: 2rem; |
|
|
text-align: center; |
|
|
border-radius: 0 0 20px 20px; |
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1); |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
|
|
|
.main-header h1 { |
|
|
font-size: 2.5rem !important; |
|
|
font-weight: 700 !important; |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
|
|
|
.main-header p { |
|
|
font-size: 1.1rem; |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
/* Chat container */ |
|
|
.chat-container { |
|
|
background: white; |
|
|
border-radius: 20px; |
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1); |
|
|
padding: 0; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
/* Mesaj alanı */ |
|
|
#chatbot { |
|
|
height: 500px !important; |
|
|
border: none !important; |
|
|
background: #fafafa !important; |
|
|
} |
|
|
|
|
|
/* Mesaj baloncukları */ |
|
|
.message { |
|
|
margin: 1rem !important; |
|
|
animation: fadeIn 0.3s ease-out; |
|
|
} |
|
|
|
|
|
@keyframes fadeIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateY(10px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
/* Kullanıcı mesajları */ |
|
|
.user-message { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
|
color: white !important; |
|
|
border-radius: 18px 18px 4px 18px !important; |
|
|
padding: 1rem 1.5rem !important; |
|
|
margin-left: auto !important; |
|
|
max-width: 70% !important; |
|
|
box-shadow: 0 3px 15px rgba(102, 126, 234, 0.3) !important; |
|
|
} |
|
|
|
|
|
/* Bot mesajları */ |
|
|
.bot-message { |
|
|
background: white !important; |
|
|
color: #2d3748 !important; |
|
|
border: 1px solid #e2e8f0 !important; |
|
|
border-radius: 18px 18px 18px 4px !important; |
|
|
padding: 1rem 1.5rem !important; |
|
|
max-width: 80% !important; |
|
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.08) !important; |
|
|
} |
|
|
|
|
|
/* Markdown stil düzenlemeleri */ |
|
|
.bot-message h1, .bot-message h2, .bot-message h3 { |
|
|
color: #e60012 !important; |
|
|
margin-top: 1rem !important; |
|
|
margin-bottom: 0.5rem !important; |
|
|
} |
|
|
|
|
|
.bot-message strong { |
|
|
color: #1a202c !important; |
|
|
font-weight: 600 !important; |
|
|
} |
|
|
|
|
|
.bot-message a { |
|
|
color: #e60012 !important; |
|
|
text-decoration: none !important; |
|
|
font-weight: 500 !important; |
|
|
border-bottom: 1px solid transparent !important; |
|
|
transition: border-bottom 0.2s !important; |
|
|
} |
|
|
|
|
|
.bot-message a:hover { |
|
|
border-bottom: 1px solid #e60012 !important; |
|
|
} |
|
|
|
|
|
/* Input alanı */ |
|
|
.message-textbox { |
|
|
border: 2px solid #e2e8f0 !important; |
|
|
border-radius: 12px !important; |
|
|
padding: 1rem !important; |
|
|
font-size: 1rem !important; |
|
|
transition: all 0.3s !important; |
|
|
background: white !important; |
|
|
} |
|
|
|
|
|
.message-textbox:focus { |
|
|
border-color: #e60012 !important; |
|
|
box-shadow: 0 0 0 3px rgba(230, 0, 18, 0.1) !important; |
|
|
outline: none !important; |
|
|
} |
|
|
|
|
|
/* Gönder butonu */ |
|
|
.send-button { |
|
|
background: linear-gradient(135deg, #e60012 0%, #c70000 100%) !important; |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
border-radius: 12px !important; |
|
|
padding: 0.75rem 2rem !important; |
|
|
font-weight: 600 !important; |
|
|
font-size: 1rem !important; |
|
|
cursor: pointer !important; |
|
|
transition: all 0.3s !important; |
|
|
} |
|
|
|
|
|
.send-button:hover { |
|
|
transform: translateY(-2px) !important; |
|
|
box-shadow: 0 5px 20px rgba(230, 0, 18, 0.3) !important; |
|
|
} |
|
|
|
|
|
/* Quick actions */ |
|
|
.quick-actions { |
|
|
display: flex; |
|
|
gap: 1rem; |
|
|
margin: 1rem 0; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.quick-action-btn { |
|
|
background: white; |
|
|
border: 2px solid #e2e8f0; |
|
|
border-radius: 20px; |
|
|
padding: 0.5rem 1rem; |
|
|
font-size: 0.9rem; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.quick-action-btn:hover { |
|
|
border-color: #e60012; |
|
|
color: #e60012; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
/* Yan panel */ |
|
|
.side-panel { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 1.5rem; |
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
.side-panel h3 { |
|
|
color: #1a202c; |
|
|
font-size: 1.2rem; |
|
|
margin-bottom: 1rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
/* Feature cards */ |
|
|
.feature-card { |
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%); |
|
|
border-radius: 12px; |
|
|
padding: 1rem; |
|
|
margin-bottom: 1rem; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
.feature-card:hover { |
|
|
transform: translateX(5px); |
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
/* Loading animation */ |
|
|
.typing-indicator { |
|
|
display: flex; |
|
|
gap: 4px; |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.typing-dot { |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
background: #718096; |
|
|
border-radius: 50%; |
|
|
animation: typing 1.4s infinite; |
|
|
} |
|
|
|
|
|
.typing-dot:nth-child(2) { |
|
|
animation-delay: 0.2s; |
|
|
} |
|
|
|
|
|
.typing-dot:nth-child(3) { |
|
|
animation-delay: 0.4s; |
|
|
} |
|
|
|
|
|
@keyframes typing { |
|
|
0%, 60%, 100% { |
|
|
transform: translateY(0); |
|
|
} |
|
|
30% { |
|
|
transform: translateY(-10px); |
|
|
} |
|
|
} |
|
|
|
|
|
/* Responsive tasarım */ |
|
|
@media (max-width: 768px) { |
|
|
.gradio-container { |
|
|
padding: 0 !important; |
|
|
} |
|
|
|
|
|
.main-header { |
|
|
border-radius: 0 !important; |
|
|
padding: 1.5rem !important; |
|
|
} |
|
|
|
|
|
.main-header h1 { |
|
|
font-size: 1.8rem !important; |
|
|
} |
|
|
|
|
|
.chat-container { |
|
|
border-radius: 0 !important; |
|
|
margin: 0 !important; |
|
|
} |
|
|
|
|
|
.user-message, .bot-message { |
|
|
max-width: 90% !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Emoji desteği */ |
|
|
.emoji { |
|
|
font-size: 1.2em; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
/* Smooth scrolling */ |
|
|
html { |
|
|
scroll-behavior: smooth; |
|
|
} |
|
|
|
|
|
/* Custom scrollbar */ |
|
|
::-webkit-scrollbar { |
|
|
width: 8px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-track { |
|
|
background: #f1f1f1; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-thumb { |
|
|
background: #888; |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
|
background: #555; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
custom_js = """ |
|
|
<script> |
|
|
// Sayfa yüklendiğinde |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
// Welcome animasyonu |
|
|
setTimeout(() => { |
|
|
const welcomeMsg = document.querySelector('.bot-message'); |
|
|
if (welcomeMsg) { |
|
|
welcomeMsg.classList.add('animate-bounce'); |
|
|
} |
|
|
}, 500); |
|
|
|
|
|
// Placeholder ayarla |
|
|
const textarea = document.querySelector('textarea'); |
|
|
if (textarea) { |
|
|
textarea.placeholder = "Merhaba! Size nasıl yardımcı olabilirim? (Örn: Yol bisikleti arıyorum)"; |
|
|
} |
|
|
|
|
|
// Enter tuşu ile gönder |
|
|
textarea?.addEventListener('keypress', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
const sendBtn = document.querySelector('.send-button'); |
|
|
sendBtn?.click(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
// Mesaj gönderildiğinde scroll to bottom |
|
|
function scrollToBottom() { |
|
|
const chatbot = document.querySelector('#chatbot'); |
|
|
if (chatbot) { |
|
|
chatbot.scrollTop = chatbot.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
// Observer for new messages |
|
|
const observer = new MutationObserver(scrollToBottom); |
|
|
const chatElement = document.querySelector('#chatbot'); |
|
|
if (chatElement) { |
|
|
observer.observe(chatElement, { childList: true, subtree: true }); |
|
|
} |
|
|
</script> |
|
|
""" |
|
|
|
|
|
|
|
|
assistant = TrekAssistant() |
|
|
|
|
|
|
|
|
def create_welcome_message(): |
|
|
"""Hoş geldin mesajı oluştur""" |
|
|
return f""" |
|
|
🚴 **Trek Bisiklet'e Hoş Geldiniz!** |
|
|
|
|
|
Ben Trek AI Asistanınızım. Size şu konularda yardımcı olabilirim: |
|
|
|
|
|
🔍 **Ürün Arama**: Stok durumu, fiyat bilgisi, ürün detayları |
|
|
🎯 **Bisiklet Önerisi**: Size en uygun bisikleti bulalım |
|
|
🏪 **Mağaza Bilgileri**: Adres, telefon, çalışma saatleri |
|
|
🛠️ **Teknik Destek**: Bakım, servis, garanti konuları |
|
|
💡 **Genel Bilgi**: Trek hakkında her şey |
|
|
|
|
|
Nasıl yardımcı olabilirim? |
|
|
""" |
|
|
|
|
|
def process_chat(message, history, session_id): |
|
|
"""Chat işleme wrapper fonksiyonu""" |
|
|
if not message: |
|
|
return "" |
|
|
|
|
|
try: |
|
|
|
|
|
formatted_history = [] |
|
|
for msg in history: |
|
|
if isinstance(msg, dict): |
|
|
formatted_history.append(msg) |
|
|
elif isinstance(msg, list) and len(msg) == 2: |
|
|
if msg[0]: |
|
|
formatted_history.append({"role": "user", "content": msg[0]}) |
|
|
if msg[1]: |
|
|
formatted_history.append({"role": "assistant", "content": msg[1]}) |
|
|
|
|
|
|
|
|
loop = asyncio.new_event_loop() |
|
|
asyncio.set_event_loop(loop) |
|
|
response = loop.run_until_complete( |
|
|
assistant.process_message(message, formatted_history, session_id) |
|
|
) |
|
|
|
|
|
return response |
|
|
except Exception as e: |
|
|
print(f"Chat işleme hatası: {e}") |
|
|
return "❌ Bir hata oluştu. Lütfen tekrar deneyin." |
|
|
|
|
|
def create_quick_action_buttons(): |
|
|
"""Hızlı aksiyon butonları""" |
|
|
return gr.HTML(""" |
|
|
<div class="quick-actions"> |
|
|
<button class="quick-action-btn" onclick="document.querySelector('textarea').value='Yol bisikleti modelleri nelerdir?'; document.querySelector('.send-button').click();"> |
|
|
🚴 Yol Bisikletleri |
|
|
</button> |
|
|
<button class="quick-action-btn" onclick="document.querySelector('textarea').value='Elektrikli bisiklet fiyatları'; document.querySelector('.send-button').click();"> |
|
|
⚡ E-Bike Fiyatları |
|
|
</button> |
|
|
<button class="quick-action-btn" onclick="document.querySelector('textarea').value='Size göre bisiklet önerir misiniz?'; document.querySelector('.send-button').click();"> |
|
|
🎯 Bisiklet Önerisi |
|
|
</button> |
|
|
<button class="quick-action-btn" onclick="document.querySelector('textarea').value='Mağaza adresleri'; document.querySelector('.send-button').click();"> |
|
|
📍 Mağazalar |
|
|
</button> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
def create_side_panel(): |
|
|
"""Yan panel içeriği""" |
|
|
return gr.HTML(""" |
|
|
<div class="side-panel"> |
|
|
<h3>🌟 Öne Çıkan Özellikler</h3> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<h4>🏆 Ömür Boyu Garanti</h4> |
|
|
<p>Tüm Trek bisiklet kadroları ömür boyu garantilidir!</p> |
|
|
</div> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<h4>🔧 Bike Fit Hizmeti</h4> |
|
|
<p>Caddebostan mağazamızda profesyonel bike fit hizmeti</p> |
|
|
</div> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<h4>📱 Canlı Destek</h4> |
|
|
<p>Web sitemizden 7/24 canlı destek alabilirsiniz</p> |
|
|
</div> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<h4>🚚 Hızlı Teslimat</h4> |
|
|
<p>24 saat içinde kargoya teslim</p> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
try: |
|
|
with gr.Blocks( |
|
|
title="Trek Bisiklet - AI Asistan", |
|
|
theme=gr.themes.Soft( |
|
|
primary_hue="red", |
|
|
secondary_hue="slate", |
|
|
), |
|
|
css=custom_css, |
|
|
head=custom_js |
|
|
) as demo: |
|
|
|
|
|
|
|
|
session_id = gr.State(value=str(time.time())) |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="main-header"> |
|
|
<h1>🚴 Trek Bisiklet AI Asistanı</h1> |
|
|
<p>Türkiye'nin en güvenilir bisiklet markası - 2000'den beri Alatin Bisiklet güvencesiyle</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
chatbot = gr.Chatbot( |
|
|
value=[], |
|
|
elem_id="chatbot", |
|
|
show_label=False, |
|
|
height=500 |
|
|
) |
|
|
|
|
|
|
|
|
create_quick_action_buttons() |
|
|
|
|
|
with gr.Row(): |
|
|
msg = gr.Textbox( |
|
|
show_label=False, |
|
|
placeholder="Sorunuzu yazın...", |
|
|
lines=2, |
|
|
max_lines=5, |
|
|
elem_classes="message-textbox" |
|
|
) |
|
|
submit = gr.Button( |
|
|
"Gönder", |
|
|
variant="primary", |
|
|
elem_classes="send-button" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=1): |
|
|
create_side_panel() |
|
|
|
|
|
|
|
|
clear = gr.Button("🗑️ Sohbeti Temizle", variant="secondary") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="side-panel" style="margin-top: 1rem;"> |
|
|
<h3>📊 Canlı İstatistikler</h3> |
|
|
<p>🟢 Sistem Durumu: Aktif</p> |
|
|
<p>⏱️ Ortalama Yanıt: 2-3 saniye</p> |
|
|
<p>📦 Stok Durumu: Güncel</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div style="text-align: center; margin-top: 2rem; padding: 1rem; color: #718096;"> |
|
|
<p>© 2024 Trek Bisiklet Türkiye | Alatin Bisiklet Tic. Ltd. Şti.</p> |
|
|
<p> |
|
|
<a href="https://www.trekbisiklet.com.tr" target="_blank" style="color: #e60012;">Web Sitemiz</a> | |
|
|
<a href="https://www.instagram.com/trekturkiye" target="_blank" style="color: #e60012;">Instagram</a> | |
|
|
<a href="https://www.facebook.com/trekturkiye" target="_blank" style="color: #e60012;">Facebook</a> |
|
|
</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
def user_submit(message, history, session_id): |
|
|
if not message: |
|
|
return "", history |
|
|
|
|
|
|
|
|
if not history: |
|
|
history = [[None, create_welcome_message()]] |
|
|
|
|
|
|
|
|
history = history + [[message, None]] |
|
|
|
|
|
|
|
|
response = process_chat(message, [{"role": "user", "content": m[0]} for m in history[1:-1] if m[0]] + |
|
|
[{"role": "assistant", "content": m[1]} for m in history[1:-1] if m[1]], |
|
|
session_id) |
|
|
|
|
|
|
|
|
history[-1][1] = response |
|
|
|
|
|
return "", history |
|
|
|
|
|
|
|
|
submit.click( |
|
|
user_submit, |
|
|
inputs=[msg, chatbot, session_id], |
|
|
outputs=[msg, chatbot], |
|
|
queue=True |
|
|
) |
|
|
|
|
|
msg.submit( |
|
|
user_submit, |
|
|
inputs=[msg, chatbot, session_id], |
|
|
outputs=[msg, chatbot], |
|
|
queue=True |
|
|
) |
|
|
|
|
|
|
|
|
clear.click( |
|
|
lambda: [], |
|
|
outputs=[chatbot] |
|
|
) |
|
|
|
|
|
|
|
|
demo.load( |
|
|
None, |
|
|
None, |
|
|
None, |
|
|
js=""" |
|
|
() => { |
|
|
// Quick action buttons için event listener ekle |
|
|
document.querySelectorAll('.quick-action-btn').forEach(btn => { |
|
|
btn.addEventListener('click', function() { |
|
|
this.style.background = '#e60012'; |
|
|
this.style.color = 'white'; |
|
|
setTimeout(() => { |
|
|
this.style.background = 'white'; |
|
|
this.style.color = 'inherit'; |
|
|
}, 300); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
""" |
|
|
) |
|
|
except Exception as e: |
|
|
print(f"Demo oluşturma hatası: {e}") |
|
|
raise |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.queue(max_size=50) |
|
|
demo.launch( |
|
|
share=True, |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
show_error=True, |
|
|
quiet=False, |
|
|
show_api=False |
|
|
) |