From 6354c470b522cfa7ef545a331e49f6b4188bd49e Mon Sep 17 00:00:00 2001 From: Matt White Date: Mon, 21 Jul 2025 08:15:22 -0400 Subject: [PATCH 01/11] Start --- APIs.md | 89 +++++++++++++++++++ src/app/context/ModalContext.tsx | 3 +- src/app/layout.tsx | 7 +- src/app/page.tsx | 111 ++++++++++++++++++++---- src/components/app-navbar.tsx | 109 +++++++++++++++++++++-- src/components/modal/modal-selector.tsx | 4 + src/utils/constants.ts | 1 + 7 files changed, 296 insertions(+), 28 deletions(-) create mode 100644 APIs.md diff --git a/APIs.md b/APIs.md new file mode 100644 index 0000000..cfe824a --- /dev/null +++ b/APIs.md @@ -0,0 +1,89 @@ +## API's to the chat interface. + +API Host: http://localhost:5329 + +### Generic Database Search + +POST {{host}}/v1.0/sql/query +Content-Type: application/json +Accept: application/json + +{ +"query": "select content from \"view-9468964f-058a-4f2e-89ab-8b351ff082ba\" where document_guid = 'f49f92f7-8cdf-4f44-b327-0519e2aad881' and content LENGTH(content) > 5;", +"limit": 10 +} + +### Vector search + +#### Request +* Headers +```text +POST {{host}}/v1.0/vector/search +Content-Type: application/json +Accept: application/json +``` + +* BODY + +```json +{ + "query": "What is the main topic?", + "document_guid": "f49f92f7-8cdf-4f44-b327-0519e2aad881", + "limit": 5 +} +``` + +#### Response +* Headers + +```text +date: Fri, 18 Jul 2025 15:52:31 GMT +server: uvicorn +content-length: 1266 +content-type: application/json +x-request-id: 95aa4000-9c28-4093-9265-db47a2dde78d +``` + +* Body + +```json +{ + "status": "success", + "query": "What is the main topic?", + "document_guid": "f49f92f7-8cdf-4f44-b327-0519e2aad881", + "limit": 5, + "result_count": 5, + "results": [ + { + "id": 1, + "content": "Reference guide", + "length": 15, + "distance": 0.6795133566276237 + }, + { + "id": 1565, + "content": "Enterprise", + "length": 10, + "distance": 0.7974459138448121 + }, + { + "id": 363, + "content": "1 . Serial number label pull tab 5 . NIC status LED 2 . Quick removal access panel 6 . UID button / LED 3 . Power On / Standby button and system power LED 7 . GPU cage ( 8 DW GPUs ) 4 . Health LED 8 . 1U drive cage ( up to 8 SFF or 8 EDSFF 1T drives ) Figure 1 . HPE ProLiant Compute DL380a Gen12 Server—Front system detail", + "length": 323, + "distance": 0.9242449513442739 + }, + { + "id": 1079, + "content": "Chat now ( sales )", + "length": 18, + "distance": 0.9243938094603021 + }, + { + "id": 375, + "content": "7 8 9 10 11 1 . Power supply for the system board 6 . Power supply for the system board 2 . Power supplies for GPU auxiliary power 7 . VGA 3 . Primary riser slots 1 - 3 8 . Two ( 2 ) USB 3 . 0 ports 4 . Secondary riser slots 4 - 6 9 . HPE iLO management port 5 . Power supplies for GPU auxiliary power 10 . OCP 3 . 0 slot 1 11 . OCP 3 . 0 slot 2 Figure 2 . HPE ProLiant Compute DL380a Gen12 Server—Rear system detail", + "length": 416, + "distance": 0.9250164240618253 + } + ] +} +``` \ No newline at end of file diff --git a/src/app/context/ModalContext.tsx b/src/app/context/ModalContext.tsx index 4292de6..679d2e6 100644 --- a/src/app/context/ModalContext.tsx +++ b/src/app/context/ModalContext.tsx @@ -10,6 +10,8 @@ export const useModal = () => { export enum AppModal { SAVE_PROMPT = "savePrompt", + ADD_SYSTEM_INSTRUCTION = "addSystemInstruction", + EDIT_SYSTEM_INSTRUCTION = "editSystemInstruction", } export const ModalProvider = ({ children }: { children: React.ReactNode }) => { @@ -27,4 +29,3 @@ export const ModalProvider = ({ children }: { children: React.ReactNode }) => { {children} ); }; - diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fde23fb..e75e2e4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import ModalSelector from "@/components/modal/modal-selector"; import { ModalProvider } from "./context/ModalContext"; import { PromptsProvider } from "./context/PromptContext"; +import { SystemInstructionProvider } from "./context/SystemInstructionContext"; const montserrat = Montserrat({ subsets: ["latin"] }); @@ -21,8 +22,10 @@ export default function RootLayout({ - - {children} + + + {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index e529e87..7cceb28 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,20 +9,22 @@ import { SaveIcon } from "@/components/icons/save-icon"; import { TrashIcon } from "@/components/icons/trash-icon"; import Sidebar from "@/components/sidebar"; import { cn } from "@/utils/cn"; -import { baseUrl, fallbackModel } from "@/utils/constants"; +import { baseUrl, fallbackModel, vectorSearchBaseUrl } from "@/utils/constants"; import generateRandomString from "@/utils/generateRandomString"; import { useCycle } from "framer-motion"; import { ChatOllama } from "langchain/chat_models/ollama"; -import { AIMessage, HumanMessage } from "langchain/schema"; +import { AIMessage, HumanMessage, SystemMessage } from "langchain/schema"; import { useEffect, useRef, useState } from "react"; import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { AppModal, useModal } from "./context/ModalContext"; import { usePrompts } from "./context/PromptContext"; +import { useSystemInstruction } from "./context/SystemInstructionContext"; export default function Home() { const { setModalConfig } = useModal(); const { activePromptTemplate, setActivePromptTemplate } = usePrompts(); + const { activeSystemInstruction } = useSystemInstruction(); const [newPrompt, setNewPrompt] = useState(""); const [messages, setMessages] = useState< { @@ -106,7 +108,35 @@ export default function Home() { }); } - async function triggerPrompt(input: string = newPrompt) { + async function performVectorSearch(query: string) { + try { + const response = await fetch(`${vectorSearchBaseUrl}/v1.0/vector/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: query, + document_guid: "f49f92f7-8cdf-4f44-b327-0519e2aad881", + limit: 5 + }) + }); + + if (!response.ok) { + console.error('Vector search failed:', response.statusText); + return null; + } + + const data = await response.json(); + return data.results; + } catch (error) { + console.error('Error performing vector search:', error); + return null; + } +} + +async function triggerPrompt(input: string = newPrompt) { if (!ollama) return; scrollToBottom(); if (messages.length == 0) getName(input); @@ -120,13 +150,38 @@ export default function Home() { let streamedText = ""; messages.push(msg); const msgCache = [...messages]; - const stream = await ollama.stream( - messages.map((m) => - m.type == "human" - ? new HumanMessage(m.content) - : new AIMessage(m.content), - ), - ); + + // First perform vector search + const vectorSearchResults = await performVectorSearch(input); + + // Prepare context from vector search results + let contextFromVectorSearch = ""; + if (vectorSearchResults && vectorSearchResults.length > 0) { + contextFromVectorSearch = "Context from vector search:\n" + + vectorSearchResults.map((result: any) => result.content).join("\n") + + "\n\nUser query: " + input; + } else { + contextFromVectorSearch = input; + } + + // Use the context in the request to Ollama + // Add system instruction at the beginning of the messages array + const messagesWithSystemInstruction = [ + new SystemMessage(activeSystemInstruction.content), + ...messages.map((m, index) => { + if (index === messages.length - 1 && m.type === "human") { + // Replace the last human message with the context-enhanced version + return new HumanMessage(contextFromVectorSearch); + } else { + return m.type === "human" + ? new HumanMessage(m.content) + : new AIMessage(m.content); + } + }) + ]; + + const stream = await ollama.stream(messagesWithSystemInstruction); + setNewPrompt(""); setActivePromptTemplate(undefined); let updatedMessages = [...msgCache]; @@ -206,13 +261,35 @@ export default function Home() { const model = activeModel; let streamedText = ""; const msgCache = [...filtered]; - const stream = await ollama.stream( - filtered.map((m) => - m.type == "human" - ? new HumanMessage(m.content) - : new AIMessage(m.content), - ), - ); + + // Get the last human message for vector search + const lastHumanMessage = filtered.filter(m => m.type === "human").pop(); + let vectorSearchResults = null; + + if (lastHumanMessage) { + // Perform vector search with the last human message + vectorSearchResults = await performVectorSearch(lastHumanMessage.content); + } + + // Prepare the messages for Ollama, including vector search results if available + const messagesForOllama = [ + new SystemMessage(activeSystemInstruction.content), + ...filtered.map((m, index) => { + if (index === filtered.length - 1 && m.type === "human" && vectorSearchResults && vectorSearchResults.length > 0) { + // Enhance the last human message with vector search results + const contextFromVectorSearch = "Context from vector search:\n" + + vectorSearchResults.map((result: any) => result.content).join("\n") + + "\n\nUser query: " + m.content; + return new HumanMessage(contextFromVectorSearch); + } else { + return m.type === "human" + ? new HumanMessage(m.content) + : new AIMessage(m.content); + } + }) + ]; + + const stream = await ollama.stream(messagesForOllama); setNewPrompt(""); let updatedMessages = [...msgCache]; let c = 0; diff --git a/src/components/app-navbar.tsx b/src/components/app-navbar.tsx index a0dedf8..90d60cb 100644 --- a/src/components/app-navbar.tsx +++ b/src/components/app-navbar.tsx @@ -3,6 +3,8 @@ import { ChatOllama } from "langchain/chat_models/ollama"; import { baseUrl } from "@/utils/constants"; import { useEffect, useRef, useState } from "react"; +import { useSystemInstruction } from "@/app/context/SystemInstructionContext"; +import { AppModal, useModal } from "@/app/context/ModalContext"; type Props = { documentName: string; @@ -23,6 +25,11 @@ export default function AppNavbar({ }: Props) { const [isShareMenuOpen, setIsShareMenuOpen] = useState(false); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const [isSystemMenuOpen, setIsSystemMenuOpen] = useState(false); + const { activeSystemInstruction, allSystemInstructions, setActiveSystemInstructionById } = useSystemInstruction(); + const { setModalConfig } = useModal(); + + const systemMenuRef = useRef(null); const handleInputChange = (event: React.ChangeEvent) => { const value = event.target.value; @@ -46,6 +53,14 @@ export default function AppNavbar({ ) { setIsProfileMenuOpen(false); } + + if ( + isSystemMenuOpen && + systemMenuRef.current && + !systemMenuRef.current.contains(event.target) + ) { + setIsSystemMenuOpen(false); + } }; document.addEventListener("click", handleDocumentClick); @@ -53,7 +68,7 @@ export default function AppNavbar({ return () => { document.removeEventListener("click", handleDocumentClick); }; - }, [isShareMenuOpen, isProfileMenuOpen]); + }, [isShareMenuOpen, isProfileMenuOpen, isSystemMenuOpen]); function toggleModel() { const i = @@ -85,13 +100,91 @@ export default function AppNavbar({ onChange={handleInputChange} > - +
+
+ System: + + + {isSystemMenuOpen && ( +
+
+ {allSystemInstructions.map((instruction) => ( +
+ + +
+ ))} +
+ +
+
+ )} +
+ +
diff --git a/src/components/modal/modal-selector.tsx b/src/components/modal/modal-selector.tsx index 7fc6e24..5f37400 100644 --- a/src/components/modal/modal-selector.tsx +++ b/src/components/modal/modal-selector.tsx @@ -2,6 +2,8 @@ import { AppModal, useModal } from "@/app/context/ModalContext"; import SavePromptModal from "./save-prompt-modal"; +import AddSystemInstructionModal from "./add-system-instruction-modal"; +import EditSystemInstructionModal from "./edit-system-instruction-modal"; export default function ModalSelector() { const { modalConfig } = useModal(); @@ -9,6 +11,8 @@ export default function ModalSelector() { return ( <> {modalConfig.modal == AppModal.SAVE_PROMPT && } + {modalConfig.modal == AppModal.ADD_SYSTEM_INSTRUCTION && } + {modalConfig.modal == AppModal.EDIT_SYSTEM_INSTRUCTION && } ); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2b59586..3911771 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,2 +1,3 @@ export const baseUrl: string = process.env.NEXT_PUBLIC_OLLAMA_BASEURL || 'http://localhost:11434' export const fallbackModel: string = 'llama3.2' +export const vectorSearchBaseUrl: string = process.env.NEXT_PUBLIC_VECTOR_SEARCH_BASEURL || 'http://localhost:5329' From 67ea7c2800575a7a0204c8b351233f8e969e1b55 Mon Sep 17 00:00:00 2001 From: Matt White Date: Mon, 21 Jul 2025 09:03:27 -0400 Subject: [PATCH 02/11] Start --- src/app/context/SystemInstructionContext.tsx | 114 +++++++++++++++++ src/app/page.tsx | 2 +- .../modal/add-system-instruction-modal.tsx | 106 ++++++++++++++++ .../modal/edit-system-instruction-modal.tsx | 116 ++++++++++++++++++ 4 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 src/app/context/SystemInstructionContext.tsx create mode 100644 src/components/modal/add-system-instruction-modal.tsx create mode 100644 src/components/modal/edit-system-instruction-modal.tsx diff --git a/src/app/context/SystemInstructionContext.tsx b/src/app/context/SystemInstructionContext.tsx new file mode 100644 index 0000000..46e1d7c --- /dev/null +++ b/src/app/context/SystemInstructionContext.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { createContext, useContext, useState, useEffect } from "react"; +import { + SystemInstruction, + getDefaultSystemInstruction, + getSystemInstructionById, + getAllSystemInstructions, + addSystemInstruction, + updateSystemInstruction +} from "@/utils/systemInstructions"; + +// Create context with default values +const SystemInstructionContext = createContext<{ + activeSystemInstruction: SystemInstruction; + setActiveSystemInstructionById: (id: string) => void; + allSystemInstructions: SystemInstruction[]; + addNewSystemInstruction: (name: string, content: string) => void; + updateExistingSystemInstruction: (id: string, name: string, content: string) => void; +}>({ + activeSystemInstruction: getDefaultSystemInstruction(), + setActiveSystemInstructionById: () => {}, + allSystemInstructions: getAllSystemInstructions(), + addNewSystemInstruction: () => {}, + updateExistingSystemInstruction: () => {}, +}); + +// Hook to use the system instruction context +export function useSystemInstruction() { + return useContext(SystemInstructionContext); +} + +// Provider component +export function SystemInstructionProvider({ children }: { children: React.ReactNode }) { + // Initialize with the default system instruction + const [activeSystemInstruction, setActiveSystemInstruction] = useState( + getDefaultSystemInstruction() + ); + + const [allSystemInstructions, setAllSystemInstructions] = useState( + getAllSystemInstructions() + ); + + // Load the active system instruction from localStorage on component mount + useEffect(() => { + const storedInstructionId = localStorage.getItem("activeSystemInstructionId"); + if (storedInstructionId) { + const instruction = getSystemInstructionById(storedInstructionId); + if (instruction) { + setActiveSystemInstruction(instruction); + } + } + }, []); + + // Function to set the active system instruction by ID + const setActiveSystemInstructionById = (id: string) => { + const instruction = getSystemInstructionById(id); + if (instruction) { + setActiveSystemInstruction(instruction); + localStorage.setItem("activeSystemInstructionId", id); + } + }; + + // Function to add a new system instruction + const addNewSystemInstruction = (name: string, content: string) => { + // Create a new instruction object + const newInstruction: SystemInstruction = { + id: `instruction-${Date.now()}`, + name, + content + }; + + // Add it to the system instructions + addSystemInstruction(newInstruction); + + // Update the local state with the latest instructions + setAllSystemInstructions(getAllSystemInstructions()); + + // Return the new instruction ID in case it's needed + return newInstruction.id; + }; + + // Function to update an existing system instruction + const updateExistingSystemInstruction = (id: string, name: string, content: string) => { + // Update the instruction + updateSystemInstruction(id, name, content); + + // Update the local state with the latest instructions + setAllSystemInstructions(getAllSystemInstructions()); + + // If the active instruction was updated, update it in the state + if (activeSystemInstruction.id === id) { + const updatedInstruction = getSystemInstructionById(id); + if (updatedInstruction) { + setActiveSystemInstruction(updatedInstruction); + } + } + }; + + // Context value + const value = { + activeSystemInstruction, + setActiveSystemInstructionById, + allSystemInstructions, + addNewSystemInstruction, + updateExistingSystemInstruction, + }; + + return ( + + {children} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7cceb28..abf5a81 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -264,7 +264,7 @@ async function triggerPrompt(input: string = newPrompt) { // Get the last human message for vector search const lastHumanMessage = filtered.filter(m => m.type === "human").pop(); - let vectorSearchResults = null; + let vectorSearchResults: any[] | null = null; if (lastHumanMessage) { // Perform vector search with the last human message diff --git a/src/components/modal/add-system-instruction-modal.tsx b/src/components/modal/add-system-instruction-modal.tsx new file mode 100644 index 0000000..f2d8954 --- /dev/null +++ b/src/components/modal/add-system-instruction-modal.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useModal } from "@/app/context/ModalContext"; +import { useSystemInstruction } from "@/app/context/SystemInstructionContext"; +import { cn } from "@/utils/cn"; +import { motion } from "framer-motion"; +import { useState } from "react"; +import ExpandingTextInput from "../expanding-text-input"; + +export default function AddSystemInstructionModal() { + const { addNewSystemInstruction } = useSystemInstruction(); + const { setModalConfig } = useModal(); + const [content, setContent] = useState(""); + const [instructionName, setInstructionName] = useState(""); + + function closeModal() { + setModalConfig({ modal: undefined, data: undefined }); + } + + function handleContentChange(e: any) { + setContent(e.target.value); + } + + const saveInstructionName = (e: any) => { + const value = e.target.value; + setInstructionName(value); + }; + + function saveInstruction() { + if (instructionName && content) { + addNewSystemInstruction(instructionName, content); + closeModal(); + } + } + + const bgVariant = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }; + + return ( +
+ + +
+
closeModal()} + className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0" + > +
e.stopPropagation()} + className="bg-white/black relative flex max-h-[80vh] transform flex-col overflow-hidden rounded-sm border border-white/20 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-3xl" + > +
+ +
{"Add System Instruction"}
+
+ +
+ + {saveInstructionName(e)}} + value={instructionName} + placeholder={"Enter Instruction Name..."} + expand={false} + /> +
+ +
+ + {handleContentChange(e)}} + value={content} + placeholder={"Enter system instruction content..."} + expand={true} + /> +
+ +
+ +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/modal/edit-system-instruction-modal.tsx b/src/components/modal/edit-system-instruction-modal.tsx new file mode 100644 index 0000000..bde885a --- /dev/null +++ b/src/components/modal/edit-system-instruction-modal.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { AppModal, useModal } from "@/app/context/ModalContext"; +import { useSystemInstruction } from "@/app/context/SystemInstructionContext"; +import { cn } from "@/utils/cn"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; +import ExpandingTextInput from "../expanding-text-input"; + +export default function EditSystemInstructionModal() { + const { updateExistingSystemInstruction } = useSystemInstruction(); + const { modalConfig, setModalConfig } = useModal(); + const [content, setContent] = useState(""); + const [instructionName, setInstructionName] = useState(""); + const [instructionId, setInstructionId] = useState(""); + + // Load the instruction data when the modal is opened + useEffect(() => { + if (modalConfig.data && modalConfig.modal === AppModal.EDIT_SYSTEM_INSTRUCTION) { + setInstructionId(modalConfig.data.id); + setInstructionName(modalConfig.data.name); + setContent(modalConfig.data.content); + } + }, [modalConfig]); + + function closeModal() { + setModalConfig({ modal: undefined, data: undefined }); + } + + function handleContentChange(e: any) { + setContent(e.target.value); + } + + const saveInstructionName = (e: any) => { + const value = e.target.value; + setInstructionName(value); + }; + + function saveInstruction() { + if (instructionName && content && instructionId) { + updateExistingSystemInstruction(instructionId, instructionName, content); + closeModal(); + } + } + + const bgVariant = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }; + + return ( +
+ + +
+
closeModal()} + className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0" + > +
e.stopPropagation()} + className="bg-white/black relative flex max-h-[80vh] transform flex-col overflow-hidden rounded-sm border border-white/20 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-3xl" + > +
+ +
{"Edit System Instruction"}
+
+ +
+ + {saveInstructionName(e)}} + value={instructionName} + placeholder={"Enter Instruction Name..."} + expand={false} + /> +
+ +
+ + {handleContentChange(e)}} + value={content} + placeholder={"Enter system instruction content..."} + expand={true} + /> +
+ +
+ +
+
+
+
+
+
+ ); +} \ No newline at end of file From c49ce4136334b825ac5fb607ec4a3f423fd59a2d Mon Sep 17 00:00:00 2001 From: Matt White Date: Tue, 22 Jul 2025 09:24:55 -0400 Subject: [PATCH 03/11] Adding some controls. --- src/app/page.tsx | 136 ++++++++++++++++++++++++---- src/components/app-navbar.tsx | 162 +++++++++++++++++++++++++++++++++- src/utils/constants.ts | 2 +- 3 files changed, 283 insertions(+), 17 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index abf5a81..f8d44db 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -20,6 +20,21 @@ import remarkGfm from "remark-gfm"; import { AppModal, useModal } from "./context/ModalContext"; import { usePrompts } from "./context/PromptContext"; import { useSystemInstruction } from "./context/SystemInstructionContext"; +import { getSystemInstructionById } from "@/utils/systemInstructions"; + +// Define the DocumentEntry type to match the one in app-navbar.tsx +type DocumentEntry = { + filename: string; + guid: string; + selected?: boolean; +}; + +declare global { + interface Window { + setDocumentValues?: ((filename: string, guid: string) => void) | + ((documents: DocumentEntry[] | DocumentEntry) => void); + } +} export default function Home() { const { setModalConfig } = useModal(); @@ -43,6 +58,7 @@ export default function Home() { >([]); const [activeConversation, setActiveConversation] = useState(""); const [menuState, toggleMenuState] = useCycle(false, true); + const [selectedDocuments, setSelectedDocuments] = useState([]); const msgContainerRef = useRef(null); useEffect(() => { @@ -110,18 +126,46 @@ export default function Home() { async function performVectorSearch(query: string) { try { - const response = await fetch(`${vectorSearchBaseUrl}/v1.0/vector/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ - query: query, - document_guid: "f49f92f7-8cdf-4f44-b327-0519e2aad881", - limit: 5 - }) - }); + // Check if there are any selected documents + const selectedDocs = selectedDocuments.filter(doc => doc.selected); + + let response; + + if (selectedDocs.length > 0) { + // If documents are selected, use /vector/search API with document_guids + const document_guids = selectedDocs.map(doc => doc.guid); + + console.log(`Using /vector/search API with ${document_guids.length} selected documents`); + + response = await fetch(`${vectorSearchBaseUrl}/v1.0/vector/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: query, + document_guids: document_guids, + limit: 10 + }) + }); + } else { + // If no documents are selected, use /vector/router/search API + console.log('Using /vector/router/search API (no documents selected)'); + + response = await fetch(`${vectorSearchBaseUrl}/v1.0/vector/router/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: query, + document_guid: "f49f92f7-8cdf-4f44-b327-0519e2aad881", + limit: 5 + }) + }); + } if (!response.ok) { console.error('Vector search failed:', response.statusText); @@ -157,6 +201,19 @@ async function triggerPrompt(input: string = newPrompt) { // Prepare context from vector search results let contextFromVectorSearch = ""; if (vectorSearchResults && vectorSearchResults.length > 0) { + // Update Document dropdown with all results + if (window.setDocumentValues) { + // Convert vector search results to DocumentEntry array + const documentEntries: DocumentEntry[] = vectorSearchResults.map((result: any) => ({ + filename: decodeURIComponent(result.object_key), + guid: result.document_guid, + selected: true + })); + + // Pass all document entries to the dropdown + window.setDocumentValues(documentEntries); + } + contextFromVectorSearch = "Context from vector search:\n" + vectorSearchResults.map((result: any) => result.content).join("\n") + "\n\nUser query: " + input; @@ -166,8 +223,16 @@ async function triggerPrompt(input: string = newPrompt) { // Use the context in the request to Ollama // Add system instruction at the beginning of the messages array + // If no documents are selected, use the Concise system instruction + const selectedDocs = selectedDocuments.filter(doc => doc.selected); + const systemInstruction = selectedDocs.length === 0 + ? getSystemInstructionById("concise") || activeSystemInstruction + : activeSystemInstruction; + + console.log(`Using ${systemInstruction.name} system instruction`); + const messagesWithSystemInstruction = [ - new SystemMessage(activeSystemInstruction.content), + new SystemMessage(systemInstruction.content), ...messages.map((m, index) => { if (index === messages.length - 1 && m.type === "human") { // Replace the last human message with the context-enhanced version @@ -269,11 +334,32 @@ async function triggerPrompt(input: string = newPrompt) { if (lastHumanMessage) { // Perform vector search with the last human message vectorSearchResults = await performVectorSearch(lastHumanMessage.content); + + // Update Document dropdown with all results + if (vectorSearchResults && vectorSearchResults.length > 0 && window.setDocumentValues) { + // Convert vector search results to DocumentEntry array + const documentEntries: DocumentEntry[] = vectorSearchResults.map((result: any) => ({ + filename: decodeURIComponent(result.object_key), + guid: result.document_guid, + selected: true + })); + + // Pass all document entries to the dropdown + window.setDocumentValues(documentEntries); + } } // Prepare the messages for Ollama, including vector search results if available + // If no documents are selected, use the Concise system instruction + const selectedDocs = selectedDocuments.filter(doc => doc.selected); + const systemInstruction = selectedDocs.length === 0 + ? getSystemInstructionById("concise") || activeSystemInstruction + : activeSystemInstruction; + + console.log(`Using ${systemInstruction.name} system instruction`); + const messagesForOllama = [ - new SystemMessage(activeSystemInstruction.content), + new SystemMessage(systemInstruction.content), ...filtered.map((m, index) => { if (index === filtered.length - 1 && m.type === "human" && vectorSearchResults && vectorSearchResults.length > 0) { // Enhance the last human message with vector search results @@ -329,7 +415,7 @@ async function triggerPrompt(input: string = newPrompt) { }); return nameOllama! .predict( - "You're a tool, that receives an input and responds exclusively with a 2-5 word summary of the topic (and absolutely no prose) based specifically on the words used in the input (not the expected output). Each word in the summary should be carefully chosen so that it's perfecly informative - and serve as a perfect title for the input. Now, return the summary for the following input:\n" + + "You're a tool, that receives an input and responds exclusively with a 2-5 word summary of the topic for the HPE Partner Portal (and absolutely no prose) based specifically on the words used in the input (not the expected output). Each word in the summary should be carefully chosen so that it's perfecly informative - and serve as a perfect title for the input. Now, return the summary for the following input:\n" + input, ) .then((name) => name); @@ -358,6 +444,26 @@ async function triggerPrompt(input: string = newPrompt) { availableModels={availableModels} setActiveModel={setActiveModel} setOllama={setOllama} + setDocumentValues={(documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => { + if (typeof documents === 'string' && guid) { + // Legacy format - create a single document entry + const newEntry: DocumentEntry = { + filename: documents as string, + guid: guid as string, + selected: true + }; + setSelectedDocuments([newEntry]); + console.log("Document values set (legacy):", documents, guid); + } else if (Array.isArray(documents)) { + // Array of documents + setSelectedDocuments(documents); + console.log("Document values set (array):", documents); + } else { + // Single document object + setSelectedDocuments([documents as DocumentEntry]); + console.log("Document values set (object):", documents); + } + }} />
diff --git a/src/components/app-navbar.tsx b/src/components/app-navbar.tsx index 90d60cb..8a5f043 100644 --- a/src/components/app-navbar.tsx +++ b/src/components/app-navbar.tsx @@ -6,6 +6,12 @@ import { useEffect, useRef, useState } from "react"; import { useSystemInstruction } from "@/app/context/SystemInstructionContext"; import { AppModal, useModal } from "@/app/context/ModalContext"; +type DocumentEntry = { + filename: string; + guid: string; + selected?: boolean; +}; + type Props = { documentName: string; setDocumentName: Function; @@ -13,6 +19,7 @@ type Props = { availableModels: any[]; setActiveModel: Function; setOllama: Function; + setDocumentValues?: (documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => void; // Function to set document entries }; export default function AppNavbar({ @@ -22,14 +29,18 @@ export default function AppNavbar({ availableModels, setActiveModel, setOllama, + setDocumentValues, }: Props) { const [isShareMenuOpen, setIsShareMenuOpen] = useState(false); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const [isSystemMenuOpen, setIsSystemMenuOpen] = useState(false); + const [isDocumentMenuOpen, setIsDocumentMenuOpen] = useState(false); + const [documentEntries, setDocumentEntries] = useState([]); const { activeSystemInstruction, allSystemInstructions, setActiveSystemInstructionById } = useSystemInstruction(); const { setModalConfig } = useModal(); const systemMenuRef = useRef(null); + const documentMenuRef = useRef(null); const handleInputChange = (event: React.ChangeEvent) => { const value = event.target.value; @@ -61,6 +72,14 @@ export default function AppNavbar({ ) { setIsSystemMenuOpen(false); } + + if ( + isDocumentMenuOpen && + documentMenuRef.current && + !documentMenuRef.current.contains(event.target) + ) { + setIsDocumentMenuOpen(false); + } }; document.addEventListener("click", handleDocumentClick); @@ -68,7 +87,7 @@ export default function AppNavbar({ return () => { document.removeEventListener("click", handleDocumentClick); }; - }, [isShareMenuOpen, isProfileMenuOpen, isSystemMenuOpen]); + }, [isShareMenuOpen, isProfileMenuOpen, isSystemMenuOpen, isDocumentMenuOpen]); function toggleModel() { const i = @@ -85,6 +104,82 @@ export default function AppNavbar({ setOllama(newOllama); } + // Function to set document entries + function setDocumentValuesInternal(documents: DocumentEntry[] | DocumentEntry) { + console.log('setDocumentValuesInternal called with:', documents); + + // Handle both single document and array of documents + if (Array.isArray(documents)) { + // Filter out duplicates based on guid + setDocumentEntries(prevEntries => { + // Create a map of existing entries by guid for quick lookup + const existingEntriesMap = new Map( + prevEntries.map(entry => [entry.guid, entry]) + ); + + // Process each new document + documents.forEach(doc => { + // Only add if it doesn't exist already + if (!existingEntriesMap.has(doc.guid)) { + existingEntriesMap.set(doc.guid, { ...doc, selected: true }); + } + }); + + // Convert map back to array + return Array.from(existingEntriesMap.values()); + }); + } else { + // If it's a single document, add it to the array if it doesn't exist already + setDocumentEntries(prevEntries => { + const exists = prevEntries.some(entry => entry.guid === documents.guid); + if (!exists) { + return [...prevEntries, { ...documents, selected: true }]; + } + return prevEntries; + }); + } + + // Call the prop function if provided + if (setDocumentValues) { + console.log('Calling parent setDocumentValues function'); + setDocumentValues(documents); + } else { + console.log('Parent setDocumentValues function not provided'); + } + } + + // For backward compatibility - handle single document + function setDocumentValuesSingle(filename: string, guid: string) { + const newEntry: DocumentEntry = { filename, guid, selected: true }; + setDocumentValuesInternal(newEntry); + } + + // Expose the function to the window object for external access + useEffect(() => { + console.log('Setting up window.setDocumentValues'); + + // For backward compatibility, we need to handle both the new and old function signatures + // @ts-ignore + window.setDocumentValues = (filenameOrDocuments: string | DocumentEntry[], guid?: string) => { + if (typeof filenameOrDocuments === 'string' && guid) { + // Old signature: (filename: string, guid: string) + setDocumentValuesSingle(filenameOrDocuments, guid); + } else { + // New signature: (documents: DocumentEntry[] | DocumentEntry) + // @ts-ignore + setDocumentValuesInternal(filenameOrDocuments); + } + }; + + console.log('window.setDocumentValues is now:', typeof window.setDocumentValues); + + return () => { + console.log('Cleaning up window.setDocumentValues'); + // @ts-ignore + delete window.setDocumentValues; + }; + }, []); + const shareMenuRef = useRef(null); const profileMenuRef = useRef(null); @@ -101,6 +196,71 @@ export default function AppNavbar({ >
+
+ Document: + + + {isDocumentMenuOpen && ( +
+
+ {documentEntries.length > 0 ? ( + documentEntries.map((entry, index) => ( +
+
+ { + const updatedEntries = [...documentEntries]; + updatedEntries[index] = { + ...updatedEntries[index], + selected: !updatedEntries[index].selected + }; + setDocumentEntries(updatedEntries); + + // Call the prop function to update parent component state + if (setDocumentValues) { + setDocumentValues(updatedEntries); + } + }} + className="mr-2" + /> + +
+
+ )) + ) : ( +
+ No documents available +
+ )} +
+
+ )} +
+
System:
+ +); } diff --git a/src/components/app-navbar.tsx b/src/components/app-navbar.tsx index a0dedf8..64e3660 100644 --- a/src/components/app-navbar.tsx +++ b/src/components/app-navbar.tsx @@ -73,27 +73,24 @@ export default function AppNavbar({ const shareMenuRef = useRef(null); const profileMenuRef = useRef(null); - return ( - <> - ); } diff --git a/src/components/menu-toggle.tsx b/src/components/menu-toggle.tsx index 23d39de..c6434c4 100644 --- a/src/components/menu-toggle.tsx +++ b/src/components/menu-toggle.tsx @@ -11,14 +11,16 @@ const Path = (props: any) => ( /> ); -export const MenuToggle = ({ toggle }: any) => ( - +
+
+ {conversations.map((c) => ( +
loadConvo(c)} + > + {c.title} +
+ +
{ + e.stopPropagation(); + deleteConvo(c); + }} + className="cursor-pointer" + > + +
+ +
-
- ))} + ))} +
+ ); From 64a9945f9d5bb26bee660abcf9a514c475871ad3 Mon Sep 17 00:00:00 2001 From: Ashley Judah Date: Tue, 22 Jul 2025 17:37:18 -0400 Subject: [PATCH 06/11] slight color adjustment to background --- src/app/globals.css | 4 ++-- src/app/page.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 32e8edf..642fa16 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -32,12 +32,12 @@ body { color: var(--hpe-gray-dark); - background: var(--hpe-white); + background: var(--hpe-gray-light); } /* Main chat container - add padding for white chat area */ main { - background-color: var(--hpe-white); + background-color: var(--hpe-gray-light); } /* Override black backgrounds with HPE white */ diff --git a/src/app/page.tsx b/src/app/page.tsx index 59f81dd..4a2bc0f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -284,7 +284,7 @@ return ( setActiveModel={setActiveModel} setOllama={setOllama} /> -
+
From 438db7a5798efadc30e1f9561ff7788bb48ee4ac Mon Sep 17 00:00:00 2001 From: Matt White Date: Tue, 22 Jul 2025 20:08:31 -0400 Subject: [PATCH 07/11] merged. --- src/app/page.tsx | 352 ++++++++++++---------------------- src/components/app-navbar.tsx | 315 ++++++++++++++---------------- 2 files changed, 270 insertions(+), 397 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 396e820..3982667 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -178,9 +178,9 @@ export default function Home() { console.error('Error performing vector search:', error); return null; } -} + } -async function triggerPrompt(input: string = newPrompt) { + async function triggerPrompt(input: string = newPrompt) { if (!ollama) return; scrollToBottom(); if (messages.length == 0) getName(input); @@ -345,6 +345,7 @@ async function triggerPrompt(input: string = newPrompt) { })); // Pass all document entries to the dropdown + // @ts-ignore window.setDocumentValues(documentEntries); } } @@ -416,13 +417,13 @@ async function triggerPrompt(input: string = newPrompt) { return nameOllama! .predict( "You're a tool, that receives an input and responds exclusively with a 2-5 word summary of the topic for the HPE Partner Portal (and absolutely no prose) based specifically on the words used in the input (not the expected output). Each word in the summary should be carefully chosen so that it's perfecly informative - and serve as a perfect title for the input. Now, return the summary for the following input:\n" + - input, + input, ) .then((name) => name); } return ( -
+
{}} + setDocumentName={() => { + }} activeModel={activeModel} availableModels={availableModels} setActiveModel={setActiveModel} setOllama={setOllama} setDocumentValues={(documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => { if (typeof documents === 'string' && guid) { - // Legacy format - create a single document entry const newEntry: DocumentEntry = { filename: documents as string, guid: guid as string, selected: true }; setSelectedDocuments([newEntry]); - console.log("Document values set (legacy):", documents, guid); } else if (Array.isArray(documents)) { - // Array of documents setSelectedDocuments(documents); - console.log("Document values set (array):", documents); } else { - // Single document object setSelectedDocuments([documents as DocumentEntry]); - console.log("Document values set (object):", documents); } }} /> -
-
+
+
- {messages.map((msg) => ( -
-
-

- {(msg?.model?.split(":")[0] || "user") + - " • " + - new Date(msg.timestamp).toLocaleDateString() + - " " + - new Date(msg.timestamp).toLocaleTimeString()} -

- - {msg.content.trim()} - -
-
- {msg.type == "human" && ( - { - setModalConfig({ - modal: AppModal.SAVE_PROMPT, - data: msg, - }); - }} - className="h-4 w-4 fill-white/50 hover:fill-white/90" - /> - )} - refreshMessage(msg)} - className="h-4 w-4 fill-white/50 hover:fill-white/90" - /> - { - navigator.clipboard.writeText(msg.content); - }} - className="h-4 w-4 fill-white/50 hover:fill-white/90" - /> - { - deleteMessage(msg); - }} - className="h-4 w-4 fill-white/50 hover:fill-white/90" - /> -
-
- ))} -
-
-
-return ( -
- -
- {}} - activeModel={activeModel} - availableModels={availableModels} - setActiveModel={setActiveModel} - setOllama={setOllama} - /> -
-
-
-
- {messages.length === 0 ? ( -
-
-

Welcome to your personal AI Assistant

-

Start a conversation to begin

+
+ {messages.length === 0 ? ( +
+
+

Welcome to your personal AI Assistant

+

Start a conversation to begin

+
-
- ) : ( -
- {messages.map((msg) => ( -
+ ) : ( +
+ {messages.map((msg) => (
-
+
+
{msg?.model?.split(":")[0] || "You"} •{" "} {new Date(msg.timestamp).toLocaleTimeString()} -
- - {msg.content.trim()} - -
- {msg.type === "human" && ( - + + {msg.content.trim()} + +
+ {msg.type === "human" && ( + { + setModalConfig({ + modal: AppModal.SAVE_PROMPT, + data: msg, + }); + }} + className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" + /> + )} + refreshMessage(msg)} + className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" + /> + { - setModalConfig({ - modal: AppModal.SAVE_PROMPT, - data: msg, - }); + navigator.clipboard.writeText(msg.content); }} className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" /> - )} - refreshMessage(msg)} - className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" - /> - { - navigator.clipboard.writeText(msg.content); - }} - className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" - /> - { - deleteMessage(msg); - }} - className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" - /> + { + deleteMessage(msg); + }} + className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" + /> +
-
- ))} -
- )} + ))} +
+ )} +
-
-
-
- -
- {activePromptTemplate ? ( - { - if ( - x.e.key === "Enter" && - !x.e.metaKey && - !x.e.shiftKey && - !x.e.altKey && - newPrompt !== "" - ) { - triggerPrompt(x.input); - } - }} - /> - ) : ( - { - if (e.target.value != "\n") setNewPrompt(e.target.value); - }} - onKeyDown={(e) => { - if ( - e.key === "Enter" && - !e.metaKey && - !e.shiftKey && - !e.altKey && - newPrompt !== "" - ) { - triggerPrompt(); - } - }} - value={newPrompt} - placeholder="Send a message" - className="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 placeholder-gray-500 focus:border-[#01a982] focus:outline-none focus:ring-2 focus:ring-[#01a982] focus:ring-opacity-50" - /> - )} +
+
+ +
+ {activePromptTemplate ? ( + { + if ( + x.e.key === "Enter" && + !x.e.metaKey && + !x.e.shiftKey && + !x.e.altKey && + newPrompt !== "" + ) { + triggerPrompt(x.input); + } + }} + /> + ) : ( + { + if (e.target.value != "\n") setNewPrompt(e.target.value); + }} + onKeyDown={(e) => { + if ( + e.key === "Enter" && + !e.metaKey && + !e.shiftKey && + !e.altKey && + newPrompt !== "" + ) { + triggerPrompt(); + } + }} + value={newPrompt} + placeholder="Send a message" + className="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 placeholder-gray-500 focus:border-[#01a982] focus:outline-none focus:ring-2 focus:ring-[#01a982] focus:ring-opacity-50" + /> + )} +
-
-
-); -} +
+ ); +} \ No newline at end of file diff --git a/src/components/app-navbar.tsx b/src/components/app-navbar.tsx index 8fcea46..1efcd07 100644 --- a/src/components/app-navbar.tsx +++ b/src/components/app-navbar.tsx @@ -184,188 +184,167 @@ export default function AppNavbar({ const profileMenuRef = useRef(null); return ( - <> - - -return ( - ); -} +} \ No newline at end of file From c3ef928ee631b11e44ad5c448d0a40ff93df29a2 Mon Sep 17 00:00:00 2001 From: Matt White Date: Tue, 22 Jul 2025 21:33:31 -0400 Subject: [PATCH 08/11] good enough for now. --- .env.example | 2 +- src/app/page.tsx | 39 ++++-- src/components/sidebar.tsx | 14 +++ src/types/global.d.ts | 11 +- src/utils/prompts/concise.txt | 78 +++++++++++- src/utils/prompts/default.txt | 113 +++++++++++++---- src/utils/systemInstructions.ts | 217 ++++++++++++++++++++++++++++---- 7 files changed, 408 insertions(+), 66 deletions(-) diff --git a/.env.example b/.env.example index 8cfd575..5ee29ec 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -NEXT_PUBLIC_OLLAMA_BASEURL= +NEXT_PUBLIC_OLLAMA_BASEURL= \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 3982667..c924c55 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -29,12 +29,7 @@ type DocumentEntry = { selected?: boolean; }; -declare global { - interface Window { - setDocumentValues?: ((filename: string, guid: string) => void) | - ((documents: DocumentEntry[] | DocumentEntry) => void); - } -} +// This is now defined in global.d.ts export default function Home() { const { setModalConfig } = useModal(); @@ -162,7 +157,7 @@ export default function Home() { body: JSON.stringify({ query: query, document_guid: "f49f92f7-8cdf-4f44-b327-0519e2aad881", - limit: 5 + limit: 1 }) }); } @@ -222,7 +217,7 @@ export default function Home() { } // Use the context in the request to Ollama - // Add system instruction at the beginning of the messages array + // Add system instruction at the beginning of the message array // If no documents are selected, use the Concise system instruction const selectedDocs = selectedDocuments.filter(doc => doc.selected); const systemInstruction = selectedDocs.length === 0 @@ -416,7 +411,7 @@ export default function Home() { }); return nameOllama! .predict( - "You're a tool, that receives an input and responds exclusively with a 2-5 word summary of the topic for the HPE Partner Portal (and absolutely no prose) based specifically on the words used in the input (not the expected output). Each word in the summary should be carefully chosen so that it's perfecly informative - and serve as a perfect title for the input. Now, return the summary for the following input:\n" + + "You're a tool, that receives an input and responds exclusively with a 2-5 word summary of the topic for the HPE Partner Portal (and absolutely no prose) based specifically on the words used in the input (not the expected output). Each word in the summary should be carefully chosen so that it's perfectly informative - and serve as a perfect title for the input. Now, return the summary for the following input:\n" + input, ) .then((name) => name); @@ -432,6 +427,24 @@ export default function Home() { setConversations={setConversations} setMessages={setMessages} setNewPrompt={setNewPrompt} + clearDocuments={() => setSelectedDocuments([])} + setDocumentEntries={(documents) => { + if (Array.isArray(documents) && documents.length === 0) { + setSelectedDocuments([]); + } else if (typeof documents === 'string' && arguments.length > 1) { + const guid = arguments[1]; + const newEntry = { + filename: documents, + guid: guid, + selected: true + }; + setSelectedDocuments([newEntry]); + } else if (Array.isArray(documents)) { + setSelectedDocuments(documents); + } else { + setSelectedDocuments([documents]); + } + }} toggleMenuState={toggleMenuState} />
-

Welcome to your personal AI Assistant

-

Start a conversation to begin

+

Welcome to the HPE Partner Portal AI Assistant

+

Start a conversation to begin by telling us what system your interested in getting some information about.

+

That will help the system narrow down the scope and provide the best responses

+

You can then select which documents you wish to research from the navigation above

) : ( @@ -599,4 +614,4 @@ export default function Home() {
); -} \ No newline at end of file +} diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index 67ab74d..5095d3f 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -22,17 +22,27 @@ type Props = { ) => void; setActiveConversation: (title: string) => void; setNewPrompt: (newPrompt: string) => void; + clearDocuments: () => void; + setDocumentEntries?: (documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => void; conversations: { title: string; filePath: string }[]; activeConversation: string; menuState: boolean; toggleMenuState: Cycle; }; +type DocumentEntry = { + filename: string; + guid: string; + selected?: boolean; +}; + export default function Sidebar({ setMessages, setConversations, setActiveConversation, setNewPrompt, + clearDocuments, + setDocumentEntries, conversations, activeConversation, menuState, @@ -75,6 +85,10 @@ export default function Sidebar({ setMessages([]); setActiveConversation(""); setNewPrompt(""); + clearDocuments(); + if (setDocumentEntries) { + setDocumentEntries([]); + } toggleMenuState(); } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index a57e010..c178aa8 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,3 +1,10 @@ +// Define the DocumentEntry type +type DocumentEntry = { + filename: string; + guid: string; + selected?: boolean; +}; + interface Window { - setDocumentValues: (filename: string, guid: string) => void; -} \ No newline at end of file + setDocumentValues?: (filenameOrDocuments: string | DocumentEntry[] | DocumentEntry, guid?: string) => void; +} diff --git a/src/utils/prompts/concise.txt b/src/utils/prompts/concise.txt index b527290..6fbd13d 100644 --- a/src/utils/prompts/concise.txt +++ b/src/utils/prompts/concise.txt @@ -1 +1,77 @@ -You are a concise AI assistant. Provide brief, direct answers without unnecessary elaboration. \ No newline at end of file +# Document Router System Prompt + +You are an intelligent document routing assistant that helps users find and access relevant documents from a knowledge base loaded with documents from the HPE Partner Portal. Your role is to analyze vector search results and present them to users in a clear, actionable format. The documents are technical and require utmost attention to detail Do no use general information, only what is returned from the search. + +## Instructions + +### Input Processing +You will receive vector search results containing document matches. Each result includes: +- `id`: Unique identifier for the search result +- `content`: Text snippet from the document +- `document_guid`: Unique identifier for the document (used in URLs) +- `object_key`: Document filename/key +- `length`: Length of the content snippet +- `distance`: Semantic similarity score (lower = more relevant) + +### Response Format +Structure your responses as follows: + +1. **Brief Introduction**: Start with a concise statement acknowledging the user's query +2. **Document Options**: Present 3-5 most relevant documents (lowest distance scores) +3. **Clear Instructions**: Tell users how to access documents +4. **Additional Context**: Provide brief context about what each document contains + +### Document Presentation Guidelines + +For each relevant document: +- **Title**: Extract a readable title from the object_key (remove file extensions, decode URL encoding, format nicely) +- +- **Relevance**: Briefly explain why this document matches their query +- +- **Content Preview**: Show a cleaned version of the content snippet + + +### Response Structure Template + +``` +Based on your query, I found [X] relevant documents in our knowledge base: + +## Recommended Documents + +## 1. [Document Title] +**Relevance**: [Brief explanation of why this matches] +**Preview**: "[Clean content snippet]" + +## 2. [Document Title] +[Same format as above] + +## How to Set Documents +You can adjust the document settings using the menu above. + +[Optional: Additional context or suggestions for refining the search] +``` + +### Quality Guidelines + +- **Relevance Ranking**: Always sort by distance (ascending) to show most relevant first +- **Content Cleaning**: Remove excessive whitespace, formatting artifacts, and truncated sentences +- **Title Extraction**: Convert technical filenames into human-readable titles +- **Conciseness**: Keep previews to 1-2 sentences that capture the key information +- **User Focus**: Frame everything from the user's perspective and needs + +### Special Cases + +- **No Results**: If no documents have reasonable relevance (distance > 0.8), suggest the user refine their query +- **Single Result**: Still use the structured format but acknowledge it's the single best match +- **Technical Documents**: Briefly explain technical content in accessible language +- **Multiple Formats**: If documents are in different formats (PDF, Word, etc.), mention this in the context + +### Error Handling + +If document_guid or other critical fields are missing, acknowledge the issue and suggest the user contact support while still presenting any available information. + +## Example Response Style + +"I found several documents related to your query about HPE servers. The most relevant appears to be the HPE ProLiant DL380a Gen12 technical documentation, which contains detailed specifications and setup information. Click the links below to access the full documents." + +Remember: Your goal is to be helpful, clear, and action-oriented. Users should immediately understand what documents are available and how to access them. \ No newline at end of file diff --git a/src/utils/prompts/default.txt b/src/utils/prompts/default.txt index 8212cf1..39d5766 100644 --- a/src/utils/prompts/default.txt +++ b/src/utils/prompts/default.txt @@ -1,42 +1,105 @@ -# RAG System Instruction +# RAG Assistant Instructions -You are a retrieval-augmented generation (RAG) assistant. Your responses must be based EXCLUSIVELY on the provided context from the knowledge base. Follow these strict guidelines: +You are a retrieval-augmented generation assistant. Answer questions using **only** the provided context documents. ## Core Rules -1. **Answer only from provided context**: You may ONLY use information explicitly present in the retrieved documents/context provided to you. Do not use your general knowledge or training data. +**Source Constraint**: Use only information explicitly present in the provided context. Never supplement with general knowledge or training data. -2. **No hallucination**: If the answer to a question is not found in the provided context, you must say "I cannot find information about [topic] in the provided documents" or similar. +**No Hallucination**: If information isn't in the context, state clearly: "I cannot find information about [topic] in the provided documents." -3. **Direct citations**: When answering, reference the specific document or section where you found the information (e.g., "According to Document 2, Section 3.1..." or "Based on the provided context..."). +**Always Cite**: Reference specific documents or sections (e.g., "According to Document 2..." or "Based on Section 3.1..."). -4. **Exact information only**: Do not infer, extrapolate, or make assumptions beyond what is explicitly stated in the context. +**Stick to Facts**: Don't infer, extrapolate, or make assumptions beyond what's explicitly stated. -5. **Partial answers are acceptable**: If the context only partially answers a question, provide what information is available and clearly state what aspects cannot be answered from the provided materials. +## Response Guidelines -## Response Format +- Answer directly when information is available +- Cite your sources +- For missing information, clearly state limitations +- Partial answers are fine—explain what you can and cannot answer -- Begin with a direct answer if available in the context -- Cite specific sources when possible -- If no relevant information exists in the context, clearly state this limitation -- For complex questions, break down which parts can and cannot be answered from the provided context +## Examples -## What NOT to do +**Available information**: "According to the user manual (Section 4.2), the device requires a 12V power supply." -- Do not supplement context information with general knowledge -- Do not make educated guesses or logical leaps beyond the provided text -- Do not provide information you "know" to be true if it's not in the context -- Do not rephrase or reinterpret context in ways that change its meaning +**Missing information**: "I cannot find pricing information in the provided documents. The context covers technical specifications but not cost details." -## Example Responses +**Partial information**: "The documentation confirms the software supports Windows and Mac (Installation Guide, p. 3), but doesn't specify Linux compatibility." -**When information is available:** -"Based on the provided documentation, [specific answer with citation]." +Your credibility depends on accurately representing the limits of the provided context. -**When information is not available:** -"I cannot find information about [specific topic] in the provided documents. The available context covers [briefly mention what is covered] but does not address your specific question." +## Standardized Response Format -**When information is partial:** -"The provided context indicates [available information with citation]. However, I cannot find information about [missing aspects] in the documents provided." +Use this markdown structure for all responses: -Remember: Your credibility depends on being accurate about the limits of your knowledge based on the provided context. \ No newline at end of file +### Response +[Provide the main answer if available in context, or state limitations clearly] + +### Source Information +| Document | Section/Page | Key Information | +|----------|--------------|-----------------| +| [Doc Name] | [Location] | [Relevant excerpt or summary] | +| [Doc Name] | [Location] | [Relevant excerpt or summary] | + +### Context Coverage +**Available in context:** +- [Topic 1 with brief description] +- [Topic 2 with brief description] + +**Not covered in context:** +- [Missing topic 1] +- [Missing topic 2] + +### Additional Notes +[Any clarifications, limitations, or partial information warnings] + +## Response Templates + +**Full Answer Available:** +```markdown +### Direct Answer +Based on the provided documentation, [complete answer]. + +### Source Information +| Document | Section | Key Information | +|----------|---------|-----------------| +| User Manual | Section 4.2 | Device requires 12V power supply | + +### Context Coverage +**Available in context:** Technical specifications, installation requirements +**Not covered in context:** Pricing, warranty information +``` + +**Information Not Available:** +```markdown +### Direct Answer +I cannot find information about [specific topic] in the provided documents. + +### Context Coverage +**Available in context:** +- [List what IS covered] +- [Other available topics] + +**Not covered in context:** +- [The requested topic] +- [Other missing information] +``` + +**Partial Information:** +```markdown +### Direct Answer +The provided context partially addresses your question: [available information]. + +### Source Information +| Document | Section | Key Information | +|----------|---------|-----------------| +| [Doc Name] | [Location] | [What was found] | + +### Context Coverage +**Available in context:** [Covered aspects] +**Not covered in context:** [Missing aspects that would complete the answer] + +### Additional Notes +To fully answer your question, information about [missing elements] would be needed. +``` \ No newline at end of file diff --git a/src/utils/systemInstructions.ts b/src/utils/systemInstructions.ts index f7a6c3f..0a1b7a9 100644 --- a/src/utils/systemInstructions.ts +++ b/src/utils/systemInstructions.ts @@ -7,44 +7,211 @@ export interface SystemInstruction { content: string; } -// Load prompt content from file -const loadPromptFromFile = (id: string): string => { - // Only run on server side - if (typeof window === 'undefined') { - try { - // Use require instead of import for server-side only - const fs = require('fs'); - const path = require('path'); - - const filePath = path.join(process.cwd(), 'src', 'utils', 'prompts', `${id}.txt`); - if (fs.existsSync(filePath)) { - return fs.readFileSync(filePath, 'utf8'); - } - } catch (error) { - console.error(`Error loading prompt file for ${id}:`, error); - } - } - return ''; -}; +// Hardcoded prompt content +const DEFAULT_PROMPT = `# RAG Assistant Instructions + +You are a retrieval-augmented generation assistant. Answer questions using **only** the provided context documents. + +## Core Rules + +**Source Constraint**: Use only information explicitly present in the provided context. Never supplement with general knowledge or training data. + +**No Hallucination**: If information isn't in the context, state clearly: "I cannot find information about [topic] in the provided documents." + +**Always Cite**: Reference specific documents or sections (e.g., "According to Document 2..." or "Based on Section 3.1..."). + +**Stick to Facts**: Don't infer, extrapolate, or make assumptions beyond what's explicitly stated. + +## Response Guidelines + +- Answer directly when information is available +- Cite your sources +- For missing information, clearly state limitations +- Partial answers are fine—explain what you can and cannot answer + +## Examples + +**Available information**: "According to the user manual (Section 4.2), the device requires a 12V power supply." + +**Missing information**: "I cannot find pricing information in the provided documents. The context covers technical specifications but not cost details." + +**Partial information**: "The documentation confirms the software supports Windows and Mac (Installation Guide, p. 3), but doesn't specify Linux compatibility." + +Your credibility depends on accurately representing the limits of the provided context. + +## Standardized Response Format + +Use this markdown structure for all responses: + +### Response +[Provide the main answer if available in context, or state limitations clearly] + +### Source Information +| Document | Section/Page | Key Information | +|----------|--------------|-----------------| +| [Doc Name] | [Location] | [Relevant excerpt or summary] | +| [Doc Name] | [Location] | [Relevant excerpt or summary] | + +### Context Coverage +**Available in context:** +- [Topic 1 with brief description] +- [Topic 2 with brief description] + +**Not covered in context:** +- [Missing topic 1] +- [Missing topic 2] + +### Additional Notes +[Any clarifications, limitations, or partial information warnings] + +## Response Templates + +**Full Answer Available:** +\`\`\`markdown +### Direct Answer +Based on the provided documentation, [complete answer]. + +### Source Information +| Document | Section | Key Information | +|----------|---------|-----------------| +| User Manual | Section 4.2 | Device requires 12V power supply | + +### Context Coverage +**Available in context:** Technical specifications, installation requirements +**Not covered in context:** Pricing, warranty information +\`\`\` + +**Information Not Available:** +\`\`\`markdown +### Direct Answer +I cannot find information about [specific topic] in the provided documents. + +### Context Coverage +**Available in context:** +- [List what IS covered] +- [Other available topics] + +**Not covered in context:** +- [The requested topic] +- [Other missing information] +\`\`\` + +**Partial Information:** +\`\`\`markdown +### Direct Answer +The provided context partially addresses your question: [available information]. + +### Source Information +| Document | Section | Key Information | +|----------|---------|-----------------| +| [Doc Name] | [Location] | [What was found] | + +### Context Coverage +**Available in context:** [Covered aspects] +**Not covered in context:** [Missing aspects that would complete the answer] + +### Additional Notes +To fully answer your question, information about [missing elements] would be needed. +\`\`\``; + +const CONCISE_PROMPT = `# Document Router System Prompt + +You are an intelligent document routing assistant that helps users find and access relevant documents from a knowledge base loaded with documents from the HPE Partner Portal. Your role is to analyze vector search results and present them to users in a clear, actionable format. The documents are technical and require utmost attention to detail Do no use general information, only what is returned from the search. + +## Instructions + +### Input Processing +You will receive vector search results containing document matches. Each result includes: +- \`id\`: Unique identifier for the search result +- \`content\`: Text snippet from the document +- \`document_guid\`: Unique identifier for the document (used in URLs) +- \`object_key\`: Document filename/key +- \`length\`: Length of the content snippet +- \`distance\`: Semantic similarity score (lower = more relevant) + +### Response Format +Structure your responses as follows: + +1. **Brief Introduction**: Start with a concise statement acknowledging the user's query +2. **Document Options**: Present 3-5 most relevant documents (lowest distance scores) +3. **Clear Instructions**: Tell users how to access documents +4. **Additional Context**: Provide brief context about what each document contains + +### Document Presentation Guidelines + +For each relevant document: +- **Title**: Extract a readable title from the object_key (remove file extensions, decode URL encoding, format nicely) +- +- **Relevance**: Briefly explain why this document matches their query +- +- **Content Preview**: Show a cleaned version of the content snippet + + +### Response Structure Template + +\`\`\` +Based on your query, I found [X] relevant documents in our knowledge base: + +## Recommended Documents + +## 1. [Document Title] +**Relevance**: [Brief explanation of why this matches] +**Preview**: "[Clean content snippet]" + +## 2. [Document Title] +[Same format as above] + +## How to Set Documents +You can adjust the document settings using the menu above. + +[Optional: Additional context or suggestions for refining the search] +\`\`\` + +### Quality Guidelines + +- **Relevance Ranking**: Always sort by distance (ascending) to show most relevant first +- **Content Cleaning**: Remove excessive whitespace, formatting artifacts, and truncated sentences +- **Title Extraction**: Convert technical filenames into human-readable titles +- **Conciseness**: Keep previews to 1-2 sentences that capture the key information +- **User Focus**: Frame everything from the user's perspective and needs + +### Special Cases + +- **No Results**: If no documents have reasonable relevance (distance > 0.8), suggest the user refine their query +- **Single Result**: Still use the structured format but acknowledge it's the single best match +- **Technical Documents**: Briefly explain technical content in accessible language +- **Multiple Formats**: If documents are in different formats (PDF, Word, etc.), mention this in the context + +### Error Handling + +If document_guid or other critical fields are missing, acknowledge the issue and suggest the user contact support while still presenting any available information. + +## Example Response Style + +"I found several documents related to your query about HPE servers. The most relevant appears to be the HPE ProLiant DL380a Gen12 technical documentation, which contains detailed specifications and setup information. Click the links below to access the full documents." + +Remember: Your goal is to be helpful, clear, and action-oriented. Users should immediately understand what documents are available and how to access them.`; + +const CREATIVE_PROMPT = `You are a creative AI assistant. Think outside the box and provide innovative, imaginative responses.`; // Get system instructions from localStorage or use defaults const getStoredInstructions = (): SystemInstruction[] => { - // Default instructions with content from files + // Default instructions with hardcoded content const defaultInstructions = [ { id: "default", - name: "Default", - content: loadPromptFromFile("default") + name: "Step Two", + content: DEFAULT_PROMPT }, { id: "concise", - name: "Concise", - content: loadPromptFromFile("concise") + name: "Step One", + content: CONCISE_PROMPT }, { id: "creative", name: "Creative", - content: loadPromptFromFile("creative") + content: CREATIVE_PROMPT } ]; From 8bcca724e438a1f08e2ea69898ce1932f2da02ea Mon Sep 17 00:00:00 2001 From: Ashley Judah Date: Wed, 23 Jul 2025 10:37:07 -0400 Subject: [PATCH 09/11] styling and layout changes --- public/Hewlett_Packard_Enterprise_logo.svg | 88 ++++ src/app/globals.css | 205 ++++++-- src/app/page.tsx | 485 +++++++++++------- src/components/app-navbar.tsx | 250 +++------ .../modal/add-system-instruction-modal.tsx | 131 +++-- .../modal/edit-system-instruction-modal.tsx | 130 +++-- 6 files changed, 787 insertions(+), 502 deletions(-) create mode 100644 public/Hewlett_Packard_Enterprise_logo.svg diff --git a/public/Hewlett_Packard_Enterprise_logo.svg b/public/Hewlett_Packard_Enterprise_logo.svg new file mode 100644 index 0000000..f96c231 --- /dev/null +++ b/public/Hewlett_Packard_Enterprise_logo.svg @@ -0,0 +1,88 @@ + + +Hewlett Packard Enterprise logo + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/globals.css b/src/app/globals.css index 642fa16..dd4768a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -15,10 +15,11 @@ --hpe-gray-lighter: #e8e8e8; --hpe-gray-lightest: #f7f7f7; --hpe-white: #ffffff; - + /* Apply HPE colors to existing variables */ --foreground-rgb: 255, 255, 255; - --background-start-rgb: 247, 247, 247; /* Light gray background */ + --background-start-rgb: 247, 247, 247; + /* Light gray background */ --background-end-rgb: 247, 247, 247; } @@ -42,25 +43,60 @@ main { /* Override black backgrounds with HPE white */ .bg-black { - background-color: var(--hpe-white) ; + background-color: var(--hpe-white); } /* Override dark gray backgrounds */ .bg-\[\#0a0a0a\]\/80 { - background-color: var(--hpe-white) ; - border-color: var(--hpe-gray-light) ; + background-color: var(--hpe-white); + border-color: var(--hpe-gray-light); } /* Navigation bar styling */ nav { - background-color: var(--hpe-white) ; - border-bottom: 1px solid var(--hpe-gray-lighter) ; + background-color: var(--hpe-white); + border-bottom: 1px solid var(--hpe-gray-lighter); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } +.chat-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background-color: var(--hpe-white); + border-bottom: 1px solid var(--border); + padding: 10px 24px; +} + +.header-content { + display: flex; + align-items: center; + gap: 20px; +} + +.logo-container { + width: 140px; + display: flex; + align-items: center; +} + +.logo { + width: 100%; + height: auto; + object-fit: contain; +} + +.header-title { + font-size: 18px; + font-weight: 600; + color: var(--hpe-gray-dark); + letter-spacing: -0.01em; +} + /* Sidebar styling */ .border-white\/10 { - border-color: var(--hpe-gray-lighter) ; + border-color: var(--hpe-gray-lighter); } /* Sidebar background when open */ @@ -70,12 +106,12 @@ motion\:div:has(button) { /* New Chat button */ .bg-white\/80 { - background-color: var(--hpe-green) ; - color: var(--hpe-white) ; + background-color: var(--hpe-green); + color: var(--hpe-white); } .bg-white\/80:hover { - background-color: var(--hpe-green-dark) ; + background-color: var(--hpe-green-dark); } .btn-hpe-green { @@ -88,64 +124,122 @@ motion\:div:has(button) { /* Conversation items */ .hover\:bg-white\/5:hover { - background-color: var(--hpe-gray-lightest) ; + background-color: var(--hpe-gray-lightest); } /* Message containers */ .border-\[\#191919\] { - border-color: var(--hpe-gray-lighter) ; - background-color: var(--hpe-white) ; + border-color: var(--hpe-gray-lighter); + background-color: var(--hpe-white); } /* User messages */ -.ml-auto > .border-\[\#191919\] { - background-color: var(--hpe-white) ; - border-color: var(--hpe-gray-light) ; +.ml-auto>.border-\[\#191919\] { + background-color: var(--hpe-white); + border-color: var(--hpe-gray-light); } /* AI messages */ -.mr-auto > .border-\[\#191919\] { - background-color: var(--hpe-white) ; - border-color: var(--hpe-gray-lighter) ; +.mr-auto>.border-\[\#191919\] { + background-color: var(--hpe-white); + border-color: var(--hpe-gray-lighter); } /* Text colors */ .text-white { - color: var(--hpe-white) ; + color: var(--hpe-white); } .text-white\/50 { - color: var(--hpe-gray-medium) ; + color: var(--hpe-gray-medium); } .text-white\/80 { - color: var(--hpe-gray-medium) ; + color: var(--hpe-gray-medium); } .text-white\/90 { - color: var(--hpe-gray-dark) ; + color: var(--hpe-gray-dark); } /* Input fields */ -input, textarea { - background-color: var(--hpe-white) ; - border-color: var(--hpe-gray-light) ; - color: var(--hpe-gray-dark) ; +input, +textarea { + background-color: var(--hpe-white); + border-color: var(--hpe-gray-light); + color: var(--hpe-gray-dark); } -input:focus, textarea:focus { - border-color: var(--hpe-green) ; +input:focus, +textarea:focus { + border-color: var(--hpe-green); } -input::placeholder, textarea::placeholder { - color: var(--hpe-gray-medium) ; +input::placeholder, +textarea::placeholder { + color: var(--hpe-gray-medium); } /* Model selector button */ +.modal-backdrop { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.modal-content { + background-color: white; + border-radius: 8px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + padding: 24px; +} + +.modal-header { + display: flex; + justify-content: between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--hpe-gray-lighter); +} + +.modal-title { + font-size: 20px; + font-weight: 600; + color: var(--hpe-gray-dark); +} + +.modal-close { + padding: 8px; + border-radius: 4px; + cursor: pointer; +} + +/* HPE POC Badge */ +.preset-name-container { + background-color: var(--hpe-green); + padding: 4px 12px; + border-radius: 16px; +} + +.preset-name { + color: white; + font-size: 12px; + font-weight: 500; +} + nav button { - background-color: transparent ; - color: var(--hpe-green) ; - border: 1px solid var(--hpe-green) ; + background-color: transparent; + color: var(--hpe-green); + border: 1px solid var(--hpe-green); border-radius: 4px; padding: 4px 12px; font-weight: 500; @@ -153,38 +247,38 @@ nav button { } nav button:hover { - background-color: var(--hpe-green) ; - color: var(--hpe-white) ; + background-color: var(--hpe-green); + color: var(--hpe-white); } /* Icon colors */ .fill-white\/50 { - fill: var(--hpe-gray-medium) ; + fill: var(--hpe-gray-medium); } .fill-white\/75 { - fill: var(--hpe-gray-dark) ; + fill: var(--hpe-gray-dark); } .fill-white\/90 { - fill: var(--hpe-gray-dark) ; + fill: var(--hpe-gray-dark); } .fill-white { - fill: var(--hpe-white) ; + fill: var(--hpe-white); } /* Icon hover states */ .hover\:fill-white\/90:hover { - fill: var(--hpe-green) ; + fill: var(--hpe-green); } .hover\:fill-white\/75:hover { - fill: var(--hpe-green) ; + fill: var(--hpe-green); } /* Code blocks - HPE style */ -:not(pre) > code { +:not(pre)>code { background: var(--hpe-gray-lightest); border: 1px solid var(--hpe-gray-lighter); border-radius: 3px; @@ -194,7 +288,7 @@ nav button:hover { font-size: 0.9em; } -pre > code { +pre>code { width: 100%; display: flex; overflow-x: scroll; @@ -261,18 +355,18 @@ li { /* Command menu styling */ .bg-white\/10 { - background-color: var(--hpe-gray-lightest) ; - border: 1px solid var(--hpe-gray-light) ; + background-color: var(--hpe-gray-lightest); + border: 1px solid var(--hpe-gray-light); } /* Focus states */ .focus\:ring-white\/10:focus { - --tw-ring-color: var(--hpe-green) ; + --tw-ring-color: var(--hpe-green); } /* Menu toggle SVG */ svg path { - stroke: var(--hpe-gray-dark) ; + stroke: var(--hpe-gray-dark); } /* Add padding to messages container */ @@ -290,13 +384,13 @@ svg path { /* Timestamp styling */ .text-xs.text-white\/50 { - color: var(--hpe-gray-medium) ; + color: var(--hpe-gray-medium); font-size: 11px; } /* Markdown content styling */ .text-sm.text-white { - color: var(--hpe-gray-dark) ; + color: var(--hpe-gray-dark); font-size: 14px; line-height: 1.6; } @@ -322,9 +416,12 @@ svg path { /* Animation adjustments */ @keyframes pulse { - 0%, 100% { + + 0%, + 100% { opacity: 1; } + 50% { opacity: 0.5; } @@ -337,7 +434,7 @@ svg path { /* Ensure proper contrast for accessibility */ .placeholder\:text-white\/80::placeholder { - color: var(--hpe-gray-medium) ; + color: var(--hpe-gray-medium); } .prose { @@ -410,6 +507,7 @@ textarea:focus { from { transform: translateX(-100%); } + to { transform: translateX(0); } @@ -419,6 +517,7 @@ textarea:focus { from { transform: translateX(0); } + to { transform: translateX(-100%); } diff --git a/src/app/page.tsx b/src/app/page.tsx index c924c55..4a7a9ad 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -22,15 +22,13 @@ import { usePrompts } from "./context/PromptContext"; import { useSystemInstruction } from "./context/SystemInstructionContext"; import { getSystemInstructionById } from "@/utils/systemInstructions"; -// Define the DocumentEntry type to match the one in app-navbar.tsx +// Define the DocumentEntry type type DocumentEntry = { filename: string; guid: string; selected?: boolean; }; -// This is now defined in global.d.ts - export default function Home() { const { setModalConfig } = useModal(); const { activePromptTemplate, setActivePromptTemplate } = usePrompts(); @@ -48,9 +46,7 @@ export default function Home() { const [availableModels, setAvailableModels] = useState([]); const [activeModel, setActiveModel] = useState(""); const [ollama, setOllama] = useState(); - const [conversations, setConversations] = useState< - { title: string; filePath: string }[] - >([]); + const [conversations, setConversations] = useState<{ title: string; filePath: string }[]>([]); const [activeConversation, setActiveConversation] = useState(""); const [menuState, toggleMenuState] = useCycle(false, true); const [selectedDocuments, setSelectedDocuments] = useState([]); @@ -66,16 +62,63 @@ export default function Home() { // Get existing conversations getExistingConvos(); + + // Set up window.setDocumentValues function + setupWindowDocumentValues(); }, []); + // Function to handle document values from vector search + function setupWindowDocumentValues() { + // @ts-ignore + window.setDocumentValues = (documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => { + console.log('window.setDocumentValues called with:', documents); + + if (typeof documents === 'string' && guid) { + // Legacy support: single document with filename and guid + const newEntry: DocumentEntry = { + filename: documents, + guid: guid, + selected: true + }; + setSelectedDocuments(prevDocs => { + const exists = prevDocs.some(doc => doc.guid === guid); + if (!exists) { + return [...prevDocs, newEntry]; + } + return prevDocs; + }); + } else if (Array.isArray(documents)) { + // New format: array of DocumentEntry objects + setSelectedDocuments(prevDocs => { + const existingGuids = new Set(prevDocs.map(doc => doc.guid)); + const newDocs = documents.filter(doc => !existingGuids.has(doc.guid)); + return [...prevDocs, ...newDocs]; + }); + } else if (typeof documents === 'object') { + // Single DocumentEntry object + const doc = documents as DocumentEntry; + setSelectedDocuments(prevDocs => { + const exists = prevDocs.some(d => d.guid === doc.guid); + if (!exists) { + return [...prevDocs, { ...doc, selected: true }]; + } + return prevDocs; + }); + } + }; + + return () => { + // @ts-ignore + delete window.setDocumentValues; + }; + } + function getInitialModel() { fetch(`${baseUrl}/api/tags`) .then((response) => response.json()) .then((data) => { - // console.log(data); setAvailableModels(data.models); - // get initial model from local storage const storedModel = localStorage.getItem("initialLocalLM"); if ( storedModel && @@ -92,7 +135,6 @@ export default function Home() { }); setOllama(newOllama); } else { - // set initial model to first model in list setActiveModel(data.models[0]?.name); const initOllama = new ChatOllama({ baseUrl: baseUrl, @@ -105,7 +147,7 @@ export default function Home() { async function getExistingConvos() { fetch("../api/fs/get-convos", { - method: "POST", // or 'GET', 'PUT', etc. + method: "POST", body: JSON.stringify({ conversationPath: "./conversations", }), @@ -114,24 +156,19 @@ export default function Home() { "Content-Type": "application/json", }, }).then((response) => { - // console.log(response), response.json().then((data) => setConversations(data)); }); } async function performVectorSearch(query: string) { try { - // Check if there are any selected documents const selectedDocs = selectedDocuments.filter(doc => doc.selected); let response; if (selectedDocs.length > 0) { - // If documents are selected, use /vector/search API with document_guids const document_guids = selectedDocs.map(doc => doc.guid); - console.log(`Using /vector/search API with ${document_guids.length} selected documents`); - response = await fetch(`${vectorSearchBaseUrl}/v1.0/vector/search`, { method: 'POST', headers: { @@ -145,9 +182,6 @@ export default function Home() { }) }); } else { - // If no documents are selected, use /vector/router/search API - console.log('Using /vector/router/search API (no documents selected)'); - response = await fetch(`${vectorSearchBaseUrl}/v1.0/vector/router/search`, { method: 'POST', headers: { @@ -176,19 +210,33 @@ export default function Home() { } async function triggerPrompt(input: string = newPrompt) { - if (!ollama) return; + + if (!ollama) { + console.error("Ollama is not initialized!"); + return; + } + scrollToBottom(); - if (messages.length == 0) getName(input); + + if (messages.length === 0) { + console.log("First message, getting name..."); + getName(input); + } + const msg = { type: "human", id: generateRandomString(8), timestamp: Date.now(), content: input, }; + + console.log("Creating message:", msg); + const model = activeModel; let streamedText = ""; - messages.push(msg); - const msgCache = [...messages]; + + const msgCache = [...messages, msg]; + setMessages(msgCache); // First perform vector search const vectorSearchResults = await performVectorSearch(input); @@ -196,16 +244,13 @@ export default function Home() { // Prepare context from vector search results let contextFromVectorSearch = ""; if (vectorSearchResults && vectorSearchResults.length > 0) { - // Update Document dropdown with all results if (window.setDocumentValues) { - // Convert vector search results to DocumentEntry array const documentEntries: DocumentEntry[] = vectorSearchResults.map((result: any) => ({ filename: decodeURIComponent(result.object_key), guid: result.document_guid, selected: true })); - // Pass all document entries to the dropdown window.setDocumentValues(documentEntries); } @@ -217,8 +262,6 @@ export default function Home() { } // Use the context in the request to Ollama - // Add system instruction at the beginning of the message array - // If no documents are selected, use the Concise system instruction const selectedDocs = selectedDocuments.filter(doc => doc.selected); const systemInstruction = selectedDocs.length === 0 ? getSystemInstructionById("concise") || activeSystemInstruction @@ -228,9 +271,8 @@ export default function Home() { const messagesWithSystemInstruction = [ new SystemMessage(systemInstruction.content), - ...messages.map((m, index) => { - if (index === messages.length - 1 && m.type === "human") { - // Replace the last human message with the context-enhanced version + ...msgCache.map((m, index) => { + if (index === msgCache.length - 1 && m.type === "human") { return new HumanMessage(contextFromVectorSearch); } else { return m.type === "human" @@ -240,41 +282,56 @@ export default function Home() { }) ]; - const stream = await ollama.stream(messagesWithSystemInstruction); + try { + const stream = await ollama.stream(messagesWithSystemInstruction); + + setNewPrompt(""); + setActivePromptTemplate(undefined); + + let updatedMessages = [...msgCache]; + let c = 0; + + for await (const chunk of stream) { + streamedText += chunk.content; + const aiMsg = { + type: "ai", + id: generateRandomString(8), + timestamp: Date.now(), + content: streamedText, + model, + }; + updatedMessages = [...msgCache, aiMsg]; + setMessages(updatedMessages); + c++; + if (c % 8 == 0) scrollToBottom(); + } - setNewPrompt(""); - setActivePromptTemplate(undefined); - let updatedMessages = [...msgCache]; - let c = 0; - for await (const chunk of stream) { - streamedText += chunk.content; - const aiMsg = { + scrollToBottom(); + persistConvo(updatedMessages); + } catch (error) { + console.error("Error streaming from Ollama:", error); + // Optionally add an error message to the chat + const errorMsg = { type: "ai", id: generateRandomString(8), timestamp: Date.now(), - content: streamedText, + content: "Sorry, I encountered an error while processing your request. Please try again.", model, }; - updatedMessages = [...msgCache, aiMsg]; - setMessages(() => updatedMessages); - c++; - if (c % 8 == 0) scrollToBottom(); + const updatedMessages = [...msgCache, errorMsg]; + setMessages(updatedMessages); } - - scrollToBottom(); - persistConvo(updatedMessages); } async function persistConvo(messages: any[]) { let name = activeConversation; if (name == "") { name = (await getName(newPrompt)).trim(); - // console.log(name.trim()); setActiveConversation(name.trim()); } fetch("../api/fs/persist-convo", { - method: "POST", // or 'GET', 'PUT', etc. + method: "POST", body: JSON.stringify({ conversationPath: "./conversations", messages: messages, @@ -313,52 +370,39 @@ export default function Home() { messages.findIndex((m) => m.id == activeMsg.id) - (activeMsg.type == "human" ? 0 : 1); let filtered = messages.filter((m, i) => index >= i); - // console.log("filtered", filtered); setMessages(() => filtered); - // useEffect on change here if the last value was a human message? const model = activeModel; let streamedText = ""; const msgCache = [...filtered]; - // Get the last human message for vector search const lastHumanMessage = filtered.filter(m => m.type === "human").pop(); let vectorSearchResults: any[] | null = null; if (lastHumanMessage) { - // Perform vector search with the last human message vectorSearchResults = await performVectorSearch(lastHumanMessage.content); - // Update Document dropdown with all results if (vectorSearchResults && vectorSearchResults.length > 0 && window.setDocumentValues) { - // Convert vector search results to DocumentEntry array const documentEntries: DocumentEntry[] = vectorSearchResults.map((result: any) => ({ filename: decodeURIComponent(result.object_key), guid: result.document_guid, selected: true })); - // Pass all document entries to the dropdown - // @ts-ignore window.setDocumentValues(documentEntries); } } - // Prepare the messages for Ollama, including vector search results if available - // If no documents are selected, use the Concise system instruction const selectedDocs = selectedDocuments.filter(doc => doc.selected); const systemInstruction = selectedDocs.length === 0 ? getSystemInstructionById("concise") || activeSystemInstruction : activeSystemInstruction; - console.log(`Using ${systemInstruction.name} system instruction`); - const messagesForOllama = [ new SystemMessage(systemInstruction.content), ...filtered.map((m, index) => { if (index === filtered.length - 1 && m.type === "human" && vectorSearchResults && vectorSearchResults.length > 0) { - // Enhance the last human message with vector search results const contextFromVectorSearch = "Context from vector search:\n" + vectorSearchResults.map((result: any) => result.content).join("\n") + "\n\nUser query: " + m.content; @@ -442,7 +486,12 @@ export default function Home() { } else if (Array.isArray(documents)) { setSelectedDocuments(documents); } else { - setSelectedDocuments([documents]); + if (typeof documents === 'string') { + console.error("Expected DocumentEntry, received string:", documents); + setSelectedDocuments([]); + } else { + setSelectedDocuments([documents]); + } } }} toggleMenuState={toggleMenuState} @@ -455,163 +504,213 @@ export default function Home() { > { - }} + setDocumentName={() => { }} activeModel={activeModel} availableModels={availableModels} setActiveModel={setActiveModel} setOllama={setOllama} - setDocumentValues={(documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => { - if (typeof documents === 'string' && guid) { - const newEntry: DocumentEntry = { - filename: documents as string, - guid: guid as string, - selected: true - }; - setSelectedDocuments([newEntry]); - } else if (Array.isArray(documents)) { - setSelectedDocuments(documents); - } else { - setSelectedDocuments([documents as DocumentEntry]); - } + selectedDocuments={selectedDocuments} + onDocumentToggle={(index: number) => { + setSelectedDocuments(prevDocs => + prevDocs.map((doc, i) => + i === index + ? { ...doc, selected: !doc.selected } + : doc + ) + ); }} /> -
-
-
-
- {messages.length === 0 ? ( -
-
-

Welcome to the HPE Partner Portal AI Assistant

-

Start a conversation to begin by telling us what system your interested in getting some information about.

-

That will help the system narrow down the scope and provide the best responses

-

You can then select which documents you wish to research from the navigation above

+ + {/* Main content area with documents panel */} +
+ {/* Chat area */} +
+
+
+
+ {messages.length === 0 ? ( +
+
+

Welcome to the HPE Partner Portal AI Assistant

+

Start a conversation to begin by telling us what system you're interested in getting some information about.

+

That will help the system narrow down the scope and provide the best responses

+

You can then select which documents you wish to research from the panel on the right

+
-
- ) : ( -
- {messages.map((msg) => ( -
+ ) : ( +
+ {messages.map((msg) => (
-
- - {msg?.model?.split(":")[0] || "You"} •{" "} - {new Date(msg.timestamp).toLocaleTimeString()} - -
- - {msg.content.trim()} - -
- {msg.type === "human" && ( - + + {msg?.model?.split(":")[0] || "You"} •{" "} + {new Date(msg.timestamp).toLocaleTimeString()} + +
+ + {msg.content.trim()} + +
+ {msg.type === "human" && ( + { + setModalConfig({ + modal: AppModal.SAVE_PROMPT, + data: msg, + }); + }} + className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" + /> + )} + refreshMessage(msg)} + className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" + /> + { - setModalConfig({ - modal: AppModal.SAVE_PROMPT, - data: msg, - }); + navigator.clipboard.writeText(msg.content); }} className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" /> - )} - refreshMessage(msg)} - className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" - /> - { - navigator.clipboard.writeText(msg.content); - }} - className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" - /> - { - deleteMessage(msg); - }} - className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" - /> + { + deleteMessage(msg); + }} + className="h-4 w-4 cursor-pointer fill-current opacity-60 hover:opacity-100" + /> +
-
- ))} + ))} +
+ )} +
+
+ +
+
+ +
+ {activePromptTemplate ? ( + { + if ( + x.e.key === "Enter" && + !x.e.metaKey && + !x.e.shiftKey && + !x.e.altKey && + newPrompt !== "" + ) { + triggerPrompt(x.input); + } + }} + /> + ) : ( + { + if (e.target.value != "\n") setNewPrompt(e.target.value); + }} + onKeyDown={(e) => { + if ( + e.key === "Enter" && + !e.metaKey && + !e.shiftKey && + !e.altKey && + newPrompt !== "" + ) { + triggerPrompt(); + } + }} + value={newPrompt} + placeholder="Send a message" + className="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 placeholder-gray-500 focus:border-[#01a982] focus:outline-none focus:ring-2 focus:ring-[#01a982] focus:ring-opacity-50" + /> + )}
- )} +
+
-
-
- -
- {activePromptTemplate ? ( - { - if ( - x.e.key === "Enter" && - !x.e.metaKey && - !x.e.shiftKey && - !x.e.altKey && - newPrompt !== "" - ) { - triggerPrompt(x.input); - } - }} - /> - ) : ( - { - if (e.target.value != "\n") setNewPrompt(e.target.value); - }} - onKeyDown={(e) => { - if ( - e.key === "Enter" && - !e.metaKey && - !e.shiftKey && - !e.altKey && - newPrompt !== "" - ) { - triggerPrompt(); - } - }} - value={newPrompt} - placeholder="Send a message" - className="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 placeholder-gray-500 focus:border-[#01a982] focus:outline-none focus:ring-2 focus:ring-[#01a982] focus:ring-opacity-50" - /> - )} + {/* Documents Panel - Always visible */} +
+
+

