diff --git a/backend/auth.py b/backend/auth.py
index e57c1da..8699322 100644
--- a/backend/auth.py
+++ b/backend/auth.py
@@ -12,10 +12,10 @@ from sqlalchemy.orm import Session
from backend.db import get_db
from backend.models import User
-pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
+pwd_context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto")
bearer = HTTPBearer(auto_error=False)
-AUTH_VERSION = "argon2-2026-04-07"
+AUTH_VERSION = "argon2-bcrypt-compat-2026-04-07"
def _env(name: str, default: str) -> str:
@@ -48,6 +48,10 @@ def verify_password(password: str, password_hash: str) -> bool:
) from e
+def password_needs_update(password_hash: str) -> bool:
+ return pwd_context.needs_update(password_hash)
+
+
def create_access_token(*, user_id: str) -> str:
now = datetime.now(timezone.utc)
payload = {
diff --git a/backend/main.py b/backend/main.py
index 50e4291..3a69fc6 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -27,6 +27,7 @@ from backend.auth import ( # noqa: E402
create_access_token,
get_current_user,
hash_password,
+ password_needs_update,
verify_password,
)
from backend.db import engine, get_db # noqa: E402
@@ -130,6 +131,11 @@ def login(req: LoginRequest, db: Session = Depends(get_db)):
if not user.is_active:
raise HTTPException(status_code=403, detail="User is inactive")
+ if password_needs_update(user.password_hash):
+ user.password_hash = hash_password(req.password)
+ db.add(user)
+ db.commit()
+
token = create_access_token(user_id=str(user.id))
return {"access_token": token, "token_type": "bearer"}
except HTTPException:
diff --git a/backend/requirements-backend.txt b/backend/requirements-backend.txt
index dec289f..9015974 100644
--- a/backend/requirements-backend.txt
+++ b/backend/requirements-backend.txt
@@ -6,5 +6,7 @@ sqlalchemy
psycopg[binary]
passlib[argon2]
argon2-cffi
+passlib[bcrypt]
+bcrypt
python-jose[cryptography]
pydantic[email]
diff --git a/frontend/ui.py b/frontend/ui.py
index de10ec9..d172d59 100644
--- a/frontend/ui.py
+++ b/frontend/ui.py
@@ -4,10 +4,21 @@ from datetime import datetime, timezone
import requests
import streamlit as st
-st.set_page_config(page_title="DevOps AI Assistant", page_icon="π")
+# Π Π΄ΡΡ
Π΅ https://github.com/streamlit/demo-ai-assistant β ΠΊΡΡΠΏΠ½ΡΠΉ Π³Π΅ΡΠΎΠΉ, Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π² ΡΡΠ΄ Ρ Π΄Π΅ΠΉΡΡΠ²ΠΈΠ΅ΠΌ
+st.set_page_config(
+ page_title="DevOps AI Assistant",
+ page_icon="β¨",
+ layout="wide",
+ initial_sidebar_state="expanded",
+)
-st.title("DevOps AI Assistant")
-st.markdown("ΠΠΎΡΡΠ°Π» Ρ Π΄Π°Π½Π½ΡΠΌΠΈ ΠΈ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΠ°ΠΌΠΈ Π΄Π»Ρ ΠΠ‘/Π€Π.")
+_HERO_HTML = """
+
+
+"""
def _env(name: str, default: str) -> str:
@@ -24,14 +35,36 @@ def _normalize_base_url(value: str) -> str:
v = v[: -len("/generate")]
return v.rstrip("/")
+
+def _title_bar(*, show_logout: bool):
+ st.html(_HERO_HTML)
+ row = st.container(horizontal=True, vertical_alignment="bottom")
+ with row:
+ st.title("DevOps AI Assistant", anchor=False)
+ if show_logout:
+
+ def _logout():
+ st.session_state.token = None
+ st.rerun()
+
+ st.button(
+ "ΠΡΠΉΡΠΈ",
+ icon=":material/logout:",
+ on_click=_logout,
+ help="ΠΡΠΉΡΠΈ ΠΈΠ· ΠΏΠΎΡΡΠ°Π»Π°",
+ )
+
+
with st.sidebar:
- st.header("ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ")
+ st.markdown("#### ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ")
backend_base_url = _normalize_base_url(st.text_input("Backend URL", default_backend_base_url))
model = st.selectbox(
- "LLM ΠΠΎΠ΄Π΅Π»Ρ",
+ "LLM ΠΌΠΎΠ΄Π΅Π»Ρ",
["deepseek-r1:1.5b", "qwen2.5-coder:1.5b", "llama3.1:8b", "Arena Model"],
)
platform = st.radio("ΠΠ»Π°ΡΡΠΎΡΠΌΠ°", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"])
+ st.divider()
+ st.caption("ΠΠΎΡΡΠ°Π» ΠΠ‘/Π€Π Β· ΠΊΠΎΠ½ΡΠΈΠ³ΡΡΠ°ΡΠΈΠΈ Π² ΡΠ°Π·Π΄Π΅Π»Π΅ Β«ΠΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡΒ»")
if "history" not in st.session_state:
st.session_state.history = []
@@ -67,6 +100,7 @@ def _auth_headers():
return {}
return {"Authorization": f"Bearer {st.session_state.token}"}
+
def _detail_or_text(resp: requests.Response) -> str:
try:
payload = resp.json()
@@ -78,35 +112,37 @@ def _detail_or_text(resp: requests.Response) -> str:
def _show_account():
- st.subheader("ΠΡΡΠ΅Π½ΡΠΈΡΠΈΠΊΠ°ΡΠΈΡ")
+ _title_bar(show_logout=bool(st.session_state.token))
+ st.caption("ΠΠΎΠΉΠ΄ΠΈΡΠ΅ ΠΈΠ»ΠΈ ΡΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΡΡΡΡΠ½ΡΡ Π·Π°ΠΏΠΈΡΡ, ΡΡΠΎΠ±Ρ ΠΎΡΠΊΡΡΡΡ ΡΠ°Π·Π΄Π΅Π»Ρ ΠΏΠΎΡΡΠ°Π»Π°.")
- c1, c2 = st.columns(2)
+ # ΠΠ²Π° Π±Π»ΠΎΠΊΠ° Ρ ΡΠ°ΠΌΠΊΠΎΠΉ β Π±Π»ΠΈΠΆΠ΅ ΠΊ Β«ΠΊΠ°ΡΡΠΎΡΠ½ΠΎΠΌΡΒ» Π²ΠΈΠ΄Ρ Π΄Π΅ΠΌΠΎ
+ c1, c2 = st.columns(2, gap="large")
with c1:
- st.markdown("**Π Π΅Π³ΠΈΡΡΡΠ°ΡΠΈΡ**")
- reg_email = st.text_input("Email", key="reg_email")
- reg_full_name = st.text_input("ΠΠΌΡ (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ)", key="reg_full_name")
- reg_password = st.text_input("ΠΠ°ΡΠΎΠ»Ρ", type="password", key="reg_password")
- if st.button("Π‘ΠΎΠ·Π΄Π°ΡΡ Π°ΠΊΠΊΠ°ΡΠ½Ρ", key="btn_register"):
- try:
- r = requests.post(
- f"{backend_base_url}/auth/register",
- json={"email": reg_email, "password": reg_password, "full_name": reg_full_name or None},
- timeout=30,
- )
- if r.status_code >= 400:
- st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΡΠ΅Π³ΠΈΡΡΡΠ°ΡΠΈΠΈ: {_detail_or_text(r)}")
- else:
- st.success("ΠΠΊΠΊΠ°ΡΠ½Ρ ΡΠΎΠ·Π΄Π°Π½. Π’Π΅ΠΏΠ΅ΡΡ ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΠΉΡΠΈ.")
- except Exception as e:
- st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρ: {e}")
+ with st.container(border=True):
+ st.markdown("**Π Π΅Π³ΠΈΡΡΡΠ°ΡΠΈΡ**")
+ reg_email = st.text_input("Email", key="reg_email")
+ reg_full_name = st.text_input("ΠΠΌΡ (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ)", key="reg_full_name")
+ reg_password = st.text_input("ΠΠ°ΡΠΎΠ»Ρ", type="password", key="reg_password")
+ if st.button("Π‘ΠΎΠ·Π΄Π°ΡΡ Π°ΠΊΠΊΠ°ΡΠ½Ρ", key="btn_register", use_container_width=True):
+ try:
+ r = requests.post(
+ f"{backend_base_url}/auth/register",
+ json={"email": reg_email, "password": reg_password, "full_name": reg_full_name or None},
+ timeout=30,
+ )
+ if r.status_code >= 400:
+ st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΡΠ΅Π³ΠΈΡΡΡΠ°ΡΠΈΠΈ: {_detail_or_text(r)}")
+ else:
+ st.success("ΠΠΊΠΊΠ°ΡΠ½Ρ ΡΠΎΠ·Π΄Π°Π½. Π’Π΅ΠΏΠ΅ΡΡ ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΠΉΡΠΈ.")
+ except Exception as e:
+ st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρ: {e}")
with c2:
- st.markdown("**ΠΡ
ΠΎΠ΄**")
- login_email = st.text_input("Email ", key="login_email")
- login_password = st.text_input("ΠΠ°ΡΠΎΠ»Ρ ", type="password", key="login_password")
- login_col_a, login_col_b = st.columns([1, 1])
- with login_col_a:
- if st.button("ΠΠΎΠΉΡΠΈ", type="primary", key="btn_login"):
+ with st.container(border=True):
+ st.markdown("**ΠΡ
ΠΎΠ΄**")
+ login_email = st.text_input("Email ", key="login_email")
+ login_password = st.text_input("ΠΠ°ΡΠΎΠ»Ρ ", type="password", key="login_password")
+ if st.button("ΠΠΎΠΉΡΠΈ", type="primary", key="btn_login", use_container_width=True):
try:
r = requests.post(
f"{backend_base_url}/auth/login",
@@ -121,10 +157,6 @@ def _show_account():
st.rerun()
except Exception as e:
st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρ: {e}")
- with login_col_b:
- if st.button("ΠΡΠΉΡΠΈ", disabled=not bool(st.session_state.token), key="btn_logout"):
- st.session_state.token = None
- st.rerun()
st.divider()
if st.session_state.token:
@@ -135,16 +167,20 @@ def _show_account():
st.warning("Π‘Π΅ΡΡΠΈΡ ΠΈΡΡΠ΅ΠΊΠ»Π° ΠΈΠ»ΠΈ ΡΠΎΠΊΠ΅Π½ Π½Π΅Π²Π΅ΡΠ½ΡΠΉ. ΠΠΎΠΉΠ΄ΠΈΡΠ΅ Π·Π°Π½ΠΎΠ²ΠΎ.")
st.rerun()
else:
- st.json(r.json())
+ with st.expander("ΠΡΠΎΡΠΈΠ»Ρ", expanded=False):
+ st.json(r.json())
except Exception as e:
st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρ: {e}")
-# Auth wall: ΠΏΠΎΠΊΠ° ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π½Π΅ Π²ΠΎΡΡΠ» β ΠΏΠΎΠΊΠ°Π·ΡΠ²Π°Π΅ΠΌ ΡΠΎΠ»ΡΠΊΠΎ ΡΠΊΡΠ°Π½ Π²Ρ
ΠΎΠ΄Π°/ΡΠ΅Π³ΠΈΡΡΡΠ°ΡΠΈΠΈ.
+# Auth wall
if not st.session_state.token:
_show_account()
st.stop()
+# ΠΠΎΡΠ»Π΅ Π²Ρ
ΠΎΠ΄Π° β ΡΠΎΡ ΠΆΠ΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ + Π²ΠΊΠ»Π°Π΄ΠΊΠΈ
+_title_bar(show_logout=True)
+st.caption("ΠΠΎΡΡΠ°Π» Ρ Π΄Π°Π½Π½ΡΠΌΠΈ ΠΈ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΠ°ΠΌΠΈ Π΄Π»Ρ ΠΠ‘/Π€Π.")
tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
[
@@ -158,79 +194,97 @@ tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
with tab_info:
st.subheader("Π‘Π²Π΅Π΄Π΅Π½ΠΈΡ ΠΎΠ± ΠΠ‘/Π€Π")
- st.caption("Π§Π΅ΡΠ½ΠΎΠ²ΠΈΠΊ ΡΠΎΡΠΌΡ. ΠΠ°Π»ΡΡΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡΠΈΠ²ΡΠ·Π°ΡΡ ΠΊ Postgres ΠΈ Ρ
ΡΠ°Π½ΠΈΡΡ ΠΊΠ°ΠΊ ΡΡΡΠ½ΠΎΡΡΡ.")
- st.session_state.asfp_info["name"] = st.text_input("ΠΠ°Π·Π²Π°Π½ΠΈΠ΅", st.session_state.asfp_info["name"])
- st.session_state.asfp_info["owner"] = st.text_input("ΠΠ»Π°Π΄Π΅Π»Π΅Ρ", st.session_state.asfp_info["owner"])
- st.session_state.asfp_info["contacts"] = st.text_input("ΠΠΎΠ½ΡΠ°ΠΊΡΡ", st.session_state.asfp_info["contacts"])
- st.session_state.asfp_info["description"] = st.text_area(
- "ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅", st.session_state.asfp_info["description"], height=120
- )
- st.divider()
- st.json(st.session_state.asfp_info)
+ st.caption("Π§Π΅ΡΠ½ΠΎΠ²ΠΈΠΊ ΡΠΎΡΠΌΡ. ΠΠ°Π»ΡΡΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡΠΈΠ²ΡΠ·Π°ΡΡ ΠΊ Postgres.")
+ with st.container(border=True):
+ st.session_state.asfp_info["name"] = st.text_input("ΠΠ°Π·Π²Π°Π½ΠΈΠ΅", st.session_state.asfp_info["name"])
+ st.session_state.asfp_info["owner"] = st.text_input("ΠΠ»Π°Π΄Π΅Π»Π΅Ρ", st.session_state.asfp_info["owner"])
+ st.session_state.asfp_info["contacts"] = st.text_input("ΠΠΎΠ½ΡΠ°ΠΊΡΡ", st.session_state.asfp_info["contacts"])
+ st.session_state.asfp_info["description"] = st.text_area(
+ "ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅", st.session_state.asfp_info["description"], height=120
+ )
+ with st.expander("JSON", expanded=False):
+ st.json(st.session_state.asfp_info)
with tab_autonomous:
st.subheader("ΠΠ²ΡΠΎΠ½ΠΎΠΌΠ½ΡΠ΅ Π²Π½Π΅Π΄ΡΠ΅Π½ΠΈΡ")
- st.info("Π‘ΡΠ΄Π° ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ ΡΠΏΠΈΡΠΎΠΊ Π²Π½Π΅Π΄ΡΠ΅Π½ΠΈΠΉ, ΡΡΠ°ΡΡΡ, Π΄Π°ΡΡ, ΠΎΡΠ²Π΅ΡΡΡΠ²Π΅Π½Π½ΡΡ
ΠΈ ΡΡΡΠ»ΠΊΠΈ Π½Π° ΡΠ΅Π»ΠΈΠ·Ρ/ΡΠΈΠΊΠ΅ΡΡ.")
+ with st.container(border=True):
+ st.info(
+ "Π‘ΡΠ΄Π° ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ ΡΠΏΠΈΡΠΎΠΊ Π²Π½Π΅Π΄ΡΠ΅Π½ΠΈΠΉ, ΡΡΠ°ΡΡΡ, Π΄Π°ΡΡ, ΠΎΡΠ²Π΅ΡΡΡΠ²Π΅Π½Π½ΡΡ
ΠΈ ΡΡΡΠ»ΠΊΠΈ Π½Π° ΡΠ΅Π»ΠΈΠ·Ρ/ΡΠΈΠΊΠ΅ΡΡ."
+ )
with tab_infra:
st.subheader("ΠΠ½ΡΡΠ°ΡΡΡΡΠΊΡΡΡΠ° ΠΠ‘/Π€Π")
- st.info("Π‘ΡΠ΄Π° ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΡ, ΠΊΠ»Π°ΡΡΠ΅ΡΠ°, ΡΠ΅ΡΠ²ΠΈΡΡ, ΡΠ΅ΡΡΡΡΡ, ΡΠ΅ΡΠ΅Π²ΡΠ΅ Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠΈ, ΡΠ΅ΠΊΡΠ΅ΡΡ ΠΈ Ρ.Π΄.")
+ with st.container(border=True):
+ st.info(
+ "Π‘ΡΠ΄Π° ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΡ, ΠΊΠ»Π°ΡΡΠ΅ΡΠ°, ΡΠ΅ΡΠ²ΠΈΡΡ, ΡΠ΅ΡΡΡΡΡ, ΡΠ΅ΡΠ΅Π²ΡΠ΅ Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠΈ ΠΈ Ρ.Π΄."
+ )
with tab_tools:
st.subheader("ΠΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ")
- st.caption(f"Backend URL: {backend_base_url}")
+ st.caption(f"Backend: `{backend_base_url}`")
- st.markdown("### Pipeline generator")
- user_input = st.text_area(
- "ΠΠΏΠΈΡΠΈΡΠ΅ ΠΏΠ°ΠΉΠΏΠ»Π°ΠΉΠ½ (Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ: 'Python app with pytest and docker build')",
- height=150,
- key="generator_prompt",
- )
+ with st.container(border=True):
+ st.markdown("#### Pipeline generator")
+ user_input = st.text_area(
+ "ΠΠΏΠΈΡΠΈΡΠ΅ ΠΏΠ°ΠΉΠΏΠ»Π°ΠΉΠ½ (Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ: 'Python app with pytest and docker build')",
+ height=150,
+ key="generator_prompt",
+ )
- col_a, col_b = st.columns([1, 3])
- with col_a:
- generate_clicked = st.button("Π‘Π³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°ΡΡ ΠΊΠΎΠ½ΡΠΈΠ³ΡΡΠ°ΡΠΈΡ", type="primary")
- with col_b:
- st.caption("ΠΠΎΠ΄ΡΠΊΠ°Π·ΠΊΠ°: ΡΠ΅ΠΌ ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½Π΅Π΅ ΡΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ, ΡΠ΅ΠΌ Π²Π°Π»ΠΈΠ΄Π½Π΅Π΅ YAML.")
+ col_a, col_b = st.columns([1, 3])
+ with col_a:
+ generate_clicked = st.button("Π‘Π³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°ΡΡ", type="primary", use_container_width=True)
+ with col_b:
+ st.caption("Π§Π΅ΠΌ ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½Π΅Π΅ ΡΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ, ΡΠ΅ΠΌ ΡΡΠ°Π±ΠΈΠ»ΡΠ½Π΅Π΅ YAML.")
- if generate_clicked:
- if user_input.strip():
- with st.spinner("LLM Π΄ΡΠΌΠ°Π΅Ρ..."):
- try:
- res = requests.post(
- f"{backend_base_url}/generate",
- json={"description": user_input, "platform": platform, "model": model},
- headers=_auth_headers(),
- timeout=90,
- )
- res.raise_for_status()
- data = res.json()
+ if generate_clicked:
+ if user_input.strip():
+ with st.spinner("LLM Π΄ΡΠΌΠ°Π΅Ρ..."):
+ try:
+ res = requests.post(
+ f"{backend_base_url}/generate",
+ json={"description": user_input, "platform": platform, "model": model},
+ headers=_auth_headers(),
+ timeout=90,
+ )
+ res.raise_for_status()
+ data = res.json()
- status = data.get("status", "error")
- config = data.get("config", "")
- err = data.get("validation_error")
+ status = data.get("status", "error")
+ config = data.get("config", "")
+ err = data.get("validation_error")
- _push_history(
- platform=platform,
- model=model,
- prompt=user_input,
- status=status,
- config=config,
- error=err,
- )
+ _push_history(
+ platform=platform,
+ model=model,
+ prompt=user_input,
+ status=status,
+ config=config,
+ error=err,
+ )
- if status == "ok":
- st.success("ΠΠΎΠ½ΡΠΈΠ³ΡΡΠ°ΡΠΈΡ Π²Π°Π»ΠΈΠ΄Π½Π°.")
- else:
- st.error(f"ΠΡΠΈΠ±ΠΊΠ° Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΠΈ: {err}")
+ if status == "ok":
+ st.success("ΠΠΎΠ½ΡΠΈΠ³ΡΡΠ°ΡΠΈΡ Π²Π°Π»ΠΈΠ΄Π½Π°.")
+ else:
+ st.error(f"ΠΡΠΈΠ±ΠΊΠ° Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΠΈ: {err}")
- st.code(config, language="yaml")
- st.download_button("Π‘ΠΊΠ°ΡΠ°ΡΡ .yml", config, file_name="pipeline.yml")
- except Exception as e:
- st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρ: {e}")
- else:
- st.warning("ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ°, Π²Π²Π΅Π΄ΠΈΡΠ΅ ΠΎΠΏΠΈΡΠ°Π½ΠΈΠ΅.")
+ st.code(config, language="yaml")
+ st.download_button("Π‘ΠΊΠ°ΡΠ°ΡΡ .yml", config, file_name="pipeline.yml")
+ except Exception as e:
+ st.error(f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρ: {e}")
+ else:
+ st.warning("ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ°, Π²Π²Π΅Π΄ΠΈΡΠ΅ ΠΎΠΏΠΈΡΠ°Π½ΠΈΠ΅.")
with tab_news:
st.subheader("ΠΠΎΠ²ΠΎΡΡΠΈ")
- st.info("Π‘ΡΠ΄Π° ΠΌΠΎΠΆΠ½ΠΎ Π²ΡΠ²Π΅ΡΡΠΈ Π½ΠΎΠ²ΠΎΡΡΠΈ/ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ: ΡΠ΅Π»ΠΈΠ·Ρ, ΠΈΠ½ΡΠΈΠ΄Π΅Π½ΡΡ, ΡΠ΅Π³Π»Π°ΠΌΠ΅Π½ΡΠ½ΡΠ΅ ΡΠ°Π±ΠΎΡΡ, ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΠΎΠ².")
+ with st.container(border=True):
+ st.info(
+ "Π Π΅Π»ΠΈΠ·Ρ, ΠΈΠ½ΡΠΈΠ΄Π΅Π½ΡΡ, ΡΠ΅Π³Π»Π°ΠΌΠ΅Π½ΡΠ½ΡΠ΅ ΡΠ°Π±ΠΎΡΡ, ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΠΎΠ² β ΡΡΠ΄Π° ΠΌΠΎΠΆΠ½ΠΎ Π²ΡΠ²Π΅ΡΡΠΈ Π»Π΅Π½ΡΡ ΠΏΠΎΠ·ΠΆΠ΅."
+ )
+
+st.divider()
+st.button(
+ " :small[:gray[:material/balance: Π ΠΏΠΎΡΡΠ°Π»Π΅]]",
+ type="tertiary",
+ help="DevOps AI Assistant β Π²Π½ΡΡΡΠ΅Π½Π½ΠΈΠΉ ΠΏΠΎΡΡΠ°Π».",
+)