diff --git a/app/next-client-app/api/recommendations.ts b/app/next-client-app/api/recommendations.ts index 7d52a74ed..c01dcf811 100644 --- a/app/next-client-app/api/recommendations.ts +++ b/app/next-client-app/api/recommendations.ts @@ -2,32 +2,41 @@ import request from "@/lib/api/request"; import { - unisonBaseUrl, - unisonApiKey, - recommendationService, + recommendationServiceBaseUrl, + recommendationServiceApiKey, + recommendationServiceName, } from "@/constants"; -export const getConceptRecommendationsUnison = async ( - // Unison can query by concept name, or concept code (exact match). - // The latter is used first in searching, then the former. +export const getRecommendations = async ( queryValue: string, domainId: string -): Promise => { +): Promise => { try { - if (recommendationService === "unison") { - const endpoint = `${queryValue}?apiKey=${unisonApiKey}&domain=${domainId}`; - return await request(endpoint, { - baseUrl: unisonBaseUrl, + // Unison recommendation service + // Unison can query by concept name, or concept code (exact match). + // The latter is used first in searching, then the former. + if (recommendationServiceName === "unison") { + const endpoint = `${queryValue}?apiKey=${recommendationServiceApiKey}&domain=${domainId}`; + return await request(endpoint, { + baseUrl: recommendationServiceBaseUrl, headers: { Accept: "application/json", }, }); } - // TODO: Implement Lettuce recommendation service - else if (recommendationService === "lettuce") { - console.log("Lettuce recommendation service"); + // Lettuce recommendation service + if (recommendationServiceName === "lettuce") { + const endpoint = `${queryValue}?domain=${domainId}`; + return await request(endpoint, { + baseUrl: recommendationServiceBaseUrl, + headers: { + Accept: "application/json", + Authorization: `Bearer ${recommendationServiceApiKey}`, + }, + authMode: "apiKey", + }); } - + // Other recommendation services not supported throw new Error("Recommendation service not supported"); } catch (error) { console.error("Error fetching recommendations:", error); diff --git a/app/next-client-app/components/core/Tooltips.tsx b/app/next-client-app/components/core/Tooltips.tsx index deee96272..7b17b3b66 100644 --- a/app/next-client-app/components/core/Tooltips.tsx +++ b/app/next-client-app/components/core/Tooltips.tsx @@ -2,7 +2,7 @@ import { Tooltip, TooltipContent, TooltipProvider, - TooltipTrigger + TooltipTrigger, } from "@/components/ui/tooltip"; import { InfoIcon } from "lucide-react"; @@ -22,7 +22,10 @@ export function Tooltips({ - +

{content} {link && ( diff --git a/app/next-client-app/components/recommendations/ai-suggesions-button.tsx b/app/next-client-app/components/recommendations/ai-suggesions-button.tsx index d4489beec..32cb097d9 100644 --- a/app/next-client-app/components/recommendations/ai-suggesions-button.tsx +++ b/app/next-client-app/components/recommendations/ai-suggesions-button.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { Button } from "../ui/button"; import { Sparkles, Loader2 } from "lucide-react"; import AISuggestionDialog from "./ai-suggestions-dialog"; -import { getConceptRecommendationsUnison } from "@/api/recommendations"; +import { getRecommendations } from "@/api/recommendations"; import { addConcept } from "@/api/concepts"; import { toast } from "sonner"; import { @@ -11,7 +11,7 @@ import { DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuTrigger + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { domains } from "@/constants/domains"; import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu"; @@ -20,7 +20,7 @@ export function AISuggestionsButton({ value, tableId, rowId, - contentType + contentType, }: { value: string; tableId: string; @@ -29,7 +29,8 @@ export function AISuggestionsButton({ }) { const [isLoading, setIsLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); - const [suggestions, setSuggestions] = useState([]); + const [suggestions, setSuggestions] = useState([]); + const [metadata, setMetadata] = useState(null); const [domainId, setDomainId] = useState(""); // Fetches AI suggestions from the API @@ -42,14 +43,16 @@ export function AISuggestionsButton({ setIsLoading(true); try { - // Call the getConceptRecommendations function - const recommendations: UnisonConceptResponse = - await getConceptRecommendationsUnison(value, domainId); + const recommendations: RecommendationServiceResponse = + await getRecommendations(value, domainId); // Filter to get only unique concept IDs const uniqueRecommendations = recommendations.items.filter( (item, index, array) => array.findIndex((i) => i.conceptId === item.conceptId) === index ); + if (recommendations.metadata) { + setMetadata(recommendations.metadata); + } setSuggestions(uniqueRecommendations); setIsOpen(true); @@ -136,6 +139,7 @@ export function AISuggestionsButton({ rowId={rowId} domainId={domainId} contentType={contentType} + metadata={metadata} /> ); diff --git a/app/next-client-app/components/recommendations/ai-suggestions-dialog.tsx b/app/next-client-app/components/recommendations/ai-suggestions-dialog.tsx index bd8ce54ac..54b9e64ea 100644 --- a/app/next-client-app/components/recommendations/ai-suggestions-dialog.tsx +++ b/app/next-client-app/components/recommendations/ai-suggestions-dialog.tsx @@ -10,7 +10,7 @@ import { InfoItem } from "../core/InfoItem"; interface AISuggestionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - suggestions: UnisonConceptItem[]; + suggestions: RecommendationItem[]; onApplySuggestion: (data: { concept: number; object_id: number; @@ -23,6 +23,7 @@ interface AISuggestionDialogProps { rowId: number; domainId: string; contentType: string; + metadata?: RecommendationMetadata | null; } // Main dialog component combining all parts @@ -36,6 +37,7 @@ export default function AISuggestionDialog({ rowId, domainId, contentType, + metadata, }: AISuggestionDialogProps) { return (

@@ -66,6 +68,16 @@ export default function AISuggestionDialog({ } className="py-1 md:py-0 md:px-3" /> + {metadata && ( + + )} diff --git a/app/next-client-app/components/recommendations/columns.tsx b/app/next-client-app/components/recommendations/columns.tsx index 03b1ff872..052ddf3ee 100644 --- a/app/next-client-app/components/recommendations/columns.tsx +++ b/app/next-client-app/components/recommendations/columns.tsx @@ -16,7 +16,7 @@ export const columns = ( table_id: string; }) => void, contentType: string -): ColumnDef[] => [ +): ColumnDef[] => [ { id: "Concept Name", header: ({ column }) => ( @@ -24,7 +24,7 @@ export const columns = ( ), cell: ({ row }) => { const { conceptName } = row.original; - return
{conceptName}
; + return
{conceptName}
; }, enableSorting: false, enableHiding: true, @@ -63,13 +63,20 @@ export const columns = ( id: "Accuracy", header: ({ column }) => (
- +
), enableSorting: false, enableHiding: true, cell: ({ row }) => { - const { accuracy, explanation } = row.original; + const { accuracy, explanation, scores } = row.original; + if (scores) { + return ( +
+ {((1 - (scores?.["vector-search"] || 0)) * 100).toFixed(2)}% +
+ ); + } return (
{accuracy} diff --git a/app/next-client-app/components/recommendations/stored-recommendations-button.tsx b/app/next-client-app/components/recommendations/stored-recommendations-button.tsx index b55de20d8..e0e03b875 100644 --- a/app/next-client-app/components/recommendations/stored-recommendations-button.tsx +++ b/app/next-client-app/components/recommendations/stored-recommendations-button.tsx @@ -10,7 +10,7 @@ import { DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuTrigger + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { domains } from "@/constants/domains"; import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu"; @@ -20,7 +20,7 @@ export function StoredRecommendationsButton({ tableId, rowId, contentType, - mappingRecommendations + mappingRecommendations, }: { value: string; tableId: string; @@ -31,7 +31,7 @@ export function StoredRecommendationsButton({ mappingRecommendations: MappingRecommendation[]; }) { const [isOpen, setIsOpen] = useState(false); - const [suggestions, setSuggestions] = useState([]); + const [suggestions, setSuggestions] = useState([]); const [domainId, setDomainId] = useState(""); // Handle click to show stored recommendations @@ -57,7 +57,7 @@ export function StoredRecommendationsButton({ // Transform mapping recommendations to expected format const transformMappingRecommendations = ( recommendations: MappingRecommendation[] - ): UnisonConceptItem[] => { + ): RecommendationItem[] => { return recommendations.map((rec) => ({ accuracy: rec.score ?? null, conceptId: rec.concept.concept_id, @@ -68,7 +68,7 @@ export function StoredRecommendationsButton({ conceptClass: rec.concept.concept_class_id ?? "Unknown", explanation: rec.score ? `Pre-computed recommendation from ${rec.tool_name} (score: ${rec.score})` - : `Pre-computed recommendation from ${rec.tool_name} (no score)` + : `Pre-computed recommendation from ${rec.tool_name} (no score)`, })); }; diff --git a/app/next-client-app/components/recommendations/stored-recommendations-dialog.tsx b/app/next-client-app/components/recommendations/stored-recommendations-dialog.tsx index f7623faed..fbac9da45 100644 --- a/app/next-client-app/components/recommendations/stored-recommendations-dialog.tsx +++ b/app/next-client-app/components/recommendations/stored-recommendations-dialog.tsx @@ -9,7 +9,7 @@ import { InfoItem } from "../core/InfoItem"; interface RecommendationsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - suggestions: UnisonConceptItem[]; + suggestions: RecommendationItem[]; onApplySuggestion: (data: { concept: number; object_id: number; @@ -36,7 +36,7 @@ export default function RecommendationsDialog({ rowId, domainId, contentType, - source + source, }: RecommendationsDialogProps) { const getSourceLabel = () => { return source === "v3" diff --git a/app/next-client-app/constants/index.ts b/app/next-client-app/constants/index.ts index fb67d81d6..81cc8a7e6 100644 --- a/app/next-client-app/constants/index.ts +++ b/app/next-client-app/constants/index.ts @@ -3,11 +3,11 @@ import { MAX_FILE_SIZE_BYTES } from "./config"; export const apiUrl = process.env.BACKEND_URL; -export const recommendationService = process.env.RECOMMENDATION_SERVICE; +export const recommendationServiceBaseUrl = + process.env.RECOMMENDATION_SERVICE_BASE_URL; -export const unisonBaseUrl = process.env.UNISON_BASE_URL; - -export const unisonApiKey = process.env.UNISON_API_KEY; +export const recommendationServiceApiKey = + process.env.RECOMMENDATION_SERVICE_API_KEY; export const enableReuseTriggerOption = env( "NEXT_PUBLIC_ENABLE_REUSE_TRIGGER_OPTION" @@ -22,7 +22,7 @@ export const enableStoredRecommendation = env( ); export const recommendationServiceName = env( - "NEXT_PUBLIC_RECOMMENDATION_SERVICE_NAME" + "NEXT_PUBLIC_RECOMMENDATION_SERVICE" ); // Re-export MAX_FILE_SIZE_BYTES from config.js diff --git a/app/next-client-app/package-lock.json b/app/next-client-app/package-lock.json index b560f383c..5f2039d6c 100644 --- a/app/next-client-app/package-lock.json +++ b/app/next-client-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "next-client-app", - "version": "0.1.0", + "version": "4.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "next-client-app", - "version": "0.1.0", + "version": "4.2.2", "dependencies": { "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1", diff --git a/app/next-client-app/types/recommendation.ts b/app/next-client-app/types/recommendation.ts index 20c375c10..c972b9ac2 100644 --- a/app/next-client-app/types/recommendation.ts +++ b/app/next-client-app/types/recommendation.ts @@ -1,4 +1,4 @@ -interface UnisonConceptItem { +interface RecommendationItem { accuracy: number | null; conceptId: number; conceptName: string; @@ -7,11 +7,21 @@ interface UnisonConceptItem { domain: string; conceptClass: string; explanation: string; + standardConcept?: string; + scores?: { "vector-search": number }; } -interface UnisonConceptResponse { - items: UnisonConceptItem[]; - count: number; +interface RecommendationMetadata { + assistant: string; + version: string; + pipeline: string; + info: string | null; +} + +interface RecommendationServiceResponse { + items: RecommendationItem[]; + count?: number; + metadata?: RecommendationMetadata; } interface MappingRecommendation { @@ -23,4 +33,4 @@ interface MappingRecommendation { tool_name: string; tool_version: string; created_at: Date; -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index 5e0e131b4..38439dcbf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,10 +73,9 @@ services: - WATCHPACK_POLLING=true - NEXT_PUBLIC_ENABLE_REUSE_TRIGGER_OPTION=true - NEXT_PUBLIC_ENABLE_AI_RECOMMENDATION=true - - NEXT_PUBLIC_RECOMMENDATION_SERVICE_NAME=unison - - RECOMMENDATION_SERVICE=unison - - UNISON_API_KEY=unison-api-key - - UNISON_BASE_URL=https://api.hyperunison.com/api/public/suggester/generate + - NEXT_PUBLIC_RECOMMENDATION_SERVICE=unison + - RECOMMENDATION_SERVICE_BASE_URL=https://api.hyperunison.com/api/public/suggester/generate + - RECOMMENDATION_SERVICE_API_KEY=unison-api-key - NEXT_PUBLIC_BODY_SIZE_LIMIT=31457280 volumes: - ./app/next-client-app:/app