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:
co-authored by
Claude Opus 5.5
parent
8fd84239be
commit
9aab5b3f40
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user