Skip to content

Conversation

@sfc-gh-nbellante
Copy link
Contributor

@sfc-gh-nbellante sfc-gh-nbellante commented Oct 20, 2025

Describe your changes

Added new chat input components for the Streamlit frontend:

  1. ChatComposer - A versatile chat input component that supports:
    • Text input with keyboard shortcuts (Enter to send, Shift+Enter for new line)
    • File attachments via a file picker
    • Audio recording capabilities when enabled
  2. ChatAudioRecorder - A component for recording audio messages that:
    • Displays a waveform visualization during recording
    • Provides approve/cancel controls for the recording
    • Handles audio format conversion for different browser implementations

These components are designed to be used together to create a modern chat interface with multimedia support.

Testing Plan

  • Unit Tests: Added comprehensive test suites for both components:
    • ChatAudioRecorder.test.tsx - Tests recording lifecycle, error handling, and UI states
    • ChatComposer.test.tsx - Tests input handling, file selection, and audio recording integration
  • The tests cover key functionality including:
    • Recording start/stop/cancel flows
    • Error handling during recording
    • Accessibility features
    • State management during recording
    • File format handling
  • Manual testing should include:
    • Testing in different browsers to verify audio recording compatibility
    • Testing with screen readers to verify accessibility
    • Testing the responsive behavior of the components

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

@snyk-io
Copy link
Contributor

snyk-io bot commented Oct 20, 2025

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Licenses 0 0 0 0 0 issues
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Contributor Author

sfc-gh-nbellante commented Oct 20, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 20, 2025

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-12834/streamlit-1.51.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-12834.streamlit.app (☁️ Deploy here if not accessible)

@github-actions
Copy link
Contributor

github-actions bot commented Oct 20, 2025

📉 Python coverage change detected

The Python unit test coverage has decreased by 0.0611%

  • Current PR: 92.7643% (20081 statements, 1453 missed)
  • Latest develop: 92.8254% (20057 statements, 1439 missed)

💡 Consider adding more unit tests to maintain or improve coverage.

