レッスン3:見やすく・使いやすくする
— UI/UXの“ねらい”を理解してから実装する(初心者向け)
前回(レッスン2)で「質問 → 答え+根拠」は動きました。 ここからは“使う人の気持ち”に寄り添って、UIを迷わず・気持ちよく整えます。まずはなぜこのUIにするのか(ねらい)をやさしく理解し、そのあとどこに何を書けばよいかを迷わない手順で実装します。最後に差分と完成版コードを載せます。
なぜ UI を整えるの?(ユーザーの気持ちから逆算)
最初の画面で、いきなり空の入力欄だけだと……👇
┌─────────────── 以前 ───────────────┐
│ [ 何を書けばいいの? ] │ ← ブランクの壁
└──────────────────────────────┘
初心者はここで固まりがちです。 だから、導き(ガイド)と安心感を足します。
┌───────────────────────────────────── 今回 ─────────────────────────────────────┐
│ [質問] プリセット(簡潔/バランス/じっくり) 🧹クリア │
│ └─ ヒント(例)ボタン:・要点だけ ・結論と根拠 ・前提と制約 │
│ │
│ 🧠 回答カード:最初に3行で要点 → その後に本文(落ち着いて読める) │
│ 🔎 参照した文脈:折りたたみ+先頭プレビュー+軽いハイライト(根拠の所在が一目で) │
└──────────────────────────────────────────────────────────────────────────────┘
UIの“ねらい”と嬉しさ
プリセット(簡潔/バランス/じっくり) 初心者に難しい「top_k / トークン量 / temperature」を3択に凝縮。 → “とりあえず選べる” → 手戻りが減る。
ヒントボタン 「聞き方が分からない」を1クリックで解消。 → “ゼロから考えなくていい” → 迷いが消える。
回答カードを「要点 → 本文」 最初に3行以内の要点を先出し→その後に本文。 → “流し見→読み込む”の自然な順番で疲れない。
参照(根拠)は折りたたみ+プレビュー+ハイライト いきなり長文を見せず、先頭だけ+太字で視線誘導。 → “どこが根拠?”が一目でわかる。信頼できる。
🧹クリア(小リセット) 入れ替えや試行錯誤のとき、ワンボタンで状態を掃除。 → “試しやすい・戻しやすい”。
置き場所マップ(どこに書く?)
すべて 1ファイル:
app.py
に追記・置換します。 レッスン2の完成版をベースに、以下の手順どおりに入れてください。
- 先頭の import 群に
import re
を追加(ハイライトに使う) build_messages()
内のuser_text
を置換(要点→本文の書式に)answer_query()
より上にpreview()
/highlight()
関数を新規追加page_ask_my_pdf()
内を手直し
- プリセット → ヒントボタン → (初期値つき)質問欄 → クリア → 回答生成 → 参照表示
- Streamlitの再実行API差を吸収するため、
force_rerun()
を追加してst.rerun()
/ 旧APIを両対応
- Streamlitの再実行API差を吸収するため、
ここからはハンズオンで、各ステップを「どこに書くか」つきで示します。
ハンズオン
ステップ1|import re
を追加
場所:app.py
先頭の import 群
import re # ← これを追加(太字ハイライトに使う)
ステップ2|回答カードの書式(要点→本文)に変更
場所:def build_messages(query, contexts):
の中
やること:user_text = (
から始まるブロックを丸ごと置換
user_text = (
f"次の文脈に基づいて日本語で丁寧に回答してください。\n"
f"出力形式:\n"
f"1) 最初に3行以内で要点(箇条書き、記号は「・」)\n"
f"2) 続いて本文(必要なら段落)\n"
f"注意: 文脈にないことは書かない。分からない場合は「分かりません」。\n\n"
f"[文脈]\n{context_text}\n\n"
f"[質問]\n{query}\n\n"
f"[回答]\n"
)
ステップ3|参照を“のぞきやすく”する関数を追加
場所:def answer_query(...):
より上(ヘルパー群の近く)
def preview(text: str, chars=240):
text = text.replace("\n", " ")
return text[:chars] + ("…" if len(text) > chars else "")
def highlight(text: str, query: str):
# 記号除去後、3文字以上の語を太字に(超かんたんハイライト)
words = [w for w in re.split(r"[^\wぁ-んァ-ヶ一-龠ー]+", query) if len(w) >= 3]
out = text
for w in set(words):
out = re.sub(fr"({re.escape(w)})", r"**\1**", out, flags=re.IGNORECASE)
return out
ステップ4|Streamlit の再実行(rerun)を両対応に
場所:st.set_page_config(...)
の直後あたり(どこでもOK)
def force_rerun():
"""Streamlit のバージョン差を吸収して再実行する."""
if hasattr(st, "rerun"):
st.rerun() # 新API
else:
st.experimental_rerun() # 旧API
こうしておくと、あなたの
requirements.txt
(streamlit>=1.28.0
)でも壊れません。
ステップ5|質問ページ page_ask_my_pdf()
を整える
場所:def page_ask_my_pdf():
の中身
やること:以下のパーツをこの順番で入れていきます。
- プリセット(簡潔/バランス/じっくり →
top_k
/トークン量/temperature
を自動セット) - ヒントボタン(クリックで例文が質問欄に入る)
- 質問欄(初期値はヒントの内容を入れる)
- 🧹クリア(
session_state
を掃除 →force_rerun()
) - 回答生成ボタン → 回答カード表示
- 参照表示(
preview()
+highlight()
で見やすく)
具体的なコードは、すぐ下の「差分」と「完成版」にそのまま載せています。 置き場所に迷ったら、完成版をコピペで上書きしてOKです。
動作チェック(小さなチェックリスト)
- [ ] PDFをアップロード → 「🚀 処理を開始」で保存できる
- [ ] 「Ask My PDF(s)」でプリセットを選べる
- [ ] ヒントを押すと質問欄に文言が入る(画面がサッと更新される)
- [ ] 💬 回答を生成で「要点→本文」の形で答えが出る
- [ ] 参照(根拠)を開くと、先頭プレビュー+太字ハイライトで見やすい
- [ ] 🧹 クリアで状態がリセットされる(ヒントや質問が消える)
差分(レッスン2 → レッスン3)
既存の
app.py
への最小変更だけを示します。手で直すときの道しるべにどうぞ。
--- a/app.py
+++ b/app.py
@@
-import os
-import io
-import uuid
-import hashlib
+import os
+import io
+import uuid
+import hashlib
+import re
@@
st.set_page_config(page_title="PDFに質問しよう", page_icon="📄", layout="wide")
+# ---- Streamlit 互換ラッパ(rerun) ----
+def force_rerun():
+ """Streamlit のバージョン差を吸収して再実行する."""
+ if hasattr(st, "rerun"):
+ st.rerun()
+ else:
+ st.experimental_rerun()
+
@@
def build_messages(query, contexts):
@@
- user_text = (
- f"次の文脈に基づいて質問に答えてください。\n\n"
- f"[文脈]\n{context_text}\n\n"
- f"[質問]\n{query}\n\n"
- f"[回答](日本語で簡潔に)"
- )
+ user_text = (
+ f"次の文脈に基づいて日本語で丁寧に回答してください。\n"
+ f"出力形式:\n"
+ f"1) 最初に3行以内で要点(箇条書き、記号は「・」)\n"
+ f"2) 続いて本文(必要なら段落)\n"
+ f"注意: 文脈にないことは書かない。分からない場合は「分かりません」。\n\n"
+ f"[文脈]\n{context_text}\n\n"
+ f"[質問]\n{query}\n\n"
+ f"[回答]\n"
+ )
@@
+def preview(text: str, chars=240):
+ text = text.replace("\n", " ")
+ return text[:chars] + ("…" if len(text) > chars else "")
+
+def highlight(text: str, query: str):
+ # 超かんたんハイライト(3文字以上の語を対象)
+ words = [w for w in re.split(r"[^\wぁ-んァ-ヶ一-龠ー]+", query) if len(w) >= 3]
+ out = text
+ for w in set(words):
+ out = re.sub(fr"({re.escape(w)})", r"**\1**", out, flags=re.IGNORECASE)
+ return out
@@
def page_ask_my_pdf():
- st.title("🤔 Ask My PDF(s)")
- st.markdown("アップロードしたPDFの内容に基づいて質問できます。")
+ st.title("🤔 Ask My PDF(s)")
+ st.markdown("アップロードしたPDFの内容に基づいて質問できます。使い方に迷わないよう、プリセットとヒントを用意しました。")
@@
- st.markdown("---")
- query = st.text_input("質問を入力してください", placeholder="例:この資料の売上の要点は?")
- c1, c2, c3 = st.columns([1, 1, 1])
- with c1:
- top_k = st.slider("検索チャンク数(top_k)", 1, 10, 5)
- with c2:
- max_ctx = st.slider("文脈の最大トークン", 500, 4000, 2000, step=100)
- with c3:
- temp = st.slider("創造性(temperature)", 0.0, 1.0, 0.2, step=0.1)
+ st.markdown("---")
+ # ① プリセット(初心者向け)
+ preset = st.radio("回答スタイル", ["簡潔", "バランス", "じっくり"], horizontal=True)
+ if preset == "簡潔":
+ top_k, max_ctx, temp = 3, 1200, 0.1
+ elif preset == "じっくり":
+ top_k, max_ctx, temp = 7, 2600, 0.3
+ else:
+ top_k, max_ctx, temp = 5, 2000, 0.2
+
+ # ② ヒント(例文ボタン)
+ st.markdown("**ヒント:こんな聞き方も**")
+ cols = st.columns(3)
+ examples = ["要点だけ教えて", "結論と数値の根拠を教えて", "この資料の前提と制約は?"]
+ for c, ex in zip(cols, examples):
+ if c.button(f"💡 {ex}"):
+ st.session_state["last_query"] = ex
+ force_rerun()
+
+ # ③ 質問欄(ヒントの反映を初期値に)
+ query_default = st.session_state.get("last_query", "")
+ query = st.text_input("質問を入力してください", value=query_default, placeholder="例:この資料の売上の要点は?")
+
+ # ④ クリア(小さなリセット)
+ reset_cols = st.columns([1,3,1])
+ with reset_cols[2]:
+ if st.button("🧹 クリア(質問と表示をリセット)"):
+ for k in ["chat", "last_query"]:
+ if k in st.session_state:
+ del st.session_state[k]
+ force_rerun()
@@
- with st.expander(f"チャンク {i}(類似度: {r['score']:.3f})"):
- st.write(r["text"])
+ with st.expander(f"チャンク {i}(類似度: {r['score']:.3f})"):
+ pv = preview(r["text"])
+ st.markdown(highlight(pv, query))
@@
+ # (任意)詳細設定:慣れてきた人向け
+ with st.expander("⚙️ 詳細設定(上級者向け・任意)"):
+ top_k = st.slider("検索チャンク数(top_k)", 1, 10, top_k, key="adv_topk")
+ max_ctx = st.slider("文脈の最大トークン", 500, 4000, max_ctx, step=100, key="adv_ctx")
+ temp = st.slider("創造性(temperature)", 0.0, 1.0, temp, step=0.1, key="adv_temp")
これで、UIの意図がわかり、どこに何を書くかが迷わず進められます。 次はレッスン4で、「取りすぎず・足りなすぎず」を実現する検索品質のチューニングに進みましょう。