From 2abe5fa5238c930bbe9869b7450f57403945e39a Mon Sep 17 00:00:00 2001 From: Levi Perkins Date: Thu, 13 Apr 2023 09:08:58 -0400 Subject: [PATCH 1/4] feat(chat-interface) --- src/components/Message.tsx | 66 ++++++++ src/components/MessageInput.tsx | 40 +++++ src/components/TypingIndicator.tsx | 15 ++ src/pages/api/chatgpt.ts | 57 +++---- src/pages/assistiveintel.tsx | 251 ++++++++++++++--------------- src/styles/globals.css | 54 ++++++- 6 files changed, 310 insertions(+), 173 deletions(-) create mode 100644 src/components/Message.tsx create mode 100644 src/components/MessageInput.tsx create mode 100644 src/components/TypingIndicator.tsx diff --git a/src/components/Message.tsx b/src/components/Message.tsx new file mode 100644 index 0000000..66db58b --- /dev/null +++ b/src/components/Message.tsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; + +interface CodeBlockProps { + code: string; +} + +const CodeBlock: React.FC = ({ code }) => { + const [copyStatus, setCopyStatus] = useState<"idle" | "copied">("idle"); + + const handleCopyClick = () => { + navigator.clipboard.writeText(code).then(() => { + setCopyStatus("copied"); + + setTimeout(() => { + setCopyStatus("idle"); + }, 2000); + }); + }; + return ( +
+
+        {code}
+      
+ +
+ ); +}; + +interface MessageProps { + role: "user" | "system" | "assistant"; + content: string; +} + +const Message: React.FC = ({ role, content }) => { + const parts = content.split(/(```[\s\S]+?```)/g); + + return ( +
+
+ {parts.map((part, index) => { + if (part.startsWith("```")) { + const codeContent = part.slice(3, part.length - 3); + return ; + } else { + return {part}; + } + })} +
+
+ ); +}; + +export default Message; diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx new file mode 100644 index 0000000..eca7f8b --- /dev/null +++ b/src/components/MessageInput.tsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; + +interface MessageInputProps { + onSend: (message: string) => void; +} + +const MessageInput: React.FC = ({ onSend }) => { + const [input, setInput] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (input.trim() !== "") { + onSend(input); + setInput(""); + } + }; + + return ( +
+ setInput(e.target.value)} + className="w-full rounded-lg border px-4 py-2 focus:border-blue-300 focus:outline-none focus:ring" + placeholder="Type your message..." + /> + +
+ ); +}; + +export default MessageInput; diff --git a/src/components/TypingIndicator.tsx b/src/components/TypingIndicator.tsx new file mode 100644 index 0000000..cb9d6df --- /dev/null +++ b/src/components/TypingIndicator.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +const TypingIndicator = () => { + return ( +
+
+
+
+
+
+
+ ); +}; + +export default TypingIndicator; diff --git a/src/pages/api/chatgpt.ts b/src/pages/api/chatgpt.ts index 5507548..f56404b 100644 --- a/src/pages/api/chatgpt.ts +++ b/src/pages/api/chatgpt.ts @@ -1,46 +1,39 @@ -import { Configuration, OpenAIApi } from "openai"; -import {topics} from '../../constants/topic'; - +// Create a new OpenAI API client const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(configuration); -const constructPrompt = (topic: string, prompt='', language = 'javascript', level = 'junior', position = 'full-stack engineer') => { - switch(topic) { - case topics.EXPLAIN_CODE: - return `Explain this block of code: ${prompt}`; - case topics.FIX_CODE: - return `Is there anything wrong this block of code: ${prompt}`; - case topics.WRITE_CODE: - return `Write Some Code that does the following: ${prompt}`; - case topics.INTERVIEW_QUESTION: - return `Give me a list of interview questions for a ${level} ${position} developer.` - case topics.CODING_QUESTION: - return `Give me a list of ${level} coding questions in ${language}` - case topics.CODING_QUESTION_ANSWER: - return `Give me the answers to the following questions: ${prompt}` - default: - return prompt; - } +// Retrieve the user messages from the request body +const messages = req.body; + +if (!messages || !Array.isArray(messages)) { + res.status(400).json({ error: "Invalid request: messages should be an array" }); + return; } -export default async function chatGpt(req, res) { - const { prompt, language, level, position, topic } = req.body; - const content = constructPrompt(topic, prompt, language, level, position); + +// Format the user messages +const openaiMessages = messages.map((message) => ({ + role: message.role, + content: message.content, +})); + +// Call the OpenAI API to generate a response +try { const completion = await openai.createChatCompletion({ - messages: [ - { - role: "user", - content: content + '\n\n###\n\n', - } - ], + messages: openaiMessages, temperature: 0.7, - max_tokens: 256, + max_tokens: 1500, top_p: 1, frequency_penalty: 0, presence_penalty: 0, model: "gpt-3.5-turbo", -}); + }); + + // Send the response back to the client const response = completion?.data?.choices[0]?.message?.content; - res.status(200).json({ result: response }); + res.status(200).json({ role: "assistant", content: response }); +} catch (error) { + console.error(error); + res.status(500).json({ error: "An error occurred while processing the request" }); } \ No newline at end of file diff --git a/src/pages/assistiveintel.tsx b/src/pages/assistiveintel.tsx index 75f4262..df6284d 100644 --- a/src/pages/assistiveintel.tsx +++ b/src/pages/assistiveintel.tsx @@ -1,147 +1,132 @@ -import React, { useState } from 'react' -import { TailSpin } from 'react-loader-spinner' -import Answer from '../components/Answer' -import Interview from '../components/Interview' -import Prompt from '../components/Prompt' -import Tabs from '../components/Tabs' -import { topics } from '../constants/topic' +import React, { useState, useEffect, useRef } from "react"; +import Message from "../components/Message"; +import MessageInput from "../components/MessageInput"; +import TypingIndicator from "../components/TypingIndicator"; -interface AnswerProps { - question: string - answer: string -} +const systemPrompts = { + TECH_QUESTION: `Hello! I'm an AI developer assistant. Feel free to ask me any technical questions, and I'll do my best to help you out.`, + EXPLAIN_CODE: `As a helpful assistant, I can help you understand blocks of code. Please provide the code you'd like me to explain, and I'll walk you through it.`, + FIX_CODE: `I'm here to help you fix errors in your code. Please share the problematic code block, and I'll analyze it for any issues or improvements.`, + WRITE_CODE: `I can write code to help you solve a specific problem. Please describe the problem, and I'll provide you with a solution in code.`, + INTERVIEW_QUESTION: `Welcome to our virtual interview! I'm a professional developer who will be asking you questions. To get started, please tell describe the role you're interviewing for. (you can paste the job description here)`, +}; + +const buttonGroupClasses = { + first: + "relative inline-flex items-center rounded-l-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-indigo-500 focus:z-10", + middle: + "relative -ml-px inline-flex items-center bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-indigo-500 focus:z-10", + last: "relative -ml-px inline-flex items-center rounded-r-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-indigo-500 focus:z-10", +}; -const selects = [ - { - label: 'What Position', - value: 'position', - options: [ - { label: 'Frontend', value: 'frontend' }, - { label: 'Backend', value: 'backend' }, - { label: 'Fullstack', value: 'fullstack' }, - { label: 'React Dev', value: 'react developer' }, - { label: 'Vuejs', value: 'vue.js developer' }, - { label: 'Angular', value: 'angular developer' }, - { label: 'Nodejs', value: 'node.js developer' }, - { label: 'Python', value: 'python developer' }, - { label: 'Java', value: 'java developer' }, - ], - }, - { - label: 'Experience level', - value: 'experience', - options: [ - { label: 'Junior', value: 'junior' }, - { label: 'Mid', value: 'mid' }, - { label: 'Senior', value: 'senior' }, - ], - }, -] +interface IMessage { + role: "user" | "system" | "assistant"; + content: string; +} -export default function AssistiveIntel() { - const tabs = ['Tech Question', 'Fix my code', 'Write my code', 'Explain my code', 'Interview'] - const [question, setQuestion] = useState('') - const [loading, setLoading] = useState(false) - const [answer, setAnswer] = useState([]) - const [tab, setTab] = useState('Tech Question') - const [selectOptions, setSelectOptions] = useState<{ [key: string]: string }[]>([{position: 'frontend'}, {experience: 'junior'}]) +const App: React.FC = () => { + const [messages, setMessages] = useState>( + Object.keys(systemPrompts).reduce( + (acc, prompt) => ({ ...acc, [prompt]: [] }), + {} + ) + ); - const convertTabToTopic = (tab: string) => { - switch (tab) { - case 'Tech Question': - return topics.TECH_QUESTION - case 'Fix my code': - return topics.FIX_CODE - case 'Write my code': - return topics.WRITE_CODE - case 'Explain my code': - return topics.EXPLAIN_CODE - case 'Interview': - return topics.INTERVIEW_QUESTION - default: - return topics.TECH_QUESTION - } - } + const [loading, setLoading] = useState(false); + const [tab, setTab] = useState("TECH_QUESTION"); + const handleTabChange = (tab: string) => { + setTab(tab); + }; + const messagesEndRef = useRef(null); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + useEffect(() => { + scrollToBottom(); + }, [messages, loading]); - const askQuestion = async () => { - setLoading(true) - const response = await fetch('/api/chatgpt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ prompt: question, topic: convertTabToTopic(tab) }), - }) - const data = await response.json() - setAnswer([...answer, {question: question, answer: data.result}]) - setQuestion('') - setLoading(false) - } + const sendMessage = async (content: string) => { + // Create a new message object + const newMessage: IMessage = { role: "user", content }; + // Add the new message to the existing messages in state + setMessages((prevMessages) => ({ + ...prevMessages, + [tab]: [...prevMessages[tab], newMessage], + })); + // Set loading to true to display the loading indicator + setLoading(true); - const clearResponse = async () => { - setAnswer([]) - } + // Call the /api/chatgpt endpoint with the messages as the body + const response = await fetch("/api/chatgpt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { role: "system", content: systemPrompts[tab] }, + ...messages[tab], + newMessage, + ]), + }); + // Get the response from the /api/chatgpt endpoint + const data = await response.json(); - const changeTopic = (item: string) => { - setTab(item); - setAnswer([]); - } - const interviewRequest = async () => { - setLoading(true) - console.log('options', selectOptions) - const response = await fetch('/api/chatgpt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({topic: convertTabToTopic(tab), position: selectOptions[0]?.position, level: selectOptions[1]?.experience }), - }) - const data = await response.json() - setAnswer([...answer, {question: question, answer: data.result}]) - setLoading(false) - } + // Set loading to false to stop displaying the loading indicator + setLoading(false); + // Add the response from the /api/chatgpt endpoint to the messages in state + setMessages((prevMessages) => ({ + ...prevMessages, + [tab]: [...prevMessages[tab], { role: data.role, content: data.content }], + })); + }; return ( -
-
-
-
-

