Genisi / index.html
AnesKAM's picture
Update index.html
3c07901 verified
Raw
History Blame Contribute Delete
22.6 kB
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Genisi AI</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
Inter,
Arial,
"Noto Sans Arabic",
sans-serif;
background: #0b0d12;
color: #f4f4f5;
min-height: 100vh;
}
button,
textarea,
select {
font: inherit;
}
.app {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 280px;
background: #11141b;
border-left: 1px solid #272b35;
display: flex;
flex-direction: column;
padding: 18px;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 21px;
font-weight: 800;
margin-bottom: 22px;
}
.logo-icon {
width: 38px;
height: 38px;
border-radius: 12px;
background: linear-gradient(
135deg,
#7c3aed,
#2563eb
);
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
}
.new-chat {
width: 100%;
border: 1px solid #343946;
background: #191d26;
color: white;
padding: 12px;
border-radius: 12px;
cursor: pointer;
margin-bottom: 15px;
}
.new-chat:hover {
background: #222733;
}
.sidebar-title {
font-size: 12px;
color: #8e95a3;
margin: 12px 5px;
}
.history {
flex: 1;
overflow-y: auto;
}
.history-item {
padding: 10px;
border-radius: 9px;
color: #c8ccd5;
cursor: pointer;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item:hover {
background: #1c212b;
}
.sidebar-footer {
color: #777e8b;
font-size: 12px;
padding-top: 12px;
}
/* Main */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
height: 66px;
border-bottom: 1px solid #272b35;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
background: #0e1016;
}
.model-area {
display: flex;
align-items: center;
gap: 10px;
}
.model-select {
background: #191d25;
border: 1px solid #343946;
color: white;
padding: 9px 12px;
border-radius: 10px;
outline: none;
}
.status {
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: #969ca8;
}
.dot {
width: 8px;
height: 8px;
background: #31d17c;
border-radius: 50%;
}
/* Chat */
.chat {
flex: 1;
overflow-y: auto;
padding: 30px 20px 180px;
}
.messages {
max-width: 900px;
margin: auto;
}
.welcome {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.welcome-inner {
max-width: 650px;
}
.welcome h1 {
font-size: 40px;
margin-bottom: 10px;
}
.welcome p {
color: #9ba1ad;
line-height: 1.8;
}
.message {
display: flex;
margin-bottom: 25px;
gap: 12px;
}
.message.user {
justify-content: flex-start;
}
.message.assistant {
justify-content: flex-start;
}
.avatar {
width: 36px;
height: 36px;
flex: 0 0 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: #202530;
font-size: 14px;
}
.message.user .avatar {
background: #303746;
}
.bubble {
max-width: min(850px, 90%);
line-height: 1.75;
white-space: pre-wrap;
word-wrap: break-word;
}
.user .bubble {
background: #1d2330;
border: 1px solid #2c3341;
padding: 12px 15px;
border-radius: 16px;
}
.assistant .bubble {
padding: 5px 2px;
}
.bubble pre {
direction: ltr;
text-align: left;
background: #090b0f;
border: 1px solid #292e38;
padding: 15px;
border-radius: 12px;
overflow-x: auto;
}
/* Composer */
.composer-wrapper {
position: absolute;
bottom: 0;
left: 280px;
right: 0;
padding: 15px 20px 20px;
background:
linear-gradient(
transparent,
#0b0d12 35%
);
}
.composer {
max-width: 900px;
margin: auto;
background: #171b23;
border: 1px solid #303641;
border-radius: 17px;
box-shadow: 0 10px 40px rgba(0,0,0,.35);
}
textarea {
width: 100%;
resize: none;
min-height: 62px;
max-height: 180px;
border: 0;
outline: 0;
background: transparent;
color: white;
padding: 17px;
}
textarea::placeholder {
color: #777f8e;
}
.composer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px 10px;
}
.tools {
display: flex;
gap: 7px;
align-items: center;
}
.tool {
background: #202530;
border: 1px solid #303641;
color: #d4d7de;
border-radius: 9px;
padding: 7px 10px;
cursor: pointer;
font-size: 12px;
}
.tool.active {
background: #35206a;
border-color: #7548c8;
color: #fff;
}
.send {
width: 42px;
height: 42px;
border: 0;
border-radius: 12px;
background: #fff;
color: #08090c;
cursor: pointer;
font-weight: 900;
}
.send:disabled {
opacity: .45;
cursor: not-allowed;
}
/* Image */
.image-panel {
max-width: 900px;
margin: 0 auto 20px;
display: none;
}
.image-panel.show {
display: block;
}
.image-box {
background: #151820;
border: 1px solid #303641;
border-radius: 15px;
padding: 15px;
}
.image-box img {
max-width: 100%;
max-height: 650px;
display: block;
margin: auto;
border-radius: 12px;
}
.download {
display: inline-block;
margin-top: 12px;
background: #fff;
color: #000;
padding: 9px 14px;
border-radius: 9px;
text-decoration: none;
font-weight: 700;
}
.error {
color: #ff7070;
padding: 10px;
}
@media (max-width: 800px) {
.sidebar {
display: none;
}
.composer-wrapper {
left: 0;
}
.welcome h1 {
font-size: 30px;
}
.topbar {
padding: 0 12px;
}
.chat {
padding-left: 12px;
padding-right: 12px;
}
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="logo">
<div class="logo-icon">G</div>
<span>Genisi AI</span>
</div>
<button class="new-chat" onclick="newChat()">
+ محادثة جديدة
</button>
<div class="sidebar-title">
المحادثات
</div>
<div id="history" class="history"></div>
<div class="sidebar-footer">
Genisi AI · Custom Interface
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="model-area">
<select id="model" class="model-select">
<option value="flash_lite">
Genisi 4 Flash Lite
</option>
<option value="flash" selected>
Genisi 4 Flash
</option>
<option value="pro">
Genisi 4 Pro
</option>
</select>
</div>
<div class="status">
<span class="dot"></span>
<span id="statusText">جاهز</span>
</div>
</header>
<section id="chat" class="chat">
<div id="messages" class="messages">
<div id="welcome" class="welcome">
<div class="welcome-inner">
<h1>أهلاً بك في Genisi AI</h1>
<p>
اختر النموذج المناسب وابدأ المحادثة.
يمكنك استخدام Flash Lite للسرعة،
Flash للاستخدام اليومي،
وPro للمهام الأكثر تعقيداً.
</p>
</div>
</div>
</div>
</section>
<div id="imagePanel" class="image-panel">
<div class="image-box">
<img id="generatedImage" alt="Generated image">
<a
id="downloadImage"
class="download"
download="genisi-image.png"
>
تنزيل الصورة
</a>
</div>
</div>
<div class="composer-wrapper">
<div class="composer">
<textarea
id="input"
placeholder="اكتب رسالتك هنا..."
rows="1"
></textarea>
<div class="composer-bottom">
<div class="tools">
<button
id="thinking"
class="tool"
onclick="toggleThinking()"
>
🧠 التفكير
</button>
<button
class="tool"
onclick="generateImage()"
>
🎨 صورة
</button>
</div>
<button
id="send"
class="send"
onclick="sendMessage()"
>
</button>
</div>
</div>
</div>
</main>
</div>
<script>
const input = document.getElementById("input");
const sendButton = document.getElementById("send");
const messagesElement = document.getElementById("messages");
const statusText = document.getElementById("statusText");
const thinkingButton = document.getElementById("thinking");
let messages = [];
let thinkingEnabled = false;
let isGenerating = false;
/* ---------------------------
Helpers
---------------------------- */
function escapeHTML(text) {
return text
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function removeWelcome() {
const welcome = document.getElementById("welcome");
if (welcome) {
welcome.remove();
}
}
function addMessage(role, text) {
removeWelcome();
const wrapper = document.createElement("div");
wrapper.className =
"message " +
(role === "user" ? "user" : "assistant");
const avatar = document.createElement("div");
avatar.className = "avatar";
avatar.textContent =
role === "user" ? "أنت" : "G";
const bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = text;
wrapper.appendChild(avatar);
wrapper.appendChild(bubble);
messagesElement.appendChild(wrapper);
scrollBottom();
return bubble;
}
function scrollBottom() {
const chat = document.getElementById("chat");
chat.scrollTop = chat.scrollHeight;
}
function setStatus(text) {
statusText.textContent = text;
}
/* ---------------------------
Thinking
---------------------------- */
function toggleThinking() {
thinkingEnabled = !thinkingEnabled;
thinkingButton.classList.toggle(
"active",
thinkingEnabled
);
}
/* ---------------------------
Send message
---------------------------- */
async function sendMessage() {
if (isGenerating) return;
const text = input.value.trim();
if (!text) return;
isGenerating = true;
sendButton.disabled = true;
input.value = "";
addMessage("user", text);
messages.push({
role: "user",
content: text
});
const model = document.getElementById("model").value;
setStatus("جاري التفكير...");
const bubble = addMessage(
"assistant",
""
);
try {
const response = await fetch(
"/api/chat/stream",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
messages: messages,
model: model,
thinking: thinkingEnabled
})
}
);
if (!response.ok) {
throw new Error(
"HTTP " + response.status
);
}
const reader =
response.body.getReader();
const decoder =
new TextDecoder("utf-8");
let buffer = "";
let assistantText = "";
while (true) {
const { value, done } =
await reader.read();
if (done) break;
buffer += decoder.decode(
value,
{ stream: true }
);
const events =
buffer.split("\n\n");
buffer =
events.pop() || "";
for (const event of events) {
const line =
event
.split("\n")
.find(x =>
x.startsWith("data:")
);
if (!line) continue;
const raw =
line.substring(5).trim();
if (!raw) continue;
let data;
try {
data = JSON.parse(raw);
} catch {
continue;
}
if (data.type === "text") {
assistantText +=
data.text;
bubble.textContent =
assistantText;
scrollBottom();
}
if (data.type === "error") {
throw new Error(
data.error
);
}
}
}
messages.push({
role: "assistant",
content: assistantText
});
saveHistory();
setStatus("جاهز");
} catch (error) {
bubble.textContent =
"❌ حدث خطأ: " +
error.message;
setStatus("خطأ");
} finally {
isGenerating = false;
sendButton.disabled = false;
input.focus();
}
}
/* ---------------------------
Image generation
---------------------------- */
async function generateImage() {
const prompt =
input.value.trim();
if (!prompt) {
alert(
"اكتب وصف الصورة أولاً."
);
return;
}
const panel =
document.getElementById(
"imagePanel"
);
const image =
document.getElementById(
"generatedImage"
);
const download =
document.getElementById(
"downloadImage"
);
panel.classList.add("show");
image.removeAttribute("src");
setStatus("جاري إنشاء الصورة...");
try {
const url =
"/api/image?prompt=" +
encodeURIComponent(prompt) +
"&image=" +
encodeURIComponent("") +
"&style=" +
encodeURIComponent("");
const response =
await fetch(url);
const data =
await response.json();
/*
* API قد يعيد رابط الصورة
* داخل حقول مختلفة.
*/
const imageUrl =
findImageURL(data);
if (!imageUrl) {
throw new Error(
"لم يتم العثور على رابط الصورة في استجابة API."
);
}
image.src = imageUrl;
download.href = imageUrl;
setStatus("تم إنشاء الصورة");
} catch (error) {
setStatus("فشل إنشاء الصورة");
alert(
"❌ " + error.message
);
}
}
function findImageURL(data) {
if (!data) return null;
if (typeof data === "string") {
if (
data.startsWith("http://") ||
data.startsWith("https://") ||
data.startsWith("data:image/")
) {
return data;
}
}
if (Array.isArray(data)) {
for (const item of data) {
const found =
findImageURL(item);
if (found) return found;
}
}
if (typeof data === "object") {
const possibleKeys = [
"url",
"image",
"imageUrl",
"image_url",
"src",
"result",
"output",
"data"
];
for (const key of possibleKeys) {
if (key in data) {
const found =
findImageURL(data[key]);
if (found) return found;
}
}
}
return null;
}
/* ---------------------------
New chat
---------------------------- */
function newChat() {
messages = [];
messagesElement.innerHTML = `
<div id="welcome" class="welcome">
<div class="welcome-inner">
<h1>أهلاً بك في Genisi AI</h1>
<p>
ابدأ محادثة جديدة.
</p>
</div>
</div>
`;
document
.getElementById("imagePanel")
.classList.remove("show");
setStatus("جاهز");
input.focus();
}
/* ---------------------------
History
---------------------------- */
function saveHistory() {
if (!messages.length) return;
const firstUser =
messages.find(
x => x.role === "user"
);
if (!firstUser) return;
const history =
JSON.parse(
localStorage.getItem(
"genisi_history"
) || "[]"
);
history.unshift(
firstUser.content
);
localStorage.setItem(
"genisi_history",
JSON.stringify(
history.slice(0, 20)
)
);
renderHistory();
}
function renderHistory() {
const container =
document.getElementById(
"history"
);
const history =
JSON.parse(
localStorage.getItem(
"genisi_history"
) || "[]"
);
container.innerHTML = "";
history.forEach(item => {
const div =
document.createElement("div");
div.className =
"history-item";
div.textContent = item;
div.onclick = () => {
input.value = item;
input.focus();
};
container.appendChild(div);
});
}
/* ---------------------------
Keyboard
---------------------------- */
input.addEventListener(
"keydown",
event => {
if (
event.key === "Enter" &&
!event.shiftKey
) {
event.preventDefault();
sendMessage();
}
}
);
/* Auto resize */
input.addEventListener(
"input",
() => {
input.style.height = "auto";
input.style.height =
Math.min(
input.scrollHeight,
180
) + "px";
}
);
/* Start */
renderHistory();
input.focus();
</script>
</body>
</html>