Nagomi-SugarHeart commited on
Commit
cf27eb2
·
verified ·
1 Parent(s): 1c9921f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -171
app.py CHANGED
@@ -6,9 +6,8 @@ import os
6
  import re
7
  import io
8
  import time
9
- from PIL import Image, ImageChops
10
 
11
- # --- 初期設定 ---
12
  GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
13
  CMS_SERVICE_ID = os.environ.get("MICROCMS_SERVICE_ID")
14
  CMS_API_KEY = os.environ.get("MICROCMS_API_KEY")
@@ -19,33 +18,32 @@ ARTICLES_PER_PAGE = 10
19
 
20
  TEMPLATES = {
21
  "テンプレートなし": "",
22
- "📅 イベントレポート": """<h2>イベント概要</h2>
23
- <p>イベント名:<br>日時:<br>場所:</p>
24
- <h2>内容</h2>
25
- <p>ここに内容を書いてください。</p>
26
- <h2>感想</h2>
27
- <p>ここに感想を書いてください。</p>""",
28
- "📝 日常記事": """<h2>はじめに</h2>
29
- <p>ここに導入を書いてください。</p>
30
- <h2>本文</h2>
31
- <p>ここに本文を書いてください。</p>
32
- <h2>おわりに</h2>
33
- <p>ここにまとめを書いてください。</p>""",
34
- "🎵 ライブ・コンサートレポート": """<h2>公演情報</h2>
35
- <p>公演名:<br>日時:<br>会場:<br>出演:</p>
36
- <h2>セットリスト</h2>
37
- <p>1. <br>2. <br>3. </p>
38
- <h2>見どころ・感想</h2>
39
- <p>ここに感想を書いてください。</p>""",
40
- "📣 お知らせ": """<h2>お知らせ</h2>
41
- <p>ここにお知らせ内容を書いてください。</p>
42
- <h2>詳細</h2>
43
- <p>日時:<br>場所:<br>参加方法:</p>""",
44
  }
45
 
46
  if GEMINI_KEY:
47
  genai.configure(api_key=GEMINI_KEY)
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  def clean_ai_html(text):
50
  text = re.sub(r'```html?', '', text, flags=re.IGNORECASE)
51
  text = text.replace('```', '')
@@ -57,19 +55,17 @@ def clean_quill_html(html):
57
  html = re.sub(r'</figure>', '</div>', html)
58
  return html.strip()
59
 
 
 
 
 
60
  def render_html(html, height=600):
61
  wrapped = f"""
62
  <html><head><style>
63
- body {{
64
- margin: 0; padding: 0;
65
- font-family: sans-serif;
66
- font-size: 16px;
67
- color: #111;
68
- word-break: break-word;
69
- }}
70
- img {{ max-width: 100%; height: auto; display: block; }}
71
- p {{ margin: 0.5em 0; }}
72
- h1,h2,h3 {{ margin: 0.8em 0 0.4em; }}
73
  </style></head>
74
  <body>{html}</body></html>
75
  """
@@ -92,8 +88,7 @@ def upload_image_to_microcms(image_file, max_width=1200):
92
 
93
  if img.width > max_width:
94
  ratio = max_width / img.width
95
- new_size = (max_width, int(img.height * ratio))
96
- img = img.resize(new_size, Image.LANCZOS)
97
 
98
  buf = io.BytesIO()
99
  if img.mode in ("RGBA", "P"):
@@ -109,7 +104,6 @@ def upload_image_to_microcms(image_file, max_width=1200):
109
  if res.status_code not in [200, 201]:
110
  st.error(f"アップロードエラー詳細: {res.status_code} / {res.text}")
111
  return None
112
-
113
  return res.json().get("url")
114
  except Exception as e:
115
  st.error(f"画像アップロードエラー: {e}")
@@ -120,9 +114,8 @@ def get_articles(limit=ARTICLES_PER_PAGE, offset=0):
120
  return [], 0
121
  url = f"https://{CMS_SERVICE_ID}.microcms.io/api/v1/blog"
122
  headers = {"X-MICROCMS-API-KEY": CMS_API_KEY}
123
- params = {"limit": limit, "offset": offset}
124
  try:
125
- response = requests.get(url, headers=headers, params=params)
126
  response.raise_for_status()
