refactor: gradio codes
Browse files- AGENTS.md +14 -0
- app.py +95 -42
- fix_css.py +187 -0
- fix_css2.py +245 -0
- fix_css3.py +203 -0
- src/retrieval/finRetrieval.py +5 -2
- src/utils/ui_templates.py +514 -0
- stitch_screen.html +0 -0
AGENTS.md
CHANGED
|
@@ -121,6 +121,13 @@ def test_4_core_scenarios():
|
|
| 121 |
- [x] **3. 커스텀 CSS 및 버튼 고대비화**: 흰색 배경에서 버튼이 완벽하게 보이도록 고대비 Indigo/Blue 색상 및 프리미엄 스타일 지정
|
| 122 |
- [x] **4. 정적/동적 방어 테스트**: Ruff/Mypy 통과, `python -c "import app"` 정상 빌드, `smoke_test_rag.py` 성공 검증
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
## 배포 및 자동화 파이프라인 (Pipeline Automation)
|
| 126 |
- [x] **매일 새벽 1시(KST) 최신화 파이프라인 구축**: 크롤링(`finScrapping.py`) ➡️ 지식 그래프 적재(`finGraph.py`)로 이어지는 엔드투엔드(End-to-End) 자동화.
|
|
@@ -147,3 +154,10 @@ def test_4_core_scenarios():
|
|
| 147 |
3. `finRetrieval.py` 내의 `Text2Cypher` 예제들과 RAG 시스템 프롬프트를 전면 개편하여 구조적 Cypher 검색 시에도 실제 기사의 제목 및 URL([출처 링크])을 자동 매핑하여 답변하도록 출처 신뢰성을 대폭 강화.
|
| 148 |
- **검증**: `ruff`, `mypy` 린트와 타입 검사를 무결점 통과하였고, 4대 골드 시나리오를 검증하는 `smoke_test_rag.py`에서 **4/4 시나리오 전원 초고속 완전 합격(PASS)**하여 최고의 완성도를 입증함.
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
- [x] **3. 커스텀 CSS 및 버튼 고대비화**: 흰색 배경에서 버튼이 완벽하게 보이도록 고대비 Indigo/Blue 색상 및 프리미엄 스타일 지정
|
| 122 |
- [x] **4. 정적/동적 방어 테스트**: Ruff/Mypy 통과, `python -c "import app"` 정상 빌드, `smoke_test_rag.py` 성공 검증
|
| 123 |
|
| 124 |
+
## 개발 체크리스트 (Gradio UI/UX 디테일 개선 단계)
|
| 125 |
+
- [ ] **1. 화면 너비 대폭 확대**: `.gradio-container` 및 블록 레이아웃의 max-width를 대폭 확장하여 대화면 지원
|
| 126 |
+
- [ ] **2. 예시 질문 최상단(챗봇 위) 이동**: CSS Flexbox order 또는 Blocks 구조 개편을 통해 예시 질문을 화면 맨 위로 고정
|
| 127 |
+
- [ ] **3. 버튼 테두리 얇게 개선**: 예시 질문 버튼의 포인트 보더 두께를 축소하고 얇고 깔끔하게 미니멀리즘 디자인 적용
|
| 128 |
+
- [ ] **4. 정적/동적 검증**: Ruff/Mypy 통과 및 `browser_subagent`를 통한 실제 렌더링 무결성 스크린샷 검증
|
| 129 |
+
|
| 130 |
+
|
| 131 |
|
| 132 |
## 배포 및 자동화 파이프라인 (Pipeline Automation)
|
| 133 |
- [x] **매일 새벽 1시(KST) 최신화 파이프라인 구축**: 크롤링(`finScrapping.py`) ➡️ 지식 그래프 적재(`finGraph.py`)로 이어지는 엔드투엔드(End-to-End) 자동화.
|
|
|
|
| 154 |
3. `finRetrieval.py` 내의 `Text2Cypher` 예제들과 RAG 시스템 프롬프트를 전면 개편하여 구조적 Cypher 검색 시에도 실제 기사의 제목 및 URL([출처 링크])을 자동 매핑하여 답변하도록 출처 신뢰성을 대폭 강화.
|
| 155 |
- **검증**: `ruff`, `mypy` 린트와 타입 검사를 무결점 통과하였고, 4대 골드 시나리오를 검증하는 `smoke_test_rag.py`에서 **4/4 시나리오 전원 초고속 완전 합격(PASS)**하여 최고의 완성도를 입증함.
|
| 156 |
|
| 157 |
+
- [x] **메인 진입점(app.py) 프레젠테이션 자원 모듈화 및 클린 코드 개편**:
|
| 158 |
+
- **현상**: 450줄이 넘는 방대한 정적 CSS 스타일시트와 HTML 문자열 템플릿(GNB, 2x3 상태 대시보드 템플릿 등)이 메인 진입점인 `app.py` 내에 인라인으로 섞여 있어, 개발 유지 보수 효율성과 코드 가독성이 현저히 저해되는 문제 확인.
|
| 159 |
+
- **조치**:
|
| 160 |
+
1. 모든 정적/동적 프레젠테이션 요소(`CUSTOM_CSS`, `GNB_HTML`, `build_stats_html`)를 신규 유틸리티 모듈인 `src/utils/ui_templates.py`로 완벽하게 이전하여 코드를 물리적으로 완전 분리.
|
| 161 |
+
2. `app.py`에서는 간단히 `from src.utils.ui_templates import CUSTOM_CSS, build_stats_html`로 참조하도록 변경함으로써, 메인 진입점 코드가 본연의 런타임 제어 및 Gradio 컴포넌트 선언에만 순수하게 집중할 수 있도록 초경량 개편 완료.
|
| 162 |
+
- **검증**: `ruff` 정적 린트 및 `mypy` 타입 검사를 100% 무결점으로 통과하였으며, `python -c "import app"` 및 `tests/smoke_test_rag.py` 하이브리드 RAG 테스트도 전원 완벽하게 합격(PASS)함.
|
| 163 |
+
|
app.py
CHANGED
|
@@ -248,6 +248,15 @@ def get_db_stats() -> Dict[str, Any]:
|
|
| 248 |
"RETURN t.name as name, COALESCE(t.description, 'AI 혁신 기술 인프라') as desc LIMIT 8"
|
| 249 |
)
|
| 250 |
stats["techs_list"] = [{"name": r["name"], "desc": r["desc"]} for r in res_tech_list]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
# 3. 최근 기사 목록 조회 (최근 4개)
|
| 253 |
res_art_list = session.run(
|
|
@@ -275,6 +284,15 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
|
|
| 275 |
if not keyword_html:
|
| 276 |
keyword_html = '<div style="font-size:12px; color:#94a3b8;">등록된 키워드가 없습니다.</div>'
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
# 2. 최근 기사 리스트 HTML 생성 (최대 4개) - 전체 영역 클릭 시 이동하도록 a 태그로 래핑
|
| 279 |
news_list_html: str = ""
|
| 280 |
for a in stats.get("recent_articles", []):
|
|
@@ -293,7 +311,7 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
|
|
| 293 |
if not news_list_html:
|
| 294 |
news_list_html = '<div style="font-size:12px; color:#94a3b8;">최근 수집된 기사가 없습니다.</div>'
|
| 295 |
|
| 296 |
-
node_count = stats['companies'] + stats['technologies']
|
| 297 |
|
| 298 |
html: str = f"""
|
| 299 |
<div class="dashboard-container">
|
|
@@ -305,7 +323,7 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
|
|
| 305 |
</div>
|
| 306 |
<p style="font-size: 11px; color: #475569; margin-top: -2px; margin-bottom: 12px; font-weight: 600;">GraphRAG 실시간 분석 엔진 상태</p>
|
| 307 |
|
| 308 |
-
<!-- 실시간 엔진 텔레메트리 (4개 메트릭) -->
|
| 309 |
<div class="stats-grid">
|
| 310 |
<div class="stat-card">
|
| 311 |
<div class="stat-lbl">💡 분석 모델</div>
|
|
@@ -320,34 +338,21 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
|
|
| 320 |
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">{stats['technologies']}개</div>
|
| 321 |
</div>
|
| 322 |
<div class="stat-card">
|
| 323 |
-
<div class="stat-lbl">🔐 DB 연결</div>
|
| 324 |
-
<div class="stat-val" style="font-size: 12px !important; font-weight: 800 !important; color: #0d9488; margin-top: 3px;"><span style="background: rgba(13, 148, 136, 0.12); padding: 2px 6px; border-radius: 5px; display: inline-block;">Active</span></div>
|
| 325 |
-
</div>
|
| 326 |
-
</div>
|
| 327 |
-
|
| 328 |
-
<!-- 수집된 데이터 규모 -->
|
| 329 |
-
<div class="stats-grid" style="margin-top: 10px; margin-bottom: 8px;">
|
| 330 |
-
<div class="stat-card" style="padding: 8px 10px;">
|
| 331 |
<div class="stat-lbl">📰 분석용 뉴스 기사</div>
|
| 332 |
-
<div class="stat-val" style="color: #334155;">{stats['articles']}건</div>
|
| 333 |
-
</div>
|
| 334 |
-
<div class="stat-card" style="padding: 8px 10px;">
|
| 335 |
-
<div class="stat-lbl">🧬 추출된 지식 연결망</div>
|
| 336 |
-
<div class="stat-val" style="color: #334155;">{node_count}개</div>
|
| 337 |
</div>
|
| 338 |
</div>
|
| 339 |
-
|
| 340 |
-
<!-- 데이터 배경 정보 설명 패널 (사용자 이해를 돕는 배경 정보 친절 서술) -->
|
| 341 |
-
<div style="font-size: 11px; color: #475569; line-height: 1.5; margin-top: 8px; margin-bottom: 15px; padding: 10px; background: rgba(241, 245, 249, 0.7); border: 1px solid #cbd5e1; border-radius: 8px;">
|
| 342 |
-
ℹ️ <b>데이터 수집 배경 정보</b><br>
|
| 343 |
-
실시간 뉴스 웹 크롤러가 국내 IT/금융 기사 <b>{stats['articles']}건</b>을 정밀 수집하였으며, 뉴스 본문을 분석하여 기사 속 주요 기업·핵심 기술·서비스 간의 입체적 연관 관계 <b>{node_count}개</b>를 연결망(지식 그래프)으로 완벽하게 연동하였습니다.
|
| 344 |
-
</div>
|
| 345 |
|
| 346 |
<div class="section-subtitle" style="color: #334155;">💡 최신 뉴스 키워드</div>
|
| 347 |
<div class="keyword-container">
|
| 348 |
{keyword_html}
|
| 349 |
</div>
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
<div class="section-subtitle" style="color: #334155;">📰 최신 뉴스 피드</div>
|
| 352 |
<div class="news-feed-container">
|
| 353 |
<div class="news-feed">
|
|
@@ -581,25 +586,59 @@ body {
|
|
| 581 |
border-left-color: #2dd4bf !important;
|
| 582 |
}
|
| 583 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
/* 2x2 grid layout for chatbot example buttons (Stitch Action Grid style) */
|
| 585 |
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 586 |
display: grid !important;
|
| 587 |
grid-template-columns: repeat(2, 1fr) !important;
|
| 588 |
gap: 10px !important;
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
}
|
| 594 |
[class*="examples"] button {
|
| 595 |
-
text-align:
|
| 596 |
padding: 14px 18px !important;
|
| 597 |
-
background: #
|
| 598 |
-
border: 1px solid #
|
| 599 |
border-radius: 10px !important;
|
| 600 |
font-size: 13px !important;
|
| 601 |
font-weight: 600 !important;
|
| 602 |
-
color: #
|
| 603 |
line-height: 1.4 !important;
|
| 604 |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02) !important;
|
| 605 |
transition: all 0.2s ease-in-out !important;
|
|
@@ -607,6 +646,9 @@ body {
|
|
| 607 |
height: auto !important;
|
| 608 |
min-height: 54px !important;
|
| 609 |
cursor: pointer !important;
|
|
|
|
|
|
|
|
|
|
| 610 |
}
|
| 611 |
.dark [class*="examples"] button {
|
| 612 |
background: rgba(30, 41, 59, 0.5) !important;
|
|
@@ -615,9 +657,9 @@ body {
|
|
| 615 |
}
|
| 616 |
[class*="examples"] button:hover {
|
| 617 |
transform: translateY(-1px) !important;
|
| 618 |
-
background: #
|
| 619 |
-
border-color: #
|
| 620 |
-
color: #
|
| 621 |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.06) !important;
|
| 622 |
}
|
| 623 |
.dark [class*="examples"] button:hover {
|
|
@@ -724,15 +766,14 @@ button.variant-secondary:hover,
|
|
| 724 |
}
|
| 725 |
|
| 726 |
/* Chatbot 라벨/탭 완전 숨김 (불필요한 보라색/하늘색 색상 및 테두리 원천 차단) */
|
| 727 |
-
.chatbot > div:first-child
|
| 728 |
-
[class*="chatbot"] > div:first-child,
|
| 729 |
.chatbot-label,
|
| 730 |
div[class*="chatbot"] .label,
|
| 731 |
[data-testid="chatbot"] .label,
|
| 732 |
.chatbot-header,
|
| 733 |
-
div[class*="chatbot"] > div:first-child span,
|
| 734 |
.gr-panel-title,
|
| 735 |
-
.gr-chatbot-label
|
|
|
|
| 736 |
display: none !important;
|
| 737 |
}
|
| 738 |
|
|
@@ -751,11 +792,11 @@ div[class*="chatbot"] > div:first-child span,
|
|
| 751 |
textarea,
|
| 752 |
[class*="input-container"] textarea,
|
| 753 |
[data-testid="textbox"] textarea {
|
| 754 |
-
height:
|
| 755 |
-
min-height:
|
| 756 |
-
max-height:
|
| 757 |
font-size: 13px !important;
|
| 758 |
-
padding:
|
| 759 |
line-height: 1.5 !important;
|
| 760 |
border-radius: 8px !important;
|
| 761 |
border: 1px solid #cbd5e1 !important;
|
|
@@ -782,7 +823,7 @@ button[class*="submit-btn"],
|
|
| 782 |
margin-left: 12px !important;
|
| 783 |
border-radius: 8px !important;
|
| 784 |
min-width: 95px !important;
|
| 785 |
-
height:
|
| 786 |
padding: 0 16px !important;
|
| 787 |
display: flex !important;
|
| 788 |
align-items: center !important;
|
|
@@ -822,9 +863,21 @@ div:has(> [data-testid="submit-button"]),
|
|
| 822 |
}
|
| 823 |
"""
|
| 824 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
interface_kwargs = {
|
| 826 |
"fn": chat,
|
| 827 |
-
"chatbot": gr.Chatbot(height=
|
| 828 |
"textbox": gr.Textbox(
|
| 829 |
placeholder="분석하고 싶은 내용을 자연어로 입력해주세요...",
|
| 830 |
container=False,
|
|
|
|
| 248 |
"RETURN t.name as name, COALESCE(t.description, 'AI 혁신 기술 인프라') as desc LIMIT 8"
|
| 249 |
)
|
| 250 |
stats["techs_list"] = [{"name": r["name"], "desc": r["desc"]} for r in res_tech_list]
|
| 251 |
+
|
| 252 |
+
# 2.5 최신 주목 기업 리스트 (상위 5개)
|
| 253 |
+
res_comp_list = session.run(
|
| 254 |
+
"MATCH (c:AICompany) "
|
| 255 |
+
"OPTIONAL MATCH (a:Article)-[:MENTIONS]->(c) "
|
| 256 |
+
"RETURN c.name as name, count(a) as cnt "
|
| 257 |
+
"ORDER BY cnt DESC LIMIT 5"
|
| 258 |
+
)
|
| 259 |
+
stats["companies_list"] = [{"name": r["name"]} for r in res_comp_list]
|
| 260 |
|
| 261 |
# 3. 최근 기사 목록 조회 (최근 4개)
|
| 262 |
res_art_list = session.run(
|
|
|
|
| 284 |
if not keyword_html:
|
| 285 |
keyword_html = '<div style="font-size:12px; color:#94a3b8;">등록된 키워드가 없습니다.</div>'
|
| 286 |
|
| 287 |
+
# 1.5. 최신 주목 기업 배지 HTML 생성 (키워드 배지와 동일 스타일로 통일)
|
| 288 |
+
company_html: str = ""
|
| 289 |
+
for c in stats.get("companies_list", []):
|
| 290 |
+
company_html += f"""
|
| 291 |
+
<span class="keyword-badge">🏢 {c['name']}</span>
|
| 292 |
+
"""
|
| 293 |
+
if not company_html:
|
| 294 |
+
company_html = '<div style="font-size:12px; color:#94a3b8;">등록된 기업이 없습니다.</div>'
|
| 295 |
+
|
| 296 |
# 2. 최근 기사 리스트 HTML 생성 (최대 4개) - 전체 영역 클릭 시 이동하도록 a 태그로 래핑
|
| 297 |
news_list_html: str = ""
|
| 298 |
for a in stats.get("recent_articles", []):
|
|
|
|
| 311 |
if not news_list_html:
|
| 312 |
news_list_html = '<div style="font-size:12px; color:#94a3b8;">최근 수집된 기사가 없습니다.</div>'
|
| 313 |
|
| 314 |
+
# node_count = stats['companies'] + stats['technologies']
|
| 315 |
|
| 316 |
html: str = f"""
|
| 317 |
<div class="dashboard-container">
|
|
|
|
| 323 |
</div>
|
| 324 |
<p style="font-size: 11px; color: #475569; margin-top: -2px; margin-bottom: 12px; font-weight: 600;">GraphRAG 실시간 분석 엔진 상태</p>
|
| 325 |
|
| 326 |
+
<!-- 실시간 엔진 텔레메트리 (4개 메트릭 통폐합) -->
|
| 327 |
<div class="stats-grid">
|
| 328 |
<div class="stat-card">
|
| 329 |
<div class="stat-lbl">💡 분석 모델</div>
|
|
|
|
| 338 |
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">{stats['technologies']}개</div>
|
| 339 |
</div>
|
| 340 |
<div class="stat-card">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
<div class="stat-lbl">📰 분석용 뉴스 기사</div>
|
| 342 |
+
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">{stats['articles']}건</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
</div>
|
| 344 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
<div class="section-subtitle" style="color: #334155;">💡 최신 뉴스 키워드</div>
|
| 347 |
<div class="keyword-container">
|
| 348 |
{keyword_html}
|
| 349 |
</div>
|
| 350 |
|
| 351 |
+
<div class="section-subtitle" style="color: #334155;">🏢 최신 주목 기업</div>
|
| 352 |
+
<div class="keyword-container">
|
| 353 |
+
{company_html}
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
<div class="section-subtitle" style="color: #334155;">📰 최신 뉴스 피드</div>
|
| 357 |
<div class="news-feed-container">
|
| 358 |
<div class="news-feed">
|
|
|
|
| 586 |
border-left-color: #2dd4bf !important;
|
| 587 |
}
|
| 588 |
|
| 589 |
+
/* ── 웰컴 보드(소개글 카드) 상단 짤림 방어 및 결합 준비 ── */
|
| 590 |
+
.placeholder, [class*="placeholder"] {
|
| 591 |
+
justify-content: flex-start !important;
|
| 592 |
+
padding-top: 15px !important; /* 상단 짤림 완전 방지 */
|
| 593 |
+
margin-bottom: 0 !important; /* 아래쪽 간격 완전 제거 */
|
| 594 |
+
padding-bottom: 0 !important;
|
| 595 |
+
height: auto !important; /* 껍데기 높이 고정 해제 */
|
| 596 |
+
min-height: unset !important;
|
| 597 |
+
}
|
| 598 |
+
.placeholder .prose {
|
| 599 |
+
background: #f8fafc !important; /* 쿨톤 회색 */
|
| 600 |
+
border: 1px solid #e2e8f0 !important;
|
| 601 |
+
border-bottom: none !important; /* 아래쪽 둥근 선 제거로 결합 준비 */
|
| 602 |
+
border-radius: 12px 12px 0 0 !important; /* 아래 모서리 직각 */
|
| 603 |
+
padding: 24px 24px 10px 24px !important;
|
| 604 |
+
max-width: 800px !important;
|
| 605 |
+
width: 100% !important;
|
| 606 |
+
margin: 0 auto !important;
|
| 607 |
+
position: relative !important;
|
| 608 |
+
z-index: 2 !important;
|
| 609 |
+
display: block !important; /* 은신 마법 방어 */
|
| 610 |
+
visibility: visible !important;
|
| 611 |
+
color: #334155 !important;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
/* 2x2 grid layout for chatbot example buttons (Stitch Action Grid style) */
|
| 615 |
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 616 |
display: grid !important;
|
| 617 |
grid-template-columns: repeat(2, 1fr) !important;
|
| 618 |
gap: 10px !important;
|
| 619 |
+
|
| 620 |
+
/* 🌟 핵심: 웰컴 보드와 한 몸이 되기 위한 완벽 밀착 및 껍데기 🌟 */
|
| 621 |
+
margin: -1px auto 40px auto !important; /* 웰컴 보드 바로 밑에 찰싹 붙음 */
|
| 622 |
+
background: #f8fafc !important; /* 웰컴 보드와 동일한 회색 배경 */
|
| 623 |
+
border: 1px solid #e2e8f0 !important;
|
| 624 |
+
border-top: none !important; /* 윗선 제거로 한 몸 결합 */
|
| 625 |
+
border-radius: 0 0 12px 12px !important; /* 윗 모서리 직각 */
|
| 626 |
+
padding: 5px 24px 24px 24px !important;
|
| 627 |
+
max-width: 800px !important; /* 웰컴 보드 너비 일치 */
|
| 628 |
+
width: 100% !important;
|
| 629 |
+
position: relative !important;
|
| 630 |
+
z-index: 1 !important; /* 겹칠 때 흰 선 안보이게 밑으로 */
|
| 631 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 632 |
}
|
| 633 |
[class*="examples"] button {
|
| 634 |
+
text-align: center !important; /* 사용자 요청: 가운데 정렬 */
|
| 635 |
padding: 14px 18px !important;
|
| 636 |
+
background: #e0f2fe !important; /* 사용자 요청: 연한 하늘색(파랑) 배경 */
|
| 637 |
+
border: 1px solid #bae6fd !important;
|
| 638 |
border-radius: 10px !important;
|
| 639 |
font-size: 13px !important;
|
| 640 |
font-weight: 600 !important;
|
| 641 |
+
color: #000000 !important; /* 사용자 요청: 검은색 텍스트 */
|
| 642 |
line-height: 1.4 !important;
|
| 643 |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02) !important;
|
| 644 |
transition: all 0.2s ease-in-out !important;
|
|
|
|
| 646 |
height: auto !important;
|
| 647 |
min-height: 54px !important;
|
| 648 |
cursor: pointer !important;
|
| 649 |
+
display: flex !important;
|
| 650 |
+
align-items: center !important;
|
| 651 |
+
justify-content: center !important;
|
| 652 |
}
|
| 653 |
.dark [class*="examples"] button {
|
| 654 |
background: rgba(30, 41, 59, 0.5) !important;
|
|
|
|
| 657 |
}
|
| 658 |
[class*="examples"] button:hover {
|
| 659 |
transform: translateY(-1px) !important;
|
| 660 |
+
background: #bae6fd !important; /* 약간 짙은 하늘색 호버 */
|
| 661 |
+
border-color: #7dd3fc !important;
|
| 662 |
+
color: #1e3a8a !important; /* 사용자 요청: 진한 파랑색 텍스트 */
|
| 663 |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.06) !important;
|
| 664 |
}
|
| 665 |
.dark [class*="examples"] button:hover {
|
|
|
|
| 766 |
}
|
| 767 |
|
| 768 |
/* Chatbot 라벨/탭 완전 숨김 (불필요한 보라색/하늘색 색상 및 테두리 원천 차단) */
|
| 769 |
+
/* 주의: .chatbot > div:first-child 는 웰컴카드를 지워버릴 수 있으므로 삭제! */
|
|
|
|
| 770 |
.chatbot-label,
|
| 771 |
div[class*="chatbot"] .label,
|
| 772 |
[data-testid="chatbot"] .label,
|
| 773 |
.chatbot-header,
|
|
|
|
| 774 |
.gr-panel-title,
|
| 775 |
+
.gr-chatbot-label,
|
| 776 |
+
.label-wrap {
|
| 777 |
display: none !important;
|
| 778 |
}
|
| 779 |
|
|
|
|
| 792 |
textarea,
|
| 793 |
[class*="input-container"] textarea,
|
| 794 |
[data-testid="textbox"] textarea {
|
| 795 |
+
height: 48px !important;
|
| 796 |
+
min-height: 48px !important;
|
| 797 |
+
max-height: 48px !important;
|
| 798 |
font-size: 13px !important;
|
| 799 |
+
padding: 13px 16px !important; /* 위아래 패딩을 줄여서 높이에 맞게 조절 */
|
| 800 |
line-height: 1.5 !important;
|
| 801 |
border-radius: 8px !important;
|
| 802 |
border: 1px solid #cbd5e1 !important;
|
|
|
|
| 823 |
margin-left: 12px !important;
|
| 824 |
border-radius: 8px !important;
|
| 825 |
min-width: 95px !important;
|
| 826 |
+
height: 48px !important; /* 입력창의 height(48px)와 100% 동일하게 일치 */
|
| 827 |
padding: 0 16px !important;
|
| 828 |
display: flex !important;
|
| 829 |
align-items: center !important;
|
|
|
|
| 863 |
}
|
| 864 |
"""
|
| 865 |
|
| 866 |
+
CHATBOT_DESCRIPTION = """
|
| 867 |
+
<div class="prose">
|
| 868 |
+
<h3>🌌 국내 AI 뉴스 기사를 기반으로 구축된 지식 그래프(GraphRAG)에 질문하세요.</h3>
|
| 869 |
+
<ul>
|
| 870 |
+
<li>📰 <b>기업별 AI 트렌드</b> — 삼성, 카카오, 네이버 등 주요 기업의 최신 AI 동향</li>
|
| 871 |
+
<li>🔬 <b>기술 키워드 분석</b> — LLM, 생성형 AI, 파운데이션 모델 등 핵심 기술 정리</li>
|
| 872 |
+
<li>🔗 <b>실제 뉴스 출처 제공</b> — 답변마다 근거 기사 링크 포함</li>
|
| 873 |
+
</ul>
|
| 874 |
+
<p>👇 아래 예시 질문 버튼을 클릭하거나 직접 입력해 보세요.</p>
|
| 875 |
+
</div>
|
| 876 |
+
"""
|
| 877 |
+
|
| 878 |
interface_kwargs = {
|
| 879 |
"fn": chat,
|
| 880 |
+
"chatbot": gr.Chatbot(height=700, placeholder=CHATBOT_DESCRIPTION),
|
| 881 |
"textbox": gr.Textbox(
|
| 882 |
placeholder="분석하고 싶은 내용을 자연어로 입력해주세요...",
|
| 883 |
container=False,
|
fix_css.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
with open('app.py', 'r', encoding='utf-8') as f:
|
| 4 |
+
content = f.read()
|
| 5 |
+
|
| 6 |
+
start_marker = 'FINAL_CSS = CUSTOM_CSS + """'
|
| 7 |
+
end_marker = '"""\n\nlaunch_kwargs = {'
|
| 8 |
+
|
| 9 |
+
start_idx = content.find(start_marker)
|
| 10 |
+
end_idx = content.find(end_marker)
|
| 11 |
+
|
| 12 |
+
if start_idx != -1 and end_idx != -1:
|
| 13 |
+
before = content[:start_idx + len(start_marker)]
|
| 14 |
+
after = content[end_idx:]
|
| 15 |
+
|
| 16 |
+
new_css = '''
|
| 17 |
+
/* ── 전체 화면 너비 대폭 확대 ── */
|
| 18 |
+
.gradio-container {
|
| 19 |
+
max-width: 1400px !important;
|
| 20 |
+
width: 95% !important;
|
| 21 |
+
margin: 0 auto !important;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* ── 챗봇 컨테이너 짤림 원천 봉쇄 ── */
|
| 25 |
+
div[data-testid="chatbot"], .chatbot-container, .chatbot {
|
| 26 |
+
border: none !important;
|
| 27 |
+
overflow: visible !important;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ── 챗봇 내부 Placeholder(소개글 영역) 위쪽 짤림 영구 방어 ── */
|
| 31 |
+
.placeholder, [class*="placeholder"] {
|
| 32 |
+
display: flex !important;
|
| 33 |
+
flex-direction: column !important;
|
| 34 |
+
align-items: center !important;
|
| 35 |
+
justify-content: flex-start !important; /* 위쪽 짤림 방지 */
|
| 36 |
+
padding-top: 5% !important; /* 위에서 살짝 내림 */
|
| 37 |
+
height: 50% !important;
|
| 38 |
+
flex-grow: 0 !important;
|
| 39 |
+
overflow: visible !important;
|
| 40 |
+
margin: 0 auto !important;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* ── 소개글(Prose) 쿨톤 회색 웰컴 카드 ── */
|
| 44 |
+
.placeholder .prose {
|
| 45 |
+
background: #f8fafc !important; /* 대시보드 카드와 동일한 색상 */
|
| 46 |
+
border: 1px solid #e2e8f0 !important;
|
| 47 |
+
border-radius: 12px !important;
|
| 48 |
+
padding: 16px 24px !important; /* 높이 세이브 */
|
| 49 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 50 |
+
max-width: 700px !important;
|
| 51 |
+
margin: 0 auto 12px auto !important;
|
| 52 |
+
display: block !important;
|
| 53 |
+
height: auto !important;
|
| 54 |
+
}
|
| 55 |
+
.placeholder h3, [class*="placeholder"] h3 {
|
| 56 |
+
color: #334155 !important;
|
| 57 |
+
font-weight: 800 !important;
|
| 58 |
+
margin-top: 0 !important;
|
| 59 |
+
margin-bottom: 12px !important;
|
| 60 |
+
}
|
| 61 |
+
.placeholder .prose ul {
|
| 62 |
+
list-style-type: none !important;
|
| 63 |
+
padding-left: 0 !important;
|
| 64 |
+
margin-bottom: 12px !important;
|
| 65 |
+
}
|
| 66 |
+
.placeholder .prose li {
|
| 67 |
+
margin-bottom: 4px !important;
|
| 68 |
+
color: #475569 !important;
|
| 69 |
+
font-size: 14px !important;
|
| 70 |
+
line-height: 1.5 !important;
|
| 71 |
+
}
|
| 72 |
+
.placeholder .prose p:last-child {
|
| 73 |
+
font-weight: 700 !important;
|
| 74 |
+
color: #4c1d95 !important;
|
| 75 |
+
background: #f3e8ff !important;
|
| 76 |
+
padding: 8px 16px !important;
|
| 77 |
+
border-radius: 8px !important;
|
| 78 |
+
display: inline-block !important;
|
| 79 |
+
margin-bottom: 0 !important;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* ── 예시 질문 컨테이너 (그리드 배치) ── */
|
| 83 |
+
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 84 |
+
display: grid !important;
|
| 85 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 86 |
+
gap: 12px !important;
|
| 87 |
+
width: 100% !important;
|
| 88 |
+
max-width: 900px !important; /* 잘림 방지 */
|
| 89 |
+
margin: 0 auto 40px auto !important;
|
| 90 |
+
background: transparent !important;
|
| 91 |
+
border: none !important;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* 개별 버튼 디자인 (연보라색 테마 & 강력한 중앙 정렬) */
|
| 95 |
+
.examples-container button, div[data-testid="chatbot"] button.example, button.example, .example-btn {
|
| 96 |
+
border-radius: 8px !important;
|
| 97 |
+
padding: 16px !important;
|
| 98 |
+
text-align: center !important; /* 텍스트 가운데 정렬 */
|
| 99 |
+
font-size: 14px !important;
|
| 100 |
+
font-weight: 600 !important;
|
| 101 |
+
color: #4c1d95 !important;
|
| 102 |
+
background: #f5f3ff !important; /* 진짜 연보라색 배경 */
|
| 103 |
+
background-color: #f5f3ff !important; /* 덮어쓰기 철벽 방어 */
|
| 104 |
+
border: 1px solid #e9d5ff !important;
|
| 105 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
| 106 |
+
transition: all 0.2s ease-in-out !important;
|
| 107 |
+
display: flex !important;
|
| 108 |
+
align-items: center !important;
|
| 109 |
+
justify-content: center !important; /* 완벽 중앙 배치 */
|
| 110 |
+
min-height: 80px !important;
|
| 111 |
+
width: 100% !important;
|
| 112 |
+
white-space: normal !important; /* 두줄 허용하되 900px 덕분에 보통 한 줄로 나옴 */
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.examples-container button:hover, div[data-testid="chatbot"] button.example:hover, button.example:hover, .example-btn:hover {
|
| 116 |
+
border-color: #a855f7 !important;
|
| 117 |
+
background: #f3e8ff !important;
|
| 118 |
+
background-color: #f3e8ff !important;
|
| 119 |
+
transform: translateY(-2px) !important;
|
| 120 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08) !important;
|
| 121 |
+
color: #7c3aed !important;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* ── 메시지 말풍선 정리 ── */
|
| 125 |
+
.message.user, [data-testid="user"] .message {
|
| 126 |
+
background-color: #111827 !important;
|
| 127 |
+
border-radius: 12px 12px 0 12px !important;
|
| 128 |
+
padding: 7px 13px !important;
|
| 129 |
+
margin: 2px 0 !important;
|
| 130 |
+
border: none !important;
|
| 131 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08) !important;
|
| 132 |
+
min-height: unset !important;
|
| 133 |
+
}
|
| 134 |
+
[data-testid="user"] > div, .bubble-wrap [data-testid="user"], .message-wrap.user > div, .message-row.user {
|
| 135 |
+
background: transparent !important;
|
| 136 |
+
background-color: transparent !important;
|
| 137 |
+
border: none !important;
|
| 138 |
+
}
|
| 139 |
+
[data-testid="user"] .message *, .message.user * {
|
| 140 |
+
color: #ffffff !important;
|
| 141 |
+
line-height: 1.4 !important;
|
| 142 |
+
margin: 0 !important;
|
| 143 |
+
}
|
| 144 |
+
[data-testid="bot"] .message, [data-testid="bot"] > div, .message-wrap.bot > div, .message.bot, .message-row.bot .message {
|
| 145 |
+
background-color: #ffffff !important;
|
| 146 |
+
color: #1f2937 !important;
|
| 147 |
+
border: 1px solid #e5e7eb !important;
|
| 148 |
+
border-radius: 12px 12px 12px 0 !important;
|
| 149 |
+
padding: 16px 20px !important;
|
| 150 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ── 피드백 및 전송 버튼 ── */
|
| 154 |
+
.message-buttons, button[aria-label="Good response"], button[aria-label="Bad response"], .like-dislike-area { display: none !important; }
|
| 155 |
+
button[class*="submit-btn"], #submit-btn, button[id*="submit"], button.submit-button {
|
| 156 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #7c3aed 100%) !important;
|
| 157 |
+
color: white !important;
|
| 158 |
+
border-radius: 8px !important;
|
| 159 |
+
font-weight: bold !important;
|
| 160 |
+
border: none !important;
|
| 161 |
+
display: block !important;
|
| 162 |
+
opacity: 1 !important;
|
| 163 |
+
visibility: visible !important;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ── 메인 레이아웃 사이드바 균형 최적화 ── */
|
| 167 |
+
.sidebar-container {
|
| 168 |
+
border: 1px solid #e2e8f0 !important;
|
| 169 |
+
border-radius: 12px !important;
|
| 170 |
+
padding: 24px 20px !important;
|
| 171 |
+
background: #ffffff !important;
|
| 172 |
+
height: 700px !important;
|
| 173 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04) !important;
|
| 174 |
+
display: flex !important;
|
| 175 |
+
flex-direction: column !important;
|
| 176 |
+
}
|
| 177 |
+
.news-feed-container {
|
| 178 |
+
flex-grow: 1 !important;
|
| 179 |
+
max-height: 250px !important;
|
| 180 |
+
overflow-y: auto !important;
|
| 181 |
+
}
|
| 182 |
+
'''
|
| 183 |
+
with open('app.py', 'w', encoding='utf-8') as f:
|
| 184 |
+
f.write(before + '\n' + new_css + '\n' + after)
|
| 185 |
+
print('CSS Update Success')
|
| 186 |
+
else:
|
| 187 |
+
print('Markers not found')
|
fix_css2.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
with open('app.py', 'r', encoding='utf-8') as f:
|
| 4 |
+
lines = f.readlines()
|
| 5 |
+
|
| 6 |
+
# 157번째 라인부터 문제가 생김 (res_comp = session.run(...))
|
| 7 |
+
# 156번째 라인은 '# 상위 5개 회사 키워드 리스트업 추가' 임
|
| 8 |
+
cut_idx = -1
|
| 9 |
+
for i, line in enumerate(lines):
|
| 10 |
+
if '# 상위 5개 회사 키워드 리스트업 추가' in line:
|
| 11 |
+
cut_idx = i
|
| 12 |
+
break
|
| 13 |
+
|
| 14 |
+
if cut_idx == -1:
|
| 15 |
+
print("Cannot find cut index!")
|
| 16 |
+
sys.exit(1)
|
| 17 |
+
|
| 18 |
+
# 그 아래로 launch_kwargs = { 가 있는 라인을 찾음
|
| 19 |
+
end_idx = -1
|
| 20 |
+
for i in range(cut_idx, len(lines)):
|
| 21 |
+
if 'launch_kwargs = {' in line or 'launch_kwargs =' in lines[i]:
|
| 22 |
+
end_idx = i
|
| 23 |
+
break
|
| 24 |
+
|
| 25 |
+
if end_idx == -1:
|
| 26 |
+
print("Cannot find end index!")
|
| 27 |
+
sys.exit(1)
|
| 28 |
+
|
| 29 |
+
before_lines = lines[:cut_idx + 1]
|
| 30 |
+
after_lines = lines[end_idx:]
|
| 31 |
+
|
| 32 |
+
correct_middle = ''' res_comp = session.run(
|
| 33 |
+
"MATCH (c:AICompany) "
|
| 34 |
+
"OPTIONAL MATCH (a:Article)-[:MENTIONS]->(c) "
|
| 35 |
+
"RETURN c.name as name, count(a) as cnt "
|
| 36 |
+
"ORDER BY cnt DESC LIMIT 5"
|
| 37 |
+
)
|
| 38 |
+
stats["companies_list"] = [{"name": row["name"]} for row in res_comp]
|
| 39 |
+
|
| 40 |
+
res_art = session.run(
|
| 41 |
+
"MATCH (a:Article) "
|
| 42 |
+
"RETURN a.title as title, a.published_date as date, a.url as url "
|
| 43 |
+
"ORDER BY a.published_date DESC LIMIT 4"
|
| 44 |
+
)
|
| 45 |
+
stats["recent_articles"] = [
|
| 46 |
+
{"title": r["title"], "date": r["date"], "url": r["url"]}
|
| 47 |
+
for r in res_art
|
| 48 |
+
]
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"⚠️ [통계 조회 실패] {e}")
|
| 51 |
+
return stats
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ──────────────────────────────────────────
|
| 55 |
+
# 5. Gradio UI 구성
|
| 56 |
+
# ──────────────────────────────────────────
|
| 57 |
+
|
| 58 |
+
theme_obj = gr.themes.Soft(primary_hue="indigo", secondary_hue="slate")
|
| 59 |
+
|
| 60 |
+
CHATBOT_DESCRIPTION = """
|
| 61 |
+
### 🌌 국내 AI 뉴스 기사를 기반으로 구축된 지식 그래프(GraphRAG)에 질문하세요.
|
| 62 |
+
|
| 63 |
+
- 📰 **기업별 AI 트렌드** — 삼성, 카카오, 네이버 등 주요 기업의 최신 AI 동향
|
| 64 |
+
- 🔬 **기술 키워드 분석** — LLM, 생성형 AI, 파운데이션 모델 등 핵심 기술 정리
|
| 65 |
+
- 🔗 **실제 뉴스 출처 제공** — 답변마다 근거 기사 링크 포함
|
| 66 |
+
|
| 67 |
+
👇 **아래 예시 질문 버튼을 클릭하거나 직접 입력해 보세요.**
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
FINAL_CSS = CUSTOM_CSS + """
|
| 71 |
+
/* ── 전체 화면 너비 대폭 확대 ── */
|
| 72 |
+
.gradio-container {
|
| 73 |
+
max-width: 1400px !important;
|
| 74 |
+
width: 95% !important;
|
| 75 |
+
margin: 0 auto !important;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* ── 챗봇 컨테이너 테두리 및 짤림(Overflow) 방지 ── */
|
| 79 |
+
div[data-testid="chatbot"], .chatbot-container, .chatbot {
|
| 80 |
+
border: none !important;
|
| 81 |
+
overflow: visible !important;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* ── 챗봇 내부 Placeholder(소개글 영역) 위쪽 짤림 영구 방어 ── */
|
| 85 |
+
.placeholder, [class*="placeholder"] {
|
| 86 |
+
display: flex !important;
|
| 87 |
+
flex-direction: column !important;
|
| 88 |
+
align-items: center !important;
|
| 89 |
+
justify-content: flex-start !important; /* 위쪽 짤림 방지 */
|
| 90 |
+
padding-top: 5% !important; /* 위에서 살짝 내림 */
|
| 91 |
+
height: 50% !important;
|
| 92 |
+
flex-grow: 0 !important;
|
| 93 |
+
overflow: visible !important;
|
| 94 |
+
margin: 0 auto !important;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/* ── 소개글(Prose) 아이보리/쿨톤 회색 웰컴 카드 ── */
|
| 98 |
+
.placeholder .prose {
|
| 99 |
+
background: #f8fafc !important; /* 대시보드 카드와 동일한 색상 */
|
| 100 |
+
border: 1px solid #e2e8f0 !important;
|
| 101 |
+
border-radius: 12px !important;
|
| 102 |
+
padding: 16px 24px !important; /* 높이 세이브 */
|
| 103 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 104 |
+
max-width: 700px !important;
|
| 105 |
+
margin: 0 auto 12px auto !important;
|
| 106 |
+
display: block !important;
|
| 107 |
+
height: auto !important;
|
| 108 |
+
}
|
| 109 |
+
.placeholder h3, [class*="placeholder"] h3 {
|
| 110 |
+
color: #334155 !important;
|
| 111 |
+
font-weight: 800 !important;
|
| 112 |
+
margin-top: 0 !important;
|
| 113 |
+
margin-bottom: 12px !important;
|
| 114 |
+
}
|
| 115 |
+
.placeholder .prose ul {
|
| 116 |
+
list-style-type: none !important;
|
| 117 |
+
padding-left: 0 !important;
|
| 118 |
+
margin-bottom: 12px !important;
|
| 119 |
+
}
|
| 120 |
+
.placeholder .prose li {
|
| 121 |
+
margin-bottom: 4px !important;
|
| 122 |
+
color: #475569 !important;
|
| 123 |
+
font-size: 14px !important;
|
| 124 |
+
line-height: 1.5 !important;
|
| 125 |
+
}
|
| 126 |
+
.placeholder .prose p:last-child {
|
| 127 |
+
font-weight: 700 !important;
|
| 128 |
+
color: #4c1d95 !important;
|
| 129 |
+
background: #f3e8ff !important;
|
| 130 |
+
padding: 8px 16px !important;
|
| 131 |
+
border-radius: 8px !important;
|
| 132 |
+
display: inline-block !important;
|
| 133 |
+
margin-bottom: 0 !important;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/* ── 예시 질문 컨테이너 (그리드 배치) ── */
|
| 137 |
+
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 138 |
+
display: grid !important;
|
| 139 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 140 |
+
gap: 12px !important;
|
| 141 |
+
width: 100% !important;
|
| 142 |
+
max-width: 900px !important; /* 잘림 방지 */
|
| 143 |
+
margin: 0 auto 40px auto !important;
|
| 144 |
+
background: transparent !important;
|
| 145 |
+
border: none !important;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* 개별 버튼 디자인 (연보라색 테마 & 강력한 중앙 정렬) */
|
| 149 |
+
.examples-container button, div[data-testid="chatbot"] button.example, button.example, .example-btn {
|
| 150 |
+
border-radius: 8px !important;
|
| 151 |
+
padding: 16px !important;
|
| 152 |
+
text-align: center !important; /* 텍스트 가운데 정렬 */
|
| 153 |
+
font-size: 14px !important;
|
| 154 |
+
font-weight: 600 !important;
|
| 155 |
+
color: #4c1d95 !important;
|
| 156 |
+
background: #f5f3ff !important; /* 진짜 연보라색 배경 */
|
| 157 |
+
background-color: #f5f3ff !important; /* 덮어쓰기 철벽 방어 */
|
| 158 |
+
border: 1px solid #e9d5ff !important;
|
| 159 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
| 160 |
+
transition: all 0.2s ease-in-out !important;
|
| 161 |
+
display: flex !important;
|
| 162 |
+
align-items: center !important;
|
| 163 |
+
justify-content: center !important; /* 완벽 중앙 배치 */
|
| 164 |
+
min-height: 80px !important;
|
| 165 |
+
width: 100% !important;
|
| 166 |
+
white-space: normal !important; /* 두줄 허용하되 900px 덕분에 보통 한 줄로 나옴 */
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.examples-container button:hover, div[data-testid="chatbot"] button.example:hover, button.example:hover, .example-btn:hover {
|
| 170 |
+
border-color: #a855f7 !important;
|
| 171 |
+
background: #f3e8ff !important;
|
| 172 |
+
background-color: #f3e8ff !important;
|
| 173 |
+
transform: translateY(-2px) !important;
|
| 174 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08) !important;
|
| 175 |
+
color: #7c3aed !important;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* ── 메시지 말풍선 정리 ── */
|
| 179 |
+
.message.user, [data-testid="user"] .message {
|
| 180 |
+
background-color: #111827 !important;
|
| 181 |
+
border-radius: 12px 12px 0 12px !important;
|
| 182 |
+
padding: 7px 13px !important;
|
| 183 |
+
margin: 2px 0 !important;
|
| 184 |
+
border: none !important;
|
| 185 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08) !important;
|
| 186 |
+
min-height: unset !important;
|
| 187 |
+
}
|
| 188 |
+
[data-testid="user"] > div, .bubble-wrap [data-testid="user"], .message-wrap.user > div, .message-row.user {
|
| 189 |
+
background: transparent !important;
|
| 190 |
+
background-color: transparent !important;
|
| 191 |
+
border: none !important;
|
| 192 |
+
}
|
| 193 |
+
[data-testid="user"] .message *, .message.user * {
|
| 194 |
+
color: #ffffff !important;
|
| 195 |
+
line-height: 1.4 !important;
|
| 196 |
+
margin: 0 !important;
|
| 197 |
+
}
|
| 198 |
+
[data-testid="bot"] .message, [data-testid="bot"] > div, .message-wrap.bot > div, .message.bot, .message-row.bot .message {
|
| 199 |
+
background-color: #ffffff !important;
|
| 200 |
+
color: #1f2937 !important;
|
| 201 |
+
border: 1px solid #e5e7eb !important;
|
| 202 |
+
border-radius: 12px 12px 12px 0 !important;
|
| 203 |
+
padding: 16px 20px !important;
|
| 204 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* ── 피드백 및 전송 버튼 ── */
|
| 208 |
+
.message-buttons, button[aria-label="Good response"], button[aria-label="Bad response"], .like-dislike-area { display: none !important; }
|
| 209 |
+
button[class*="submit-btn"], #submit-btn, button[id*="submit"], button.submit-button {
|
| 210 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #7c3aed 100%) !important;
|
| 211 |
+
color: white !important;
|
| 212 |
+
border-radius: 8px !important;
|
| 213 |
+
font-weight: bold !important;
|
| 214 |
+
border: none !important;
|
| 215 |
+
display: block !important;
|
| 216 |
+
opacity: 1 !important;
|
| 217 |
+
visibility: visible !important;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/* ── 메인 레이아웃 사이드바 균형 최적화 ── */
|
| 221 |
+
.sidebar-container {
|
| 222 |
+
border: 1px solid #e2e8f0 !important;
|
| 223 |
+
border-radius: 12px !important;
|
| 224 |
+
padding: 24px 20px !important;
|
| 225 |
+
background: #ffffff !important;
|
| 226 |
+
height: 700px !important;
|
| 227 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04) !important;
|
| 228 |
+
display: flex !important;
|
| 229 |
+
flex-direction: column !important;
|
| 230 |
+
}
|
| 231 |
+
.news-feed-container {
|
| 232 |
+
flex-grow: 1 !important;
|
| 233 |
+
max-height: 250px !important;
|
| 234 |
+
overflow-y: auto !important;
|
| 235 |
+
}
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
'''
|
| 239 |
+
|
| 240 |
+
final_content = "".join(before_lines) + "\n" + correct_middle + "\n" + "".join(after_lines)
|
| 241 |
+
|
| 242 |
+
with open('app.py', 'w', encoding='utf-8') as f:
|
| 243 |
+
f.write(final_content)
|
| 244 |
+
|
| 245 |
+
print("APP.PY PERFECTLY RECOVERED AND UPDATED!")
|
fix_css3.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
with open('app.py', 'r', encoding='utf-8') as f:
|
| 4 |
+
lines = f.readlines()
|
| 5 |
+
|
| 6 |
+
start_idx = -1
|
| 7 |
+
for i, line in enumerate(lines):
|
| 8 |
+
if 'FINAL_CSS = CUSTOM_CSS + """' in line:
|
| 9 |
+
start_idx = i
|
| 10 |
+
break
|
| 11 |
+
|
| 12 |
+
if start_idx == -1:
|
| 13 |
+
print('Cannot find FINAL_CSS')
|
| 14 |
+
sys.exit(1)
|
| 15 |
+
|
| 16 |
+
clean_css = '''FINAL_CSS = CUSTOM_CSS + """
|
| 17 |
+
/* ── 전체 화면 너비 확대 ── */
|
| 18 |
+
.gradio-container {
|
| 19 |
+
max-width: 1400px !important;
|
| 20 |
+
width: 95% !important;
|
| 21 |
+
margin: 0 auto !important;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* ── 챗봇 컨테이너 테두리 제거 ── */
|
| 25 |
+
div[data-testid="chatbot"], .chatbot-container, .chatbot {
|
| 26 |
+
border: none !important;
|
| 27 |
+
overflow: visible !important;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ── 챗봇 내부 Placeholder(소개글 영역) 상단 짤림 영구 차단 ── */
|
| 31 |
+
.placeholder, [class*="placeholder"] {
|
| 32 |
+
display: flex !important;
|
| 33 |
+
flex-direction: column !important;
|
| 34 |
+
align-items: center !important;
|
| 35 |
+
justify-content: flex-start !important; /* 항상 위에서부터 뿌림 */
|
| 36 |
+
padding-top: 40px !important; /* 상단 여백 넉넉히 주어 짤림 절대 불가 */
|
| 37 |
+
height: 100% !important; /* 전체 높이 사용 */
|
| 38 |
+
min-height: 400px !important;
|
| 39 |
+
flex-grow: 1 !important;
|
| 40 |
+
overflow: visible !important;
|
| 41 |
+
margin: 0 auto !important;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* ── 소개글(Prose) 웰컴 보드 (위쪽 절반) ── */
|
| 45 |
+
.placeholder .prose {
|
| 46 |
+
background: #f8fafc !important; /* 쿨톤 회색 */
|
| 47 |
+
border: 1px solid #e2e8f0 !important;
|
| 48 |
+
border-bottom: none !important; /* 아래쪽 경계선 제거로 결합 준비 */
|
| 49 |
+
border-radius: 12px 12px 0 0 !important; /* 아래쪽 모서리 직각으로 펴서 결합 준비 */
|
| 50 |
+
padding: 24px 24px 10px 24px !important;
|
| 51 |
+
max-width: 800px !important; /* 800px 고정너비 */
|
| 52 |
+
width: 100% !important;
|
| 53 |
+
margin: 0 auto !important;
|
| 54 |
+
display: block !important;
|
| 55 |
+
position: relative !important;
|
| 56 |
+
z-index: 2 !important; /* 아래쪽 컨테이너 위로 살짝 덮게 함 */
|
| 57 |
+
}
|
| 58 |
+
.placeholder h3, [class*="placeholder"] h3 {
|
| 59 |
+
color: #334155 !important;
|
| 60 |
+
font-weight: 800 !important;
|
| 61 |
+
margin-top: 0 !important;
|
| 62 |
+
margin-bottom: 12px !important;
|
| 63 |
+
}
|
| 64 |
+
.placeholder .prose ul {
|
| 65 |
+
list-style-type: none !important;
|
| 66 |
+
padding-left: 0 !important;
|
| 67 |
+
margin-bottom: 12px !important;
|
| 68 |
+
}
|
| 69 |
+
.placeholder .prose li {
|
| 70 |
+
margin-bottom: 4px !important;
|
| 71 |
+
color: #475569 !important;
|
| 72 |
+
font-size: 14px !important;
|
| 73 |
+
line-height: 1.5 !important;
|
| 74 |
+
}
|
| 75 |
+
.placeholder .prose p:last-child {
|
| 76 |
+
font-weight: 700 !important;
|
| 77 |
+
color: #4c1d95 !important;
|
| 78 |
+
background: #f3e8ff !important;
|
| 79 |
+
padding: 8px 16px !important;
|
| 80 |
+
border-radius: 8px !important;
|
| 81 |
+
display: inline-block !important;
|
| 82 |
+
margin-bottom: 0 !important;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* ── 예시 질문 컨테이너 (아래쪽 절반: 보드 병합 완료) ── */
|
| 86 |
+
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 87 |
+
display: grid !important;
|
| 88 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 89 |
+
gap: 12px !important;
|
| 90 |
+
width: 100% !important;
|
| 91 |
+
max-width: 800px !important; /* 소개글과 800px 너비 일치 */
|
| 92 |
+
|
| 93 |
+
/* 🌟 핵심: 위쪽 보드와 완벽 결합을 위한 마이너스 마진 🌟 */
|
| 94 |
+
margin: -32px auto 40px auto !important; /* 32px 갭 제거 (Gradio 기본 갭) */
|
| 95 |
+
|
| 96 |
+
background: #f8fafc !important; /* 소개글과 동일한 회색 배경 */
|
| 97 |
+
border: 1px solid #e2e8f0 !important;
|
| 98 |
+
border-top: none !important; /* 위쪽 경계선 제거 */
|
| 99 |
+
border-radius: 0 0 12px 12px !important; /* 위쪽 모서리 직각으로 펴서 결합 완료 */
|
| 100 |
+
padding: 10px 24px 24px 24px !important;
|
| 101 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 102 |
+
position: relative !important;
|
| 103 |
+
z-index: 1 !important; /* 위쪽 컨테이너 밑으로 겹치게 하여 흰줄 완전 소멸 */
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* 개별 버튼 디자인 (연보라색 테마 & 강력한 중앙 정렬) */
|
| 107 |
+
.examples-container button, div[data-testid="chatbot"] button.example, button.example, .example-btn {
|
| 108 |
+
border-radius: 8px !important;
|
| 109 |
+
padding: 16px !important;
|
| 110 |
+
text-align: center !important; /* 텍스트 가운데 정렬 */
|
| 111 |
+
font-size: 14px !important;
|
| 112 |
+
font-weight: 600 !important;
|
| 113 |
+
color: #4c1d95 !important;
|
| 114 |
+
background: #f5f3ff !important; /* 진짜 연보라색 배경 */
|
| 115 |
+
background-color: #f5f3ff !important; /* 덮어쓰기 철벽 방어 */
|
| 116 |
+
border: 1px solid #e9d5ff !important;
|
| 117 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
| 118 |
+
transition: all 0.2s ease-in-out !important;
|
| 119 |
+
display: flex !important;
|
| 120 |
+
align-items: center !important;
|
| 121 |
+
justify-content: center !important; /* 완벽 중앙 배치 */
|
| 122 |
+
min-height: 80px !important;
|
| 123 |
+
width: 100% !important;
|
| 124 |
+
white-space: normal !important;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.examples-container button:hover, div[data-testid="chatbot"] button.example:hover, button.example:hover, .example-btn:hover {
|
| 128 |
+
border-color: #a855f7 !important;
|
| 129 |
+
background: #f3e8ff !important;
|
| 130 |
+
background-color: #f3e8ff !important;
|
| 131 |
+
transform: translateY(-2px) !important;
|
| 132 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08) !important;
|
| 133 |
+
color: #7c3aed !important;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/* ── 메시지 말풍선 정리 ── */
|
| 137 |
+
.message.user, [data-testid="user"] .message {
|
| 138 |
+
background-color: #111827 !important;
|
| 139 |
+
border-radius: 12px 12px 0 12px !important;
|
| 140 |
+
padding: 7px 13px !important;
|
| 141 |
+
margin: 2px 0 !important;
|
| 142 |
+
border: none !important;
|
| 143 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08) !important;
|
| 144 |
+
min-height: unset !important;
|
| 145 |
+
}
|
| 146 |
+
[data-testid="user"] > div, .bubble-wrap [data-testid="user"], .message-wrap.user > div, .message-row.user {
|
| 147 |
+
background: transparent !important;
|
| 148 |
+
background-color: transparent !important;
|
| 149 |
+
border: none !important;
|
| 150 |
+
}
|
| 151 |
+
[data-testid="user"] .message *, .message.user * {
|
| 152 |
+
color: #ffffff !important;
|
| 153 |
+
line-height: 1.4 !important;
|
| 154 |
+
margin: 0 !important;
|
| 155 |
+
}
|
| 156 |
+
[data-testid="bot"] .message, [data-testid="bot"] > div, .message-wrap.bot > div, .message.bot, .message-row.bot .message {
|
| 157 |
+
background-color: #ffffff !important;
|
| 158 |
+
color: #1f2937 !important;
|
| 159 |
+
border: 1px solid #e5e7eb !important;
|
| 160 |
+
border-radius: 12px 12px 12px 0 !important;
|
| 161 |
+
padding: 16px 20px !important;
|
| 162 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/* ── 피드백 및 전송 버튼 ── */
|
| 166 |
+
.message-buttons, button[aria-label="Good response"], button[aria-label="Bad response"], .like-dislike-area { display: none !important; }
|
| 167 |
+
button[class*="submit-btn"], #submit-btn, button[id*="submit"], button.submit-button {
|
| 168 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #7c3aed 100%) !important;
|
| 169 |
+
color: white !important;
|
| 170 |
+
border-radius: 8px !important;
|
| 171 |
+
font-weight: bold !important;
|
| 172 |
+
border: none !important;
|
| 173 |
+
display: block !important;
|
| 174 |
+
opacity: 1 !important;
|
| 175 |
+
visibility: visible !important;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* ── 메인 레이아웃 사이드바 균형 최적화 ── */
|
| 179 |
+
.sidebar-container {
|
| 180 |
+
border: 1px solid #e2e8f0 !important;
|
| 181 |
+
border-radius: 12px !important;
|
| 182 |
+
padding: 24px 20px !important;
|
| 183 |
+
background: #ffffff !important;
|
| 184 |
+
height: 700px !important;
|
| 185 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04) !important;
|
| 186 |
+
display: flex !important;
|
| 187 |
+
flex-direction: column !important;
|
| 188 |
+
}
|
| 189 |
+
.news-feed-container {
|
| 190 |
+
flex-grow: 1 !important;
|
| 191 |
+
max-height: 250px !important;
|
| 192 |
+
overflow-y: auto !important;
|
| 193 |
+
}
|
| 194 |
+
"""
|
| 195 |
+
'''
|
| 196 |
+
|
| 197 |
+
before_lines = lines[:start_idx]
|
| 198 |
+
final_content = ''.join(before_lines) + clean_css
|
| 199 |
+
|
| 200 |
+
with open('app.py', 'w', encoding='utf-8') as f:
|
| 201 |
+
f.write(final_content)
|
| 202 |
+
|
| 203 |
+
print('Clean CSS recovery done!')
|
src/retrieval/finRetrieval.py
CHANGED
|
@@ -209,9 +209,12 @@ _prompt_template = CustomRagTemplate(
|
|
| 209 |
- **실전 자소서/면접 활용 Tip**: [지원동기나 역량 기술서 작성 시 본인의 역량과 어떻게 연계하여 풀어낼지에 대한 맞춤 가이드]
|
| 210 |
|
| 211 |
|
| 212 |
-
### 📰 4. 근거 뉴스 출처
|
| 213 |
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
---
|
| 217 |
|
|
|
|
| 209 |
- **실전 자소서/면접 활용 Tip**: [지원동기나 역량 기술서 작성 시 본인의 역량과 어떻게 연계하여 풀어낼지에 대한 맞춤 가이드]
|
| 210 |
|
| 211 |
|
| 212 |
+
### 📰 4. 근거 뉴스 출처 (GraphRAG 추천 기사)
|
| 213 |
|
| 214 |
+
> **GraphRAG 추천 관련 뉴스 3선**
|
| 215 |
+
> 1. *[기사 제목 1](기사 URL 1)* - 보도일자/언론사
|
| 216 |
+
> 2. *[기사 제목 2](기사 URL 2)* - 보도일자/언론사
|
| 217 |
+
> 3. *[기사 제목 3](기사 URL 3)* - 보도일자/언론사
|
| 218 |
|
| 219 |
---
|
| 220 |
|
src/utils/ui_templates.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""FinGraph UI Templates and Styling Assets.
|
| 3 |
+
This module houses all custom CSS and HTML templates used by the main Gradio
|
| 4 |
+
application to keep the main app.py clean and readable.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from typing import Any, Dict
|
| 8 |
+
|
| 9 |
+
# ── 남색 팔레트 (Navy Blue) ──────────────────────────────────────
|
| 10 |
+
# 주 텍스트: #1e3a5f / 보조: #3b5a82 / 연한: #6b8ab0
|
| 11 |
+
# 강조(바이올렛): #7c3aed / 포인트 테두리: rgba(124,58,237,0.15)
|
| 12 |
+
|
| 13 |
+
# ── 1. 커스텀 CSS ────────────────────────────────────────────────
|
| 14 |
+
CUSTOM_CSS: str = """
|
| 15 |
+
/* ── Google Fonts 로드 ── */
|
| 16 |
+
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;600&display=swap');
|
| 17 |
+
|
| 18 |
+
/* ── 전체 배경 / 기본 폰트 ── */
|
| 19 |
+
body, .gradio-container {
|
| 20 |
+
background-color: #F6F5FA !important;
|
| 21 |
+
font-family: 'Sora', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
| 22 |
+
color: #1e3a5f !important;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* ── Ambient glow ── */
|
| 26 |
+
.ambient-glow {
|
| 27 |
+
position: fixed;
|
| 28 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 29 |
+
background:
|
| 30 |
+
radial-gradient(circle at 80% 10%, rgba(124,58,237,0.05) 0%, transparent 40%),
|
| 31 |
+
radial-gradient(circle at 20% 90%, rgba(12,217,247,0.05) 0%, transparent 40%);
|
| 32 |
+
z-index: -1;
|
| 33 |
+
pointer-events: none;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ── 사이드바 전체 컨테이너 ── */
|
| 37 |
+
.sidebar-container {
|
| 38 |
+
border: 1px solid #e2e8f0 !important;
|
| 39 |
+
border-radius: 12px !important;
|
| 40 |
+
padding: 20px 18px !important;
|
| 41 |
+
background: #ffffff !important;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* ── 구분선 ── */
|
| 45 |
+
.divider {
|
| 46 |
+
border: none;
|
| 47 |
+
border-top: 1px solid #e2e8f0;
|
| 48 |
+
margin: 14px 0;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* ── 패널 라벨 (섹션 제목) ── */
|
| 52 |
+
.panel-label {
|
| 53 |
+
font-size: 11px;
|
| 54 |
+
font-weight: 700;
|
| 55 |
+
color: #64748b;
|
| 56 |
+
text-transform: uppercase;
|
| 57 |
+
letter-spacing: 0.07em;
|
| 58 |
+
margin-bottom: 10px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* ── 상단 2 카드 (가로 배치) ── */
|
| 62 |
+
.top-cards {
|
| 63 |
+
display: grid;
|
| 64 |
+
grid-template-columns: repeat(2, 1fr);
|
| 65 |
+
gap: 10px;
|
| 66 |
+
margin-bottom: 12px;
|
| 67 |
+
}
|
| 68 |
+
.top-card {
|
| 69 |
+
background: #f8fafc !important;
|
| 70 |
+
border: 1px solid #e2e8f0 !important;
|
| 71 |
+
border-radius: 10px !important;
|
| 72 |
+
padding: 14px 14px 12px !important;
|
| 73 |
+
}
|
| 74 |
+
.top-card-lbl {
|
| 75 |
+
font-size: 11px;
|
| 76 |
+
font-weight: 600;
|
| 77 |
+
color: #64748b;
|
| 78 |
+
margin-bottom: 5px;
|
| 79 |
+
}
|
| 80 |
+
.top-card-val {
|
| 81 |
+
font-size: 22px;
|
| 82 |
+
font-weight: 800;
|
| 83 |
+
color: #1e293b;
|
| 84 |
+
line-height: 1.1;
|
| 85 |
+
}
|
| 86 |
+
.top-card-sub {
|
| 87 |
+
font-size: 10px;
|
| 88 |
+
color: #94a3b8;
|
| 89 |
+
margin-top: 3px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* ── 인사이트 행 ── */
|
| 93 |
+
.insight-row {
|
| 94 |
+
display: grid;
|
| 95 |
+
grid-template-columns: repeat(2, 1fr);
|
| 96 |
+
gap: 10px;
|
| 97 |
+
margin-bottom: 16px;
|
| 98 |
+
}
|
| 99 |
+
.insight-card {
|
| 100 |
+
background: #f0f4ff !important;
|
| 101 |
+
border: 1px solid #c7d2fe !important;
|
| 102 |
+
border-radius: 9px !important;
|
| 103 |
+
padding: 10px 12px !important;
|
| 104 |
+
}
|
| 105 |
+
.insight-lbl {
|
| 106 |
+
font-size: 10px;
|
| 107 |
+
font-weight: 700;
|
| 108 |
+
color: #6366f1;
|
| 109 |
+
text-transform: uppercase;
|
| 110 |
+
letter-spacing: 0.05em;
|
| 111 |
+
margin-bottom: 4px;
|
| 112 |
+
}
|
| 113 |
+
.insight-val {
|
| 114 |
+
font-size: 13px;
|
| 115 |
+
font-weight: 700;
|
| 116 |
+
color: #1e293b;
|
| 117 |
+
white-space: nowrap;
|
| 118 |
+
overflow: hidden;
|
| 119 |
+
text-overflow: ellipsis;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* ── 주요 기술 키워드 배지 (배경색 없음, 연한 보라 텍스트 개편) ── */
|
| 123 |
+
.keyword-container {
|
| 124 |
+
display: flex;
|
| 125 |
+
flex-wrap: wrap;
|
| 126 |
+
gap: 6px;
|
| 127 |
+
margin-bottom: 14px;
|
| 128 |
+
}
|
| 129 |
+
.keyword-badge {
|
| 130 |
+
display: inline-block;
|
| 131 |
+
background: transparent !important; /* 배경색 없음 */
|
| 132 |
+
border: 1px solid #ddd6fe !important; /* 아주 연한 보라 테두리 */
|
| 133 |
+
border-radius: 9999px !important;
|
| 134 |
+
padding: 4px 12px !important;
|
| 135 |
+
font-size: 11px !important;
|
| 136 |
+
font-weight: 600 !important;
|
| 137 |
+
color: #8b5cf6 !important; /* 기업(진한 보라)보다 연하고 밝은 보라 텍스트 */
|
| 138 |
+
}
|
| 139 |
+
.keyword-badge-first {
|
| 140 |
+
background: transparent !important;
|
| 141 |
+
color: #8b5cf6 !important;
|
| 142 |
+
border: 1px solid #ddd6fe !important;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* ── 회사 키워드 배지 (키워드 배지와 동일한 색, 크기, 배경색으로 통일) ── */
|
| 146 |
+
.company-badge-container {
|
| 147 |
+
display: flex;
|
| 148 |
+
flex-wrap: wrap;
|
| 149 |
+
gap: 6px;
|
| 150 |
+
margin-bottom: 14px;
|
| 151 |
+
}
|
| 152 |
+
.company-badge {
|
| 153 |
+
display: inline-block;
|
| 154 |
+
background: transparent !important; /* 배경색 없음 */
|
| 155 |
+
border: 1px solid #ddd6fe !important; /* 아주 연한 보라 테두리 */
|
| 156 |
+
border-radius: 9999px !important;
|
| 157 |
+
padding: 4px 12px !important;
|
| 158 |
+
font-size: 11px !important;
|
| 159 |
+
font-weight: 600 !important;
|
| 160 |
+
color: #8b5cf6 !important; /* 주요 기술 키워드와 동일한 색상 */
|
| 161 |
+
}
|
| 162 |
+
.company-badge-first {
|
| 163 |
+
background: transparent !important;
|
| 164 |
+
color: #8b5cf6 !important;
|
| 165 |
+
border: 1px solid #ddd6fe !important;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* ── 최신 뉴스 피드 ── */
|
| 169 |
+
.news-feed-container {
|
| 170 |
+
max-height: 260px;
|
| 171 |
+
overflow-y: auto;
|
| 172 |
+
}
|
| 173 |
+
.news-feed-container::-webkit-scrollbar { width: 3px; }
|
| 174 |
+
.news-feed-container::-webkit-scrollbar-track { background: transparent; }
|
| 175 |
+
.news-feed-container::-webkit-scrollbar-thumb {
|
| 176 |
+
background: #cbd5e1;
|
| 177 |
+
border-radius: 2px;
|
| 178 |
+
}
|
| 179 |
+
.news-item-link {
|
| 180 |
+
text-decoration: none;
|
| 181 |
+
display: block;
|
| 182 |
+
margin-bottom: 8px;
|
| 183 |
+
}
|
| 184 |
+
.news-item-link:last-child { margin-bottom: 0; }
|
| 185 |
+
.news-item {
|
| 186 |
+
border-left: 3px solid #6366f1 !important;
|
| 187 |
+
padding: 10px 12px !important;
|
| 188 |
+
background: #f8fafc !important;
|
| 189 |
+
border-radius: 0 8px 8px 0 !important;
|
| 190 |
+
transition: all 0.18s ease !important;
|
| 191 |
+
}
|
| 192 |
+
.news-item-link:hover .news-item {
|
| 193 |
+
background: #ffffff !important;
|
| 194 |
+
border-left-color: #06b6d4 !important;
|
| 195 |
+
transform: translateX(3px) !important;
|
| 196 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
|
| 197 |
+
}
|
| 198 |
+
.news-title {
|
| 199 |
+
font-size: 13px !important;
|
| 200 |
+
font-weight: 600 !important;
|
| 201 |
+
color: #1e293b !important;
|
| 202 |
+
line-height: 1.45 !important;
|
| 203 |
+
white-space: normal;
|
| 204 |
+
display: -webkit-box;
|
| 205 |
+
-webkit-line-clamp: 2;
|
| 206 |
+
-webkit-box-orient: vertical;
|
| 207 |
+
overflow: hidden;
|
| 208 |
+
}
|
| 209 |
+
.news-meta {
|
| 210 |
+
font-size: 11px !important;
|
| 211 |
+
color: #94a3b8 !important;
|
| 212 |
+
margin-top: 4px;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* ── 예시 질문 버튼 (2x2 그리드) ── */
|
| 216 |
+
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 217 |
+
display: grid !important;
|
| 218 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 219 |
+
gap: 8px !important;
|
| 220 |
+
margin-top: 12px !important;
|
| 221 |
+
margin-bottom: 8px !important;
|
| 222 |
+
background: transparent !important;
|
| 223 |
+
border: none !important;
|
| 224 |
+
}
|
| 225 |
+
[class*="examples"] button {
|
| 226 |
+
text-align: left !important;
|
| 227 |
+
padding: 11px 13px !important;
|
| 228 |
+
background: rgba(255,255,255,0.70) !important;
|
| 229 |
+
border: 1px solid rgba(124,58,237,0.09) !important;
|
| 230 |
+
border-radius: 9px !important;
|
| 231 |
+
font-size: 12px !important;
|
| 232 |
+
font-weight: 600 !important;
|
| 233 |
+
color: #1e3a5f !important;
|
| 234 |
+
line-height: 1.4 !important;
|
| 235 |
+
box-shadow: none !important;
|
| 236 |
+
transition: all 0.16s ease !important;
|
| 237 |
+
white-space: normal !important;
|
| 238 |
+
height: auto !important;
|
| 239 |
+
min-height: 46px !important;
|
| 240 |
+
cursor: pointer !important;
|
| 241 |
+
}
|
| 242 |
+
[class*="examples"] button:hover {
|
| 243 |
+
background: rgba(255,255,255,0.97) !important;
|
| 244 |
+
border-color: rgba(124,58,237,0.20) !important;
|
| 245 |
+
color: #1e3a5f !important;
|
| 246 |
+
box-shadow: 0 2px 8px rgba(124,58,237,0.06) !important;
|
| 247 |
+
transform: translateY(-1px) !important;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* ── 전송 버튼: 너비 넓고 높이 입력창에 맞춤 ── */
|
| 251 |
+
button[class*="submit-btn"],
|
| 252 |
+
[data-testid="submit-button"],
|
| 253 |
+
#submit-btn {
|
| 254 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #7c3aed 100%) !important;
|
| 255 |
+
color: white !important;
|
| 256 |
+
font-weight: 700 !important;
|
| 257 |
+
font-size: 13px !important;
|
| 258 |
+
border: none !important;
|
| 259 |
+
border-radius: 8px !important;
|
| 260 |
+
box-shadow: 0 2px 10px rgba(30,58,95,0.18) !important;
|
| 261 |
+
transition: all 0.16s ease !important;
|
| 262 |
+
cursor: pointer !important;
|
| 263 |
+
height: 46px !important;
|
| 264 |
+
min-width: 68px !important;
|
| 265 |
+
max-width: 88px !important;
|
| 266 |
+
padding: 0 16px !important;
|
| 267 |
+
display: flex !important;
|
| 268 |
+
align-items: center !important;
|
| 269 |
+
justify-content: center !important;
|
| 270 |
+
box-sizing: border-box !important;
|
| 271 |
+
}
|
| 272 |
+
button[class*="submit-btn"]:hover,
|
| 273 |
+
[data-testid="submit-button"]:hover {
|
| 274 |
+
background: linear-gradient(135deg, #2a4f82 0%, #8b47ff 100%) !important;
|
| 275 |
+
box-shadow: 0 4px 16px rgba(124,58,237,0.24) !important;
|
| 276 |
+
transform: translateY(-1px) !important;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* ── 입력창 ── */
|
| 280 |
+
textarea,
|
| 281 |
+
[class*="input-container"] textarea,
|
| 282 |
+
[data-testid="textbox"] textarea {
|
| 283 |
+
height: 46px !important;
|
| 284 |
+
min-height: 46px !important;
|
| 285 |
+
max-height: 46px !important;
|
| 286 |
+
font-size: 13px !important;
|
| 287 |
+
padding: 14px 14px !important;
|
| 288 |
+
line-height: 1.5 !important;
|
| 289 |
+
border-radius: 8px !important;
|
| 290 |
+
border: 1px solid rgba(30,58,95,0.15) !important;
|
| 291 |
+
background: rgba(255,255,255,0.80) !important;
|
| 292 |
+
color: #1e3a5f !important;
|
| 293 |
+
resize: none !important;
|
| 294 |
+
overflow-y: hidden !important;
|
| 295 |
+
box-sizing: border-box !important;
|
| 296 |
+
}
|
| 297 |
+
textarea:focus {
|
| 298 |
+
border-color: #7c3aed !important;
|
| 299 |
+
background: rgba(255,255,255,0.97) !important;
|
| 300 |
+
box-shadow: 0 0 0 3px rgba(124,58,237,0.09) !important;
|
| 301 |
+
outline: none !important;
|
| 302 |
+
}
|
| 303 |
+
div:has(> button[class*="submit-btn"]),
|
| 304 |
+
div:has(> [data-testid="submit-button"]),
|
| 305 |
+
.input-container, [class*="input-container"] {
|
| 306 |
+
gap: 9px !important;
|
| 307 |
+
align-items: center !important;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* ── 챗봇 컨테이너 ── */
|
| 311 |
+
div[data-testid="chatbot"] {
|
| 312 |
+
background: transparent !important;
|
| 313 |
+
border: 1px solid rgba(30,58,95,0.08) !important;
|
| 314 |
+
border-radius: 12px !important;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* ── 챗봇 탭/라벨 숨김 ── */
|
| 318 |
+
.chatbot > div:first-child, [class*="chatbot"] > div:first-child,
|
| 319 |
+
.chatbot-label, div[class*="chatbot"] .label,
|
| 320 |
+
[data-testid="chatbot"] .label, .chatbot-header,
|
| 321 |
+
.gr-panel-title, .gr-chatbot-label,
|
| 322 |
+
[data-testid="chatbot"] > div:first-child,
|
| 323 |
+
label.svelte-1ipelgc, span.svelte-1ipelgc {
|
| 324 |
+
display: none !important;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/* ── 사용자 버블 ── */
|
| 328 |
+
.message.user {
|
| 329 |
+
background: rgba(30,58,95,0.06) !important;
|
| 330 |
+
border: 1px solid rgba(30,58,95,0.14) !important;
|
| 331 |
+
border-radius: 12px !important;
|
| 332 |
+
}
|
| 333 |
+
.message.user p, .message.user span,
|
| 334 |
+
.message.user li, .message.user div {
|
| 335 |
+
color: #1e3a5f !important;
|
| 336 |
+
font-weight: 600 !important;
|
| 337 |
+
background: transparent !important;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/* ── 봇 버블 ── */
|
| 341 |
+
.message.bot {
|
| 342 |
+
background: rgba(255,255,255,0.65) !important;
|
| 343 |
+
border: 1px solid rgba(30,58,95,0.08) !important;
|
| 344 |
+
border-radius: 12px !important;
|
| 345 |
+
}
|
| 346 |
+
.message.bot p, .message.bot span,
|
| 347 |
+
.message.bot li, .message.bot div {
|
| 348 |
+
color: #1e3a5f !important;
|
| 349 |
+
background: transparent !important;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* ── 메시지 내부 보더 완전 제거 ── */
|
| 353 |
+
.message p, .message li,
|
| 354 |
+
[class*="message"] p, [class*="message"] li {
|
| 355 |
+
line-height: 1.68 !important;
|
| 356 |
+
margin-bottom: 12px !important;
|
| 357 |
+
border: none !important;
|
| 358 |
+
border-left: none !important;
|
| 359 |
+
box-shadow: none !important;
|
| 360 |
+
background: transparent !important;
|
| 361 |
+
color: #1e3a5f !important;
|
| 362 |
+
}
|
| 363 |
+
.message blockquote, [class*="message"] blockquote {
|
| 364 |
+
border: none !important;
|
| 365 |
+
border-left: none !important;
|
| 366 |
+
padding: 0 !important;
|
| 367 |
+
background: transparent !important;
|
| 368 |
+
}
|
| 369 |
+
.message h3, [class*="message"] h3 {
|
| 370 |
+
margin-top: 18px !important;
|
| 371 |
+
margin-bottom: 8px !important;
|
| 372 |
+
font-weight: 800 !important;
|
| 373 |
+
color: #1e3a5f !important;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
/* ── 전역 링크 / CSS 변수 ── */
|
| 377 |
+
:root {
|
| 378 |
+
--color-accent: #7c3aed !important;
|
| 379 |
+
--primary-500: #7c3aed !important;
|
| 380 |
+
--primary-600: #6d28d9 !important;
|
| 381 |
+
}
|
| 382 |
+
a { color: #7c3aed !important; }
|
| 383 |
+
|
| 384 |
+
/* ── secondary 버튼 ── */
|
| 385 |
+
button.secondary, button.lg.secondary, button.sm.secondary,
|
| 386 |
+
button.wrap, button.variant-secondary, .secondary-btn {
|
| 387 |
+
background-color: rgba(255,255,255,0.75) !important;
|
| 388 |
+
color: #1e3a5f !important;
|
| 389 |
+
border: 1px solid rgba(30,58,95,0.13) !important;
|
| 390 |
+
font-weight: 600 !important;
|
| 391 |
+
transition: all 0.16s ease !important;
|
| 392 |
+
}
|
| 393 |
+
button.secondary:hover, button.variant-secondary:hover {
|
| 394 |
+
background-color: rgba(255,255,255,0.97) !important;
|
| 395 |
+
border-color: rgba(124,58,237,0.24) !important;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
/* ── 메인 레이아웃 컬럼 높이 동기화 (챗봇 내부 깨짐 방지) ── */
|
| 399 |
+
#main-row { align-items: stretch !important; }
|
| 400 |
+
#main-row > div[class*="column"] {
|
| 401 |
+
display: flex !important;
|
| 402 |
+
flex-direction: column !important;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
/* ── 왼쪽 패널 너비 오버플로우 완전 차단 ── */
|
| 406 |
+
.sidebar-container * {
|
| 407 |
+
max-width: 100% !important;
|
| 408 |
+
box-sizing: border-box !important;
|
| 409 |
+
word-break: break-word !important;
|
| 410 |
+
overflow-wrap: break-word !important;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
/* ── 엄지 피드백 버튼 숨김 ── */
|
| 414 |
+
.feedback-area,
|
| 415 |
+
[data-testid="like-dislike"],
|
| 416 |
+
.like-dislike-area,
|
| 417 |
+
.message-buttons,
|
| 418 |
+
.chatbot-action-buttons,
|
| 419 |
+
button[aria-label="Good response"],
|
| 420 |
+
button[aria-label="Bad response"],
|
| 421 |
+
button[aria-label="thumbs up"],
|
| 422 |
+
button[aria-label="thumbs down"],
|
| 423 |
+
.bot + div > div > button,
|
| 424 |
+
.svelte-1ed2p3z { display: none !important; }
|
| 425 |
+
"""
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def build_stats_html(stats: Dict[str, Any]) -> str:
|
| 429 |
+
"""왼쪽 사이드바 전체 HTML 생성.
|
| 430 |
+
|
| 431 |
+
구성:
|
| 432 |
+
- 상단 2 카드 (분석 모델 / 기억수)
|
| 433 |
+
- 구분선
|
| 434 |
+
- 회사 키워드 배지 (5개)
|
| 435 |
+
- 구분선
|
| 436 |
+
- 뉴스 키워드 배지
|
| 437 |
+
- 구분선
|
| 438 |
+
- 최신 뉴스 피드
|
| 439 |
+
"""
|
| 440 |
+
# ── 회사 키워드 배지 (5개) ───────────────────────────
|
| 441 |
+
company_html = ""
|
| 442 |
+
for idx, c in enumerate(stats.get("companies_list", [])):
|
| 443 |
+
css = "company-badge company-badge-first" if idx == 0 else "company-badge"
|
| 444 |
+
company_html += f'<span class="{css}"># {c["name"]}</span>\n'
|
| 445 |
+
if not company_html:
|
| 446 |
+
company_html = '<span style="font-size:10px;color:#6b8ab0;">등록된 회사 없음</span>'
|
| 447 |
+
|
| 448 |
+
# ── 뉴스 키워드 배지 ──────────────────────────────
|
| 449 |
+
keyword_html = ""
|
| 450 |
+
for idx, t in enumerate(stats.get("techs_list", [])):
|
| 451 |
+
css = "keyword-badge keyword-badge-first" if idx == 0 else "keyword-badge"
|
| 452 |
+
keyword_html += f'<span class="{css}"># {t["name"]}</span>\n'
|
| 453 |
+
if not keyword_html:
|
| 454 |
+
keyword_html = '<span style="font-size:10px;color:#6b8ab0;">키워드 없음</span>'
|
| 455 |
+
|
| 456 |
+
# ── 최신 뉴스 피드 ──────────────────────────────
|
| 457 |
+
news_html = ""
|
| 458 |
+
for a in stats.get("recent_articles", []):
|
| 459 |
+
title = a["title"] or ""
|
| 460 |
+
url = a["url"] if a["url"] and str(a["url"]).lower() != "nan" else "#"
|
| 461 |
+
target = 'target="_blank"' if url != "#" else ""
|
| 462 |
+
date_str = str(a["date"])[:10] if a["date"] else ""
|
| 463 |
+
news_html += f"""
|
| 464 |
+
<a class="news-item-link" href="{url}" {target}>
|
| 465 |
+
<div class="news-item">
|
| 466 |
+
<div class="news-title">{title}</div>
|
| 467 |
+
<div class="news-meta">{date_str}</div>
|
| 468 |
+
</div>
|
| 469 |
+
</a>"""
|
| 470 |
+
if not news_html:
|
| 471 |
+
news_html = '<div style="font-size:10px;color:#6b8ab0;">기사를 불러오는 중...</div>'
|
| 472 |
+
|
| 473 |
+
return f"""
|
| 474 |
+
<div class="sidebar-container">
|
| 475 |
+
|
| 476 |
+
<!-- 상단 2 카드 -->
|
| 477 |
+
<div class="top-cards">
|
| 478 |
+
<div class="top-card">
|
| 479 |
+
<div class="top-card-lbl">🤖 분석 모델</div>
|
| 480 |
+
<div class="top-card-val">GPT-4o</div>
|
| 481 |
+
<div class="top-card-sub">초거대 AI 엔진</div>
|
| 482 |
+
</div>
|
| 483 |
+
<div class="top-card">
|
| 484 |
+
<div class="top-card-lbl">🏢 대상 기업</div>
|
| 485 |
+
<div class="top-card-val">{stats["companies"]}곳</div>
|
| 486 |
+
<div class="top-card-sub">그래프 내 기업</div>
|
| 487 |
+
</div>
|
| 488 |
+
</div>
|
| 489 |
+
|
| 490 |
+
<hr class="divider">
|
| 491 |
+
|
| 492 |
+
<!-- 회사 키워드 -->
|
| 493 |
+
<div class="panel-label">🏢 주요 대상 기업</div>
|
| 494 |
+
<div class="company-badge-container">
|
| 495 |
+
{company_html}
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
<hr class="divider">
|
| 499 |
+
|
| 500 |
+
<!-- 뉴스 키워드 -->
|
| 501 |
+
<div class="panel-label">🏷️ 주요 기술 키워드</div>
|
| 502 |
+
<div class="keyword-container">
|
| 503 |
+
{keyword_html}
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<hr class="divider">
|
| 507 |
+
|
| 508 |
+
<!-- 최신 뉴스 피드 -->
|
| 509 |
+
<div class="panel-label">📡 최신 뉴스</div>
|
| 510 |
+
<div class="news-feed-container">
|
| 511 |
+
{news_html}
|
| 512 |
+
</div>
|
| 513 |
+
</div>
|
| 514 |
+
"""
|
stitch_screen.html
ADDED
|
File without changes
|