Skip to content

Commit e31be3d

Browse files
committed
feat(chatbot-evolution): Add interactive stats, markdown formatting, and UI improvements
- Add clickable architecture items and stats that navigate to relevant eras - Add Plotly log-scale chart showing parameter growth over time (with A-E key) - Add markdown/code formatting for all bot responses (main chat + compare) - Add spinner animation for model loading, bouncing dots for typing - Rename Claude 4 Opus to Claude 4.5 Opus with link to claude.ai - Fix text alignment in architecture section - Add CSS for inline code and code blocks in comparison items
1 parent 1952af1 commit e31be3d

File tree

3 files changed

+234
-36
lines changed

3 files changed

+234
-36
lines changed

demos/chatbot-evolution/css/chatbot-evolution.css

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,20 @@ a.paper-item:hover {
412412
border-radius: 8px;
413413
}
414414

415+
.stat-item.clickable-stat,
416+
.stat-item.external-link {
417+
cursor: pointer;
418+
transition: all 0.2s ease;
419+
border: 1px solid transparent;
420+
}
421+
422+
.stat-item.clickable-stat:hover,
423+
.stat-item.external-link:hover {
424+
background: var(--surface-hover);
425+
border-color: var(--primary-color);
426+
transform: translateY(-2px);
427+
}
428+
415429
.stat-label {
416430
font-size: 0.85em;
417431
font-weight: 600;
@@ -948,6 +962,25 @@ a.paper-item:hover {
948962
}
949963
}
950964