Coverage by files
Name Stmts Miss Cover
streamlit/__init__.py 141 0 100%
streamlit/__main__.py 3 3 0%
streamlit/auth_util.py 100 25 75%
streamlit/cli_util.py 39 6 85%
streamlit/column_config.py 3 0 100%
streamlit/commands/__init__.py 0 0 100%
streamlit/commands/echo.py 54 11 80%
streamlit/commands/execution_control.py 59 10 83%
streamlit/commands/experimental_query_params.py 40 2 95%
streamlit/commands/logo.py 41 6 85%
streamlit/commands/navigation.py 106 2 98%
streamlit/commands/page_config.py 101 4 96%
streamlit/components/__init__.py 0 0 100%
streamlit/components/lib/__init__.py 0 0 100%
streamlit/components/lib/local_component_registry.py 35 2 94%
streamlit/components/types/__init__.py 0 0 100%
streamlit/components/types/base_component_registry.py 14 0 100%
streamlit/components/types/base_custom_component.py 49 6 88%
streamlit/components/v1/__init__.py 5 0 100%
streamlit/components/v1/component_arrow.py 33 8 76%
streamlit/components/v1/component_registry.py 41 3 93%
streamlit/components/v1/components.py 4 4 0%
streamlit/components/v1/custom_component.py 85 7 92%
streamlit/components/v2/__init__.py 24 0 100%
streamlit/components/v2/bidi_component/__init__.py 4 0 100%
streamlit/components/v2/bidi_component/constants.py 5 0 100%
streamlit/components/v2/bidi_component/main.py 109 12 89%
streamlit/components/v2/bidi_component/serialization.py 81 5 94%
streamlit/components/v2/bidi_component/state.py 13 0 100%
streamlit/components/v2/component_definition_resolver.py 30 0 100%
streamlit/components/v2/component_file_watcher.py 117 9 92%
streamlit/components/v2/component_manager.py 96 16 83%
streamlit/components/v2/component_manifest_handler.py 24 0 100%
streamlit/components/v2/component_path_utils.py 68 5 93%
streamlit/components/v2/component_registry.py 118 8 93%
streamlit/components/v2/get_bidi_component_manager.py 8 1 88%
streamlit/components/v2/manifest_scanner.py 224 25 89%
streamlit/components/v2/presentation.py 84 19 77%
streamlit/components/v2/types.py 8 8 0%
streamlit/config.py 412 12 97%
streamlit/config_option.py 79 3 96%
streamlit/config_util.py 288 7 98%
streamlit/connections/__init__.py 6 0 100%
streamlit/connections/base_connection.py 45 0 100%
streamlit/connections/snowflake_connection.py 60 13 78%
streamlit/connections/snowpark_connection.py 44 3 93%
streamlit/connections/sql_connection.py 56 6 89%
streamlit/connections/util.py 33 0 100%
streamlit/cursor.py 82 2 98%
streamlit/dataframe_util.py 501 45 91%
streamlit/delta_generator.py 205 6 97%
streamlit/delta_generator_singletons.py 76 8 89%
streamlit/deprecation_util.py 59 4 93%
streamlit/development.py 1 0 100%
streamlit/elements/__init__.py 0 0 100%
streamlit/elements/alert.py 60 0 100%
streamlit/elements/arrow.py 198 15 92%
streamlit/elements/balloons.py 10 0 100%
streamlit/elements/bokeh_chart.py 27 0 100%
streamlit/elements/code.py 20 1 95%
streamlit/elements/deck_gl_json_chart.py 104 10 90%
streamlit/elements/dialog_decorator.py 38 0 100%
streamlit/elements/doc_string.py 227 9 96%
streamlit/elements/empty.py 16 4 75%
streamlit/elements/exception.py 101 10 90%
streamlit/elements/form.py 54 2 96%
streamlit/elements/graphviz_chart.py 35 1 97%
streamlit/elements/heading.py 56 0 100%
streamlit/elements/html.py 48 0 100%
streamlit/elements/iframe.py 29 0 100%
streamlit/elements/image.py 32 0 100%
streamlit/elements/json.py 39 2 95%
streamlit/elements/layouts.py 140 3 98%
streamlit/elements/lib/__init__.py 0 0 100%
streamlit/elements/lib/built_in_chart_utils.py 387 26 93%
streamlit/elements/lib/color_util.py 100 4 96%
streamlit/elements/lib/column_config_utils.py 168 1 99%
streamlit/elements/lib/column_types.py 189 4 98%
streamlit/elements/lib/dialog.py 67 1 99%
streamlit/elements/lib/dicttools.py 39 2 95%
streamlit/elements/lib/file_uploader_utils.py 30 0 100%
streamlit/elements/lib/form_utils.py 26 0 100%
streamlit/elements/lib/image_utils.py 176 21 88%
streamlit/elements/lib/js_number.py 28 3 89%
streamlit/elements/lib/layout_utils.py 109 1 99%
streamlit/elements/lib/mutable_status_container.py 73 4 95%
streamlit/elements/lib/options_selector_utils.py 90 0 100%
streamlit/elements/lib/pandas_styler_utils.py 80 2 98%
streamlit/elements/lib/policies.py 56 1 98%
streamlit/elements/lib/streamlit_plotly_theme.py 49 0 100%
streamlit/elements/lib/subtitle_utils.py 76 13 83%
streamlit/elements/lib/utils.py 76 5 93%
streamlit/elements/map.py 110 1 99%
streamlit/elements/markdown.py 63 2 97%
streamlit/elements/media.py 181 8 96%
streamlit/elements/metric.py 91 0 100%
streamlit/elements/pdf.py 49 2 96%
streamlit/elements/plotly_chart.py 114 4 96%
streamlit/elements/progress.py 36 0 100%
streamlit/elements/pyplot.py 39 2 95%
streamlit/elements/snow.py 10 0 100%
streamlit/elements/space.py 12 0 100%
streamlit/elements/spinner.py 34 0 100%
streamlit/elements/text.py 16 0 100%
streamlit/elements/toast.py 26 0 100%
streamlit/elements/vega_charts.py 226 3 99%
streamlit/elements/widgets/__init__.py 0 0 100%
streamlit/elements/widgets/audio_input.py 68 10 85%
streamlit/elements/widgets/button.py 214 3 99%
streamlit/elements/widgets/button_group.py 161 1 99%
streamlit/elements/widgets/camera_input.py 62 10 84%
streamlit/elements/widgets/chat.py 193 54 72%
streamlit/elements/widgets/checkbox.py 52 0 100%
streamlit/elements/widgets/color_picker.py 56 2 96%
streamlit/elements/widgets/data_editor.py 239 14 94%
streamlit/elements/widgets/file_uploader.py 103 18 83%
streamlit/elements/widgets/multiselect.py 105 4 96%
streamlit/elements/widgets/number_input.py 143 5 97%
streamlit/elements/widgets/radio.py 83 5 94%
streamlit/elements/widgets/select_slider.py 97 0 100%
streamlit/elements/widgets/selectbox.py 91 2 98%
streamlit/elements/widgets/slider.py 241 8 97%
streamlit/elements/widgets/text_widgets.py 130 6 95%
streamlit/elements/widgets/time_widgets.py 248 15 94%
streamlit/elements/write.py 166 30 82%
streamlit/emojis.py 4 0 100%
streamlit/env_util.py 21 3 86%
streamlit/error_util.py 33 2 94%
streamlit/errors.py 188 25 87%
streamlit/external/__init__.py 0 0 100%
streamlit/external/langchain/__init__.py 2 0 100%
streamlit/external/langchain/streamlit_callback_handler.py 141 82 42%
streamlit/file_util.py 84 8 90%
streamlit/git_util.py 100 5 95%
streamlit/logger.py 54 0 100%
streamlit/material_icon_names.py 1 0 100%
streamlit/navigation/__init__.py 0 0 100%
streamlit/navigation/page.py 78 2 97%
streamlit/net_util.py 55 3 95%
streamlit/platform.py 10 1 90%
streamlit/runtime/__init__.py 8 0 100%
streamlit/runtime/app_session.py 447 94 79%
streamlit/runtime/caching/__init__.py 19 0 100%
streamlit/runtime/caching/cache_data_api.py 164 3 98%
streamlit/runtime/caching/cache_errors.py 45 4 91%
streamlit/runtime/caching/cache_resource_api.py 122 0 100%
streamlit/runtime/caching/cache_type.py 11 1 91%
streamlit/runtime/caching/cache_utils.py 165 9 95%
streamlit/runtime/caching/cached_message_replay.py 108 1 99%
streamlit/runtime/caching/hashing.py 311 25 92%
streamlit/runtime/caching/legacy_cache_api.py 14 0 100%
streamlit/runtime/caching/storage/__init__.py 2 0 100%
streamlit/runtime/caching/storage/cache_storage_protocol.py 31 2 94%
streamlit/runtime/caching/storage/dummy_cache_storage.py 21 0 100%
streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py 60 0 100%
streamlit/runtime/caching/storage/local_disk_cache_storage.py 86 4 95%
streamlit/runtime/connection_factory.py 85 9 89%
streamlit/runtime/context.py 140 0 100%
streamlit/runtime/context_util.py 18 0 100%
streamlit/runtime/credentials.py 139 4 97%
streamlit/runtime/forward_msg_cache.py 23 2 91%
streamlit/runtime/forward_msg_queue.py 63 4 94%
streamlit/runtime/fragment.py 112 2 98%
streamlit/runtime/media_file_manager.py 69 7 90%
streamlit/runtime/media_file_storage.py 15 0 100%
streamlit/runtime/memory_media_file_storage.py 68 0 100%
streamlit/runtime/memory_session_storage.py 15 0 100%
streamlit/runtime/memory_uploaded_file_manager.py 41 1 98%
streamlit/runtime/metrics_util.py 193 12 94%
streamlit/runtime/pages_manager.py 59 2 97%
streamlit/runtime/runtime.py 248 18 93%
streamlit/runtime/runtime_util.py 30 1 97%
streamlit/runtime/script_data.py 16 0 100%
streamlit/runtime/scriptrunner/__init__.py 5 0 100%
streamlit/runtime/scriptrunner/exec_code.py 49 5 90%
streamlit/runtime/scriptrunner/magic.py 83 1 99%
streamlit/runtime/scriptrunner/magic_funcs.py 10 1 90%
streamlit/runtime/scriptrunner/script_cache.py 27 0 100%
streamlit/runtime/scriptrunner/script_runner.py 230 27 88%
streamlit/runtime/scriptrunner_utils/__init__.py 0 0 100%
streamlit/runtime/scriptrunner_utils/exceptions.py 11 1 91%
streamlit/runtime/scriptrunner_utils/script_requests.py 106 5 95%
streamlit/runtime/scriptrunner_utils/script_run_context.py 135 2 99%
streamlit/runtime/secrets.py 242 25 90%
streamlit/runtime/session_manager.py 60 1 98%
streamlit/runtime/state/__init__.py 7 0 100%
streamlit/runtime/state/common.py 52 2 96%
streamlit/runtime/state/presentation.py 19 4 79%
streamlit/runtime/state/query_params.py 110 3 97%
streamlit/runtime/state/query_params_proxy.py 71 0 100%
streamlit/runtime/state/safe_session_state.py 77 11 86%
streamlit/runtime/state/session_state.py 433 29 93%
streamlit/runtime/state/session_state_proxy.py 62 8 87%
streamlit/runtime/state/widgets.py 15 1 93%
streamlit/runtime/stats.py 42 0 100%
streamlit/runtime/theme_util.py 46 1 98%
streamlit/runtime/uploaded_file_manager.py 39 3 92%
streamlit/runtime/websocket_session_manager.py 66 0 100%
streamlit/source_util.py 36 1 97%
streamlit/string_util.py 88 7 92%
streamlit/temporary_directory.py 18 1 94%
streamlit/testing/__init__.py 0 0 100%
streamlit/testing/v1/__init__.py 2 0 100%
streamlit/testing/v1/app_test.py 239 6 97%
streamlit/testing/v1/element_tree.py 1327 87 93%
streamlit/testing/v1/local_script_runner.py 71 2 97%
streamlit/testing/v1/util.py 17 0 100%
streamlit/time_util.py 28 1 96%
streamlit/type_util.py 138 12 91%
streamlit/url_util.py 39 5 87%
streamlit/user_info.py 87 8 91%
streamlit/util.py 38 1 97%
streamlit/version.py 3 0 100%
streamlit/watcher/__init__.py 3 0 100%
streamlit/watcher/event_based_path_watcher.py 181 24 87%
streamlit/watcher/folder_black_list.py 14 1 93%
streamlit/watcher/local_sources_watcher.py 127 9 93%
streamlit/watcher/path_watcher.py 43 3 93%
streamlit/watcher/polling_path_watcher.py 55 2 96%
streamlit/watcher/util.py 49 1 98%
streamlit/web/__init__.py 0 0 100%
streamlit/web/bootstrap.py 138 18 87%
streamlit/web/cache_storage_manager_config.py 5 0 100%
streamlit/web/cli.py 186 17 91%
streamlit/web/server/__init__.py 5 0 100%
streamlit/web/server/app_static_file_handler.py 29 3 90%
streamlit/web/server/authlib_tornado_integration.py 18 1 94%
streamlit/web/server/bidi_component_request_handler.py 65 8 88%
streamlit/web/server/browser_websocket_handler.py 115 31 73%
streamlit/web/server/component_file_utils.py 24 0 100%
streamlit/web/server/component_request_handler.py 55 4 93%
streamlit/web/server/media_file_handler.py 65 9 86%
streamlit/web/server/oauth_authlib_routes.py 118 18 85%
streamlit/web/server/oidc_mixin.py 44 0 100%
streamlit/web/server/routes.py 90 7 92%
streamlit/web/server/server.py 188 11 94%
streamlit/web/server/server_util.py 67 5 93%
streamlit/web/server/stats_request_handler.py 53 4 92%
streamlit/web/server/upload_file_request_handler.py 53 9 83%
streamlit/web/server/websocket_headers.py 19 1 95%
TOTAL 20081 1453 93%

