Trudy's picture
Add P5WithEditor component and improve UI/UX
ac97894
/**
* 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" },
];
interface SidePanelProps {
initialCollapsed?: boolean;
}
export default function SidePanel({ initialCollapsed = false }: SidePanelProps) {
const { connected, client } = useLiveAPIContext();
const [open, setOpen] = useState(!initialCollapsed && 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);
if (!initialCollapsed) {
setOpen(!mobileScreen);
}
};
// Initial check
handleResize();
// Add event listener for window resize
window.addEventListener('resize', handleResize);
// Cleanup
return () => window.removeEventListener('resize', handleResize);
}, [initialCollapsed]);
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&nbsp;something...
</span>
<button
type="button"
className="send-button material-symbols-outlined filled"
onClick={handleSubmit}
>
send
</button>
</div>
</div>
</div>
);
}