NUDR / frontend /src /components /ResearchProgressList.tsx
magicboris's picture
Upload 83 files
3647b02 verified
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use client';
import { useEffect, useRef } from 'react';
import styles from './ResearchProgressList.module.css';
import ResearchProgressItem from './ResearchProgressItem';
import ResearchProgressFooter from './ResearchProgressFooter';
import ResearchProgressHeader from './ResearchProgressHeader';
import { ApplicationState } from '@/types/ApplicationState';
export type ResearchEventType =
| 'prompt_received'
| 'prompt_analysis_started'
| 'prompt_analysis_completed'
| 'task_analysis_completed'
| 'topic_exploration_started'
| 'search_started'
| 'search_result_processing_started'
| 'aggregation_started'
| 'research_completed'
| 'report_building'
| 'report_formatting'
| 'report_done'
| 'error';
export interface ResearchEvent {
id: string;
type: ResearchEventType;
description: string;
timestamp: number;
}
interface ResearchProgressListProps {
state: ApplicationState;
onStop?: () => void;
onClear?: () => void;
onFinalize?: () => void;
onViewError?: () => void;
}
export default function ResearchProgressList({
state,
onStop,
onClear,
onFinalize,
onViewError
}: ResearchProgressListProps) {
const listRef = useRef<HTMLDivElement>(null);
const prevEventsLengthRef = useRef<number>(0);
const userHasScrolledRef = useRef<boolean>(false);
useEffect(() => {
// Only scroll if we have new events, the list exists, and user hasn't scrolled
if (listRef.current && state.events.length > prevEventsLengthRef.current && !userHasScrolledRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
prevEventsLengthRef.current = state.events.length;
//console.log('scrolling to bottom');
}
}, [state.events]);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const element = e.currentTarget;
// Check if we're not at the bottom
if (element.scrollHeight - element.scrollTop - element.clientHeight > 10) {
userHasScrolledRef.current = true;
} else {
// Reset the flag if user scrolls back to bottom
userHasScrolledRef.current = false;
//console.log('resetting scroll flag');
}
};
return (
<div className={styles.wrapper}>
{state.type !== 'idle' && (
<div className={styles.content}>
<div className={styles.container}>
<ResearchProgressHeader
state={state}
onStop={onStop}
onClear={onClear}
onFinalize={onFinalize}
onViewError={onViewError}
/>
<div
ref={listRef}
className={styles.list}
onScroll={handleScroll}
>
{state.events.map((event) => (
<ResearchProgressItem key={event.id} event={event} />
))}
</div>
<ResearchProgressFooter state={state} />
</div>
</div>
)}
</div>
);
}