diff --git a/app/package.json b/app/package.json index 2c1b7c27..53c11b5a 100644 --- a/app/package.json +++ b/app/package.json @@ -3,20 +3,23 @@ "version": "1.0.0", "description": "Sample Client app showing communication with OCI Generative AI services via Websocket", "dependencies": { - "@oracle/oraclejet": "~16.0.0", - "@oracle/oraclejet-core-pack": "~16.0.0", + "@oracle/oraclejet": "~16.1.0", + "@oracle/oraclejet-core-pack": "~16.1.0", + "@stomp/stompjs": "^7.0.0", "marked": "^4.3.0", "uuid": "^9.0.1" }, "devDependencies": { - "@oracle/ojet-cli": "~16.0.0", - "@oracle/oraclejet-audit": "^16.0.0", + "@oracle/ojet-cli": "~16.1.0", + "@oracle/oraclejet-audit": "^16.1.0", "@types/uuid": "^9.0.7", + "express-http-proxy": "^2.0.0", "extract-zip": "^1.7.0", "fs-extra": "^8.1.0", "glob": "7.2.0", "typescript": "5.3.2", "underscore": "^1.10.2", + "url": "^0.11.3", "yargs-parser": "13.1.2" }, "engines": { diff --git a/app/path_mapping.json b/app/path_mapping.json index 128fddc3..609e43a6 100644 --- a/app/path_mapping.json +++ b/app/path_mapping.json @@ -380,6 +380,17 @@ "path": "libs/chai/chai.js", "cdnPath": "chai/chai-4.3.10.min" } + }, + "stompjs": { + "cwd": "node_modules/@stomp/stompjs/bundles", + "debug": { + "src": "stomp.umd.js", + "path": "libs/stompjs/stomp.umd.js" + }, + "release": { + "src": "stomp.umd.min.js", + "path": "libs/stompjs/stomp.umd.min.js" + } } } } diff --git a/app/scripts/hooks/before_serve.js b/app/scripts/hooks/before_serve.js index 40a9ea84..ba495a0e 100644 --- a/app/scripts/hooks/before_serve.js +++ b/app/scripts/hooks/before_serve.js @@ -5,19 +5,28 @@ */ -'use strict'; +"use strict"; module.exports = function (configObj) { return new Promise((resolve, reject) => { - console.log('Running before_serve hook.'); + console.log("Running before_serve hook."); // ojet custom connect and serve options - // { connectOpts, serveOpts } = configObj; - // const express = require('express'); - // const http = require('http'); + const { connectOpts, serveOpts } = configObj; + const express = require("express"); + const http = require("http"); + const proxy = require("express-http-proxy"); + const url = require("url"); + + // New hostname+path as specified by question: + const apiProxy = proxy("http://localhost:8080", { + proxyReqPathResolver: (req) => url.parse("/api" + req.url).path, + }); + const app = express(); + app.use("/api", apiProxy); // pass back custom http - // configObj['http'] = http; + configObj["http"] = http; // pass back custom express app - // configObj['express'] = express(); + configObj["express"] = app; // pass back custom options for http.createServer // const serverOptions = {...}; // configObj['serverOptions'] = serverOptions; diff --git a/app/src/components/app.tsx b/app/src/components/app.tsx index cf8f46e5..1a9ffa6c 100644 --- a/app/src/components/app.tsx +++ b/app/src/components/app.tsx @@ -1,19 +1,24 @@ import { Header } from "./header"; import Content from "./content/index"; import { registerCustomElement } from "ojs/ojvcomponent"; -import "preact"; +import { createContext } from "preact"; type Props = { appName: string; }; +const convoUUID = window.crypto.randomUUID(); +export const ConvoCtx = createContext(convoUUID); export const App = registerCustomElement("app-root", (props: Props) => { props.appName = "Generative AI JET UI"; return (
-
- + + {console.log("UUID: ", convoUUID)} +
+ +
); }); diff --git a/app/src/components/content/index.tsx b/app/src/components/content/index.tsx index 64814312..84df4f33 100644 --- a/app/src/components/content/index.tsx +++ b/app/src/components/content/index.tsx @@ -9,28 +9,44 @@ import "oj-c/drawer-popup"; import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider"); import { MessageToastItem } from "oj-c/message-toast"; import { InputSearchElement } from "ojs/ojinputsearch"; -import { useState, useEffect, useRef } from "preact/hooks"; +import { useState, useEffect, useRef, useContext } from "preact/hooks"; import * as Questions from "text!./data/questions.json"; import * as Answers from "text!./data/answers.json"; +import { initWebSocket } from "./websocket-interface"; +import { InitStomp, sendPrompt } from "./stomp-interface"; +import { Client } from "@stomp/stompjs"; +import { ConvoCtx } from "../app"; type ServiceTypes = "text" | "summary" | "sim"; +type BackendTypes = "java" | "python"; type Chat = { id?: number; question?: string; answer?: string; loading?: string; }; + +const defaultServiceType: string = localStorage.getItem("service") || "text"; +const defaultBackendType: string = localStorage.getItem("backend") || "java"; + const Content = () => { + const conversationId = useContext(ConvoCtx); const [update, setUpdate] = useState>([]); const [busy, setBusy] = useState(false); - const [summaryResults, setSummaryResults] = useState(""); + const [summaryResults, setSummaryResults] = useState(""); + const [modelId, setModelId] = useState(null); const [summaryPrompt, setSummaryPrompt] = useState(); - const [serviceType, setServiceType] = useState("summary"); + const [serviceType, setServiceType] = useState( + defaultServiceType as ServiceTypes + ); + const [backendType, setBackendType] = useState( + defaultBackendType as BackendTypes + ); const [settingsOpened, setSettingsOpened] = useState(false); const question = useRef(); const chatData = useRef>([]); const socket = useRef(); - const [connState, setConnState] = useState("Disconnected"); + const [client, setClient] = useState(null); const messagesDP = useRef( new MutableArrayDataProvider( @@ -39,77 +55,6 @@ const Content = () => { ) ); - const gateway = `ws://${window.location.hostname}:1986`; - let sockTimer: any = null; - - // setup the websocket connection - const initWebSocket = () => { - console.log("Trying to open a WebSocket connection..."); - socket.current = new WebSocket(gateway); - socket.current.binaryType = "arraybuffer"; - socket.current.onopen = onOpen; - socket.current.onerror = onError; - socket.current.onclose = onClose; - socket.current.onmessage = onMessage; - }; - - // handle all messages coming from the websocket service - const onMessage = (event: any) => { - const msg = JSON.parse(event.data); - - switch (msg.msgType) { - // switch (Object.keys(msg)[0]) { - case "message": - console.log("message: ", msg.data); - return msg.data; - case "question": - console.log("question: ", msg.data); - return msg.data; - case "summary": - console.log("summary"); - setSummaryResults(msg.data); - return; - case "answer": - console.log("answer: ", msg.data); - if (msg.data !== "connected") { - let tempArray = [...chatData.current]; - // remove the animation item before adding answer - setBusy(false); - tempArray.pop(); - messagesDP.current.data = []; - tempArray.push({ - id: tempArray.length as number, - answer: msg.data, - }); - chatData.current = tempArray; - setUpdate(chatData.current); - } - return msg.data; - default: - return "unknown"; - } - }; - - const onOpen = () => { - clearInterval(sockTimer); - console.log("Connection opened"); - socket.current?.send( - JSON.stringify({ msgType: "message", data: "connected" }) - ); - setConnState("Connected"); - }; - - // if the connection is lost, wait one minute and try again. - const onError = () => { - sockTimer = setInterval(initWebSocket, 1000 * 60); - }; - function onClose() { - console.log("Connection closed"); - setConnState("Disconnected"); - socket.current ? (socket.current.onclose = () => {}) : null; - socket.current?.close(); - } - // Simulation code const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const runSimulation = async () => { @@ -121,7 +66,6 @@ const Content = () => { if (Q) { if (x > 0) tempArray.pop(); tempArray.push({ question: JSON.parse(Questions)[x] }); - // tempArray.push({ loading: "loading" }); Q = false; x++; } else { @@ -135,24 +79,52 @@ const Content = () => { await sleep(2000); } }; + useEffect(() => { switch (serviceType) { case "text": - initWebSocket(); - console.log("Running Gen AI"); + if (backendType === "python") { + initWebSocket( + setSummaryResults, + setBusy, + setUpdate, + messagesDP, + socket, + chatData + ); + } else { + setClient( + InitStomp(setBusy, setUpdate, messagesDP, chatData, serviceType) + ); + } + console.log("Running Generative service"); return; case "sim": runSimulation(); - console.log("running simulation"); + console.log("Running simulation"); return; case "summary": - initWebSocket(); - console.log("summary loading"); + if (backendType === "python") { + initWebSocket( + setSummaryResults, + setBusy, + setUpdate, + messagesDP, + socket, + chatData + ); + } else { + setClient( + InitStomp(setBusy, setUpdate, messagesDP, chatData, serviceType) + ); + } + console.log("Running Summarization service"); return; } return () => { socket.current ? (socket.current.onclose = () => {}) : null; socket.current?.close(); + client?.deactivate(); }; }, [serviceType]); @@ -168,7 +140,6 @@ const Content = () => { autoTimeout: "on", }, ]; - //alert("Still waiting for an answer! Hang in there a little longer."); return; } if (event.detail.value) { @@ -191,12 +162,13 @@ const Content = () => { setUpdate(chatData.current); setBusy(true); - // simulating the delay for now just to show what the animation looks like. - setTimeout(() => { + if (backendType === "python") { socket.current?.send( JSON.stringify({ msgType: "question", data: question.current }) ); - }, 300); + } else { + sendPrompt(client, question.current!, modelId!, conversationId!); + } } }; @@ -215,12 +187,22 @@ const Content = () => { }; const serviceTypeChangeHandler = (service: ServiceTypes) => { + localStorage.setItem("service", service); setUpdate([]); chatData.current = []; setServiceType(service); - toggleDrawer(); }; - + const backendTypeChangeHandler = (backend: BackendTypes) => { + setUpdate([]); + chatData.current = []; + setBackendType(backend); + localStorage.setItem("backend", backend); + location.reload(); + }; + const modelIdChangeHandler = (event: CustomEvent) => { + console.log("model Id: ", event.detail.value); + if (event.detail.value != null) setModelId(event.detail.value); + }; const clearSummary = () => { setSummaryResults(""); }; @@ -228,6 +210,9 @@ const Content = () => { const updateSummaryPrompt = (val: string) => { setSummaryPrompt(val); }; + const updateSummaryResults = (summary: string) => { + setSummaryResults(summary); + }; return (
@@ -238,8 +223,11 @@ const Content = () => { aria-label="Settings Drawer" >
@@ -248,9 +236,7 @@ const Content = () => { position="top" onojClose={handleToastClose} > - {/*

*/}
- {/*
{connState}
*/} @@ -273,9 +259,11 @@ const Content = () => { {serviceType === "summary" && ( )}
diff --git a/app/src/components/content/settings.tsx b/app/src/components/content/settings.tsx index 681c51aa..3863b301 100644 --- a/app/src/components/content/settings.tsx +++ b/app/src/components/content/settings.tsx @@ -1,48 +1,112 @@ -import "preact"; -import { useState } from "preact/hooks"; +import { ComponentProps } from "preact"; +import { useEffect, useRef } from "preact/hooks"; import "oj-c/radioset"; import "oj-c/form-layout"; +import "oj-c/select-single"; import { CRadiosetElement } from "oj-c/radioset"; import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider"); type ServiceTypeVal = "text" | "summary" | "sim"; +type BackendTypeVal = "java" | "python"; type Services = { label: string; value: ServiceTypeVal; }; type Props = { - serviceType: "text" | "summary" | "sim"; - serviceChange: (service: ServiceTypeVal) => void; + aiServiceType: ServiceTypeVal; + backendType: BackendTypeVal; + aiServiceChange: (service: ServiceTypeVal) => void; + backendChange: (backend: BackendTypeVal) => void; + modelIdChange: (modelName: any) => void; }; const serviceTypes = [ { value: "text", label: "Generative Text" }, { value: "summary", label: "Summarize" }, - { value: "sim", label: "Simulation" }, +]; +// { value: "sim", label: "Simulation" }, + +const backendTypes = [ + { value: "java", label: "Java" }, + { value: "python", label: "Python" }, ]; const serviceOptionsDP = new MutableArrayDataProvider< Services["value"], Services >(serviceTypes, { keyAttributes: "value" }); +const backendOptionsDP = new MutableArrayDataProvider< + Services["value"], + Services +>(backendTypes, { keyAttributes: "value" }); export const Settings = (props: Props) => { const handleServiceTypeChange = (event: any) => { if (event.detail.updatedFrom === "internal") - props.serviceChange(event.detail.value); + props.aiServiceChange(event.detail.value); + }; + const handleBackendTypeChange = (event: any) => { + if (event.detail.updatedFrom === "internal") + props.backendChange(event.detail.value); + }; + + const modelDP = useRef( + new MutableArrayDataProvider([], { keyAttributes: "id" }) + ); + + const fetchModels = async () => { + try { + const response = await fetch("/api/genai/models"); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + const json = await response.json(); + modelDP.current.data = json; + } catch (error: any) { + console.log( + "Java service not available for fetching list of Models: ", + error.message + ); + } }; + useEffect(() => { + fetchModels(); + }, []); return (
-

Service Settings

+

AI service types

+

Backend service types

+ + + + {props.aiServiceType == "text" && props.backendType == "java" && ( + <> +

Model options

+ + + + + )}
); }; diff --git a/app/src/components/content/stomp-interface.tsx b/app/src/components/content/stomp-interface.tsx new file mode 100644 index 00000000..cd77256a --- /dev/null +++ b/app/src/components/content/stomp-interface.tsx @@ -0,0 +1,87 @@ +import "preact"; +import { Client } from "stompjs"; + +// setup the Stompjs connection +export const InitStomp = ( + setBusy: any, + setUpdate: any, + messagesDP: any, + chatData: any, + serviceType: any +) => { + //const [test, setTest] = useState(); + const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"; + const hostname = + window.location.hostname === "localhost" + ? "localhost:8080" + : window.location.hostname; + const serviceURL = `${protocol}${hostname}/websocket`; + console.log("in the stomp init module"); + const client = new Client({ + brokerURL: serviceURL, + onConnect: () => { + if (serviceType === "text") { + client.subscribe("/user/queue/answer", (message: any) => { + console.log("Answer message: ", JSON.parse(message.body).content); + onMessage(message); + }); + } else if (serviceType === "summary") { + client.subscribe("/user/queue/summary", (message: any) => { + console.log("Summary message: ", JSON.parse(message.body).content); + onMessage(message); + }); + } + }, + onStompError: (e) => { + console.log("Stomp Error: ", e); + }, + onWebSocketError: () => { + console.log("Error connecting to Websocket service"); + serviceType === "text" + ? client.unsubscribe("/user/queue/answer") + : client.unsubscribe("/user/queue/summary"); + client.deactivate(); + }, + }); + client.activate(); + + const onMessage = (msg: any) => { + let aiAnswer = JSON.parse(msg.body).content; + //console.log("answer: ", aiAnswer); + if (msg.data !== "connected") { + let tempArray = [...chatData.current]; + // remove the animation item before adding answer + setBusy(false); + tempArray.pop(); + messagesDP.current.data = []; + tempArray.push({ + id: tempArray.length as number, + answer: aiAnswer, + }); + chatData.current = tempArray; + setUpdate(chatData.current); + } + }; + return client; +}; + +export const sendPrompt = ( + client: Client | null, + prompt: string, + modelId: string, + convoId: string +) => { + if (client?.connected) { + console.log("Sending prompt: ", prompt); + client.publish({ + destination: "/genai/prompt", + body: JSON.stringify({ + conversationId: convoId, + content: prompt, + modelId: modelId, + }), + }); + } else { + console.log("Error, no Stomp connection"); + } +}; diff --git a/app/src/components/content/summary.tsx b/app/src/components/content/summary.tsx index 275c50a4..960bd67e 100644 --- a/app/src/components/content/summary.tsx +++ b/app/src/components/content/summary.tsx @@ -23,18 +23,35 @@ declare global { } type Props = { fileChanged: (file: ArrayBuffer) => void; - summary: string | null; clear: () => void; prompt: (val: string) => void; + summaryChanged: (summary: string) => void; + summary: string; + backendType: any; }; - +const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"; +const hostname = + window.location.hostname === "localhost" + ? "localhost:8080" + : window.location.hostname; +const serviceRootURL = `${protocol}${hostname}`; const acceptArr: string[] = ["application/pdf", "*.pdf"]; const messages: { id: number; severity: string; summary: string }[] = []; +const FILE_SIZE = 120000; -export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { +export const Summary = ({ + fileChanged, + clear, + prompt, + summaryChanged, + summary, + backendType, +}: Props) => { const [invalidMessage, setInvalidMessage] = useState(null); const [summaryPrompt, setSummaryPrompt] = useState(""); + const [summaryResults, setSummaryResults] = useState(summary); const [fileNames, setFileNames] = useState(null); + const [file, setFile] = useState(null); const [messages, setMessages] = useState([]); const [pdfFile, setPDFFile] = useState(); const [loading, setLoading] = useState(false); @@ -62,6 +79,29 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { prompt(tempStr); }; + const sendToJavaBackend = async (file: File, prompt: string) => { + const formData = new FormData(); + formData.append("file", file); + + const res = await fetch("/api/upload", { + // const res = await fetch("http://localhost:5173/api/upload", { + method: "POST", + mode: "cors", + referrerPolicy: "strict-origin-when-cross-origin", + body: formData, + }); + console.log("Response: ", res); + const responseData = await res.json(); + const { content, errorMessage } = responseData; + if (errorMessage.length) { + // setErrorMessage(errorMessage); + // setShowError(true); + } else { + console.log("Response: ", content); + setSummaryResults(content); + summaryChanged(content); + } + }; // FilePicker related methods const selectListener = async (event: CFilePickerElement.ojSelect) => { setInvalidMessage(""); @@ -71,14 +111,19 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { return file?.name; }); - const fr = new FileReader(); - let ab = new ArrayBuffer(200000000); - fr.onload = (ev: ProgressEvent) => { - let ab = fr.result; - setPDFFile(ab as ArrayBuffer); - }; - fr.readAsArrayBuffer(files[0]); - setFileNames(names); + if (backendType === "java") { + setFile(files[0]); + setFileNames(names); + } else { + const fr = new FileReader(); + let ab = new ArrayBuffer(200000000); + fr.onload = (ev: ProgressEvent) => { + let ab = fr.result; + setPDFFile(ab as ArrayBuffer); + }; + fr.readAsArrayBuffer(files[0]); + setFileNames(names); + } }; const buildSummaryData = (rawData: ArrayBuffer) => { @@ -123,8 +168,9 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { for (let i = 0; i < files.length; i++) { file = files[i]; - // Cohere has a character limit of 100kb so we are restricting it here as well. - if (file.size > 100000) { + // Cohere has a character limit of ~100kb so we are restricting it here as well. + // We can use LangChain in this area to support larger files. + if (file.size > FILE_SIZE) { tempArray.push(file.name); invalidFiles.current = tempArray; } @@ -141,7 +187,7 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { summary: "File " + invalidFiles.current[0] + - " is too big. The maximum size is 100KB.", + ` is too big. The maximum size is ${FILE_SIZE / 1000}KB.`, }); setMessages(temp); } else { @@ -153,7 +199,7 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { summary: "These files are too big: " + fileNames + - ". The maximum size is 100KB.", + `. The maximum size is ${FILE_SIZE / 1000}KB.`, }); setMessages(temp); } @@ -163,7 +209,11 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { }; useEffect(() => { - if (summary !== "") setLoading(!loading); + if (summaryResults !== "") setLoading(!loading); + }, [summaryResults]); + + useEffect(() => { + setSummaryResults(summary); }, [summary]); useEffect(() => { @@ -193,7 +243,11 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { console.log("Calling websocket API to process PDF"); console.log("Filename: ", fileNames); console.log("Prompt: ", summaryPrompt); - fileChanged(buildSummaryData(pdfFile as ArrayBuffer)); + if (backendType === "python") { + fileChanged(buildSummaryData(pdfFile as ArrayBuffer)); + } else { + sendToJavaBackend(file!, summaryPrompt); + } setLoading(true); } }; @@ -224,18 +278,22 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => { onojSelect={selectListener} onojInvalidSelect={invalidListener} onojBeforeSelect={beforeSelectListener} - secondaryText="Maximum file size is 100KB per PDF file." + secondaryText={`Maximum file size is ${ + FILE_SIZE / 1000 + }KB per PDF file.`} > - + {backendType === "python" && ( + + )} {invalidFiles.current.length !== 1 && fileNames && ( <> @@ -276,7 +334,7 @@ export const Summary = ({ fileChanged, summary, clear, prompt }: Props) => {
)} diff --git a/app/src/components/content/websocket-interface.tsx b/app/src/components/content/websocket-interface.tsx new file mode 100644 index 00000000..2edc5ee5 --- /dev/null +++ b/app/src/components/content/websocket-interface.tsx @@ -0,0 +1,80 @@ +import "preact"; + +const gateway = `ws://${window.location.hostname}:1986`; +let sockTimer: any = null; + +export const initWebSocket = ( + setSummaryResults: any, + setBusy: any, + setUpdate: any, + messagesDP: any, + socket: any, + chatData: any +) => { + // const [connState, setConnState] = useState("Disconnected"); + console.log("Trying to open a WebSocket connection..."); + socket.current = new WebSocket(gateway); + socket.current.binaryType = "arraybuffer"; + + // handle all messages coming from the websocket service + const onMessage = (event: any) => { + const msg = JSON.parse(event.data); + + switch (msg.msgType) { + // switch (Object.keys(msg)[0]) { + case "message": + console.log("message: ", msg.data); + return msg.data; + case "question": + console.log("question: ", msg.data); + return msg.data; + case "summary": + console.log("summary: ", msg.data); + setSummaryResults(msg.data); + return; + case "answer": + console.log("answer: ", msg.data); + if (msg.data !== "connected") { + let tempArray = [...chatData.current]; + // remove the animation item before adding answer + setBusy(false); + tempArray.pop(); + messagesDP.current.data = []; + tempArray.push({ + id: tempArray.length as number, + answer: msg.data, + }); + chatData.current = tempArray; + setUpdate(chatData.current); + } + return msg.data; + default: + return "unknown"; + } + }; + + const onOpen = () => { + clearInterval(sockTimer); + console.log("Connection opened"); + socket.current?.send( + JSON.stringify({ msgType: "message", data: "connected" }) + ); + //setConnState("Connected"); + }; + + // if the connection is lost, wait one minute and try again. + const onError = () => { + //sockTimer = setInterval(initWebSocket, 1000 * 60); + }; + function onClose() { + console.log("Connection closed"); + //setConnState("Disconnected"); + socket.current ? (socket.current.onclose = () => {}) : null; + socket.current?.close(); + } + + socket.current.onopen = onOpen; + socket.current.onerror = onError; + socket.current.onclose = onClose; + socket.current.onmessage = onMessage; +}; diff --git a/app/tsconfig.json b/app/tsconfig.json index 7afa2973..76b073dc 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -22,7 +22,8 @@ "preact": ["./node_modules/preact"], "marked/*": ["./node_modules/marked/*"], "oj-sample/*": ["./src/components/oj-sample/types/*"], - "md-wrapper": ["./src/components/md-wrapper/types/index"] + "md-wrapper": ["./src/components/md-wrapper/types/index"], + "stompjs": ["./node_modules/@stomp/stompjs/index"] }, "declaration": true, "noEmitOnError": true,