Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import google.generativeai as genai
|
| 3 |
import requests
|
| 4 |
import os
|
| 5 |
-
import re
|
|
|
|
| 6 |
|
| 7 |
# --- 初期設定 ---
|
| 8 |
GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
|
|
@@ -13,19 +15,25 @@ AUTHOR_LIST = ["にき", "かん", "なごみ", "蜜柑餅"]
|
|
| 13 |
if GEMINI_KEY:
|
| 14 |
genai.configure(api_key=GEMINI_KEY)
|
| 15 |
|
| 16 |
-
# --- デバッグ
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
st.error(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# --- 関数:microCMSから記事を取得 ---
|
| 31 |
def get_articles():
|
|
@@ -43,8 +51,8 @@ def get_articles():
|
|
| 43 |
# --- 画面制御 ---
|
| 44 |
params = st.query_params
|
| 45 |
|
| 46 |
-
# 1. 個別記事ページ(URLに ?id=xxx がある場合)
|
| 47 |
if "id" in params:
|
|
|
|
| 48 |
article_id = params["id"]
|
| 49 |
articles = get_articles()
|
| 50 |
article = next((a for a in articles if a["id"] == article_id), None)
|
|
@@ -55,16 +63,14 @@ if "id" in params:
|
|
| 55 |
st.rerun()
|
| 56 |
|
| 57 |
st.title(article.get("title", "無題"))
|
| 58 |
-
st.caption(f"
|
| 59 |
st.divider()
|
| 60 |
-
# 本文をHTMLとして表示
|
| 61 |
st.markdown(article.get("content", ""), unsafe_allow_html=True)
|
| 62 |
st.divider()
|
| 63 |
|
| 64 |
-
|
| 65 |
-
space_id = os.environ.get('SPACE_ID', 'your-space-id')
|
| 66 |
share_url = f"https://huggingface.co/spaces/{space_id}?id={article_id}"
|
| 67 |
-
st.write("🔗
|
| 68 |
st.code(share_url)
|
| 69 |
else:
|
| 70 |
st.error("記事が見つかりませんでした。")
|
|
@@ -72,71 +78,69 @@ if "id" in params:
|
|
| 72 |
st.query_params.clear()
|
| 73 |
st.rerun()
|
| 74 |
|
| 75 |
-
# 2. メイン画面(一覧表示 & 投稿)
|
| 76 |
else:
|
|
|
|
| 77 |
tab_view, tab_post = st.tabs(["📖 ブログを読む", "✍️ 記事を書く"])
|
| 78 |
|
| 79 |
with tab_view:
|
| 80 |
st.title("サークル活動ブログ")
|
| 81 |
-
|
| 82 |
-
# 検索・絞り込み機能
|
| 83 |
search_col1, search_col2 = st.columns(2)
|
| 84 |
with search_col1:
|
| 85 |
-
search_query = st.text_input("🔍 タイトル
|
| 86 |
with search_col2:
|
| 87 |
-
# 検索用リストには「全員」を追加
|
| 88 |
search_author_list = ["全員"] + AUTHOR_LIST
|
| 89 |
author_filter = st.selectbox("👤 著者で絞り込み", search_author_list)
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
# 一覧表示
|
| 94 |
for a in articles:
|
| 95 |
title = a.get("title", "無題")
|
| 96 |
content_html = a.get("content", "")
|
| 97 |
author = a.get("author", "不明")
|
| 98 |
pub_date = a.get("publishedAt", "-----")[:10]
|
| 99 |
-
|
| 100 |
clean_text = re.sub(r'<[^>]*?>', '', content_html)
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
search_match = (search_query.lower() in title.lower() or search_query.lower() in clean_text.lower())
|
| 105 |
-
|
| 106 |
-
if author_match and search_match:
|
| 107 |
with st.container():
|
| 108 |
st.subheader(title)
|
| 109 |
-
st.caption(f"
|
| 110 |
snippet = clean_text[:100] + "..."
|
| 111 |
st.write(f'<p style="font-size:0.9em; color:#555;">{snippet}</p>', unsafe_allow_html=True)
|
| 112 |
-
|
| 113 |
if st.button("続きを読む", key=a["id"]):
|
| 114 |
st.query_params.id = a["id"]
|
| 115 |
st.rerun()
|
| 116 |
st.divider()
|
| 117 |
|
| 118 |
-
# 記事投稿画面(tab_post のスコープ内にすべて収める)
|
| 119 |
with tab_post:
|
| 120 |
st.title("秘密の管理画面")
|
| 121 |
-
|
| 122 |
-
password = st.text_input("合言葉を入力してください", type="password")
|
| 123 |
|
| 124 |
if password == "1234":
|
| 125 |
st.success("ログイン成功!")
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
if st.button("✨ AIモードで
|
| 132 |
if raw_content:
|
| 133 |
try:
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
ai_response = model.generate_content(prompt)
|
| 139 |
-
# 万が一 ``` が含まれた場合の除去
|
| 140 |
clean_html = ai_response.text.replace('```html', '').replace('```', '').strip()
|
| 141 |
st.session_state.formatted_html = clean_html
|
| 142 |
except Exception as e:
|
|
@@ -144,21 +148,48 @@ else:
|
|
| 144 |
else:
|
| 145 |
st.warning("本文を入力してください")
|
| 146 |
|
|
|
|
| 147 |
if "formatted_html" in st.session_state:
|
| 148 |
-
st.
|
| 149 |
-
|
| 150 |
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
url = f"https://{CMS_SERVICE_ID}.microcms.io/api/v1/blog"
|
| 153 |
headers = {"X-MICROCMS-API-KEY": CMS_API_KEY, "Content-Type": "application/json"}
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
res = requests.post(url, headers=headers, json=post_data)
|
| 157 |
if res.status_code == 201:
|
| 158 |
-
st.success("
|
| 159 |
-
|
| 160 |
-
del st.session_state.formatted_html
|
| 161 |
else:
|
| 162 |
-
st.error(f"
|
| 163 |
elif password != "":
|
| 164 |
st.error("合言葉が違います。")
|
|
|
|
|
|
| 1 |
+
```python
|
| 2 |
import streamlit as st
|
| 3 |
import google.generativeai as genai
|
| 4 |
import requests
|
| 5 |
import os
|
| 6 |
+
import re
|
| 7 |
+
from datetime import datetime
|
| 8 |
|
| 9 |
# --- 初期設定 ---
|
| 10 |
GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
|
|
|
|
| 15 |
if GEMINI_KEY:
|
| 16 |
genai.configure(api_key=GEMINI_KEY)
|
| 17 |
|
| 18 |
+
# --- サイドバー:デバッグ & モデル設定 ---
|
| 19 |
+
with st.sidebar:
|
| 20 |
+
st.title("⚙️ 設定")
|
| 21 |
+
model_choice = st.selectbox(
|
| 22 |
+
"使用するAIモデル",
|
| 23 |
+
["gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0-flash-exp"],
|
| 24 |
+
index=0,
|
| 25 |
+
help="Proは賢いですが少し遅いです。Flashは爆速です。"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
with st.expander("🔍 デバッグ情報"):
|
| 29 |
+
if not GEMINI_KEY:
|
| 30 |
+
st.error("❌ APIキー未設定")
|
| 31 |
+
else:
|
| 32 |
+
try:
|
| 33 |
+
available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]
|
| 34 |
+
st.write("利用可能モデル:", available_models)
|
| 35 |
+
except Exception as e:
|
| 36 |
+
st.error(f"取得失敗: {e}")
|
| 37 |
|
| 38 |
# --- 関数:microCMSから記事を取得 ---
|
| 39 |
def get_articles():
|
|
|
|
| 51 |
# --- 画面制御 ---
|
| 52 |
params = st.query_params
|
| 53 |
|
|
|
|
| 54 |
if "id" in params:
|
| 55 |
+
# --- 個別記事ページ ---
|
| 56 |
article_id = params["id"]
|
| 57 |
articles = get_articles()
|
| 58 |
article = next((a for a in articles if a["id"] == article_id), None)
|
|
|
|
| 63 |
st.rerun()
|
| 64 |
|
| 65 |
st.title(article.get("title", "無題"))
|
| 66 |
+
st.caption(f"👤 {article.get('author')} | 📅 {article.get('publishedAt')[:10]}")
|
| 67 |
st.divider()
|
|
|
|
| 68 |
st.markdown(article.get("content", ""), unsafe_allow_html=True)
|
| 69 |
st.divider()
|
| 70 |
|
| 71 |
+
space_id = os.environ.get('SPACE_ID', 'default')
|
|
|
|
| 72 |
share_url = f"https://huggingface.co/spaces/{space_id}?id={article_id}"
|
| 73 |
+
st.write("🔗 固定リンク:")
|
| 74 |
st.code(share_url)
|
| 75 |
else:
|
| 76 |
st.error("記事が見つかりませんでした。")
|
|
|
|
| 78 |
st.query_params.clear()
|
| 79 |
st.rerun()
|
| 80 |
|
|
|
|
| 81 |
else:
|
| 82 |
+
# --- メイン画面 ---
|
| 83 |
tab_view, tab_post = st.tabs(["📖 ブログを読む", "✍️ 記事を書く"])
|
| 84 |
|
| 85 |
with tab_view:
|
| 86 |
st.title("サークル活動ブログ")
|
|
|
|
|
|
|
| 87 |
search_col1, search_col2 = st.columns(2)
|
| 88 |
with search_col1:
|
| 89 |
+
search_query = st.text_input("🔍 本文・タイトル検索", "")
|
| 90 |
with search_col2:
|
|
|
|
| 91 |
search_author_list = ["全員"] + AUTHOR_LIST
|
| 92 |
author_filter = st.selectbox("👤 著者で絞り込み", search_author_list)
|
| 93 |
|
| 94 |
+
articles = get_articles()
|
|
|
|
|
|
|
| 95 |
for a in articles:
|
| 96 |
title = a.get("title", "無題")
|
| 97 |
content_html = a.get("content", "")
|
| 98 |
author = a.get("author", "不明")
|
| 99 |
pub_date = a.get("publishedAt", "-----")[:10]
|
|
|
|
| 100 |
clean_text = re.sub(r'<[^>]*?>', '', content_html)
|
| 101 |
|
| 102 |
+
if ((author_filter == "全員") or (author_filter == author)) and \
|
| 103 |
+
(search_query.lower() in title.lower() or search_query.lower() in clean_text.lower()):
|
|
|
|
|
|
|
|
|
|
| 104 |
with st.container():
|
| 105 |
st.subheader(title)
|
| 106 |
+
st.caption(f"👤 {author} | 📅 {pub_date}")
|
| 107 |
snippet = clean_text[:100] + "..."
|
| 108 |
st.write(f'<p style="font-size:0.9em; color:#555;">{snippet}</p>', unsafe_allow_html=True)
|
|
|
|
| 109 |
if st.button("続きを読む", key=a["id"]):
|
| 110 |
st.query_params.id = a["id"]
|
| 111 |
st.rerun()
|
| 112 |
st.divider()
|
| 113 |
|
|
|
|
| 114 |
with tab_post:
|
| 115 |
st.title("秘密の管理画面")
|
| 116 |
+
password = st.text_input("合言葉", type="password")
|
|
|
|
| 117 |
|
| 118 |
if password == "1234":
|
| 119 |
st.success("ログイン成功!")
|
| 120 |
|
| 121 |
+
# 入力エリア
|
| 122 |
+
col_t, col_a = st.columns([2, 1])
|
| 123 |
+
with col_t:
|
| 124 |
+
title_input = st.text_input("記事タイトル")
|
| 125 |
+
with col_a:
|
| 126 |
+
author_select = st.selectbox("著者", AUTHOR_LIST)
|
| 127 |
+
|
| 128 |
+
raw_content = st.text_area("本文メモ(AIが装飾します)", height=150)
|
| 129 |
|
| 130 |
+
if st.button("✨ AIモードで装飾・プレビュー生成"):
|
| 131 |
if raw_content:
|
| 132 |
try:
|
| 133 |
+
model = genai.GenerativeModel(model_name=model_choice)
|
| 134 |
+
prompt = f"""
|
| 135 |
+
以下の文章をブログ記事用に、読みやすいHTMLタグ(h3, p, strong, br等)を使って装飾してください。
|
| 136 |
+
【ルール】
|
| 137 |
+
- 返答は純粋なHTMLコードのみ。
|
| 138 |
+
- 挨拶や解説、```html などの囲みは一切不要。
|
| 139 |
+
- オシャレでモダンな作りかつ、読者がワクワクするような構成。
|
| 140 |
+
本文: {raw_content}
|
| 141 |
+
"""
|
| 142 |
+
with st.spinner(f'{model_choice} が執筆中...'):
|
| 143 |
ai_response = model.generate_content(prompt)
|
|
|
|
| 144 |
clean_html = ai_response.text.replace('```html', '').replace('```', '').strip()
|
| 145 |
st.session_state.formatted_html = clean_html
|
| 146 |
except Exception as e:
|
|
|
|
| 148 |
else:
|
| 149 |
st.warning("本文を入力してください")
|
| 150 |
|
| 151 |
+
# プレビュー & 最終編集エリア
|
| 152 |
if "formatted_html" in st.session_state:
|
| 153 |
+
st.divider()
|
| 154 |
+
st.subheader("👀 プレビュー・最終確認")
|
| 155 |
|
| 156 |
+
# プレビュー表示
|
| 157 |
+
with st.container():
|
| 158 |
+
st.info("実際の表示イメージ:")
|
| 159 |
+
st.markdown(f"""
|
| 160 |
+
<div style="border:1px solid #ddd; padding:20px; border-radius:10px; background:#fff; color:#333;">
|
| 161 |
+
<h3>{title_input}</h3>
|
| 162 |
+
<small>著者: {author_select}</small>
|
| 163 |
+
<hr>
|
| 164 |
+
{st.session_state.formatted_html}
|
| 165 |
+
</div>
|
| 166 |
+
""", unsafe_allow_html=True)
|
| 167 |
+
|
| 168 |
+
st.write("▼ HTMLを微調整(必要であれば)")
|
| 169 |
+
final_html = st.text_area("編集用コード", value=st.session_state.formatted_html, height=150)
|
| 170 |
+
|
| 171 |
+
# 公開設定
|
| 172 |
+
status = st.radio("公開設定", ["すぐに公開", "下書きとして保存"], horizontal=True)
|
| 173 |
+
|
| 174 |
+
if st.button("🚀 microCMSへ送信"):
|
| 175 |
url = f"https://{CMS_SERVICE_ID}.microcms.io/api/v1/blog"
|
| 176 |
headers = {"X-MICROCMS-API-KEY": CMS_API_KEY, "Content-Type": "application/json"}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
display_title = title_input if status == "すぐに公開" else f"【下書き】{title_input}"
|
| 180 |
+
|
| 181 |
+
post_data = {
|
| 182 |
+
"title": display_title,
|
| 183 |
+
"content": final_html,
|
| 184 |
+
"author": author_select
|
| 185 |
+
}
|
| 186 |
|
| 187 |
res = requests.post(url, headers=headers, json=post_data)
|
| 188 |
if res.status_code == 201:
|
| 189 |
+
st.success(f"送信完了!ステータス: {status}")
|
| 190 |
+
del st.session_state.formatted_html
|
|
|
|
| 191 |
else:
|
| 192 |
+
st.error(f"送信失敗:{res.text}")
|
| 193 |
elif password != "":
|
| 194 |
st.error("合言葉が違います。")
|
| 195 |
+
|