Skip to content

Commit dab7d61

Browse files
committed
feat(tools): improve result previews and outage diagnostics
1 parent 81c39bd commit dab7d61

10 files changed

Lines changed: 1005 additions & 63 deletions

File tree

docs/JARVIS_WEB_UI.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,17 @@ A **standalone web application** (`jarvis-web`) providing the full Jarvis experi
187187
crypto charts.
188188
- The renderer uses a registry of tool adapters. Each adapter maps its tool's
189189
payload onto the same bounded presentation model; the shared renderer then
190-
handles escaping, safe HTTP links, responsive horizontal scrolling, images,
191-
metadata chips, and actions. Overflowing rails expose previous/next controls
192-
in the preview header without covering card content; touch swiping and the
193-
native scrollbar remain available.
194-
- Current adapters cover focused Amazon, Home Depot, and eBay products plus
195-
Google Hotels, Yelp, Google Flights, and Google Maps results.
190+
handles escaping, safe HTTP links, images, metadata chips, and actions. The
191+
presentation model supports `rail` for visual browsing, `list` for dense
192+
records, and `metrics` for compact measurements. Overflowing rails expose
193+
previous/next controls in the preview header without covering card content;
194+
touch swiping and the native scrollbar remain available.
195+
- Current adapters cover Amazon and Google Shopping, Home Depot, eBay search
196+
and product details, Google Hotels, Yelp, Google Flights, Google Maps,
197+
SerpApi YouTube search, and weather forecasts.
198+
- YouTube search uses the compact rail for the complete shortlist while Jarvis
199+
Web keeps one large playable top-result embed. Direct YouTube detail and
200+
downloaded-media results retain their existing specialized players.
196201
- Add a future structured display with
197202
`window.structuredResultsRenderer.register(toolName, adapter)`. Keep provider
198203
payload parsing inside the adapter and return only the small shared

