- 
                Notifications
    You must be signed in to change notification settings 
- Fork 832
feat(inspect): Add train model dialog, job logs and models dataset #3053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(inspect): Add train model dialog, job logs and models dataset #3053
Conversation
Set end_time to the current UTC time when a job reaches COMPLETED, FAILED, or CANCELED.
Emit SSE-formatted messages (prepend "data: " and send a DONE sentinel) from the backend stream so clients can detect completion. Add a new fullscreen Job Logs dialog component (show-job-logs.component.tsx) that uses EventSource and react-query streamedQuery to stream and render job logs in the UI.
This allows us to change the tab that's been opened by changing the url. Which is useful when we want to focus the models tab after training a model.
Load the '/thumbnail' image endpoint instead of '/full' for dataset items to reduce bandwidth and improve list rendering performance.
Introduce TrainModelButton, TrainModelDialog, and TrainableModelListBox to enable starting model training from the UI. Show a persistent toast with a Train action when a project has >= 20 images. Wire the button into the Dataset header, simplify Dataset content (remove status panel/divider), and adjust upload button styling.
Reorder providers so SelectedMediaItemProvider wraps InferenceProvider. Use useSelectedMediaItem inside InferenceProvider and add an onSetSelectedModelId wrapper that sets the model and invokes onInference when a media item is selected. Pass the wrapper into the context.
Add a new Models component to list project models and active training jobs. Export useProjectTrainingJobs and useRefreshModelsOnJobUpdates and improve the logic for detecting job completion to trigger model refreshes. Refactor TrainingInProgress: extract status/heading mapping, consolidate rendering, show job logs, and simplify imports. Remove the ReadyToTrain UI and wire the new Models view into the sidebar (fix icon import and props).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a complete workflow for model training and inspection including a train model dialog with model selection, real-time job logs viewing, and a models tab to track training progress. The changes enable users to select and train models, monitor their progress, and view associated logs through the UI. Query parameters are now used to persist the selected sidebar tab across navigation.
Key Changes
- Added a train model dialog allowing users to select from available models and initiate training
- Implemented real-time job logs viewer using Server-Sent Events (SSE)
- Created a models tab displaying trained models and in-progress training jobs
- Updated navigation to use query parameters for tab state management
Reviewed Changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description | 
|---|---|
| application/ui/src/routes/welcome.tsx | Sets default mode to Dataset tab via query parameter | 
| application/ui/src/routes/router.tsx | Updates redirect to include Dataset mode query parameter | 
| application/ui/src/routes/inspect/inspect.tsx | Reorders provider hierarchy for inference context access | 
| application/ui/src/features/inspect/train-model/trainable-model-list-box.component.tsx | Implements model selection UI with performance ratings | 
| application/ui/src/features/inspect/train-model/train-model.module.scss | Adds styles for model selection cards and ratings | 
| application/ui/src/features/inspect/train-model/train-model-dialog.component.tsx | Creates dialog component for initiating model training | 
| application/ui/src/features/inspect/train-model/train-model-button.component.tsx | Adds button to trigger train model dialog with validation | 
| application/ui/src/features/inspect/sidebar.component.tsx | Integrates query parameter-based tab navigation | 
| application/ui/src/features/inspect/projects-management/project-list-item/project-list-item.component.tsx | Updates project navigation to set Dataset mode | 
| application/ui/src/features/inspect/models/models.component.tsx | Creates models view displaying training jobs and completed models | 
| application/ui/src/features/inspect/jobs/show-job-logs.component.tsx | Implements SSE-based job logs viewer | 
| application/ui/src/features/inspect/inference-provider.component.tsx | Adds automatic inference triggering on model selection | 
| application/ui/src/features/inspect/dataset/dataset.component.tsx | Integrates train button and training notification toast | 
| application/ui/src/features/inspect/dataset/dataset-status-panel.component.tsx | Refactors training status display and adds job logs button | 
| application/ui/src/features/inspect/dataset/dataset-item/dataset-item.component.tsx | Changes image endpoint from full to thumbnail | 
| application/backend/src/services/job_service.py | Updates job end time tracking and SSE message formatting | 
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
        
          
                application/ui/src/features/inspect/dataset/dataset-status-panel.component.tsx
          
            Show resolved
            Hide resolved
        
              
          
                application/ui/src/features/inspect/jobs/show-job-logs.component.tsx
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                application/ui/src/features/inspect/train-model/trainable-model-list-box.component.tsx
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                application/ui/src/features/inspect/models/models.component.tsx
              
                Outdated
          
            Show resolved
            Hide resolved
        
      Log the original message on SSE parse failures to aid debugging. Change duration to allow null and use null for in-progress jobs instead of Infinity. Remove unused InfoTooltip and related imports from the trainable model list to clean up dead code.
| await queryClient.invalidateQueries({ queryKey: imagesOptions.queryKey }); | ||
| const images = await queryClient.ensureQueryData(imagesOptions); | ||
|  | ||
| if (images.media.length >= 20) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is 20 going to be the constant number used for training?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we already have a constant for it but I forgot to use it here, will update.
We might also change this to a lower number in the future.
| console.error('Could not parse message:', message); | ||
| } | ||
|  | ||
| ({ promise, resolve, reject } = Promise.withResolvers<string>()); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First time using this method, felt like a nice use case. :D
I did worry a bit that maybe this code is a bit too weird, but I expect we'd only need it here so I don't see too many issues with it.
| padding='size-200' | ||
| backgroundColor={'gray-50'} | ||
| UNSAFE_style={{ | ||
| fontSize: '11px', | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn’t we use the Spectrum CSS variables instead?
| if (job) { | ||
| const end = job.end_time ? new Date(job.end_time) : new Date(); | ||
| // Job duration in seconds | ||
| duration = Math.floor((end.getTime() - start.getTime()) / 1000); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let’s rename it to durationInSeconds so the comment isn’t needed anymore
| } | ||
|  | ||
| return { | ||
| id: model.id!, | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, let’s avoid using the Non-Null Assertion Operator. When we’re sure about the value, we can just use String(model.id) instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the one thing I wanted to check as well is if we can make these ids non-nullable as that would save us a lot of effort. @maxxgx can you look into this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I understand where nullable model.id is coming from? Where does the UI get the schema from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We get this from the OpenAPI schema from the server. If you have the server running (on the default port) then you can run,
npm run build:api
from the ui folder and this will generate two files: a openapi-spec.json and openapi-spec.d.ts. The first is taken directly from the backend's OpenAPI json schema. The second is typescript types inferred based on that.
In this case the type inference happens on the models endpoint which returns a ModelList which in turn returns the Model pydantic model.
The nullable propery comes from BaseIDModel's ID field which takes a default_factory=uuid which makes it nullable. Removing that default factory should solve it from the UI's point of view, but we might need to double check that it doesn't introduce regressions in the other models.
| <Flex alignItems={'center'} gap={'size-100'}> | ||
| {RateColors[rating].map((color, idx) => ( | ||
| <View | ||
| key={idx} | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declare const rateLabels = Object.keys(RateColors); outside the component
| key={idx} | |
| key={`rate-${rateLabels[idx]}`} | 
| setSelectedModelId(modelId); | ||
|  | ||
| if (modelId) { | ||
| if (selectedMediaItem) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason for having them in separate if statements instead of combining them into one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not anymore, will combine them. I think before I had some extra stuff in the first body but that's no longer needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| const shouldRefetchModels = jobs.some((job, idx) => { | ||
| // NOTE: assuming index stays the same | ||
| return job.status === 'completed' && job.status !== prevJobsRef.current.at(idx)?.status; | ||
| }); | 
    
      
    
      Copilot
AI
    
    
    
      Oct 24, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic assumes array indices remain stable between job updates, which may not be true if jobs are reordered, filtered, or removed. This could cause the model refresh to be triggered incorrectly or not at all. Consider comparing jobs by ID instead of index.
| // NOTE: we will need to update the trainable models endpoint to return more info | ||
| const models = trainableModels.map((model) => { | ||
| return { | ||
| modelTemplateId: model.id, | ||
| name: capitalize(model.name), | ||
| license: 'Apache 2.0', | ||
| performanceRatings: { | ||
| accuracy: 1, | ||
| inferenceSpeed: 1, | ||
| trainingTime: 1, | ||
| }, | ||
| performanceCategory: PerformanceCategory.OTHER, | ||
| } satisfies SupportedAlgorithm; | ||
| }); | 
    
      
    
      Copilot
AI
    
    
    
      Oct 24, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded placeholder values (all ratings set to 1, license 'Apache 2.0', category 'OTHER') reduce the usefulness of the performance rating UI. The TODO comment indicates this is temporary, but consider adding validation or warnings if the backend doesn't provide this data to prevent misleading users with incorrect performance metrics.
| } catch { | ||
| console.error('Could not parse message:', message); | 
    
      
    
      Copilot
AI
    
    
    
      Oct 24, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message logs to console.error but doesn't expose the parsing error details to help diagnose why JSON parsing failed. Consider logging the actual error or the problematic message content for better debugging.
| } catch { | |
| console.error('Could not parse message:', message); | |
| } catch (err) { | |
| console.error('Could not parse message:', message, 'Error:', err); | 
6bf1e55
      into
      
  
    open-edge-platform:feature/geti-inspect
  
    
📝 Description
This PR contains the changes I made for yesterday's demo. Please check the individual commits for more specific changes.
In summary this:
On the backend I made two changes:
✨ Changes
Select what type of change your PR is:
✅ Checklist
Before you submit your pull request, please make sure you have completed the following steps:
For more information about code review checklists, see the Code Review Checklist.