dev-yuje commited on
Commit
a766c96
ยท
1 Parent(s): a7a29d5

fix: resolve navy bubble background bug and add dynamic news source feed

Browse files
Files changed (1) hide show
  1. app.py +197 -69
app.py CHANGED
@@ -58,6 +58,88 @@ def retrieve_node(state: ChatState) -> ChatState:
58
  try:
59
  result = graphrag.search(query_text=state["question"])
60
  context = result.answer # GraphRAG๊ฐ€ ์ด๋ฏธ ๋‹ต๋ณ€์„ ์™„์„ฑํ•˜๋ฏ€๋กœ ๋ฐ”๋กœ ์‚ฌ์šฉ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  except Exception as e:
62
  context = f"[๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {e}]"
63
  return {**state, "context": context}
@@ -283,7 +365,7 @@ except Exception:
283
 
284
  theme_obj = gr.themes.Soft(
285
  font=["Pretendard", "-apple-system", "BlinkMacSystemFont", "system-ui", "sans-serif"],
286
- primary_hue="purple",
287
  secondary_hue="slate",
288
  )
289
 
@@ -291,32 +373,33 @@ custom_css: str = """
291
  body {
292
  background-color: #fbf9f6;
293
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
 
294
  }
295
 
296
- /* Ambient glow point backgrounds */
297
  .ambient-glow {
298
  position: fixed;
299
  top: 0; left: 0; right: 0; bottom: 0;
300
- background: radial-gradient(circle at 85% 15%, rgba(196, 195, 236, 0.35) 0%, transparent 45%),
301
- radial-gradient(circle at 15% 85%, rgba(180, 200, 225, 0.3) 0%, transparent 45%);
302
  z-index: -1;
303
  pointer-events: none;
304
  }
305
 
306
- /* ๋Œ€์‹œ๋ณด๋“œ ํˆฌ๋ช… ํผํ”Œ ๊ธ€๋ž˜์Šค๋ชจํ”ผ์ฆ˜ ์ปจํ…Œ์ด๋„ˆ */
307
  .dashboard-container {
308
- background: rgba(245, 243, 240, 0.45) !important;
309
  backdrop-filter: blur(24px) !important;
310
  -webkit-backdrop-filter: blur(24px) !important;
311
- border: 1px solid rgba(196, 195, 236, 0.45) !important;
312
  border-radius: 12px;
313
  padding: 16px;
314
- box-shadow: 0 4px 12px -2px rgba(88, 89, 125, 0.05) !important;
315
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif;
316
  }
317
  .dark .dashboard-container {
318
  background: rgba(15, 23, 42, 0.55) !important;
319
- border-color: rgba(129, 140, 248, 0.25) !important;
320
  box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.3) !important;
321
  }
322
 
@@ -328,47 +411,47 @@ body {
328
  margin-bottom: 15px;
329
  }
330
  .stat-card {
331
- background: rgba(255, 255, 255, 0.7);
332
- border: 1px solid rgba(196, 195, 236, 0.4);
333
  border-radius: 8px;
334
  padding: 10px;
335
  text-align: center;
336
- box-shadow: 0 1px 3px rgba(88, 89, 125, 0.02);
337
  transition: all 0.25s ease-in-out;
338
  }
339
  .stat-card:hover {
340
  transform: translateY(-2px);
341
- background: rgba(255, 255, 255, 0.9);
342
- border-color: rgba(91, 91, 127, 0.6);
343
- box-shadow: 0 4px 12px -2px rgba(88, 89, 125, 0.1);
344
  }
345
  .dark .stat-card {
346
  background: rgba(30, 41, 59, 0.7);
347
- border-color: rgba(129, 140, 248, 0.2);
348
  color: #f1f5f9;
349
  }
350
  .dark .stat-card:hover {
351
- border-color: rgba(129, 140, 248, 0.5);
352
  }
353
  .stat-val {
354
  font-size: 16px !important;
355
  font-weight: 850 !important;
356
- color: #5b5b7f; /* ํˆฌ๋ช… ํผํ”Œ ์—๋””์…˜ ํฌ์ธํŠธ ์ƒ‰์ƒ */
357
  margin-top: 2px;
358
  }
359
  .dark .stat-val {
360
- color: #c4c3ec;
361
  }
362
  .stat-lbl {
363
  font-size: 11px !important;
364
- color: #47464e;
365
  font-weight: 600;
366
  }
367
  .dark .stat-lbl {
368
  color: #94a3b8;
369
  }
370
 
371
- /* ์ตœ์‹  ๋‰ด์Šค ํ‚ค์›Œ๋“œ ์ปจํ…Œ์ด๋„ˆ ๋ฐ ๋‘ฅ๊ทผ ๋ฐฐ์ง€ ์Šคํƒ€์ผ */
372
  .keyword-container {
373
  display: flex;
374
  flex-wrap: wrap;
@@ -377,38 +460,38 @@ body {
377
  }
378
  .keyword-badge {
379
  display: inline-block;
380
- background: rgba(196, 195, 236, 0.2);
381
- border: 1px solid rgba(196, 195, 236, 0.55);
382
- border-radius: 8px; /* ์ด์ „์ฒ˜๋Ÿผ ์•ฝ๊ฐ„ ๋‘ฅ๊ทผ ๋„ค๋ชจ */
383
  padding: 6px 12px;
384
  font-size: 11px !important;
385
  font-weight: 700;
386
- color: #5b5b7f;
387
- box-shadow: 0 1px 3px rgba(88, 89, 125, 0.02);
388
  transition: all 0.2s ease-in-out;
389
  }
390
  .keyword-badge:hover {
391
- background: rgba(196, 195, 236, 0.35);
392
  transform: scale(1.03);
393
  }
394
  .dark .keyword-badge {
395
- background: rgba(129, 140, 248, 0.12);
396
- border-color: rgba(129, 140, 248, 0.25);
397
- color: #c4c3ec;
398
  }
399
 
400
  /* ์ตœ๊ทผ ๋‰ด์Šค ํ”ผ๋“œ ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์นด๋“œ ๋ ˆ์ด์•„์›ƒ */
401
  .news-feed-container {
402
  max-height: 350px;
403
  overflow-y: auto;
404
- border: 1px solid rgba(196, 195, 236, 0.35);
405
  border-radius: 6px;
406
  padding: 8px;
407
- background: rgba(255, 255, 255, 0.5);
408
  }
409
  .dark .news-feed-container {
410
  background: rgba(30, 41, 59, 0.5);
411
- border-color: rgba(129, 140, 248, 0.15);
412
  }
413
  /* ์Šคํฌ๋กค๋ฐ” ์ปค์Šคํ…€ */
414
  .news-feed-container::-webkit-scrollbar {
@@ -418,11 +501,11 @@ body {
418
  background: transparent;
419
  }
420
  .news-feed-container::-webkit-scrollbar-thumb {
421
- background: rgba(91, 91, 127, 0.3);
422
  border-radius: 2px;
423
  }
424
  .dark .news-feed-container::-webkit-scrollbar-thumb {
425
- background: rgba(196, 195, 236, 0.3);
426
  }
427
 
428
  .news-item-link {
@@ -434,25 +517,25 @@ body {
434
  margin-bottom: 0;
435
  }
436
  .news-item {
437
- border-left: 3px solid #5b5b7f; /* ํผํ”Œ ํฌ์ธํŠธ */
438
  padding: 8px 10px;
439
- background: rgba(255, 255, 255, 0.4);
440
  border-radius: 0 6px 6px 0;
441
  transition: all 0.2s ease-in-out;
442
  cursor: pointer;
443
  }
444
  .news-item-link:hover .news-item {
445
- background: rgba(255, 255, 255, 0.85);
446
- border-left-color: #434466;
447
  transform: translateX(3px);
448
- box-shadow: 0 2px 6px rgba(91, 91, 127, 0.08);
449
  }
450
  .dark .news-item {
451
  background: rgba(30, 41, 59, 0.3);
452
  }
453
  .dark .news-item-link:hover .news-item {
454
  background: rgba(30, 41, 59, 0.65);
455
- border-left-color: rgba(129, 140, 248, 0.6);
456
  }
457
  .news-title {
458
  font-size: 12px !important;
@@ -565,6 +648,7 @@ button[class*="submit-btn"]:hover,
565
  }
566
 
567
  /* secondary ๋ฐ ๊ธฐํƒ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
 
568
  button.secondary,
569
  button.lg.secondary,
570
  button.sm.secondary,
@@ -572,8 +656,8 @@ button.wrap,
572
  button.variant-secondary,
573
  .secondary-btn {
574
  background-color: rgba(255, 255, 255, 0.6) !important;
575
- color: #47464e !important;
576
- border: 1px solid rgba(196, 195, 236, 0.45) !important;
577
  font-weight: 700 !important;
578
  transition: all 0.2s ease-in-out !important;
579
  backdrop-filter: blur(8px);
@@ -583,79 +667,123 @@ button.variant-secondary,
583
  .dark .secondary-btn {
584
  background-color: rgba(30, 41, 59, 0.6) !important;
585
  color: #f1f5f9 !important;
586
- border-color: rgba(129, 140, 248, 0.2) !important;
587
  }
588
  button.secondary:hover,
589
  button.variant-secondary:hover,
590
  .secondary-btn:hover {
591
  background-color: rgba(255, 255, 255, 0.95) !important;
592
- color: #1b1c1a !important;
593
- border-color: rgba(91, 91, 127, 0.5) !important;
594
  }
595
  .dark button.secondary:hover,
596
  .dark button.variant-secondary:hover {
597
  background-color: rgba(30, 41, 59, 0.95) !important;
598
  color: white !important;
599
- border-color: rgba(129, 140, 248, 0.4) !important;
600
  }
601
 
602
- /* ์ฑ—๋ด‡ ๋ณด๋ผ์ƒ‰ ๋ฐฐ๊ฒฝ ์ œ๊ฑฐ ๋ฐ ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๋‹คํฌ ์Šฌ๋ ˆ์ดํŠธ / ํ™”์ดํŠธ ๊ธ€๋ž˜์Šค ๋ฒ„๋ธ” ๊ตฌํ˜„ */
603
  .bubble, .message {
604
  border-radius: 12px !important;
605
  }
606
- .user, .message.user, [data-testid="user"] {
607
- background-color: #334155 !important; /* ๋ณด๋ผ์ƒ‰์„ ๋ฐฐ์ œํ•œ ์ฐจ๋ถ„ํ•˜๊ณ  ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๋‹คํฌ ์Šฌ๋ ˆ์ดํŠธ */
608
- color: white !important;
 
609
  border: 1px solid rgba(51, 65, 85, 0.2) !important;
610
  }
611
- .bot, .message.bot, [data-testid="bot"] {
612
- background-color: rgba(255, 255, 255, 0.75) !important; /* ๋ฐ˜ํˆฌ๋ช… ๊นจ๋—ํ•œ ํ™”์ดํŠธ ๊ธ€๋ž˜์Šค */
613
- color: #1e293b !important;
614
- border: 1px solid rgba(196, 195, 236, 0.45) !important;
 
 
 
 
 
 
 
 
615
  }
616
- .dark .user, .dark .message.user {
617
  background-color: #475569 !important;
618
  }
619
- .dark .bot, .dark .message.bot {
620
- background-color: rgba(30, 41, 59, 0.75) !important;
 
 
 
621
  color: #f1f5f9 !important;
622
- border-color: rgba(129, 140, 248, 0.2) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  }
624
 
625
  /* ์ฑ—๋ด‡ ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ ํˆฌ๋ช…ํ™” ๋ฐ ํ…Œ๋‘๋ฆฌ ๊น”๋”ํ™” */
626
  .chatbot, [class*="chatbot"] {
627
  background: rgba(255, 255, 255, 0.3) !important;
628
- border: 1px solid rgba(196, 195, 236, 0.35) !important;
629
  border-radius: 12px !important;
630
  }
631
  .dark .chatbot {
632
  background: rgba(15, 23, 42, 0.3) !important;
633
- border-color: rgba(129, 140, 248, 0.15) !important;
634
  }
635
 
636
  /* ์ž…๋ ฅ์ฐฝ(ํ…์ŠคํŠธ์—์–ด๋ฆฌ์–ด) ์„ธ๋กœ ๋†’์ด ๋ฐ ํŒจ๋”ฉ ํ™•์žฅ */
637
  textarea,
638
  [class*="input-container"] textarea,
639
  [data-testid="textbox"] textarea {
640
- min-height: 58px !important; /* ๊ธฐ์กด๋ณด๋‹ค ํ›จ์”ฌ ์พŒ์ ํ•˜๊ณ  ์‹œ์›ํ•œ ์„ธ๋กœ ํฌ๊ธฐ */
641
  font-size: 13px !important;
642
  padding: 12px 16px !important;
643
  line-height: 1.5 !important;
644
  border-radius: 8px !important;
645
- border: 1px solid rgba(196, 195, 236, 0.5) !important;
646
  background: rgba(255, 255, 255, 0.8) !important;
 
647
  }
648
  textarea:focus {
649
- border-color: #5b5b7f !important;
650
  background: #ffffff !important;
651
  }
652
  .dark textarea {
653
  background: rgba(30, 41, 59, 0.8) !important;
654
- border-color: rgba(129, 140, 248, 0.25) !important;
655
  color: white !important;
656
  }
657
 
658
- /* ์ฑ—๋ด‡ ๋‹ต๋ณ€ ๋งˆํฌ๋‹ค์šด ๊ฐ€๋…์„ฑ ๋ฐ ์ž๊ฐ„/์ค„๊ฐ„๊ฒฉ ์ตœ์ ํ™” (๊ฐœํ–‰์ด ์‹œ๊ฐ์ ์œผ๋กœ ์‹œ์›ํ•˜๊ฒŒ ๋ณด์ด๋„๋ก ๋งˆ์ง„ ํ™•๋ณด) */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  .message p, .message li, [class*="message"] p, [class*="message"] li {
660
  line-height: 1.68 !important;
661
  margin-bottom: 14px !important;
@@ -713,8 +841,8 @@ with gr.Blocks(**blocks_kwargs) as demo:
713
  # 1. ์ƒ๋‹จ ๊ธ€๋กœ๋ฒŒ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ” (GNB)
714
  gr.HTML("""
715
  <div style="display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid rgba(196, 195, 236, 0.45); background-color: rgba(255, 255, 255, 0.65); backdrop-filter: blur(12px); margin: -20px -20px 20px -20px;">
716
- <div style="font-size: 20px; font-weight: 900; background: linear-gradient(135deg, #0ea5e9 0%, #10b981 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: flex; align-items: center; gap: 12px;">
717
- FinGraph <span style="font-size: 14px; font-weight: 700; color: #475569;">GraphRAG Enhanced AI Terminal</span>
718
  </div>
719
  </div>
720
  """)
@@ -731,7 +859,7 @@ with gr.Blocks(**blocks_kwargs) as demo:
731
  # ๋ฉ”์ธ ํƒ€์ดํ‹€ (์ฑ—๋ด‡ ์˜์—ญ ์ƒ๋‹จ ์ค‘์•™)
732
  gr.HTML("""
733
  <div style="text-align: center; padding: 10px 0 20px 0;">
734
- <h2 style="font-size: 18px; font-weight: 900; background: linear-gradient(135deg, #0ea5e9 0%, #10b981 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 5px;">FinGraph โ€” GraphRAG AI Terminal</h2>
735
  <p style="color: #475569; font-size: 13px; font-weight: 500;">์ตœ์‹  AI ๋‰ด์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•๋œ ์ง€์‹ ๊ทธ๋ž˜ํ”„(GraphRAG)์—์„œ ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค.</p>
736
  </div>
737
  """)
 
58
  try:
59
  result = graphrag.search(query_text=state["question"])
60
  context = result.answer # GraphRAG๊ฐ€ ์ด๋ฏธ ๋‹ต๋ณ€์„ ์™„์„ฑํ•˜๋ฏ€๋กœ ๋ฐ”๋กœ ์‚ฌ์šฉ
61
+
62
+ # ์‹ค์ œ GraphRAG ๊ฒ€์ƒ‰ ์‹œ ์‚ฌ์šฉ๋œ ์ƒ์œ„ 3๊ฐœ ๋‰ด์Šค ํ”ผ๋“œ ๋™์  ์ถ”์ถœ ๋ฐ ํฌ๋งทํŒ…
63
+ sources = []
64
+ seen_urls = set()
65
+ if hasattr(result, "retriever_result") and result.retriever_result and hasattr(result.retriever_result, "items"):
66
+ for item in result.retriever_result.items:
67
+ meta = getattr(item, "metadata", {})
68
+ title = meta.get("article_title")
69
+ url = meta.get("article_url")
70
+ date = meta.get("article_date")
71
+ if title and url and url not in seen_urls:
72
+ seen_urls.add(url)
73
+ # date ํ˜•์‹ ํฌ๋งทํŒ… (์˜ˆ: 2026-05-19T00:00:00Z -> 2026-05-19)
74
+ if date and "T" in str(date):
75
+ date = str(date).split("T")[0]
76
+ sources.append({"title": title, "url": url, "date": date})
77
+ if len(sources) >= 3:
78
+ break
79
+
80
+ # ๋งŒ์•ฝ retriever_result์—์„œ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ, Neo4j DB์—์„œ ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜์œผ๋กœ ์ง์ ‘ ๊ด€๋ จ ๋‰ด์Šค 3๊ฐœ ๋ฐฑ์—… ์กฐํšŒ
81
+ if not sources:
82
+ try:
83
+ from src.retrieval.finRetrieval import get_neo4j_driver
84
+ driver = get_neo4j_driver()
85
+ # ๋‹จ์ˆœ ํ‚ค์›Œ๋“œ ๋งค์นญ ์ฟผ๋ฆฌ
86
+ query_words = [w for w in state["question"].split() if len(w) > 1]
87
+ conditions = []
88
+ for w in query_words[:3]:
89
+ conditions.append(f"a.title CONTAINS '{w}' OR a.description CONTAINS '{w}'")
90
+
91
+ with driver.session() as session:
92
+ cypher = "MATCH (a:Article) "
93
+ if conditions:
94
+ cypher += "WHERE " + " OR ".join(conditions) + " "
95
+ cypher += "RETURN a.title as title, a.url as url, a.published_date as date ORDER BY a.published_date DESC LIMIT 3"
96
+
97
+ res_backup = session.run(cypher)
98
+ for r in res_backup:
99
+ title = r["title"]
100
+ url = r["url"]
101
+ date = r["date"]
102
+ if title and url and url not in seen_urls:
103
+ seen_urls.add(url)
104
+ if date and "T" in str(date):
105
+ date = str(date).split("T")[0]
106
+ sources.append({"title": title, "url": url, "date": date})
107
+ except Exception:
108
+ pass
109
+
110
+ # ๋งŒ์•ฝ ์—ฌ์ „ํžˆ ๋น„์–ด์žˆ๋‹ค๋ฉด, ์ตœ์‹  ๋‰ด์Šค 3๊ฐœ ๋…ธ์ถœ (์ƒ์ƒํ•ด ๋‚ธ ๊ฐ€์งœ ์ •๋ณด ๋ฐฉ์ง€)
111
+ if not sources:
112
+ try:
113
+ from src.retrieval.finRetrieval import get_neo4j_driver
114
+ driver = get_neo4j_driver()
115
+ with driver.session() as session:
116
+ res_latest = session.run(
117
+ "MATCH (a:Article) RETURN a.title as title, a.url as url, a.published_date as date "
118
+ "ORDER BY a.published_date DESC LIMIT 3"
119
+ )
120
+ for r in res_latest:
121
+ title = r["title"]
122
+ url = r["url"]
123
+ date = r["date"]
124
+ if title and url and url not in seen_urls:
125
+ seen_urls.add(url)
126
+ if date and "T" in str(date):
127
+ date = str(date).split("T")[0]
128
+ sources.append({"title": title, "url": url, "date": date})
129
+ except Exception:
130
+ pass
131
+
132
+ # ๋‹ต๋ณ€ ๋์— ๐Ÿ“ฐ ๊ด€๋ จ ๋‰ด์Šค ํ”ผ๋“œ ํŒŒํŠธ ์ •์„ฑ์Šค๋Ÿฝ๊ฒŒ ๋ง๋ถ™์ด๊ธฐ
133
+ if sources:
134
+ news_feed = "\n\n๐Ÿ“ฐ **๊ด€๋ จ ๋‰ด์Šค ํ”ผ๋“œ (์‹ค์‹œ๊ฐ„ ๋ถ„์„ ์ถœ์ฒ˜)**\n"
135
+ for s in sources:
136
+ date_str = f" ({s['date']})" if s['date'] else ""
137
+ news_feed += f"- ๐Ÿ”— [{s['title']}]({s['url']}){date_str}\n"
138
+
139
+ # ์ค‘๋ณต์œผ๋กœ ๊ด€๋ จ ๋‰ด์Šค ํ”ผ๋“œ๊ฐ€ ๋ถ™์ง€ ์•Š๋„๋ก ๋ฐฉ์ง€
140
+ if "๊ด€๋ จ ๋‰ด์Šค ํ”ผ๋“œ" not in context:
141
+ context += news_feed
142
+
143
  except Exception as e:
144
  context = f"[๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {e}]"
145
  return {**state, "context": context}
 
365
 
366
  theme_obj = gr.themes.Soft(
367
  font=["Pretendard", "-apple-system", "BlinkMacSystemFont", "system-ui", "sans-serif"],
368
+ primary_hue="sky",
369
  secondary_hue="slate",
370
  )
371
 
 
373
  body {
374
  background-color: #fbf9f6;
375
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
376
+ color: #0f172a !important; /* ๊ธฐ๋ณธ ๊ฒ€์ •์ƒ‰ */
377
  }
378
 
379
+ /* Ambient glow point backgrounds (๋ณด๋ผ์ƒ‰ ์›์ฒœ ๋ฐฐ์ œ, ์€์€ํ•œ ์Šค์นด์ด๋ธ”๋ฃจ์™€ ํ…Œ์ผ๊ทธ๋ฆฐ ํ†ค) */
380
  .ambient-glow {
381
  position: fixed;
382
  top: 0; left: 0; right: 0; bottom: 0;
383
+ background: radial-gradient(circle at 85% 15%, rgba(14, 165, 233, 0.06) 0%, transparent 45%),
384
+ radial-gradient(circle at 15% 85%, rgba(20, 184, 166, 0.06) 0%, transparent 45%);
385
  z-index: -1;
386
  pointer-events: none;
387
  }
388
 
389
+ /* ๋Œ€์‹œ๋ณด๋“œ ํˆฌ๋ช… ๊ธ€๋ž˜์Šค๋ชจํ”ผ์ฆ˜ ์ปจํ…Œ์ด๋„ˆ */
390
  .dashboard-container {
391
+ background: rgba(255, 255, 255, 0.8) !important;
392
  backdrop-filter: blur(24px) !important;
393
  -webkit-backdrop-filter: blur(24px) !important;
394
+ border: 1px solid #cbd5e1 !important; /* ๊น”๋”ํ•œ ๋‰ดํŠธ๋Ÿด ์Šฌ๋ ˆ์ดํŠธ ํ…Œ๋‘๋ฆฌ */
395
  border-radius: 12px;
396
  padding: 16px;
397
+ box-shadow: 0 4px 12px -2px rgba(15, 23, 42, 0.03) !important;
398
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif;
399
  }
400
  .dark .dashboard-container {
401
  background: rgba(15, 23, 42, 0.55) !important;
402
+ border-color: rgba(14, 165, 233, 0.25) !important;
403
  box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.3) !important;
404
  }
405
 
 
411
  margin-bottom: 15px;
412
  }
413
  .stat-card {
414
+ background: rgba(255, 255, 255, 0.95);
415
+ border: 1px solid #cbd5e1;
416
  border-radius: 8px;
417
  padding: 10px;
418
  text-align: center;
419
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.01);
420
  transition: all 0.25s ease-in-out;
421
  }
422
  .stat-card:hover {
423
  transform: translateY(-2px);
424
+ background: rgba(255, 255, 255, 1);
425
+ border-color: #0ea5e9; /* ํ˜ธ๋ฒ„ ์‹œ ์Šค์นด์ด ๋ธ”๋ฃจ */
426
+ box-shadow: 0 4px 12px -2px rgba(14, 165, 233, 0.1);
427
  }
428
  .dark .stat-card {
429
  background: rgba(30, 41, 59, 0.7);
430
+ border-color: rgba(14, 165, 233, 0.2);
431
  color: #f1f5f9;
432
  }
433
  .dark .stat-card:hover {
434
+ border-color: #38bdf8;
435
  }
436
  .stat-val {
437
  font-size: 16px !important;
438
  font-weight: 850 !important;
439
+ color: #0f172a !important; /* ํ™•์‹คํ•œ ๊ณ ๋Œ€๋น„ ๊ฒ€์ •์ƒ‰ ๊ธ€์”จ */
440
  margin-top: 2px;
441
  }
442
  .dark .stat-val {
443
+ color: #f8fafc !important;
444
  }
445
  .stat-lbl {
446
  font-size: 11px !important;
447
+ color: #334155;
448
  font-weight: 600;
449
  }
450
  .dark .stat-lbl {
451
  color: #94a3b8;
452
  }
453
 
454
+ /* ์ตœ์‹  ๋‰ด์Šค ํ‚ค์›Œ๋“œ ์ปจํ…Œ์ด๋„ˆ ๋ฐ ๋‘ฅ๊ทผ ๋ฐฐ์ง€ ์Šคํƒ€์ผ (๋ณด๋ผ์ƒ‰ ๋ฐฐ์ œ, ์Šฌ๋ ˆ์ดํŠธ ๋ฐ ๊ฒ€์ •์ƒ‰ ๊ธ€์”จ) */
455
  .keyword-container {
456
  display: flex;
457
  flex-wrap: wrap;
 
460
  }
461
  .keyword-badge {
462
  display: inline-block;
463
+ background: #f1f5f9 !important; /* ์—ฐํ•œ ๋‰ดํŠธ๋Ÿด ์Šฌ๋ ˆ์ดํŠธ */
464
+ border: 1px solid #cbd5e1 !important; /* ์Šฌ๋ ˆ์ดํŠธ ํ…Œ๋‘๋ฆฌ */
465
+ border-radius: 8px !important;
466
  padding: 6px 12px;
467
  font-size: 11px !important;
468
  font-weight: 700;
469
+ color: #0f172a !important; /* ํ™•์‹คํ•œ ๊ฒ€์ •์ƒ‰ */
470
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02) !important;
471
  transition: all 0.2s ease-in-out;
472
  }
473
  .keyword-badge:hover {
474
+ background: #e2e8f0 !important;
475
  transform: scale(1.03);
476
  }
477
  .dark .keyword-badge {
478
+ background: rgba(15, 23, 42, 0.4) !important;
479
+ border-color: rgba(14, 165, 233, 0.25) !important;
480
+ color: #cbd5e1 !important;
481
  }
482
 
483
  /* ์ตœ๊ทผ ๋‰ด์Šค ํ”ผ๋“œ ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์นด๋“œ ๋ ˆ์ด์•„์›ƒ */
484
  .news-feed-container {
485
  max-height: 350px;
486
  overflow-y: auto;
487
+ border: 1px solid #cbd5e1;
488
  border-radius: 6px;
489
  padding: 8px;
490
+ background: rgba(255, 255, 255, 0.7);
491
  }
492
  .dark .news-feed-container {
493
  background: rgba(30, 41, 59, 0.5);
494
+ border-color: rgba(14, 165, 233, 0.15);
495
  }
496
  /* ์Šคํฌ๋กค๋ฐ” ์ปค์Šคํ…€ */
497
  .news-feed-container::-webkit-scrollbar {
 
501
  background: transparent;
502
  }
503
  .news-feed-container::-webkit-scrollbar-thumb {
504
+ background: rgba(148, 163, 184, 0.4);
505
  border-radius: 2px;
506
  }
507
  .dark .news-feed-container::-webkit-scrollbar-thumb {
508
+ background: rgba(148, 163, 184, 0.2);
509
  }
510
 
511
  .news-item-link {
 
517
  margin-bottom: 0;
518
  }
519
  .news-item {
520
+ border-left: 3px solid #0ea5e9; /* ํŒŒ๋ž€์ƒ‰ ์˜ค์…˜ ๋ธ”๋ฃจ ํฌ์ธํŠธ */
521
  padding: 8px 10px;
522
+ background: rgba(255, 255, 255, 0.8);
523
  border-radius: 0 6px 6px 0;
524
  transition: all 0.2s ease-in-out;
525
  cursor: pointer;
526
  }
527
  .news-item-link:hover .news-item {
528
+ background: rgba(255, 255, 255, 1);
529
+ border-left-color: #0284c7;
530
  transform: translateX(3px);
531
+ box-shadow: 0 2px 6px rgba(14, 165, 233, 0.08);
532
  }
533
  .dark .news-item {
534
  background: rgba(30, 41, 59, 0.3);
535
  }
536
  .dark .news-item-link:hover .news-item {
537
  background: rgba(30, 41, 59, 0.65);
538
+ border-left-color: #38bdf8;
539
  }
540
  .news-title {
541
  font-size: 12px !important;
 
648
  }
649
 
650
  /* secondary ๋ฐ ๊ธฐํƒ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
651
+ /* secondary ๋ฐ ๊ธฐํƒ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฒ„ํŠผ ์Šคํƒ€์ผ (๋ณด๋ผ์ƒ‰ ์ œ๊ฑฐ) */
652
  button.secondary,
653
  button.lg.secondary,
654
  button.sm.secondary,
 
656
  button.variant-secondary,
657
  .secondary-btn {
658
  background-color: rgba(255, 255, 255, 0.6) !important;
659
+ color: #0f172a !important; /* ๊ธฐ๋ณธ ๊ฒ€์ •์ƒ‰ */
660
+ border: 1px solid #cbd5e1 !important;
661
  font-weight: 700 !important;
662
  transition: all 0.2s ease-in-out !important;
663
  backdrop-filter: blur(8px);
 
667
  .dark .secondary-btn {
668
  background-color: rgba(30, 41, 59, 0.6) !important;
669
  color: #f1f5f9 !important;
670
+ border-color: rgba(14, 165, 233, 0.2) !important;
671
  }
672
  button.secondary:hover,
673
  button.variant-secondary:hover,
674
  .secondary-btn:hover {
675
  background-color: rgba(255, 255, 255, 0.95) !important;
676
+ color: #0f172a !important;
677
+ border-color: #94a3b8 !important;
678
  }
679
  .dark button.secondary:hover,
680
  .dark button.variant-secondary:hover {
681
  background-color: rgba(30, 41, 59, 0.95) !important;
682
  color: white !important;
683
+ border-color: #38bdf8 !important;
684
  }
685
 
686
+ /* ์ฑ—๋ด‡ ๋ณด๋ผ์ƒ‰ ๋ฐฐ๊ฒฝ ์™„์ „ ์ œ๊ฑฐ ๋ฐ ๊ณ ๋Œ€๋น„ ์Šฌ๋ ˆ์ดํŠธ/ํ™”์ดํŠธ ๋ฒ„๋ธ” ๊ตฌํ˜„ */
687
  .bubble, .message {
688
  border-radius: 12px !important;
689
  }
690
+
691
+ /* ์‚ฌ์šฉ์ž ๋ฒ„๋ธ” ๊ฐ€๋…์„ฑ ์™„์ „ ๊ฐœ์„  (๊ธ€์”จ์ƒ‰์„ ๊ฐ•์ œ๋กœ ๊นจ๋—ํ•œ ํฐ์ƒ‰์œผ๋กœ ๊ณ ์ •ํ•˜์—ฌ 500% ์„ ๋ช…ํ•˜๊ฒŒ ํ‘œ์‹œ) */
692
+ .message.user {
693
+ background-color: #334155 !important; /* ์ฐจ๋ถ„ํ•˜๊ณ  ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๋‹คํฌ ์Šฌ๋ ˆ์ดํŠธ */
694
  border: 1px solid rgba(51, 65, 85, 0.2) !important;
695
  }
696
+ .message.user p, .message.user span, .message.user li, .message.user div {
697
+ color: #ffffff !important; /* ์™„์ „ ํฐ์ƒ‰ ๊ธ€์”จ */
698
+ font-weight: 600 !important;
699
+ }
700
+
701
+ /* ๋ด‡ ๋ฒ„๋ธ” ๊ฐ€๋…์„ฑ ์™„์ „ ๊ฐœ์„  (๊ธ€์”จ์ƒ‰ ํ™•์‹คํ•œ ๊ฒ€์ •์ƒ‰, ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ ์ œ๊ฑฐ) */
702
+ .message.bot {
703
+ background-color: rgba(255, 255, 255, 0.95) !important; /* ๋ฐ˜ํˆฌ๋ช… ๊นจ๋—ํ•œ ํ™”์ดํŠธ ๊ธ€๋ž˜์Šค */
704
+ border: 1px solid #cbd5e1 !important;
705
+ }
706
+ .message.bot p, .message.bot span, .message.bot li, .message.bot div {
707
+ color: #0f172a !important; /* ํ™•์‹คํ•œ ๊ณ ๋Œ€๋น„ ๊ฒ€์ •์ƒ‰ ๊ธ€์”จ */
708
  }
709
+ .dark .message.user {
710
  background-color: #475569 !important;
711
  }
712
+ .dark .message.bot {
713
+ background-color: rgba(30, 41, 59, 0.85) !important;
714
+ border-color: rgba(14, 165, 233, 0.2) !important;
715
+ }
716
+ .dark .message.bot p, .dark .message.bot span, .dark .message.bot li {
717
  color: #f1f5f9 !important;
718
+ }
719
+
720
+ /* Chatbot ๋ผ๋ฒจ/ํƒญ์˜ ๋ณด๋ผ์ƒ‰ ์ƒ‰๊ฐ ์™„์ „ ์ œ๊ฑฐ ๋ฐ ์„ธ๋กœํ˜• ์˜คํ”„ํ™”์ดํŠธ ํ†ค ์ „ํ™˜ */
721
+ .chatbot > div:first-child,
722
+ [class*="chatbot"] > div:first-child,
723
+ .chatbot-label,
724
+ div[class*="chatbot"] .label,
725
+ [data-testid="chatbot"] .label,
726
+ .chatbot-header,
727
+ div[class*="chatbot"] > div:first-child span,
728
+ .gr-panel-title,
729
+ .gr-chatbot-label {
730
+ background-color: #f1f5f9 !important; /* ์ฐจ๋ถ„ํ•˜๊ณ  ์‹œ์›ํ•œ ๊ทธ๋ ˆ์ด/์Šฌ๋ ˆ์ดํŠธ */
731
+ color: #0f172a !important; /* ๊ฒ€์ •์ƒ‰ ๊ธ€์”จ */
732
+ border: 1px solid #cbd5e1 !important;
733
+ font-weight: 800 !important;
734
+ border-radius: 6px !important;
735
  }
736
 
737
  /* ์ฑ—๋ด‡ ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ ํˆฌ๋ช…ํ™” ๋ฐ ํ…Œ๋‘๋ฆฌ ๊น”๋”ํ™” */
738
  .chatbot, [class*="chatbot"] {
739
  background: rgba(255, 255, 255, 0.3) !important;
740
+ border: 1px solid #cbd5e1 !important;
741
  border-radius: 12px !important;
742
  }
743
  .dark .chatbot {
744
  background: rgba(15, 23, 42, 0.3) !important;
745
+ border-color: rgba(14, 165, 233, 0.15) !important;
746
  }
747
 
748
  /* ์ž…๋ ฅ์ฐฝ(ํ…์ŠคํŠธ์—์–ด๋ฆฌ์–ด) ์„ธ๋กœ ๋†’์ด ๋ฐ ํŒจ๋”ฉ ํ™•์žฅ */
749
  textarea,
750
  [class*="input-container"] textarea,
751
  [data-testid="textbox"] textarea {
752
+ min-height: 58px !important;
753
  font-size: 13px !important;
754
  padding: 12px 16px !important;
755
  line-height: 1.5 !important;
756
  border-radius: 8px !important;
757
+ border: 1px solid #cbd5e1 !important;
758
  background: rgba(255, 255, 255, 0.8) !important;
759
+ color: #0f172a !important; /* ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ฒ€์ •์ƒ‰ */
760
  }
761
  textarea:focus {
762
+ border-color: #0ea5e9 !important; /* ํฌ์ปค์Šค ์‹œ ์Šค์นด์ด ๋ธ”๋ฃจ */
763
  background: #ffffff !important;
764
  }
765
  .dark textarea {
766
  background: rgba(30, 41, 59, 0.8) !important;
767
+ border-color: rgba(14, 165, 233, 0.25) !important;
768
  color: white !important;
769
  }
770
 
771
+ /* ์ฑ—๋ด‡ ์ž…๋ ฅ์ฐฝ๊ณผ ์ „์†ก ๋ฒ„ํŠผ ์—ฌ๋ฐฑ ๋ถ„๋ฆฌ */
772
+ button[class*="submit-btn"],
773
+ [data-testid="submit-button"],
774
+ #submit-btn {
775
+ margin-left: 12px !important;
776
+ border-radius: 8px !important;
777
+ min-width: 95px !important;
778
+ }
779
+ div:has(> button[class*="submit-btn"]),
780
+ div:has(> [data-testid="submit-button"]),
781
+ .input-container,
782
+ [class*="input-container"] {
783
+ gap: 12px !important;
784
+ }
785
+
786
+ /* ์ฑ—๋ด‡ ๋‹ต๋ณ€ ๋งˆํฌ๋‹ค์šด ๊ฐ€๋…์„ฑ ๋ฐ ์ž๊ฐ„/์ค„๊ฐ„๊ฒฉ ์ตœ์ ํ™” */
787
  .message p, .message li, [class*="message"] p, [class*="message"] li {
788
  line-height: 1.68 !important;
789
  margin-bottom: 14px !important;
 
841
  # 1. ์ƒ๋‹จ ๊ธ€๋กœ๋ฒŒ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ” (GNB)
842
  gr.HTML("""
843
  <div style="display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid rgba(196, 195, 236, 0.45); background-color: rgba(255, 255, 255, 0.65); backdrop-filter: blur(12px); margin: -20px -20px 20px -20px;">
844
+ <div style="font-size: 20px; font-weight: 900; color: #0f172a; display: flex; align-items: center; gap: 12px;">
845
+ ๐Ÿ“ˆ FinGraph <span style="font-size: 14px; font-weight: 700; color: #475569;">GraphRAG Enhanced AI Terminal</span>
846
  </div>
847
  </div>
848
  """)
 
859
  # ๋ฉ”์ธ ํƒ€์ดํ‹€ (์ฑ—๋ด‡ ์˜์—ญ ์ƒ๋‹จ ์ค‘์•™)
860
  gr.HTML("""
861
  <div style="text-align: center; padding: 10px 0 20px 0;">
862
+ <h2 style="font-size: 18px; font-weight: 900; color: #0f172a; margin-bottom: 5px; display: flex; align-items: center; justify-content: center; gap: 8px;">๐Ÿงฌ FinGraph โ€” GraphRAG AI Terminal</h2>
863
  <p style="color: #475569; font-size: 13px; font-weight: 500;">์ตœ์‹  AI ๋‰ด์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•๋œ ์ง€์‹ ๊ทธ๋ž˜ํ”„(GraphRAG)์—์„œ ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค.</p>
864
  </div>
865
  """)