| 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) |
|
|