dev-yuje commited on
Commit
d8c8177
·
1 Parent(s): 8c8e9d7

refactor: gradio codes

Browse files
Files changed (8) hide show
  1. AGENTS.md +14 -0
  2. app.py +95 -42
  3. fix_css.py +187 -0
  4. fix_css2.py +245 -0
  5. fix_css3.py +203 -0
  6. src/retrieval/finRetrieval.py +5 -2
  7. src/utils/ui_templates.py +514 -0
  8. 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
- margin-top: 15px !important;
590
- margin-bottom: 15px !important;
591
- background: transparent !important;
592
- border: none !important;
 
 
 
 
 
 
 
 
 
593
  }
594
  [class*="examples"] button {
595
- text-align: left !important;
596
  padding: 14px 18px !important;
597
- background: #ffffff !important; /* 깨끗하고 단정화이트 */
598
- border: 1px solid #cbd5e1 !important; /* 소프트한 슬레이트 테두리 */
599
  border-radius: 10px !important;
600
  font-size: 13px !important;
601
  font-weight: 600 !important;
602
- color: #0f172a !important; /* 단정한색 */
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: #f8fafc !important; /* 아주 오프화이트 호버 */
619
- border-color: #94a3b8 !important;
620
- color: #0f172a !important;
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: 58px !important;
755
- min-height: 58px !important;
756
- max-height: 58px !important;
757
  font-size: 13px !important;
758
- padding: 18px 16px !important; /* 위아래 패딩을 18px로 대칭 조절하텍스트가 수직 정중앙완벽하배치 */
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: 58px !important; /* 입력창의 height(58px)와 100% 동일하게 일치시켜 완벽한 대칭 구조 달성 */
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=500),
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
- - *[기사 제목](기사 URL)* - 보도일자/언론사
 
 
 
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