📊 View detailed coverage comparison

@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch from deb8f05 to 53062ef Compare October 21, 2025 14:07
@sfc-gh-nbellante sfc-gh-nbellante added security-assessment-completed Security assessment has been completed for PR impact:internal PR changes only affect internal code change:feature PR contains new feature or enhancement implementation labels Oct 21, 2025
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch from 53062ef to 662c525 Compare October 21, 2025 14:33
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch from 662c525 to fa19cbe Compare October 21, 2025 14:54
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch 4 times, most recently from 5dbf8af to 53b4659 Compare October 21, 2025 21:01
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr6-audio-infra branch 2 times, most recently from c3d5122 to ba14e2a Compare October 23, 2025 14:18
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch from 53b4659 to 9ba9e8d Compare October 23, 2025 14:18
@github-actions
Copy link
Contributor

github-actions bot commented Oct 23, 2025

📉 Frontend coverage change detected

The frontend unit test (vitest) coverage has decreased by 0.0200%

  • Current PR: 85.9900% (50459 lines, 7068 missed)
  • Latest develop: 86.0100% (49932 lines, 6981 missed)

✅ Coverage change is within normal range.

📊 View detailed coverage comparison

@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch 3 times, most recently from e9eb59c to e5cdcf3 Compare October 23, 2025 21:18
Refactor handleApprove to use finally block for proper state cleanup.
Ensures pending state is reset regardless of success or failure.

