Skip to content

Commit

Permalink
Merge pull request #1202 from samchon/feat/chat
Browse files Browse the repository at this point in the history
Make `@nestia/agent` responsive.
  • Loading branch information
samchon authored Jan 24, 2025
2 parents 86cba72 + 74f53aa commit f9f85eb
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 85 deletions.
2 changes: 1 addition & 1 deletion packages/chat/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/chat",
"version": "0.3.2",
"version": "0.3.3",
"type": "module",
"main": "./lib/index.mjs",
"typings": "./lib/index.d.ts",
Expand Down
224 changes: 144 additions & 80 deletions packages/chat/src/movies/NestiaChatMovie.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import AddAPhotoIcon from "@mui/icons-material/AddAPhoto";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import SendIcon from "@mui/icons-material/Send";
import {
AppBar,
Button,
Container,
Drawer,
IconButton,
Input,
Theme,
Toolbar,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
INestiaAgentEvent,
INestiaAgentOperationSelection,
INestiaAgentPrompt,
INestiaAgentTokenUsage,
Expand All @@ -23,13 +30,17 @@ import { NestiaChatMessageMovie } from "./messages/NestiaChatMessageMovie";
import { NestiaChatSideMovie } from "./sides/NestiaChatSideMovie";

export const NestiaChatMovie = ({ agent }: NestiaChatMovie.IProps) => {
//----
// VARIABLES
//----
// REFERENCES
const upperDivRef = useRef<HTMLDivElement>(null);
const middleDivRef = useRef<HTMLDivElement>(null);
const bottomDivRef = useRef<HTMLDivElement>(null);
const bodyContainerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

// STATES
const [error, setError] = useState<Error | null>(null);
const [text, setText] = useState("");
const [histories, setHistories] = useState<INestiaAgentPrompt[]>(
Expand All @@ -43,41 +54,57 @@ export const NestiaChatMovie = ({ agent }: NestiaChatMovie.IProps) => {
const [selections, setSelections] = useState<
INestiaAgentOperationSelection[]
>([]);
const [openSide, setOpenSide] = useState(false);

useEffect(() => {
if (inputRef.current !== null) inputRef.current.select();
agent.on("text", (evt) => {
histories.push(evt);
setHistories(histories);
});
agent.on("describe", (evt) => {
histories.push(evt);
setHistories(histories);
//----
// EVENT INTERACTIONS
//----
// EVENT LISTENERS
const handleText = (evt: INestiaAgentEvent.IText) => {
histories.push(evt);
setHistories(histories);
};
const handleDescribe = (evt: INestiaAgentEvent.IDescribe) => {
histories.push(evt);
setHistories(histories);
};
const handleSelect = (evt: INestiaAgentEvent.ISelect) => {
histories.push({
type: "select",
id: "something",
operations: [
{
...evt.operation,
reason: evt.reason,
toJSON: () => ({}) as any,
} satisfies INestiaAgentOperationSelection,
],
});
agent.on("select", (evt) => {
histories.push({
type: "select",
id: "something",
operations: [
{
...evt.operation,
reason: evt.reason,
toJSON: () => ({}) as any,
} satisfies INestiaAgentOperationSelection,
],
});
setHistories(histories);
setHistories(histories);

selections.push({
...evt.operation,
reason: evt.reason,
toJSON: () => ({}) as any,
} satisfies INestiaAgentOperationSelection);
setSelections(selections);
});
selections.push({
...evt.operation,
reason: evt.reason,
toJSON: () => ({}) as any,
} satisfies INestiaAgentOperationSelection);
setSelections(selections);
};

// INITIALIZATION
useEffect(() => {
if (inputRef.current !== null) inputRef.current.select();
agent.on("text", handleText);
agent.on("describe", handleDescribe);
agent.on("select", handleSelect);
setTokenUsage(agent.getTokenUsage());
return () => {
agent.off("text", handleText);
agent.off("describe", handleDescribe);
agent.off("select", handleSelect);
};
}, []);

// EVENT HANDLERS
const handleKeyUp = async (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && event.shiftKey === false) {
if (enabled === false) event.preventDefault();
Expand Down Expand Up @@ -106,9 +133,10 @@ export const NestiaChatMovie = ({ agent }: NestiaChatMovie.IProps) => {
try {
await agent.conversate(text);
} catch (error) {
console.log(error);
if (error instanceof Error) setError(error);
else setError(new Error("Unknown error"));
if (error instanceof Error) {
alert(error.message);
setError(error);
} else setError(new Error("Unknown error"));
return;
}

Expand Down Expand Up @@ -141,7 +169,6 @@ export const NestiaChatMovie = ({ agent }: NestiaChatMovie.IProps) => {

const capture = async () => {
if (bodyContainerRef.current === null) return;

const canvas: HTMLCanvasElement = await html2canvas(
bodyContainerRef.current,
{
Expand All @@ -158,20 +185,78 @@ export const NestiaChatMovie = ({ agent }: NestiaChatMovie.IProps) => {
});
};

//----
// RENDERERS
//----
const theme: Theme = useTheme();
const isMobile: boolean = useMediaQuery(theme.breakpoints.down("lg"));
const bodyMovie = (): JSX.Element => (
<Container
ref={bodyContainerRef}
maxWidth={false}
style={{
marginBottom: 15,
paddingBottom: 50,
width: isMobile ? "100%" : `calc(100% - ${SIDE_WIDTH}px)`,
height: "100%",
overflowY: "scroll",
backgroundColor: "lightblue",
}}
>
{histories
.map((prompt) => <NestiaChatMessageMovie prompt={prompt} />)
.filter((elem) => elem !== null)}
</Container>
);
const sideMovie = (): JSX.Element => (
<div
style={{
width: isMobile ? undefined : SIDE_WIDTH,
height: "100%",
overflowY: "auto",
backgroundColor: "#eeeeee",
}}
>
<Container
maxWidth={false}
onClick={isMobile ? () => setOpenSide(false) : undefined}
>
<NestiaChatSideMovie
provider={agent.getProvider()}
config={agent.getConfig()}
usage={tokenUsage}
selections={selections}
error={error}
/>
</Container>
</div>
);

return (
<div style={{ width: "100%", height: "100%" }}>
<AppBar ref={upperDivRef} position="relative" component="div">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Nestia A.I. Chatbot
</Typography>
<Button
color="inherit"
startIcon={<AddAPhotoIcon />}
onClick={capture}
>
Screenshot Capture
</Button>
{isMobile ? (
<>
<IconButton onClick={capture}>
<AddAPhotoIcon />
</IconButton>
<IconButton onClick={() => setOpenSide(true)}>
<ReceiptLongIcon />
</IconButton>
</>
) : (
<Button
color="inherit"
startIcon={<AddAPhotoIcon />}
onClick={capture}
>
Screenshot Capture
</Button>
)}
</Toolbar>
</AppBar>
<div
Expand All @@ -183,44 +268,23 @@ export const NestiaChatMovie = ({ agent }: NestiaChatMovie.IProps) => {
flexDirection: "row",
}}
>
<div
style={{
paddingBottom: 50,
width: `calc(100% - ${RIGHT_WIDTH}px)`,
overflowY: "scroll",
backgroundColor: "lightblue",
}}
>
<Container
ref={bodyContainerRef}
style={{
marginBottom: 15,
}}
>
{histories
.map((prompt) => <NestiaChatMessageMovie prompt={prompt} />)
.filter((elem) => elem !== null)}
</Container>
</div>
<div
ref={scrollRef}
style={{
paddingBottom: 50,
width: RIGHT_WIDTH,
overflowY: "auto",
backgroundColor: "#eeeeee",
}}
>
<Container maxWidth={false}>
<NestiaChatSideMovie
provider={agent.getProvider()}
config={agent.getConfig()}
usage={tokenUsage}
selections={selections}
error={error}
/>
</Container>
</div>
{isMobile ? (
<>
{bodyMovie()}
<Drawer
anchor="right"
open={openSide}
onClose={() => setOpenSide(false)}
>
{sideMovie()}
</Drawer>
</>
) : (
<>
{bodyMovie()}
{sideMovie()}
</>
)}
</div>
<AppBar
ref={bottomDivRef}
Expand Down Expand Up @@ -261,4 +325,4 @@ export namespace NestiaChatMovie {
}
}

const RIGHT_WIDTH = 450;
const SIDE_WIDTH = 450;
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const NestiaChatDescribeMessageMovie = ({
style={{
marginTop: 15,
marginBottom: 15,
marginRight: 200,
marginRight: "15%",
}}
>
<CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const NestiaChatSelectMessageMovie = ({
style={{
marginTop: 15,
marginBottom: 15,
marginRight: 200,
marginRight: "15%",
}}
>
<CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export const NestiaChatTextMessageMovie = ({
style={{
marginTop: 15,
marginBottom: 15,
marginLeft: prompt.role === "user" ? 200 : undefined,
marginRight: prompt.role === "assistant" ? 200 : undefined,
marginLeft: prompt.role === "user" ? "15%" : undefined,
marginRight: prompt.role === "assistant" ? "15%" : undefined,
textAlign: prompt.role === "user" ? "right" : "left",
backgroundColor: prompt.role === "user" ? "lightyellow" : undefined,
}}
Expand Down

0 comments on commit f9f85eb

Please # to comment.