From e9296a11ae9efdaf9faae6c7c00ec3e4a57eed97 Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:11:04 -0500 Subject: [PATCH 01/16] add back NH Type, Year Pub, and Clear filters --- .../PublicationSearchToolbar.tsx | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/client/modules/datafiles/src/publications/PublicationSearchToolbar/PublicationSearchToolbar.tsx b/client/modules/datafiles/src/publications/PublicationSearchToolbar/PublicationSearchToolbar.tsx index 282a69022..e753a92d1 100644 --- a/client/modules/datafiles/src/publications/PublicationSearchToolbar/PublicationSearchToolbar.tsx +++ b/client/modules/datafiles/src/publications/PublicationSearchToolbar/PublicationSearchToolbar.tsx @@ -1,7 +1,9 @@ -import { Button, Form, Input, Collapse } from 'antd'; +import { Button, Form, Input, Collapse, Select } from 'antd'; import React, { useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useSyncExternalStore } from 'react'; +import * as dropdownOptions from '../../projects/forms/ProjectFormDropdowns'; +import styles from './PublicationSearchToolbar.module.css' const { Panel } = Collapse; @@ -71,8 +73,16 @@ export const PublicationSearchToolbar: React.FC = () => { setSearchParams(params); }; + const currentYear = new Date(Date.now()).getUTCFullYear(); + //Show events going back to 2015 + const datesInRange = []; + for (let i = currentYear; i >= 2015; i--) { + datesInRange.push(i); + } + const yearOptions = [...datesInRange.map((y) => ({ label: y, value: y }))]; + return ( -
{
)} +
+ +   + setSearchParam('pub-year', v)} + /> +
+ + ); }; From e010ff54ca3330947f4d999758dac9d2e98490d1 Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:17:44 -0500 Subject: [PATCH 02/16] Client side component to render keyword suggestions --- .../src/hooks/useKeywordSuggestions.ts | 32 +++++++++++++++ .../src/projects/forms/BaseProjectForm.tsx | 39 ++++++++++++++++++- .../modules/datafiles/src/types/keywords.ts | 11 ++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 client/modules/datafiles/src/hooks/useKeywordSuggestions.ts create mode 100644 client/modules/datafiles/src/types/keywords.ts diff --git a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts new file mode 100644 index 000000000..5a61f9a2f --- /dev/null +++ b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { KeywordSuggestionResponse } from '../types/keywords'; +import { mockKeywordSuggestions } from '../types/keywords'; + +export function useKeywordSuggestions(title: string, description: string) { + return useQuery({ + queryKey: ['keywordSuggestions', title, description], + // TEMPORARY MOCK: immediately resolve with our mock list + queryFn: async () => { + // simulate network delay if you like: + await new Promise((r) => setTimeout(r, 300)); + return mockKeywordSuggestions; + }, + enabled: !!title.trim() && !!description.trim(), + }); +} + +// export function useKeywordSuggestions(title: string, description: string) { +// return useQuery({ +// queryKey: ['keywordSuggestions', title, description], +// queryFn: async () => { +// const res = await axios.post( +// '/api/keyword-suggestions', +// { title, description } +// ); +// return res.data.keywords; +// }, +// enabled: !!title.trim() && !!description.trim(), +// }); +// } + diff --git a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx index 1cf751732..7ae3c0dba 100644 --- a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx +++ b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Form, Input, Popconfirm, Select } from 'antd'; +import { Alert, Button, Form, Input, Popconfirm, Select, Tag } from 'antd'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { nhTypeOptions, @@ -24,6 +24,7 @@ import { import { customRequiredMark } from './_common'; import { AuthorSelect } from './_fields/AuthorSelect'; import { ProjectTypeRadioSelect } from '../modals/ProjectTypeRadioSelect'; +import { useKeywordSuggestions } from '../../hooks/useKeywordSuggestions'; export const ProjectTypeInput: React.FC<{ projectType: TBaseProjectValue['projectType']; @@ -139,6 +140,25 @@ export const BaseProjectForm: React.FC<{ [watchedPi, watchedCoPis, watchedMembers, watchedGuestMembers] ); + const watchedTitle = Form.useWatch('title', form) ?? ''; + const watchedDescription = Form.useWatch('description', form) ?? ''; + const watchedSelected = Form.useWatch('keywords', form) ?? []; + + const { data: suggestedKeywords = [] } = useKeywordSuggestions( + watchedTitle, + watchedDescription + ); + const availableSuggestions = suggestedKeywords.filter( + (kw) => !watchedSelected.includes(kw) +); + + useEffect(() => { + console.log('Project Title:', watchedTitle); + console.log('Project Description:', watchedDescription); + console.log('Suggested Keywords:', suggestedKeywords); + }, [watchedTitle, watchedDescription, suggestedKeywords]); + + const { user } = useAuthenticatedUser(); const [showConfirm, setShowConfirm] = useState(false); const onFormSubmit = ( @@ -379,6 +399,23 @@ export const BaseProjectForm: React.FC<{ tokenSeparators={[',']} > + {availableSuggestions.length > 0 && ( +
+

Suggested Keywords:

+ {availableSuggestions.map((kw) => ( + { + form.setFieldValue('keywords', [...watchedSelected, kw]); + }} + > + {kw} + + ))} +
+ )} )} diff --git a/client/modules/datafiles/src/types/keywords.ts b/client/modules/datafiles/src/types/keywords.ts new file mode 100644 index 000000000..494989491 --- /dev/null +++ b/client/modules/datafiles/src/types/keywords.ts @@ -0,0 +1,11 @@ +export interface KeywordSuggestionResponse { + keywords: string[]; +} + +export const mockKeywordSuggestions = [ + "storm surge", + "coastal flooding", + "numerical simulation", + "wind load", + "LiDAR analysis" +]; \ No newline at end of file From f6f49185a864f8e69a52cf60f587615e66ce79e4 Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:28:21 -0500 Subject: [PATCH 03/16] formating --- .../datafiles/src/hooks/useKeywordSuggestions.ts | 1 - .../src/projects/forms/BaseProjectForm.tsx | 13 ++++++------- client/modules/datafiles/src/types/keywords.ts | 12 ++++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts index 5a61f9a2f..98902d37e 100644 --- a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts +++ b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts @@ -29,4 +29,3 @@ export function useKeywordSuggestions(title: string, description: string) { // enabled: !!title.trim() && !!description.trim(), // }); // } - diff --git a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx index 7ae3c0dba..7ee54114d 100644 --- a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx +++ b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx @@ -149,15 +149,14 @@ export const BaseProjectForm: React.FC<{ watchedDescription ); const availableSuggestions = suggestedKeywords.filter( - (kw) => !watchedSelected.includes(kw) -); + (kw) => !watchedSelected.includes(kw) + ); useEffect(() => { - console.log('Project Title:', watchedTitle); - console.log('Project Description:', watchedDescription); - console.log('Suggested Keywords:', suggestedKeywords); - }, [watchedTitle, watchedDescription, suggestedKeywords]); - + console.log('Project Title:', watchedTitle); + console.log('Project Description:', watchedDescription); + console.log('Suggested Keywords:', suggestedKeywords); + }, [watchedTitle, watchedDescription, suggestedKeywords]); const { user } = useAuthenticatedUser(); const [showConfirm, setShowConfirm] = useState(false); diff --git a/client/modules/datafiles/src/types/keywords.ts b/client/modules/datafiles/src/types/keywords.ts index 494989491..8725966af 100644 --- a/client/modules/datafiles/src/types/keywords.ts +++ b/client/modules/datafiles/src/types/keywords.ts @@ -3,9 +3,9 @@ export interface KeywordSuggestionResponse { } export const mockKeywordSuggestions = [ - "storm surge", - "coastal flooding", - "numerical simulation", - "wind load", - "LiDAR analysis" -]; \ No newline at end of file + 'storm surge', + 'coastal flooding', + 'numerical simulation', + 'wind load', + 'LiDAR analysis', +]; From 78d11569c5f27b7712216fa1e4053099a51a88ea Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:30:18 -0500 Subject: [PATCH 04/16] remove unused imports --- client/modules/datafiles/src/hooks/useKeywordSuggestions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts index 98902d37e..d0c02239c 100644 --- a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts +++ b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import axios from 'axios'; -import { KeywordSuggestionResponse } from '../types/keywords'; +// import axios from 'axios'; +// import { KeywordSuggestionResponse } from '../types/keywords'; import { mockKeywordSuggestions } from '../types/keywords'; export function useKeywordSuggestions(title: string, description: string) { From bfe7729a12b1712516828117820fdc69e821d954 Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:08:30 -0500 Subject: [PATCH 05/16] chore: re-export useKeywordSuggestions in central @client/hooks barrel and update BaseProjectForm to import via the alias instead of a relative path --- client/modules/_hooks/src/index.ts | 2 ++ client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/modules/_hooks/src/index.ts b/client/modules/_hooks/src/index.ts index 701dc2d83..b9582312a 100644 --- a/client/modules/_hooks/src/index.ts +++ b/client/modules/_hooks/src/index.ts @@ -2,6 +2,8 @@ export { useAuthenticatedUser, type TUser } from './useAuthenticatedUser'; export { useDebounceValue } from './useDebounceValue'; export { useSystemOverview } from './useSystemOverview'; export { useSystemQueue } from './useSystemQueue'; +export { useKeywordSuggestions } from '../../datafiles/src/hooks/useKeywordSuggestions'; + export { default as apiClient } from './apiClient'; export * from './workspace'; export * from './datafiles'; diff --git a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx index 7ee54114d..65cde5206 100644 --- a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx +++ b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx @@ -19,12 +19,12 @@ import { TProjectUser } from './_fields/UserSelect'; import { TBaseProjectValue, useAuthenticatedUser, + useKeywordSuggestions, useProjectDetail, } from '@client/hooks'; import { customRequiredMark } from './_common'; import { AuthorSelect } from './_fields/AuthorSelect'; import { ProjectTypeRadioSelect } from '../modals/ProjectTypeRadioSelect'; -import { useKeywordSuggestions } from '../../hooks/useKeywordSuggestions'; export const ProjectTypeInput: React.FC<{ projectType: TBaseProjectValue['projectType']; From 200c88bd15e4cf5e07c65b71c072a03deac2274f Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:33:30 -0500 Subject: [PATCH 06/16] update mapping --- client/modules/_hooks/src/index.ts | 2 +- client/tsconfig.base.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/modules/_hooks/src/index.ts b/client/modules/_hooks/src/index.ts index b9582312a..5e816d44a 100644 --- a/client/modules/_hooks/src/index.ts +++ b/client/modules/_hooks/src/index.ts @@ -2,7 +2,7 @@ export { useAuthenticatedUser, type TUser } from './useAuthenticatedUser'; export { useDebounceValue } from './useDebounceValue'; export { useSystemOverview } from './useSystemOverview'; export { useSystemQueue } from './useSystemQueue'; -export { useKeywordSuggestions } from '../../datafiles/src/hooks/useKeywordSuggestions'; +export { useKeywordSuggestions } from '@client/datafiles/hooks/useKeywordSuggestions'; export { default as apiClient } from './apiClient'; export * from './workspace'; diff --git a/client/tsconfig.base.json b/client/tsconfig.base.json index 613b2c639..2405a1b1c 100644 --- a/client/tsconfig.base.json +++ b/client/tsconfig.base.json @@ -17,6 +17,7 @@ "paths": { "@client/common-components": ["modules/_common_components/src/index.ts"], "@client/datafiles": ["modules/datafiles/src/index.ts"], + "@client/datafiles/hooks/*": ["modules/datafiles/src/hooks/*"], "@client/hooks": ["modules/_hooks/src/index.ts"], "@client/onboarding": ["modules/onboarding/src/index.ts"], "@client/test-fixtures": ["modules/_test-fixtures/src/index.ts"], From b22b92beccd73075d0b6094253be0978a605f4ec Mon Sep 17 00:00:00 2001 From: Sal Tijerina Date: Fri, 12 Sep 2025 15:11:14 -0500 Subject: [PATCH 07/16] task/WIN-41: Keyword RAG as view (#1616) * wip rag as view * add CHROMA_ENDPOINT setting * fix missing packages; fix image tag * import SN settings from designsafe.env * working rag view * add error handling; add debounce to rag requests * fix linting * add test settings * use prev value as placeholder; memoize watched values and set debounce to 1 sec * linting * Task/WC-179: Software publication type (#1633) * implement Software publication type * fixes for testing session issues * task/WC-307: updating search checkboxes to include software type (#1637) * implement Software publication type * adding new software checkbox to right search menu * fixing spacing issue hopefully * adding back missing tag * another spacing fix * yet another spacing fix --------- Co-authored-by: Jake Rosenberg * adding extra words to publish-amend-version button (#1628) Co-authored-by: Jake Rosenberg * adding flags for tombstones to disable file listings and entity trees (#1621) Co-authored-by: Jake Rosenberg --------- Co-authored-by: Jake Rosenberg Co-authored-by: Sarah Gray --- client/modules/_hooks/src/datafiles/index.ts | 2 + .../_hooks/src/datafiles/projects/types.ts | 3 + .../_hooks/src/datafiles/useGithubListing.ts | 46 + .../src/datafiles/useKeywordSuggestions.ts | 29 + client/modules/_hooks/src/index.ts | 1 - .../src/AddFileFolder/AddFileFolder.tsx | 16 +- .../src/hooks/useKeywordSuggestions.ts | 31 - .../src/projects/BaseProjectDetails.tsx | 67 +- .../ProjectGithubFileListing.tsx | 164 + .../ProjectGithubTransfer.tsx | 216 + .../ProjectPipeline/PipelineOrderAuthors.tsx | 6 +- .../PipelineProofreadProjectStep.tsx | 14 +- .../ProjectPipeline/PipelinePublishModal.tsx | 34 +- .../ProjectPipeline/ProjectPipeline.tsx | 37 +- .../ProjectPreview/ProjectPreview.tsx | 32 +- .../datafiles/src/projects/constants.ts | 1 + .../src/projects/forms/BaseProjectForm.tsx | 44 +- .../modules/datafiles/src/projects/index.ts | 5 + .../modals/ChangeProjectTypeModal.tsx | 11 +- .../modals/ProjectBestPracticesModal.tsx | 12 +- .../PublicationSearchSidebar.tsx | 26 + .../PublishedListing/PublishedListing.tsx | 1 + .../modules/datafiles/src/types/keywords.ts | 11 - .../projects/ProjectCurationLayout.tsx | 13 +- .../layouts/projects/ProjectDetailLayout.tsx | 12 +- .../layouts/projects/ProjectPreviewLayout.tsx | 34 +- .../layouts/projects/ProjectWorkdirLayout.tsx | 12 +- .../published/PublishedDetailLayout.tsx | 21 +- .../PublishedEntityListingLayout.tsx | 49 +- client/tsconfig.base.json | 1 - designsafe/apps/api/ai_keywords/__init__.py | 0 designsafe/apps/api/ai_keywords/urls.py | 8 + designsafe/apps/api/ai_keywords/views.py | 125 + .../operations/datacite_operations.py | 15 + .../operations/graph_operations.py | 4 +- .../operations/project_meta_operations.py | 53 + .../operations/project_publish_operations.py | 76 +- .../api/projects_v2/schema_models/base.py | 6 + designsafe/apps/api/projects_v2/views.py | 9 + .../operations/fedora_graph_operations.py | 2 +- designsafe/apps/api/urls.py | 3 +- designsafe/settings/common_settings.py | 5 + designsafe/settings/test_settings.py | 5 + .../vendor/bootstrap-ds/css/bootstrap.css | 2 +- poetry.lock | 4201 ++++++++++++++++- pyproject.toml | 12 +- 46 files changed, 5062 insertions(+), 415 deletions(-) create mode 100644 client/modules/_hooks/src/datafiles/useGithubListing.ts create mode 100644 client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts delete mode 100644 client/modules/datafiles/src/hooks/useKeywordSuggestions.ts create mode 100644 client/modules/datafiles/src/projects/ProjectGithubFileListing/ProjectGithubFileListing.tsx create mode 100644 client/modules/datafiles/src/projects/ProjectGithubTransfer/ProjectGithubTransfer.tsx delete mode 100644 client/modules/datafiles/src/types/keywords.ts create mode 100644 designsafe/apps/api/ai_keywords/__init__.py create mode 100644 designsafe/apps/api/ai_keywords/urls.py create mode 100644 designsafe/apps/api/ai_keywords/views.py diff --git a/client/modules/_hooks/src/datafiles/index.ts b/client/modules/_hooks/src/datafiles/index.ts index 6c528a1d4..e0ed0a799 100644 --- a/client/modules/_hooks/src/datafiles/index.ts +++ b/client/modules/_hooks/src/datafiles/index.ts @@ -21,6 +21,8 @@ export { useNewFolder } from './useNewFolder'; export { useUploadFile } from './useUploadFile'; export { useUploadFolder } from './useUploadFolder'; export { useFileDetail } from './useFileDetail'; +export * from './useKeywordSuggestions'; +export { useGithubListing, type TGithubFileObj } from './useGithubListing'; export * from './usePathDisplayName'; diff --git a/client/modules/_hooks/src/datafiles/projects/types.ts b/client/modules/_hooks/src/datafiles/projects/types.ts index 39f7f5b22..fd28c58fe 100644 --- a/client/modules/_hooks/src/datafiles/projects/types.ts +++ b/client/modules/_hooks/src/datafiles/projects/types.ts @@ -72,6 +72,7 @@ export type TBaseProjectValue = { | 'hybrid_simulation' | 'field_recon' | 'field_reconnaissance' + | 'software' | 'None'; title: string; @@ -97,6 +98,8 @@ export type TBaseProjectValue = { hazmapperMaps?: THazmapperMap[]; + githubUrl?: string; + license?: string; }; diff --git a/client/modules/_hooks/src/datafiles/useGithubListing.ts b/client/modules/_hooks/src/datafiles/useGithubListing.ts new file mode 100644 index 000000000..8a41822e8 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useGithubListing.ts @@ -0,0 +1,46 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../apiClient'; +import { useCallback } from 'react'; + +type TGihubParams = { + org: string; + repo: string; + ref: string; +}; + +export type TGithubFileObj = { + download_url: string; + git_url: string; + html_url: string; + name: string; + path: string; + sha: string; + size: number; + type: string; + url: string; + _links: { + self: string; + git: string; + }; +}; +async function getGithubListing({ org, repo, ref }: TGihubParams) { + const githubUrl = `https://api.github.com/repos/${org}/${repo}/contents/?ref=${ref}`; + const resp = await apiClient.get(githubUrl); + return resp.data; +} + +export function useGithubListing({ org, repo, ref }: TGihubParams) { + const githubListingCallback = useCallback( + () => getGithubListing({ org, repo, ref }), + [org, repo, ref] + ); + return useQuery({ + queryKey: ['githubListing', org, repo, ref], + queryFn: githubListingCallback, + enabled: !!(org && repo && ref), + retry: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }); +} diff --git a/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts b/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts new file mode 100644 index 000000000..0bd144abb --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../apiClient'; + +export type TGetKeywordSuggestionsParams = { + title: string; + description: string; +}; + +export interface KeywordSuggestionResponse { + response: string[]; +} + +export function useKeywordSuggestions( + searchParams: TGetKeywordSuggestionsParams +) { + return useQuery({ + queryKey: ['keywordSuggestions', searchParams], + queryFn: async () => { + const res = await apiClient.get( + '/api/keyword-suggestions/', + { params: searchParams } + ); + return res.data.response; + }, + enabled: !!searchParams.title.trim() && !!searchParams.description.trim(), + staleTime: Infinity, + placeholderData: (prev) => prev ?? [], + }); +} diff --git a/client/modules/_hooks/src/index.ts b/client/modules/_hooks/src/index.ts index 5e816d44a..ea5a65385 100644 --- a/client/modules/_hooks/src/index.ts +++ b/client/modules/_hooks/src/index.ts @@ -2,7 +2,6 @@ export { useAuthenticatedUser, type TUser } from './useAuthenticatedUser'; export { useDebounceValue } from './useDebounceValue'; export { useSystemOverview } from './useSystemOverview'; export { useSystemQueue } from './useSystemQueue'; -export { useKeywordSuggestions } from '@client/datafiles/hooks/useKeywordSuggestions'; export { default as apiClient } from './apiClient'; export * from './workspace'; diff --git a/client/modules/datafiles/src/AddFileFolder/AddFileFolder.tsx b/client/modules/datafiles/src/AddFileFolder/AddFileFolder.tsx index b8b530d49..7282fec97 100644 --- a/client/modules/datafiles/src/AddFileFolder/AddFileFolder.tsx +++ b/client/modules/datafiles/src/AddFileFolder/AddFileFolder.tsx @@ -23,19 +23,21 @@ export const AddFileFolder: React.FC = () => { const isPublished = matches.find((m) => m.id === 'published'); const isNees = matches.find((m) => m.id === 'nees'); - const isReadOnly = !!( - (isProjects && !projectId) || - isPublished || - isNees || - system === 'designsafe.storage.community' - ); - if (!isProjects) projectId = ''; const { data } = useProjectDetail(projectId ?? ''); if (projectId) { system = `project-${data?.baseProject.uuid}`; api = 'tapis'; } + const isSoftwareProject = data?.baseProject.value.projectType === 'software'; + + const isReadOnly = !!( + (isProjects && !projectId) || + isPublished || + isNees || + isSoftwareProject || + system === 'designsafe.storage.community' + ); if (!user) return null; diff --git a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts b/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts deleted file mode 100644 index d0c02239c..000000000 --- a/client/modules/datafiles/src/hooks/useKeywordSuggestions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -// import axios from 'axios'; -// import { KeywordSuggestionResponse } from '../types/keywords'; -import { mockKeywordSuggestions } from '../types/keywords'; - -export function useKeywordSuggestions(title: string, description: string) { - return useQuery({ - queryKey: ['keywordSuggestions', title, description], - // TEMPORARY MOCK: immediately resolve with our mock list - queryFn: async () => { - // simulate network delay if you like: - await new Promise((r) => setTimeout(r, 300)); - return mockKeywordSuggestions; - }, - enabled: !!title.trim() && !!description.trim(), - }); -} - -// export function useKeywordSuggestions(title: string, description: string) { -// return useQuery({ -// queryKey: ['keywordSuggestions', title, description], -// queryFn: async () => { -// const res = await axios.post( -// '/api/keyword-suggestions', -// { title, description } -// ); -// return res.data.keywords; -// }, -// enabled: !!title.trim() && !!description.trim(), -// }); -// } diff --git a/client/modules/datafiles/src/projects/BaseProjectDetails.tsx b/client/modules/datafiles/src/projects/BaseProjectDetails.tsx index 7ee9850b0..3934ec303 100644 --- a/client/modules/datafiles/src/projects/BaseProjectDetails.tsx +++ b/client/modules/datafiles/src/projects/BaseProjectDetails.tsx @@ -139,6 +139,7 @@ export const UsernamePopover: React.FC<{ user: TProjectUser }> = ({ user }) => { const projectTypeMapping = { field_recon: 'Field research', other: 'Other', + software: 'Software', experimental: 'Experimental', simulation: 'Simulation', hybrid_simulation: 'Hybrid Simulation', @@ -190,7 +191,7 @@ export const BaseProjectDetails: React.FC<{ - {pi && projectValue.projectType !== 'other' && ( + {pi && !['other', 'software'].includes(projectValue.projectType) && ( PI @@ -198,21 +199,22 @@ export const BaseProjectDetails: React.FC<{ )} - {coPis.length > 0 && projectValue.projectType !== 'other' && ( - - Co-PIs - - {coPis.map((u, i) => ( - - - {i !== coPis.length - 1 && '; '} - - ))} - - - )} + {coPis.length > 0 && + !['other', 'software'].includes(projectValue.projectType) && ( + + Co-PIs + + {coPis.map((u, i) => ( + + + {i !== coPis.length - 1 && '; '} + + ))} + + + )} {projectValue.authors.length > 0 && - projectValue.projectType === 'other' && ( + ['other', 'software'].includes(projectValue.projectType) && ( Authors @@ -225,7 +227,7 @@ export const BaseProjectDetails: React.FC<{ )} - {projectValue.projectType !== 'other' && ( + {!['other', 'software'].includes(projectValue.projectType) && ( Project Type @@ -376,6 +378,20 @@ export const BaseProjectDetails: React.FC<{ )} + {projectValue.githubUrl && ( + + Software Release + + + {projectValue.githubUrl} + + + + )} {projectValue.dois && projectValue.dois[0] && ( @@ -384,14 +400,15 @@ export const BaseProjectDetails: React.FC<{ )} - {projectValue.projectType === 'other' && projectValue.license && ( - - License - - - - - )} + {['other', 'software'].includes(projectValue.projectType) && + projectValue.license && ( + + License + + + + + )} {versions && versions.length > 1 && ( @@ -418,7 +435,7 @@ export const BaseProjectDetails: React.FC<{ {isPublished && (
- {!['other', 'field_reconnaissance'].includes( + {!['other', 'software', 'field_reconnaissance'].includes( projectValue.projectType ) && ( <> diff --git a/client/modules/datafiles/src/projects/ProjectGithubFileListing/ProjectGithubFileListing.tsx b/client/modules/datafiles/src/projects/ProjectGithubFileListing/ProjectGithubFileListing.tsx new file mode 100644 index 000000000..5ad3b28e6 --- /dev/null +++ b/client/modules/datafiles/src/projects/ProjectGithubFileListing/ProjectGithubFileListing.tsx @@ -0,0 +1,164 @@ +import { + TGithubFileObj, + useGithubListing, + useProjectDetail, +} from '@client/hooks'; +import { Alert, Button, ConfigProvider, Table, TableProps } from 'antd'; +import React from 'react'; +import { toBytes } from '../../FileListing/FileListing'; +import { Link } from 'react-router-dom'; + +const releaseRegex = + /^(?:https?:\/\/)?(?:www\.)?github\.com\/(\S+)\/(\S+)\/releases\/tag\/(\S+)$/; + +const GithubFileListingTable: React.FC<{ + org: string; + repo: string; + releaseRef: string; +}> = ({ org, repo, releaseRef }) => { + const { data: githubListing } = useGithubListing({ + org, + repo, + ref: releaseRef, + }); + + const columns: TableProps['columns'] = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + width: '50%', + render: (_, record) => ( + <> + + + {record.name} + + + ), + }, + { + title: 'Size', + dataIndex: 'size', + key: 'size', + render: (v) => toBytes(v), + }, + { + dataIndex: undefined, + + align: 'end', + key: 'path', + title: () => ( + + + + + + ), + }, + ]; + + return ( + + columns={columns} + dataSource={githubListing} + rowKey="path" + pagination={false} + > + ); +}; + +export const ProjectGithubFileListing: React.FC<{ projectId: string }> = ({ + projectId, +}) => { + const { data } = useProjectDetail(projectId ?? ''); + + const githubUrl = data?.baseProject.value.githubUrl; + + if (!githubUrl) { + return ( + + Please go to the{' '} + + Curation Directory + {' '} + to associate a GitHub repository with your project. + + } + /> + ); + } + return ( + <> + + If you need to modify the GitHub release associated with this + project, you can do so from the{' '} + + Curation Directory + {' '} + . + + } + /> + + + ); +}; + +export const BaseGithubFileListing: React.FC<{ githubUrl?: string }> = ({ + githubUrl, +}) => { + if (!githubUrl) { + return
No Github URL specified.
; + } + const regexMatch = releaseRegex.exec(githubUrl ?? ''); + + if (regexMatch) { + const [, org, repo, releaseRef] = regexMatch; + //return
hi
+ if (!org || !repo || !releaseRef) { + return ( + + ); + } + return ( + + ); + } +}; diff --git a/client/modules/datafiles/src/projects/ProjectGithubTransfer/ProjectGithubTransfer.tsx b/client/modules/datafiles/src/projects/ProjectGithubTransfer/ProjectGithubTransfer.tsx new file mode 100644 index 000000000..60156e8d6 --- /dev/null +++ b/client/modules/datafiles/src/projects/ProjectGithubTransfer/ProjectGithubTransfer.tsx @@ -0,0 +1,216 @@ +import { usePatchProjectMetadata, useProjectDetail } from '@client/hooks'; +import { Alert, Button, ConfigProvider, Form, Input, Spin } from 'antd'; +import { AxiosError } from 'axios'; +import React, { useMemo } from 'react'; + +export const ProjectGithubTransfer: React.FC<{ projectId: string }> = ({ + projectId, +}) => { + const { mutate, error, isSuccess, isPending } = + usePatchProjectMetadata(projectId); + + const { data: projectMetadata } = useProjectDetail(projectId); + + const updateUrl = (url: string) => + mutate({ patchMetadata: { githubUrl: url } }); + + const errorType = useMemo(() => { + if (!error) return undefined; + if ((error as AxiosError).status !== 400) { + return undefined; + } + const errorList = (error as AxiosError<{ message: string[] }>).response + ?.data.message; + return errorList; + }, [error]); + + return ( +
+ + + Transfer GitHub Release + +
+ Enter the release URL and DesignSafe will automatically transfer and + store a zip file of your software. +
+
+ Only one GitHub release can be published. Re-transferring will overwrite + any previous transfer. +
+
+ No additional files besides the zip file containing the GitHub release + can be published. +
+ +
+ +
updateUrl(f.url)} + style={{ marginBottom: '1rem' }} + > + + + + +
+ + How to find the release URL + +
+ + {errorType?.includes('readme') && ( + + Create a README.md file in your repository root and + include it in your GitHub release. + + } + /> + )} + {errorType?.includes('codemeta') && ( + + Generate a codemeta.json file at{' '} + + https://codemeta.github.io/codemeta-generator/ + {' '} + and include it in your GitHub release. + + } + /> + )} + {isSuccess && ( + Continue to the Publication Preview} + /> + )} + +
+ The following files must be included in the GitHub + release. +
+
+ +
+ CodeMeta File (codemeta.json)
+ Generate a metadata file at{' '} + + https://codemeta.github.io/codemeta-generator/ + {' '} + and include it in your GitHub release.
+ Software name, description, license, and version from this file will + overwrite project metadata to maintain consistency +
+
+
+
+ +
+ README File (README.md)
+ Include a README file in addition to the code metadata in your + GitHub release. +
+
+
+
+ ); +}; diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx index 067705fa7..f0268bcd1 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx @@ -122,9 +122,9 @@ export const PipelineOrderAuthors: React.FC<{ ) .join(', ')} .{' '} - {data.baseProject.value.projectType !== 'other' && ( - "{entity.value.title}", in - )} + {!['other', 'software'].includes( + data.baseProject.value.projectType + ) && "{entity.value.title}", in } {data.baseProject.value.title}. DesignSafe-CI. (DOI will appear after publication) diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx index be46b8924..4215ff6ca 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx @@ -5,7 +5,7 @@ import { Button } from 'antd'; export const PipelineProofreadProjectStep: React.FC<{ projectId: string; - prevStep: () => void; + prevStep?: () => void; nextStep: () => void; }> = ({ projectId, prevStep, nextStep }) => { const { data } = useProjectDetail(projectId ?? ''); @@ -20,10 +20,14 @@ export const PipelineProofreadProjectStep: React.FC<{ marginTop: 24, }} > - + {prevStep ? ( + + ) : ( + /* Empty element to force Continue button to the right */ + )} { /> +
+ {' '} +
+ toggleProjectTypeFilter('software')} + /> +   + Software +
+ {/*
+ + - {availableSuggestions.length > 0 && ( + {availableSuggestions.length === 0 ? ( +
+ Suggested Keywords: + + Enter a project title and{' '} + description to see keyword suggestions. + +
+ ) : (

Suggested Keywords:

{availableSuggestions.slice(0, 10).map((kw) => ( @@ -435,7 +469,11 @@ export const BaseProjectForm: React.FC<{ color="blue" style={{ cursor: 'pointer', marginBottom: 4 }} onClick={() => { - form.setFieldValue('keywords', [...watchedKeywords, kw]); + const current: string[] = + form.getFieldValue('keywords') || []; + if (!current.includes(kw)) { + form.setFieldValue('keywords', [...current, kw]); + } }} > {kw} @@ -445,31 +483,7 @@ export const BaseProjectForm: React.FC<{ )} )} - - What is this project about? How can data in this project be reused? How - is this project unique? Who is the audience? Description must be between - 50 and 5000 characters in length. - - - - + {hasValidationErrors && ( Date: Mon, 20 Oct 2025 11:08:20 -0500 Subject: [PATCH 15/16] New component --- .../src/datafiles/useKeywordSuggestions.ts | 10 +- .../src/projects/forms/BaseProjectForm.tsx | 77 +------ .../src/projects/forms/KeywordSuggestor.tsx | 95 ++++++++ .../projects/forms/PublishableEntityForm.tsx | 208 +++++++++++------- 4 files changed, 233 insertions(+), 157 deletions(-) create mode 100644 client/modules/datafiles/src/projects/forms/KeywordSuggestor.tsx diff --git a/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts b/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts index 0bd144abb..799de6e17 100644 --- a/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts +++ b/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts @@ -14,7 +14,12 @@ export function useKeywordSuggestions( searchParams: TGetKeywordSuggestionsParams ) { return useQuery({ - queryKey: ['keywordSuggestions', searchParams], + queryKey: [ + 'keywordSuggestions', + , + searchParams.title.trim(), + searchParams.description.trim(), + ], queryFn: async () => { const res = await apiClient.get( '/api/keyword-suggestions/', @@ -23,7 +28,6 @@ export function useKeywordSuggestions( return res.data.response; }, enabled: !!searchParams.title.trim() && !!searchParams.description.trim(), - staleTime: Infinity, - placeholderData: (prev) => prev ?? [], + staleTime: 0, }); } diff --git a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx index f86de8371..e6960f331 100644 --- a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx +++ b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx @@ -19,14 +19,12 @@ import { TProjectUser } from './_fields/UserSelect'; import { TBaseProjectValue, useAuthenticatedUser, - useKeywordSuggestions, useProjectDetail, - useDebounceValue, - TGetKeywordSuggestionsParams, } from '@client/hooks'; import { customRequiredMark } from './_common'; import { AuthorSelect } from './_fields/AuthorSelect'; import { ProjectTypeRadioSelect } from '../modals/ProjectTypeRadioSelect'; +import { KeywordSuggestor } from './KeywordSuggestor'; export const ProjectTypeInput: React.FC<{ projectType: TBaseProjectValue['projectType']; @@ -150,42 +148,6 @@ export const BaseProjectForm: React.FC<{ [watchedPi, watchedCoPis, watchedMembers, watchedGuestMembers] ); - // Watch title, description, and keywords for AI keyword suggestions - const watchedTitle: string = Form.useWatch('title', form) ?? ''; - const watchedDescription: string = Form.useWatch('description', form) ?? ''; - const watchedKeywords: string[] = Form.useWatch('keywords', form) ?? []; - - const titleMemo = useMemo(() => watchedTitle, [watchedTitle]); - const descriptionMemo = useMemo( - () => watchedDescription, - [watchedDescription] - ); - const keywordsMemo = useMemo(() => watchedKeywords, [watchedKeywords]); - - const [searchTerms, setSearchTerms] = useState({ - title: watchedTitle, - description: watchedDescription, - }); - - // Debounce search terms to avoid excessive API calls - const debouncedSearchTerms = useDebounceValue( - searchTerms, - 1000 - ); - - const { data: suggestedKeywords = [] } = - useKeywordSuggestions(debouncedSearchTerms); - const availableSuggestions = suggestedKeywords.filter( - (kw: string) => !keywordsMemo.includes(kw) - ); - - useEffect(() => { - setSearchTerms({ - title: titleMemo, - description: descriptionMemo, - }); - }, [titleMemo, descriptionMemo, keywordsMemo]); - const { user } = useAuthenticatedUser(); const [showConfirm, setShowConfirm] = useState(false); const onFormSubmit = ( @@ -450,37 +412,14 @@ export const BaseProjectForm: React.FC<{ mode="tags" notFoundContent={null} tokenSeparators={[',']} - > + /> - {availableSuggestions.length === 0 ? ( -
- Suggested Keywords: - - Enter a project title and{' '} - description to see keyword suggestions. - -
- ) : ( -
-

Suggested Keywords:

- {availableSuggestions.slice(0, 10).map((kw) => ( - { - const current: string[] = - form.getFieldValue('keywords') || []; - if (!current.includes(kw)) { - form.setFieldValue('keywords', [...current, kw]); - } - }} - > - {kw} - - ))} -
- )} + )} diff --git a/client/modules/datafiles/src/projects/forms/KeywordSuggestor.tsx b/client/modules/datafiles/src/projects/forms/KeywordSuggestor.tsx new file mode 100644 index 000000000..4a04f3153 --- /dev/null +++ b/client/modules/datafiles/src/projects/forms/KeywordSuggestor.tsx @@ -0,0 +1,95 @@ +// KeywordSuggestor.tsx +import React, { useMemo } from 'react'; +import { Form, Tag, Spin } from 'antd'; +import type { FormInstance } from 'antd'; +import { useDebounceValue, useKeywordSuggestions } from '@client/hooks'; + +type Props = { + form: FormInstance; + titlePath: (string | number)[]; + descriptionPath: (string | number)[]; + keywordsPath: (string | number)[]; +}; + +export const KeywordSuggestor: React.FC = ({ + form, + titlePath, + descriptionPath, + keywordsPath, +}) => { + const title: string = Form.useWatch(titlePath, form) ?? ''; + const description: string = Form.useWatch(descriptionPath, form) ?? ''; + const keywords: string[] = Form.useWatch(keywordsPath, form) ?? []; + + const debounced = useDebounceValue( + { title: title.trim(), description: description.trim() }, + 800 + ); + + const { + data: suggestions = [], + isLoading, + isFetching, + error, + } = useKeywordSuggestions(debounced); + + const available = useMemo( + () => suggestions.filter((kw) => !keywords.includes(kw)), + [suggestions, keywords] + ); + + const hasText = + debounced.title.length > 0 && debounced.description.length > 0; + + if (!hasText) { + return ( +
+ Suggested Keywords: + + Enter a project title and{' '} + description to see keyword suggestions. + +
+ ); + } + + if (error) return null; + + const list = available.slice(0, 10); + const loading = isLoading || isFetching; + + return ( +
+

Suggested Keywords:

+ + {/* While loading and nothing cached yet, show a friendly status instead of an empty area */} + {loading && list.length === 0 ? ( +
+ + Finding suggestions… +
+ ) : list.length === 0 ? ( + No suggestions yet. + ) : ( + list.map((kw) => ( + { + const current: string[] = form.getFieldValue(keywordsPath) || []; + if (!current.includes(kw)) { + form.setFieldValue(keywordsPath, [...current, kw]); + } + }} + > + {kw} + + )) + )} +
+ ); +}; diff --git a/client/modules/datafiles/src/projects/forms/PublishableEntityForm.tsx b/client/modules/datafiles/src/projects/forms/PublishableEntityForm.tsx index 511025601..b2ea7b29b 100644 --- a/client/modules/datafiles/src/projects/forms/PublishableEntityForm.tsx +++ b/client/modules/datafiles/src/projects/forms/PublishableEntityForm.tsx @@ -7,7 +7,7 @@ import { simulationTypeOptions, HybridSimTypeOptions, } from './ProjectFormDropdowns'; - +import { KeywordSuggestor } from './KeywordSuggestor'; import { TBaseProjectValue, TProjectUser, @@ -128,22 +128,6 @@ const ExperimentFormFields: React.FC<{ - - Choose informative words that indicate the content of the project. - Keywords should be comma-separated. - - - - - What was under investigation? How was it tested? What was the outcome? How can the data be reused? Description must be between 50 and 5000 @@ -170,6 +154,28 @@ const ExperimentFormFields: React.FC<{ + + Choose informative words that indicate the content of the project. + Keywords should be comma-separated. + + + + + + You can order the authors during the publication process. = ({ projectUsers, currentAuthors = [] }) => { +}> = ({ form, projectUsers, currentAuthors = [] }) => { return ( <> @@ -259,22 +266,6 @@ const SimulationFormFields: React.FC<{ - - Choose informative words that indicate the content of the project. - Keywords should be comma-separated. - - - - - What was under investigation? How was it tested? What was the outcome? How can the data be reused? Description must be between 50 and 5000 @@ -301,6 +292,28 @@ const SimulationFormFields: React.FC<{ + + Choose informative words that indicate the content of the project. + Keywords should be comma-separated. + + + + + + You can order the authors during the publication process. = ({ projectUsers, currentAuthors = [] }) => { +}> = ({ form, projectUsers, currentAuthors = [] }) => { return ( <> @@ -390,22 +404,6 @@ const HybridSimFormFields: React.FC<{ - - Choose informative words that indicate the content of the project. - Keywords should be comma-separated. - - - - - What was under investigation? How was it tested? What was the outcome? How can the data be reused? Description must be between 50 and 5000 @@ -432,6 +430,28 @@ const HybridSimFormFields: React.FC<{ + + Choose informative words that indicate the content of the project. + Keywords should be comma-separated. + + + + + + You can order the authors during the publication process. = ({ projectUsers, currentAuthors = [] }) => { +}> = ({ form, projectUsers, currentAuthors = [] }) => { return ( <> @@ -575,22 +596,6 @@ const MissionFormFields: React.FC<{
- - Choose informative words that indicate the content of the project. - Keywords should be comma-separated. - - - - - What was under investigation? How was it tested? What was the outcome? How can the data be reused? Description must be between 50 and 5000 @@ -616,14 +621,37 @@ const MissionFormFields: React.FC<{ + + + Choose informative words that indicate the content of the project. + Keywords should be comma-separated. + + + + + ); }; const DocumentFormFields: React.FC<{ + form: FormInstance; projectUsers: TProjectUser[]; currentAuthors?: TProjectUser[]; -}> = ({ projectUsers, currentAuthors = [] }) => { +}> = ({ form, projectUsers, currentAuthors = [] }) => { return ( <> @@ -686,22 +714,6 @@ const DocumentFormFields: React.FC<{ - - Choose informative words that indicate the content of the project. - Keywords should be comma-separated. - - - - - What was under investigation? How was it tested? What was the outcome? How can the data be reused? Description must be between 50 and 5000 @@ -727,6 +739,28 @@ const DocumentFormFields: React.FC<{ + + + Choose informative words that indicate the content of the project. + Keywords should be comma-separated. + + + + + ); }; @@ -790,24 +824,28 @@ export const PublishableEntityForm: React.FC<{ )} {projectType === 'simulation' && ( )} {projectType === 'hybrid_simulation' && ( )} {entityName === constants.FIELD_RECON_MISSION && ( )} {entityName === constants.FIELD_RECON_REPORT && ( From 8bb271bc275e3c003f947de92ade35c287414764 Mon Sep 17 00:00:00 2001 From: Van Go <35277477+van-go@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:56:13 -0500 Subject: [PATCH 16/16] fix linting --- client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts b/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts index 799de6e17..6da886b0b 100644 --- a/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts +++ b/client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts @@ -16,7 +16,6 @@ export function useKeywordSuggestions( return useQuery({ queryKey: [ 'keywordSuggestions', - , searchParams.title.trim(), searchParams.description.trim(), ],