Resolves comment 1
Standardize callback types to always return Promise<void> for callbacks
that are awaited internally, making the API contract clearer and more consistent.

Resolves comment 2
Remove over-engineered customizable strings prop from ChatAudioRecorder
and ChatComposer. Hardcode English UI strings to align with Streamlit patterns.

Resolves comments 3 and 7
Remove unnecessary requestAnimationFrame delay before starting recording.
The mount ref is always rendered (even when hidden), so the delay is not needed.

Resolves comment 4
Add error logging to the recording startup catch block using Streamlit's
loglevel logger instead of silently swallowing errors.

Resolves comment 5
Removed $ transient prop pattern (not used elsewhere in codebase) and
simplified code by removing confusing intermediate boolean variables.
Changed $variant to variant and use direct comparisons for clarity.

Resolves comments 11 and 12
Use toBeVisible() instead of getByRole + toBeInTheDocument() pattern.
The query already throws if element is not found, making the additional
assertion redundant. toBeVisible() is more meaningful as it tests actual
visibility to the user.

Resolves PR #12834 comment #13
The input element is always mounted in the DOM (never conditionally
rendered), just toggled between enabled/disabled states. Therefore,
requestAnimationFrame is not needed for focus management - we can
focus the element directly.

Resolves PR #12834 comment #4
Removed customizable strings from ChatComposer to match ChatAudioRecorder.
This pattern was over-engineering - no i18n system exists yet and it adds
unnecessary complexity. Now uses hard-coded aria-labels directly.

