Download app.py from Nerdur/Freechat: direct link, hf CLI and curl.
- Browser
- Download file 23.5 kB
-
https://huggingface.co/spaces/Nerdur/Freechat/resolve/main/app.py
- Command line
-
hf download hf://spaces/Nerdur/Freechat/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Nerdur/Freechat/resolve/main/app.py
23.5 kB
| import os | |
| import json | |
| import datetime | |
| import re | |
| import math | |
| import random | |
| # === PATCH: HfFolder za Gradio (HF Spaces fix) === | |
| import huggingface_hub | |
| if not hasattr(huggingface_hub, 'HfFolder'): | |
| class HfFolder: | |
| def get_token(): return os.getenv('HF_TOKEN') | |
| def save_token(token): pass | |
| def delete_token(): pass | |
| huggingface_hub.HfFolder = HfFolder | |
| # ================================================ | |
| from dotenv import load_dotenv | |
| import requests | |
| import openai | |
| import gradio as gr | |
| load_dotenv() | |
| # === API KLJUCEVI === | |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") | |
| GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") | |
| OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") | |
| SAMBANOVA_API_KEY = os.getenv("SAMBANOVA_API_KEY", "") | |
| MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY", "") | |
| COHERE_API_KEY = os.getenv("COHERE_API_KEY", "") | |
| NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") | |
| GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "") | |
| # ============================================================ | |
| # PROVIDERS | |
| # ============================================================ | |
| PROVIDERS = { | |
| "Groq": { | |
| "models": { | |
| "llama-3.3-70b-versatile": {"tools": True, "ctx": 128000}, | |
| "llama-3.1-8b-instant": {"tools": True, "ctx": 128000}, | |
| "mixtral-8x7b-32768": {"tools": True, "ctx": 32768}, | |
| }, "key": GROQ_API_KEY, "env": "GROQ_API_KEY", "type": "groq", | |
| }, | |
| "Google Gemini": { | |
| "models": { | |
| "gemini-1.5-flash": {"tools": True, "ctx": 1048576}, | |
| "gemini-1.5-flash-8b": {"tools": True, "ctx": 1048576}, | |
| "gemini-2.0-flash-exp": {"tools": True, "ctx": 1048576}, | |
| }, "key": GOOGLE_API_KEY, "env": "GOOGLE_API_KEY", "type": "gemini", | |
| }, | |
| "OpenRouter": { | |
| "models": { | |
| "meta-llama/llama-3.2-3b-instruct:free": {"tools": True, "ctx": 32768}, | |
| "google/gemma-2-9b-it:free": {"tools": False, "ctx": 8192}, | |
| }, "key": OPENROUTER_API_KEY, "env": "OPENROUTER_API_KEY", "type": "oai", | |
| "url": "https://openrouter.ai/api/v1", | |
| }, | |
| "SambaNova": { | |
| "models": { | |
| "Meta-Llama-3.3-70B-Instruct": {"tools": True, "ctx": 128000}, | |
| "DeepSeek-V3.1": {"tools": True, "ctx": 131072}, | |
| "Qwen2.5-72B-Instruct": {"tools": True, "ctx": 131072}, | |
| }, "key": SAMBANOVA_API_KEY, "env": "SAMBANOVA_API_KEY", "type": "oai", | |
| "url": "https://api.sambanova.ai/v1", | |
| }, | |
| "Mistral AI": { | |
| "models": { | |
| "mistral-small-latest": {"tools": True, "ctx": 32768}, | |
| "mistral-nemo-latest": {"tools": True, "ctx": 131072}, | |
| "codestral-latest": {"tools": True, "ctx": 256000}, | |
| }, "key": MISTRAL_API_KEY, "env": "MISTRAL_API_KEY", "type": "oai", | |
| "url": "https://api.mistral.ai/v1", | |
| }, | |
| "Cohere": { | |
| "models": { | |
| "command-r-plus": {"tools": True, "ctx": 128000}, | |
| "command-r": {"tools": True, "ctx": 128000}, | |
| "command-r7b": {"tools": True, "ctx": 128000}, | |
| }, "key": COHERE_API_KEY, "env": "COHERE_API_KEY", "type": "cohere", | |
| }, | |
| "NVIDIA NIM": { | |
| "models": { | |
| "meta/llama-3.3-70b-instruct": {"tools": True, "ctx": 131072}, | |
| "mistralai/mistral-7b-instruct-v0.3": {"tools": True, "ctx": 32768}, | |
| "nvidia/llama-3.1-nemotron-70b-instruct": {"tools": True, "ctx": 131072}, | |
| }, "key": NVIDIA_API_KEY, "env": "NVIDIA_API_KEY", "type": "oai", | |
| "url": "https://integrate.api.nvidia.com/v1", | |
| }, | |
| "GitHub Models": { | |
| "models": { | |
| "gpt-4o-mini": {"tools": True, "ctx": 128000}, | |
| "Phi-4": {"tools": True, "ctx": 16384}, | |
| "Mistral-large-2411": {"tools": True, "ctx": 131072}, | |
| }, "key": GITHUB_TOKEN, "env": "GITHUB_TOKEN", "type": "oai", | |
| "url": "https://models.inference.ai.azure.com", | |
| }, | |
| } | |
| # ============================================================ | |
| # TOOL DEFINICIJE | |
| # ============================================================ | |
| TOOLS = [ | |
| {"type": "function", "function": { | |
| "name": "get_current_time", | |
| "description": "Trenutno vrijeme u bilo kojoj vremenskoj zoni", | |
| "parameters": {"type": "object", "properties": { | |
| "timezone": {"type": "string", "description": "Npr. Europe/Sarajevo"} | |
| }}, | |
| }}, | |
| {"type": "function", "function": { | |
| "name": "calculate", | |
| "description": "Matematički izraz", | |
| "parameters": {"type": "object", "properties": { | |
| "expression": {"type": "string", "description": "Izraz npr. 2+2"} | |
| }, "required": ["expression"]}, | |
| }}, | |
| {"type": "function", "function": { | |
| "name": "search_web", | |
| "description": "Pretraži internet", | |
| "parameters": {"type": "object", "properties": { | |
| "query": {"type": "string", "description": "Upit"} | |
| }, "required": ["query"]}, | |
| }}, | |
| {"type": "function", "function": { | |
| "name": "get_random_number", | |
| "description": "Nasumični broj", | |
| "parameters": {"type": "object", "properties": { | |
| "min": {"type": "number", "description": "Min"}, | |
| "max": {"type": "number", "description": "Max"}, | |
| }}, | |
| }}, | |
| {"type": "function", "function": { | |
| "name": "analyze_document", | |
| "description": "Analiziraj učitani dokument", | |
| "parameters": {"type": "object", "properties": { | |
| "question": {"type": "string", "description": "Pitanje"} | |
| }, "required": ["question"]}, | |
| }}, | |
| ] | |
| # ============================================================ | |
| # TOOL EXECUTION | |
| # ============================================================ | |
| def run_tool(name, args, file_text=""): | |
| try: | |
| if name == "get_current_time": | |
| tz = args.get("timezone", "Europe/Sarajevo") | |
| try: | |
| import pytz | |
| now = datetime.datetime.now(pytz.timezone(tz)) | |
| return f"Vrijeme u {tz}: {now.strftime('%H:%M:%S, %d.%m.%Y.')}" | |
| except: | |
| return f"UTC: {datetime.datetime.utcnow().strftime('%H:%M:%S, %d.%m.%Y.')} UTC" | |
| elif name == "calculate": | |
| safe = {"__builtins__": {k: v for k, v in __builtins__.items() | |
| if k in ("abs","round","int","float","str","bool","min","max","sum","len","True","False","None")}} | |
| safe.update({"sqrt":math.sqrt,"sin":math.sin,"cos":math.cos,"tan":math.tan, | |
| "log":math.log,"pi":math.pi,"e":math.e,"floor":math.floor,"ceil":math.ceil}) | |
| return f"Rezultat: {eval(args.get('expression',''), safe, safe)}" | |
| elif name == "search_web": | |
| from duckduckgo_search import DDGS | |
| q = args.get("query","") | |
| with DDGS() as ddgs: | |
| res = list(ddgs.text(q, max_results=4)) | |
| if res: | |
| out = f"Pretraga za '{q}':\n\n" | |
| for i, r in enumerate(res, 1): | |
| out += f"{i}. **{r['title']}**\n{r['body'][:200]}...\n\n" | |
| return out | |
| return f"Nema rezultata." | |
| elif name == "get_random_number": | |
| mn, mx = int(args.get("min",1)), int(args.get("max",100)) | |
| if mn > mx: mn, mx = mx, mn | |
| return f"🎲 {random.randint(mn, mx)}" | |
| elif name == "analyze_document": | |
| if not file_text: return "Nema dokumenta." | |
| q = args.get("question","").lower() | |
| sents = [s.strip() for s in re.split(r'[.!?\n]+', file_text) if any(w in s.lower() for w in q.split())] | |
| return "Iz dokumenta:\n\n" + "\n".join(sents[:5]) if sents else f"Dokument: {len(file_text.split())} riječi." | |
| except Exception as e: | |
| return f"Greška: {str(e)}" | |
| # ============================================================ | |
| # LLM FUNKCIJE | |
| # ============================================================ | |
| def call_groq(model, messages, tools_on, file_text): | |
| from groq import Groq | |
| client = Groq(api_key=GROQ_API_KEY) | |
| msgs = [{"role": m["role"], "content": m["content"]} for m in messages] | |
| kw = {"model": model, "messages": msgs, "temperature": 0.7, "max_tokens": 4096} | |
| if tools_on: kw.update({"tools": TOOLS, "tool_choice": "auto"}) | |
| try: | |
| r = client.chat.completions.create(**kw) | |
| c = r.choices[0] | |
| if c.finish_reason == "tool_calls" and c.message.tool_calls: | |
| tc = c.message.tool_calls[0] | |
| name = tc.function.name | |
| args = json.loads(tc.function.arguments) if tc.function.arguments else {} | |
| result = run_tool(name, args, file_text) | |
| msgs.append({"role": "assistant", "content": c.message.content or "", | |
| "tool_calls": [{"id": tc.id, "type": "function", | |
| "function": {"name": name, "arguments": tc.function.arguments}}]}) | |
| msgs.append({"role": "tool", "tool_call_id": tc.id, "content": result}) | |
| f = client.chat.completions.create(**{**kw, "messages": msgs}) | |
| return f.choices[0].message.content or "", result, name | |
| return c.message.content or "", None, None | |
| except Exception as e: | |
| return f"**Groq greška:** {str(e)}", None, None | |
| def call_gemini(model, messages, tools_on, file_text): | |
| import google.generativeai as genai | |
| genai.configure(api_key=GOOGLE_API_KEY) | |
| m = f"models/{model}" | |
| hist = [{"role": "user" if m.get("role") in ("user","tool") else "model", | |
| "parts": [m["content"]]} for m in messages[:-1] if m.get("content")] | |
| last = messages[-1]["content"] if messages else "" | |
| try: | |
| if tools_on: | |
| fl = [genai.protos.FunctionDeclaration(name=t["function"]["name"], | |
| description=t["function"].get("description",""), | |
| parameters=t["function"]["parameters"]) for t in TOOLS] | |
| chat = genai.GenerativeModel(m, tools=fl).start_chat(history=hist) | |
| r = chat.send_message(last) | |
| if r.candidates[0].content.parts[0].function_call: | |
| fc = r.candidates[0].content.parts[0].function_call | |
| args = {k: v for k, v in fc.args.items()} | |
| result = run_tool(fc.name, args, file_text) | |
| r2 = chat.send_message(genai.protos.Content(parts=[genai.protos.Part( | |
| function_response=genai.protos.FunctionResponse(name=fc.name, response={"result": result}))])) | |
| return r2.text, result, fc.name | |
| return r.text, None, None | |
| return genai.GenerativeModel(m).start_chat(history=hist).send_message(last).text, None, None | |
| except Exception as e: | |
| return f"**Gemini greška:** {str(e)}", None, None | |
| def call_oai(model, messages, base_url, api_key, tools_on, file_text): | |
| client = openai.OpenAI(base_url=base_url, api_key=api_key) | |
| msgs = [{"role": m["role"], "content": m["content"]} for m in messages] | |
| kw = {"model": model, "messages": msgs, "temperature": 0.7, "max_tokens": 4096} | |
| if tools_on: kw.update({"tools": TOOLS, "tool_choice": "auto"}) | |
| try: | |
| r = client.chat.completions.create(**kw) | |
| c = r.choices[0] | |
| if c.finish_reason == "tool_calls" and c.message.tool_calls: | |
| tc = c.message.tool_calls[0] | |
| name = tc.function.name | |
| args = json.loads(tc.function.arguments) if tc.function.arguments else {} | |
| result = run_tool(name, args, file_text) | |
| msgs.append({"role": "assistant", "content": c.message.content or "", | |
| "tool_calls": [{"id": tc.id, "type": "function", | |
| "function": {"name": name, "arguments": tc.function.arguments}}]}) | |
| msgs.append({"role": "tool", "tool_call_id": tc.id, "content": result}) | |
| f = client.chat.completions.create(**{**kw, "messages": msgs}) | |
| return f.choices[0].message.content or "", result, name | |
| return c.message.content or "", None, None | |
| except Exception as e: | |
| return f"**Greška:** {str(e)}", None, None | |
| def call_cohere(model, messages, tools_on, file_text): | |
| headers = {"Authorization": f"Bearer {COHERE_API_KEY}", "Content-Type": "application/json"} | |
| msgs = [{"role": m["role"], "content": m["content"]} for m in messages] | |
| payload = {"model": model, "messages": msgs, "temperature": 0.7, "max_tokens": 4096} | |
| if tools_on: payload["tools"] = TOOLS | |
| try: | |
| r = requests.post("https://api.cohere.com/v2/chat", headers=headers, json=payload, timeout=30) | |
| r.raise_for_status() | |
| d = r.json() | |
| msg = d.get("message", {}) | |
| if "tool_calls" in msg: | |
| tc = msg["tool_calls"][0] | |
| name = tc.get("function",{}).get("name","") | |
| args = tc.get("function",{}).get("arguments",{}) | |
| result = run_tool(name, args, file_text) | |
| payload["messages"].append({"role": "assistant", "content": msg.get("content","")}) | |
| payload["messages"].append({"role": "tool", "content": result, "tool_call_id": tc.get("id","")}) | |
| r2 = requests.post("https://api.cohere.com/v2/chat", headers=headers, json=payload, timeout=30) | |
| d2 = r2.json() | |
| txt = d2.get("message",{}).get("content","") or d2.get("text","") | |
| return txt, result, name | |
| return msg.get("content","") or d.get("text",""), None, None | |
| except Exception as e: | |
| return f"**Cohere greška:** {str(e)}", None, None | |
| # ============================================================ | |
| # FILE / IMAGE | |
| # ============================================================ | |
| def read_file(file): | |
| if file is None: return "" | |
| name = getattr(file, 'name', '') or getattr(file, 'orig_name', '') | |
| try: | |
| if name.lower().endswith('.pdf'): | |
| import PyPDF2 | |
| return "\n".join(p.extract_text() for p in PyPDF2.PdfReader(file).pages) | |
| elif name.lower().endswith('.docx'): | |
| from docx import Document | |
| return "\n".join(p.text for p in Document(file).paragraphs) | |
| elif name.lower().endswith('.txt'): | |
| return file.read().decode('utf-8', errors='ignore') | |
| return f"[Nepodržan: {name}]" | |
| except Exception as e: | |
| return f"[Greška: {str(e)}]" | |
| def gen_image(prompt): | |
| try: | |
| return f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?width=1024&height=1024&nologo=true" | |
| except: | |
| return None | |
| # ============================================================ | |
| # CHAT FUNKCIJA | |
| # ============================================================ | |
| def chat_fn(msg, history, provider, model, uploaded_file, tools_on, img_mode): | |
| if history is None: history = [] | |
| file_text = read_file(uploaded_file) if uploaded_file else "" | |
| if img_mode and msg.strip(): | |
| url = gen_image(msg.strip()) | |
| history.append({"role": "user", "content": msg}) | |
| history.append({"role": "assistant", "content": f"" if url else "❌ Greška"}) | |
| return history, history | |
| cfg = PROVIDERS.get(provider, {}) | |
| if not cfg.get("key"): | |
| err = f"⚠️ **API ključ nije podešen za {provider}!**\n\nPostavi `{cfg.get('env','?')}` u HF Secrets." | |
| history.append({"role": "user", "content": msg}) | |
| history.append({"role": "assistant", "content": err}) | |
| return history, history | |
| msgs = [] | |
| for h in history: | |
| if isinstance(h, dict) and h.get("content"): | |
| msgs.append({"role": h["role"], "content": h["content"]}) | |
| elif isinstance(h, (list,tuple)) and len(h)==2: | |
| if h[0]: msgs.append({"role": "user", "content": str(h[0])}) | |
| if h[1]: msgs.append({"role": "assistant", "content": str(h[1])}) | |
| full_msg = msg.strip() | |
| if file_text and not file_text.startswith("["): | |
| full_msg += f"\n\n---\nSadržaj fajla:\n```\n{file_text[:3000]}\n```" | |
| msgs.append({"role": "user", "content": full_msg}) | |
| try: | |
| t = cfg.get("type","") | |
| if t == "groq": resp, tres, tname = call_groq(model, msgs, tools_on, file_text) | |
| elif t == "gemini": resp, tres, tname = call_gemini(model, msgs, tools_on, file_text) | |
| elif t == "cohere": resp, tres, tname = call_cohere(model, msgs, tools_on, file_text) | |
| elif t == "oai": resp, tres, tname = call_oai(model, msgs, cfg.get("url",""), cfg["key"], tools_on, file_text) | |
| else: resp, tres, tname = f"Nepoznat tip: {t}", None, None | |
| final = resp | |
| if tname and tres: | |
| emoji = {"get_current_time":"🕐","calculate":"🧮","search_web":"🔍","get_random_number":"🎲","analyze_document":"📄"}.get(tname,"🛠️") | |
| final += f"\n\n---\n{emoji} **Alat:** `{tname}`\n```\n{tres}\n```" | |
| history.append({"role": "user", "content": msg}) | |
| history.append({"role": "assistant", "content": final}) | |
| except Exception as e: | |
| history.append({"role": "user", "content": msg}) | |
| history.append({"role": "assistant", "content": f"❌ **Greška:**\n```\n{str(e)}\n```"}) | |
| return history, history | |
| def share_chat(history): | |
| if not history: return None | |
| ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") | |
| txt = f"# 💬 FreeToChat Clone\n📅 {ts}\n\n---\n\n" | |
| for h in history: | |
| if isinstance(h, dict): | |
| r, c = h.get("role",""), h.get("content","") | |
| if r == "user": txt += f"**👤 User:**\n{c}\n\n" | |
| elif r == "assistant": txt += f"**🤖 AI:**\n{c}\n\n---\n\n" | |
| elif isinstance(h, (list,tuple)) and len(h)==2: | |
| if h[0]: txt += f"**👤 User:**\n{h[0]}\n\n" | |
| if h[1]: txt += f"**🤖 AI:**\n{h[1]}\n\n---\n\n" | |
| p = "/tmp/chat_export.md" | |
| with open(p, "w", encoding="utf-8") as f: f.write(txt) | |
| return p | |
| # ============================================================ | |
| # UI | |
| # ============================================================ | |
| def create_ui(): | |
| plist = list(PROVIDERS.keys()) | |
| avail = [p for p, c in PROVIDERS.items() if c["key"]] | |
| default = avail[0] if avail else plist[0] | |
| css = """ | |
| .app-title { text-align: center; margin-bottom: 12px; } | |
| .app-title h1 { margin: 0; font-size: 2.2em; background: linear-gradient(135deg,#667eea,#764ba2); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } | |
| .app-title p { color: #888; margin: 4px 0; font-size: 0.9em; } | |
| .toolbar { display: flex; justify-content: space-between; font-size: 0.75em; color: #888; padding: 4px 0; } | |
| """ | |
| with gr.Blocks(title="FreeToChat Clone", css=css, | |
| theme=gr.themes.Soft(primary_hue="violet", secondary_hue="blue", neutral_hue="slate")) as demo: | |
| chat_state = gr.State([]) | |
| gr.HTML(""" | |
| <div class="app-title"> | |
| <h1>🚀 FreeToChat Clone</h1> | |
| <p>🔒 Privatno · 🛠️ Tool Calling · 📄 Analiza · 🎨 Slike · 8 Providera</p> | |
| </div> | |
| """) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=1, min_width=240): | |
| gr.Markdown("### ⚙️ Provider") | |
| prov_dd = gr.Dropdown(choices=plist, value=default, label="🤖 Provider") | |
| model_dd = gr.Dropdown( | |
| choices=list(PROVIDERS[default]["models"].keys()), | |
| value=list(PROVIDERS[default]["models"].keys())[0], | |
| label="🧠 Model") | |
| with gr.Row(): | |
| tools_cb = gr.Checkbox(value=True, label="🛠️ Alati") | |
| img_cb = gr.Checkbox(value=False, label="🎨 Slike") | |
| gr.Markdown("---") | |
| gr.Markdown("### 📂 Fajl") | |
| file_up = gr.File(file_count="single", file_types=[".pdf",".docx",".txt"], label="📎 Upload") | |
| gr.Markdown("---") | |
| gr.Markdown("### 💬 Opcije") | |
| gr.Button("🗑️ Novo", variant="secondary", size="sm").click( | |
| fn=lambda: ([],[],None), inputs=[], outputs=[chat_state, gr.skip(), file_up]) | |
| share_btn = gr.Button("📤 Podijeli", variant="secondary", size="sm") | |
| share_file = gr.File(label="Download", visible=True) | |
| share_btn.click(fn=share_chat, inputs=[chat_state], outputs=[share_file]) | |
| gr.Markdown("---") | |
| gr.HTML(""" | |
| <div style="font-size:0.85em"> | |
| <b>🔑 API ključevi (besplatno):</b><br> | |
| · <a href="https://console.groq.com/keys" target="_blank">Groq</a><br> | |
| · <a href="https://aistudio.google.com/app/apikey" target="_blank">Gemini</a><br> | |
| · <a href="https://cloud.sambanova.ai/" target="_blank">SambaNova</a><br> | |
| · <a href="https://console.mistral.ai/" target="_blank">Mistral</a><br> | |
| · <a href="https://dashboard.cohere.com/" target="_blank">Cohere</a><br> | |
| · <a href="https://build.nvidia.com/" target="_blank">NVIDIA</a><br> | |
| · <a href="https://github.com/settings/tokens" target="_blank">GitHub PAT</a> | |
| <p style="font-size:0.75em;color:#888;text-align:center;margin-top:8px">🔒 Sve poruke ostaju na tvom uređaju</p> | |
| </div> | |
| """) | |
| with gr.Column(scale=3): | |
| chatbot = gr.Chatbot(label="💬 Chat", height=500, | |
| bubble_full_width=False, show_copy_button=True, | |
| avatar_images=(None, "🤖"), type="messages") | |
| with gr.Row(): | |
| msg_tb = gr.Textbox(label="Poruka", | |
| placeholder="Napiši poruku... (ili uključi Image Mode za slike)", | |
| scale=4, container=True) | |
| send_btn = gr.Button("➡️", variant="primary", scale=1, min_width=60) | |
| gr.HTML(""" | |
| <div class="toolbar"> | |
| <span>🤖 Alati: 🕐 Vrijeme · 🧮 Kalkulator · 🔍 Web · 🎲 Random · 📄 Dokument</span> | |
| <span>🔌 Groq · Gemini · OpenRouter · SambaNova · Mistral · Cohere · NVIDIA · GitHub</span> | |
| </div> | |
| """) | |
| # === Events === | |
| def upd_models(p): | |
| m = list(PROVIDERS.get(p,{}).get("models",{}).keys()) | |
| return gr.Dropdown(choices=m, value=m[0] if m else None) | |
| prov_dd.change(fn=upd_models, inputs=[prov_dd], outputs=[model_dd]) | |
| def upd_ph(img): | |
| return gr.update(placeholder="Opiši sliku za generisanje..." if img else "Napiši poruku...") | |
| img_cb.change(fn=upd_ph, inputs=[img_cb], outputs=[msg_tb]) | |
| inputs = [msg_tb, chat_state, prov_dd, model_dd, file_up, tools_cb, img_cb] | |
| outputs = [chat_state, chatbot, msg_tb] | |
| send_btn.click(fn=chat_fn, inputs=inputs, outputs=outputs) | |
| msg_tb.submit(fn=chat_fn, inputs=inputs, outputs=outputs) | |
| return demo | |
| # ============================================================ | |
| # MAIN | |
| # ============================================================ | |
| if __name__ == "__main__": | |
| print("=" * 60) | |
| print(" 🚀 FreeToChat Clone — 8 Providera") | |
| print(" Groq · Gemini · OpenRouter · SambaNova") | |
| print(" Mistral · Cohere · NVIDIA · GitHub") | |
| print("=" * 60) | |
| if not any([GROQ_API_KEY, GOOGLE_API_KEY]): | |
| print("⚠️ Postavi API ključeve u HF Spaces → Secrets!") | |
| demo = create_ui() | |
| port = int(os.getenv("SPACE_PORT", 7860)) | |
| demo.launch(server_name="0.0.0.0", server_port=port, share=False) | |