"""Gradio UI for single-output proofreading feedback. Flow: 1. User enters source text and clicks "교정 실행". 2. The configured pipeline runs. 3. Output + diff are shown. 4. User picks Good / Not Bad / Critical (+ optional comment). 5. On submit: rating is saved to Supabase ratings table. """ from __future__ import annotations import time from typing import Any import gradio as gr from diff_utils import highlight_diff from pipelines import run_pipeline from . import db PipelineConfig = tuple[str, str, str] # (pipeline_key, model, prompt_key) def _format_summary(counts: dict[str, int]) -> str: total = sum(counts.values()) if total == 0: return "아직 피드백이 없습니다." g = counts.get("good", 0) n = counts.get("not_bad", 0) c = counts.get("critical", 0) return f"**총 피드백**: {total}\n\n- Good: **{g}**\n- Not Bad: **{n}**\n- Critical: **{c}**" def build_feedback_tab( client: Any, vocabulary: list[dict], pipeline_config: PipelineConfig, elem_id_prefix: str = "compare", ) -> None: """Build the single-pipeline feedback UI. Call inside a gr.Blocks/Tab. Args: elem_id_prefix: Unique prefix for component elem_ids. Required when building this UI multiple times in the same Blocks (e.g. for two tabs) — Gradio errors on duplicate elem_ids. """ pipeline_key, model, prompt_key = pipeline_config pipeline_run_id_state = gr.State(None) input_text = gr.Textbox( label="원문 입력", lines=8, placeholder="교정할 텍스트를 입력하세요.", ) run_btn = gr.Button( "교정 실행 (⌘+Enter / Ctrl+Enter)", variant="primary", elem_id=f"{elem_id_prefix}-run-btn", ) status = gr.Markdown("") output = gr.Textbox(label="교정 결과", lines=12, interactive=False) diff_html = gr.HTML(label="원문 대비 diff") gr.Markdown("### 피드백") with gr.Row(): rate_good = gr.Button("👍 Good", variant="primary") rate_notbad = gr.Button("🆗 Not Bad") rate_critical = gr.Button("🚨 Critical", variant="stop") comment = gr.Textbox(label="코멘트 (선택)", lines=2) rating_status = gr.Markdown("") summary_md = gr.Markdown("") def _on_run(text: str): if not text or not text.strip(): return ( gr.update(value="입력 텍스트가 비어있습니다."), gr.update(value=""), gr.update(value=""), None, gr.update(value=""), ) if client is None: return ( gr.update(value="UPSTAGE_API_KEY 미설정."), gr.update(value=""), gr.update(value=""), None, gr.update(value=""), ) start = time.time() try: result = run_pipeline(text, pipeline_key, model, prompt_key, client, vocabulary) except Exception as exc: return ( gr.update(value=f"에러: {exc}"), gr.update(value=""), gr.update(value=""), None, gr.update(value=""), ) elapsed = time.time() - start out_text = result.get("output", "") article_id = db.save_article(text) run_id = db.save_pipeline_run( article_id, pipeline_key=pipeline_key, prompt_key=prompt_key, model=model, output=out_text, processing_time_s=float(elapsed), ) return ( gr.update(value=f"완료 · {elapsed:.1f}s"), gr.update(value=out_text), gr.update(value=highlight_diff(text, out_text)), run_id, gr.update(value=""), ) run_btn.click( _on_run, inputs=[input_text], outputs=[status, output, diff_html, pipeline_run_id_state, rating_status], ) def _make_rating_handler(rating: str): def handler(run_id, comment_text): saved = db.save_rating(run_id, rating, comment_text) note = ( "✅ 피드백 저장됨" if saved else "⚠️ 저장되지 않았습니다 (먼저 교정 실행 후 피드백을 남겨주세요)" ) summary = _format_summary(db.fetch_rating_counts()) return gr.update(value=note), gr.update(value=summary) return handler rate_good.click( _make_rating_handler("good"), inputs=[pipeline_run_id_state, comment], outputs=[rating_status, summary_md], ) rate_notbad.click( _make_rating_handler("not_bad"), inputs=[pipeline_run_id_state, comment], outputs=[rating_status, summary_md], ) rate_critical.click( _make_rating_handler("critical"), inputs=[pipeline_run_id_state, comment], outputs=[rating_status, summary_md], ) refresh_btn = gr.Button("집계 새로고침", size="sm") refresh_btn.click( lambda: gr.update(value=_format_summary(db.fetch_rating_counts())), outputs=[summary_md], )