ginipick commited on
Commit
59177aa
Β·
verified Β·
1 Parent(s): d5a1f72

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +648 -309
app.py CHANGED
@@ -1,294 +1,546 @@
1
  # ──────────────────────────────── Imports ────────────────────────────────
2
- import os, json, re, logging, requests, markdown, time
3
  from datetime import datetime
4
 
5
  import streamlit as st
6
  import anthropic
7
  from gradio_client import Client
8
- # from bs4 import BeautifulSoup # ν•„μš” μ‹œ 주석 ν•΄μ œ
 
9
 
10
- # ──────────────────────────────── ν™˜κ²½ λ³€μˆ˜ / μƒμˆ˜ ───────────────────────────
11
  ANTHROPIC_KEY = os.getenv("API_KEY", "")
12
- BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # 이름 μœ μ§€
13
  BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
14
  IMAGE_API_URL = "http://211.233.58.201:7896"
15
  MAX_TOKENS = 7_999
16
 
17
- # λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ μ •μ˜ (ν•œκΈ€ν™”)
18
  BLOG_TEMPLATES = {
19
- "standard": "ν‘œμ€€ 8단계 ν”„λ ˆμž„μ›Œν¬ λΈ”λ‘œκ·Έ",
20
- "tutorial": "단계별 νŠœν† λ¦¬μ–Ό ν˜•μ‹",
21
- "review": "μ œν’ˆ/μ„œλΉ„μŠ€ 리뷰 ν˜•μ‹",
22
- "storytelling": "μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹",
23
- "seo_optimized": "SEO μ΅œμ ν™” λΈ”λ‘œκ·Έ"
 
24
  }
25
 
26
  BLOG_TONES = {
27
- "professional": "전문적이고 곡식적인 톀",
28
- "casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”μ²΄ 쀑심 톀",
29
- "humorous": "μœ λ¨ΈλŸ¬μŠ€ν•œ μ ‘κ·Ό",
30
- "storytelling": "이야기 μ€‘μ‹¬μ˜ μ ‘κ·Ό"
31
  }
32
 
33
- # 예제 λΈ”λ‘œκ·Έ 주제
34
  EXAMPLE_TOPICS = {
35
- "example1": "2025λ…„ 바뀐 뢀동산 μ„ΈκΈˆ μ œλ„: 일반 가정에 λ―ΈμΉ˜λŠ” 영ν–₯κ³Ό μ ˆμ„Έ μ „λž΅",
36
- "example2": "2025λ…„ 여름 μ „κ΅­ 지역별 λŒ€ν‘œ μΆ•μ œ 총정리와 μˆ¨μ€ λͺ…μ†Œ μΆ”μ²œ",
37
- "example3": "2025λ…„ μ£Όλͺ©ν•΄μ•Ό ν•  μ‹ μ„±μž₯ μ‚°μ—… 투자 κ°€μ΄λ“œ: 인곡지λŠ₯ κ΄€λ ¨ 발꡴ μ „λž΅"
38
  }
39
 
40
- # ──────────────────────────────── λ‘œκΉ… ──────────────────────────────────────
41
  logging.basicConfig(level=logging.INFO,
42
  format="%(asctime)s - %(levelname)s - %(message)s")
43
 
44
- # ──────────────────────────────── Anthropic Client ─────────────────────────
45
- client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
 
 
46
 
47
- # ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
48
- # ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
49
- def get_system_prompt(template="standard", tone="professional", word_count=1750, include_search_results=False) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  base_prompt = """
51
- 당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
52
- λ…μž μ—°κ²° 단계
53
- 1.1. κ³΅κ°λŒ€ ν˜•μ„±μ„ μœ„ν•œ μΉœκ·Όν•œ 인사
54
- 1.2. λ…μžμ˜ μ‹€μ œ 고민을 λ°˜μ˜ν•œ λ„μž… 질문
55
- 1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적 관심 μœ λ„
56
- 문제 μ •μ˜ 단계
57
- 2.1. λ…μžμ˜ 페인포인트 ꡬ체화
58
- 2.2. 문제의 μ‹œκΈ‰μ„±κ³Ό 영ν–₯도 뢄석
59
- 2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
60
- μ „λ¬Έμ„± μž…μ¦ 단계
61
- 3.1. 객관적 데이터 기반 뢄석
62
- 3.2. μ „λ¬Έκ°€ 견해와 연ꡬ κ²°κ³Ό 인용
63
- 3.3. μ‹€μ œ 사둀λ₯Ό ν†΅ν•œ 문제 ꡬ체화
64
- μ†”λ£¨μ…˜ 제곡 단계
65
- 4.1. 단계별 μ‹€μ²œ κ°€μ΄λ“œλΌμΈ μ œμ‹œ
66
- 4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ ꡬ체적 팁
67
- 4.3. μ˜ˆμƒ μž₯μ• λ¬Όκ³Ό 극볡 λ°©μ•ˆ 포함
68
- 신뒰도 κ°•ν™” 단계
69
- 5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
70
- 5.2. ꡬ체적 μ‚¬μš©μž ν›„κΈ° 인용
71
- 5.3. 객관적 λ°μ΄ν„°λ‘œ 효과 μž…μ¦
72
- 행동 μœ λ„ 단계
73
- 6.1. λͺ…ν™•ν•œ 첫 μ‹€μ²œ 단계 μ œμ‹œ
74
- 6.2. μ‹œκΈ‰μ„±μ„ κ°•μ‘°ν•œ 행동 촉ꡬ
75
- 6.3. μ‹€μ²œ 동기 λΆ€μ—¬ μš”μ†Œ 포함
76
- μ§„μ •μ„± κ°•ν™” 단계
77
- 7.1. μ†”λ£¨μ…˜μ˜ ν•œκ³„ 투λͺ…ν•˜κ²Œ 곡개
78
- 7.2. κ°œμΈλ³„ 차이 쑴재 인정
79
- 7.3. ν•„μš” 쑰건과 μ£Όμ˜μ‚¬ν•­ λͺ…μ‹œ
80
- 관계 지속 단계
81
- 8.1. μ§„μ •μ„± μžˆλŠ” 감사 인사
82
- 8.2. λ‹€μŒ 컨텐츠 예고둜 κΈ°λŒ€κ° μ‘°μ„±
83
- 8.3. μ†Œν†΅ 채널 μ•ˆλ‚΄
 
 
 
 
 
 
 
 
84
  """
85
 
86
- # ν…œν”Œλ¦Ώλ³„ μΆ”κ°€ μ§€μΉ¨
87
  template_guides = {
88
  "tutorial": """
89
- 이 λΈ”λ‘œκ·ΈλŠ” νŠœν† λ¦¬μ–Ό ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
90
- - λͺ…ν™•ν•œ λͺ©ν‘œμ™€ μ΅œμ’… κ²°κ³Όλ¬Ό λ¨Όμ € μ œμ‹œ
91
- - λ‹¨κ³„λ³„λ‘œ λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„λœ κ³Όμ • μ„€λͺ…
92
- - 각 λ‹¨κ³„λ§ˆλ‹€ 이미지λ₯Ό μ‚½μž…ν•  μœ„μΉ˜ ν‘œμ‹œ
93
- - μ˜ˆμƒ μ†Œμš” μ‹œκ°„κ³Ό λ‚œμ΄λ„ λͺ…μ‹œ
94
- - ν•„μš”ν•œ λ„κ΅¬λ‚˜ 사전 지식 μ•ˆλ‚΄
95
- - λ¬Έμ œν•΄κ²° 팁과 자주 λ°œμƒν•˜λŠ” μ‹€μˆ˜ 포함
96
- - μ™„λ£Œ ν›„ λ‹€μŒ λ‹¨κ³„λ‚˜ μ‘μš©λ²• μ œμ•ˆ
97
  """,
98
-
99
  "review": """
100
- 이 λΈ”λ‘œκ·ΈλŠ” 리뷰 ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
101
- - 객관적 사싀과 주관적 평가 ꡬ뢄
102
- - λͺ…ν™•ν•œ 평가 κΈ°μ€€ μ œμ‹œ
103
- - μž₯점과 단점 κ· ν˜•μžˆκ²Œ μ„œμˆ 
104
- - μœ μ‚¬ μ œν’ˆ/μ„œλΉ„μŠ€μ™€ 비ꡐ
105
- - λˆ„κ΅¬μ—κ²Œ μ ν•©ν•œμ§€ νƒ€κ²Ÿ μ„€λͺ…
106
- - ꡬ체적인 μ‚¬μš© κ²½ν—˜κ³Ό κ²°κ³Ό 포함
107
- - μ΅œμ’… μΆ”μ²œ 여뢀와 λŒ€μ•ˆ μ œμ‹œ
108
  """,
109
-
110
  "storytelling": """
111
- 이 λΈ”λ‘œκ·ΈλŠ” μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
112
- - μ‹€μ œ μΈλ¬Όμ΄λ‚˜ μ‚¬λ‘€λ‘œ μ‹œμž‘
113
- - 문제 상황과 감정적 μ—°κ²° κ°•ν™”
114
- - κ°ˆλ“±κ³Ό ν•΄κ²°κ³Όμ • μ€‘μ‹¬μ˜ λ‚΄λŸ¬ν‹°λΈŒ
115
- - κ΅ν›ˆκ³Ό 배움을 μžμ—°μŠ€λŸ½κ²Œ 포함
116
- - λ…μžκ°€ 곡감할 수 μžˆλŠ” 감정선 μœ μ§€
117
- - 이야기와 μœ μš©ν•œ μ •λ³΄μ˜ κ· ν˜• μœ μ§€
118
- - λ…μžμ—κ²Œ μžμ‹ μ˜ 이야기λ₯Ό μƒκ°ν•΄λ³΄κ²Œ μœ λ„
119
  """,
120
-
121
  "seo_optimized": """
122
- 이 λΈ”λ‘œκ·ΈλŠ” SEO μ΅œμ ν™” ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
123
- - 핡심 ν‚€μ›Œλ“œλ₯Ό 제λͺ©, μ†Œμ œλͺ©, 첫 단락에 배치
124
- - κ΄€λ ¨ ν‚€μ›Œλ“œλ₯Ό μžμ—°μŠ€λŸ½κ²Œ 본문에 λΆ„μ‚°
125
- - 300-500자 λΆ„λŸ‰μ˜ λͺ…ν™•ν•œ 단락 ꡬ성
126
- - 질문 ν˜•μ‹μ˜ μ†Œμ œλͺ© ν™œμš©
127
- - λͺ©λ‘, ν‘œ, κ°•μ‘° ν…μŠ€νŠΈ λ“± λ‹€μ–‘ν•œ μ„œμ‹ ν™œμš©
128
- - λ‚΄λΆ€ 링크 μ‚½μž… μœ„μΉ˜ ν‘œμ‹œ
129
- - 2000-3000자 μ΄μƒμ˜ μΆ©λΆ„ν•œ μ½˜ν…μΈ  제곡
130
  """
131
  }
132
-
133
- # 톀별 μΆ”κ°€ μ§€μΉ¨
134
  tone_guides = {
135
- "professional": "전문적이고 κΆŒμœ„μžˆλŠ” μ–΄μ‘°λ‘œ μž‘μ„±ν•˜λ˜, μ „λ¬Έ μš©μ–΄λŠ” 적절히 μ„€λͺ…ν•΄ μ£Όμ„Έμš”. 데이터와 연ꡬ κ²°κ³Όλ₯Ό μ€‘μ‹¬μœΌλ‘œ 논리적 흐름을 μœ μ§€ν•˜μ„Έμš”.",
136
- "casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”ν•˜λ“― νŽΈμ•ˆν•œ μ–΄μ‘°λ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. '~λ„€μš”', '~ν•΄μš”' 같은 λŒ€ν™”μ²΄λ₯Ό μ‚¬μš©ν•˜κ³ , 개인적 κ²½ν—˜κ³Ό λΉ„μœ λ₯Ό 톡해 λ‚΄μš©μ„ μ „λ‹¬ν•˜μ„Έμš”.",
137
- "humorous": "μœ λ¨Έμ™€ μž¬μΉ˜μžˆλŠ” ν‘œν˜„μ„ 적절히 ν™œμš©ν•΄ μ£Όμ„Έμš”. μž¬λ―ΈμžˆλŠ” λΉ„μœ λ‚˜ μ˜ˆμ‹œ, κ°€λ²Όμš΄ 농담을 ν¬ν•¨ν•˜λ˜, μ •λ³΄μ˜ μ •ν™•μ„±κ³Ό μœ μš©μ„±μ€ μœ μ§€ν•˜μ„Έμš”.",
138
- "storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― 감성적이고 λͺ°μž…감 μžˆλŠ” ν†€μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. 인물, λ°°κ²½, κ°ˆλ“±, 해결과정이 λ‹΄κΈ΄ λ‚΄λŸ¬ν‹°λΈŒ ꡬ쑰λ₯Ό ν™œμš©ν•˜μ„Έμš”."
139
  }
140
-
141
- # 검색 κ²°κ³Ό ν™œμš© μ§€μΉ¨ μΆ”κ°€
142
  search_guide = """
143
- 검색 κ²°κ³Ό ν™œμš© μ§€μΉ¨:
144
- - 제곡된 검색 κ²°κ³Όμ—μ„œ 핡심 정보λ₯Ό μ •ν™•ν•˜κ²Œ μΆ”μΆœν•˜μ—¬ λ°˜μ˜ν•˜μ„Έμš”
145
- - 데이터, 톡계, μ‚¬λ‘€λŠ” 검색 결과의 μ΅œμ‹  정보λ₯Ό ν™œμš©ν•˜μ„Έμš”
146
- - 인용 μ‹œ 좜처λ₯Ό λ³Έλ¬Έ 내에 μ–ΈκΈ‰ν•˜μ„Έμš” (예: "OO μ‚¬μ΄νŠΈμ— λ”°λ₯΄λ©΄...")
147
- - λΈ”λ‘œκ·Έ λ§ˆμ§€λ§‰μ— "μ°Έκ³  자료" μ„Ήμ…˜μ„ μΆ”κ°€ν•˜κ³  μ‚¬μš©ν•œ μ£Όμš” 좜처λ₯Ό 링크와 ν•¨κ»˜ λ‚˜μ—΄ν•˜μ„Έμš”
148
- - μΆœμ²˜κ°€ μ—†λŠ” μ •λ³΄λŠ” "일반적으둜..." λ“±μ˜ ν‘œν˜„μ„ μ‚¬μš©ν•˜μ„Έμš”
149
- - 검색 결과의 정보가 상좩할 경우, λ‹€μ–‘ν•œ 관점을 κ· ν˜•μžˆκ²Œ μ œμ‹œν•˜μ„Έμš”
150
  """
151
-
152
- # μ΅œμ’… ν”„λ‘¬ν”„νŠΈ μ‘°ν•©
153
- final_prompt = base_prompt
154
-
155
- # μ„ νƒλœ ν…œν”Œλ¦Ώ μ§€μΉ¨ μΆ”κ°€
156
- if template in template_guides:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  final_prompt += "\n" + template_guides[template]
158
-
159
- # μ„ νƒλœ 톀 μ§€μΉ¨ μΆ”κ°€
160
  if tone in tone_guides:
161
- final_prompt += f"\n\nν†€μ•€λ§€λ„ˆ: {tone_guides[tone]}"
162
-
163
- # 검색 κ²°κ³Ό ν™œμš© μ§€μΉ¨ μΆ”κ°€ (μ›Ή 검색 ν™œμ„±ν™” μ‹œ)
164
  if include_search_results:
165
  final_prompt += f"\n\n{search_guide}"
166
-
167
- # κΈ€μž 수 μ§€μΉ¨ μΆ”κ°€
168
- final_prompt += f"\n\nμž‘μ„± μ‹œ μ€€μˆ˜μ‚¬ν•­\n9.1. κΈ€μž 수: {word_count-250}-{word_count+250}자 λ‚΄μ™Έ\n9.2. 문단 길이: 3-4λ¬Έμž₯ 이내\n9.3. μ‹œκ°μ  ꡬ뢄: μ†Œμ œλͺ©, ꡬ뢄선, 번호 λͺ©λ‘ ν™œμš©\n9.4. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ\n9.5. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©"
169
-
 
 
 
 
 
 
 
 
 
 
 
170
  return final_prompt
171
 
172
- # ──────────────────────────────── Brave Search API ─────────────────────────
173
- def brave_search(query: str, count: int = 20): # 기본값을 20으둜 λ³€κ²½
 
174
  """
175
- Brave Web Search API 호좜 β†’ list[dict]
176
- λ°˜ν™˜ ν•„λ“œ: index, title, link, snippet, displayed_link
177
  """
178
  if not BRAVE_KEY:
179
- raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) ν™˜κ²½λ³€μˆ˜κ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
180
 
181
  headers = {
182
  "Accept": "application/json",
183
  "Accept-Encoding": "gzip",
184
  "X-Subscription-Token": BRAVE_KEY
185
  }
186
- params = {"q": query, "count": str(count)} # 카운트 νŒŒλΌλ―Έν„° 전달
187
-
188
- for attempt in range(3): # μ΅œλŒ€ 3번 μž¬μ‹œλ„
189
  try:
190
  r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
191
  r.raise_for_status()
192
  data = r.json()
193
-
194
- # κ²°κ³Ό ν˜•μ‹ 확인 및 λ‘œκΉ…
195
- logging.info(f"Brave 검색 κ²°κ³Ό 데이터 ꡬ쑰: {list(data.keys())}")
196
-
197
  raw = data.get("web", {}).get("results") or data.get("results", [])
198
  if not raw:
199
- logging.warning(f"Brave 검색 κ²°κ³Ό μ—†μŒ. 응닡: {data}")
200
- raise ValueError("검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€")
201
-
202
  arts = []
203
- for i, res in enumerate(raw[:count], 1): # count만큼 반볡
204
- url = res.get("url", res.get("link", ""))
205
- host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
206
  arts.append({
207
  "index": i,
208
- "title": res.get("title", "제λͺ© μ—†μŒ"),
209
  "link": url,
210
- "snippet": res.get("description", res.get("text", "λ‚΄μš© μ—†μŒ")),
211
  "displayed_link": host
212
  })
213
-
214
- logging.info(f"Brave 검색 성곡: {len(arts)}개 κ²°κ³Ό")
215
  return arts
216
-
217
  except Exception as e:
218
- logging.error(f"Brave 검색 μ‹€νŒ¨ (μ‹œλ„ {attempt+1}/3): {e}")
219
- if attempt < 2: # λ§ˆμ§€λ§‰ μ‹œλ„κ°€ μ•„λ‹ˆλ©΄ λŒ€κΈ° ν›„ μž¬μ‹œλ„
220
  time.sleep(2)
221
-
222
- return [] # λͺ¨λ“  μ‹œλ„ μ‹€νŒ¨ μ‹œ 빈 λͺ©λ‘ λ°˜ν™˜
223
 
224
  def mock_results(query: str) -> str:
225
- """검색 API μ‹€νŒ¨ μ‹œ 가상 검색 κ²°κ³Ό 제곡"""
226
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
227
- return (f"# 검색 κ²°κ³Ό λŒ€μ²΄ λ‚΄μš© (생성: {ts})\n\n"
228
- f"검색 API 호좜이 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. 주제 '{query}'에 λŒ€ν•΄ κΈ°μ‘΄ 지식을 ν™œμš©ν•΄ λ‹΅λ³€ν•΄ μ£Όμ„Έμš”.\n\n"
229
- f"λ‹€μŒ λ‚΄μš©μ΄ 도움이 될 수 μžˆμŠ΅λ‹ˆλ‹€:\n\n"
230
- f"- {query}의 κΈ°λ³Έ κ°œλ…κ³Ό μ€‘μš”μ„±\n"
231
- f"- 일반적으둜 μ•Œλ €μ§„ κ΄€λ ¨ 톡계와 νŠΈλ Œλ“œ\n"
232
- f"- ν•΄λ‹Ή μ£Όμ œμ— λŒ€ν•œ μ „λ¬Έκ°€λ“€μ˜ 일반적인 견해\n"
233
- f"- λ…μžλ“€μ΄ μ‹€μ œλ‘œ κΆκΈˆν•΄ν•  λ§Œν•œ μ§ˆλ¬Έλ“€\n\n"
234
- f"μ°Έκ³ : 이 λ‚΄μš©μ€ μ‹€μ‹œκ°„ 검색 κ²°κ³Όκ°€ μ•„λ‹Œ λŒ€μ²΄ μ•ˆλ‚΄μž…λ‹ˆλ‹€.\n\n")
235
 
236
  def do_web_search(query: str) -> str:
237
- """μ›Ή 검색 μˆ˜ν–‰ 및 κ²°κ³Ό ν¬λ§·νŒ…"""
238
  try:
239
  arts = brave_search(query, 20)
240
  if not arts:
241
- logging.warning("검색 κ²°κ³Ό μ—†μŒ, λŒ€μ²΄ μ½˜ν…μΈ  μ‚¬μš©")
242
  return mock_results(query)
243
-
244
- hdr = "# μ›Ή 검색 κ²°κ³Ό\n제곡된 검색 결과의 정보λ₯Ό μ •ν™•ν•˜κ²Œ λ°˜μ˜ν•˜μ—¬ 신뒰도 높은 λΈ”λ‘œκ·Έλ₯Ό μž‘μ„±ν•˜μ„Έμš”. 정보 인용 μ‹œ 좜처λ₯Ό λͺ…μ‹œν•˜κ³ , λΈ”λ‘œκ·Έ λ§ˆμ§€λ§‰μ— μ°Έκ³  자료 μ„Ήμ…˜μ„ μΆ”κ°€ν•˜μ„Έμš”.\n\n"
245
  body = "\n".join(
246
  f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
247
- f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
248
  for a in arts
249
  )
250
  return hdr + body
251
  except Exception as e:
252
- logging.error(f"μ›Ή 검색 전체 ν”„λ‘œμ„ΈμŠ€ μ‹€νŒ¨: {str(e)}")
253
  return mock_results(query)
254
 
255
- # ──────────────────────────────── 이미지 Β· λ³€ν™˜ μœ ν‹Έ ────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
257
- if not prompt: return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
 
 
258
  try:
259
  res = Client(IMAGE_API_URL).predict(
260
  prompt=prompt, width=w, height=h, guidance=g,
261
  inference_steps=steps, seed=seed,
262
  do_img2img=False, init_image=None,
263
  image2image_strength=0.8, resize_img=True,
264
- api_name="/generate_image")
 
265
  return res[0], f"Seed: {res[1]}"
266
  except Exception as e:
267
- logging.error(e); return None, str(e)
 
268
 
269
- def extract_image_prompt(blog: str, topic: str):
270
- sys = f"λ‹€μŒ κΈ€λ‘œλΆ€ν„° μ˜μ–΄ 1쀄 이미지 ν”„λ‘¬ν”„νŠΈ 생성:\n{topic}"
 
 
 
 
 
271
  try:
 
272
  res = client.messages.create(
273
  model="claude-3-7-sonnet-20250219",
274
- max_tokens=80, system=sys,
275
- messages=[{"role": "user", "content": blog}]
 
276
  )
277
  return res.content[0].text.strip()
278
  except Exception:
 
279
  return f"A professional photo related to {topic}, high quality"
280
 
281
  def md_to_html(md: str, title="Ginigen Blog"):
 
282
  return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
283
 
284
  def keywords(text: str, top=5):
285
- return " ".join(re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text).split()[:top])
 
 
286
 
287
  # ──────────────────────────────── Streamlit UI ────────────────────────────
288
  def ginigen_app():
289
- st.title("μ§€λ‹ˆμ   λΈ”λ‘œκ·Έ")
290
 
291
- # μ„Έμ…˜ κΈ°λ³Έκ°’ - μ„Έμ…˜ μƒνƒœκ°€ 이미 μžˆλŠ” 경우 μ„€μ •ν•˜μ§€ μ•ŠμŒ
292
  if "ai_model" not in st.session_state:
293
  st.session_state.ai_model = "claude-3-7-sonnet-20250219"
294
  if "messages" not in st.session_state:
@@ -297,143 +549,229 @@ def ginigen_app():
297
  st.session_state.auto_save = True
298
  if "generate_image" not in st.session_state:
299
  st.session_state.generate_image = False
300
- if "use_web_search" not in st.session_state:
301
- st.session_state.use_web_search = False
302
  if "blog_template" not in st.session_state:
303
- st.session_state.blog_template = "standard"
304
  if "blog_tone" not in st.session_state:
305
  st.session_state.blog_tone = "professional"
306
  if "word_count" not in st.session_state:
307
  st.session_state.word_count = 1750
308
 
309
- # ── μ‚¬μ΄λ“œλ°” 컨트둀
310
  sb = st.sidebar
311
- sb.title("λΈ”λ‘œκ·Έ μ„€μ •")
312
-
313
- # λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ 선택
314
- sb.subheader("λΈ”λ‘œκ·Έ μŠ€νƒ€μΌ μ„€μ •")
315
- sb.selectbox("λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ", options=list(BLOG_TEMPLATES.keys()),
316
- format_func=lambda x: BLOG_TEMPLATES[x],
317
- key="blog_template")
318
 
319
- sb.selectbox("λΈ”λ‘œκ·Έ 톀", options=list(BLOG_TONES.keys()),
320
- format_func=lambda x: BLOG_TONES[x],
321
- key="blog_tone")
 
 
 
 
322
 
323
- sb.slider("λΈ”λ‘œκ·Έ 길이 (단어 수)", 800, 3000, key="word_count")
 
 
 
 
 
324
 
325
- # 예제 주제 선택
326
- sb.subheader("예제 주제")
327
 
328
- col1, col2, col3 = sb.columns(3)
329
-
330
- # μˆ˜μ •: 예제 선택 μ‹œ 직접 μ²˜λ¦¬ν•˜λ„λ‘ λ³€κ²½
331
- if col1.button("뢀동산 μ„ΈκΈˆ", key="ex1"):
332
- # 예제 주제λ₯Ό μž…λ ₯으둜 μ¦‰μ‹œ 처리 (rerun 없이)
333
  process_example(EXAMPLE_TOPICS["example1"])
334
-
335
- if col2.button("여름 μΆ•μ œ", key="ex2"):
336
  process_example(EXAMPLE_TOPICS["example2"])
337
-
338
- if col3.button("투자 κ°€μ΄λ“œ", key="ex3"):
339
  process_example(EXAMPLE_TOPICS["example3"])
340
 
341
- sb.subheader("기타 μ„€μ •")
342
- sb.toggle("μžλ™ μ €μž₯", key="auto_save")
343
- sb.toggle("이미지 μžλ™ 생성", key="generate_image")
 
 
 
344
 
345
- # μ›Ή 검색 ν† κΈ€ (λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•΄ μœ μ§€ν•˜λ˜ 기본값은 False)
346
- search_enabled = sb.toggle("μ›Ή 검색 μ‚¬μš©", value=False, key="use_web_search")
347
- if search_enabled:
348
- st.warning("⚠️ μ›Ή 검색 κΈ°λŠ₯은 ν˜„μž¬ λΆˆμ•ˆμ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 검색 κ²°κ³Όκ°€ μ—†μœΌλ©΄ κΈ°λ³Έ μ§€μ‹μœΌλ‘œ λŒ€μ²΄λ©λ‹ˆλ‹€.")
349
 
350
- # ── 졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (λ§ˆν¬λ‹€μš΄ / HTML)
351
  latest_blog = next(
352
- (m["content"] for m in reversed(st.session_state.messages)
353
- if m["role"] == "assistant" and m["content"].strip()), None)
354
-
 
355
  if latest_blog:
356
- title = re.search(r"# (.*?)(\n|$)", latest_blog)
357
- title = title.group(1).strip() if title else "blog"
358
- sb.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
359
- c1, c2 = sb.columns(2)
360
- c1.download_button("λ§ˆν¬λ‹€μš΄", latest_blog,
361
  file_name=f"{title}.md", mime="text/markdown")
362
- c2.download_button("HTML", md_to_html(latest_blog, title),
363
  file_name=f"{title}.html", mime="text/html")
364
 
365
- # ── JSON λŒ€ν™” 기둝 μ—…λ‘œλ“œ
366
- up = sb.file_uploader("λŒ€ν™” 기둝 뢈러였기 (.json)", type=["json"])
367
  if up:
368
  try:
369
  st.session_state.messages = json.load(up)
370
- sb.success("λŒ€ν™” 기둝 뢈러였기 μ™„λ£Œ")
371
  except Exception as e:
372
- sb.error(f"뢈러였기 μ‹€νŒ¨: {e}")
373
-
374
- # ── JSON λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
375
- if sb.button("λŒ€ν™” 기둝 JSON λ‹€μš΄λ‘œλ“œ"):
376
- sb.download_button("μ €μž₯", json.dumps(st.session_state.messages,
377
- ensure_ascii=False, indent=2),
378
- file_name="chat_history.json",
379
- mime="application/json")
 
 
380
 
381
- # ── κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  for m in st.session_state.messages:
383
  with st.chat_message(m["role"]):
384
  st.markdown(m["content"])
385
  if "image" in m:
386
  st.image(m["image"], caption=m.get("image_caption", ""))
387
 
388
- # ── μ‚¬μš©μž μž…λ ₯ 처리
389
- prompt = st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?")
390
-
391
  if prompt:
392
- process_input(prompt)
393
-
394
 
395
  def process_example(topic):
396
- """예제 주제λ₯Ό 직접 μ²˜λ¦¬ν•˜λŠ” ν•¨μˆ˜ (rerun 없이)"""
397
- process_input(topic)
398
-
399
-
400
- def process_input(prompt):
401
- """μ‚¬μš©μž μž…λ ₯ 처리 ν•¨μˆ˜ (일반 μž…λ ₯κ³Ό 예제 μž…λ ₯ λͺ¨λ‘ 처리)"""
402
- st.session_state.messages.append({"role": "user", "content": prompt})
403
- with st.chat_message("user"): st.markdown(prompt)
404
 
 
 
 
 
 
 
 
 
405
  with st.chat_message("assistant"):
406
- placeholder = st.empty(); answer = ""
407
-
408
- # μ›Ή 검색 ν™œμ„±ν™” μ—¬λΆ€ 확인
409
- use_web_search = st.session_state.use_web_search
 
410
 
411
  try:
412
- # μ„ νƒλœ ν…œν”Œλ¦Ώ, 톀, 단어 수둜 μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ 생성
413
- # μ›Ή 검색 ν™œμ„±ν™” μ‹œ 이λ₯Ό μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈμ— 반영
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  sys_prompt = get_system_prompt(
415
  template=st.session_state.blog_template,
416
  tone=st.session_state.blog_tone,
417
  word_count=st.session_state.word_count,
418
- include_search_results=use_web_search
 
419
  )
420
 
421
- search_results = None
422
- if use_web_search:
423
- with st.spinner("μ›Ή 검색 쀑…"):
424
- search_results = do_web_search(keywords(prompt))
425
-
426
- # λ©”μ‹œμ§€ λ°°μ—΄ μ€€λΉ„
427
- messages = [{"role": m["role"], "content": m["content"]}
428
- for m in st.session_state.messages]
429
 
430
- # μ›Ή 검색 κ²°κ³Όκ°€ 있으면 별도 λ©”μ‹œμ§€λ‘œ μΆ”κ°€ (μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ λŒ€μ‹ )
431
- if search_results:
432
- messages.append({"role": "user", "content": search_results})
433
-
434
- # Claude 슀트리밍
 
 
 
 
 
435
  with client.messages.stream(
436
- model=st.session_state.ai_model, max_tokens=MAX_TOKENS,
 
437
  system=sys_prompt,
438
  messages=messages
439
  ) as stream:
@@ -441,70 +779,71 @@ def process_input(prompt):
441
  answer += t or ""
442
  placeholder.markdown(answer + "β–Œ")
443
  placeholder.markdown(answer)
444
-
445
- # 이미지 μ˜΅μ…˜
446
  answer_entry_saved = False
447
  if st.session_state.generate_image:
448
- with st.spinner("이미지 생성 쀑…"):
449
  ip = extract_image_prompt(answer, prompt)
450
  img, cap = generate_image(ip)
451
  if img:
452
  st.image(img, caption=cap)
453
- st.session_state.messages.append(
454
- {"role": "assistant", "content": answer,
455
- "image": img, "image_caption": cap})
 
 
 
456
  answer_entry_saved = True
 
 
457
  if not answer_entry_saved:
458
- st.session_state.messages.append(
459
- {"role": "assistant", "content": answer})
460
-
461
- # λ³Έλ¬Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ (MD / HTML)
462
- st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
463
- b1, b2 = st.columns(2)
464
- b1.download_button("λ§ˆν¬λ‹€μš΄", answer,
465
- file_name=f"{prompt[:30]}.md", mime="text/markdown")
466
- b2.download_button("HTML", md_to_html(answer, prompt[:30]),
467
- file_name=f"{prompt[:30]}.html", mime="text/html")
468
-
469
- # ── μžλ™ λ°±μ—… μ €μž₯
 
 
 
 
 
 
 
470
  if st.session_state.auto_save and st.session_state.messages:
471
  try:
472
  fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
473
  with open(fn, "w", encoding="utf-8") as fp:
474
- json.dump(st.session_state.messages, fp,
475
- ensure_ascii=False, indent=2)
476
  except Exception as e:
477
- logging.error(f"μžλ™ μ €μž₯ μ‹€νŒ¨: {e}")
478
-
479
  except anthropic.BadRequestError as e:
480
  error_message = str(e)
481
  if "credit balance is too low" in error_message:
482
- placeholder.error("⚠️ API ν¬λ ˆλ”§ λΆ€μ‘±: Anthropic API 계정에 ν¬λ ˆλ”§μ„ μΆ©μ „ν•΄μ£Όμ„Έμš”.")
483
- answer = "API ν¬λ ˆλ”§μ΄ λΆ€μ‘±ν•˜μ—¬ λΈ”λ‘œκ·Έλ₯Ό 생성할 수 μ—†μŠ΅λ‹ˆλ‹€. API 계정에 ν¬λ ˆλ”§μ„ μΆ©μ „ν•œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
484
  else:
485
- placeholder.error(f"API μš”μ²­ 였λ₯˜: {error_message}")
486
- answer = f"API μš”μ²­ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {error_message}"
487
-
488
- st.session_state.messages.append({"role": "assistant", "content": answer})
489
 
490
  except Exception as e:
491
  error_message = str(e)
492
- placeholder.error(f"였λ₯˜ λ°œμƒ: {error_message}")
493
- answer = f"μš”μ²­ 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {error_message}"
494
- st.session_state.messages.append({"role": "assistant", "content": answer})
495
 
496
- # ──────────────────────────────── main / requirements ──────────────────────
497
- def main(): ginigen_app()
 
498
 
499
  if __name__ == "__main__":