Also restored requestAnimationFrame in focusInput after tests proved it's
necessary. RAF ensures focus happens after click events fully process,
preventing race conditions when transitioning focus from buttons to input.

Resolves PR #12834 comment #7
Addresses PR #12834 comment #4
Removed the strings prop from ChatAudioRecorder test that was passing
a prop that doesn't exist in the component interface. This prop was
removed when we fixed the over-engineering in comment #3, but the test
wasn't updated. Test still passes as it searches for hard-coded labels.

Resolves PR #12834 comment #14
Removed the .catch() handler from the Approve button's onClick since it's
unreachable code. The handleApprove() function already catches errors
internally and doesn't rethrow them, so the returned promise never rejects.
This simplifies the code and removes the confusion about duplicate error
handling.

Resolves PR #12834 Graphite Agent comment
Add memoized handleApproveClick wrapper to satisfy @typescript-eslint/no-misused-promises rule while maintaining referential stability. The async handleApprove function is now wrapped in a useCallback that explicitly discards the Promise with void operator.
Replace toBeInTheDocument() with toBeVisible() to follow React Testing Library best practices. Using getBy* queries with toBeInTheDocument() is redundant since getBy* already throws if element not found.

Resolves comment 2
Remove hardcoded minWidth from audio buttons to allow natural content-based sizing. Investigated theme.sizes and found no appropriate minElementWidth constant exists, so letting buttons size to their content is cleaner.

Resolves comment 3
Convert all outlineOffset values from pixel units (2) to relative units ("0.125rem") to follow coding guidelines. This ensures better accessibility and proper scaling with user font preferences.

Resolves comment 4
Removed incomplete catch block with placeholder comment and added proper error logging to handleSubmit and handleRecordingApprove using LOG.error() for better observability and debugging.

Resolves comment 4
Remove the isRecording prop from ChatAudioRecorder and implement internal
state management with callback notification pattern. This eliminates tight
coupling between parent and child components while maintaining proper
React patterns.

Changes:
- Remove isRecording prop from ChatAudioRecorderProps
- Add internal isRecording state to ChatAudioRecorder
- Add onRecordingStateChange callback to notify parent of state changes
- Expose isRecording as readonly property on ChatAudioRecorderRef
- Update ChatComposer to use callback pattern for state synchronization
- Update all tests to use ref-based API (ref.current?.startRecording())
- Fix ChatComposer.test.tsx mock to properly implement ref interface

This refactor fixes the failing JS unit tests by aligning test expectations
with the ref-based imperative API pattern introduced in the previous PR
feedback iteration.
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nico/pr7-chatinput-components branch from 37a8398 to 8c11177 Compare October 31, 2025 17:17
Define MockProps and MockRef interfaces for the ChatAudioRecorder mock
component instead of using 'any' types. This provides proper type safety
and matches the actual component interface.
When using queryBy* queries (which return null if not found), use toBeNull()
instead of not.toBeInTheDocument() to avoid redundant assertions. This
follows the TypeScript Test Guide best practices.
Explicitly type the props and ref parameters in the forwardRef callback
since TypeScript cannot infer types from require('react'). This resolves
TS2347 and TS7006 errors.
Cannot pass type arguments to forwardRef from require('react') as it's
untyped. Instead, remove the generic type arguments from forwardRef and
add them to useImperativeHandle where they're actually needed. This fixes
TS2347 error in strict mode.
Remove generic type arguments from useImperativeHandle call in the mock
ChatAudioRecorder component. Functions imported via require("react") are
untyped and cannot accept TypeScript generic type arguments.

This is the same issue previously fixed for forwardRef on line 73.
@sfc-gh-nbellante
Copy link
Contributor Author

Gonna close this one for now until I've had more time to think through this abstraction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:internal PR changes only affect internal code security-assessment-completed Security assessment has been completed for PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants