implement document view
Run Tests / Run Tests (push) Successful in 38s Details

This commit is contained in:
James Ravenscroft 2024-12-10 16:05:24 +00:00
parent 7b15955bf6
commit 9222739df1
10 changed files with 329 additions and 24 deletions

View File

@ -43,7 +43,8 @@ INSTALLED_APPS = [
"django.contrib.messages",
"django.contrib.staticfiles",
"webui",
"markdown_deux"
"markdown_deux",
"markdownify.apps.MarkdownifyConfig",
]
MIDDLEWARE = [
@ -74,6 +75,15 @@ TEMPLATES = [
},
]
MARKDOWNIFY = {
"default": {
"MARKDOWN_EXTENSIONS": [
"markdown.extensions.fenced_code", # dotted path
"fenced_code", # also works
]
}
}
WSGI_APPLICATION = "penparse.wsgi.application"

View File

@ -11,13 +11,13 @@ from django.conf import settings
from .models import ImageMemo, MemoStatus
from datetime import datetime
TRANSCRIBE_PROMPT = """Transcribe the hand written notes in the attached image and present them as markdown inside a fence like so
TRANSCRIBE_PROMPT = """Transcribe the hand written notes in the attached image and present them as markdown.
```markdown
<Content>
```
Do not use a fence, simply respond using markdown.
If any words or letters are unclear, denote them with a '?<word>?'. For example if you were not sure whether a word is blow or blew you would transcribe it as '?blow?'
If any words or letters are unclear, denote them with a '?<word>?'.
For example if you were not sure whether a word is blow or blew you would transcribe it as '?blow?'
"""

View File

@ -23,20 +23,26 @@
alt="{{ document.title }} thumbnail"
class="w-full h-48 object-cover mb-4 rounded"
/>
<div class="flex justify-between items-center mb-2">
<h3 class="text-xl font-semibold">{{ document.title }}</h3>
<table class="w-full text-sm">
<tr>
<td class="font-medium pr-4">Status:</td>
<td>
<span
class="px-2 py-1 text-xs font-semibold rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}"
class="px-3 py-1 text-sm font-medium rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}"
>
{{ document.status|title }}
</span>
</div>
<p class="text-gray-600 mb-4">
Created: {{ document.created_at }}
</p>
<p class="text-gray-600 mb-4">
Last Updated: {{ document.updated_at }}
</p>
</td>
</tr>
<tr>
<td class="font-medium pr-4">Created:</td>
<td class="text-gray-600">{{ document.created_at|date:"d/m/Y H:i" }}</td>
</tr>
<tr>
<td class="font-medium pr-4">Updated:</td>
<td class="text-gray-600">{{ document.updated_at|date:"d/m/Y H:i" }}</td>
</tr>
</table>
{% if document.content %}
<div class="text-gray-700 mb-4">
<h4 class="font-semibold mb-2">Content Preview:</h4>

View File

@ -0,0 +1,181 @@
{% extends "main.html" %} {% load markdown_deux_tags %} {% load markdownify %}
{% block content %}
<section class="max-w-6xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-gray-900 mb-6">Document View</h1>
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<div class="p-6">
<div class="flex flex-col lg:flex-row gap-8">
<div class="w-full lg:w-1/2">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">
{{ document.title }}
</h2>
<div class="mb-4">
<table class="w-full text-sm">
<tr>
<td class="font-medium pr-4">Status:</td>
<td>
<span
class="px-3 py-1 text-sm font-medium rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}"
>
{{ document.status|title }}
</span>
</td>
</tr>
<tr>
<td class="font-medium pr-4">Created:</td>
<td class="text-gray-600">{{ document.created_at|date:"d/m/Y H:i" }}</td>
</tr>
<tr>
<td class="font-medium pr-4">Updated:</td>
<td class="text-gray-600">{{ document.updated_at|date:"d/m/Y H:i" }}</td>
</tr>
</table>
</div>
<div class="prose max-w-full">
<div class="flex justify-between items-center mb-3">
<h3 class="text-xl font-semibold text-gray-800">Content:</h3>
<button
id="copyButton"
class="bg-gray-200 text-gray-700 px-3 py-1 rounded-md hover:bg-gray-300 transition duration-300 flex items-center text-sm"
>
<svg
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
></path>
</svg>
Copy to Clipboard
</button>
</div>
<div id="markdown-rendered" class="bg-gray-50 p-4 rounded-md">
{{ document.content|markdown }}
</div>
<div id="markdown-content" class="hidden">{{ document.content}}</div>
</div>
<div class="mt-8 flex space-x-4">
<a
href="{% url 'download_document' document.id %}"
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-300 flex items-center"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
></path>
</svg>
Export
</a>
<form
action="{% url 'delete_document' document.id %}"
method="post"
onsubmit="return confirm('Are you sure you want to delete this document?');"
>
{% csrf_token %}
<button
type="submit"
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 transition duration-300 flex items-center"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
Delete
</button>
</form>
</div>
</div>
<div class="w-full lg:w-1/2">
<div class="bg-gray-100 rounded-lg overflow-hidden">
<img
src="{% url 'document_image' pk=document.id %}"
alt="{{ document.title }}"
class="w-full h-auto object-contain"
/>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-4">
<a
href="{% url 'dashboard' %}"
class="text-blue-600 hover:text-blue-800 flex items-center"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
</svg>
Back to Dashboard
</a>
</section>
{% endblock %} {% block extra_js %}
<script>
document.addEventListener("DOMContentLoaded", (event) => {
// Syntax highlighting
document.querySelectorAll("#markdown-content pre code").forEach((block) => {
hljs.highlightElement(block);
});
// Copy to clipboard functionality
const copyButton = document.getElementById("copyButton");
const markdownContent = document.getElementById("markdown-content");
copyButton.addEventListener("click", () => {
const textToCopy = markdownContent.innerText;
navigator.clipboard
.writeText(textToCopy)
.then(() => {
copyButton.textContent = "Copied!";
copyButton.classList.remove("bg-gray-200", "text-gray-700");
copyButton.classList.add("bg-green-500", "text-white");
setTimeout(() => {
copyButton.innerHTML =
'<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>Copy to Clipboard';
copyButton.classList.remove("bg-green-500", "text-white");
copyButton.classList.add("bg-gray-200", "text-gray-700");
}, 2000);
})
.catch((err) => {
console.error("Failed to copy text: ", err);
});
});
});
</script>
{% endblock %}

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PenParse - Intelligent OCR for Handwritten Notes</title>
<script src="https://cdn.tailwindcss.com"></script>
{% block extra_js %}{% endblock %}
</head>
<body class="bg-gray-100 font-sans">
<header class="bg-white shadow-sm">{% include "partial/nav.html" %}</header>

View File

@ -13,6 +13,11 @@ urlpatterns = [
views.document_thumbnail,
name="document_thumbnail",
),
path(
"documents/<str:pk>/image",
views.document_image,
name="document_image",
),
path(
"documents/<str:pk>/download", views.download_document, name="download_document"
),

View File

@ -3,7 +3,7 @@ import os
from django.contrib import messages
from django.shortcuts import redirect, render
from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from ..models import ImageMemo
@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
logger = logging.getLogger(__name__)
from .thumbnail import document_thumbnail
from .thumbnail import document_thumbnail, document_image
from .register import register
from .upload import upload_document
from .delete import delete_document
@ -27,6 +27,7 @@ def index(request):
__all__ = [
"index",
"document_thumbnail",
"document_image",
"register",
"dashboard",
"settings",
@ -54,8 +55,19 @@ def settings(request):
@login_required
def view_document(request):
return render(request, "document.html")
def view_document(request: HttpRequest, pk: str):
## check that the document exists and belongs to the user
# find document with given ID (pk path param) and current user id
document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first()
if not document:
logger.debug(f"No memo found for user={request.user.id} and memo_id={pk}")
return HttpResponse(content="Document not found", status=404)
return render(request, "document.html", context={"document": document})
@login_required

View File

@ -13,6 +13,29 @@ from django.contrib.auth.decorators import login_required
from ..models import ImageMemo
@login_required
def document_image(request, pk):
"""Given a document uuid, look it up, ensure that it belongs to the current user and respond with an image"""
# find document with given ID (pk path param) and current user id
document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first()
if not document:
logger.debug(f"No memo found for user={request.user.id} and memo_id={pk}")
return HttpResponse(content="Document not found", status=404)
# look up the file on disk
if not default_storage.exists(document.image.name):
logger.warning(
f"The file associated with memo {document.id} does not exist"
)
return HttpResponse(content="Document not found", status=404)
# Return the thumbnail as an HTTP response
with default_storage.open(document.image.name,'rb') as f:
return HttpResponse(f.read(), content_type="image/jpeg")
@login_required
def document_thumbnail(request: HttpRequest, pk: str):
"""Given a document uuid, look it up, ensure that it belongs to the current user and respond with a thumbnail"""

View File

@ -7,6 +7,7 @@ requires-python = ">=3.9"
dependencies = [
"celery>=5.4.0",
"django-markdown-deux>=1.0.6",
"django-markdownify>=0.9.5",
"django>=4.2.16",
"litellm>=1.54.1",
"loguru>=0.7.3",

66
uv.lock
View File

@ -190,6 +190,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 },
]
[[package]]
name = "bleach"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 },
]
[package.optional-dependencies]
css = [
{ name = "tinycss2" },
]
[[package]]
name = "celery"
version = "5.4.0"
@ -393,6 +410,20 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/26/af/3ed785b661e4545709ba1618926bb33bd585d1fd2faa42a548756743e874/django-markdown-deux-1.0.6.zip", hash = "sha256:1f7b4da6b4dd1a9a84e3da90887d356f8afdd9a1e7d6468c081b8ac50a7980b1", size = 18157 }
[[package]]
name = "django-markdownify"
version = "0.9.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bleach", extra = ["css"] },
{ name = "django" },
{ name = "markdown" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/33/3abb966e2b238af4c9a5d3ee38a7aa7e51b644b4b20bf8533b6fd1c1bf96/django_markdownify-0.9.5.tar.gz", hash = "sha256:34c34eba4a797282a5c5bd97b13cec84d6a4c0673ad47ce1c1d000d74dd8d4ab", size = 7939 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/35/c7a4bd957b279a8e7c808116bed399b73874ed3da78689993ee76f30d9f6/django_markdownify-0.9.5-py3-none-any.whl", hash = "sha256:2c4ae44e386c209453caf5e9ea1b74f64535985d338ad2d5ad5e7089cc94be86", size = 10342 },
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
@ -751,6 +782,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
]
[[package]]
name = "markdown"
version = "3.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
]
[[package]]
name = "markdown2"
version = "2.5.1"
@ -951,6 +994,7 @@ dependencies = [
{ name = "celery" },
{ name = "django" },
{ name = "django-markdown-deux" },
{ name = "django-markdownify" },
{ name = "litellm" },
{ name = "loguru" },
{ name = "pillow" },
@ -966,6 +1010,7 @@ requires-dist = [
{ name = "celery", specifier = ">=5.4.0" },
{ name = "django", specifier = ">=4.2.16" },
{ name = "django-markdown-deux", specifier = ">=1.0.6" },
{ name = "django-markdownify", specifier = ">=0.9.5" },
{ name = "litellm", specifier = ">=1.54.1" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "pillow", specifier = ">=11.0.0" },
@ -1686,6 +1731,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/3b/7c8812952ca55e1bab08afc1dda3c5991804c71b550b9402e82a082ab795/tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02", size = 884803 },
]
[[package]]
name = "tinycss2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 },
]
[[package]]
name = "tokenizers"
version = "0.21.0"
@ -1807,6 +1864,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 },
]
[[package]]
name = "win32-setctime"
version = "1.2.0"