From 945caa84e3ae3cc0168a950a642bce75ef71908c Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Thu, 20 Feb 2025 18:51:18 +0000 Subject: [PATCH 01/26] WIP wired up to reporter service call --- .../records/AiExpressionSummary.tsx | 94 +++++++++++++++++++ .../GeneRecordClasses.GeneRecordClass.jsx | 3 + .../js/client/types/aiExpressionTypes.ts | 11 +++ 3 files changed, 108 insertions(+) create mode 100644 packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx create mode 100644 packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx new file mode 100644 index 0000000000..b6e1aed2ad --- /dev/null +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { + CollapsibleSection, + RecordAttribute, +} from '@veupathdb/wdk-client/lib/Components'; +import { Props } from '@veupathdb/wdk-client/lib/Views/Records/RecordAttributes/RecordAttributeSection'; + +import { DefaultSectionTitle } from '@veupathdb/wdk-client/lib/Views/Records/SectionTitle'; +import { ErrorBoundary } from '@veupathdb/wdk-client/lib/Controllers'; +import { useWdkService } from '@veupathdb/wdk-client/lib/Hooks/WdkServiceHook'; +import { isGenomicsService } from '../../wrapWdkService'; +import { + AnswerSpec, + StandardReportConfig, +} from '@veupathdb/wdk-client/lib/Utils/WdkModel'; +import { AiExpressionSummaryResponse } from '../../types/aiExpressionTypes'; +import { AnswerFormatting } from '@veupathdb/wdk-client/lib/Service/Mixins/SearchReportsService'; + +/** Display AI Expression Summary UI and results in a collapsible section */ +export function AiExpressionSummary(props: Props) { + const { + attribute, + record, + recordClass, + isCollapsed, + onCollapsedChange, + title, + } = props; + const { displayName, help, name } = attribute; + + const headerContent = title ?? ( + + ); + + return ( + + + {record.attributes['ai_expression'] == 'YES' ? ( + + ) : ( +
Sorry, this feature is not currently available.
+ )} +
+
+ ); +} + +// if the AI expression summary is cached, render it +// otherwise render a "gate" where the user is given some verbiage +// about the request taking a minute or two and a button to initiate +// the request. + +function CacheGate(props: Props) { + const geneId = props.record.attributes['source_id']?.toString(); + if (geneId == null) return null; + + const cachedSummary = useAiExpression(geneId, false); // do not populate cache + + return
this is going to be the AI summary
; +} + +function useAiExpression( + geneId: string, + shouldPopulateCache: boolean +): AiExpressionSummaryResponse | undefined { + return useWdkService(async (wdkService) => { + if (!isGenomicsService(wdkService)) throw new Error('nasty'); + const { projectId } = await wdkService.getConfig(); + const answerSpec = { + searchName: 'single_record_question_GeneRecordClasses_GeneRecordClass', + searchConfig: { + parameters: { + primaryKeys: `${geneId},${projectId}`, + }, + }, + }; + const formatting = { + format: 'aiExpression', + formatConfig: { + shouldPopulateCache, + }, + }; + return await wdkService.getAnswer( + answerSpec, + formatting + ); + }); +} diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx index f3c9ea7e6d..17aac6fcbd 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx @@ -44,6 +44,7 @@ import { import betaImage from '@veupathdb/wdk-client/lib/Core/Style/images/beta2-30.png'; import { LinksPosition } from '@veupathdb/coreui/lib/components/inputs/checkboxes/CheckboxTree/CheckboxTree'; import { AlphaFoldRecordSection } from './AlphaFoldAttributeSection'; +import { AiExpressionSummary } from './AiExpressionSummary'; import { DEFAULT_TABLE_STATE } from '@veupathdb/wdk-client/lib/StoreModules/RecordStoreModule'; import { Link } from 'react-router-dom'; @@ -354,6 +355,8 @@ export function RecordAttributeSection(props) { switch (restProps.attribute.name) { case 'alphafold_url': return ; + case 'ai_expression': + return ; default: return ; } diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts new file mode 100644 index 0000000000..c485831e52 --- /dev/null +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts @@ -0,0 +1,11 @@ +export interface AiExpressionSummaryResponse { + headline: string; + one_paragraph_summary: string; + sections: AiExpressionSummarySection[]; +} + +interface AiExpressionSummarySection { + headline: string; + one_sentence_summary: string; + dataset_ids: string[]; +} From 8979816207562c0db70b9dbfcd07ac957460eef9 Mon Sep 17 00:00:00 2001 From: Bob Date: Sun, 23 Feb 2025 18:07:42 +0000 Subject: [PATCH 02/26] WIP --- .../records/AiExpressionSummary.tsx | 81 +++++++++++++------ .../js/client/types/aiExpressionTypes.ts | 13 ++- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx index b6e1aed2ad..3ecf092fb8 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx @@ -1,8 +1,5 @@ -import React from 'react'; -import { - CollapsibleSection, - RecordAttribute, -} from '@veupathdb/wdk-client/lib/Components'; +import React, { useEffect, useState } from 'react'; +import { CollapsibleSection } from '@veupathdb/wdk-client/lib/Components'; import { Props } from '@veupathdb/wdk-client/lib/Views/Records/RecordAttributes/RecordAttributeSection'; import { DefaultSectionTitle } from '@veupathdb/wdk-client/lib/Views/Records/SectionTitle'; @@ -10,22 +7,13 @@ import { ErrorBoundary } from '@veupathdb/wdk-client/lib/Controllers'; import { useWdkService } from '@veupathdb/wdk-client/lib/Hooks/WdkServiceHook'; import { isGenomicsService } from '../../wrapWdkService'; import { - AnswerSpec, - StandardReportConfig, -} from '@veupathdb/wdk-client/lib/Utils/WdkModel'; -import { AiExpressionSummaryResponse } from '../../types/aiExpressionTypes'; -import { AnswerFormatting } from '@veupathdb/wdk-client/lib/Service/Mixins/SearchReportsService'; + AiExpressionSummary, + AiExpressionSummaryResponse, +} from '../../types/aiExpressionTypes'; /** Display AI Expression Summary UI and results in a collapsible section */ export function AiExpressionSummary(props: Props) { - const { - attribute, - record, - recordClass, - isCollapsed, - onCollapsedChange, - title, - } = props; + const { attribute, record, isCollapsed, onCollapsedChange, title } = props; const { displayName, help, name } = attribute; const headerContent = title ?? ( @@ -42,7 +30,7 @@ export function AiExpressionSummary(props: Props) { > {record.attributes['ai_expression'] == 'YES' ? ( - + ) : (
Sorry, this feature is not currently available.
)} @@ -51,21 +39,64 @@ export function AiExpressionSummary(props: Props) { ); } -// if the AI expression summary is cached, render it +// If the AI expression summary is cached, render it // otherwise render a "gate" where the user is given some verbiage // about the request taking a minute or two and a button to initiate // the request. -function CacheGate(props: Props) { +function AiSummaryGate(props: Props) { const geneId = props.record.attributes['source_id']?.toString(); - if (geneId == null) return null; + if (geneId == null) throw new Error('geneId should not be missing'); + + const [shouldPopulateCache, setShouldPopulateCache] = useState(false); + + const aiExpressionSummary = useAiExpressionSummary( + geneId, + shouldPopulateCache + ); + + if (aiExpressionSummary) { + if (aiExpressionSummary[geneId]?.cacheStatus === 'hit') { + const summary = aiExpressionSummary[geneId].expressionSummary; + return ; + } else { + // Cache miss: render button to populate cache + return ( +
+

+ Click below to start an AI summary of this gene. It could take up to + three minutes. +

+ - const cachedSummary = useAiExpression(geneId, false); // do not populate cache + {/* Debugging: Display cache miss reason if present */} + {aiExpressionSummary[geneId]?.reason && ( +

+ Debug: Cache miss reason - {aiExpressionSummary[geneId].reason} +

+ )} +
+ ); + } + } - return
this is going to be the AI summary
; + return
🤖 Summarising... 🤖
; +} + +function AiExpressionResult(props: Props & { summary: AiExpressionSummary }) { + const headline = props.summary.headline; + return ( +
+ Here are today's headlines: +
+ {headline} +
+ ); } -function useAiExpression( +function useAiExpressionSummary( geneId: string, shouldPopulateCache: boolean ): AiExpressionSummaryResponse | undefined { diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts index c485831e52..7d82c787ac 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts @@ -1,4 +1,15 @@ -export interface AiExpressionSummaryResponse { +export type AiExpressionSummaryResponse = Record< + string, + AiExpressionGeneResponse +>; + +export interface AiExpressionGeneResponse { + cacheStatus: 'hit' | 'miss'; + reason?: string; // only for misses + expressionSummary: AiExpressionSummary; +} + +export interface AiExpressionSummary { headline: string; one_paragraph_summary: string; sections: AiExpressionSummarySection[]; From 01a4ec98df6bb2e2cb9e66b58469e1c329f43c52 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 24 Feb 2025 17:56:45 +0000 Subject: [PATCH 03/26] bare bones UI is working, yay --- .../records/AiExpressionSummary.tsx | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx index 3ecf092fb8..5f2687419f 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { CollapsibleSection } from '@veupathdb/wdk-client/lib/Components'; import { Props } from '@veupathdb/wdk-client/lib/Views/Records/RecordAttributes/RecordAttributeSection'; @@ -64,8 +64,8 @@ function AiSummaryGate(props: Props) { return (

- Click below to start an AI summary of this gene. It could take up to - three minutes. + Click below to request an AI summary of this gene. It could take up + to three minutes. When complete it will be cached for all users.

+ const [pollingCounter, setPollingCounter] = useState(-1); + const pollingTimeout = useRef>(); + const pollingResponse = useAiExpressionSummary(geneId, false, pollingCounter); - {/* Debugging: Display cache miss reason if present */} - {aiExpressionSummary[geneId]?.reason && ( -

- Debug: Cache miss reason - {aiExpressionSummary[geneId].reason} -

- )} -
+ // update polling counter when the main request is active + useEffect(() => { + if (shouldPopulateCache && completeExpressionSummary == null) { + pollingTimeout.current = setTimeout( + () => setPollingCounter(pollingCounter + 1), + POLL_TIME_MS ); } + return () => clearTimeout(pollingTimeout.current); + }, [shouldPopulateCache, completeExpressionSummary, pollingCounter]); + + if (completeExpressionSummary) { + return ( + + ); + } else if (!shouldPopulateCache) { + // Cache miss: render button to populate cache + return ( +
+

+ Click below to request an AI summary of this gene. It could take up to + three minutes. When complete it will be cached for all users. +

+ + + {/* Debugging: Display cache miss reason if present */} +

+ Debug: resultStatus = {geneResponse?.resultStatus} +

+
+ ); } if (shouldPopulateCache) { + const { numExperiments = 0, numExperimentsComplete = 0 } = + pollingResponse?.[geneId] ?? {}; + return (
-

🤖 Summarizing... (can take up to three minutes) 🤖

- +

🤖 Summarizing... 🤖

+ + {numExperimentsComplete}/{numExperiments} +
); } else { @@ -309,11 +332,13 @@ const AiExpressionResult = connector((props: AiExpressionResultProps) => { function useAiExpressionSummary( geneId: string, - shouldPopulateCache: boolean + shouldPopulateCache: boolean, + pollingCounter: number = 0 ): AiExpressionSummaryResponse | undefined { return useWdkService( async (wdkService) => { if (!isGenomicsService(wdkService)) throw new Error('nasty'); + if (pollingCounter < 0) return undefined; const { projectId } = await wdkService.getConfig(); const answerSpec = { searchName: 'single_record_question_GeneRecordClasses_GeneRecordClass', @@ -334,6 +359,6 @@ function useAiExpressionSummary( formatting ); }, - [geneId, shouldPopulateCache] + [geneId, shouldPopulateCache, pollingCounter] ); } diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts index fb738de132..d5277c9ca7 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/types/aiExpressionTypes.ts @@ -3,10 +3,21 @@ export type AiExpressionSummaryResponse = Record< AiExpressionGeneResponse >; +type StatusString = + | 'present' + | 'missing' + | 'failed' + | 'expired' + | 'corrupted' + | 'undetermined'; + +type SummaryStatusString = StatusString | 'experiments_incomplete'; + export interface AiExpressionGeneResponse { - cacheStatus: 'hit' | 'miss'; - reason?: string; // only for misses - expressionSummary: AiExpressionSummary; + resultStatus: SummaryStatusString; + numExperiments?: number; + numExperimentsComplete?: number; + expressionSummary?: AiExpressionSummary; } export interface AiExpressionSummary { From 8bf5bb1373cbb4d080c245284b631ee660dedd64 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 6 Mar 2025 18:49:41 +0000 Subject: [PATCH 24/26] handle initial loading properly --- .../js/client/components/records/AiExpressionSummary.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx index ecd0948b0a..6f44e6fd23 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx @@ -116,7 +116,9 @@ function AiSummaryGate(props: Props) { return () => clearTimeout(pollingTimeout.current); }, [shouldPopulateCache, completeExpressionSummary, pollingCounter]); - if (completeExpressionSummary) { + if (aiExpressionSummary == null) { + return
Loading...
; + } else if (completeExpressionSummary) { return ( ); @@ -138,8 +140,7 @@ function AiSummaryGate(props: Props) {

); - } - if (shouldPopulateCache) { + } else { const { numExperiments = 0, numExperimentsComplete = 0 } = pollingResponse?.[geneId] ?? {}; @@ -151,8 +152,6 @@ function AiSummaryGate(props: Props) { ); - } else { - return
Loading...
; } } From ad4c07e8243dbe3bd3e3caeab663afdc067f313d Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 7 Mar 2025 16:17:36 +0000 Subject: [PATCH 25/26] fixed up Loading spinner behaviour --- .../components/records/AiExpressionSummary.scss | 8 ++++++++ .../client/components/records/AiExpressionSummary.tsx | 11 +++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss index 5c437c0456..27ac3bb5f9 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss @@ -10,3 +10,11 @@ div.ai-summary { a.javascript-link { cursor: grab; } + +.AiExpressionResult-Loading { + text-align: center; + padding: 0; + margin-top: 4em; + height: unset; + max-width: 125px; +} diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx index 6f44e6fd23..85f3566427 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx @@ -141,14 +141,17 @@ function AiSummaryGate(props: Props) { ); } else { - const { numExperiments = 0, numExperimentsComplete = 0 } = + const { numExperiments, numExperimentsComplete } = pollingResponse?.[geneId] ?? {}; - + const progressString = + numExperiments != null && numExperimentsComplete != null + ? `${numExperimentsComplete}/${numExperiments + 1}` + : ''; return (

🤖 Summarizing... 🤖

- - {numExperimentsComplete}/{numExperiments} + + {progressString}
); From b3a5764e9850866534bc143a94bb79e5d1847de5 Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 8 Mar 2025 00:01:28 +0000 Subject: [PATCH 26/26] final progress styling tweaks --- .../js/client/components/records/AiExpressionSummary.scss | 4 ++-- .../js/client/components/records/AiExpressionSummary.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss index 27ac3bb5f9..fd89ba55d1 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.scss @@ -14,7 +14,7 @@ a.javascript-link { .AiExpressionResult-Loading { text-align: center; padding: 0; - margin-top: 4em; + margin-top: 3em; height: unset; - max-width: 125px; + max-width: 75px; } diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx index 85f3566427..f36b9f3d6c 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AiExpressionSummary.tsx @@ -146,10 +146,10 @@ function AiSummaryGate(props: Props) { const progressString = numExperiments != null && numExperimentsComplete != null ? `${numExperimentsComplete}/${numExperiments + 1}` - : ''; + : '🤖'; return (
-

🤖 Summarizing... 🤖

+

Summarizing...

{progressString}