Spaces:
Runtime error
Runtime error
Файл interface
Browse files- app.py +151 -557
- interface.py +217 -0
app.py
CHANGED
@@ -1,14 +1,8 @@
|
|
1 |
import os
|
2 |
-
import
|
3 |
-
from datetime import datetime
|
4 |
-
import pandas as pd
|
5 |
-
import numpy as np
|
6 |
-
import matplotlib.pyplot as plt
|
7 |
-
import seaborn as sns
|
8 |
from pathlib import Path
|
9 |
-
import
|
10 |
import traceback
|
11 |
-
import logging
|
12 |
|
13 |
# Налаштування логування
|
14 |
logging.basicConfig(
|
@@ -25,571 +19,171 @@ logger = logging.getLogger("jira_assistant")
|
|
25 |
for directory in ["data", "reports", "temp", "logs"]:
|
26 |
Path(directory).mkdir(exist_ok=True, parents=True)
|
27 |
|
28 |
-
#
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
def __init__(self):
|
31 |
-
self.
|
32 |
-
self.
|
33 |
-
self.
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
try:
|
38 |
-
logger.info(f"
|
39 |
-
|
40 |
-
# Спробуємо різні кодування
|
41 |
-
try:
|
42 |
-
self.df = pd.read_csv(file_path, encoding='utf-8')
|
43 |
-
except UnicodeDecodeError:
|
44 |
-
logger.warning("Помилка декодування UTF-8, спроба з latin1")
|
45 |
-
self.df = pd.read_csv(file_path, encoding='latin1')
|
46 |
-
|
47 |
-
logger.info(f"Успішно завантажено {len(self.df)} записів")
|
48 |
-
return self.df
|
49 |
-
except Exception as e:
|
50 |
-
logger.error(f"Помилка при завантаженні CSV-файлу: {e}")
|
51 |
-
raise
|
52 |
-
|
53 |
-
def process_dates(self):
|
54 |
-
"""Обробка дат у DataFrame"""
|
55 |
-
date_columns = ['Created', 'Updated', 'Resolved', 'Due Date']
|
56 |
-
|
57 |
-
for col in date_columns:
|
58 |
-
if col in self.df.columns:
|
59 |
-
try:
|
60 |
-
self.df[col] = pd.to_datetime(self.df[col], errors='coerce')
|
61 |
-
except Exception as e:
|
62 |
-
logger.warning(f"Не вдалося конвертувати колонку {col} до datetime: {e}")
|
63 |
-
|
64 |
-
def prepare_data(self):
|
65 |
-
"""Підготовка даних для аналізу"""
|
66 |
-
# Обробка дат
|
67 |
-
self.process_dates()
|
68 |
-
|
69 |
-
# Додаткова обробка
|
70 |
-
if 'Status' in self.df.columns:
|
71 |
-
self.df['Status'] = self.df['Status'].fillna('Unknown')
|
72 |
-
|
73 |
-
if 'Priority' in self.df.columns:
|
74 |
-
self.df['Priority'] = self.df['Priority'].fillna('Not set')
|
75 |
-
|
76 |
-
# Створення додаткових колонок
|
77 |
-
if 'Created' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Created']):
|
78 |
-
self.df['Created_Date'] = self.df['Created'].dt.date
|
79 |
-
self.df['Created_Month'] = self.df['Created'].dt.to_period('M')
|
80 |
-
|
81 |
-
if 'Updated' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Updated']):
|
82 |
-
self.df['Updated_Date'] = self.df['Updated'].dt.date
|
83 |
-
self.df['Days_Since_Update'] = (datetime.now() - self.df['Updated']).dt.days
|
84 |
-
|
85 |
-
def generate_stats(self):
|
86 |
-
"""Генерація базової статистики"""
|
87 |
-
self.stats = {
|
88 |
-
'total_tickets': len(self.df),
|
89 |
-
'status_counts': {},
|
90 |
-
'type_counts': {},
|
91 |
-
'priority_counts': {}
|
92 |
-
}
|
93 |
-
|
94 |
-
# Статистика за статусами
|
95 |
-
if 'Status' in self.df.columns:
|
96 |
-
self.stats['status_counts'] = self.df['Status'].value_counts().to_dict()
|
97 |
-
|
98 |
-
# Статистика за типами
|
99 |
-
if 'Issue Type' in self.df.columns:
|
100 |
-
self.stats['type_counts'] = self.df['Issue Type'].value_counts().to_dict()
|
101 |
-
|
102 |
-
# Статистика за пріоритетами
|
103 |
-
if 'Priority' in self.df.columns:
|
104 |
-
self.stats['priority_counts'] = self.df['Priority'].value_counts().to_dict()
|
105 |
-
|
106 |
-
return self.stats
|
107 |
-
|
108 |
-
def find_inactive_issues(self, days=14):
|
109 |
-
"""Аналіз неактивних тікетів"""
|
110 |
-
self.inactive_issues = {
|
111 |
-
'total_count': 0,
|
112 |
-
'percentage': 0,
|
113 |
-
'by_status': {},
|
114 |
-
'top_inactive': []
|
115 |
-
}
|
116 |
-
|
117 |
-
if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']):
|
118 |
-
return self.inactive_issues
|
119 |
-
|
120 |
-
# Визначення неактивних тікетів
|
121 |
-
cutoff_date = datetime.now() - pd.Timedelta(days=days)
|
122 |
-
inactive = self.df[self.df['Updated'] < cutoff_date]
|
123 |
-
|
124 |
-
if len(inactive) == 0:
|
125 |
-
return self.inactive_issues
|
126 |
-
|
127 |
-
self.inactive_issues['total_count'] = len(inactive)
|
128 |
-
self.inactive_issues['percentage'] = round(len(inactive) / len(self.df) * 100, 2) if len(self.df) > 0 else 0
|
129 |
-
|
130 |
-
# Розподіл за статусами
|
131 |
-
if 'Status' in inactive.columns:
|
132 |
-
self.inactive_issues['by_status'] = inactive['Status'].value_counts().to_dict()
|
133 |
-
|
134 |
-
# Топ 5 неактивних тікетів
|
135 |
-
top_inactive = []
|
136 |
-
for _, row in inactive.sort_values('Updated', ascending=True).head(5).iterrows():
|
137 |
-
issue_data = {
|
138 |
-
'key': row.get('Issue key', 'Невідомо'),
|
139 |
-
'summary': row.get('Summary', 'Невідомо'),
|
140 |
-
'status': row.get('Status', 'Невідомо'),
|
141 |
-
'days_inactive': (datetime.now() - row['Updated']).days if pd.notna(row['Updated']) else 'Невідомо'
|
142 |
-
}
|
143 |
-
top_inactive.append(issue_data)
|
144 |
-
|
145 |
-
self.inactive_issues['top_inactive'] = top_inactive
|
146 |
-
return self.inactive_issues
|
147 |
-
|
148 |
-
def plot_status_counts(self):
|
149 |
-
"""Діаграма розподілу за статусами"""
|
150 |
-
if 'Status' not in self.df.columns:
|
151 |
-
return None
|
152 |
-
|
153 |
-
status_counts = self.df['Status'].value_counts()
|
154 |
-
|
155 |
-
fig, ax = plt.subplots(figsize=(10, 6))
|
156 |
-
bars = sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax)
|
157 |
-
|
158 |
-
# Додаємо підписи значень над стовпцями
|
159 |
-
for i, v in enumerate(status_counts.values):
|
160 |
-
ax.text(i, v + 0.5, str(v), ha='center')
|
161 |
-
|
162 |
-
ax.set_title('Розподіл тікетів за статусами')
|
163 |
-
ax.set_xlabel('Статус')
|
164 |
-
ax.set_ylabel('Кількість')
|
165 |
-
plt.xticks(rotation=45)
|
166 |
-
plt.tight_layout()
|
167 |
-
|
168 |
-
return fig
|
169 |
-
|
170 |
-
def plot_priority_counts(self):
|
171 |
-
"""Діаграма розподілу за пріоритетами"""
|
172 |
-
if 'Priority' not in self.df.columns:
|
173 |
-
return None
|
174 |
-
|
175 |
-
priority_counts = self.df['Priority'].value_counts()
|
176 |
-
|
177 |
-
fig, ax = plt.subplots(figsize=(10, 6))
|
178 |
-
bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax)
|
179 |
-
|
180 |
-
# Додаємо підписи значень над стовпцями
|
181 |
-
for i, v in enumerate(priority_counts.values):
|
182 |
-
ax.text(i, v + 0.5, str(v), ha='center')
|
183 |
-
|
184 |
-
ax.set_title('Розподіл тікетів за пріоритетами')
|
185 |
-
ax.set_xlabel('Пріоритет')
|
186 |
-
ax.set_ylabel('Кількість')
|
187 |
-
plt.xticks(rotation=45)
|
188 |
-
plt.tight_layout()
|
189 |
-
|
190 |
-
return fig
|
191 |
-
|
192 |
-
def plot_created_timeline(self):
|
193 |
-
"""Часова діаграма створення тікетів"""
|
194 |
-
if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']):
|
195 |
-
return None
|
196 |
-
|
197 |
-
# Додаємо колонку з датою створення (без часу)
|
198 |
-
if 'Created_Date' not in self.df.columns:
|
199 |
-
self.df['Created_Date'] = self.df['Created'].dt.date
|
200 |
-
|
201 |
-
# Кількість створених тікетів за датами
|
202 |
-
created_by_date = self.df['Created_Date'].value_counts().sort_index()
|
203 |
-
|
204 |
-
# Створення діаграми
|
205 |
-
fig, ax = plt.subplots(figsize=(12, 6))
|
206 |
-
created_by_date.plot(kind='line', marker='o', ax=ax)
|
207 |
-
|
208 |
-
ax.set_title('Кількість створених тікетів за датами')
|
209 |
-
ax.set_xlabel('Дата')
|
210 |
-
ax.set_ylabel('Кількість')
|
211 |
-
ax.grid(True)
|
212 |
-
plt.tight_layout()
|
213 |
-
|
214 |
-
return fig
|
215 |
-
|
216 |
-
def generate_report(self, inactive_days=14):
|
217 |
-
"""Генерація звіту"""
|
218 |
-
report = []
|
219 |
-
|
220 |
-
# Заголовок звіту
|
221 |
-
report.append("# Звіт аналізу Jira")
|
222 |
-
report.append(f"*Створено: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
223 |
-
|
224 |
-
# Загальна статистика
|
225 |
-
report.append("\n## Загальна статистика")
|
226 |
-
report.append(f"**Загальна кількість тікетів:** {len(self.df)}")
|
227 |
-
|
228 |
-
# Статистика за статусами
|
229 |
-
if 'Status' in self.df.columns:
|
230 |
-
status_counts = self.df['Status'].value_counts()
|
231 |
-
report.append("\n### Статуси тікетів")
|
232 |
-
|
233 |
-
for status, count in status_counts.items():
|
234 |
-
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0
|
235 |
-
report.append(f"- **{status}:** {count} ({percentage:.1f}%)")
|
236 |
-
|
237 |
-
# Статистика за типами
|
238 |
-
if 'Issue Type' in self.df.columns:
|
239 |
-
type_counts = self.df['Issue Type'].value_counts()
|
240 |
-
report.append("\n### Типи тікетів")
|
241 |
-
|
242 |
-
for type_name, count in type_counts.items():
|
243 |
-
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0
|
244 |
-
report.append(f"- **{type_name}:** {count} ({percentage:.1f}%)")
|
245 |
-
|
246 |
-
# Статистика за пріоритетами
|
247 |
-
if 'Priority' in self.df.columns:
|
248 |
-
priority_counts = self.df['Priority'].value_counts()
|
249 |
-
report.append("\n### Пріоритети тікетів")
|
250 |
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
# Неактивні тікети
|
256 |
-
self.find_inactive_issues(inactive_days)
|
257 |
-
|
258 |
-
if self.inactive_issues['total_count'] > 0:
|
259 |
-
report.append(f"\n## Неактивні тікети (>{inactive_days} днів)")
|
260 |
-
report.append(f"**Загальна кількість неактивних тікетів:** {self.inactive_issues['total_count']} ({self.inactive_issues['percentage']}%)")
|
261 |
|
262 |
-
if self.
|
263 |
-
|
264 |
-
for status, count in self.inactive_issues['by_status'].items():
|
265 |
-
report.append(f"- **{status}:** {count}")
|
266 |
|
267 |
-
|
268 |
-
|
269 |
-
for i, ticket in enumerate(self.inactive_issues['top_inactive']):
|
270 |
-
report.append(f"{i+1}. **{ticket['key']}:** {ticket['summary']}")
|
271 |
-
report.append(f" - Статус: {ticket['status']}")
|
272 |
-
report.append(f" - Днів неактивності: {ticket['days_inactive']}")
|
273 |
-
|
274 |
-
return "\n".join(report)
|
275 |
-
|
276 |
-
# Виправлена функція analyze_csv
|
277 |
-
def analyze_csv(file_obj, inactive_days, include_ai):
|
278 |
-
if file_obj is None:
|
279 |
-
return "Помилка: файл не вибрано", None, None, None, None
|
280 |
-
|
281 |
-
try:
|
282 |
-
logger.info(f"Отримано файл: {file_obj.name}, тип: {type(file_obj)}")
|
283 |
-
|
284 |
-
# Створення тимчасового файлу
|
285 |
-
temp_file_path = os.path.join("temp", f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
|
286 |
-
|
287 |
-
# У Gradio 5.19.0 об'єкт файлу має різну структуру
|
288 |
-
# file_obj може бути шляхом до файлу або містити атрибут 'name'
|
289 |
-
if hasattr(file_obj, 'name'):
|
290 |
-
source_path = file_obj.name
|
291 |
|
292 |
-
#
|
293 |
-
|
294 |
-
shutil.copy2(source_path, temp_file_path)
|
295 |
-
else:
|
296 |
-
# Якщо це не шлях до файлу, ймовірно це вже самі дані
|
297 |
-
with open(temp_file_path, "w", encoding="utf-8") as f:
|
298 |
-
f.write(str(file_obj))
|
299 |
-
|
300 |
-
# Аналіз даних
|
301 |
-
analyzer = JiraAnalyzer()
|
302 |
-
analyzer.load_csv(temp_file_path)
|
303 |
-
analyzer.prepare_data()
|
304 |
-
analyzer.generate_stats()
|
305 |
-
|
306 |
-
# Візуалізації
|
307 |
-
status_fig = analyzer.plot_status_counts()
|
308 |
-
priority_fig = analyzer.plot_priority_counts()
|
309 |
-
timeline_fig = analyzer.plot_created_timeline()
|
310 |
-
|
311 |
-
# Генерація звіту
|
312 |
-
report = analyzer.generate_report(inactive_days=inactive_days)
|
313 |
-
|
314 |
-
# AI аналіз (заглушка)
|
315 |
-
ai_analysis = None
|
316 |
-
if include_ai:
|
317 |
-
ai_analysis = "AI аналіз буде доступний у наступних версіях додатку."
|
318 |
-
|
319 |
-
# Видалення тимчасового файлу
|
320 |
-
try:
|
321 |
-
os.remove(temp_file_path)
|
322 |
-
except:
|
323 |
-
pass
|
324 |
-
|
325 |
-
return report, status_fig, priority_fig, timeline_fig, ai_analysis
|
326 |
-
|
327 |
-
except Exception as e:
|
328 |
-
error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}"
|
329 |
-
logger.error(error_msg)
|
330 |
-
return error_msg, None, None, None, None
|
331 |
-
|
332 |
-
# Функція для збереження звіту
|
333 |
-
def save_report(report_text, format_type, include_visualizations, status_fig, priority_fig, timeline_fig):
|
334 |
-
if not report_text:
|
335 |
-
return "Помилка: спочатку виконайте аналіз даних"
|
336 |
-
|
337 |
-
try:
|
338 |
-
# Створення імені файлу
|
339 |
-
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
340 |
-
report_filename = f"jira_report_{timestamp}"
|
341 |
-
reports_dir = Path("reports")
|
342 |
-
|
343 |
-
if format_type == "markdown":
|
344 |
-
filepath = reports_dir / f"{report_filename}.md"
|
345 |
-
with open(filepath, 'w', encoding='utf-8') as f:
|
346 |
-
f.write(report_text)
|
347 |
-
|
348 |
-
if include_visualizations:
|
349 |
-
# Збереження візуалізацій
|
350 |
-
charts_dir = reports_dir / f"{report_filename}_charts"
|
351 |
-
charts_dir.mkdir(exist_ok=True)
|
352 |
-
|
353 |
-
if status_fig:
|
354 |
-
status_fig.savefig(charts_dir / "status_counts.png")
|
355 |
-
if priority_fig:
|
356 |
-
priority_fig.savefig(charts_dir / "priority_counts.png")
|
357 |
-
if timeline_fig:
|
358 |
-
timeline_fig.savefig(charts_dir / "timeline.png")
|
359 |
-
|
360 |
-
elif format_type == "html":
|
361 |
-
from markdown import markdown
|
362 |
|
363 |
-
|
|
|
364 |
|
365 |
-
#
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }}
|
375 |
-
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
|
376 |
-
th {{ background-color: #0052CC; color: white; }}
|
377 |
-
img {{ max-width: 100%; margin: 20px 0; }}
|
378 |
-
</style>
|
379 |
-
</head>
|
380 |
-
<body>
|
381 |
-
{markdown(report_text)}
|
382 |
-
"""
|
383 |
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
timeline_path = charts_dir / "timeline.png"
|
403 |
-
timeline_fig.savefig(timeline_path)
|
404 |
-
html_content += f'<div><h3>Часова шкала</h3><img src="{timeline_path.relative_to(reports_dir)}" alt="Часова шкала"></div>'
|
405 |
|
406 |
-
|
|
|
|
|
|
|
|
|
|
|
407 |
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
try:
|
413 |
-
from weasyprint import HTML
|
414 |
-
|
415 |
-
filepath = reports_dir / f"{report_filename}.pdf"
|
416 |
-
|
417 |
-
# Створюємо тимчасовий HTML-файл
|
418 |
-
temp_html_path = reports_dir / f"{report_filename}_temp.html"
|
419 |
-
|
420 |
-
# Конвертація Markdown в HTML
|
421 |
-
from markdown import markdown
|
422 |
-
html_content = f"""<!DOCTYPE html>
|
423 |
-
<html>
|
424 |
-
<head>
|
425 |
-
<meta charset="utf-8">
|
426 |
-
<title>Звіт аналізу Jira</title>
|
427 |
-
<style>
|
428 |
-
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }}
|
429 |
-
h1, h2, h3 {{ color: #0052CC; }}
|
430 |
-
table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }}
|
431 |
-
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
|
432 |
-
th {{ background-color: #0052CC; color: white; }}
|
433 |
-
img {{ max-width: 100%; margin: 20px 0; }}
|
434 |
-
</style>
|
435 |
-
</head>
|
436 |
-
<body>
|
437 |
-
{markdown(report_text)}
|
438 |
-
"""
|
439 |
-
|
440 |
-
if include_visualizations and (status_fig or priority_fig or timeline_fig):
|
441 |
-
# Збереження візуалізацій
|
442 |
-
charts_dir = reports_dir / f"{report_filename}_charts"
|
443 |
-
charts_dir.mkdir(exist_ok=True)
|
444 |
-
|
445 |
-
html_content += "<h2>Візуалізації</h2>"
|
446 |
-
|
447 |
-
if status_fig:
|
448 |
-
status_path = charts_dir / "status_counts.png"
|
449 |
-
status_fig.savefig(status_path)
|
450 |
-
html_content += f'<div><h3>Статуси тікетів</h3><img src="{status_path}" alt="Статуси тікетів"></div>'
|
451 |
-
|
452 |
-
if priority_fig:
|
453 |
-
priority_path = charts_dir / "priority_counts.png"
|
454 |
-
priority_fig.savefig(priority_path)
|
455 |
-
html_content += f'<div><h3>Пріоритети тікетів</h3><img src="{priority_path}" alt="Пріоритети тікетів"></div>'
|
456 |
-
|
457 |
-
if timeline_fig:
|
458 |
-
timeline_path = charts_dir / "timeline.png"
|
459 |
-
timeline_fig.savefig(timeline_path)
|
460 |
-
html_content += f'<div><h3>Часова шкала</h3><img src="{timeline_path}" alt="Часова шкала"></div>'
|
461 |
-
|
462 |
-
html_content += "</body></html>"
|
463 |
-
|
464 |
-
with open(temp_html_path, 'w', encoding='utf-8') as f:
|
465 |
-
f.write(html_content)
|
466 |
-
|
467 |
-
# Конвертація HTML в PDF
|
468 |
-
HTML(filename=str(temp_html_path)).write_pdf(filepath)
|
469 |
-
|
470 |
-
# Видалення тимчасового HTML-файлу
|
471 |
-
try:
|
472 |
-
os.remove(temp_html_path)
|
473 |
-
except:
|
474 |
-
pass
|
475 |
-
|
476 |
-
except ImportError:
|
477 |
-
return "Поми��ка: для генерації PDF потрібна бібліотека weasyprint"
|
478 |
-
|
479 |
-
return f"Звіт успішно збережено: {filepath}"
|
480 |
-
|
481 |
-
except Exception as e:
|
482 |
-
error_msg = f"Помилка при збереженні звіту: {str(e)}\n\n{traceback.format_exc()}"
|
483 |
-
logger.error(error_msg)
|
484 |
-
return error_msg
|
485 |
-
|
486 |
-
# Інтерфейс Gradio
|
487 |
-
with gr.Blocks(title="Jira AI Assistant") as app:
|
488 |
-
gr.Markdown("# 🔍 Jira AI Assistant")
|
489 |
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
timeline_plot = gr.Plot(label="Часова шкала")
|
526 |
-
|
527 |
-
with gr.Tab("AI Аналіз"):
|
528 |
-
ai_output = gr.Markdown()
|
529 |
-
|
530 |
-
# Встановлюємо обробники подій
|
531 |
-
analyze_btn.click(
|
532 |
-
analyze_csv,
|
533 |
-
inputs=[file_input, inactive_days, include_ai],
|
534 |
-
outputs=[report_output, status_plot, priority_plot, timeline_plot, ai_output]
|
535 |
)
|
536 |
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
|
|
|
|
541 |
)
|
542 |
-
|
543 |
-
with gr.Tab("Jira API"):
|
544 |
-
gr.Markdown("## Підключення до Jira API")
|
545 |
-
|
546 |
-
with gr.Row():
|
547 |
-
jira_url = gr.Textbox(
|
548 |
-
label="Jira URL",
|
549 |
-
placeholder="https://your-company.atlassian.net"
|
550 |
-
)
|
551 |
-
jira_username = gr.Textbox(
|
552 |
-
label="Ім'я користувача Jira",
|
553 |
-
placeholder="email@example.com"
|
554 |
-
)
|
555 |
-
jira_api_token = gr.Textbox(
|
556 |
-
label="Jira API Token",
|
557 |
-
type="password"
|
558 |
-
)
|
559 |
|
560 |
-
|
561 |
-
|
|
|
|
|
562 |
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
)
|
586 |
-
slack_message = gr.Textbox(
|
587 |
-
label="Повідомлення",
|
588 |
-
placeholder="Тижневий звіт по проекту",
|
589 |
-
lines=3
|
590 |
-
)
|
591 |
-
slack_send_btn = gr.Button("Надіслати у Slack", interactive=False)
|
592 |
|
593 |
-
#
|
594 |
if __name__ == "__main__":
|
595 |
-
|
|
|
|
1 |
import os
|
2 |
+
import logging
|
|
|
|
|
|
|
|
|
|
|
3 |
from pathlib import Path
|
4 |
+
from datetime import datetime
|
5 |
import traceback
|
|
|
6 |
|
7 |
# Налаштування логування
|
8 |
logging.basicConfig(
|
|
|
19 |
for directory in ["data", "reports", "temp", "logs"]:
|
20 |
Path(directory).mkdir(exist_ok=True, parents=True)
|
21 |
|
22 |
+
# Імпорт необхідних модулів
|
23 |
+
from modules.data_import.csv_importer import JiraCsvImporter
|
24 |
+
from modules.data_analysis.statistics import JiraDataAnalyzer
|
25 |
+
from modules.data_analysis.visualizations import JiraVisualizer
|
26 |
+
from modules.reporting.report_generator import ReportGenerator
|
27 |
+
from modules.core.app_manager import AppManager
|
28 |
+
|
29 |
+
class JiraAssistantApp:
|
30 |
+
"""
|
31 |
+
Головний клас додатку, який координує роботу всіх компонентів
|
32 |
+
"""
|
33 |
def __init__(self):
|
34 |
+
self.app_manager = AppManager()
|
35 |
+
self.current_data = None
|
36 |
+
self.current_analysis = None
|
37 |
+
self.visualizations = None
|
38 |
+
|
39 |
+
def analyze_csv_file(self, file_path, inactive_days=14, include_ai=False, api_key=None, model_type="openai"):
|
40 |
+
"""
|
41 |
+
Аналіз CSV-файлу Jira
|
42 |
+
|
43 |
+
Args:
|
44 |
+
file_path (str): Шлях до CSV-файлу
|
45 |
+
inactive_days (int): Кількість днів для визначення неактивних тікетів
|
46 |
+
include_ai (bool): Чи використовувати AI-аналіз
|
47 |
+
api_key (str): API ключ для LLM (якщо include_ai=True)
|
48 |
+
model_type (str): Тип моделі LLM ("openai" або "gemini")
|
49 |
+
|
50 |
+
Returns:
|
51 |
+
dict: Результати аналізу
|
52 |
+
"""
|
53 |
try:
|
54 |
+
logger.info(f"Аналіз файлу: {file_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
+
# Завантаження даних
|
57 |
+
csv_importer = JiraCsvImporter(file_path)
|
58 |
+
self.current_data = csv_importer.load_data()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
|
60 |
+
if self.current_data is None:
|
61 |
+
return {"error": "Не вдалося завантажити дані з CSV-файлу"}
|
|
|
|
|
62 |
|
63 |
+
# Аналіз даних
|
64 |
+
analyzer = JiraDataAnalyzer(self.current_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
|
66 |
+
# Базова статистика
|
67 |
+
stats = analyzer.generate_basic_statistics()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
+
# Аналіз неактивних тікетів
|
70 |
+
inactive_issues = analyzer.analyze_inactive_issues(days=inactive_days)
|
71 |
|
72 |
+
# Створення візуалізацій
|
73 |
+
visualizer = JiraVisualizer(self.current_data)
|
74 |
+
self.visualizations = {
|
75 |
+
"status": visualizer.plot_status_counts(),
|
76 |
+
"priority": visualizer.plot_priority_counts(),
|
77 |
+
"type": visualizer.plot_type_counts(),
|
78 |
+
"created_timeline": visualizer.plot_created_timeline(),
|
79 |
+
"inactive": visualizer.plot_inactive_issues(days=inactive_days)
|
80 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
+
# AI аналіз, якщо потрібен
|
83 |
+
ai_analysis = None
|
84 |
+
if include_ai and api_key:
|
85 |
+
from modules.ai_analysis.llm_connector import LLMConnector
|
86 |
+
llm = LLMConnector(api_key=api_key, model_type=model_type)
|
87 |
+
ai_analysis = llm.analyze_jira_data(stats, inactive_issues)
|
88 |
+
|
89 |
+
# Генерація звіту
|
90 |
+
report_generator = ReportGenerator(self.current_data, stats, inactive_issues, ai_analysis)
|
91 |
+
report = report_generator.create_markdown_report(inactive_days=inactive_days)
|
92 |
+
|
93 |
+
# Зберігаємо поточний аналіз
|
94 |
+
self.current_analysis = {
|
95 |
+
"stats": stats,
|
96 |
+
"inactive_issues": inactive_issues,
|
97 |
+
"report": report,
|
98 |
+
"ai_analysis": ai_analysis
|
99 |
+
}
|
|
|
|
|
|
|
100 |
|
101 |
+
return {
|
102 |
+
"report": report,
|
103 |
+
"visualizations": self.visualizations,
|
104 |
+
"ai_analysis": ai_analysis,
|
105 |
+
"error": None
|
106 |
+
}
|
107 |
|
108 |
+
except Exception as e:
|
109 |
+
error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}"
|
110 |
+
logger.error(error_msg)
|
111 |
+
return {"error": error_msg}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
+
def save_report(self, format_type="markdown", include_visualizations=True, filepath=None):
|
114 |
+
"""
|
115 |
+
Збереження звіту у файл
|
116 |
+
|
117 |
+
Args:
|
118 |
+
format_type (str): Формат звіту ("markdown", "html", "pdf")
|
119 |
+
include_visualizations (bool): Чи включати візуалізації у звіт
|
120 |
+
filepath (str): Шлях для збереження файлу
|
121 |
+
|
122 |
+
Returns:
|
123 |
+
str: Шлях до збереженого файлу або повідомлення про помилку
|
124 |
+
"""
|
125 |
+
try:
|
126 |
+
if not self.current_analysis or "report" not in self.current_analysis:
|
127 |
+
return "Помилка: спочатку виконайте аналіз даних"
|
128 |
+
|
129 |
+
# Створення імені файлу, якщо не вказано
|
130 |
+
if not filepath:
|
131 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
132 |
+
report_filename = f"jira_report_{timestamp}"
|
133 |
+
reports_dir = Path("reports")
|
134 |
+
|
135 |
+
if format_type == "markdown":
|
136 |
+
filepath = reports_dir / f"{report_filename}.md"
|
137 |
+
elif format_type == "html":
|
138 |
+
filepath = reports_dir / f"{report_filename}.html"
|
139 |
+
elif format_type == "pdf":
|
140 |
+
filepath = reports_dir / f"{report_filename}.pdf"
|
141 |
+
|
142 |
+
# Створення генератора звітів
|
143 |
+
report_generator = ReportGenerator(
|
144 |
+
self.current_data,
|
145 |
+
self.current_analysis.get("stats"),
|
146 |
+
self.current_analysis.get("inactive_issues"),
|
147 |
+
self.current_analysis.get("ai_analysis")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
)
|
149 |
|
150 |
+
# Збереження звіту
|
151 |
+
saved_path = report_generator.save_report(
|
152 |
+
filepath=filepath,
|
153 |
+
format=format_type,
|
154 |
+
include_visualizations=include_visualizations,
|
155 |
+
visualization_data=self.visualizations if include_visualizations else None
|
156 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
|
158 |
+
if saved_path:
|
159 |
+
return f"Звіт успішно збережено: {saved_path}"
|
160 |
+
else:
|
161 |
+
return "Не вдалося зберегти звіт"
|
162 |
|
163 |
+
except Exception as e:
|
164 |
+
error_msg = f"Помилка при збереженні звіту: {str(e)}\n\n{traceback.format_exc()}"
|
165 |
+
logger.error(error_msg)
|
166 |
+
return error_msg
|
167 |
+
|
168 |
+
def test_jira_connection(self, jira_url, username, api_token):
|
169 |
+
"""
|
170 |
+
Тестування підключення до Jira
|
171 |
+
|
172 |
+
Args:
|
173 |
+
jira_url (str): URL сервера Jira
|
174 |
+
username (str): Ім'я користувача
|
175 |
+
api_token (str): API токен
|
176 |
+
|
177 |
+
Returns:
|
178 |
+
bool: True якщо підключення успішне, False інакше
|
179 |
+
"""
|
180 |
+
from modules.data_import.jira_api import JiraConnector
|
181 |
+
return JiraConnector.test_connection(jira_url, username, api_token)
|
182 |
+
|
183 |
+
# Створення екземпляру додатку
|
184 |
+
app = JiraAssistantApp()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
|
186 |
+
# Точка входу для запуску з командного рядка
|
187 |
if __name__ == "__main__":
|
188 |
+
from interface import launch_interface
|
189 |
+
launch_interface(app)
|
interface.py
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
from pathlib import Path
|
4 |
+
from datetime import datetime
|
5 |
+
import matplotlib.pyplot as plt
|
6 |
+
import logging
|
7 |
+
|
8 |
+
logger = logging.getLogger("jira_assistant_interface")
|
9 |
+
|
10 |
+
def launch_interface(app):
|
11 |
+
"""
|
12 |
+
Запуск інтерфейсу користувача Gradio
|
13 |
+
|
14 |
+
Args:
|
15 |
+
app: Екземпляр JiraAssistantApp
|
16 |
+
"""
|
17 |
+
# Функція для обробки завантаження та аналізу CSV
|
18 |
+
def analyze_csv(file_obj, inactive_days, include_ai):
|
19 |
+
if file_obj is None:
|
20 |
+
return "Помилка: файл не вибрано", None, None, None, None
|
21 |
+
|
22 |
+
try:
|
23 |
+
logger.info(f"Отримано файл: {file_obj.name}, тип: {type(file_obj)}")
|
24 |
+
|
25 |
+
# Створення тимчасового файлу
|
26 |
+
temp_file_path = os.path.join("temp", f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
|
27 |
+
|
28 |
+
# У Gradio 5.19.0 об'єкт файлу має різну структуру
|
29 |
+
# file_obj може бути шляхом до файлу або містити атрибут 'name'
|
30 |
+
if hasattr(file_obj, 'name'):
|
31 |
+
source_path = file_obj.name
|
32 |
+
|
33 |
+
# Копіювання файлу
|
34 |
+
import shutil
|
35 |
+
shutil.copy2(source_path, temp_file_path)
|
36 |
+
else:
|
37 |
+
# Якщо це не шлях до файлу, ймовірно це вже самі дані
|
38 |
+
with open(temp_file_path, "w", encoding="utf-8") as f:
|
39 |
+
f.write(str(file_obj))
|
40 |
+
|
41 |
+
# Аналіз даних
|
42 |
+
api_key = os.getenv("OPENAI_API_KEY") if include_ai else None
|
43 |
+
result = app.analyze_csv_file(
|
44 |
+
temp_file_path,
|
45 |
+
inactive_days=inactive_days,
|
46 |
+
include_ai=include_ai,
|
47 |
+
api_key=api_key
|
48 |
+
)
|
49 |
+
|
50 |
+
# Видалення тимчасового файлу
|
51 |
+
try:
|
52 |
+
os.remove(temp_file_path)
|
53 |
+
except:
|
54 |
+
pass
|
55 |
+
|
56 |
+
if result.get("error"):
|
57 |
+
return result.get("error"), None, None, None, None
|
58 |
+
|
59 |
+
return (
|
60 |
+
result.get("report", ""),
|
61 |
+
result.get("visualizations", {}).get("status"),
|
62 |
+
result.get("visualizations", {}).get("priority"),
|
63 |
+
result.get("visualizations", {}).get("created_timeline"),
|
64 |
+
result.get("ai_analysis", "AI аналіз буде доступний у наступних версіях додатку.")
|
65 |
+
)
|
66 |
+
|
67 |
+
except Exception as e:
|
68 |
+
import traceback
|
69 |
+
error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}"
|
70 |
+
logger.error(error_msg)
|
71 |
+
return error_msg, None, None, None, None
|
72 |
+
|
73 |
+
# Функція для збереження звіту
|
74 |
+
def save_report_handler(report_text, format_type, include_visualizations):
|
75 |
+
if not report_text:
|
76 |
+
return "Помилка: спочатку виконайте аналіз даних"
|
77 |
+
|
78 |
+
return app.save_report(
|
79 |
+
format_type=format_type,
|
80 |
+
include_visualizations=include_visualizations
|
81 |
+
)
|
82 |
+
|
83 |
+
# Функція для тестування підключення до Jira
|
84 |
+
def test_jira_connection_handler(url, username, api_token):
|
85 |
+
if not url or not username or not api_token:
|
86 |
+
return "Помилка: необхідно заповнити всі поля (URL, користувач, API токен)"
|
87 |
+
|
88 |
+
success = app.test_jira_connection(url, username, api_token)
|
89 |
+
if success:
|
90 |
+
return "✅ Успішне підключення до Jira API"
|
91 |
+
else:
|
92 |
+
return "❌ Помилка підключення до Jira. Перевірте введені дані."
|
93 |
+
|
94 |
+
# Створення інтерфейсу Gradio
|
95 |
+
with gr.Blocks(title="Jira AI Assistant") as interface:
|
96 |
+
gr.Markdown("# 🔍 Jira AI Assistant")
|
97 |
+
|
98 |
+
with gr.Tabs():
|
99 |
+
with gr.Tab("CSV Аналіз"):
|
100 |
+
with gr.Row():
|
101 |
+
with gr.Column(scale=1):
|
102 |
+
file_input = gr.File(label="Завантажити CSV файл Jira")
|
103 |
+
inactive_days = gr.Slider(minimum=1, maximum=90, value=14, step=1,
|
104 |
+
label="Кількість днів для визначення неактивних тікетів")
|
105 |
+
|
106 |
+
include_ai = gr.Checkbox(label="Включити AI аналіз", value=False)
|
107 |
+
|
108 |
+
analyze_btn = gr.Button("Аналізувати", variant="primary")
|
109 |
+
|
110 |
+
with gr.Accordion("Збереження звіту", open=False):
|
111 |
+
format_type = gr.Dropdown(
|
112 |
+
choices=["markdown", "html", "pdf"],
|
113 |
+
value="markdown",
|
114 |
+
label="Формат звіту"
|
115 |
+
)
|
116 |
+
include_visualizations = gr.Checkbox(
|
117 |
+
label="Включити візуалізації",
|
118 |
+
value=True
|
119 |
+
)
|
120 |
+
save_btn = gr.Button("Зберегти звіт")
|
121 |
+
save_output = gr.Textbox(label="Статус збереження")
|
122 |
+
|
123 |
+
with gr.Column(scale=2):
|
124 |
+
with gr.Tabs():
|
125 |
+
with gr.Tab("Звіт"):
|
126 |
+
report_output = gr.Markdown()
|
127 |
+
|
128 |
+
with gr.Tab("Візуалізації"):
|
129 |
+
with gr.Row():
|
130 |
+
status_plot = gr.Plot(label="Статуси тікетів")
|
131 |
+
priority_plot = gr.Plot(label="Пріоритети тікетів")
|
132 |
+
|
133 |
+
timeline_plot = gr.Plot(label="Часова шкала")
|
134 |
+
|
135 |
+
with gr.Tab("AI Аналіз"):
|
136 |
+
ai_output = gr.Markdown()
|
137 |
+
|
138 |
+
# Встановлюємо обробники подій
|
139 |
+
analyze_btn.click(
|
140 |
+
analyze_csv,
|
141 |
+
inputs=[file_input, inactive_days, include_ai],
|
142 |
+
outputs=[report_output, status_plot, priority_plot, timeline_plot, ai_output]
|
143 |
+
)
|
144 |
+
|
145 |
+
save_btn.click(
|
146 |
+
save_report_handler,
|
147 |
+
inputs=[report_output, format_type, include_visualizations],
|
148 |
+
outputs=[save_output]
|
149 |
+
)
|
150 |
+
|
151 |
+
with gr.Tab("Jira API"):
|
152 |
+
gr.Markdown("## Підключення до Jira API")
|
153 |
+
|
154 |
+
with gr.Row():
|
155 |
+
jira_url = gr.Textbox(
|
156 |
+
label="Jira URL",
|
157 |
+
placeholder="https://your-company.atlassian.net"
|
158 |
+
)
|
159 |
+
jira_username = gr.Textbox(
|
160 |
+
label="Ім'я користувача Jira",
|
161 |
+
placeholder="email@example.com"
|
162 |
+
)
|
163 |
+
jira_api_token = gr.Textbox(
|
164 |
+
label="Jira API Token",
|
165 |
+
type="password"
|
166 |
+
)
|
167 |
+
|
168 |
+
test_connection_btn = gr.Button("Тестувати з'єднання")
|
169 |
+
connection_status = gr.Textbox(label="Статус підключення")
|
170 |
+
|
171 |
+
test_connection_btn.click(
|
172 |
+
test_jira_connection_handler,
|
173 |
+
inputs=[jira_url, jira_username, jira_api_token],
|
174 |
+
outputs=[connection_status]
|
175 |
+
)
|
176 |
+
|
177 |
+
gr.Markdown("## ⚠️ Ця функція буде доступна у наступних версіях")
|
178 |
+
|
179 |
+
with gr.Tab("AI Асистенти"):
|
180 |
+
gr.Markdown("## AI Асистенти для Jira")
|
181 |
+
gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях")
|
182 |
+
|
183 |
+
with gr.Accordion("Зразок інтерфейсу"):
|
184 |
+
question = gr.Textbox(
|
185 |
+
label="Запитання",
|
186 |
+
placeholder="Наприклад: Які тікети мають найвищий пріоритет?",
|
187 |
+
lines=2
|
188 |
+
)
|
189 |
+
answer = gr.Markdown(label="Відповідь")
|
190 |
+
|
191 |
+
with gr.Tab("Інтеграції"):
|
192 |
+
gr.Markdown("## Інтеграції з зовнішніми системами")
|
193 |
+
gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях")
|
194 |
+
|
195 |
+
with gr.Accordion("Slack інтеграція"):
|
196 |
+
slack_channel = gr.Textbox(
|
197 |
+
label="Slack канал",
|
198 |
+
placeholder="#project-updates"
|
199 |
+
)
|
200 |
+
slack_message = gr.Textbox(
|
201 |
+
label="Повідомлення",
|
202 |
+
placeholder="Тижневий звіт по проекту",
|
203 |
+
lines=3
|
204 |
+
)
|
205 |
+
slack_send_btn = gr.Button("Надіслати у Slack", interactive=False)
|
206 |
+
|
207 |
+
# Запуск інтерфейсу
|
208 |
+
interface.launch()
|
209 |
+
|
210 |
+
# Можливість запустити інтерфейс самостійно (для тестування)
|
211 |
+
if __name__ == "__main__":
|
212 |
+
import sys
|
213 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
214 |
+
|
215 |
+
from app import JiraAssistantApp
|
216 |
+
app_instance = JiraAssistantApp()
|
217 |
+
launch_interface(app_instance)
|