Spaces:
Running
Running
| /** | |
| * Copyright 2024 Google LLC | |
| * | |
| * 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. | |
| */ | |
| import cn from "classnames"; | |
| import { useEffect, useRef, useState } from "react"; | |
| import { RiSidebarFoldLine, RiSidebarUnfoldLine } from "react-icons/ri"; | |
| import Select from "react-select"; | |
| import { useLiveAPIContext } from "../../contexts/LiveAPIContext"; | |
| import { useLoggerStore } from "../../lib/store-logger"; | |
| import Logger from "../logger/Logger"; | |
| import type { LoggerFilterType } from "../logger/Logger"; | |
| import "./side-panel.scss"; | |
| const filterOptions = [ | |
| { value: "conversations", label: "Conversations" }, | |
| { value: "tools", label: "Tool Use" }, | |
| { value: "none", label: "All" }, | |
| ]; | |
| export default function SidePanel() { | |
| const { connected, client } = useLiveAPIContext(); | |
| const [open, setOpen] = useState(window.innerWidth >= 768); | |
| const [isMobile, setIsMobile] = useState(window.innerWidth < 768); | |
| const loggerRef = useRef<HTMLDivElement>(null); | |
| const loggerLastHeightRef = useRef<number>(-1); | |
| const { log, logs } = useLoggerStore(); | |
| // Add effect to handle responsive behavior | |
| useEffect(() => { | |
| const handleResize = () => { | |
| const mobileScreen = window.innerWidth < 768; | |
| setIsMobile(mobileScreen); | |
| setOpen(!mobileScreen); | |
| }; | |
| // Initial check | |
| handleResize(); | |
| // Add event listener for window resize | |
| window.addEventListener('resize', handleResize); | |
| // Cleanup | |
| return () => window.removeEventListener('resize', handleResize); | |
| }, []); | |
| const [textInput, setTextInput] = useState(""); | |
| const [selectedOption, setSelectedOption] = useState<{ | |
| value: string; | |
| label: string; | |
| } | null>(null); | |
| const inputRef = useRef<HTMLTextAreaElement>(null); | |
| //scroll the log to the bottom when new logs come in | |
| useEffect(() => { | |
| const el = loggerRef.current; | |
| if (el) { | |
| const scrollHeight = el.scrollHeight; | |
| if (scrollHeight !== loggerLastHeightRef.current) { | |
| el.scrollTop = scrollHeight; | |
| loggerLastHeightRef.current = scrollHeight; | |
| } | |
| } | |
| }, []); | |
| // listen for log events and store them | |
| useEffect(() => { | |
| client.on("log", log); | |
| return () => { | |
| client.off("log", log); | |
| }; | |
| }, [client, log]); | |
| const handleSubmit = () => { | |
| client.send([{ text: textInput }]); | |
| setTextInput(""); | |
| if (inputRef.current) { | |
| inputRef.current.innerText = ""; | |
| } | |
| }; | |
| return ( | |
| <div className={cn("side-panel", { | |
| open, | |
| mobile: isMobile | |
| })}> | |
| <header className="top"> | |
| <h2>Console</h2> | |
| {open ? ( | |
| <button type="button" className="opener" onClick={() => setOpen(false)}> | |
| <RiSidebarFoldLine color="#b4b8bb" /> | |
| </button> | |
| ) : ( | |
| <button type="button" className="opener" onClick={() => setOpen(true)}> | |
| <RiSidebarUnfoldLine color="#b4b8bb" /> | |
| </button> | |
| )} | |
| </header> | |
| <section className="indicators"> | |
| <Select | |
| className="react-select" | |
| classNamePrefix="react-select" | |
| styles={{ | |
| control: (baseStyles) => ({ | |
| ...baseStyles, | |
| background: "var(--Neutral-15)", | |
| color: "var(--Neutral-90)", | |
| minHeight: "33px", | |
| maxHeight: "33px", | |
| border: 0, | |
| }), | |
| option: (styles, { isFocused, isSelected }) => ({ | |
| ...styles, | |
| backgroundColor: isFocused | |
| ? "var(--Neutral-30)" | |
| : isSelected | |
| ? "var(--Neutral-20)" | |
| : undefined, | |
| }), | |
| }} | |
| defaultValue={selectedOption} | |
| options={filterOptions} | |
| onChange={(e) => { | |
| setSelectedOption(e); | |
| }} | |
| /> | |
| <div className={cn("streaming-indicator", { connected })}> | |
| {connected | |
| ? `🔵${open ? " Streaming" : ""}` | |
| : `⏸️${open ? " Paused" : ""}`} | |
| </div> | |
| </section> | |
| <div className="side-panel-container" ref={loggerRef}> | |
| <Logger | |
| filter={(selectedOption?.value as LoggerFilterType) || "none"} | |
| /> | |
| </div> | |
| <div className={cn("input-container", { disabled: !connected })}> | |
| <div className="input-content"> | |
| <textarea | |
| className="input-area" | |
| ref={inputRef} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| handleSubmit(); | |
| } | |
| }} | |
| onChange={(e) => setTextInput(e.target.value)} | |
| value={textInput} | |
| /> | |
| <span | |
| className={cn("input-content-placeholder", { | |
| hidden: textInput.length, | |
| })} | |
| > | |
| Type something... | |
| </span> | |
| <button | |
| type="button" | |
| className="send-button material-symbols-outlined filled" | |
| onClick={handleSubmit} | |
| > | |
| send | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |