diff --git a/inc/js/core.mjs b/inc/js/core.mjs index 8d32ab9..cd3f284 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -1,6 +1,5 @@ // imports import EventEmitter from 'events' -import chalk from 'chalk' // server-specific imports import initRouter from './routes.mjs' // define export Classes for Members and MyLife @@ -225,15 +224,13 @@ class Organization extends Member { // form=organization get values(){ return this.core.values } - get version(){ - return this.core.version ?? '0.0.17' - } get vision(){ return this.core.vision } } class MyLife extends Organization { // form=server #avatar // MyLife's private class avatar, _same_ object reference as Member Class's `#avatar` + #version = '0.0.0' // indicates error constructor(factory){ // no session presumed to exist super(factory) } @@ -365,6 +362,14 @@ class MyLife extends Organization { // form=server get being(){ return 'MyLife' } + get version(){ + return this.#version + } + set version(_version){ + if(!this.globals.isValidVersion(_version)) + throw new Error('Invalid version number') + this.#version = _version + } } /* exports */ export { diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index a298071..3e507ad 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -93,17 +93,17 @@ async function challenge(ctx){ * @param {Koa} ctx - Koa Context object */ async function chat(ctx){ - const { botId, message, role, } = ctx.request.body - ?? {} /* body nodes sent by fe */ + const { botId, itemId, message, shadowId, } = ctx.request.body ?? {} /* body nodes sent by fe */ if(!message?.length) ctx.throw(400, 'missing `message` content') const { avatar, MemberSession, } = ctx.state const { isMyLife, thread_id, } = MemberSession + let conversation if(isMyLife && !thread_id?.length){ - const conversation = await avatar.createConversation('system', undefined, botId, true) // pushes to this.#conversations in Avatar + conversation = await avatar.createConversation('system', undefined, botId, true) // pushes to this.#conversations in Avatar MemberSession.thread_id = conversation.thread_id } - const response = await avatar.chatRequest(botId, MemberSession.thread_id, message) + const response = await avatar.chatRequest(message, botId, MemberSession.thread_id, itemId, shadowId, conversation) ctx.body = response } async function collections(ctx){ @@ -234,15 +234,6 @@ async function privacyPolicy(ctx){ ctx.state.subtitle = `Effective Date: 2024-01-01` await ctx.render('privacy-policy') // privacy-policy } -async function shadow(ctx){ - const { avatar, } = ctx.state - const { active=true, botId, itemId, message, role, threadId, shadowId, title, } = ctx.request.body // necessary to flip active bot, or just presume to use the creator of the shadow? - if(!itemId?.length) - ctx.throw(400, `missing item id`) - if(!active) // @stub - redirect to normal chat? - ctx.throw(400, `shadow must be active`) - ctx.body = await avatar.shadow(shadowId, itemId, title, message) -} /** * Gets the list of shadows. * @returns {Object[]} - Array of shadow objects. @@ -370,7 +361,6 @@ export { members, passphraseReset, privacyPolicy, - shadow, shadows, signup, summarize, diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 19ac995..edbc347 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -244,6 +244,10 @@ class Globals extends EventEmitter { isValidGuid(text){ return typeof text === 'string' && mGuidRegex.test(text) } + isValidVersion(version) { + const regex = /^\d+\.\d+\.\d+$/ + return typeof version === 'string' && regex.test(version) + } stripCosmosFields(object){ return Object.fromEntries(Object.entries(object).filter(([k, v]) => !k.startsWith('_'))) } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 1bdafa7..9a8adfa 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -81,17 +81,18 @@ class Avatar extends EventEmitter { } /** * Processes and executes incoming chat request. - * @todo - cleanup/streamline frontend communication as it really gets limited to Q&A... other events would fire API calls on the same same session, so don't need to be in chat or conversation streams * @public + * @param {string} message - The chat message content. * @param {string} activeBotId - The active bot id. * @param {string} threadId - The openai thread id. - * @param {string} chatMessage - The chat message content. + * @param {Guid} itemId - The active collection-item id (optional). + * @param {Guid} shadowId - The active Shadow Id (optional). * @param {Conversation} conversation - The conversation object. * @param {number} processStartTime - The start time of the process. * @returns {object} - The response(s) to the chat request. */ - async chatRequest(activeBotId, threadId, chatMessage, conversation, processStartTime=Date.now()){ - if(!chatMessage) + async chatRequest(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime=Date.now()){ + if(!message) throw new Error('No message provided in context') if(!activeBotId) throw new Error('Parameter `activeBotId` required.') @@ -99,14 +100,21 @@ class Avatar extends EventEmitter { const { id: botId, thread_id, } = activeBot if(botId!==activeBotId) throw new Error(`Invalid bot id: ${ activeBotId }, active bot id: ${ botId }`) - // @stub - clean up conversation alterations - Q is immune conversation = conversation ?? this.getConversation(threadId ?? thread_id) ?? await this.createConversation('chat', threadId ?? thread_id, activeBotId) if(!conversation) throw new Error('No conversation found for thread id and could not be created.') conversation.bot_id = activeBot.bot_id // pass in via quickly mutating conversation (or independently if preferred in end), versus llmServices which are global - const messages = await mCallLLM(this.#llmServices, conversation, chatMessage, factory, this) + let messages + if(shadowId) + messages = await this.shadow(shadowId, itemId, message) + else { + // @stub - one weakness in teh chain might also be the fact that I am not including in instructions how to create integrated summary and left it primarily to the JSON description of function + if(itemId) + message = `update-memory-request: itemId=${ itemId }\n` + message + messages = await mCallLLM(this.#llmServices, conversation, message, factory, this) + } conversation.addMessages(messages) if(mAllowSave) conversation.save() @@ -116,12 +124,12 @@ class Avatar extends EventEmitter { const { activeBot: bot } = this // current fe will loop through messages in reverse chronological order const chat = conversation.messages - .filter(message=>{ // limit to current chat response(s); usually one, perhaps faithfully in future [or could be managed in LLM] - return messages.find(_message=>_message.id===message.id) - && message.type==='chat' - && message.role!=='user' + .filter(_message=>{ // limit to current chat response(s); usually one, perhaps faithfully in future [or could be managed in LLM] + return messages.find(__message=>__message.id===_message.id) + && _message.type==='chat' + && _message.role!=='user' }) - .map(message=>mPruneMessage(bot, message, 'chat', processStartTime)) + .map(_message=>mPruneMessage(bot, _message, 'chat', processStartTime)) return chat } /** @@ -472,14 +480,13 @@ class Avatar extends EventEmitter { return await this.#factory.resetPassphrase(passphrase) } /** - * Takes a shadow message and sends it to the appropriate bot for response. + * Takes a shadow message and sends it to the appropriate bot for response, returning the standard array of bot responses. * @param {Guid} shadowId - The shadow id. * @param {Guid} itemId - The item id. - * @param {string} title - The title of the original summary. * @param {string} message - The member (interacting with shadow) message content. - * @returns {Object} - The response object { error, itemId, messages, processingBotId, success, } + * @returns {Object[]} - The array of bot responses. */ - async shadow(shadowId, itemId, title, message){ + async shadow(shadowId, itemId, message){ const processingStartTime = Date.now() const shadows = await this.shadows() const shadow = shadows.find(shadow=>shadow.id===shadowId) @@ -515,12 +522,7 @@ class Avatar extends EventEmitter { messages = messages.map(message=>mPruneMessage(bot, message, 'shadow', processingStartTime)) if(tailgate?.length) messages.push(mPruneMessage(bot, tailgate, 'system')) - return { - itemId, - messages, - processingBotId: bot.id, - success: true, - } + return messages } /** * Gets the list of shadows. @@ -1150,25 +1152,28 @@ class Q extends Avatar { /* overloaded methods */ /** * Processes and executes incoming chat request. - * @todo - cleanup/streamline frontend communication as it really gets limited to Q&A... other events would fire API calls on the same same session, so don't need to be in chat or conversation streams * @public + * @param {string} message - The chat message content. * @param {string} activeBotId - The active bot id. * @param {string} threadId - The openai thread id. - * @param {string} chatMessage - The chat message content. - * @param {number} processStartTime - The process start time. + * @param {Guid} itemId - The active collection-item id (optional). + * @param {Guid} shadowId - The active Shadow Id (optional). + * @param {Conversation} conversation - The conversation object. + * @param {number} processStartTime - The start time of the process. * @returns {object} - The response(s) to the chat request. */ - async chatRequest(activeBotId=this.activeBotId, threadId, chatMessage, processStartTime=Date.now()){ - let conversation = this.getConversation(threadId) + async chatRequest(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime=Date.now()){ + conversation = conversation + ?? this.getConversation(threadId) if(!conversation) throw new Error('Conversation cannot be found') this.activeBot.bot_id = mBot_idOverride ?? this.activeBot.bot_id if(this.isValidating) // trigger confirmation until session (or vld) ends - chatMessage = `CONFIRM REGISTRATION PHASE: registrationId=${ this.registrationId }\n${ chatMessage }` + message = `CONFIRM REGISTRATION PHASE: registrationId=${ this.registrationId }\n${ message }` if(this.isCreatingAccount) - chatMessage = `CREATE ACCOUNT PHASE: ${ chatMessage }` - return super.chatRequest(activeBotId, threadId, chatMessage, conversation, processStartTime) + message = `CREATE ACCOUNT PHASE: ${ message }` + return super.chatRequest(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime) } upload(){ throw new Error('MyLife avatar cannot upload files.') diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 6819655..5c06b8b 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -21,7 +21,6 @@ import { members, passphraseReset, privacyPolicy, - shadow, shadows, signup, summarize, @@ -120,7 +119,6 @@ _memberRouter.post('/bots/activate/:bid', activateBot) _memberRouter.post('/category', category) _memberRouter.post('/mode', interfaceMode) _memberRouter.post('/passphrase', passphraseReset) -_memberRouter.post('/shadow', shadow) _memberRouter.post('/summarize', summarize) _memberRouter.post('/teams/:tid', team) _memberRouter.post('/upload', upload) diff --git a/sample.env b/sample.env index c91d1f8..5ae6fbc 100644 --- a/sample.env +++ b/sample.env @@ -22,6 +22,5 @@ MYLIFE_HOSTED_MBR_ID=[] # array of ids hosted on your server--OR--remove or leav MYLIFE_SESSION_KEY=0.0.15 # 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.15 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 8f8272f..a43a288 100644 --- a/server.js +++ b/server.js @@ -15,6 +15,7 @@ import chalk from 'chalk' import MyLife from './inc/js/mylife-agent-factory.mjs' // constants/variables // @todo - parse environment variables in Globals and then have them available via as values +const version = '0.0.18' const app = new Koa() const port = JSON.parse(process.env.PORT ?? '3000') const __filename = fileURLToPath(import.meta.url) @@ -22,6 +23,8 @@ const __dirname = path.dirname(__filename) const _Maht = await MyLife // Mylife is the pre-instantiated exported version of organization with very unique properties. MyLife class can protect fields that others cannot, #factory as first refactor will request if(!process.env.MYLIFE_HOSTING_KEY || process.env.MYLIFE_HOSTING_KEY !== _Maht.avatar.hosting_key) throw new Error('Invalid hosting key. Server will not start.') +_Maht.version = version +console.log(chalk.bgBlue('created-core-entity:'), _Maht.version) const MemoryStore = new session.MemoryStore() const mimeTypesToExtensions = { // Text Formats diff --git a/views/assets/css/chat.css b/views/assets/css/chat.css index be884c6..8c477b3 100644 --- a/views/assets/css/chat.css +++ b/views/assets/css/chat.css @@ -28,6 +28,43 @@ .await-button-spinner { margin-right: 0.5em; } +.chat-active-items { + align-items: center; + display: flex; + flex: 1 0 auto; + flex-direction: column; + padding-left: 3rem; + margin-bottom: 0.5rem; + max-width: 100%; +} +.chat-active-item { + background-color: #007BFF; + border: solid thin aliceblue; + border-radius: 0.6rem; + display: none; + flex: 1 0 auto; + flex-direction: row; + padding: 0.3rem; + width: 100%; +} +.chat-active-item-close { + align-self: top; + color: maroon; + cursor: pointer; + font-size: 1.25rem; + margin-left: 2rem; +} +.chat-active-item-icon { + color: #e9eb5c; + font-size: 1.25rem; + margin-right: 0.5em; +} +.chat-active-item-text { + color: aliceblue; + display: flex; + flex: 1 0 auto; + max-width: 80%; +} .chat-bubble { background-color: #3427ec; /* Assuming a dark bubble color */ border-radius: 0.8rem; @@ -97,15 +134,21 @@ } .chat-member, .chat-user { - align-items: center; + align-items: flex-start; display: flex; flex: 1 1 auto; - flex-direction: row; + flex-direction: column; flex-wrap: wrap; margin: 0.5em 0; max-height: 60%; width: 100%; } +.chat-member-container, +.chat-user-container { + display: flex; + flex-direction: row; + width: 100%; +} .chat-message-container { display: flex; position: relative; diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 66f8652..6381750 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -9,6 +9,7 @@ import { fetchSummary, hide, seedInput, + setActiveItem, show, submit, toggleMemberInput, @@ -47,7 +48,6 @@ document.addEventListener('DOMContentLoaded', async event=>{ throw new Error(`ERROR: No bots returned from server`) updatePageBots(bots) // includes p-a await setActiveBot(id, true) - console.log('bots.mjs::DOMContentLoaded()::mBots', mBots, mShadows) }) /* public functions */ /** @@ -113,6 +113,14 @@ async function fetchTeams(){ function getBot(type='personal-avatar', id){ return mBot(id ?? type) } +/** + * Get collection item by id. + * @param {Guid} id - The collection item id. + * @returns {object} - The collection item object. + */ +function getItem(id){ + /* return collection item by id */ +} /** * Refresh designated collection from server. **note**: external calls denied option to identify collectionList parameter, ergo must always be of same type. * @param {string} type - The collection type. @@ -183,6 +191,16 @@ async function setActiveBot(event, dynamic=false){ mGreeting(dynamic) decorateActiveBot(mActiveBot) } +/** + * Exposed method to allow externalities to toggle a specific item popup. + * @param {string} id - Id for HTML div element to toggle. + */ +function togglePopup(id, bForceState=null){ + const popup = document.getElementById(id) + if(!popup) + throw new Error(`No popup found for id: ${ id }`) + toggleVisibility(popup, bForceState) +} /** * Proxy to update bot-bar, bot-containers, and bot-greeting, if desired. Requirements should come from including module, here `members.mjs`. * @public @@ -376,15 +394,15 @@ function mCreateCollectionItemSummarize(type, id, name){ */ async function mMemoryShadow(event){ event.stopPropagation() - const { itemId, lastResponse, shadowId, } = this.dataset // type enum: [agent, member] + const { itemId, lastResponse, shadowId, } = this.dataset const shadow = mShadows.find(shadow=>shadow.id===shadowId) if(!shadow) return - const { categories, id, proxy='/shadow', text, type, } = shadow + const { categories, id, text, type, } = shadow // type enum: [agent, member] switch(type){ case 'agent': /* agent shadows go directly to server for answer */ addMessage(text, { role: 'member', }) - const response = await submit(text, { itemId, proxy, shadowId, }) /* proxy submission, use endpoint: /shadow */ + const response = await submit(text, { itemId, shadowId, }) /* proxy submission, use endpoint: /shadow */ const { error, errors: _errors, itemId: responseItemId, messages, processingBotId, success=false, } = response const errors = error?.length ? [error] : _errors if(!success || !messages?.length) @@ -398,9 +416,8 @@ async function mMemoryShadow(event){ addMessages(messages) // print to screen break case 'member': /* member shadows populate main chat input */ - const action = `update-memory` const seedText = text.replace(/(\.\.\.|…)\s*$/, '').trim() + ' ' - seedInput(proxy, action, itemId, shadowId, seedText, text) + seedInput(itemId, shadowId, seedText, text) break default: throw new Error(`Unimplemented shadow type: ${ type }`) @@ -1564,7 +1581,15 @@ function mTogglePopup(event){ popup.style.right = 'auto' popup.style.top = offsetY show(popup) - popup.dataset.active = 'true' + if(setActiveItem({ + id, + popup, + title, + type, + })){ + // @todo - deactivate any other popups + popup.dataset.active = 'true' + } } } /** @@ -1772,7 +1797,6 @@ function mUpdateBotContainerAddenda(botContainer){ : dataset.init // tested empty ?? 'false' /* update collection list */ - console.log('Library collection init:', dataset.init, id) const refresh = document.getElementById(`collection-refresh-${ id }`) if(dataset.init!=='true' && refresh) // first click hide(refresh) @@ -1978,7 +2002,9 @@ export { fetchTeam, fetchTeams, getBot, + getItem, refreshCollection, setActiveBot, + togglePopup, updatePageBots, } \ No newline at end of file diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index 3ec8339..6e998ce 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -226,13 +226,18 @@ class Globals { mShow(element, listenerFunction) } /** - * Toggles the visibility of an element. + * Toggles the visibility of an element with option to force state. * @param {HTMLElement} element - The element to toggle. + * @param {boolean} bForceState - The state to force the element to, defaults to `null`. * @returns {void} */ - toggleVisibility(element){ - const { classList, } = element - mIsVisible(classList) ? mHide(element) : mShow(element) + toggleVisibility(element, bForceState=null){ + if(bForceState!=null){ /* loose type equivalence intentional */ + bForceState ? mShow(element) : mHide(element) + } else { + const { classList, } = element + mIsVisible(classList) ? mHide(element) : mShow(element) + } } /** * Returns the URL parameters as an object. diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 49ac926..c3319c2 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -9,8 +9,10 @@ import { } from './experience.mjs' import { activeBot, + getItem, refreshCollection, setActiveBot as _setActiveBot, + togglePopup, } from './bots.mjs' import Globals from './globals.mjs' /* variables */ @@ -27,6 +29,7 @@ let mAutoplay=false, let activeCategory, awaitButton, botBar, + chatActiveItem, chatContainer, chatInput, chatInputField, @@ -43,6 +46,7 @@ document.addEventListener('DOMContentLoaded', async event=>{ /* post-DOM population constants */ awaitButton = document.getElementById('await-button') botBar = document.getElementById('bot-bar') + chatActiveItem = document.getElementById('chat-active-item') chatContainer = document.getElementById('chat-container') chatInput = document.getElementById('chat-member') chatInputField = document.getElementById('chat-member-input') @@ -145,6 +149,18 @@ async function fetchSummary(fileId, fileName){ throw new Error('fetchSummary::Error()::`fileId` or `fileName` is required') return await mFetchSummary(fileId, fileName) } +function getActiveItem(){ + +} +/** + * Gets the active chat item id to send to server. + * @requires chatActiveItem + * @returns {Guid} - The return is the active item ID. + */ +function getActiveItemId(){ + const id = chatActiveItem.dataset?.id?.split('_')?.pop() + return id +} function getInputValue(){ return chatInputField.value.trim() } @@ -234,6 +250,38 @@ function replaceElement(element, newType, retainValue=true, onEvent, listenerFun async function setActiveBot(){ return await _setActiveBot(...arguments) } +/** + * Sets the active item, ex. `memory`, `entry`, `story` in the chat system for member operation(s). + * @public + * @todo - edit title with double-click + * @requires chatActiveItem + * @param {object} item - The item to set as active. + * @property {string} item.id - The item id. + * @property {HTMLDivElement} item.popup - The associated popup HTML object. + * @property {string} item.title - The item title. + * @property {string} item.type - The item type. + * @returns {void} + */ +function setActiveItem(item){ + const { id, popup, title, type, } = item + const itemId = id?.split('_')?.pop() + if(!itemId) + throw new Error('setActiveItem::Error()::valid `id` is required') + const chatActiveItemTitleText = document.getElementById('chat-active-item-text') + const chatActiveItemClose = document.getElementById('chat-active-item-close') + if(chatActiveItemTitleText){ + chatActiveItemTitleText.innerHTML = `Active: ${ title }` + chatActiveItemTitleText.dataset.popupId = popup.id + chatActiveItemTitleText.dataset.title = title + chatActiveItemTitleText.addEventListener('click', mToggleItemPopup) + // @stub - edit title with double-click? + } + if(chatActiveItemClose) + chatActiveItemClose.addEventListener('click', unsetActiveItem, { once: true }) + chatActiveItem.dataset.id = id + chatActiveItem.dataset.itemId = itemId + show(chatActiveItem) +} /** * Proxy for Globals.show(). * @public @@ -282,6 +330,25 @@ function stageTransition(experienceId){ function toggleVisibility(){ mGlobals.toggleVisibility(...arguments) } +/** + * Unsets the active item in the chat system. + * @public + * @requires chatActiveItem + * @returns {void} + */ +function unsetActiveItem(){ + const chatActiveItemTitleText = document.getElementById('chat-active-item-text') + const chatActiveItemClose = document.getElementById('chat-active-item-close') + if(chatActiveItemTitleText){ + chatActiveItemTitleText.innerHTML = '' + chatActiveItemTitleText.dataset.popupId = null + chatActiveItemTitleText.dataset.title = null + chatActiveItemTitleText.removeEventListener('click', mToggleItemPopup) + } + delete chatActiveItem.dataset.id + delete chatActiveItem.dataset.itemId + hide(chatActiveItem) +} /** * Waits for user action. * @public @@ -351,7 +418,7 @@ async function mAddMemberMessage(event){ typeDelay: 1, }) }) - toggleMemberInput(true)/* show */ + toggleMemberInput(true) /* show */ } /** * Adds specified string message to interface. @@ -493,22 +560,19 @@ function mInitializePageListeners(){ } /** * Primitive step to set a "modality" or intercession for the member chat. Currently will key off dataset in `chatInputField`. - * @todo - mature this architecture - * @param {string} proxy - The proxy endpoint for this chat exchange. - * @param {string} action - The action to take on the proxy endpoint. - * @param {Guid} itemId - The item ID as context for chat exchange. - * @param {Guid} shadowId - The shadow ID as context for chat exchange. - * @param {string} value - The value to seed the input with. - * @param {string} placeholder - The placeholder to seed the input with. - */ -function seedInput(proxy, action, itemId, shadowId, value, placeholder){ - chatInputField.dataset.action = action - chatInputField.dataset.active = 'true' - chatInputField.dataset.itemId = itemId - chatInputField.dataset.proxy = proxy - chatInputField.dataset.shadowId = shadowId + * @public + * @requires chatActiveItem + * @requires chatInputField + * @param {Guid} itemId - The Active Item ID + * @param {Guid} shadowId - The shadow ID + * @param {string} value - The value to seed the input with + * @param {string} placeholder - The placeholder to seed the input with (optional) + */ +function seedInput(itemId, shadowId, value, placeholder){ + chatActiveItem.dataset.itemId = itemId + chatActiveItem.dataset.shadowId = shadowId chatInputField.value = value - chatInputField.placeholder = placeholder + chatInputField.placeholder = placeholder ?? chatInputField.placeholder chatInputField.focus() } /** @@ -577,27 +641,24 @@ function mStageTransitionMember(includeSidebar=true){ } /** * Submits a message to MyLife Member Services chat. + * @requires chatActiveItem * @param {string} message - The message to submit. - * @param {object} proxyInfo - The proxy information { itemId, proxy, shadowId }. * @param {boolean} hideMemberChat - The hide member chat flag, default=`true`. * @returns */ -async function submit(message, proxyInfo, hideMemberChat=true){ +async function submit(message, hideMemberChat=true){ if(!message?.length) throw new Error('submit(): `message` argument is required') - const { action, active, itemId, proxy='', shadowId, } = proxyInfo - ?? chatInputField.dataset - const url = window.location.origin + '/members' + proxy - const { id: botId, thread_id: threadId, } = activeBot() + const { action, itemId, shadowId, } = chatActiveItem.dataset + const url = window.location.origin + '/members' + const { id: botId, } = activeBot() const request = { action, - active, botId, itemId, message, role: 'member', shadowId, - threadId, } const options = { method: 'POST', @@ -609,10 +670,8 @@ async function submit(message, proxyInfo, hideMemberChat=true){ if(hideMemberChat) toggleMemberInput(false) const chatResponse = await submitChat(url, options) - /* clear dataset, proxy only request level */ - for(let key in chatInputField.dataset){ // nuclear erasure - delete chatInputField.dataset[key] - } + /* clear dataset */ + delete chatActiveItem.dataset.shadowId // shadow one-run only if(hideMemberChat) toggleMemberInput(true) return chatResponse @@ -665,6 +724,11 @@ function toggleInputTextarea(event){ chatInputField.style.height = chatInputField.scrollHeight + 'px' // Set height based on content toggleSubmitButtonState() } +function mToggleItemPopup(event){ + event.stopPropagation() + event.preventDefault() + togglePopup(event.target.dataset.popupId, true) +} function toggleSubmitButtonState() { memberSubmit.disabled = !(chatInputField.value?.trim()?.length ?? true) } @@ -702,6 +766,7 @@ export { escapeHtml, expunge, fetchSummary, + getActiveItemId, getInputValue, getSystemChat, mGlobals as globals, @@ -712,6 +777,7 @@ export { sceneTransition, seedInput, setActiveBot, + setActiveItem, show, showMemberChat, showSidebar, diff --git a/views/members.html b/views/members.html index c18afbe..115b46f 100644 --- a/views/members.html +++ b/views/members.html @@ -4,16 +4,27 @@
-
-
- +
+
+ +
+
+
+
-
- +
+
+
+
+ +
+
+ +
+ +
- -