# -*- coding: utf-8 -*-
"""FinGraph UI Templates and Styling Assets.
This module houses all custom CSS and HTML templates used by the main Gradio
application to keep the main app.py clean and readable.
"""
from typing import Any, Dict
# ── 남색 팔레트 (Navy Blue) ──────────────────────────────────────
# 주 텍스트: #1e3a5f / 보조: #3b5a82 / 연한: #6b8ab0
# 강조(바이올렛): #7c3aed / 포인트 테두리: rgba(124,58,237,0.15)
# ── 1. 커스텀 CSS ────────────────────────────────────────────────
CUSTOM_CSS: str = """
/* ── Google Fonts 로드 ── */
@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');
/* ── 전체 배경 / 기본 폰트 / 화면 너비 확대 ── */
body, .gradio-container {
background-color: #F6F5FA !important;
font-family: 'Sora', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
color: #1e3a5f !important;
}
.gradio-container {
max-width: 1400px !important;
width: 95% !important;
margin: 0 auto !important;
}
/* ── Ambient glow ── */
.ambient-glow {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(circle at 80% 10%, rgba(124,58,237,0.05) 0%, transparent 40%),
radial-gradient(circle at 20% 90%, rgba(12,217,247,0.05) 0%, transparent 40%);
z-index: -1;
pointer-events: none;
}
/* ── 사이드바 전체 컨테이너 ── */
.sidebar-container {
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 16px 16px !important;
background: #ffffff !important;
min-height: 700px !important;
height: 100% !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04) !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
gap: 8px !important;
}
/* ── 구분선 ── */
.divider {
border: none;
border-top: 1px solid #e2e8f0;
margin: 6px 0 !important;
}
/* ── 패널 라벨 (섹션 제목) ── */
.panel-label {
font-size: 12.5px !important;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 4px !important;
margin-top: 2px !important;
}
/* ── 상단 2 카드 (가로 배치) ── */
.top-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 4px !important;
}
.top-card {
background: #f8fafc !important;
border: 1px solid #e2e8f0 !important;
border-radius: 10px !important;
padding: 14px 14px 12px !important;
}
.top-card-lbl {
font-size: 11px;
font-weight: 600;
color: #64748b;
margin-bottom: 5px;
}
.top-card-val {
font-size: 22px;
font-weight: 800;
color: #1e293b;
line-height: 1.1;
}
.top-card-sub {
font-size: 10px;
color: #94a3b8;
margin-top: 3px;
}
/* ── 인사이트 행 ── */
.insight-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.insight-card {
background: #f0f4ff !important;
border: 1px solid #c7d2fe !important;
border-radius: 9px !important;
padding: 10px 12px !important;
}
.insight-lbl {
font-size: 10px;
font-weight: 700;
color: #6366f1;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 4px;
}
.insight-val {
font-size: 13px;
font-weight: 700;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── 주요 기술 키워드 배지 (배경색 없음, 연한 보라 텍스트 개편) ── */
.keyword-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 4px !important;
}
.keyword-badge {
display: inline-block;
background: transparent !important;
border: 1px solid #ddd6fe !important;
border-radius: 9999px !important;
padding: 4px 12px !important;
font-size: 12.5px !important;
font-weight: 600 !important;
color: #8b5cf6 !important;
}
.keyword-badge-first {
background: transparent !important;
color: #8b5cf6 !important;
border: 1px solid #ddd6fe !important;
}
/* ── 회사 키워드 배지 (키워드 배지와 동일한 색, 크기, 배경색으로 통일) ── */
.company-badge-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 4px !important;
}
.company-badge {
display: inline-block;
background: transparent !important;
border: 1px solid #ddd6fe !important;
border-radius: 9999px !important;
padding: 4px 12px !important;
font-size: 12.5px !important;
font-weight: 600 !important;
color: #8b5cf6 !important;
}
.company-badge-first {
background: transparent !important;
color: #8b5cf6 !important;
border: 1px solid #ddd6fe !important;
}
/* ── 최신 뉴스 피드 ── */
.news-feed-container {
flex-grow: 1 !important;
max-height: 250px !important;
overflow-y: auto !important;
}
.news-feed-container::-webkit-scrollbar { width: 3px; }
.news-feed-container::-webkit-scrollbar-track { background: transparent; }
.news-feed-container::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 2px;
}
.news-item-link {
text-decoration: none;
display: block;
margin-bottom: 8px;
}
.news-item-link:last-child { margin-bottom: 0; }
.news-item {
border-left: 3px solid #6366f1 !important;
padding: 10px 12px !important;
background: #f8fafc !important;
border-radius: 0 8px 8px 0 !important;
transition: all 0.18s ease !important;
}
.news-item-link:hover .news-item {
background: #ffffff !important;
border-left-color: #06b6d4 !important;
transform: translateX(3px) !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
}
.news-title {
font-size: 13px !important;
font-weight: 600 !important;
color: #1e293b !important;
line-height: 1.45 !important;
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-meta {
font-size: 11px !important;
color: #94a3b8 !important;
margin-top: 4px;
}
/* ── 챗봇 컨테이너 테두리 제거 ── */
div[data-testid="chatbot"], .chatbot-container, .chatbot {
border: none !important;
}
/* ── 챗봇 영역 너비 확대 및 중앙 정렬 ── */
#chat-column {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
width: 100% !important;
}
#chat-column > div,
#chat-column > .form {
width: 96% !important;
max-width: 100% !important;
margin: 0 auto !important;
display: flex !important;
flex-direction: column !important;
gap: 6px !important; /* 컴포넌트 간 위아래 간격 축소 */
}
/* ── 챗봇 내부 Placeholder(소개글 영역) ── */
.placeholder, [class*="placeholder"] {
padding: 0 !important;
overflow: auto !important;
margin: 0 auto !important;
width: 100% !important;
max-width: 100% !important;
}
/* ── 소개글(Prose) 웰컴 보드 (독립적인 프리미엄 라운드 카드 구조) ── */
.placeholder .prose {
background: #f8fafc !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 10px 14px !important; /* 패딩 축소 */
max-width: 100% !important;
width: 100% !important;
margin: 0 auto !important;
display: block !important;
position: relative !important;
z-index: 2 !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
}
.placeholder h3, [class*="placeholder"] h3 {
color: #334155 !important;
font-weight: 800 !important;
font-size: 15px !important; /* 폰트 살짝 축소 */
margin-top: 0 !important;
margin-bottom: 6px !important;
}
.placeholder .prose ul {
list-style-type: none !important;
padding-left: 0 !important;
margin-bottom: 6px !important;
}
.placeholder .prose li {
margin-bottom: 2px !important; /* 간격 축소 */
color: #475569 !important;
font-size: 13px !important;
line-height: 1.4 !important;
}
.placeholder .prose p {
font-size: 13px !important;
line-height: 1.4 !important;
margin-bottom: 4px !important;
}
.placeholder .prose p:last-child {
font-size: 12.5px !important;
font-weight: 700 !important;
color: #4c1d95 !important;
background: #f3e8ff !important;
padding: 4px 10px !important; /* 패딩 축소 */
border-radius: 6px !important;
display: inline-block !important;
margin-bottom: 0 !important;
}
/* ── 예시 질문 컨테이너 (위쪽 보드와 동일 너비로 정렬된 독립 라운드 카드) ── */
[class*="examples"], .gr-samples-wrapper, .examples-container {
display: grid !important;
grid-template-columns: repeat(2, 1fr) !important;
gap: 6px !important; /* 갭 축소 */
width: 100% !important;
max-width: 100% !important;
margin: 4px auto 6px auto !important; /* 마진 축소 */
background: #f8fafc !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 10px 14px !important; /* 패딩 축소 */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
position: relative !important;
z-index: 1 !important;
}
/* 개별 버튼 디자인 (연보라색 테마 & 강력한 중앙 정렬) */
.examples-container button, div[data-testid="chatbot"] button.example, button.example, .example-btn {
border-radius: 8px !important;
padding: 10px 12px !important;
text-align: center !important;
font-size: 13px !important;
font-weight: 600 !important;
color: #4c1d95 !important;
background: #f5f3ff !important;
background-color: #f5f3ff !important;
border: 1px solid #e9d5ff !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
transition: all 0.2s ease-in-out !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
min-height: 52px !important;
width: 100% !important;
white-space: normal !important;
}
.examples-container button:hover, div[data-testid="chatbot"] button.example:hover, button.example:hover, .example-btn:hover {
border-color: #a855f7 !important;
background: #f3e8ff !important;
background-color: #f3e8ff !important;
transform: translateY(-2px) !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08) !important;
color: #7c3aed !important;
}
/* ── 전송 버튼: 너비 넓고 높이 입력창에 맞춤 ── */
button[class*="submit-btn"], #submit-btn, button[id*="submit"], button.submit-button {
background: linear-gradient(135deg, #1e3a5f 0%, #7c3aed 100%) !important;
color: white !important;
font-weight: 700 !important;
font-size: 13px !important;
border: none !important;
border-radius: 8px !important;
box-shadow: 0 2px 10px rgba(30,58,95,0.18) !important;
transition: all 0.16s ease !important;
cursor: pointer !important;
height: 46px !important;
min-width: 68px !important;
max-width: 88px !important;
padding: 0 16px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-sizing: border-box !important;
opacity: 1 !important;
visibility: visible !important;
}
button[class*="submit-btn"]:hover,
[data-testid="submit-button"]:hover {
background: linear-gradient(135deg, #2a4f82 0%, #8b47ff 100%) !important;
box-shadow: 0 4px 16px rgba(124,58,237,0.24) !important;
transform: translateY(-1px) !important;
}
/* ── 입력창 ── */
textarea,
[class*="input-container"] textarea,
[data-testid="textbox"] textarea {
height: 46px !important;
min-height: 46px !important;
max-height: 46px !important;
font-size: 13px !important;
padding: 14px 14px !important;
line-height: 1.5 !important;
border-radius: 8px !important;
border: 1px solid rgba(30,58,95,0.15) !important;
background: rgba(255,255,255,0.80) !important;
color: #1e3a5f !important;
resize: none !important;
overflow-y: hidden !important;
box-sizing: border-box !important;
}
textarea:focus {
border-color: #7c3aed !important;
background: rgba(255,255,255,0.97) !important;
box-shadow: 0 0 0 3px rgba(124,58,237,0.09) !important;
outline: none !important;
}
div:has(> button[class*="submit-btn"]),
div:has(> [data-testid="submit-button"]),
.input-container, [class*="input-container"] {
gap: 6px !important; /* 갭 축소 */
align-items: center !important;
max-width: 100% !important;
width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
margin-top: 4px !important; /* 위아래 간격 최적화 */
}
/* ── 챗봇 탭/라벨 숨김 ── */
.chatbot-label, div[class*="chatbot"] .label,
[data-testid="chatbot"] .label, .chatbot-header,
.gr-panel-title, .gr-chatbot-label,
[data-testid="chatbot"] > label,
div[class*="chatbot"] > label,
label.svelte-1ipelgc, span.svelte-1ipelgc {
display: none !important;
}
/* ── 메시지 버블 기본 크기 축소 (사용자/봇 공통 세로 높이 최적화 & 100% 부모 너비 채움) ── */
.message-wrap, .message-container, div[data-testid="chatbot"] .wrapper {
max-width: 100% !important;
width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.message,
.message-wrap .message,
[data-testid="user-message"],
[data-testid="bot-message"] {
padding: 10px 14px !important;
min-height: auto !important;
}
/* ── 사용자 버블 (다크그레이 프리미엄 테마) ── */
.message.user,
.message.user-message,
[data-testid="user-message"] {
background-color: #111827 !important;
border-radius: 12px 12px 0 12px !important;
padding: 10px 14px !important;
margin: 2px 0 !important;
border: none !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08) !important;
color: #ffffff !important;
}
.message.user *,
.message.user-message *,
[data-testid="user-message"] * {
color: #ffffff !important;
line-height: 1.4 !important;
margin: 0 !important;
background: transparent !important;
}
/* ── 봇 버블 (화이트 & 그레이 경계선 테마) ── */
.message.bot,
.message.bot-message,
[data-testid="bot-message"] {
background-color: #ffffff !important;
color: #1f2937 !important;
border: 1px solid #e5e7eb !important;
border-radius: 12px 12px 12px 0 !important;
padding: 16px 20px !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
}
.message.bot *,
.message.bot-message *,
[data-testid="bot-message"] * {
color: #1e3a5f !important;
background: transparent !important;
}
/* ── 메시지 내부 여백 완전 제어 ── */
.message p, .message li,
[class*="message"] p, [class*="message"] li {
line-height: 1.55 !important;
margin: 0 !important;
margin-bottom: 6px !important;
padding: 0 !important;
border: none !important;
border-left: none !important;
box-shadow: none !important;
background: transparent !important;
color: #1e3a5f !important;
}
.message p:last-child, .message li:last-child {
margin-bottom: 0 !important;
}
.message blockquote, [class*="message"] blockquote {
border: none !important;
border-left: none !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
.message h3, [class*="message"] h3 {
margin-top: 14px !important;
margin-bottom: 6px !important;
font-weight: 800 !important;
color: #1e3a5f !important;
}
/* ── 전역 링크 / CSS 변수 ── */
:root {
--color-accent: #7c3aed !important;
--primary-500: #7c3aed !important;
--primary-600: #6d28d9 !important;
}
a { color: #7c3aed !important; }
/* ── secondary 버튼 ── */
button.secondary, button.lg.secondary, button.sm.secondary,
button.wrap, button.variant-secondary, .secondary-btn {
background-color: rgba(255,255,255,0.75) !important;
color: #1e3a5f !important;
border: 1px solid rgba(30,58,95,0.13) !important;
font-weight: 600 !important;
transition: all 0.16s ease !important;
}
button.secondary:hover, button.variant-secondary:hover {
background-color: rgba(255,255,255,0.97) !important;
border-color: rgba(124,58,237,0.24) !important;
}
/* ── 메인 레이아웃 컬럼 높이 동기화 ── */
#main-row { align-items: stretch !important; }
#main-row > div[class*="column"] {
display: flex !important;
flex-direction: column !important;
}
/* ── 왼쪽 패널 너비 오버플로우 완전 차단 ── */
.sidebar-container * {
max-width: 100% !important;
box-sizing: border-box !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
/* ── 엄지 피드백 버튼 숨김 ── */
.feedback-area,
[data-testid="like-dislike"],
.like-dislike-area,
.message-buttons,
.chatbot-action-buttons,
button[aria-label="Good response"],
button[aria-label="Bad response"],
button[aria-label="thumbs up"],
button[aria-label="thumbs down"],
.bot + div > div > button,
.svelte-1ed2p3z { display: none !important; }
/* ── Gradio 6.x Chatbot 내부 오버플로우 및 스크롤바 최적화 ── */
div[data-testid="chatbot"] .message-wrap,
div[data-testid="chatbot"] .message-container,
div[data-testid="chatbot"] > div.wrapper,
div[data-testid="chatbot"] > div.scrollbar {
overflow-y: auto !important;
max-height: 600px !important;
}
/* ── 모바일 화면 대응: 800px 이하에서는 100% 너비 ── */
@media (max-width: 800px) {
#chat-column > div,
#chat-column > .form {
width: 100% !important;
max-width: 100% !important;
}
[class*="examples"], .gr-samples-wrapper, .examples-container {
grid-template-columns: 1fr !important;
padding: 10px !important;
}
}
"""
def build_stats_html(stats: Dict[str, Any]) -> str:
"""왼쪽 사이드바 전체 HTML 생성.
구성:
- 상단 2 카드 (분석 모델 / 기억수)
- 구분선
- 회사 키워드 배지 (5개)
- 구분선
- 뉴스 키워드 배지
- 구분선
- 최신 뉴스 피드
"""
# ── 회사 키워드 배지 (5개) ───────────────────────────
company_html = ""
for idx, c in enumerate(stats.get("companies_list", [])):
css = "company-badge company-badge-first" if idx == 0 else "company-badge"
company_html += f'# {c["name"]}\n'
if not company_html:
company_html = '등록된 회사 없음'
# ── 뉴스 키워드 배지 ──────────────────────────────
keyword_html = ""
for idx, t in enumerate(stats.get("techs_list", [])):
css = "keyword-badge keyword-badge-first" if idx == 0 else "keyword-badge"
keyword_html += f'# {t["name"]}\n'
if not keyword_html:
keyword_html = '키워드 없음'
# ── 최신 뉴스 피드 ──────────────────────────────
news_html = ""
for a in stats.get("recent_articles", []):
title = a["title"] or ""
url = a["url"] if a["url"] and str(a["url"]).lower() != "nan" else "#"
target = 'target="_blank"' if url != "#" else ""
date_str = str(a["date"])[:10] if a["date"] else ""
news_html += f"""