Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/node_modules/*
**/out/*
**/.next/*
**/dist/*

coverage/*

Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# dependencies
/node_modules
genkit/node_modules

# ai session data
genkit/data

# next.js
/.next/
Expand All @@ -15,6 +19,7 @@ npm-debug.log*
/.env*.local
/.env.development
/.env.production
genkit/.env

# typescript
*.tsbuildinfo
Expand All @@ -31,6 +36,7 @@ npm-debug.log*

# Build Dir
/out
genkit/dist

# python
venv
Expand Down
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
package.json
package-lock.json
node_modules
genkit/package.json
genkit/package-lock.json
genkit/node_modules

# build
out
coverage
genkit/dist

# python
venv
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,23 @@ Using Node.js version `20.10.0`, run `npm install` in the root directory of the

## Using the development server

### Running the web app only

The app can be run for development using `npm run dev`, and accessed at `http://localhost:3000`.

### Running the Genkit server only

The Genkit server can be run for development using `npm run genkit:dev`, and accessed at `http://localhost:3100`.

### Running both services together

To run both the web app and Genkit server together, use `npm run dev:all`. This will start:

- The web app at `http://localhost:3000`
- The Genkit server at `http://localhost:3100`

This is the recommended approach for development, especially when working with features that require both services, such as the chat component.

## Building the app locally

Run `npm run build:local` to build. The built app can be run using `npm start`, and accessed at `http://localhost:3000`.
Expand Down
271 changes: 271 additions & 0 deletions app/components/Chat/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import React, { useState, useEffect, useRef } from "react";
import {
Box,
TextField,
Button,
Typography,
Paper,
CircularProgress,
} from "@mui/material";
import SendIcon from "@mui/icons-material/Send";
import ReactMarkdown from "react-markdown";
import { styled } from "@mui/material/styles";

// Define message interface
interface ChatMessage {
content: string;
role: "user" | "assistant";
}

interface ChatProps {
sessionId?: string | null;
}

// Styled components
const ChatContainer = styled(Box)(() => ({
display: "flex",
flexDirection: "column",
height: "100%",
overflow: "hidden",
width: "100%",
}));

const ChatMessages = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
display: "flex",
flex: 1,
flexDirection: "column",
gap: theme.spacing(2),
overflowY: "auto",
padding: theme.spacing(2),
paddingTop: theme.spacing(3),
}));

const MessageBubble = styled(Paper, {
shouldForwardProp: (prop) => prop !== "isUser",
})<{ isUser: boolean }>(({ isUser, theme }) => ({
"& p": {
margin: 0,
},
alignSelf: isUser ? "flex-end" : "flex-start",
animation: "fadeIn 0.3s ease",
backgroundColor: isUser
? theme.palette.primary.main
: theme.palette.background.paper,
borderBottomLeftRadius: isUser ? theme.spacing(2) : theme.spacing(0.5),
borderBottomRightRadius: isUser ? theme.spacing(0.5) : theme.spacing(2),
borderRadius: theme.spacing(2),
color: isUser
? theme.palette.primary.contrastText
: theme.palette.text.primary,
maxWidth: "90%",
overflowWrap: "break-word",
padding: theme.spacing(1.5, 2),
width: "auto",
wordBreak: "break-word",
}));

const ErrorMessage = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.error.light,
borderRadius: theme.shape.borderRadius,
color: theme.palette.error.main,
fontSize: "0.875rem",
margin: theme.spacing(1, 0),
padding: theme.spacing(1),
}));

// API endpoint
const API_URL = "http://localhost:3100/api/chat";

export const Chat: React.FC<ChatProps> = ({
sessionId: initialSessionId = null,
}) => {
// Chat state
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sessionId, setSessionId] = useState<string | null>(initialSessionId);
const messagesEndRef = useRef<HTMLDivElement>(null);

// Load session data
const loadSession = async (): Promise<void> => {
try {
setIsLoading(true);
setError(null);

// In a real implementation, you would fetch the session data from the server
// For now, we'll just set a welcome message
setMessages([
{
content:
"Hi! I'm BioBuddy. I can help you with information about organisms, assemblies, workflows, and more. How can I assist you today?",
role: "assistant",
},
]);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to load session: ${errorMessage}`);
console.error("Error loading session:", err);
} finally {
setIsLoading(false);
}
};

// Scroll to bottom of messages
const scrollToBottom = (): void => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};

useEffect(() => {
scrollToBottom();
}, [messages]);

useEffect(() => {
if (sessionId) {
// TODO: Load session data from server
loadSession();
} else {
setMessages([
{
content:
"Hi! I'm BioBuddy. I can help you with information about organisms, assemblies, workflows, and more. How can I assist you today?",
role: "assistant",
},
]);
}
}, [sessionId]);

useEffect(() => {
if (sessionId !== initialSessionId && initialSessionId !== null) {
// TODO: Load session data from server
loadSession();
setSessionId(initialSessionId);
}
}, [initialSessionId, sessionId]);

// Send message to the backend
const sendMessage = async (): Promise<void> => {
if (!userInput.trim()) return;

const userMessage = userInput.trim();
setUserInput("");

// Add user message to chat
setMessages((prev) => [...prev, { content: userMessage, role: "user" }]);

try {
setIsLoading(true);
setError(null);

const response = await fetch(API_URL, {
body: JSON.stringify({
message: userMessage,
...(sessionId ? { session_id: sessionId } : {}),
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});

if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}

const data = await response.json();

// Store the session ID for future requests
if (data.sessionId) {
setSessionId(data.sessionId);
console.log(`Using session ID: ${data.sessionId}`);
}

// Add assistant response to chat
setMessages((prev) => [
...prev,
{ content: data.response, role: "assistant" },
]);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to send message: ${errorMessage}`);
console.error("Error sending message:", err);
} finally {
setIsLoading(false);
}
};

// Handle form submission
const handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
sendMessage();
};

// Handle Enter key press
const handleKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};

return (
<ChatContainer>
<ChatMessages>
{messages.map((message, index) => (
<MessageBubble key={index} isUser={message.role === "user"}>
<ReactMarkdown>{message.content}</ReactMarkdown>
</MessageBubble>
))}

{isLoading && (
<Box sx={{ display: "flex", justifyContent: "center", p: 2 }}>
<CircularProgress size={24} />
</Box>
)}

{error && <ErrorMessage variant="body2">{error}</ErrorMessage>}

<div ref={messagesEndRef} />
</ChatMessages>

<Box
component="form"
onSubmit={handleSubmit}
sx={{
alignItems: "flex-end",
backgroundColor: (theme) => theme.palette.background.paper,
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
display: "flex",
gap: (theme) => theme.spacing(1),
padding: (theme) => theme.spacing(2),
}}
>
<TextField
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message here..."
multiline
rows={3}
fullWidth
disabled={isLoading}
variant="outlined"
size="small"
/>

<Button
type="submit"
variant="contained"
color="primary"
disabled={isLoading || !userInput.trim()}
endIcon={<SendIcon />}
>
{isLoading ? "Sending..." : "Send"}
</Button>
</Box>
</ChatContainer>
);
};

export default Chat;
Loading
Loading