import 'dart:convert'; import 'package:capstone_project/utils/agrigator.dart'; import 'package:http/http.dart' as http; const _baseUrl = 'http://duriin.imbenji.net'; // pair of (article, semantic distance from the query / seed). distance is // null when the underlying endpoint doesnt return one. class SimilarHit { final FeedItem item; final double? distance; SimilarHit({required this.item, this.distance}); } class DuriinService { Future> search({ required String ticker, required String companyName, required DateTime from, required DateTime to, int limit = 100, }) async { final fromStr = from.toUtc().toIso8601String(); final toStr = to.toUtc().toIso8601String(); // build the keyword set: the ticker itself plus the company brand name // (first token of the registered name — "NVIDIA Corporation" -> "NVIDIA", // "Apple Inc" -> "Apple"). the full legal name rarely appears verbatim in // articles so matching on it would miss most hits. final keywords = {}; final t = ticker.trim(); if (t.isNotEmpty) keywords.add(t); final brand = companyName.trim().split(RegExp(r"\s+")).firstOrNull ?? ""; if (brand.isNotEmpty) keywords.add(brand); final hits = await _fetchHits(queryParams: { 'keyword': keywords.toList(), 'keyword_mode': 'or', 'from': fromStr, 'to': toStr, 'limit': '$limit', 'order': 'newest', }); final items = hits.map((h) => h.item).toList(); print('Duriin: ${items.length} articles for $ticker / $companyName (kw=${keywords.join(",")})'); return items; } // neighbours of a given article via the vector index. used by the event // clusterer to build real clusters instead of arbitrary time slices. Future> findSimilar(int articleId, {int limit = 25}) async { return _fetchHits(queryParams: { 'similar_to_article': '$articleId', 'limit': '$limit', }); } Future> _fetchHits({required Map queryParams}) async { final uri = Uri.parse(_baseUrl).replace( path: '/articles', queryParameters: queryParams, ); try { final response = await http.get(uri); if (response.statusCode != 200) { print('Duriin: HTTP ${response.statusCode} for $uri'); return []; } final body = response.body.trim(); if (body.isEmpty) return []; final list = jsonDecode(body) as List; return list.map((raw) { final m = raw as Map; final id = m['id'] is int ? m['id'] as int : int.tryParse('${m['id']}'); final item = FeedItem( id: id, title: (m['title'] ?? '').toString(), description: (m['description'] ?? '').toString(), content: (m['content'] ?? '').toString(), link: (m['url'] ?? '').toString(), source: (m['source'] ?? '').toString().isNotEmpty ? m['source'].toString() : null, pubDate: m['pub_date'] != null ? DateTime.tryParse(m['pub_date'].toString()) : null, ); final distRaw = m['distance']; final distance = distRaw is num ? distRaw.toDouble() : null; return SimilarHit(item: item, distance: distance); }).where((h) => h.item.title.isNotEmpty && h.item.link.isNotEmpty).toList(); } catch (e) { print('Duriin fetch error: $e'); return []; } } }