Skip to content

Instantly share code, notes, and snippets.

@hamelsmu
Created March 12, 2025 18:56
Show Gist options
  • Save hamelsmu/ea2b587bc85f8e46cc4a1fcbfe5b4254 to your computer and use it in GitHub Desktop.
Save hamelsmu/ea2b587bc85f8e46cc4a1fcbfe5b4254 to your computer and use it in GitHub Desktop.
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