Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding initial skeleton for ai chat box. #177

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion client/config/webpack.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const config: Configuration = {
{
test: /\.css$/,
include: [pathTo("../../node_modules/monaco-editor")],
use: ["style-loader", "css-loader"],
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.ttf$/,
Expand Down
12 changes: 12 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@
"@patternfly/react-core": "^5.2.1",
"@patternfly/react-table": "^5.2.1",
"@patternfly/react-tokens": "^5.2.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@segment/analytics-next": "^1.64.0",
"@tanstack/react-query": "^5.50.1",
"@tanstack/react-query-devtools": "^5.50.1",
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.7",
"ejs": "^3.1.10",
"file-saver": "^2.0.5",
"lucide-react": "^0.447.0",
"monaco-editor": "0.34.1",
"oidc-client-ts": "^2.4.0",
"packageurl-js": "^1.2.1",
Expand All @@ -48,6 +53,8 @@
"react-monaco-editor": "0.51.0",
"react-oidc-context": "^2.3.1",
"react-router-dom": "^6.21.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^2.14.0",
"web-vitals": "^0.2.4",
"xmllint": "^0.1.1",
Expand All @@ -57,6 +64,7 @@
"devDependencies": {
"@hey-api/openapi-ts": "^0.53.5",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.4.3",
Expand All @@ -65,6 +73,7 @@
"@types/file-saver": "^2.0.2",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.4.20",
"browserslist": "^4.19.1",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"copy-webpack-plugin": "^11.0.0",
Expand All @@ -78,13 +87,16 @@
"mini-css-extract-plugin": "^2.5.2",
"monaco-editor-webpack-plugin": "^7.0.1",
"msw": "^1.2.3",
"postcss": "^8.4.47",
"postcss-loader": "^8.1.1",
"raw-loader": "^4.0.2",
"react-refresh": "^0.14.0",
"react-refresh-typescript": "^2.0.9",
"sass-loader": "^12.4.0",
"source-map-explorer": "^2.5.2",
"style-loader": "^3.3.1",
"svg-url-loader": "^7.1.1",
"tailwindcss": "^3.4.13",
"terser-webpack-plugin": "^5.3.0",
"ts-loader": "^9.4.1",
"tsconfig-paths-webpack-plugin": "^4.0.0",
Expand Down
7 changes: 7 additions & 0 deletions client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-env node */
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
2 changes: 1 addition & 1 deletion client/src/app/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@

table.vertical-middle-aligned-table tr td {
vertical-align: middle;
}
}
10 changes: 7 additions & 3 deletions client/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./App.css";
import "./tailwind.css";
import React from "react";
import { BrowserRouter as Router } from "react-router-dom";

Expand All @@ -9,15 +10,18 @@ import { AnalyticsProvider } from "./components/AnalyticsProvider";

import "@patternfly/patternfly/patternfly.css";
import "@patternfly/patternfly/patternfly-addons.css";
import { AIAssistantProvider } from "./components/ai-assistant";

const App: React.FC = () => {
return (
<Router>
<AnalyticsProvider>
<NotificationsProvider>
<DefaultLayout>
<AppRoutes />
</DefaultLayout>
<AIAssistantProvider>
<DefaultLayout>
<AppRoutes />
</DefaultLayout>
</AIAssistantProvider>
</NotificationsProvider>
</AnalyticsProvider>
</Router>
Expand Down
243 changes: 243 additions & 0 deletions client/src/app/components/ai-assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { MessageCircle, Send, X } from "lucide-react";
import { ChatMessage, ChatState } from "../client";
import { useCompletionMutation, useFetchAiFlagsQuery } from "../queries/ai";
import ReactMarkdown from "react-markdown";

interface Conversation {
isEnabled: boolean;
isOpen: boolean;
setIsOpen: (value: boolean) => void;
chatState: ChatState;
setChatState: (value: ChatState) => void;
}

const ConversationContext = React.createContext<Conversation>({
isEnabled: true,
isOpen: false,
setIsOpen: () => {},
chatState: {} as ChatState,
setChatState: () => {},
});

export function AIAssistantProvider({
children,
}: {
children: React.ReactNode;
}) {
const aiFlags = useFetchAiFlagsQuery();
const isEnabled = aiFlags.data?.completions || false;

const [isOpen, setIsOpen] = useState(false);
const [chatState, setChatState] = useState({ messages: [] } as ChatState);

return (
<ConversationContext.Provider
value={{
isEnabled,
isOpen,
setIsOpen,
chatState,
setChatState,
}}
>
{children}
</ConversationContext.Provider>
);
}

const VIEWING_MESSAGE_PREFIX = "--> I'm currently viewing ";

export function AIAssistant({ viewing }: { viewing?: string }) {
const { isEnabled, isOpen, setIsOpen, chatState, setChatState } =
useContext(ConversationContext);
const [input, setInput] = useState("");
const completionMutation = useCompletionMutation();

const handleSubmit = async () => {
const messages = [...chatState.messages];

// Push extra context of what the user is currently viewing into the chat history...
if (viewing) {
messages.push({
message_type: "human",
content: VIEWING_MESSAGE_PREFIX + viewing,
});
}
const userMessage = {
message_type: "human",
content: input,
} as ChatMessage;
messages.push(userMessage);

setChatState({
messages: [...chatState.messages, userMessage],
});
setInput("");

if (completionMutation) {
const newState = await completionMutation.mutateAsync({ messages });
const newMessageState = newState.messages.filter(
(m: ChatMessage) =>
!(
m.message_type == "human" &&
m.content.startsWith(VIEWING_MESSAGE_PREFIX)
)
);
setChatState({
messages: newMessageState,
});
}
};

const messages = useMemo(() => {
return chatState.messages.filter(
(m) =>
(m.message_type == "human" || m.message_type == "ai") &&
m.content !== ""
);
}, [chatState]);

const handleClose = () => {
setIsOpen(false);
setChatState({
messages: [],
});
};

const scrollAreaRef = useRef(null as HTMLDivElement | null);
const scrollToBottom = () => {
setTimeout(() => {
if (scrollAreaRef?.current) {
const scrollArea = scrollAreaRef.current;
scrollArea.scrollTop = scrollArea.scrollHeight;
}
}, 0);
};

useEffect(() => {
scrollToBottom();
}, [completionMutation.isPending]);

if (!isEnabled) {
return null;
}
return (
<>
{/* Floating button */}
{!isOpen && (
<Button
className="fixed bottom-20 right-4 rounded-full p-4 z-10"
onClick={() => setIsOpen(true)}
>
<MessageCircle className="h-6 w-6" />
<span className="sr-only">Open AI Assistant</span>
</Button>
)}

{/* Chat box */}
{isOpen && (
<Card className="fixed bottom-20 right-4 w-[25rem] h-[40rem] flex flex-col z-10 bg-white">
<CardHeader className="flex flex-row items-center">
<CardTitle className="flex-1">AI Assistant</CardTitle>
<Button
className="relative -top-6 -right-4"
variant="ghost"
size="icon"
onClick={handleClose}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</CardHeader>
<CardContent className="flex-grow overflow-hidden">
<ScrollArea className="h-full w-full pr-4" ref={scrollAreaRef}>
<div className={`mb-4 text-left`}>
<span className={`inline-block p-2 rounded-lg bg-muted`}>
Hello there! How can I help you today?
</span>
</div>
{messages.map((m, index) => (
<div
key={index}
className={`mb-4 ${
m.message_type === "ai" ? "text-left" : "text-right"
}`}
>
<span
className={`inline-block p-2 rounded-lg max-w-[90%] prose prose-sm ${
m.message_type === "ai"
? "bg-muted"
: "bg-primary text-primary-foreground"
}`}
>
<ReactMarkdown>{m.content}</ReactMarkdown>
</span>
</div>
))}

{completionMutation.isPending && (
<div className="mb-4 text-left">
<span className="inline-block p-2 rounded-lg bg-muted">
<LoadingBubbles />
</span>
</div>
)}
</ScrollArea>
</CardContent>
<CardFooter>
<div className="flex w-full items-center space-x-2">
<Input
value={input}
onKeyDown={(event) => {
if (event.key === "Enter") {
// Perform the action you want to handle when Enter is pressed
console.log("Enter key pressed!");
handleSubmit();
}
}}
onChange={(e) => {
setInput(e.target.value);
}}
placeholder="Type your message..."
/>
<Button type="submit" size="icon" onMouseDown={handleSubmit}>
<Send className="h-4 w-4" />
<span className="sr-only">Send</span>
</Button>
</div>
</CardFooter>
</Card>
)}
</>
);
}

function LoadingBubbles() {
return (
<div className="flex space-x-2">
<div
className="w-2 h-2 rounded-full bg-gray-500 animate-bounce"
style={{ animationDelay: "0ms" }}
></div>
<div
className="w-2 h-2 rounded-full bg-gray-500 animate-bounce"
style={{ animationDelay: "150ms" }}
></div>
<div
className="w-2 h-2 rounded-full bg-gray-500 animate-bounce"
style={{ animationDelay: "300ms" }}
></div>
</div>
);
}
8 changes: 7 additions & 1 deletion client/src/app/pages/home/home.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from "react";
import { AIAssistant } from "../../components/ai-assistant";

export const Home: React.FC = () => {
return <>Dashboard here</>;
return (
<>
<AIAssistant />
<div>Dashboard here</div>
</>
);
};
3 changes: 3 additions & 0 deletions client/src/app/pages/importer-list/importer-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { useLocalTableControls } from "@app/hooks/table-controls";

import { ImporterForm } from "./components/importer-form";
import { ImporterStatusIcon } from "./components/importer-status-icon";
import { AIAssistant } from "../../components/ai-assistant";

export const ImporterList: React.FC = () => {
const { pushNotification } = React.useContext(NotificationsContext);
Expand Down Expand Up @@ -277,6 +278,8 @@ export const ImporterList: React.FC = () => {

return (
<>
<AIAssistant viewing={`a list of importers`} />

<PageSection variant={PageSectionVariants.light}>
<TextContent>
<Text component="h1">Importers</Text>
Expand Down
Loading
Loading