2024-05-21 17:18:47 +08:00
const faqString = `
* * How can I expose the Ollama server ? * *
By default , Ollama allows cross origin requests from 127.0 . 0.1 and 0.0 . 0.0 .
To support more origins , you can use the OLLAMA _ORIGINS environment variable :
\ ` \` \`
OLLAMA _ORIGINS = $ { window . location . origin } ollama serve
\ ` \` \`
Also see : https : //github.com/jmorganca/ollama/blob/main/docs/faq.md
` ;
const clipboardIcon = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
< path d = "M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" / >
< path d = "M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" / >
< / s v g > `
const textBoxBaseHeight = 40 ; // This should match the default height set in CSS
// change settings of marked from default to remove deprecation warnings
// see conversation here: https://github.com/markedjs/marked/issues/2793
marked . use ( {
2024-05-21 18:31:32 +08:00
mangle : false ,
headerIds : false
2024-05-21 17:18:47 +08:00
} ) ;
function autoFocusInput ( ) {
2024-05-21 18:31:32 +08:00
const userInput = document . getElementById ( 'user-input' ) ;
userInput . focus ( ) ;
2024-05-21 17:18:47 +08:00
}
/ *
takes in model as a string
updates the query parameters of page url to include model name
* /
function updateModelInQueryString ( model ) {
2024-05-21 18:31:32 +08:00
// make sure browser supports features
if ( window . history . replaceState && 'URLSearchParams' in window ) {
const searchParams = new URLSearchParams ( window . location . search ) ;
searchParams . set ( "model" , model ) ;
// replace current url without reload
const newPathWithQuery = ` ${ window . location . pathname } ? ${ searchParams . toString ( ) } `
window . history . replaceState ( null , '' , newPathWithQuery ) ;
}
2024-05-21 17:18:47 +08:00
}
// Fetch available models and populate the dropdown
async function populateModels ( ) {
2024-05-21 18:31:32 +08:00
document . getElementById ( 'send-button' ) . addEventListener ( 'click' , submitRequest ) ;
2024-05-21 17:18:47 +08:00
2024-05-21 18:31:32 +08:00
try {
const data = await getModels ( ) ;
2024-05-21 17:18:47 +08:00
2024-05-21 18:31:32 +08:00
const selectElement = document . getElementById ( 'model-select' ) ;
2024-05-21 17:18:47 +08:00
2024-05-21 18:31:32 +08:00
// set up handler for selection
selectElement . onchange = ( ( ) => updateModelInQueryString ( selectElement . value ) ) ;
2024-05-21 17:18:47 +08:00
2024-05-21 18:31:32 +08:00
if ( data !== undefined ) {
data . models . forEach ( ( model ) => {
const option = document . createElement ( 'option' ) ;
option . value = model . name ;
option . innerText = model . name ;
selectElement . appendChild ( option ) ;
} ) ;
}
2024-05-21 17:18:47 +08:00
2024-05-21 18:31:32 +08:00
// select option present in url parameter if present
const queryParams = new URLSearchParams ( window . location . search ) ;
const requestedModel = queryParams . get ( 'model' ) ;
// update the selection based on if requestedModel is a value in options
if ( [ ... selectElement . options ] . map ( o => o . value ) . includes ( requestedModel ) ) {
selectElement . value = requestedModel ;
}
// otherwise set to the first element if exists and update URL accordingly
else if ( selectElement . options . length ) {
selectElement . value = selectElement . options [ 0 ] . value ;
updateModelInQueryString ( selectElement . value ) ;
}
} catch ( error ) {
document . getElementById ( 'errorText' ) . innerHTML =
DOMPurify . sanitize ( marked . parse (
` Ollama-ui was unable to communitcate with Ollama due to the following error: \n \n `
+ ` \` \` \` ${ error . message } \` \` \` \n \n --------------------- \n `
+ faqString ) ) ;
let modal = new bootstrap . Modal ( document . getElementById ( 'errorModal' ) ) ;
modal . show ( ) ;
2024-05-21 17:18:47 +08:00
}
}
// adjusts the padding at the bottom of scrollWrapper to be the height of the input box
function adjustPadding ( ) {
2024-05-21 18:31:32 +08:00
const inputBoxHeight = document . getElementById ( 'input-area' ) . offsetHeight ;
const scrollWrapper = document . getElementById ( 'scroll-wrapper' ) ;
scrollWrapper . style . paddingBottom = ` ${ inputBoxHeight + 15 } px ` ;
2024-05-21 17:18:47 +08:00
}
// sets up padding resize whenever input box has its height changed
const autoResizePadding = new ResizeObserver ( ( ) => {
2024-05-21 18:31:32 +08:00
adjustPadding ( ) ;
2024-05-21 17:18:47 +08:00
} ) ;
autoResizePadding . observe ( document . getElementById ( 'input-area' ) ) ;
// Function to get the selected model
function getSelectedModel ( ) {
2024-05-21 18:31:32 +08:00
return document . getElementById ( 'model-select' ) . value ;
2024-05-21 17:18:47 +08:00
}
// variables to handle auto-scroll
// we only need one ResizeObserver and isAutoScrollOn variable globally
// no need to make a new one for every time submitRequest is called
const scrollWrapper = document . getElementById ( 'scroll-wrapper' ) ;
let isAutoScrollOn = true ;
// autoscroll when new line is added
const autoScroller = new ResizeObserver ( ( ) => {
2024-05-21 18:31:32 +08:00
if ( isAutoScrollOn ) {
scrollWrapper . scrollIntoView ( { behavior : "smooth" , block : "end" } ) ;
}
2024-05-21 17:18:47 +08:00
} ) ;
// event listener for scrolling
let lastKnownScrollPosition = 0 ;
let ticking = false ;
document . addEventListener ( "scroll" , ( event ) => {
2024-05-21 18:31:32 +08:00
// if user has scrolled up and autoScroll is on we turn it off
if ( ! ticking && isAutoScrollOn && window . scrollY < lastKnownScrollPosition ) {
window . requestAnimationFrame ( ( ) => {
isAutoScrollOn = false ;
ticking = false ;
} ) ;
ticking = true ;
}
// if user has scrolled nearly all the way down and autoScroll is disabled, re-enable
else if ( ! ticking && ! isAutoScrollOn &&
window . scrollY > lastKnownScrollPosition && // make sure scroll direction is down
window . scrollY >= document . documentElement . scrollHeight - window . innerHeight - 30 // add 30px of space--no need to scroll all the way down, just most of the way
) {
window . requestAnimationFrame ( ( ) => {
isAutoScrollOn = true ;
ticking = false ;
} ) ;
ticking = true ;
}
lastKnownScrollPosition = window . scrollY ;
2024-05-21 17:18:47 +08:00
} ) ;
// Function to handle the user input and call the API functions
async function submitRequest ( ) {
2024-05-21 18:31:32 +08:00
document . getElementById ( 'chat-container' ) . style . display = 'block' ;
const input = document . getElementById ( 'user-input' ) . value ;
const selectedModel = getSelectedModel ( ) ;
const context = document . getElementById ( 'chat-history' ) . context ;
const systemPrompt = document . getElementById ( 'system-prompt' ) . value ;
const data = { model : selectedModel , prompt : input , context : context , system : systemPrompt } ;
// Create user message element and append to chat history
let chatHistory = document . getElementById ( 'chat-history' ) ;
let userMessageDiv = document . createElement ( 'div' ) ;
userMessageDiv . className = 'mb-2 user-message' ;
userMessageDiv . innerText = input ;
chatHistory . appendChild ( userMessageDiv ) ;
// Create response container
let responseDiv = document . createElement ( 'div' ) ;
responseDiv . className = 'response-message mb-2 text-start' ;
responseDiv . style . minHeight = '3em' ; // make sure div does not shrink if we cancel the request when no text has been generated yet
spinner = document . createElement ( 'div' ) ;
spinner . className = 'spinner-border text-light' ;
spinner . setAttribute ( 'role' , 'status' ) ;
responseDiv . appendChild ( spinner ) ;
chatHistory . appendChild ( responseDiv ) ;
// create button to stop text generation
let interrupt = new AbortController ( ) ;
let stopButton = document . createElement ( 'button' ) ;
stopButton . className = 'btn btn-danger' ;
stopButton . innerHTML = 'Stop' ;
stopButton . onclick = ( e ) => {
e . preventDefault ( ) ;
interrupt . abort ( 'Stop button pressed' ) ;
}
// add button after sendButton
const sendButton = document . getElementById ( 'send-button' ) ;
sendButton . insertAdjacentElement ( 'beforebegin' , stopButton ) ;
// change autoScroller to keep track of our new responseDiv
autoScroller . observe ( responseDiv ) ;
postRequest ( data , interrupt . signal )
. then ( async response => {
await getResponse ( response , parsedResponse => {
// let word = parsedResponse.response;
console . log ( response ) ;
console . log ( parsedResponse ) ;
let word = parsedResponse ? . choices [ 0 ] ? . message ? . content ;
console . log ( word ) ;
if ( parsedResponse . done ) {
chatHistory . context = parsedResponse . context ;
// Copy button
let copyButton = document . createElement ( 'button' ) ;
copyButton . className = 'btn btn-secondary copy-button' ;
copyButton . innerHTML = clipboardIcon ;
copyButton . onclick = ( ) => {
navigator . clipboard . writeText ( responseDiv . hidden _text ) . then ( ( ) => {
console . log ( 'Text copied to clipboard' ) ;
} ) . catch ( err => {
console . error ( 'Failed to copy text:' , err ) ;
} ) ;
} ;
responseDiv . appendChild ( copyButton ) ;
}
// add word to response
if ( word != undefined && word != "" ) {
if ( responseDiv . hidden _text == undefined ) {
responseDiv . hidden _text = "" ;
}
responseDiv . hidden _text += word ;
responseDiv . innerHTML = DOMPurify . sanitize ( marked . parse ( responseDiv . hidden _text ) ) ; // Append word to response container
}
2024-05-21 17:18:47 +08:00
} ) ;
2024-05-21 18:31:32 +08:00
} )
. then ( ( ) => {
stopButton . remove ( ) ; // Remove stop button from DOM now that all text has been generated
spinner . remove ( ) ;
} )
. catch ( error => {
if ( error !== 'Stop button pressed' ) {
console . error ( error ) ;
}
stopButton . remove ( ) ;
spinner . remove ( ) ;
} ) ;
// Clear user input
const element = document . getElementById ( 'user-input' ) ;
element . value = '' ;
$ ( element ) . css ( "height" , textBoxBaseHeight + "px" ) ;
2024-05-21 17:18:47 +08:00
}
// Event listener for Ctrl + Enter or CMD + Enter
document . getElementById ( 'user-input' ) . addEventListener ( 'keydown' , function ( e ) {
2024-05-21 18:31:32 +08:00
if ( ( e . ctrlKey || e . metaKey ) && e . key === 'Enter' ) {
submitRequest ( ) ;
}
2024-05-21 17:18:47 +08:00
} ) ;
window . onload = ( ) => {
2024-05-21 18:31:32 +08:00
updateChatList ( ) ;
populateModels ( ) ;
adjustPadding ( ) ;
autoFocusInput ( ) ;
document . getElementById ( "delete-chat" ) . addEventListener ( "click" , deleteChat ) ;
document . getElementById ( "new-chat" ) . addEventListener ( "click" , startNewChat ) ;
document . getElementById ( "saveName" ) . addEventListener ( "click" , saveChat ) ;
document . getElementById ( "chat-select" ) . addEventListener ( "change" , loadSelectedChat ) ;
document . getElementById ( "host-address" ) . addEventListener ( "change" , setHostAddress ) ;
document . getElementById ( "system-prompt" ) . addEventListener ( "change" , setSystemPrompt ) ;
2024-05-21 17:18:47 +08:00
}
function deleteChat ( ) {
2024-05-21 18:31:32 +08:00
const selectedChat = document . getElementById ( "chat-select" ) . value ;
localStorage . removeItem ( selectedChat ) ;
updateChatList ( ) ;
2024-05-21 17:18:47 +08:00
}
// Function to save chat with a unique name
function saveChat ( ) {
2024-05-21 18:31:32 +08:00
const chatName = document . getElementById ( 'userName' ) . value ;
// Close the modal
const bootstrapModal = bootstrap . Modal . getInstance ( document . getElementById ( 'nameModal' ) ) ;
bootstrapModal . hide ( ) ;
if ( chatName === null || chatName . trim ( ) === "" ) return ;
const history = document . getElementById ( "chat-history" ) . innerHTML ;
const context = document . getElementById ( 'chat-history' ) . context ;
const systemPrompt = document . getElementById ( 'system-prompt' ) . value ;
const model = getSelectedModel ( ) ;
localStorage . setItem ( chatName , JSON . stringify ( {
"history" : history ,
"context" : context ,
system : systemPrompt ,
"model" : model
} ) ) ;
updateChatList ( ) ;
2024-05-21 17:18:47 +08:00
}
// Function to load selected chat from dropdown
function loadSelectedChat ( ) {
2024-05-21 18:31:32 +08:00
const selectedChat = document . getElementById ( "chat-select" ) . value ;
const obj = JSON . parse ( localStorage . getItem ( selectedChat ) ) ;
document . getElementById ( "chat-history" ) . innerHTML = obj . history ;
document . getElementById ( "chat-history" ) . context = obj . context ;
document . getElementById ( "system-prompt" ) . value = obj . system ;
updateModelInQueryString ( obj . model )
document . getElementById ( 'chat-container' ) . style . display = 'block' ;
2024-05-21 17:18:47 +08:00
}
function startNewChat ( ) {
document . getElementById ( "chat-history" ) . innerHTML = null ;
document . getElementById ( "chat-history" ) . context = null ;
document . getElementById ( 'chat-container' ) . style . display = 'none' ;
updateChatList ( ) ;
}
// Function to update chat list dropdown
function updateChatList ( ) {
2024-05-21 18:31:32 +08:00
const chatList = document . getElementById ( "chat-select" ) ;
chatList . innerHTML = '<option value="" disabled selected>Select a chat</option>' ;
for ( let i = 0 ; i < localStorage . length ; i ++ ) {
const key = localStorage . key ( i ) ;
if ( key === "host-address" || key == "system-prompt" ) continue ;
const option = document . createElement ( "option" ) ;
option . value = key ;
option . text = key ;
chatList . add ( option ) ;
}
2024-05-21 17:18:47 +08:00
}
function autoGrow ( element ) {
const maxHeight = 200 ; // This should match the max-height set in CSS
// Count the number of lines in the textarea based on newline characters
const numberOfLines = $ ( element ) . val ( ) . split ( '\n' ) . length ;
// Temporarily reset the height to auto to get the actual scrollHeight
$ ( element ) . css ( "height" , "auto" ) ;
let newHeight = element . scrollHeight ;
// If content is one line, set the height to baseHeight
if ( numberOfLines === 1 ) {
newHeight = textBoxBaseHeight ;
} else if ( newHeight > maxHeight ) {
newHeight = maxHeight ;
}
$ ( element ) . css ( "height" , newHeight + "px" ) ;
}