| 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) |
|
|