Skip to content

Commit 7415db4

Browse files
feat: progress streaming to UI
1 parent a423763 commit 7415db4

File tree

7 files changed

+700
-54
lines changed

7 files changed

+700
-54
lines changed

app/api/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def get_task_status_route(task_id):
8888
"length": result.get("length", 0),
8989
"search_time": result.get("search_time"),
9090
"nodes_explored": result.get("nodes_explored"),
91+
"search_stats": result.get("search_stats"),
9192
},
9293
}
9394
else:

app/core/factory.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def get_wikipedia_client(cls):
6666
return cls._wikipedia_client
6767

6868
@classmethod
69-
def create_pathfinding_service(cls, algorithm: str = "bfs") -> PathFindingService:
69+
def create_pathfinding_service(cls, algorithm: str = "bfs", progress_callback: callable = None) -> PathFindingService:
7070
"""Create pathfinding service with specified algorithm."""
7171
wikipedia_client = cls.get_wikipedia_client()
7272
cache_service = cls.get_cache_service()
@@ -82,7 +82,12 @@ def create_pathfinding_service(cls, algorithm: str = "bfs") -> PathFindingServic
8282
)
8383
else: # Default to regular BFS
8484
path_finder = RedisBasedBFSPathFinder(
85-
wikipedia_client, cache_service, queue_service, max_depth, batch_size
85+
wikipedia_client,
86+
cache_service,
87+
queue_service,
88+
max_depth,
89+
batch_size,
90+
progress_callback
8691
)
8792

8893
return PathFindingService(path_finder, cache_service, wikipedia_client)
@@ -125,9 +130,9 @@ def cleanup(cls):
125130
logger.info("Service factory cleaned up")
126131

127132

128-
def get_pathfinding_service(algorithm: str = "bfs") -> PathFindingService:
133+
def get_pathfinding_service(algorithm: str = "bfs", progress_callback: callable = None) -> PathFindingService:
129134
"""Convenience function to get pathfinding service."""
130-
return ServiceFactory.create_pathfinding_service(algorithm)
135+
return ServiceFactory.create_pathfinding_service(algorithm, progress_callback)
131136

132137

133138
def get_explore_service() -> ExploreService:

app/core/pathfinding.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ def __init__(
2929
queue_service: QueueInterface,
3030
max_depth: int = 6,
3131
batch_size: int = 50,
32+
progress_callback: Optional[callable] = None,
3233
):
3334
self.wikipedia_client = wikipedia_client
3435
self.cache_service = cache_service
3536
self.queue_service = queue_service
3637
self.max_depth = max_depth
3738
self.batch_size = batch_size
39+
self.progress_callback = progress_callback
3840

3941
def find_shortest_path(self, start_page: str, end_page: str) -> Dict[str, Any]:
4042
"""
@@ -105,6 +107,8 @@ def _perform_bfs_search(
105107

106108
# Simple BFS: process one item at a time from the queue
107109
nodes_explored = 0
110+
import time
111+
search_start_time = time.time()
108112

109113
while self.queue_service.length(queue_key) > 0:
110114
current_item = self.queue_service.pop(queue_key)
@@ -119,6 +123,22 @@ def _perform_bfs_search(
119123
f"Processing page '{current_page}' at depth {current_depth} (node #{nodes_explored})"
120124
)
121125

126+
# Report progress every 3 nodes
127+
if (self.progress_callback and nodes_explored % 3 == 0):
128+
queue_size = self.queue_service.length(queue_key)
129+
elapsed_time = time.time() - search_start_time
130+
131+
self.progress_callback({
132+
"status": "Searching...",
133+
"search_stats": {
134+
"nodes_explored": nodes_explored,
135+
"current_depth": current_depth,
136+
"last_node": current_page,
137+
"queue_size": queue_size,
138+
},
139+
"search_time_elapsed": round(elapsed_time, 2)
140+
})
141+
122142
# Check depth limit
123143
if current_depth > self.max_depth:
124144
logger.warning(

app/infrastructure/tasks.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,20 @@ def find_path_task(self, start_page: str, end_page: str, algorithm: str = "bfs")
7676
},
7777
)
7878

79-
# Get pathfinding service
80-
pathfinding_service = get_pathfinding_service(algorithm)
79+
# Create progress callback for real-time updates
80+
def progress_update(progress_data):
81+
# Add start/end pages to progress data
82+
progress_data["search_stats"]["start_page"] = start_page
83+
progress_data["search_stats"]["end_page"] = end_page
84+
progress_data["search_stats"]["max_depth"] = 6 # From config
85+
86+
self.update_state(
87+
state="PROGRESS",
88+
meta=progress_data
89+
)
90+
91+
# Get pathfinding service with progress callback
92+
pathfinding_service = get_pathfinding_service(algorithm, progress_update)
8193

8294
# Validate that pages exist
8395
start_exists, end_exists = pathfinding_service.validate_pages(
@@ -100,34 +112,28 @@ def find_path_task(self, start_page: str, end_page: str, algorithm: str = "bfs")
100112
"code": "PAGE_NOT_FOUND",
101113
}
102114

103-
# Update progress
115+
# Update progress - starting search
104116
self.update_state(
105117
state="PROGRESS",
106118
meta={
107-
"current": 25,
108-
"total": 100,
109119
"status": "Starting pathfinding search...",
110-
"start_page": start_page,
111-
"end_page": end_page,
120+
"search_stats": {
121+
"nodes_explored": 0,
122+
"current_depth": 0,
123+
"last_node": start_page,
124+
"queue_size": 1,
125+
"start_page": start_page,
126+
"end_page": end_page,
127+
"max_depth": 6,
128+
},
129+
"search_time_elapsed": 0
112130
},
113131
)
114132

115-
# Perform pathfinding
133+
# Perform pathfinding (will report real-time progress via callback)
116134
result = pathfinding_service.find_path(search_request)
117135

118-
# Update progress
119-
self.update_state(
120-
state="PROGRESS",
121-
meta={
122-
"current": 90,
123-
"total": 100,
124-
"status": "Finalizing results...",
125-
"path_length": result.length,
126-
"search_time": result.search_time,
127-
},
128-
)
129-
130-
# Return successful result
136+
# Return successful result with detailed search stats
131137
success_result = {
132138
"status": "SUCCESS",
133139
"path": result.path,
@@ -137,6 +143,14 @@ def find_path_task(self, start_page: str, end_page: str, algorithm: str = "bfs")
137143
"search_time": result.search_time,
138144
"nodes_explored": result.nodes_explored,
139145
"algorithm": algorithm,
146+
"search_stats": {
147+
"nodes_explored": result.nodes_explored,
148+
"final_depth": result.length - 1 if result.path else 0,
149+
"start_page": result.start_page,
150+
"end_page": result.end_page,
151+
"max_depth": 6, # From config
152+
"search_completed": True,
153+
},
140154
}
141155

142156
logger.info(

static/index.html

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,33 @@
3232
<h1 style="margin: 0;">Iris: Wikipedia Path Finder</h1>
3333
</div>
3434
<p>Discover the shortest path between any two Wikipedia pages through connected links</p>
35+
<div class="header-actions">
36+
<button id="toggleAboutBtn" class="link-button" aria-controls="aboutBox" aria-expanded="false">About Iris</button>
37+
</div>
3538
</header>
3639

40+
<!-- About / How-To info box -->
41+
<div id="aboutBox" class="info-box hidden" role="region" aria-label="About Iris and how to use it">
42+
<div class="info-header">
43+
<h3 class="info-title">What is Iris? How to use</h3>
44+
</div>
45+
<div class="info-content">
46+
<p><strong>Iris</strong> discovers the shortest hyperlink path between two Wikipedia pages. Under the hood it runs a high‑performance graph search and streams live progress, then visualizes the path so you can explore how topics connect.</p>
47+
<ul class="info-list">
48+
<li><strong>Pick pages:</strong> Enter two Wikipedia page titles (e.g., “Barack Obama” → “Mathematics”). Exact titles work best.</li>
49+
<li><strong>Start:</strong> Click <em>Find Path</em>. The progress card shows depth, nodes explored, queue size, and elapsed time.</li>
50+
<li><strong>Explore:</strong> Drag nodes to tidy the layout and click any step to open it on Wikipedia.</li>
51+
<li><strong>Resume:</strong> Your latest search is saved locally so you can refresh and continue later.</li>
52+
</ul>
53+
<p class="info-note">Tip: Some pairs can take longer or may not connect quickly. Try broader page titles if you get stuck.</p>
54+
<div class="info-links">
55+
<a href="/api" target="_blank" rel="noopener">API docs</a>
56+
<span aria-hidden="true"></span>
57+
<a href="https://www.mediawiki.org/wiki/API:Main_page" target="_blank" rel="noopener">Wikipedia API</a>
58+
</div>
59+
</div>
60+
</div>
61+
3762
<div class="input-section">
3863
<div class="form-row">
3964
<div class="form-group">
@@ -67,6 +92,49 @@ <h1 style="margin: 0;">Iris: Wikipedia Path Finder</h1>
6792
</div>
6893
<div>Building your path visualization...</div>
6994
</div>
95+
96+
<div class="search-progress hidden" id="searchProgress">
97+
<div class="search-progress-header">
98+
<span>Searching for path:</span>
99+
<span id="searchPath">Barack Obama → Mathematics</span>
100+
</div>
101+
102+
<div class="activity-bar">
103+
<div class="activity-bar-fill"></div>
104+
</div>
105+
106+
<div class="depth-indicator">
107+
<div class="depth-label">Depth:</div>
108+
<div class="depth-dots" id="depthDots">
109+
<div class="depth-dot"></div>
110+
<div class="depth-dot"></div>
111+
<div class="depth-dot"></div>
112+
<div class="depth-dot"></div>
113+
<div class="depth-dot"></div>
114+
<div class="depth-dot"></div>
115+
</div>
116+
</div>
117+
118+
<div class="search-stats">
119+
<div class="search-stat">
120+
<div class="stat-label">Nodes Explored</div>
121+
<div class="stat-value" id="nodesExplored">0</div>
122+
</div>
123+
<div class="search-stat">
124+
<div class="stat-label">Queue Size</div>
125+
<div class="stat-value" id="queueSize">1</div>
126+
</div>
127+
<div class="search-stat">
128+
<div class="stat-label">Elapsed Time</div>
129+
<div class="stat-value" id="elapsedTime">0s</div>
130+
</div>
131+
<div class="search-stat last-node">
132+
<div class="stat-label">Currently Exploring</div>
133+
<div class="stat-value" id="lastNode" onclick="openWikipediaPage(this.textContent)">-</div>
134+
</div>
135+
</div>
136+
</div>
137+
70138
<svg id="graph" class="hidden"></svg>
71139
</div>
72140

@@ -76,6 +144,7 @@ <h3>Path Found</h3>
76144
<div class="path-stats">
77145
<span id="pathLength" class="stat"></span>
78146
<span id="searchTime" class="stat"></span>
147+
<span id="nodesExploredStat" class="stat"></span>
79148
</div>
80149
</div>
81150
<div id="pathSteps" class="path-steps"></div>
@@ -84,6 +153,53 @@ <h3>Path Found</h3>
84153
</div>
85154

86155
<script src="/static/script.js"></script>
156+
<script>
157+
// About box toggle with timed resurfacing and sticky open state
158+
document.addEventListener('DOMContentLoaded', () => {
159+
const RESURFACE_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
160+
const KEY_NEXT_SHOW_AT = 'iris_about_next_show_at';
161+
const KEY_PINNED = 'iris_about_pinned';
162+
try {
163+
const box = document.getElementById('aboutBox');
164+
const toggle = document.getElementById('toggleAboutBtn');
165+
const now = Date.now();
166+
const nextShowAt = parseInt(localStorage.getItem(KEY_NEXT_SHOW_AT) || '0', 10);
167+
const pinned = localStorage.getItem(KEY_PINNED) === 'true';
168+
169+
const showBox = () => {
170+
if (box) box.classList.remove('hidden');
171+
if (toggle) toggle.setAttribute('aria-expanded', 'true');
172+
localStorage.setItem(KEY_PINNED, 'true');
173+
};
174+
const hideBox = () => {
175+
if (box) box.classList.add('hidden');
176+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
177+
localStorage.setItem(KEY_PINNED, 'false');
178+
localStorage.setItem(KEY_NEXT_SHOW_AT, String(Date.now() + RESURFACE_INTERVAL_MS));
179+
};
180+
181+
if (pinned) {
182+
// Stay open across refreshes until user hides it
183+
showBox();
184+
} else if (!nextShowAt || now >= nextShowAt) {
185+
showBox();
186+
} else {
187+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
188+
}
189+
190+
if (toggle) {
191+
toggle.addEventListener('click', () => {
192+
const isHidden = box.classList.contains('hidden');
193+
if (isHidden) {
194+
showBox();
195+
} else {
196+
hideBox();
197+
}
198+
});
199+
}
200+
} catch (e) { /* no-op */ }
201+
});
202+
</script>
87203
</body>
88204

89-
</html>
205+
</html>

0 commit comments

Comments
 (0)