FincH12 commited on
Commit
fd40f81
·
verified ·
1 Parent(s): 1a5f0da

Deploy Dynamic Dashboard, Analytics and Prompts Editor

Browse files
Files changed (9) hide show
  1. config.py +2 -6
  2. content_generator.py +25 -102
  3. dashboard.html +329 -374
  4. keep_alive_server.py +36 -0
  5. list_secrets.py +12 -0
  6. main.py +26 -21
  7. make_private.py +12 -0
  8. settings_manager.py +122 -0
  9. update_config.py +15 -0
config.py CHANGED
@@ -21,15 +21,11 @@ COOKIES_PATH = os.path.join(BASE_DIR, "cookies.json")
21
  os.makedirs(OUTPUT_DIR, exist_ok=True)
22
  os.makedirs(FONTS_DIR, exist_ok=True)
23
 
24
- # Суточные лимиты и интервалы (в минутах)
25
- DAILY_CAROUSELS_LIMIT = 1
26
- DAILY_SINGLE_POSTS_LIMIT = 1
27
  ENABLE_EXTERNAL_COMMENTS = True
28
  DAILY_EXTERNAL_COMMENTS_LIMIT = 12
29
 
30
- # Интервалы планировщика
31
- MIN_POST_INTERVAL = 90
32
- MAX_POST_INTERVAL = 150
33
  POST_RANDOM_OFFSET_MIN = 7
34
  POST_RANDOM_OFFSET_MAX = 19
35
 
 
21
  os.makedirs(OUTPUT_DIR, exist_ok=True)
22
  os.makedirs(FONTS_DIR, exist_ok=True)
23
 
24
+ # Суточные лимиты (внешние комментарии)
 
 
25
  ENABLE_EXTERNAL_COMMENTS = True
26
  DAILY_EXTERNAL_COMMENTS_LIMIT = 12
27
 
28
+ # Интервалы планировщика (рандомизация)
 
 
29
  POST_RANDOM_OFFSET_MIN = 7
30
  POST_RANDOM_OFFSET_MAX = 19
31
 
content_generator.py CHANGED
@@ -5,6 +5,7 @@ import re
5
  import random
6
  import xml.etree.ElementTree as ET
7
  from config import GEMINI_API_BASE, GEMINI_MODEL
 
8
 
9
  def verify_and_clean_urls(text):
10
  """
@@ -190,34 +191,14 @@ def generate_single_post(news_context, exclude_posts=None):
190
  """
