Spaces:
Sleeping
Sleeping
| """DartLab Streamlit Demo β AI μ±ν κΈ°λ° κΈ°μ λΆμ.""" | |
| from __future__ import annotations | |
| import gc | |
| import io | |
| import os | |
| import re | |
| import pandas as pd | |
| import streamlit as st | |
| import dartlab | |
| # ββ μ€μ ββββββββββββββββββββββββββββββββββββββββββββββ | |
| _MAX_CACHE = 2 | |
| _LOGO_URL = "https://raw.githubusercontent.com/eddmpython/dartlab/master/.github/assets/logo.png" | |
| _BLOG_URL = "https://eddmpython.github.io/dartlab/blog/dartlab-easy-start/" | |
| _DOCS_URL = "https://eddmpython.github.io/dartlab/docs/getting-started/quickstart" | |
| _COLAB_URL = "https://colab.research.google.com/github/eddmpython/dartlab/blob/master/notebooks/showcase/01_quickstart.ipynb" | |
| _REPO_URL = "https://github.com/eddmpython/dartlab" | |
| _HAS_OPENAI = bool(os.environ.get("OPENAI_API_KEY")) | |
| if _HAS_OPENAI: | |
| dartlab.llm.configure(provider="openai", api_key=os.environ["OPENAI_API_KEY"]) | |
| # ββ νμ΄μ§ μ€μ ββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title="DartLab β AI κΈ°μ λΆμ", | |
| page_icon=None, | |
| layout="centered", | |
| ) | |
| # ββ CSS βββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| /* λ€ν¬ ν λ§ κ°μ */ | |
| html, body, [data-testid="stAppViewContainer"], | |
| [data-testid="stApp"], .main, .block-container { | |
| background-color: #050811 !important; | |
| color: #f1f5f9 !important; | |
| } | |
| [data-testid="stHeader"] { background: #050811 !important; } | |
| [data-testid="stSidebar"] { background: #0f1219 !important; } | |
| /* μ λ ₯ νλ */ | |
| input, textarea, | |
| [data-baseweb="input"] input, [data-baseweb="textarea"] textarea, | |
| [data-baseweb="input"], [data-baseweb="base-input"] { | |
| background-color: #0f1219 !important; | |
| color: #f1f5f9 !important; | |
| border-color: #1e2433 !important; | |
| } | |
| /* μ λ νΈ/λλ‘λ€μ΄ */ | |
| [data-baseweb="select"] > div { | |
| background-color: #0f1219 !important; | |
| border-color: #1e2433 !important; | |
| color: #f1f5f9 !important; | |
| } | |
| [data-baseweb="popover"], [data-baseweb="menu"] { | |
| background-color: #0f1219 !important; | |
| } | |
| [data-baseweb="menu"] li { color: #f1f5f9 !important; } | |
| [data-baseweb="menu"] li:hover { background-color: #1a1f2b !important; } | |
| /* λΌλμ€ */ | |
| [data-testid="stRadio"] label { color: #f1f5f9 !important; } | |
| /* λ²νΌ β dartlab primary ν΅μΌ */ | |
| button, [data-testid="stBaseButton-primary"], | |
| [data-testid="stBaseButton-secondary"], | |
| [data-testid="stFormSubmitButton"] button, | |
| [data-testid="stChatInputSubmitButton"] { | |
| background-color: #ea4647 !important; | |
| color: #fff !important; | |
| border: none !important; | |
| font-weight: 600 !important; | |
| } | |
| button:hover, [data-testid="stBaseButton-primary"]:hover, | |
| [data-testid="stChatInputSubmitButton"]:hover { | |
| background-color: #c83232 !important; | |
| } | |
| [data-testid="stDownloadButton"] button { | |
| background-color: #0f1219 !important; | |
| color: #f1f5f9 !important; | |
| border: 1px solid #1e2433 !important; | |
| } | |
| [data-testid="stDownloadButton"] button:hover { | |
| border-color: #ea4647 !important; | |
| color: #ea4647 !important; | |
| background-color: #0f1219 !important; | |
| } | |
| /* expander ν κΈμ λ°°κ²½μ μ κ±° */ | |
| [data-testid="stExpander"] button { | |
| background-color: transparent !important; | |
| color: #f1f5f9 !important; | |
| } | |
| /* Expander */ | |
| [data-testid="stExpander"] { | |
| background-color: #0f1219 !important; | |
| border-color: #1e2433 !important; | |
| } | |
| /* Chat */ | |
| [data-testid="stChatMessage"] { | |
| background-color: #0a0e17 !important; | |
| border-color: #1e2433 !important; | |
| } | |
| [data-testid="stChatInput"], [data-testid="stChatInput"] textarea { | |
| background-color: #0f1219 !important; | |
| border-color: #1e2433 !important; | |
| color: #f1f5f9 !important; | |
| } | |
| /* ν μ€νΈ */ | |
| p, span, label, h1, h2, h3, h4, h5, h6, | |
| [data-testid="stMarkdownContainer"], | |
| [data-testid="stMarkdownContainer"] p { | |
| color: #f1f5f9 !important; | |
| } | |
| [data-testid="stCaption"] { color: #64748b !important; } | |
| /* DataFrame */ | |
| [data-testid="stDataFrame"] { font-variant-numeric: tabular-nums; } | |
| /* 컀μ€ν */ | |
| .dl-header { | |
| text-align: center; | |
| padding: 1.5rem 0 0.5rem; | |
| } | |
| .dl-header img { | |
| border-radius: 50%; | |
| box-shadow: 0 0 48px rgba(234,70,71,0.25); | |
| } | |
| .dl-header h1 { | |
| background: linear-gradient(135deg, #ea4647, #f87171, #ea4647); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 2.4rem !important; | |
| font-weight: 800 !important; | |
| margin: 0.5rem 0 0.1rem !important; | |
| letter-spacing: -0.03em; | |
| } | |
| .dl-header .tagline { color: #94a3b8 !important; font-size: 1rem; margin: 0; } | |
| .dl-header .sub { color: #64748b !important; font-size: 0.82rem; margin: 0.15rem 0 0; } | |
| .dl-card { | |
| background: linear-gradient(135deg, #0f1219 0%, #0a0d16 100%); | |
| border: 1px solid #1e2433; | |
| border-radius: 12px; | |
| padding: 1.2rem 1.5rem; | |
| margin: 0.8rem 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .dl-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, #ea4647, #f87171, #fb923c); | |
| } | |
| .dl-card h3 { color: #f1f5f9 !important; font-size: 1.3rem !important; margin: 0 0 0.8rem !important; } | |
| .dl-card .meta { display: flex; gap: 2.5rem; flex-wrap: wrap; } | |
| .dl-card .meta-item { display: flex; flex-direction: column; gap: 0.1rem; } | |
| .dl-card .meta-label { | |
| color: #64748b !important; font-size: 0.72rem; | |
| text-transform: uppercase; letter-spacing: 0.08em; | |
| } | |
| .dl-card .meta-value { | |
| color: #e2e8f0 !important; font-size: 1.1rem; font-weight: 600; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .dl-section { | |
| color: #ea4647 !important; | |
| font-weight: 700 !important; | |
| font-size: 1.05rem !important; | |
| border-bottom: 2px solid #ea4647; | |
| padding-bottom: 0.3rem; | |
| margin: 1rem 0 0.6rem; | |
| } | |
| .dl-footer { | |
| text-align: center; | |
| padding: 1.5rem 0 0.8rem; | |
| border-top: 1px solid #1e2433; | |
| margin-top: 2rem; | |
| color: #475569 !important; | |
| font-size: 0.82rem; | |
| } | |
| .dl-footer a { color: #94a3b8 !important; text-decoration: none; margin: 0 0.5rem; } | |
| .dl-footer a:hover { color: #ea4647 !important; } | |
| .dl-hero-glow { | |
| position: fixed; | |
| top: 0; left: 50%; | |
| transform: translateX(-50%); | |
| width: 600px; height: 400px; | |
| background: radial-gradient(ellipse at top, rgba(234,70,71,0.05) 0%, transparent 60%); | |
| pointer-events: none; z-index: 0; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ μ νΈ ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _toPandas(df): | |
| """Polars/pandas DataFrame -> pandas.""" | |
| if df is None: | |
| return None | |
| if hasattr(df, "to_pandas"): | |
| return df.to_pandas() | |
| return df | |
| def _formatDf(df: pd.DataFrame) -> pd.DataFrame: | |
| """μ«μλ₯Ό μ²λ¨μ μ½€λ§ λ¬Έμμ΄λ‘ λ³ν (μμμ μ κ±°).""" | |
| if df is None or df.empty: | |
| return df | |
| result = df.copy() | |
| for col in result.columns: | |
| if pd.api.types.is_numeric_dtype(result[col]): | |
| result[col] = result[col].apply( | |
| lambda x: f"{int(x):,}" if pd.notna(x) and x == x else "" | |
| ) | |
| return result | |
| def _toExcel(df: pd.DataFrame) -> bytes: | |
| """DataFrame -> Excel bytes.""" | |
| buf = io.BytesIO() | |
| df.to_excel(buf, index=False, engine="openpyxl") | |
| return buf.getvalue() | |
| def _showDf(df: pd.DataFrame, key: str = "", downloadName: str = ""): | |
| """DataFrame νμ + Excel λ€μ΄λ‘λ.""" | |
| if df is None or df.empty: | |
| st.caption("λ°μ΄ν° μμ") | |
| return | |
| st.dataframe(_formatDf(df), use_container_width=True, hide_index=True, key=key or None) | |
| if downloadName: | |
| st.download_button( | |
| label="Excel λ€μ΄λ‘λ", | |
| data=_toExcel(df), | |
| file_name=f"{downloadName}.xlsx", | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| key=f"dl_{key}" if key else None, | |
| ) | |
| def _getCompany(code: str): | |
| """μΊμλ Company.""" | |
| gc.collect() | |
| return dartlab.Company(code) | |
| # ββ μ’ λͺ©μ½λ μΆμΆ ββββββββββββββββββββββββββββββββββββ | |
| def _extractCode(message: str) -> str | None: | |
| """λ©μμ§μμ μ’ λͺ©μ½λ/νμ¬λͺ μΆμΆ.""" | |
| msg = message.strip() | |
| # 6μ리 μ«μ | |
| m = re.search(r"\b(\d{6})\b", msg) | |
| if m: | |
| return m.group(1) | |
| # μλ¬Έ ν°μ»€ (λ¨λ λλ¬Έμ 1~5μ) | |
| m = re.search(r"\b([A-Z]{1,5})\b", msg) | |
| if m: | |
| return m.group(1) | |
| # νκΈ νμ¬λͺ β dartlab.search | |
| cleaned = re.sub( | |
| r"(μ\s*λν΄|μ\s*λν|μλν΄|μ’|μ|λ₯Ό|μ|μ|λ|μ΄|κ°|λ|λ§|λΆν°|κΉμ§|νκ³ |μ΄λ|λ|λ‘|μΌλ‘|μ|κ³Ό|νν |μμ|μκ²)\b", | |
| " ", | |
| msg, | |
| ) | |
| # λΆνμν λμ¬/μ‘°λμ¬ μ κ±° | |
| cleaned = re.sub( | |
| r"\b(μλ €μ€|보μ¬μ€|λΆμ|ν΄μ€|ν΄λ΄|μ΄λ|보μ|λ³Όλ|μ€|ν΄|μ’|μ)\b", | |
| " ", | |
| cleaned, | |
| ) | |
| tokens = re.findall(r"[κ°-ν£A-Za-z0-9]+", cleaned) | |
| # κΈ΄ ν ν° μ°μ (νμ¬λͺ μΌ κ°λ₯μ± λμ) | |
| tokens.sort(key=len, reverse=True) | |
| for token in tokens: | |
| if len(token) >= 2: | |
| try: | |
| results = dartlab.search(token) | |
| if results is not None and len(results) > 0: | |
| return str(results[0, "μ’ λͺ©μ½λ"]) | |
| except Exception: | |
| continue | |
| return None | |
| def _detectTopic(message: str) -> str | None: | |
| """λ©μμ§μμ νΉμ topic ν€μλ κ°μ§.""" | |
| topicMap = { | |
| "λ°°λΉ": "dividend", | |
| "μ£Όμ£Ό": "majorHolder", | |
| "λμ£Όμ£Ό": "majorHolder", | |
| "μ§μ": "employee", | |
| "μμ": "executive", | |
| "μμ보μ": "executivePay", | |
| "보μ": "executivePay", | |
| "μΈκ·Έλ¨ΌνΈ": "segments", | |
| "λΆλ¬Έ": "segments", | |
| "μ¬μ λΆ": "segments", | |
| "μ νμμ°": "tangibleAsset", | |
| "무νμμ°": "intangibleAsset", | |
| "μμ¬λ£": "rawMaterial", | |
| "μμ£Ό": "salesOrder", | |
| "μ ν": "productService", | |
| "μνμ¬": "subsidiary", | |
| "μ’ μ": "subsidiary", | |
| "λΆμ±": "contingentLiability", | |
| "μ°λ°": "contingentLiability", | |
| "νμ": "riskDerivative", | |
| "μ¬μ±": "bond", | |
| "μ΄μ¬ν": "boardOfDirectors", | |
| "κ°μ¬": "audit", | |
| "μλ³Έλ³λ": "capitalChange", | |
| "μκΈ°μ£Όμ": "treasuryStock", | |
| "μ¬μ κ°μ": "business", | |
| "μ¬μ λ³΄κ³ ": "business", | |
| "μ°ν": "companyHistory", | |
| } | |
| msg = message.lower() | |
| for keyword, topic in topicMap.items(): | |
| if keyword in msg: | |
| return topic | |
| return None | |
| # ββ AI ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _askAi(stockCode: str, question: str) -> str: | |
| """AI μ§λ¬Έ. OpenAI μ°μ , HF λ¬΄λ£ fallback.""" | |
| if _HAS_OPENAI: | |
| try: | |
| q = f"{stockCode} {question}" if stockCode else question | |
| answer = dartlab.ask(q, stream=False, raw=False) | |
| return answer or "μλ΅ μμ" | |
| except Exception as e: | |
| return f"λΆμ μ€ν¨: {e}" | |
| try: | |
| from huggingface_hub import InferenceClient | |
| token = os.environ.get("HF_TOKEN") | |
| client = InferenceClient( | |
| model="meta-llama/Llama-3.1-8B-Instruct", | |
| token=token if token else None, | |
| ) | |
| context = _buildAiContext(stockCode) | |
| systemMsg = ( | |
| "λΉμ μ νκ΅ κΈ°μ μ¬λ¬΄ λΆμ μ λ¬Έκ°μ λλ€. " | |
| "μλ μ¬λ¬΄ λ°μ΄ν°λ₯Ό λ°νμΌλ‘ μ¬μ©μμ μ§λ¬Έμ νκ΅μ΄λ‘ λ΅λ³νμΈμ. " | |
| "μ«μλ μ²λ¨μ μ½€λ§λ₯Ό μ¬μ©νκ³ , κ·Όκ±°λ₯Ό λͺ νν μ μνμΈμ.\n\n" | |
| f"{context}" | |
| ) | |
| response = client.chat_completion( | |
| messages=[ | |
| {"role": "system", "content": systemMsg}, | |
| {"role": "user", "content": question}, | |
| ], | |
| max_tokens=1024, | |
| ) | |
| return response.choices[0].message.content or "μλ΅ μμ" | |
| except Exception as e: | |
| return f"AI λΆμ μ€ν¨: {e}" | |
| def _buildAiContext(stockCode: str) -> str: | |
| """AI 컨ν μ€νΈ ꡬμ±.""" | |
| try: | |
| c = _getCompany(stockCode) | |
| except Exception: | |
| return f"μ’ λͺ©μ½λ: {stockCode}" | |
| parts = [f"κΈ°μ : {c.corpName} ({c.stockCode}), μμ₯: {c.market}"] | |
| for name, attr in [("μμ΅κ³μ°μ", "IS"), ("μ¬λ¬΄μνν", "BS"), ("μ¬λ¬΄λΉμ¨", "ratios")]: | |
| try: | |
| df = _toPandas(getattr(c, attr, None)) | |
| if df is not None and not df.empty: | |
| parts.append(f"\n[{name}]\n{df.head(15).to_string()}") | |
| except Exception: | |
| pass | |
| return "\n".join(parts) | |
| # ββ λμ보λ λ λλ§ ββββββββββββββββββββββββββββββββββ | |
| def _renderCompanyCard(c): | |
| """κΈ°μ μΉ΄λ.""" | |
| currency = "" | |
| if hasattr(c, "currency") and c.currency: | |
| currency = c.currency | |
| currencyHtml = ( | |
| f"<div class='meta-item'><span class='meta-label'>ν΅ν</span>" | |
| f"<span class='meta-value'>{currency}</span></div>" | |
| if currency else "" | |
| ) | |
| st.markdown(f""" | |
| <div class="dl-card"> | |
| <h3>{c.corpName}</h3> | |
| <div class="meta"> | |
| <div class="meta-item"> | |
| <span class="meta-label">μ’ λͺ©μ½λ</span> | |
| <span class="meta-value">{c.stockCode}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <span class="meta-label">μμ₯</span> | |
| <span class="meta-value">{c.market}</span> | |
| </div> | |
| {currencyHtml} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def _renderFullDashboard(c, code: str): | |
| """μ 체 μ¬λ¬΄ λμ보λ.""" | |
| _renderCompanyCard(c) | |
| # μ¬λ¬΄μ ν | |
| st.markdown('<div class="dl-section">μ¬λ¬΄μ ν</div>', unsafe_allow_html=True) | |
| for label, attr in [("IS (μμ΅κ³μ°μ)", "IS"), ("BS (μ¬λ¬΄μνν)", "BS"), | |
| ("CF (νκΈνλ¦ν)", "CF"), ("ratios (μ¬λ¬΄λΉμ¨)", "ratios")]: | |
| with st.expander(label, expanded=(attr == "IS")): | |
| try: | |
| df = _toPandas(getattr(c, attr, None)) | |
| _showDf(df, key=f"dash_{attr}", downloadName=f"{code}_{attr}") | |
| except Exception: | |
| st.caption("λ‘λ μ€ν¨") | |
| # Sections | |
| topics = [] | |
| try: | |
| topics = list(c.topics) if c.topics else [] | |
| except Exception: | |
| pass | |
| if topics: | |
| st.markdown('<div class="dl-section">곡μ λ°μ΄ν°</div>', unsafe_allow_html=True) | |
| selectedTopic = st.selectbox("topic", topics, label_visibility="collapsed", key="dash_topic") | |
| if selectedTopic: | |
| try: | |
| result = c.show(selectedTopic) | |
| if result is not None: | |
| if hasattr(result, "to_pandas"): | |
| _showDf(_toPandas(result), key="dash_sec", downloadName=f"{code}_{selectedTopic}") | |
| else: | |
| st.markdown(str(result)) | |
| except Exception as e: | |
| st.caption(f"μ‘°ν μ€ν¨: {e}") | |
| def _renderTopicData(c, code: str, topic: str): | |
| """νΉμ topic λ°μ΄ν°λ§ λ λλ§.""" | |
| try: | |
| result = c.show(topic) | |
| if result is not None: | |
| if hasattr(result, "to_pandas"): | |
| _showDf(_toPandas(result), key=f"topic_{topic}", downloadName=f"{code}_{topic}") | |
| else: | |
| st.markdown(str(result)) | |
| else: | |
| st.caption(f"'{topic}' λ°μ΄ν° μμ") | |
| except Exception as e: | |
| st.caption(f"μ‘°ν μ€ν¨: {e}") | |
| # ββ ν리λ‘λ ββββββββββββββββββββββββββββββββββββββββββ | |
| def _warmup(): | |
| """listing μΊμ.""" | |
| try: | |
| dartlab.search("μΌμ±μ μ") | |
| except Exception: | |
| pass | |
| return True | |
| _warmup() | |
| # ββ ν€λ ββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(f""" | |
| <div class="dl-hero-glow"></div> | |
| <div class="dl-header"> | |
| <img src="{_LOGO_URL}" width="80" height="80" alt="DartLab"> | |
| <h1>DartLab</h1> | |
| <p class="tagline">μ’ λͺ©μ½λ νλ. κΈ°μ μ μ 체 μ΄μΌκΈ°.</p> | |
| <p class="sub">DART / EDGAR 곡μ λ°μ΄ν°λ₯Ό ꡬ쑰ννμ¬ μ 곡ν©λλ€</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ μΈμ μ΄κΈ°ν ββββββββββββββββββββββββββββββββββββββ | |
| if "messages" not in st.session_state: | |
| st.session_state.messages = [] | |
| if "code" not in st.session_state: | |
| st.session_state.code = "" | |
| # ββ λμ보λ μμ (μ’ λͺ©μ΄ μμΌλ©΄ νμ) ββββββββββββββββ | |
| if st.session_state.code: | |
| try: | |
| _dashCompany = _getCompany(st.session_state.code) | |
| _renderFullDashboard(_dashCompany, st.session_state.code) | |
| except Exception as e: | |
| st.error(f"κΈ°μ λ‘λ μ€ν¨: {e}") | |
| st.markdown("---") | |
| # ββ μ±ν μμ ββββββββββββββββββββββββββββββββββββββββ | |
| # νμ€ν 리 νμ | |
| for msg in st.session_state.messages: | |
| with st.chat_message(msg["role"]): | |
| st.markdown(msg["content"]) | |
| # μ λ ₯ | |
| if prompt := st.chat_input("μΌμ±μ μμ λν΄ μλ €μ€, λ°°λΉ νν©μ? ..."): | |
| # μ¬μ©μ λ©μμ§ νμ | |
| st.session_state.messages.append({"role": "user", "content": prompt}) | |
| with st.chat_message("user"): | |
| st.markdown(prompt) | |
| # μ’ λͺ©μ½λ μΆμΆ μλ | |
| newCode = _extractCode(prompt) | |
| if newCode and newCode != st.session_state.code: | |
| st.session_state.code = newCode | |
| code = st.session_state.code | |
| if not code: | |
| # μ’ λͺ© λͺ» μ°Ύμ | |
| reply = "μ’ λͺ©μ μ°Ύμ§ λͺ»νμ΅λλ€. νμ¬λͺ μ΄λ μ’ λͺ©μ½λλ₯Ό ν¬ν¨ν΄μ λ€μ μ§λ¬Έν΄μ£ΌμΈμ.\n\nμ: μΌμ±μ μμ λν΄ μλ €μ€, 005930 λΆμ, AAPL μ¬λ¬΄" | |
| st.session_state.messages.append({"role": "assistant", "content": reply}) | |
| with st.chat_message("assistant"): | |
| st.markdown(reply) | |
| else: | |
| # μλ΅ μμ± | |
| with st.chat_message("assistant"): | |
| # νΉμ topic κ°μ§ | |
| topic = _detectTopic(prompt) | |
| if topic: | |
| # νΉμ topicλ§ λ³΄μ¬μ£ΌκΈ° | |
| try: | |
| c = _getCompany(code) | |
| _renderTopicData(c, code, topic) | |
| except Exception: | |
| pass | |
| # AI μμ½ | |
| with st.spinner("λΆμ μ€..."): | |
| aiAnswer = _askAi(code, prompt) | |
| st.markdown(aiAnswer) | |
| st.session_state.messages.append({"role": "assistant", "content": aiAnswer}) | |
| # λμ보λ κ°±μ μ μν΄ rerun | |
| if newCode and newCode != "": | |
| st.rerun() | |
| # ββ μ΄κΈ° μλ΄ (λν μμ λ) βββββββββββββββββββββββββ | |
| if not st.session_state.messages and not st.session_state.code: | |
| st.markdown(""" | |
| <div style="text-align: center; color: #64748b; padding: 2rem 1rem;"> | |
| <p style="font-size: 1.1rem; color: #94a3b8;"> | |
| μλ μ λ ₯μ°½μ μμ°μ΄λ‘ μ§λ¬ΈνμΈμ | |
| </p> | |
| <p style="margin-top: 0.5rem;"> | |
| <code>μΌμ±μ μμ λν΄ μλ €μ€</code> · | |
| <code>005930 λΆμ</code> · | |
| <code>AAPL μ¬λ¬΄ 보μ¬μ€</code> | |
| </p> | |
| <p style="margin-top: 0.3rem; font-size: 0.85rem;"> | |
| μ’ λͺ©μ λ§νλ©΄ μ¬λ¬΄μ ν/곡μ λ°μ΄ν°κ° λ°λ‘ νμλκ³ , AIκ° λΆμμ λ§λΆμ λλ€ | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ νΈν° ββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(f""" | |
| <div class="dl-footer"> | |
| <a href="{_BLOG_URL}">μ΄λ³΄μ κ°μ΄λ</a> / | |
| <a href="{_DOCS_URL}">곡μ λ¬Έμ</a> / | |
| <a href="{_COLAB_URL}">Colab</a> / | |
| <a href="{_REPO_URL}">GitHub</a> | |
| <br><span style="color:#334155; font-size:0.78rem; margin-top:0.4rem; display:inline-block;"> | |
| pip install dartlab | |
| </span> | |
| </div> | |
| """, unsafe_allow_html=True) | |