From 33215d33cbb2bd8e7191426a686802be66cf0239 Mon Sep 17 00:00:00 2001 From: artem Date: Wed, 25 Feb 2026 22:48:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=D1=81=D1=8F=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/infra/web/recognizer.py | 1 + server/modules/recognizer/service.py | 37 ++++++++++++------ server/static/scripts.js | 58 ++++++++++++++++++++++++++-- server/static/styles.css | 41 ++++++++++++++++++++ server/templates/base.html | 4 +- server/templates/share.html | 9 ++++- 6 files changed, 131 insertions(+), 19 deletions(-) diff --git a/server/infra/web/recognizer.py b/server/infra/web/recognizer.py index d6bf633..d530264 100644 --- a/server/infra/web/recognizer.py +++ b/server/infra/web/recognizer.py @@ -63,5 +63,6 @@ class BreedsController(Controller): context={ "result": result, "attachment": attachments[0], + "share_id": result_id, }, ) diff --git a/server/modules/recognizer/service.py b/server/modules/recognizer/service.py index 305d541..adb6864 100644 --- a/server/modules/recognizer/service.py +++ b/server/modules/recognizer/service.py @@ -47,6 +47,7 @@ class RecognizerResult(UJsonMixin): images: list description: dict[str, list] | None uploaded_attach_id: str | None + share_id: str | None @dataclass @@ -90,12 +91,13 @@ class RecognizerService: async def create_result( self, attachment: Attachment, user_id: str, device_id: str, beerd_results: list[ResultNameBeerds] - ): + ) -> str: beerd_names = {b.name: b for b in beerd_results} characters = await self._repository_characters.get_characters() + share_id = str(uuid4()) await self._repository.create_result_with_beerds( models.Results( - id=str(uuid4()), + id=share_id, attachment_id=attachment.id, user_id=user_id, device_id=device_id, @@ -107,15 +109,16 @@ class RecognizerService: if ch.name in beerd_names ], ) + return share_id async def get_results(self, result_id: str) -> SharingResult: results = await self._repository.get_results() beerds_store: dict[str, Breed] = {b.id: b for b in await self._repository_characters.get_characters()} images_dogs = await self._repository.images_dogs() + beers: list[SharingBeerds] = [] for r in results: if r.result.id != result_id: continue - beers: list[SharingBeerds] = [] for beerd in r.beerds: name = beerds_store[beerd.beerd_id].name.replace(" ", "_") beers.append( @@ -148,16 +151,20 @@ class RecognizerService: ) description.setdefault(name, []).append(f"/dogs-characteristics/{name.replace(' ', '_')}") results[probabilities] = name - asyncio.create_task( - self.create_result( - attachment, - user_id, - device_id, - [ResultNameBeerds(name=results[key], probability=key*100) for key in results], - ) + + share_id = await self.create_result( + attachment, + user_id, + device_id, + [ResultNameBeerds(name=results[key], probability=key*100) for key in results], ) + return RecognizerResult( - results=results, images=images, description=description, uploaded_attach_id=attachment.id + results=results, + images=images, + description=description, + uploaded_attach_id=attachment.id, + share_id=share_id ) async def predict_cat_image(self, image: bytes) -> RecognizerResult: @@ -176,7 +183,13 @@ class RecognizerService: ) ) results[probabilities] = name - return RecognizerResult(results=results, images=images, description=None, uploaded_attach_id=attachment.id) + return RecognizerResult( + results=results, + images=images, + description=None, + uploaded_attach_id=attachment.id, + share_id=None + ) def _predict(self, image: bytes, model, device="cpu") -> list[Any]: img_size = (224, 224) diff --git a/server/static/scripts.js b/server/static/scripts.js index 701a0e9..cc8ed4f 100644 --- a/server/static/scripts.js +++ b/server/static/scripts.js @@ -16,8 +16,23 @@ async function SavePhoto(self) { if (response.ok) { let json = await response.json(); currentAttachmentID = json.uploaded_attach_id; - - let text = "

Результаты

"; + let shareID = json.share_id; + let style = ""; + if (!shareID) { + style = "style='display:none'" + } + console.log(style, !shareID); + let text = ` +
+

Результаты

+
+ + + +
+
+ `; + let uniqChecker = {}; // Функция для создания HTML галереи @@ -199,7 +214,10 @@ function closeModal() { /* ────────────────────────────────────────────────────── */ (() => { const ratingContainer = document.querySelector('.star-rating'); - const stars = Array.from(ratingContainer.querySelectorAll('.star')); + let stars = [] + if (ratingContainer) { + stars = Array.from(ratingContainer.querySelectorAll('.star')); + } const feedback = document.getElementById('feedback'); let currentRating = 0; // 0 = пока не выбрано @@ -277,4 +295,36 @@ function closeModal() { // По умолчанию не показываем выбранную оценку setRating(0); -})(); \ No newline at end of file +})(); + + +document.addEventListener('click', function (event) { + const shareBtn = event.target.closest('#share-icon'); + + if (shareBtn) { + const link = shareBtn.getAttribute('data-link'); + + if (link) { + navigator.clipboard.writeText(link).then(() => { + showTooltip(shareBtn, 'Скопировано!'); + }); + } + } +}); + +function showTooltip(parent, text) { + // Проверяем, не висит ли уже тултип + if (parent.querySelector('.tooltip')) return; + + const tooltip = document.createElement('span'); + tooltip.className = 'tooltip'; + tooltip.textContent = text; + + parent.appendChild(tooltip); + + // Удаляем тултип через 2 секунды + setTimeout(() => { + tooltip.classList.add('fade-out'); + setTimeout(() => tooltip.remove(), 300); + }, 2000); +} \ No newline at end of file diff --git a/server/static/styles.css b/server/static/styles.css index 75a396b..7badd61 100644 --- a/server/static/styles.css +++ b/server/static/styles.css @@ -369,4 +369,45 @@ input[type="text"]:hover { .star:focus-visible { /* обводка для клавиатурного фокуса */ outline: 2px solid #333; outline-offset: 2px; +} + +.title-block { + display: flex; + justify-content: space-around; + width: 164px; + margin: 0 auto; +} + +#share-icon { + position: relative; /* Нужно для позиционирования тултипа */ + cursor: pointer; + display: inline-flex; + height: 10px; + padding: 20px 0 0 0; +} + + +.tooltip { + position: absolute; + bottom: 120%; /* Над иконкой */ + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: #fff; + margin: 5px 10px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + animation: fadeIn 0.3s ease; + padding: 0 4px 0 4px; +} + +.tooltip.fade-out { + opacity: 0; + transition: opacity 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translate(-50%, 10px); } + to { opacity: 1; transform: translate(-50%, 0); } } \ No newline at end of file diff --git a/server/templates/base.html b/server/templates/base.html index afe88d7..5cdccd3 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -8,7 +8,7 @@ {% block meta %}{% endblock %} {% block title %}{% endblock %} - + + diff --git a/server/templates/share.html b/server/templates/share.html index c7566ec..0730aac 100644 --- a/server/templates/share.html +++ b/server/templates/share.html @@ -9,12 +9,19 @@ {% endblock %} {% block form %}
-
+
Ваше изображение:
+

Результаты

+
+ + + +
+
{% for result in result.beerds %}
{{ result.name }} (вероятность: {{ result.probability }}%)
Описание и фото