929 lines
33 KiB
HTML
929 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Advanced Transcript Search - Claude Code DevTools</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--bg-primary: #0d1117;
|
|
--bg-secondary: #161b22;
|
|
--bg-tertiary: #21262d;
|
|
--border-color: #30363d;
|
|
--text-primary: #c9d1d9;
|
|
--text-secondary: #8b949e;
|
|
--text-muted: #6e7681;
|
|
--accent-blue: #58a6ff;
|
|
--accent-green: #3fb950;
|
|
--accent-purple: #bc8cff;
|
|
--accent-orange: #ff9966;
|
|
--accent-red: #f85149;
|
|
--shadow: rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
body {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
header {
|
|
background: var(--bg-secondary);
|
|
padding: 2rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
box-shadow: 0 4px 8px var(--shadow);
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
.tagline {
|
|
color: var(--text-secondary);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
main {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.search-interface {
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.search-box {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
input[type="text"] {
|
|
flex: 1;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
color: var(--text-primary);
|
|
font-family: inherit;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: var(--accent-blue);
|
|
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
|
|
}
|
|
|
|
button {
|
|
padding: 0.75rem 1.5rem;
|
|
background: var(--accent-blue);
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: #000;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
button:hover {
|
|
background: #6cb6ff;
|
|
}
|
|
|
|
button.secondary {
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
button.secondary:hover {
|
|
background: var(--border-color);
|
|
}
|
|
|
|
.filters {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.filter-group label {
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
select {
|
|
padding: 0.5rem;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
color: var(--text-primary);
|
|
font-family: inherit;
|
|
}
|
|
|
|
.stats-bar {
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 1rem 2rem;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid var(--border-color);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.stat-value {
|
|
color: var(--accent-green);
|
|
font-weight: 700;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
#results {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.result-item {
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
border: 1px solid var(--border-color);
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.result-item.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.result-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.75rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.result-meta {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.role-badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 12px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.role-user {
|
|
background: rgba(88, 166, 255, 0.2);
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
.role-assistant {
|
|
background: rgba(63, 185, 80, 0.2);
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
.timestamp {
|
|
color: var(--text-muted);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.score-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.score-bar-bg {
|
|
width: 100px;
|
|
height: 8px;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.score-bar {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--accent-orange), var(--accent-green));
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.score-text {
|
|
font-size: 0.85rem;
|
|
color: var(--accent-green);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-content {
|
|
color: var(--text-primary);
|
|
line-height: 1.8;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.highlight {
|
|
background: rgba(255, 153, 102, 0.3);
|
|
color: var(--accent-orange);
|
|
padding: 2px 4px;
|
|
border-radius: 3px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-tools {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tool-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
color: var(--accent-purple);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 64px;
|
|
height: 64px;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.docs {
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
margin-top: 3rem;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.docs h2 {
|
|
color: var(--accent-blue);
|
|
margin-bottom: 1.5rem;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.docs h3 {
|
|
color: var(--accent-purple);
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.docs ul, .docs ol {
|
|
margin-left: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.docs li {
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.docs strong {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.docs a {
|
|
color: var(--accent-blue);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.docs a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.doc-content {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
footer {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--text-muted);
|
|
border-top: 1px solid var(--border-color);
|
|
margin-top: 4rem;
|
|
}
|
|
|
|
footer a {
|
|
color: var(--accent-blue);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.keyboard-hint {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
kbd {
|
|
background: var(--bg-tertiary);
|
|
padding: 0.2rem 0.5rem;
|
|
border-radius: 3px;
|
|
border: 1px solid var(--border-color);
|
|
font-family: inherit;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.filters {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stats-bar {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.result-header {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Advanced Transcript Search</h1>
|
|
<p class="tagline">Fuzzy search across Claude Code transcripts with D3.js-powered visualizations</p>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="search-interface">
|
|
<div class="search-box">
|
|
<input
|
|
type="text"
|
|
id="searchInput"
|
|
placeholder="Search messages, code, tool calls... (fuzzy matching enabled)"
|
|
autofocus
|
|
>
|
|
<button onclick="performSearch()">Search</button>
|
|
<button class="secondary" onclick="clearSearch()">Clear</button>
|
|
</div>
|
|
|
|
<div class="filters">
|
|
<div class="filter-group">
|
|
<label for="roleFilter">Role</label>
|
|
<select id="roleFilter" onchange="performSearch()">
|
|
<option value="all">All Roles</option>
|
|
<option value="user">User</option>
|
|
<option value="assistant">Assistant</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="dateFilter">Date Range</label>
|
|
<select id="dateFilter" onchange="performSearch()">
|
|
<option value="all">All Time</option>
|
|
<option value="today">Today</option>
|
|
<option value="week">Last 7 Days</option>
|
|
<option value="month">Last 30 Days</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="toolFilter">Tools Used</label>
|
|
<select id="toolFilter" onchange="performSearch()">
|
|
<option value="all">All Tools</option>
|
|
<option value="Read">Read</option>
|
|
<option value="Write">Write</option>
|
|
<option value="Bash">Bash</option>
|
|
<option value="Edit">Edit</option>
|
|
<option value="Grep">Grep</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="scoreThreshold">Min Score</label>
|
|
<select id="scoreThreshold" onchange="performSearch()">
|
|
<option value="0">0% - All Results</option>
|
|
<option value="25">25% - Weak Match</option>
|
|
<option value="50" selected>50% - Good Match</option>
|
|
<option value="75">75% - Strong Match</option>
|
|
<option value="90">90% - Exact Match</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="keyboard-hint">
|
|
<kbd>Ctrl</kbd> + <kbd>K</kbd> to focus search • <kbd>Enter</kbd> to search • <kbd>Esc</kbd> to clear
|
|
</div>
|
|
</section>
|
|
|
|
<div class="stats-bar" id="statsBar" style="display: none;">
|
|
<div class="stat">
|
|
<span class="stat-label">Results:</span>
|
|
<span class="stat-value" id="resultCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Avg Score:</span>
|
|
<span class="stat-value" id="avgScore">0%</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Search Time:</span>
|
|
<span class="stat-value" id="searchTime">0ms</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="results"></div>
|
|
|
|
<section class="docs">
|
|
<h2>About This Tool</h2>
|
|
<div class="doc-content">
|
|
<h3>Purpose</h3>
|
|
<p>Advanced search interface for Claude Code JSONL transcripts featuring fuzzy matching, score-based ranking, and D3.js-powered result visualization. Enables developers to quickly find relevant conversations, code snippets, and tool usage patterns across coding sessions.</p>
|
|
|
|
<h3>Features</h3>
|
|
<ul>
|
|
<li><strong>Fuzzy Search Algorithm:</strong> Tolerates typos and approximate matches using Levenshtein distance</li>
|
|
<li><strong>D3.js Data Binding:</strong> Dynamic result rendering with smooth enter/exit transitions</li>
|
|
<li><strong>Linear Scale Visualization:</strong> Score bars showing match relevance from 0-100%</li>
|
|
<li><strong>Multi-Facet Filtering:</strong> Filter by role, date range, tools, and minimum score threshold</li>
|
|
<li><strong>Context Highlighting:</strong> Matching terms highlighted in result snippets</li>
|
|
<li><strong>Score-Based Ranking:</strong> Results sorted by relevance with visual indicators</li>
|
|
<li><strong>Keyboard Navigation:</strong> Power-user shortcuts for efficient searching</li>
|
|
<li><strong>Real-Time Statistics:</strong> Live result count, average score, and search performance metrics</li>
|
|
</ul>
|
|
|
|
<h3>Web Research Integration</h3>
|
|
<p><strong>Source:</strong> <a href="https://d3js.org/getting-started" target="_blank">D3.js Getting Started Guide</a></p>
|
|
<p><strong>Techniques Applied:</strong></p>
|
|
<ul>
|
|
<li><strong>D3 Data Joining & Selection:</strong> Using selectAll().data().join() pattern for efficient DOM updates when search results change</li>
|
|
<li><strong>Linear Scale Functions:</strong> Implemented d3.scaleLinear() to map relevance scores (0-100) to visual bar widths, creating intuitive score visualization</li>
|
|
<li><strong>Smooth Transitions:</strong> Applied D3 transition() with duration and delay for staggered result animations, creating polished user experience</li>
|
|
</ul>
|
|
|
|
<h3>Usage</h3>
|
|
<ol>
|
|
<li>Type your search query in the search box (fuzzy matching automatically enabled)</li>
|
|
<li>Optionally apply filters for role, date range, tools, or minimum score</li>
|
|
<li>Press Enter or click Search to execute</li>
|
|
<li>Results appear with relevance scores shown as visual bars</li>
|
|
<li>Matching terms are highlighted in context snippets</li>
|
|
<li>Use keyboard shortcuts for faster workflow: Ctrl+K to focus, Esc to clear</li>
|
|
</ol>
|
|
|
|
<h3>Sample Data</h3>
|
|
<p>This tool includes realistic sample transcript data demonstrating various Claude Code interactions including file operations, git commands, web research, and code generation. In production, load your actual JSONL transcript files.</p>
|
|
|
|
<h3>D3.js Integration Details</h3>
|
|
<p><strong>Why D3.js for Search Results?</strong></p>
|
|
<p>Traditional DOM manipulation creates jerky, unnatural result updates. D3's data-binding approach enables:</p>
|
|
<ul>
|
|
<li><strong>Declarative Updates:</strong> Define what should exist based on data, D3 handles the transitions</li>
|
|
<li><strong>Enter/Exit Patterns:</strong> Smooth animations when results appear or disappear</li>
|
|
<li><strong>Scale Functions:</strong> Automatic mapping of data ranges to visual properties</li>
|
|
<li><strong>Performance:</strong> Efficient DOM updates only where data changed</li>
|
|
</ul>
|
|
|
|
<h3>Fuzzy Search Algorithm</h3>
|
|
<p>Implements simplified Levenshtein distance calculation for typo tolerance. Scores based on:</p>
|
|
<ul>
|
|
<li>Character-by-character matching (case-insensitive)</li>
|
|
<li>Sequence matching bonuses</li>
|
|
<li>Position weighting (earlier matches score higher)</li>
|
|
<li>Normalized to 0-100 scale for consistency</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer>
|
|
<p>Claude Code DevTools | Iteration 6 - D3.js Search Interface</p>
|
|
<p>Web Source: <a href="https://d3js.org/getting-started" target="_blank">https://d3js.org/getting-started</a></p>
|
|
<p style="margin-top: 0.5rem; font-size: 0.85rem;">Generated via web-enhanced infinite loop • D3.js v7</p>
|
|
</footer>
|
|
|
|
<!-- D3.js Library -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<script>
|
|
// Sample transcript data for demonstration
|
|
const sampleTranscriptData = [
|
|
{
|
|
uuid: "msg-001",
|
|
timestamp: new Date(Date.now() - 3600000).toISOString(),
|
|
role: "user",
|
|
content: "Can you help me implement a fuzzy search algorithm in JavaScript?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-002",
|
|
timestamp: new Date(Date.now() - 3500000).toISOString(),
|
|
role: "assistant",
|
|
content: "I'll help you implement a fuzzy search algorithm using Levenshtein distance. This allows matching even with typos.",
|
|
tools: ["Read", "Write"]
|
|
},
|
|
{
|
|
uuid: "msg-003",
|
|
timestamp: new Date(Date.now() - 3000000).toISOString(),
|
|
role: "user",
|
|
content: "How do I visualize search results with D3.js?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-004",
|
|
timestamp: new Date(Date.now() - 2900000).toISOString(),
|
|
role: "assistant",
|
|
content: "D3.js provides powerful data binding for visualizations. I'll show you how to use scales and transitions for search result bars.",
|
|
tools: ["WebFetch", "Write"]
|
|
},
|
|
{
|
|
uuid: "msg-005",
|
|
timestamp: new Date(Date.now() - 2400000).toISOString(),
|
|
role: "user",
|
|
content: "Can you add highlighting for matched terms in the search results?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-006",
|
|
timestamp: new Date(Date.now() - 2300000).toISOString(),
|
|
role: "assistant",
|
|
content: "I'll implement text highlighting by wrapping matched terms in span elements with a highlight class. This makes search results easier to scan.",
|
|
tools: ["Edit"]
|
|
},
|
|
{
|
|
uuid: "msg-007",
|
|
timestamp: new Date(Date.now() - 1800000).toISOString(),
|
|
role: "user",
|
|
content: "How can I filter results by role and date range?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-008",
|
|
timestamp: new Date(Date.now() - 1700000).toISOString(),
|
|
role: "assistant",
|
|
content: "I'll add filter functions that combine with the search algorithm. The filters will work together to narrow results by role, date, and tool usage.",
|
|
tools: ["Edit", "Read"]
|
|
},
|
|
{
|
|
uuid: "msg-009",
|
|
timestamp: new Date(Date.now() - 1200000).toISOString(),
|
|
role: "user",
|
|
content: "What's the best way to score search relevance?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-010",
|
|
timestamp: new Date(Date.now() - 1100000).toISOString(),
|
|
role: "assistant",
|
|
content: "For search relevance scoring, we can use a combination of fuzzy matching score, term frequency, and position weighting. Higher scores for exact matches and earlier positions.",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-011",
|
|
timestamp: new Date(Date.now() - 600000).toISOString(),
|
|
role: "user",
|
|
content: "Show me how to implement smooth animations when results update",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-012",
|
|
timestamp: new Date(Date.now() - 500000).toISOString(),
|
|
role: "assistant",
|
|
content: "D3's transition() function creates smooth animations. I'll add staggered delays so results appear sequentially with fade-in and slide-up effects.",
|
|
tools: ["Edit", "Write"]
|
|
},
|
|
{
|
|
uuid: "msg-013",
|
|
timestamp: new Date(Date.now() - 300000).toISOString(),
|
|
role: "user",
|
|
content: "Can you add keyboard shortcuts like Ctrl+K for search focus?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-014",
|
|
timestamp: new Date(Date.now() - 200000).toISOString(),
|
|
role: "assistant",
|
|
content: "I'll implement keyboard event listeners for power-user shortcuts: Ctrl+K to focus search, Enter to execute, and Esc to clear.",
|
|
tools: ["Edit"]
|
|
},
|
|
{
|
|
uuid: "msg-015",
|
|
timestamp: new Date(Date.now() - 60000).toISOString(),
|
|
role: "user",
|
|
content: "How do I export search results to a file?",
|
|
tools: []
|
|
},
|
|
{
|
|
uuid: "msg-016",
|
|
timestamp: new Date(Date.now() - 30000).toISOString(),
|
|
role: "assistant",
|
|
content: "I can add an export function that converts results to JSON or CSV format and triggers a browser download using the File API.",
|
|
tools: ["Grep", "Bash", "Write"]
|
|
}
|
|
];
|
|
|
|
let transcriptData = sampleTranscriptData;
|
|
let currentResults = [];
|
|
|
|
// D3.js Scale for score visualization
|
|
const scoreScale = d3.scaleLinear()
|
|
.domain([0, 100])
|
|
.range([0, 100]); // Percentage width
|
|
|
|
// Fuzzy search algorithm using simplified Levenshtein distance
|
|
function fuzzyMatch(query, text) {
|
|
if (!query || !text) return 0;
|
|
|
|
query = query.toLowerCase();
|
|
text = text.toLowerCase();
|
|
|
|
// Exact match bonus
|
|
if (text.includes(query)) return 100;
|
|
|
|
let score = 0;
|
|
let queryIndex = 0;
|
|
let consecutiveMatches = 0;
|
|
|
|
for (let i = 0; i < text.length && queryIndex < query.length; i++) {
|
|
if (text[i] === query[queryIndex]) {
|
|
score += 10;
|
|
queryIndex++;
|
|
consecutiveMatches++;
|
|
// Bonus for consecutive matches
|
|
if (consecutiveMatches > 1) {
|
|
score += consecutiveMatches * 2;
|
|
}
|
|
} else {
|
|
consecutiveMatches = 0;
|
|
}
|
|
}
|
|
|
|
// Penalty for incomplete match
|
|
if (queryIndex < query.length) {
|
|
score -= (query.length - queryIndex) * 5;
|
|
}
|
|
|
|
// Normalize to 0-100
|
|
score = Math.max(0, Math.min(100, score));
|
|
return score;
|
|
}
|
|
|
|
// Highlight matching terms in text
|
|
function highlightMatches(text, query) {
|
|
if (!query) return text;
|
|
|
|
const regex = new RegExp(`(${query.split('').join('.*?')})`, 'gi');
|
|
return text.replace(regex, '<span class="highlight">$1</span>');
|
|
}
|
|
|
|
// Filter by date range
|
|
function filterByDate(timestamp, range) {
|
|
if (range === 'all') return true;
|
|
|
|
const msgDate = new Date(timestamp);
|
|
const now = new Date();
|
|
const diff = now - msgDate;
|
|
|
|
switch(range) {
|
|
case 'today':
|
|
return diff < 86400000; // 24 hours
|
|
case 'week':
|
|
return diff < 604800000; // 7 days
|
|
case 'month':
|
|
return diff < 2592000000; // 30 days
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Main search function
|
|
function performSearch() {
|
|
const startTime = performance.now();
|
|
|
|
const query = document.getElementById('searchInput').value.trim();
|
|
const roleFilter = document.getElementById('roleFilter').value;
|
|
const dateFilter = document.getElementById('dateFilter').value;
|
|
const toolFilter = document.getElementById('toolFilter').value;
|
|
const scoreThreshold = parseInt(document.getElementById('scoreThreshold').value);
|
|
|
|
// Filter and score results
|
|
let results = transcriptData.map(msg => {
|
|
const score = fuzzyMatch(query, msg.content);
|
|
return { ...msg, score };
|
|
});
|
|
|
|
// Apply filters
|
|
results = results.filter(r => {
|
|
if (r.score < scoreThreshold) return false;
|
|
if (roleFilter !== 'all' && r.role !== roleFilter) return false;
|
|
if (!filterByDate(r.timestamp, dateFilter)) return false;
|
|
if (toolFilter !== 'all' && !r.tools.includes(toolFilter)) return false;
|
|
return true;
|
|
});
|
|
|
|
// Sort by score descending
|
|
results.sort((a, b) => b.score - a.score);
|
|
|
|
currentResults = results;
|
|
|
|
const endTime = performance.now();
|
|
const searchTime = Math.round(endTime - startTime);
|
|
|
|
// Update stats
|
|
updateStats(results.length, results, searchTime);
|
|
|
|
// Render results with D3.js
|
|
renderResultsWithD3(results, query);
|
|
}
|
|
|
|
// Update statistics bar
|
|
function updateStats(count, results, time) {
|
|
const statsBar = document.getElementById('statsBar');
|
|
statsBar.style.display = count > 0 ? 'flex' : 'none';
|
|
|
|
document.getElementById('resultCount').textContent = count;
|
|
document.getElementById('searchTime').textContent = time + 'ms';
|
|
|
|
if (count > 0) {
|
|
const avgScore = Math.round(results.reduce((sum, r) => sum + r.score, 0) / count);
|
|
document.getElementById('avgScore').textContent = avgScore + '%';
|
|
}
|
|
}
|
|
|
|
// Render results using D3.js data binding
|
|
function renderResultsWithD3(results, query) {
|
|
const container = d3.select('#results');
|
|
|
|
if (results.length === 0) {
|
|
container.html(`
|
|
<div class="empty-state">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
<p>No results found. Try adjusting your search or filters.</p>
|
|
</div>
|
|
`);
|
|
return;
|
|
}
|
|
|
|
// D3 data binding with enter/update/exit pattern
|
|
const resultItems = container
|
|
.selectAll('.result-item')
|
|
.data(results, d => d.uuid);
|
|
|
|
// Exit: remove old results
|
|
resultItems.exit()
|
|
.transition()
|
|
.duration(300)
|
|
.style('opacity', 0)
|
|
.remove();
|
|
|
|
// Enter: add new results
|
|
const enter = resultItems.enter()
|
|
.append('div')
|
|
.attr('class', 'result-item')
|
|
.style('opacity', 0);
|
|
|
|
enter.append('div')
|
|
.attr('class', 'result-header')
|
|
.html(d => `
|
|
<div class="result-meta">
|
|
<span class="role-badge role-${d.role}">${d.role}</span>
|
|
<span class="timestamp">${new Date(d.timestamp).toLocaleString()}</span>
|
|
</div>
|
|
<div class="score-container">
|
|
<div class="score-bar-bg">
|
|
<div class="score-bar" style="width: ${scoreScale(d.score)}%"></div>
|
|
</div>
|
|
<span class="score-text">${d.score}%</span>
|
|
</div>
|
|
`);
|
|
|
|
enter.append('div')
|
|
.attr('class', 'result-content')
|
|
.html(d => highlightMatches(d.content, query));
|
|
|
|
enter.append('div')
|
|
.attr('class', 'result-tools')
|
|
.html(d => d.tools.length > 0
|
|
? d.tools.map(tool => `<span class="tool-badge">${tool}</span>`).join('')
|
|
: '<span style="color: var(--text-muted); font-size: 0.85rem;">No tools used</span>'
|
|
);
|
|
|
|
// Update + Enter: apply to all items
|
|
const merged = enter.merge(resultItems);
|
|
|
|
// Smooth staggered animation using D3 transitions
|
|
merged.transition()
|
|
.duration(500)
|
|
.delay((d, i) => i * 50) // Stagger delay
|
|
.style('opacity', 1)
|
|
.on('end', function() {
|
|
d3.select(this).classed('visible', true);
|
|
});
|
|
|
|
// Animate score bars
|
|
merged.selectAll('.score-bar')
|
|
.transition()
|
|
.duration(800)
|
|
.delay((d, i) => i * 50)
|
|
.style('width', d => scoreScale(d.score) + '%');
|
|
}
|
|
|
|
// Clear search
|
|
function clearSearch() {
|
|
document.getElementById('searchInput').value = '';
|
|
document.getElementById('roleFilter').value = 'all';
|
|
document.getElementById('dateFilter').value = 'all';
|
|
document.getElementById('toolFilter').value = 'all';
|
|
document.getElementById('scoreThreshold').value = '50';
|
|
|
|
document.getElementById('results').innerHTML = `
|
|
<div class="empty-state">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
<p>Enter a search query to find messages in your transcripts.</p>
|
|
</div>
|
|
`;
|
|
document.getElementById('statsBar').style.display = 'none';
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ctrl+K or Cmd+K to focus search
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
document.getElementById('searchInput').focus();
|
|
}
|
|
|
|
// Escape to clear
|
|
if (e.key === 'Escape') {
|
|
clearSearch();
|
|
}
|
|
|
|
// Enter to search when focused
|
|
if (e.key === 'Enter' && document.activeElement.id === 'searchInput') {
|
|
performSearch();
|
|
}
|
|
});
|
|
|
|
// Initialize with empty state
|
|
clearSearch();
|
|
|
|
// Auto-search on input (debounced)
|
|
let searchTimeout;
|
|
document.getElementById('searchInput').addEventListener('input', () => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
const query = document.getElementById('searchInput').value.trim();
|
|
if (query.length > 0) {
|
|
performSearch();
|
|
}
|
|
}, 300);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|