127
  data = response.json()
128
  return data.get("contents", []), data.get("totalCount", 0)
@@ -134,15 +127,13 @@ def get_all_articles():
134
  return []
135
  url = f"https://{CMS_SERVICE_ID}.microcms.io/api/v1/blog"
136
  headers = {"X-MICROCMS-API-KEY": CMS_API_KEY}
137
- params = {"limit": 100}
138
  try:
139
- response = requests.get(url, headers=headers, params=params)
140
  response.raise_for_status()
141
  return response.json().get("contents", [])
142
  except:
143
  return []
144
 
145
- # --- 画面制御 ---
146
  params = st.query_params
147
 
148
  if "id" in params:
@@ -178,11 +169,7 @@ if "id" in params:
178
  else:
179
  with st.sidebar:
180
  st.title("⚙️ 設定")
181
- model_choice = st.selectbox(
182
- "使用するAIモデル",
183
- ["gemini-2.5-flash", "gemini-2.5-pro"],
184
- index=0
185
- )
186
  show_drafts_in_view = st.checkbox("閲覧画面で下書きも表示する", value=False)
187
 
188
  tab_view, tab_manage = st.tabs(["📖 ブログを読む", "🛠️ 記事の管理・投稿"])
@@ -193,13 +180,10 @@ else:
193
  with search_col1:
194
  search_query = st.text_input("🔍 本文・タイトル検索", "", key="search_text")
195
  with search_col2:
196
- search_author_list = ["全員"] + AUTHOR_LIST
197
- author_filter = st.selectbox("👤 著者で絞り込み", search_author_list, key="search_author")
198
 
199
- if "view_page" not in st.session_state:
200
- st.session_state.view_page = 0
201
- if "last_search" not in st.session_state:
202
- st.session_state.last_search = ""
203
  if search_query != st.session_state.last_search:
204
  st.session_state.view_page = 0
205
  st.session_state.last_search = search_query
@@ -219,11 +203,11 @@ else:
219
  pub_date = a.get("publishedAt", "-----")[:10]
220
  eyecatch = a.get("eyecatch")
221
 
222
- is_draft = title.startswith("【下書き】")
223
- if is_draft and not show_drafts_in_view:
224
  continue
225
 
226
- clean_text = re.sub(r'<[^>]*?>', '', content_html)
 
227
 
228
  if ((author_filter == "全員") or (author_filter == author)) and \
229
  (search_query.lower() in title.lower() or search_query.lower() in clean_text.lower()):
@@ -232,33 +216,33 @@ else:
232
  st.image(eyecatch.get("url", ""), use_container_width=True)
233
  st.subheader(title)
234
  st.caption(f"👤 {author} | 📅 {pub_date}")
 
235
  snippet = clean_text[:100] + "..."
236
- st.write(f'<p style="font-size:0.9em; color:#555;">{snippet}</p>', unsafe_allow_html=True)
237
  if st.button("続きを読む", key=f"read_{a['id']}"):
238
  st.query_params.id = a["id"]
239
  st.rerun()
240
  st.divider()
241
 