191
  tones = [
192
  "Провокативний стратег (гарячі тейки, злам стереотипів)",
193
- "Глибокий аналітик (розкладає по поличках складні речі)",
194
- "Інсайдер-візіонер (розповідає про майбутнє як про доконаний факт)"
195
- ]
196
- import random
197
- tone = random.choice(tones)
198
-
199
- exclude_str = ""
200
- if exclude_posts:
201
- exclude_items = []
202
- for i, post_text in enumerate(exclude_posts):
203
- post_preview = post_text.replace('\n', ' ').strip()[:150]
204
- exclude_items.append(f"- {post_preview}...")
205
- exclude_list_str = "\n".join(exclude_items)
206
- exclude_str = f"\nКАТЕГОРИЧНО ЗАБОРОНЕНО обирати теми або новини, про які вже було опубліковано останні пости:\n{exclude_list_str}\nБудь ласка, обери повністю іншу новину з контексту, яка не має жодного відношення до вищеперелічених тем та новин!"
207
-
208
- prompt = f"""
209
- Використовуючи наступний новинний контекст:
210
- \"\"\"{news_context}\"\"\"
211
- {exclude_str}
212
-
213
- Створи 1 одиночний пост для Threads українською мовою. Вибери одну найцікавішу новину з фокусом на одну з тем: ІІ-безпека/етика, DeFi/криптовалюти, або стартапи/тех-сектор.
214
-
215
- ⚡ ТВОЇ СУПЕРСИЛИ (SKILLS) & КРИТИЧНІ ПРАВИЛА:
216
-
217
- 1. РОЛЬ: SENIOR SOCIAL MEDIA COPYWRITER (Текст посту)
218
- - Психологія: Пиши максимально живим, людським стилем (як реальний експерт у Threads), уникаючи будь-якого ШІ-жаргону (\"безумовно\", \"у сучасному світі\", \"важливо зазначити\", \"інноваційний\"). Тон: {tone}.
219
- - Позиція та думка: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати шаблон \"[Факт]. [Риторичний вопрос]. Деталі...\". Замість цього додай власну чітку експертну думку/позицію перед будь-яким запитанням (наприклад: \"Я вважаю X, тому що Y. А як до цього ставитесь ви?\"). Позиція має бути до запитання, а не замість неї. Уникай пустих риторичних питань.
220
- - Самодостатність: Пост має бути повністю самодостатнім і нести цінність одразу в тексті. Посилання або картинка — лише доповнення, а не привід клікати.
221
  - Заголовки: Пиши заголовки-крючки (hook), що чіпляють увагу, а не нудні заголовки-новини.
222
  - Посилання: КАТЕГОРИЧНО ЗАБОРОНЕНО вставляти будь-які URL або посилання в полі description. Посилання на джерело має повертатися виключно окремо в ключі source_url.
223
  - Хештеги: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати масові теги (#AI, #Crypto, #ШІ, #Технології). Використовуй лише 1-2 вузьких тега максимум (наприклад, #DeFiУкраїна, #LLMбезпека, #ХарківTech). Розташовуй теги строго в самому кінці тексту.
@@ -269,63 +250,16 @@ def generate_carousel_post(news_context, exclude_posts=None):
269
  exclude_list_str = "\n".join(exclude_items)
270
  exclude_str = f"\nКАТЕГОРИЧНО ЗАБОРОНЕНО обирати теми або новини, про які вже було опубліковано останні пости:\n{exclude_list_str}\nБудь ласка, обери повністю іншу новину з контексту, яка не має жодного відношення до вищеперелічених тем та новин!"
271
 
272
- prompt = f"""
273
- Використовуючи наступний новинний контекст:
274
- \"\"\"{news_context}\"\"\"
275
- {exclude_str}
276
-
277
- Створи глибокий розбір однієї з новин у вигляді каруселі з 5 слайдів для Threads українською мовою з фокусом на теми: безпека/етика ІІ, DeFi/криптовалюти, або стартапи/тех-сфера.
278
-
279
- ⚡ ТВОЇ СУПЕРСИЛИ (SKILLS) & КРИТИЧНІ ПРАВИЛА:
280
-
281
- 1. РОЛЬ: SENIOR SOCIAL MEDIA COPYWRITER
282
- - Сюжет: Будуй карусель як історію (Curiosity Gap). Кожен слайд змушує свайпнути далі. Тон: {tone}.
283
- - Текст посту: Напиши коментар автора до каруселі. Він має бути повністю самодостатнім, нести самостійну цінність та містити твою чітку експертну думку/позицію перед питанням до аудиторії.
284
- - Заборонені шаблони: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати шаблони на кшталт "[Факт]. [Риторичне питання]. Деталі в каруселі. Джерело". Натомість використовуй заголовок-крючок (hook).
285
- - Заборонені фрази: Уникай типових ШІ-фразочок ("безумовно", "у сучасному світі", "у світі технологій"). Пиши як реальний живий експерт.
286
- - Посилання: КАТЕГОРИЧНО ЗАБОРОНЕНО вставляти будь-які URL або посилання в полі description. Посилання на джерело має повертатися виключно окремо в ключі source_url.
287
- - Хештеги: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати масові теги (#AI, #Crypto, #ШІ, #Технології). Використовуй лише 1-2 вузьких тега максимум (наприклад, #DeFiУкраїна, #LLMбезпека, #ХарківTech). Розташовуй теги строго в самому кінці тексту.
288
-
289
- 2. РОЛЬ: ART DIRECTOR & UI/UX DESIGNER
290
- - Фонова динаміка: Ти пишеш промпти для фонів так, щоб вони еволюціонували від слайду до слайду, зберігаючи загальну кінематографічную стилістику (premium dark aesthetics, neon, volumetric lighting).
291
- - Читабельність: Завжди гарантуй вільний простір для тексту ('smooth clean negative space, absolutely no text').
292
-
293
- Поверни результат строго у форматі JSON із наступними ключами:
294
- {{
295
- "category": "одна з трьох категорій: 'AI', 'Business' або 'Crypto'",
296
- "description": "Супровідний текст посту (до 350 символів). Жива людська мова, що виражає твою позицію та інтригує переглянути карусель. Жодних посилань/URL у цьому полі! Строго 1-2 вузьких тега в самому кінці.",
297
- "source_url": "коротке посилання на першоджерело новини (обов'язково)",
298
- "slides": [
299
- {{
300
- "title": "ЗАГОЛОВОК ОБКЛАДИНКИ (Слайд 1 - Хук)",
301
- "bullets": ["короткий підзаголовок (гачок)"],
302
- "bg_prompt": "англійською мовою детальний промпт (Flux.1). Premium dark aesthetics, volumetric lighting. В кінці: ', background only, smooth clean negative space, absolutely no text'"
303
- }},
304
- {{
305
- "title": "Розвиток думки (Слайд 2)",
306
- "bullets": ["основний факт", "контекст"],
307
- "bg_prompt": "еволюція фону для слайду 2"
308
- }},
309
- {{
310
- "title": "Глибокий аналіз (Слайд 3)",
311
- "bullets": ["неочевидна деталь", "чому це важливо"],
312
- "bg_prompt": "еволюція фону для слайду 3"
313
- }},
314
- {{
315
- "title": "Наслідки (Слайд 4)",
316
- "bullets": ["що це означає", "прогноз"],
317
- "bg_prompt": "еволюція фону для слайду 4"
318
- }},
319
- {{
320
- "title": "ЩО ДУМАЄТЕ? (Слайд 5 - Дія)",
321
- "bullets": ["Специфічне питання перед обговоренням", "Пишіть в коментарях!"],
322
- "bg_prompt": "фінальний фон"
323
- }}
324
- ]
325
- }}
326
-
327
- Повертай ТІЛЬКИ чистий JSON-об'єкт.
328
- """
329
  print("[Content Generator] Генерация поста-карусели (Persona Skills)...")
330
  raw_res = generate_text_via_gemini(prompt)
331
  data = clean_json_response(raw_res)
@@ -348,23 +282,12 @@ def predict_post_success(post_text, historical_data=None):
348
  history_list = "\n".join([f"- Пост (Лайки: {p.get('likes', 0)}): {p.get('text')[:100]}..." for p in historical_data])
349
  context = f"Для справки, вот наши самые успешные посты в прошлом:\n{history_list}\n"
350
 
351
- prompt = f"""
352
- Ты — AI-редактор и эксперт по виральному контенту в соцсетях.
353
- Твоя задача — оценить черновик поста для Threads перед его публикацией.
354
- {context}
355
- Черновик поста:
356
- --- {post_text} ---
357
-
358
- Оцени потенциал этого поста по 10-балльной шкале (где 10 — абсолютный хит, а 1 — скучная новостная сводка, которую никто не лайкнет).
359
- Пост должен иметь мощный 'крючок' (hook) в первом предложении, вызывать эмоции (шок, интерес, несогласие) и стимулировать дискуссию в комментариях.
360
-
361
- Верни СТРОГО JSON:
362
- {{
363
- "score": 8,
364
- "criticism": "оставь пустым, если балл >= 7, иначе напиши 1-2 предложения, что именно изменить (например, 'добавь провокационный вопрос в конце' или 'сделай первое предложение более шокирующим')"
365
- }}
366
- Возвращай только чистый JSON без markdown (без ```json).
367
- """
368
  headers = {"Content-Type": "application/json"}
369
  payload = {
370
  "model": GEMINI_MODEL,
 
5
  import random
6
  import xml.etree.ElementTree as ET
7
  from config import GEMINI_API_BASE, GEMINI_MODEL
8
+ import settings_manager
9
 
10
  def verify_and_clean_urls(text):
11
  """
 
191
  """
192
  tones = [
193
  "Провокативний стратег (гарячі тейки, злам стереотипів)",
194
+ "Глибокий аналітик (ро� settings = settings_manager.load_settings()
195
+ prompt_template = settings.get("PROMPT_SINGLE", "")
196
+
197
+ prompt = prompt_template.format(
198
+ news_context=news_context,
199
+ exclude_str=exclude_str,
200
+ tone=tone
201
+ )нення, а не привід клікати.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  - Заголовки: Пиши заголовки-крючки (hook), що чіпляють увагу, а не нудні заголовки-новини.
203
  - Посилання: КАТЕГОРИЧНО ЗАБОРОНЕНО вставляти будь-які URL або посилання в полі description. Посилання на джерело має повертатися виключно окремо в ключі source_url.
204
  - Хештеги: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати масові теги (#AI, #Crypto, #ШІ, #Технології). Використовуй лише 1-2 вузьких тега максимум (наприклад, #DeFiУкраїна, #LLMбезпека, #ХарківTech). Розташовуй теги строго в самому кінці тексту.
 
250
  exclude_list_str = "\n".join(exclude_items)
251
  exclude_str = f"\nКАТЕГОРИЧНО ЗАБОРОНЕНО обирати теми або новини, про які вже було опубліковано останні пости:\n{exclude_list_str}\nБудь ласка, обери повністю іншу новину з контексту, яка не має жодного відношення до вищеперелічених тем та новин!"
252
 
253
+ settings = settings_manager.load_settings()
254
+ prompt_template = settings.get("PROMPT_CAROUSEL", "")
255
+ slide_count = settings.get("CAROUSEL_SLIDE_COUNT", 5)
256
+
257
+ prompt = prompt_template.format(
258
+ news_context=news_context,
259
+ exclude_str=exclude_str,
260
+ tone=tone,
261
+ slide_count=slide_count
262
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  print("[Content Generator] Генерация поста-карусели (Persona Skills)...")
264
  raw_res = generate_text_via_gemini(prompt)
265
  data = clean_json_response(raw_res)
 
282
  history_list = "\n".join([f"- Пост (Лайки: {p.get('likes', 0)}): {p.get('text')[:100]}..." for p in historical_data])
283
  context = f"Для справки, вот наши самые успешные посты в прошлом:\n{history_list}\n"
284
 
285
+ settings = settings_manager.load_settings()
286
+ prompt_template = settings.get("PROMPT_PREDICTOR", "")
287
+ prompt = prompt_template.format(
288
+ context=context,
289
+ post_text=post_text
290
+ )
 
 
 
 
 
 
 
 
 
 
 
291
  headers = {"Content-Type": "application/json"}
292
  payload = {
293
  "model": GEMINI_MODEL,
dashboard.html CHANGED
@@ -5,6 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Threads Automation Agent</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
 
8
  <style>
9
  :root {
10
  --bg-base: #050505;
@@ -20,12 +21,7 @@
20
  --error: #ef4444;
21
  }
22
 
23
- * {
24
- box-sizing: border-box;
25
- margin: 0;
26
- padding: 0;
27
- }
28
-
29
  body {
30
  font-family: 'Outfit', sans-serif;
31
  background-color: var(--bg-base);
@@ -38,301 +34,131 @@
38
 
39
  /* Dynamic Background */
40
  .ambient-bg {
41
- position: fixed;
42
- top: 0; left: 0; width: 100vw; height: 100vh;
43
- z-index: -1;
44
- overflow: hidden;
45
- background: var(--bg-base);
46
  }
47
  .orb {
48
- position: absolute;
49
- border-radius: 50%;
50
- filter: blur(100px);
51
- opacity: 0.4;
52
  animation: float 20s infinite alternate cubic-bezier(0.4, 0, 0.2, 1);
53
  }
54
- .orb-1 {
55
- top: -10%; left: -10%; width: 50vw; height: 50vw;
56
- background: radial-gradient(circle, var(--accent-secondary) 0%, transparent 70%);
57
- animation-delay: -5s;
58
- }
59
- .orb-2 {
60
- bottom: -20%; right: -10%; width: 60vw; height: 60vw;
61
- background: radial-gradient(circle, var(--accent-primary) 0%, transparent 70%);
62
- }
63
-
64
- @keyframes float {
65
- 0% { transform: translate(0, 0) scale(1); }
66
- 100% { transform: translate(10%, 15%) scale(1.2); }
67
- }
68
 
69
  /* Layout */
70
  .app-container {
71
- max-width: 1400px;
72
- margin: 0 auto;
73
- padding: 2rem;
74
- flex: 1;
75
- display: flex;
76
- flex-direction: column;
77
- gap: 2rem;
78
- width: 100%;
79
  }
80
 
81
  /* Header */
82
  header {
83
- display: flex;
84
- justify-content: space-between;
85
- align-items: center;
86
- padding: 1.5rem 2rem;
87
- background: var(--bg-surface);
88
- backdrop-filter: blur(20px);
89
- -webkit-backdrop-filter: blur(20px);
90
- border: 1px solid var(--glass-border);
91
- border-radius: 24px;
92
- box-shadow: 0 8px 32px rgba(0,0,0,0.2);
93
  }
94
-
95
- .logo-area {
96
- display: flex;
97
- align-items: center;
98
- gap: 1rem;
99
- }
100
-
101
  .logo-icon {
102
- width: 48px;
103
- height: 48px;
104
- background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
105
- border-radius: 14px;
106
- display: flex;
107
- align-items: center;
108
- justify-content: center;
109
  box-shadow: 0 0 20px rgba(112, 0, 255, 0.4);
110
  }
111
-
112
- .logo-icon svg {
113
- width: 28px;
114
- height: 28px;
115
- color: white;
116
- }
117
-
118
- .title {
119
- font-size: 1.8rem;
120
- font-weight: 800;
121
- letter-spacing: -0.5px;
122
- background: linear-gradient(to right, #fff, #a1a1aa);
123
- -webkit-background-clip: text;
124
- -webkit-text-fill-color: transparent;
125
- }
126
-
127
  .status-pill {
128
- display: flex;
129
- align-items: center;
130
- gap: 0.5rem;
131
- padding: 0.5rem 1rem;
132
- background: rgba(16, 185, 129, 0.1);
133
- border: 1px solid rgba(16, 185, 129, 0.2);
134
- border-radius: 99px;
135
- font-size: 0.9rem;
136
- font-weight: 600;
137
- color: var(--success);
138
- box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
139
  }
 
 
140
 
141
- .pulse-dot {
142
- width: 8px; height: 8px;
143
- background: var(--success);
144
- border-radius: 50%;
145
- animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
146
  }
147
-
148
- @keyframes ping {
149
- 75%, 100% { transform: scale(2.5); opacity: 0; }
 
150
  }
 
 
151
 
152
  /* Main Grid */
153
  .dashboard-grid {
154
- display: grid;
155
- grid-template-columns: 350px 1fr;
156
- gap: 2rem;
157
- height: calc(100vh - 180px);
158
- min-height: 600px;
159
- }
160
-
161
- @media (max-width: 1024px) {
162
- .dashboard-grid {
163
- grid-template-columns: 1fr;
164
- height: auto;
165
- }
166
  }
 
 
 
 
167
 
168
- /* Cards / Panels */
169
  .panel {
170
- background: var(--bg-surface);
171
- backdrop-filter: blur(20px);
172
- -webkit-backdrop-filter: blur(20px);
173
- border: 1px solid var(--glass-border);
174
- border-radius: 24px;
175
- padding: 1.5rem;
176
- display: flex;
177
- flex-direction: column;
178
- box-shadow: 0 8px 32px rgba(0,0,0,0.2);
179
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s ease;
180
- overflow: hidden;
181
- }
182
-
183
- .panel:hover {
184
- border-color: rgba(255, 255, 255, 0.15);
185
- transform: translateY(-4px);
186
- }
187
-
188
- .panel-header {
189
- display: flex;
190
- align-items: center;
191
- gap: 0.75rem;
192
- margin-bottom: 1.5rem;
193
- padding-bottom: 1rem;
194
- border-bottom: 1px solid var(--glass-border);
195
- }
196
-
197
- .panel-title {
198
- font-size: 1.1rem;
199
- font-weight: 700;
200
- color: #fff;
201
- text-transform: uppercase;
202
- letter-spacing: 1px;
203
  }
204
-
205
- .panel-icon {
206
- color: var(--accent-primary);
207
- }
208
-
209
- /* Stats Section */
210
- .stats-container {
211
- display: flex;
212
- flex-direction: column;
213
- gap: 1rem;
214
- flex: 1;
215
- }
216
-
217
- .stat-box {
218
- background: rgba(0,0,0,0.3);
219
- border: 1px solid var(--glass-border);
220
- border-radius: 16px;
221
- padding: 1.25rem;
222
- display: flex;
223
- align-items: center;
224
- justify-content: space-between;
225
- position: relative;
226
- overflow: hidden;
227
- }
228
-
229
- .stat-box::before {
230
- content: '';
231
- position: absolute;
232
- top: 0; left: 0; width: 4px; height: 100%;
233
- background: var(--accent-primary);
234
- border-radius: 4px 0 0 4px;
235
- }
236
-
237
- .stat-box:nth-child(2)::before { background: var(--accent-secondary); }
238
- .stat-box:nth-child(3)::before { background: var(--success); }
239
-
240
- .stat-info { display: flex; flex-direction: column; gap: 0.25rem; }
241
 
242
- .stat-label {
243
- font-size: 0.85rem;
244
- color: var(--text-muted);
245
- font-weight: 500;
246
- text-transform: uppercase;
247
- letter-spacing: 1px;
248
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
- .stat-value {
251
- font-size: 2.5rem;
252
- font-weight: 800;
253
- color: #fff;
254
- line-height: 1;
255
  }
256
-
257
- /* Terminal Logs Section */
258
- .terminal-panel {
259
- grid-column: span 1;
260
- display: flex;
261
- flex-direction: column;
 
262
  }
 
263
 
 
 
264
  .terminal-window {
265
- flex: 1;
266
- background: #09090b;
267
- border-radius: 12px;
268
- padding: 1rem;
269
- font-family: 'JetBrains Mono', monospace;
270
- font-size: 0.85rem;
271
- color: #d4d4d8;
272
- overflow-y: auto;
273
- border: 1px solid #27272a;
274
- box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);
275
- line-height: 1.6;
276
- scroll-behavior: smooth;
277
  }
278
-
279
- .terminal-window::-webkit-scrollbar { width: 6px; }
280
- .terminal-window::-webkit-scrollbar-track { background: transparent; }
281
- .terminal-window::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
282
-
283
  .log-time { color: #52525b; margin-right: 8px; }
284
  .log-error { color: var(--error); font-weight: 700; }
285
- .log-success { color: var(--success); font-weight: 700; text-shadow: 0 0 10px rgba(16,185,129,0.3); }
286
  .log-warning { color: var(--warning); }
287
  .log-info { color: var(--accent-primary); }
288
- .log-highlight { color: #fff; background: rgba(255,255,255,0.1); padding: 0 4px; border-radius: 4px;}
289
-
290
- /* Top Posts */
291
- .top-posts {
292
- margin-top: 1rem;
293
- display: flex;
294
- flex-direction: column;
295
- gap: 0.75rem;
296
- overflow-y: auto;
297
- max-height: 250px;
298
- padding-right: 5px;
299
- }
300
-
301
- .top-posts::-webkit-scrollbar { width: 4px; }
302
- .top-posts::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; }
303
-
304
- .post-card {
305
- background: rgba(0,0,0,0.2);
306
- border: 1px solid var(--glass-border);
307
- border-radius: 12px;
308
- padding: 1rem;
309
- font-size: 0.9rem;
310
- transition: background 0.2s;
311
- }
312
- .post-card:hover { background: rgba(255,255,255,0.05); }
313
-
314
- .post-text {
315
- color: #e4e4e7;
316
- margin-bottom: 0.5rem;
317
- display: -webkit-box;
318
- -webkit-line-clamp: 2;
319
- -webkit-box-orient: vertical;
320
- overflow: hidden;
321
- line-height: 1.4;
322
- }
323
-
324
- .post-metrics {
325
- display: flex;
326
- gap: 1rem;
327
- font-size: 0.8rem;
328
- color: var(--accent-primary);
329
- font-weight: 600;
330
- }
331
- .metric-badge {
332
- background: rgba(0, 240, 255, 0.1);
333
- padding: 2px 8px;
334
- border-radius: 4px;
335
- }
336
  </style>
337
  </head>
338
  <body>
@@ -344,65 +170,128 @@
344
  <div class="app-container">
345
  <header>
346
  <div class="logo-area">
347
- <div class="logo-icon">
348
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
349
- <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
350
- </svg>
351
- </div>
352
  <div class="title">Threads Automation Agent</div>
353
  </div>
354
  <div class="status-pill">
355
- <div class="pulse-dot"></div>
356
- System Online
357
  </div>
358
  </header>
359
 
 
 
 
 
 
 
360
  <div class="dashboard-grid">
361
- <!-- Left Column: Stats & Top Posts -->
362
- <div class="stats-container">
363
- <div class="panel">
364
- <div class="panel-header">
365
- <svg class="panel-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>
366
- <h2 class="panel-title">Live Metrics</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  </div>
368
- <div style="display:flex; flex-direction:column; gap:1rem;">
369
- <div class="stat-box">
370
- <div class="stat-info">
371
- <span class="stat-label">Total Posts</span>
372
- <span class="stat-value" id="val-posts">0</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  </div>
374
  </div>
375
- <div class="stat-box">
376
- <div class="stat-info">
377
- <span class="stat-label">Total Likes</span>
378
- <span class="stat-value" id="val-likes">0</span>
 
 
 
 
379
  </div>
380
  </div>
381
- <div class="stat-box">
382
- <div class="stat-info">
383
- <span class="stat-label">Total Comments</span>
384
- <span class="stat-value" id="val-replies">0</span>
 
 
 
 
 
 
385
  </div>
386
  </div>
 
 
 
 
387
  </div>
388
  </div>
389
 
390
- <div class="panel" style="flex:1;">
391
- <div class="panel-header">
392
- <svg class="panel-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
393
- <h2 class="panel-title">Top Performing</h2>
394
- </div>
395
- <div class="top-posts" id="top-posts-container">
396
- <div style="color:var(--text-muted); text-align:center; padding: 2rem 0;">Awaiting data...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  </div>
398
  </div>
 
399
  </div>
400
 
401
  <!-- Right Column: Terminal -->
402
  <div class="panel terminal-panel">
403
  <div class="panel-header">
404
- <svg class="panel-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
405
- <h2 class="panel-title">Execution Logs</h2>
406
  </div>
407
  <div class="terminal-window" id="terminal-window">
408
  <div style="color: #a1a1aa;">> Initiating connection...</div>
@@ -411,107 +300,173 @@
411
  </div>
412
  </div>
413
 
 
 
414
  <script>
415
- function colorizeLog(line) {
416
- // Apply colors based on keywords
417
- if (line.includes('[ERROR]') || line.includes('Exception') || line.includes('Error')) {
418
- return `<span class="log-error">${line}</span>`;
419
- } else if (line.includes('SUCCESS') || line.includes('одобрен') || line.includes('успешно') || line.includes('Успішно')) {
420
- return `<span class="log-success">${line}</span>`;
421
- } else if (line.includes('Predictor') || line.includes('[Auto-Improvement]')) {
422
- return `<span class="log-warning">${line}</span>`;
423
- } else if (line.includes('Scheduler') || line.includes('Keep-Alive') || line.includes('Content Generator')) {
424
- return `<span class="log-info">${line}</span>`;
425
- } else if (line.includes('[ТЕСТ]')) {
426
- return `<span class="log-highlight">${line}</span>`;
427
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
 
429
- // Extract timestamps if they exist e.g. [2026-06-07 10:00:00]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  return line.replace(/^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\])(.*)/, '<span class="log-time">$1</span>$2');
431
  }
432
 
433
  let lastLogs = "";
434
-
435
  async function fetchLogs() {
436
  try {
437
- const response = await fetch('/api/logs');
438
- const data = await response.json();
439
  const term = document.getElementById('terminal-window');
440
-
441
  if (data.logs !== lastLogs) {
442
  const isScrolledToBottom = term.scrollHeight - term.clientHeight <= term.scrollTop + 5;
443
-
444
- const formattedLogs = data.logs.split('\n')
445
- .filter(l => l.trim() !== '')
446
- .map(l => colorizeLog(l))
447
- .join('<br>');
448
-
449
- term.innerHTML = formattedLogs || '<span style="color:#a1a1aa">> No logs available yet...</span>';
450
  lastLogs = data.logs;
451
-
452
- if (isScrolledToBottom) {
453
- term.scrollTop = term.scrollHeight;
454
- }
455
  }
456
- } catch (err) {
457
- console.error("Failed to fetch logs:", err);
458
- }
459
  }
460
 
461
  async function fetchStats() {
462
  try {
463
- const response = await fetch('/api/stats');
464
- const data = await response.json();
465
-
466
- // Animate numbers
467
- animateValue("val-posts", parseInt(document.getElementById('val-posts').innerText) || 0, data.total_posts, 500);
468
- animateValue("val-likes", parseInt(document.getElementById('val-likes').innerText) || 0, data.total_likes, 500);
469
- animateValue("val-replies", parseInt(document.getElementById('val-replies').innerText) || 0, data.total_replies, 500);
470
 
471
- const topPostsContainer = document.getElementById('top-posts-container');
472
  if (data.top_posts && data.top_posts.length > 0) {
473
- let html = '';
474
- data.top_posts.forEach(post => {
475
- let text = post.text;
476
- if(text.length > 120) text = text.substring(0, 120) + '...';
477
- html += `
478
- <div class="post-card">
479
- <div class="post-text">${text}</div>
480
- <div class="post-metrics">
481
- <span class="metric-badge">❤️ ${post.likes || 0}</span>
482
- <span class="metric-badge">💬 ${post.replies || 0}</span>
483
- ${post.predicted_score ? `<span class="metric-badge">Score: ${post.predicted_score}/10</span>` : ''}
484
- </div>
485
  </div>
486
- `;
487
- });
488
- topPostsContainer.innerHTML = html;
489
- }
490
- } catch (err) {
491
- console.error("Failed to fetch stats:", err);
492
- }
493
- }
494
-
495
- function animateValue(id, start, end, duration) {
496
- if (start === end) return;
497
- let current = start;
498
- const range = end - start;
499
- const increment = end > start ? 1 : -1;
500
- const stepTime = Math.abs(Math.floor(duration / range));
501
- const obj = document.getElementById(id);
502
- const timer = setInterval(function() {
503
- current += increment;
504
- obj.innerText = current;
505
- if (current == end) {
506
- clearInterval(timer);
507
  }
508
- }, stepTime);
 
509
  }
510
 
511
- // Initialize
 
512
  fetchLogs();
513
  fetchStats();
514
- setInterval(fetchLogs, 2000); // Fast log updates
515
  setInterval(fetchStats, 10000);
516
  </script>
517
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Threads Automation Agent</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
  <style>
10
  :root {
11
  --bg-base: #050505;
 
21
  --error: #ef4444;
22
  }
23
 
24
+ * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
25
  body {
26
  font-family: 'Outfit', sans-serif;
27
  background-color: var(--bg-base);
 
34
 
35
  /* Dynamic Background */
36
  .ambient-bg {
37
+ position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
38
+ z-index: -1; overflow: hidden; background: var(--bg-base);
 
 
 
39
  }
40
  .orb {
41
+ position: absolute; border-radius: 50%; filter: blur(100px); opacity: 0.4;
 
 
 
42
  animation: float 20s infinite alternate cubic-bezier(0.4, 0, 0.2, 1);
43
  }
44
+ .orb-1 { top: -10%; left: -10%; width: 50vw; height: 50vw; background: radial-gradient(circle, var(--accent-secondary) 0%, transparent 70%); animation-delay: -5s; }
45
+ .orb-2 { bottom: -20%; right: -10%; width: 60vw; height: 60vw; background: radial-gradient(circle, var(--accent-primary) 0%, transparent 70%); }
46
+ @keyframes float { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(10%, 15%) scale(1.2); } }
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  /* Layout */
49
  .app-container {
50
+ max-width: 1500px; margin: 0 auto; padding: 2rem;
51
+ flex: 1; display: flex; flex-direction: column; gap: 2rem; width: 100%;
 
 
 
 
 
 
52
  }
53
 
54
  /* Header */
55
  header {
56
+ display: flex; justify-content: space-between; align-items: center;
57
+ padding: 1.5rem 2rem; background: var(--bg-surface);
58
+ backdrop-filter: blur(20px); border: 1px solid var(--glass-border);
59
+ border-radius: 24px; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
 
 
 
 
 
 
60
  }
61
+ .logo-area { display: flex; align-items: center; gap: 1rem; }
 
 
 
 
 
 
62
  .logo-icon {
63
+ width: 48px; height: 48px; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
64
+ border-radius: 14px; display: flex; align-items: center; justify-content: center;
 
 
 
 
 
65
  box-shadow: 0 0 20px rgba(112, 0, 255, 0.4);
66
  }
67
+ .logo-icon svg { width: 28px; height: 28px; color: white; }
68
+ .title { font-size: 1.8rem; font-weight: 800; background: linear-gradient(to right, #fff, #a1a1aa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  .status-pill {
70
+ display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem;
71
+ background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2);
72
+ border-radius: 99px; font-size: 0.9rem; font-weight: 600; color: var(--success);
 
 
 
 
 
 
 
 
73
  }
74
+ .pulse-dot { width: 8px; height: 8px; background: var(--success); border-radius: 50%; animation: ping 1.5s infinite; }
75
+ @keyframes ping { 75%, 100% { transform: scale(2.5); opacity: 0; } }
76
 
77
+ /* Tabs Nav */
78
+ .tabs {
79
+ display: flex; gap: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1rem;
 
 
80
  }
81
+ .tab-btn {
82
+ background: transparent; color: var(--text-muted); border: none; font-size: 1.1rem;
83
+ font-family: 'Outfit'; font-weight: 600; cursor: pointer; padding: 0.5rem 1rem;
84
+ border-radius: 8px; transition: 0.2s;
85
  }
86
+ .tab-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
87
+ .tab-btn.active { color: var(--accent-primary); background: rgba(0, 240, 255, 0.1); }
88
 
89
  /* Main Grid */
90
  .dashboard-grid {
91
+ display: grid; grid-template-columns: 1fr 400px; gap: 2rem; height: calc(100vh - 250px); min-height: 600px;
 
 
 
 
 
 
 
 
 
 
 
92
  }
93
+ .tab-content { display: none; flex-direction: column; gap: 1.5rem; overflow-y: auto; padding-right: 10px;}
94
+ .tab-content.active { display: flex; }
95
+ .tab-content::-webkit-scrollbar { width: 6px; }
96
+ .tab-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
97
 
98
+ /* Panels */
99
  .panel {
100
+ background: var(--bg-surface); backdrop-filter: blur(20px);
101
+ border: 1px solid var(--glass-border); border-radius: 24px; padding: 1.5rem;
102
+ display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
+ .panel-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--glass-border); justify-content: space-between;}
105
+ .panel-title { font-size: 1.1rem; font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 1px; display: flex; align-items: center; gap: 0.5rem;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ /* Stats & Overview */
108
+ .stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
109
+ .stat-box { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); border-radius: 16px; padding: 1.25rem; border-left: 4px solid var(--accent-primary); }
110
+ .stat-box:nth-child(2) { border-left-color: var(--accent-secondary); }
111
+ .stat-box:nth-child(3) { border-left-color: var(--success); }
112
+ .stat-label { font-size: 0.85rem; color: var(--text-muted); text-transform: uppercase; }
113
+ .stat-value { font-size: 2.5rem; font-weight: 800; color: #fff; line-height: 1.2; }
114
+
115
+ .chart-container { height: 250px; width: 100%; position: relative; }
116
+
117
+ .post-card { background: rgba(0,0,0,0.2); border: 1px solid var(--glass-border); border-radius: 12px; padding: 1rem; margin-bottom: 0.75rem;}
118
+ .post-text { color: #e4e4e7; margin-bottom: 0.5rem; font-size: 0.9rem; }
119
+ .post-metrics { display: flex; gap: 1rem; font-size: 0.8rem; color: var(--accent-primary); font-weight: 600; }
120
+ .metric-badge { background: rgba(0, 240, 255, 0.1); padding: 2px 8px; border-radius: 4px; }
121
+
122
+ /* Forms & Inputs */
123
+ .form-group { margin-bottom: 1rem; }
124
+ .form-label { display: block; margin-bottom: 0.5rem; color: #e4e4e7; font-size: 0.9rem; font-weight: 500; }
125
+ .form-input {
126
+ width: 100%; padding: 0.75rem 1rem; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border);
127
+ border-radius: 8px; color: #fff; font-family: 'Outfit'; font-size: 1rem; transition: border 0.2s;
128
+ }
129
+ .form-input:focus { outline: none; border-color: var(--accent-primary); }
130
+ textarea.form-input { min-height: 250px; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; resize: vertical; }
131
+
132
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
133
 
134
+ .btn-save {
135
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
136
+ color: #fff; border: none; padding: 0.75rem 2rem; border-radius: 8px;
137
+ font-family: 'Outfit'; font-weight: 700; font-size: 1rem; cursor: pointer;
138
+ box-shadow: 0 4px 15px rgba(112,0,255,0.3); transition: transform 0.2s;
139
  }
140
+ .btn-save:hover { transform: translateY(-2px); }
141
+
142
+ .toast {
143
+ position: fixed; bottom: 20px; right: 20px; background: var(--success); color: #000;
144
+ padding: 1rem 2rem; border-radius: 8px; font-weight: 700; font-size: 1rem;
145
+ box-shadow: 0 4px 20px rgba(16,185,129,0.4); transform: translateY(150%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
146
+ z-index: 1000;
147
  }
148
+ .toast.show { transform: translateY(0); }
149
 
150
+ /* Terminal */
151
+ .terminal-panel { height: 100%; }
152
  .terminal-window {
153
+ flex: 1; background: #09090b; border-radius: 12px; padding: 1rem;
154
+ font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: #d4d4d8;
155
+ overflow-y: auto; border: 1px solid #27272a; line-height: 1.6;
 
 
 
 
 
 
 
 
 
156
  }
 
 
 
 
 
157
  .log-time { color: #52525b; margin-right: 8px; }
158
  .log-error { color: var(--error); font-weight: 700; }
159
+ .log-success { color: var(--success); font-weight: 700; }
160
  .log-warning { color: var(--warning); }
161
  .log-info { color: var(--accent-primary); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  </style>
163
  </head>
164
  <body>
 
170
  <div class="app-container">
171
  <header>
172
  <div class="logo-area">
173
+ <div class="logo-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg></div>
 
 
 
 
174
  <div class="title">Threads Automation Agent</div>
175
  </div>
176
  <div class="status-pill">
177
+ <div class="pulse-dot"></div> System Online
 
178
  </div>
179
  </header>
180
 
181
+ <div class="tabs">
182
+ <button class="tab-btn active" onclick="switchTab('overview')">📊 Overview & Analytics</button>
183
+ <button class="tab-btn" onclick="switchTab('settings')">⚙️ Settings & Limits</button>
184
+ <button class="tab-btn" onclick="switchTab('prompts')">🧠 Prompt Engineering</button>
185
+ </div>
186
+
187
  <div class="dashboard-grid">
188
+ <!-- Left Column: Tabs Content -->
189
+ <div style="display: flex; flex-direction: column; overflow: hidden; height: 100%;">
190
+
191
+ <!-- TAB: OVERVIEW -->
192
+ <div id="tab-overview" class="tab-content active">
193
+ <div class="stats-row">
194
+ <div class="stat-box"><div class="stat-label">Total Posts</div><div class="stat-value" id="val-posts">0</div></div>
195
+ <div class="stat-box"><div class="stat-label">Total Likes</div><div class="stat-value" id="val-likes">0</div></div>
196
+ <div class="stat-box"><div class="stat-label">Total Replies</div><div class="stat-value" id="val-replies">0</div></div>
197
+ </div>
198
+
199
+ <div class="panel">
200
+ <div class="panel-header"><div class="panel-title">📈 Engagement Analytics</div></div>
201
+ <div class="chart-container">
202
+ <canvas id="analyticsChart"></canvas>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="panel">
207
+ <div class="panel-header"><div class="panel-title">⭐ Top Performing Posts</div></div>
208
+ <div id="top-posts-container">
209
+ <div style="color:var(--text-muted); text-align:center; padding: 1rem 0;">Awaiting data...</div>
210
+ </div>
211
  </div>
212
+ </div>
213
+
214
+ <!-- TAB: SETTINGS -->
215
+ <div id="tab-settings" class="tab-content">
216
+ <div class="panel">
217
+ <div class="panel-header">
218
+ <div class="panel-title">⚙️ Bot Configuration</div>
219
+ <button class="btn-save" onclick="saveSettings()">Save Changes</button>
220
+ </div>
221
+
222
+ <h3 style="margin-bottom: 1rem; color: var(--accent-primary);">Schedule Slots (Kyiv Time)</h3>
223
+ <div class="form-row">
224
+ <div class="form-group">
225
+ <label class="form-label">Morning Slot Start</label>
226
+ <input type="time" class="form-input" id="set_morning_start">
227
+ </div>
228
+ <div class="form-group">
229
+ <label class="form-label">Morning Slot End</label>
230
+ <input type="time" class="form-input" id="set_morning_end">
231
  </div>
232
  </div>
233
+ <div class="form-row">
234
+ <div class="form-group">
235
+ <label class="form-label">Evening Slot Start</label>
236
+ <input type="time" class="form-input" id="set_evening_start">
237
+ </div>
238
+ <div class="form-group">
239
+ <label class="form-label">Evening Slot End</label>
240
+ <input type="time" class="form-input" id="set_evening_end">
241
  </div>
242
  </div>
243
+
244
+ <h3 style="margin: 1.5rem 0 1rem 0; color: var(--accent-primary);">Daily Limits & Content</h3>
245
+ <div class="form-row">
246
+ <div class="form-group">
247
+ <label class="form-label">Daily Single Posts Limit</label>
248
+ <input type="number" class="form-input" id="set_single_limit" min="0" max="10">
249
+ </div>
250
+ <div class="form-group">
251
+ <label class="form-label">Daily Carousels Limit</label>
252
+ <input type="number" class="form-input" id="set_carousel_limit" min="0" max="10">
253
  </div>
254
  </div>
255
+ <div class="form-group">
256
+ <label class="form-label">Slides per Carousel</label>
257
+ <input type="number" class="form-input" id="set_slide_count" min="2" max="10">
258
+ </div>
259
  </div>
260
  </div>
261
 
262
+ <!-- TAB: PROMPTS -->
263
+ <div id="tab-prompts" class="tab-content">
264
+ <div class="panel">
265
+ <div class="panel-header">
266
+ <div class="panel-title">🧠 AI Prompt Editor</div>
267
+ <button class="btn-save" onclick="saveSettings()">Save Prompts</button>
268
+ </div>
269
+
270
+ <div class="form-group">
271
+ <label class="form-label">Single Post Generation Prompt</label>
272
+ <textarea class="form-input" id="set_prompt_single"></textarea>
273
+ <small style="color:var(--text-muted); display:block; margin-top:5px;">Available vars: {news_context}, {exclude_str}, {tone}</small>
274
+ </div>
275
+
276
+ <div class="form-group">
277
+ <label class="form-label">Carousel Generation Prompt</label>
278
+ <textarea class="form-input" id="set_prompt_carousel"></textarea>
279
+ <small style="color:var(--text-muted); display:block; margin-top:5px;">Available vars: {news_context}, {exclude_str}, {tone}, {slide_count}</small>
280
+ </div>
281
+
282
+ <div class="form-group">
283
+ <label class="form-label">Viral Predictor AI Prompt</label>
284
+ <textarea class="form-input" id="set_prompt_predictor"></textarea>
285
+ </div>
286
  </div>
287
  </div>
288
+
289
  </div>
290
 
291
  <!-- Right Column: Terminal -->
292
  <div class="panel terminal-panel">
293
  <div class="panel-header">
294
+ <div class="panel-title">Terminal Logs</div>
 
295
  </div>
296
  <div class="terminal-window" id="terminal-window">
297
  <div style="color: #a1a1aa;">> Initiating connection...</div>
 
300
  </div>
301
  </div>
302
 
303
+ <div class="toast" id="toast">Settings Saved Successfully!</div>
304
+
305
  <script>
306
+ let myChart = null;
307
+
308
+ function switchTab(tabId) {
309
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
310
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
311
+
312
+ event.target.classList.add('active');
313
+ document.getElementById('tab-' + tabId).classList.add('active');
314
+ }
315
+
316
+ function showToast() {
317
+ const t = document.getElementById('toast');
318
+ t.classList.add('show');
319
+ setTimeout(() => t.classList.remove('show'), 3000);
320
+ }
321
+
322
+ async function loadSettings() {
323
+ try {
324
+ const res = await fetch('/api/settings');
325
+ const data = await res.json();
326
+
327
+ document.getElementById('set_morning_start').value = data.MORNING_SLOT_START;
328
+ document.getElementById('set_morning_end').value = data.MORNING_SLOT_END;
329
+ document.getElementById('set_evening_start').value = data.EVENING_SLOT_START;
330
+ document.getElementById('set_evening_end').value = data.EVENING_SLOT_END;
331
+
332
+ document.getElementById('set_single_limit').value = data.DAILY_SINGLE_POSTS_LIMIT;
333
+ document.getElementById('set_carousel_limit').value = data.DAILY_CAROUSELS_LIMIT;
334
+ document.getElementById('set_slide_count').value = data.CAROUSEL_SLIDE_COUNT;
335
+
336
+ document.getElementById('set_prompt_single').value = data.PROMPT_SINGLE;
337
+ document.getElementById('set_prompt_carousel').value = data.PROMPT_CAROUSEL;
338
+ document.getElementById('set_prompt_predictor').value = data.PROMPT_PREDICTOR;
339
+ } catch (err) { console.error("Error loading settings:", err); }
340
+ }
341
+
342
+ async function saveSettings() {
343
+ const data = {
344
+ MORNING_SLOT_START: document.getElementById('set_morning_start').value,
345
+ MORNING_SLOT_END: document.getElementById('set_morning_end').value,
346
+ EVENING_SLOT_START: document.getElementById('set_evening_start').value,
347
+ EVENING_SLOT_END: document.getElementById('set_evening_end').value,
348
+
349
+ DAILY_SINGLE_POSTS_LIMIT: parseInt(document.getElementById('set_single_limit').value),
350
+ DAILY_CAROUSELS_LIMIT: parseInt(document.getElementById('set_carousel_limit').value),
351
+ CAROUSEL_SLIDE_COUNT: parseInt(document.getElementById('set_slide_count').value),
352
+
353
+ PROMPT_SINGLE: document.getElementById('set_prompt_single').value,
354
+ PROMPT_CAROUSEL: document.getElementById('set_prompt_carousel').value,
355
+ PROMPT_PREDICTOR: document.getElementById('set_prompt_predictor').value,
356
+ };
357
+
358
+ try {
359
+ const res = await fetch('/api/save_settings', {
360
+ method: 'POST',
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify(data)
363
+ });
364
+ if(res.ok) showToast();
365
+ } catch (err) { alert("Error saving settings."); }
366
+ }
367
+
368
+ // Analytics Chart Setup
369
+ function initChart(chartData) {
370
+ const ctx = document.getElementById('analyticsChart').getContext('2d');
371
+
372
+ // Dummy historical data simulation if not enough data
373
+ const labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
374
+ const likesData = [0,0,0,0,0,0, chartData.total_likes];
375
+ const repliesData = [0,0,0,0,0,0, chartData.total_replies];
376
+
377
+ if(myChart) myChart.destroy();
378
 
379
+ myChart = new Chart(ctx, {
380
+ type: 'line',
381
+ data: {
382
+ labels: labels,
383
+ datasets: [
384
+ {
385
+ label: 'Likes',
386
+ data: likesData,
387
+ borderColor: '#00f0ff',
388
+ backgroundColor: 'rgba(0, 240, 255, 0.1)',
389
+ borderWidth: 2,
390
+ fill: true,
391
+ tension: 0.4
392
+ },
393
+ {
394
+ label: 'Comments',
395
+ data: repliesData,
396
+ borderColor: '#7000ff',
397
+ backgroundColor: 'rgba(112, 0, 255, 0.1)',
398
+ borderWidth: 2,
399
+ fill: true,
400
+ tension: 0.4
401
+ }
402
+ ]
403
+ },
404
+ options: {
405
+ responsive: true,
406
+ maintainAspectRatio: false,
407
+ plugins: { legend: { labels: { color: '#a1a1aa' } } },
408
+ scales: {
409
+ y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#a1a1aa' } },
410
+ x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#a1a1aa' } }
411
+ }
412
+ }
413
+ });
414
+ }
415
+
416
+ // Terminal & Stats Logic
417
+ function colorizeLog(line) {
418
+ if (line.includes('[ERROR]') || line.includes('Exception') || line.includes('Error')) return `<span class="log-error">${line}</span>`;
419
+ if (line.includes('SUCCESS') || line.includes('одобрен') || line.includes('успешно')) return `<span class="log-success">${line}</span>`;
420
+ if (line.includes('Predictor') || line.includes('[Auto-Improvement]')) return `<span class="log-warning">${line}</span>`;
421
+ if (line.includes('Scheduler') || line.includes('Keep-Alive') || line.includes('Content Generator')) return `<span class="log-info">${line}</span>`;
422
  return line.replace(/^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\])(.*)/, '<span class="log-time">$1</span>$2');
423
  }
424
 
425
  let lastLogs = "";
 
426
  async function fetchLogs() {
427
  try {
428
+ const res = await fetch('/api/logs');
429
+ const data = await res.json();
430
  const term = document.getElementById('terminal-window');
 
431
  if (data.logs !== lastLogs) {
432
  const isScrolledToBottom = term.scrollHeight - term.clientHeight <= term.scrollTop + 5;
433
+ term.innerHTML = data.logs.split('\n').filter(l => l.trim() !== '').map(l => colorizeLog(l)).join('<br>') || '<span style="color:#a1a1aa">> No logs...</span>';
 
 
 
 
 
 
434
  lastLogs = data.logs;
435
+ if (isScrolledToBottom) term.scrollTop = term.scrollHeight;
 
 
 
436
  }
437
+ } catch(e) {}
 
 
438
  }
439
 
440
  async function fetchStats() {
441
  try {
442
+ const res = await fetch('/api/stats');
443
+ const data = await res.json();
444
+ document.getElementById('val-posts').innerText = data.total_posts;
445
+ document.getElementById('val-likes').innerText = data.total_likes;
446
+ document.getElementById('val-replies').innerText = data.total_replies;
 
 
447
 
448
+ const tc = document.getElementById('top-posts-container');
449
  if (data.top_posts && data.top_posts.length > 0) {
450
+ tc.innerHTML = data.top_posts.map(post => `
451
+ <div class="post-card">
452
+ <div class="post-text">${post.text.length > 120 ? post.text.substring(0,120)+'...' : post.text}</div>
453
+ <div class="post-metrics">
454
+ <span class="metric-badge">❤️ ${post.likes || 0}</span>
455
+ <span class="metric-badge">💬 ${post.replies || 0}</span>
456
+ ${post.predicted_score ? `<span class="metric-badge">Score: ${post.predicted_score}/10</span>` : ''}
 
 
 
 
 
457
  </div>
458
+ </div>
459
+ `).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  }
461
+ if(!myChart) initChart(data);
462
+ } catch(e) {}
463
  }
464
 
465
+ // Init
466
+ loadSettings();
467
  fetchLogs();
468
  fetchStats();
469
+ setInterval(fetchLogs, 2000);
470
  setInterval(fetchStats, 10000);
471
  </script>
472
  </body>
keep_alive_server.py CHANGED
@@ -6,6 +6,7 @@ import threading
6
  import requests
7
  from http.server import HTTPServer, BaseHTTPRequestHandler
8
  from tracker import load_tracker
 
9
 
10
  class DashboardHandler(BaseHTTPRequestHandler):
11
  def do_GET(self):
@@ -17,6 +18,16 @@ class DashboardHandler(BaseHTTPRequestHandler):
17
  self.serve_logs()
18
  elif path == '/api/stats':
19
  self.serve_stats()
 
 
 
 
 
 
 
 
 
 
20
  else:
21
  self.send_response(404)
22
  self.end_headers()
@@ -93,6 +104,31 @@ class DashboardHandler(BaseHTTPRequestHandler):
93
 
94
  self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def run_server():
97
  server = HTTPServer(("0.0.0.0", 7860), DashboardHandler)
98
  print("[Keep-Alive] Веб-сервер дашборда запущен на порту 7860")
 
6
  import requests
7
  from http.server import HTTPServer, BaseHTTPRequestHandler
8
  from tracker import load_tracker
9
+ import settings_manager
10
 
11
  class DashboardHandler(BaseHTTPRequestHandler):
12
  def do_GET(self):
 
18
  self.serve_logs()
19
  elif path == '/api/stats':
20
  self.serve_stats()
21
+ elif path == '/api/settings':
22
+ self.serve_settings()
23
+ else:
24
+ self.send_response(404)
25
+ self.end_headers()
26
+
27
+ def do_POST(self):
28
+ path = self.path.split('?')[0]
29
+ if path == '/api/save_settings':
30
+ self.save_settings()
31
  else:
32
  self.send_response(404)
33
  self.end_headers()
 
104
 
105
  self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
106
 
107
+ def serve_settings(self):
108
+ self.send_response(200)
109
+ self.send_header("Content-type", "application/json; charset=utf-8")
110
+ self.end_headers()
111
+ settings = settings_manager.load_settings()
112
+ self.wfile.write(json.dumps(settings, ensure_ascii=False).encode('utf-8'))
113
+
114
+ def save_settings(self):
115
+ content_length = int(self.headers.get('Content-Length', 0))
116
+ post_data = self.rfile.read(content_length)
117
+
118
+ try:
119
+ new_settings = json.loads(post_data.decode('utf-8'))
120
+ if settings_manager.save_settings(new_settings):
121
+ self.send_response(200)
122
+ self.send_header("Content-type", "application/json; charset=utf-8")
123
+ self.end_headers()
124
+ self.wfile.write(json.dumps({"status": "success"}).encode('utf-8'))
125
+ else:
126
+ self.send_response(500)
127
+ self.end_headers()
128
+ except Exception as e:
129
+ self.send_response(400)
130
+ self.end_headers()
131
+
132
  def run_server():
133
  server = HTTPServer(("0.0.0.0", 7860), DashboardHandler)
134
  print("[Keep-Alive] Веб-сервер дашборда запущен на порту 7860")
list_secrets.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from huggingface_hub import get_token
3
+
4
+ token = get_token()
5
+ headers = {"Authorization": f"Bearer {token}"}
6
+ url = "https://huggingface.co/api/spaces/FincH12/Automation_Threads/secrets"
7
+ resp = requests.get(url, headers=headers)
8
+ if resp.status_code == 200:
9
+ for s in resp.json():
10
+ print(f"Found secret: {s['name']}")
11
+ else:
12
+ print(f"Failed to fetch secrets: {resp.status_code} {resp.text}")
main.py CHANGED
@@ -7,6 +7,7 @@ import argparse
7
  from datetime import datetime
8
 
9
  import config
 
10
 
11
  class LoggerOutput:
12
  def __init__(self, stream):
@@ -271,35 +272,35 @@ def run_test_carousel():
271
  # --- ОСНОВНОЙ РЕЖИМ ПЛАНИРОВАНИЯ ---
272
 
273
 
274
- def calculate_next_prime_time():
275
  """
276
- Рассчитывает время следующей публикации:
277
- Разносит посты по времени:
278
- - Утренний пост: с 08:30 до 10:00 по Киеву
279
- - Вечерний пост: с 18:30 до 20:00 по Киеву
280
- Если сейчас уже позже вечернего окна, планирует на утро следующего дня.
281
  """
282
  from zoneinfo import ZoneInfo
283
  tz = ZoneInfo("Europe/Kyiv")
284
  now = datetime.now(tz)
285
 
 
 
 
 
286
  # Определяем утреннее окно на сегодня
287
- morning_start = now.replace(hour=8, minute=30, second=0, microsecond=0)
288
- morning_end = now.replace(hour=10, minute=0, second=0, microsecond=0)
289
 
290
  # Определяем вечернее окно на сегодня
291
- evening_start = now.replace(hour=18, minute=30, second=0, microsecond=0)
292
- evening_end = now.replace(hour=20, minute=0, second=0, microsecond=0)
293
 
294
  if now < morning_end:
295
  # Планируем на сегодняшнее утро
296
- target_dt = morning_start.replace(minute=random.randint(30, 59))
297
  if target_dt < now:
298
  return now.timestamp() + 300 # Если время уже немного в прошлом, запускаем через 5 минут
299
  return target_dt.timestamp()
300
  elif now < evening_end:
301
  # Планируем на сегодняшний вечер
302
- target_dt = evening_start.replace(minute=random.randint(30, 59))
303
  if target_dt < now:
304
  return now.timestamp() + 300
305
  return target_dt.timestamp()
@@ -307,7 +308,8 @@ def calculate_next_prime_time():
307
  # Планируем на завтрашнее утро
308
  tomorrow = now.timestamp() + 24 * 3600
309
  tomorrow_dt = datetime.fromtimestamp(tomorrow, tz=tz)
310
- target_dt = tomorrow_dt.replace(hour=8, minute=random.randint(30, 59), second=0, microsecond=0)
 
311
  return target_dt.timestamp()
312
 
313
  def main_loop():
@@ -324,8 +326,10 @@ def main_loop():
324
  last_post_time = 0
325
  last_reset_day = datetime.now(tz).day
326
 
 
 
327
  # Рассчитываем время следующего поста
328
- next_post_time = calculate_next_prime_time()
329
  wait_minutes = (next_post_time - time.time()) / 60
330
  next_post_dt_str = datetime.fromtimestamp(next_post_time, tz=tz).strftime("%Y-%m-%d %H:%M:%S")
331
  print(f"[Scheduler] Следующая публикация запланирована на {next_post_dt_str} (Киев) (через {wait_minutes:.1f} минут).")
@@ -334,6 +338,7 @@ def main_loop():
334
  try:
335
  current_time = time.time()
336
  now_tz = datetime.now(tz)
 
337
 
338
  # Сброс суточных лимитов в полночь
339
  if now_tz.day != last_reset_day:
@@ -352,13 +357,13 @@ def main_loop():
352
  print("[Scheduler] Сегодня воскресенье — тихий день. Пропускаем публикации, оставляем только комментирование (вовлечение).")
353
  else:
354
  # Проверяем, есть ли доступные лимиты
355
- if carousels_today < config.DAILY_CAROUSELS_LIMIT or single_posts_today < config.DAILY_SINGLE_POSTS_LIMIT:
356
  # Выбираем тип публикации на основе лимитов
357
- if carousels_today < config.DAILY_CAROUSELS_LIMIT and single_posts_today < config.DAILY_SINGLE_POSTS_LIMIT:
358
  # Если оба лимита не превышены, выбираем с весом
359
- total_limit = config.DAILY_CAROUSELS_LIMIT + config.DAILY_SINGLE_POSTS_LIMIT
360
- is_carousel = random.random() < (config.DAILY_CAROUSELS_LIMIT / total_limit)
361
- elif carousels_today < config.DAILY_CAROUSELS_LIMIT:
362
  is_carousel = True
363
  else:
364
  is_carousel = False
@@ -377,10 +382,10 @@ def main_loop():
377
 
378
  last_post_time = current_time
379
  else:
380
- print(f"[Scheduler] Все суточные лимиты публикаций исчерпаны на сегодня ({config.DAILY_CAROUSELS_LIMIT} каруселей и {config.DAILY_SINGLE_POSTS_LIMIT} постов).")
381
 
382
  # Планируем следующий пост
383
- next_post_time = calculate_next_prime_time()
384
  wait_minutes = (next_post_time - time.time()) / 60
385
  next_post_dt_str = datetime.fromtimestamp(next_post_time, tz=tz).strftime("%Y-%m-%d %H:%M:%S")
386
  print(f"[Scheduler] Пост обработан. Следующий запланирован на {next_post_dt_str} (Киев) (через {wait_minutes:.1f} минут).")
 
7
  from datetime import datetime
8
 
9
  import config
10
+ import settings_manager
11
 
12
  class LoggerOutput:
13
  def __init__(self, stream):
 
272
  # --- ОСНОВНОЙ РЕЖИМ ПЛАНИРОВАНИЯ ---
273
 
274
 
275
+ def calculate_next_prime_time(settings):
276
  """
277
+ Рассчитывает время следующей публикации с учетом настроек из settings.json.
 
 
 
 
278
  """
279
  from zoneinfo import ZoneInfo
280
  tz = ZoneInfo("Europe/Kyiv")
281
  now = datetime.now(tz)
282
 
283
+ def parse_time(time_str):
284
+ h, m = map(int, time_str.split(':'))
285
+ return now.replace(hour=h, minute=m, second=0, microsecond=0)
286
+
287
  # Определяем утреннее окно на сегодня
288
+ morning_start = parse_time(settings["MORNING_SLOT_START"])
289
+ morning_end = parse_time(settings["MORNING_SLOT_END"])
290
 
291
  # Определяем вечернее окно на сегодня
292
+ evening_start = parse_time(settings["EVENING_SLOT_START"])
293
+ evening_end = parse_time(settings["EVENING_SLOT_END"])
294
 
295
  if now < morning_end:
296
  # Планируем на сегодняшнее утро
297
+ target_dt = morning_start.replace(minute=random.randint(0, 59))
298
  if target_dt < now:
299
  return now.timestamp() + 300 # Если время уже немного в прошлом, запускаем через 5 минут
300
  return target_dt.timestamp()
301
  elif now < evening_end:
302
  # Планируем на сегодняшний вечер
303
+ target_dt = evening_start.replace(minute=random.randint(0, 59))
304
  if target_dt < now:
305
  return now.timestamp() + 300
306
  return target_dt.timestamp()
 
308
  # Планируем на завтрашнее утро
309
  tomorrow = now.timestamp() + 24 * 3600
310
  tomorrow_dt = datetime.fromtimestamp(tomorrow, tz=tz)
311
+ h, m = map(int, settings["MORNING_SLOT_START"].split(':'))
312
+ target_dt = tomorrow_dt.replace(hour=h, minute=random.randint(0, 59), second=0, microsecond=0)
313
  return target_dt.timestamp()
314
 
315
  def main_loop():
 
326
  last_post_time = 0
327
  last_reset_day = datetime.now(tz).day
328
 
329
+ settings = settings_manager.load_settings()
330
+
331
  # Рассчитываем время следующего поста
332
+ next_post_time = calculate_next_prime_time(settings)
333
  wait_minutes = (next_post_time - time.time()) / 60
334
  next_post_dt_str = datetime.fromtimestamp(next_post_time, tz=tz).strftime("%Y-%m-%d %H:%M:%S")
335
  print(f"[Scheduler] Следующая публикация запланирована на {next_post_dt_str} (Киев) (через {wait_minutes:.1f} минут).")
 
338
  try:
339
  current_time = time.time()
340
  now_tz = datetime.now(tz)
341
+ settings = settings_manager.load_settings() # Динамически обновляем настройки
342
 
343
  # Сброс суточных лимитов в полночь
344
  if now_tz.day != last_reset_day:
 
357
  print("[Scheduler] Сегодня воскресенье — тихий день. Пропускаем публикации, оставляем только комментирование (вовлечение).")
358
  else:
359
  # Проверяем, есть ли доступные лимиты
360
+ if carousels_today < settings["DAILY_CAROUSELS_LIMIT"] or single_posts_today < settings["DAILY_SINGLE_POSTS_LIMIT"]:
361
  # Выбираем тип публикации на основе лимитов
362
+ if carousels_today < settings["DAILY_CAROUSELS_LIMIT"] and single_posts_today < settings["DAILY_SINGLE_POSTS_LIMIT"]:
363
  # Если оба лимита не превышены, выбираем с весом
364
+ total_limit = settings["DAILY_CAROUSELS_LIMIT"] + settings["DAILY_SINGLE_POSTS_LIMIT"]
365
+ is_carousel = random.random() < (settings["DAILY_CAROUSELS_LIMIT"] / total_limit)
366
+ elif carousels_today < settings["DAILY_CAROUSELS_LIMIT"]:
367
  is_carousel = True
368
  else:
369
  is_carousel = False
 
382
 
383
  last_post_time = current_time
384
  else:
385
+ print(f"[Scheduler] Все суточные лимиты публикаций исчерпаны на сегодня ({settings['DAILY_CAROUSELS_LIMIT']} каруселей и {settings['DAILY_SINGLE_POSTS_LIMIT']} постов).")
386
 
387
  # Планируем следующий пост
388
+ next_post_time = calculate_next_prime_time(settings)
389
  wait_minutes = (next_post_time - time.time()) / 60
390
  next_post_dt_str = datetime.fromtimestamp(next_post_time, tz=tz).strftime("%Y-%m-%d %H:%M:%S")
391
  print(f"[Scheduler] Пост обработан. Следующий запланирован на {next_post_dt_str} (Киев) (через {wait_minutes:.1f} минут).")
make_private.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from huggingface_hub import HfApi
3
+
4
+ api = HfApi()
5
+ new_space_id = "FincH12/Automation_Threads_EU"
6
+
7
+ try:
8
+ api.update_repo_visibility(repo_id=new_space_id, repo_type="space", private=True)
9
+ print("Space is now PRIVATE.")
10
+ except Exception as e:
11
+ print(f"Error: {e}")
12
+
settings_manager.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ SETTINGS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "settings.json")
5
+
6
+ DEFAULT_SETTINGS = {
7
+ # Расписание
8
+ "MORNING_SLOT_START": "08:30",
9
+ "MORNING_SLOT_END": "10:00",
10
+ "EVENING_SLOT_START": "18:30",
11
+ "EVENING_SLOT_END": "20:00",
12
+ "DAILY_CAROUSELS_LIMIT": 1,
13
+ "DAILY_SINGLE_POSTS_LIMIT": 1,
14
+ "CAROUSEL_SLIDE_COUNT": 5,
15
+
16
+ # Промпты
17
+ "PROMPT_SINGLE": """Використовуючи наступний новинний контекст:
18
+ \"\"\"{news_context}\"\"\"
19
+ {exclude_str}
20
+
21
+ Створи 1 одиночний пост для Threads українською мовою. Вибери одну найцікавішу новину з фокусом на одну з тем: ІІ-безпека/етика, DeFi/криптовалюти, або стартапи/тех-сектор.
22
+
23
+ ⚡ ТВОЇ СУПЕРСИЛИ (SKILLS) & КРИТИЧНІ ПРАВИЛА:
24
+
25
+ 1. РОЛЬ: SENIOR SOCIAL MEDIA COPYWRITER (Текст посту)
26
+ - Психологія: Пиши максимально живим, людським стилем (як реальний експерт у Threads), уникаючи будь-якого ШІ-жаргону. Тон: {tone}.
27
+ - Позиція та думка: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати шаблон "[Факт]. [Риторичний вопрос]. Деталі...". Замість цього додай власну чітку експертну думку/позицію перед будь-яким запитанням (наприклад: "Я вважаю X, тому що Y. А як до цього ставитесь ви?"). Позиція має бути до запитання, а не замість неї. Уникай пустих риторичних питань.
28
+ - Самодостатність: Пост має бути повністю самодостатнім і нести цінність одразу в тексті. Посилання або картинка — лише доповнення, а не привід клікати.
29
+ - Заголовки: Пиши заголовки-крючки (hook), що чіпляють увагу, а не нудні заголовки-новини.
30
+ - Посилання: КАТЕГОРИЧНО ЗАБОРОНЕНО вставляти будь-які URL або посилання в полі description. Посилання на джерело має повертатися виключно окремо в ключі source_url.
31
+ - Хештеги: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати масові теги (#AI, #Crypto, #ШІ, #Технології). Використовуй лише 1-2 вузьких тега максимум (наприклад, #DeFiУкраїна, #LLMбезпека, #ХарківTech). Розташовуй теги строго в самому кінці тексту.
32
+
33
+ 2. РОЛЬ: ART DIRECTOR & UI/UX DESIGNER (Візуал)
34
+ - Дизайн фону: Ти створюєш преміальні промпти для генерації зображень (Flux.1). Ти ідеально відчуваєш композицію, тому завжди вимагаєш "clean negative space" для читабельності тексту.
35
+ - Копірайт для карток: Заголовок картки — до 4 слів (punchy). Тези — до 8 слів, тільки саме "м'ясо".
36
+
37
+ Поверни результат строго у форматі JSON із наступними ключами:
38
+ {
39
+ "category": "одна з трьох категорій: 'AI', 'Business' або 'Crypto'",
40
+ "bg_prompt": "англійською мовою детальний промпт для генерації фону (Flux.1). Premium dark aesthetics, volumetric lighting, glowing elements, background only, smooth clean negative space, absolutely no text",
41
+ "card_title": "короткий заголовок (до 4 слів, без води)",
42
+ "card_bullets": [
43
+ "теза 1 (чіткий факт до 8 слів)",
44
+ "теза 2 (висновок або інсайт до 8 слів)"
45
+ ],
46
+ "description": "Супровідний текст посту (до 400 символів). Жива мова з власною позицією, без шаблонів. Жодних посилань/URL у цьому тексті! Строго 1-2 вузьких тега в самому кінці.",
47
+ "source_url": "коротке посилання на першоджерело новини (обов'язково)"
48
+ }
49
+
50
+ Повертай ТІЛЬКИ чистий JSON-об'єкт.""",
51
+
52
+ "PROMPT_CAROUSEL": """Використовуючи наступний новинний контекст:
53
+ \"\"\"{news_context}\"\"\"
54
+ {exclude_str}
55
+
56
+ Створи глибокий розбір однієї з новин у вигляді каруселі з {slide_count} слайдів для Threads українською мовою з фокусом на теми: безпека/етика ІІ, DeFi/криптовалюти, або стартапи/тех-сфера.
57
+
58
+ ⚡ ТВОЇ СУПЕРСИЛИ (SKILLS) & КРИТИЧНІ ПРАВИЛА:
59
+
60
+ 1. РОЛЬ: SENIOR SOCIAL MEDIA COPYWRITER
61
+ - Сюжет: Будуй карусель як історію (Curiosity Gap). Кожен слайд змушує свайпнути далі. Тон: {tone}.
62
+ - Текст посту: Напиши коментар автора до каруселі. Він має бути повністю самодостатнім, нести самостійну цінність та містити твою чітку експертну думку/позицію перед питанням до аудиторії.
63
+ - Заборонені шаблони: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати шаблони на кшталт "[Факт]. [Риторичне питання]. Деталі в каруселі. Джерело". Натомість використовуй заголовок-крючок (hook).
64
+ - Заборонені фрази: Уникай типових ШІ-фразочок ("безумовно", "у сучасному світі", "у світі технологій"). Пиши як реальний живий експерт.
65
+ - Посилання: КАТЕГОРИЧНО ЗАБОРОНЕНО вставляти будь-які URL або посилання в полі description. Посилання на джерело має повертатися виключно окремо в ключі source_url.
66
+ - Хештеги: КАТЕГОРИЧНО ЗАБОРОНЕНО використовувати масові теги (#AI, #Crypto, #ШІ, #Технології). Використовуй лише 1-2 вузьких тега максимум. Розташовуй теги строго в самому кінці тексту.
67
+
68
+ 2. РОЛЬ: ART DIRECTOR & UI/UX DESIGNER
69
+ - Фонова динаміка: Ти пишеш промпти для фонів так, щоб вони еволюціонували від слайду до слайду, зберігаючи загальну кінематографічную стилістику (premium dark aesthetics, neon, volumetric lighting).
70
+ - Читабельність: Завжди гарантуй вільний простір для тексту ('smooth clean negative space, absolutely no text').
71
+
72
+ Поверни результат строго у форматі JSON із наступними ключами:
73
+ {
74
+ "category": "одна з трьох категорій: 'AI', 'Business' або 'Crypto'",
75
+ "description": "Супровідний текст посту (до 350 символів). Жива людська мова, що виражає твою позицію та інтригує переглянути карусель. Жодних посилань/URL у цьому полі! Строго 1-2 вузьких тега в самому кінці.",
76
+ "source_url": "коротке посилання на першоджерело новини (обов'язково)",
77
+ "slides": [ масив з {slide_count} об'єктів слайдів: { "title": "...", "bullets": ["...", "..."], "bg_prompt": "..." } ]
78
+ }
79
+
80
+ Повертай ТІЛЬКИ чистий JSON-об'єкт.""",
81
+
82
+ "PROMPT_PREDICTOR": """Ты — AI-редактор и эксперт по виральному контенту в соцсетях.
83
+ Твоя задача — оценить черновик поста для Threads перед его публикацией.
84
+ {context}
85
+ Черновик поста:
86
+ --- {post_text} ---
87
+
88
+ Оцени потенциал этого поста по 10-балльной шкале (где 10 — абсолютный хит, а 1 — скучная новостная сводка, которую никто не лайкнет).
89
+ Пост должен иметь мощный 'крючок' (hook) в первом предложении, вызывать эмоции (шок, интерес, несогласие) и стимулировать дискуссию в комментариях.
90
+
91
+ Верни СТРОГО JSON:
92
+ {
93
+ "score": 8,
94
+ "criticism": "оставь пустым, если балл >= 7, иначе напиши 1-2 предложения, что именно изменить"
95
+ }
96
+ Возвращай только чистый JSON без markdown."""
97
+ }
98
+
99
+ def load_settings():
100
+ if not os.path.exists(SETTINGS_PATH):
101
+ return DEFAULT_SETTINGS.copy()
102
+ try:
103
+ with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
104
+ data = json.load(f)
105
+ # Merge with defaults to ensure all keys exist
106
+ merged = DEFAULT_SETTINGS.copy()
107
+ merged.update(data)
108
+ return merged
109
+ except Exception as e:
110
+ print(f"[SettingsManager] Ошибка чтения settings.json: {e}")
111
+ return DEFAULT_SETTINGS.copy()
112
+
113
+ def save_settings(new_settings):
114
+ try:
115
+ current = load_settings()
116
+ current.update(new_settings)
117
+ with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
118
+ json.dump(current, f, ensure_ascii=False, indent=2)
119
+ return True
120
+ except Exception as e:
121
+ print(f"[SettingsManager] Ошибка записи settings.json: {e}")
122
+ return False
update_config.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import HfApi
2
+ api = HfApi()
3
+ new_space_id = "FincH12/Automation_Threads_EU"
4
+
5
+ print("Uploading safe config...")
6
+ api.upload_file(
7
+ path_or_fileobj="config.py",
8
+ path_in_repo="config.py",
9
+ repo_id=new_space_id,
10
+ repo_type="space",
11
+ commit_message="Remove hardcoded API keys for security"
12
+ )
13
+ print("Config uploaded. Making Space public...")
14
+ api.update_repo_settings(repo_id=new_space_id, repo_type="space", private=False)
15
+ print("Space is now PUBLIC again.")