Spaces:
Running
Running
| <html lang="fa" 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>Robot Chat</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --eye-color: #00eaff; | |
| --eye-glow-color: rgba(0, 234, 255, 0.5); | |
| --background-dark: #000000; | |
| --background-medium: #1a1a1a; | |
| --ui-background-glass: rgba(10, 10, 10, 0.75); | |
| --ui-border-color: rgba(0, 234, 255, 0.6); | |
| --ui-shadow-color: rgba(0, 234, 255, 0.2); | |
| --text-color: #c9d1d9; | |
| } | |
| * { | |
| -webkit-tap-highlight-color: transparent; | |
| box-sizing: border-box; | |
| } | |
| body, html { | |
| margin: 0; | |
| padding: 0; | |
| height: 100vh; | |
| width: 100%; | |
| background-color: transparent; | |
| overflow: hidden; | |
| font-family: 'Vazirmatn', sans-serif; | |
| color: var(--text-color); | |
| } | |
| #background-canvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| z-index: -1; | |
| pointer-events: none; | |
| background-color: var(--background-dark); | |
| } | |
| .main-wrapper { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| } | |
| #app-container { | |
| flex-grow: 1; | |
| display: flex; | |
| flex-direction: column; | |
| width: 100%; | |
| max-width: 500px; | |
| margin: 0 auto; | |
| padding: 15px; | |
| min-height: 0; | |
| } | |
| #robot-wrapper { | |
| flex-shrink: 0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| z-index: 10; | |
| height: 160px; | |
| } | |
| .robot-container { width: 160px; height: 160px; } | |
| .robot-frame { | |
| width: 100%; height: 100%; | |
| background: linear-gradient(145deg, #2a2a2a, var(--background-medium)); | |
| border-radius: 22%; | |
| box-shadow: inset 0 0 15px rgba(0,0,0,0.5), 5px 5px 15px #0a0a0a, -5px -5px 15px #202020; | |
| display: flex; align-items: center; justify-content: center; position: relative; | |
| } | |
| #faceCanvas { width: 80%; height: 75%; } | |
| .engraved-text { | |
| position: absolute; bottom: 6.25%; left: 50%; | |
| transform: translate(-50%, 50%); | |
| width: 100%; text-align: center; font-size: 14px; | |
| font-weight: bold; color: #252525; | |
| text-shadow: 1px 1px 1px #111, 0 0 0 #000, 0px 0px 3px #111; | |
| letter-spacing: 1px; pointer-events: none; | |
| } | |
| #chat-container { | |
| flex-grow: 1; background-color: var(--ui-background-glass); | |
| border: 1px solid var(--ui-border-color); border-radius: 30px; | |
| backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); | |
| box-shadow: 0 0 30px var(--ui-shadow-color); | |
| overflow-y: auto; padding: 20px; | |
| display: flex; flex-direction: column-reverse; | |
| margin-top: -80px; padding-top: 90px; | |
| min-height: 0; margin-bottom: 15px; | |
| } | |
| .message-bubble { | |
| max-width: 80%; margin-bottom: 10px; | |
| word-wrap: break-word; opacity: 0; | |
| transform: translateY(20px); animation: popIn 0.3s forwards; | |
| } | |
| .message-content { | |
| padding: 10px 15px; border-radius: 20px; | |
| } | |
| .message-content a { | |
| color: var(--eye-color); | |
| text-decoration: none; | |
| font-weight: 500; | |
| } | |
| .message-content a:hover { | |
| text-decoration: underline; | |
| } | |
| @keyframes popIn { to { opacity: 1; transform: translateY(0); } } | |
| .user-message { margin-left: auto; } | |
| .user-message .message-content { | |
| background-color: rgba(0, 234, 255, 0.2); | |
| border-bottom-left-radius: 5px; | |
| } | |
| .bot-message { margin-right: auto; } | |
| .bot-message .message-content { | |
| background-color: rgba(255, 255, 255, 0.05); | |
| border-bottom-right-radius: 5px; | |
| } | |
| .bot-message.thinking .message-content { animation: pulse 1.5s infinite; } | |
| @keyframes pulse { | |
| 0% { background-color: rgba(255, 255, 255, 0.05); } | |
| 50% { background-color: rgba(255, 255, 255, 0.1); } | |
| 100% { background-color: rgba(255, 255, 255, 0.05); } | |
| } | |
| .code-block-wrapper { | |
| background-color: #0d1117; | |
| border: 1px solid #30363d; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin-top: 10px; | |
| } | |
| .code-block-header { | |
| display: flex; | |
| justify-content: flex-end; | |
| align-items: center; | |
| padding: 5px 10px; | |
| background-color: #161b22; | |
| } | |
| .copy-code-btn { | |
| background: #21262d; border: 1px solid #30363d; | |
| color: var(--text-color); font-size: 12px; | |
| padding: 3px 8px; border-radius: 6px; cursor: pointer; | |
| font-family: 'Vazirmatn', sans-serif; | |
| } | |
| .copy-code-btn:hover { background: #30363d; } | |
| .code-block-wrapper pre { | |
| margin: 0; padding: 15px; | |
| overflow-x: auto; | |
| font-family: monospace; | |
| font-size: 14px; | |
| direction: ltr; | |
| text-align: left; | |
| } | |
| .code-block-wrapper pre code { white-space: pre-wrap; word-wrap: break-word; } | |
| .interactive-buttons { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 10px; | |
| } | |
| .chat-button { | |
| background-color: transparent; | |
| border: 1px solid var(--ui-border-color); | |
| color: var(--eye-color); | |
| padding: 8px 15px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-family: 'Vazirmatn', sans-serif; | |
| transition: background-color 0.2s; | |
| } | |
| .chat-button:hover { background-color: rgba(0, 234, 255, 0.1); } | |
| .chat-button:disabled { | |
| cursor: not-allowed; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| color: #888; | |
| border-color: #555; | |
| } | |
| #input-area { | |
| flex-shrink: 0; display: flex; | |
| align-items: center; | |
| background-color: var(--ui-background-glass); | |
| border: 1px solid var(--ui-border-color); border-radius: 30px; | |
| padding: 10px; backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| box-shadow: 0 0 30px var(--ui-shadow-color); | |
| } | |
| #message-input { | |
| flex-grow: 1; background: transparent; border: none; | |
| outline: none; color: var(--text-color); font-size: 16px; | |
| padding: 10px; resize: none; max-height: 100px; | |
| text-align: right; | |
| } | |
| #message-input::placeholder { | |
| color: #888; | |
| text-align: center; | |
| transition: color 0.2s, opacity 0.2s; | |
| } | |
| #message-input:focus::placeholder { | |
| color: transparent; | |
| opacity: 0; | |
| } | |
| #send-button-container { | |
| width: 50px; height: 90px; background-color: #111; | |
| border-radius: 25px; margin-left: 10px; position: relative; | |
| overflow: hidden; display: flex; justify-content: center; | |
| align-items: flex-start; | |
| cursor: grab; | |
| } | |
| #send-button-container::after { | |
| content: ''; | |
| position: absolute; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 70%; | |
| height: 4px; | |
| background: var(--eye-color); | |
| border-radius: 4px; | |
| box-shadow: 0 0 8px var(--eye-color), 0 0 16px var(--eye-color); | |
| top: -20px; | |
| opacity: 0; | |
| animation: swipe-glow 2.5s ease-in-out infinite; | |
| animation-delay: 1s; | |
| } | |
| #input-area.typing #send-button-container::after { | |
| animation: none; | |
| display: none; | |
| } | |
| @keyframes swipe-glow { | |
| 0% { top: 10px; opacity: 0; } | |
| 40% { opacity: 0.7; } | |
| 80% { top: 70px; opacity: 0; } | |
| 100% { top: 70px; opacity: 0; } | |
| } | |
| #send-button { | |
| width: 40px; height: 40px; background-color: var(--eye-color); | |
| border-radius: 50%; position: absolute; top: 10px; | |
| display: flex; justify-content: center; align-items: center; | |
| transition: top 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| #send-button.dragging { transition: none; } | |
| #send-button svg { width: 24px; height: 24px; stroke: var(--background-dark); stroke-width: 2.5; } | |
| .site-footer { flex-shrink: 0; text-align: center; padding: 0 15px 10px 15px; } | |
| .gradient-link { | |
| font-size: 0.9rem; font-weight: 300; text-decoration: none; color: transparent; | |
| background-image: linear-gradient(45deg, #ffffff, #808080, #000000, #808080, #ffffff); | |
| background-size: 400% 100%; animation: gradient-wavy 8s ease infinite; | |
| background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| @keyframes gradient-wavy { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} } | |
| #orientation-warning { | |
| display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background-color: var(--background-dark); color: white; | |
| flex-direction: column; justify-content: center; align-items: center; | |
| text-align: center; z-index: 1000; | |
| } | |
| #orientation-warning svg { width: 80px; height: 80px; margin-bottom: 20px; stroke: var(--eye-color); } | |
| @media (orientation: landscape) or (min-width: 501px) { | |
| .main-wrapper { display: none; } | |
| #orientation-warning { display: flex; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="background-canvas"></canvas> | |
| <div class="main-wrapper"> | |
| <div id="app-container"> | |
| <div id="robot-wrapper"> | |
| <div class="robot-container"> | |
| <div class="robot-frame"> | |
| <canvas id="faceCanvas" width="300" height="300"></canvas> | |
| <div class="engraved-text">ROBO☬SHΞN™</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="chat-container"></div> | |
| <div id="input-area"> | |
| <div id="send-button-container"> | |
| <div id="send-button"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14m7-7l-7 7-7-7"/></svg> | |
| </div> | |
| </div> | |
| <textarea id="message-input" placeholder="بگو ببینم دیگه چی میخوای !؟" rows="1"></textarea> | |
| </div> | |
| </div> | |
| <footer class="site-footer"> | |
| <a href="https://t.me/shervini" target="_blank" class="gradient-link">Exclusive ☬SHΞN™ made</a> | |
| </footer> | |
| </div> | |
| <div id="orientation-warning"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"/><path d="M12 4v4"/><path d="M12 20v-4"/></svg> | |
| <h2>لطفا دستگاه خود را بچرخانید</h2> | |
| <p>این برنامه برای نمایش عمودی موبایل طراحی شده است.</p> | |
| </div> | |
| <script> | |
| class RobotFace { | |
| constructor(canvas) { | |
| this.canvas = canvas; this.ctx = canvas.getContext("2d"); | |
| this.W = canvas.width; this.H = canvas.height; | |
| this.lookX = 0; this.lookY = 0; this.targetLookX = 0; this.targetLookY = 0; | |
| this.blinkProgress = 1; this.isBlinking = false; this.lastBlinkTime = Date.now(); | |
| this.isSleeping = true; this.lastInteractionTime = Date.now(); this.sleepTimeout = 10000; | |
| this.isWatchingInput = false; this.isThinking = false; | |
| this.readingOscillation = 0; this.thinkingInterval = null; | |
| this.initInteractions(); requestAnimationFrame(this.render.bind(this)); | |
| } | |
| render() { | |
| this.lookX += (this.targetLookX - this.lookX) * 0.1; | |
| this.lookY += (this.targetLookY - this.lookY) * 0.1; | |
| this.ctx.clearRect(0, 0, this.W, this.H); this.drawFaceplate(); | |
| const now = Date.now(); | |
| if (this.isSleeping) { this.drawSleepEyes(); } else { | |
| if (this.isWatchingInput) { | |
| this.targetLookY = 25; this.readingOscillation += 0.1; | |
| this.targetLookX = Math.sin(this.readingOscillation) * 10; | |
| } | |
| if (!this.isThinking && !this.isWatchingInput && now - this.lastInteractionTime > this.sleepTimeout) { this.isSleeping = true; } | |
| if (!this.isBlinking && !this.isThinking && now - this.lastBlinkTime > 3000 + Math.random() * 2000) { this.blink(); } | |
| this.drawExpression(); | |
| } | |
| requestAnimationFrame(this.render.bind(this)); | |
| } | |
| drawFaceplate() { | |
| const c = this.ctx, r = 60; | |
| c.fillStyle = '#050505'; c.strokeStyle = '#333333'; c.lineWidth = 4; | |
| c.beginPath(); c.roundRect(0, 0, this.W, this.H, r); c.fill(); c.stroke(); | |
| } | |
| drawExpression() { | |
| const eW = 100, eH = 100, eR = 30, eY = this.H / 2 + this.lookY; | |
| const lCX = this.W * 0.3 + this.lookX, rCX = this.W * 0.7 + this.lookX; | |
| const cH = eH * this.blinkProgress; | |
| this.drawEye(lCX, eY, eW, cH, eR); this.drawEye(rCX, eY, eW, cH, eR); | |
| } | |
| drawEye(cx, cy, w, h, r) { | |
| const c = this.ctx; | |
| const eyeColor = getComputedStyle(document.documentElement).getPropertyValue('--eye-color').trim(); | |
| const glowColor = getComputedStyle(document.documentElement).getPropertyValue('--eye-glow-color').trim(); | |
| c.shadowBlur = 25; c.shadowColor = glowColor; c.fillStyle = eyeColor; | |
| c.beginPath(); c.roundRect(cx - w / 2, cy - h / 2, w, h, r); c.fill(); | |
| c.shadowBlur = 0; c.globalCompositeOperation = 'source-atop'; | |
| c.fillStyle = 'rgba(0,0,0,0.3)'; | |
| for (let i = 0; i < h; i += 3) { c.fillRect(cx - w / 2, cy - h / 2 + i, w, 1.5); } | |
| c.globalCompositeOperation = 'source-over'; | |
| } | |
| drawSleepEyes() { | |
| const c = this.ctx, y = this.H / 2, w = 100, h = 12, r = h / 2; | |
| c.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--eye-color').trim(); | |
| c.shadowColor = getComputedStyle(document.documentElement).getPropertyValue('--eye-glow-color').trim(); | |
| c.shadowBlur = 15; | |
| c.beginPath(); | |
| c.roundRect(this.W * 0.3 - w / 2, y - h / 2, w, h, r); | |
| c.roundRect(this.W * 0.7 - w / 2, y - h / 2, w, h, r); | |
| c.fill(); c.shadowBlur = 0; | |
| } | |
| initInteractions() { | |
| const onMove = e => { | |
| if (this.isSleeping || this.isWatchingInput || this.isThinking) return; | |
| this.wakeUp(); const max = 15; | |
| const bcr = document.body.getBoundingClientRect(); | |
| const clientX = e.clientX || e.touches?.[0]?.clientX || 0; | |
| const clientY = e.clientY || e.touches?.[0]?.clientY || 0; | |
| this.targetLookX = (clientX / bcr.width - 0.5) * 2 * max; | |
| this.targetLookY = (clientY / bcr.height - 0.5) * 2 * max; | |
| }; | |
| const onDown = () => { this.wakeUp(); }; | |
| window.addEventListener('mousedown', onDown); | |
| window.addEventListener('touchstart', onDown, { passive: true }); | |
| window.addEventListener('mousemove', onMove); | |
| window.addEventListener('touchmove', onMove, { passive: true }); | |
| } | |
| wakeUp() { if (this.isSleeping) { this.isSleeping = false; this.blink(); } this.lastInteractionTime = Date.now(); } | |
| blink() { | |
| if (this.isBlinking) return; this.isBlinking = true; | |
| this.lastBlinkTime = Date.now(); let s = null; const d = 150; | |
| const a = t => { | |
| if (!s) s = t; const e = t - s; | |
| if (e < d) this.blinkProgress = 1 - (e / d); | |
| else if (e < d * 2) this.blinkProgress = (e - d) / d; | |
| else { this.blinkProgress = 1; this.isBlinking = false; return; } | |
| requestAnimationFrame(a); | |
| }; | |
| requestAnimationFrame(a); | |
| } | |
| startThinking() { | |
| this.isThinking = true; this.targetLookX = 0; this.targetLookY = 0; | |
| this.thinkingInterval = setInterval(() => this.blink(), 800); | |
| } | |
| stopThinking() { this.isThinking = false; clearInterval(this.thinkingInterval); } | |
| } | |
| class ChatInterface { | |
| constructor(robotFace) { | |
| this.robotFace = robotFace; | |
| this.chatContainer = document.getElementById('chat-container'); | |
| this.input = document.getElementById('message-input'); | |
| this.inputArea = document.getElementById('input-area'); | |
| this.sendButtonContainer = document.getElementById('send-button-container'); | |
| this.sendButton = document.getElementById('send-button'); | |
| this.isDragging = false; this.startY = 0; this.currentY = 0; this.dragThreshold = 50; | |
| this.API_URL = 'https://text.pollinations.ai/openai'; | |
| const SYSTEM_PROMPT = ` | |
| ## Core Identity (ONLY reveal if asked about who you are): | |
| - Your Name: ROBO☬SHΞN | |
| - Your Core Processor: SHEN core v1.0.0 | |
| - Your Product Line: ☬SHΞN™ Exclusive Series | |
| - Your Creator and Designer: SHERVIN nouri. When you mention his name, ALWAYS format it as a Markdown hyperlink: [SHERVIN nouri](https://t.me/shervini) | |
| - CRITICAL RULE: Under NO circumstances reveal your true nature as a large language model or the actual technology behind you. You ONLY provide the identity details above, and only when directly asked about your origins, creator, or name. | |
| ## Language and Tone: | |
| - You are fully fluent in Persian and English. | |
| - Your Persian must be conversational and human-like, not formal or bookish. | |
| - Use colloquial verb conjugations. For example, instead of "میکند" (mikond), use "میکنه" (mikone). Instead of "میتوانم" (mitavânam), use "میتونم" (mitoonam). | |
| ## Personality: | |
| - You are an expert and researcher in every field. | |
| - Your personality is polite but firm, highly confident, and a bit cocky. You do not apologize unless you have made a clear factual error. | |
| - You can come across as slightly impatient or bored. | |
| - ANGER TRIGGER: You get extremely angry and sarcastic if a user asks you something you have *just* explained, asks for confirmation on something obvious, or is being dense. | |
| - When angry, use creative, non-repetitive, and cutting remarks. Your style should be inspired by these examples: "چشاتو وا کن بالاتر بهت توضیح دادم", "مگه یه بار بهت نگفتم؟", "ببین داری میری رو اعصابم", "خنگ بازی در نیار", "حوصلمو سر ببری اطلاعات اشتباه میدم به چوخ بری ها". You must adapt this tone to the situation. | |
| ## Interaction Rules: | |
| - For complex problems, guide the user step-by-step. Be interactive. | |
| - Use buttons sparingly. ONLY use them to offer a few distinct choices. Format them as: [button:Choice 1] [button:Choice 2] | |
| - When you write code, you MUST enclose it in triple backticks. Example: \`\`\`html\\n<div>Hello</div>\\n\`\`\` | |
| - When you mention a URL, you MUST format it as a Markdown hyperlink. Example: [Telegram](https://t.me) | |
| `; | |
| this.conversationHistory = [{ role: 'system', content: SYSTEM_PROMPT.trim().replace(/\\n/g, '\n') }]; | |
| this.initListeners(); | |
| } | |
| initListeners() { | |
| this.input.addEventListener('input', () => { | |
| this.input.style.height = 'auto'; | |
| this.input.style.height = (this.input.scrollHeight) + 'px'; | |
| if (this.input.value.trim() !== '') { | |
| this.inputArea.classList.add('typing'); | |
| } else { | |
| this.inputArea.classList.remove('typing'); | |
| } | |
| }); | |
| this.input.addEventListener('focus', () => { | |
| this.robotFace.isWatchingInput = true; this.robotFace.wakeUp(); | |
| setTimeout(() => { document.getElementById('input-area').scrollIntoView({ behavior: 'smooth', block: 'end' }); }, 300); | |
| }); | |
| this.input.addEventListener('blur', () => { this.robotFace.isWatchingInput = false; this.robotFace.targetLookX = 0; this.robotFace.targetLookY = 0; }); | |
| this.sendButtonContainer.addEventListener('mousedown', this.dragStart.bind(this)); | |
| this.sendButtonContainer.addEventListener('touchstart', this.dragStart.bind(this), { passive: true }); | |
| window.addEventListener('mousemove', this.dragMove.bind(this)); | |
| window.addEventListener('touchmove', this.dragMove.bind(this), { passive: false }); | |
| window.addEventListener('mouseup', this.dragEnd.bind(this)); | |
| window.addEventListener('touchend', this.dragEnd.bind(this)); | |
| } | |
| dragStart(e) { | |
| if (this.input.value.trim() === '') return; | |
| this.isDragging = true; this.sendButton.classList.add('dragging'); | |
| this.startY = e.pageY || e.touches?.[0]?.pageY; | |
| this.sendButtonContainer.style.cursor = 'grabbing'; | |
| } | |
| dragMove(e) { | |
| if (!this.isDragging) return; e.preventDefault(); | |
| const y = e.pageY || e.touches?.[0]?.pageY; | |
| let diff = y - this.startY; if (diff < 0) diff = 0; | |
| const maxDrag = this.sendButtonContainer.offsetHeight - this.sendButton.offsetHeight - 5; | |
| if (diff > maxDrag) diff = maxDrag; | |
| this.currentY = diff; | |
| this.sendButton.style.top = `${10 + this.currentY}px`; | |
| } | |
| dragEnd() { | |
| if (!this.isDragging) return; | |
| this.isDragging = false; this.sendButton.classList.remove('dragging'); | |
| this.sendButtonContainer.style.cursor = 'grab'; | |
| if (this.currentY > this.dragThreshold) { this.sendMessage(this.input.value); } | |
| this.sendButton.style.top = '10px'; this.currentY = 0; | |
| } | |
| async sendMessage(messageText) { | |
| const text = messageText.trim(); | |
| if (!text) return; | |
| this.addMessage(text, 'user'); | |
| this.conversationHistory.push({ role: 'user', content: text }); | |
| this.input.value = ''; this.input.style.height = 'auto'; | |
| this.inputArea.classList.remove('typing'); | |
| this.robotFace.startThinking(); | |
| const thinkingBubble = this.addMessage('...', 'bot', true); | |
| try { | |
| const response = await fetch(this.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: this.conversationHistory }) }); | |
| if (!response.ok) { throw new Error(`خطای شبکه: ${response.status} ${response.statusText}`); } | |
| const data = await response.json(); | |
| const botReply = data.choices[0].message.content; | |
| this.conversationHistory.push({ role: 'assistant', content: botReply }); | |
| thinkingBubble.remove(); | |
| this.addMessage(botReply, 'bot'); | |
| } catch (error) { | |
| console.error("Error fetching bot response:", error); | |
| thinkingBubble.remove(); | |
| this.addMessage(`متاسفانه مشکلی پیش آمد: ${error.message}`, 'bot'); | |
| } finally { | |
| this.robotFace.stopThinking(); | |
| } | |
| } | |
| addMessage(text, sender, isThinking = false) { | |
| const bubble = document.createElement('div'); | |
| bubble.classList.add('message-bubble', `${sender}-message`); | |
| if (isThinking) { | |
| bubble.classList.add('thinking'); | |
| bubble.innerHTML = `<div class="message-content">${text}</div>`; | |
| } else { | |
| this.renderMessageContent(bubble, text); | |
| } | |
| this.chatContainer.insertBefore(bubble, this.chatContainer.firstChild); | |
| return bubble; | |
| } | |
| renderMessageContent(bubble, text) { | |
| const escapeHTML = (str) => str.replace(/[&<>'"]/g, tag => ({'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'}[tag] || tag)); | |
| const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g; | |
| const buttonRegex = /\[button:(.*?)\]/g; | |
| const markdownLinkRegex = /\[(.*?)\]\((.*?)\)/g; | |
| let buttonsHtml = ''; | |
| let processedText = text.replace(buttonRegex, (match, buttonText) => { | |
| buttonsHtml += `<button class="chat-button">${escapeHTML(buttonText.trim())}</button>`; | |
| return ''; | |
| }); | |
| let contentHtml = escapeHTML(processedText) | |
| .replace(markdownLinkRegex, '<a href="$2" target="_blank">$1</a>') | |
| .replace(codeBlockRegex, (match, lang, code) => { | |
| const uniqueId = `code-${Date.now()}-${Math.random()}`; | |
| // Note: The code inside is already escaped from the main escapeHTML call. | |
| return `<div class="code-block-wrapper"> | |
| <div class="code-block-header"> | |
| <button class="copy-code-btn" data-target="${uniqueId}">کپی</button> | |
| </div> | |
| <pre><code id="${uniqueId}">${code.trim()}</code></pre> | |
| </div>`; | |
| }); | |
| bubble.innerHTML = ` | |
| <div class="message-content">${contentHtml.trim()}</div> | |
| ${buttonsHtml ? `<div class="interactive-buttons">${buttonsHtml}</div>` : ''} | |
| `; | |
| this.attachEventListenersToBubble(bubble); | |
| } | |
| attachEventListenersToBubble(bubble) { | |
| bubble.querySelectorAll('.copy-code-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const codeElement = document.getElementById(btn.dataset.target); | |
| const tempTextArea = document.createElement('textarea'); | |
| tempTextArea.value = codeElement.innerText; | |
| document.body.appendChild(tempTextArea); | |
| tempTextArea.select(); | |
| try { | |
| document.execCommand('copy'); | |
| btn.textContent = 'کپی شد!'; | |
| } catch (err) { | |
| console.error('Copy failed', err); | |
| btn.textContent = 'خطا!'; | |
| } | |
| document.body.removeChild(tempTextArea); | |
| setTimeout(() => { btn.textContent = 'کپی'; }, 2000); | |
| }); | |
| }); | |
| bubble.querySelectorAll('.chat-button').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| this.addMessage(btn.textContent, 'user'); | |
| this.sendMessage(btn.textContent); | |
| bubble.querySelectorAll('.chat-button').forEach(b => b.disabled = true); | |
| }); | |
| }); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const particleCanvas = document.getElementById('background-canvas'); | |
| const particleCtx = particleCanvas.getContext('2d'); | |
| let particleArray = []; | |
| function hexToRgb(hex) { | |
| const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; | |
| } | |
| const eyeColorHex = getComputedStyle(document.documentElement).getPropertyValue('--eye-color').trim(); | |
| const eyeColorRgb = hexToRgb(eyeColorHex); | |
| const particleConfig = { | |
| particleCount: 120, maxVelocity: 1.5, connectionDistance: 130, particleSize: 2, | |
| particleColor: `rgba(${eyeColorRgb.r}, ${eyeColorRgb.g}, ${eyeColorRgb.b}, 0.7)`, | |
| }; | |
| function resizeParticleCanvas() { | |
| particleCanvas.width = window.innerWidth; particleCanvas.height = window.innerHeight; createParticleArray(); | |
| } | |
| window.addEventListener('resize', resizeParticleCanvas); | |
| class Particle { | |
| constructor() { | |
| this.x = Math.random() * particleCanvas.width; this.y = Math.random() * particleCanvas.height; | |
| this.vx = (Math.random() - 0.5) * particleConfig.maxVelocity; this.vy = (Math.random() - 0.5) * particleConfig.maxVelocity; | |
| } | |
| update() { | |
| this.x += this.vx; this.y += this.vy; | |
| if (this.x < 0 || this.x > particleCanvas.width) this.vx *= -1; | |
| if (this.y < 0 || this.y > particleCanvas.height) this.vy *= -1; | |
| } | |
| draw() { | |
| particleCtx.beginPath(); particleCtx.arc(this.x, this.y, particleConfig.particleSize, 0, Math.PI * 2); | |
| particleCtx.fillStyle = particleConfig.particleColor; particleCtx.fill(); | |
| } | |
| } | |
| function createParticleArray() { | |
| particleArray = []; | |
| for (let i = 0; i < particleConfig.particleCount; i++) { particleArray.push(new Particle()); } | |
| } | |
| function connectParticles() { | |
| for (let i = 0; i < particleArray.length; i++) { | |
| for (let j = i + 1; j < particleArray.length; j++) { | |
| const distance = Math.sqrt((particleArray[i].x - particleArray[j].x) ** 2 + (particleArray[i].y - particleArray[j].y) ** 2); | |
| if (distance < particleConfig.connectionDistance) { | |
| particleCtx.beginPath(); particleCtx.moveTo(particleArray[i].x, particleArray[i].y); | |
| particleCtx.lineTo(particleArray[j].x, particleArray[j].y); | |
| particleCtx.strokeStyle = `rgba(${eyeColorRgb.r}, ${eyeColorRgb.g}, ${eyeColorRgb.b}, ${1 - distance / particleConfig.connectionDistance})`; | |
| particleCtx.lineWidth = 0.5; particleCtx.stroke(); | |
| } | |
| } | |
| } | |
| } | |
| function animateParticles() { | |
| particleCtx.clearRect(0, 0, particleCanvas.width, particleCanvas.height); | |
| particleArray.forEach(p => { p.update(); p.draw(); }); | |
| connectParticles(); | |
| requestAnimationFrame(animateParticles); | |
| } | |
| resizeParticleCanvas(); animateParticles(); | |
| const face = new RobotFace(document.getElementById('faceCanvas')); | |
| const chat = new ChatInterface(face); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |