Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>U2INVEST | Master the Market</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { --u2-navy: #003399; --u2-sky: #33CCFF; --yt-text: #0f0f0f; --yt-gray: #606060; } | |
| body { font-family: 'Inter', sans-serif; background-color: #fff; color: #1d1d1f; -webkit-font-smoothing: antialiased; height: 100vh; overflow: hidden; } | |
| .page-view { transition: opacity 0.5s ease, transform 0.5s ease; position: absolute; width: 100%; height: 100vh; overflow: hidden; top: 0; padding-top: 60px; } | |
| #view-landing { padding-top: 0; height: 100vh; overflow-y: auto; } | |
| .view-hidden { opacity: 0; pointer-events: none; transform: translateY(20px); display: none;} | |
| .logo-main { width: 280px; height: auto; mix-blend-mode: multiply; margin: 0 auto; display: block; } | |
| .ds-card { background: #fff; border: 2px solid #f3f4f6; border-radius: 28px; padding: 3rem; transition: 0.4s; cursor: pointer; text-align: left; } | |
| .ds-card:hover { border-color: var(--u2-navy); background: #f0f7ff; transform: translateY(-10px); } | |
| #roadmap-svg { background: #fcfcfc; border-radius: 40px; border: 1px solid #f3f4f6; cursor: grab; } | |
| .node-completed { filter: drop-shadow(0 0 10px #FBBF24); stroke: #FBBF24 ; stroke-width: 6px ; } | |
| .video-box { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; border-radius: 32px; background: #000; width: 100%; } | |
| .video-box iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; } | |
| #overlay { position: fixed; inset: 0; background: #fff; z-index: 5000; display: none; opacity: 0; transform: translateX(100%); transition: 0.5s; overflow-y: auto; } | |
| #overlay.active { display: block; opacity: 1; transform: translateX(0); } | |
| .k-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 24px; transition: 0.4s; overflow: hidden; } | |
| .yt-avatar { width: 40px; height: 40px; border-radius: 50%; background: #f3f4f6; display: flex; align-items: center; justify-content: center; font-weight: 700; color: #003399; flex-shrink: 0; } | |
| .yt-avatar-sm { width: 24px; height: 24px; font-size: 10px; } | |
| .yt-action-btn { display: flex; align-items: center; gap: 6px; cursor: pointer; color: var(--yt-gray); transition: 0.2s; font-size: 13px; font-weight: 600; } | |
| .yt-action-btn:hover { color: #000; } | |
| .yt-reply-input { border-bottom: 1px solid #e5e5e5; transition: 0.3s; } | |
| .yt-reply-input:focus-within { border-bottom: 2px solid #000; } | |
| .star { font-size: 32px; color: #e5e7eb; cursor: pointer; transition: 0.2s; } | |
| .star.active { color: #fbbf24; } | |
| .ms-pill { background: #f3f4f6; padding: 5px; border-radius: 99px; display: inline-flex; } | |
| .ms-pill-item { padding: 10px 32px; border-radius: 99px; font-size: 14px; font-weight: 700; cursor: pointer; color: #718096; } | |
| .ms-pill-item.active { background: var(--u2-navy); color: #fff; box-shadow: 0 8px 20px rgba(0,51,153,0.2); } | |
| .cmt-filter-btn { font-size: 14px; font-weight: 700; color: var(--yt-gray); cursor: pointer; padding: 4px 12px; border-radius: 8px; } | |
| .cmt-filter-btn.active { color: #000; background: #f2f2f2; } | |
| #share-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 7000; align-items: center; justify-content: center; backdrop-filter: blur(4px); } | |
| .share-box { background: white; width: 450px; border-radius: 12px; padding: 20px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); position: relative; } | |
| .share-icon-circle { width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; transition: 0.2s; } | |
| .share-icon-circle:hover { transform: scale(1.1); } | |
| .share-url-container { background: #f9f9f9; border: 1px solid #ddd; padding: 8px 12px; border-radius: 8px; display: flex; align-items: center; justify-content: space-between; margin-top: 20px; } | |
| .roadmap-link { fill: none ; stroke: #e2e8f0; stroke-width: 2.5; stroke-dasharray: 4; animation: flow 10s linear infinite; } | |
| @keyframes flow { to { stroke-dashoffset: -40; } } | |
| .custom-line { fill: none ; stroke: var(--u2-sky); stroke-width: 4; cursor: pointer; transition: 0.3s; filter: url(#glow); } | |
| .custom-line.active { stroke: #fff; stroke-width: 6; } | |
| .node-port { fill: #94a3b8; cursor: pointer; transition: 0.3s; } | |
| .node-port.active { fill: var(--u2-sky); r: 8; filter: drop-shadow(0 0 5px var(--u2-sky)); } | |
| .roadmap-legend { position: absolute; bottom: 30px; right: 30px; background: rgba(255,255,255,0.9); padding: 15px; border-radius: 20px; border: 1px solid #eee; backdrop-filter: blur(10px); z-index: 50; } | |
| #confirm-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 9999; align-items: center; justify-content: center; backdrop-filter: blur(4px); } | |
| .modal-box { background: white; padding: 40px; border-radius: 35px; width: 400px; text-align: center; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.2); } | |
| #node-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 6000; align-items: center; justify-content: center; backdrop-filter: blur(5px); } | |
| .lab-panel { background: #fff; border-radius: 20px; border: 2px solid #f3f4f6; padding: 1.5rem; } | |
| .compact-stat { background: #fff; border-radius: 16px; border: 2px solid #f3f4f6; padding: 1rem; } | |
| .stock-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 1rem; transition: 0.3s; cursor: pointer; } | |
| .stock-card:hover { border-color: var(--u2-navy); box-shadow: 0 4px 12px rgba(0, 51, 153, 0.1); } | |
| .stock-card.selected { border-color: var(--u2-navy); background: #f0f7ff; } | |
| .price-up { color: #10b981; } | |
| .price-down { color: #ef4444; } | |
| .trade-btn { padding: 0.75rem 2rem; border-radius: 12px; font-weight: 700; transition: 0.3s; cursor: pointer; border: none; } | |
| .trade-btn.buy { background: #10b981; color: white; } | |
| .trade-btn.sell { background: #ef4444; color: white; } | |
| .holding-card { background: #f8fafc; border-radius: 12px; padding: 0.75rem; margin-bottom: 0.5rem; border-left: 4px solid var(--u2-navy); } | |
| #agent-chat-container { display: flex; height: 100%; background: #fff; } | |
| .agent-sidebar { width: 260px; border-right: 1px solid #e5e7eb; background: #f9fafb; padding: 1rem; flex-shrink: 0; } | |
| .agent-main { flex: 1; display: flex; flex-direction: column; } | |
| .agent-header { padding: 1rem 2rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; } | |
| .agent-messages { flex: 1; overflow-y: auto; padding: 2rem; background: #fff; scroll-behavior: smooth; } | |
| .agent-input-area { border-top: 1px solid #e5e7eb; padding: 1.5rem 2rem; background: #fff; position: relative; } | |
| .message-bubble { max-width: 85%; margin-bottom: 2rem; display: flex; flex-direction: column; gap: 0.5rem; animation: fadeIn 0.3s ease; } | |
| .message-bubble.user { margin-left: auto; align-items: flex-end; } | |
| .message-bubble.assistant { margin-right: auto; align-items: flex-start; } | |
| .message-content { padding: 1.25rem 1.75rem; border-radius: 20px; font-size: 15px; line-height: 1.7; color: #374151; box-shadow: 0 2px 5px rgba(0,0,0,0.02); } | |
| .message-bubble.user .message-content { background: #f0f4ff; color: #1e3a8a; border-radius: 20px 20px 4px 20px; } | |
| .message-bubble.assistant .message-content { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 20px 20px 20px 4px; width: 100%; } | |
| .message-timestamp { font-size: 11px; color: #9ca3af; margin-top: 4px; padding: 0 4px; } | |
| /* Chart & Visuals */ | |
| .chat-chart-container { width: 100%; height: 300px; margin-top: 1rem; border: 1px solid #f3f4f6; border-radius: 12px; padding: 10px; background: #fafafa; } | |
| .json-chart-block { display: none; } /* Hide raw JSON */ | |
| /* Markdown-ish Styles */ | |
| .msg-header { font-weight: 700; font-size: 1.1em; margin-top: 1em; margin-bottom: 0.5em; color: #111827; } | |
| .msg-list { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1em; } | |
| .msg-list li { margin-bottom: 0.25em; } | |
| /* Input Area & Buttons */ | |
| .agent-input-box { display: flex; gap: 1rem; align-items: flex-end; background: #fff; border-radius: 16px; padding: 0.75rem 1rem; border: 1px solid #e5e7eb; transition: all 0.2s; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); } | |
| .agent-input-box:focus-within { border-color: var(--u2-navy); box-shadow: 0 4px 12px rgba(0, 51, 153, 0.1); } | |
| .agent-input-box textarea { flex: 1; border: none; outline: none; background: transparent; resize: none; font-size: 15px; padding: 0.5rem; font-family: 'Inter', sans-serif; max-height: 200px; min-height: 24px; } | |
| .send-btn { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; border: none; flex-shrink: 0; } | |
| .send-btn.active { background: var(--u2-navy); color: white; } | |
| .send-btn.stop { background: #ef4444; color: white; } | |
| .send-btn:disabled { background: #e5e7eb; color: #9ca3af; cursor: not-allowed; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .stock-tag { display: inline-flex; align-items: center; gap: 0.5rem; background: var(--u2-navy); color: white; padding: 0.5rem 1rem; border-radius: 20px; font-size: 13px; font-weight: 600; margin: 0.25rem; cursor: pointer; transition: 0.3s; } | |
| </style> | |
| <script> | |
| function showView(v) { | |
| document.querySelectorAll('.page-view').forEach(el => { | |
| if (el.id === `view-${v}`) { | |
| el.classList.remove('view-hidden'); | |
| } else { | |
| el.classList.add('view-hidden'); | |
| } | |
| }); | |
| setTimeout(() => { | |
| if (v === 'academy') loadData(); | |
| if (v === 'lab') loadLabData(); | |
| if (v === 'agent') loadAgentData(); | |
| }, 10); | |
| } | |
| </script> | |
| </head> | |
| <body class="bg-white"> | |
| <header class="fixed top-0 w-full h-14 flex items-center justify-between px-10 bg-white/90 backdrop-blur-md border-b border-gray-100 z-[1000]"> | |
| <div class="flex items-center space-x-3 cursor-pointer" onclick="showView('landing')"> | |
| <img src="/static/images/LOGO_final.png" class="h-8" style="mix-blend-mode: multiply;"> | |
| <span class="font-extrabold text-lg tracking-tighter text-[#003399]">U2INVEST</span> | |
| </div> | |
| <nav class="flex items-center space-x-12"> | |
| <a onclick="showView('academy')" class="text-xs font-black text-gray-500 uppercase tracking-widest cursor-pointer hover:text-[#003399]">Academy</a> | |
| <a onclick="showView('lab')" class="text-xs font-black text-gray-500 uppercase tracking-widest cursor-pointer hover:text-[#003399]">Lab</a> | |
| <a onclick="showView('agent')" class="text-xs font-black text-gray-500 uppercase tracking-widest cursor-pointer hover:text-[#003399]">U2CHAT</a> | |
| </nav> | |
| </header> | |
| <main id="view-landing" class="page-view flex flex-col items-center justify-center"> | |
| <div class="text-center max-w-6xl px-6"> | |
| <img src="/static/images/LOGO_final.png" class="logo-main mb-6" alt="U2INVEST"> | |
| <h1 class="text-5xl font-black mb-4 tracking-tighter text-[#003399]">Your path, Your Choice, Your Future, You to Invest.</h1> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mt-16"> | |
| <div class="ds-card" onclick="showView('academy')"><h3 class="text-2xl font-bold">Start Learning</h3><p>50 Academic modules from Foundations to Advanced Mastery.</p><span class="text-xs font-black text-[#003399]">Enter Academy →</span></div> | |
| <div class="ds-card" onclick="showView('lab')"><h3 class="text-2xl font-bold">Trading Lab</h3><p>Real-time market simulation in zero-risk environment.</p><span class="text-xs font-black text-[#003399]">Start Trading →</span></div> | |
| <div class="ds-card" onclick="showView('agent')"><h3 class="text-2xl font-bold">U2CHAT</h3><p>Interactive analyst for strategy and market concept guidance.</p><span class="text-xs font-black text-[#003399]">Ask AI →</span></div> | |
| </div> | |
| </div> | |
| </main> | |
| <main id="view-academy" class="page-view view-hidden flex flex-col bg-gray-50"> | |
| <div class="w-full py-12 flex flex-col items-center border-b border-gray-200 bg-white"> | |
| <h2 class="text-2xl font-black mb-8 uppercase tracking-widest text-[#003399]">Knowledge Academy</h2> | |
| <div class="ms-pill"> | |
| <div class="ms-pill-item active" id="tab-blocks" onclick="switchAcademy('blocks')">Course Modules</div> | |
| <div class="ms-pill-item" id="tab-roadmap" onclick="switchAcademy('roadmap')">Learning Roadmap</div> | |
| </div> | |
| <div id="roadmap-controls" class="hidden mt-6 flex space-x-4"> | |
| <div class="flex bg-gray-100 p-1 rounded-full shadow-inner border"> | |
| <button id="btn-mode-auto" onclick="setRoadmapMode('auto')" class="px-6 py-2 rounded-full text-[10px] font-black uppercase transition-all bg-white shadow text-[#003399]">Designed By U2INVEST</button> | |
| <button id="btn-mode-custom" onclick="setRoadmapMode('custom')" class="px-6 py-2 rounded-full text-[10px] font-black uppercase transition-all text-gray-400">You to Design</button> | |
| </div> | |
| <button id="btn-add-node" onclick="openNodeModal()" class="hidden px-6 py-2 bg-[#003399] text-white rounded-full text-[10px] font-black uppercase shadow-lg">Add Node +</button> | |
| </div> | |
| </div> | |
| <div class="flex-grow overflow-y-auto custom-scroll p-10 relative"> | |
| <div id="grid-blocks" class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8"></div> | |
| <div id="roadmap-box" class="hidden h-[700px] w-full bg-white rounded-[40px] shadow-sm relative p-4 overflow-hidden"> | |
| <div id="legend-auto" class="roadmap-legend text-[10px] font-bold text-gray-500 uppercase space-y-2"> | |
| <p class="text-gray-400 mb-1 border-b pb-1">Difficulty Levels</p> | |
| <div class="flex items-center space-x-2"><div class="w-3 h-3 rounded-full bg-[#33CCFF]"></div><span>Foundation</span></div> | |
| <div class="flex items-center space-x-2"><div class="w-3 h-3 rounded-full bg-[#2563EB]"></div><span>Advanced</span></div> | |
| <div class="flex items-center space-x-2"><div class="w-3 h-3 rounded-full bg-[#7C3AED]"></div><span>Professional</span></div> | |
| <div class="flex items-center space-x-2"><div class="w-3 h-3 rounded-full bg-[#FFD60A]"></div><span>Completed</span></div> | |
| </div> | |
| <div id="legend-custom" class="hidden roadmap-legend text-[10px] font-bold text-gray-500 uppercase space-y-2"> | |
| <p class="text-[#003399] mb-1 border-b pb-1">Design Mode</p> | |
| <div class="flex items-center space-x-2"><div class="w-3 h-3 rounded-full bg-[#33CCFF]"></div><span>Active Point</span></div> | |
| <div class="text-[9px] text-gray-400 pt-1">Click points to Link<br>Click line to Break</div> | |
| </div> | |
| <svg id="roadmap-svg" class="w-full h-full"><defs><filter id="glow"><feGaussianBlur stdDeviation="3" result="blur"/><feComposite in="SourceGraphic" in2="blur" operator="over"/></filter></defs></svg> | |
| </div> | |
| </div> | |
| </main> | |
| <div id="overlay" class="custom-scroll"> | |
| <div class="max-w-5xl mx-auto py-24 px-10 relative"> | |
| <button onclick="closeModule()" class="fixed top-20 left-10 text-xs font-bold uppercase bg-black text-white px-8 py-4 rounded-full z-[6000] shadow-2xl hover:scale-105 transition">✕ Exit Academy</button> | |
| <div class="video-box mb-12"><div id="video-target"></div></div> | |
| <div class="flex justify-between items-baseline mb-8"> | |
| <h1 id="mod-name" class="text-5xl font-black text-[#003399]"></h1> | |
| <div class="text-right flex flex-col items-end"> | |
| <div class="text-[10px] font-black text-gray-400 uppercase">Avg Rating</div> | |
| <div id="avg-rating" class="text-4xl font-black text-[#fbbf24]">0.0</div> | |
| <button onclick="openShare()" class="mt-4 flex items-center gap-2 text-xs font-bold text-gray-500 hover:text-black transition uppercase"><i class="bi bi-share"></i> Share</button> | |
| </div> | |
| </div> | |
| <p id="mod-intro" class="text-xl text-gray-500 italic mb-12"></p> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16"> | |
| <div class="bg-gray-50 p-8 rounded-3xl"><h4 class="text-xs font-black uppercase mb-4 text-[#003399]">Outcomes</h4><div id="mod-outcomes" class="space-y-2"></div></div> | |
| <div class="bg-blue-50 p-8 rounded-3xl"><h4 class="text-xs font-black uppercase mb-4 text-[#003399]">Takeaways</h4><div id="mod-takeaways" class="space-y-2"></div></div> | |
| </div> | |
| <div id="status-box" class="mb-24"></div> | |
| <hr class="my-24 border-gray-100"> | |
| <div class="text-left max-w-4xl mx-auto"> | |
| <div class="flex items-center gap-8 mb-10"><h4 class="font-bold text-xl text-[#0f0f0f]">Comments</h4><div class="flex items-center gap-2"><i class="bi bi-filter-left text-2xl"></i><div class="flex space-x-1"><span onclick="sortComments('top')" id="sort-top" class="cmt-filter-btn active">Top</span><span onclick="sortComments('newest')" id="sort-newest" class="cmt-filter-btn">Newest</span></div></div></div> | |
| <div id="reply-indicator" class="hidden flex items-center justify-between bg-gray-50 px-6 py-2 rounded-t-lg border-b border-gray-200"><span class="text-xs font-bold text-[#606060]" id="reply-label">Replying to @user</span><button onclick="cancelReply()" class="text-xs font-black text-gray-400 hover:text-black">CANCEL</button></div> | |
| <div class="flex gap-4 mb-16"><div class="yt-avatar">U</div><div class="flex-grow"><div class="yt-reply-input"><textarea id="cmt-input" class="w-full bg-transparent py-2 text-sm outline-none resize-none h-10 overflow-hidden" placeholder="Add a comment..." oninput="this.style.height = '';this.style.height = this.scrollHeight + 'px'"></textarea></div><div class="flex justify-end mt-2 space-x-3"><button onclick="cancelReply()" class="px-4 py-2 text-sm font-bold rounded-full hover:bg-gray-100 transition">Cancel</button><button onclick="postComment()" class="px-4 py-2 bg-[#003399] text-white text-sm font-bold rounded-full hover:bg-opacity-90 shadow-md">Comment</button></div></div></div> | |
| <div id="comment-list" class="space-y-10 pb-20"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="share-modal" onclick="if(event.target === this) closeShare()"> | |
| <div class="share-box"> | |
| <div class="flex justify-between items-center mb-6"><h3 class="text-lg font-bold">Share</h3><button onclick="closeShare()" class="text-2xl text-gray-400 hover:text-black">×</button></div> | |
| <div class="flex justify-between items-center gap-4 px-2"> | |
| <a id="share-whatsapp" href="#" target="_blank" class="flex flex-col items-center gap-2"><div class="share-icon-circle bg-[#25D366]"><i class="bi bi-whatsapp"></i></div><span class="text-[10px] font-bold">WhatsApp</span></a> | |
| <a id="share-facebook" href="#" target="_blank" class="flex flex-col items-center gap-2"><div class="share-icon-circle bg-[#1877F2]"><i class="bi bi-facebook"></i></div><span class="text-[10px] font-bold">Facebook</span></a> | |
| <a id="share-x" href="#" target="_blank" class="flex flex-col items-center gap-2"><div class="share-icon-circle bg-[#000000]"><i class="bi bi-twitter-x"></i></div><span class="text-[10px] font-bold">X</span></a> | |
| <a id="share-email" href="#" target="_blank" class="flex flex-col items-center gap-2"><div class="share-icon-circle bg-[#8e8e8e]"><i class="bi bi-envelope-fill"></i></div><span class="text-[10px] font-bold">Email</span></a> | |
| <a id="share-linkedin" href="#" target="_blank" class="flex flex-col items-center gap-2"><div class="share-icon-circle bg-[#0077b5]"><i class="bi bi-linkedin"></i></div><span class="text-[10px] font-bold">LinkedIn</span></a> | |
| </div> | |
| <div class="share-url-container mt-8"><span id="share-url-text" class="text-xs text-gray-500 truncate mr-4">https://u2invest.com/module/1</span><button onclick="copyLink()" class="bg-[#003399] text-white px-4 py-2 rounded-full text-[10px] font-bold uppercase shrink-0 hover:bg-blue-800 transition">Copy</button></div> | |
| </div> | |
| </div> | |
| <main id="view-lab" class="page-view view-hidden bg-gray-50"> | |
| <!-- GATEWAY COMPONENT --> | |
| <div id="lab-gateway" class="w-full h-full flex items-center justify-center p-6 transition-all duration-500"> | |
| <div class="max-w-5xl w-full grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <!-- Advanced Path --> | |
| <div onclick="setLabView('dashboard')" class="group relative bg-white border-2 border-gray-100 hover:border-[#003399] rounded-[40px] p-10 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 overflow-hidden"> | |
| <div class="absolute top-0 right-0 bg-[#003399] text-white text-[10px] font-black uppercase px-4 py-2 rounded-bl-2xl">Advanced</div> | |
| <div class="mb-6 w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center text-[#003399] text-2xl group-hover:scale-110 transition"><i class="bi bi-graph-up-arrow"></i></div> | |
| <h2 class="text-3xl font-black text-[#003399] mb-4">The Lab</h2> | |
| <p class="text-gray-500 mb-8 leading-relaxed">Direct access to the professional trading dashboard. Real-time market data, technical analysis tools, and portfolio management.</p> | |
| <span class="text-xs font-black text-[#003399] uppercase tracking-widest group-hover:underline">Enter Dashboard →</span> | |
| </div> | |
| <!-- Beginner Path --> | |
| <div onclick="setLabView('guide')" class="group relative bg-[#003399] text-white rounded-[40px] p-10 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 overflow-hidden"> | |
| <div class="absolute top-0 right-0 bg-white/20 text-white text-[10px] font-black uppercase px-4 py-2 rounded-bl-2xl">Beginner</div> | |
| <div class="mb-6 w-16 h-16 bg-white/10 rounded-2xl flex items-center justify-center text-white text-2xl group-hover:scale-110 transition"><i class="bi bi-compass"></i></div> | |
| <h2 class="text-3xl font-black text-white mb-4">Investment 101</h2> | |
| <p class="text-blue-100 mb-8 leading-relaxed">A step-by-step guided experience. Learn the basics of trading, understand terminology, and make your first simulated trade.</p> | |
| <span class="text-xs font-black text-white uppercase tracking-widest group-hover:underline">Start Guide →</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- DASHBOARD COMPONENT (Existing Lab) --> | |
| <div id="lab-dashboard" class="w-full h-full hidden overflow-y-auto custom-scroll relative"> | |
| <button onclick="setLabView('gateway')" class="absolute top-6 right-6 z-50 text-[10px] font-black uppercase text-gray-400 hover:text-[#003399] bg-white/80 px-3 py-1 rounded-full backdrop-blur border border-gray-200">Back to Gateway</button> | |
| <div class="w-full py-4 px-6 mt-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="flex justify-between items-center mb-4"><h2 class="text-2xl font-black text-[#003399]">TRADING LAB</h2><div class="flex gap-3"><button onclick="refreshLab()" class="px-4 py-2 bg-gray-100 rounded-full text-xs font-bold hover:bg-gray-200 transition"><i class="bi bi-arrow-clockwise"></i> Refresh</button><button onclick="resetPortfolio()" class="px-4 py-2 bg-red-500 text-white rounded-full text-xs font-bold hover:bg-red-600 transition"><i class="bi bi-trash"></i> Reset</button></div></div> | |
| <div class="grid grid-cols-4 gap-3 mb-4"><div class="compact-stat"><div class="text-xs text-gray-500 mb-1">Total Assets</div><div class="text-xl font-black text-[#003399]" id="total-assets">$100,000</div></div><div class="compact-stat"><div class="text-xs text-gray-500 mb-1">Cash</div><div class="text-lg font-bold" id="available-cash">$100,000</div></div><div class="compact-stat"><div class="text-xs text-gray-500 mb-1">Profit</div><div class="text-lg font-bold price-up" id="total-profit">$0</div></div><div class="compact-stat"><div class="text-xs text-gray-500 mb-1">Return</div><div class="text-lg font-bold price-up" id="return-rate">0%</div></div></div> | |
| <div class="grid grid-cols-12 gap-4"> | |
| <div class="col-span-3"><div class="lab-panel" style="max-height: 520px;"><h3 class="text-sm font-bold mb-3">Stock Pool</h3><select id="sector-select" onchange="loadSectorStocks()" class="w-full p-2 border rounded-lg text-sm mb-3"><option value="Popular">Popular Stocks</option><option value="Tech">Technology</option><option value="Energy">New Energy</option><option value="Finance">Finance</option></select><div id="stock-list" class="space-y-2 overflow-y-auto" style="max-height: 420px;"><div class="text-center text-gray-400 py-8 text-sm">Loading...</div></div></div></div> | |
| <div class="col-span-6 space-y-4"> | |
| <div class="lab-panel"><div class="flex justify-between items-center mb-3"><h3 class="text-sm font-bold" id="chart-title">Select a stock</h3><div class="flex gap-2"><button onclick="changeTimeRange(60)" class="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200">60D</button><button onclick="changeTimeRange(120)" class="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200">120D</button><button onclick="changeTimeRange(250)" class="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200">250D</button></div></div><div id="kline-chart" style="height: 300px;"></div></div> | |
| <div class="lab-panel"><h3 class="text-sm font-bold mb-3">Trade</h3><div class="grid grid-cols-4 gap-3 mb-4"><div><label class="block text-xs font-bold mb-1">Symbol</label><input type="text" id="trade-symbol" readonly class="w-full p-2 border rounded-lg bg-gray-50 text-sm"></div><div><label class="block text-xs font-bold mb-1">Price</label><input type="text" id="trade-price" readonly class="w-full p-2 border rounded-lg bg-gray-50 text-sm"></div><div><label class="block text-xs font-bold mb-1">Shares</label><input type="number" id="trade-shares" min="100" step="100" class="w-full p-2 border rounded-lg text-sm" placeholder="100" oninput="calculateTotal()"></div><div><label class="block text-xs font-bold mb-1">Total</label><input type="text" id="trade-total" readonly class="w-full p-2 border rounded-lg bg-gray-50 text-sm"></div></div><div class="flex gap-3"><button onclick="executeTrade('buy')" class="flex-1 trade-btn buy text-sm py-2"><i class="bi bi-arrow-up-circle"></i> BUY</button><button onclick="executeTrade('sell')" class="flex-1 trade-btn sell text-sm py-2"><i class="bi bi-arrow-down-circle"></i> SELL</button></div></div> | |
| </div> | |
| <div class="col-span-3 space-y-4"><div class="lab-panel" style="max-height: 250px;"><h3 class="text-sm font-bold mb-3">Holdings</h3><div id="holdings-list" class="space-y-2 overflow-y-auto" style="max-height: 180px;"><div class="text-center text-gray-400 py-4 text-xs">No holdings</div></div></div><div class="lab-panel" style="max-height: 250px;"><h3 class="text-sm font-bold mb-3">History</h3><div id="history-list" class="space-y-2 overflow-y-auto" style="max-height: 180px;"><div class="text-center text-gray-400 py-4 text-xs">No trades</div></div></div></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GUIDE COMPONENT (New) --> | |
| <div id="lab-guide" class="w-full h-full hidden overflow-y-auto custom-scroll bg-white"> | |
| <div class="max-w-3xl mx-auto py-20 px-6"> | |
| <button onclick="setLabView('gateway')" class="mb-8 text-xs font-black uppercase text-gray-400 hover:text-[#003399]">← Back to Choice</button> | |
| <div class="space-y-12"> | |
| <div class="text-center"> | |
| <span id="guide-step-indicator" class="text-[#003399] font-bold tracking-widest uppercase text-xs">Step 1 of 3</span> | |
| <h2 id="guide-title" class="text-4xl font-black mt-4 mb-6">The Concept of Ownership</h2> | |
| <p id="guide-desc" class="text-gray-500 text-lg leading-relaxed">Buying a stock isn't just betting on numbers. It means you legally own a small piece of that company's future earnings and assets.</p> | |
| </div> | |
| <!-- Dynamic Content Area --> | |
| <div id="guide-content" class="min-h-[250px] flex flex-col items-center justify-center p-10 bg-gray-50 rounded-[40px] border-2 border-gray-100"> | |
| <!-- Default Step 1 Content --> | |
| <div class="text-center"> | |
| <div class="w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6 text-4xl text-[#003399] shadow-inner"><i class="bi bi-building-check"></i></div> | |
| <p class="font-bold text-gray-700 text-xl">Example: Buying 1 share of Moutai means you are a shareholder.</p> | |
| </div> | |
| </div> | |
| <div class="flex justify-center gap-4"> | |
| <button id="guide-prev-btn" onclick="prevGuideStep()" class="hidden px-8 py-4 bg-gray-100 text-gray-500 rounded-full font-bold hover:bg-gray-200 transition">Back</button> | |
| <button id="guide-next-btn" onclick="nextGuideStep()" class="px-10 py-4 bg-[#003399] text-white rounded-full font-bold shadow-xl hover:scale-105 transition">Next Step →</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <main id="view-agent" class="page-view view-hidden"> | |
| <div id="agent-chat-container"> | |
| <div class="agent-sidebar"><button onclick="newChat()" class="w-full px-4 py-3 bg-[#003399] text-white rounded-xl font-bold mb-4 hover:bg-[#002266] transition text-sm"><i class="bi bi-plus-lg"></i> New Chat</button><div class="mb-4"><h3 class="text-xs font-black uppercase text-gray-500 mb-2">Recent</h3><div id="chat-sessions" class="space-y-1"><div class="text-xs text-gray-400 p-2">No history</div></div></div></div> | |
| <div class="agent-main"><div class="agent-header"><div><h2 class="text-xl font-black text-[#003399]">U2CHAT</h2><p class="text-sm text-gray-500">Stock analysis powered by DeepSeek-V3, LangChain-1.1 & AkShare (RAG-Enabled)</p></div><button onclick="clearChat()" class="px-4 py-2 text-sm text-gray-500 hover:text-gray-700"><i class="bi bi-trash"></i> Clear</button></div><div class="px-8 py-3 bg-gray-50 border-b border-gray-200"><div class="stock-selector"><div class="flex justify-between items-center mb-2"><span class="text-sm font-bold text-gray-700">Quick Select</span><select id="agent-sector" onchange="loadAgentStocks()" class="text-sm p-2 border rounded-lg"><option value="Popular">Popular</option><option value="Tech">Technology</option><option value="Energy">New Energy</option><option value="Finance">Finance</option></select></div><div id="agent-stock-tags" class="flex flex-wrap"></div></div><div id="selected-stocks" class="flex flex-wrap gap-2 mt-2"></div></div><div class="agent-messages" id="agent-messages"><div class="text-center py-20"><div class="text-6xl mb-4">💬</div><h3 class="text-2xl font-bold text-gray-700 mb-2">Start a conversation</h3><p class="text-gray-500">Ask about stocks, trends, or strategies</p></div></div><div class="agent-input-area"><div class="agent-input-box"><textarea id="agent-input" rows="1" placeholder="Ask about stocks..." onkeydown="handleAgentKey(event)" oninput="autoResize(this)"></textarea><button id="send-btn" onclick="sendMessage()" class="send-btn active"><i class="bi bi-send-fill"></i></button></div><div class="text-xs text-gray-400 mt-2 text-center">Press Enter to send, Shift+Enter for new line</div></div></div> | |
| </div> | |
| </main> | |
| <div id="confirm-modal"> | |
| <div class="modal-box"><h3 id="modal-title" class="text-xl font-black mb-4 uppercase text-[#003399]">Confirm?</h3><p id="modal-desc" class="text-gray-500 text-sm mb-10"></p><div class="flex justify-center space-x-4"><button onclick="closeConfirm()" class="px-8 py-3 text-xs font-bold text-gray-400">Cancel</button><button id="modal-ok" class="px-10 py-3 bg-[#003399] text-white rounded-full text-xs font-black uppercase">Confirm</button></div></div> | |
| </div> | |
| <div id="node-modal"> | |
| <div class="bg-white p-10 rounded-[40px] w-96 shadow-2xl border-2 border-[#003399]"><h3 class="text-xl font-black text-[#003399] mb-6 uppercase">Add Concept</h3><input type="text" id="node-input" class="w-full p-4 bg-gray-50 rounded-2xl mb-6 outline-none border focus:ring-2 focus:ring-[#003399]" placeholder="Topic name..."><div class="flex justify-end space-x-4"><button onclick="closeNodeModal()" class="px-6 py-2 text-xs font-bold text-gray-400">Cancel</button><button onclick="addCustomNode()" class="px-8 py-3 bg-[#003399] text-white rounded-full text-xs font-bold">Add Node</button></div></div> | |
| </div> | |
| <div id="toast" class="fixed bottom-10 left-1/2 -translate-x-1/2 bg-black text-white px-8 py-3 rounded-full text-xs font-bold opacity-0 transition-opacity pointer-events-none z-[9999]">SUCCESS</div> | |
| <script> | |
| let currentID = null, masterData = [], currentSort = 'top', roadmapMode = 'auto'; | |
| let customNodes = [], customLinks = [], activePort = null; | |
| let replyingToId = null; | |
| const likedIds = new Set(), likedReplyIds = new Set(); | |
| async function loadData() { | |
| try { | |
| const res = await fetch('/api/academy'); masterData = await res.json(); | |
| document.getElementById('grid-blocks').innerHTML = masterData.map(d => ` | |
| <div class="k-card cursor-pointer group flex flex-col h-full" onclick="openModule(${d.id})"> | |
| <div class="relative overflow-hidden h-52 bg-gray-100"><img src="/static/images/academy/${d.id}.jpg" onerror="this.onerror=null; this.src='https://placehold.co/600x400/e0f2fe/0369a1?text=${d.cat}';" class="w-full h-full object-cover group-hover:scale-110 transition duration-700">${d.completed ? '<div class="absolute top-4 right-4 bg-green-500 text-white p-2 rounded-full shadow-lg"><i class="bi bi-check-lg"></i></div>' : ''}</div> | |
| <div class="p-8 flex-grow flex flex-col justify-between"><div><span class="text-[10px] font-black uppercase tracking-widest text-gray-400">${d.cat}</span><h4 class="font-bold text-xl mt-3 mb-6 group-hover:text-[#003399] leading-tight transition">${d.name}</h4></div><div class="flex justify-between items-center text-xs font-bold uppercase"><span class="text-gray-300">${d.views} STUDENTS</span><span class="transition-all duration-300 group-hover:text-[#33CCFF] group-hover:translate-x-2 text-gray-400">START →</span></div></div> | |
| </div>`).join(''); | |
| } catch (err) { console.error('Academy load error:', err); } | |
| } | |
| async function openModule(id) { | |
| try { | |
| const res = await fetch(`/api/academy/${id}`); const d = await res.json(); currentID = id; | |
| document.getElementById('mod-name').innerText = d.name; | |
| document.getElementById('avg-rating').innerText = d.avg_rating || "0.0"; | |
| document.getElementById('video-target').innerHTML = `<iframe src="https://www.youtube.com/embed/${d.video}?hl=en" frameborder="0" allowfullscreen></iframe>`; | |
| document.getElementById('mod-intro').innerText = d.video_intro || ""; | |
| document.getElementById('mod-outcomes').innerHTML = (d.outcomes || []).map(o => `<div class="flex items-start gap-2"><i class="bi bi-check-circle-fill text-green-500 mt-1 flex-shrink-0"></i><span class="text-sm font-bold text-gray-700">${o}</span></div>`).join(''); | |
| document.getElementById('mod-takeaways').innerHTML = (d.takeaways || []).map(t => `<div class="flex items-start gap-2"><i class="bi bi-lightbulb-fill text-yellow-500 mt-1 flex-shrink-0"></i><span class="text-sm font-bold text-gray-700">${t}</span></div>`).join(''); | |
| const currentUrl = window.location.origin + "/module/" + id; const shareText = encodeURIComponent("Check out this course on U2INVEST: " + d.name); | |
| document.getElementById('share-url-text').innerText = currentUrl; | |
| document.getElementById('share-whatsapp').href = `https://api.whatsapp.com/send?text=${shareText}%20${currentUrl}`; | |
| document.getElementById('share-facebook').href = `https://www.facebook.com/sharer/sharer.php?u=${currentUrl}`; | |
| document.getElementById('share-x').href = `https://twitter.com/intent/tweet?text=${shareText}&url=${currentUrl}`; | |
| document.getElementById('share-linkedin').href = `https://www.linkedin.com/sharing/share-offsite/?url=${currentUrl}`; | |
| document.getElementById('share-email').href = `mailto:?subject=${d.name}&body=${shareText}%20${currentUrl}`; | |
| renderStatusBox(d.completed); renderComments(d.comments); | |
| const overlay = document.getElementById('overlay'); overlay.style.display = 'block'; setTimeout(() => overlay.classList.add('active'), 10); | |
| } catch(err) { console.error("Open module error:", err); } | |
| } | |
| function openShare() { document.getElementById('share-modal').style.display = 'flex'; } | |
| function closeShare() { document.getElementById('share-modal').style.display = 'none'; } | |
| function copyLink() { navigator.clipboard.writeText(document.getElementById('share-url-text').innerText).then(() => showToast("LINK COPIED")); } | |
| function renderComments(list) { | |
| let sorted = [...list]; if(currentSort === 'top') sorted.sort((a,b) => b.likes - a.likes); else sorted.sort((a,b) => b.timestamp - a.timestamp); | |
| document.getElementById('comment-list').innerHTML = sorted.map(c => ` | |
| <div class="flex gap-4 group"><div class="yt-avatar">${c.user[0]}</div><div class="flex-grow"><div class="flex items-center gap-2 mb-1"><span class="text-sm font-bold">@${c.user}</span><span class="text-xs text-[#606060]">2 hours ago</span></div><div class="text-sm text-[#0f0f0f] leading-relaxed mb-3">${c.text}</div><div class="flex items-center gap-4"><div class="yt-action-btn" onclick="toggleLike('${c.id}')"><i class="bi ${likedIds.has(c.id)?'bi-hand-thumbs-up-fill text-black':'bi-hand-thumbs-up'}"></i><span>${c.likes}</span></div><div class="yt-action-btn"><i class="bi bi-hand-thumbs-down"></i></div><button class="text-xs font-bold px-3 py-1.5 rounded-full hover:bg-gray-100 transition" onclick="replyAt('${c.user}', '${c.id}')">Reply</button></div> | |
| ${c.replies && c.replies.length > 0 ? `<div class="mt-4 space-y-4 border-l-2 border-gray-100 pl-4">${c.replies.map(r => ` | |
| <div class="flex gap-3"><div class="yt-avatar yt-avatar-sm">${r.user[0]}</div><div class="flex-grow"><div class="flex items-center gap-2 mb-0.5"><span class="text-xs font-bold">@${r.user}</span><span class="text-[10px] text-[#606060]">1 hour ago</span></div><div class="text-sm text-[#0f0f0f] mb-2">${r.text}</div><div class="flex items-center gap-3"><div class="yt-action-btn text-[11px]" onclick="toggleReplyLike('${c.id}','${r.id}')"><i class="bi ${likedReplyIds.has(r.id)?'bi-hand-thumbs-up-fill text-black':'bi-hand-thumbs-up'}"></i><span>${r.likes||0}</span></div><button class="text-[11px] font-bold hover:bg-gray-100 px-2 py-1 rounded" onclick="replyToReply('${r.user}', '${c.id}')">Reply</button></div></div></div>`).join('')}</div>` : ''}</div></div>`).join(''); | |
| } | |
| function replyAt(user, commentId) { replyingToId = commentId; const indicator = document.getElementById('reply-indicator'); document.getElementById('reply-label').innerText = `Replying to @${user}`; indicator.classList.remove('hidden'); document.getElementById('cmt-input').placeholder = "Add a reply..."; document.getElementById('cmt-input').value = ""; document.getElementById('cmt-input').focus(); document.getElementById('cmt-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); } | |
| function replyToReply(user, parentCommentId) { replyingToId = parentCommentId; const indicator = document.getElementById('reply-indicator'); document.getElementById('reply-label').innerText = `Replying to @${user}`; indicator.classList.remove('hidden'); document.getElementById('cmt-input').value = `@${user} `; document.getElementById('cmt-input').focus(); document.getElementById('cmt-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); } | |
| function cancelReply() { replyingToId = null; document.getElementById('reply-indicator').classList.add('hidden'); document.getElementById('cmt-input').placeholder = "Add a comment..."; document.getElementById('cmt-input').value = ""; document.getElementById('cmt-input').style.height = '40px'; } | |
| async function postComment() { const txt = document.getElementById('cmt-input').value; if(!txt.trim()) return; const res = await fetch('/api/comment', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: currentID, text: txt, parentId: replyingToId}) }); if(res.ok) { const d = await res.json(); renderComments(d.comments); cancelReply(); showToast("POSTED"); } } | |
| function setRoadmapMode(mode) { | |
| roadmapMode = mode; document.getElementById('btn-mode-auto').className = `px-6 py-2 rounded-full text-[10px] font-black uppercase transition-all ${mode==='auto'?'bg-white shadow text-[#003399]':'text-gray-400'}`; document.getElementById('btn-mode-custom').className = `px-6 py-2 rounded-full text-[10px] font-black uppercase transition-all ${mode==='custom'?'bg-white shadow text-[#003399]':'text-gray-400'}`; | |
| document.getElementById('btn-add-node').classList.toggle('hidden', mode !== 'custom'); document.getElementById('legend-auto').classList.toggle('hidden', mode !== 'auto'); document.getElementById('legend-custom').classList.toggle('hidden', mode !== 'custom'); | |
| initRoadmap(); | |
| } | |
| function initRoadmap() { | |
| const svg = d3.select("#roadmap-svg"); svg.selectAll("*").remove(); | |
| const box = document.getElementById('roadmap-box'); | |
| const width = box.clientWidth, height = box.clientHeight; | |
| const center = { x: width/2, y: height/2 }; | |
| // 1. Defs: Glow & Gradients | |
| const defs = svg.append("defs"); | |
| const filter = defs.append("filter").attr("id", "sun-glow"); | |
| filter.append("feGaussianBlur").attr("stdDeviation", "8").attr("result", "coloredBlur"); | |
| const feMerge = filter.append("feMerge"); | |
| feMerge.append("feMergeNode").attr("in", "coloredBlur"); | |
| feMerge.append("feMergeNode").attr("in", "SourceGraphic"); | |
| const g = svg.append("g").attr("class", "zoom-container") | |
| .attr("transform", `translate(${center.x},${center.y})`); | |
| svg.call(d3.zoom().scaleExtent([0.1, 4]).on("zoom", (e) => { | |
| g.attr("transform", `translate(${center.x + e.transform.x},${center.y + e.transform.y}) scale(${e.transform.k})`); | |
| })); | |
| if(roadmapMode === 'custom') return renderCustomRoadmap(g, svg); | |
| // Helper: Summarize Title | |
| const summarize = (str) => { | |
| const stops = ["Understanding", "Introduction", "to", "The", "Basics", "of", "How", "Works", "What", "is", "a", "Explained"]; | |
| const words = str.split(' ').filter(w => !stops.includes(w)); | |
| return words.slice(0, 3).join(' '); | |
| }; | |
| // 2. Data Hierarchy Building | |
| const catMap = { | |
| "Foundations": "FOUNDATIONS", "Economics": "FOUNDATIONS", "Regulations": "FOUNDATIONS", | |
| "Analysis": "ANALYSIS", | |
| "Strategy": "STRATEGY", "Psychology": "ADVANCED", | |
| "Advanced": "ADVANCED" | |
| }; | |
| const subThemes = { | |
| "FOUNDATIONS": ["Basics", "Mechanics", "Macro"], | |
| "ANALYSIS": ["Financials", "Technical", "Valuation"], | |
| "STRATEGY": ["Portfolios", "Trading", "Risk"], | |
| "ADVANCED": ["Derivatives", "Global", "Psychology"] | |
| }; | |
| const rootData = { name: "Academy", type: "core", children: [] }; | |
| const catNodes = {}; | |
| // Create Categories | |
| ["FOUNDATIONS", "ANALYSIS", "STRATEGY", "ADVANCED"].forEach(c => { | |
| const node = { name: c, type: "category", children: [] }; | |
| rootData.children.push(node); | |
| catNodes[c] = node; | |
| subThemes[c].forEach(st => { | |
| node.children.push({ name: st, type: "subtheme", children: [] }); | |
| }); | |
| }); | |
| // Distribute Modules | |
| const findSubNode = (cat, subName) => catNodes[cat].children.find(c => c.name === subName); | |
| masterData.forEach((m, i) => { | |
| const cName = catMap[m.cat] || "ADVANCED"; | |
| const themes = subThemes[cName]; | |
| const targetSub = themes[i % themes.length]; | |
| const p = findSubNode(cName, targetSub); | |
| // Summarize Name Here | |
| if(p) p.children.push({ ...m, name: summarize(m.name), type: "module", full_name: m.name }); | |
| }); | |
| // 3. Layout Initialization | |
| const root = d3.hierarchy(rootData); | |
| // Increased Radii for Horizontal Text | |
| const R_CAT = 140; | |
| const R_SUB = 260; | |
| const R_MOD = 420; // Pushed out for spacing | |
| const treeLayout = d3.tree().size([2 * Math.PI, R_MOD]); | |
| treeLayout(root); | |
| const nodes = root.descendants(); | |
| const links = root.links(); | |
| nodes.forEach(d => { | |
| const angle = d.x - Math.PI / 2; | |
| const r = d.depth === 0 ? 0 : (d.depth === 1 ? R_CAT : (d.depth === 2 ? R_SUB : R_MOD + (Math.random()*60-30))); | |
| d.x = Math.cos(angle) * r; | |
| d.y = Math.sin(angle) * r; | |
| // Increased Node Sizes | |
| d.r = d.depth === 0 ? 40 : (d.depth === 1 ? 18 : (d.depth === 2 ? 8 : 8)); // Modules: 4->8 (Double size) | |
| }); | |
| // 4. Visuals | |
| const orbits = [R_CAT, R_SUB, R_MOD]; | |
| g.append("g").attr("class", "orbits") | |
| .selectAll("circle").data(orbits).join("circle") | |
| .attr("r", d => d).attr("fill", "none").attr("stroke", "#e2e8f0").attr("stroke-width", 1).attr("stroke-dasharray", "4 4"); | |
| // 5. Force Simulation with Label Collision Logic | |
| const simulation = d3.forceSimulation(nodes) | |
| .force("link", d3.forceLink(links).id(d => d.id).strength(0.8)) | |
| .force("charge", d3.forceManyBody().strength(d => d.depth === 0 ? -1500 : -100)) | |
| // Large collision radius to account for horizontal text width (~60px) | |
| .force("collide", d3.forceCollide(d => d.depth === 3 ? 45 : d.r + 20)) | |
| .force("radial", d3.forceRadial(d => { | |
| if(d.depth === 0) return 0; | |
| if(d.depth === 1) return R_CAT; | |
| if(d.depth === 2) return R_SUB; | |
| return R_MOD; | |
| }, 0, 0).strength(0.8)); | |
| // 6. Draw Links | |
| const link = g.append("g").selectAll("path") | |
| .data(links).join("path") | |
| .attr("fill", "none").attr("stroke", "#cbd5e1") | |
| .attr("stroke-width", d => d.target.depth === 1 ? 2 : 1).attr("stroke-opacity", 0.6); | |
| // 7. Draw Nodes | |
| const colorMap = { 1: "#33CCFF", 2: "#2563EB", 3: "#7C3AED" }; | |
| const node = g.append("g").selectAll("g") | |
| .data(nodes).join("g") | |
| .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended)) | |
| .on("click", (e, d) => { if (d.data.type === "module") openModule(d.data.id); }); | |
| node.append("circle") | |
| .attr("r", d => d.r) | |
| .attr("fill", d => { | |
| if(d.depth === 0) return "#FFD60A"; | |
| if(d.depth === 1) return "#fff"; | |
| if(d.depth === 2) return "#94a3b8"; | |
| if(d.data.completed) return "#10b981"; | |
| return colorMap[d.data.difficulty] || "#003399"; | |
| }) | |
| .attr("stroke", d => d.depth === 1 ? "#003399" : "#fff") | |
| .attr("stroke-width", d => d.depth === 1 ? 3 : 2) // Thicker stroke | |
| .style("filter", d => d.depth === 0 ? "url(#sun-glow)" : "") | |
| .style("cursor", d => d.data.type === "module" ? "pointer" : "default"); | |
| // 8. Labels (Horizontal & Collision Aware) | |
| const label = g.append("g").selectAll("text") | |
| .data(nodes.filter(d => d.depth > 0)) | |
| .join("text") | |
| .text(d => d.data.name) // Already summarized | |
| .attr("class", d => { | |
| if(d.depth === 1) return "text-[10px] font-black fill-gray-600 uppercase tracking-widest"; | |
| if(d.depth === 2) return "text-[9px] font-bold fill-gray-400 uppercase"; | |
| return "text-[9px] font-bold fill-gray-700"; // Bolder font | |
| }) | |
| .attr("text-anchor", "middle") | |
| .attr("dy", d => d.depth === 3 ? "1.5em" : "0.35em") // Modules: Text below node | |
| .style("text-shadow", "0 2px 4px white, 0 0 4px white") | |
| .style("pointer-events", "none"); // Click-through to node | |
| g.append("text").attr("text-anchor", "middle").attr("dy", "0.35em") | |
| .text("ACADEMY").attr("class", "text-[10px] font-black fill-[#b45309] uppercase tracking-widest pointer-events-none"); | |
| // Tick Update | |
| simulation.on("tick", () => { | |
| link.attr("d", d => `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`); | |
| node.attr("transform", d => `translate(${d.x},${d.y})`); | |
| // Simple Horizontal Labels attached to nodes | |
| label.attr("x", d => d.x).attr("y", d => d.y); | |
| }); | |
| function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } | |
| function dragged(event, d) { d.fx = event.x; d.fy = event.y; } | |
| function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } | |
| } | |
| function renderCustomRoadmap(g, svg) { | |
| svg.attr("class", "w-full h-full bg-slate-900 rounded-[40px]"); if(customNodes.length === 0) customNodes = masterData.slice(0, 4).map((d,i) => ({...d, x: 200+i*250, y: 300})); const gLinks = g.append("g"), gNodes = g.append("g"); | |
| const update = () => { | |
| const linkGen = d => { const midX = (d.source.x + d.target.x) / 2; return `M ${d.source.x+70} ${d.source.y} C ${midX} ${d.source.y}, ${midX} ${d.target.y}, ${d.target.x-70} ${d.target.y}`; }; | |
| gLinks.selectAll(".custom-line").data(customLinks).join("path").attr("class", "custom-line").attr("d", linkGen).on("click", (e, d) => { d3.selectAll(".custom-line").classed("active", false); d3.select(e.target).classed("active", true); openConfirm("Disconnect?", "Break this link?", () => { customLinks = customLinks.filter(l => l !== d); update(); }); }); | |
| gNodes.selectAll("g.node-unit").data(customNodes, d => d.id).join(enter => { | |
| const grp = enter.append("g").attr("class", "node-unit").on("dblclick", (e, d) => openConfirm("Delete?", `Remove "${d.name}"?`, () => { customNodes = customNodes.filter(n => n.id !== d.id); customLinks = customLinks.filter(l => l.source.id !== d.id && l.target.id !== d.id); initRoadmap(); })).call(d3.drag().on("drag", (e, d) => { d.x = e.x; d.y = e.y; grp.attr("transform", `translate(${d.x},${d.y})`); update(); })); | |
| grp.append("rect").attr("width", 140).attr("height", 50).attr("x", -70).attr("y", -25).attr("rx", 15).attr("fill", "#1e293b").attr("stroke", "#334155").attr("stroke-width", 2); grp.append("text").attr("text-anchor", "middle").attr("dy", 5).attr("fill", "#fff").attr("class", "text-[9px] font-black").text(d => d.name); | |
| grp.append("circle").attr("class", "node-port out").attr("r", 6).attr("cx", 70).on("click", function(e, d) { e.stopPropagation(); resetPorts(); activePort = {node: d, type: 'out'}; d3.select(this).classed("active", true); }); | |
| grp.append("circle").attr("class", "node-port in").attr("r", 6).attr("cx", -70).on("click", function(e, d) { e.stopPropagation(); if(activePort && activePort.node !== d && activePort.type === 'out') { const source = activePort.node; d3.select(this).classed("active", true); openConfirm("Link?", `Connect to "${d.name}"?`, () => { customLinks.push({source: source, target: d}); resetPorts(); update(); }, resetPorts); } }); | |
| return grp; | |
| }).attr("transform", d => `translate(${d.x},${d.y})`); | |
| }; update(); | |
| } | |
| function resetPorts() { d3.selectAll(".node-port").classed("active", false); activePort = null; } | |
| function openConfirm(title, desc, onOk, onCancel) { const m = document.getElementById('confirm-modal'); document.getElementById('modal-title').innerText = title; document.getElementById('modal-desc').innerText = desc; m.style.display = 'flex'; document.getElementById('modal-ok').onclick = () => { m.style.display = 'none'; onOk(); }; window.closeConfirm = () => { m.style.display = 'none'; if(onCancel) onCancel(); }; } | |
| function openNodeModal() { document.getElementById('node-modal').style.display = 'flex'; } | |
| function closeNodeModal() { document.getElementById('node-modal').style.display = 'none'; } | |
| function addCustomNode() { const val = document.getElementById('node-input').value; if(!val) return; customNodes.push({id: "c-"+Date.now(), name: val, x: 200, y: 200}); closeNodeModal(); document.getElementById('node-input').value=''; initRoadmap(); } | |
| function switchAcademy(tab) { document.getElementById('grid-blocks').classList.toggle('hidden', tab !== 'blocks'); document.getElementById('roadmap-box').classList.toggle('hidden', tab !== 'roadmap'); document.getElementById('roadmap-controls').classList.toggle('hidden', tab !== 'roadmap'); document.getElementById('tab-blocks').classList.toggle('active', tab === 'blocks'); document.getElementById('tab-roadmap').classList.toggle('active', tab === 'roadmap'); if(tab === 'roadmap') setTimeout(initRoadmap, 50); } | |
| function closeModule() { | |
| document.getElementById('video-target').innerHTML = ''; | |
| document.getElementById('overlay').classList.remove('active'); | |
| setTimeout(() => { document.getElementById('overlay').style.display = 'none'; }, 500); | |
| } | |
| function renderStatusBox(isDone) { | |
| const box = document.getElementById('status-box'); | |
| box.className = isDone ? "p-16 rounded-[50px] text-center border-2 border-green-200 bg-green-50 mb-20 shadow-inner" : "p-16 rounded-[50px] text-center border-2 border-dashed border-gray-200 bg-gray-50 mb-20"; | |
| box.innerHTML = isDone ? | |
| `<div class="text-green-600 mb-6"><i class="bi bi-patch-check-fill text-6xl"></i></div><h3 class="text-3xl font-black mb-8">Lesson Mastered!</h3><div class="flex justify-center items-center gap-4"><button onclick="toggleStatus(false)" class="px-8 py-3 border text-gray-400 rounded-full text-xs font-bold hover:text-black transition">Reset</button><button onclick="closeModule(); switchAcademy('roadmap')" class="px-8 py-3 bg-[#003399] text-white rounded-full text-xs font-bold uppercase shadow-lg hover:bg-blue-800 transition">View Roadmap →</button></div>` : | |
| `<h3 class="text-3xl font-black mb-8 text-gray-400">NOT COMPLETED</h3><button onclick="toggleStatus(true)" class="px-12 py-5 bg-[#003399] text-white rounded-full text-xs font-black uppercase shadow-xl">Mark as Completed</button>`; | |
| } | |
| async function toggleStatus(isDone) { await fetch('/api/complete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: currentID, status: isDone}) }); renderStatusBox(isDone); loadData(); } | |
| function sortComments(type) { currentSort = type; document.getElementById('sort-top').classList.toggle('active', type === 'top'); document.getElementById('sort-newest').classList.toggle('active', type === 'newest'); fetch(`/api/academy/${currentID}`).then(r => r.json()).then(d => renderComments(d.comments)); } | |
| function showToast(msg) { const t = document.getElementById('toast'); t.innerText = msg; t.style.opacity = '1'; setTimeout(() => t.style.opacity = '0', 2000); } | |
| // --- Lab Logic --- | |
| let currentStock = null, currentPrice = 0, klineChart = null, selectedStocks = []; let portfolioData = { cash: 100000, holdings: {}, history: [] }; | |
| async function loadLabData() { try { await loadSectorStocks(); const res = await fetch('/api/lab/portfolio'); if (res.ok) { portfolioData = await res.json(); updatePortfolioUI(); } initKlineChart(); } catch (err) { console.error('Lab load error:', err); } } | |
| async function loadSectorStocks() { const sector = document.getElementById('sector-select').value; const stockList = document.getElementById('stock-list'); try { const poolRes = await fetch('/api/lab/stocks'); const stockPool = await poolRes.json(); const symbols = stockPool[sector] || []; const quoteRes = await fetch(`/api/lab/quote?symbols=${symbols.join(',')}`); const quoteData = await quoteRes.json(); stockList.innerHTML = ''; quoteData.data.forEach(stock => { const changeClass = stock.change >= 0 ? 'price-up' : 'price-down'; const changeIcon = stock.change >= 0 ? '▲' : '▼'; const card = document.createElement('div'); card.className = 'stock-card'; card.onclick = () => selectStock(stock); card.innerHTML = `<div class="flex justify-between items-start mb-1"><div><div class="font-bold text-xs">${stock.name}</div><div class="text-[10px] text-gray-400">${stock.symbol}</div></div><div class="text-right"><div class="font-black text-sm">$${stock.price.toFixed(2)}</div><div class="text-[10px] ${changeClass}">${changeIcon} ${stock.change_pct.toFixed(2)}%</div></div></div>`; stockList.appendChild(card); }); } catch (err) { stockList.innerHTML = '<div class="text-center text-red-500 p-4 text-xs">Failed to load</div>'; } } | |
| function selectStock(stock) { currentStock = stock; currentPrice = stock.price; document.querySelectorAll('.stock-card').forEach(el => el.classList.remove('selected')); event.currentTarget.classList.add('selected'); document.getElementById('chart-title').innerText = `${stock.name} (${stock.symbol})`; document.getElementById('trade-symbol').value = stock.symbol; document.getElementById('trade-price').value = `$${stock.price.toFixed(2)}`; loadKlineData(stock.symbol); } | |
| function initKlineChart() { klineChart = echarts.init(document.getElementById('kline-chart')); klineChart.setOption({ title: { text: 'Select a stock', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 14 } }, grid: { left: 60, right: 60, top: 40, bottom: 60 } }); } | |
| async function loadKlineData(symbol, days = 60) { try { const res = await fetch(`/api/lab/kline?symbol=${symbol}&days=${days}`); const data = await res.json(); const dates = data.data.map(d => d.date); const values = data.data.map(d => [d.open, d.close, d.low, d.high]); klineChart.setOption({ xAxis: { type: 'category', data: dates }, yAxis: { scale: true }, series: [{ type: 'candlestick', data: values, itemStyle: { color: '#ef4444', color0: '#10b981' } }] }); } catch (err) {} } | |
| function calculateTotal() { const shares = parseInt(document.getElementById('trade-shares').value) || 0; document.getElementById('trade-total').value = `$${(shares * currentPrice).toFixed(2)}`; } | |
| async function executeTrade(action) { const symbol = document.getElementById('trade-symbol').value; const shares = parseInt(document.getElementById('trade-shares').value); if (!symbol || !shares || shares < 100) { alert('Invalid trade'); return; } try { const res = await fetch('/api/lab/trade', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, symbol, shares, price: currentPrice }) }); const data = await res.json(); if (data.status === 'success') { showToast(action.toUpperCase() + ' SUCCESS'); loadLabData(); document.getElementById('trade-shares').value = ''; } else { alert(data.message); } } catch (err) { alert('Trade failed'); } } | |
| function updatePortfolioUI() { const totalAssets = portfolioData.cash + Object.values(portfolioData.holdings).reduce((sum, h) => sum + (h.shares * h.avg_price), 0); document.getElementById('total-assets').innerText = `$${totalAssets.toLocaleString()}`; document.getElementById('available-cash').innerText = `$${portfolioData.cash.toLocaleString()}`; const profit = totalAssets - 100000; document.getElementById('total-profit').innerText = `$${profit.toLocaleString()}`; document.getElementById('total-profit').className = profit >= 0 ? 'text-lg font-bold price-up' : 'text-lg font-bold price-down'; const hList = document.getElementById('holdings-list'); hList.innerHTML = Object.entries(portfolioData.holdings).length ? Object.entries(portfolioData.holdings).map(([s, h]) => `<div class="holding-card"><div class="flex justify-between items-center"><div><div class="font-bold text-xs">${s}</div><div class="text-[10px] text-gray-500">${h.shares} @ $${h.avg_price.toFixed(2)}</div></div><div class="text-xs font-bold">$${(h.shares * h.avg_price).toFixed(2)}</div></div></div>`).join('') : '<div class="text-center text-gray-400 py-4 text-xs">No holdings</div>'; } | |
| async function resetPortfolio() { if (confirm('Reset portfolio?')) { await fetch('/api/lab/reset', { method: 'POST' }); loadLabData(); } } | |
| function refreshLab() { loadLabData(); showToast('REFRESHED'); } | |
| // --- Agent Logic (Refactored) --- | |
| let currentSessionId = null; | |
| let isGenerating = false; | |
| let abortController = null; | |
| async function loadAgentData() { | |
| loadAgentStocks(); | |
| loadChatSessions(); | |
| } | |
| async function loadAgentStocks() { | |
| const sector = document.getElementById('agent-sector').value; | |
| try { | |
| const poolRes = await fetch('/api/lab/stocks'); | |
| const stockPool = await poolRes.json(); | |
| const symbols = stockPool[sector] || []; | |
| const quoteRes = await fetch(`/api/lab/quote?symbols=${symbols.join(',')}`); | |
| const quoteData = await quoteRes.json(); | |
| document.getElementById('agent-stock-tags').innerHTML = quoteData.data.map(stock => `<div class="stock-tag" onclick="addStockToChat({name:'${stock.name}',symbol:'${stock.symbol}'})"><span>${stock.name}</span></div>`).join(''); | |
| } catch (err) {} | |
| } | |
| function addStockToChat(stock) { | |
| if (selectedStocks.find(s => s.symbol === stock.symbol)) return; | |
| selectedStocks.push(stock); | |
| updateSelectedStocks(); | |
| const input = document.getElementById('agent-input'); | |
| input.value += `@${stock.name}(${stock.symbol}) `; | |
| input.focus(); | |
| autoResize(input); | |
| } | |
| function updateSelectedStocks() { | |
| document.getElementById('selected-stocks').innerHTML = selectedStocks.map((s, i) => `<div class="stock-tag"><span>${s.name}</span><i class="bi bi-x-circle" onclick="removeStock(${i})"></i></div>`).join(''); | |
| } | |
| function removeStock(i) { selectedStocks.splice(i, 1); updateSelectedStocks(); } | |
| async function sendMessage() { | |
| const input = document.getElementById('agent-input'); | |
| const msg = input.value.trim(); | |
| if (isGenerating) { | |
| // STOP Logic | |
| if (abortController) abortController.abort(); | |
| isGenerating = false; | |
| updateSendButtonState(); | |
| return; | |
| } | |
| if (!msg) return; | |
| // clear input & add user message | |
| input.value = ''; | |
| autoResize(input); | |
| addMessageToUI('user', msg); | |
| // Start Generation | |
| isGenerating = true; | |
| updateSendButtonState(); | |
| abortController = new AbortController(); | |
| const loadingId = addMessageToUI('assistant', '<div class="animate-pulse">Thinking...</div>'); | |
| try { | |
| const res = await fetch('/api/agent/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message: msg, session_id: currentSessionId }), | |
| signal: abortController.signal | |
| }); | |
| if (!res.ok) throw new Error("Network response was not ok"); | |
| const data = await res.json(); | |
| // Update current session ID if it was new | |
| if (data.session_id && currentSessionId !== data.session_id) { | |
| currentSessionId = data.session_id; | |
| loadChatSessions(); // Refresh sidebar to show new session | |
| } | |
| document.getElementById(loadingId).remove(); | |
| addMessageToUI('assistant', data.response || 'No response', data.tools_used); | |
| } catch (err) { | |
| document.getElementById(loadingId).remove(); | |
| if (err.name === 'AbortError') { | |
| addMessageToUI('assistant', '<i>Generation stopped by user.</i>'); | |
| } else { | |
| addMessageToUI('assistant', '<i>Error generating response. Please try again.</i>'); | |
| } | |
| } finally { | |
| isGenerating = false; | |
| abortController = null; | |
| updateSendButtonState(); | |
| } | |
| } | |
| function updateSendButtonState() { | |
| const btn = document.getElementById('send-btn'); | |
| const icon = btn.querySelector('i'); | |
| if (isGenerating) { | |
| btn.className = "send-btn stop"; | |
| icon.className = "bi bi-stop-fill"; | |
| btn.title = "Stop Generation"; | |
| } else { | |
| btn.className = "send-btn active"; | |
| icon.className = "bi bi-send-fill"; | |
| btn.title = "Send Message"; | |
| } | |
| } | |
| function addMessageToUI(role, content, tools = []) { | |
| const div = document.getElementById('agent-messages'); | |
| if (div.querySelector('.text-center')) div.innerHTML = ''; | |
| const id = 'msg-' + Date.now(); | |
| const b = document.createElement('div'); | |
| b.id = id; | |
| b.className = `message-bubble ${role}`; | |
| // Basic Parsing for "Claude Style" | |
| let formattedContent = content; | |
| // Extract JSON Chart block | |
| const chartRegex = /```json-chart\s*([\s\S]*?)```/; | |
| let chartData = null; | |
| const match = content.match(chartRegex); | |
| if (match) { | |
| try { | |
| chartData = JSON.parse(match[1]); | |
| formattedContent = content.replace(match[0], `<div id="chart-${id}" class="chat-chart-container"></div>`); | |
| } catch (e) { console.error("Chart JSON Parse Error", e); } | |
| } | |
| // Simple Markdown formatting | |
| formattedContent = formattedContent | |
| .replace(/^### (.*$)/gim, '<div class="msg-header">$1</div>') | |
| .replace(/^- (.*$)/gim, '<li>$1</li>') | |
| .replace(/(<li>.*<\/li>)/gim, '<ul class="msg-list">$1</ul>') | |
| .replace(/\n/g, '<br>'); | |
| b.innerHTML = ` | |
| <div class="message-content"> | |
| ${role === 'assistant' ? '<div class="text-[10px] font-bold text-[#003399] mb-2 uppercase tracking-widest">U2CHAT AI</div>' : ''} | |
| ${formattedContent} | |
| </div> | |
| <div class="message-timestamp ${role === 'user' ? 'text-right' : ''}">${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div> | |
| `; | |
| div.appendChild(b); | |
| div.scrollTop = div.scrollHeight; | |
| // Render Chart if present | |
| if (chartData && role === 'assistant') { | |
| setTimeout(() => renderChart(`chart-${id}`, chartData), 100); | |
| } | |
| return id; | |
| } | |
| function renderChart(containerId, data) { | |
| const chartDom = document.getElementById(containerId); | |
| if (!chartDom) return; | |
| const myChart = echarts.init(chartDom); | |
| const option = { | |
| title: { text: data.title || 'Data Analysis', left: 'center', textStyle: { fontSize: 14 } }, | |
| tooltip: { trigger: 'axis' }, | |
| grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, | |
| xAxis: { type: 'category', data: data.labels || [] }, | |
| yAxis: { type: 'value' }, | |
| series: [{ | |
| data: data.data || [], | |
| type: data.type || 'line', | |
| smooth: true, | |
| itemStyle: { color: '#003399' }, | |
| areaStyle: { opacity: 0.1 } | |
| }] | |
| }; | |
| myChart.setOption(option); | |
| new ResizeObserver(() => myChart.resize()).observe(chartDom); | |
| } | |
| async function loadChatSessions() { | |
| try { | |
| const res = await fetch('/api/agent/sessions'); | |
| const data = await res.json(); | |
| const sessions = data.sessions || []; | |
| const list = document.getElementById('chat-sessions'); | |
| list.innerHTML = sessions.length ? '' : '<div class="text-xs text-gray-400 p-2">No history</div>'; | |
| sessions.forEach(s => { | |
| const item = document.createElement('div'); | |
| item.className = `text-xs p-3 hover:bg-gray-100 rounded-lg cursor-pointer truncate border-b border-gray-100 transition ${currentSessionId === s.id ? 'bg-blue-50 text-[#003399] font-bold' : 'text-gray-600'}`; | |
| item.innerText = s.title; | |
| item.onclick = () => loadChatSession(s.id); | |
| list.appendChild(item); | |
| }); | |
| // If currentSessionId is null but there are sessions, verify if we should load latest? | |
| // For now, let's keep "New Chat" as default state if page reloads without persistent state. | |
| } catch (err) {} | |
| } | |
| async function loadChatSession(sessionId) { | |
| currentSessionId = sessionId; | |
| loadChatSessions(); // Update active state in sidebar | |
| try { | |
| const res = await fetch(`/api/agent/history?session_id=${sessionId}`); | |
| const data = await res.json(); | |
| const history = data.history || []; | |
| document.getElementById('agent-messages').innerHTML = ''; | |
| history.forEach(m => addMessageToUI(m.role, m.content)); | |
| } catch (err) {} | |
| } | |
| async function clearChat() { | |
| if (currentSessionId && confirm('Clear this chat?')) { | |
| await fetch('/api/agent/clear', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: currentSessionId }) | |
| }); | |
| newChat(); // Reset to new chat state | |
| loadChatSessions(); // Refresh list | |
| } | |
| } | |
| function newChat() { | |
| currentSessionId = null; | |
| document.getElementById('agent-messages').innerHTML = '<div class="text-center py-20"><div class="text-6xl mb-4">💬</div><h3 class="text-2xl font-bold text-gray-700 mb-2">Start a conversation</h3><p class="text-gray-500">Ask about stocks, trends, or strategies</p></div>'; | |
| selectedStocks = []; | |
| updateSelectedStocks(); | |
| loadChatSessions(); // Refresh list to remove active highlight | |
| } | |
| function handleAgentKey(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function autoResize(t) { | |
| t.style.height = 'auto'; | |
| t.style.height = Math.min(t.scrollHeight, 200) + 'px'; | |
| } | |
| // --- Guide Logic --- | |
| let guideStep = 1; | |
| const guideData = [ | |
| { | |
| step: 1, | |
| title: "The Concept of Ownership", | |
| desc: "Buying a stock isn't just betting on numbers. It means you legally own a small piece of that company's future earnings and assets.", | |
| content: `<div class="text-center"><div class="w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6 text-4xl text-[#003399] shadow-inner"><i class="bi bi-building-check"></i></div><p class="font-bold text-gray-700 text-xl">Example: Buying 1 share of Moutai means you are a shareholder.</p></div>` | |
| }, | |
| { | |
| step: 2, | |
| title: "Risk & Your $100k", | |
| desc: "You have $100,000 in virtual cash. Never put it all in one basket. 'Diversification' means spreading your money across different stocks to reduce risk.", | |
| content: `<div class="grid grid-cols-2 gap-8 text-center w-full max-w-md"><div class="p-8 bg-white rounded-[30px] border-2 border-green-100 shadow-sm"><i class="bi bi-pie-chart-fill text-5xl text-green-500 mb-4 block"></i><span class="text-sm font-black uppercase text-green-600">Diversified</span><p class="text-xs text-gray-400 mt-2">Safer growth</p></div><div class="p-8 bg-white rounded-[30px] border-2 border-red-100 shadow-sm"><i class="bi bi-exclamation-triangle-fill text-5xl text-red-500 mb-4 block"></i><span class="text-sm font-black uppercase text-red-600">All-in</span><p class="text-xs text-gray-400 mt-2">High risk</p></div></div>` | |
| }, | |
| { | |
| step: 3, | |
| title: "How to Use the Lab", | |
| desc: "1. Select a stock. 2. Check the chart. 3. Enter shares. 4. Buy.", | |
| content: `<div class="bg-white p-8 rounded-[30px] border-2 border-gray-100 shadow-xl w-full max-w-sm transform hover:scale-105 transition duration-500"><div class="flex justify-between mb-6 border-b border-gray-100 pb-4"><div class="flex flex-col text-left"><span class="font-black text-lg text-gray-800">Moutai</span><span class="text-xs text-gray-400">600519</span></div><span class="text-green-500 font-black text-xl">$1800.00</span></div><div class="flex gap-3 mb-2"><input type="number" value="100" class="w-full p-3 border-2 border-gray-200 rounded-xl bg-gray-50 text-center font-bold outline-none" disabled><button class="bg-[#10b981] text-white px-6 py-3 rounded-xl font-black shadow-lg hover:bg-green-600 transition" onclick="setLabView('dashboard')">BUY</button></div><div class="text-[10px] text-gray-400 text-center uppercase tracking-widest mt-4">Simulated Trade Panel</div></div>` | |
| } | |
| ]; | |
| function updateGuideUI() { | |
| const data = guideData[guideStep - 1]; | |
| document.getElementById('guide-step-indicator').innerText = `Step ${guideStep} of 3`; | |
| document.getElementById('guide-title').innerText = data.title; | |
| document.getElementById('guide-desc').innerText = data.desc; | |
| document.getElementById('guide-content').innerHTML = data.content; | |
| const nextBtn = document.getElementById('guide-next-btn'); | |
| const prevBtn = document.getElementById('guide-prev-btn'); | |
| prevBtn.classList.toggle('hidden', guideStep === 1); | |
| if (guideStep === 3) { | |
| nextBtn.innerText = "Enter Lab →"; | |
| nextBtn.onclick = () => setLabView('dashboard'); | |
| nextBtn.className = "px-10 py-4 bg-green-500 text-white rounded-full font-bold shadow-xl hover:scale-105 transition hover:bg-green-600"; | |
| } else { | |
| nextBtn.innerText = "Next Step →"; | |
| nextBtn.onclick = nextGuideStep; | |
| nextBtn.className = "px-10 py-4 bg-[#003399] text-white rounded-full font-bold shadow-xl hover:scale-105 transition"; | |
| } | |
| } | |
| function nextGuideStep() { | |
| if (guideStep < 3) { | |
| guideStep++; | |
| updateGuideUI(); | |
| } | |
| } | |
| function prevGuideStep() { | |
| if (guideStep > 1) { | |
| guideStep--; | |
| updateGuideUI(); | |
| } | |
| } | |
| function setLabView(view) { | |
| document.getElementById('lab-gateway').classList.add('hidden'); | |
| document.getElementById('lab-gateway').classList.remove('flex'); // Remove flex when hidden | |
| document.getElementById('lab-dashboard').classList.add('hidden'); | |
| document.getElementById('lab-guide').classList.add('hidden'); | |
| if (view === 'gateway') { | |
| document.getElementById('lab-gateway').classList.remove('hidden'); | |
| document.getElementById('lab-gateway').classList.add('flex'); // Restore flex | |
| } else if (view === 'dashboard') { | |
| document.getElementById('lab-dashboard').classList.remove('hidden'); | |
| loadLabData(); // Ensure data is loaded | |
| } else if (view === 'guide') { | |
| guideStep = 1; // Reset step | |
| updateGuideUI(); // Initial render | |
| document.getElementById('lab-guide').classList.remove('hidden'); | |
| } | |
| } | |
| window.onload = () => { showView('landing'); console.log('U2INVEST loaded successfully'); }; | |
| </script> | |
| </body> | |
| </html> |