| | import os |
| | import gradio as gr |
| | from fastapi import FastAPI, Request, File, UploadFile |
| | from fastapi.responses import StreamingResponse, HTMLResponse |
| | from openai import OpenAI |
| |
|
| | app = FastAPI() |
| |
|
| | |
| | HTML_UI = """ |
| | <!DOCTYPE html> |
| | <html lang="ar" dir="rtl"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| | <title>Neural Vault</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> |
| | <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> |
| | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
| | <style> |
| | body { background: #020617; color: #f8fafc; height: 100dvh; overflow: hidden; margin: 0; position: fixed; width: 100vw; } |
| | .chat-area { height: calc(100dvh - 160px); overflow-y: auto; padding: 20px; -webkit-overflow-scrolling: touch; } |
| | .sidebar { position: fixed; top: 0; right: -100%; width: 80%; max-width: 300px; height: 100%; background: #0f172a; z-index: 2000; transition: 0.3s ease; border-left: 1px solid #1e293b; } |
| | .sidebar.open { right: 0; } |
| | .input-bar { position: fixed; bottom: 0; width: 100%; padding: 15px; background: #020617; border-top: 1px solid #1e293b; } |
| | .bubble { max-width: 85%; padding: 12px; border-radius: 18px; font-size: 14px; margin-bottom: 10px; line-height: 1.5; } |
| | .user { background: #1e293b; margin-right: auto; border-bottom-right-radius: 4px; } |
| | .bot { background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.2); margin-left: auto; border-bottom-left-radius: 4px; } |
| | </style> |
| | </head> |
| | <body> |
| | <div id="root"></div> |
| | <script type="text/babel"> |
| | const { useState, useEffect, useRef } = React; |
| | function App() { |
| | const [messages, setMessages] = useState([]); |
| | const [input, setInput] = useState(""); |
| | const [isSideOpen, setSideOpen] = useState(false); |
| | const [status, setStatus] = useState("idle"); |
| | const chatRef = useRef(null); |
| | |
| | useEffect(() => { chatRef.current?.scrollTo(0, chatRef.current.scrollHeight); }, [messages]); |
| | |
| | const send = async () => { |
| | if(!input.trim()) return; |
| | const userMsg = {role: 'user', content: input}; |
| | setMessages(prev => [...prev, userMsg]); |
| | setInput(""); |
| | setStatus("thinking"); |
| | |
| | try { |
| | const res = await fetch('/v1/chat/completions', { |
| | method: 'POST', |
| | headers: {'Content-Type': 'application/json'}, |
| | body: JSON.stringify({ messages: [...messages, userMsg] }) |
| | }); |
| | |
| | const reader = res.body.getReader(); |
| | let botMsg = { role: 'assistant', content: "" }; |
| | setMessages(p => [...p, botMsg]); |
| | |
| | while(true) { |
| | const {done, value} = await reader.read(); |
| | if(done) break; |
| | const chunk = new TextDecoder().decode(value).replace(/data: /g, ''); |
| | if(chunk.includes("[DONE]")) break; |
| | botMsg.content += chunk; |
| | setMessages(p => [...p.slice(0, -1), {...botMsg}]); |
| | } |
| | } catch (e) { console.error(e); } |
| | setStatus("idle"); |
| | }; |
| | |
| | return ( |
| | <div className="flex flex-col h-full"> |
| | <header className="p-4 border-b border-white/5 flex justify-between items-center bg-slate-900/50"> |
| | <button onClick={() => setSideOpen(true)} className="text-xl">☰</button> |
| | <span className="font-bold text-sm tracking-widest">NEURAL VAULT</span> |
| | <div className={`w-2 h-2 rounded-full ${status === 'idle' ? 'bg-emerald-500' : 'bg-indigo-500 animate-pulse'}`}></div> |
| | </header> |
| | |
| | <div ref={chatRef} className="chat-area space-y-4"> |
| | {messages.map((m, i) => ( |
| | <div key={i} className={`bubble ${m.role === 'user' ? 'user' : 'bot'}`}> |
| | {m.content} |
| | </div> |
| | ))} |
| | </div> |
| | |
| | <div className="input-bar"> |
| | <div className="flex gap-2 max-w-xl mx-auto"> |
| | <input |
| | className="flex-1 bg-slate-900 border border-white/10 rounded-xl px-4 py-2 outline-none text-sm" |
| | placeholder="اكتب سؤالك..." |
| | value={input} |
| | onChange={e => setInput(e.target.value)} |
| | onKeyDown={e => e.key === 'Enter' && send()} |
| | /> |
| | <button onClick={send} className="bg-indigo-600 px-5 rounded-xl">↑</button> |
| | </div> |
| | </div> |
| | |
| | <div className={`sidebar p-6 ${isSideOpen ? 'open' : ''}`}> |
| | <div className="flex justify-between items-center mb-10"> |
| | <h2 className="font-bold">الإعدادات</h2> |
| | <button onClick={() => setSideOpen(false)}>✕</button> |
| | </div> |
| | <div className="p-4 bg-slate-800/50 rounded-xl text-xs"> |
| | <p>الموديل: Mistral-Small-24B</p> |
| | <p className="mt-2 text-emerald-400">الحالة: متصل</p> |
| | </div> |
| | </div> |
| | </div> |
| | ); |
| | } |
| | const root = ReactDOM.createRoot(document.getElementById('root')); |
| | root.render(<App />); |
| | </script> |
| | </body> |
| | </html> |
| | """ |
| |
|
| | @app.get("/") |
| | async def ui(): |
| | return HTMLResponse(HTML_UI) |
| |
|
| | @app.post("/v1/chat/completions") |
| | async def chat_api(request: Request): |
| | body = await request.json() |
| | |
| | |
| | api_key = os.getenv("HF_TOKEN") |
| | |
| | client = OpenAI( |
| | base_url="https://router.huggingface.co/hf-inference/v1", |
| | api_key=api_key |
| | ) |
| |
|
| | def stream_gen(): |
| | resp = client.chat.completions.create( |
| | model="huihui-ai/Mistral-Small-24B-Instruct-2501-abliterated", |
| | messages=body.get("messages", []), |
| | stream=True |
| | ) |
| | for chunk in resp: |
| | if chunk.choices[0].delta.content: |
| | yield f"data: {chunk.choices[0].delta.content}\n\n" |
| | yield "data: [DONE]\n\n" |
| |
|
| | return StreamingResponse(stream_gen(), media_type="text/event-stream") |
| |
|
| | |
| | with gr.Blocks() as auth_interface: |
| | gr.Markdown("# مرحبا بك في الدهليز") |
| | |
| | gr.LoginButton("الدخول عبر Hugging Face") |
| |
|
| | app = gr.mount_gradio_app(app, auth_interface, path="/auth") |
| |
|
| | if __name__ == "__main__": |
| | import uvicorn |
| | uvicorn.run(app, host="0.0.0.0", port=7860) |
| |
|