|
|
import os |
|
|
import io |
|
|
import json |
|
|
from flask import Flask, request, jsonify, Response |
|
|
from PIL import Image |
|
|
import google.generativeai as genai |
|
|
|
|
|
app = Flask(__name__) |
|
|
|
|
|
API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" |
|
|
|
|
|
html_template = """ |
|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> |
|
|
<title>SYNKRIS AI</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=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
:root { |
|
|
--light-bg: #F9F9F9; |
|
|
--light-chat-bg: #FFFFFF; |
|
|
--light-user-bubble: #007AFF; |
|
|
--light-user-text: #FFFFFF; |
|
|
--light-ai-bubble: #EFEFF4; |
|
|
--light-ai-text: #000000; |
|
|
--light-text-primary: #000000; |
|
|
--light-text-secondary: #6D6D72; |
|
|
--light-border: #E5E5EA; |
|
|
--light-input-bg: #F0F0F0; |
|
|
|
|
|
--dark-bg: #000000; |
|
|
--dark-chat-bg: #1C1C1E; |
|
|
--dark-user-bubble: #0A84FF; |
|
|
--dark-user-text: #FFFFFF; |
|
|
--dark-ai-bubble: #2C2C2E; |
|
|
--dark-ai-text: #FFFFFF; |
|
|
--dark-text-primary: #FFFFFF; |
|
|
--dark-text-secondary: #8E8E93; |
|
|
--dark-border: #3A3A3C; |
|
|
--dark-input-bg: #2C2C2E; |
|
|
|
|
|
--bg-color: var(--light-bg); |
|
|
--chat-bg-color: var(--light-chat-bg); |
|
|
--user-bubble-color: var(--light-user-bubble); |
|
|
--user-text-color: var(--light-user-text); |
|
|
--ai-bubble-color: var(--light-ai-bubble); |
|
|
--ai-text-color: var(--light-ai-text); |
|
|
--text-primary: var(--light-text-primary); |
|
|
--text-secondary: var(--light-text-secondary); |
|
|
--border-color: var(--light-border); |
|
|
--input-bg-color: var(--light-input-bg); |
|
|
} |
|
|
|
|
|
@media (prefers-color-scheme: dark) { |
|
|
:root { |
|
|
--bg-color: var(--dark-bg); |
|
|
--chat-bg-color: var(--dark-chat-bg); |
|
|
--user-bubble-color: var(--dark-user-bubble); |
|
|
--user-text-color: var(--dark-user-text); |
|
|
--ai-bubble-color: var(--dark-ai-bubble); |
|
|
--ai-text-color: var(--dark-ai-text); |
|
|
--text-primary: var(--dark-text-primary); |
|
|
--text-secondary: var(--dark-text-secondary); |
|
|
--border-color: var(--dark-border); |
|
|
--input-bg-color: var(--dark-input-bg); |
|
|
} |
|
|
} |
|
|
|
|
|
html { |
|
|
height: -webkit-fill-available; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; |
|
|
margin: 0; |
|
|
background-color: var(--bg-color); |
|
|
color: var(--text-primary); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
height: 100vh; |
|
|
height: -webkit-fill-available; |
|
|
-webkit-font-smoothing: antialiased; |
|
|
-moz-osx-font-smoothing: grayscale; |
|
|
} |
|
|
|
|
|
.chat-container { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
height: 100%; |
|
|
max-width: 800px; |
|
|
width: 100%; |
|
|
margin: 0 auto; |
|
|
background-color: var(--chat-bg-color); |
|
|
box-shadow: 0 0 20px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
.chat-header { |
|
|
padding: 15px 20px; |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
text-align: center; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.chat-header h1 { |
|
|
font-size: 20px; |
|
|
font-weight: 600; |
|
|
margin: 0; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.chat-header span { |
|
|
font-size: 12px; |
|
|
font-weight: 500; |
|
|
color: var(--text-secondary); |
|
|
} |
|
|
|
|
|
.chat-messages { |
|
|
flex-grow: 1; |
|
|
overflow-y: auto; |
|
|
padding: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.message-bubble { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
max-width: 80%; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
|
|
|
.message-content { |
|
|
padding: 12px 18px; |
|
|
border-radius: 22px; |
|
|
line-height: 1.5; |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.user { |
|
|
align-self: flex-end; |
|
|
align-items: flex-end; |
|
|
} |
|
|
|
|
|
.user .message-content { |
|
|
background-color: var(--user-bubble-color); |
|
|
color: var(--user-text-color); |
|
|
border-bottom-right-radius: 6px; |
|
|
} |
|
|
|
|
|
.ai { |
|
|
align-self: flex-start; |
|
|
align-items: flex-start; |
|
|
} |
|
|
|
|
|
.ai .message-content { |
|
|
background-color: var(--ai-bubble-color); |
|
|
color: var(--ai-text-color); |
|
|
border-bottom-left-radius: 6px; |
|
|
} |
|
|
|
|
|
.error .message-content { |
|
|
background-color: #FF3B301A; |
|
|
color: #FF453A; |
|
|
border: 1px solid #FF3B3080; |
|
|
} |
|
|
|
|
|
.typing-indicator { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 5px; |
|
|
padding: 12px 18px; |
|
|
} |
|
|
|
|
|
.typing-indicator span { |
|
|
height: 8px; |
|
|
width: 8px; |
|
|
background-color: var(--text-secondary); |
|
|
border-radius: 50%; |
|
|
opacity: 0.4; |
|
|
animation: bounce 1.4s infinite ease-in-out both; |
|
|
} |
|
|
|
|
|
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; } |
|
|
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; } |
|
|
|
|
|
@keyframes bounce { |
|
|
0%, 80%, 100% { transform: scale(0); } |
|
|
40% { transform: scale(1.0); } |
|
|
} |
|
|
|
|
|
.chat-input-area { |
|
|
flex-shrink: 0; |
|
|
padding: 15px 20px; |
|
|
padding-bottom: calc(15px + env(safe-area-inset-bottom)); |
|
|
border-top: 1px solid var(--border-color); |
|
|
background-color: var(--chat-bg-color); |
|
|
} |
|
|
|
|
|
.chat-input-form { |
|
|
display: flex; |
|
|
align-items: flex-end; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
#message-input { |
|
|
flex-grow: 1; |
|
|
border: none; |
|
|
padding: 14px 18px; |
|
|
border-radius: 22px; |
|
|
background-color: var(--input-bg-color); |
|
|
color: var(--text-primary); |
|
|
font-family: inherit; |
|
|
font-size: 16px; |
|
|
line-height: 1.4; |
|
|
resize: none; |
|
|
max-height: 150px; |
|
|
outline: none; |
|
|
transition: box-shadow 0.2s; |
|
|
} |
|
|
|
|
|
#message-input:focus { |
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--user-bubble-color) 25%, transparent); |
|
|
} |
|
|
|
|
|
#send-button { |
|
|
border: none; |
|
|
background-color: var(--user-bubble-color); |
|
|
color: white; |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
border-radius: 50%; |
|
|
cursor: pointer; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
flex-shrink: 0; |
|
|
transition: background-color 0.2s, transform 0.1s; |
|
|
} |
|
|
|
|
|
#send-button svg { |
|
|
width: 22px; |
|
|
height: 22px; |
|
|
transition: transform 0.2s ease-in-out; |
|
|
} |
|
|
|
|
|
#send-button:hover { |
|
|
background-color: color-mix(in srgb, var(--user-bubble-color) 90%, #000); |
|
|
} |
|
|
|
|
|
#send-button:active { |
|
|
transform: scale(0.9); |
|
|
} |
|
|
|
|
|
#send-button:disabled { |
|
|
background-color: var(--text-secondary); |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
#send-button:not(:disabled) svg { |
|
|
transform: translateX(1px); |
|
|
} |
|
|
|
|
|
@media (max-width: 800px) { |
|
|
.chat-container { |
|
|
border-radius: 0; |
|
|
box-shadow: none; |
|
|
} |
|
|
} |
|
|
|
|
|
@media (max-width: 600px) { |
|
|
.message-content { |
|
|
font-size: 15px; |
|
|
padding: 10px 15px; |
|
|
} |
|
|
#message-input { |
|
|
font-size: 15px; |
|
|
padding: 12px 16px; |
|
|
} |
|
|
#send-button { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
} |
|
|
#send-button svg { |
|
|
width: 20px; |
|
|
height: 20px; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="chat-container"> |
|
|
<header class="chat-header"> |
|
|
<h1>SYNKRIS AI</h1> |
|
|
<span>by Morshen Group</span> |
|
|
</header> |
|
|
|
|
|
<div class="chat-messages" id="chat-messages"> |
|
|
<div class="message-bubble ai" id="initial-message"> |
|
|
<div class="message-content"> |
|
|
Здравствуйте! Я — SYNKRIS AI 2.0. Чем могу помочь? |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<footer class="chat-input-area"> |
|
|
<form class="chat-input-form" id="chat-form"> |
|
|
<textarea id="message-input" placeholder="Введите ваше сообщение..." rows="1" required></textarea> |
|
|
<button type="submit" id="send-button"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" /></svg> |
|
|
</button> |
|
|
</form> |
|
|
</footer> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const chatForm = document.getElementById('chat-form'); |
|
|
const messageInput = document.getElementById('message-input'); |
|
|
const sendButton = document.getElementById('send-button'); |
|
|
const chatMessages = document.getElementById('chat-messages'); |
|
|
|
|
|
let conversationHistory = []; |
|
|
|
|
|
const addMessageToUI = (sender, message, type = 'text') => { |
|
|
const messageBubble = document.createElement('div'); |
|
|
messageBubble.classList.add('message-bubble', sender); |
|
|
|
|
|
const messageContent = document.createElement('div'); |
|
|
|
|
|
if (type === 'error') { |
|
|
messageBubble.classList.add('error'); |
|
|
messageContent.textContent = `Ошибка: ${message}`; |
|
|
} else if (type === 'loading') { |
|
|
messageContent.classList.add('typing-indicator'); |
|
|
messageContent.innerHTML = '<span></span><span></span><span></span>'; |
|
|
messageBubble.id = 'loading-indicator'; |
|
|
} else { |
|
|
messageContent.classList.add('message-content'); |
|
|
messageContent.textContent = message; |
|
|
} |
|
|
|
|
|
messageBubble.appendChild(messageContent); |
|
|
chatMessages.appendChild(messageBubble); |
|
|
chatMessages.scrollTop = chatMessages.scrollHeight; |
|
|
}; |
|
|
|
|
|
const autoResizeTextarea = () => { |
|
|
messageInput.style.height = 'auto'; |
|
|
let scrollHeight = messageInput.scrollHeight; |
|
|
let maxHeight = parseInt(window.getComputedStyle(messageInput).maxHeight); |
|
|
if (scrollHeight > maxHeight) { |
|
|
messageInput.style.height = maxHeight + 'px'; |
|
|
messageInput.style.overflowY = 'auto'; |
|
|
} else { |
|
|
messageInput.style.height = scrollHeight + 'px'; |
|
|
messageInput.style.overflowY = 'hidden'; |
|
|
} |
|
|
}; |
|
|
|
|
|
messageInput.addEventListener('input', autoResizeTextarea); |
|
|
|
|
|
messageInput.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
chatForm.requestSubmit(); |
|
|
} |
|
|
}); |
|
|
|
|
|
chatForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
const userMessage = messageInput.value.trim(); |
|
|
if (!userMessage) return; |
|
|
|
|
|
addMessageToUI('user', userMessage); |
|
|
conversationHistory.push({ role: 'user', parts: [{ text: userMessage }] }); |
|
|
|
|
|
messageInput.value = ''; |
|
|
autoResizeTextarea(); |
|
|
sendButton.disabled = true; |
|
|
|
|
|
addMessageToUI('ai', '', 'loading'); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/chat', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ history: conversationHistory }) |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
const loadingIndicator = document.getElementById('loading-indicator'); |
|
|
if (loadingIndicator) loadingIndicator.remove(); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(result.error || `Ошибка сервера: ${response.status}`); |
|
|
} |
|
|
|
|
|
const aiMessage = result.text; |
|
|
addMessageToUI('ai', aiMessage); |
|
|
conversationHistory.push({ role: 'model', parts: [{ text: aiMessage }] }); |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Fetch Error:", error); |
|
|
const loadingIndicator = document.getElementById('loading-indicator'); |
|
|
if (loadingIndicator) loadingIndicator.remove(); |
|
|
addMessageToUI('ai', error.message, 'error'); |
|
|
} finally { |
|
|
sendButton.disabled = false; |
|
|
messageInput.focus(); |
|
|
} |
|
|
}); |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
return Response(html_template, mimetype='text/html') |
|
|
|
|
|
@app.route('/chat', methods=['POST']) |
|
|
def handle_chat(): |
|
|
try: |
|
|
data = request.get_json() |
|
|
if not data or 'history' not in data: |
|
|
return jsonify({"error": "Некорректный запрос. Отсутствует история диалога."}), 400 |
|
|
|
|
|
history = data['history'] |
|
|
|
|
|
genai.configure(api_key=API_KEY_INTERNAL) |
|
|
|
|
|
system_instruction = { |
|
|
"role": "user", |
|
|
"parts": [{ |
|
|
"text": "Ты — SYNKRIS AI 2.0, большая языковая модель, разработанная AI лабораторией Synkris, принадлежащей компании Morshen Group. Веди диалог вежливо, будь полезным и отвечай на вопросы пользователя точно и по существу. Всегда отвечай на том же языке, на котором к тебе обратился пользователь." |
|
|
}] |
|
|
} |
|
|
|
|
|
model_response_instruction = { |
|
|
"role": "model", |
|
|
"parts": [{ |
|
|
"text": "Здравствуйте! Я — SYNKRIS AI 2.0. Я готов помочь вам. На каком языке вы предпочитаете общаться?" |
|
|
}] |
|
|
} |
|
|
|
|
|
full_history = [system_instruction, model_response_instruction] + history |
|
|
|
|
|
model = genai.GenerativeModel('gemma-2-27b-it') |
|
|
|
|
|
response = model.generate_content(full_history) |
|
|
|
|
|
if not hasattr(response, 'text') or not response.text: |
|
|
if response.prompt_feedback and response.prompt_feedback.block_reason: |
|
|
reason = response.prompt_feedback.block_reason |
|
|
return jsonify({"error": f"Ответ заблокирован из-за политики безопасности (Причина: {reason})."}), 400 |
|
|
else: |
|
|
return jsonify({"error": "Модель не сгенерировала ответ. Попробуйте переформулировать запрос."}), 500 |
|
|
|
|
|
return jsonify({"text": response.text}) |
|
|
|
|
|
except Exception as e: |
|
|
error_message = str(e) |
|
|
if "API key not valid" in error_message: |
|
|
return jsonify({"error": "Неверный или неактивный ключ API. Проверьте конфигурацию на сервере."}), 500 |
|
|
elif "resource has been exhausted" in error_message: |
|
|
return jsonify({"error": "Квота запросов к API исчерпана. Пожалуйста, попробуйте позже."}), 429 |
|
|
else: |
|
|
return jsonify({"error": f"Произошла внутренняя ошибка сервера: {error_message}"}), 500 |
|
|
|
|
|
if __name__ == '__main__': |
|
|
app.run(host='0.0.0.0', port=7860, debug=False) |