import * as utils from "@helpers";
// import {
// type FederatedIndexManager,
// getFederatedIndexManager,
// type IndexType,
// type NegotiatedIndexEntry,
// } from "@helpers/dsp/FederatedIndexManager";
import { msg } from "@lit/localize";
import { Task } from "@lit/task";
import type { Resource } from "@src/component";
import { css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
/**
* Search result with source attribution and auto-negotiation flag
*/
interface IndexSearchResult {
resource: Resource;
indexSource: string;
indexType: any; //IndexType;
providerId: string;
wasAutoNegotiated?: boolean;
}
@customElement("solid-tems-index-search")
export class TemsIndexSearch extends utils.OrbitComponent {
static styles = css`
:host {
display: block;
}
.search-container {
padding: 1rem;
}
.search-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.search-input-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 0.5rem 1rem;
}
.search-input-wrapper input {
flex: 1;
border: none;
outline: none;
font-size: 1rem;
}
.search-input-wrapper icon-mingcute-search-3-line {
color: #666;
}
.indexes-section {
margin-bottom: 1.5rem;
}
.indexes-section h3 {
font-size: 0.875rem;
color: #666;
margin-bottom: 0.5rem;
}
.indexes-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.index-tag {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.875rem;
background: #f5f5f5;
border: 1px solid #e0e0e0;
}
.index-tag.active {
background: #e3f2fd;
border-color: #1976d2;
color: #1976d2;
}
.index-tag .provider-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.no-indexes {
text-align: center;
padding: 3rem;
background: #fafafa;
border-radius: 8px;
color: #666;
}
.no-indexes icon-material-symbols-database-outline {
font-size: 3rem;
color: #ccc;
margin-bottom: 1rem;
}
.no-indexes h3 {
margin: 0 0 0.5rem 0;
color: #333;
}
.no-indexes p {
margin: 0;
font-size: 0.875rem;
}
.results-section {
margin-top: 1.5rem;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.results-count {
font-size: 0.875rem;
color: #666;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.result-card {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e0e0e0;
cursor: pointer;
transition: box-shadow 0.2s;
}
.result-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.result-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.result-card-title {
font-weight: 600;
color: #333;
}
.result-card-type {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 4px;
background: #f5f5f5;
}
.result-card-description {
font-size: 0.875rem;
color: #666;
margin-bottom: 0.5rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.result-card-source {
font-size: 0.75rem;
color: #999;
display: flex;
align-items: center;
gap: 0.25rem;
}
.searching-indicator {
text-align: center;
padding: 2rem;
color: #666;
}
.no-results {
text-align: center;
padding: 2rem;
color: #666;
}
.auto-negotiated-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: 0.5rem;
padding: 0.125rem 0.5rem;
background: #e8f5e9;
color: #2e7d32;
border-radius: 4px;
font-size: 0.75rem;
}
.auto-negotiated-badge icon-mdi-auto-fix {
font-size: 0.875rem;
}
.result-card.auto-negotiated {
border-color: #4caf50;
background: linear-gradient(to right, rgba(76, 175, 80, 0.05), white);
}
.result-card-badges {
display: flex;
align-items: center;
gap: 0.5rem;
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.25rem;
border-radius: 4px;
font-size: 0.75rem;
}
.badge-auto {
background: #e8f5e9;
color: #2e7d32;
}
.badge-auto icon-mdi-auto-fix {
font-size: 0.875rem;
}
/* Detail Modal Styles */
.detail-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.detail-modal {
background: white;
border-radius: 12px;
width: 100%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.detail-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
}
.detail-modal-header h2 {
margin: 0;
font-size: 1.25rem;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-modal-close {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
color: #666;
}
.detail-modal-close:hover {
background: #e0e0e0;
}
.detail-modal-close icon-material-symbols-close-rounded {
font-size: 1.25rem;
}
.detail-modal-content {
padding: 1.5rem;
overflow-y: auto;
}
.detail-source-info {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
font-size: 0.875rem;
color: #666;
padding: 0.75rem;
background: #f5f5f5;
border-radius: 8px;
}
.detail-index-type {
font-weight: 500;
color: #333;
}
.detail-separator {
color: #ccc;
}
.detail-auto-negotiated {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: #2e7d32;
}
.detail-auto-negotiated icon-mdi-auto-fix {
font-size: 0.875rem;
}
.detail-section {
margin-bottom: 1.25rem;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h3 {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #999;
margin: 0 0 0.5rem 0;
letter-spacing: 0.05em;
}
.detail-section p {
margin: 0;
color: #333;
word-break: break-word;
}
.detail-resource-id {
font-family: monospace;
font-size: 0.875rem;
background: #f5f5f5;
padding: 0.5rem;
border-radius: 4px;
word-break: break-all;
}
.detail-properties {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-property {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
background: #fafafa;
border-radius: 6px;
}
.detail-property-label {
font-size: 0.75rem;
font-weight: 600;
color: #666;
}
.detail-property-value {
font-size: 0.875rem;
color: #333;
word-break: break-word;
}
`;
@property({ attribute: "header", type: String })
header?: string = "Index Search";
/**
* Filter to a specific index type.
* If not set, searches across all available index types.
* Valid values: "fact-check", "3d-object", "generic"
*/
@property({ attribute: "index-type", type: String })
indexType?: IndexType;
@state()
searchQuery = "";
@state()
negotiatedIndexes: NegotiatedIndexEntry[] = [];
@state()
searchResults: IndexSearchResult[] = [];
@state()
isSearching = false;
@state()
hasSearched = false;
@state()
autoNegotiatedCount = 0;
@state()
selectedResult: IndexSearchResult | null = null;
private federatedIndexManager: FederatedIndexManager | null = null;
/**
* Load negotiated indexes using FederatedIndexManager
*/
_loadIndexes = new Task(this, {
task: async () => {
if (!this.orbit) return [];
// Debug: Log the index-type attribute valu
// Initialize FederatedIndexManager with consumer info
// Get participant ID from dspStore config or orbit component, with sensible fallback
const dspStore = window.dspStore;
const orbitClient = this.orbit?.client as any;
const consumerParticipantId =
dspStore?.config?.participantId ||
orbitClient?.participantId ||
"stbx-consumer";
const consumerConnectorUrl =
dspStore?.config?.endpoint || orbitClient?.connectorUrl || "";
try {
this.federatedIndexManager = getFederatedIndexManager({
consumerParticipantId,
consumerConnectorUrl,
dspStore,
});
} catch {
// Manager already initialized, get existing instance
this.federatedIndexManager = getFederatedIndexManager();
}
// Build federated indexes from active contracts
await this.federatedIndexManager.buildFederatedIndexes();
// Get index entries - filter by type if specified
let indexes: NegotiatedIndexEntry[];
if (this.indexType) {
indexes = this.federatedIndexManager.getEntriesByType(this.indexType);
} else {
indexes = this.federatedIndexManager.getAllEntries();
}
this.negotiatedIndexes = indexes;
return indexes;
},
args: () => [this.currentRoute],
});
/**
* Execute search across all negotiated indexes using FederatedIndexManager
*/
private async _executeSearch() {
if (!this.searchQuery.trim() || this.negotiatedIndexes.length === 0) {
return;
}
if (!this.federatedIndexManager) {
console.error("[Index Search] FederatedIndexManager not initialized");
return;
}
this.isSearching = true;
this.hasSearched = true;
this.searchResults = [];
this.autoNegotiatedCount = 0;
try {
// Get index types to search - use specified type or all available types
const indexTypes: IndexType[] = this.indexType
? [this.indexType]
: [...new Set(this.negotiatedIndexes.map((idx) => idx.type))];
// Search across index types
const allResults: IndexSearchResult[] = [];
for (const indexType of indexTypes) {
const { results, negotiatedResources } =
await this.federatedIndexManager.searchWithAutoNegotiation({
indexType,
query: this.searchQuery,
propertyName: "sib:title", // Title-based search using proper RDF property
});
// Track auto-negotiated resources
this.autoNegotiatedCount += negotiatedResources.length;
// Convert FederatedSearchResult to IndexSearchResult
for (const result of results) {
allResults.push({
resource: result.resource as Resource,
indexSource: result.indexSource,
indexType: result.indexType,
providerId: result.providerId,
wasAutoNegotiated: result.wasAutoNegotiated,
});
}
}
this.searchResults = allResults;
} catch (error) {
console.error("[Index Search] Search failed:", error);
} finally {
this.isSearching = false;
}
}
/**
* Handle search input
*/
private _onSearchInput(e: Event) {
const input = e.target as HTMLInputElement;
this.searchQuery = input.value;
}
/**
* Handle search submit
*/
private _onSearchSubmit(e: Event) {
e.preventDefault();
this._executeSearch();
}
/**
* Handle Enter key in search input
*/
private _onSearchKeydown(e: KeyboardEvent) {
if (e.key === "Enter") {
e.preventDefault();
this._executeSearch();
}
}
/**
* Get icon for index type
*/
private _getIndexIcon(type: IndexType) {
switch (type) {
case "fact-check":
return html`
${msg( "Negotiate access to datasets with searchable indexes in the Consumer Catalog to enable searching.", )}
${msg("Searching across")} ${this.negotiatedIndexes.length} ${msg("indexes")}...
${msg("No results found for")} "${this.searchQuery}"