diff --git a/package.json b/package.json index e997c70..81a03fa 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "ts-node": "^10.9.1", "jest-environment-jsdom": "^29.7.0", "rehype-sanitize": "6.0.0", - "react-markdown": "9.0.1" + "react-markdown": "9.0.1", + "pieces-copilot-sdk": "^1.1.9" }, "devDependencies": { "@testing-library/jest-dom": "^6.2.0", diff --git a/src/app/App.tsx b/src/app/App.tsx index ca97baa..a87f68e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -7,7 +7,6 @@ import {Header} from './components/Header/Header' import {CopilotChat} from './components/Copilot/Copilot' import {connect} from './utils/Connect' import { Indicator } from "./components/Indicator/Indicator"; -import CopilotStreamController from "./controllers/copilotStreamController"; import "./global.css"; import WorkflowActivityList from "./components/WorkflowActivity"; import { OSApi } from "@pieces.app/pieces-os-client"; @@ -80,8 +79,7 @@ export function App(): React.JSX.Element { useEffect(() => { refreshSnippetList(); - CopilotStreamController.getInstance(); -}, []); + }, []); const clearArray = () => { setArray([]) diff --git a/src/app/components/Copilot/Copilot.tsx b/src/app/components/Copilot/Copilot.tsx index 2fbac0b..5e3ea0e 100644 --- a/src/app/components/Copilot/Copilot.tsx +++ b/src/app/components/Copilot/Copilot.tsx @@ -1,55 +1,21 @@ import * as React from 'react' import {useEffect, useState} from 'react' -import * as Pieces from "@pieces.app/pieces-os-client"; -import {ConversationTypeEnum, SeededConversation} from "@pieces.app/pieces-os-client"; import "./Copilot.css"; - -import { applicationData } from "../../App"; -import CopilotStreamController from '../../controllers/copilotStreamController'; import Markdown from '../ResponseFormat/Markdown'; -import { config } from '../../../platform.config'; +import { piecesClient } from '../../../platform.config'; +// Replace 'your_base_url' with your actual base URL let GlobalConversationID: string; - // going to use get all conversations with a few extra steps to store the current conversations locally. -export function createNewConversation() { +export async function createNewConversation() { try { - - // logs --> CREATING CONVERSATION - console.log('Begin creating conversation...') - - // to create a new conversation, you need to first pass in a seeded conversation in the request body. - // the only mandatory parameter is the ConversationTypeEnum.Copilot value. - let seededConversation: SeededConversation = { type: ConversationTypeEnum.Copilot, name: "Demo Seeded Conversation" } - - console.log('Conversation seeded') - console.log('Passing over the new conversation with name: ' + seededConversation.name) - - // creates new conversation, .then is for confirmation on creation. - // note the usage of transfereables here to expose the full conversation data and give access to the id and other - // conversation values. - new Pieces.ConversationsApi(config).conversationsCreateSpecificConversationRaw({transferables: true, seededConversation}).then((_c) => { - console.log('Conversation created! : Here is the response:'); - console.log(_c); - - // check and ensure the response back is clean. - if (_c.raw.ok == true && _c.raw.status == 200) { - console.log('CLEAN RESPONSE BACK.') - _c.value().then(_conversation => { - console.log('Returning new conversation values.'); - // console.log('ID | ' + _conversation.id); - // console.log('NAME | ' + _conversation.name); - // console.log('CREATED | ' + _conversation.created.readable); - // console.log('ID: ' + _conversation.); - - // Set the conversation variable here for the local file: - GlobalConversationID = _conversation.id; - }) - } - }) + const newConversation = await piecesClient.createConversation({ + name: 'Hello World Conversation' + }); + GlobalConversationID = newConversation.conversation.id; } catch (error) { console.error('An error occurred while creating a conversation:', error); } @@ -77,9 +43,11 @@ export function createNewConversation() { // }) // } export function CopilotChat(): React.JSX.Element { - const [chatSelected, setChatSelected] = useState('-- no chat selected --'); + const [chatSelected, setChatSelected] = useState('no chat selected'); const [chatInputData, setData] = useState(''); const [message, setMessage] = useState(''); + const [conversations, setConversations] = useState([]); + // handles the data changes on the chat input. const handleCopilotChatInputChange = (event: { target: { value: React.SetStateAction; }; }) => { @@ -87,14 +55,19 @@ export function CopilotChat(): React.JSX.Element { }; // handles the ask button click. - const handleCopilotAskbuttonClick = async (chatInputData, setMessage)=>{ - CopilotStreamController.getInstance().askQGPT({ - query: chatInputData, - setMessage, - }); - setData(""); - } - + const handleCopilotAskButtonClick = async (chatInputData) => { + try { + const { text } = await piecesClient.promptConversation({ + message: chatInputData, + conversationId: GlobalConversationID + }); + setMessage(text); + setData(""); + } catch (error) { + console.error('An error occurred while prompting the conversation:', error); + } + }; + // handles the new conversation button click. const handleNewConversation = async () => { createNewConversation(); @@ -104,29 +77,54 @@ export function CopilotChat(): React.JSX.Element { // for setting the initial copilot chat that takes place on page load. useEffect(() => { - const getInitialChat = async () => { - let _name: string; - - await new Pieces.ConversationsApi(config) - .conversationsSnapshot({}) - .then((output) => { - if ( - output.iterable.length > 0 && - output.iterable.at(0).hasOwnProperty("name") - ) { - _name = output.iterable.at(0).name; - GlobalConversationID = output.iterable.at(0).id; + const getInitialChat = async () => { + try { + const allConversations = await piecesClient.getConversations(); + setConversations(allConversations); + // console.log('allConversations', allConversations); + if (allConversations.length > 0) { + const { id, name, messages } = allConversations[0]; + GlobalConversationID = id; + setChatSelected(name); + getConversationMessage(id); + } + } catch (error) { + console.error('Error fetching conversations:', error); } - return _name; - }); - - if (_name) { - setChatSelected(_name); - } - }; - getInitialChat(); + }; + getInitialChat(); }, []); + const getConversationMessage = async (selectedId) => { + try { + const {rawMessages} = await piecesClient.getConversation({ + conversationId: selectedId, + includeRawMessages: true, + }); + console.log("getMessages === ",rawMessages); + if (rawMessages.length>1) { + if(rawMessages[1].isUserMessage){ + setMessage(rawMessages[2].message) + } + else setMessage(rawMessages[1].message) + } + else setMessage("No previous conversation history, please ask the question below."); + } catch (error) { + console.error('Error fetching conversations:', error); + } + } + + const handleConversationChange = (event: React.ChangeEvent) => { + const selectedId = event.target.value; + const selectedConversation = conversations.find(convo => convo.id === selectedId); + if (selectedConversation) { + GlobalConversationID = selectedId; + setChatSelected(selectedConversation.name); + + getConversationMessage(selectedId); + } + }; + return (
@@ -134,16 +132,21 @@ export function CopilotChat(): React.JSX.Element {

Copilot Chat

-
- -

{chatSelected}

- +
+ Select your chat: +
- +
diff --git a/src/app/controllers/copilotStreamController.tsx b/src/app/controllers/copilotStreamController.tsx deleted file mode 100644 index 5175ac6..0000000 --- a/src/app/controllers/copilotStreamController.tsx +++ /dev/null @@ -1,201 +0,0 @@ - -import * as Pieces from "@pieces.app/pieces-os-client"; -import { BASE_URL, WS_URL } from "../../platform.config"; - -export type MessageOutput = { - answer: string; -}; - -/** - * Stream controller class for interacting with the QGPT websocket - */ -export default class CopilotStreamController { - private static instance: CopilotStreamController; - - private ws: WebSocket | null = null; // the qgpt websocket - - private setMessage: (message: string) => void; // the current answer element to be updated from socket events - - // this will resolve the current promise that is created by this.handleMessage - private messageResolver: null | ((arg0: MessageOutput) => void) = null; - - // this will reject the current promise that is created by this.handleMessage - private messageRejector: null | ((arg0: any) => void) = null; - - // this is resolved when the socket is ready. - private connectionPromise: Promise = new Promise((res) => res); - - //@TODO implement socket unloading - private constructor() { - this.connect(); - } - - /** - * cleanup function - */ - public closeSocket() { - this.ws?.close(); - } - - /** - * This is the entry point for all chat messages into this socket. - * @param param0 The inputted user query, any relevant snippets, and the answer element to be updated - * @returns a promise which is resolved when we get a 'COMPLETED' status from the socket, or rejected on a socket error. - */ - public async askQGPT({ - query, - setMessage - }: { - query: string; - setMessage: (message: string) => void; - }): Promise { - if (!this.ws) { - this.connect(); - } // need to connect the socket if it's not established. - await fetch(`${BASE_URL}/.well-known/health`).catch(() => { - // @TODO add error handling here - }); - - // @TODO add conversation id - const input: Pieces.QGPTStreamInput = { - question: { - query, - relevant: {iterable: []} //@TODO hook up /relevance here for context - }, - }; - - return this.handleMessages({ input, setMessage }); - } - - /** - * Connects the websocket, handles all message callbacks, error handling, and rendering. - */ - private connect() { - this.ws = new WebSocket(`${WS_URL}/qgpt/stream`); - - let totalMessage = ''; - let relevantSnippets: Pieces.RelevantQGPTSeed[] = []; - - this.ws.onmessage = (msg) => { - const json = JSON.parse(msg.data); - const result = Pieces.QGPTStreamOutputFromJSON(json); - let answer: Pieces.QGPTQuestionAnswer | undefined; - let relevant: Pieces.RelevantQGPTSeeds | undefined; - - // we got something from /relevance - if (result.relevance) { - relevant = result.relevance.relevant; - } else { - relevant = { iterable: [] }; - } - - // there is relevant snippets from the socket - if (relevant) { - for (const el of relevant.iterable) { - relevantSnippets.push(el); - } - } - // we got something from /question - if (result.question) { - answer = result.question.answers.iterable[0]; - } else { - // the message is complete, or we do nothing - if (result.status === 'COMPLETED') { - // add the buttons to the answer element's code blocks. - if (!totalMessage) { - this.setMessage("I'm sorry, it seems I don't have any relevant context to that question. Please try again 😃") - } - - // render the new total message - this.handleRender( - totalMessage, - ); - this.messageResolver!({ - answer: totalMessage, - }); - // cleanup - totalMessage = ''; - - } else if (result.status === 'FAILED' || result.status === 'UNKNOWN') { - if (this.messageRejector) this.messageRejector(result); - totalMessage = ''; - relevantSnippets = []; - } - return; - } - // add to the total message - if (answer?.text) { - totalMessage += answer.text; - } - // render the new total message - this.handleRender(totalMessage); - }; - - const refreshSockets = (error?: any) => { - if (error) console.error(error); - totalMessage = ''; - relevantSnippets = []; - if (this.messageRejector) this.messageRejector(error); - this.ws = null; - }; - // on error or close, reject the 'handleMessage' promise, and close the socket. - this.ws.onerror = refreshSockets; - this.ws.onclose = refreshSockets; - - this.connectionPromise = new Promise((res) => { - if (!this.ws) - throw new Error( - 'There is no websocket in Copilot Stream Controller (race condition)' - ); - this.ws.onopen = () => res(); - }); - } - - /** - * - * @param param0 the input into the websocket, and the answer element to be updated. - * @returns a promise that is resolved when the chat is complete, or rejected on an error. - */ - private async handleMessages({ - input, - setMessage, - }: { - input: Pieces.QGPTStreamInput; - setMessage: (message: string) => void; - }) { - if (!this.ws) this.connect(); - await this.connectionPromise; - this.setMessage = setMessage; - - // init message promise - const promise = new Promise((res, rej) => { - this.messageResolver = res; - this.messageRejector = rej; - }); - - try { - this.ws!.send(JSON.stringify(input)); - } catch (err) { - console.error('err', err); - this.messageRejector?.(err); - } - - return promise; - } - - /** - * This converts our raw markdown into HTML, then syntax highlights the pre > code blocks, then renders the result. - * @param totalMessage The total message to rendre - * @param answerEl the answer element to update - */ - private handleRender( - totalMessage: string, - ) { - // this is set up to only do one dom change, that way we prevent flickering in the case we want to do markdown parsing or syntax highlighting - this.setMessage?.(totalMessage); - } - - public static getInstance() { - return (CopilotStreamController.instance ??= new CopilotStreamController()); - } -} diff --git a/src/platform.config.ts b/src/platform.config.ts index ea7cc5a..8dabb84 100644 --- a/src/platform.config.ts +++ b/src/platform.config.ts @@ -1,17 +1,27 @@ -import * as Pieces from "@pieces.app/pieces-os-client"; +import * as Pieces from '@pieces.app/pieces-os-client'; +import { PiecesClient } from 'pieces-copilot-sdk'; let BASE_URL: string; let WS_URL: string; +let PORT: number; -if (Pieces.PlatformEnum.Linux) { - BASE_URL = 'http://localhost:5323'; - WS_URL = 'ws://localhost:5323'; +const platform = navigator.platform.toLowerCase(); + +if (platform.includes('win') || platform.includes('mac')) { + PORT = 1000; +} else if (platform.includes('linux')) { + PORT = 5323; } else { - BASE_URL = 'http://localhost:1000'; - WS_URL = 'ws://localhost:1000'; + console.log('PORT not set for : ', platform); } +BASE_URL = 'http://localhost:' + PORT; +WS_URL = 'ws://localhost:' + PORT; + const config = new Pieces.Configuration({ basePath: BASE_URL, }); -export { BASE_URL, WS_URL, config }; \ No newline at end of file + +const piecesClient = new PiecesClient({ baseUrl: BASE_URL }); + +export { BASE_URL, WS_URL, config, piecesClient };