dev-yuje commited on
Commit
80ff70e
·
1 Parent(s): d1f6f57

feat: 50/50 대칭형 레이아웃 적용, 2x2 예제 버튼 그리드 구현, 대시보드 통계 및 텍스트 간소화

Browse files
Files changed (1) hide show
  1. app.py +95 -91
app.py CHANGED
@@ -140,8 +140,6 @@ def get_db_stats() -> Dict[str, Any]:
140
  "articles": 0,
141
  "companies": 0,
142
  "technologies": 0,
143
- "relationships": 0,
144
- "companies_list": [],
145
  "techs_list": [],
146
  "recent_articles": [],
147
  }
@@ -149,7 +147,7 @@ def get_db_stats() -> Dict[str, Any]:
149
  from src.retrieval.finRetrieval import get_neo4j_driver
150
  driver = get_neo4j_driver()
151
  with driver.session() as session:
152
- # 1. 각 노드 및 에지 갯수 조회
153
  res_articles = session.run("MATCH (a:Article) RETURN count(a) as cnt").single()
154
  if res_articles:
155
  stats["articles"] = res_articles["cnt"]
@@ -162,17 +160,7 @@ def get_db_stats() -> Dict[str, Any]:
162
  if res_techs:
163
  stats["technologies"] = res_techs["cnt"]
164
 
165
- res_edges = session.run("MATCH ()-[r]->() RETURN count(r) as cnt").single()
166
- if res_edges:
167
- stats["relationships"] = res_edges["cnt"]
168
-
169
- # 2. 기업 및 기술 목록 & 설명 조회 (상위 5개)
170
- res_comp_list = session.run(
171
- "MATCH (c:AICompany) "
172
- "RETURN c.name as name, COALESCE(c.description, '최근 주목받는 AI 핵심 기업') as desc LIMIT 5"
173
- )
174
- stats["companies_list"] = [{"name": r["name"], "desc": r["desc"]} for r in res_comp_list]
175
-
176
  res_tech_list = session.run(
177
  "MATCH (t:AITechnology) "
178
  "RETURN t.name as name, COALESCE(t.description, 'AI 혁신 기술 인프라') as desc LIMIT 5"
@@ -196,19 +184,7 @@ def get_db_stats() -> Dict[str, Any]:
196
 
197
  def build_stats_html(stats: Dict[str, Any]) -> str:
198
  """조회된 지식 그래프 통계 정보들을 바탕으로 미려하고 컴팩트한 대시보드용 HTML을 생성합니다."""
199
- # 1. 기 리스트 HTML 생성
200
- comp_html: str = ""
201
- for c in stats.get("companies_list", []):
202
- comp_html += f"""
203
- <div class="definition-item">
204
- <span class="definition-name">🏢 {c['name']}</span>
205
- <span class="definition-desc">{c['desc']}</span>
206
- </div>
207
- """
208
- if not comp_html:
209
- comp_html = '<div style="font-size:10px; color:#94a3b8;">등록된 기업이 없습니다.</div>'
210
-
211
- # 2. 기술 리스트 HTML 생성
212
  tech_html: str = ""
213
  for t in stats.get("techs_list", []):
214
  tech_html += f"""
@@ -218,9 +194,9 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
218
  </div>
219
  """
220
  if not tech_html:
221
- tech_html = '<div style="font-size:10px; color:#94a3b8;">등록된 기술이 없습니다.</div>'
222
 
223
- # 3. 최근 기사 리스트 HTML 생성 (최대 3개)
224
  news_list_html: str = ""
225
  for a in stats.get("recent_articles", []):
226
  title = a["title"]
@@ -234,7 +210,7 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
234
  </div>
235
  """
236
  if not news_list_html:
237
- news_list_html = '<div style="font-size:10px; color:#94a3b8;">최근 수집된 기사가 없습니다.</div>'
238
 
239
  node_count = stats['companies'] + stats['technologies']
240
 
@@ -243,10 +219,10 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
243
  <!-- Ambient background elements for beautiful glass effects -->
244
  <div class="ambient-glow"></div>
245
 
246
- <div style="font-size: 14px; font-weight: 850; color: #5b5b7f; margin-bottom: 2px; display: flex; align-items: center; gap: 5px; letter-spacing: -0.02em;">
247
  📊 <span>FinGraph AI Terminal</span>
248
  </div>
249
- <p style="font-size: 9px; color: #47464e; margin-top: -2px; margin-bottom: 10px; font-weight: 500;">GraphRAG 실시간 분석 엔진</p>
250
 
251
  <div class="stats-grid">
252
  <div class="stat-card">
@@ -257,25 +233,9 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
257
  <div class="stat-lbl">🧬 지식 노드</div>
258
  <div class="stat-val">{node_count}개</div>
259
  </div>
260
- <div class="stat-card">
261
- <div class="stat-lbl">⛓️ 관계 연결</div>
262
- <div class="stat-val">{stats['relationships']}개</div>
263
- </div>
264
- <div class="stat-card">
265
- <div class="stat-lbl">⚡ 엔진 상태</div>
266
- <div class="stat-val" style="color: #4d6075; display: flex; align-items: center; justify-content: center; gap: 3px;">
267
- <span style="width: 6px; height: 6px; background-color: #5b5b7f; border-radius: 50%; display: inline-block; box-shadow: 0 0 6px #5b5b7f;"></span>
268
- Active
269
- </div>
270
- </div>
271
- </div>
272
-
273
- <div class="section-subtitle">🏢 주요 분석 기업 및 개요</div>
274
- <div class="definition-list">
275
- {comp_html}
276
  </div>
277
 
278
- <div class="section-subtitle">💡 주요 핵심 기술 및 정의</div>
279
  <div class="definition-list">
280
  {tech_html}
281
  </div>
@@ -330,7 +290,7 @@ body {
330
  -webkit-backdrop-filter: blur(24px) !important;
331
  border: 1px solid rgba(196, 195, 236, 0.45) !important;
332
  border-radius: 12px;
333
- padding: 12px;
334
  box-shadow: 0 4px 12px -2px rgba(88, 89, 125, 0.05) !important;
335
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif;
336
  }
@@ -344,23 +304,23 @@ body {
344
  .stats-grid {
345
  display: grid;
346
  grid-template-columns: repeat(2, 1fr);
347
- gap: 6px;
348
- margin-bottom: 10px;
349
  }
350
  .stat-card {
351
  background: rgba(255, 255, 255, 0.7);
352
  border: 1px solid rgba(196, 195, 236, 0.4);
353
- border-radius: 6px;
354
- padding: 5px 6px;
355
  text-align: center;
356
  box-shadow: 0 1px 3px rgba(88, 89, 125, 0.02);
357
  transition: all 0.25s ease-in-out;
358
  }
359
  .stat-card:hover {
360
- transform: translateY(-1px);
361
  background: rgba(255, 255, 255, 0.9);
362
  border-color: rgba(91, 91, 127, 0.6);
363
- box-shadow: 0 4px 8px -2px rgba(88, 89, 125, 0.1);
364
  }
365
  .dark .stat-card {
366
  background: rgba(30, 41, 59, 0.7);
@@ -371,38 +331,38 @@ body {
371
  border-color: rgba(129, 140, 248, 0.5);
372
  }
373
  .stat-val {
374
- font-size: 13px;
375
- font-weight: 800;
376
  color: #5b5b7f; /* 투명 퍼플 에디션 포인트 색상 */
377
- margin-top: 1px;
378
  }
379
  .dark .stat-val {
380
  color: #c4c3ec;
381
  }
382
  .stat-lbl {
383
- font-size: 9px;
384
  color: #47464e;
385
- font-weight: 500;
386
  }
387
  .dark .stat-lbl {
388
  color: #94a3b8;
389
  }
390
 
391
- /* 주요 기업 및 기술 정의 리스트 글래스모피즘 스타일 */
392
  .definition-list {
393
  display: flex;
394
  flex-direction: column;
395
- gap: 4px;
396
- margin-bottom: 8px;
397
  }
398
  .definition-item {
399
- background: rgba(255, 255, 255, 0.5);
400
- border: 1px solid rgba(196, 195, 236, 0.3);
401
- border-radius: 5px;
402
- padding: 4px 6px;
403
  display: flex;
404
  flex-direction: column;
405
- gap: 1px;
406
  box-shadow: 0 1px 2px rgba(88, 89, 125, 0.01);
407
  transition: all 0.2s ease;
408
  }
@@ -419,20 +379,20 @@ body {
419
  border-color: rgba(129, 140, 248, 0.4);
420
  }
421
  .definition-name {
422
- font-size: 10px;
423
  font-weight: 800;
424
  color: #5b5b7f; /* 퍼플 포인트 */
425
  display: flex;
426
  align-items: center;
427
- gap: 3px;
428
  }
429
  .dark .definition-name {
430
  color: #c4c3ec;
431
  }
432
  .definition-desc {
433
- font-size: 9px;
434
  color: #47464e;
435
- line-height: 1.3;
436
  }
437
  .dark .definition-desc {
438
  color: #cbd5e1;
@@ -440,11 +400,11 @@ body {
440
 
441
  /* 최근 뉴스 타임라인 및 스크롤바 스타일 */
442
  .news-feed-container {
443
- max-height: 100px;
444
  overflow-y: auto;
445
  border: 1px solid rgba(196, 195, 236, 0.35);
446
- border-radius: 5px;
447
- padding: 5px;
448
  background: rgba(255, 255, 255, 0.5);
449
  }
450
  .dark .news-feed-container {
@@ -467,20 +427,20 @@ body {
467
  }
468
 
469
  .news-item {
470
- border-left: 2px solid #5b5b7f; /* 퍼플 포인트 */
471
- padding-left: 6px;
472
- margin-bottom: 5px;
473
  position: relative;
474
  }
475
  .news-item:last-child {
476
  margin-bottom: 0;
477
  }
478
  .news-title {
479
- font-size: 10px;
480
  font-weight: 600;
481
  color: #1b1c1a;
482
  text-decoration: none;
483
- line-height: 1.3;
484
  display: block;
485
  white-space: nowrap;
486
  overflow: hidden;
@@ -497,19 +457,19 @@ body {
497
  color: #c4c3ec;
498
  }
499
  .news-meta {
500
- font-size: 8px;
501
  color: #94a3b8;
502
- margin-top: 1px;
503
  }
504
 
505
  /* 서브타이틀 헤더 스타일 */
506
  .section-subtitle {
507
- font-size: 11px;
508
- font-weight: 700;
509
  color: #1b1c1a;
510
- margin: 8px 0 4px 0;
511
  border-bottom: 1px solid rgba(196, 195, 236, 0.35);
512
- padding-bottom: 3px;
513
  display: flex;
514
  align-items: center;
515
  gap: 4px;
@@ -519,6 +479,49 @@ body {
519
  border-color: rgba(129, 140, 248, 0.2);
520
  }
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  /* 챗봇 버튼 퍼플 포인트 스타일 (흰색으로 안 보이던 현상 해결) */
523
  button.primary,
524
  .primary-btn,
@@ -583,6 +586,7 @@ interface_kwargs = {
583
  placeholder="분석하고 싶은 내용을 자연어로 입력해주세요...",
584
  container=False,
585
  scale=7,
 
586
  ),
587
  "title": "FinGraph — GraphRAG AI Terminal",
588
  "description": "> 최신 AI 뉴스를 기반으로 구축된 지식 그래프(GraphRAG)에서 답변합니다.",
@@ -630,14 +634,14 @@ with gr.Blocks(**blocks_kwargs) as demo:
630
  """)
