Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# app.py (ссылки и кнопка «Показать ещё» работают)
import json
from flask import Flask, render_template, request, jsonify, session
import spacy
from utils.wikidata_helpers import find_and_describe
Expand All @@ -11,6 +10,47 @@
nlp_ru = spacy.load("ru_core_news_sm")
nlp_en = spacy.load("en_core_web_sm")

ASK_PROMPTS = {
"ru": "Пожалуйста, введите вопрос.",
"en": "Please enter a question.",
"zh": "请输入问题。",
}

EMPTY_RESPONSES = {
"ru": "Ничего не найдено. Попробуй переформулировать.",
"en": "Nothing was found. Try rephrasing your question.",
"zh": "没有找到结果,请尝试换一种问法。",
}

ERROR_RESPONSES = {
"timeout": {
"ru": "Wikidata долго не отвечает. Попробуй ещё раз чуть позже.",
"en": "Wikidata is taking too long to respond. Try again in a moment.",
"zh": "Wikidata 响应超时,请稍后再试。",
},
"request": {
"ru": "Не удалось подключиться к Wikidata. Проверь соединение и повтори попытку.",
"en": "Unable to reach Wikidata right now. Please check your connection and retry.",
"zh": "目前无法连接到 Wikidata,请检查网络后重试。",
},
"decode": {
"ru": "Wikidata прислала неожиданный ответ. Попробуй позже ещё раз.",
"en": "Wikidata returned an unexpected response. Please try again later.",
"zh": "Wikidata 返回了异常数据,请稍后再试。",
},
}

GENERIC_ERROR = {
"ru": "Не получилось получить данные из Wikidata. Попробуй ещё раз позже.",
"en": "Could not retrieve data from Wikidata. Please try again later.",
"zh": "未能从 Wikidata 获取数据,请稍后再试。",
}


def localize(table, lang, fallback):
return table.get(lang) if isinstance(table, dict) and table.get(lang) else fallback


@app.route("/")
def index():
lang = session.get("lang") or request.accept_languages.best_match(LANGUAGES.keys()) or "ru"
Expand All @@ -30,22 +70,39 @@ def ask():
lang = session.get("lang", "ru")

if not question:
return jsonify({"answer": "Пожалуйста, введите вопрос."})
return jsonify({
"answer": localize(ASK_PROMPTS, lang, ASK_PROMPTS["en"]),
"status": "empty",
"more": False,
})

nlp = nlp_ru if lang == "ru" else nlp_en
doc = nlp(question)
query = next((ent.text for ent in doc.ents if ent.label_ in {"PER", "ORG", "LOC"}), question)
found = find_and_describe(query, lang)
result = find_and_describe(query, lang)

if result["error"]:
err_table = ERROR_RESPONSES.get(result["error"], GENERIC_ERROR)
return jsonify({
"answer": localize(err_table, lang, GENERIC_ERROR["en"]),
"status": "error",
"more": False,
})

if not found:
return jsonify({"answer": "Ничего не найдено. Попробуй переформулировать."})
items = result["items"] or []
if not items:
return jsonify({
"answer": localize(EMPTY_RESPONSES, lang, EMPTY_RESPONSES["en"]),
"status": "empty",
"more": False,
})

if len(found) == 1:
text = found[0]["text"]
if len(items) == 1:
text = items[0]["text"]
more = False
else:
show = found[:3]
rest = found[3:]
show = items[:3]
rest = items[3:]
lines = ["Я нашёл несколько объектов:\n"]
for f in show:
lines.append(f"• **{f['label']}** — {f['descr']}\n[🔗 Источник]({f['url']})")
Expand All @@ -54,19 +111,36 @@ def ask():
text = "\n".join(lines)
more = bool(rest)

return jsonify({"answer": text, "more": more, "question": question})
return jsonify({"answer": text, "more": more, "question": question, "status": "ok"})

@app.route("/more", methods=["POST"])
def more():
data = request.get_json() or {}
question = data.get("question", "").strip()
lang = session.get("lang", "ru")
found = find_and_describe(question, lang)
result = find_and_describe(question, lang)

if result["error"]:
err_table = ERROR_RESPONSES.get(result["error"], GENERIC_ERROR)
return jsonify({
"answer": localize(err_table, lang, GENERIC_ERROR["en"]),
"status": "error",
"more": False,
})

items = result["items"] or []
if not items:
return jsonify({
"answer": localize(EMPTY_RESPONSES, lang, EMPTY_RESPONSES["en"]),
"status": "empty",
"more": False,
})

lines = []
for f in found:
for f in items:
lines.append(f"• **{f['label']}** — {f['descr']}\n{f['text']}")
full_text = "\n\n".join(lines)
return jsonify({"answer": full_text, "more": False})
return jsonify({"answer": full_text, "more": False, "status": "ok"})

if __name__ == "__main__":
app.run(debug=True, host="127.0.0.1", port=5000)
Expand Down
29 changes: 29 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ h1 {
border-top-left-radius: 4px;
}

.message-error {
border-left: 3px solid #ff6b6b;
}

.message-info {
border-left: 3px solid #4dabf7;
}

.message-time {
font-size: 0.7rem;
color: #777;
Expand Down Expand Up @@ -143,6 +151,27 @@ h1 {
cursor: not-allowed;
}

.show-more-button {
align-self: flex-start;
margin-left: 20px;
padding: 6px 14px;
border-radius: 12px;
border: 1px solid #fbec5d;
background: transparent;
color: #fbec5d;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}

.show-more-button:hover {
background: rgba(251, 236, 93, 0.15);
}

.show-more-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}

.examples {
display: flex;
gap: 10px;
Expand Down
197 changes: 125 additions & 72 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,82 +43,135 @@ <h1>🧠 Wikidata AI</h1>
</div>
</div>

<script>
const langSel = document.getElementById("langSel");
const messagesArea = document.getElementById("messagesArea");
const messageInput = document.getElementById("messageInput");
const sendButton = document.getElementById("sendButton");
<script>
const langSel = document.getElementById("langSel");
const messagesArea = document.getElementById("messagesArea");
const messageInput = document.getElementById("messageInput");
const sendButton = document.getElementById("sendButton");

langSel.onchange = () => fetch(`/set_lang/${langSel.value}`).then(() => location.reload());
langSel.onchange = () => fetch(`/set_lang/${langSel.value}`).then(() => location.reload());

let lastQuestion = "";
let lastQuestion = "";

function addMessage(text, isUser=false, showMore=false){
const div = document.createElement("div");
div.className = `message ${isUser?'user-message':'bot-message'}`;
const time = new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
// превращаем Markdown-ссылки в HTML <a target="_blank">
const html = text
.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
.replace(/\[🔗 Источник\]\((https?:\/\/[^)]+)\)/g,
'<a href="$1" target="_blank" rel="noopener">🔗 Источник</a>');
div.innerHTML = `${html}<div class="message-time">${time}</div>`;
messagesArea.appendChild(div);
if(showMore){
const moreBtn = document.createElement("button");
moreBtn.textContent = "Показать ещё";
moreBtn.onclick = () => loadMore();
messagesArea.appendChild(moreBtn);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
}
function addMessage(text, isUser=false, showMore=false, status="ok"){
const div = document.createElement("div");
div.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
if(status === "error"){
div.classList.add("message-error");
}else if(status === "empty"){
div.classList.add("message-info");
}
const time = new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
const html = text
.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
.replace(/\[🔗 Источник\]\((https?:\/\/[^)]+)\)/g,
'<a href="$1" target="_blank" rel="noopener">🔗 Источник</a>')
.replace(/\n/g,'<br>');
div.innerHTML = `${html}<div class="message-time">${time}</div>`;
messagesArea.appendChild(div);
if(showMore){
const moreBtn = document.createElement("button");
moreBtn.textContent = "Показать ещё";
moreBtn.className = "show-more-button";
moreBtn.dataset.question = lastQuestion;
moreBtn.onclick = () => loadMore(moreBtn);
messagesArea.appendChild(moreBtn);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
return div;
}

function useExample(text){
messageInput.value = text;
messageInput.focus();
}
function showThinking(){
const div = document.createElement("div");
div.className = "message bot-message";
div.innerHTML = "<div class=\"thinking\">Думаю…</div>";
messagesArea.appendChild(div);
messagesArea.scrollTop = messagesArea.scrollHeight;
return div;
}

async function sendMessage(){
const msg = messageInput.value.trim();
if(!msg) return;
addMessage(msg, true);
messageInput.value = '';
sendButton.disabled = true;
lastQuestion = msg;
addMessage("<div class=thinking>Думаю…</div>");
try{
const r = await fetch("/ask",{method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({question:msg})});
const j = await r.json();
messagesArea.removeChild(messagesArea.lastChild);
addMessage(j.answer, false, j.more);
}catch(e){
messagesArea.removeChild(messagesArea.lastChild);
addMessage("Ошибка связи с сервером.");
}
sendButton.disabled = false;
}
function useExample(text){
messageInput.value = text;
messageInput.focus();
}

async function loadMore(){
const btn = document.querySelector("button"); // кнопка «Показать ещё»
if(btn) btn.disabled = true;
addMessage("<div class=thinking>Думаю…</div>");
try{
const r = await fetch("/more",{method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({question:lastQuestion})});
const j = await r.json();
messagesArea.removeChild(messagesArea.lastChild);
addMessage(j.answer);
}catch(e){
messagesArea.removeChild(messagesArea.lastChild);
addMessage("Ошибка связи с сервером.");
}
}
async function sendMessage(){
const msg = messageInput.value.trim();
if(!msg) return;
addMessage(msg, true);
messageInput.value = '';
sendButton.disabled = true;
lastQuestion = msg;
const thinking = showThinking();
try{
const r = await fetch("/ask",{method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({question:msg})});
if(!r.ok) throw new Error(`HTTP ${r.status}`);
const j = await r.json();
if(thinking.parentNode){
thinking.remove();
}
if(j.status === "error"){
addMessage(j.answer, false, false, "error");
}else if(j.status === "empty"){
addMessage(j.answer, false, false, "empty");
}else{
addMessage(j.answer, false, j.more, "ok");
}
}catch(e){
if(thinking.parentNode){
thinking.remove();
}
addMessage("Ошибка связи с сервером.", false, false, "error");
}finally{
sendButton.disabled = false;
}
}

async function loadMore(btn){
const button = btn instanceof HTMLElement ? btn : null;
const question = button?.dataset?.question || lastQuestion;
if(!question){
return;
}
if(button){
button.disabled = true;
}
const thinking = showThinking();
try{
const r = await fetch("/more",{method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({question})});
if(!r.ok) throw new Error(`HTTP ${r.status}`);
const j = await r.json();
if(thinking.parentNode){
thinking.remove();
}
if(button && button.parentNode){
button.remove();
}
if(j.status === "error"){
addMessage(j.answer, false, false, "error");
}else if(j.status === "empty"){
addMessage(j.answer, false, false, "empty");
}else{
addMessage(j.answer);
}
}catch(e){
if(thinking.parentNode){
thinking.remove();
}
if(button){
button.disabled = false;
}
addMessage("Ошибка связи с сервером.", false, false, "error");
}
}

sendButton.onclick = sendMessage;
messageInput.onkeypress = e => {if(e.key==="Enter"){e.preventDefault(); sendMessage();}};
</script>
</body>
</html>

sendButton.onclick = sendMessage;
messageInput.onkeypress = e => {if(e.key==="Enter") sendMessage();};
</script>
</body>
</html>
Loading