Created
March 12, 2025 18:56
-
-
Save hamelsmu/ea2b587bc85f8e46cc4a1fcbfe5b4254 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fasthtml.common import * | |
import csv | |
import io | |
from datetime import datetime | |
# Add DaisyUI and TailwindCSS via CDN | |
tw_styles = Script(src="https://cdn.tailwindcss.com") | |
# Configure application with DaisyFT resources | |
app, rt, db, DataItem = fast_app( | |
'annotations.db', | |
pico=False, | |
surreal=False, | |
live=True, | |
hdrs=(tw_styles,), | |
htmlkw=dict(lang="en", dir="ltr", data_theme="light"), | |
bodykw=dict(cls="min-h-screen bg-gray-50"), | |
id=int, | |
input=str, | |
output=str, | |
notes=str, | |
timestamp=str, | |
pk='id', | |
render=lambda item: render(item) | |
) | |
def Arrow(direction, idx, disabled=False): | |
icon = "←" if direction == "prev" else "→" | |
print(f"Arrow clicked: direction={direction}, idx={idx}") # Debug print | |
classes = """ | |
px-4 py-2 text-sm font-medium | |
border border-gray-300 rounded-md shadow-sm | |
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 | |
disabled:opacity-50 disabled:cursor-not-allowed | |
bg-white hover:bg-gray-50 | |
""" | |
if disabled: | |
classes += " opacity-50 cursor-not-allowed" | |
return Button(icon, disabled="disabled", cls=classes) | |
return Button( | |
icon, | |
hx_get=f"/annotate/{idx}", | |
hx_target="body", | |
hx_swap="innerHTML", | |
hx_push_url="true", | |
cls=classes | |
) | |
def render(Item): | |
print(f"Rendering item: id={Item.id}") # Debug print | |
print(f"Item notes: {Item.notes}") # Debug notes value | |
# Navigation controls | |
nav = Div(cls="flex justify-between items-center mb-6")( | |
H1(f"Entry {Item.id} out of {total_items_length}", cls="text-2xl font-bold text-gray-900"), | |
Div(cls="flex space-x-2")( | |
Arrow("prev", Item.id - 1, Item.id <= 0), | |
Arrow("next", Item.id + 1, Item.id >= total_items_length) | |
) | |
) | |
# Timestamp | |
timestamp = Div(cls="mb-8 text-sm text-gray-500")( | |
"Last updated: ", Time(Item.timestamp) | |
) | |
# Content cards | |
content = Div(cls="space-y-6")( | |
# Input card | |
Div(cls="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 p-6")( | |
H2("Input", cls="text-lg font-semibold text-gray-900 mb-4"), | |
Pre(Item.input, cls="text-sm text-gray-600 whitespace-pre-wrap") | |
), | |
# Output card | |
Div(cls="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 p-6")( | |
H2("Output", cls="text-lg font-semibold text-gray-900 mb-4"), | |
Pre(Item.output, cls="text-sm text-gray-600 whitespace-pre-wrap") | |
), | |
# Notes form | |
Form( | |
cls="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 p-6", | |
hx_post=f"/annotate/{Item.id}", | |
hx_target="body", | |
hx_swap="innerHTML", | |
hx_push_url="true" | |
)( | |
H2("Notes", cls="text-lg font-semibold text-gray-900 mb-4"), | |
Textarea( | |
Item.notes or "", # Ensure we handle None values | |
name="notes", | |
placeholder="Add your notes here...", | |
cls="w-full h-32 px-3 py-2 text-sm text-gray-900 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500", | |
rows="4" | |
), | |
Button( | |
"Save Notes", | |
type="submit", | |
cls="mt-4 w-full bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" | |
) | |
) | |
) | |
return Div(nav, timestamp, content) | |
def upload_view(): | |
"""Render the upload page view""" | |
return Main( | |
Div(cls="min-h-screen bg-gray-50 py-12")( | |
Div(cls="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8")( | |
H1("CSV Data Annotation Tool", cls="text-3xl font-bold text-gray-900 mb-8"), | |
Div(cls="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 p-6")( | |
Form(hx_post="/upload", hx_encoding="multipart/form-data", cls="space-y-4")( | |
Label("Upload your CSV file", cls="block text-sm font-medium text-gray-700"), | |
Input( | |
type="file", | |
name="file", | |
accept=".csv", | |
cls="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" | |
), | |
Button( | |
"Upload CSV", | |
type="submit", | |
cls="w-full bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" | |
) | |
) | |
) | |
) | |
) | |
) | |
def annotate_view(current_item): | |
"""Render the annotation view""" | |
return Main( | |
Div(cls="min-h-screen bg-gray-50 py-12")( | |
Div(cls="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8")( | |
Div(cls="flex justify-between items-center mb-8")( | |
H1("CSV Data Annotation Tool", cls="text-3xl font-bold text-gray-900"), | |
A("Upload More", href="/", cls="text-indigo-600 hover:text-indigo-900") | |
), | |
render(current_item) | |
) | |
) | |
) | |
@rt("/") | |
def home(): | |
"""Home route - shows upload view if no data, otherwise redirects to annotation""" | |
items = db() | |
if not items: | |
return upload_view() | |
return Redirect("/annotate/0") | |
@rt("/upload") | |
def post_upload(file: UploadFile): | |
"""Handle file upload""" | |
content = file.file.read().decode('utf-8') | |
csv_data = csv.DictReader(io.StringIO(content)) | |
for row in csv_data: | |
db.insert( | |
input=row.get('input', ''), | |
output=row.get('output', ''), | |
timestamp=row.get('timestamp', datetime.now().isoformat()), | |
notes='' | |
) | |
return Redirect('/annotate/0') | |
@rt("/annotate/{idx}") | |
def post(idx: int, notes: str = None): | |
"""Handle note updates""" | |
print(f"Saving notes for item {idx}: {notes}") # Debug print | |
item = db.get(idx) | |
if not item: return "Item not found", 404 | |
# Debug database state | |
print(f"Before update - item.notes: {item.notes}") | |
item.notes = notes | |
item.timestamp = datetime.now().isoformat() | |
print(f"After update - item.notes: {item.notes}") | |
db.update(item) | |
# Verify update | |
updated_item = db.get(idx) | |
print(f"After db update - item.notes: {updated_item.notes}") | |
# Return the next item after saving | |
items = db() | |
next_item = next((i for i in items if i.id > item.id), items[0]) | |
print(f"Moving to next item: {next_item.id}") # Debug print | |
return annotate_view(next_item) | |
@rt("/annotate/{idx}") | |
def get(idx: int = 0): | |
"""Show annotation interface""" | |
items = db() | |
if not items: | |
return Redirect("/") | |
global total_items_length | |
total_items_length = len(items) | |
print(f"Get request: idx={idx}, total_items={total_items_length}") # Debug print | |
# Get item directly by ID | |
current_item = db.get(idx) or items[0] # Fallback to first item if ID not found | |
return annotate_view(current_item) | |
# Start the server | |
serve() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment