diff --git a/src/components/ai/JobSelector.tsx b/src/components/ai/JobSelector.tsx index a6dc57e..13ce50b 100644 --- a/src/components/ai/JobSelector.tsx +++ b/src/components/ai/JobSelector.tsx @@ -28,10 +28,6 @@ export const JobSelector = ({ const [popoverJob, setPopoverJob] = useState(null); const theme = useTheme(); - useEffect(() => { - fetchJobs(setJobs, { chat: true }); - }, []); - const handlePopoverOpen = ( event: React.MouseEvent, job: Job @@ -124,7 +120,9 @@ export const JobSelector = ({ fontStyle: "italic", }} > - {new Date(popoverJob.time_created).toLocaleString()} + {popoverJob.time_created + ? new Date(popoverJob.time_created).toLocaleString() + : "Unknown"} diff --git a/src/components/pages/chat/chat.tsx b/src/components/pages/chat/chat.tsx new file mode 100644 index 0000000..0d2e4a1 --- /dev/null +++ b/src/components/pages/chat/chat.tsx @@ -0,0 +1,351 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + Box, + TextField, + Typography, + Paper, + useTheme, + IconButton, + Tooltip, +} from "@mui/material"; +import { JobSelector } from "../../ai"; +import { Job, Message } from "../../../types"; +import { useSearchParams } from "next/navigation"; +import { checkAI, fetchJob, fetchJobs, updateJob } from "../../../lib"; +import SendIcon from "@mui/icons-material/Send"; +import EditNoteIcon from "@mui/icons-material/EditNote"; + +export const AI: React.FC = () => { + const theme = useTheme(); + const [currentMessage, setCurrentMessage] = useState(""); + const [selectedJob, setSelectedJob] = useState(null); + const [messages, setMessages] = useState([]); + const [aiEnabled, setAiEnabled] = useState(false); + const [jobs, setJobs] = useState([]); + const [thinking, setThinking] = useState(false); + + const searchParams = useSearchParams(); + + const getJobFromParam = async () => { + const jobId = searchParams.get("job"); + + if (jobId) { + const job = await fetchJob(jobId); + + if (job.length) { + setSelectedJob(job[0]); + if (job[0].chat) { + setMessages(job[0].chat); + } + } + } + }; + + useEffect(() => { + checkAI(setAiEnabled); + getJobFromParam(); + }, []); + + useEffect(() => { + if (selectedJob?.chat) { + setMessages(selectedJob?.chat); + return; + } + + setMessages([]); + }, [selectedJob]); + + const handleMessageSend = async (msg: string) => { + if (!selectedJob) { + throw Error("Job is not currently selected, but should be."); + } + + const updatedMessages = await sendMessage(msg); + await updateJob([selectedJob?.id], "chat", updatedMessages); + }; + + const sendMessage = async (msg: string) => { + const newMessage = { + content: msg, + role: "user", + }; + + setMessages((prevMessages) => [...prevMessages, newMessage]); + setCurrentMessage(""); + setThinking(true); + + const jobMessage = { + role: "system", + content: `Here is the content return from a scraping job: ${JSON.stringify( + selectedJob?.result + )} for the url: ${ + selectedJob?.url + }. The following messages will pertain to the content of the scraped job.`, + }; + + const response = await fetch("/api/ai", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: { messages: [jobMessage, ...messages, newMessage] }, + }), + }); + + const updatedMessages = [...messages, newMessage]; + + const reader = response.body?.getReader(); + const decoder = new TextDecoder("utf-8"); + + let aiResponse = ""; + if (reader) { + setThinking(false); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value, { stream: true }); + aiResponse += chunk; + + setMessages((prevMessages) => { + const lastMessage = prevMessages[prevMessages.length - 1]; + if (lastMessage && lastMessage.role === "assistant") { + return [ + ...prevMessages.slice(0, -1), + { ...lastMessage, content: aiResponse }, + ]; + } else { + return [ + ...prevMessages, + { + content: aiResponse, + role: "assistant", + }, + ]; + } + }); + } + } + return [...updatedMessages, { role: "assistant", content: aiResponse }]; + }; + + const handleNewChat = (selectedJob: Job) => { + updateJob([selectedJob.id], "chat", []); + setMessages([]); + }; + + useEffect(() => { + fetchJobs(setJobs); + }, []); + + return ( + + {aiEnabled ? ( + <> + + + + Chat with AI + + + + + + {!selectedJob ? ( + + + Select a Job to Begin Chatting + + + ) : ( + <> + {messages && + messages.map((message, index) => ( + + + {message.content} + + + ))} + {thinking && ( + + + AI is thinking... + + + )} + + )} + + + + 0)} + sx={{ marginRight: 2 }} + size="medium" + onClick={() => { + if (!selectedJob) { + throw new Error("Selected job must be present but isn't."); + } + handleNewChat(selectedJob); + }} + > + + + + setCurrentMessage(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleMessageSend(currentMessage); + } + }} + sx={{ borderRadius: "8px" }} + /> + + + { + handleMessageSend(currentMessage); + }} + > + + + + + + ) : ( + +

+ Must set either OPENAI_KEY or OLLAMA_MODEL to use AI features. +

+
+ )} +
+ ); +}; diff --git a/src/pages/chat.tsx b/src/pages/chat.tsx index b6b927f..ab2535e 100644 --- a/src/pages/chat.tsx +++ b/src/pages/chat.tsx @@ -1,348 +1 @@ -import React, { useEffect, useRef, useState } from "react"; -import { - Box, - TextField, - Typography, - Paper, - useTheme, - IconButton, - Tooltip, -} from "@mui/material"; -import { JobSelector } from "../components/ai"; -import { Job, Message } from "../types"; -import { useSearchParams } from "next/navigation"; -import { checkAI, fetchJob, fetchJobs, updateJob } from "../lib"; -import SendIcon from "@mui/icons-material/Send"; -import EditNoteIcon from "@mui/icons-material/EditNote"; - -const AI: React.FC = () => { - const theme = useTheme(); - const [currentMessage, setCurrentMessage] = useState(""); - const [selectedJob, setSelectedJob] = useState(null); - const [messages, setMessages] = useState([]); - const [aiEnabled, setAiEnabled] = useState(false); - const [jobs, setJobs] = useState([]); - const [thinking, setThinking] = useState(false); - - const searchParams = useSearchParams(); - - const getJobFromParam = async () => { - const jobId = searchParams.get("job"); - if (jobId) { - const job = await fetchJob(jobId); - if (job.length) { - setSelectedJob(job[0]); - if (job[0].chat) { - setMessages(job[0].chat); - } - } - } - }; - - useEffect(() => { - checkAI(setAiEnabled); - getJobFromParam(); - }, []); - - useEffect(() => { - if (selectedJob?.chat) { - setMessages(selectedJob?.chat); - return; - } - - setMessages([]); - }, [selectedJob]); - - const handleMessageSend = async (msg: string) => { - if (!selectedJob) { - throw Error("Job is not currently selected, but should be."); - } - - const updatedMessages = await sendMessage(msg); - await updateJob([selectedJob?.id], "chat", updatedMessages); - }; - - const sendMessage = async (msg: string) => { - const newMessage = { - content: msg, - role: "user", - }; - - setMessages((prevMessages) => [...prevMessages, newMessage]); - setCurrentMessage(""); - setThinking(true); - - const jobMessage = { - role: "system", - content: `Here is the content return from a scraping job: ${JSON.stringify( - selectedJob?.result - )} for the url: ${ - selectedJob?.url - }. The following messages will pertain to the content of the scraped job.`, - }; - - const response = await fetch("/api/ai", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - data: { messages: [jobMessage, ...messages, newMessage] }, - }), - }); - - const updatedMessages = [...messages, newMessage]; - - const reader = response.body?.getReader(); - const decoder = new TextDecoder("utf-8"); - - let aiResponse = ""; - if (reader) { - setThinking(false); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value, { stream: true }); - aiResponse += chunk; - - setMessages((prevMessages) => { - const lastMessage = prevMessages[prevMessages.length - 1]; - if (lastMessage && lastMessage.role === "assistant") { - return [ - ...prevMessages.slice(0, -1), - { ...lastMessage, content: aiResponse }, - ]; - } else { - return [ - ...prevMessages, - { - content: aiResponse, - role: "assistant", - }, - ]; - } - }); - } - } - return [...updatedMessages, { role: "assistant", content: aiResponse }]; - }; - - const handleNewChat = (selectedJob: Job) => { - updateJob([selectedJob.id], "chat", []); - setMessages([]); - fetchJobs(setJobs, { chat: true }); - }; - - return ( - - {aiEnabled ? ( - <> - - - - Chat with AI - - - - - - {!selectedJob ? ( - - - Select a Job to Begin Chatting - - - ) : ( - <> - {messages && - messages.map((message, index) => ( - - - {message.content} - - - ))} - {thinking && ( - - - AI is thinking... - - - )} - - )} - - - - 0)} - sx={{ marginRight: 2 }} - size="medium" - onClick={() => { - if (!selectedJob) { - throw new Error("Selected job must be present but isn't."); - } - handleNewChat(selectedJob); - }} - > - - - - setCurrentMessage(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleMessageSend(currentMessage); - } - }} - sx={{ borderRadius: "8px" }} - /> - - - { - handleMessageSend(currentMessage); - }} - > - - - - - - ) : ( - -

- Must set either OPENAI_KEY or OLLAMA_MODEL to use AI features. -

-
- )} -
- ); -}; - -export default AI; +export { AI as default } from "../components/pages/chat/chat";