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" }, | |
]; | |
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 something... | |
</span> | |
<button | |
type="button" | |
className="send-button material-symbols-outlined filled" | |
onClick={handleSubmit} | |
> | |
send | |
</button> | |
</div> | |
</div> | |
</div> | |
); | |
} | |