-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from fluentci-io/feat/ai-assistant
feat: add ai assistant
- Loading branch information
Showing
24 changed files
with
727 additions
and
4 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
webui/src/Components/AskAI/ContentLoader/ContentLoader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
225
webui/src/Components/AskAI/DrawerContent/DrawerContent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
8 changes: 8 additions & 0 deletions
8
webui/src/Components/AskAI/DrawerContent/DrawerContentWithData.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import DrawerContent from "./DrawerContentWithData"; | ||
|
||
export default DrawerContent; |
Oops, something went wrong.