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``; case "3d-object": return html``; default: return html``; } } /** * Render no indexes state */ private _renderNoIndexes() { return html`

${msg("No indexes available")}

${msg( "Negotiate access to datasets with searchable indexes in the Consumer Catalog to enable searching.", )}

`; } /** * Render search results */ private _renderResults() { if (this.isSearching) { return html`

${msg("Searching across")} ${this.negotiatedIndexes.length} ${msg("indexes")}...

`; } if (!this.hasSearched) { return nothing; } if (this.searchResults.length === 0) { return html`

${msg("No results found for")} "${this.searchQuery}"

`; } return html`
${this.searchResults.length} ${msg("results")} ${this.autoNegotiatedCount > 0 ? html` ${this.autoNegotiatedCount} ${msg("auto-negotiated")} ` : nothing}
${this.searchResults.map( (result) => html`
this._onResultClick(result)} >
${result.resource.title || result.resource.name || result.resource["@id"]}
${result.wasAutoNegotiated ? html` ` : nothing} ${result.indexType}
${result.resource.description ? html`
${result.resource.description}
` : nothing}
${this._getIndexIcon(result.indexType)} ${result.indexSource}
`, )}
`; } /** * Handle result card click - open detail modal */ private _onResultClick(result: IndexSearchResult) { this.selectedResult = result; } /** * Close detail modal */ private _closeDetailModal() { this.selectedResult = null; } /** * Get resource display title */ private _getResourceTitle(resource: Resource): string { return ( (resource.title as string) || (resource.name as string) || resource["@id"] || "Unknown" ); } /** * Format resource properties for display */ private _formatPropertyValue(value: any): string { if (value === null || value === undefined) return ""; if (typeof value === "string") return value; if (typeof value === "boolean") return value ? "Yes" : "No"; if (typeof value === "number") return value.toString(); if (Array.isArray(value)) return value.map((v) => this._formatPropertyValue(v)).join(", "); if (typeof value === "object" && value["@id"]) return value["@id"]; if (typeof value === "object") return JSON.stringify(value); return String(value); } /** * Render detail modal for selected search result */ private _renderDetailModal() { if (!this.selectedResult) return nothing; const resource = this.selectedResult.resource; const title = this._getResourceTitle(resource); // Get displayable properties (exclude internal ones) const excludeProps = ["@id", "@type", "@context"]; const displayProps = Object.keys(resource) .filter( (key) => !excludeProps.includes(key) && resource[key] !== undefined && resource[key] !== null, ) .map((key) => ({ key, label: this._formatPropertyLabel(key), value: this._formatPropertyValue(resource[key]), })) .filter((prop) => prop.value.length > 0); return html`
e.stopPropagation()}>

${title}

${this._getIndexIcon(this.selectedResult.indexType)} ${this.selectedResult.indexType} ${msg("Source")}: ${this.selectedResult.indexSource} ${this.selectedResult.wasAutoNegotiated ? html` ${msg("Auto-negotiated")} ` : nothing}

${msg("Resource ID")}

${resource["@id"]}

${resource["@type"] ? html`

${msg("Type")}

${Array.isArray(resource["@type"]) ? resource["@type"].join(", ") : resource["@type"]}

` : nothing} ${displayProps.length > 0 ? html`

${msg("Properties")}

${displayProps.map( (prop) => html`
${prop.label} ${prop.value}
`, )}
` : nothing}
`; } /** * Format property key for display (e.g., "sib:title" -> "Title") */ private _formatPropertyLabel(key: string): string { // Remove namespace prefix if present let label = key.includes(":") ? key.split(":")[1] : key; // Convert camelCase to Title Case label = label.replace(/([A-Z])/g, " $1").trim(); // Capitalize first letter return label.charAt(0).toUpperCase() + label.slice(1); } render() { if (!this.orbit) { return nothing; } return this._loadIndexes.render({ pending: () => html``, complete: (indexes) => html`
${indexes && indexes.length > 0 ? html`
this._executeSearch()} ?disabled=${!this.searchQuery.trim()} >

${msg("Available Indexes")} (${indexes.length})

${indexes.map( (index) => html`
${this._getIndexIcon(index.type)} ${index.name}
`, )}
${this._renderResults()} ` : this._renderNoIndexes()}
${this._renderDetailModal()} `, }); } }