feat(items): integrate Wikidata API for external data enrichment

- Added functionality to fetch additional attributes for selected items from Wikidata.
- Defined structures for handling Wikidata API responses.
- Incorporated async data fetching using futures and `gloo_net::http::Request`.
- Enhanced the UI with a button to fetch external data for comparison.
- Updated the comparison table to include external descriptions retrieved from Wikidata.
This commit is contained in:
Ryan Mwangi 2024-12-18 21:49:26 +03:00
parent da720a48d3
commit 2733fc958b
5 changed files with 114 additions and 3 deletions

28
Cargo.lock generated
View File

@ -721,6 +721,8 @@ dependencies = [
"actix-files", "actix-files",
"actix-web", "actix-web",
"console_error_panic_hook", "console_error_panic_hook",
"futures",
"gloo-net 0.5.0",
"http 1.2.0", "http 1.2.0",
"leptos", "leptos",
"leptos_actix", "leptos_actix",
@ -731,6 +733,7 @@ dependencies = [
"tokio", "tokio",
"uuid", "uuid",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
] ]
@ -1123,6 +1126,27 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gloo-net"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"http 0.2.12",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "gloo-net" name = "gloo-net"
version = "0.6.0" version = "0.6.0"
@ -1735,7 +1759,7 @@ checksum = "8d71dea7d42c0d29c40842750232d3425ed1cf10e313a1f898076d20871dad32"
dependencies = [ dependencies = [
"cached", "cached",
"cfg-if", "cfg-if",
"gloo-net", "gloo-net 0.6.0",
"itertools", "itertools",
"js-sys", "js-sys",
"lazy_static", "lazy_static",
@ -2662,7 +2686,7 @@ dependencies = [
"const_format", "const_format",
"dashmap", "dashmap",
"futures", "futures",
"gloo-net", "gloo-net 0.6.0",
"http 1.2.0", "http 1.2.0",
"inventory", "inventory",
"js-sys", "js-sys",

View File

@ -21,6 +21,9 @@ uuid = { version = "1.0", features = ["v4"] }
web-sys = { version = "0.3", features = ["Event"] } web-sys = { version = "0.3", features = ["Event"] }
nostr-sdk = "0.37" nostr-sdk = "0.37"
tokio = "1" tokio = "1"
gloo-net = "0.5"
futures = "0.3"
wasm-bindgen-futures = "0.4"
[features] [features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]

View File

@ -39,6 +39,7 @@ pub fn App() -> impl IntoView {
description, description,
tags, tags,
reviews: vec![ReviewWithRating { content: review.clone(), rating }], reviews: vec![ReviewWithRating { content: review.clone(), rating }],
wikidata_id: None,
}; };
items.push(item); items.push(item);
}); });

View File

