diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..85fc23e --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "sidekick", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..85fc23e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "sidekick", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/server/Dockerfile b/server/Dockerfile index 7fa0d5d..28873d0 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,15 +1,28 @@ FROM python:3.10-alpine -RUN apk add gcc musl-dev libffi-dev + +RUN apk add --no-cache gcc musl-dev libffi-dev + +RUN adduser -D -u 1000 sidekick + WORKDIR /sidekick_server + COPY Pipfile.lock ./ -RUN pip install --upgrade pip -RUN pip install pipenv -RUN pipenv requirements > requirements.txt -RUN pip install --no-cache-dir --upgrade -r requirements.txt +RUN pip install --upgrade pip && \ + pip install pipenv && \ + pipenv requirements > requirements.txt && \ + pip install --no-cache-dir --upgrade -r requirements.txt + COPY init.py app.py models.py routes.py utils.py docker-entrypoint.sh ./ +COPY custom_utils ./custom_utils COPY default_documents ./default_documents COPY default_settings ./default_settings COPY system_settings ./system_settings COPY migrations ./migrations + RUN chmod +x docker-entrypoint.sh -ENTRYPOINT ["/bin/sh", "docker-entrypoint.sh"] + +RUN chown -R sidekick:sidekick /sidekick_server + +USER sidekick + +ENTRYPOINT ["/bin/sh", "docker-entrypoint.sh"] \ No newline at end of file diff --git a/server/app.py b/server/app.py index f388883..1e4fb3d 100644 --- a/server/app.py +++ b/server/app.py @@ -20,6 +20,7 @@ app.config["JWT_ACCESS_TOKEN_EXPIRES"] = False app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["SQLALCHEMY_DATABASE_URI"] app.config["OPENAI_API_KEY"] = os.environ["OPENAI_API_KEY"] +app.config["OPENAI_BASE_URL"] = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") app.config["OPENAI_PROXY"] = os.environ.get("OPENAI_PROXY") # Optionallay count chat tokens if specified in the env var diff --git a/server/custom_utils/get_openai_token.py b/server/custom_utils/get_openai_token.py new file mode 100644 index 0000000..6289ed0 --- /dev/null +++ b/server/custom_utils/get_openai_token.py @@ -0,0 +1,4 @@ +from app import app + +def get_openai_token(): + return app.config['OPENAI_API_KEY'] diff --git a/server/default_settings/model_settings.json b/server/default_settings/model_settings.json index 2a90f11..d9a0e88 100644 --- a/server/default_settings/model_settings.json +++ b/server/default_settings/model_settings.json @@ -4,6 +4,15 @@ "providers": { "OpenAI": { "models": { + "gpt-4o": { + "temperature": 0.7, + "topP": 1, + "frequencyPenalty": 0, + "presencePenalty": 0, + "contextTokenSize": 128000, + "systemMessage": "You are a helpful advisor.", + "notes": "Points to the latest version of the gpt-4o (Omni) model. Cheaper and faster than gpt-4-turbo." + }, "gpt-4-turbo-preview": { "temperature": 0.7, "topP": 1, diff --git a/server/routes.py b/server/routes.py index 9c320e7..a99eb1e 100644 --- a/server/routes.py +++ b/server/routes.py @@ -2,7 +2,6 @@ import json import requests import socket -import uuid import sseclient from collections import OrderedDict @@ -10,6 +9,7 @@ from utils import DBUtils, construct_ai_request, RequestLogger,\ server_stats, increment_server_stat, openai_num_tokens_from_messages, \ get_random_string, num_characters_from_messages, update_default_settings +from custom_utils.get_openai_token import get_openai_token from flask import request, jsonify, Response, stream_with_context, redirect, session, url_for from flask_jwt_extended import get_jwt_identity, jwt_required, \ @@ -101,10 +101,10 @@ def test_ai(): with RequestLogger(request) as rl: increment_server_stat(category="requests", stat_name="healthAi") try: - url = 'https://api.openai.com/v1/chat/completions' + url = f"{app.config['OPENAI_BASE_URL']}/chat/completions" headers = { 'content-type': 'application/json; charset=utf-8', - 'Authorization': f"Bearer {app.config['OPENAI_API_KEY']}" + 'Authorization': f"Bearer {get_openai_token()}" } ai_request = { "model": "gpt-3.5-turbo", @@ -337,10 +337,10 @@ def construct_name_topic_request(request): return ai_request try: - url = 'https://api.openai.com/v1/chat/completions' + url = f"{app.config['OPENAI_BASE_URL']}/chat/completions" headers = { 'content-type': 'application/json; charset=utf-8', - 'Authorization': f"Bearer {app.config['OPENAI_API_KEY']}" + 'Authorization': f"Bearer {get_openai_token()}" } ai_request = construct_name_topic_request(request) promptCharacters = num_characters_from_messages(ai_request["messages"]) @@ -416,10 +416,10 @@ def construct_query_ai_request(request): promptCharacters = num_characters_from_messages(ai_request["messages"]) increment_server_stat(category="usage", stat_name="promptCharacters", increment=promptCharacters) increment_server_stat(category="usage", stat_name="totalCharacters", increment=promptCharacters) - url = 'https://api.openai.com/v1/chat/completions' + url = f"{app.config['OPENAI_BASE_URL']}/chat/completions" headers = { 'content-type': 'application/json; charset=utf-8', - 'Authorization': f"Bearer {app.config['OPENAI_API_KEY']}" + 'Authorization': f"Bearer {get_openai_token()}" } message_usage["prompt_characters"] = num_characters_from_messages( ai_request["messages"]) @@ -473,10 +473,10 @@ def chat_v2(): increment_server_stat(category="requests", stat_name="chatV2") def generate(): - url = 'https://api.openai.com/v1/chat/completions' + url = f"{app.config['OPENAI_BASE_URL']}/chat/completions" headers = { 'content-type': 'application/json; charset=utf-8', - 'Authorization': f"Bearer {app.config['OPENAI_API_KEY']}" + 'Authorization': f"Bearer {get_openai_token()}" } ai_request = construct_ai_request(request) ai_request["stream"] = True diff --git a/web_ui/src/Chat.js b/web_ui/src/Chat.js index e7cc7e4..d18717a 100644 --- a/web_ui/src/Chat.js +++ b/web_ui/src/Chat.js @@ -43,6 +43,7 @@ import AddOutlinedIcon from '@mui/icons-material/AddOutlined'; import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff'; import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; +import SchemaIcon from '@mui/icons-material/Schema'; import { SystemContext } from './SystemContext'; import ContentFormatter from './ContentFormatter'; @@ -306,6 +307,7 @@ const Chat = ({ const [systemPrompt, setSystemPrompt] = useState(""); const [promptPlaceholder, setPromptPlaceholder] = useState(userPromptReady.current); const [menuPromptsAnchorEl, setMenuPromptsAnchorEl] = useState(null); + const [menuDiagramsAnchorEl, setMenuDiagramsAnchorEl] = useState(null); const [menuPanelAnchorEl, setMenuPanelAnchorEl] = useState(null); const [menuPromptEditorAnchorEl, setMenuPromptEditorAnchorEl] = useState(null); const [menuMessageContext, setMenuMessageContext] = useState(null); @@ -1186,6 +1188,7 @@ const Chat = ({ handleMenuMessageContextClose(); handleMenuPromptEditorClose(); handleMenuPromptsClose(); + handleMenuDiagramsClose(); } const runMenuAction = (functionToRun, thenFocusOnPrompt=true) => { @@ -1204,6 +1207,14 @@ const Chat = ({ setMenuPromptsAnchorEl(null); }; + const handleMenuDiagramsOpen = (event) => { + setMenuDiagramsAnchorEl(event.currentTarget); + }; + + const handleMenuDiagramsClose = () => { + setMenuDiagramsAnchorEl(null); + }; + const handleMenuCommandsOpen = (event) => { setMenuCommandsAnchorEl(event.currentTarget); }; @@ -1362,6 +1373,20 @@ const Chat = ({ setMenuMessageContext(null); }; + const handleDeleteAllMessagesUpToHere = () => { + const updatedMessages = messages.slice(menuMessageContext.index + 1); + setMessages(updatedMessages); + setMenuMessageContext(null); + }; + + const handleDeleteAllMessagesFromHere = () => { + if (menuMessageContext.index > 0) { + const updatedMessages = messages.slice(0, menuMessageContext.index); + setMessages(updatedMessages); + setMenuMessageContext(null); + } + }; + const handleUseAsChatInput = () => { setChatPrompt(menuMessageContext.message.content); setPromptFocus(); @@ -1461,7 +1486,7 @@ const Chat = ({ (event) => { onClick && onClick(); - if (event.altKey) { + if (event.altKey || messages.length === 0) { runMenuAction(()=>{setChatPrompt(prompt)}); } else { runMenuAction(()=>{sendPrompt(prompt)}); @@ -1471,11 +1496,8 @@ const Chat = ({ onKeyDown={ (event) => { if (event.key === 'ArrowRight') { - setChatPrompt(prompt); onClick && onClick(); - if (chatPromptRef.current) { - chatPromptRef.current.focus(); - } + runMenuAction(()=>{setChatPrompt(prompt)}); } } } @@ -1505,6 +1527,7 @@ const Chat = ({ } const promptSelectionInstructions = "Click on a prompt to run it, ALT+Click (or Right-Arrow when using via slash command) to place in prompt editor so you can edit it"; + const diagramSelectionInstructions = "Click on a diagram (or press enter) to generate it based on context, ALT+Click (or Right-Arrow when using via slash command) to place in prompt editor so you can edit it and describe what you want"; const toolbar = + { + if (event.key === 'ArrowRight') { + handleMenuDiagramsOpen(event); + } + } + } + > + + Diagrams + + + + + {runMenuAction(togglePromptEngineerOpen, false);}}> + + Prompt Engineer + {runMenuAction(handleReload);}}> Reload last prompt for editing @@ -1675,25 +1718,22 @@ const Chat = ({ Delete last prompt/response - {runMenuAction(togglePromptEngineerOpen);}}> - - Prompt Engineer - {runMenuAction(handleNewChat);}}> New Chat - {runMenuAction(); handleToggleMarkdownRendering();}}> - { markdownRenderingOn ? : } - { markdownRenderingOn ? "Turn off markdown rendering" : "Turn on markdown rendering" } + {runMenuAction(handleToggleMarkdownRendering);}}> + { markdownRenderingOn ? : } + { markdownRenderingOn ? "Turn off markdown rendering" : "Turn on markdown rendering" } + { isMobile ? null : - {runMenuAction(); handleToggleWindowMaximise();}}> + {runMenuAction(handleToggleWindowMaximise);}}> { windowMaximized ? : } { windowMaximized ? "Shrink window" : "Expand window" } } - {runMenuAction(); handleClose();}}> + {runMenuAction(handleClose, false);}}> Close Window @@ -1753,6 +1793,8 @@ const Chat = ({ Delete this message Delete this and previous message + Delete this and all previous messages + Delete this and all subseqeunt messages Delete all messages + > @@ -1867,6 +1909,47 @@ const Chat = ({ + { + if (event.key === 'ArrowLeft') { + handleMenuDiagramsClose(event); + } + } + } + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + + + + Diagrams + + + + + + + + + + + + + + + + - - + { Object.keys(selectedAiLibraryNotes).length === 0 ? : } @@ -2469,11 +2554,17 @@ const Chat = ({ { aiLibraryOpen ? - - Loaded knowledge: { Object.keys(selectedAiLibraryNotes).length === 0 ? "None" : ""} + + AI Library + Loaded notes: {Object.keys(selectedAiLibraryNotes).length} - + + {setAiLibraryOpen(false)}}> + + + + {Object.values(selectedAiLibraryNotes).map(note =>( { const mermaidRef = useRef(null); const mermaidId = `mermaid-diagram-${uuidv4()}`; const [svg, setSvg] = useState(null); + const [error, setError] = useState(null); + const [showError, setShowError] = useState(false); useEffect(() => { mermaid.initialize({ startOnLoad: true, theme: 'default', securityLevel: 'loose', + suppressErrorRendering: true, }); }, []); @@ -23,20 +27,63 @@ const MermaidDiagram = memo(({ markdown }) => { }, [svg]); useEffect(() => { + let isCancelled = false; + if (mermaidRef.current && markdown !== "") { mermaid.render(mermaidId, markdown) - .then(({svg}) => { + .then(({svg}) => { + if (!isCancelled) { setSvg(svg); - }) - .catch((error) => { - console.error(error); - }); + } + }) + .catch((error) => { + if (!isCancelled) { + setError({ message: 'Error rendering mermaid diagram.', error: error}); + const mermaidInjectedErrorElement = document.getElementById(mermaidId); + if (mermaidInjectedErrorElement) { + // Hide the mermaid 'Syntax error in text' bomb message + mermaidInjectedErrorElement.style.display = 'none'; + } + } + }); } + + // This cleanup function is called when the component unmounts or when the markdown prop changes. + // It sets isCancelled to true, to prevent the .then and .catch callbacks from updating the state + // to avoid race conditions due to the asynchronous nature of the mermaid.render() function. + return () => { + isCancelled = true; + }; }, [markdown]); return ( -
-
+ + { + !error + ? +
+
+ : + + + + + {error.error.toString()} + + + + Here's the markdown that generated the error: + {markdown} + + + } +
); }); diff --git a/web_ui/src/Note.js b/web_ui/src/Note.js index 49cac7e..775a066 100644 --- a/web_ui/src/Note.js +++ b/web_ui/src/Note.js @@ -43,12 +43,14 @@ const Note = ({noteOpen, setNoteOpen, appendNoteContent, loadNote, createNote, d setNewPromptPart, setNewPrompt, setChatRequest, onChange, setOpenNoteId, modelSettings, persona, serverUrl, token, setToken, maxWidth, isMobile}) => { + const panelWindowRef = useRef(null); + const [notePanelKey, setNotePanelKey] = useState(Date.now()); // used to force re-renders + const StyledToolbar = styled(Toolbar)(({ theme }) => ({ backgroundColor: darkMode ? green[900] : green[300], marginRight: theme.spacing(2), })); - const panelWindowRef = useRef(null); const newNoteName = "New Note"; const systemPrompt = `You are DocumentGPT. You take CONTEXT_TEXT from a document along with a REQUEST to generate more text to include in the document. @@ -143,6 +145,7 @@ You always do your best to generate text in the same style as the context text p const noteInstantiated = useRef(false); const [renameInProcess, setRenameInProcess] = useState(false); const [timeToSave, setTimeToSave] = useState(false); + const [noteContentBuffer, setNoteContentBuffer] = useState(""); useEffect(() => { @@ -155,6 +158,7 @@ You always do your best to generate text in the same style as the context text p const setContent = (text) => { if (noteContentRef.current) { noteContentRef.current.innerText = text; + setNoteContentBuffer(text); noteInstantiated.current = true; if (saveStatus.current === "saved") { saveStatus.current = "changed"; @@ -213,8 +217,10 @@ Don't repeat the CONTEXT_TEXT or the REQUEST in your response. Create a response }; useEffect(()=>{ - if(pageLoaded && appendNoteContent.content !== "" && noteContentRef?.current) { - setNoteOpen({id: id, timestamp: Date.now()}); + if (markdownRenderingOn) { + system.warning("Note is now in markdown rendering mode. To enable edit, turn this off by clicking the markdown icon in the toolbar."); + } else if (pageLoaded && appendNoteContent.content !== "" && noteContentRef?.current) { + setNoteOpen({id: id, timestamp: Date.now()}); // to scroll the note into view if its off the screen let newNotePart = appendNoteContent.content.trim(); if(typeof newNotePart === "string") { let newNote = noteContentRef.current.innerText; @@ -721,7 +727,7 @@ Don't repeat the CONTEXT_TEXT or the REQUEST in your response. Create a response { markdownRenderingOn ? : }
- + { inAILibrary ? : } @@ -814,7 +820,7 @@ Don't repeat the CONTEXT_TEXT or the REQUEST in your response. Create a response ); - const render = { markdownRenderingOn && - + }
{ const system = useContext(SystemContext); + const [myMarkdown, setMyMarkdown] = useState(null); + const [myRenderedMarkdown, setMyRenderedMarkdown] = useState(null); + + useEffect(() => { + setMyMarkdown(markdown); + }, [markdown]); + + useEffect(() => { + if (myMarkdown) { + setMyRenderedMarkdown(renderMarkdown(myMarkdown)); + } + }, [myMarkdown]); const renderMarkdown = (markdown) => { try { @@ -24,6 +37,7 @@ const SidekickMarkdown = memo(({ markdown }) => { let language = match[1]; if (language === "" || !language) { language = "code"; } // provide a default if ``` used wuthout specifying a language const code = match[2]; + const codeMarkdown = `\`\`\`${language}\n${code.trim()}\n\`\`\`\n`; const startIndex = match.index; const endIndex = codeRegex.lastIndex; const before = markdown.slice(lastIndex, startIndex); @@ -35,7 +49,7 @@ const SidekickMarkdown = memo(({ markdown }) => { {language} { navigator.clipboard.writeText(code); event.stopPropagation(); }}> + onClick={(event) => { navigator.clipboard.writeText(codeMarkdown); event.stopPropagation(); }}> @@ -63,10 +77,7 @@ const SidekickMarkdown = memo(({ markdown }) => { return {markdown}; } }; - if (!markdown) { - return null; - } - const result = renderMarkdown(markdown); + const result = myRenderedMarkdown ? myRenderedMarkdown : null; return (result); }); diff --git a/web_ui/src/SystemContext.js b/web_ui/src/SystemContext.js index 679af84..f59a0c9 100644 --- a/web_ui/src/SystemContext.js +++ b/web_ui/src/SystemContext.js @@ -66,6 +66,7 @@ export const SystemProvider = ({ serverUrl, setStatusUpdates, setModalDialogInfo console.error(message, context ? context: context, error ? error : ""); }, warning: (message, context="") => { + setModalDialogInfo({ title: "Warning", message: message }); setStatusUpdates(prevStatusUpdates => [...prevStatusUpdates, { message: message, type: 'warning', timestamp: _dateTimeString() }]); console.log(message, context); },