import os import gradio as gr import pandas as pd from anthropic import Anthropic from datetime import datetime ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") client = Anthropic(api_key=ANTHROPIC_API_KEY) if ANTHROPIC_API_KEY else None SYSTEM_PROMPT = """You are a Hybrid Intelligence (HI) research classifier. Analyze human-AI interaction excerpts and return a structured classification in this exact markdown format: **Primary Classification:** [one of: Collaborative Synthesis / Delegative / Augmentative / Corrective / Exploratory / Dependent / Resistant / Co-creative] **Confidence:** [High / Medium / Low] **Confidence Reasoning:** [1 sentence explaining your confidence level] **Secondary Tags:** [comma-separated list of relevant tags e.g. user-led, task-completion, emotional-labour, error-correction, creative-expansion] **Interaction Dynamics:** [2-3 sentences on power balance, initiative, and who is driving the interaction] **HI Research Notes:** [2-3 sentences on what makes this excerpt significant for Hybrid Intelligence research] **Recruiter-Facing Summary:** [1-2 sentences in plain language describing what happened, suitable for a non-technical audience] Be precise and consistent. Use only the classification categories listed above.""" def classify_single(excerpt, notes): if not excerpt.strip(): return "Please paste an interaction excerpt first.", None if client is None: return "Missing ANTHROPIC_API_KEY — add it in Space Settings → Secrets.", None try: response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1000, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": f"Classify this interaction:\n\n{excerpt}"}] ) result = response.content[0].text log_entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "excerpt": excerpt[:200] + "..." if len(excerpt) > 200 else excerpt, "classification": result, "user_notes": notes } return result, log_entry except Exception as e: return f"API error: {str(e)}", None def classify_batch(batch_text): if not batch_text.strip(): return "Please paste at least one excerpt.", [] if client is None: return "Missing ANTHROPIC_API_KEY — add it in Space Settings → Secrets.", [] excerpts = [e.strip() for e in batch_text.split("---") if e.strip()] if not excerpts: return "No excerpts found. Separate them with --- on its own line.", [] results = [] log_entries = [] for i, excerpt in enumerate(excerpts, 1): try: response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1000, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": f"Classify this interaction:\n\n{excerpt}"}] ) result = response.content[0].text results.append(f"## Excerpt {i}\n\n{result}\n\n---") log_entries.append({ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "excerpt": excerpt[:200] + "..." if len(excerpt) > 200 else excerpt, "classification": result, "user_notes": f"Batch item {i}" }) except Exception as e: results.append(f"## Excerpt {i}\n\nAPI error: {str(e)}\n\n---") return "\n\n".join(results), log_entries def add_to_log(current_log, new_entry): if new_entry: current_log.append(new_entry) return current_log, render_log(current_log) def add_batch_to_log(current_log, new_entries): current_log.extend(new_entries) return current_log, render_log(current_log) def render_log(current_log): if not current_log: return pd.DataFrame(columns=["timestamp", "excerpt", "classification", "user_notes"]) return pd.DataFrame(current_log) def export_log(current_log): if not current_log: return None df = pd.DataFrame(current_log) file_path = "hi_classifications_export.csv" df.to_csv(file_path, index=False) return file_path custom_css = """ body, .gradio-container { background-color: #0d0d1a !important; color: #e0e0f0 !important; font-family: 'Georgia', serif !important; } .gradio-container h1 { font-size: 2.2em !important; font-weight: 900 !important; background: linear-gradient(90deg, #a855f7, #f59e0b) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; padding-bottom: 6px !important; } .gradio-container p, .gradio-container label { color: #c4b5fd !important; } .tab-nav { background: #1a1a2e !important; border-bottom: 2px solid #7c3aed !important; } .tab-nav button { color: #a0aec0 !important; font-weight: 600 !important; font-size: 1em !important; border-radius: 6px 6px 0 0 !important; padding: 10px 24px !important; } .tab-nav button.selected { background: #7c3aed !important; color: #ffffff !important; border-bottom: none !important; } input[type="text"], textarea, select, .gr-box { background-color: #1a1a2e !important; color: #e0e0f0 !important; border: 1px solid #4c1d95 !important; border-radius: 6px !important; } input[type="text"]:focus, textarea:focus { border-color: #a855f7 !important; outline: none !important; box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.3) !important; } button.primary { background: linear-gradient(90deg, #7c3aed, #a855f7) !important; color: white !important; border: none !important; font-weight: 700 !important; font-size: 1em !important; padding: 10px 28px !important; border-radius: 8px !important; cursor: pointer !important; transition: opacity 0.2s !important; } button.primary:hover { opacity: 0.85 !important; } button.secondary { background: #1a1a2e !important; color: #a855f7 !important; border: 1px solid #7c3aed !important; font-weight: 600 !important; padding: 10px 28px !important; border-radius: 8px !important; cursor: pointer !important; } table { background-color: #12122a !important; border-collapse: collapse !important; width: 100% !important; } th { background-color: #4c1d95 !important; color: #f59e0b !important; font-weight: 700 !important; text-transform: uppercase !important; font-size: 0.78em !important; letter-spacing: 0.08em !important; padding: 10px 14px !important; border-bottom: 2px solid #7c3aed !important; } td { background-color: #0d0d1a !important; color: #e0e0f0 !important; padding: 6px 14px !important; border-bottom: 1px solid #1e1e3a !important; font-size: 0.9em !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; vertical-align: middle !important; } tr:hover td { background-color: #1a1a2e !important; } ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: #0d0d1a; } ::-webkit-scrollbar-thumb { background: #7c3aed; border-radius: 3px; } """ with gr.Blocks(title="HI Interaction Classifier") as demo: gr.Markdown("# HI Interaction Classifier") gr.Markdown( "Classify human-AI interaction excerpts for the Hybrid Intelligence research dataset." ) log_state = gr.State([]) with gr.Tab("Classify"): excerpt_input = gr.Textbox( label="Interaction Excerpt", placeholder="Paste the interaction excerpt here...", lines=8 ) notes_input = gr.Textbox( label="Your Notes (optional)", placeholder="Context, observations, flags...", lines=2 ) with gr.Row(): classify_btn = gr.Button("Classify", variant="primary") save_btn = gr.Button("Save to Log", variant="secondary") classification_output = gr.Markdown(label="Classification") pending_entry = gr.State(None) classify_btn.click( fn=classify_single, inputs=[excerpt_input, notes_input], outputs=[classification_output, pending_entry] ) save_btn.click( fn=add_to_log, inputs=[log_state, pending_entry], outputs=[log_state, gr.Dataframe(visible=False)] ).then(lambda: "✅ Saved to log!", outputs=gr.Markdown()) with gr.Tab("Batch Classify"): gr.Markdown( "Paste multiple excerpts separated by `---` on its own line. " "All results will be auto-saved to the log." ) batch_input = gr.Textbox( label="Batch Excerpts", placeholder="Excerpt one...\n---\nExcerpt two...\n---\nExcerpt three...", lines=12 ) batch_btn = gr.Button("Classify All", variant="primary") batch_output = gr.Markdown(label="Batch Results") batch_entries = gr.State([]) batch_btn.click( fn=classify_batch, inputs=[batch_input], outputs=[batch_output, batch_entries] ).then( fn=add_batch_to_log, inputs=[log_state, batch_entries], outputs=[log_state, gr.Dataframe(visible=False)] ) with gr.Tab("Log & Export"): refresh_btn = gr.Button("Refresh Log", variant="primary") log_table = gr.Dataframe( label="Classification History", wrap=False, elem_classes=["table-wrap"] ) export_btn = gr.Button("Export as CSV", variant="secondary") download = gr.File(label="Download CSV") refresh_btn.click( fn=render_log, inputs=[log_state], outputs=[log_table] ) export_btn.click( fn=export_log, inputs=[log_state], outputs=[download] ) demo.launch(css=custom_css)