docs/tools/serp-api-tool/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ Standard tool contract:
7373
}
7474
```
7575

76+
### Incident-aware failures
77+
78+
After a final transient SerpApi failure or tool-process timeout, Jarvis makes one
79+
short, bounded request to SerpApi's public unresolved-incidents JSON endpoint.
80+
If an active incident specifically matches the engine that failed, the error
81+
response includes `data.serpapi_incident`, `failure_reason=active_provider_incident`,
82+
the provider's latest update, its status-page URL, and a recommendation to retry
83+
later. Unrelated incidents do not replace the original tool error, validation
84+
errors do not trigger the lookup, and a failed status lookup is ignored.
85+
7686
## Parameters
7787

7888
| Parameter | Type | Required | Notes |

jarvis-web/client/css/main.css

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,24 @@ body {
21662166
background: rgba(255, 255, 255, 0.34);
21672167
}
21682168

2169+
.structured-results-layout-list .structured-results-track {
2170+
display: flex;
2171+
flex-direction: column;
2172+
gap: 8px;
2173+
padding-bottom: 0;
2174+
overflow: visible;
2175+
scroll-snap-type: none;
2176+
}
2177+
2178+
.structured-results-layout-metrics .structured-results-track {
2179+
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
2180+
grid-auto-flow: row;
2181+
gap: 8px;
2182+
padding-bottom: 0;
2183+
overflow: visible;
2184+
scroll-snap-type: none;
2185+
}
2186+
21692187
.structured-result-card {
21702188
display: flex;
21712189
min-width: 0;
@@ -2180,6 +2198,15 @@ body {
21802198
border-top: 2px solid var(--accent-primary, #8ec5ff);
21812199
}
21822200

2201+
.structured-result-card-video {
2202+
flex-direction: column;
2203+
}
2204+
2205+
.structured-result-card-weather {
2206+
min-height: 118px;
2207+
border-top: 2px solid #38bdf8;
2208+
}
2209+
21832210
.structured-result-image {
21842211
display: block;
21852212
width: 96px;
@@ -2195,6 +2222,19 @@ body {
21952222
display: block;
21962223
}
21972224

2225+
.structured-result-card-video .structured-result-image {
2226+
width: 100%;
2227+
min-width: 0;
2228+
aspect-ratio: 16 / 9;
2229+
}
2230+
2231+
.structured-result-card-video .structured-result-image img {
2232+
width: 100%;
2233+
height: 100%;
2234+
min-height: 0;
2235+
object-fit: cover;
2236+
}
2237+
21982238
.structured-result-body {
21992239
display: flex;
22002240
flex: 1;
@@ -2205,6 +2245,10 @@ body {
22052245
}
22062246

22072247
.structured-result-title {
2248+
display: -webkit-box;
2249+
overflow: hidden;
2250+
-webkit-box-orient: vertical;
2251+
-webkit-line-clamp: 3;
22082252
color: var(--text-primary);
22092253
font-weight: 700;
22102254
line-height: 1.3;
@@ -2241,6 +2285,17 @@ body {
22412285
line-height: 1.3;
22422286
}
22432287

2288+
.structured-result-card-video .structured-result-detail {
2289+
display: -webkit-box;
2290+
overflow: hidden;
2291+
-webkit-box-orient: vertical;
2292+
-webkit-line-clamp: 2;
2293+
}
2294+
2295+
.structured-result-card-weather .structured-result-detail {
2296+
text-transform: capitalize;
2297+
}
2298+
22442299
.structured-result-link {
22452300
margin-top: auto;
22462301
font-size: 0.78rem;
@@ -2263,9 +2318,13 @@ body {
22632318
gap: 10px;
22642319
}
22652320

2266-
.structured-results-track {
2321+
.structured-results-layout-rail .structured-results-track {
22672322
grid-auto-columns: minmax(235px, 82vw);
22682323
}
2324+
2325+
.structured-results-layout-metrics .structured-results-track {
2326+
grid-template-columns: repeat(2, minmax(0, 1fr));
2327+
}
22692328
}
22702329

22712330
.product-preview-image {

jarvis-web/client/js/chat.js

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4517,7 +4517,7 @@ class ChatUI {
45174517
return null;
45184518
}
45194519

4520-
/** Pull SerpApi YouTube tool payloads (possibly arrays) for iframe embedding. */
4520+
/** Pull SerpApi YouTube payloads with tool provenance for iframe embedding. */
45214521
_youtubeToolPayloadsForEmbeds(toolResultsData = {}) {
45224522
const out = [];
45234523
if (!toolResultsData || typeof toolResultsData !== 'object') return out;
@@ -4526,10 +4526,14 @@ class ChatUI {
45264526
if (!tr) continue;
45274527
if (Array.isArray(tr)) {
45284528
for (const item of tr) {
4529-
if (item && typeof item === 'object') out.push(item);
4529+
if (item && typeof item === 'object') {
4530+
const payload = item.data && typeof item.data === 'object' ? item.data : item;
4531+
out.push({toolName: key, payload});
4532+
}
45304533
}
45314534
} else if (typeof tr === 'object') {
4532-
out.push(tr);
4535+
const payload = tr.data && typeof tr.data === 'object' ? tr.data : tr;
4536+
out.push({toolName: key, payload});
45334537
}
45344538
}
45354539
return out;
@@ -4553,6 +4557,7 @@ class ChatUI {
45534557

45544558
const embeds = [];
45554559
const seenIds = new Set();
4560+
const youtubeSearchResultIds = new Set();
45564561

45574562
const pushEmbed = (videoId, titleHint = '') => {
45584563
if (!videoId || seenIds.has(videoId) || downloadedIds.has(videoId)) return;
@@ -4567,18 +4572,26 @@ class ChatUI {
45674572
});
45684573
};
45694574

4570-
for (const payload of this._youtubeToolPayloadsForEmbeds(toolResultsData)) {
4575+
for (const {toolName, payload} of this._youtubeToolPayloadsForEmbeds(toolResultsData)) {
45714576
const primaryTitle = typeof payload.title === 'string' ? payload.title.trim() : '';
4577+
const candidates = new Map();
4578+
const addCandidate = (videoId, titleHint = '') => {
4579+
if (!videoId) return;
4580+
const id = String(videoId).trim();
4581+
if (!id) return;
4582+
const title = typeof titleHint === 'string' ? titleHint.trim() : '';
4583+
if (!candidates.has(id) || (!candidates.get(id) && title)) candidates.set(id, title);
4584+
};
45724585
if (payload.top_url) {
45734586
const vid = this._extractYouTubeVideoId(payload.top_url);
4574-
if (vid) pushEmbed(vid, primaryTitle);
4587+
if (vid) addCandidate(vid, primaryTitle);
45754588
}
45764589
if (typeof payload.url === 'string') {
45774590
const vid = this._extractYouTubeVideoId(payload.url);
4578-
if (vid) pushEmbed(vid, primaryTitle);
4591+
if (vid) addCandidate(vid, primaryTitle);
45794592
}
45804593
if (payload.video_id != null && String(payload.video_id).trim()) {
4581-
pushEmbed(String(payload.video_id).trim(), primaryTitle);
4594+
addCandidate(String(payload.video_id).trim(), primaryTitle);
45824595
}
45834596
for (const listKey of ['results', 'top_results', 'candidates']) {
45844597
const list = payload[listKey];
@@ -4589,12 +4602,19 @@ class ChatUI {
45894602
const hint = itemTitle || primaryTitle;
45904603
if (typeof item.url === 'string') {
45914604
const vid = this._extractYouTubeVideoId(item.url);
4592-
if (vid) pushEmbed(vid, hint);
4605+
if (vid) addCandidate(vid, hint);
45934606
} else if (item.video_id != null && String(item.video_id).trim()) {
4594-
pushEmbed(String(item.video_id).trim(), hint);
4607+
addCandidate(String(item.video_id).trim(), hint);
45954608
}
45964609
}
45974610
}
4611+
if (toolName === 'serpapi_youtube_search') {
4612+
for (const videoId of candidates.keys()) youtubeSearchResultIds.add(videoId);
4613+
const first = candidates.entries().next().value;
4614+
if (first) pushEmbed(first[0], first[1]);
4615+
} else {
4616+
for (const [videoId, title] of candidates.entries()) pushEmbed(videoId, title);
4617+
}
45984618
}
45994619

46004620
const sources = [displayText, rawResponse];
@@ -4607,7 +4627,12 @@ class ChatUI {
46074627
if (embeds.length >= maxEmbeds) break;
46084628

46094629
const videoId = this._extractYouTubeVideoId(rawUrl);
4610-
if (!videoId || seenIds.has(videoId) || downloadedIds.has(videoId)) continue;
4630+
if (
4631+
!videoId
4632+
|| seenIds.has(videoId)
4633+
|| downloadedIds.has(videoId)
4634+
|| youtubeSearchResultIds.has(videoId)
4635+
) continue;
46114636

46124637
pushEmbed(videoId, '');
46134638
}

0 commit comments

Comments
 (0)