@ -2,11 +2,74 @@
/// Iterates through the items and renders their name, description, tags, and reviews. /// Iterates through the items and renders their name, description, tags, and reviews.
use leptos::*; use leptos::*;
use crate::models::item::Item; use crate::models::item::Item;
use serde::Deserialize;
use futures::future;
use gloo_net::http::Request;
use wasm_bindgen_futures::spawn_local;
use std::sync::Arc;
// Define the structure for Wikidata API response
#[derive(Deserialize, Clone, Debug)]
struct WikidataResponse {
entities: std::collections::HashMap<String, WikidataEntity>,
}
#[derive(Deserialize, Clone, Debug)]
struct WikidataEntity {
labels: Option<std::collections::HashMap<String, WikidataLabel>>,
descriptions: Option<std::collections::HashMap<String, WikidataLabel>>,
}
#[derive(Deserialize, Clone, Debug)]
struct WikidataLabel {
value: String,
}
#[component] #[component]
pub fn ItemsList(items: ReadSignal<Vec<Item>>) -> impl IntoView { pub fn ItemsList(items: ReadSignal<Vec<Item>>) -> impl IntoView {
// Create a signal for selected items // Create a signal for selected items
let (selected_items_signal, set_selected_items) = create_signal(Vec::<usize>::new()); let (selected_items_signal, set_selected_items) = create_signal(Vec::<usize>::new());
let (wikidata_data, set_wikidata_data) = create_signal(Vec::<Option<WikidataEntity>>::new());
// Fetch additional data from Wikidata for selected items
let fetch_wikidata = move || {
let selected_indices = selected_items_signal.get();
let selected_items: Vec<Item> = selected_indices
.iter()
.map(|&i| items.get()[i].clone())
.collect();
// Wrap `selected_items` in an `Arc` so it can be cloned
let selected_items = Arc::new(selected_items);
// Clone selected_items before the async block to avoid borrowing issues
let selected_items_clone: Arc<Vec<Item>> = Arc::clone(&selected_items);
// For each selected item, fetch Wikidata attributes
let futures = selected_items_clone.iter().map(move |item| {
let wikidata_id = item.wikidata_id.clone();
async move {
if let Some(id) = wikidata_id {
let url = format!("https://www.wikidata.org/wiki/Special:EntityData/{}.json", id);
match Request::get(&url).send().await {
Ok(response) => match response.json::<WikidataResponse>().await {
Ok(parsed_data) => parsed_data.entities.get(&id).cloned(),
Err(_) => None,
},
Err(_) => None,
}
} else {
None
}
}
}).collect::<Vec<_>>();
spawn_local(async move {
let results = future::join_all(futures).await;
set_wikidata_data.set(results);
});
};
// Function to toggle selection of an item // Function to toggle selection of an item
let toggle_selection = move |i: usize| { let toggle_selection = move |i: usize| {
@ -52,6 +115,9 @@ pub fn ItemsList(items: ReadSignal<Vec<Item>>) -> impl IntoView {
}).collect::<Vec<_>>() } }).collect::<Vec<_>>() }
</ul> </ul>
// Button to fetch Wikidata attributes
<button on:click=move |_| fetch_wikidata()>{ "Fetch External Data" }</button>
// Comparison Table // Comparison Table
<h2>{ "Comparison Table" }</h2> <h2>{ "Comparison Table" }</h2>
<table> <table>
@ -61,13 +127,15 @@ pub fn ItemsList(items: ReadSignal<Vec<Item>>) -> impl IntoView {
<th>{ "Description" }</th> <th>{ "Description" }</th>
<th>{ "Tags" }</th> <th>{ "Tags" }</th>
<th>{ "Reviews" }</th> <th>{ "Reviews" }</th>
<th>{ "External Description (Wikidata)" }</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{move || { {move || {
let selected_indices = selected_items_signal.get(); let selected_indices = selected_items_signal.get();
selected_indices.iter().map(|&i| { selected_indices.iter().enumerate().map(|(idx, &i)| {
let item = &items.get()[i]; let item = &items.get()[i];
let wikidata_entity = wikidata_data.get().get(idx).cloned().flatten();
view! { view! {
<tr key={i.to_string()}> <tr key={i.to_string()}>
<td>{ &item.name }</td> <td>{ &item.name }</td>
@ -82,6 +150,20 @@ pub fn ItemsList(items: ReadSignal<Vec<Item>>) -> impl IntoView {
<span>{ format!("Rating: {}/5 ", review.rating) }</span> <span>{ format!("Rating: {}/5 ", review.rating) }</span>
}).collect::<Vec<_>>()} }).collect::<Vec<_>>()}
</td> </td>
<td>
{move || {
let entity = wikidata_entity.clone(); // Clone the value
match entity {
Some(entity) => entity
.descriptions
.as_ref()
.and_then(|descriptions| descriptions.get("en"))
.map(|label| label.value.clone())
.unwrap_or_default(),
None => String::from("No description"),
}
}}
</td>
</tr> </tr>
} }
}).collect::<Vec<_>>() }).collect::<Vec<_>>()

View File

@ -9,6 +9,7 @@ pub struct Item {
pub description: String, pub description: String,
pub tags: Vec<(String, String)>, pub tags: Vec<(String, String)>,
pub reviews: Vec<ReviewWithRating>, pub reviews: Vec<ReviewWithRating>,
pub wikidata_id: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]