import gradio as gr from sentence_transformers import SentenceTransformer import json import numpy as np import os import httpx import hashlib # Load environment variables from .env file (optional, for local development) try: from dotenv import load_dotenv load_dotenv() print("✅ Loaded .env file") except ImportError: print("ℹ️ python-dotenv not installed, using system environment variables") # Google GenAI SDK (new library) - optional, graceful fallback if not available try: from google import genai from google.genai import types GENAI_AVAILABLE = True print("✅ google-genai loaded successfully") except ImportError as e: GENAI_AVAILABLE = False print(f"⚠️ google-genai not available: {e}") genai = None types = None # ==================== CONFIGURATION ==================== # Model - akan auto-download dari HF Hub saat pertama kali HF_MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" # Path lokal untuk development (opsional, diabaikan jika tidak ada) LOCAL_MODEL_PATH = r"E:\huggingface_models\hub\models--sentence-transformers--paraphrase-multilingual-MiniLM-L12-v2\snapshots" # Supabase configuration (dari environment variables untuk keamanan) # Di HF Space: Settings > Repository secrets # Di lokal: set environment variable atau gunakan default untuk testing SUPABASE_URL = os.environ.get("SUPABASE_URL", "") SUPABASE_KEY = os.environ.get("SUPABASE_KEY", "") # Gemini API configuration with key rotation GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-pro") # atau gemini-2.5-flash, gemini-2.5-flash-lite # Load multiple API keys for rotation GEMINI_API_KEYS = [] for i in range(1, 10): # Support up to 9 keys key = os.environ.get(f"GEMINI_API_KEY_{i}", "") if key: GEMINI_API_KEYS.append(key) # Fallback to single key if no numbered keys found if not GEMINI_API_KEYS: single_key = os.environ.get("GEMINI_API_KEY", "") if single_key: GEMINI_API_KEYS.append(single_key) # Track current key index for rotation current_key_index = 0 def get_gemini_client(): """Get Gemini client with current API key""" global current_key_index if not GENAI_AVAILABLE or genai is None: return None if not GEMINI_API_KEYS: return None return genai.Client(api_key=GEMINI_API_KEYS[current_key_index]) def rotate_api_key(): """Rotate to next API key""" global current_key_index if len(GEMINI_API_KEYS) > 1: current_key_index = (current_key_index + 1) % len(GEMINI_API_KEYS) print(f"🔄 Rotated to API key #{current_key_index + 1}") return current_key_index def call_gemini_with_retry(prompt: str, max_retries: int = None): """Call Gemini API with automatic key rotation on rate limit""" global current_key_index if not GEMINI_API_KEYS: return None, "No API keys configured" if max_retries is None: max_retries = len(GEMINI_API_KEYS) last_error = None for attempt in range(max_retries): try: client = get_gemini_client() response = client.models.generate_content( model=GEMINI_MODEL, contents=prompt ) return response, None except Exception as e: error_str = str(e).lower() last_error = str(e) # Check if rate limit error if "429" in error_str or "rate" in error_str or "quota" in error_str or "resource" in error_str: print(f"⚠️ Rate limit hit on key #{current_key_index + 1}: {e}") rotate_api_key() continue else: # Non-rate-limit error, don't retry return None, str(e) return None, f"All API keys exhausted. Last error: {last_error}" # Initialize and print status if GEMINI_API_KEYS: print(f"✅ Gemini configured with {len(GEMINI_API_KEYS)} API key(s)") print(f" Model: {GEMINI_MODEL}") else: print("⚠️ No Gemini API keys found") def get_model_path(): """Deteksi environment dan return path model yang sesuai""" # Cek apakah folder lokal ada if os.path.exists(LOCAL_MODEL_PATH): # Cari snapshot terbaru snapshots = os.listdir(LOCAL_MODEL_PATH) if snapshots: return os.path.join(LOCAL_MODEL_PATH, snapshots[0]) # Fallback ke HF Hub (untuk deployment di Space) return HF_MODEL_NAME # Load model saat startup print("Loading model...") model = None try: model_path = get_model_path() print(f"Using model from: {model_path}") model = SentenceTransformer(model_path) print("✅ Model loaded successfully!") except Exception as e: print(f"❌ Failed to load model: {e}") model = None def get_embedding(text: str): """Generate embedding untuk single text""" if model is None: return {"error": "Model not loaded"} if not text or not text.strip(): return {"error": "Text tidak boleh kosong"} try: embedding = model.encode(text.strip()) return {"embedding": embedding.tolist()} except Exception as e: return {"error": str(e)} def get_embeddings_batch(texts_json: str): """Generate embeddings untuk multiple texts (JSON array)""" try: texts = json.loads(texts_json) if not isinstance(texts, list): return {"error": "Input harus JSON array"} if len(texts) == 0: return {"error": "Array tidak boleh kosong"} # Filter empty strings texts = [t.strip() for t in texts if t and t.strip()] if len(texts) == 0: return {"error": "Semua text kosong"} embeddings = model.encode(texts) return {"embeddings": embeddings.tolist()} except json.JSONDecodeError: return {"error": "Invalid JSON format. Gunakan format: [\"teks 1\", \"teks 2\"]"} except Exception as e: return {"error": str(e)} def calculate_similarity(text1: str, text2: str): """Hitung cosine similarity antara dua teks""" if not text1 or not text1.strip(): return {"error": "Text 1 tidak boleh kosong"} if not text2 or not text2.strip(): return {"error": "Text 2 tidak boleh kosong"} try: embeddings = model.encode([text1.strip(), text2.strip()]) # Cosine similarity similarity = np.dot(embeddings[0], embeddings[1]) / ( np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1]) ) return { "similarity": float(similarity), "percentage": f"{similarity * 100:.2f}%" } except Exception as e: return {"error": str(e)} # ==================== SUPABASE PROXY FUNCTIONS ==================== def get_supabase_headers(): """Get headers untuk Supabase API calls""" return { "apikey": SUPABASE_KEY, "Authorization": f"Bearer {SUPABASE_KEY}", "Content-Type": "application/json", "Prefer": "return=representation" } def db_get_all_embeddings(): """Ambil semua embeddings dari Supabase""" if not SUPABASE_URL or not SUPABASE_KEY: return {"error": "Supabase not configured"} try: url = f"{SUPABASE_URL}/rest/v1/proposal_embeddings?select=nim,content_hash,embedding_combined,embedding_judul,embedding_deskripsi,embedding_problem,embedding_metode,nama,judul" with httpx.Client(timeout=30.0) as client: response = client.get(url, headers=get_supabase_headers()) if response.status_code == 200: return {"data": response.json(), "count": len(response.json())} else: return {"error": f"Supabase error: {response.status_code}", "detail": response.text} except Exception as e: return {"error": str(e)} def db_get_embedding(nim: str, content_hash: str): """Ambil embedding untuk NIM dan content_hash tertentu""" if not SUPABASE_URL or not SUPABASE_KEY: return {"error": "Supabase not configured"} try: url = f"{SUPABASE_URL}/rest/v1/proposal_embeddings?nim=eq.{nim}&content_hash=eq.{content_hash}&select=*" with httpx.Client(timeout=30.0) as client: response = client.get(url, headers=get_supabase_headers()) if response.status_code == 200: data = response.json() return {"data": data[0] if data else None, "found": len(data) > 0} else: return {"error": f"Supabase error: {response.status_code}"} except Exception as e: return {"error": str(e)} def db_save_embedding(data_json: str): """Simpan embedding ke Supabase (upsert)""" if not SUPABASE_URL or not SUPABASE_KEY: return {"error": "Supabase not configured"} try: data = json.loads(data_json) # Validate required fields if not data.get("nim") or not data.get("content_hash"): return {"error": "nim and content_hash are required"} if not data.get("embedding_combined"): return {"error": "embedding_combined is required"} url = f"{SUPABASE_URL}/rest/v1/proposal_embeddings" headers = get_supabase_headers() headers["Prefer"] = "resolution=merge-duplicates,return=representation" payload = { "nim": data["nim"], "content_hash": data["content_hash"], "embedding_combined": data["embedding_combined"], "embedding_judul": data.get("embedding_judul"), "embedding_deskripsi": data.get("embedding_deskripsi"), "embedding_problem": data.get("embedding_problem"), "embedding_metode": data.get("embedding_metode"), "nama": data.get("nama"), "judul": data.get("judul") } with httpx.Client(timeout=30.0) as client: response = client.post(url, headers=headers, json=payload) if response.status_code in [200, 201]: return {"success": True, "data": response.json()} else: return {"error": f"Supabase error: {response.status_code}", "detail": response.text} except json.JSONDecodeError: return {"error": "Invalid JSON format"} except Exception as e: return {"error": str(e)} def db_check_connection(): """Test koneksi ke Supabase""" if not SUPABASE_URL or not SUPABASE_KEY: return {"connected": False, "error": "Supabase URL or KEY not configured"} try: url = f"{SUPABASE_URL}/rest/v1/proposal_embeddings?select=id&limit=1" with httpx.Client(timeout=10.0) as client: response = client.get(url, headers=get_supabase_headers()) return { "connected": response.status_code == 200, "status_code": response.status_code, "supabase_url": SUPABASE_URL[:30] + "..." if len(SUPABASE_URL) > 30 else SUPABASE_URL } except Exception as e: return {"connected": False, "error": str(e)} # ==================== LLM CACHE FUNCTIONS (SUPABASE) ==================== def db_get_llm_analysis(pair_hash: str): """Ambil cached LLM analysis dari Supabase by pair_hash""" if not SUPABASE_URL or not SUPABASE_KEY: return None try: url = f"{SUPABASE_URL}/rest/v1/llm_analysis?pair_hash=eq.{pair_hash}&select=*" with httpx.Client(timeout=10.0) as client: response = client.get(url, headers=get_supabase_headers()) if response.status_code == 200: data = response.json() if data and len(data) > 0: result = data[0] # Parse similar_aspects from JSONB if isinstance(result.get('similar_aspects'), str): result['similar_aspects'] = json.loads(result['similar_aspects']) result['from_cache'] = True return result return None except Exception as e: print(f"Error getting cached LLM analysis: {e}") return None def db_save_llm_analysis(pair_hash: str, proposal1_judul: str, proposal2_judul: str, result: dict): """Simpan LLM analysis result ke Supabase""" if not SUPABASE_URL or not SUPABASE_KEY: return False try: url = f"{SUPABASE_URL}/rest/v1/llm_analysis" headers = get_supabase_headers() headers["Prefer"] = "resolution=merge-duplicates" # Upsert payload = { "pair_hash": pair_hash, "proposal1_judul": proposal1_judul[:500] if proposal1_judul else "", "proposal2_judul": proposal2_judul[:500] if proposal2_judul else "", "similarity_score": result.get("similarity_score"), "verdict": result.get("verdict"), "reasoning": result.get("reasoning"), "saran": result.get("saran"), "similar_aspects": json.dumps(result.get("similar_aspects", {})), "differentiator": result.get("differentiator"), "model_used": result.get("model_used", GEMINI_MODEL) } with httpx.Client(timeout=10.0) as client: response = client.post(url, headers=headers, json=payload) if response.status_code in [200, 201]: print(f"✅ LLM result cached: {pair_hash[:8]}...") return True else: print(f"⚠️ Failed to cache LLM result: {response.status_code}") return False except Exception as e: print(f"Error saving LLM analysis: {e}") return False # ==================== LLM FUNCTIONS (GEMINI) ==================== def generate_pair_hash(proposal1: dict, proposal2: dict) -> str: """Generate unique hash untuk pasangan proposal""" def proposal_hash(p): content = f"{p.get('nim', '')}|{p.get('judul', '')}|{p.get('deskripsi', '')}|{p.get('problem', '')}|{p.get('metode', '')}" return hashlib.md5(content.encode()).hexdigest()[:16] h1 = proposal_hash(proposal1) h2 = proposal_hash(proposal2) # Sort untuk konsistensi (A,B = B,A) sorted_hashes = sorted([h1, h2]) return hashlib.md5(f"{sorted_hashes[0]}|{sorted_hashes[1]}".encode()).hexdigest()[:32] def llm_analyze_pair(proposal1_json: str, proposal2_json: str, use_cache: bool = True): """Analisis kemiripan dua proposal menggunakan Gemini LLM""" if not GEMINI_API_KEYS: return {"error": "Gemini API key not configured. Set GEMINI_API_KEY_1, GEMINI_API_KEY_2, etc in .env file"} try: proposal1 = json.loads(proposal1_json) proposal2 = json.loads(proposal2_json) except json.JSONDecodeError: return {"error": "Invalid JSON format for proposals"} # Generate pair hash untuk caching pair_hash = generate_pair_hash(proposal1, proposal2) # Check cache first if use_cache: cached_result = db_get_llm_analysis(pair_hash) if cached_result: print(f"📦 Using cached LLM result: {pair_hash[:8]}...") return cached_result # Build prompt prompt = f"""Anda adalah penilai kemiripan proposal skripsi yang ahli dan berpengalaman. Analisis dua proposal berikut dengan KRITERIA AKADEMIK yang benar. ATURAN PENILAIAN PENTING: 1. Proposal skripsi dianggap BERMASALAH hanya jika KETIGA aspek ini SAMA: Topik/Domain + Dataset/Objek Penelitian + Metode/Algoritma 2. Jika METODE BERBEDA (walaupun topik & dataset sama) → AMAN, karena memberikan kontribusi ilmiah berbeda 3. Jika DATASET/OBJEK BERBEDA (walaupun topik & metode sama) → AMAN, karena studi kasus berbeda 4. Jika TOPIK/DOMAIN BERBEDA → AMAN 5. Penelitian replikasi dengan variasi adalah HAL YANG WAJAR dalam dunia akademik PROPOSAL 1: - NIM: {proposal1.get('nim', 'N/A')} - Nama: {proposal1.get('nama', 'N/A')} - Judul: {proposal1.get('judul', 'N/A')} - Deskripsi: {proposal1.get('deskripsi', 'N/A')[:500] if proposal1.get('deskripsi') else 'N/A'} - Problem Statement: {proposal1.get('problem', 'N/A')[:500] if proposal1.get('problem') else 'N/A'} - Metode: {proposal1.get('metode', 'N/A')} PROPOSAL 2: - NIM: {proposal2.get('nim', 'N/A')} - Nama: {proposal2.get('nama', 'N/A')} - Judul: {proposal2.get('judul', 'N/A')} - Deskripsi: {proposal2.get('deskripsi', 'N/A')[:500] if proposal2.get('deskripsi') else 'N/A'} - Problem Statement: {proposal2.get('problem', 'N/A')[:500] if proposal2.get('problem') else 'N/A'} - Metode: {proposal2.get('metode', 'N/A')} ANALISIS dengan cermat, lalu berikan output JSON (HANYA JSON, tanpa markdown): {{ "similarity_score": <0-100, tinggi HANYA jika topik+dataset+metode SEMUA sama>, "verdict": "=80, PERLU_REVIEW jika 50-79, AMAN jika <50>", "similar_aspects": {{ "topik": , "dataset": , "metode": , "pendekatan": }}, "differentiator": "", "reasoning": "", "saran": "" }}""" # Call Gemini API with retry/rotation response, error = call_gemini_with_retry(prompt) if error: return {"error": f"Gemini API error: {error}"} try: # Parse response response_text = response.text.strip() # Clean response (remove markdown code blocks if present) if response_text.startswith("```"): lines = response_text.split("\n") response_text = "\n".join(lines[1:-1]) # Remove first and last lines result = json.loads(response_text) result["pair_hash"] = pair_hash result["model_used"] = GEMINI_MODEL result["api_key_used"] = current_key_index + 1 result["from_cache"] = False # Save to cache db_save_llm_analysis( pair_hash=pair_hash, proposal1_judul=proposal1.get('judul', ''), proposal2_judul=proposal2.get('judul', ''), result=result ) return result except json.JSONDecodeError as e: return { "error": "Failed to parse LLM response as JSON", "raw_response": response_text if 'response_text' in dir() else "No response", "parse_error": str(e) } def llm_check_status(): """Check Gemini API status""" if not GENAI_AVAILABLE: return { "configured": False, "error": "google-genai package not available" } if not GEMINI_API_KEYS: return { "configured": False, "error": "No GEMINI_API_KEY found in environment" } response, error = call_gemini_with_retry("Respond with only: OK") if error: return { "configured": True, "total_keys": len(GEMINI_API_KEYS), "model": GEMINI_MODEL, "status": "error", "error": error } return { "configured": True, "total_keys": len(GEMINI_API_KEYS), "current_key": current_key_index + 1, "model": GEMINI_MODEL, "status": "connected", "test_response": response.text.strip()[:50] } def llm_analyze_simple(judul1: str, judul2: str, metode1: str, metode2: str): """Simplified analysis - hanya judul dan metode (untuk testing cepat)""" if not GEMINI_API_KEYS: return {"error": "Gemini API key not configured"} prompt = f"""Anda adalah penilai kemiripan proposal skripsi yang ahli. Bandingkan dua proposal berikut dengan KRITERIA AKADEMIK yang benar. ATURAN PENILAIAN PENTING: 1. Proposal skripsi dianggap BERMASALAH hanya jika KETIGA aspek ini SAMA: Topik/Domain + Dataset + Metode 2. Jika METODE BERBEDA (walaupun topik sama) → AMAN, karena kontribusi berbeda 3. Jika DATASET BERBEDA (walaupun topik & metode sama) → AMAN, karena studi kasus berbeda 4. Jika TOPIK/DOMAIN BERBEDA → AMAN Proposal 1: - Judul: {judul1} - Metode: {metode1} Proposal 2: - Judul: {judul2} - Metode: {metode2} ANALISIS dengan cermat, lalu berikan output JSON (HANYA JSON, tanpa markdown): {{ "similarity_score": <0-100, tinggi HANYA jika topik+dataset+metode SEMUA sama>, "verdict": "=80, PERLU_REVIEW jika 50-79, AMAN jika <50>", "topik_sama": , "metode_sama": , "differentiator": "", "reasoning": "", "saran": "" }}""" response, error = call_gemini_with_retry(prompt) if error: return {"error": error} try: response_text = response.text.strip() if response_text.startswith("```"): lines = response_text.split("\n") response_text = "\n".join(lines[1:-1]) result = json.loads(response_text) result["model_used"] = GEMINI_MODEL result["api_key_used"] = current_key_index + 1 return result except json.JSONDecodeError as e: return {"error": f"Failed to parse response: {e}", "raw": response_text} # Gradio Interface with gr.Blocks(title="Semantic Embedding API") as demo: gr.Markdown("# 🔤 Semantic Embedding API") gr.Markdown("API untuk menghasilkan text embedding menggunakan `paraphrase-multilingual-MiniLM-L12-v2`") gr.Markdown("**Model**: Multilingual, mendukung 50+ bahasa termasuk Bahasa Indonesia") with gr.Tab("🔢 Single Embedding"): gr.Markdown("Generate embedding vector untuk satu teks") text_input = gr.Textbox( label="Input Text", placeholder="Masukkan teks untuk di-embed...", lines=2 ) single_output = gr.JSON(label="Embedding Result") single_btn = gr.Button("Generate Embedding", variant="primary") single_btn.click(fn=get_embedding, inputs=text_input, outputs=single_output, api_name="get_embedding") with gr.Tab("📦 Batch Embedding"): gr.Markdown("Generate embeddings untuk multiple teks sekaligus") batch_input = gr.Textbox( label="JSON Array of Texts", placeholder='["teks pertama", "teks kedua", "teks ketiga"]', lines=4 ) batch_output = gr.JSON(label="Embeddings Result") batch_btn = gr.Button("Generate Embeddings", variant="primary") batch_btn.click(fn=get_embeddings_batch, inputs=batch_input, outputs=batch_output, api_name="get_embeddings_batch") with gr.Tab("📊 Similarity Check"): gr.Markdown("Hitung kemiripan semantik antara dua teks") with gr.Row(): sim_text1 = gr.Textbox(label="Text 1", placeholder="Teks pertama...", lines=2) sim_text2 = gr.Textbox(label="Text 2", placeholder="Teks kedua...", lines=2) sim_output = gr.JSON(label="Similarity Result") sim_btn = gr.Button("Calculate Similarity", variant="primary") sim_btn.click(fn=calculate_similarity, inputs=[sim_text1, sim_text2], outputs=sim_output, api_name="calculate_similarity") with gr.Tab("💾 Database (Supabase)"): gr.Markdown("### Supabase Cache Operations") gr.Markdown("Proxy untuk akses Supabase (API key aman di server)") gr.Markdown("*Note: Operasi write (save) hanya tersedia melalui API untuk keamanan.*") with gr.Row(): db_check_btn = gr.Button("🔌 Check Connection", variant="secondary") db_check_output = gr.JSON(label="Connection Status") db_check_btn.click(fn=db_check_connection, outputs=db_check_output, api_name="db_check_connection") gr.Markdown("---") gr.Markdown("#### Get All Cached Embeddings") db_all_btn = gr.Button("📥 Get All Embeddings", variant="primary") db_all_output = gr.JSON(label="All Embeddings") db_all_btn.click(fn=db_get_all_embeddings, outputs=db_all_output, api_name="db_get_all_embeddings") gr.Markdown("---") gr.Markdown("#### Get Single Embedding by NIM") with gr.Row(): db_nim_input = gr.Textbox(label="NIM", placeholder="10121xxx") db_hash_input = gr.Textbox(label="Content Hash", placeholder="abc123...") db_get_btn = gr.Button("🔍 Get Embedding", variant="primary") db_get_output = gr.JSON(label="Embedding Result") db_get_btn.click(fn=db_get_embedding, inputs=[db_nim_input, db_hash_input], outputs=db_get_output, api_name="db_get_embedding") with gr.Tab("🤖 LLM Analysis (Gemini)"): gr.Markdown("### Analisis Kemiripan dengan LLM") gr.Markdown("Menggunakan Google Gemini untuk analisis mendalam dengan penjelasan") with gr.Row(): llm_check_btn = gr.Button("🔌 Check Gemini Status", variant="secondary") llm_check_output = gr.JSON(label="Gemini Status") llm_check_btn.click(fn=llm_check_status, outputs=llm_check_output, api_name="llm_check_status") gr.Markdown("---") gr.Markdown("#### Quick Analysis (Judul + Metode saja)") with gr.Row(): with gr.Column(): llm_judul1 = gr.Textbox(label="Judul Proposal 1", placeholder="Analisis Sentimen dengan SVM...", lines=2) llm_metode1 = gr.Textbox(label="Metode 1", placeholder="Support Vector Machine") with gr.Column(): llm_judul2 = gr.Textbox(label="Judul Proposal 2", placeholder="Klasifikasi Sentimen dengan SVM...", lines=2) llm_metode2 = gr.Textbox(label="Metode 2", placeholder="Support Vector Machine") llm_simple_btn = gr.Button("🚀 Analyze (Quick)", variant="primary") llm_simple_output = gr.JSON(label="Quick Analysis Result") llm_simple_btn.click( fn=llm_analyze_simple, inputs=[llm_judul1, llm_judul2, llm_metode1, llm_metode2], outputs=llm_simple_output, api_name="llm_analyze_simple" ) gr.Markdown("---") gr.Markdown("#### Full Analysis (Complete Proposal Data)") gr.Markdown("*Hasil di-cache ke Supabase. Request yang sama akan menggunakan cache.*") with gr.Row(): llm_proposal1 = gr.Textbox( label="Proposal 1 (JSON)", placeholder='{"nim": "123", "nama": "Ahmad", "judul": "...", "deskripsi": "...", "problem": "...", "metode": "..."}', lines=5 ) llm_proposal2 = gr.Textbox( label="Proposal 2 (JSON)", placeholder='{"nim": "456", "nama": "Budi", "judul": "...", "deskripsi": "...", "problem": "...", "metode": "..."}', lines=5 ) with gr.Row(): llm_use_cache = gr.Checkbox(label="Gunakan Cache", value=True, info="Uncheck untuk force refresh dari Gemini") llm_full_btn = gr.Button("🔍 Analyze (Full)", variant="primary") llm_full_output = gr.JSON(label="Full Analysis Result") llm_full_btn.click( fn=llm_analyze_pair, inputs=[llm_proposal1, llm_proposal2, llm_use_cache], outputs=llm_full_output, api_name="llm_analyze_pair" ) gr.Markdown(""" **Output mencakup:** - `similarity_score`: Skor 0-100 (tinggi hanya jika topik+dataset+metode sama) - `verdict`: BERMASALAH / PERLU_REVIEW / AMAN - `reasoning`: Analisis mendalam dari AI - `similar_aspects`: Aspek yang mirip (topik/dataset/metode/pendekatan) - `differentiator`: Pembeda utama - `saran`: Nasihat untuk mahasiswa - `from_cache`: true jika hasil dari cache """) with gr.Accordion("📡 API Usage (untuk Developer)", open=False): gr.Markdown(""" ### Endpoints #### Embedding - `get_embedding` - Single text embedding - `get_embeddings_batch` - Batch text embeddings - `calculate_similarity` - Compare two texts #### Database (Supabase Proxy) - `db_check_connection` - Test Supabase connection - `db_get_all_embeddings` - Get all cached embeddings - `db_get_embedding` - Get embedding by NIM + hash - `db_save_embedding` - Save embedding to cache ### Example API Call ```javascript // Get all cached embeddings const response = await fetch("YOUR_SPACE_URL/gradio_api/call/db_get_all_embeddings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: [] }) }); const result = await response.json(); const eventId = result.event_id; // Get result const dataResponse = await fetch(`YOUR_SPACE_URL/gradio_api/call/db_get_all_embeddings/${eventId}`); ``` """) gr.Markdown("---") gr.Markdown("*Dibuat untuk Monitoring Proposal Skripsi KK E - UNIKOM*") # Hidden API-only endpoints (tidak tampil di UI, tapi bisa diakses via API) with gr.Row(visible=False): api_save_input = gr.Textbox() api_save_output = gr.JSON() api_save_btn = gr.Button() api_save_btn.click(fn=db_save_embedding, inputs=api_save_input, outputs=api_save_output, api_name="db_save_embedding") # Launch dengan API enabled demo.launch()