631
 
632
  with gr.Row():
633
- # 2. 왼쪽 컬럼: 사이드바 (대시보드 및 하단 메뉴)
634
- with gr.Column(scale=1, min_width=300):
635
  stats_data = get_db_stats()
636
  stats_html = build_stats_html(stats_data)
637
  gr.HTML(stats_html)
638
 
639
- # 3. 오른쪽 컬럼: 메인 챗봇 에어리어
640
- with gr.Column(scale=3):
641
  # 메인 타이틀 (챗봇 영역 상단 중앙)
642
  gr.HTML("""
643
  <div style="text-align: center; padding: 10px 0 20px 0;">
 
140
  "articles": 0,
141
  "companies": 0,
142
  "technologies": 0,
 
 
143
  "techs_list": [],
144
  "recent_articles": [],
145
  }
 
147
  from src.retrieval.finRetrieval import get_neo4j_driver
148
  driver = get_neo4j_driver()
149
  with driver.session() as session:
150
+ # 1. 각 노드 갯수 조회
151
  res_articles = session.run("MATCH (a:Article) RETURN count(a) as cnt").single()
152
  if res_articles:
153
  stats["articles"] = res_articles["cnt"]
 
160
  if res_techs:
161
  stats["technologies"] = res_techs["cnt"]
162
 
163
+ # 2. 기술 목록 & 설명 조회 (상위 5개)
 
 
 
 
 
 
 
 
 
 
164
  res_tech_list = session.run(
165
  "MATCH (t:AITechnology) "
166
  "RETURN t.name as name, COALESCE(t.description, 'AI 혁신 기술 인프라') as desc LIMIT 5"
 
184
 
185
  def build_stats_html(stats: Dict[str, Any]) -> str:
186
  """조회된 지식 그래프 통계 정보들을 바탕으로 미려하고 컴팩트한 대시보드용 HTML을 생성합니다."""
187
+ # 1. 기 리스트 HTML 생성
 
 
 
 
 
 
 
 
 
 
 
 
188
  tech_html: str = ""
189
  for t in stats.get("techs_list", []):
190
  tech_html += f"""
 
194
  </div>
195
  """
196
  if not tech_html:
197
+ tech_html = '<div style="font-size:12px; color:#94a3b8;">등록된 기술이 없습니다.</div>'
198
 
199
+ # 2. 최근 기사 리스트 HTML 생성 (최대 3개)
200
  news_list_html: str = ""
201
  for a in stats.get("recent_articles", []):
202
  title = a["title"]
 
210
  </div>
211
  """
212
  if not news_list_html:
213
+ news_list_html = '<div style="font-size:12px; color:#94a3b8;">최근 수집된 기사가 없습니다.</div>'
214
 
215
  node_count = stats['companies'] + stats['technologies']
216
 
 
219
  <!-- Ambient background elements for beautiful glass effects -->
220
  <div class="ambient-glow"></div>
221
 
222
+ <div style="font-size: 16px; font-weight: 850; color: #5b5b7f; margin-bottom: 2px; display: flex; align-items: center; gap: 6px; letter-spacing: -0.02em;">
223
  📊 <span>FinGraph AI Terminal</span>
224
  </div>
225
+ <p style="font-size: 11px; color: #47464e; margin-top: -2px; margin-bottom: 12px; font-weight: 500;">GraphRAG 실시간 분석 엔진</p>
226
 
227
  <div class="stats-grid">
228
  <div class="stat-card">
 
233
  <div class="stat-lbl">🧬 지식 노드</div>
234
  <div class="stat-val">{node_count}개</div>
235
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  </div>
237
 
238
+ <div class="section-subtitle">💡 핵심 AI 기술 사전</div>
239
  <div class="definition-list">
240
  {tech_html}
241
  </div>
 
290
  -webkit-backdrop-filter: blur(24px) !important;
291
  border: 1px solid rgba(196, 195, 236, 0.45) !important;
292
  border-radius: 12px;
293
+ padding: 16px;
294
  box-shadow: 0 4px 12px -2px rgba(88, 89, 125, 0.05) !important;
295
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif;
296
  }
 
304
  .stats-grid {
305
  display: grid;
306
  grid-template-columns: repeat(2, 1fr);
307
+ gap: 10px;
308
+ margin-bottom: 15px;
309
  }
310
  .stat-card {
311
  background: rgba(255, 255, 255, 0.7);
312
  border: 1px solid rgba(196, 195, 236, 0.4);
313
+ border-radius: 8px;
314
+ padding: 10px;
315
  text-align: center;
316
  box-shadow: 0 1px 3px rgba(88, 89, 125, 0.02);
317
  transition: all 0.25s ease-in-out;
318
  }
319
  .stat-card:hover {
320
+ transform: translateY(-2px);
321
  background: rgba(255, 255, 255, 0.9);
322
  border-color: rgba(91, 91, 127, 0.6);
323
+ box-shadow: 0 4px 12px -2px rgba(88, 89, 125, 0.1);
324
  }
325
  .dark .stat-card {
326
  background: rgba(30, 41, 59, 0.7);
 
331
  border-color: rgba(129, 140, 248, 0.5);
332
  }
333
  .stat-val {
334
+ font-size: 16px !important;
335
+ font-weight: 850 !important;
336
  color: #5b5b7f; /* 투명 퍼플 에디션 포인트 색상 */
337
+ margin-top: 2px;
338
  }
339
  .dark .stat-val {
340
  color: #c4c3ec;
341
  }
342
  .stat-lbl {
343
+ font-size: 11px !important;
344
  color: #47464e;
345
+ font-weight: 600;
346
  }
347
  .dark .stat-lbl {
348
  color: #94a3b8;
349
  }
350
 
351
+ /* 주요 기술 정의 리스트 글래스모피즘 스타일 */
352
  .definition-list {
353
  display: flex;
354
  flex-direction: column;
355
+ gap: 6px;
356
+ margin-bottom: 12px;
357
  }
358
  .definition-item {
359
+ background: rgba(255, 255, 255, 0.55);
360
+ border: 1px solid rgba(196, 195, 236, 0.35);
361
+ border-radius: 6px;
362
+ padding: 8px 10px;
363
  display: flex;
364
  flex-direction: column;
365
+ gap: 2px;
366
  box-shadow: 0 1px 2px rgba(88, 89, 125, 0.01);
367
  transition: all 0.2s ease;
368
  }
 
379
  border-color: rgba(129, 140, 248, 0.4);
380
  }
381
  .definition-name {
382
+ font-size: 13px !important;
383
  font-weight: 800;
384
  color: #5b5b7f; /* 퍼플 포인트 */
385
  display: flex;
386
  align-items: center;
387
+ gap: 4px;
388
  }
389
  .dark .definition-name {
390
  color: #c4c3ec;
391
  }
392
  .definition-desc {
393
+ font-size: 11px !important;
394
  color: #47464e;
395
+ line-height: 1.4;
396
  }
