Anchoring custom streamlit components

Hi. I am a relative beginner to typescript but I am trying to implement a custom bi-directional streamlit component. I have the basic functionality done, but I want to anchor my component around the bottom of the page. Kind of like the way st.chat_input is anchored at the bottom and the chat history is visible as a scrollable section in the rest of the page.
I tried wrapping my component’s “render” around a div whose position is defined as “absolute”. However, upon doing so, the component stops showing altogether.
I imaginge this is a result of the fact that the streamlit components are rendered inside an iframe so defining an “absolute” position probably doesnt mean anything in the context of the entire page.
Attaching my react typescipt file modified from the component template.

Any help would be much appreciated!

import React, { ReactNode } from "react";
import {
  Streamlit,
  StreamlitComponentBase,
  withStreamlitConnection
} from "streamlit-component-lib";

interface State {
  uploadedImages: string[];
  textInput: string;
}

class ChatInputComponent extends StreamlitComponentBase<State> {
  public state = {
    uploadedImages: [],
    textInput: "",
  };

  handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    this.setState({ textInput: event.target.value });
  };

  handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    this.processFiles(event.target.files);
  };

  handleRemoveImage = (indexToRemove: number) => {
    this.setState(prevState => ({
      uploadedImages: prevState.uploadedImages.filter((_, index) => index !== indexToRemove)
    }));
  };

  handleSubmit = () => {
    Streamlit.setComponentValue({
      images: this.state.uploadedImages,
      text: this.state.textInput
    });

    // Clear state after sending
    this.setState({
      uploadedImages: [],
      textInput: ""
    });
  };

  handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
    const clipboardData = event.clipboardData;
    const items = clipboardData.items;
  
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.startsWith("image")) {
        const blob = items[i].getAsFile();
        if (blob) { // Ensure blob is not null before proceeding
          const reader = new FileReader();
  
          reader.onloadend = () => {
            this.setState(prevState => ({
              uploadedImages: [...prevState.uploadedImages, reader.result as string]
            }));
          };
          reader.readAsDataURL(blob);
          event.preventDefault(); // Prevent the image from being pasted as text
        }
      }
    }
  };
  

  processFiles = (files: FileList | null) => {
    if (!files) return;

    Array.from(files).forEach(file => {
      const reader = new FileReader();
      reader.onloadend = () => {
        this.setState(prevState => ({
          uploadedImages: [...prevState.uploadedImages, reader.result as string]
        }));
      };
      reader.readAsDataURL(file);
    });
  };

  public render = (): ReactNode => {
    return (
      <div aria-disabled={this.props.disabled} style={{ position: "relative", display: "flex", flexDirection: "column", border: "1px solid gray", borderRadius: "8px", padding: "8px" }} className="stChatFloatingInputContainer st-emotion-cache-usj992 e1d2x3se2" >
        
        {/* Uploaded Images Staging */}
        <div style={{ marginBottom: "5px" }}>
          {this.state.uploadedImages.map((image, index) => (
            <div key={index} style={{ position: "relative", display: "inline-block", margin: "5px", transition: "0.3s", borderRadius: "5px", overflow: "hidden" }}>
              <img src={image} alt="Uploaded preview" style={{ width: "50px", height: "50px" }} />
              <button onClick={() => this.handleRemoveImage(index)} style={{ position: "absolute", top: 0, right: 0, background: "red", color: "white", borderRadius: "50%", width: "15px", height: "15px", fontSize: "10px", display: "flex", alignItems: "center", justifyContent: "center" }}>
                ×
              </button>
            </div>
          ))}
        </div>
        
        {/* Chat Input Area */}
        <div style={{ display: "flex", alignItems: "center" }}>
          {/* Image Upload Button */}
          <label style={{ marginRight: "10px" }}>
            📎
            <input type="file" accept="image/*" multiple onChange={this.handleImageChange} style={{ display: "none" }} />
          </label>

          {/* Textarea for Chat */}
          <textarea
            value={this.state.textInput}
            onChange={this.handleInputChange}
            onPaste={this.handlePaste}
            placeholder="Type a message..."
            style={{ flexGrow: 1, padding: "8px", borderRadius: "8px", border: "1px solid gray", backgroundColor: "transparent", resize: "none", overflow: "auto", color: "white" }}
            onKeyDown={e => {
              if (e.key === "Enter" && !e.shiftKey) {
                // Only ENTER -> Emulate send button press
                e.preventDefault();
                this.handleSubmit();
              }
            }}
          />

          {/* Send Button */}
          <button onClick={this.handleSubmit} style={{ marginLeft: "10px", padding: "5px 10px", borderRadius: "50%", backgroundColor: "#6200ea", color: "#ffffff" }}>
            ➤
          </button>
        </div>
      </div>
    );
  };
}

export default withStreamlitConnection(ChatInputComponent);

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.