965+
/* Inline spinner for comparison panel */
966+
.inline-spinner {
967+
display: inline-block;
968+
width: 14px;
969+
height: 14px;
970+
border: 2px solid var(--border-color);
971+
border-top-color: var(--primary-color);
972+
border-radius: 50%;
973+
animation: spin 0.8s linear infinite;
974+
vertical-align: middle;
975+
flex-shrink: 0;
976+
}
977+
978+
.loading-indicator {
979+
display: inline-flex !important;
980+
align-items: center;
981+
padding: 8px 12px;
982+
}
983+
951984
.loading-text {
952985
font-size: 1.1em;
953986
font-weight: 600;
@@ -1603,10 +1636,43 @@ a.paper-item:hover {
16031636
font-family: system-ui, sans-serif;
16041637
}
16051638

1606-
.message .inline-code {
1639+
.message .inline-code,
1640+
.comparison-item .inline-code {
16071641
background: var(--surface-hover);
16081642
padding: 2px 6px;
16091643
border-radius: 4px;
16101644
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
16111645
font-size: 0.9em;
16121646
}
1647+
1648+
.comparison-item .code-block {
1649+
background: var(--bg-color);
1650+
border: 1px solid var(--border-color);
1651+
border-radius: 6px;
1652+
padding: 10px;
1653+
margin: 6px 0;
1654+
overflow-x: auto;
1655+
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
1656+
font-size: 0.8em;
1657+
line-height: 1.4;
1658+
}
1659+
1660+
.comparison-item .code-block code {
1661+
background: none;
1662+
padding: 0;
1663+
}
1664+
1665+
.comparison-item .code-block::before {
1666+
content: attr(data-language);
1667+
display: block;
1668+
font-size: 0.7em;
1669+
color: var(--text-secondary);
1670+
margin-bottom: 6px;
1671+
text-transform: uppercase;
1672+
font-family: system-ui, sans-serif;
1673+
}
1674+
1675+
.comparison-item .bot-response {
1676+
display: block;
1677+
margin-top: 4px;
1678+
}

demos/chatbot-evolution/index.html

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -625,27 +625,28 @@ <h3>Key Papers</h3>
625625
<div class="panel stats-panel">
626626
<h3>Evolution Stats</h3>
627627
<div class="stats-grid">
628-
<div class="stat-item">
628+
<div class="stat-item clickable-stat" data-era="1960s">
629629
<span class="stat-label">ELIZA (1966)</span>
630630
<span class="stat-value">~200 rules</span>
631631
</div>
632-
<div class="stat-item">
632+
<div class="stat-item clickable-stat" data-era="1990s">
633633
<span class="stat-label">ALICE (1995)</span>
634634
<span class="stat-value">~41K patterns</span>
635635
</div>
636-
<div class="stat-item">
636+
<div class="stat-item clickable-stat" data-era="2010s">
637637
<span class="stat-label">BlenderBot (2020)</span>
638638
<span class="stat-value">90M params</span>
639639
</div>
640-
<div class="stat-item">
640+
<div class="stat-item clickable-stat" data-era="2020s">
641641
<span class="stat-label">SmolLM2 (2024)</span>
642642
<span class="stat-value">135M-360M params</span>
643643
</div>
644-
<div class="stat-item">
645-
<span class="stat-label">Claude 4 Opus (2025)</span>
644+
<div class="stat-item external-link" data-url="https://claude.ai/">
645+
<span class="stat-label">Claude 4.5 Opus (2025)</span>
646646
<span class="stat-value">~2T params*</span>
647647
</div>
648648
</div>
649+
<div id="evolution-chart" style="width: 100%; height: 200px; margin-top: 15px;"></div>
649650
<p class="stats-note">*Estimated, not public</p>
650651
</div>
651652

@@ -654,6 +655,7 @@ <h3>Evolution Stats</h3>
654655
</div>
655656

656657
<!-- Scripts -->
658+
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
657659
<script type="module" src="js/eliza.js"></script>
658660
<script type="module" src="js/parry.js"></script>
659661
<script type="module" src="js/alice.js"></script>
@@ -688,6 +690,77 @@ <h3>Evolution Stats</h3>
688690
updateThemeIcon(newTheme);
689691
});
690692
}
693+
694+
document.addEventListener('DOMContentLoaded', () => {
695+
document.querySelectorAll('.clickable-stat').forEach(el => {
696+
el.addEventListener('click', () => {
697+
const era = el.dataset.era;
698+
if (era && window.timelineApp) {
699+
window.timelineApp.switchEra(era);
700+
}
701+
});
702+
});
703+
704+
document.querySelectorAll('.external-link').forEach(el => {
705+
el.addEventListener('click', () => {
706+
const url = el.dataset.url;
707+
if (url) window.open(url, '_blank');
708+
});
709+
});
710+
711+
const chartData = [
712+
{ year: 1966, name: 'ELIZA', label: 'A', params: 200 },
713+
{ year: 1995, name: 'ALICE', label: 'B', params: 41000 },
714+
{ year: 2020, name: 'BlenderBot', label: 'C', params: 90000000 },
715+
{ year: 2024, name: 'SmolLM2', label: 'D', params: 360000000 },
716+
{ year: 2025, name: 'Claude 4.5', label: 'E', params: 2000000000000 }
717+
];
718+
719+
const isDark = html.getAttribute('data-theme') === 'dark';
720+
const plotConfig = {
721+
x: chartData.map(d => d.year),
722+
y: chartData.map(d => d.params),
723+
text: chartData.map(d => d.label),
724+
mode: 'lines+markers+text',
725+
type: 'scatter',
726+
textposition: ['top center', 'top center', 'top center', 'bottom center', 'bottom center'],
727+
textfont: { size: 11, color: isDark ? '#e0e0e0' : '#333', family: 'system-ui, sans-serif' },
728+
line: { color: '#6366f1', width: 3 },
729+
marker: { size: 8, color: '#6366f1' }
730+
};
731+
732+
const layout = {
733+
margin: { l: 55, r: 25, t: 40, b: 40 },
734+
paper_bgcolor: 'transparent',
735+
plot_bgcolor: 'transparent',
736+
xaxis: {
737+
title: { text: 'Year', font: { size: 10, color: isDark ? '#aaa' : '#666' } },
738+
tickfont: { size: 9, color: isDark ? '#aaa' : '#666' },
739+
gridcolor: isDark ? '#333' : '#ddd',
740+
range: [1955, 2035]
741+
},
742+
yaxis: {
743+
title: { text: 'Rules / Params', font: { size: 10, color: isDark ? '#aaa' : '#666' } },
744+
type: 'log',
745+
tickfont: { size: 9, color: isDark ? '#aaa' : '#666' },
746+
gridcolor: isDark ? '#333' : '#ddd',
747+
range: [1.5, 13.5]
748+
},
749+
showlegend: false
750+
};
751+
752+
if (document.getElementById('evolution-chart')) {
753+
Plotly.newPlot('evolution-chart', [plotConfig], layout, { displayModeBar: false, responsive: true });
754+
755+
const keyHtml = chartData.map(d =>
756+
`<span style="margin-right:10px;font-size:0.75em;"><strong>${d.label}</strong>=${d.name}</span>`
757+
).join('');
758+
const keyDiv = document.createElement('div');
759+
keyDiv.style.cssText = 'text-align:center;margin-top:5px;color:var(--text-secondary);line-height:1.6;';
760+
keyDiv.innerHTML = keyHtml;
761+
document.getElementById('evolution-chart').parentNode.insertBefore(keyDiv, document.getElementById('evolution-chart').nextSibling);
762+
}
763+
});
691764
</script>
692765
</body>
693766
</html>

demos/chatbot-evolution/js/timeline-app.js

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,40 @@ import { GPTBot } from './gpt-bot.js';
1111
import { ElizaBreakdownRenderer } from '../../eliza/js/eliza-breakdown-renderer.js';
1212
import { RulesViewer } from './rules-viewer.js';
1313

14+
function formatMarkdown(text) {
15+
let html = text;
16+
17+
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
18+
const language = lang || 'plaintext';
19+
const escapedCode = code
20+
.replace(/&/g, '&amp;')
21+
.replace(/</g, '&lt;')
22+
.replace(/>/g, '&gt;')
23+
.trim();
24+
return `<pre class="code-block" data-language="${language}"><code>${escapedCode}</code></pre>`;
25+
});
26+
27+
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
28+
29+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
30+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
31+
32+
html = html.replace(/^### (.+)$/gm, '<strong style="font-size:1.1em;">$1</strong>');
33+
html = html.replace(/^## (.+)$/gm, '<strong style="font-size:1.2em;">$1</strong>');
34+
html = html.replace(/^# (.+)$/gm, '<strong style="font-size:1.3em;">$1</strong>');
35+
36+
html = html.replace(/^[-*] (.+)$/gm, '• $1');
37+
html = html.replace(/^\d+\. (.+)$/gm, (match, item, offset, str) => {
38+
const before = str.substring(0, offset);
39+
const num = (before.match(/^\d+\. /gm) || []).length + 1;
40+
return `${num}. ${item}`;
41+
});
42+
43+
html = html.replace(/\n/g, '<br>');
44+
45+
return html;
46+
}
47+
1448
class TimelineApp {
1549
constructor() {
1650
this.bots = {
@@ -754,8 +788,8 @@ class TimelineApp {
754788

755789
if (useHTML) {
756790
messageDiv.innerHTML = text;
757-
} else if (botName === 'gpt' && type === 'bot' && this.bots.gpt.formatResponseAsHTML) {
758-
messageDiv.innerHTML = this.bots.gpt.formatResponseAsHTML(text);
791+
} else if (type === 'bot') {
792+
messageDiv.innerHTML = formatMarkdown(text);
759793
} else {
760794
messageDiv.textContent = text;
761795
}
@@ -774,17 +808,13 @@ class TimelineApp {
774808
}
775809

776810
const resultsDiv = document.getElementById('comparison-results');
777-
resultsDiv.textContent = ''; // Clear previous results
811+
resultsDiv.textContent = '';
778812

779-
// Get compare button and set loading state
780813
const compareBtn = document.getElementById('compare-btn');
781814
compareBtn.disabled = true;
782815
compareBtn.textContent = 'Comparing...';
783816

784-
// Ensure ELIZA is initialized before getting response
785-
await this.bots.eliza.ensureInitialized();
786-
787-
// Add user message to display
817+
// Show user message IMMEDIATELY before any async operations
788818
const userMsgDiv = document.createElement('div');
789819
userMsgDiv.className = 'comparison-item';
790820
userMsgDiv.style.background = 'var(--primary-color)';
@@ -837,13 +867,14 @@ class TimelineApp {
837867
// Scroll to show new content
838868
resultsDiv.scrollTop = resultsDiv.scrollHeight;
839869

840-
// Get responses - rule-based first (sync), then neural (async)
841870
const updateBotResponse = (bot, response) => {
842871
const item = botDivs[bot];
843-
// Remove typing indicator and add response
844-
const typing = item.querySelector('.typing-indicator');
872+
const typing = item.querySelector('.typing-indicator') || item.querySelector('.loading-indicator');
845873
if (typing) typing.remove();
846-
item.appendChild(document.createTextNode(response));
874+
const responseSpan = document.createElement('span');
875+
responseSpan.className = 'bot-response';
876+
responseSpan.innerHTML = formatMarkdown(response);
877+
item.appendChild(responseSpan);
847878
};
848879

849880
// ELIZA is async (needs to ensure rules loaded)
@@ -876,9 +907,24 @@ class TimelineApp {
876907
const item = botDivs[botName];
877908
const typing = item.querySelector('.typing-indicator');
878909
if (typing) {
879-
typing.innerHTML = '<span class="loading-text">Loading model...</span>';
910+
typing.innerHTML = '';
911+
typing.className = 'loading-indicator';
912+
const spinner = document.createElement('span');
913+
spinner.className = 'inline-spinner';
914+
const text = document.createElement('span');
915+
text.textContent = 'Loading model...';
916+
text.style.marginLeft = '8px';
917+
typing.appendChild(spinner);
918+
typing.appendChild(text);
880919
}
881920
await bot.loadModel();
921+
if (typing) {
922+
typing.innerHTML = '';
923+
typing.className = 'typing-indicator';
924+
for (let i = 0; i < 3; i++) {
925+
typing.appendChild(document.createElement('span'));
926+
}
927+
}
882928
}
883929
return bot.getResponse(prompt);
884930
};
@@ -900,24 +946,37 @@ class TimelineApp {
900946
displayArchitecture() {
901947
const vizDiv = document.getElementById('architecture-viz');
902948

903-
const architectures = {
904-
'ELIZA (1966)': 'Pattern → Rules → Response',
905-
'PARRY (1972)': 'Input → State Machine → Emotional Model → Response',
906-
'ALICE (1995)': 'Input → AIML Parser → Category Match → Response',
907-
'BlenderBot (2020)': 'Input → Encoder → Decoder → Response',
908-
'SmolLM2 (2024)': 'Input → Decoder-Only Transformer → Response'
909-
};
949+
const architectures = [
950+
{ name: 'ELIZA (1966)', arch: 'Pattern → Rules → Response', era: '1960s' },
951+
{ name: 'PARRY (1972)', arch: 'Input → State Machine → Emotional Model → Response', era: '1970s' },
952+
{ name: 'ALICE (1995)', arch: 'Input → AIML Parser → Category Match → Response', era: '1990s' },
953+
{ name: 'BlenderBot (2020)', arch: 'Input → Encoder → Decoder → Response', era: '2010s' },
954+
{ name: 'SmolLM2 (2024)', arch: 'Input → Decoder-Only Transformer → Response', era: '2020s' }
955+
];
910956

911-
let html = '<div style="padding: 15px; text-align: left;">';
912-
for (const [name, arch] of Object.entries(architectures)) {
913-
html += `<div style="margin-bottom: 12px; font-size: 0.8em;">
914-
<strong>${name}</strong><br>
915-
<code style="font-size: 0.85em;">${arch}</code>
916-
</div>`;
917-
}
918-
html += '</div>';
957+
const container = document.createElement('div');
958+
container.style.cssText = 'padding: 15px; text-align: left;';
919959

920-
vizDiv.innerHTML = html;
960+
for (const item of architectures) {
961+
const div = document.createElement('div');
962+
div.style.cssText = 'margin-bottom: 10px; font-size: 0.8em; cursor: pointer; padding: 8px; border-radius: 6px; transition: background 0.2s; line-height: 1.4;';
963+
const strong = document.createElement('strong');
964+
strong.textContent = item.name;
965+
strong.style.display = 'block';
966+
strong.style.marginBottom = '2px';
967+
const code = document.createElement('code');
968+
code.textContent = item.arch;
969+
code.style.cssText = 'font-size: 0.8em; word-break: break-word; display: block;';
970+
div.appendChild(strong);
971+
div.appendChild(code);
972+
div.addEventListener('mouseenter', () => div.style.background = 'var(--surface-hover)');
973+
div.addEventListener('mouseleave', () => div.style.background = 'transparent');
974+
div.addEventListener('click', () => this.switchEra(item.era));
975+
container.appendChild(div);
976+
}
977+
978+
vizDiv.innerHTML = '';
979+
vizDiv.appendChild(container);
921980
}
922981
}
923982

0 commit comments

Comments
 (0)