|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const handleResize = () => { |
|
|
const mobileScreen = window.innerWidth < 768; |
|
|
setIsMobile(mobileScreen); |
|
|
setOpen(!mobileScreen); |
|
|
}; |
|
|
|
|
|
|
|
|
handleResize(); |
|
|
|
|
|
|
|
|
window.addEventListener('resize', handleResize); |
|
|
|
|
|
|
|
|
return () => window.removeEventListener('resize', handleResize); |
|
|
}, []); |
|
|
|
|
|
const [textInput, setTextInput] = useState(""); |
|
|
const [selectedOption, setSelectedOption] = useState<{ |
|
|
value: string; |
|
|
label: string; |
|
|
} | null>(null); |
|
|
const inputRef = useRef<HTMLTextAreaElement>(null); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const el = loggerRef.current; |
|
|
if (el) { |
|
|
const scrollHeight = el.scrollHeight; |
|
|
if (scrollHeight !== loggerLastHeightRef.current) { |
|
|
el.scrollTop = scrollHeight; |
|
|
loggerLastHeightRef.current = scrollHeight; |
|
|
} |
|
|
} |
|
|
}, []); |
|
|
|
|
|
|
|
|
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> |
|
|
); |
|
|
} |
|
|
|