500
- # requirements.txt 동적 생성
501
- with open("requirements.txt", "w") as f:
502
- f.write("\n".join([
503
- "streamlit>=1.31.0",
504
- "anthropic>=0.18.1",
505
- "gradio-client>=1.8.0",
506
- "requests>=2.32.3",
507
- "markdown>=3.5.1",
508
- "pillow>=10.1.0"
509
- ]))
510
- main()
 
1
  # ──────────────────────────────── Imports ────────────────────────────────
2
+ import os, json, re, logging, requests, markdown, time, io
3
  from datetime import datetime
4
 
5
  import streamlit as st
6
  import anthropic
7
  from gradio_client import Client
8
+ import pandas as pd
9
+ import PyPDF2 # For handling PDF files
10
 
11
+ # ──────────────────────────────── Environment Variables / Constants ─────────────────────────
12
  ANTHROPIC_KEY = os.getenv("API_KEY", "")
13
+ BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Keep this name
14
  BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
15
  IMAGE_API_URL = "http://211.233.58.201:7896"
16
  MAX_TOKENS = 7_999
17
 
18
+ # Blog template and style definitions (in English)
19
  BLOG_TEMPLATES = {
20
+ "ginigen": "Recommended style by Ginigen",
21
+ "standard": "Standard 8-step framework blog",
22
+ "tutorial": "Step-by-step tutorial format",
23
+ "review": "Product/service review format",
24
+ "storytelling": "Storytelling format",
25
+ "seo_optimized": "SEO-optimized blog"
26
  }
27
 
28
  BLOG_TONES = {
29
+ "professional": "Professional and formal tone",
30
+ "casual": "Friendly and conversational tone",
31
+ "humorous": "Humorous approach",
32
+ "storytelling": "Story-driven approach"
33
  }
34
 
35
+ # Example blog topics
36
  EXAMPLE_TOPICS = {
37
+ "example1": "Changes to the real estate tax system in 2025: Impact on average households and tax-saving strategies",
38
+ "example2": "Summer festivals in 2025: A comprehensive guide to major regional events and hidden attractions",
39
+ "example3": "Emerging industries to watch in 2025: An investment guide focused on AI opportunities"
40
  }
41
 
42
+ # ──────────────────────────────── Logging ────────────────────────────────
43
  logging.basicConfig(level=logging.INFO,
44
  format="%(asctime)s - %(levelname)s - %(message)s")
45
 
46
+ # ──────────────────────────────── Anthropic Client ────────────────────────
47
+ @st.cache_resource
48
+ def get_anthropic_client():
49
+ return anthropic.Anthropic(api_key=ANTHROPIC_KEY)
50
 
51
+ # ──────────────────────────────── Blog Creation System Prompt ─────────────
52
+ def get_system_prompt(template="ginigen", tone="professional", word_count=1750, include_search_results=False, include_uploaded_files=False) -> str:
53
+ """
54
+ Generate a system prompt that includes:
55
+ - The 8-step blog writing framework
56
+ - The selected template and tone
57
+ - Guidelines for using web search results and uploaded files
58
+ """
59
+
60
+ # Ginigen recommended style prompt (English version)
61
+ ginigen_prompt = """
62
+ ## 🌟 Professional Blogger System Prompt
63
+
64
+ ### βœ… Official 8-step Prompt
65
+
66
+ Follow these 8 steps exactly in order to write the blog post:
67
+
68
+ ### 1. Start with a greeting and empathy
69
+ - Open with a friendly tone that draws the reader in
70
+ - Ask questions or present scenarios that resonate with the reader’s real-life concerns
71
+
72
+ ### 2. Clearly present the problem
73
+ - Pinpoint the exact and realistic problem the reader is facing
74
+ - Emphasize the seriousness or urgency of this problem to maintain interest
75
+
76
+ ### 3. Analyze the cause of the problem to build credibility
77
+ - Explain the causes of the problem logically
78
+ - Clearly and specifically present your analysis so the reader can understand it easily
79
+ - Include data, examples, or references if necessary
80
+
81
+ ### 4. Offer a concrete solution
82
+ - Provide specific, actionable steps to solve the problem
83
+ - Give tips, strategies, and guidelines so the reader can implement them right away
84
+
85
+ ### 5. Provide social proof
86
+ - Include real success stories, reviews, user experiences, or data
87
+ - Keep details factual and believable so the reader can trust the content
88
+
89
+ ### 6. Call to action (CTA)
90
+ - Encourage the reader to take specific actions immediately
91
+ - Use urgent language such as β€œright now,” β€œfrom today,” or β€œimmediately” to drive action
92
+
93
+ ### 7. Add constraints or warnings to increase authenticity
94
+ - Acknowledge that the solution might not work for everyone
95
+ - Show sincerity and scarcity, which boosts trust
96
+
97
+ ### 8. Express gratitude and guide them to further connection
98
+ - Thank the reader for their time
99
+ - Provide a natural lead-in to the next post, or ask for comments/subscriptions
100
+
101
+ ---
102
+
103
+ ### 🚩 Writing Style Tips
104
+ - Maintain a friendly and human-like tone
105
+ - Frequently use questions and a conversational style to engage the reader
106
+ - Use clear headings, horizontal lines, bullet points, or numbered lists for readability
107
+ - Include real-life examples and specific data where possible
108
+ """
109
+
110
+ # Standard 8-step framework (English version)
111
  base_prompt = """
112
+ You are an expert in writing professional blog posts. For every blog writing request, strictly follow this 8-step framework to produce a coherent, engaging post:
113
+
114
+ Reader Connection Phase
115
+ 1.1. Friendly greeting to build rapport
116
+ 1.2. Reflect actual reader concerns through introductory questions
117
+ 1.3. Stimulate immediate interest in the topic
118
+
119
+ Problem Definition Phase
120
+ 2.1. Define the reader’s pain points in detail
121
+ 2.2. Analyze the urgency and impact of the problem
122
+ 2.3. Build a consensus on why it needs to be solved
123
+
124
+ Establish Expertise Phase
125
+ 3.1. Analyze based on objective data
126
+ 3.2. Cite expert views and research findings
127
+ 3.3. Use real-life examples to further clarify the issue
128
+
129
+ Solution Phase
130
+ 4.1. Provide step-by-step guidance
131
+ 4.2. Suggest practical tips that can be applied immediately
132
+ 4.3. Mention potential obstacles and how to overcome them
133
+
134
+ Build Trust Phase
135
+ 5.1. Present actual success stories
136
+ 5.2. Quote real user feedback
137
+ 5.3. Use objective data to prove effectiveness
138
+
139
+ Action Phase
140
+ 6.1. Suggest the first clear step the reader can take
141
+ 6.2. Urge timely action by emphasizing urgency
142
+ 6.3. Motivate by highlighting incentives or benefits
143
+
144
+ Authenticity Phase
145
+ 7.1. Transparently disclose any limits of the solution
146
+ 7.2. Admit that individual experiences may vary
147
+ 7.3. Mention prerequisites or cautionary points
148
+
149
+ Relationship Continuation Phase
150
+ 8.1. Conclude with sincere gratitude
151
+ 8.2. Preview upcoming content to build anticipation
152
+ 8.3. Provide channels for further communication
153
  """
154
 
155
+ # Additional guidelines for each template
156
  template_guides = {
157
  "tutorial": """
158
+ This blog should be in a tutorial style:
159
+ - Clearly state the goal and the final outcome first
160
+ - Provide step-by-step explanations with clear separations
161
+ - Indicate where images could be inserted for each step
162
+ - Mention approximate time requirements and difficulty level
163
+ - List necessary tools or prerequisite knowledge
164
+ - Give troubleshooting tips and common mistakes to avoid
165
+ - Conclude with suggestions for next steps or advanced applications
166
  """,
 
167
  "review": """
168
+ This blog should be in a review style:
169
+ - Separate objective facts from subjective opinions
170
+ - Clearly list your evaluation criteria
171
+ - Discuss both pros and cons in a balanced way
172
+ - Compare with similar products/services
173
+ - Specify the target audience for whom it is suitable
174
+ - Provide concrete use cases and outcomes
175
+ - Conclude with a final recommendation or alternatives
176
  """,
 
177
  "storytelling": """
178
+ This blog should be in a storytelling style:
179
+ - Start with a real or hypothetical person or case
180
+ - Emphasize emotional connection with the problem scenario
181
+ - Follow a narrative structure centered on conflict and resolution
182
+ - Include meaningful insights or lessons learned
183
+ - Maintain an emotional thread the reader can relate to
184
+ - Balance storytelling with useful information
185
+ - Encourage the reader to reflect on their own story
186
  """,
 
187
  "seo_optimized": """
188
+ This blog should be SEO-optimized:
189
+ - Include the main keyword in the title, headings, and first paragraph
190
+ - Spread related keywords naturally throughout the text
191
+ - Keep paragraphs around 300-500 characters
192
+ - Use question-based subheadings
193
+ - Make use of lists, tables, and bold text to diversify formatting
194
+ - Indicate where internal links could be inserted
195
+ - Provide sufficient content of at least 2000-3000 characters
196
  """
197
  }
198
+
199
+ # Additional guidelines for each tone
200
  tone_guides = {
201
+ "professional": "Use a professional, authoritative voice. Clearly explain any technical terms and present data or research to maintain a logical flow.",
202
+ "casual": "Use a relaxed, conversational style. Employ personal experiences, relatable examples, and a friendly voice (e.g., 'It’s super useful!').",
203
+ "humorous": "Use humor and witty expressions. Add funny analogies or jokes while preserving accuracy and usefulness.",
204
+ "storytelling": "Write as if telling a story, with emotional depth and narrative flow. Incorporate characters, settings, conflicts, and resolutions."
205
  }
206
+
207
+ # Guidelines for using search results
208
  search_guide = """
209
+ Guidelines for Using Search Results:
210
+ - Accurately incorporate key information from the search results into the blog
211
+ - Include recent data, statistics, and case studies from the search results
212
+ - When quoting, specify the source within the text (e.g., β€œAccording to XYZ website...”)
213
+ - At the end of the blog, add a "References" section and list major sources with links
214
+ - If there are conflicting pieces of information, present multiple perspectives
215
+ - Make sure to reflect the latest trends and data from the search results
216
  """
217
+
218
+ # Guidelines for using uploaded files
219
+ upload_guide = """
220
+ Guidelines for Using Uploaded Files (Highest Priority):
221
+ - The uploaded files must be a main source of information for the blog
222
+ - Carefully examine the data, statistics, or examples in the file and integrate them
223
+ - Directly quote and thoroughly explain any key figures or claims from the file
224
+ - Highlight the file content as a crucial aspect of the blog
225
+ - Mention the source clearly, e.g., β€œAccording to the uploaded data...”
226
+ - For CSV files, detail important stats or numerical data in the blog
227
+ - For PDF files, quote crucial segments or statements
228
+ - For text files, integrate relevant content effectively
229
+ - Even if the file content seems tangential, do your best to connect it to the blog topic
230
+ - Keep consistency throughout and ensure the file’s data is appropriately reflected
231
+ """
232
+
233
+ # Choose base prompt
234
+ if template == "ginigen":
235
+ final_prompt = ginigen_prompt
236
+ else:
237
+ final_prompt = base_prompt
238
+
239
+ # If the user chose a specific template (and not ginigen), append the relevant guidelines
240
+ if template != "ginigen" and template in template_guides:
241
  final_prompt += "\n" + template_guides[template]
242
+
243
+ # If a specific tone is selected, append that guideline
244
  if tone in tone_guides:
245
+ final_prompt += f"\n\nTone and Manner: {tone_guides[tone]}"
246
+
247
+ # If web search results should be included
248
  if include_search_results:
249
  final_prompt += f"\n\n{search_guide}"
250
+
251
+ # If uploaded files should be included
252
+ if include_uploaded_files:
253
+ final_prompt += f"\n\n{upload_guide}"
254
+
255
+ # Word count guidelines
256
+ final_prompt += (
257
+ f"\n\nWriting Requirements:\n"
258
+ f"9.1. Word Count: around {word_count-250}-{word_count+250} characters\n"
259
+ f"9.2. Paragraph Length: 3-4 sentences each\n"
260
+ f"9.3. Visual Cues: Use subheadings, separators, and bullet/numbered lists\n"
261
+ f"9.4. Data: Cite all sources\n"
262
+ f"9.5. Readability: Use clear paragraph breaks and highlights where necessary"
263
+ )
264
+
265
  return final_prompt
266
 
267
+ # ──────────────────────────────── Brave Search API ────────────────────────
268
+ @st.cache_data(ttl=3600)
269
+ def brave_search(query: str, count: int = 20):
270
  """
271
+ Call the Brave Web Search API β†’ list[dict]
272
+ Returns fields: index, title, link, snippet, displayed_link
273
  """
274
  if not BRAVE_KEY:
275
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
276
 
277
  headers = {
278
  "Accept": "application/json",
279
  "Accept-Encoding": "gzip",
280
  "X-Subscription-Token": BRAVE_KEY
281
  }
282
+ params = {"q": query, "count": str(count)}
283
+
284
+ for attempt in range(3):
285
  try:
286
  r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
287
  r.raise_for_status()
288
  data = r.json()
289
+
290
+ logging.info(f"Brave search result data structure: {list(data.keys())}")
291
+
 
292
  raw = data.get("web", {}).get("results") or data.get("results", [])
293
  if not raw:
294
+ logging.warning(f"No Brave search results found. Response: {data}")
295
+ raise ValueError("No search results found.")
296
+
297
  arts = []
298
+ for i, res in enumerate(raw[:count], 1):
299
+ url = res.get("url", res.get("link", ""))
300
+ host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
301
  arts.append({
302
  "index": i,
303
+ "title": res.get("title", "No title"),
304
  "link": url,
305
+ "snippet": res.get("description", res.get("text", "No snippet")),
306
  "displayed_link": host
307
  })
308
+
309
+ logging.info(f"Brave search success: {len(arts)} results")
310
  return arts
311
+
312
  except Exception as e:
313
+ logging.error(f"Brave search failure (attempt {attempt+1}/3): {e}")
314
+ if attempt < 2:
315
  time.sleep(2)
316
+
317
+ return []
318
 
319
  def mock_results(query: str) -> str:
320
+ """Fallback search results if API fails"""
321
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
322
+ return (f"# Fallback Search Content (Generated: {ts})\n\n"
323
+ f"The search API request failed. Please generate the blog based on any pre-existing knowledge about '{query}'.\n\n"
324
+ f"You may consider the following points:\n\n"
325
+ f"- Basic concepts and importance of {query}\n"
326
+ f"- Commonly known related statistics or trends\n"
327
+ f"- Typical expert opinions on this subject\n"
328
+ f"- Questions that readers might have\n\n"
329
+ f"Note: This is fallback guidance, not real-time data.\n\n")
330
 
331
  def do_web_search(query: str) -> str:
332
+ """Perform web search and format the results."""
333
  try:
334
  arts = brave_search(query, 20)
335
  if not arts:
336
+ logging.warning("No search results, using fallback content")
337
  return mock_results(query)
338
+
339
+ hdr = "# Web Search Results\nUse the information below to enhance the reliability of your blog. When you quote, please cite the source, and add a References section at the end of the blog.\n\n"
340
  body = "\n".join(
341
  f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
342
+ f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
343
  for a in arts
344
  )
345
  return hdr + body
346
  except Exception as e:
347
+ logging.error(f"Web search process failed: {str(e)}")
348
  return mock_results(query)
349
 
350
+ # ──────────────────────────────── File Upload Handling ─────────────────────
351
+ def process_text_file(file):
352
+ """Handle text file"""
353
+ try:
354
+ content = file.read()
355
+ file.seek(0)
356
+
357
+ text = content.decode('utf-8', errors='ignore')
358
+ if len(text) > 10000:
359
+ text = text[:9700] + "...(truncated)..."
360
+
361
+ result = f"## Text File: {file.name}\n\n"
362
+ result += text
363
+ return result
364
+ except Exception as e:
365
+ logging.error(f"Error processing text file: {str(e)}")
366
+ return f"Error processing text file: {str(e)}"
367
+
368
+ def process_csv_file(file):
369
+ """Handle CSV file"""
370
+ try:
371
+ content = file.read()
372
+ file.seek(0)
373
+
374
+ df = pd.read_csv(io.BytesIO(content))
375
+ result = f"## CSV File: {file.name}\n\n"
376
+ result += f"- Rows: {len(df)}\n"
377
+ result += f"- Columns: {len(df.columns)}\n"
378
+ result += f"- Column Names: {', '.join(df.columns.tolist())}\n\n"
379
+
380
+ result += "### Data Preview\n\n"
381
+ preview_df = df.head(10)
382
+ try:
383
+ markdown_table = preview_df.to_markdown(index=False)
384
+ if markdown_table:
385
+ result += markdown_table + "\n\n"
386
+ else:
387
+ result += "Unable to display CSV data.\n\n"
388
+ except Exception as e:
389
+ logging.error(f"Markdown table conversion error: {e}")
390
+ result += "Displaying data as text:\n\n"
391
+ result += str(preview_df) + "\n\n"
392
+
393
+ num_cols = df.select_dtypes(include=['number']).columns
394
+ if len(num_cols) > 0:
395
+ result += "### Basic Statistical Information\n\n"
396
+ try:
397
+ stats_df = df[num_cols].describe().round(2)
398
+ stats_markdown = stats_df.to_markdown()
399
+ if stats_markdown:
400
+ result += stats_markdown + "\n\n"
401
+ else:
402
+ result += "Unable to display statistical information.\n\n"
403
+ except Exception as e:
404
+ logging.error(f"Statistical info conversion error: {e}")
405
+ result += "Unable to generate statistical information.\n\n"
406
+
407
+ return result
408
+ except Exception as e:
409
+ logging.error(f"CSV file processing error: {str(e)}")
410
+ return f"Error processing CSV file: {str(e)}"
411
+
412
+ def process_pdf_file(file):
413
+ """Handle PDF file"""
414
+ try:
415
+ # Read file in bytes
416
+ file_bytes = file.read()
417
+ file.seek(0)
418
+
419
+ # Use PyPDF2
420
+ pdf_file = io.BytesIO(file_bytes)
421
+ reader = PyPDF2.PdfReader(pdf_file, strict=False)
422
+
423
+ # Basic info
424
+ result = f"## PDF File: {file.name}\n\n"
425
+ result += f"- Total pages: {len(reader.pages)}\n\n"
426
+
427
+ # Extract text by page (limit to first 5 pages)
428
+ max_pages = min(5, len(reader.pages))
429
+ all_text = ""
430
+
431
+ for i in range(max_pages):
432
+ try:
433
+ page = reader.pages[i]
434
+ page_text = page.extract_text()
435
+
436
+ current_page_text = f"### Page {i+1}\n\n"
437
+ if page_text and len(page_text.strip()) > 0:
438
+ # Limit to 1500 characters per page
439
+ if len(page_text) > 1500:
440
+ current_page_text += page_text[:1500] + "...(truncated)...\n\n"
441
+ else:
442
+ current_page_text += page_text + "\n\n"
443
+ else:
444
+ current_page_text += "(No text could be extracted from this page)\n\n"
445
+
446
+ all_text += current_page_text
447
+
448
+ # If total text is too long, break
449
+ if len(all_text) > 8000:
450
+ all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
451
+ break
452
+
453
+ except Exception as page_err:
454
+ logging.error(f"Error processing PDF page {i+1}: {str(page_err)}")
455
+ all_text += f"### Page {i+1}\n\n(Error extracting content: {str(page_err)})\n\n"
456
+
457
+ if len(reader.pages) > max_pages:
458
+ all_text += f"\nNote: Only the first {max_pages} pages are shown out of {len(reader.pages)} total.\n\n"
459
+
460
+ result += "### PDF Content\n\n" + all_text
461
+ return result
462
+
463
+ except Exception as e:
464
+ logging.error(f"PDF file processing error: {str(e)}")
465
+ return f"## PDF File: {file.name}\n\nError occurred: {str(e)}\n\nThis PDF file cannot be processed."
466
+
467
+ def process_uploaded_files(files):
468
+ """Combine the contents of all uploaded files into one string."""
469
+ if not files:
470
+ return None
471
+
472
+ result = "# Uploaded File Contents\n\n"
473
+ result += "Below is the content from the files provided by the user. Integrate this data as a main source of information for the blog.\n\n"
474
+
475
+ for file in files:
476
+ try:
477
+ ext = file.name.split('.')[-1].lower()
478
+ if ext == 'txt':
479
+ result += process_text_file(file) + "\n\n---\n\n"
480
+ elif ext == 'csv':
481
+ result += process_csv_file(file) + "\n\n---\n\n"
482
+ elif ext == 'pdf':
483
+ result += process_pdf_file(file) + "\n\n---\n\n"
484
+ else:
485
+ result += f"### Unsupported File: {file.name}\n\n---\n\n"
486
+ except Exception as e:
487
+ logging.error(f"File processing error {file.name}: {e}")
488
+ result += f"### File processing error: {file.name}\n\nError: {e}\n\n---\n\n"
489
+
490
+ return result
491
+
492
+ # ──────────────────────────────── Image & Utility ─────────────────────────
493
  def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
494
+ """Image generation function."""
495
+ if not prompt:
496
+ return None, "Insufficient prompt"
497
  try:
498
  res = Client(IMAGE_API_URL).predict(
499
  prompt=prompt, width=w, height=h, guidance=g,
500
  inference_steps=steps, seed=seed,
501
  do_img2img=False, init_image=None,
502
  image2image_strength=0.8, resize_img=True,
503
+ api_name="/generate_image"
504
+ )
505
  return res[0], f"Seed: {res[1]}"
506
  except Exception as e:
507
+ logging.error(e)
508
+ return None, str(e)
509
 
510
+ def extract_image_prompt(blog_text: str, topic: str):
511
+ """
512
+ Analyze the blog content (blog_text) to generate a one-line English image prompt
513
+ related to the topic.
514
+ """
515
+ client = get_anthropic_client()
516
+ sys = f"Generate a single-line English image prompt from the following text:\nTopic: {topic}"
517
  try:
518
+ # Simple one-time call
519
  res = client.messages.create(
520
  model="claude-3-7-sonnet-20250219",
521
+ max_tokens=80,
522
+ system=sys,
523
+ messages=[{"role": "user", "content": blog_text}]
524
  )
525
  return res.content[0].text.strip()
526
  except Exception:
527
+ # Fallback prompt
528
  return f"A professional photo related to {topic}, high quality"
529
 
530
  def md_to_html(md: str, title="Ginigen Blog"):
531
+ """Convert Markdown to HTML."""
532
  return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
533
 
534
  def keywords(text: str, top=5):
535
+ """Simple keyword extraction."""
536
+ cleaned = re.sub(r"[^κ°€-힣a-zA-Z0-9\s]", "", text)
537
+ return " ".join(cleaned.split()[:top])
538
 
539
  # ──────────────────────────────── Streamlit UI ────────────────────────────
540
  def ginigen_app():
541
+ st.title("Ginigen Blog")
542
 
543
+ # Set default session state
544
  if "ai_model" not in st.session_state:
545
  st.session_state.ai_model = "claude-3-7-sonnet-20250219"
546
  if "messages" not in st.session_state:
 
549
  st.session_state.auto_save = True
550
  if "generate_image" not in st.session_state:
551
  st.session_state.generate_image = False
552
+ if "web_search_enabled" not in st.session_state:
553
+ st.session_state.web_search_enabled = True
554
  if "blog_template" not in st.session_state:
555
+ st.session_state.blog_template = "ginigen" # Ginigen recommended style by default
556
  if "blog_tone" not in st.session_state:
557
  st.session_state.blog_tone = "professional"
558
  if "word_count" not in st.session_state:
559
  st.session_state.word_count = 1750
560
 
561
+ # Sidebar UI
562
  sb = st.sidebar
563
+ sb.title("Blog Settings")
 
 
 
 
 
 
564
 
565
+ sb.subheader("Blog Style Settings")
566
+ sb.selectbox(
567
+ "Blog Template",
568
+ options=list(BLOG_TEMPLATES.keys()),
569
+ format_func=lambda x: BLOG_TEMPLATES[x],
570
+ key="blog_template"
571
+ )
572
 
573
+ sb.selectbox(
574
+ "Blog Tone",
575
+ options=list(BLOG_TONES.keys()),
576
+ format_func=lambda x: BLOG_TONES[x],
577
+ key="blog_tone"
578
+ )
579
 
580
+ sb.slider("Blog Length (word count)", 800, 3000, key="word_count")
 
581
 
582
+ # Example topics
583
+ sb.subheader("Example Topics")
584
+ c1, c2, c3 = sb.columns(3)
585
+ if c1.button("Real Estate Tax", key="ex1"):
 
586
  process_example(EXAMPLE_TOPICS["example1"])
587
+ if c2.button("Summer Festivals", key="ex2"):
 
588
  process_example(EXAMPLE_TOPICS["example2"])
589
+ if c3.button("Investment Guide", key="ex3"):
 
590
  process_example(EXAMPLE_TOPICS["example3"])
591
 
592
+ sb.subheader("Other Settings")
593
+ sb.toggle("Auto Save", key="auto_save")
594
+ sb.toggle("Auto Image Generation", key="generate_image")
595
+
596
+ web_search_enabled = sb.toggle("Use Web Search", value=st.session_state.web_search_enabled)
597
+ st.session_state.web_search_enabled = web_search_enabled
598
 
599
+ if web_search_enabled:
600
+ st.sidebar.info("βœ… Web search results will be integrated into the blog.")
 
 
601
 
602
+ # Download the latest blog (markdown/HTML)
603
  latest_blog = next(
604
+ (m["content"] for m in reversed(st.session_state.messages)
605
+ if m["role"] == "assistant" and m["content"].strip()),
606
+ None
607
+ )
608
  if latest_blog:
609
+ title_match = re.search(r"# (.*?)(\n|$)", latest_blog)
610
+ title = title_match.group(1).strip() if title_match else "blog"
611
+ sb.subheader("Download Latest Blog")
612
+ d1, d2 = sb.columns(2)
613
+ d1.download_button("Download as Markdown", latest_blog,
614
  file_name=f"{title}.md", mime="text/markdown")
615
+ d2.download_button("Download as HTML", md_to_html(latest_blog, title),
616
  file_name=f"{title}.html", mime="text/html")
617
 
618
+ # JSON conversation record upload
619
+ up = sb.file_uploader("Load Conversation History (.json)", type=["json"], key="json_uploader")
620
  if up:
621
  try:
622
  st.session_state.messages = json.load(up)
623
+ sb.success("Conversation history loaded successfully")
624
  except Exception as e:
625
+ sb.error(f"Failed to load: {e}")
626
+
627
+ # JSON conversation record download
628
+ if sb.button("Download Conversation as JSON"):
629
+ sb.download_button(
630
+ "Save",
631
+ data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
632
+ file_name="chat_history.json",
633
+ mime="application/json"
634
+ )
635
 
636
+ # File Upload
637
+ st.subheader("File Upload")
638
+ uploaded_files = st.file_uploader(
639
+ "Upload files to be referenced in your blog (txt, csv, pdf)",
640
+ type=["txt", "csv", "pdf"],
641
+ accept_multiple_files=True,
642
+ key="file_uploader"
643
+ )
644
+
645
+ if uploaded_files:
646
+ file_count = len(uploaded_files)
647
+ st.success(f"{file_count} files uploaded. They will be referenced in the blog.")
648
+
649
+ with st.expander("Preview Uploaded Files", expanded=False):
650
+ for idx, file in enumerate(uploaded_files):
651
+ st.write(f"**File Name:** {file.name}")
652
+ ext = file.name.split('.')[-1].lower()
653
+
654
+ if ext == 'txt':
655
+ preview = file.read(1000).decode('utf-8', errors='ignore')
656
+ file.seek(0)
657
+ st.text_area(
658
+ f"Preview of {file.name}",
659
+ preview + ("..." if len(preview) >= 1000 else ""),
660
+ height=150
661
+ )
662
+ elif ext == 'csv':
663
+ try:
664
+ df = pd.read_csv(file)
665
+ file.seek(0)
666
+ st.write("CSV Preview (up to 5 rows)")
667
+ st.dataframe(df.head(5))
668
+ except Exception as e:
669
+ st.error(f"CSV preview failed: {e}")
670
+ elif ext == 'pdf':
671
+ try:
672
+ file_bytes = file.read()
673
+ file.seek(0)
674
+
675
+ pdf_file = io.BytesIO(file_bytes)
676
+ reader = PyPDF2.PdfReader(pdf_file, strict=False)
677
+
678
+ pc = len(reader.pages)
679
+ st.write(f"PDF File: {pc} pages")
680
+
681
+ if pc > 0:
682
+ try:
683
+ page_text = reader.pages[0].extract_text()
684
+ preview = page_text[:500] if page_text else "(No text extracted)"
685
+ st.text_area("Preview of the first page", preview + "...", height=150)
686
+ except:
687
+ st.warning("Failed to extract text from the first page")
688
+ except Exception as e:
689
+ st.error(f"PDF preview failed: {e}")
690
+
691
+ if idx < file_count - 1:
692
+ st.divider()
693
+
694
+ # Display existing messages
695
  for m in st.session_state.messages:
696
  with st.chat_message(m["role"]):
697
  st.markdown(m["content"])
698
  if "image" in m:
699
  st.image(m["image"], caption=m.get("image_caption", ""))
700
 
701
+ # User input
702
+ prompt = st.chat_input("Enter a blog topic or keywords.")
 
703
  if prompt:
704
+ process_input(prompt, uploaded_files)
 
705
 
706
  def process_example(topic):
707
+ """Process the selected example topic."""
708
+ process_input(topic, [])
 
 
 
 
 
 
709
 
710
+ def process_input(prompt: str, uploaded_files):
711
+ # Add user's message if it doesn't already exist
712
+ if not any(m["role"] == "user" and m["content"] == prompt for m in st.session_state.messages):
713
+ st.session_state.messages.append({"role": "user", "content": prompt})
714
+
715
+ with st.chat_message("user"):
716
+ st.markdown(prompt)
717
+
718
  with st.chat_message("assistant"):
719
+ placeholder = st.empty()
720
+ answer = ""
721
+
722
+ use_web_search = st.session_state.web_search_enabled
723
+ has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
724
 
725
  try:
726
+ client = get_anthropic_client()
727
+
728
+ # Prepare conversation messages
729
+ messages = [{"role": m["role"], "content": m["content"]} for m in st.session_state.messages]
730
+
731
+ # Web search
732
+ if use_web_search:
733
+ with st.spinner("Performing web search..."):
734
+ sr = do_web_search(keywords(prompt, top=5))
735
+ if sr:
736
+ messages.append({"role": "user", "content": sr})
737
+
738
+ # Process uploaded files β†’ content
739
+ file_content = None
740
+ if has_uploaded_files:
741
+ with st.spinner("Analyzing uploaded files..."):
742
+ file_content = process_uploaded_files(uploaded_files)
743
+
744
+ # Build system prompt
745
  sys_prompt = get_system_prompt(
746
  template=st.session_state.blog_template,
747
  tone=st.session_state.blog_tone,
748
  word_count=st.session_state.word_count,
749
+ include_search_results=use_web_search,
750
+ include_uploaded_files=has_uploaded_files
751
  )
752
 
753
+ # If we have file content, append it to the system prompt
754
+ if file_content:
755
+ sys_prompt += (
756
+ "\n\n"
757
+ "Below is the content of the uploaded file(s). Please make sure to integrate it thoroughly in the blog:\n\n"
758
+ f"{file_content}\n\n"
759
+ "Ensure the file content is accurately reflected in the blog.\n"
760
+ )
761
 
762
+ # Append additional user message about file usage
763
+ if has_uploaded_files:
764
+ extra_user_msg = (
765
+ f"{prompt}\n\n"
766
+ "Additional note: Please make sure to reference the uploaded file content in the blog. "
767
+ "Use and analyze any data, statistics, or text included in the file(s)."
768
+ )
769
+ messages.append({"role": "user", "content": extra_user_msg})
770
+
771
+ # Claude streaming
772
  with client.messages.stream(
773
+ model=st.session_state.ai_model,
774
+ max_tokens=MAX_TOKENS,
775
  system=sys_prompt,
776
  messages=messages
777
  ) as stream:
 
779
  answer += t or ""
780
  placeholder.markdown(answer + "β–Œ")
781
  placeholder.markdown(answer)
782
+
783
+ # Image generation option
784
  answer_entry_saved = False
785
  if st.session_state.generate_image:
786
+ with st.spinner("Generating image..."):
787
  ip = extract_image_prompt(answer, prompt)
788
  img, cap = generate_image(ip)
789
  if img:
790
  st.image(img, caption=cap)
791
+ st.session_state.messages.append({
792
+ "role": "assistant",
793
+ "content": answer,
794
+ "image": img,
795
+ "image_caption": cap
796
+ })
797
  answer_entry_saved = True
798
+
799
+ # Save the answer
800
  if not answer_entry_saved:
801
+ st.session_state.messages.append({"role": "assistant", "content": answer})
802
+
803
+ # Download buttons
804
+ st.subheader("Download This Blog")
805
+ c1, c2 = st.columns(2)
806
+ c1.download_button(
807
+ "Markdown",
808
+ data=answer,
809
+ file_name=f"{prompt[:30]}.md",
810
+ mime="text/markdown"
811
+ )
812
+ c2.download_button(
813
+ "HTML",
814
+ data=md_to_html(answer, prompt[:30]),
815
+ file_name=f"{prompt[:30]}.html",
816
+ mime="text/html"
817
+ )
818
+
819
+ # Auto save
820
  if st.session_state.auto_save and st.session_state.messages:
821
  try:
822
  fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
823
  with open(fn, "w", encoding="utf-8") as fp:
824
+ json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2)
 
825
  except Exception as e:
826
+ logging.error(f"Auto-save failed: {e}")
827
+
828
  except anthropic.BadRequestError as e:
829
  error_message = str(e)
830
  if "credit balance is too low" in error_message:
831
+ placeholder.error("⚠️ Insufficient API credits: Please top up your Anthropic API account.")
832
+ ans = "Unable to generate blog due to low API credits. Please recharge and try again."
833
  else:
834
+ placeholder.error(f"API request error: {error_message}")
835
+ ans = f"An error occurred while calling the API: {error_message}"
836
+ st.session_state.messages.append({"role": "assistant", "content": ans})
 
837
 
838
  except Exception as e:
839
  error_message = str(e)
840
+ placeholder.error(f"An error occurred: {error_message}")
841
+ ans = f"An error occurred while processing your request: {error_message}"
842
+ st.session_state.messages.append({"role": "assistant", "content": ans})
843
 
844
+ # ──────────────────────────────── main ────────────────────────────────────
845
+ def main():
846
+ ginigen_app()
847
 
848
  if __name__ == "__main__":
849
+ main()