Support second turn judgement and singe answer grading
diff --git a/fastchat/llm_judge/common.py b/fastchat/llm_judge/common.py
index e1b0f95c94..7ea156f148 100644
--- a/fastchat/llm_judge/common.py
+++ b/fastchat/llm_judge/common.py
@@ -489,7 +489,7 @@ def normalize_game_key_dict(judgment_dict):
     return ret
 
 
-def load_model_judgments(filename: str):
+def load_pairwise_model_judgments(filename: str):
     """Load model judgments.
 
     The return value is a dict of type:
@@ -532,10 +532,35 @@ def load_model_judgments(filename: str):
     return normalized
 
 
-def resolve_default_judgment_dict(
+def load_single_model_judgments(filename: str):
+    """Load model judgments.
+
+    The return value is a dict of type:
+    Dict[judge: Tuple -> Dict[game_key: tuple -> game_result: dict]
+    """
+    judge_dict = {}
+
+    for line in open(filename):
+        obj = json.loads(line)
+        judge = tuple(obj["judge"])
+        qid, model = obj["question_id"], obj["model"]
+
+        if judge not in judge_dict:
+            judge_dict[judge] = {}
+
+        gamekey = (qid, model)
+
+        judge_dict[judge][gamekey] = {
+            "score": obj["score"],
+            "judgment": obj["judgment"],
+        }
+    return judge_dict
+
+
+def resolve_pairwise_judgment_dict(
     question, model_judgments_normal, model_judgments_math, multi_turn=False
 ):
-    """Return the correct default judge."""
+    """Return the correct pairwise judge."""
     if multi_turn:
         if question["category"] in NEED_REF_CATS:
             return model_judgments_math[("gpt-4", "pair-math-v1-multi-turn")]
@@ -547,7 +572,22 @@ def resolve_default_judgment_dict(
         return model_judgments_normal[("gpt-4", "pair-v2")]
 
 
-def get_model_judge_explanation(gamekey, judgment_dict):
+def resolve_single_judgment_dict(
+    question, model_judgments_normal, model_judgments_math, multi_turn=False
+):
+    """Return the correct single answer grading judge."""
+    if multi_turn:
+        if question["category"] in NEED_REF_CATS:
+            return model_judgments_math[("gpt-4", "single-math-v1-multi-turn")]
+        return model_judgments_normal[("gpt-4", "single-v1-multi-turn")]
+
+    if question["category"] in NEED_REF_CATS:
+        return model_judgments_math[("gpt-4", "single-math-v1")]
+    else:
+        return model_judgments_normal[("gpt-4", "single-v1")]
+
+
+def get_pairwise_judge_explanation(gamekey, judgment_dict):
     """Get model judge explanation."""
     try:
         qid, model_1, model_2 = gamekey
@@ -572,6 +612,24 @@ def get_model_judge_explanation(gamekey, judgment_dict):
         return "N/A"
 
 
+def get_single_judge_explanation(gamekey, judgment_dict):
+    """Get model judge explanation."""
+    try:
+        qid, model = gamekey
+
+        res = judgment_dict[gamekey]
+
+        g1_judgment = res["judgment"]
+        g1_score = res["score"]
+
+        return (
+            f"**Game 1**. **A**: {model}, **Score**: {g1_score}\n\n"
+            f"**Judgment**: {g1_judgment}"
+        )
+    except KeyError:
+        return "N/A"
+
+
 def check_data(questions, model_answers, ref_answers, models, judges):
     # check model answers
     for m in models:
diff --git a/fastchat/llm_judge/qa_browser.py b/fastchat/llm_judge/qa_browser.py
index 0650f6a916..d75d755cde 100644
--- a/fastchat/llm_judge/qa_browser.py
+++ b/fastchat/llm_judge/qa_browser.py
@@ -12,17 +12,23 @@
 from fastchat.llm_judge.common import (
     load_questions,
     load_model_answers,
-    load_model_judgments,
-    resolve_default_judgment_dict,
-    get_model_judge_explanation,
+    load_single_model_judgments,
+    load_pairwise_model_judgments,
+    resolve_single_judgment_dict,
+    resolve_pairwise_judgment_dict,
+    get_single_judge_explanation,
+    get_pairwise_judge_explanation,
 )
-from fastchat.serve.gradio_web_server import block_css as old_block_css
 
 
 questions = []
 model_answers = {}
-model_judgments_normal = {}
-model_judgments_math = {}
+
+model_judgments_normal_single = {}
+model_judgments_math_single = {}
+
+model_judgments_normal_pairwise = {}
+model_judgments_math_pairwise = {}
 
 question_selector_map = {}
 category_selector_map = defaultdict(list)
@@ -36,7 +42,7 @@ def display_question(category_selector, request: gr.Request):
     )
 
 
-def display_answer(
+def display_pairwise_answer(
     question_selector, model_selector1, model_selector2, request: gr.Request
 ):
     q = question_selector_map[question_selector]
@@ -45,25 +51,63 @@ def display_answer(
     ans1 = model_answers[model_selector1][qid]
     ans2 = model_answers[model_selector2][qid]
 
-    chat_mds = to_gradio_chat_mds(q, ans1, ans2)
+    chat_mds = pairwise_to_gradio_chat_mds(q, ans1, ans2)
     gamekey = (qid, model_selector1, model_selector2)
 
-    judgment_dict = resolve_default_judgment_dict(
-        q, model_judgments_normal, model_judgments_math, multi_turn=False
+    judgment_dict = resolve_pairwise_judgment_dict(
+        q,
+        model_judgments_normal_pairwise,
+        model_judgments_math_pairwise,
+        multi_turn=False,
+    )
+
+    explanation = (
+        "##### Model Judgment (first turn)\n"
+        + get_pairwise_judge_explanation(gamekey, judgment_dict)
+    )
+
+    judgment_dict_turn2 = resolve_pairwise_judgment_dict(
+        q,
+        model_judgments_normal_pairwise,
+        model_judgments_math_pairwise,
+        multi_turn=True,
+    )
+
+    explanation_turn2 = (
+        "##### Model Judgment (second turn)\n"
+        + get_pairwise_judge_explanation(gamekey, judgment_dict_turn2)
+    )
+
+    return chat_mds + [explanation] + [explanation_turn2]
+
+
+def display_single_answer(question_selector, model_selector1, request: gr.Request):
+    q = question_selector_map[question_selector]
+    qid = q["question_id"]
+
+    ans1 = model_answers[model_selector1][qid]
+
+    chat_mds = single_to_gradio_chat_mds(q, ans1)
+    gamekey = (qid, model_selector1)
+
+    judgment_dict = resolve_single_judgment_dict(
+        q, model_judgments_normal_single, model_judgments_math_single, multi_turn=False
     )
-    explanation = "##### Model Judgment (first turn)\n" + get_model_judge_explanation(
+
+    explanation = "##### Model Judgment (first turn)\n" + get_single_judge_explanation(
         gamekey, judgment_dict
     )
 
-    judgment_dict_turn_2 = resolve_default_judgment_dict(
-        q, model_judgments_normal, model_judgments_math, multi_turn=True
+    judgment_dict_turn2 = resolve_single_judgment_dict(
+        q, model_judgments_normal_single, model_judgments_math_single, multi_turn=True
     )
-    explanation_turn_2 = (
+
+    explanation_turn2 = (
         "##### Model Judgment (second turn)\n"
-        + get_model_judge_explanation(gamekey, judgment_dict_turn_2)
+        + get_single_judge_explanation(gamekey, judgment_dict_turn2)
     )
 
-    return chat_mds + [explanation] + [explanation_turn_2]
+    return chat_mds + [explanation] + [explanation_turn2]
 
 
 newline_pattern1 = re.compile("\n\n(\d+\. )")
@@ -78,7 +122,7 @@ def post_process_answer(x):
     return x
 
 
-def to_gradio_chat_mds(question, ans_a, ans_b, turn=None):
+def pairwise_to_gradio_chat_mds(question, ans_a, ans_b, turn=None):
     end = len(question["turns"]) if turn is None else turn + 1
 
     mds = ["", "", "", "", "", "", ""]
@@ -110,6 +154,35 @@ def to_gradio_chat_mds(question, ans_a, ans_b, turn=None):
     return mds
 
 
+def single_to_gradio_chat_mds(question, ans, turn=None):
+    end = len(question["turns"]) if turn is None else turn + 1
+
+    mds = ["", "", "", "", ""]
+    for i in range(end):
+        base = i * 2
+        if i == 0:
+            mds[base + 0] = "##### User\n" + question["turns"][i]
+        else:
+            mds[base + 0] = "##### User's follow-up question \n" + question["turns"][i]
+        mds[base + 1] = "##### Assistant A\n" + post_process_answer(
+            ans["choices"][0]["turns"][i].strip()
+        )
+
+    ref = question.get("reference", ["", ""])
+
+    ref_md = ""
+    if turn is None:
+        if ref[0] != "" or ref[1] != "":
+            mds[4] = f"##### Reference Solution\nQ1. {ref[0]}\nQ2. {ref[1]}"
+    else:
+        x = ref[turn] if turn < len(ref) else ""
+        if x:
+            mds[4] = f"##### Reference Solution\n{ref[turn]}"
+        else:
+            mds[4] = ""
+    return mds
+
+
 def build_pairwise_browser_tab():
     global question_selector_map, category_selector_map
 
@@ -165,28 +238,123 @@ def build_pairwise_browser_tab():
     chat_mds.append(reference)
 
     model_explanation = gr.Markdown(elem_id="model_explanation")
-    model_explanation_2 = gr.Markdown(elem_id="model_explanation")
+    model_explanation2 = gr.Markdown(elem_id="model_explanation")
 
     # Callbacks
     category_selector.change(display_question, [category_selector], [question_selector])
     question_selector.change(
-        display_answer,
+        display_pairwise_answer,
         [question_selector] + model_selectors,
-        chat_mds + [model_explanation] + [model_explanation_2],
+        chat_mds + [model_explanation] + [model_explanation2],
     )
 
     for i in range(num_sides):
         model_selectors[i].change(
-            display_answer,
+            display_pairwise_answer,
             [question_selector] + model_selectors,
-            chat_mds + [model_explanation] + [model_explanation_2],
+            chat_mds + [model_explanation] + [model_explanation2],
         )
 
     return (category_selector,)
 
 
-block_css = old_block_css + (
-    """
+def build_single_answer_browser_tab():
+    global question_selector_map, category_selector_map
+
+    models = list(model_answers.keys())
+    num_sides = 1
+    num_turns = 2
+    side_names = ["A"]
+
+    # # Build question selector map
+    # for q in questions:
+    #     preview = f"{q['question_id']}: " + q["turns"][0][:128] + "..."
+    #     question_selector_map[preview] = q
+    #     category_selector_map[q["category"]].append(preview)
+    question_selector_choices = list(question_selector_map.keys())
+    category_selector_choices = list(category_selector_map.keys())
+
+    # Selectors
+    with gr.Row():
+        with gr.Column(scale=1, min_width=200):
+            category_selector = gr.Dropdown(
+                choices=category_selector_choices,
+                label="Category",
+            ).style(container=False)
+        with gr.Column(scale=100):
+            question_selector = gr.Dropdown(
+                choices=question_selector_choices,
+                label="Question",
+            ).style(container=False)
+
+    model_selectors = [None] * num_sides
+    with gr.Row():
+        for i in range(num_sides):
+            with gr.Column():
+                model_selectors[i] = gr.Dropdown(
+                    choices=models,
+                    value=models[i] if len(models) > i else "",
+                    label=f"Model {side_names[i]}",
+                ).style(container=False)
+
+    # Conversation
+    chat_mds = []
+    for i in range(num_turns):
+        chat_mds.append(gr.Markdown(elem_id=f"user_question_{i+1}"))
+        with gr.Row():
+            for j in range(num_sides):
+                with gr.Column(scale=100):
+                    chat_mds.append(gr.Markdown())
+
+                if j == 0:
+                    with gr.Column(scale=1, min_width=8):
+                        gr.Markdown()
+
+    reference = gr.Markdown(elem_id=f"reference")
+    chat_mds.append(reference)
+
+    model_explanation = gr.Markdown(elem_id="model_explanation")
+    model_explanation2 = gr.Markdown(elem_id="model_explanation")
+
+    # Callbacks
+    category_selector.change(display_question, [category_selector], [question_selector])
+    question_selector.change(
+        display_single_answer,
+        [question_selector] + model_selectors,
+        chat_mds + [model_explanation] + [model_explanation2],
+    )
+
+    for i in range(num_sides):
+        model_selectors[i].change(
+            display_single_answer,
+            [question_selector] + model_selectors,
+            chat_mds + [model_explanation] + [model_explanation2],
+        )
+
+    return (category_selector,)
+
+
+block_css = """
+#notice_markdown {
+    font-size: 104%
+}
+#notice_markdown th {
+    display: none;
+}
+#notice_markdown td {
+    padding-top: 6px;
+    padding-bottom: 6px;
+}
+#leaderboard_markdown {
+    font-size: 104%
+}
+#leaderboard_markdown td {
+    padding-top: 6px;
+    padding-bottom: 6px;
+}
+#leaderboard_dataframe td {
+    line-height: 0.1em;
+}
 #user_question_1 {
     background-color: #DEEBF7;
 }
@@ -200,7 +368,6 @@ def build_pairwise_browser_tab():
     background-color: #FBE5D6;
 }
 """
-)
 
 
 def load_demo():
@@ -220,9 +387,12 @@ def build_demo():
 The code to generate answers and judgments is at [fastchat.llm_judge](https://github.com/lm-sys/FastChat/tree/main/fastchat/llm_judge).
 """
         )
-        (category_selector,) = build_pairwise_browser_tab()
-
+        with gr.Tab("Pairwise Comparison"):
+            (category_selector,) = build_pairwise_browser_tab()
+        with gr.Tab("Single Answer Grading"):
+            (category_selector2,) = build_single_answer_browser_tab()
         demo.load(load_demo, [], [category_selector])
+        demo.load(load_demo, [], [category_selector2])
 
     return demo
 
@@ -238,7 +408,12 @@ def build_demo():
 
     question_file = f"data/{args.bench_name}/question.jsonl"
     answer_dir = f"data/{args.bench_name}/model_answer"
-    model_judgment_file = f"data/{args.bench_name}/model_judgment/gpt-4_pair.jsonl"
+    pairwise_model_judgment_file = (
+        f"data/{args.bench_name}/model_judgment/gpt-4_pair.jsonl"
+    )
+    single_model_judgment_file = (
+        f"data/{args.bench_name}/model_judgment/gpt-4_single.jsonl"
+    )
 
     # Load questions
     questions = load_questions(question_file, None, None)
@@ -247,9 +422,12 @@ def build_demo():
     model_answers = load_model_answers(answer_dir)
 
     # Load model judgments
-    model_judgments_normal = model_judgments_math = load_model_judgments(
-        model_judgment_file
-    )
+    model_judgments_normal_single = (
+        model_judgments_math_single
+    ) = load_single_model_judgments(single_model_judgment_file)
+    model_judgments_normal_pairwise = (
+        model_judgments_math_pairwise
+    ) = load_pairwise_model_judgments(pairwise_model_judgment_file)
 
     demo = build_demo()
     demo.queue(concurrency_count=10, status_update_rate=10, api_open=False).launch(

<!-- Thank you for your contribution! -->

<!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. -->

## Why are these changes needed?

Requested by @merrymercy to support second turn judgement for MT-bench. Also added single answer grading as a second tab to the MT-bench judge. 

<!-- Please give a short summary of the change and the problem this solves. -->

You can take a look at the new site at: https://a5070c83f1fe17e8b1.gradio.live/


## Related issue number (if applicable)

<!-- For example: "Closes #1234" -->

## Checks

- [x] I've run `format.sh` to lint the changes in this PR.
- [x] I've included any doc changes needed.
- [x] I've made sure the relevant tests are passing (if applicable).