242
- if not search_query and author_filter == "全員":
243
  total_pages = -(-total_count // ARTICLES_PER_PAGE)
244
- if total_pages > 1:
245
- st.caption(f"{total_count}件中 {offset+1}〜{min(offset+ARTICLES_PER_PAGE, total_count)}件を表示")
246
- col_prev, col_page, col_next = st.columns([1, 2, 1])
247
- with col_prev:
248
- if st.session_state.view_page > 0:
249
- if st.button("← 前へ", use_container_width=True):
250
- st.session_state.view_page -= 1
251
- st.rerun()
252
- with col_page:
253
- st.markdown(f"<p style='text-align:center;'>{st.session_state.view_page + 1} / {total_pages} ページ</p>", unsafe_allow_html=True)
254
- with col_next:
255
- if st.session_state.view_page < total_pages - 1:
256
- if st.button("次へ →", use_container_width=True):
257
- st.session_state.view_page += 1
258
- st.rerun()
259
 
260
  with tab_manage:
261
- st.title("管理画面")
262
  password = st.text_input("合言葉", type="password")
263
 
264
  if password == ADMIN_PASSWORD:
@@ -271,34 +255,12 @@ else:
271
  if "eyecatch_url" not in st.session_state: st.session_state.eyecatch_url = ""
272
  if "inline_image_urls" not in st.session_state: st.session_state.inline_image_urls = []
273
  if "delete_confirm_id" not in st.session_state: st.session_state.delete_confirm_id = None
274
- if "autosave_html" not in st.session_state: st.session_state.autosave_html = ""
275
- if "autosave_title" not in st.session_state: st.session_state.autosave_title = ""
276
- if "autosave_time" not in st.session_state: st.session_state.autosave_time = None
277
  if "quill_key" not in st.session_state: st.session_state.quill_key = 0
278
- # ✅ コピー表示用フラグ
279
- if "copied_index" not in st.session_state: st.session_state.copied_index = None
280
 
281
  st.divider()
282
  mode_label = f"📝 記事の編集 (ID: {st.session_state.edit_id})" if st.session_state.edit_id else "✍️ 新規記事の投稿"
283
  st.subheader(mode_label)
284
 
285
- if st.session_state.autosave_html and not st.session_state.working_html:
286
- saved_time = st.session_state.autosave_time or "不明"
287
- st.info(f"💾 自動保存された下書きがあります({saved_time})")
288
- col_r1, col_r2 = st.columns(2)
289
- with col_r1:
290
- if st.button("📂 復元する"):
291
- st.session_state.working_html = st.session_state.autosave_html
292
- st.session_state.edit_data["title"] = st.session_state.autosave_title
293
- st.session_state.quill_key += 1
294
- st.rerun()
295
- with col_r2:
296
- if st.button("🗑️ 破棄する"):
297
- st.session_state.autosave_html = ""
298
- st.session_state.autosave_title = ""
299
- st.session_state.autosave_time = None
300
- st.rerun()
301
-
302
  col_t, col_a = st.columns([2, 1])
303
  with col_t:
304
  title_input = st.text_input("記事タイトル", value=st.session_state.edit_data["title"])
@@ -322,7 +284,6 @@ else:
322
  st.warning("テンプレートなしが選択されています")
323
 
324
  st.divider()
325
-
326
  st.subheader("🖼️ キャッチ画像(サムネイル)")
327
  if st.session_state.eyecatch_url:
328
  st.image(st.session_state.eyecatch_url, width=300)
@@ -330,19 +291,15 @@ else:
330
  st.session_state.eyecatch_url = ""
331
  st.rerun()
332
  else:
333
- eyecatch_file = st.file_uploader(
334
- "キャッチ画像をアップロード(JPG/PNG)",
335
- type=["jpg", "jpeg", "png"],
336
- key="eyecatch_uploader"
337
- )
338
  if eyecatch_file:
339
- col_ec1, col_ec2 = st.columns([1, 1])
340
  with col_ec1:
341
  ec_max_w = st.number_input("最大横幅(px)", min_value=200, max_value=2000, value=1200, step=100, key="ec_maxw")
342
  with col_ec2:
343
  st.write("")
344
  if st.button("☁️ アップロード", key="ec_upload_btn"):
345
- with st.spinner("キャッチ画像をアップロード中..."):
346
  url = upload_image_to_microcms(eyecatch_file, max_width=int(ec_max_w))
347
  if url:
348
  st.session_state.eyecatch_url = url
@@ -350,18 +307,12 @@ else:
350
  st.rerun()
351
 
352
  st.divider()
353
-
354
  st.subheader("📷 本文内差し込み画像")
355
  st.caption("アップロード後、ボタンでエディタに挿入またはHTMLタグをコピーしてください")
356
 
357
- inline_files = st.file_uploader(
358
- "差し込み画像(複数選択可)",
359
- type=["jpg", "jpeg", "png"],
360
- accept_multiple_files=True,
361
- key="inline_uploader"
362
- )
363
 
364
- col_in1, col_in2 = st.columns([1, 1])
365
  with col_in1:
366
  inline_max_w = st.number_input("最大横幅(px)", min_value=200, max_value=2000, value=900, step=100, key="inline_maxw")
367
  with col_in2:
@@ -386,15 +337,12 @@ else:
386
  for i, img_info in enumerate(st.session_state.inline_image_urls):
387
  max_w = img_info.get("max_w", 900)
388
  img_tag = f'<img src="{img_info["url"]}" alt="{img_info["name"]}" style="width:{max_w}px;max-width:100%;height:auto;display:block;" />'
389
-
390
  with st.container():
391
  col_il1, col_il2 = st.columns([1, 2])
392
  with col_il1:
393
  st.caption(img_info["name"])
394
  st.image(img_info["url"], width=100)
395
-
396
  with col_il2:
397
- # ✅ st.codeでコピー可能な表示(Streamlit標準のコピーアイコン付き)
398
  st.code(img_tag, language=None)
399
  col_btn1, col_btn2 = st.columns(2)
400
  with col_btn1:
@@ -410,7 +358,6 @@ else:
410
  st.divider()
411
 
412
  st.divider()
413
-
414
  st.subheader("📝 本文エディタ")
415
  col_mode1, col_mode2 = st.columns(2)
416
  with col_mode1:
@@ -445,33 +392,25 @@ else:
445
  st.session_state.working_html = rich_html
446
  except ImportError:
447
  st.error("streamlit-quill が未インストールです。")
448
-
449
  else:
450
- edited_html = st.text_area(
451
- "HTMLコード(直接編集)",
452
- value=st.session_state.working_html,
453
- height=300,
454
- key="html_editor"
455
- )
456
  st.session_state.working_html = edited_html
457
 
458
  st.markdown("---")
459
- raw_content = st.text_area("✨ AI装飾用メモ(本文をAIに渡す)", height=120, key="ai_raw")
460
- col_btn1, _ = st.columns(2)
461
- with col_btn1:
462
- if st.button("✨ AIモードで装飾"):
463
- if raw_content:
464
- try:
465
- model = genai.GenerativeModel(model_name=model_choice)
466
- prompt = f"以下の文章をブログ記事用に装飾してください。本文の内容は全く変更をしないで装飾のみに務めること。装飾はモダンでオシャレに。HTMLのみ出力すること。本文: {raw_content}"
467
- with st.spinner('AIが執筆中...'):
468
- ai_response = model.generate_content(prompt)
469
- st.session_state.working_html = clean_ai_html(ai_response.text)
470
- st.rerun()
471
- except Exception as e:
472
- st.error(f"AIエラー: {e}")
473
- else:
474
- st.warning("AI装飾用メモを入力してください")
475
 
476
  with st.expander("🪄 AIにデザインの修正を指示する"):
477
  feedback_input = st.text_input("修正内容", key="ai_feedback")
@@ -479,33 +418,22 @@ else:
479
  if feedback_input and st.session_state.working_html:
480
  try:
481
  model = genai.GenerativeModel(model_name=model_choice)
482
- feedback_prompt = f"""
483
- 現在のHTMLコードを、以下の指示に基づいて修正してください。
484
- 本文の内容は絶対に変更せず、HTMLタグやスタイルのみを変更すること。
485
- 出力は純粋なHTMLコードのみを返してください。
486
- 【現在のHTML】
487
- {st.session_state.working_html}
488
- 【修正指示】
489
- {feedback_input}
490
- """
491
- with st.spinner('再修正中...'):
492
  ai_response = model.generate_content(feedback_prompt)
493
  st.session_state.working_html = clean_ai_html(ai_response.text)
494
  st.rerun()
495
  except Exception as e:
496
  st.error(f"AIエラー: {e}")
497
 
498
- if st.session_state.working_html and title_input:
499
- st.session_state.autosave_html = st.session_state.working_html
500
- st.session_state.autosave_title = title_input
501
- st.session_state.autosave_time = time.strftime("%H:%M")
502
- st.caption(f"💾 自動保存済み({st.session_state.autosave_time})")
503
-
504
  if st.session_state.working_html:
505
  st.divider()
506
  st.subheader("👀 プレビュー")
507
- preview_html = clean_quill_html(st.session_state.working_html)
508
- render_html(preview_html, height=400)
509
 
510
  st.divider()
511
  col_save1, col_save2 = st.columns(2)
@@ -534,13 +462,8 @@ else:
534
  headers = {"X-MICROCMS-API-KEY": CMS_API_KEY, "Content-Type": "application/json"}
535
  clean_title = title_input.replace("【下書き】", "").strip()
536
  save_title = f"【下書き】{clean_title}" if status_save == "下書き" else clean_title
537
-
538
  cleaned_content = clean_quill_html(st.session_state.working_html)
539
- post_data = {
540
- "title": save_title,
541
- "content": cleaned_content,
542
- "author": author_select,
543
- }
544
  if st.session_state.eyecatch_url:
545
  post_data["eyecatch"] = st.session_state.eyecatch_url
546
 
@@ -552,9 +475,6 @@ else:
552
 
553
  if res.status_code in [200, 201]:
554
  st.success("保存完了!")
555
- st.session_state.autosave_html = ""
556
- st.session_state.autosave_title = ""
557
- st.session_state.autosave_time = None
558
  st.session_state.edit_id = None
559
  st.session_state.edit_data = {"title": "", "author": AUTHOR_LIST[0], "content": ""}
560
  st.session_state.working_html = ""
@@ -581,11 +501,7 @@ else:
581
  with col_m2:
582
  if st.button("📝 編集", key=f"edit_btn_{ma_id}"):
583
  st.session_state.edit_id = ma_id
584
- st.session_state.edit_data = {
585
- "title": ma.get("title"),
586
- "author": ma.get("author"),
587
- "content": ma.get("content", "")
588
- }
589
  st.session_state.working_html = ma.get("content", "")
590
  ec = ma.get("eyecatch")
591
  st.session_state.eyecatch_url = ec.get("url", "") if isinstance(ec, dict) else (ec or "")
 
6
  import re
7
  import io
8
  import time
9
+ from PIL import Image
10
 
 
11
  GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
12
  CMS_SERVICE_ID = os.environ.get("MICROCMS_SERVICE_ID")
13
  CMS_API_KEY = os.environ.get("MICROCMS_API_KEY")
 
18
 
19
  TEMPLATES = {
20
  "テンプレートなし": "",
21
+ "📅 イベントレポート": "<h2>イベント概要</h2><p>イベント名:<br>日時:<br>場所:</p><h2>内容</h2><p>ここに内容を書いてください。</p><h2>感想</h2><p>ここに感想を書いてください。</p>",
22
+ "📝 日常記事": "<h2>はじめに</h2><p>ここに導入を書いてください。</p><h2>本文</h2><p>ここに本文を書いてください。</p><h2>おわりに</h2><p>ここにまとめを書いてください。</p>",
23
+ "🎵 ライブ・コンサートレポート": "<h2>公演情報</h2><p>公演名:<br>日時:<br>会場:<br>出演:</p><h2>セットリスト</h2><p>1. <br>2. <br>3. </p><h2>見どころ・感想</h2><p>ここに感想を書いてください。</p>",
24
+ "📣 お知らせ": "<h2>お知らせ</h2><p>ここにお知らせ内容を書いてください。</p><h2>詳細</h2><p>日時:<br>場所:<br>参加方法:</p>",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
 
27
  if GEMINI_KEY:
28
  genai.configure(api_key=GEMINI_KEY)
29
 
30
+ # --- Quillツールバー固定&スマホ拡大抑制CSS ---
31
+ st.markdown("""
32
+ <style>
33
+ /* Quillツールバーを固定 */
34
+ .ql-toolbar {
35
+ position: sticky !important;
36
+ top: 0 !important;
37
+ z-index: 999 !important;
38
+ background: white !important;
39
+ }
40
+ /* スマホでの自動拡大を抑制 */
41
+ input, textarea, select {
42
+ font-size: 16px !important;
43
+ }
44
+ </style>
45
+ """, unsafe_allow_html=True)
46
+
47
  def clean_ai_html(text):
48
  text = re.sub(r'```html?', '', text, flags=re.IGNORECASE)
49
  text = text.replace('```', '')
 
55
  html = re.sub(r'</figure>', '</div>', html)
56
  return html.strip()
57
 
58
+ # --- HTMLタグを除去してプレーンテキストに ---
59
+ def strip_html(html):
60
+ return re.sub(r'<[^>]*?>', '', html)
61
+
62
  def render_html(html, height=600):
63
  wrapped = f"""
64
  <html><head><style>
65
+ body {{ margin:0; padding:0; font-family:sans-serif; font-size:16px; color:#111; word-break:break-word; }}
66
+ img {{ max-width:100%; height:auto; display:block; }}
67
+ p {{ margin:0.5em 0; }}
68
+ h1,h2,h3 {{ margin:0.8em 0 0.4em; }}
 
 
 
 
 
 
69
  </style></head>
70
  <body>{html}</body></html>
71
  """
 
88
 
89
  if img.width > max_width:
90
  ratio = max_width / img.width
91
+ img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS)
 
92
 
93
  buf = io.BytesIO()
94
  if img.mode in ("RGBA", "P"):
 
104
  if res.status_code not in [200, 201]:
105
  st.error(f"アップロードエラー詳細: {res.status_code} / {res.text}")
106
  return None
 
107
  return res.json().get("url")
108
  except Exception as e:
109
  st.error(f"画像アップロードエラー: {e}")
 
114
  return [], 0
115
  url = f"https://{CMS_SERVICE_ID}.microcms.io/api/v1/blog"
116
  headers = {"X-MICROCMS-API-KEY": CMS_API_KEY}
 
117
  try:
118
+ response = requests.get(url, headers=headers, params={"limit": limit, "offset": offset})
119
  response.raise_for_status()
120
  data = response.json()
121
  return data.get("contents", []), data.get("totalCount", 0)
 
127
  return []
128
  url = f"https://{CMS_SERVICE_ID}.microcms.io/api/v1/blog"
129
  headers = {"X-MICROCMS-API-KEY": CMS_API_KEY}
 
130
  try:
131
+ response = requests.get(url, headers=headers, params={"limit": 100})
132
  response.raise_for_status()
133
  return response.json().get("contents", [])
134
  except:
135
  return []
136
 
 
137
  params = st.query_params
138
 
139
  if "id" in params:
 
169
  else:
170
  with st.sidebar:
171
  st.title("⚙️ 設定")
172
+ model_choice = st.selectbox("使用するAIモデル", ["gemini-2.5-flash", "gemini-2.5-pro"], index=0)
 
 
 
 
173
  show_drafts_in_view = st.checkbox("閲覧画面で下書きも表示する", value=False)
174
 
175
  tab_view, tab_manage = st.tabs(["📖 ブログを読む", "🛠️ 記事の管理・投稿"])
 
180
  with search_col1:
181
  search_query = st.text_input("🔍 本文・タイトル検索", "", key="search_text")
182
  with search_col2:
183
+ author_filter = st.selectbox("👤 著者で絞り込み", ["全員"] + AUTHOR_LIST, key="search_author")
 
184
 
185
+ if "view_page" not in st.session_state: st.session_state.view_page = 0
186
+ if "last_search" not in st.session_state: st.session_state.last_search = ""
 
 
187
  if search_query != st.session_state.last_search:
188
  st.session_state.view_page = 0
189
  st.session_state.last_search = search_query
 
203
  pub_date = a.get("publishedAt", "-----")[:10]
204
  eyecatch = a.get("eyecatch")
205
 
206
+ if title.startswith("【下書き】") and not show_drafts_in_view:
 
207
  continue
208
 
209
+ # ✅ HTMLタグを除去したプレーンテキスト
210
+ clean_text = strip_html(content_html)
211
 
212
  if ((author_filter == "全員") or (author_filter == author)) and \
213
  (search_query.lower() in title.lower() or search_query.lower() in clean_text.lower()):
 
216
  st.image(eyecatch.get("url", ""), use_container_width=True)
217
  st.subheader(title)
218
  st.caption(f"👤 {author} | 📅 {pub_date}")
219
+ # ✅ st.captionに変更してHTMLが描画されないように
220
  snippet = clean_text[:100] + "..."
221
+ st.caption(snippet)
222
  if st.button("続きを読む", key=f"read_{a['id']}"):
223
  st.query_params.id = a["id"]
224
  st.rerun()
225
  st.divider()
226
 
227
+ if not search_query and author_filter == "全員" and total_count > ARTICLES_PER_PAGE:
228
  total_pages = -(-total_count // ARTICLES_PER_PAGE)
229
+ st.caption(f"{total_count}件中 {offset+1}〜{min(offset+ARTICLES_PER_PAGE, total_count)}件を表示")
230
+ col_prev, col_page, col_next = st.columns([1, 2, 1])
231
+ with col_prev:
232
+ if st.session_state.view_page > 0:
233
+ if st.button("← 前へ", use_container_width=True):
234
+ st.session_state.view_page -= 1
235
+ st.rerun()
236
+ with col_page:
237
+ st.markdown(f"<p style='text-align:center;'>{st.session_state.view_page + 1} / {total_pages} ページ</p>", unsafe_allow_html=True)
238
+ with col_next:
239
+ if st.session_state.view_page < total_pages - 1:
240
+ if st.button("次へ →", use_container_width=True):
241
+ st.session_state.view_page += 1
242
+ st.rerun()
 
243
 
244
  with tab_manage:
245
+ st.title("秘密の管理画面")
246
  password = st.text_input("合言葉", type="password")
247
 
248
  if password == ADMIN_PASSWORD:
 
255
  if "eyecatch_url" not in st.session_state: st.session_state.eyecatch_url = ""
256
  if "inline_image_urls" not in st.session_state: st.session_state.inline_image_urls = []
257
  if "delete_confirm_id" not in st.session_state: st.session_state.delete_confirm_id = None
 
 
 
258
  if "quill_key" not in st.session_state: st.session_state.quill_key = 0
 
 
259
 
260
  st.divider()
261
  mode_label = f"📝 記事の編集 (ID: {st.session_state.edit_id})" if st.session_state.edit_id else "✍️ 新規記事の投稿"
262
  st.subheader(mode_label)
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  col_t, col_a = st.columns([2, 1])
265
  with col_t:
266
  title_input = st.text_input("記事タイトル", value=st.session_state.edit_data["title"])
 
284
  st.warning("テンプレートなしが選択されています")
285
 
286
  st.divider()
 
287
  st.subheader("🖼️ キャッチ画像(サムネイル)")
288
  if st.session_state.eyecatch_url:
289
  st.image(st.session_state.eyecatch_url, width=300)
 
291
  st.session_state.eyecatch_url = ""
292
  st.rerun()
293
  else:
294
+ eyecatch_file = st.file_uploader("キャッチ画像をアップロード(JPG/PNG)", type=["jpg", "jpeg", "png"], key="eyecatch_uploader")
 
 
 
 
295
  if eyecatch_file:
296
+ col_ec1, col_ec2 = st.columns(2)
297
  with col_ec1:
298
  ec_max_w = st.number_input("最大横幅(px)", min_value=200, max_value=2000, value=1200, step=100, key="ec_maxw")
299
  with col_ec2:
300
  st.write("")
301
  if st.button("☁️ アップロード", key="ec_upload_btn"):
302
+ with st.spinner("アップロード中..."):
303
  url = upload_image_to_microcms(eyecatch_file, max_width=int(ec_max_w))
304
  if url:
305
  st.session_state.eyecatch_url = url
 
307
  st.rerun()
308
 
309
  st.divider()
 
310
  st.subheader("📷 本文内差し込み画像")
311
  st.caption("アップロード後、ボタンでエディタに挿入またはHTMLタグをコピーしてください")
312
 
313
+ inline_files = st.file_uploader("差し込み画像(複数選択可)", type=["jpg", "jpeg", "png"], accept_multiple_files=True, key="inline_uploader")
 
 
 
 
 
314
 
315
+ col_in1, col_in2 = st.columns(2)
316
  with col_in1:
317
  inline_max_w = st.number_input("最大横幅(px)", min_value=200, max_value=2000, value=900, step=100, key="inline_maxw")
318
  with col_in2:
 
337
  for i, img_info in enumerate(st.session_state.inline_image_urls):
338
  max_w = img_info.get("max_w", 900)
339
  img_tag = f'<img src="{img_info["url"]}" alt="{img_info["name"]}" style="width:{max_w}px;max-width:100%;height:auto;display:block;" />'
 
340
  with st.container():
341
  col_il1, col_il2 = st.columns([1, 2])
342
  with col_il1:
343
  st.caption(img_info["name"])
344
  st.image(img_info["url"], width=100)
 
345
  with col_il2:
 
346
  st.code(img_tag, language=None)
347
  col_btn1, col_btn2 = st.columns(2)
348
  with col_btn1:
 
358
  st.divider()
359
 
360
  st.divider()
 
361
  st.subheader("📝 本文エディタ")
362
  col_mode1, col_mode2 = st.columns(2)
363
  with col_mode1:
 
392
  st.session_state.working_html = rich_html
393
  except ImportError:
394
  st.error("streamlit-quill が未インストールです。")
 
395
  else:
396
+ edited_html = st.text_area("HTMLコード(直接編集)", value=st.session_state.working_html, height=300, key="html_editor")
 
 
 
 
 
397
  st.session_state.working_html = edited_html
398
 
399
  st.markdown("---")
400
+ raw_content = st.text_area("✨ AI装飾用メモ", height=120, key="ai_raw")
401
+ if st.button("✨ AIモードで装飾"):
402
+ if raw_content:
403
+ try:
404
+ model = genai.GenerativeModel(model_name=model_choice)
405
+ prompt = f"以下の文章をブログ記事用に装飾してください。本文の内容は全く変更をしないで装飾のみに務めること。装飾はモダンでオシャレに。HTMLのみ出力すること。本文: {raw_content}"
406
+ with st.spinner("AIが執筆中..."):
407
+ ai_response = model.generate_content(prompt)
408
+ st.session_state.working_html = clean_ai_html(ai_response.text)
409
+ st.rerun()
410
+ except Exception as e:
411
+ st.error(f"AIエラー: {e}")
412
+ else:
413
+ st.warning("AI装飾用メモを入力してください")
 
 
414
 
415
  with st.expander("🪄 AIにデザインの修正を指示する"):
416
  feedback_input = st.text_input("修正内容", key="ai_feedback")
 
418
  if feedback_input and st.session_state.working_html:
419
  try:
420
  model = genai.GenerativeModel(model_name=model_choice)
421
+ feedback_prompt = f"""現在のHTMLコードを、以下の指示に基づいて修正してください。
422
+ 本文の内容は絶対に変更せず、HTMLタグやスタイルのみを変更すること。
423
+ 出力は純粋なHTMLコードのみを返してください。
424
+ 【現在のHTML】{st.session_state.working_html}
425
+ 【修正指示】{feedback_input}"""
426
+ with st.spinner("再修正中..."):
 
 
 
 
427
  ai_response = model.generate_content(feedback_prompt)
428
  st.session_state.working_html = clean_ai_html(ai_response.text)
429
  st.rerun()
430
  except Exception as e:
431
  st.error(f"AIエラー: {e}")
432
 
 
 
 
 
 
 
433
  if st.session_state.working_html:
434
  st.divider()
435
  st.subheader("👀 プレビュー")
436
+ render_html(clean_quill_html(st.session_state.working_html), height=400)
 
437
 
438
  st.divider()
439
  col_save1, col_save2 = st.columns(2)
 
462
  headers = {"X-MICROCMS-API-KEY": CMS_API_KEY, "Content-Type": "application/json"}
463
  clean_title = title_input.replace("【下書き】", "").strip()
464
  save_title = f"【下書き】{clean_title}" if status_save == "下書き" else clean_title
 
465
  cleaned_content = clean_quill_html(st.session_state.working_html)
466
+ post_data = {"title": save_title, "content": cleaned_content, "author": author_select}
 
 
 
 
467
  if st.session_state.eyecatch_url:
468
  post_data["eyecatch"] = st.session_state.eyecatch_url
469
 
 
475
 
476
  if res.status_code in [200, 201]:
477
  st.success("保存完了!")
 
 
 
478
  st.session_state.edit_id = None
479
  st.session_state.edit_data = {"title": "", "author": AUTHOR_LIST[0], "content": ""}
480
  st.session_state.working_html = ""
 
501
  with col_m2:
502
  if st.button("📝 編集", key=f"edit_btn_{ma_id}"):
503
  st.session_state.edit_id = ma_id
504
+ st.session_state.edit_data = {"title": ma.get("title"), "author": ma.get("author"), "content": ma.get("content", "")}
 
 
 
 
505
  st.session_state.working_html = ma.get("content", "")
506
  ec = ma.get("eyecatch")
507
  st.session_state.eyecatch_url = ec.get("url", "") if isinstance(ec, dict) else (ec or "")