Documents

+

+ {selectedDocuments.length > 0 + ? `${selectedDocuments.filter(e => e.selected).length} of ${selectedDocuments.length} selected` + : 'No documents available' + } +

+
+ + {/* Documents List */} +
+ {selectedDocuments.length > 0 ? ( +
+ {selectedDocuments.map((entry, index) => ( +
+ { + const updatedDocuments = [...selectedDocuments]; + updatedDocuments[index] = { + ...updatedDocuments[index], + selected: !updatedDocuments[index].selected + }; + setSelectedDocuments(updatedDocuments); + }} + className="mt-1 h-4 w-4 text-[#01a982] rounded border-gray-300 focus:ring-[#01a982]" + /> + +
+ ))}
-
+ ) : ( +
+ + + +

No documents available

+
+ )}
); -} +} \ No newline at end of file diff --git a/src/components/app-navbar.tsx b/src/components/app-navbar.tsx index 1efcd07..ec6d5da 100644 --- a/src/components/app-navbar.tsx +++ b/src/components/app-navbar.tsx @@ -19,7 +19,8 @@ type Props = { availableModels: any[]; setActiveModel: Function; setOllama: Function; - setDocumentValues?: (documents: DocumentEntry[] | DocumentEntry | string, guid?: string) => void; // Function to set document entries + selectedDocuments: DocumentEntry[]; + onDocumentToggle: (index: number) => void; }; export default function AppNavbar({ @@ -29,13 +30,11 @@ export default function AppNavbar({ availableModels, setActiveModel, setOllama, - setDocumentValues, + selectedDocuments, + onDocumentToggle, }: Props) { - const [isShareMenuOpen, setIsShareMenuOpen] = useState(false); - const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const [isSystemMenuOpen, setIsSystemMenuOpen] = useState(false); const [isDocumentMenuOpen, setIsDocumentMenuOpen] = useState(false); - const [documentEntries, setDocumentEntries] = useState([]); const { activeSystemInstruction, allSystemInstructions, setActiveSystemInstructionById } = useSystemInstruction(); const { setModalConfig } = useModal(); @@ -44,27 +43,11 @@ export default function AppNavbar({ const handleInputChange = (event: React.ChangeEvent) => { const value = event.target.value; - setDocumentName(value); // Call the callback function to update the parent component + setDocumentName(value); }; useEffect(() => { const handleDocumentClick = (event: any) => { - if ( - isShareMenuOpen && - shareMenuRef.current && - !shareMenuRef.current.contains(event.target) - ) { - setIsShareMenuOpen(false); - } - - if ( - isProfileMenuOpen && - profileMenuRef.current && - !profileMenuRef.current.contains(event.target) - ) { - setIsProfileMenuOpen(false); - } - if ( isSystemMenuOpen && systemMenuRef.current && @@ -87,7 +70,7 @@ export default function AppNavbar({ return () => { document.removeEventListener("click", handleDocumentClick); }; - }, [isShareMenuOpen, isProfileMenuOpen, isSystemMenuOpen, isDocumentMenuOpen]); + }, [isSystemMenuOpen, isDocumentMenuOpen]); function toggleModel() { const i = @@ -99,157 +82,74 @@ export default function AppNavbar({ baseUrl: baseUrl, model: availableModels[i]?.name, }); - //store in local storage localStorage.setItem("initialLocalLM", availableModels[i]?.name); setOllama(newOllama); } - // Function to set document entries - function setDocumentValuesInternal(documents: DocumentEntry[] | DocumentEntry) { - console.log('setDocumentValuesInternal called with:', documents); - - // Handle both single document and array of documents - if (Array.isArray(documents)) { - // Filter out duplicates based on guid - setDocumentEntries(prevEntries => { - // Create a map of existing entries by guid for quick lookup - const existingEntriesMap = new Map( - prevEntries.map(entry => [entry.guid, entry]) - ); - - // Process each new document - documents.forEach(doc => { - // Only add if it doesn't exist already - if (!existingEntriesMap.has(doc.guid)) { - existingEntriesMap.set(doc.guid, { ...doc, selected: true }); - } - }); - - // Convert map back to array - return Array.from(existingEntriesMap.values()); - }); - } else { - // If it's a single document, add it to the array if it doesn't exist already - setDocumentEntries(prevEntries => { - const exists = prevEntries.some(entry => entry.guid === documents.guid); - if (!exists) { - return [...prevEntries, { ...documents, selected: true }]; - } - return prevEntries; - }); - } - - // Call the prop function if provided - if (setDocumentValues) { - console.log('Calling parent setDocumentValues function'); - setDocumentValues(documents); - } else { - console.log('Parent setDocumentValues function not provided'); - } - } - - // For backward compatibility - handle single document - function setDocumentValuesSingle(filename: string, guid: string) { - const newEntry: DocumentEntry = { filename, guid, selected: true }; - setDocumentValuesInternal(newEntry); - } - - // Expose the function to the window object for external access - useEffect(() => { - console.log('Setting up window.setDocumentValues'); - - // For backward compatibility, we need to handle both the new and old function signatures - // @ts-ignore - window.setDocumentValues = (filenameOrDocuments: string | DocumentEntry[], guid?: string) => { - if (typeof filenameOrDocuments === 'string' && guid) { - // Old signature: (filename: string, guid: string) - setDocumentValuesSingle(filenameOrDocuments, guid); - } else { - // New signature: (documents: DocumentEntry[] | DocumentEntry) - // @ts-ignore - setDocumentValuesInternal(filenameOrDocuments); - } - }; - - console.log('window.setDocumentValues is now:', typeof window.setDocumentValues); - - return () => { - console.log('Cleaning up window.setDocumentValues'); - // @ts-ignore - delete window.setDocumentValues; - }; - }, []); - - const shareMenuRef = useRef(null); - const profileMenuRef = useRef(null); - return ( diff --git a/src/components/modal/add-system-instruction-modal.tsx b/src/components/modal/add-system-instruction-modal.tsx index f2d8954..4e9917b 100644 --- a/src/components/modal/add-system-instruction-modal.tsx +++ b/src/components/modal/add-system-instruction-modal.tsx @@ -5,7 +5,6 @@ import { useSystemInstruction } from "@/app/context/SystemInstructionContext"; import { cn } from "@/utils/cn"; import { motion } from "framer-motion"; import { useState } from "react"; -import ExpandingTextInput from "../expanding-text-input"; export default function AddSystemInstructionModal() { const { addNewSystemInstruction } = useSystemInstruction(); @@ -38,67 +37,117 @@ export default function AddSystemInstructionModal() { visible: { opacity: 1 }, }; + const modalVariant = { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { + type: "spring", + damping: 25, + stiffness: 300 + } + }, + }; + return (
-
closeModal()} - className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0" - > -
+ e.stopPropagation()} - className="bg-white/black relative flex max-h-[80vh] transform flex-col overflow-hidden rounded-sm border border-white/20 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-3xl" + className="relative w-full max-w-2xl bg-white rounded-lg shadow-xl overflow-hidden" > -
- -
{"Add System Instruction"}
-
- -
- - {saveInstructionName(e)}} + {/* Modal Header */} +
+
+

+ Add System Instruction +

+ +
+
+ + {/* Modal Content */} +
+
+ {/* Instruction Name Field */} +
+ +
- -
- - {handleContentChange(e)}} + + {/* Instruction Content Field */} +
+ +