397
  .dark .definition-desc {
398
  color: #cbd5e1;
 
400
 
401
  /* 최근 뉴스 타임라인 및 스크롤바 스타일 */
402
  .news-feed-container {
403
+ max-height: 140px;
404
  overflow-y: auto;
405
  border: 1px solid rgba(196, 195, 236, 0.35);
406
+ border-radius: 6px;
407
+ padding: 8px;
408
  background: rgba(255, 255, 255, 0.5);
409
  }
410
  .dark .news-feed-container {
 
427
  }
428
 
429
  .news-item {
430
+ border-left: 3px solid #5b5b7f; /* 퍼플 포인트 */
431
+ padding-left: 8px;
432
+ margin-bottom: 8px;
433
  position: relative;
434
  }
435
  .news-item:last-child {
436
  margin-bottom: 0;
437
  }
438
  .news-title {
439
+ font-size: 12px !important;
440
  font-weight: 600;
441
  color: #1b1c1a;
442
  text-decoration: none;
443
+ line-height: 1.4;
444
  display: block;
445
  white-space: nowrap;
446
  overflow: hidden;
 
457
  color: #c4c3ec;
458
  }
459
  .news-meta {
460
+ font-size: 10px !important;
461
  color: #94a3b8;
462
+ margin-top: 2px;
463
  }
464
 
465
  /* 서브타이틀 헤더 스타일 */
466
  .section-subtitle {
467
+ font-size: 13px !important;
468
+ font-weight: 750;
469
  color: #1b1c1a;
470
+ margin: 15px 0 6px 0;
471
  border-bottom: 1px solid rgba(196, 195, 236, 0.35);
472
+ padding-bottom: 4px;
473
  display: flex;
474
  align-items: center;
475
  gap: 4px;
 
479
  border-color: rgba(129, 140, 248, 0.2);
480
  }
481
 
482
+ /* 2x2 grid layout for chatbot example buttons (Stitch Action Grid style) */
483
+ [class*="examples"], .gr-samples-wrapper, .examples-container {
484
+ display: grid !important;
485
+ grid-template-columns: repeat(2, 1fr) !important;
486
+ gap: 10px !important;
487
+ margin-top: 15px !important;
488
+ margin-bottom: 15px !important;
489
+ background: transparent !important;
490
+ border: none !important;
491
+ }
492
+ [class*="examples"] button {
493
+ text-align: left !important;
494
+ padding: 14px 18px !important;
495
+ background: rgba(255, 255, 255, 0.75) !important;
496
+ border: 1px solid rgba(196, 195, 236, 0.5) !important;
497
+ border-radius: 8px !important;
498
+ font-size: 12px !important;
499
+ font-weight: 600 !important;
500
+ color: #47464e !important;
501
+ line-height: 1.4 !important;
502
+ box-shadow: 0 2px 5px rgba(88, 89, 125, 0.03) !important;
503
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
504
+ white-space: normal !important;
505
+ height: auto !important;
506
+ min-height: 54px !important;
507
+ cursor: pointer !important;
508
+ }
509
+ .dark [class*="examples"] button {
510
+ background: rgba(30, 41, 59, 0.75) !important;
511
+ border-color: rgba(129, 140, 248, 0.25) !important;
512
+ color: #cbd5e1 !important;
513
+ }
514
+ [class*="examples"] button:hover {
515
+ transform: translateY(-2px) !important;
516
+ background: rgba(255, 255, 255, 0.95) !important;
517
+ border-color: #5b5b7f !important;
518
+ box-shadow: 0 6px 12px rgba(91, 91, 127, 0.15) !important;
519
+ }
520
+ .dark [class*="examples"] button:hover {
521
+ background: rgba(30, 41, 59, 0.95) !important;
522
+ border-color: rgba(129, 140, 248, 0.6) !important;
523
+ }
524
+
525
  /* 챗봇 버튼 퍼플 포인트 스타일 (흰색으로 안 보이던 현상 해결) */
526
  button.primary,
527
  .primary-btn,
 
586
  placeholder="분석하고 싶은 내용을 자연어로 입력해주세요...",
587
  container=False,
588
  scale=7,
589
+ submit_btn="전송 📤",
590
  ),
591
  "title": "FinGraph — GraphRAG AI Terminal",
592
  "description": "> 최신 AI 뉴스를 기반으로 구축된 지식 그래프(GraphRAG)에서 답변합니다.",
 
634
  """)
635
 
636
  with gr.Row():
637
+ # 2. 왼쪽 컬럼: 사이드바 (대시보드 및 하단 메뉴) - 반반 (50/50) split을 위해 scale=1 설정
638
+ with gr.Column(scale=1, min_width=450):
639
  stats_data = get_db_stats()
640
  stats_html = build_stats_html(stats_data)
641
  gr.HTML(stats_html)
642
 
643
+ # 3. 오른쪽 컬럼: 메인 챗봇 에어리어 - 반반 (50/50) split을 위해 scale=1 설정
644
+ with gr.Column(scale=1, min_width=450):
645
  # 메인 타이틀 (챗봇 영역 상단 중앙)
646
  gr.HTML("""
647
  <div style="text-align: center; padding: 10px 0 20px 0;">