- Assistive Intelligence -

-

- Allow Assistive Intelligence to be your AI assistant. Select a category and ask away. -

-
-
- -
- { - tab === 'Interview' ? - : - - } +
+
+

+ Assistive Intelligence +

+

+ Your virtual coding assistant, without the salary or benefits. +

+
+ {/* // This code maps through each assistant in systemPrompts and returns a button element for each one +// The button element has a key value of the assistant name, a className value that toggles between the first, middle, and last button styles, and an onClick value that calls handleTabChange with the assistant name as an argument +// The button text is the assistant name with underscores replaced with spaces */} + + {Object.keys(systemPrompts).map((assistant, index) => ( + + ))}
-
- {loading && ( -
- -
+
+ + {messages[tab].map( + (msg, index) => + msg.role !== "system" && ( + + ) )} - + + {loading && } +
+
- ) -} + ); +}; + +export default App; diff --git a/src/styles/globals.css b/src/styles/globals.css index d9cd32b..5ebd4a6 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,28 +2,32 @@ @tailwind components; @tailwind utilities; - .shake { - animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; + animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; transform: translate3d(0, 0, 0); backface-visibility: hidden; perspective: 1000px; } @keyframes shake { - 10%, 90% { + 10%, + 90% { transform: translate3d(-1px, 0, 0); } - - 20%, 80% { + + 20%, + 80% { transform: translate3d(2px, 0, 0); } - 30%, 50%, 70% { + 30%, + 50%, + 70% { transform: translate3d(-4px, 0, 0); } - 40%, 60% { + 40%, + 60% { transform: translate3d(4px, 0, 0); } } @@ -39,4 +43,38 @@ to { transform: rotate(359deg); } -} \ No newline at end of file +} + +.typing-indicator { + display: flex; + justify-content: space-between; + align-items: center; + width: 36px; +} + +.typing-indicator-dot { + background-color: #4f46e5; + border-radius: 50%; + width: 8px; + height: 8px; + animation: typing-indicator-bounce 1.4s infinite ease-in-out; +} + +.typing-indicator-dot:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-indicator-dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing-indicator-bounce { + 0%, + 80%, + 100% { + transform: scaleY(0.4); + } + 40% { + transform: scaleY(1); + } +} From 560e105cdf80990e498a6dba2869f00051948aee Mon Sep 17 00:00:00 2001 From: Levi Perkins Date: Thu, 13 Apr 2023 09:14:51 -0400 Subject: [PATCH 2/4] feat(input-limit): stay below openai limit --- src/components/MessageInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index eca7f8b..4515c47 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -26,6 +26,7 @@ const MessageInput: React.FC = ({ onSend }) => { onChange={(e) => setInput(e.target.value)} className="w-full rounded-lg border px-4 py-2 focus:border-blue-300 focus:outline-none focus:ring" placeholder="Type your message..." + maxLength={2000} />