Skip to content

Commit

Permalink
Merge pull request #8 from fluentci-io/feat/ai-assistant
Browse files Browse the repository at this point in the history
feat: add ai assistant
  • Loading branch information
tsirysndr authored Aug 16, 2024
2 parents 108d16c + 8e3d7ac commit 99d741c
Show file tree
Hide file tree
Showing 24 changed files with 727 additions and 4 deletions.
Binary file modified webui/bun.lockb
Binary file not shown.
7 changes: 7 additions & 0 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@emotion/styled": "^11.11.0",
"@fontsource/inconsolata": "^5.0.16",
"@hookform/resolvers": "^3.3.4",
"@oramacloud/client": "^1.3.11",
"@styled-icons/bootstrap": "^10.47.0",
"@styled-icons/boxicons-regular": "^10.47.0",
"@styled-icons/boxicons-solid": "^10.47.0",
Expand All @@ -61,18 +62,24 @@
"dayjs": "^1.11.10",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.1.8",
"framer-motion": "^11.3.28",
"graphql": "15.7.2",
"nanoid": "^5.0.7",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-content-loader": "^7.0.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.4",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.22.0",
"react-syntax-highlighter": "npm:@fengkx/[email protected]",
"react-use-websocket": "^4.8.1",
"recoil": "^0.7.7",
"recoil-toolkit": "^0.3.0",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"rxjs": "^7.8.1",
"styled-components": "^6.1.12",
"styletron-engine-atomic": "^1.6.2",
"styletron-engine-monolithic": "^1.0.0",
"styletron-react": "^6.1.1",
Expand Down
57 changes: 57 additions & 0 deletions webui/src/Components/AskAI/AskAI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FC, useState } from "react";
import { Robot } from "@styled-icons/bootstrap";
import { Drawer } from "baseui/drawer";
import { Container } from "./styles";
import DrawerContent from "./DrawerContent";

const AskAI: FC = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Drawer
isOpen={isOpen}
autoFocus
onClose={() => setIsOpen(false)}
overrides={{
Root: {
style: {
zIndex: 2,
margin: 0,
},
},
DrawerContainer: {
style: {
backgroundColor: "#090119",
color: "#fff",
margin: "0 !important",
},
},
DrawerBody: {
style: {
color: "#fff",
margin: "0 !important",
paddingBottom: "0 !important",
fontFamily: 'Lexend !important',
},
},
Close: {
style: {
outline: "none",
},
},
}}
>
<DrawerContent />
</Drawer>
<Container
onClick={() => {
setIsOpen(true);
}}
>
<Robot size={25} style={{ marginTop: -4 }} />
</Container>
</>
);
};

export default AskAI;
8 changes: 8 additions & 0 deletions webui/src/Components/AskAI/AskAiWithData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FC } from "react";
import AskAI from "./AskAI";

const AskAIWithData: FC = () => {
return <AskAI />;
};

export default AskAIWithData;
23 changes: 23 additions & 0 deletions webui/src/Components/AskAI/ContentLoader/ContentLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FC } from "react";
import ContentLoader from "react-content-loader";

export type CodeProps = {
foregroundColor: string;
backgroundColor: string;
speed: number;
};

const Code: FC<CodeProps> = (props) => (
<ContentLoader viewBox="0 0 340 84" {...props}>
<rect x="0" y="0" width="67" height="11" rx="3" />
<rect x="76" y="0" width="140" height="11" rx="3" />
<rect x="127" y="48" width="53" height="11" rx="3" />
<rect x="187" y="48" width="72" height="11" rx="3" />
<rect x="18" y="48" width="100" height="11" rx="3" />
<rect x="0" y="71" width="37" height="11" rx="3" />
<rect x="18" y="23" width="140" height="11" rx="3" />
<rect x="166" y="23" width="173" height="11" rx="3" />
</ContentLoader>
);

export default Code;
3 changes: 3 additions & 0 deletions webui/src/Components/AskAI/ContentLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ContentLoader from "./ContentLoader";

export default ContentLoader;
225 changes: 225 additions & 0 deletions webui/src/Components/AskAI/DrawerContent/DrawerContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { FC, useRef, useState } from "react";
import { Robot, ArrowUpShort, StopFill } from "@styled-icons/bootstrap";
import {
Avatar,
Sample,
TextAreaWrapper,
Textarea,
SendButton,
Bubble,
BubbleWrapper,
Clear,
} from "./styles";
import { AnswerSession, Message, OramaClient } from "@oramacloud/client";
import { useRecoilState } from "recoil";
import { promptsState } from "../PromptsState";
import TypeWriterMarkdown from "../TypewriterMarkdown";

const orama = new OramaClient({
endpoint: "https://cloud.orama.run/v1/indexes/docs-fluentci-io-rr701q",
api_key: "1NBssCY5GlpVeFLDpglDHp6xLLS4g5vq",
});

const DrawerContent: FC = () => {
const [rows, setRows] = useState(1);
const [value, setValue] = useState("");
const [prompts, setPrompts] = useRecoilState(promptsState);
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
const [loadingResponse, setLoadingResponse] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_aborted, setAborted] = useState(false);
const [loading, setLoading] = useState(false);
const [session, setSession] = useState<AnswerSession | null>(null);
const chatEndRef = useRef<HTMLDivElement>(null);
const samples = [
"How to deploy to Cloudflare?",
"How does FluentCI work internally?",
"How to run MySQL and Redis as a background service in my CI Pipeline?",
"How do I create my own plugin?",
];

const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value);
const textAreaLineHeight = 64;
const previousRows = e.target.rows;
e.target.rows = rows;
const currentRows = ~~(e.target.scrollHeight / textAreaLineHeight);

if (currentRows === previousRows) {
e.target.rows = currentRows;
}

setRows(currentRows < 10 ? currentRows : 10);
};

const onClickPrompt = (query: string) => {
setValue("");
// eslint-disable-next-line @typescript-eslint/no-floating-promises
ask(query);
};

const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter") {
e.preventDefault();
if (e.shiftKey) {
setValue((value) => value + "\n");

if (rows < 10) {
setRows((rows) => rows + 1);
}

return;
}

const question = value.trim();
setValue("");
// eslint-disable-next-line @typescript-eslint/no-floating-promises
ask(question);
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
}
};

const handleSend = () => {
if (value.trim().length === 0) {
return;
}

const question = value.trim();
setValue("");
// eslint-disable-next-line @typescript-eslint/no-floating-promises
ask(question);
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
};

const ask = async (question: string) => {
setLoadingResponse(true);
const answerSession = orama.createAnswerSession({
inferenceType: "documentation",
// optional
initialMessages,
// optional
events: {
onMessageChange: (messages) => {
setInitialMessages(
messages.map((x) => ({
role: x.role,
content: x.content,
}))
);
},
onMessageLoading: (value) => setLoading(value),
onAnswerAborted: (value) => setAborted(value),
onStateChange: (state) => {
if (state[0].response.length > 1) {
setLoadingResponse(false);
}

setPrompts([
...prompts,
...[...state].map((s) => ({
query: question,
response: s.response,
interactionId: s.interactionId,
})),
]);
},
},
});
setSession(answerSession);
await answerSession.ask({ term: question });
};

return (
<>
<div
style={{
display: "flex",
flexDirection: "row",
padding: 23,
paddingBottom: 0,
}}
>
<Avatar>
<Robot size={30} style={{ marginTop: -4 }} />
</Avatar>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
flex: 1,
}}
>
<div style={{ fontWeight: 600, fontSize: 18 }}>FluentCI AI</div>
<div style={{ fontWeight: 600, color: "#02f3e6" }}>Assistant</div>
</div>
{prompts.length > 0 && (
<Clear onClick={() => setPrompts([])}>Clear</Clear>
)}
</div>
<div
style={{
height: "calc(100% - 167px)",
overflowY: "auto",
paddingLeft: 32,
paddingRight: 32,
}}
>
<div style={{ marginTop: 20, color: "#fff", marginBottom: 50 }}>
Hi!
<br />
I'm an AI assistant trained to help you with your CI/CD needs.
<br />
How can I help you?
</div>
<div>
<div style={{ color: "#cfe8fccf", marginBottom: 5 }}>
Try something like
</div>
{samples.map((sample) => (
<Sample key={sample} onClick={() => onClickPrompt(sample)}>
{sample}
</Sample>
))}
</div>
{prompts.map((prompt, index) => (
<div key={prompt.interactionId} style={{ marginBottom: "5rem" }}>
<BubbleWrapper>
<Bubble>{prompt.query}</Bubble>
</BubbleWrapper>
<div className="markdown-body markdown-dark">
<TypeWriterMarkdown
markdown={prompt.response}
chatEndRef={chatEndRef}
loading={loadingResponse && prompts.length === index + 1}
/>
</div>
</div>
))}
<div ref={chatEndRef} />
</div>
<TextAreaWrapper>
<Textarea
autoFocus={true}
rows={rows}
placeholder={"I want to ..."}
onChange={onChange}
onKeyDown={onKeyDown}
value={value}
/>
{!loading && (
<SendButton enabled={value.trim().length > 0} onClick={handleSend}>
<ArrowUpShort size={30} />
</SendButton>
)}
{loading && (
<SendButton enabled={true} onClick={() => session?.abortAnswer()}>
<StopFill size={30} />
</SendButton>
)}
</TextAreaWrapper>
</>
);
};

export default DrawerContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FC } from "react";
import DrawerContent from "./DrawerContent";

const DrawerContentWithData: FC = () => {
return <DrawerContent />;
};

export default DrawerContentWithData;
3 changes: 3 additions & 0 deletions webui/src/Components/AskAI/DrawerContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DrawerContent from "./DrawerContentWithData";

export default DrawerContent;
Loading

0 comments on commit 99d741c

Please sign in to comment.