fr1: books.ruzzy.dev — читалка с ИИ (PDF/EPUB → главы, рекапы, перевод/объяснение выделенного, вопросы с RAG; Claude Code по подписке без инструментов; вход через Telegram)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gilfanovr
2026-10-13 10:39:51 +03:00
co-authored by Claude Opus 5.5
parent 8fd84239be
commit 9aab5b3f40
19 changed files with 1455 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>{% block title %}Книги{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css?v=1">
</head>
<body>
<header class="top">
<a href="/" class="brand">📚 Книги</a>
<form action="/search" method="get" class="topsearch">
<input name="q" placeholder="Поиск по книгам и рекапам" value="{{ q or '' }}">
</form>
<a href="/logout" class="muted small">выйти</a>
</header>
<main>{% block body %}{% endblock %}</main>
<script src="/static/app.js?v=1"></script>
{% block script %}{% endblock %}
</body>
</html>
+51
View File
@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}{{ b.title }} — Книги{% endblock %}
{% block body %}
<h1>{{ b.title or b.orig }}</h1>
<p class="muted">{% if b.author %}{{ b.author }} · {% endif %}{{ b.fmt|upper }}{% if b.pages %} · {{ b.pages }} с.{% endif %}
· <a href="/book/{{ b.id }}/file" target="_blank">оригинал</a>
· <a href="/book/{{ b.id }}/recaps.md">рекапы (.md)</a>
· <a href="/search?book={{ b.id }}">поиск</a></p>
{% if b.note %}<p class="warn">{{ b.note }}</p>{% endif %}
{% if b.status in ('parsing','indexing') %}<p class="warn" id="busy" data-book="{{ b.id }}">Книга обрабатывается… страница обновится сама.</p>{% endif %}
<section class="card">
<div class="row between">
<h2>Главы</h2>
<span><button id="recap-selected" data-book="{{ b.id }}">Рекап выбранных</button>
<label class="small"><input type="checkbox" id="only-missing" checked> только без рекапа</label></span>
</div>
<ol class="toc">
{% for ch in chapters %}
<li class="lvl{{ ch.level }}" data-idx="{{ ch.idx }}">
<input type="checkbox" class="pick" value="{{ ch.idx }}" {% if ch.has_recap %}data-has="1"{% endif %}>
<a href="/book/{{ b.id }}/ch/{{ ch.idx }}">{{ ch.title }}</a>
<span class="small muted">с. {{ ch.p1 }}{% if ch.p2 != ch.p1 %}–{{ ch.p2 }}{% endif %}</span>
{% if ch.read %}<span title="прочитано">✓</span>{% endif %}
{% if ch.has_recap %}<span class="small" title="есть рекап">📝</span>{% endif %}
<span class="small state"></span>
</li>
{% endfor %}
</ol>
</section>
<section class="card">
<h2>Спросить по книге</h2>
<form class="ask" data-book="{{ b.id }}">
<textarea name="q" rows="2" placeholder="Например: чем отличается X от Y по мнению автора?"></textarea>
<div class="row"><button>Спросить</button>
<label class="small"><input type="checkbox" name="web"> проверить по интернету</label></div>
</form>
<div class="answer md"></div>
{% if qa %}<details><summary class="muted">Прошлые вопросы ({{ qa|length }})</summary>
{% for x in qa %}<div class="qa"><b>{{ x.q }}</b><div class="md">{{ x.a }}</div></div>{% endfor %}</details>{% endif %}
</section>
<details class="card"><summary>Название, автор, удаление</summary>
<form action="/book/{{ b.id }}/title" method="post" class="row">
<input name="title" value="{{ b.title or '' }}" placeholder="Название">
<input name="author" value="{{ b.author or '' }}" placeholder="Автор"><button>Сохранить</button></form>
<form action="/book/{{ b.id }}/delete" method="post" class="row">
<input name="confirm" placeholder="впиши «да», чтобы удалить книгу со всеми рекапами"><button class="danger">Удалить</button></form>
</details>
{% endblock %}
+48
View File
@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}{{ ch.title }} — {{ b.title }}{% endblock %}
{% block body %}
<nav class="row between small">
<a href="/book/{{ b.id }}">← {{ b.title }}</a>
<span>{% if ch.idx > 1 %}<a href="/book/{{ b.id }}/ch/{{ ch.idx - 1 }}">‹ пред.</a>{% endif %}
· {{ ch.idx }}/{{ n }} ·
{% if ch.idx < n %}<a href="/book/{{ b.id }}/ch/{{ ch.idx + 1 }}">след. ›</a>{% endif %}</span>
</nav>
<h1>{{ ch.title }}</h1>
<p class="small muted">с. {{ ch.p1 }}–{{ ch.p2 }} ·
<label><input type="checkbox" id="read" data-book="{{ b.id }}" data-ch="{{ ch.idx }}" {% if ch.read %}checked{% endif %}> прочитано</label>
· <a href="#recap">рекап</a> · <a href="/book/{{ b.id }}/file#page={{ ch.p1 }}" target="_blank">в оригинале</a></p>
<article id="text" class="reader" data-book="{{ b.id }}" data-ch="{{ ch.idx }}">{{ body|safe }}</article>
<section class="card" id="recap">
<div class="row between"><h2>Рекап</h2>
<button id="gen-recap" data-book="{{ b.id }}" data-ch="{{ ch.idx }}">{% if recap and recap.md %}Сделать заново{% else %}Сделать рекап{% endif %}</button></div>
<div id="recap-view" class="md">{{ recap.md if recap else '' }}</div>
<details><summary class="small muted">Править</summary>
<textarea id="recap-edit" rows="12">{{ recap.md if recap else '' }}</textarea>
<button id="save-recap" data-book="{{ b.id }}" data-ch="{{ ch.idx }}">Сохранить</button>
{% if recap and recap.ai_md and recap.ai_md != recap.md %}<p class="small muted">Есть более свежий вариант ИИ — <a href="#" id="use-ai">подставить</a></p>
<textarea id="ai-md" hidden>{{ recap.ai_md }}</textarea>{% endif %}
</details>
</section>
<section class="card">
<h2>Вопрос по главе</h2>
<form class="ask" data-book="{{ b.id }}" data-ch="{{ ch.idx }}">
<textarea name="q" rows="2" placeholder="Спроси о прочитанном"></textarea>
<div class="row"><button>Спросить</button>
<label class="small"><input type="checkbox" name="web"> проверить по интернету</label></div>
</form>
<div class="answer md"></div>
</section>
<div id="selbar" hidden>
<button data-act="translate">Перевести</button>
<button data-act="explain">Объяснить</button>
<button data-act="ask">Спросить…</button>
</div>
<div id="panel" hidden><div class="row between"><b id="panel-title"></b><button id="panel-close">✕</button></div>
<div id="panel-ask" hidden><textarea rows="2" placeholder="Вопрос о выделенном"></textarea>
<div class="row"><button id="panel-ask-go">Спросить</button><label class="small"><input type="checkbox" id="panel-web"> проверить по интернету</label></div></div>
<div id="panel-body" class="md"></div></div>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block body %}
<section class="card">
<form action="/upload" method="post" enctype="multipart/form-data" class="row" id="upload">
<input type="file" name="file" accept=".pdf,.epub,application/pdf,application/epub+zip" required>
<button>Загрузить книгу</button>
</form>
<p class="muted small">PDF или EPUB до 200 МБ. Оглавление разберётся само; без оглавления — по 20 страниц.</p>
</section>
{% if not books %}<p class="muted">Книг пока нет.</p>{% endif %}
<ul class="books">
{% for b in books %}
<li>
<a href="/book/{{ b.id }}{% if b.last_ch %}/ch/{{ b.last_ch }}{% endif %}" class="btitle">{{ b.title or b.orig or 'Без названия' }}</a>
{% if b.author %}<span class="muted"> — {{ b.author }}</span>{% endif %}
<div class="small muted">
{{ b.fmt|upper }}{% if b.pages %} · {{ b.pages }} с.{% endif %}{% if b.lang %} · {{ b.lang }}{% endif %}
{% if b.status != 'ready' %} · <b>{{ {'parsing':'разбираю…','indexing':'индексирую…','error':'ошибка'}[b.status] or b.status }}</b>{% endif %}
{% if b.n %} · прочитано {{ b.nread }}/{{ b.n }} · рекапов {{ b.nrecap }}{% endif %}
· <a href="/book/{{ b.id }}">оглавление</a>
</div>
</li>
{% endfor %}
</ul>
{% endblock %}
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"><title>Вход — Книги</title>
<link rel="stylesheet" href="/static/style.css?v=1"></head>
<body class="center">
<main class="login">
<h1>📚 Книги</h1>
<p class="muted">Личная читалка. Вход — через Telegram.</p>
{% if err %}<p class="err">{{ err }}</p>{% endif %}
{% if bot %}
<script async src="https://telegram.org/js/telegram-widget.js?22" data-telegram-login="{{ bot }}" data-size="large"
data-auth-url="/auth/telegram" data-request-access="write"></script>
{% else %}<p class="err">Вход не настроен (нет TG_BOT_NAME).</p>{% endif %}
</main>
</body></html>
+14
View File
@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block body %}
<form action="/search" method="get" class="row">
<input name="q" value="{{ q }}" placeholder="Что найти" autofocus>
{% if book %}<input type="hidden" name="book" value="{{ book }}">{% endif %}<button>Найти</button></form>
{% if q and not hits %}<p class="muted">Ничего не нашлось.</p>{% endif %}
<ul class="hits">
{% for h in hits %}
<li><a href="/book/{{ h.book_id }}/ch/{{ h.ch_idx }}#p{{ h.page }}"><b>{{ h.book_title }}</b> — гл. {{ h.ch_idx }} «{{ h.ch_title }}», с. {{ h.page }}</a>
{% if h.kind == 'recap' %}<span class="small">📝 рекап</span>{% endif %}
<p class="small">{{ h.text[:400] }}{% if h.text|length > 400 %}…{% endif %}</p></li>
{% endfor %}
</ul>
{% endblock %}