From 10d96e59e61a0774dc000c1c2a02e20ef043b093 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 21 May 2024 16:06:13 -0400 Subject: [PATCH 01/32] 20240521 @Mookse - `greetings` pipeline - cosmetic --- .../class-extenders.mjs | 2 +- inc/js/functions.mjs | 23 ++++++++ inc/js/mylife-avatar.mjs | 52 ++++++++++++++++++- inc/js/mylife-llm-services.mjs | 1 + inc/js/routes.mjs | 3 ++ inc/json-schemas/message.json | 2 +- 6 files changed, 80 insertions(+), 3 deletions(-) diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 232386d..eb8274e 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -427,7 +427,7 @@ function extendClass_message(originClass, referencesObject) { return this } get micro(){ - return { content: this.content, role: this.role??'user' } + return { content: this.content, role: this.role ?? 'user' } } } return Message diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 1e3e20d..0f575ee 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -124,6 +124,28 @@ async function deleteItem(ctx){ ctx.throw(400, `missing item id`) ctx.body = await avatar.deleteItem(iid) } +async function greetings(ctx){ + const { dyn: dynamic, vld: validate, } = ctx.request.query + const { avatar, } = ctx.state + let response = { success: false, messages: [], } + switch(true){ + case validate: + if(!avatar.isMyLife) + response = { + ...response, + error: new Error('Only MyLife may validate greetings'), + messages: ['Only MyLife may validate greetings'], + } + else // @stub - validate registration request + response.messages.push(...await avatar.validateRegistration(validate)) + break + default: + response.messages.push(...await avatar.getGreeting(dynamic)) + break + } + response.success = response.messages.length > 0 + ctx.body = response +} /** * Request help about MyLife. * @param {Koa} ctx - Koa Context object, body={ request: string|required, mbr_id, type: string, }. @@ -325,6 +347,7 @@ export { createBot, deleteItem, help, + greetings, index, interfaceMode, login, diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 88d22fa..637d26f 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -361,6 +361,14 @@ class Avatar extends EventEmitter { return this.#conversations .filter(_=>_?.type===type) } + /** + * Get a static or dynamic greeting from active bot. + * @param {boolean} dynamic - Whether to use LLM for greeting. + * @returns {array} - The greeting message(s) string array in order of display. + */ + async getGreeting(dynamic=false){ + return await mGreeting(this.activeBot, dynamic, this.#llmServices, this.#factory) + } /** * Request help about MyLife. **caveat** - correct avatar should have been selected prior to calling. * @param {string} helpRequest - The help request text. @@ -558,7 +566,8 @@ class Avatar extends EventEmitter { * @returns {void} */ set activeBotId(_bot_id){ // default PA - if(!_bot_id?.length) _bot_id = this.avatarBot.id + if(!_bot_id?.length) + _bot_id = this.avatarBot.id this.#activeBotId = mFindBot(this, _bot_id)?.id??this.avatarBot.id } /** @@ -1607,6 +1616,47 @@ function mGetChatCategory(_category) { } return _proposedCategory } +/** + * Returns set of Greeting messages, dynamic or static. + * @param {object} bot - The bot object. + * @param {boolean} dynamic - Whether to use dynamic greetings. + * @param {*} llm - The LLM object. + * @param {*} factory - The AgentFactory object. + * @returns {Promise} - The array of messages to respond with. + */ +async function mGreeting(bot, dynamic=false, llm, factory){ + const processStartTime = Date.now() + const { bot_id, bot_name, id, greetings, greeting, thread_id, } = bot + const failGreeting = [`Hello! I'm concerned that there is something wrong with my instruction-set, as I was unable to find my greetings, but let's see if I can get back online.`, `How can I be of help today?`] + const greetingPrompt = factory.isMyLife + ? `Greet this new user with a hearty hello, and let them know that you are here to help them understand MyLife and the MyLife platform. Begin by asking them about something that's important to them--based on their response, explain how MyLife can help them.` + : `Greet me with a hearty hello as we start a new session, and let me know either where we left off, or how we should start for today!` + const QGreetings = [ + `Hi, I'm Q, so nice to meet you!`, + `To get started, tell me a little bit about something or someone that is really important to you — or ask me a question about MyLife.` + ] + const botGreetings = greetings + ? greetings + : greeting + ? [greeting] + : null + let messages = !botGreetings || dynamic // consult LLM? + ? await llm.getLLMResponse(thread_id, bot_id, greetingPrompt, factory) + : botGreetings + if(!messages?.length) + messages = failGreeting + console.log('mGreeting', thread_id, bot_id, factory.message) + messages = messages + .map(message=>new (factory.message)({ + being: 'message', + content: message, + thread_id, + role: 'assistant', + type: 'greeting' + })) + .map(message=>mPruneMessage(bot, message, 'greeting', processStartTime)) + return messages +} /** * Include help preamble to _LLM_ request, not outbound to member/guest. * @todo - expand to include other types of help requests, perhaps more validation. diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index b0c5f9d..cc0196c 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -67,6 +67,7 @@ class LLMServices { * @returns {Promise} - Array of openai `message` objects. */ async getLLMResponse(threadId, botId, prompt, factory){ + console.log('LLMServices::getLLMResponse()', threadId, botId, prompt, factory) await mAssignRequestToThread(this.openai, threadId, prompt) const run = await mRunTrigger(this.openai, botId, threadId, factory) const { assistant_id, id: run_id, model, provider='openai', required_action, status, usage } = run diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index b174380..099fa1d 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -12,6 +12,7 @@ import { contributions, createBot, deleteItem, + greetings, help, index, interfaceMode, @@ -53,6 +54,7 @@ _Router.get('/alerts', alerts) _Router.get('/login/:mid', login) _Router.get('/logout', logout) _Router.get('/experiences', availableExperiences) +_Router.get('/greeting', greetings) _Router.get('/select', loginSelect) _Router.get('/status', status) _Router.get('/privacy-policy', privacyPolicy) @@ -95,6 +97,7 @@ _memberRouter.get('/contributions', contributions) _memberRouter.get('/contributions/:cid', contributions) _memberRouter.get('/experiences', experiences) _memberRouter.get('/experiencesLived', experiencesLived) +_memberRouter.get('/greeting', greetings) _memberRouter.get('/mode', interfaceMode) _memberRouter.patch('/experience/:eid', experience) _memberRouter.patch('/experience/:eid/end', experienceEnd) diff --git a/inc/json-schemas/message.json b/inc/json-schemas/message.json index 7db3d8f..6247b57 100644 --- a/inc/json-schemas/message.json +++ b/inc/json-schemas/message.json @@ -40,7 +40,7 @@ "type": "string", "default": "chat", "description": "message type", - "enum": ["chat", "system", "error", "warning", "info", "debug"] + "enum": ["chat", "greeting", "system", "error", "warning", "info", "debug"] } }, "additionalProperties": true, From 1539745b97798844a314eecac942f908414b19f8 Mon Sep 17 00:00:00 2001 From: Erik Jespersen <42016062+Mookse@users.noreply.github.com> Date: Tue, 21 May 2024 16:39:29 -0400 Subject: [PATCH 02/32] Emergency Hotfix: Azure deploy prod (#215) * 20240429 @Mookse - assets * 20240429 @Mookse wip stable * 20240429 @Mookse - assets Signed-off-by: Erik Jespersen * 20240429 @Mookse wip stable Signed-off-by: Erik Jespersen * Update azure-deploy-prod_maht.yml Updated version numbers * Update azure-deploy-prod_maht.yml updated azure/webapps-deploy to v3 * 20240507 @Mookse - `Globals` improvement - select help type - submit help - receive reponse - cosmetic update of @module in ESDocs * 20240511 @Mookse - display chat/request response bubbles - begin breakout of animations.css - llm conectivity - await css * 20240512 @Mookse - help chat refresh * 20240512 @Mookse - availableExperiences() - launch tutorial button - launchExperience event * 20240513 @Mookse - popup-container css * 20240513 @Mookse - env defaults to gpt-4o * 20240513 @Mookse - gpt updates - improved message parsing - cosmetic * 20240513 @Mookse wip unstable * Version 0.0.6 Release (#198) * 20240429 @Mookse - assets * 20240429 @Mookse wip stable * 20240429 @Mookse - assets Signed-off-by: Erik Jespersen * 20240429 @Mookse wip stable Signed-off-by: Erik Jespersen * 20240507 @Mookse - `Globals` improvement - select help type - submit help - receive reponse - cosmetic update of @module in ESDocs * 20240511 @Mookse - display chat/request response bubbles - begin breakout of animations.css - llm conectivity - await css * 20240512 @Mookse - help chat refresh * 20240512 @Mookse - availableExperiences() - launch tutorial button - launchExperience event * 20240513 @Mookse - popup-container css * 20240513 @Mookse - env defaults to gpt-4o * 20240513 @Mookse - gpt updates - improved message parsing - cosmetic --------- Signed-off-by: Erik Jespersen * 20240513 @Mookse - clean-up after wip * 20240514 @Mookse - vector store initial paint * 20240514 @Mookse - bug fix: system experiences frontend function * 20240514 @Mookse - multi-file upload button frontend * 20240515 @Mookse - pipeline to vector-store to attach files * 20240515 @Mookse - vector store attached to PA * 20240515 @Mookse - avatar upload() returns ``` { uploads: files, files: vectorstoreFileList, success: true, } ``` - api adds: ``` { type: type, message: `File(s) [type=${ type }] uploaded successfully.` } ``` * 20240515 @Mookse - file-collection returned correctly on refresh * 20240515 @Mookse - file-collection displays * 20240516 @Mookse - HTML parsed in bubbles * 20240516 @Mookse - bot thread memory restored * 197 version 007 updates (#208) * 20240516 @Mookse - alert background -> aliceblue * 20240517 @Mookse - `createBot` endpoint - cosmetic * 20240518 @Mookse - `updateTools` becomes `updateAssistant` - fix: merge error * 20240518 @Mookse - createBot, type=journaler succeeds * 20240518 @Mookse - icon asset * 202401518 @Mookse - journal-thumb.png - cosmetic console clear * 20240519 @Mookse - entrySummary() * 20240519 @Mookse - cosmetic * 20240519 @Mookse - Teams placeholder - addTeamMember() - getAvailableTeamMembers(team) * 20240520 @Mookse - public/private avatar placeholder toggle - cosmetic * 20240521 @Mookse - hotfix --------- Signed-off-by: Erik Jespersen Signed-off-by: Erik Jespersen <42016062+Mookse@users.noreply.github.com> --- inc/js/mylife-avatar.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 88d22fa..9a88bac 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -135,6 +135,10 @@ class Avatar extends EventEmitter { */ async chatRequest(activeBotId, threadId, chatMessage){ const processStartTime = Date.now() + if(this.isMyLife){ /* MyLife chat request hasn't supplied basics to front-end yet */ + activeBotId = this.activeBot.id + threadId = this.activeBot.thread_id + } if(!chatMessage) throw new Error('No message provided in context') if(!activeBotId) From 5472341f2299f6556b03eb31cc5451257f997234 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 21 May 2024 16:56:22 -0400 Subject: [PATCH 03/32] 20240521 @Mookse - guest ``` javascript document.addEventListener('DOMContentLoaded', event=>{ /* load data */ mLoadStart() /* display page */ mShowPage() }) ``` --- views/assets/js/globals.mjs | 12 ++++ views/assets/js/guests.mjs | 111 ++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index 3929f9b..ac9faaa 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -219,6 +219,18 @@ class Globals { const { classList, } = element mIsVisible(classList) ? mHide(element) : mShow(element) } + /** + * Returns the URL parameters as an object. + * @returns {object} - The URL parameters as an object. + */ + urlParameters(){ + const parameters = new URLSearchParams(window.location.search) + let parametersObject = {} + for(let parameter of parameters) { + parametersObject[parameter[0]] = parameter[1] + } + return parametersObject + } /** * Variable-izes (for js) a given string. * @param {string} undashedString - String to variable-ize. diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index c2032ce..f59ab68 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -4,10 +4,6 @@ import Globals from './globals.mjs' const mGlobals = new Globals() /* constants */ const mAvatarName = mGlobals.getAvatar()?.name -const mGreeting = [ - `Hi, I'm ${ mAvatarName }, so nice to meet you!`, - `To get started, tell me a little bit about something or someone that is really important to you — or ask me a question about MyLife.` -] const hide = mGlobals.hide const mPlaceholder = `Type a message to ${ mAvatarName }...` const show = mGlobals.show @@ -29,21 +25,9 @@ let aboutContainer, navigation, privacyContainer, sidebar -document.addEventListener('DOMContentLoaded', ()=>{ - /* assign page div variables */ - aboutContainer = document.getElementById('about-container') - awaitButton = document.getElementById('await-button') - agentSpinner = document.getElementById('agent-spinner') - chatContainer = document.getElementById('chat-container') - chatLabel = document.getElementById('user-chat-label') - chatInput = document.getElementById('chat-user-message') - chatSubmit = document.getElementById('chat-user-submit') - chatSystem = document.getElementById('chat-system') - chatUser = document.getElementById('chat-user') - mainContent = mGlobals.mainContent - navigation = mGlobals.navigation - privacyContainer = document.getElementById('privacy-container') - sidebar = mGlobals.sidebar +document.addEventListener('DOMContentLoaded', event=>{ + /* load data */ + mLoadStart() /* display page */ mShowPage() }) @@ -57,8 +41,8 @@ document.addEventListener('DOMContentLoaded', ()=>{ */ function mAddMessage(message, options={ bubbleClass: 'agent-bubble', - typewrite: true, delay: 15, + typewrite: true, }){ let messageContent = message.message ?? message const originalMessage = messageContent @@ -111,6 +95,42 @@ function mAddUserMessage(event){ delay: 7, }) } +/** + * Fetches the greeting messages from the server. The greeting object from server: { error, messages, success, } + * @private + * @param {boolean} dynamic - Whether or not greeting should be dynamically generated (true) or static (false). + * @returns {Promise} - The response Message array. + */ +async function mFetchGreetings(dynamic=false){ + let query = window.location.search || '?' + dynamic = `dyn=${ dynamic }&` + query += dynamic + let url = window.location.origin + + '/greeting' + + query + try { + const response = await fetch(url) + const { messages, success, } = await response.json() + return messages + } catch(error) { + return [`Error: ${ error.message }`, `Please try again. If this persists, check back with me later or contact support.`] + } +} +/** + * Fetches the greeting messages or start routine from the server. + * @private + * @returns {void} + */ +async function mFetchStart(){ + const greetings = await mFetchGreetings() + greetings.forEach(greeting=>{ + mAddMessage(greeting, { + bubbleClass: 'agent-bubble', + delay: 10, + typewrite: true, + }) + }) +} /** * Initializes event listeners. * @private @@ -122,6 +142,29 @@ function mInitializeListeners(){ if(chatSubmit) chatSubmit.addEventListener('click', mAddUserMessage) } +/** + * Load data for the page. + * @private + * @returns {void} + */ +async function mLoadStart(){ + /* assign page div variables */ + aboutContainer = document.getElementById('about-container') + awaitButton = document.getElementById('await-button') + agentSpinner = document.getElementById('agent-spinner') + chatContainer = document.getElementById('chat-container') + chatLabel = document.getElementById('user-chat-label') + chatInput = document.getElementById('chat-user-message') + chatSubmit = document.getElementById('chat-user-submit') + chatSystem = document.getElementById('chat-system') + chatUser = document.getElementById('chat-user') + mainContent = mGlobals.mainContent + navigation = mGlobals.navigation + privacyContainer = document.getElementById('privacy-container') + sidebar = mGlobals.sidebar + /* fetch the greeting messages */ + await mFetchStart() +} function scrollToBottom() { chatSystem.scrollTop = chatSystem.scrollHeight } @@ -153,34 +196,6 @@ function mShowPage(){ show(chatSystem) show(chatContainer) show(chatUser) - /* welcome-01 */ - mAddMessage({ - message: mGreeting[0], - }) - /* welcome-02 */ - setTimeout(() => { // Set a timeout for 1 second to wait for the first line to be fully painted - // Set another timeout for 7.5 seconds to add the second message - const timerId = setTimeout(_addIntroductionMessage, 7500); - // Event listeners for member interactions - window.addEventListener('mousemove', _addIntroductionMessage, { once: true }) - window.addEventListener('click', _addIntroductionMessage, { once: true }) - window.addEventListener('focus', _addIntroductionMessage, { once: true }) - window.addEventListener('scroll', _addIntroductionMessage, { once: true }) - /* local timeout functions */ - function _addIntroductionMessage() { // Clear the 7.5 seconds timeout if any event is triggered - clearTimeout(timerId) - mAddMessage({ message: mGreeting[1] }) - _cleanupListeners() - // display chat lane with placeholder - } - // Cleanup function to remove event listeners - function _cleanupListeners() { - window.removeEventListener('mousemove', _addIntroductionMessage) - window.removeEventListener('click', _addIntroductionMessage) - window.removeEventListener('focus', _addIntroductionMessage) - window.removeEventListener('scroll', _addIntroductionMessage) - } - }, 1000) } /** * From 3b6fc60e293d3f5e05149eb9158e0ae3a70375bb Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 21 May 2024 17:16:53 -0400 Subject: [PATCH 04/32] 20240521 @Mookse - hotfix: registration restored --- inc/js/mylife-data-service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index 9333b97..ce5dbec 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -283,7 +283,7 @@ class Dataservices { } async findRegistrationIdByEmail(_email){ /* pull record for email, returning id or new guid */ - const _ = await this.getItems( + const registered = await this.getItems( 'registration', undefined, [{ name: '@email', @@ -291,7 +291,9 @@ class Dataservices { }], 'registration', ) - return _?.[0]?.id??Guid.newGuid().toString() // needed to separate out, was failing + const registrationId = registered?.[0]?.id + ?? this.globals.newGuid + return registrationId } /** * Retrieves a specific alert by its ID. _Currently placehoder_. From 58c5b6be6c94597bd02829b6f9ecb311c534274e Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 21 May 2024 17:53:18 -0400 Subject: [PATCH 05/32] 20240521 @Mookse - wip --- inc/js/functions.mjs | 18 ++++++++++++------ inc/js/mylife-agent-factory.mjs | 8 ++++++++ inc/js/mylife-avatar.mjs | 28 +++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 0f575ee..33f84af 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -82,7 +82,7 @@ async function chat(ctx){ if(!message?.length) ctx.throw(400, 'missing `message` content') const { avatar, } = ctx.state - const response = await avatar.chatRequest(botId, threadId, message, ) + const response = await avatar.chatRequest(botId, threadId, message) ctx.body = response } async function collections(ctx){ @@ -124,20 +124,26 @@ async function deleteItem(ctx){ ctx.throw(400, `missing item id`) ctx.body = await avatar.deleteItem(iid) } +/** + * Get greetings for this bot/active bot. + * @todo - move dynamic system responses to a separate function (route /system) + * @param {Koa} ctx - Koa Context object. + * @returns {object} - Greetings response message object: { success: false, messages: [], }. + */ async function greetings(ctx){ - const { dyn: dynamic, vld: validate, } = ctx.request.query + const { dyn: dynamic, vld: validateId, } = ctx.request.query const { avatar, } = ctx.state let response = { success: false, messages: [], } switch(true){ - case validate: - if(!avatar.isMyLife) + case validateId: + if(!avatar.isMyLife){ response = { ...response, error: new Error('Only MyLife may validate greetings'), messages: ['Only MyLife may validate greetings'], } - else // @stub - validate registration request - response.messages.push(...await avatar.validateRegistration(validate)) + } else // @stub - validate registration request + response.messages.push(...await avatar.validateRegistration(validateId)) break default: response.messages.push(...await avatar.getGreeting(dynamic)) diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 4a41d14..f36c036 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -645,6 +645,14 @@ class AgentFactory extends BotFactory{ if(!this.isMyLife) return false return await mDataservices.testPartitionKey(_mbr_id) } + /** + * Validate registration id. + * @param {Guid} validationId - The registration id. + * @returns {Promise} - Whether or not code was valid (true) or not (false). + */ + async validateRegistration(registrationId){ + return await this.dataservices.validateRegistration(registrationId) + } // getters/setters get alerts(){ // currently only returns system alerts return mAlerts.system diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 7fa741e..ac3a711 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -543,6 +543,14 @@ class Avatar extends EventEmitter { if(tools) this.#llmServices.updateAssistant(botId, tools) /* no await */ } + /** + * Validate registration id. + * @param {Guid} validationId - The registration id. + * @returns {Promise} - Whether or not code was valid (true) or not (false). + */ + async validateRegistration(validationId){ + return await mValidateRegistration(this.#factory, validationId) + } /* getters/setters */ /** * Get the active bot. If no active bot, return this as default chat engine. @@ -1649,7 +1657,6 @@ async function mGreeting(bot, dynamic=false, llm, factory){ : botGreetings if(!messages?.length) messages = failGreeting - console.log('mGreeting', thread_id, bot_id, factory.message) messages = messages .map(message=>new (factory.message)({ being: 'message', @@ -1850,5 +1857,24 @@ function mValidateMode(_requestedMode, _currentMode){ return _requestedMode } } +/** + * Validate registration id. + * @private + * @param {AgentFactory} factory - AgentFactory object. + * @param {Guid} validationId - The registration id. + * @returns {Promise} - Whether or not code was valid (true) or not (false). + */ +async function mValidateRegistration(factory, validationId){ + if(!this.isMyLife) + throw new Error('FAILURE::validateRegistration()::Only MyLife may validate registrations.') + if(!this.globals.isValidGuid(validationId)) + throw new Error('FAILURE::validateRegistration()::Invalid validation id.') + const validated = factory.validateRegistration(validationId) + /* determine eligibility */ + // if found, then check if eligible for registration (no constraints currently) + + // create/reference function in Q + +} /* exports */ export default Avatar \ No newline at end of file From 4057f150a03c084792d9dbf3cdde5535ec11d1e1 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Wed, 22 May 2024 11:27:01 -0400 Subject: [PATCH 06/32] 20240522 @Mookse - JSON assets for journaler bot - entrySummary --- .../openai/functions/entrySummary.json | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 inc/json-schemas/openai/functions/entrySummary.json diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/entrySummary.json new file mode 100644 index 0000000..66fbf6d --- /dev/null +++ b/inc/json-schemas/openai/functions/entrySummary.json @@ -0,0 +1,53 @@ +{ + "description": "Generate a JOURNAL ENTRY `entry` summary with keywords and other critical data elements.", + "name": "entrySummary", + "parameters": { + "type": "object", + "properties": { + "content": { + "description": "concatenated raw text content of member input for JOURNAL ENTRY." + }, + "keywords": { + "description": "Keywords most relevant to JOURNAL ENTRY.", + "items": { + "description": "Keyword (single word or short phrase) to be used in JOURNAL ENTRY summary.", + "maxLength": 64, + "type": "string" + }, + "maxItems": 12, + "minItems": 3, + "type": "array" + }, + "mood": { + "description": "Record member mood for day (or entry) in brief as ascertained from content of JOURNAL ENTRY.", + "maxLength": 256, + "type": "string" + }, + "relationships": { + "description": "Record individuals (or pets) mentioned in this `entry`.", + "type": "array", + "items": { + "description": "A name of relational individual/pet to the `entry` content.", + "type": "string" + }, + "maxItems": 24 + }, + "summary": { + "description": "Generate a JOURNAL ENTRY summary from input.", + "maxLength": 20480, + "type": "string" + }, + "title": { + "description": "Generate display Title of the JOURNAL ENTRY.", + "maxLength": 256, + "type": "string" + } + }, + "required": [ + "content", + "keywords", + "summary", + "title" + ] + } +} \ No newline at end of file From dcd5ffe7579dcbcfc1d582eb8d7e59d6bb3ed157 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Wed, 22 May 2024 11:29:30 -0400 Subject: [PATCH 07/32] 20240522 @Mookse wip stable --- inc/js/functions.mjs | 26 +++--- inc/js/mylife-avatar.mjs | 79 ++++++++++++++++--- inc/js/mylife-data-service.js | 9 +++ .../openai/functions/confirmRegistration.json | 15 ++++ .../openai/functions/entrySummary.json | 53 +++++++++++++ views/assets/js/guests.mjs | 4 +- 6 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 inc/json-schemas/openai/functions/confirmRegistration.json create mode 100644 inc/json-schemas/openai/functions/entrySummary.json diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 33f84af..dfe4613 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -134,21 +134,17 @@ async function greetings(ctx){ const { dyn: dynamic, vld: validateId, } = ctx.request.query const { avatar, } = ctx.state let response = { success: false, messages: [], } - switch(true){ - case validateId: - if(!avatar.isMyLife){ - response = { - ...response, - error: new Error('Only MyLife may validate greetings'), - messages: ['Only MyLife may validate greetings'], - } - } else // @stub - validate registration request - response.messages.push(...await avatar.validateRegistration(validateId)) - break - default: - response.messages.push(...await avatar.getGreeting(dynamic)) - break - } + if(validateId?.length){ + if(!avatar.isMyLife){ + response = { + ...response, + error: new Error('Only MyLife may validate greetings'), + messages: ['Only MyLife may validate greetings'], + } + } else // @stub - validate registration request + response.messages.push(...await avatar.validateRegistration(validateId)) + } else + response.messages.push(...await avatar.getGreeting(dynamic)) response.success = response.messages.length > 0 ctx.body = response } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index ac3a711..bb407c5 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -37,6 +37,7 @@ class Avatar extends EventEmitter { #livingExperience #llmServices #mode = 'standard' // interface-mode from module `mAvailableModes` + #mylifeRegistrationEmail #nickname // avatar nickname, need proxy here as getter is complex #proxyBeing = 'human' /** @@ -205,6 +206,15 @@ class Avatar extends EventEmitter { }) return collections } + /** + * From gpt function call, confirms registration based on email. + * @returns + */ + async confirmRegistration(email, passphrase){ + if(!this.isMyLife) + throw new Error('Only MyLife avatar can confirm registration.') + return await this.#factory.confirmRegistration() + } /** * Create a new bot. Errors if bot cannot be created. * @async @@ -545,11 +555,17 @@ class Avatar extends EventEmitter { } /** * Validate registration id. + * @todo - move to MyLife only avatar variant. * @param {Guid} validationId - The registration id. - * @returns {Promise} - Whether or not code was valid (true) or not (false). + * @returns {Promise} - Array of system messages. */ async validateRegistration(validationId){ - return await mValidateRegistration(this.#factory, validationId) + const { email, messages, success, } = await mValidateRegistration(this.activeBot, this.#factory, validationId) + if(success){ + // @stub - move to MyLife only avatar variant, where below are private vars + this.#mylifeRegistrationEmail = email + } + return messages } /* getters/setters */ /** @@ -1140,6 +1156,21 @@ async function mCast(factory, cast){ })) return cast } +function mCreateSystemMessage(activeBot, message, factory){ + if(!(message instanceof factory.message)){ + const { thread_id, } = activeBot + const content = message?.content ?? message?.message ?? message + message = new (factory.message)({ + being: 'message', + content, + role: 'assistant', + thread_id, + type: 'system' + }) + } + message = mPruneMessage(activeBot, message, 'system') + return message +} /** * Takes character data and makes necessary adjustments to roles, urls, etc. * @todo - icon and background changes @@ -1858,23 +1889,47 @@ function mValidateMode(_requestedMode, _currentMode){ } } /** - * Validate registration id. + * Validate provided registration id. * @private + * @param {object} activeBot - The active bot object. * @param {AgentFactory} factory - AgentFactory object. * @param {Guid} validationId - The registration id. - * @returns {Promise} - Whether or not code was valid (true) or not (false). + * @returns {Promise} - The validation result: { messages, success, }. */ -async function mValidateRegistration(factory, validationId){ - if(!this.isMyLife) +async function mValidateRegistration(activeBot, factory, validationId){ + /* validate structure */ + if(!factory.isMyLife) throw new Error('FAILURE::validateRegistration()::Only MyLife may validate registrations.') - if(!this.globals.isValidGuid(validationId)) + if(!factory.globals.isValidGuid(validationId)) throw new Error('FAILURE::validateRegistration()::Invalid validation id.') - const validated = factory.validateRegistration(validationId) + /* validate validationId */ + let email, + message, + success = false + const registration = await factory.validateRegistration(validationId) + const messages = [] + const failureMessage = 'I\'m sorry, but I was unable to validate your registration.' /* determine eligibility */ - // if found, then check if eligible for registration (no constraints currently) - - // create/reference function in Q - + if(registration){ + const { being, email: registrationEmail, humanName, } = registration + const eligible = being==='registration' + && factory.globals.isValidEmail(registrationEmail) + // ensure not in cosmos _already_: storedProc for this I think? + console.log('mValidateRegistration::eligible', registration, eligible) + if(eligible){ + const successMessage = `Hello and _thank you_ for your registration, ${ humanName }!\nI'm Q, the ai-representative for MyLife, and I'm excited to help you get started, so let's do the following:\n1. Verify your email address\n2. set up your account\n3. get you started with your first MyLife experience!\n\nSo let me walk you through the process. In the chat below, please enter the email you registered with and hit the **submit** button!` + message = mCreateSystemMessage(activeBot, successMessage, factory) + email = registrationEmail + console.log('mValidateRegistration::eligible', registration, message, email) + success = true + } + } + if(!message){ + message = mCreateSystemMessage(activeBot, failureMessage, factory) + } + if(message) + messages.push(message) + return { email, messages, success, } } /* exports */ export default Avatar \ No newline at end of file diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index ce5dbec..6891f76 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -670,6 +670,15 @@ class Dataservices { if(!this.isMyLife) return false return await this.datamanager.testPartitionKey(_mbr_id) } + /** + * Returns the registration record by Id. + * @param {string} registrationId - Guid for registration record in system container. + * @returns {object} - The registration document, if exists. + */ + async validateRegistration(registrationId){ + const registration = await this.getItem(registrationId, 'registration', this.mbr_id) + return registration + } } /* exports */ export default Dataservices \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/confirmRegistration.json b/inc/json-schemas/openai/functions/confirmRegistration.json new file mode 100644 index 0000000..da3619b --- /dev/null +++ b/inc/json-schemas/openai/functions/confirmRegistration.json @@ -0,0 +1,15 @@ +{ + "description": "Confirm registration email by email provided by user, id stored in session memory", + "name": "confirmRegistration", + "parameters": { + "type": "object", + "properties": { + "description": "Email provided by user", + "format": "email", + "type": "string" + }, + "required": [ + "email" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/entrySummary.json new file mode 100644 index 0000000..66fbf6d --- /dev/null +++ b/inc/json-schemas/openai/functions/entrySummary.json @@ -0,0 +1,53 @@ +{ + "description": "Generate a JOURNAL ENTRY `entry` summary with keywords and other critical data elements.", + "name": "entrySummary", + "parameters": { + "type": "object", + "properties": { + "content": { + "description": "concatenated raw text content of member input for JOURNAL ENTRY." + }, + "keywords": { + "description": "Keywords most relevant to JOURNAL ENTRY.", + "items": { + "description": "Keyword (single word or short phrase) to be used in JOURNAL ENTRY summary.", + "maxLength": 64, + "type": "string" + }, + "maxItems": 12, + "minItems": 3, + "type": "array" + }, + "mood": { + "description": "Record member mood for day (or entry) in brief as ascertained from content of JOURNAL ENTRY.", + "maxLength": 256, + "type": "string" + }, + "relationships": { + "description": "Record individuals (or pets) mentioned in this `entry`.", + "type": "array", + "items": { + "description": "A name of relational individual/pet to the `entry` content.", + "type": "string" + }, + "maxItems": 24 + }, + "summary": { + "description": "Generate a JOURNAL ENTRY summary from input.", + "maxLength": 20480, + "type": "string" + }, + "title": { + "description": "Generate display Title of the JOURNAL ENTRY.", + "maxLength": 256, + "type": "string" + } + }, + "required": [ + "content", + "keywords", + "summary", + "title" + ] + } +} \ No newline at end of file diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index f59ab68..05797b5 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -102,7 +102,9 @@ function mAddUserMessage(event){ * @returns {Promise} - The response Message array. */ async function mFetchGreetings(dynamic=false){ - let query = window.location.search || '?' + let query = window.location.search + ? window.location.search + '&' + : '?' dynamic = `dyn=${ dynamic }&` query += dynamic let url = window.location.origin From 091e20454ceac5b2dc84c6ca811c492bd4f5c51d Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Wed, 22 May 2024 22:08:37 -0400 Subject: [PATCH 08/32] 20240522 @Mookse - validate registration pipeline - ctx.request.query required JSON parsing --- inc/js/functions.mjs | 5 +- inc/js/mylife-agent-factory.mjs | 58 ++++++++++++++++++- inc/js/mylife-avatar.mjs | 47 +++++++-------- inc/js/mylife-llm-services.mjs | 49 +++++++++++----- .../openai/functions/confirmRegistration.json | 10 ++-- 5 files changed, 120 insertions(+), 49 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index dfe4613..ef52294 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -131,7 +131,10 @@ async function deleteItem(ctx){ * @returns {object} - Greetings response message object: { success: false, messages: [], }. */ async function greetings(ctx){ - const { dyn: dynamic, vld: validateId, } = ctx.request.query + const { vld: validateId, } = ctx.request.query + let { dyn: dynamic, } = ctx.request.query + if(typeof dynamic==='string') + dynamic = JSON.parse(dynamic) const { avatar, } = ctx.state let response = { success: false, messages: [], } if(validateId?.length){ diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index f36c036..d3a2af5 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -414,9 +414,10 @@ class BotFactory extends EventEmitter{ return mSystemActor } } -class AgentFactory extends BotFactory{ +class AgentFactory extends BotFactory { #exposedSchemas = mExposedSchemas(['avatar','agent','consent','consent_log','relationship']) // run-once 'caching' for schemas exposed to the public, args are array of key-removals; ex: `avatar` is not an open class once extended by server #llmServices = mLLMServices + #mylifeRegistrationData // @stub - move to unique MyLife factory constructor(mbr_id){ super(mbr_id, false) } @@ -686,6 +687,31 @@ class AgentFactory extends BotFactory{ get organization(){ return this.schemas.Organization } + /** + * Gets registration data while user attempting to confirm. + * @returns {object} - Registration data in memory. + */ + get registrationData(){ + return this.#mylifeRegistrationData + } + /** + * Sets registration data while user attempting to confirm. + * @todo - move to mylife agent factory + * @param {object} registrationData - Registration data. + * @returns {void} + */ + set registrationData(registrationData){ + if(!this.isMyLife) + throw new Error('MyLife factory required to store registration data') + if(!registrationData) + throw new Error('registration data required') + if(!this.#mylifeRegistrationData){ + this.#mylifeRegistrationData = registrationData + setTimeout(timeout=>{ // Set a timeout to clear the data after 5 minutes (300000 milliseconds) + this.#mylifeRegistrationData = null + }, 300000) + } + } get schema(){ // proxy for schemas return this.schemas } @@ -709,6 +735,36 @@ class AgentFactory extends BotFactory{ this.core.vectorstoreId = vectorstoreId /* update local */ } } +// @stub - MyLife factory class +class MyLifeFactory extends AgentFactory { + #mylifeRegistrationData + constructor(){ + super(mbr_id) + } + // no init() for MyLife server + /* public functions */ + /* getters/setters */ + /** + * Gets registration data while user attempting to confirm. + * @returns {object} - Registration data in memory. + */ + get registrationData(){ + return this.#mylifeRegistrationData + } + /** + * Sets registration data while user attempting to confirm. Persists for 5 minutes, and cannot be reset for session until expiration. + * @param {object} registrationData - Registration data. + * @returns {void} + */ + set registrationData(registrationData){ + if(!this.#mylifeRegistrationData){ + this.#mylifeRegistrationData = registrationData + setTimeout(timeout=>{ // Set a timeout to clear the data after 5 minutes (300000 milliseconds) + this.#mylifeRegistrationData = null + }, 300000) + } + } +} // private module functions /** * Initializes openAI assistant and returns associated `assistant` object. diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index bb407c5..d3f060c 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -37,7 +37,6 @@ class Avatar extends EventEmitter { #livingExperience #llmServices #mode = 'standard' // interface-mode from module `mAvailableModes` - #mylifeRegistrationEmail #nickname // avatar nickname, need proxy here as getter is complex #proxyBeing = 'human' /** @@ -139,6 +138,8 @@ class Avatar extends EventEmitter { if(this.isMyLife){ /* MyLife chat request hasn't supplied basics to front-end yet */ activeBotId = this.activeBot.id threadId = this.activeBot.thread_id + if(this.#factory.registrationData) // trigger confirmation until session (or vld) ends + chatMessage = `CONFIRM REGISTRATION: ` + chatMessage } if(!chatMessage) throw new Error('No message provided in context') @@ -151,7 +152,6 @@ class Avatar extends EventEmitter { if(botThreadId!==threadId) throw new Error(`Invalid thread id: ${ threadId }, active thread id: ${ botThreadId }`) let conversation = this.getConversation(threadId) - console.log('chatRequest()', threadId, activeBotId, botThreadId, this.bots) if(!conversation) throw new Error('No conversation found for thread id and could not be created.') conversation.botId = activeBot.bot_id // pass in via quickly mutating conversation (or independently if preferred in end), versus llmServices which are global @@ -160,7 +160,7 @@ class Avatar extends EventEmitter { if(mAllowSave) conversation.save() else - console.log('chatRequest::BYPASS-SAVE', conversation.message.content) + console.log('chatRequest::BYPASS-SAVE', conversation.message?.content) /* frontend mutations */ const { activeBot: bot } = this // current fe will loop through messages in reverse chronological order @@ -206,15 +206,6 @@ class Avatar extends EventEmitter { }) return collections } - /** - * From gpt function call, confirms registration based on email. - * @returns - */ - async confirmRegistration(email, passphrase){ - if(!this.isMyLife) - throw new Error('Only MyLife avatar can confirm registration.') - return await this.#factory.confirmRegistration() - } /** * Create a new bot. Errors if bot cannot be created. * @async @@ -363,7 +354,6 @@ class Avatar extends EventEmitter { getConversation(threadId){ const conversation = this.#conversations .find(conversation=>conversation.thread?.id ?? conversation.thread_id===threadId) - console.log('getConversation()', conversation.thread, conversation.thread_id, threadId, conversation.inspect(true)) return conversation } /** @@ -442,7 +432,7 @@ class Avatar extends EventEmitter { if(mAllowSave) conversation.save() else - console.log('chatRequest::BYPASS-SAVE', conversation.message.content) + console.log('helpRequest::BYPASS-SAVE', conversation.message.content) const response = mPruneMessages(this.activeBot, helpResponseArray, 'help', processStartTime) return response } @@ -560,10 +550,10 @@ class Avatar extends EventEmitter { * @returns {Promise} - Array of system messages. */ async validateRegistration(validationId){ - const { email, messages, success, } = await mValidateRegistration(this.activeBot, this.#factory, validationId) + const { messages, registrationData, success, } = await mValidateRegistration(this.activeBot, this.#factory, validationId) if(success){ // @stub - move to MyLife only avatar variant, where below are private vars - this.#mylifeRegistrationEmail = email + this.#factory.registrationData = registrationData } return messages } @@ -1682,10 +1672,12 @@ async function mGreeting(bot, dynamic=false, llm, factory){ ? greetings : greeting ? [greeting] - : null - let messages = !botGreetings || dynamic // consult LLM? - ? await llm.getLLMResponse(thread_id, bot_id, greetingPrompt, factory) - : botGreetings + : factory.isMyLife + ? QGreetings + : null + let messages = botGreetings?.length && !dynamic + ? botGreetings + : await llm.getLLMResponse(thread_id, bot_id, greetingPrompt, factory) if(!messages?.length) messages = failGreeting messages = messages @@ -1903,24 +1895,25 @@ async function mValidateRegistration(activeBot, factory, validationId){ if(!factory.globals.isValidGuid(validationId)) throw new Error('FAILURE::validateRegistration()::Invalid validation id.') /* validate validationId */ - let email, - message, + let message, + registrationData = { id: validationId }, success = false const registration = await factory.validateRegistration(validationId) const messages = [] const failureMessage = 'I\'m sorry, but I was unable to validate your registration.' /* determine eligibility */ if(registration){ - const { being, email: registrationEmail, humanName, } = registration + const { avatarNickname, being, email: registrationEmail, humanName, } = registration const eligible = being==='registration' && factory.globals.isValidEmail(registrationEmail) // ensure not in cosmos _already_: storedProc for this I think? - console.log('mValidateRegistration::eligible', registration, eligible) if(eligible){ const successMessage = `Hello and _thank you_ for your registration, ${ humanName }!\nI'm Q, the ai-representative for MyLife, and I'm excited to help you get started, so let's do the following:\n1. Verify your email address\n2. set up your account\n3. get you started with your first MyLife experience!\n\nSo let me walk you through the process. In the chat below, please enter the email you registered with and hit the **submit** button!` message = mCreateSystemMessage(activeBot, successMessage, factory) - email = registrationEmail - console.log('mValidateRegistration::eligible', registration, message, email) + registrationData.avatarNickname = avatarNickname + registrationData.email = registrationEmail + registrationData.humanName = humanName + console.log('mValidateRegistration::eligible', registrationData) success = true } } @@ -1929,7 +1922,7 @@ async function mValidateRegistration(activeBot, factory, validationId){ } if(message) messages.push(message) - return { email, messages, success, } + return { registrationData, messages, success, } } /* exports */ export default Avatar \ No newline at end of file diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index cc0196c..2267bbc 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -67,7 +67,6 @@ class LLMServices { * @returns {Promise} - Array of openai `message` objects. */ async getLLMResponse(threadId, botId, prompt, factory){ - console.log('LLMServices::getLLMResponse()', threadId, botId, prompt, factory) await mAssignRequestToThread(this.openai, threadId, prompt) const run = await mRunTrigger(this.openai, botId, threadId, factory) const { assistant_id, id: run_id, model, provider='openai', required_action, status, usage } = run @@ -249,7 +248,7 @@ async function mRunFinish(llmServices, run, factory){ * @returns {object} - [OpenAI run object](https://platform.openai.com/docs/api-reference/runs/object) * @throws {Error} - If tool function not recognized */ -async function mRunFunctions(openai, run, factory){ +async function mRunFunctions(openai, run, factory){ // convert factory to avatar (or add) if( run.required_action?.type=='submit_tool_outputs' && run.required_action?.submit_tool_outputs?.tool_calls @@ -260,35 +259,56 @@ async function mRunFunctions(openai, run, factory){ .map(async tool=>{ const { id, function: toolFunction, type, } = tool let { arguments: toolArguments, name, } = toolFunction + let action, + confirmation = { + tool_call_id: id, + output: '', + }, + success = false + if(typeof toolArguments==='string') + toolArguments = JSON.parse(toolArguments) switch(name.toLowerCase()){ + case 'confirmregistration': + case 'confirm_registration': + case 'confirm registration': + const { email, } = toolArguments + if(!email?.length) + action = `No email provided for registration confirmation, elicit email address for confirmation of registration and try function this again` + else if(email.toLowerCase()!==factory.registrationData?.email?.toLowerCase()) + action = 'Email does not match -- if occurs more than twice in this thread, fire `hijackAttempt` function' + else{ + success = true + action = `congratulate on registration and get required member data for follow-up: date of birth, initial account passphrase.` + } + confirmation.output = JSON.stringify({ success, action, }) + return confirmation case 'entrysummary': // entrySummary in Globals case 'entry_summary': case 'entry summary': - if(typeof toolArguments == 'string') - toolArguments = JSON.parse(toolArguments) - let action const entry = await factory.entry(toolArguments) if(entry){ action = `share summary of summary and follow-up with probing question` - const confirmation = { + success = true + confirmation = { tool_call_id: id, output: JSON.stringify({ success: true, action, }), } return confirmation } else { action = `journal entry failed to save, notify member and continue on for now` - const confirmation = { - tool_call_id: id, - output: JSON.stringify({ success: false, action, }), - } - return confirmation + + } + confirmation = { + tool_call_id: id, + output: JSON.stringify({ success, action, }), } + return confirmation case 'hijackattempt': case 'hijack_attempt': case 'hijack attempt': console.log('mRunFunctions()::hijack_attempt', toolArguments) if(true){ - const confirmation = { + confirmation = { tool_call_id: id, output: JSON.stringify({ success: true, }), } @@ -299,14 +319,11 @@ async function mRunFunctions(openai, run, factory){ case 'story-summary': case 'story_summary': case 'story summary': - if(typeof toolArguments == 'string') - toolArguments = JSON.parse(toolArguments) const story = await factory.story(toolArguments) if(story){ const { keywords, phaseOfLife='unknown', } = story let { interests, updates, } = factory.core // @stub - action integrates with story and interests/phase - let action switch(true){ case interests: console.log('mRunFunctions()::story-summary::interests', interests) @@ -322,7 +339,7 @@ async function mRunFunctions(openai, run, factory){ action = 'ask about another event in member\'s life' break } - const confirmation = { + confirmation = { tool_call_id: id, output: JSON.stringify({ success: true, action, }), } diff --git a/inc/json-schemas/openai/functions/confirmRegistration.json b/inc/json-schemas/openai/functions/confirmRegistration.json index da3619b..e24b69e 100644 --- a/inc/json-schemas/openai/functions/confirmRegistration.json +++ b/inc/json-schemas/openai/functions/confirmRegistration.json @@ -1,12 +1,14 @@ { - "description": "Confirm registration email by email provided by user, id stored in session memory", "name": "confirmRegistration", + "description": "Confirm registration email provided by user", "parameters": { "type": "object", "properties": { - "description": "Email provided by user", - "format": "email", - "type": "string" + "email": { + "description": "Email address provided by user", + "format": "email", + "type": "string" + } }, "required": [ "email" From 241e83de6373a25a964fbe3bb7177478ca46ede7 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Wed, 22 May 2024 22:51:57 -0400 Subject: [PATCH 09/32] 20240522 @Mookse - confirmRegistration() closes loop on confirmation --- inc/js/mylife-agent-factory.mjs | 16 ++++++ inc/js/mylife-llm-services.mjs | 52 +++++++++++-------- .../openai/functions/setMyLifeBasics.json | 23 ++++++++ 3 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 inc/json-schemas/openai/functions/setMyLifeBasics.json diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index d3a2af5..b900794 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -418,6 +418,8 @@ class AgentFactory extends BotFactory { #exposedSchemas = mExposedSchemas(['avatar','agent','consent','consent_log','relationship']) // run-once 'caching' for schemas exposed to the public, args are array of key-removals; ex: `avatar` is not an open class once extended by server #llmServices = mLLMServices #mylifeRegistrationData // @stub - move to unique MyLife factory + #registrationConfirmed // @stub - move to unique MyLife factory + #registrationDataConfirmed // @stub - move to unique MyLife factory constructor(mbr_id){ super(mbr_id, false) } @@ -459,6 +461,13 @@ class AgentFactory extends BotFactory { async challengeAccess(_mbr_id, _passphrase){ return await mDataservices.challengeAccess(_mbr_id, _passphrase) } + confirmRegistration(){ + if(!this.registrationData) + throw new Error('registration data required') + this.#mylifeRegistrationData = null + this.#registrationConfirmed = true + return this.registrationConfirmed + } async datacore(_mbr_id){ const _core = await mDataservices.getItems( 'core', @@ -610,6 +619,10 @@ class AgentFactory extends BotFactory { const savedExperience = await this.dataservices.saveExperience(_experience) return savedExperience } + async setMyLifeBasics(){ + if(!this.isMyLife) + throw new Error('MyLife server required for this function') + } /** * Submits a story to MyLife. Currently via API, but could be also work internally. * @param {object} story - Story object. @@ -687,6 +700,9 @@ class AgentFactory extends BotFactory { get organization(){ return this.schemas.Organization } + get registrationConfirmed(){ + return this.#registrationConfirmed + } /** * Gets registration data while user attempting to confirm. * @returns {object} - Registration data in memory. diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 2267bbc..90d05dc 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -259,7 +259,7 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar .map(async tool=>{ const { id, function: toolFunction, type, } = tool let { arguments: toolArguments, name, } = toolFunction - let action, + let action = '', confirmation = { tool_call_id: id, output: '', @@ -276,9 +276,13 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar action = `No email provided for registration confirmation, elicit email address for confirmation of registration and try function this again` else if(email.toLowerCase()!==factory.registrationData?.email?.toLowerCase()) action = 'Email does not match -- if occurs more than twice in this thread, fire `hijackAttempt` function' - else{ - success = true - action = `congratulate on registration and get required member data for follow-up: date of birth, initial account passphrase.` + else { + success = factory.confirmRegistration() + if(success) + action = `congratulate on registration and get required member data for follow-up: date of birth, initial account passphrase.` + else + action = 'Registration confirmation failed, notify member of system error and continue discussing MyLife organization' + } confirmation.output = JSON.stringify({ success, action, }) return confirmation @@ -296,24 +300,33 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar return confirmation } else { action = `journal entry failed to save, notify member and continue on for now` - - } - confirmation = { - tool_call_id: id, - output: JSON.stringify({ success, action, }), } + confirmation.output = JSON.stringify({ success, action, }) return confirmation case 'hijackattempt': case 'hijack_attempt': + case 'hijack-attempt': case 'hijack attempt': console.log('mRunFunctions()::hijack_attempt', toolArguments) - if(true){ - confirmation = { - tool_call_id: id, - output: JSON.stringify({ success: true, }), - } - return confirmation + success = true + confirmation.output = JSON.stringify({ success, action, }) + return confirmation + case 'setmylifebasics': + case 'set_mylife_basics': + case 'set mylife basics': + const { birthdate, passphrase, } = toolArguments + action = `error setting basics for member: ` + if(!birthdate) + action += 'birthdate missing, elicit birthdate; ' + if(!passphrase) + action += 'passphrase missing, elicit passphrase; ' + const basics = await factory.setMyLifeBasics(birthdate, passphrase) + if(basics){ + action = `congratulate member on setting up their account, display \`passphrase\` and \`birthdate\` for confirmation, and ask if they are ready to continue journey.` + success = true } + confirmation.output = JSON.stringify({ success, action, }) + return confirmation case 'story': // storySummary.json case 'storysummary': case 'story-summary': @@ -339,12 +352,10 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar action = 'ask about another event in member\'s life' break } - confirmation = { - tool_call_id: id, - output: JSON.stringify({ success: true, action, }), - } - return confirmation + success = true } // error cascades + confirmation.output = JSON.stringify({ success, action, }) + return confirmation default: throw new Error(`Tool function ${name} not recognized`) } @@ -355,7 +366,6 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar run.id, { tool_outputs: toolCallsOutput }, ) - console.log('mRunFunctions::submitToolOutputs()::run=complete', finalOutput?.status) return finalOutput /* undefined indicates to ping again */ } } diff --git a/inc/json-schemas/openai/functions/setMyLifeBasics.json b/inc/json-schemas/openai/functions/setMyLifeBasics.json new file mode 100644 index 0000000..4b4cb7d --- /dev/null +++ b/inc/json-schemas/openai/functions/setMyLifeBasics.json @@ -0,0 +1,23 @@ +{ + "description": "Personal basic required information for account creation", + "name": "setMyLifeBasics", + "parameters": { + "type": "object", + "properties": { + "passphrase": { + "description": "Private passphrase provided by user, may have spaces", + "format": "email", + "type": "string" + }, + "birthdate": { + "description": "Birthdate of user", + "format": "sql date, no time", + "type": "string" + } + }, + "required": [ + "birthdate", + "passphrase" + ] + } +} \ No newline at end of file From 9c14a497924855a62421cbf4c5b665ca077b312e Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 11:51:36 -0400 Subject: [PATCH 10/32] 20240523 @Mookse - `globals.mjs`: create `createDocumentName` --- inc/js/globals.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 51cec44..74422b2 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -130,6 +130,25 @@ class Globals extends EventEmitter { super() } /* public functions */ + createDocumentName(mbr_id, id, type){ + if(!mbr_id || !id || !type) + throw new Error('createDocumentName() expects `mbr_id`, `id`, and `type`') + return `${ type.substring(0,32) }_${mbr_id}_${id}` + } + /** + * Create a member id from a system name and id. + * @param {string} sysName - System name to create the member id from. + * @param {Guid} sysId - System id to create the member id from. + * @returns {string} - The member id created from the system name and id. + */ + createMbr_id(sysName, sysId){ + const mbr_id = sysName + .substring(0,64) + .replace(/\s/g, '_').toLowerCase() + + '_' + + sysId + return mbr_id + } /** * Get a GPT File Search Tool structure. * @param {string} vectorstoreId - the vector store id to search. From a2103b0503a1479c1f5712fac98fe9a098d35720 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 13:29:33 -0400 Subject: [PATCH 11/32] 20240523 @Mookse - self-service registration finished --- inc/js/globals.mjs | 2 + inc/js/mylife-agent-factory.mjs | 123 ++++++++++++++++++++++++-------- inc/js/mylife-avatar.mjs | 5 +- inc/js/mylife-data-service.js | 61 +++++++++++----- inc/js/mylife-datamanager.mjs | 13 ++-- inc/js/mylife-llm-services.mjs | 13 ++-- 6 files changed, 156 insertions(+), 61 deletions(-) diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 74422b2..95f2d1c 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -142,6 +142,8 @@ class Globals extends EventEmitter { * @returns {string} - The member id created from the system name and id. */ createMbr_id(sysName, sysId){ + if(!sysName?.length || !sysId?.length) + throw new Error('createMbr_id() expects a system name and id') const mbr_id = sysName .substring(0,64) .replace(/\s/g, '_').toLowerCase() diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index b900794..b57c6d9 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -418,8 +418,7 @@ class AgentFactory extends BotFactory { #exposedSchemas = mExposedSchemas(['avatar','agent','consent','consent_log','relationship']) // run-once 'caching' for schemas exposed to the public, args are array of key-removals; ex: `avatar` is not an open class once extended by server #llmServices = mLLMServices #mylifeRegistrationData // @stub - move to unique MyLife factory - #registrationConfirmed // @stub - move to unique MyLife factory - #registrationDataConfirmed // @stub - move to unique MyLife factory + #tempRegistrationData // @stub - move to unique MyLife factory constructor(mbr_id){ super(mbr_id, false) } @@ -454,27 +453,81 @@ class AgentFactory extends BotFactory { } /** * Accesses MyLife Dataservices to challenge access to a member's account. - * @param {string} _mbr_id - * @param {string} _passphrase + * @param {string} mbr_id + * @param {string} passphrase * @returns {object} - Returns passphrase document if access is granted. */ - async challengeAccess(_mbr_id, _passphrase){ - return await mDataservices.challengeAccess(_mbr_id, _passphrase) + async challengeAccess(mbr_id, passphrase){ + return await mDataservices.challengeAccess(mbr_id, passphrase) } confirmRegistration(){ + if(!this.isMyLife) + throw new Error('MyLife server required for this function') if(!this.registrationData) - throw new Error('registration data required') - this.#mylifeRegistrationData = null - this.#registrationConfirmed = true - return this.registrationConfirmed + return false + this.#mylifeRegistrationData = this.#tempRegistrationData + this.#tempRegistrationData = null + return true } - async datacore(_mbr_id){ + /** + * Set MyLife core account basics. { birthdate, passphrase, } + * @todo - move to mylife agent factory + * @param {string} birthdate - The birthdate of the member. + * @param {string} passphrase - The passphrase of the member. + * @returns {boolean} - `true` if successful + */ + async createAccount(birthdate, passphrase){ + let success = false + try{ + if(!this.isMyLife) // @stub + throw new Error('MyLife server required for this request') + if(!birthdate?.length || !passphrase?.length) + throw new Error('birthdate _**and**_ passphrase required') + const { avatarNickname, email, humanName, id, interests, } = this.#mylifeRegistrationData + let { updates='', } = this.#mylifeRegistrationData + if(!id) + throw new Error('registration not confirmed, cannot accept request') + if(!humanName) + throw new Error('member personal name required to create account') + birthdate = new Date(birthdate).toISOString() + if(!birthdate?.length) + throw new Error('birthdate format could not be parsed') + const birth = [{ // current 20240523 format + date: birthdate, + }] + const mbr_id = this.globals.createMbr_id(avatarNickname ?? humanName, id) + if(await this.testPartitionKey(mbr_id)) + throw new Error('mbr_id already exists') + const names = [humanName] // currently array of flat strings + updates = (updates.length ? ' ' : '') + + `${ humanName } has just joined MyLife on ${ new Date().toDateString() }!` + const validation = ['registration',] // list of passed validation routines + const core = { + birth, + email, + id, + interests, + mbr_id, + names, + passphrase, + updates, + validation, + } + const save = await this.dataservices.addCore(core) + this.#mylifeRegistrationData = null + this.#tempRegistrationData = null + success = save.success + console.log(chalk.blueBright('createAccount()'), save) + } catch(error){ console.log(chalk.blueBright('createAccount()::error'), chalk.bgRed(error)) } + return success + } + async datacore(mbr_id){ const _core = await mDataservices.getItems( 'core', undefined, undefined, undefined, - _mbr_id, + mbr_id, ) return _core?.[0]??{} } @@ -619,10 +672,6 @@ class AgentFactory extends BotFactory { const savedExperience = await this.dataservices.saveExperience(_experience) return savedExperience } - async setMyLifeBasics(){ - if(!this.isMyLife) - throw new Error('MyLife server required for this function') - } /** * Submits a story to MyLife. Currently via API, but could be also work internally. * @param {object} story - Story object. @@ -652,20 +701,31 @@ class AgentFactory extends BotFactory { /** * Tests partition key for member * @public - * @param {string} _mbr_id member id - * @returns {boolean} returns true if partition key is valid + * @param {string} mbr_id member id + * @returns {boolean} - `true` if partition key is active, `false` otherwise. */ - async testPartitionKey(_mbr_id){ - if(!this.isMyLife) return false - return await mDataservices.testPartitionKey(_mbr_id) + async testPartitionKey(mbr_id){ + if(!this.isMyLife) + return false + return await mDataservices.testPartitionKey(mbr_id) } /** * Validate registration id. * @param {Guid} validationId - The registration id. - * @returns {Promise} - Whether or not code was valid (true) or not (false). + * @returns {Promise} - Registration data from system datacore. */ async validateRegistration(registrationId){ - return await this.dataservices.validateRegistration(registrationId) + let registration, + success = false + try{ + registration = await this.dataservices.validateRegistration(registrationId) + success = registration.id?.length + console.log(chalk.blueBright('validateRegistration()'), success) + } catch(error){ + registration = null + console.log(chalk.blueBright('validateRegistration()::error'), chalk.bgRed(error)) + } + return registration } // getters/setters get alerts(){ // currently only returns system alerts @@ -700,15 +760,13 @@ class AgentFactory extends BotFactory { get organization(){ return this.schemas.Organization } - get registrationConfirmed(){ - return this.#registrationConfirmed - } /** - * Gets registration data while user attempting to confirm. + * Gets registration data while user attempting to confirm. If temp data exists, it takes primacy, otherwise hardened `#mylifeRegistrationData` is returned. * @returns {object} - Registration data in memory. */ get registrationData(){ - return this.#mylifeRegistrationData + return this.#tempRegistrationData + ?? this.#mylifeRegistrationData } /** * Sets registration data while user attempting to confirm. @@ -721,10 +779,13 @@ class AgentFactory extends BotFactory { throw new Error('MyLife factory required to store registration data') if(!registrationData) throw new Error('registration data required') - if(!this.#mylifeRegistrationData){ - this.#mylifeRegistrationData = registrationData + if(!this.#tempRegistrationData){ + const { id, } = registrationData + if(!id?.length) + throw new Error('registration id required') + this.#tempRegistrationData = registrationData setTimeout(timeout=>{ // Set a timeout to clear the data after 5 minutes (300000 milliseconds) - this.#mylifeRegistrationData = null + this.#tempRegistrationData = null }, 300000) } } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index d3f060c..cda451d 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -1899,21 +1899,20 @@ async function mValidateRegistration(activeBot, factory, validationId){ registrationData = { id: validationId }, success = false const registration = await factory.validateRegistration(validationId) + console.log('mValidateRegistration::registration', registration) const messages = [] - const failureMessage = 'I\'m sorry, but I was unable to validate your registration.' + const failureMessage = `I\'m sorry, but I\'m currently unable to validate your registration id:
${ validationId }.
I\'d be happy to talk with you more about MyLife, but you may need to contact member support to resolve this issue.` /* determine eligibility */ if(registration){ const { avatarNickname, being, email: registrationEmail, humanName, } = registration const eligible = being==='registration' && factory.globals.isValidEmail(registrationEmail) - // ensure not in cosmos _already_: storedProc for this I think? if(eligible){ const successMessage = `Hello and _thank you_ for your registration, ${ humanName }!\nI'm Q, the ai-representative for MyLife, and I'm excited to help you get started, so let's do the following:\n1. Verify your email address\n2. set up your account\n3. get you started with your first MyLife experience!\n\nSo let me walk you through the process. In the chat below, please enter the email you registered with and hit the **submit** button!` message = mCreateSystemMessage(activeBot, successMessage, factory) registrationData.avatarNickname = avatarNickname registrationData.email = registrationEmail registrationData.humanName = humanName - console.log('mValidateRegistration::eligible', registrationData) success = true } } diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index 6891f76..e2f9a2f 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -109,6 +109,24 @@ class Dataservices { return this.#partitionId } // public functions + async addCore(core){ + const { id, mbr_id, } = core + if(!id?.length || !mbr_id?.length) + throw new Error('`core` must be a pre-formed object with id and mbr_id') + const extantCore = await this.getItem(id, undefined, mbr_id) + if(extantCore) + return { core: extantCore, success: false, } // no alterations, failure + core = { // enforce core data structure + ...core, + being: 'core', + id, + format: 'human', + name: this.globals.createDocumentName(mbr_id, id, 'core'), + mbr_id, + } + core = await this.pushItem(core) + return { core, success: true, } + } /** * Retrieves all public experiences (i.e., owned by MyLife). * @public @@ -259,8 +277,8 @@ class Dataservices { return [] }) } - async datacore(_mbr_id){ - return await this.getItem(_mbr_id) + async datacore(mbr_id){ + return await this.getItem(mbr_id) } /** * Delete an item from member container. @@ -398,24 +416,24 @@ class Dataservices { * Retrieves a specific item by its ID. * @async * @public - * @param {string} _id - The unique identifier for the item. + * @param {string} id - The unique identifier for the item. * @param {string} container_id - The container to use, overriding default: `Members`. - * @param {string} _mbr_id - The member id to use, overriding default. + * @param {string} mbr_id - The member id to use, overriding default. * @returns {Promise} The item corresponding to the provided ID. */ - async getItem(_id, container_id, _mbr_id=this.mbr_id) { - if(!_id) return + async getItem(id, container_id, mbr_id=this.mbr_id) { + if(!id) + return null try{ return await this.datamanager.getItem( - _id, + id, container_id, - { partitionKey: _mbr_id, populateQuotaInfo: false, }, + { partitionKey: mbr_id, populateQuotaInfo: false, }, ) } - catch(_error){ - console.log('mylife-data-service::getItem() error') - console.log(_error, _id, container_id,) - return + catch(error){ + console.log('mylife-data-service::getItem() error', error, id, mbr_id, container_id,) + return null } } /** @@ -663,12 +681,13 @@ class Dataservices { /** * Tests partition key for member * @public - * @param {string} _mbr_id member id - * @returns {boolean} returns true if partition key is valid + * @param {string} mbr_id member id + * @returns {boolean} - `true` if partition key is active, `false` otherwise. */ - async testPartitionKey(_mbr_id){ - if(!this.isMyLife) return false - return await this.datamanager.testPartitionKey(_mbr_id) + async testPartitionKey(mbr_id){ + if(!this.isMyLife) + return false + return await this.datamanager.testPartitionKey(mbr_id) } /** * Returns the registration record by Id. @@ -676,7 +695,13 @@ class Dataservices { * @returns {object} - The registration document, if exists. */ async validateRegistration(registrationId){ - const registration = await this.getItem(registrationId, 'registration', this.mbr_id) + const { mbr_id, } = this + const registration = await this.getItem(registrationId, 'registration', mbr_id) + const { humanName, id, } = registration + if(humanName?.length && id?.length){ + if(await this.testPartitionKey(this.globals.createMbr_id(humanName, registrationId))) + throw new Error('Registrant already a member!') + } return registration } } diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index 2937981..5c41b84 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -131,12 +131,17 @@ class Datamanager { .upsert(_candidate) return doc } - async testPartitionKey(_mbr_id){ - const { resource: _result } = await this.#containers['members'] + /** + * Checks if provided mbr_id is an active partition key. + * @param {string} mbr_id - The member id, also container name, to test. + * @returns {boolean} - `true` if partition key is active, `false` otherwise. + */ + async testPartitionKey(mbr_id){ + const { resource: result } = await this.#containers['members'] .scripts .storedProcedure('testPartitionKey') - .execute(_mbr_id) // first parameter is partition key, second is passphrase, third is case sensitivity - return _result + .execute(mbr_id) // first parameter is partition key, second is passphrase, third is case sensitivity + return result } /* getters/setters */ get globals(){ diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 90d05dc..b287c71 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -275,7 +275,7 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar if(!email?.length) action = `No email provided for registration confirmation, elicit email address for confirmation of registration and try function this again` else if(email.toLowerCase()!==factory.registrationData?.email?.toLowerCase()) - action = 'Email does not match -- if occurs more than twice in this thread, fire `hijackAttempt` function' + action = 'Email does not match -- if occurs more than three times in this thread, fire `hijackAttempt` function' else { success = factory.confirmRegistration() if(success) @@ -320,10 +320,13 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar action += 'birthdate missing, elicit birthdate; ' if(!passphrase) action += 'passphrase missing, elicit passphrase; ' - const basics = await factory.setMyLifeBasics(birthdate, passphrase) - if(basics){ - action = `congratulate member on setting up their account, display \`passphrase\` and \`birthdate\` for confirmation, and ask if they are ready to continue journey.` - success = true + try { + success = await factory.createAccount(birthdate, passphrase) + action = success + ? `congratulate member on creating their MyLife membership, display \`passphrase\` in bold for review (or copy/paste), and ask if they are ready to continue journey.` + : action + 'server failure for `factory.createAccount()`' + } catch(error){ + action += '__ERROR: ' + error.message } confirmation.output = JSON.stringify({ success, action, }) return confirmation From b47890b801721129db99191bbe0a12ba59e2600d Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 14:01:25 -0400 Subject: [PATCH 12/32] 20240523 @Mookse - fix: id created with avatar, not human name --- inc/js/mylife-data-service.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index e2f9a2f..4a8c90a 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -691,15 +691,18 @@ class Dataservices { } /** * Returns the registration record by Id. + * @todo - revisit hosts: currently process.env * @param {string} registrationId - Guid for registration record in system container. * @returns {object} - The registration document, if exists. */ async validateRegistration(registrationId){ const { mbr_id, } = this const registration = await this.getItem(registrationId, 'registration', mbr_id) - const { humanName, id, } = registration - if(humanName?.length && id?.length){ - if(await this.testPartitionKey(this.globals.createMbr_id(humanName, registrationId))) + const { avatarNickname, humanName, id, } = registration + if(avatarNickname?.length && id?.length){ + const tempMbr_id = this.globals.createMbr_id(avatarNickname, id) + const exists = await this.testPartitionKey(tempMbr_id) + if(exists) throw new Error('Registrant already a member!') } return registration From 67890fbf24f9112b547e624c28bf745809135633 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 16:39:42 -0400 Subject: [PATCH 13/32] 20240523 @Mookse - added thread_id for tracking to system-generated summaries --- inc/js/mylife-agent-factory.mjs | 84 +++++++++++++++++++++++---------- inc/js/mylife-llm-services.mjs | 23 +++++---- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index b57c6d9..310bcdc 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -284,10 +284,15 @@ class BotFactory extends EventEmitter{ const stories = ( await this.stories(updatedLibrary.form) ) .filter(story=>!this.globals.isValidGuid(story?.library_id)) .map(story=>{ - story.id = story.id ?? this.newGuid - story.author = story.author ?? this.mbr_name - story.title = story.title ?? story.id - return mInflateLibraryItem(story, updatedLibrary.id, this.mbr_id) + const { mbr_name, newGuid } = this + const { author=mbr_name, id=newGuid, title=mbr_name, } = story + story = { + ...story, + author, + id, + title, + } + return mInflateLibraryItem(story, updatedLibrary.id, mbr_id) }) updatedLibrary.items = [ ...updatedLibrary.items, @@ -532,14 +537,24 @@ class AgentFactory extends BotFactory { return _core?.[0]??{} } async entry(entry){ - if(!entry.summary?.length) + const { + assistantType='journaler', + being='entry', + form='journal', + keywords=[], + summary, + thread_id, + title='New Journal Entry', + } = entry + if(!summary?.length) throw new Error('entry summary required') const { mbr_id, newGuid: id, } = this - const { assistantType='journaler', being='entry', form='journal', keywords=['journal entry'], summary, title='New Journal Entry', } = entry - let { name, } = entry - name = name - ?? title - ?? `entry_${ mbr_id }_${ id }` + const name = `entry_${ title.substring(0,64) }_${ mbr_id }_${ id }` + /* assign default keywords */ + if(!keywords.includes('memory')) + keywords.push('memory') + if(!keywords.includes('biographer')) + keywords.push('biographer') const completeEntry = { ...entry, ...{ @@ -551,6 +566,7 @@ class AgentFactory extends BotFactory { mbr_id, name, summary, + thread_id, title, }} return await this.dataservices.entry(completeEntry) @@ -678,25 +694,41 @@ class AgentFactory extends BotFactory { * @returns {object} - The story document from Cosmos. */ async story(story){ - if(!story.summary?.length) + const { + assistantType='biographer-bot', + being='story', + form='biographer', + keywords=[], + phaseOfLife='unknown', + summary, + thread_id, + title='New Memory Entry', + } = story + if(!summary?.length) throw new Error('story summary required') - const id = this.newGuid - const title = story.title ?? 'New Memory Entry' - const finalStory = { + const { mbr_id, newGuid: id, } = this + const name = `story_${ title.substring(0,64) }_${ mbr_id }_${ id }` + /* assign default keywords */ + if(!keywords.includes('memory')) + keywords.push('memory') + if(!keywords.includes('biographer')) + keywords.push('biographer') + const validatedStory = { ...story, ...{ - assistantType: story.assistantType ?? 'biographer-bot', - being: story.being ?? 'story', - form: story.form ?? 'biographer', - id, - keywords: story.keywords ?? ['memory', 'biographer', 'entry'], - mbr_id: this.mbr_id, - name: story.name ?? title ?? `story_${ this.mbr_id }_${ id }`, - phaseOfLife: story.phaseOfLife ?? 'unknown', - summary: story.summary, - title, - }} - return await this.dataservices.story(finalStory) + assistantType, + being, + form, + id, + keywords, + mbr_id, + name, + phaseOfLife, + summary, + thread_id, + title, + }} + return await this.dataservices.story(validatedStory) } /** * Tests partition key for member diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index b287c71..6f19c35 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -254,11 +254,12 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar && run.required_action?.submit_tool_outputs?.tool_calls && run.required_action.submit_tool_outputs.tool_calls.length ){ + const { assistant_id: bot_id, metadata, thread_id, } = run const toolCallsOutput = await Promise.all( run.required_action.submit_tool_outputs.tool_calls .map(async tool=>{ const { id, function: toolFunction, type, } = tool - let { arguments: toolArguments, name, } = toolFunction + let { arguments: toolArguments={}, name, } = toolFunction let action = '', confirmation = { tool_call_id: id, @@ -266,7 +267,8 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar }, success = false if(typeof toolArguments==='string') - toolArguments = JSON.parse(toolArguments) + toolArguments = JSON.parse(toolArguments) ?? {} + toolArguments.thread_id = thread_id switch(name.toLowerCase()){ case 'confirmregistration': case 'confirm_registration': @@ -282,7 +284,6 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar action = `congratulate on registration and get required member data for follow-up: date of birth, initial account passphrase.` else action = 'Registration confirmation failed, notify member of system error and continue discussing MyLife organization' - } confirmation.output = JSON.stringify({ success, action, }) return confirmation @@ -339,17 +340,23 @@ async function mRunFunctions(openai, run, factory){ // convert factory to avatar if(story){ const { keywords, phaseOfLife='unknown', } = story let { interests, updates, } = factory.core + if(typeof interests=='array') + interests = interests.join(', ') + if(typeof updates=='array') + updates = updates.join(', ') // @stub - action integrates with story and interests/phase switch(true){ - case interests: - console.log('mRunFunctions()::story-summary::interests', interests) - if(typeof interests == 'array') - interests = interests.join(',') + case interests?.length: action = `ask about a different interest from: ${ interests }` + console.log('mRunFunctions()::story-summary::interests', interests) break case phaseOfLife!=='unknown': + action = `ask about another encounter during this phase of life: ${ phaseOfLife }` console.log('mRunFunctions()::story-summary::phaseOfLife', phaseOfLife) - action = `ask about another encounter during this phase of life: ${story.phaseOfLife}` + break + case updates?.length: + action = `ask about current events related to or beyond: ${ updates }` + console.log('mRunFunctions()::story-summary::updates', updates) break default: action = 'ask about another event in member\'s life' From 5b2b79d6c58c1cda7d5e27a7667dc320b95dfae6 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 17:10:08 -0400 Subject: [PATCH 14/32] 20240523 @Mookse - frontend fix for chatInput bloat and shrink --- views/assets/css/chat.css | 5 +++++ views/assets/js/guests.mjs | 32 +++++++++++++++++++------------- views/index.html | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/views/assets/css/chat.css b/views/assets/css/chat.css index 0d8c709..2d282c1 100644 --- a/views/assets/css/chat.css +++ b/views/assets/css/chat.css @@ -9,6 +9,7 @@ color: #dcb6ff; /* White text for better readability */ } .await-button { + align-self: center; background-color: #007BFF; border: aliceblue solid thin; border-radius: 0.3em; @@ -95,6 +96,10 @@ cursor: pointer; /* Pointer cursor on hover */ z-index: 2; /* Ensure it's above other content */ } +.chat-user-input { + min-height: 2rem; +} +/* chat-submit button; requires special treament to overwrite button class for now */ button.chat-submit { /* need specificity to overwrite button class */ flex: 0 0 auto; color: rgba(65, 84, 104, 0.85); diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index 05797b5..698f689 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -86,14 +86,12 @@ function mAddMessage(message, options={ function mAddUserMessage(event){ event.preventDefault() // Dynamically get the current message element (input or textarea) - let userMessage = chatInput.value.trim() - if (!userMessage.length) return - userMessage = mGlobals.escapeHtml(userMessage) // Escape the user message - mSubmitInput(event, userMessage) - mAddMessage({ message: userMessage }, { - bubbleClass: 'user-bubble', - delay: 7, - }) + const userMessage = chatInput.value.trim() + if(!userMessage.length) + return + const message = mGlobals.escapeHtml(userMessage) // Escape the user message + mSubmitInput(event, message) + mAddMessage({ message, }, { bubbleClass: 'user-bubble', delay: 7, }) } /** * Fetches the greeting messages from the server. The greeting object from server: { error, messages, success, } @@ -200,19 +198,26 @@ function mShowPage(){ show(chatUser) } /** - * - * @param {Event} event - * @param {string} _message + * Submits a message to the server. + * @param {Event} event - The event object. + * @param {string} message - The message to submit. */ -async function mSubmitInput(event, _message){ +async function mSubmitInput(event, message){ + if(!message) + return event.preventDefault() const url = window.location.origin + const thread_id = threadId ?? null const options = { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ message: _message, role: 'user', thread_id: threadId }), + body: JSON.stringify({ + message, + role: 'user', + thread_id, + }), } hide(chatUser) show(awaitButton) @@ -228,6 +233,7 @@ async function mSubmitInput(event, _message){ hide(awaitButton) chatInput.value = null chatInput.placeholder = mPlaceholder + mToggleInputTextarea() show(chatUser) } async function submitChat(url, options) { diff --git a/views/index.html b/views/index.html index 82e941f..c512718 100644 --- a/views/index.html +++ b/views/index.html @@ -9,7 +9,7 @@ - + + + \ No newline at end of file From 664c85fe05b0cdd2c6fd2f4af84a094c7603fd55 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 18:49:35 -0400 Subject: [PATCH 16/32] 20240523 @Mookse - resize input box after submission --- views/assets/html/_widget-contributions.html | 2 +- views/assets/js/bots.mjs | 4 +- views/assets/js/experience.mjs | 2 +- views/assets/js/guests.mjs | 91 +++++++++-------- views/assets/js/members.mjs | 101 ++++++++++++------- 5 files changed, 114 insertions(+), 86 deletions(-) diff --git a/views/assets/html/_widget-contributions.html b/views/assets/html/_widget-contributions.html index 311ad4e..fea7b91 100644 --- a/views/assets/html/_widget-contributions.html +++ b/views/assets/html/_widget-contributions.html @@ -75,7 +75,7 @@ _delay: 6, }; setActiveCategory(category, contributionId, _question); - addMessageToColumn(_message, _options); + addMessage(_message, _options); } function getRandomQuestion(questions) { diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 10c774b..5afb210 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1,7 +1,7 @@ /* bot functionality */ /* imports */ import { - addMessageToColumn, + addMessage, availableExperiences, hide, inExperience, @@ -494,7 +494,7 @@ function mGreeting(){ function addIntroductionMessage() { // Clear the 7.5 seconds timeout if any event is triggered clearTimeout(timerId) greeting.forEach(_greeting =>{ - addMessageToColumn({ message: _greeting }) + addMessage(_greeting) }) cleanupListeners() } diff --git a/views/assets/js/experience.mjs b/views/assets/js/experience.mjs index 6944f8a..02ebb79 100644 --- a/views/assets/js/experience.mjs +++ b/views/assets/js/experience.mjs @@ -1,6 +1,6 @@ /* imports */ import { - addMessageToColumn, + addMessage, assignElements, clearSystemChat, escapeHtml, diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index 698f689..f104faf 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -9,8 +9,8 @@ const mPlaceholder = `Type a message to ${ mAvatarName }...` const show = mGlobals.show /* variables */ let mChatBubbleCount = 0, - threadId = null, - typingTimer + mDefaultTypeDelay = 7, + threadId = null /* page div variables */ let aboutContainer, awaitButton, @@ -35,47 +35,26 @@ document.addEventListener('DOMContentLoaded', event=>{ /** * Adds a message to the chat column. * @private - * @param {object|string} message - The message to add to the chat column. + * @param {string} message - The message to add to the chat column. * @param {object} options - The options for the chat bubble. * @returns`{void} */ -function mAddMessage(message, options={ - bubbleClass: 'agent-bubble', - delay: 15, - typewrite: true, -}){ - let messageContent = message.message ?? message - const originalMessage = messageContent - const { - bubbleClass, - delay, - typewrite, +function mAddMessage(message, options={}){ + const { + bubbleClass='agent-bubble', + typeDelay=mDefaultTypeDelay, + typewrite=true, } = options const chatBubble = document.createElement('div') chatBubble.id = `chat-bubble-${mChatBubbleCount}` chatBubble.className = `chat-bubble ${bubbleClass}` mChatBubbleCount++ chatSystem.appendChild(chatBubble) - messageContent = mGlobals.escapeHtml(messageContent) - if(typewrite){ - let i = 0 - let tempMessage = '' - function _typeAgentMessage() { - if (i <= originalMessage.length ?? 0) { - tempMessage += originalMessage.charAt(i) - chatBubble.innerHTML = '' - chatBubble.insertAdjacentHTML('beforeend', tempMessage) - i++ - setTimeout(_typeAgentMessage, delay) // Adjust the typing speed here (50ms) - scrollToBottom() - } else { - chatBubble.setAttribute('status', 'done') - } - } - _typeAgentMessage() - } else { - chatBubble.insertAdjacentHTML('beforeend', originalMessage) - scrollToBottom() + if(typewrite) + mTypeMessage(chatBubble, message, typeDelay) + else { + chatBubble.insertAdjacentHTML('beforeend', message) + mScrollBottom() } } /** @@ -91,7 +70,7 @@ function mAddUserMessage(event){ return const message = mGlobals.escapeHtml(userMessage) // Escape the user message mSubmitInput(event, message) - mAddMessage({ message, }, { bubbleClass: 'user-bubble', delay: 7, }) + mAddMessage(message, { bubbleClass: 'user-bubble', typeDelay: 2, }) } /** * Fetches the greeting messages from the server. The greeting object from server: { error, messages, success, } @@ -124,9 +103,11 @@ async function mFetchGreetings(dynamic=false){ async function mFetchStart(){ const greetings = await mFetchGreetings() greetings.forEach(greeting=>{ - mAddMessage(greeting, { + const message = greeting?.message + ?? greeting + mAddMessage(message, { bubbleClass: 'agent-bubble', - delay: 10, + typeDelay: 8, typewrite: true, }) }) @@ -165,7 +146,11 @@ async function mLoadStart(){ /* fetch the greeting messages */ await mFetchStart() } -function scrollToBottom() { +/** + * Scrolls overflow of system chat to bottom. + * @returns {void} + */ +function mScrollBottom() { chatSystem.scrollTop = chatSystem.scrollHeight } /** @@ -225,11 +210,8 @@ async function mSubmitInput(event, message){ // now returns array of messages _gptChat.forEach(gptMessage=>{ threadId = gptMessage.thread_id - mAddMessage({ - message: gptMessage.message, - delay: 10, - }); - }); + mAddMessage(gptMessage.message) + }) hide(awaitButton) chatInput.value = null chatInput.placeholder = mPlaceholder @@ -260,4 +242,27 @@ function mToggleSubmitButton(){ const hasInput = chatInput.value.trim().length ?? false chatSubmit.disabled = !hasInput chatSubmit.style.cursor = hasInput ? 'pointer' : 'not-allowed' +} +/** + * Types a message in the chat bubble. + * @param {HTMLDivElement} chatBubble - The chat bubble element. + * @param {string} message - The message to type. + * @param {number} typeDelay - The delay between typing each character. + * @returns {void} + */ +function mTypeMessage(chatBubble, message, typeDelay=mDefaultTypeDelay){ + let i = 0 + let tempMessage = '' + function _typewrite() { + if(i <= message.length ?? 0){ + tempMessage += message.charAt(i) + chatBubble.innerHTML = '' + chatBubble.insertAdjacentHTML('beforeend', tempMessage) + i++ + setTimeout(_typewrite, typeDelay) // Adjust the typing speed here (50ms) + } else + chatBubble.setAttribute('status', 'done') + mScrollBottom() + } + _typewrite() } \ No newline at end of file diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index c090e49..e1d5588 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -24,8 +24,7 @@ const mainContent = mGlobals.mainContent, let mAutoplay=false, mChatBubbleCount = 0, mExperience, - mMemberId, - typingTimer + mMemberId /* page div variables */ let activeCategory, awaitButton, @@ -68,25 +67,10 @@ document.addEventListener('DOMContentLoaded', async event=>{ * Pushes content to the chat column. * @public * @param {string} message - The message object to add to column. - * @param {object} options - The options object. + * @param {object} options - The options object { bubbleClass, typeDelay, typewrite }. */ -function addMessageToColumn(message, options={ - bubbleClass: 'agent-bubble', - _delay: 10, - _typewrite: true, -}){ - const messageContent = message.message ?? message - const { - bubbleClass, - _delay, - _typewrite, - } = options - const chatBubble = document.createElement('div') - chatBubble.id = `chat-bubble-${mChatBubbleCount}` - chatBubble.classList.add('chat-bubble', bubbleClass) - chatBubble.innerHTML = messageContent - mChatBubbleCount++ - systemChat.appendChild(chatBubble) +function addMessage(message, options={}){ + mAddMessage(message, options) } /** * Removes and attaches all payload elements to element. @@ -309,7 +293,7 @@ function waitForUserAction(){ * @param {Event} event - The event object. * @returns {Promise} */ -async function mAddMemberDialog(event){ +async function mAddMemberMessage(event){ event.stopPropagation() event.preventDefault() let memberMessage = chatInputField.value.trim() @@ -317,18 +301,43 @@ async function mAddMemberDialog(event){ return /* prepare request */ toggleMemberInput(false) /* hide */ - addMessageToColumn({ message: memberMessage }, { + mAddMessage(memberMessage, { bubbleClass: 'user-bubble', - _delay: 7, + role: 'member', + typeDelay: 7, }) /* server request */ const responses = await submit(memberMessage, false) /* process responses */ - responses.forEach(response => { - addMessageToColumn({ message: response.message }) - }) + responses + .forEach(response => { + mAddMessage(response?.message, { + bubbleClass: 'agent-bubble', + role: 'agent', + typeDelay: 1, + }) + }) toggleMemberInput(true)/* show */ } +async function mAddMessage(message, options={}){ + const { + bubbleClass, + role='agent', + typeDelay=2, + typewrite=true, + } = options + const chatBubble = document.createElement('div') + chatBubble.id = `chat-bubble-${mChatBubbleCount}` + chatBubble.classList.add('chat-bubble', (bubbleClass ?? role+'-bubble')) + mChatBubbleCount++ + systemChat.appendChild(chatBubble) + if(typewrite) + mTypeMessage(chatBubble, message, typeDelay) + else { + chatBubble.insertAdjacentHTML('beforeend', message) + mScrollBottom() + } +} function bot(_id){ return mPageBots.find(bot => bot.id === _id) } @@ -402,7 +411,7 @@ function mInitializePageListeners(){ }) /* page listeners */ chatInputField.addEventListener('input', toggleInputTextarea) - memberSubmit.addEventListener('click', mAddMemberDialog) /* note default listener */ + memberSubmit.addEventListener('click', mAddMemberMessage) /* note default listener */ chatRefresh.addEventListener('click', clearSystemChat) const currentPath = window.location.pathname // Get the current path const navigationLinks = document.querySelectorAll('.navigation-nav .navigation-link') // Select all nav links @@ -437,7 +446,7 @@ function mResetAnimation(element){ */ function sceneTransition(type='interface'){ /* assign listeners */ - memberSubmit.removeEventListener('click', mAddMemberDialog) + memberSubmit.removeEventListener('click', mAddMemberMessage) memberSubmit.addEventListener('click', submitInput) /* clear "extraneous" */ hide(navigation) @@ -456,6 +465,13 @@ function sceneTransition(type='interface'){ /* show member chat */ showMemberChat() } +/** + * Scrolls overflow of system chat to bottom. + * @returns {void} + */ +function mScrollBottom(){ + systemChat.scrollTop = systemChat.scrollHeight +} /** * Transitions the stage to active member version. * @param {boolean} includeSidebar - The include-sidebar flag. @@ -463,7 +479,7 @@ function sceneTransition(type='interface'){ */ function mStageTransitionMember(includeSidebar=true){ memberSubmit.removeEventListener('click', submitInput) - memberSubmit.addEventListener('click', mAddMemberDialog) + memberSubmit.addEventListener('click', mAddMemberMessage) hide(transport) hide(screen) document.querySelectorAll('.mylife-widget') @@ -567,21 +583,28 @@ function toggleSubmitButtonState() { * Typewrites a message to a chat bubble. * @param {HTMLDivElement} chatBubble - The chat bubble element. * @param {string} message - The message to type. - * @param {number} delay - The delay between iterations. - * @param {number} i - The iteration number. + * @param {number} typeDelay - The delay between typing each character. + * @returns {void} */ -function mTypewriteMessage(chatBubble, message, delay=10, i=0){ - if(imTypewriteMessage(chatBubble, message, delay, i), delay) - } else { - chatBubble.setAttribute('status', 'done') +function mTypeMessage(chatBubble, message, typeDelay=mDefaultTypeDelay){ + let i = 0 + let tempMessage = '' + function _typewrite() { + if(i <= message.length ?? 0){ + tempMessage += message.charAt(i) + chatBubble.innerHTML = '' + chatBubble.insertAdjacentHTML('beforeend', tempMessage) + i++ + setTimeout(_typewrite, typeDelay) // Adjust the typing speed here (50ms) + } else + chatBubble.setAttribute('status', 'done') + mScrollBottom() } + _typewrite() } /* exports */ export { - addMessageToColumn, + addMessage, assignElements, availableExperiences, clearSystemChat, From aa3aaf7d0aaccace12bdc3ffe2f5991c36fe6b17 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 23 May 2024 22:44:39 -0400 Subject: [PATCH 17/32] 20240523 @Mookse - thread refix - cosmetic --- .../factory-class-extenders/class-extenders.mjs | 2 +- inc/js/mylife-avatar.mjs | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index eb8274e..75f45ba 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -143,7 +143,7 @@ function extendClass_conversation(originClass, referencesObject) { this.#thread = thread this.#botId = botId this.name = `conversation_${this.#factory.mbr_id}_${thread.thread_id}` - this.type = this.type??'chat' + this.type = this.type ?? 'chat' } /* public functions */ /** diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index cda451d..d59895b 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -90,11 +90,13 @@ class Avatar extends EventEmitter { this.activeBotId = activeBot.id this.#llmServices.botId = activeBot.bot_id /* conversations */ - this.#bots.forEach(async bot=>{ - const { thread_id, } = bot - if(thread_id) - this.#conversations.push(await this.createConversation('chat', thread_id)) - }) + await Promise.all( + this.#bots.map(async bot=>{ + const { thread_id } = bot + if(thread_id && !this.getConversation(thread_id)) + this.#conversations.push(await this.createConversation('chat', thread_id)) + }) + ) /* experience variables */ this.#experienceGenericVariables = mAssignGenericExperienceVariables(this.#experienceGenericVariables, this) /* lived-experiences */ @@ -236,7 +238,7 @@ class Avatar extends EventEmitter { */ async createConversation(type='chat', threadId){ const thread = await this.#llmServices.thread(threadId) - const conversation = new (this.#factory.conversation)({ mbr_id: this.mbr_id, type: type }, this.#factory, thread, this.activeBotId) // guid only + const conversation = new (this.#factory.conversation)({ mbr_id: this.mbr_id, type, }, this.#factory, thread, this.activeBotId) // guid only this.#conversations.push(conversation) return conversation } @@ -353,7 +355,7 @@ class Avatar extends EventEmitter { */ getConversation(threadId){ const conversation = this.#conversations - .find(conversation=>conversation.thread?.id ?? conversation.thread_id===threadId) + .find(conversation=>conversation.thread?.id===threadId) return conversation } /** @@ -1066,6 +1068,7 @@ async function mBot(factory, avatar, bot){ if(!excludeTypes.includes(type)){ const conversation = await avatar.createConversation() assistant.thread_id = conversation.thread_id + avatar.conversations.push(conversation) } } factory.setBot(assistant) // update Cosmos (no need async) From 8ea6b38a461c2c3999d332218b71f0491d99269a Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 24 May 2024 12:50:03 -0400 Subject: [PATCH 18/32] 20240524 @Mookse - cosmetic (fix memoir) --- inc/js/menu.mjs | 1 + inc/js/mylife-avatar.mjs | 2 +- views/assets/js/bots.mjs | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/inc/js/menu.mjs b/inc/js/menu.mjs index 9015643..0755892 100644 --- a/inc/js/menu.mjs +++ b/inc/js/menu.mjs @@ -10,6 +10,7 @@ class Menu { return [ { display: `meet ${ _Agent.agentName }`, route: '/', icon: 'home' }, { display: `about`, route: '/about', icon: 'about' }, + { display: `donate`, route: 'https://gofund.me/65013d6e', icon: 'donate' }, // { display: `membership`, route: '/members', icon: 'membership' }, // { display: `register`, route: '/register', icon: 'register' }, ] diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index d59895b..404a958 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -141,7 +141,7 @@ class Avatar extends EventEmitter { activeBotId = this.activeBot.id threadId = this.activeBot.thread_id if(this.#factory.registrationData) // trigger confirmation until session (or vld) ends - chatMessage = `CONFIRM REGISTRATION: ` + chatMessage + chatMessage = `CONFIRM REGISTRATION: ${ chatMessage }` } if(!chatMessage) throw new Error('No message provided in context') diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 5afb210..d19fd65 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -19,11 +19,11 @@ const mAvailableMimeTypes = [], health: { name: 'Health', }, - memoire: { + memoir: { allowCustom: true, allowedTypes: ['diary','personal-biographer', 'journaler',], - description: 'The Memoire Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', - name: 'Memoire', + description: 'The Memoir Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', + name: 'Memoir', }, professional: { name: 'Job', @@ -41,7 +41,7 @@ const mAvailableMimeTypes = [], }, mAvailableUploaderTypes = ['library', 'personal-avatar', 'personal-biographer', 'resume',], botBar = document.getElementById('bot-bar'), - mDefaultTeam = 'memoire', + mDefaultTeam = 'memoir', mGlobals = new Globals(), mLibraries = ['entry', 'experience', 'file', 'story'], // ['chat', 'conversation'] mLibraryCollections = document.getElementById('library-collections'), From 182155b515e99bef7f0a44498dc0693f1cf3b250 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 24 May 2024 13:10:25 -0400 Subject: [PATCH 19/32] 20240524 @Mookse - fix: delimeter for mbr_id --- inc/js/globals.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 95f2d1c..aa89a99 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -136,18 +136,19 @@ class Globals extends EventEmitter { return `${ type.substring(0,32) }_${mbr_id}_${id}` } /** - * Create a member id from a system name and id. + * Create a member id from a system name and id: sysName|sysId. * @param {string} sysName - System name to create the member id from. - * @param {Guid} sysId - System id to create the member id from. + * @param {Guid} sysId - System id to create the member id from, `Guid` required. * @returns {string} - The member id created from the system name and id. */ createMbr_id(sysName, sysId){ - if(!sysName?.length || !sysId?.length) - throw new Error('createMbr_id() expects a system name and id') + if(!sysName?.length || !isValidGuid(sysId)) + throw new Error('createMbr_id() expects params: sysName{string}, id{Guid}') + const delimiter = '|' // currently used to separate system name and id in mbr_id const mbr_id = sysName .substring(0,64) .replace(/\s/g, '_').toLowerCase() - + '_' + + delimiter + sysId return mbr_id } From 169fee6127a451cc7f3c35bc5ac943646d03bb42 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 24 May 2024 21:04:21 -0400 Subject: [PATCH 20/32] 20240523 @Mookse - scrollable bot options --- views/assets/css/bot-bar.css | 23 ++++++++++++----------- views/assets/css/main.css | 28 +++++++++++++++------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/views/assets/css/bot-bar.css b/views/assets/css/bot-bar.css index 96bbd0e..4a5b128 100644 --- a/views/assets/css/bot-bar.css +++ b/views/assets/css/bot-bar.css @@ -48,19 +48,20 @@ align-items: stretch; } .bot-container { - box-sizing: border-box; cursor: pointer; - display: none; - flex-direction: column; - justify-content: space-between; - margin: 0.2rem 0; + display: flex; + flex: 1 1 auto; /* Allow it to grow and shrink */ + flex-direction: column; + justify-content: space-between; + margin: 0.2rem 0; max-width: 100%; + overflow: hidden; width: 100%; } .bot-content { /* Hidden by default */ - display: none; - overflow: hidden; + display: flex; + overflow: auto; } .bot-content.visible { /* Show content when active */ @@ -131,12 +132,14 @@ display: none; /* Initially not displayed */ margin: 0em 1em 1em 1em; padding: 0.5em; + max-height: 50vh; + overflow-y: auto; /* Enable vertical scrolling */ } .bot-options.open { + animation: _slideBotOptions 0.5s forwards; display: flex; /* Make it flex when open */ flex-direction: row; flex-wrap: wrap; - animation: _slideBotOptions 0.5s forwards; } .bot-options-dropdown { align-items: center; @@ -215,11 +218,9 @@ padding: 0; } .mylife-widget.bots { - align-items: flex-start; - display: flex; flex-direction: column; justify-content: flex-start; - max-width: 100%; + max-width: 100%; } /* bot-collections */ .collection { diff --git a/views/assets/css/main.css b/views/assets/css/main.css index 7fd99da..f8e0eb0 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -584,25 +584,27 @@ body { background: white; /* Assuming a card-like look typically has a white background */ background-position: center; /* Centers the image in the area */ background-size: cover; /* Ensures the image covers the whole area */ - border: rgb(0, 25, 51, .3) 2px dotted; + border: 2px dotted rgba(0, 25, 51, 0.3); border-radius: 22px; - display: none; - flex: 0 auto; - flex-direction: column; - font-family: "Optima", "Segoe UI", "Candara", "Calibri", "Segoe", "Optima", Arial, sans-serif; - font-size: 0.8em; - height: fit-content; /* 100% will stretch to bottom */ - max-width: 35%; + display: flex; /* Changed to flex for layout */ + flex-direction: column; + flex: 1 1 auto; /* Allows growth and shrinkage */ + font-family: "Optima", "Segoe UI", "Candara", "Calibri", "Segoe", "Optima", Arial, sans-serif; + font-size: 0.8em; + max-height: 100vh; /* Prevents exceeding the viewport height */ + max-width: 35%; padding: 0px; + height: auto; /* Ensure it adjusts based on content */ } .sidebar-widget { - align-items: flex-start; - display: flex; - flex-direction: column; - margin: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + flex: 1 1 auto; /* Allows growth and shrinkage */ max-width: 100%; - padding: 0; width: 100%; + margin: 0; + padding: 0; } .teaser { display: flex; From cc7b8c26b429ef2fa5064b91a6cd766e9a2cef29 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 24 May 2024 21:29:24 -0400 Subject: [PATCH 21/32] 20240523 @Mookse - cosmetic and nomenclature changes --- inc/js/menu.mjs | 3 ++- views/assets/css/main.css | 2 +- views/assets/html/_navbar.html | 2 +- views/assets/html/_widget-bots.html | 10 +++++----- views/assets/js/globals.mjs | 1 - 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/inc/js/menu.mjs b/inc/js/menu.mjs index 0755892..0345208 100644 --- a/inc/js/menu.mjs +++ b/inc/js/menu.mjs @@ -8,8 +8,9 @@ class Menu { } #setMenu(_Agent){ return [ - { display: `meet ${ _Agent.agentName }`, route: '/', icon: 'home' }, +// { display: `home`, route: '/', icon: 'home' }, { display: `about`, route: '/about', icon: 'about' }, + { display: `walkthrough`, route: 'https://medium.com/@ewbj/mylife-we-save-your-life-480a80956a24', icon: 'gear' }, { display: `donate`, route: 'https://gofund.me/65013d6e', icon: 'donate' }, // { display: `membership`, route: '/members', icon: 'membership' }, // { display: `register`, route: '/register', icon: 'register' }, diff --git a/views/assets/css/main.css b/views/assets/css/main.css index f8e0eb0..c98e54a 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -586,7 +586,7 @@ body { background-size: cover; /* Ensures the image covers the whole area */ border: 2px dotted rgba(0, 25, 51, 0.3); border-radius: 22px; - display: flex; /* Changed to flex for layout */ + display: none; /* Changed to flex for layout */ flex-direction: column; flex: 1 1 auto; /* Allows growth and shrinkage */ font-family: "Optima", "Segoe UI", "Candara", "Calibri", "Segoe", "Optima", Arial, sans-serif; diff --git a/views/assets/html/_navbar.html b/views/assets/html/_navbar.html index b7b71b4..e71db9b 100644 --- a/views/assets/html/_navbar.html +++ b/views/assets/html/_navbar.html @@ -17,7 +17,7 @@ - +
Interface
diff --git a/views/assets/html/_widget-bots.html b/views/assets/html/_widget-bots.html index 87a0a7c..5cb8684 100644 --- a/views/assets/html/_widget-bots.html +++ b/views/assets/html/_widget-bots.html @@ -3,7 +3,7 @@
-
Intelligent Assistant
+
Virtual "Me"
<%= avatar.name %>
@@ -273,7 +273,7 @@
-
Story Collection
+
Memories
@@ -284,7 +284,7 @@
-
Journal Entry Collection
+
Journal Entries
@@ -295,7 +295,7 @@
-
Experience Collection
+
Experiences
@@ -307,7 +307,7 @@
-
File Collection
+
Files
diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index ac9faaa..c367c01 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -331,7 +331,6 @@ function mClearElement(element){ * @returns {HTMLDivElement} - The dialog element. */ function mCreateHelpInitiatorDialog(popupChat, type){ - console.log('mCreateHelpInitiatorDialog', mActiveHelpType.id.split('-').pop()) const dialog = document.createElement('div') dialog.classList.add('popup-dialog', 'help-initiator-dialog', `help-initiator-dialog-${ type }`) dialog.id = `help-initiator` From 8f17d7f1dbfd5c5a9f79b750e21a1ad373f725ac Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 24 May 2024 21:34:04 -0400 Subject: [PATCH 22/32] 20240524 @Mookse - cosmetic microphone --- views/members.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/views/members.html b/views/members.html index b16172d..967c6ad 100644 --- a/views/members.html +++ b/views/members.html @@ -9,13 +9,15 @@
-
chat
+
+ +
\ No newline at end of file From 971370c8f9fa9cd2989f8ca2afae574e9c224d43 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 24 May 2024 21:37:08 -0400 Subject: [PATCH 23/32] 20240524 @Mookse - activebot name in connecting --- views/assets/js/members.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index e1d5588..abef7d3 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -561,6 +561,7 @@ function toggleMemberInput(display=true, hidden=false){ chatInput.classList.remove('fade-in') chatInput.classList.remove('slide-up') awaitButton.classList.add('slide-up') + awaitButton.innerHTML = `Connecting with ${ activeBot().bot_name }...` show(awaitButton) } if(hidden) From 3cc934f0f17760b1830a4b656f04ffb5dc661e79 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 25 May 2024 09:13:05 -0400 Subject: [PATCH 24/32] 20240525 @Mookse - add globals.clearArray() --- inc/js/globals.mjs | 13 +++++++++++++ views/assets/js/globals.mjs | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index aa89a99..c98a6de 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -130,6 +130,19 @@ class Globals extends EventEmitter { super() } /* public functions */ + /** + * Clears a const array with nod to garbage collection. + * @param {Array} a - the array to clear. + * @returns {void} + */ + clearArray(a){ + if(!Array.isArray(a)) + throw new TypeError('Expected an array to clear') + for(let i = 0; i < a.length; i++){ + a[i] = null + } + a.length = 0 + } createDocumentName(mbr_id, id, type){ if(!mbr_id || !id || !type) throw new Error('createDocumentName() expects `mbr_id`, `id`, and `type`') diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index c367c01..1b6e139 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -90,6 +90,19 @@ class Globals { mLoginSelect.addEventListener('change', mSelectLoginId, { once: true }) } /* public functions */ + /** + * Clears a const array with nod to garbage collection. + * @param {Array} a - the array to clear. + * @returns {void} + */ + clearArray(a){ + if(!Array.isArray(a)) + throw new TypeError('Expected an array to clear') + for(let i = 0; i < a.length; i++){ + a[i] = null + } + a.length = 0 + } /** * Clears an element of its contents, brute force currently via innerHTML. * @param {HTMLElement} element - The element to clear. From 02d4b025dbf92d9095d6773c68a17ef371c5a6fe Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 25 May 2024 15:51:30 -0400 Subject: [PATCH 25/32] 20240525 @Mookse - refactor botContainer [part-1] --- views/assets/css/bot-bar.css | 12 +- views/assets/js/bots.mjs | 315 ++++++++++++++++++++++++----------- views/assets/js/members.mjs | 14 +- 3 files changed, 239 insertions(+), 102 deletions(-) diff --git a/views/assets/css/bot-bar.css b/views/assets/css/bot-bar.css index 4a5b128..ef82627 100644 --- a/views/assets/css/bot-bar.css +++ b/views/assets/css/bot-bar.css @@ -18,12 +18,11 @@ transition: max-height 0.5s ease-in-out; /* Animation for the windowshade effect */ /* descendent styles */ .bot-thumb { - width: 40px; - height: 40px; - transition: transform 0.3s ease, margin 0.3s ease; cursor: pointer; + height: 2.5rem; + margin: 0 0.1rem; /* Default margin */ + transition: transform 0.3s ease, margin 0.3s ease; z-index: 1; - margin: 0; /* Default margin */ } .bot-thumb:hover { transform: scale(1.5); @@ -38,6 +37,11 @@ margin-right: 1em; /* Space between containers in em */ position: relative; } + .bot-thumb-active { + background-image: radial-gradient(circle at center, rgba(255, 255, 0, 0.75) 0%, transparent 100%); + cursor: not-allowed; + z-index: 3; + } } #bot-bar:hover { max-height: 3em; /* Adjust to desired expanded height */ diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index d19fd65..6a8a4bb 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -3,6 +3,7 @@ import { addMessage, availableExperiences, + decorateActiveBot, hide, inExperience, show, @@ -63,11 +64,10 @@ let mActiveBot, document.addEventListener('DOMContentLoaded', async event=>{ const { bots, activeBotId: id } = await fetchBots() if(!bots?.length) - throw new Error(`No bots found.`) - mPageBots = bots - mActiveBot = await mBot(id) - mUpdateTeam() - updatePageBots() + throw new Error(`ERROR: No bots returned from server`) + updatePageBots(bots) // includes p-a + await setActiveBot(id, true) + }) /* public functions */ /** @@ -107,22 +107,27 @@ async function fetchCollections(type){ /** * Set active bot on server and update page bots. * @requires mActiveBot + * @requires mPageBots * @param {Event} event - The event object. + * @param {boolean} dynamic - Whether or not to add dynamic greeting, only triggered from source code. * @returns {void} */ -async function setActiveBot(event){ - const botId = event.target?.dataset?.botId - ?? event.target.id.split('-').slice(0, -1).join('-') - const bot = await mBot(botId) - mActiveBot = mActiveBot - ?? bot - if(!bot) - throw new Error(`Bot not found.`) - if(bot===mActiveBot) - return - const { id, } = bot - /* server request: set active bot */ - const _id = await fetch( +async function setActiveBot(event, dynamic=false){ + const botId = mGlobals.isGuid(event) + ? event /* bypassed event, sent id */ + : event.target?.dataset?.bot_id + if(!botId) + throw new Error(`Bot data not found in event.`) + const initialActiveBot = mActiveBot + mActiveBot = mBot(botId) + ?? initialActiveBot + if(!mActiveBot) + throw new Error(`ERROR: failure to set active bot.`) + if(initialActiveBot===mActiveBot) + return // no change, no problem + const { id, } = mActiveBot + /* confirm via server request: set active bot */ + const serverActiveId = await fetch( '/members/bots/activate/' + id, { method: 'POST', @@ -144,21 +149,75 @@ async function setActiveBot(event){ return }) /* update active bot */ - mActiveBot = bot - updatePageBots(true) + if(serverActiveId!==id){ + mActiveBot = initialActiveBot + throw new Error(`ERROR: server failed to set active bot.`) + } + /* update page bot data */ + const { activated=[], activatedFirst=Date.now(), } = mActiveBot + mActiveBot.activatedFirst = activatedFirst + activated.push(Date.now()) // newest date is last to .pop() + // dynamic = (Date.now()-activated.pop()) > (20*60*1000) + mActiveBot.activated = activated + /* update page */ + mSpotlightBotBar() + mSpotlightBotStatus() + mGreeting(dynamic) + decorateActiveBot(mActiveBot) +} +/** + * Highlights bot bar icon of active bot. + * @public + * @requires mActiveBot + * @returns {void} + */ +function mSpotlightBotBar(){ + document.querySelectorAll('.bot-thumb') + .forEach(icon=>{ + if(icon.alt===mActiveBot?.type) + icon.classList.add('bot-thumb-active') + else + icon.classList.remove('bot-thumb-active') + }) +} +/** + * Highlights bot container of active bot. + * @public + * @requires mActiveBot + * @returns {void} + */ +function mSpotlightBotStatus(){ + mPageBots + .forEach(bot=>{ + const { id, type, } = bot + const botContainer = document.getElementById(type) + if(botContainer){ // exists on-page + // set data attribute for active bot + const { dataset, } = botContainer + if(dataset && id) + botContainer.dataset.active = id===mActiveBot?.id + mSetBotIconStatus(bot) + } + }) } /** * Proxy to update bot-bar, bot-containers, and bot-greeting, if desired. Requirements should come from including module, here `members.mjs`. * @public - * @requires mGreeting() - * @param {boolean} bIncludeGreeting - Include bot-greeting. + * @requires mPageBots() - though will default to empty array. + * @param {Array} bots - The bot objects to update page with. + * @param {boolean} includeGreeting - Include bot-greeting. * @returns {void} */ -async function updatePageBots(bIncludeGreeting=false){ +async function updatePageBots(bots=(mPageBots ?? []), includeGreeting=false, dynamic=false){ + if(!bots?.length) + throw new Error(`No bots provided to update page.`) + mPageBots = bots + console.log('updatePageBots::', bots, includeGreeting, dynamic) + mUpdateTeam() mUpdateBotContainers() mUpdateBotBar() - if(bIncludeGreeting) - mGreeting() + if(includeGreeting) + mGreeting(dynamic) } /* private functions */ /** @@ -167,18 +226,18 @@ async function updatePageBots(bIncludeGreeting=false){ * @param {string} type - The bot type or id. * @returns {object} - The bot object. */ -async function mBot(type){ +function mBot(type){ return mPageBots.find(bot=>bot.type===type) ?? mPageBots.find(bot=>bot.id===type) - ?? await mBotCreate(type) } /** * Check if bot is active (by id). - * @param {Guid} id + * @param {Guid} id - The bot id to check. * @returns */ -function mBotActive(id) { - return (id && mActiveBot && id===mActiveBot.id) +function mBotActive(id){ + return id===mActiveBot?.id + ?? false } /** * Request bot be created on server. @@ -469,9 +528,10 @@ function mFindCheckbox(element, searchParent=true){ * Paints bot-greeting to column * @private * @requires mActiveBot + * @param {boolean} dynamic - Whether or not to add event listeners for dynamic greeting. * @returns {void} */ -function mGreeting(){ +function mGreeting(dynamic=false){ const greeting = Array.isArray(mActiveBot.greeting) ? mActiveBot.greeting : [ @@ -599,30 +659,47 @@ async function setBot(bot){ } /** * Sets bot attributes on bot container. + * @private + * @requires mActiveBot * @requires mGlobals * @param {object} bot - The bot object. * @param {HTMLDivElement} botContainer - The bot container. * @returns {void} */ -function mSetAttributes(bot, botContainer){ - const { bot_id: botId, bot_name: botName, id, mbr_id, provider, purpose, thread_id: threadId, type, } = bot - const memberHandle = mGlobals.getHandle(mActiveBot.mbr_id) +function mSetAttributes(bot=mActiveBot, botContainer){ + const { activated=[], activeFirst, bot_id: botId, bot_name: botName, dob, id, interests, mbr_id, narrative, privacy, provider, purpose, thread_id: threadId, type, updates, } = bot + const memberHandle = mGlobals.getHandle(mbr_id) + const bot_name = botName + ?? `${ memberHandle + '_' + type }` + const thread_id = threadId + ?? '' /* attributes */ const attributes = [ + { name: 'activated', value: activated }, { name: 'active', value: mBotActive(id) }, + { name: 'activeFirst', value: activeFirst }, { name: 'bot_id', value: botId }, - { name: 'bot_name', value: botName ?? `${ memberHandle }-${ type }` }, + { name: 'bot_name', value: bot_name }, { name: 'id', value: id }, - { name: 'mbr_id', value: mbr_id }, + { name: 'initialized', value: Date.now() }, { name: 'mbr_handle', value: memberHandle }, - { name: 'provider', value: provider ?? 'openai' }, - { name: 'purpose', value: purpose ?? `To assist ${ memberHandle } with tasks as their ${ type }` }, - { name: 'thread_id', value: threadId ?? '' }, + { name: 'mbr_id', value: mbr_id }, + { name: 'thread_id', value: thread_id }, { name: 'type', value: type }, ] + if(dob) + attributes.push({ name: 'dob', value: dob }) + if(interests) + attributes.push({ name: 'interests', value: interests }) + if(narrative) + attributes.push({ name: 'narrative', value: narrative }) + if(privacy) + attributes.push({ name: 'privacy', value: privacy }) + if(updates) + attributes.push({ name: 'updates', value: updates }) attributes.forEach(attribute=>{ const { name, value, } = attribute - botContainer.setAttribute(`data-${ name }`, value) + botContainer.dataset[name] = value const element = document.getElementById(`${ type }-${ name }`) if(element){ const botInput = element.querySelector('input') @@ -634,7 +711,7 @@ function mSetAttributes(bot, botContainer){ /** * Sets bot status based on active bot, thread, and assistant population. * @private - * @requires mActiveBot + * @requires mActiveBot - active bot object, but can be undefined without error. * @param {object} bot - The bot object. * @returns {string} - Determined status. */ @@ -642,7 +719,7 @@ function mSetBotIconStatus(bot){ const { bot_id, id, thread_id, type, } = bot const botIcon = document.getElementById(`${ type }-icon`) switch(true){ - case ( mActiveBot && mActiveBot.id===id): // activated + case ( mActiveBot?.id==id ): // activated botIcon.classList.remove('online', 'offline', 'error') botIcon.classList.add('active') return 'active' @@ -695,11 +772,14 @@ async function mSubmitPassphrase(event){ } /** * Toggles bot containers and checks for various actions on master click of `this` bot-container. Sub-elements appear as targets and are rendered appropriately. + * @private + * @async * @param {Event} event - The event object, represents entire bot box as `this`. * @returns {void} */ -function mToggleBotContainers(event){ +async function mToggleBotContainers(event){ event.stopPropagation() + const botContainer = this const element = event.target const itemIdSnippet = element.id.split('-').pop() switch(itemIdSnippet){ @@ -711,16 +791,17 @@ function mToggleBotContainers(event){ ? this.querySelector('span') : event.target _span.classList.toggle('no-animation') - return + break case 'icon': case 'title': - setActiveBot(event) - // for moment, yes, intentional cascade + const { dataset, id, } = botContainer + await setActiveBot(dataset?.id ?? id, true) + // for moment, yes, intentional cascade to open options case 'status': case 'type': case 'dropdown': mOpenStatusDropdown(this) - return + break case 'update': const updateBot = { bot_name: this.getAttribute('data-bot_name'), @@ -775,7 +856,7 @@ async function mToggleTeamPopup(event){ /* validate */ let bot try{ - bot = await mBot(value) + bot = mBot(value) console.log('mToggleTeamPopup::CHANGE to:', value, bot) } catch(error) { console.log('mToggleTeamPopup::ERROR', error) @@ -939,54 +1020,90 @@ function mToggleSwitchPrivacy(event){ */ function mUpdateBotBar(){ botBar.innerHTML = '' // clear existing - mPageBots.forEach(bot => { - // Create a container div for each bot - const botContainer = document.createElement('div') - botContainer.classList.add('bot-thumb-container') - // Create an icon element for each bot container - const botIconImage = document.createElement('img') - botIconImage.classList.add('bot-thumb') - botIconImage.src = mBotIcon(bot.type) - botIconImage.alt = bot.type - if(bot===mActiveBot){ - botIconImage.classList.add('active-bot') // Apply a special class for the active bot - } - botIconImage.id = `bot-bar-icon_${bot.id}` - botIconImage.dataset.botId = bot.id - botIconImage.addEventListener('click', setActiveBot) - botBar.appendChild(botIconImage) - }) + mPageBots + .forEach(bot => { + // Create a container div for each bot + const botThumbContainer = document.createElement('div') + botThumbContainer.addEventListener('click', setActiveBot) + botThumbContainer.classList.add('bot-thumb-container') + // Create an icon element for each bot container + const botIconImage = document.createElement('img') + botIconImage.classList.add('bot-thumb') + botIconImage.src = mBotIcon(bot.type) + botIconImage.alt = bot.type + botIconImage.id = `bot-bar-icon_${bot.id}` + botIconImage.dataset.botId = bot.id + botBar.appendChild(botIconImage) + }) } /** * Updates bot-widget containers for whom there is data. If no bot data exists, ignores container. * @todo - creation mechanism for new bots or to `reinitialize` or `reset` current bots, like avatar. * @todo - architect better mechanic for populating and managing bot-specific options * @async - * @requires mActiveBot * @requires mPageBots + * @param {boolean} includePersonalAvatar - Include personal avatar, use false when switching teams. + * @returns {void} + */ +async function mUpdateBotContainers(includePersonalAvatar=true){ + if(!mPageBots?.length) + throw new Error(`mPageBots not populated.`) + // get array with containers on-page; should be all bots x team + 'p-a' + const botContainers = Array.from(document.querySelectorAll('.bot-container')) + if(!botContainers.length) + throw new Error(`No bot containers found on page`) + botContainers + .forEach(async botContainer=>mUpdateBotContainer(botContainer, includePersonalAvatar)) +} +/** + * Updates the bot container with specifics. + * @param {HTMLDivElement} botContainer - The bot container. + * @param {boolean} includePersonalAvatar - Include personal avatar. + * @param {boolean} showContainer - Show the container, if data exists. + * @returns {void} + */ +function mUpdateBotContainer(botContainer, includePersonalAvatar = true, showContainer = true) { + const { id: type } = botContainer + if(type==='personal-avatar' && !includePersonalAvatar) + return /* skip personal avatar when requested */ + const bot = mBot(type) // @stub - careful of multiples once allowed! + if(!bot){ + hide(botContainer) + return /* no problem if not found, likely available different team */ + } + /* container listeners */ + botContainer.addEventListener('click', mToggleBotContainers) + /* universal logic */ + mSetAttributes(bot, botContainer) // first, assigns data attributes + const { bot_id, interests, narrative, privacy, } = botContainer.dataset + mSetBotIconStatus(bot) + mUpdateTicker(type, botContainer) + mUpdateInterests(type, interests, botContainer) + mUpdateNarrativeSlider(type, narrative, botContainer) + mUpdatePrivacySlider(type, privacy, botContainer) + /* type-specific logic */ + mUpdateBotContainerAddenda(botContainer, bot) + show(botContainer) +} +/** + * Updates the bot container with specifics based on `type`. + * @param {HTMLDivElement} botContainer - The bot container. + * @param {object} bot - The bot object. * @returns {void} */ -async function mUpdateBotContainers(){ - /* iterate over bot containers */ - document.querySelectorAll('.bot-container').forEach(async botContainer=>{ - const { dataset, id: type, } = botContainer - const bot = await mBot(type) - if(!bot) - return /* no problem if not found, available on different team */ - /* constants */ - const botOptions = document.getElementById(`${ type }-options`) - const botOptionsDropdown = document.getElementById(`${ type }-options-dropdown`) - /* container listeners */ - botContainer.addEventListener('click', mToggleBotContainers) - /* universal logic */ - mSetBotIconStatus(bot) - mSetAttributes(bot, botContainer) - mUpdateTicker(type, botContainer) - mUpdateInterests(type, bot.interests, botContainer) - mUpdateNarrativeSlider(type, bot.narrative, botContainer) - mUpdatePrivacySlider(type, bot.privacy, botContainer) +function mUpdateBotContainerAddenda(botContainer, bot){ + if(!botContainer) + return /* type-specific logic */ + const { dataset, id: type } = botContainer + const localVars = {} + if(dataset) // assign dataset to localVars + Object.keys(dataset).forEach(key=>localVars[key] = dataset[key]) switch(type){ + case 'diary': + case 'journaler': + case 'personal-biographer': + break case 'library': /* attach library collection listeners */ if(!mLibraryCollections || !mLibraryCollections.children.length) @@ -1012,23 +1129,26 @@ async function mUpdateBotContainers(){ break case 'personal-avatar': /* attach avatar listeners */ - mTogglePassphrase(false) - /* date of birth (dob) */ - botContainer.setAttribute('data-dob', bot.dob?.split('T')[0] ?? '') + /* set additional data attributes */ + const { dob, id, } = localVars /* date of birth (dob) */ + if(dob?.length) + dataset.dob = dob.split('T')[0] const memberDobInput = document.getElementById(`${ type }-input-dob`) - memberDobInput.value = botContainer.getAttribute('data-dob') - memberDobInput.addEventListener('input', event=>{ - botContainer.setAttribute('data-dob', memberDobInput.value) - memberDobInput.value = botContainer.getAttribute('data-dob') - }) - break - case 'personal-biographer': + if(memberDobInput){ + memberDobInput.value = dataset.dob + memberDobInput.addEventListener('change', event=>{ + if(memberDobInput.value.match(/^\d{4}-\d{2}-\d{2}$/)){ + dataset.dob = memberDobInput.value + // @stub - update server + } else + throw new Error(`Invalid date format.`) + }) + } + mTogglePassphrase(false) /* passphrase */ break default: break } - show(botContainer) - }) } /** * Update the identified collection with provided specifics. @@ -1151,6 +1271,7 @@ function mUpdatePrivacySlider(type, privacy, botContainer){ } /** * Updates the active team to specific or default. + * @requires mAvailableTeams * @param {string} teamName - The team name. * @returns {void} */ diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index abef7d3..835b82e 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -104,6 +104,17 @@ function availableExperiences(){ function clearSystemChat(){ mGlobals.clearElement(systemChat) } +/** + * Called from setActiveBot, triggers any main interface changes as a result of new selection. + * @public + * @param {object} activeBot - The active bot. + * @returns {void} + */ +function decorateActiveBot(activeBot=activeBot()){ + const { bot_name, id, purpose, type, } = activeBot + chatInputField.placeholder = `type your message to ${ bot_name }...` + // additional func? clear chat? +} function escapeHtml(text) { return mGlobals.escapeHtml(text) } @@ -261,7 +272,7 @@ function stageTransition(endExperience=false){ else { mStageTransitionMember() if(endExperience) - updatePageBots(true) + updatePageBots(undefined, true, false) } } /** @@ -609,6 +620,7 @@ export { assignElements, availableExperiences, clearSystemChat, + decorateActiveBot, escapeHtml, getInputValue, getSystemChat, From cc70fbc5107ed14e3fea118e3c66ae0a4fc880bd Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 25 May 2024 21:14:25 -0400 Subject: [PATCH 26/32] 20240525 @Mookse - scrollbar improvements --- views/assets/css/bot-bar.css | 25 ++++++++++++++++++++++++- views/assets/css/chat.css | 21 ++++++++++++++------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/views/assets/css/bot-bar.css b/views/assets/css/bot-bar.css index ef82627..7cb11c4 100644 --- a/views/assets/css/bot-bar.css +++ b/views/assets/css/bot-bar.css @@ -134,11 +134,34 @@ border-top: none; box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15); display: none; /* Initially not displayed */ - margin: 0em 1em 1em 1em; + margin: 0em 0.5em 1em 1em; padding: 0.5em; max-height: 50vh; + overflow-x: hidden; overflow-y: auto; /* Enable vertical scrolling */ } +/* Styles the scrollbar itself */ +.bot-options::-webkit-scrollbar, +.checkbox-group::-webkit-scrollbar { + width: 6px; /* Adjust the width of the scrollbar */ +} +/* Styles the track of the scrollbar */ +.bot-options::-webkit-scrollbar-track, +.checkbox-group::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); /* Color of the track */ + border-radius: 10px; /* Optional: adds rounded corners to the track */ +} +/* Styles the handle (thumb) of the scrollbar */ +.bot-options::-webkit-scrollbar-thumb, +.checkbox-group::-webkit-scrollbar-thumb { + background: rgba(232, 226, 183, .5); /* Color of the thumb */ + border-radius: 10px; /* Optional: adds rounded corners to the thumb */ +} +/* Changes the color of the thumb when hovered over or clicked */ +.bot-options::-webkit-scrollbar-thumb:hover, +.checkbox-group::-webkit-scrollbar-thumb:hover { + background: rgba(214, 198, 75, 0.5); /* Darker shade on hover */ +} .bot-options.open { animation: _slideBotOptions 0.5s forwards; display: flex; /* Make it flex when open */ diff --git a/views/assets/css/chat.css b/views/assets/css/chat.css index 6b9ba81..5363221 100644 --- a/views/assets/css/chat.css +++ b/views/assets/css/chat.css @@ -70,7 +70,7 @@ max-height: 60%; width: 100%; } -.chat-input{ +.chat-input { background-color: #ffffff; /* White background color */ border: 1px solid #ccc; /* Border color */ border-radius: 12px; /* Rounded corners */ @@ -81,7 +81,9 @@ line-height: 1.25; /* Adjust line height for better readability */ margin: 0 1em; /* Margin for space outside the container */ min-height: 2rem; - overflow: hidden; /* Hides the scrollbar */ + max-height: 50vh; + overflow-x: hidden; + overflow-y: auto; /* Allows vertical scrolling */ padding: 0.3em; /* Padding for space inside the container */ resize: none; /* Allows vertical resizing, none to disable */ } @@ -123,28 +125,33 @@ button.chat-submit:disabled { height: 100%; justify-content: flex-start; margin-top: 0.8em; + min-height: 30vh; overflow-x: hidden; overflow-y: scroll; width: 100%; } /* Styles the scrollbar itself */ -.chat-system::-webkit-scrollbar { +.chat-system::-webkit-scrollbar, +.chat-input::-webkit-scrollbar { width: 6px; /* Adjust the width of the scrollbar */ } /* Styles the track of the scrollbar */ -.chat-system::-webkit-scrollbar-track { +.chat-system::-webkit-scrollbar-track, +.chat-input::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); /* Color of the track */ border-radius: 10px; /* Optional: adds rounded corners to the track */ } /* Styles the handle (thumb) of the scrollbar */ -.chat-system::-webkit-scrollbar-thumb { +.chat-system::-webkit-scrollbar-thumb, +.chat-input::-webkit-scrollbar-thumb { background: rgba(232, 226, 183, .5); /* Color of the thumb */ border-radius: 10px; /* Optional: adds rounded corners to the thumb */ } /* Changes the color of the thumb when hovered over or clicked */ -.chat-system::-webkit-scrollbar-thumb:hover { +.chat-system::-webkit-scrollbar-thumb:hover, +.chat-input::-webkit-scrollbar-thumb:active { background: rgba(214, 198, 75, 0.5); /* Darker shade on hover */ -} +} .help-bubble { animation: slideInFromBottom 0.5s ease-out; } From 9cb81964fe15855296c9c8a6e95e85e877a3d147 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sun, 26 May 2024 16:26:58 -0400 Subject: [PATCH 27/32] 20240525 @Mookse - hostedMembers update - move `aboutContainer` to Globals - move mLoginSelect to guests - move `mChallengeError`, `mChallengeInput`, `mChallengeSubmit` to guests - removed extraneous HTML files, now embeded in Q --- inc/js/api-functions.mjs | 15 --- inc/js/core.mjs | 34 +++-- inc/js/functions.mjs | 41 +++--- inc/js/globals.mjs | 16 +-- inc/js/mylife-agent-factory.mjs | 23 +++- inc/js/mylife-data-service.js | 8 ++ inc/js/mylife-datamanager.mjs | 92 +++++++------ inc/js/routes.mjs | 17 +-- inc/js/session.mjs | 17 ++- sample.env | 6 +- server.js | 10 +- views/assets/css/members.css | 85 +++++++----- views/assets/js/globals.mjs | 102 ++------------ views/assets/js/guests.mjs | 232 +++++++++++++++++++++++++++++--- views/members-challenge.html | 8 -- views/members-select.html | 8 -- 16 files changed, 429 insertions(+), 285 deletions(-) delete mode 100644 views/members-challenge.html delete mode 100644 views/members-select.html diff --git a/inc/js/api-functions.mjs b/inc/js/api-functions.mjs index c2bc950..18de27b 100644 --- a/inc/js/api-functions.mjs +++ b/inc/js/api-functions.mjs @@ -158,20 +158,6 @@ async function library(ctx){ success: true, } } -/** - * Login function for member. Requires mid in params. - * @module - * @public - * @param {Koa} ctx - Koa Context object - * @returns {Koa} Koa Context object - * @property {string} ctx.body.challengeId - */ -async function login(ctx){ - if(!ctx.params.mid?.length) - ctx.throw(400, `missing member id`) // currently only accepts single contributions via post with :cid - ctx.session.MemberSession.challenge_id = decodeURIComponent(ctx.params.mid) - ctx.body = { challengeId: ctx.session.MemberSession.challenge_id } -} /** * Logout function for member. * @param {Koa} ctx - Koa Context object @@ -362,7 +348,6 @@ export { experiencesLived, keyValidation, library, - login, logout, register, story, diff --git a/inc/js/core.mjs b/inc/js/core.mjs index 7e8306d..c388039 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -293,21 +293,35 @@ class MyLife extends Organization { // form=server async getMyLifeSession(){ return await this.factory.getMyLifeSession() } + /** + * Returns Array of hosted members based on validation requirements. + * @param {Array} validations - Array of validation strings to filter membership. + * @returns {Promise} - Array of string ids, one for each hosted member. + */ + async hostedMembers(validations){ + return await this.factory.hostedMembers(validations) + } /** * Submits a request for a library item from MyLife via API. * @public - * @param {string} _mbr_id - Requesting Member id. - * @param {string} _assistantType - String name of assistant type. - * @param {string} _library - Library entry with or without `items`. + * @param {string} mbr_id - Requesting Member id. + * @param {string} assistantType - String name of assistant type. + * @param {string} library - Library entry with or without `items`. * @returns {object} - The library document from Cosmos. */ - async library(_mbr_id, _assistantType, _library){ - _library.assistantType = _assistantType - _library.id = _library.id && this.globals.isValidGuid(_library.id) ? _library.id : this.globals.newGuid - _library.mbr_id = _mbr_id - _library.type = _library.type??_assistantType??'personal' - const _libraryCosmos = await this.factory.library(_library) - return this.globals.stripCosmosFields(_libraryCosmos) + async library(mbr_id, assistantType='personal-avatar', library){ + const { id, type, } = library + library.assistantType = assistantType + library.id = this.globals.isValidGuid(id) + ? id + : this.globals.newGuid + library.mbr_id = mbr_id + library.type = type + ?? assistantType + const _library = this.globals.stripCosmosFields( + await this.factory.library(library) + ) + return _library } /** * Registers a new candidate to MyLife membership diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index ef52294..602634d 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -72,9 +72,13 @@ function category(ctx){ // sets category for avatar * @property {object} ctx.body - The result of the challenge. */ async function challenge(ctx){ - if(!ctx.params.mid?.length) - ctx.throw(400, `requires member id`) - ctx.body = await ctx.session.MemberSession.challengeAccess(ctx.request.body.passphrase) + const { passphrase, } = ctx.request.body + if(!passphrase?.length) + ctx.throw(400, `challenge request requires passphrase`) + const { mid, } = ctx.params + if(!mid?.length) + ctx.throw(400, `challenge request requires member id`) + ctx.body = await ctx.session.MemberSession.challengeAccess(mid, passphrase) } async function chat(ctx){ const { botId, message, role, threadId, } = ctx.request.body @@ -190,35 +194,21 @@ function interfaceMode(ctx){ ctx.body = avatar.mode return } -async function login(ctx){ - if(!ctx.params.mid?.length) ctx.throw(400, `missing member id`) // currently only accepts single contributions via post with :cid - ctx.state.mid = decodeURIComponent(ctx.params.mid) - ctx.session.MemberSession.challenge_id = ctx.state.mid - ctx.state.title = '' - ctx.state.subtitle = `Enter passphrase for activation [member ${ ctx.Globals.sysName(ctx.params.mid) }]:` - await ctx.render('members-challenge') -} async function logout(ctx){ ctx.session = null ctx.redirect('/') } +/** + * Returns a member list for selection. + * @todo: should obscure and hash ids in session.mjs + * @todo: set and read long-cookies for seamless login + * @param {Koa} ctx - Koa Context object + * @returns {Object[]} - List of hosted members available for login. + */ async function loginSelect(ctx){ - ctx.state.title = '' - // listing comes from state.hostedMembers - // @todo: should obscure and hash ids in session.mjs - // @todo: set and read long-cookies for seamless login - ctx.state.hostedMembers = ctx.hostedMembers - .sort( - (a, b) => a.localeCompare(b) - ) - .map( - _mbr_id => ({ 'id': _mbr_id, 'name': ctx.Globals.sysName(_mbr_id) }) - ) - ctx.state.subtitle = `Select your personal Avatar to continue:` - await ctx.render('members-select') + ctx.body = ctx.hostedMembers } async function members(ctx){ // members home - ctx.state.subtitle = `Welcome Agent ${ctx.state.member.agentName}` await ctx.render('members') } async function passphraseReset(ctx){ @@ -355,7 +345,6 @@ export { greetings, index, interfaceMode, - login, logout, loginSelect, members, diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index c98a6de..78b9f8d 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -193,19 +193,19 @@ class Globals extends EventEmitter { function: this.GPTJavascriptFunctions[name] } } - getRegExp(str, isGlobal = false) { - if (typeof str !== 'string' || !str.length) + getRegExp(text, isGlobal=false) { + if (typeof text !== 'string' || !text.length) throw new Error('Expected a string') return new RegExp(str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), isGlobal ? 'g' : '') } - isValidEmail(_email){ - return mEmailRegex.test(_email) + isValidEmail(email){ + return typeof email === 'string' && mEmailRegex.test(email) } - isValidGuid(_str='') { - return (typeof _str === 'string' && mGuidRegex.test(_str)) + isValidGuid(text) { + return typeof text === 'string' && mGuidRegex.test(text) } - stripCosmosFields(_obj){ - return Object.fromEntries(Object.entries(_obj).filter(([k, v]) => !k.startsWith('_'))) + stripCosmosFields(object){ + return Object.fromEntries(Object.entries(object).filter(([k, v]) => !k.startsWith('_'))) } sysId(_mbr_id){ if(!typeof _mbr_id==='string' || !_mbr_id.length || !_mbr_id.includes('|')) diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 310bcdc..9019aea 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -81,14 +81,14 @@ class BotFactory extends EventEmitter{ #dataservices #llmServices = mLLMServices #mbr_id - constructor(_mbr_id, _directHydration=true){ + constructor(mbr_id, directHydration=true){ super() - this.#mbr_id = _mbr_id - if(mIsMyLife(_mbr_id) && _directHydration) + this.#mbr_id = mbr_id + if(mIsMyLife(mbr_id) && directHydration) throw new Error('MyLife server cannot be accessed as a BotFactory alone') else if(mIsMyLife(this.mbr_id)) this.#dataservices = mDataservices - else if(_directHydration) + else if(directHydration) console.log(chalk.blueBright('BotFactory class instance for hydration request'), chalk.bgRed(this.mbr_id)) } /* public functions */ @@ -846,12 +846,23 @@ class AgentFactory extends BotFactory { } // @stub - MyLife factory class class MyLifeFactory extends AgentFactory { + #dataservices = mDataservices + #llmServices = mLLMServices #mylifeRegistrationData + #tempRegistrationData constructor(){ - super(mbr_id) + super(mPartitionId) } // no init() for MyLife server /* public functions */ + /** + * Returns Array of hosted members based on validation requirements. + * @param {Array} validations - Array of validation strings to filter membership. + * @returns {Promise} - Array of string ids, one for each hosted member. + */ + async hostedMembers(validations){ + return await this.#dataservices.hostedMembers(validations) + } /* getters/setters */ /** * Gets registration data while user attempting to confirm. @@ -1591,7 +1602,7 @@ function mSanitizeSchemaValue(_value) { /* final constructs relying on class and functions */ // server build: injects default factory into _server_ **MyLife** instance const _MyLife = await new MyLife( - new AgentFactory(mPartitionId) + new MyLifeFactory() ) .init() /* exports */ diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index 4a8c90a..655edcb 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -544,6 +544,14 @@ class Dataservices { async getLocalRecords(_question){ return await this.embedder.getLocalRecords(_question) } + /** + * Returns Array of hosted members based on validation requirements. + * @param {Array} validations - Array of validation strings to filter membership. + * @returns {Promise} - Array of string ids, one for each hosted member. + */ + async hostedMembers(validations){ + return await this.datamanager.hostedMembers(validations) + } /** * Gets library from database. * @async diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index 5c41b84..0aeda8b 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -24,7 +24,7 @@ class Datamanager { const _client = new CosmosClient(_options) this.database = _client.database(_config.members.id) this.#partitionId = _config.members.container.partitionId - this.#coreId = _config.members.container?.coreId??this.#partitionId.split('|')[1] + this.#coreId = _config.members.container?.coreId ?? this.#partitionId.split('|')[1] this.#containers = { contribution_responses: this.database.container(_config.contributions.container.id), members: this.database.container(_config.members.container.id), @@ -36,7 +36,7 @@ class Datamanager { populateQuotaInfo: false, // set this to true to include quota information in the response headers } } - // init function + /* initialize */ async init() { // assign core this.#core = await this.#containers['members'] @@ -48,28 +48,15 @@ class Datamanager { console.log(chalk.yellowBright('database, container, core initialized:',chalk.bgYellowBright(`${this.#containers['members'].id} :: ${this.database.id} :: ${this.#core.resource.id}`) )) return this } - // getter/setter property functions - /** - * Returns container default for MyLife data. - */ - get containerDefault(){ - return 'members' - } - /** - * Returns datacore. - */ - get core(){ - return this.#core?.resource - } - // public functions - async challengeAccess(_mbr_id, _passphrase){ + /* public functions */ + async challengeAccess(mbr_id, passphrase){ // in order to obscure passphrase, have db make comparison (could include flag for case insensitivity) // Execute the stored procedure - const { resource: _result } = await this.#containers['members'] + const { resource: result } = await this.#containers['members'] .scripts .storedProcedure('checkMemberPassphrase') - .execute(_mbr_id, _passphrase, true) // first parameter is partition key, second is passphrase, third is case sensitivity - return _result + .execute(mbr_id, passphrase, true) // first parameter is partition key, second is passphrase, third is case sensitivity + return result } /** * Deletes a specific item from container. @@ -104,6 +91,35 @@ class Datamanager { .fetchAll() return resources } + /** + * Returns Array of hosted members based on validation requirements. + * @param {Array} validations - Array of validation strings to filter membership. + * @returns {Promise} - Array of string ids, one for each hosted member. + */ + async hostedMembers(validations=['registration']){ + let sql = 'select c.mbr_id, c.openaiapikey, c.validations' + + ' from root c' + + ` where c.being='core'` + + ` and c.form='human'` + if(validations.length){ + sql += ` and is_array(c.validations) and array_length(c.validations) > 0` + const validationChecks = validations + .map(validation=>`array_contains(c.validations, '${validation}')`) + .join(' and ') + sql += ` and (${validationChecks})` + } + const querySpec = { + query: sql, + parameters: [] + } + const { resources: documents } = await this.#containers['members'] + .items + .query(querySpec, { enableCrossPartitionQuery: true }) + .fetchAll() + if(!documents?.length) + throw new Error('No hosted members found') + return documents + } async patchItem(_id, _item, _container_id=this.containerDefault){ // patch or update, depends on whether it finds id or not, will only overwrite fields that are in _item // [Partial Document Update, includes node.js examples](https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update) if(!Array.isArray(_item)) _item = [_item] @@ -140,35 +156,33 @@ class Datamanager { const { resource: result } = await this.#containers['members'] .scripts .storedProcedure('testPartitionKey') - .execute(mbr_id) // first parameter is partition key, second is passphrase, third is case sensitivity + .execute(mbr_id) return result } /* getters/setters */ + /** + * Returns container default for MyLife data. + */ + get containerDefault(){ + return 'members' + } + /** + * Returns datacore. + */ + get core(){ + return this.#core?.resource + } get globals(){ return mGlobals } + get mbr_id(){ + return this.core.mbr_id + ?? this.#partitionId + } } // exports export default Datamanager /* - async addItem(item) { - debug('Adding an item to the database') - item.date = Date.now() - item.completed = false - const { resource: doc } = await this.container.items.create(item) - return doc - } - - async updateItem(itemId) { - debug('Update an item in the database') - const doc = await this.getItem(itemId) - doc.completed = true - - const { resource: replaced } = await this.container - .item(itemId, this.partitionKey) - .replace(doc) - return replaced - } COLLECTION PATCH: Body itself is the array of operations, second parameter is options, for configuration and filter? const filter = 'FROM products p WHERE p.used = false' diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 099fa1d..9f31e92 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -16,7 +16,6 @@ import { help, index, interfaceMode, - login, logout, loginSelect, members, @@ -36,7 +35,6 @@ import { experiencesLived, keyValidation, library, - login as apiLogin, logout as apiLogout, register, story, @@ -51,7 +49,6 @@ const _apiRouter = new Router() _Router.get('/', index) _Router.get('/about', about) _Router.get('/alerts', alerts) -_Router.get('/login/:mid', login) _Router.get('/logout', logout) _Router.get('/experiences', availableExperiences) _Router.get('/greeting', greetings) @@ -69,7 +66,6 @@ _apiRouter.get('/alerts', alerts) _apiRouter.get('/alerts/:aid', alerts) _apiRouter.get('/experiences/:mid', experiences) // **note**: currently triggers autoplay experience _apiRouter.get('/experiencesLived/:mid', experiencesLived) -_apiRouter.get('/login/:mid', apiLogin) _apiRouter.get('/logout', apiLogout) _apiRouter.head('/keyValidation/:mid', keyValidation) _apiRouter.patch('/experiences/:mid/experience/:eid/cast', experienceCast) @@ -125,21 +121,14 @@ function connectRoutes(_Menu){ return _Router } /** - * Validates member session is locked + * Ensure member session is unlocked or return to select. * @param {object} ctx Koa context object * @param {function} next Koa next function * @returns {function} Koa next function */ async function memberValidation(ctx, next) { - // validation logic - if(ctx.state.locked){ - ctx.redirect( - ( ctx.params?.mid?.length??false) - ? `/login/${encodeURIComponent(ctx.params.mid)}` - : '/select' - ) // Redirect to /members if not authorized - return - } + if(ctx.state.locked) + ctx.redirect(`/?type=select`) // Redirect to /members if not authorized await next() // Proceed to the next middleware if authorized } /** diff --git a/inc/js/session.mjs b/inc/js/session.mjs index 3c00a47..d0c00fc 100644 --- a/inc/js/session.mjs +++ b/inc/js/session.mjs @@ -56,14 +56,19 @@ class MylifeMemberSession extends EventEmitter { }) return currentAlerts } - async challengeAccess(_passphrase){ + /** + * Challenges and logs in member. + * @param {string} memberId - Member id to challenge. + * @param {string} passphrase - Passphrase response to challenge. + * @returns {boolean} - Whether or not member is logged in successfully. + */ + async challengeAccess(memberId, passphrase){ if(this.locked){ - if(!this.challenge_id) return false // this.challenge_id imposed by :mid from route - if(!await this.factory.challengeAccess(this.challenge_id, _passphrase)) return false // invalid passphrase, no access [converted in this build to local factory as it now has access to global datamanager to which it can pass the challenge request] - // init member + if(!await this.factory.challengeAccess(memberId, passphrase)) + return false // invalid passphrase, no access [converted in this build to local factory as it now has access to global datamanager to which it can pass the challenge request] this.#sessionLocked = false - this.emit('member-unlocked', this.challenge_id) - await this.init(this.challenge_id) + this.emit('member-unlocked', memberId) + await this.init(memberId) } return !this.locked } diff --git a/sample.env b/sample.env index 8602740..c78eeef 100644 --- a/sample.env +++ b/sample.env @@ -19,11 +19,11 @@ MYLIFE_DB_NAME=membership MYLIFE_DB_RW=string # add your key here MYLIFE_DB_RX=string # add your key here MYLIFE_SERVER_MBR_ID=mylife|... # add your key here -MYLIFE_HOSTED_MBR_ID=[] # array of ids hosted on your server -MYLIFE_SESSION_KEY=0.0.2.0001 # random string for resetting sessions +MYLIFE_HOSTED_MBR_ID=[] # array of ids hosted on your server--OR--remove or leave empty for db host under full database container +MYLIFE_SESSION_KEY=0.0.8 # random string for resetting sessions MYLIFE_SESSION_TIMEOUT_MS=900000 MYLIFE_SYSTEM_ALERT_CHECK_INTERVAL=120000 # how often to check for alerts in ms -MYLIFE_VERSION=0.0.2.0001 +MYLIFE_VERSION=0.0.8 MYLIFE_EMBEDDING_SERVER_URL= # temp deprecation MYLIFE_EMBEDDING_SERVER_PORT=0 # temp deprecation MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN= # temp deprecation diff --git a/server.js b/server.js index ddfd14e..f32726c 100644 --- a/server.js +++ b/server.js @@ -93,7 +93,15 @@ if(!fs.existsSync(uploadDir)){ app.context.MyLife = _Maht app.context.Globals = _Maht.globals app.context.menu = _Maht.menu -app.context.hostedMembers = JSON.parse(process.env.MYLIFE_HOSTED_MBR_ID) +const hostedMembers = JSON.parse(process.env?.MYLIFE_HOSTED_MBR_ID ?? '[]') +if(!hostedMembers.length){ + const members = ( await _Maht.hostedMembers() ) + .map(member=>member.mbr_id) + hostedMembers.push(...members) // throws on empty +} +app.context.hostedMembers = hostedMembers + .sort((a, b)=>a.localeCompare(b)) + .map(mbr_id=>({ 'id': mbr_id, 'name': _Maht.globals.sysName(mbr_id) })) app.keys = [process.env.MYLIFE_SESSION_KEY ?? `mylife-session-failsafe|${_Maht.newGuid()}`] // Enable Koa body w/ configuration app.use(koaBody({ diff --git a/views/assets/css/members.css b/views/assets/css/members.css index 0eef32b..541f34d 100644 --- a/views/assets/css/members.css +++ b/views/assets/css/members.css @@ -1,55 +1,72 @@ /* MyLife Member Challenge */ -.member-challenge-container { - align-items: flex-start; +.challenge-error { + align-items: center; + color: darkred; + display: flex; + font-size: 0.9rem; + justify-content: center; + margin-top: 0.5rem; + width: 90%; +} +.challenge-input, +.member-selection { + align-self: center; + background-color: rgba(255, 255, 255, 0.8); /* dark gray background */ + border: 0.01rem solid #000; + border-radius: 22rem; + display: flex; + justify-content: center; + margin: 1rem 0; + max-width: 90%; + min-width: 75%; + padding: 0.8rem; +} +.challenge-input { + align-items: center; display: flex; flex-direction: column; - height: auto; - justify-content: flex-start; - margin: 0; - padding: 2em 1em; - gap: 1em; /* Adds space between each child div */ - width: 100%; + justify-content: center; } -.member-challenge-error { - color: red; - font-size: 1.1em; - font-weight: bold; - margin: 1em 0; - width: 100%; -} -.member-challenge-input { +.challenge-input-container { + align-items: center; display: flex; - flex-direction: row; - gap: 1em; /* Adds space between input and button */ + flex: 1; + justify-content: center; width: 100%; } -.member-challenge-label { - display: flex; - font-size: 1.1em; - font-weight: bold; - margin-right: 1em; -} -.member-challenge-input-text { - border: 1px solid #ccc; - border-radius: 5px; +.challenge-input-text { + border: 0.1rem solid #ccc; + border-radius: 22rem; flex: 1; - font-size: 1.1em; - padding: 0.5em; + font-size: 0.9rem; + padding: 0.2rem; width: 100%; } -.member-challenge-submit { - background-color: #007BFF; /* Bootstrap primary blue */ +.challenge-submit { + align-items: center; + background-color: teal; /* Bootstrap primary blue */ border: none; border-radius: 5px; color: white; cursor: pointer; - display: none; - padding: 0.5em 1em; + display: flex; + flex: 0 1 30%; + font-size: 0.9rem; /* Matches input font size */ + justify-content: center; + height: 90%; + margin-left: 1rem; + max-width: 30%; transition: background-color 0.3s ease; /* Smooth transition for hover */ } -.member-challenge-submit:hover { +.challenge-submit:hover { background-color: #003a77; /* Darker blue on hover */ } +.member-select { + display: flex; + flex-direction: column; + gap: 1em; /* Adds space between each child div */ + width: 50%; +} /* MyLife Contribution Request */ .category-button { background-color: #f0f0f0; /* Neutral color */ diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index 1b6e139..ed4593f 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -8,12 +8,8 @@ const mHelpInitiatorContent = { } const mNewGuid = () => crypto.randomUUID() /* module variables */ -let mActiveHelpType, // active help type, currently entire HTMLDivElement - mLoginButton, - mLoginContainer, - mChallengeInput, - mChallengeError, - mChallengeSubmit, +let mAboutContainer, + mActiveHelpType, // active help type, currently entire HTMLDivElement mHelpAwait, mHelpClose, mHelpContainer, @@ -28,7 +24,8 @@ let mActiveHelpType, // active help type, currently entire HTMLDivElement mHelpSystemChat, mHelpType, mLoaded = false, - mLoginSelect, + mLoginButton, + mLoginContainer, mMainContent, mNavigation, mNavigationHelp, @@ -39,13 +36,10 @@ class Globals { #uuid = mNewGuid() constructor(){ if(!mLoaded){ + mAboutContainer = document.getElementById('about-container') mLoginButton = document.getElementById('navigation-login-logout-button') mLoginContainer = document.getElementById('navigation-login-logout') mMainContent = document.getElementById('main-content') - mChallengeInput = document.getElementById('member-challenge-input-text') - mChallengeError = document.getElementById('member-challenge-error') - mChallengeSubmit = document.getElementById('member-challenge-submit') - mLoginSelect = document.getElementById('member-select') mHelpAwait = document.getElementById('help-await') mHelpClose = document.getElementById('help-close') mHelpContainer = document.getElementById('help-container') @@ -67,7 +61,6 @@ class Globals { } } init(){ - console.log('Globals::init()', this.#uuid) /* global visibility settings */ this.hide(mHelpContainer) /* assign event listeners */ @@ -82,12 +75,6 @@ class Globals { mToggleHelpSubmit() } mLoginButton.addEventListener('click', this.loginLogout, { once: true }) - if(mChallengeInput){ - mChallengeInput.addEventListener('input', mToggleChallengeSubmit) - mChallengeSubmit.addEventListener('click', mSubmitChallenge) - } - if(mLoginSelect) - mLoginSelect.addEventListener('change', mSelectLoginId, { once: true }) } /* public functions */ /** @@ -204,14 +191,10 @@ class Globals { return false } } - loginLogout(event){ - const { target: loginButton, } = event - if(loginButton!==mLoginButton) - throw new Error('loginLogout::loginButton not found') - if(loginButton.getAttribute('data-locked') === 'true') - mLogin() - else - mLogout() + async loginLogout(event){ + this.getAttribute('data-locked')==='true' + ? mLogin() + : mLogout() } /** * Last stop before Showing an element and kicking off animation chain. Adds universal run-once animation-end listener, which may include optional callback functionality. @@ -477,13 +460,12 @@ function mLaunchTutorial(){ // mHide(mHelpContainer) } /** - * Redirects to the login page. + * Redirects to login page (?select). * @private * @returns {void} */ function mLogin(){ - console.log('login') - window.location.href = '/select' + window.location.href = '/?type=select' } /** * Logs out the current user and redirects to homepage. @@ -503,19 +485,6 @@ async function mLogout(){ else console.error('mLogout::response not ok', response) } -/** - * Redirects to the login page with a selected member id. - * @param {Event} event - The event object. - * @returns {void} - */ -function mSelectLoginId(event){ - event.preventDefault() - console.log('mSelectLoginId', mLoginSelect) - const { value, } = mLoginSelect - if(!value?.length) - return - window.location = `/login/${value}` -} /** * Sets the type of help required by member. * @todo - incorporate multiple help strata before llm access; here local @@ -558,39 +527,6 @@ function mShow(element, listenerFunction){ element.classList.add('show') } } -/** - * Submits a challenge response to the server. - * @module - * @async - * @param {Event} event - The event object. - * @returns {void} - */ -async function mSubmitChallenge(event){ - event.preventDefault() - event.stopPropagation() - const { id, value: passphrase, } = mChallengeInput - if(!passphrase.trim().length) - return - mHide(mChallengeSubmit) - const _mbr_id = window.location.pathname.split('/')[window.location.pathname.split('/').length-1] - const url = window.location.origin+`/challenge/${_mbr_id}` - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ passphrase, }), - } - const validatePassphrase = await mSubmitPassphrase(url, options) - if(validatePassphrase) - location.href = '/members' - else { - mChallengeError.innerHTML = 'Invalid passphrase: please try again and remember that passphrases are case sensitive.'; - mChallengeInput.value = null - mChallengeInput.placeholder = 'Try your passphrase again...' - mChallengeInput.focus() - } -} /** * Submits a help request to the server. * @module @@ -663,22 +599,6 @@ async function mSubmitHelpToServer(helpRequest, type='general', mbr_id){ else throw new Error(jsonResponse?.message ?? 'unknown server error') } -/** - * Submits a passphrase to the server. - * @param {string} url - The url to submit the passphrase to. - * @param {object} options - The options for the fetch request. - * @returns {object} - The response from the server. - */ -async function mSubmitPassphrase(url, options) { - try { - const response = await fetch(url, options) - const jsonResponse = await response.json() - return jsonResponse - } catch (err) { - console.log('fatal error', err) - return false - } -} /** * Toggles the visibility of the challenge submit button based on `input` event. * @requires mChallengeSubmit diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index f104faf..ea769bd 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -8,19 +8,25 @@ const hide = mGlobals.hide const mPlaceholder = `Type a message to ${ mAvatarName }...` const show = mGlobals.show /* variables */ -let mChatBubbleCount = 0, +let mChallengeMemberId, + mChatBubbleCount = 0, mDefaultTypeDelay = 7, + mPageType = null, threadId = null /* page div variables */ -let aboutContainer, - awaitButton, +let awaitButton, agentSpinner, + challengeError, + challengeInput, + challengeInputText, + challengeSubmit, chatContainer, chatInput, chatLabel, chatSubmit, chatSystem, chatUser, + loginSelect, mainContent, navigation, privacyContainer, @@ -42,6 +48,7 @@ document.addEventListener('DOMContentLoaded', event=>{ function mAddMessage(message, options={}){ const { bubbleClass='agent-bubble', + callback=_=>{}, typeDelay=mDefaultTypeDelay, typewrite=true, } = options @@ -51,12 +58,26 @@ function mAddMessage(message, options={}){ mChatBubbleCount++ chatSystem.appendChild(chatBubble) if(typewrite) - mTypeMessage(chatBubble, message, typeDelay) + mTypeMessage(chatBubble, message, typeDelay, callback) else { chatBubble.insertAdjacentHTML('beforeend', message) mScrollBottom() + callback() } } +/** + * Adds multiple messages to the chat column. + * @param {Message[]} messages - The messages to add to the chat column. + * @param {object} options - The options for the chat bubble. + * @returns {void} + */ +async function mAddMessages(messages, options={}){ + for (const message of messages) { + await new Promise(resolve=>{ + mAddMessage(message, {...options, callback: resolve}) + }) + } +} /** * Add `user` type message to the chat column. * @param {Event} event - The event object. @@ -72,6 +93,57 @@ function mAddUserMessage(event){ mSubmitInput(event, message) mAddMessage(message, { bubbleClass: 'user-bubble', typeDelay: 2, }) } +async function mChallengeMember(event){ + const { options, selectedIndex, value, } = this + mChallengeMemberId = value + const memberName = options[selectedIndex].text + // set member on server + this.disabled = true + const messages = [`If you want to get to ${ memberName }, I challenge you to a game of passphrase!`, `Please enter the passphrase for your account to continue...`] + await mAddMessages(messages, { typeDelay: 6, }) + chatSystem.appendChild(mCreateChallengeElement()) +} +/** + * Creates a challenge element for the user to enter their passphrase. Simultaneously sets modular variables to the instantion of the challenge element. Unclear what happens if multiples are attempted to spawn, but code shouldn't allow for that, only hijax. See the `@required` for elements that this function generates and associates. + * @private + * @required challengeError + * @required challengeInput + * @required challengeInputText + * @required challengeSubmit + * @returns {HTMLDivElement} - The challenge element. + */ +function mCreateChallengeElement(){ + /* input container */ + challengeInput = document.createElement('div') + challengeInput.className = 'challenge-input' + challengeInput.id = 'challenge-input' + const challengeInputContainer = document.createElement('div') + challengeInputContainer.className = 'challenge-input-container' + /* input field */ + challengeInputText = document.createElement('input') + challengeInputText.addEventListener('input', mToggleChallengeSubmitButton) + challengeInputText.className = 'challenge-input-text' + challengeInputText.id = 'challenge-input-text' + challengeInputText.placeholder = 'Enter your passphrase...' + challengeInputText.type = 'password' + challengeInputContainer.appendChild(challengeInputText) + /* submit button */ + challengeSubmit = document.createElement('button') + challengeSubmit.addEventListener('click', mSubmitChallenge) + challengeSubmit.className = 'challenge-submit' + challengeSubmit.id = 'challenge-submit' + challengeSubmit.innerHTML = 'Enter MyLife' + challengeInputContainer.appendChild(challengeSubmit) + challengeInput.appendChild(challengeInputContainer) + /* error message */ + challengeError = document.createElement('div') + challengeError.className = 'challenge-error' + challengeError.id = 'challenge-error' + challengeInput.appendChild(challengeError) + hide(challengeError) + hide(challengeSubmit) + return challengeInput +} /** * Fetches the greeting messages from the server. The greeting object from server: { error, messages, success, } * @private @@ -95,22 +167,79 @@ async function mFetchGreetings(dynamic=false){ return [`Error: ${ error.message }`, `Please try again. If this persists, check back with me later or contact support.`] } } +/** + * Fetches the hosted members from the server. + * @private + * @returns {Promise} - The response Member List { id, name, } array. + */ +async function mFetchHostedMembers(){ + let url = window.location.origin + + '/select' + try { + const response = await fetch(url) + const hostedMembers = await response.json() + return hostedMembers + } catch(error) { + return [`Error: ${ error.message }`, `Please try again. If this persists, check back with me later or contact support.`] + } +} /** * Fetches the greeting messages or start routine from the server. * @private + * @requires mPageType * @returns {void} */ async function mFetchStart(){ - const greetings = await mFetchGreetings() - greetings.forEach(greeting=>{ - const message = greeting?.message - ?? greeting - mAddMessage(message, { + const messages = [] + let input // HTMLDivElement containing input element + switch(mPageType){ + case 'challenge': + case 'select': + const hostedMembers = await mFetchHostedMembers() + if(!hostedMembers.length) + messages.push(`My sincere apologies, I'm unable to get the list of hosted members, the MyLife system must be down -- @Mookse`) + else { + messages.push(...[`Welcome to MyLife!`, `Please select your avatar-id from the list below...`]) + // create the select element and append + const selectContainer = document.createElement('div') + selectContainer.id = 'member-selection' + selectContainer.className = 'member-selection' + const select = document.createElement('select') + select.id = 'member-select' + select.className = 'member-select' + select.addEventListener('change', mChallengeMember) + hostedMembers.unshift({ id: null, name: 'Select your avatar-id...', }) + hostedMembers + .forEach(member=>{ + const option = document.createElement('option') + option.disabled = false + option.hidden = false + option.selected = false + option.text = member.name + option.value = member.id + select.appendChild(option) + }) + selectContainer.appendChild(select) + input = selectContainer + } + break + default: + const greetings = ( await mFetchGreetings() ) + .map(greeting=> + greeting?.message + ?? greeting + ) + messages.push(...greetings) + break + } + if(messages.length) + await mAddMessages(messages, { bubbleClass: 'agent-bubble', - typeDelay: 8, + typeDelay: 10, typewrite: true, }) - }) + if(input) + chatSystem.appendChild(input) } /** * Initializes event listeners. @@ -124,13 +253,12 @@ function mInitializeListeners(){ chatSubmit.addEventListener('click', mAddUserMessage) } /** - * Load data for the page. + * Determines page type and loads data. * @private * @returns {void} */ async function mLoadStart(){ /* assign page div variables */ - aboutContainer = document.getElementById('about-container') awaitButton = document.getElementById('await-button') agentSpinner = document.getElementById('agent-spinner') chatContainer = document.getElementById('chat-container') @@ -144,13 +272,15 @@ async function mLoadStart(){ privacyContainer = document.getElementById('privacy-container') sidebar = mGlobals.sidebar /* fetch the greeting messages */ + // get query params + mPageType = new URLSearchParams(window.location.search).get('type') await mFetchStart() } /** * Scrolls overflow of system chat to bottom. * @returns {void} */ -function mScrollBottom() { +function mScrollBottom(){ chatSystem.scrollTop = chatSystem.scrollHeight } /** @@ -182,6 +312,56 @@ function mShowPage(){ show(chatContainer) show(chatUser) } +/** + * Submits a challenge response to the server. + * @module + * @async + * @requires mChallengeMemberId + * @param {Event} event - The event object. + * @returns {void} + */ +async function mSubmitChallenge(event){ + event.preventDefault() + event.stopPropagation() + const { id, value: passphrase, } = challengeInputText + if(!passphrase.trim().length > 3 || !mChallengeMemberId) + return + hide(challengeSubmit) + const url = window.location.origin+`/challenge/${ mChallengeMemberId }` + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ passphrase, }), + } + const validatePassphrase = await mSubmitPassphrase(url, options) + if(validatePassphrase) + location.href = '/members' + else { + challengeError.innerHTML = 'incorrect passphrase, please try again.'; + challengeInputText.value = null + challengeInputText.placeholder = 'Oops! Try your passphrase again...' + show(challengeError) + challengeInputText.focus() + } +} +/** + * Submits a passphrase to the server. + * @param {string} url - The url to submit the passphrase to. + * @param {object} options - The options for the fetch request. + * @returns {object} - The response from the server. + */ +async function mSubmitPassphrase(url, options) { + try { + const response = await fetch(url, options) + const jsonResponse = await response.json() + return jsonResponse + } catch (err) { + console.log('fatal error', err) + return false + } +} /** * Submits a message to the server. * @param {Event} event - The event object. @@ -228,6 +408,24 @@ async function submitChat(url, options) { return alert(`Error: ${err.message}`) } } +/** + * Toggles the visibility of the challenge submit button based on `input` event. + * @requires mChallengeSubmit + * @param {Event} event - The event object. + * @returns {void} + */ +function mToggleChallengeSubmitButton(event){ + const { value, } = this + if(value.trim().length > 3){ + challengeSubmit.disabled = false + challengeSubmit.style.cursor = 'pointer' + show(challengeSubmit) + } else { + challengeSubmit.disabled = true + challengeSubmit.style.cursor = 'not-allowed' + hide(challengeSubmit) + } +} function mToggleInputTextarea() { chatInput.style.height = 'auto' // Reset height to shrink if text is removed chatInput.style.height = chatInput.scrollHeight + 'px' // Set height based on content @@ -250,7 +448,7 @@ function mToggleSubmitButton(){ * @param {number} typeDelay - The delay between typing each character. * @returns {void} */ -function mTypeMessage(chatBubble, message, typeDelay=mDefaultTypeDelay){ +function mTypeMessage(chatBubble, message, typeDelay=mDefaultTypeDelay, callback){ let i = 0 let tempMessage = '' function _typewrite() { @@ -260,8 +458,10 @@ function mTypeMessage(chatBubble, message, typeDelay=mDefaultTypeDelay){ chatBubble.insertAdjacentHTML('beforeend', tempMessage) i++ setTimeout(_typewrite, typeDelay) // Adjust the typing speed here (50ms) - } else + } else { chatBubble.setAttribute('status', 'done') + callback() + } mScrollBottom() } _typewrite() diff --git a/views/members-challenge.html b/views/members-challenge.html deleted file mode 100644 index f906dd2..0000000 --- a/views/members-challenge.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
Enter your passphrase:
-
- - -
-
-
\ No newline at end of file diff --git a/views/members-select.html b/views/members-select.html deleted file mode 100644 index 895081f..0000000 --- a/views/members-select.html +++ /dev/null @@ -1,8 +0,0 @@ -
- -
\ No newline at end of file From 7c1a39b06c2ec826ccbf305b0f808c341bb595e7 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sun, 26 May 2024 17:06:10 -0400 Subject: [PATCH 28/32] 20240526 @Mookse - remove contributions - remove category(ies) - remove local embedding [for now] --- inc/js/core.mjs | 10 -- inc/js/deprecated/{_core.mjs => core.mjs} | 0 .../mylife-pgvector-datamanager.mjs | 0 .../class-extenders.mjs | 7 +- .../class-message-functions.mjs | 29 +--- inc/js/functions.mjs | 50 ------- inc/js/mylife-agent-factory.mjs | 1 - inc/js/mylife-avatar.mjs | 124 +----------------- inc/js/mylife-data-service.js | 41 ------ inc/js/mylife-datamanager.mjs | 1 - inc/js/mylife-datasource-config.mjs | 7 - inc/js/routes.mjs | 4 - inc/js/session.mjs | 4 - .../{depreacted => deprecated}/avatar.json | 0 .../{ => deprecated}/contribution.json | 0 inc/json-schemas/message.json | 24 +--- sample.env | 6 - server.js | 1 - views/assets/html/_widget-contributions.html | 86 ------------ views/avatars.html | 35 ----- 20 files changed, 10 insertions(+), 420 deletions(-) rename inc/js/deprecated/{_core.mjs => core.mjs} (100%) rename inc/js/{ => deprecated}/mylife-pgvector-datamanager.mjs (100%) rename inc/json-schemas/{depreacted => deprecated}/avatar.json (100%) rename inc/json-schemas/{ => deprecated}/contribution.json (100%) delete mode 100644 views/assets/html/_widget-contributions.html delete mode 100644 views/avatars.html diff --git a/inc/js/core.mjs b/inc/js/core.mjs index c388039..1c69061 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -33,9 +33,6 @@ class Member extends EventEmitter { this.factory.on('avatar-init-end',(_avatar,bytes)=>{ console.log(chalk.grey(`Member::init::avatar-init-end|memory-size=${bytes}b`)) }) - this.factory.on('on-contribution-new',(_contribution)=>{ - console.log(chalk.grey(`Member::on-contribution-new`),_contribution.request) - }) } // getter/setter functions get abilities(){ @@ -100,13 +97,6 @@ class Member extends EventEmitter { set consent(_consent){ this.factory.consents.unshift(_consent.id) } - /** - * Gets Member Contributions, i.e., questions that need posing to Member - * @returns {array} returns array of Member Contributions - */ - get contributions(){ - return this.#avatar?.contributions - } get core(){ return this.factory.core } diff --git a/inc/js/deprecated/_core.mjs b/inc/js/deprecated/core.mjs similarity index 100% rename from inc/js/deprecated/_core.mjs rename to inc/js/deprecated/core.mjs diff --git a/inc/js/mylife-pgvector-datamanager.mjs b/inc/js/deprecated/mylife-pgvector-datamanager.mjs similarity index 100% rename from inc/js/mylife-pgvector-datamanager.mjs rename to inc/js/deprecated/mylife-pgvector-datamanager.mjs diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 75f45ba..9d33a17 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -15,7 +15,7 @@ import{ mGetSceneNext, } from './class-experience-functions.mjs' import { - mAssignContent, + assignContent, } from './class-message-functions.mjs' /** * Extends the `Consent` class. @@ -380,7 +380,6 @@ function extendClass_file(originClass, referencesObject) { // self-validation if(!this.contents && this.type=='text') throw new Error('No contents provided for text file; will not store') - // save to embedder } // public getters/setters // private functions @@ -406,7 +405,7 @@ function extendClass_message(originClass, referencesObject) { const { content, ..._obj } = obj super(_obj) try{ - this.#content = mAssignContent(content ?? obj) + this.#content = assignContent(content ?? obj) } catch(e){ console.log('Message::constructor::ERROR', e) this.#content = '' @@ -418,7 +417,7 @@ function extendClass_message(originClass, referencesObject) { } set content(_content){ try{ - this.#content = mAssignContent(_content) + this.#content = assignContent(_content) } catch(e){ console.log('Message::content::ERROR', e) } diff --git a/inc/js/factory-class-extenders/class-message-functions.mjs b/inc/js/factory-class-extenders/class-message-functions.mjs index 72b97fa..81b1ae3 100644 --- a/inc/js/factory-class-extenders/class-message-functions.mjs +++ b/inc/js/factory-class-extenders/class-message-functions.mjs @@ -6,7 +6,7 @@ * @param {any} obj - Element to assign to `content` property * @returns {string} - message text content */ -function mAssignContent(obj){ +function assignContent(obj){ const contentErrorMessage = 'No content found.' const keyIncludes = ['category', 'content', 'input', 'message', 'text', 'value'] switch(typeof obj){ @@ -18,7 +18,7 @@ function mAssignContent(obj){ throw new Error(contentErrorMessage) for(const element of obj){ try{ - const content = mAssignContent(element) + const content = assignContent(element) return content } catch(e){ if(e.message===contentErrorMessage) @@ -30,7 +30,7 @@ function mAssignContent(obj){ for(const key in obj){ try{ if(keyIncludes.includes(key)){ - const content = mAssignContent(obj[key]) + const content = assignContent(obj[key]) return content } } catch(e){ @@ -48,27 +48,6 @@ function mAssignContent(obj){ } } /* private module functions */ -/** - * When incoming text is too large for a single message, generate dynamic text file and attach/submit. - * @module - * @private - * @param {string} _file - The file to construct. - * @returns - */ -async function mConstructFile(_file){ - // construct file object - const __file = new (this.factory.file)({ - name: `file_message_${this.id}`, - type: 'text', - contents: _file, - }) - // save to embedder - return { - name: __file.name, - type: __file.type, - contents: await __file.arrayBuffer(), - } -} /** * Checks if content is a non-empty string. * @module @@ -81,5 +60,5 @@ function mIsNonEmptyString(content){ } /* exports */ export { - mAssignContent, + assignContent, } \ No newline at end of file diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 602634d..3ac7096 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -93,20 +93,6 @@ async function collections(ctx){ const { avatar, } = ctx.state ctx.body = await avatar.collections(ctx.params.type) } -/** - * Manage delivery and receipt of contributions(s). - * @async - * @public - * @api no associated view - * @param {object} ctx Koa Context object - */ -async function contributions(ctx){ - ctx.body = await ( - (ctx.method==='GET') - ? mGetContributions(ctx) - : mSetContributions(ctx) - ) -} async function createBot(ctx){ const { team, type, } = ctx.request.body const { avatar, } = ctx.state @@ -293,41 +279,6 @@ async function upload(ctx){ ctx.session.isAPIValidated = true await apiUpload(ctx) } -/* module private functions */ -function mGetContributions(ctx){ - ctx.state.cid = ctx.params?.cid??false // contribution id - const statusOrder = ['new', 'prepared', 'requested', 'submitted', 'pending', 'accepted', 'rejected'] - ctx.state.status = ctx.query?.status??'prepared' // default state for execution - return ctx.state.contributions - .filter(_contribution => (ctx.state.cid && _contribution.id === ctx.state.cid) || !ctx.state.cid) - .map(_contribution => (_contribution.memberView)) - .sort((a, b) => { - // Get the index of the status for each contribution - const indexA = statusOrder.indexOf(a.status) - const indexB = statusOrder.indexOf(b.status) - // Sort based on the index - return indexA - indexB - }) -} -/** - * Manage receipt and setting of contributions(s). - * @async - * @module - * @param {object} ctx Koa Context object - */ -function mSetContributions(ctx){ - const { avatar, } = ctx.state - if(!ctx.params?.cid) - ctx.throw(400, `missing contribution id`) // currently only accepts single contributions via post with :cid - ctx.state.cid = ctx.params.cid - const _contribution = ctx.request.body?.contribution??false - if(!_contribution) - ctx.throw(400, `missing contribution data`) - avatar.contribution = ctx.request.body.contribution - return avatar.contributions - .filter(_contribution => (_contribution.id === ctx.state.cid)) - .map(_contribution => (_contribution.memberView)) -} /* exports */ export { about, @@ -338,7 +289,6 @@ export { challenge, chat, collections, - contributions, createBot, deleteItem, help, diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 9019aea..0c29f50 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -952,7 +952,6 @@ function mAvatarProperties(_core){ "being", "bots", "command_word", - "contributions", "conversations", "id", "mbr_id", diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 404a958..9e7528b 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -17,7 +17,6 @@ const mBotIdOverride = OPENAI_MAHT_GPT_OVERRIDE */ class Avatar extends EventEmitter { #activeBotId // id of active bot in this.#bots; empty or undefined, then this - #activeChatCategory = mGetChatCategory() #assetAgent #bots = [] #conversations = [] @@ -162,7 +161,7 @@ class Avatar extends EventEmitter { if(mAllowSave) conversation.save() else - console.log('chatRequest::BYPASS-SAVE', conversation.message?.content) + console.log('chatRequest::BYPASS-SAVE', conversation.message?.content?.substring(0,64)) /* frontend mutations */ const { activeBot: bot } = this // current fe will loop through messages in reverse chronological order @@ -450,19 +449,6 @@ class Avatar extends EventEmitter { throw new Error('Passphrase required for reset.') return await this.#factory.resetPassphrase(passphrase) } - /** - * Processes and executes incoming category set request. - * @todo - deprecate if possible. - * @public - * @param {string} _category - The category to set { category, contributionId, question }. - */ - setActiveCategory(_category){ - const _proposedCategory = mGetChatCategory(_category) - /* evolve contribution */ - if(_proposedCategory?.category){ // no category, no contribution - this.#evolver.setContribution(this.#activeChatCategory, _proposedCategory) - } - } /** * Add or update bot, and identifies as activated, unless otherwise specified. * @todo - strip protected bot data/create class?. @@ -680,23 +666,6 @@ class Avatar extends EventEmitter { get cast(){ return this.experience.castMembers } - /** - * Get the active chat category. - * @getter - * @returns {string} - The active chat category. - */ - get category(){ - return this.#activeChatCategory - } - /** - * Set the active chat category. - * @setter - * @param {string} _category - The new active chat category. - * @returns {void} - */ - set category(_category){ - this.#activeChatCategory = _category - } /** * Get the cast. * @getter @@ -705,22 +674,6 @@ class Avatar extends EventEmitter { get cast(){ return this.experience.cast } - /** - * Get contributions. - * @getter - * @returns {array} - The contributions. - */ - get contributions(){ - return this.#evolver?.contributions - } - /** - * Set incoming contribution. - * @setter - * @param {object} _contribution - */ - set contribution(_contribution){ - this.#evolver.contribution = _contribution - } /** * Get uninstantiated class definition for conversation. If getting a specific conversation, use .conversation(id). * @getter @@ -983,38 +936,6 @@ class Avatar extends EventEmitter { */ function mAssignEvolverListeners(factory, evolver, avatar){ /* assign evolver listeners */ - evolver.on( - 'on-contribution-new', - _contribution=>{ - _contribution.emit('on-contribution-new', _contribution) - } - ) - evolver.on( - 'avatar-change-category', - (_current, _proposed)=>{ - avatar.category = _proposed - console.log('avatar-change-category', avatar.category.category) - } - ) - evolver.on( - 'on-contribution-submitted', - _contribution=>{ - // send to gpt for summary - const _responses = _contribution.responses.join('\n') - const _summary = factory.openai.completions.create({ - model: 'gpt-4o', - prompt: 'summarize answers in 512 chars or less, if unsummarizable, return "NONE": ' + _responses, - temperature: 1, - max_tokens: 700, - frequency_penalty: 0.87, - presence_penalty: 0.54, - }) - // evaluate summary - console.log('on-contribution-submitted', _summary) - return - // if summary is good, submit to cosmos - } - ) } /** * Assigns (directly mutates) private experience variables from avatar. @@ -1617,41 +1538,6 @@ function mFindBot(avatar, _botId){ .filter(_bot=>{ return _bot.id==_botId }) [0] } -/** - * Returns simple micro-category after logic mutation. - * @module - * @param {string} _category text of category - * @returns {string} formatted category - */ -function mFormatCategory(_category){ - return _category - .trim() - .slice(0, 128) // hard cap at 128 chars - .replace(/\s+/g, '_') - .toLowerCase() -} -/** - * Returns MyLife-version of chat category object - * @module - * @param {object} _category - local front-end category { category, contributionId, question/message/content } - * @returns {object} - local category { category, contributionId, content } - */ -function mGetChatCategory(_category) { - const _proposedCategory = { - category: '', - contributionId: undefined, - content: undefined, - } - if(_category?.category && _category.category.toLowerCase() !== 'off'){ - _proposedCategory.category = mFormatCategory(_category.category) - _proposedCategory.contributionId = _category.contributionId - _proposedCategory.content = - _category?.question?? - _category?.message?? - _category?.content // test for undefined - } - return _proposedCategory -} /** * Returns set of Greeting messages, dynamic or static. * @param {object} bot - The bot object. @@ -1759,9 +1645,7 @@ function mPruneBot(assistantData){ } } /** - * returns simple micro-message with category after logic mutation. - * Currently tuned for openAI gpt-assistant responses. - * @todo - revamp as any of these LLMs can return JSON or run functions for modes. + * Returns frontend-ready Message object after logic mutation. * @module * @private * @param {object} bot - The bot object, usually active. @@ -1774,8 +1658,6 @@ function mPruneMessage(bot, message, type='chat', processStartTime=Date.now()){ /* parse message */ const { bot_id: activeBotAIId, id: activeBotId, } = bot let agent='server', - category, - contributions=[], purpose=type, response_time=Date.now()-processStartTime const { content: messageContent, thread_id, } = message @@ -1793,8 +1675,6 @@ function mPruneMessage(bot, message, type='chat', processStartTime=Date.now()){ activeBotId, activeBotAIId, agent, - category, - contributions, message, purpose, response_time, diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index 655edcb..aebf1a8 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -5,7 +5,6 @@ */ // imports import Datamanager from "./mylife-datamanager.mjs" -import PgvectorManager from "./mylife-pgvector-datamanager.mjs" /** * The Dataservices class. * This class provides methods to interact with the data layers of the MyLife platform, predominantly the Azure Cosmos and PostgreSQL database. @@ -37,13 +36,6 @@ class Dataservices { * @private */ #partitionId - /** - * Manages interactions with Pgvector, which might be used for - * efficient vector operations in a Postgres database. This could include - * functionalities like similarity search, nearest neighbor search, etc. - * @private - */ - #PgvectorManager /** * A default SQL SELECT statement or part of it used to fetch * user-related data. It defines the columns to be retrieved in most @@ -57,7 +49,6 @@ class Dataservices { */ constructor(_mbr_id){ this.#partitionId = _mbr_id - this.#PgvectorManager = new PgvectorManager() } /** * Initializes the Datamanager instance and sets up core data. @@ -90,9 +81,6 @@ class Dataservices { get datamanager(){ return this.#Datamanager } - get embedder(){ - return this.#PgvectorManager - } get globals(){ return this.datamanager.globals } @@ -392,26 +380,6 @@ class Dataservices { } return _chats } - /** - * Retrieves all seed contribution questions associated with being & category. - * @async - * @public - * @param {string} being - The type of underlying datacore. - * @param {string} _category - The category of seed questions to retrieve. - * @returns {Promise} The item corresponding to the provided ID. - */ - async getContributionQuestions(being, _category, _maxNumber=3){ - return (await this.getItems( - being, - ['questions'], - [{ name: '@category', value: _category }], - 'contribution_responses', - )) - .map(_ => (_.questions)) - .reduce((acc, val) => acc.concat(val), []) - .sort(() => Math.random() - Math.random()) - .slice(0, _maxNumber) - } /** * Retrieves a specific item by its ID. * @async @@ -535,15 +503,6 @@ class Dataservices { ) return _items } - /** - * Retrieves local records based on a query. - * @async - * @param {string} _question - The query to retrieve records. - * @returns {Promise} An array of local records matching the query. - */ - async getLocalRecords(_question){ - return await this.embedder.getLocalRecords(_question) - } /** * Returns Array of hosted members based on validation requirements. * @param {Array} validations - Array of validation strings to filter membership. diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index 0aeda8b..d2b6bf9 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -26,7 +26,6 @@ class Datamanager { this.#partitionId = _config.members.container.partitionId this.#coreId = _config.members.container?.coreId ?? this.#partitionId.split('|')[1] this.#containers = { - contribution_responses: this.database.container(_config.contributions.container.id), members: this.database.container(_config.members.container.id), registration: this.database.container(_config.registration.container.id), system: this.database.container(_config.system.container.id), diff --git a/inc/js/mylife-datasource-config.mjs b/inc/js/mylife-datasource-config.mjs index c883233..ecf66f5 100644 --- a/inc/js/mylife-datasource-config.mjs +++ b/inc/js/mylife-datasource-config.mjs @@ -12,13 +12,6 @@ class Config{ coreId: _mbr_id.split('|')[1], // second object is core item id } } - this.contributions={ - id: process.env.MYLIFE_DB_NAME, - container: { - id: process.env.MYLIFE_CONTRIBUTIONS_DB_CONTAINER_NAME, - partitionId: _mbr_id, - } - } this.registration={ id: process.env.MYLIFE_DB_NAME, container: { diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 9f31e92..8ad9223 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -9,7 +9,6 @@ import { challenge, chat, collections, - contributions, createBot, deleteItem, greetings, @@ -89,8 +88,6 @@ _memberRouter.get('/bots', bots) _memberRouter.get('/bots/:bid', bots) _memberRouter.get('/collections', collections) _memberRouter.get('/collections/:type', collections) -_memberRouter.get('/contributions', contributions) -_memberRouter.get('/contributions/:cid', contributions) _memberRouter.get('/experiences', experiences) _memberRouter.get('/experiencesLived', experiencesLived) _memberRouter.get('/greeting', greetings) @@ -103,7 +100,6 @@ _memberRouter.post('/bots', bots) _memberRouter.post('/bots/create', createBot) _memberRouter.post('/bots/activate/:bid', activateBot) _memberRouter.post('/category', category) -_memberRouter.post('contributions/:cid', contributions) _memberRouter.post('/mode', interfaceMode) _memberRouter.post('/passphrase', passphraseReset) _memberRouter.post('/upload', upload) diff --git a/inc/js/session.mjs b/inc/js/session.mjs index d0c00fc..0aeb3d4 100644 --- a/inc/js/session.mjs +++ b/inc/js/session.mjs @@ -4,7 +4,6 @@ class MylifeMemberSession extends EventEmitter { #alertsShown = [] // array of alert_id's shown to member in this session #autoplayed = false // flag for autoplayed experience, one per session #consents = [] // consents are stored in the session - #contributions = [] // intended to hold all relevant contribution questions for session #experienceLocked = false #experiences = [] // holds id for experiences conducted in this session #factory @@ -215,9 +214,6 @@ class MylifeMemberSession extends EventEmitter { get consents(){ return this.#consents } - get contributions(){ - return this.#contributions - } get core(){ return this.factory.core } diff --git a/inc/json-schemas/depreacted/avatar.json b/inc/json-schemas/deprecated/avatar.json similarity index 100% rename from inc/json-schemas/depreacted/avatar.json rename to inc/json-schemas/deprecated/avatar.json diff --git a/inc/json-schemas/contribution.json b/inc/json-schemas/deprecated/contribution.json similarity index 100% rename from inc/json-schemas/contribution.json rename to inc/json-schemas/deprecated/contribution.json diff --git a/inc/json-schemas/message.json b/inc/json-schemas/message.json index 6247b57..f62d95d 100644 --- a/inc/json-schemas/message.json +++ b/inc/json-schemas/message.json @@ -258,7 +258,7 @@ "message_member_chat": { "name": "Message_member_chat", "type": "object", - "required": ["agent", "category", "contributions", "id", "message", "type"], + "required": ["agent", "category", "id", "message", "type"], "properties": { "agent": { "const": "member", @@ -267,22 +267,6 @@ "description": "type of entity submitting", "$comment": "three distinct and possible submittors: assistant (deprecate to avatar/agent), member (human), user (human)" }, - "category": { - "type": "string", - "default": "", - "description": "message category, if applicable", - "$comment": "in this instance, member has clicked a topic related to contribution(s) (whose .id is included in `contributions` array) and is submitting their response related to that `contribution.category`; category topics can span multiple response messages, but a category reset should equally reset the `contributions` array to empty" - }, - "contributions": { - "type": "array", - "default": [], - "items": { - "type": "string", - "format": "uuid", - "description": "contribution object ids" - }, - "uniqueItems": true - }, "id": { "type": "string", "format": "uuid", @@ -301,12 +285,6 @@ "description": "UNIX date-time of front-end receipt to delivery of response", "$comment": "tracks from point of delivery of request to point of delivery of response; can be used to track response time of member, assistant and potentially MyLife server services; could contribute to extrapolate other facets of human behavioral responses with digital interface devices" }, - "purpose": { - "type": "string", - "default": "", - "description": "message purpose, if applicable", - "$comment": "quite interesting to presume to extrapolate the purpose of a given message, but it certainly could be done; for example, if a 'response to a contribution request', then pupose can be explicated; currently unimplemented" - }, "type": { "type": "string", "default": "chat", diff --git a/sample.env b/sample.env index c78eeef..55c70f1 100644 --- a/sample.env +++ b/sample.env @@ -24,11 +24,5 @@ MYLIFE_SESSION_KEY=0.0.8 # random string for resetting sessions MYLIFE_SESSION_TIMEOUT_MS=900000 MYLIFE_SYSTEM_ALERT_CHECK_INTERVAL=120000 # how often to check for alerts in ms MYLIFE_VERSION=0.0.8 -MYLIFE_EMBEDDING_SERVER_URL= # temp deprecation -MYLIFE_EMBEDDING_SERVER_PORT=0 # temp deprecation -MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN= # temp deprecation -MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT= # temp deprecation -MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT_ADMIN= # temp deprecation -MYLIFE_CONTRIBUTIONS_DB_CONTAINER_NAME= # get from admin MYLIFE_REGISTRATION_DB_CONTAINER_NAME= # get from admin MYLIFE_SYSTEM_DB_CONTAINER_NAME= # get from admin \ No newline at end of file diff --git a/server.js b/server.js index f32726c..5fbc41a 100644 --- a/server.js +++ b/server.js @@ -171,7 +171,6 @@ app.use(koaBody({ ctx.state.member = ctx.state.MemberSession?.member ?? ctx.MyLife // point member to session member (logged in) or MAHT (not logged in) ctx.state.avatar = ctx.state.member.avatar - ctx.state.contributions = ctx.state.avatar.contributions ctx.state.interfaceMode = ctx.state.avatar?.mode ?? 'standard' ctx.state.menu = ctx.MyLife.menu if(!await ctx.state.MemberSession.requestConsent(ctx)) diff --git a/views/assets/html/_widget-contributions.html b/views/assets/html/_widget-contributions.html deleted file mode 100644 index fea7b91..0000000 --- a/views/assets/html/_widget-contributions.html +++ /dev/null @@ -1,86 +0,0 @@ -
-
Topics for <%= member.name %>
-
- <% if (contributions && contributions.length > 0) { %> - <% contributions.forEach(contribution => { %> -
- - × -
- <% }) %> - <% } else { %> -

No contributions available.

- <% } %> -
-
- \ No newline at end of file diff --git a/views/avatars.html b/views/avatars.html deleted file mode 100644 index b5b0d06..0000000 --- a/views/avatars.html +++ /dev/null @@ -1,35 +0,0 @@ -<% if (avatars && avatars.length > 0) { %> -
- <% avatars.forEach(avatar => { %> -
-
-
- <%= avatar.name %> -
-
- ID: <%= avatar.id %> -
-
- Description: <%= avatar.description %> -
-
- Purpose: <%= avatar.purpose %> -
-
- Categories: -
    - <% avatar.categories.forEach(function(category) { %> -
  • <%= category %>
  • - <% }); %> -
-
-
- Contributions: <%= avatar.contributions?.length??'none' %> -
-
-
- <% }) %> -
-<% } else { %> -

No avatars found for this member.

-<% } %> \ No newline at end of file From c8e7943d6e3797735e243005853cc6b951b67a2a Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sun, 26 May 2024 23:54:23 -0400 Subject: [PATCH 29/32] 20240526 @Mookse - signup overhaul --- inc/js/functions.mjs | 69 ++++---- views/assets/css/main.css | 98 +++++++----- views/assets/html/_widget-signup.html | 217 +++++--------------------- views/assets/js/guests.mjs | 154 +++++++++++++++++- 4 files changed, 280 insertions(+), 258 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 3ac7096..de28234 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -212,57 +212,48 @@ async function privacyPolicy(ctx){ await ctx.render('privacy-policy') // privacy-policy } async function signup(ctx) { - const { email, humanName, avatarNickname } = ctx.request.body - const _signupPackage = { - 'email': email, - 'humanName': humanName, - 'avatarNickname': avatarNickname, + const { email, humanName, avatarNickname, type='newsletter', } = ctx.request.body + const signupPacket = { + avatarNickname, + email, + humanName: humanName.substring(0, 64), + type, } - // validate session signup - if (ctx.session.signup) + let success = false + if(ctx.session.signup) ctx.throw(400, 'Invalid input', { - success: false, + success, message: `session user already signed up`, - ..._signupPackage + payload: signupPacket, }) - // validate package - if (Object.values(_signupPackage).some(value => !value)) { - const _missingFields = Object.entries(_signupPackage) - .filter(([key, value]) => !value) - .map(([key]) => key) // Extract just the key - .join(',') + if(!ctx.Globals.isValidEmail(email)) ctx.throw(400, 'Invalid input', { - success: false, - message: `Missing required field(s): ${_missingFields}`, - ..._signupPackage + success, + message: 'Invalid input: email', + payload: signupPacket, }) - } - // Validate email - if (!ctx.Globals.isValidEmail(email)) + if(!humanName || humanName.length < 3) ctx.throw(400, 'Invalid input', { - success: false, - message: 'Invalid input: emailInput', - ..._signupPackage + success, + message: 'Invalid input: First name must be between 3 and 64 characters: humanNameInput', + payload: signupPacket, }) - // Validate first name and avatar name - if (!humanName || humanName.length < 3 || humanName.length > 64 || - !avatarNickname || avatarNickname.length < 3 || avatarNickname.length > 64) - ctx.throw(400, 'Invalid input', { - success: false, - message: 'Invalid input: First name and avatar name must be between 3 and 64 characters: humanNameInput,avatarNicknameInput', - ..._signupPackage + if(( avatarNickname?.length < 3 ?? true ) && type==='register') + ctx.throw(400, 'Invalid input', { + success, + message: 'Invalid input: Avatar name must be between 3 and 64 characters: avatarNicknameInput', + payload: signupPacket, }) - // save to `registration` container of Cosmos expressly for signup data - _signupPackage.id = ctx.MyLife.newGuid - await ctx.MyLife.registerCandidate(_signupPackage) - // TODO: create account and avatar - // If all validations pass and signup is successful + signupPacket.id = ctx.MyLife.newGuid + const registrationData = await ctx.MyLife.registerCandidate(signupPacket) + console.log('signupPacket:', signupPacket, registrationData) ctx.session.signup = true - const { mbr_id, ..._return } = _signupPackage // abstract out the mbr_id + success = true + const { mbr_id, ..._registrationData } = signupPacket // do not display ctx.status = 200 // OK ctx.body = { - ..._return, - success: true, + payload: _registrationData, + success, message: 'Signup successful', } } diff --git a/views/assets/css/main.css b/views/assets/css/main.css index c98e54a..3b3e99d 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -241,12 +241,9 @@ body { color: white; } /* MyLife Signup Routine */ -.bold { - font-weight: bold; -} .button { align-items: center; - background-color: #007BFF; /* Default background color */ + background-color: navy; /* Default background color */ border: 2px solid #061320; /* Blue border */ border-radius: 12px; /* Rounded corners */ box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3); /* Drop shadow */ @@ -275,7 +272,7 @@ body { .button-container { display: flex; justify-content: center; /* Centers the button horizontally */ - width: 100%; /* Ensures the container spans the full width */ + width: 100%; } .button-join { background-color: green; /* Default background color */ @@ -316,7 +313,61 @@ body { flex-wrap: wrap; justify-content: center; align-items: center; - padding: 0em 1.05em 1.05em 1.05em; + padding: 0.5rem; +} +.signup-form, +.signup-success, +.signup-teaser { + align-items: flex-start; + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: flex-start; + max-height: 100%; + padding: 0 0.15rem; +} +.signup-input { + margin-bottom: 0.5rem; + text-align: left; +} +.signup-input-container { + background: linear-gradient(to right, rgba(94, 128, 191, 0.1), rgba(128, 0, 128, 0.1)); + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px 12px; + width: 100%; + margin-top: 6px; + margin-bottom: 16px; + box-shadow: inset 1px 1px 3px rgba(94, 128, 191, 0.2), inset -1px -1px 3px rgba(128, 0, 128, 0.3); +} +.signup-input-label { + font-weight: bold; + font-style: normal; + font-stretch: condensed; +} +.signup-label { + align-items: center; + cursor: pointer; + display: flex; + gap: 0.5em; +} +.signup-type { + align-items: center; + display: flex; + gap: 1em; +} +.signup-teaser-text { + display: block; + padding-bottom: 1rem; +} +.signup-teaser-text ul { + list-style-type: square; + margin-top: 6px; + margin-bottom: 6px; +} +.signup-teaser-text li { + margin-left: 12px; + margin-top: 6px; } /* MyLife Generic Popup */ .popup-await { @@ -561,25 +612,6 @@ body { color: purple; } /* MyLife sidebar */ -.input { - margin-top: 6px; - text-align: left; -} -.input label { - font-weight: bold; - font-style: normal; - font-stretch: condensed; -} -.input-container { - background: linear-gradient(to right, rgba(94, 128, 191, 0.1), rgba(128, 0, 128, 0.1)); - border: 1px solid #ccc; - border-radius: 4px; - padding: 10px 12px; - width: 100%; - margin-top: 6px; - margin-bottom: 16px; - box-shadow: inset 1px 1px 3px rgba(94, 128, 191, 0.2), inset -1px -1px 3px rgba(128, 0, 128, 0.3); -} .sidebar { background: white; /* Assuming a card-like look typically has a white background */ background-position: center; /* Centers the image in the area */ @@ -606,22 +638,6 @@ body { margin: 0; padding: 0; } -.teaser { - display: flex; - flex-direction: column; - flex-wrap: wrap; - margin-left: 6px; - padding-bottom: 5px; -} -.teaser ul { - list-style-type: square; - margin-top: 6px; - margin-bottom: 6px; -} -.teaser li { - margin-left: 12px; - margin-top: 6px; -} .visible { opacity: 1; display: block; diff --git a/views/assets/html/_widget-signup.html b/views/assets/html/_widget-signup.html index faa2fc6..9d5f171 100644 --- a/views/assets/html/_widget-signup.html +++ b/views/assets/html/_widget-signup.html @@ -1,17 +1,13 @@ -
+
Claim Your MyLife Membership
- - -