Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e9296a1
add back NH Type, Year Pub, and Clear filters
van-go Jul 3, 2025
795c38f
Merge branch 'main' of https://github.com/DesignSafe-CI/portal
van-go Jul 7, 2025
e010ff5
Client side component to render keyword suggestions
van-go Jul 17, 2025
f6f4918
formating
van-go Jul 17, 2025
78d1156
remove unused imports
van-go Jul 17, 2025
bfe7729
chore: re-export useKeywordSuggestions in central @client/hooks barre…
van-go Jul 23, 2025
200c88b
update mapping
van-go Jul 23, 2025
66a3cc3
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Aug 6, 2025
70633b1
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Aug 13, 2025
4ae8e6e
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Aug 25, 2025
c833fb4
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Sep 3, 2025
b22b92b
task/WIN-41: Keyword RAG as view (#1616)
rstijerina Sep 12, 2025
d1a0361
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Sep 12, 2025
78d0541
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Sep 15, 2025
ae32f4b
add auth to chromadb connection
rstijerina Sep 17, 2025
f65b89a
add comments; rename variable
rstijerina Sep 17, 2025
2e7cc14
add CHROMA_COLLECTION setting
rstijerina Sep 17, 2025
1051d97
add score threshold, increase returned documents to 10
rstijerina Sep 17, 2025
fe2997f
only render 10 suggestions at a time
rstijerina Sep 17, 2025
d3da95c
task/WIN-40: Add publication metadata to chromadb as part of publicat…
rstijerina Oct 6, 2025
c5ffe0d
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Oct 8, 2025
12d4131
move keyword field below description. add helper text to keyword sugg…
van-go Oct 13, 2025
39b87b4
Merge branch 'main' into task/WIN-40-keyword-suggestion-client-side-c…
rstijerina Oct 16, 2025
7c2285d
New <KeywordSuggestor /> component
van-go Oct 20, 2025
d19bfb6
Merge branch 'task/WIN-40-keyword-suggestion-client-side-component' o…
van-go Oct 20, 2025
8bb271b
fix linting
van-go Oct 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/modules/_hooks/src/datafiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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';
Expand Down
33 changes: 33 additions & 0 deletions client/modules/_hooks/src/datafiles/useKeywordSuggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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.title.trim(),
searchParams.description.trim(),
],
queryFn: async () => {
const res = await apiClient.get<KeywordSuggestionResponse>(
'/api/keyword-suggestions/',
{ params: searchParams }
);
return res.data.response;
},
enabled: !!searchParams.title.trim() && !!searchParams.description.trim(),
staleTime: 0,
});
}
45 changes: 27 additions & 18 deletions client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -24,6 +24,7 @@ import {
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'];
Expand Down Expand Up @@ -372,23 +373,6 @@ export const BaseProjectForm: React.FC<{
<HazardEventsInput name="nhEvents" />
</Form.Item>

{projectType !== 'None' && (
<Form.Item label="Keywords" required>
Choose informative words that indicate the content of the project.
Keywords should be comma-separated.
<Form.Item
name="keywords"
rules={[{ required: true }]}
className="inner-form-item"
>
<Select
mode="tags"
notFoundContent={null}
tokenSeparators={[',']}
></Select>
</Form.Item>
</Form.Item>
)}
<Form.Item label="Project Description" required>
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
Expand All @@ -414,6 +398,31 @@ export const BaseProjectForm: React.FC<{
<Input.TextArea autoSize={{ minRows: 4 }} />
</Form.Item>
</Form.Item>

{projectType !== 'None' && (
<Form.Item label="Keywords" required>
Choose informative words that indicate the content of the project.
Keywords should be comma-separated.
<Form.Item
name="keywords"
rules={[{ required: true }]}
className="inner-form-item"
>
<Select
mode="tags"
notFoundContent={null}
tokenSeparators={[',']}
/>
</Form.Item>
<KeywordSuggestor
form={form}
titlePath={['title']}
descriptionPath={['description']}
keywordsPath={['keywords']}
/>
</Form.Item>
)}

{hasValidationErrors && (
<Alert
type="error"
Expand Down
95 changes: 95 additions & 0 deletions client/modules/datafiles/src/projects/forms/KeywordSuggestor.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great idea

Original file line number Diff line number Diff line change
@@ -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<Props> = ({
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 (
<div style={{ marginTop: 8 }}>
<span>Suggested Keywords: </span>
<em style={{ color: 'rgba(0,0,0,.45)' }}>
Enter a project <strong>title</strong> and{' '}
<strong>description</strong> to see keyword suggestions.
</em>
</div>
);
}

if (error) return null;

const list = available.slice(0, 10);
const loading = isLoading || isFetching;

return (
<div style={{ marginTop: 8 }}>
<p style={{ marginBottom: 6 }}>Suggested Keywords:</p>

{/* While loading and nothing cached yet, show a friendly status instead of an empty area */}
{loading && list.length === 0 ? (
<div
aria-live="polite"
style={{ display: 'inline-flex', gap: 8, alignItems: 'center' }}
>
<Spin size="small" />
<em style={{ color: 'rgba(0,0,0,.45)' }}>Finding suggestions…</em>
</div>
) : list.length === 0 ? (
<em style={{ color: 'rgba(0,0,0,.45)' }}>No suggestions yet.</em>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another annoying can of worms that I might be opening up, but are there any CSS classes or styling we would want to use instead of setting it manually in the JSX like you do in this chunk? We haven't talked to Wes or anyone yet, so maybe that will be a to-do for later.

) : (
list.map((kw) => (
<Tag
key={kw}
color="blue"
style={{ cursor: 'pointer', marginBottom: 4 }}
onClick={() => {
const current: string[] = form.getFieldValue(keywordsPath) || [];
if (!current.includes(kw)) {
form.setFieldValue(keywordsPath, [...current, kw]);
}
}}
>
{kw}
</Tag>
))
)}
</div>
);
};
Loading