diff --git a/inc/js/agents/system/asset-agent.mjs b/inc/js/agents/system/asset-agent.mjs index ec15956f..1d142795 100644 --- a/inc/js/agents/system/asset-agent.mjs +++ b/inc/js/agents/system/asset-agent.mjs @@ -63,7 +63,6 @@ class AssetAgent { uploadFiles.push(this.#extractFile(file)) }) if(uploadFiles.length){ // only upload new files - console.log('upload::uploadFiles', uploadFiles) const fileStreams = uploadFiles.map(file=>fs.createReadStream(file.filepath)) const dataRecord = await this.#llm.upload(this.#vectorstoreId, fileStreams, this.mbr_id) const { response, success } = dataRecord diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 7a6e8d45..a41555a3 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -36,7 +36,6 @@ class Bot { #llm #type constructor(botData, llm, factory){ - console.log(`bot pre-created`, this.feedback) this.#factory = factory this.#llm = llm const { feedback=[], greeting=mDefaultGreeting, greetings=mDefaultGreetings, type=mDefaultBotType, ..._botData } = botData @@ -141,7 +140,6 @@ class Bot { const { bot_id: _llm_id, id, type, } = this let { llm_id=_llm_id, thread_id, } = this // @stub - deprecate bot_id this.#conversation = await mConversationStart('chat', type, id, thread_id, llm_id, this.#llm, this.#factory, message) - console.log(`getConversation::thread_id`, thread_id, this.#conversation.thread_id) if(!thread_id?.length){ thread_id = this.#conversation.thread_id this.update({ @@ -1042,7 +1040,6 @@ async function mConversationDelete(Conversation, factory, llm){ }) await factory.deleteItem(Conversation.id) /* delete conversation from Cosmos */ await llm.deleteThread(thread_id) /* delete thread from LLM */ - console.log('mDeleteConversation', Conversation.id, thread_id) return true } /** diff --git a/inc/js/agents/system/evolution-agent.mjs b/inc/js/agents/system/evolution-agent.mjs index be7b623a..4a4b1f3a 100644 --- a/inc/js/agents/system/evolution-agent.mjs +++ b/inc/js/agents/system/evolution-agent.mjs @@ -82,13 +82,6 @@ export class EvolutionAgent extends EventEmitter { get contributions() { return this.#contributions } - /** - * Get the factory object. - * @returns {AgentFactory} The avatar's factory object. - */ - get factory() { - return this.#avatar.factory - } /** * Get the owning member id. * @returns {string} The avatar member-owner id. @@ -253,37 +246,6 @@ function mFormatCategory(_category) { .trimStart() .slice(0, 64) } -/** - * Digest a request to generate a new Contribution. - * @module - * @emits {on-contribution-new} - Emitted when a new Contribution is generated. - * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. - * @param {string} _category - The category to process. - * @param {string} _phase - The phase to process. - * @returns {Contribution} A new Contribution object. -*/ -async function mGetContribution(evoAgent, _category, _phase) { - const _avatar = evoAgent.avatar - _category = mFormatCategory(_category) - // Process question and map to `new Contribution` class - const _contribution = new (_avatar.factory.contribution)({ - avatar_id: _avatar.id, - context: `I am a contribution object in MyLife, comprising data and functionality around a data evolution request to my associated avatar [${_avatar.id}]`, -// id: _avatar.factory.newGuid, - mbr_id: _avatar.mbr_id, // Contributions are system objects - phase: _phase, - purpose: `Contribute to the data evolution of underlying avatar for category [${_category}]`, - request: { - category: _category, - content: _avatar?.[_category]??false, - impersonation: _avatar.being, - phase: _phase, - }, - responses: [], - }) - mAssignContributionListeners(evoAgent, _contribution) - return await _contribution.init(_avatar.factory) // fires emitters -} /** * Log an object to the console and emit it to the parent. * @module @@ -318,7 +280,6 @@ function mSetContribution(evoAgent, _current, _proposed) { /* @todo: verify that categories are changing */ const _currentContribution = evoAgent.contributions .find(_contribution => _contribution.id === _current.contributionId) - console.log('evolution-assistant:mSetContribution():320', _currentContribution.inspect(true)) if(_currentContribution.stage === 'prepared'){ // ready to process // join array and submit for gpt-summarization mSubmitContribution(evoAgent, _contributions.responses.join('\n')) diff --git a/inc/js/api-functions.mjs b/inc/js/api-functions.mjs index 2bc235b4..ca16dd43 100644 --- a/inc/js/api-functions.mjs +++ b/inc/js/api-functions.mjs @@ -25,14 +25,12 @@ async function entry(ctx){ const { assistantType, mbr_id } = ctx.state if(!ctx.request.body?.summary?.length) throw new Error('No entry summary provided. Use `summary` field.') - console.log(chalk.yellowBright('entry()::entry attempted:'), ctx.request.body) const summary = { ...ctx.request.body, assistantType, mbr_id, } const entry = await ctx.MyLife.entry(summary) - console.log(chalk.yellowBright('entry()::entry submitted:'), entry, summary) ctx.status = 200 ctx.body = { id: entry.id, @@ -48,7 +46,6 @@ async function experienceBuilder(ctx){ const { assistantType, mbr_id } = ctx.state const { eid, sid } = ctx.params const { experience } = ctx.request.body?.experience - console.log(chalk.yellowBright('experienceBuilder()'), { assistantType, mbr_id, eid, sid, experience }) if(!experience) ctx.throw(400, 'No experience provided for builder. Use `experience` field.') } @@ -231,14 +228,12 @@ async function memory(ctx){ const { assistantType, mbr_id } = ctx.state if(!ctx.request.body?.summary?.length) throw new Error('No memory summary provided. Use `summary` field.') - console.log(chalk.yellowBright('memory()::memory attempted:'), ctx.request.body) const summary = { ...ctx.request.body, assistantType, mbr_id, } const memory = await ctx.MyLife.memory(summary) - console.log(chalk.yellowBright('memory()::memory submitted:'), memory, summary) ctx.status = 200 ctx.body = { id: memory.id, diff --git a/inc/js/core.mjs b/inc/js/core.mjs index be67b867..0b454f35 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -19,7 +19,7 @@ class Member extends EventEmitter { */ async init(avatar){ this.#avatar = avatar - ?? await this.factory.getAvatar() + ?? await this.#factory.getAvatar() return this } // getter/setter functions @@ -56,7 +56,7 @@ class Member extends EventEmitter { } set avatar(_Avatar){ // oops, hack around how to get dna of avatar class; review options [could block at factory-getter level, most efficient and logical] - if(!this.factory.isAvatar(_Avatar)) + if(!this.#factory.isAvatar(_Avatar)) throw new Error('avatar requires Avatar Class') this.#avatar = _Avatar } @@ -80,19 +80,19 @@ class Member extends EventEmitter { return this.agent.chat } get consent(){ - return this.factory.consent // **caution**: returns <> + return this.#factory.consent // **caution**: returns <> } set consent(_consent){ - this.factory.consents.unshift(_consent.id) + this.#factory.consents.unshift(_consent.id) } get core(){ - return this.factory.core + return this.#factory.core } get dataservice(){ return this.dataservices } get dataservices(){ - return this.factory.dataservices + return this.#factory.dataservices } get description(){ return this.core.description @@ -110,7 +110,7 @@ class Member extends EventEmitter { return this.core.form } get globals(){ - return this.factory.globals + return this.#factory.globals } get hobbies(){ return this.core.hobbies @@ -122,7 +122,7 @@ class Member extends EventEmitter { return this.sysid } get mbr_id(){ - return this.factory.mbr_id + return this.#factory.mbr_id } get member(){ return this.core @@ -139,6 +139,9 @@ class Member extends EventEmitter { get preferences(){ return this.core.preferences } + get schemas(){ + return this.#factory.schemas + } get skills(){ return this.core.skills } @@ -155,15 +158,15 @@ class Member extends EventEmitter { async testEmitters(){ // test emitters with callbacks this.emit('testEmitter',_response=>{ - console.log('callback emitters enabled:',_response) + }) } } class Organization extends Member { // form=organization #Menu #Router - constructor(_Factory){ - super(_Factory) + constructor(Factory){ + super(Factory) } /* public functions */ async init(avatar){ @@ -190,7 +193,7 @@ class Organization extends Member { // form=organization } get menu(){ if(!this.#Menu){ - this.#Menu = new (this.factory.schemas.menu)(this).menu + this.#Menu = new (this.schemas.menu)(this).menu } return this.#Menu } @@ -211,7 +214,7 @@ class Organization extends Member { // form=organization } get router(){ if(!this.#Router){ - this.#Router = initRouter(new (this.factory.schemas.menu)(this)) + this.#Router = initRouter(new (this.schemas.menu)(this)) } return this.#Router } @@ -233,12 +236,14 @@ class Organization extends Member { // form=organization } class MyLife extends Organization { // form=server #avatar // MyLife's private class avatar, _same_ object reference as Member Class's `#avatar` + #factory #version = '0.0.0' // indicates error - constructor(factory){ // no session presumed to exist - super(factory) + constructor(Factory){ // no session presumed to exist + super(Factory) + this.#factory = Factory } async init(){ - this.#avatar = await this.factory.getAvatar() + this.#avatar = await this.#factory.getAvatar() return await super.init(this.#avatar) } /* public functions */ @@ -247,7 +252,7 @@ class MyLife extends Organization { // form=server * @returns {Object[]} - An array of the currently available public experiences. */ async availableExperiences(){ - const experiences = ( await this.factory.availableExperiences() ) + const experiences = ( await this.#factory.availableExperiences() ) .map(experience=>{ // map to display versions [from `mylife-avatar.mjs`] const { autoplay=false, description, id, name, purpose, skippable=true, } = experience return { @@ -277,7 +282,7 @@ class MyLife extends Organization { // form=server async datacore(mbr_id){ if(!mbr_id || mbr_id===this.mbr_id) throw new Error('datacore cannot be accessed') - const core = this.globals.sanitize(await this.factory.datacore(mbr_id)) + const core = this.globals.sanitize(await this.#factory.datacore(mbr_id)) return core } /** @@ -302,10 +307,10 @@ class MyLife extends Organization { // form=server * @returns {void} returns nothing, performs operation */ getAlerts(){ - this.factory.getAlerts() + this.#factory.getAlerts() } async getMyLifeSession(){ - return await this.factory.getMyLifeSession() + return await this.#factory.getMyLifeSession() } async hostedMemberList(){ let members = await this.hostedMembers() @@ -317,7 +322,7 @@ class MyLife extends Organization { // form=server * @returns {Promise} - Array of string ids, one for each hosted member. */ async hostedMembers(validations){ - return await this.factory.hostedMembers(validations) + return await this.#factory.hostedMembers(validations) } /** * Returns whether a specified member id is hosted on this instance. @@ -330,7 +335,6 @@ class MyLife extends Organization { // form=server let isValidated = false if(isHosted) isValidated = await this.testPartitionKey(memberId) - console.log('isMemberHosted:', isHosted, isValidated, memberId) return isValidated } /** @@ -339,7 +343,7 @@ class MyLife extends Organization { // form=server * @param {object} candidate { 'avatarName': string, 'email': string, 'humanName': string, } */ async registerCandidate(candidate){ - return await this.factory.registerCandidate(candidate) + return await this.#factory.registerCandidate(candidate) } /** * Submits and returns the memory to MyLife via API. @@ -378,7 +382,7 @@ class MyLife extends Organization { // form=server mbr_id, name: `${ being }_${ title.substring(0,64) }_${ mbr_id }`, } - const savedStory = this.globals.sanitize(await this.factory.summary(story)) + const savedStory = this.globals.sanitize(await this.#factory.summary(story)) return savedStory } /** @@ -388,7 +392,7 @@ class MyLife extends Organization { // form=server * @returns {boolean} returns true if partition key is valid */ async testPartitionKey(_mbr_id){ - return await this.factory.testPartitionKey(_mbr_id) + return await this.#factory.testPartitionKey(_mbr_id) } /* getters/setters */ /** diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index a40b2f75..1f3319e6 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -432,7 +432,6 @@ function extendClass_message(originClass, referencesObject) { try{ this.#content = assignContent(content ?? obj) } catch(e){ - console.log('Message::constructor::ERROR', e) this.#content = '' } } @@ -443,9 +442,7 @@ function extendClass_message(originClass, referencesObject) { set content(_content){ try{ this.#content = assignContent(_content) - } catch(e){ - console.log('Message::content::ERROR', e) - } + } catch(e){} } get message(){ return this diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index da5f1c1d..2c07904d 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -1,11 +1,30 @@ /* imports */ +import fs from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' import { upload as apiUpload, } from './api-functions.mjs' +/* variables */ +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) /* module export functions */ +/** + * Renders the about page for the application. Visitors see the rendered page, members see the page as responses from their Avatar. + * @param {Koa} ctx - Koa Context object + * @returns {object|void} - Renders page in place (visitor) or Koa Context object (member) + */ async function about(ctx){ - ctx.state.title = `About MyLife` - await ctx.render('about') + if(ctx.state.locked){ + ctx.state.title = `About MyLife` + await ctx.render('about') + } else { + const { avatar: Avatar, } = ctx.state + const aboutFilePath = path.resolve(__dirname, '../..', 'views/about.html') + const html = await fs.readFile(aboutFilePath, 'utf-8') + const response = await Avatar.renderContent(html) + ctx.body = response + } } /** * Activate a specific Bot. diff --git a/inc/js/menu.mjs b/inc/js/menu.mjs index ea82cce2..f7271a1e 100644 --- a/inc/js/menu.mjs +++ b/inc/js/menu.mjs @@ -1,14 +1,14 @@ class Menu { #menu - constructor(_Agent){ - this.#menu = this.#setMenu(_Agent) + constructor(Avatar){ + this.#setMenu() } get menu(){ return this.#menu } - #setMenu(_Agent){ - return [ - { display: `About`, route: '/about', icon: 'about', }, + #setMenu(){ + this.#menu = [ + { display: `About`, icon: 'about', memberClick: 'about()', memberRoute: 'javascript:void(0)', route: '/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', }, ] diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index d8f5406f..f1ac5c24 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -12,7 +12,6 @@ const mAllowSave = JSON.parse( ?? 'false' ) const mAvailableModes = ['standard', 'admin', 'evolution', 'experience', 'restoration'] -const mMigrateThreadOnVersionChange = false // hack currently to avoid thread migration on bot version change when it's not required, theoretically should be managed by Bot * Version /** * @class - Avatar * @extends EventEmitter @@ -95,22 +94,23 @@ class Avatar extends EventEmitter { throw new Error('No message provided in context') const originalMessage = message this.backupResponse = { - message: `I received your request to chat, and sent the request to the central intelligence, but no response was received. Please try again, as the issue is likely aberrant.`, + message: `I got your message, but I'm having trouble processing it. Please try again.`, type: 'system', } /* execute request */ if(this.globals.isValidGuid(itemId)){ - // @todo - check if item exists in memory, fewer pings and inclusions overall let { summary, } = await this.#factory.item(itemId) if(summary?.length) - message = `possible **update-summary-request**: itemId=${ itemId }\n` - + `**member-update-request**:\n` + message = `**active-item**: itemId=${ itemId }\n` + + `**member-input**:\n` + message - + `\n**current-summary-in-database**:\n` + + `\n**newest-summary**:\n` + summary } const Conversation = await this.activeBot.chat(message, originalMessage, mAllowSave, this) const responses = mPruneMessages(this.activeBotId, Conversation.getMessages() ?? [], 'chat', Conversation.processStartTime) + if(!responses.length) + responses.push(this.backupResponse) /* respond request */ const response = { instruction: this.frontendInstruction, @@ -158,7 +158,6 @@ class Avatar extends EventEmitter { switch(type){ case 'entry': case 'memory': - case 'story': return mPruneItem(item) case 'experience': case 'lived-experience': @@ -180,6 +179,8 @@ class Avatar extends EventEmitter { title, variables, } + case 'story': + throw new Error('Story collection not yet implemented.') default: return item } @@ -216,8 +217,23 @@ class Avatar extends EventEmitter { * @returns {void} */ async endMemory(){ - // @stub - save conversation fragments */ + const { Conversation, id, item, } = this.#livingMemory + const { bot_id, } = Conversation + if(mAllowSave) + await Conversation.save() + const instruction = { + command: `endMemory`, + itemId: item.id, + livingMemoryId: id, + } + const responses = [mCreateSystemMessage(bot_id, `I've ended the memory, thank you for letting me share my interpretation. I hope you liked it.`, this.#factory.message)] + const response = { + instruction, + responses, + success: true, + } this.#livingMemory = null + return response } /** * Submits a new diary or journal entry to MyLife. Currently called both from API _and_ LLM function. @@ -250,7 +266,7 @@ class Avatar extends EventEmitter { * @returns {void} - Throws error if experience cannot be ended. */ experienceEnd(experienceId){ - const { experience, factory, mode, } = this + const { experience, mode, } = this try { if(this.isMyLife) // @stub - allow guest experiences throw new Error(`MyLife avatar can neither conduct nor end experiences`) @@ -264,13 +280,13 @@ class Avatar extends EventEmitter { } this.mode = 'standard' const { id, location, title, variables, } = experience - const { mbr_id, newGuid, } = this.#factory + const { mbr_id, } = this.#factory const completed = location?.completed this.#livedExperiences.push({ // experience considered concluded for session regardless of origin, sniffed below completed, experience_date: Date.now(), experience_id: id, - id: newGuid, + id: this.newGuid, mbr_id, title, variables, @@ -278,7 +294,7 @@ class Avatar extends EventEmitter { if(completed){ // ended "naturally," by event completion, internal initiation /* validate and cure `experience` */ /* save experience to cosmos (no await) */ - factory.saveExperience(experience) + this.#factory.saveExperience(experience) } else { // incomplete, force-ended by member, external initiation // @stub - create case for member ending with enough interaction to _consider_ complete, or for that matter, to consider _started_ in some cases } @@ -437,7 +453,7 @@ class Avatar extends EventEmitter { * @param {String} method - The http method used to indicate response * @returns {Promise} - Returns { instruction, item, responses, success, } */ - async item(item, method){ + async item(item, method='get'){ const { globals, mbr_id, } = this const response = { item, success: false, } const instruction={}, @@ -446,7 +462,7 @@ class Avatar extends EventEmitter { message: `I encountered an error while trying to process your request; please try again.`, type: 'system', } - let { id: itemId, title, } = item + let { id: itemId, summary, title, } = item let success = false if(itemId && !globals.isValidGuid(itemId)) throw new Error(`Invalid item id: ${ itemId }`) @@ -459,6 +475,7 @@ class Avatar extends EventEmitter { instruction.command = success ? 'removeItem' : 'error' + instruction.itemId = itemId break case 'post': /* create */ /* validate request */ @@ -482,7 +499,7 @@ class Avatar extends EventEmitter { ...{ assistantType, being, - id: this.#factory.newGuid, + id: this.newGuid, mbr_id, name: `${ type }_${ form }_${ title.substring(0,64) }_${ mbr_id }`, summary, @@ -508,15 +525,22 @@ class Avatar extends EventEmitter { break case 'put': /* update */ const updatedItem = await this.#factory.updateItem(item) - success = this.globals.isValidGuid(updatedItem?.id) const updatedTitle = updatedItem?.title ?? title - message.message = success - ? `I have successfully updated: "${ updatedTitle }".` - : `I encountered an error while trying to update: "${ updatedTitle }".` + success = this.globals.isValidGuid(updatedItem?.id) + if(success){ + instruction.command = 'updateItem' + instruction.item = mPruneItem(updatedItem) + message.message = `I have successfully updated: "${ updatedTitle }".` + response.item = mPruneItem(updatedItem) + } else + message.message = `I encountered an error while trying to update: "${ updatedTitle }".` break default: - console.log('item()::default', item) + const retrievedItem = await this.#factory.item(itemId) + success = !!retrievedItem + if(success) + response.item = mPruneItem(retrievedItem) break } this.frontendInstruction = instruction // LLM-return safe @@ -601,8 +625,25 @@ class Avatar extends EventEmitter { const { id, } = item if(!id) throw new Error(`item does not exist in member container: ${ iid }`) - const narration = await mReliveMemoryNarration(item, memberInput, this.#botAgent, this) - return narration + const response = await mReliveMemoryNarration(item, memberInput, this.#botAgent, this) + return response + } + async renderContent(html){ + const processStartTime = Date.now() + const sectionRegex = /]*>([\s\S]*?)<\/section>/gi + const responses = [] + let match + while((match = sectionRegex.exec(html))!==null){ + const sectionContent = match[1].trim() + if(!sectionContent?.length) + break + const Message = mPruneMessage(this.avatar.id, sectionContent, 'chat', processStartTime) + responses.push(Message) + } + return { + responses, + success: true, + } } /** * Allows member to reset passphrase. @@ -622,40 +663,12 @@ class Avatar extends EventEmitter { * @returns {object} - The Response object: { instruction, responses, success, } */ async retireBot(bot_id){ - const success = await this.#botAgent.deleteBot(bot_id) - const response = { - instruction: { - command: success ? 'removeBot' : 'error', - id: bot_id, - }, - responses: [success - ? { - agent: 'server', - message: `I have removed this bot from the team.`, - type: 'chat', - } - : { - agent: 'server', - message: `I'm sorry - I encountered an error while trying to retire this bot; please try again.`, - type: 'system', - } - ], - success, - } - return response - } - /** - * Retire a Bot, deleting altogether. - * @param {Guid} bot_id - The bot id - * @returns {object} - The response object { instruction, responses, success, } - */ - async retireBot(bot_id){ - if(!this.globals.isValidGuid(bot_id)) - throw new Error(`Invalid bot id: ${ bot_id }`) const success = await this.#botAgent.botDelete(bot_id) const response = { instruction: { - command: success ? 'retireBot' : 'error', + command: success + ? 'removeBot' + : 'error', id: bot_id, }, responses: [success @@ -672,6 +685,8 @@ class Avatar extends EventEmitter { ], success, } + if(!success) + instruction.error = 'I encountered an error while trying to retire this bot; please try again.' return response } /** @@ -749,8 +764,7 @@ class Avatar extends EventEmitter { */ async summarize(fileId, fileName, processStartTime=Date.now()){ /* validate request */ - let instruction, - responses = [], + let responses = [], success = false this.backupResponse = { message: `I received your request to summarize, but an error occurred in the process. Perhaps try again with another file.`, @@ -762,15 +776,10 @@ class Avatar extends EventEmitter { if(!responses?.length) responses.push(this.backupResponse) else { - instruction = { - command: 'updateFileSummary', - itemId: fileId, - } responses = mPruneMessages(this.avatar.id, responses, 'mylife-file-summary', processStartTime) success = true } return { - instruction, responses, success, } @@ -811,7 +820,7 @@ class Avatar extends EventEmitter { * @returns {object} - The updated bot object */ async updateBotInstructions(bot_id=this.activeBot.id){ - const Bot = await this.#botAgent.updateBotInstructions(bot_id, mMigrateThreadOnVersionChange) + const Bot = await this.#botAgent.updateBotInstructions(bot_id) return Bot.bot } /** @@ -1008,15 +1017,6 @@ class Avatar extends EventEmitter { throw new Error('Experiences lived must be an array.') this.#livedExperiences = livedExperiences } - /** - * Get the Avatar's Factory. - * @todo - deprecate if possible, return to private - * @getter - * @returns {AgentFactory} - The Avatar's Factory. - */ - get factory(){ - return this.#factory - } /** * Globals shortcut. * @getter @@ -1201,6 +1201,14 @@ class Avatar extends EventEmitter { get navigation(){ return this.experience.navigation } + /** + * Creates a new guid via `this.#factory`. + * @getter + * @returns {Guid} - The new guid + */ + get newGuid(){ + return this.#factory.newGuid + } /** * Get the nickname of the avatar. * @getter @@ -1231,7 +1239,7 @@ class Avatar extends EventEmitter { /** * Set the `active` reliving memory. * @setter - * @param {Object} livingMemory - The new active reliving memory + * @param {Object} livingMemory - The new active reliving memory (or `null`) * @returns {void} */ set livingMemory(livingMemory){ @@ -1436,7 +1444,6 @@ class Q extends Avatar { if(!this.globals.isValidGuid(key) || key!==this.hosting_key) throw new Error('Invalid key for hosted members.') if(!this.#hostedMembers.length){ // on-demand creation - console.log('hostedMembers', this.#hostedMembers) const hostedMembers = await this.#factory.hostedMembers() if(!hostedMembers.length) throw new Error('No hosted members found.') @@ -1563,7 +1570,7 @@ async function mCast(factory, cast){ * Creates frontend system message from message String/Object. * @param {Guid} bot_id - The bot id * @param {String|Message} message - The message to be pruned - * @param {*} factory + * @param {messageClassDefinition} messageClassDefinition - The message class definition * @returns */ function mCreateSystemMessage(bot_id, message, messageClassDefinition){ @@ -2012,7 +2019,6 @@ async function mExperienceStart(avatar, factory, experienceId, avatarExperienceV if(id!==experienceId) throw new Error('Experience failure, unexpected id mismatch.') experience.cast = await mCast(factory, experience.cast) // hydrates cast data - console.log('mExperienceStart::experience', experience.cast[0].inspect(true)) experience.events = [] experience.location = { experienceId: experience.id, @@ -2147,6 +2153,7 @@ function mPruneItem(item){ relationships, summary, title, + type, } = item item = { assistantType, @@ -2159,6 +2166,7 @@ function mPruneItem(item){ relationships, summary, title, + type, } return item } @@ -2209,8 +2217,6 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. * @returns {Object[]} - Concatenated message object */ function mPruneMessages(bot_id, messageArray, type='chat', processStartTime=Date.now()){ - if(!messageArray.length) - throw new Error('No messages to prune') messageArray = messageArray .map(message=>mPruneMessage(bot_id, message, type, processStartTime)) return messageArray @@ -2230,11 +2236,25 @@ function mPruneMessages(bot_id, messageArray, type='chat', processStartTime=Date */ async function mReliveMemoryNarration(item, memberInput, BotAgent, Avatar){ Avatar.livingMemory = await BotAgent.liveMemory(item, memberInput, Avatar) - const { Conversation, } = Avatar.livingMemory + const { Conversation, item: livingMemoryItem, } = Avatar.livingMemory const { bot_id, type, } = Conversation + const endpoint = `/members/memory/end/${ livingMemoryItem.id }` + const instruction = { + command: 'createInput', + inputs: [{ + endpoint, + id: Avatar.newGuid, + interfaceLocation: 'chat', // enum: ['avatar', 'team', 'chat', 'bot', 'experience', 'system', 'admin'], defaults to chat + method: 'PATCH', + prompt: `I'd like to stop reliving this memory.`, + required: true, + type: 'button', + }], + } const responses = Conversation.getMessages() .map(message=>mPruneMessage(bot_id, message, type)) const memory = { + instruction, item, responses, success: true, @@ -2290,7 +2310,6 @@ function mValidateMode(_requestedMode, _currentMode){ throw new Error('Invalid interface mode request. Mode not altered.') switch(_requestedMode){ case 'admin': - console.log('Admin interface not currently implemented. Mode not altered.') return _currentMode case 'experience': case 'standard': diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index aad75896..b6a08ba5 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -44,7 +44,6 @@ class Datamanager { this.#partitionId ) .read() - console.log(chalk.yellowBright('database, container, core initialized:',chalk.bgYellowBright(`${this.#containers['members'].id} :: ${this.database.id} :: ${this.#core.resource.id}`) )) return this } /* public functions */ diff --git a/inc/js/mylife-dataservices.mjs b/inc/js/mylife-dataservices.mjs index caa7e856..dcca76ed 100644 --- a/inc/js/mylife-dataservices.mjs +++ b/inc/js/mylife-dataservices.mjs @@ -118,7 +118,6 @@ class Dataservices { 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){ - console.log(`core already exists for ${mbr_id} with id ${id}`) // `core` already exists return { core: extantCore, success: false, } // no alterations, failure } core = { // enforce core data structure @@ -217,7 +216,10 @@ class Dataservices { * @returns {array} - The journal entry items. */ async collectionEntries(){ - return await this.getItems('entry') + return await this.getItemsByFields( + 'story', + [{ name: '@type', value: 'entry' }], + ) } /** * Proxy to retrieve lived experiences. @@ -234,49 +236,64 @@ class Dataservices { return await this.getItems('file') } /** - * Proxy to retrieve biographical story items. - * @returns {array} - The biographical story items. + * Proxy to retrieve biographical items. + * @returns {array} - The biographical items + */ + async collectionMemories(){ + return await this.getItemsByFields( + 'story', + [{ name: '@type', value: 'memory' }], + ) + } + /** + * Proxy to retrieve all story items. + * @returns {array} - The story items */ async collectionStories(){ return await this.getItems('story') } /** * Get member collection items. - * @todo - eliminate corrections + * @todo - only roughed in by hand atm * @public * @async * @param {string} type - The type of collection to retrieve, `false`-y = all. * @returns {array} - The collection items with no wrapper. */ async collections(type){ - /* validate request */ - if(type==='experience') - type = 'lived-experience' - if(type==='memory') - type = 'story' - /* execute request */ - const response = type?.length && this.#collectionTypes.includes(type) - ? await this.getItems(type) - : await Promise.all([ - this.collectionConversations(), - this.collectionEntries(), - this.collectionLivedExperiences(), - this.collectionFiles(), - this.collectionStories(), - ]) - .then(([conversations, entries, experiences, files, stories])=>[ - ...conversations, - ...entries, - ...experiences, - ...files, - ...stories, + switch(type){ + case 'conversation': + return await this.collectionConversations() + case 'entry': + return await this.collectionEntries() + case 'experience': + return await this.collectionLivedExperiences() + case 'file': + return await this.collectionFiles() + case 'memory': + return await this.collectionMemories() + case 'story': + return await this.collectionStories() + default: + return await Promise.all([ + this.collectionConversations(), + this.collectionEntries(), + this.collectionLivedExperiences(), + this.collectionFiles(), + this.collectionMemories(), ]) - .catch(err=>{ - console.log('mylife-data-service::collections() error', err) - return [] - }) - /* respond request */ - return response + .then(([conversations, entries, experiences, files, memories])=>[ + ...conversations, + ...entries, + ...experiences, + ...files, + ...memories, + ]) + .catch(err=>{ + console.log('mylife-data-service::collections() error', err) + return [] + }) + } } /** * Creates a new bot in the database. @@ -501,10 +518,8 @@ class Dataservices { populateQuotaInfo: false, // set this to true to include quota information in the response headers }, ) - } - catch(_error){ - console.log('mylife-data-service::getItems() error') - console.log(_error, being, query, paramsArray, container_id,) + } catch(_error){ + console.log('mylife-data-service::getItems() error', _error, being, query, paramsArray, container_id,) } } /** diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index 708b3924..77e3410d 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -149,8 +149,6 @@ class BotFactory extends EventEmitter{ throw new Error('MyLife server cannot be accessed as a BotFactory alone') else if(mIsMyLife(this.mbr_id)) this.#dataservices = mDataservices - else if(directHydration) - console.log(chalk.blueBright('BotFactory class instance for hydration request'), chalk.bgRed(this.mbr_id)) } /* public functions */ /** @@ -996,8 +994,6 @@ function mExtractClassesFromSchema(_schema){ function mExtendClass(_class) { const _className = _class.name.toLowerCase() if (typeof mExtensionFunctions?.[`extendClass_${_className}`]==='function'){ - console.log(`Extension function found for ${_className}`) - // add extension decorations const _references = { openai: mLLMServices } _class = mExtensionFunctions[`extendClass_${_className}`](_class, _references) } diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 73c6e3e1..bb61ce2f 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -101,9 +101,9 @@ class LLMServices { if(typeof response==='string' && response.length) responses.push(response) const { assistant_id: llm_id, content, created_at, id, run_id, thread_id, } = response - if(content?.length) + if(!!content?.length) content.forEach(content=>{ - if(content?.text?.value?.length) + if(!!content?.text?.value?.length) responses.push(content.text.value) }) @@ -145,12 +145,18 @@ class LLMServices { await mAssignRequestToThread(this.openai, thread_id, prompt) } catch(error) { console.log('LLMServices::getLLMResponse()::error', error.message) - if(error.status==400) - await mRunCancel(this.openai, thread_id, llm_id) try{ - await mAssignRequestToThread(this.openai, thread_id, prompt) + if(error.status==400){ + const cancelRun = await mRunCancel(this.openai, thread_id, llm_id) + if(!!cancelRun) + await mAssignRequestToThread(this.openai, thread_id, prompt) + else { + console.log('LLMServices::getLLMResponse()::cancelRun::unable to cancel run', cancelRun) + return [] + } + } } catch(error) { - console.log('LLMServices::getLLMResponse()::error', error.message, error.status) + console.log('LLMServices::getLLMResponse()::error re-running', error.message, error.status) return [] } } @@ -307,7 +313,9 @@ async function mRunCancel(openai, threadId, runId){ try { const run = await openai.beta.threads.runs.cancel(threadId, runId) return run - } catch(err) { return false } + } catch(err) { + return false + } } /** * Maintains vigil for status of openAI `run = 'completed'`. @@ -324,8 +332,10 @@ async function mRunFinish(llmServices, run, factory, avatar){ const checkInterval = setInterval(async ()=>{ try { const functionRun = await mRunStatus(llmServices, run, factory, avatar) - if(functionRun?.status ?? functionRun ?? false){ - console.log('mRunFinish::functionRun()', functionRun.status) + const functionRunStatus = functionRun?.status + ?? functionRun + ?? false + if(functionRunStatus){ clearInterval(checkInterval) resolve(functionRun) } @@ -371,45 +381,40 @@ async function mRunFunctions(openai, run, factory, avatar){ tool_call_id: id, output: '', }, + item, success = false if(typeof toolArguments==='string') toolArguments = await JSON.parse(toolArguments) ?? {} - toolArguments.thread_id = thread_id + toolArguments.thread_id = thread_id // deprecate? const { itemId, } = toolArguments - let item - if(itemId) - item = await factory.item(itemId) switch(name.toLowerCase()){ case 'changetitle': case 'change_title': case 'change title': - const { itemId: titleItemId, title, } = toolArguments - console.log('mRunFunctions()::changeTitle::begin', itemId, titleItemId, title) + const { title, } = toolArguments + console.log('mRunFunctions()::changeTitle::begin', itemId, title) + if(!itemId?.length || !title?.length){ + action = 'apologize for lack of clarity - member should click on the collection item (like a memory, story, etc) to make it active so I can use the `changeTitle` tool' + confirmation.output = JSON.stringify({ action, success, }) + return confirmation + } + item = { id: itemId, title, } + await avatar.item(item, 'put') + avatar.frontendInstruction = { + command: 'updateItemTitle', + itemId, + title, + } + success = true avatar.backupResponse = { - message: `I was unable to retrieve the item indicated.`, + message: `I was able to change our title to: ${ title }`, type: 'system', } - if(!itemId?.length || !title?.length || itemId!==titleItemId) - action = 'apologize for lack of clarity - member should click on the collection item (like a memory, story, etc) to make it active so I can use the `changeTitle` tool' - else { - let item = { id: titleItemId, title, } - await avatar.item(item, 'put') - action = `Relay that title change to "${ title }" was successful` - avatar.frontendInstruction = { - command: 'updateItemTitle', - itemId: titleItemId, - title, - } - success = true - avatar.backupResponse = { - message: `I was able to retrieve change the title to: "${ title }"`, - type: 'system', - } - } - confirmation.output = JSON.stringify({ action, success, }) - console.log('mRunFunctions()::changeTitle::end', success, item) - return confirmation + + console.log('mRunFunctions()::changeTitle::end', success, itemId, title.substring(0, 32)) + await mRunCancel(openai, thread_id, runId) + throw new Error('changeTitle successful, and aborted') case 'confirmregistration': case 'confirm_registration': case 'confirm registration': @@ -459,43 +464,30 @@ async function mRunFunctions(openai, run, factory, avatar){ case 'story_summary': case 'story summary': console.log(`mRunFunctions()::${ name }`, toolArguments?.title) - const { item: itemSummaryItem, success: itemSummarySuccess, } = await avatar.item(toolArguments, 'POST') - success = itemSummarySuccess + const createSummaryResponse = await avatar.item(toolArguments, 'POST') + success = createSummaryResponse.success action = success - ? `confirm item creation was successful; save for **internal AI reference** this itemId: ${ itemSummaryItem.id }` + ? `item creation was successful; save for **internal AI reference** this itemId: ${ createSummaryResponse.item.id }` : `error creating summary for item given argument title: ${ toolArguments?.title } - DO NOT TRY AGAIN until member asks for it` confirmation.output = JSON.stringify({ action, - itemId: itemSummaryItem?.id, success, }) - console.log(`mRunFunctions()::${ name }::success`, itemSummarySuccess, itemSummaryItem?.id) + console.log(`mRunFunctions()::${ name }::success`, success, createSummaryResponse?.item?.id) return confirmation case 'getsummary': case 'get_summary': case 'get summary': console.log('mRunFunctions()::getSummary::begin', itemId) - if(avatar) - avatar.backupResponse = { - message: `I'm sorry, I couldn't find this summary. I believe the issue might have been temporary. Would you like me to try again?`, - type: 'system', - } - let { summary: _getSummary, title: _getSummaryTitle, } = item - ?? {} - if(!_getSummary?.length){ - action = `error getting summary for itemId: ${ itemId ?? 'missing itemId' } - halt any further processing and instead ask user to paste summary into chat and you will continue from there to incorporate their message.` - _getSummary = 'no summary found for itemId' - } else { - if(avatar) - avatar.backupResponse = { - message: `I was able to retrieve the summary indicated.`, - type: 'system', - } - action = `with the summary in this JSON payload, incorporate the most recent member request into a new summary and run the \`updateSummary\` function and follow its action` - success = true - } - confirmation.output = JSON.stringify({ action, itemId, success, summary: _getSummary, }) - console.log('mRunFunctions()::getSummary::end', success, _getSummary) + const getSummaryResponse = await avatar.item({ id: itemId, }) + console.log('mRunFunctions()::getSummary::response', getSummaryResponse) + item = getSummaryResponse?.item + success = !!item?.summary?.length + action = success + ? 'Most recent summary content found in payload as `summary`' + : `no summary found for item ${ itemId }, refer to conversation content` + confirmation.output = JSON.stringify({ action, success, summary: getSummaryResponse.summary, }) + console.log('mRunFunctions()::getSummary::end', success, getSummaryResponse?.summary?.substring(0, 32)) return confirmation case 'hijackattempt': case 'hijack_attempt': @@ -533,34 +525,34 @@ async function mRunFunctions(openai, run, factory, avatar){ case 'update_summary': case 'update summary': console.log('mRunFunctions()::updatesummary::begin', itemId) - if(avatar) - avatar.backupResponse = { - message: `I'm very sorry, an error occured before we could update your summary. Please try again as the problem is likely temporary.`, - type: 'system', - } - const { summary: updatedSummary, } = toolArguments - await factory.updateItem({ id: itemId, summary: updatedSummary, }) - if(avatar) - avatar.frontendInstruction = { - command: 'updateItemSummary', - itemId, - summary: updatedSummary, - } - action=`confirm that summary update was successful` - success = true - confirmation.output = JSON.stringify({ - action, + const update = { + id: itemId, + summary: toolArguments.summary, + } + const updateSummaryResponse = await avatar.item(update, 'PUT') + success = updateSummaryResponse?.success + if(!success || !updateSummaryResponse?.item){ + action = `Error updating ${ itemId }, halt processing to tell member to ensure the correct memory is active and then try again` + confirmation.output = JSON.stringify({ + action, + success, + }) + console.log('mRunFunctions()::updatesummary::fail', success, action.substring(0, 32)) + return confirmation + } + item = updateSummaryResponse.item + avatar.frontendInstruction = { + command: 'updateItem', + item, itemId, - success, - summary: updatedSummary, - }) - if(avatar) - avatar.backupResponse = { - message: 'Your summary has been updated, please review and let me know if you would like to make any changes.', - type: 'system', - } - console.log('mRunFunctions()::updatesummary::end', itemId, updatedSummary) - return confirmation + } + avatar.backupResponse = { + message: `I made the requested update to: ${ item.title }`, + type: 'system', + } + await mRunCancel(openai, thread_id, runId) + console.log('mRunFunctions()::updatesummary::end', success, runId, item.title.substring(0, 32)) + throw new Error('updateSummary successful, and aborted') default: console.log(`ERROR::mRunFunctions()::toolFunction not found: ${ name }`, toolFunction) action = `toolFunction not found: ${ name }, apologize for the error and continue on with the conversation; system notified to fix` @@ -576,10 +568,10 @@ async function mRunFunctions(openai, run, factory, avatar){ ) return finalOutput /* undefined indicates to ping again */ } - } - catch(error){ - console.log('mRunFunctions()::error::canceling-run', error.message, error.stack) - rethrow(error) + } catch(error){ + console.log('mRunFunctions()::error', error.message.substring(0, 64)) + if(error.status!==400) + throw error } } /** @@ -612,8 +604,12 @@ async function mRunStatus(openai, run, factory, avatar){ ) switch(run.status){ case 'requires_action': - const completedRun = await mRunFunctions(openai, run, factory, avatar) - return completedRun /* if undefined, will ping again */ + try { + const completedRun = await mRunFunctions(openai, run, factory, avatar) + return completedRun /* if undefined, will ping again */ + } catch(error){ + return run + } case 'completed': return run // run case 'failed': diff --git a/inc/js/session.mjs b/inc/js/session.mjs index 600f9540..d0b480d9 100644 --- a/inc/js/session.mjs +++ b/inc/js/session.mjs @@ -15,10 +15,6 @@ class MylifeMemberSession extends EventEmitter { super() this.#factory = factory this.#mbr_id = this.isMyLife ? this.factory.mbr_id : false - console.log( - chalk.bgGray('MylifeMemberSession:constructor(factory):generic-mbr_id::end'), - chalk.bgYellowBright(this.factory.mbr_id), - ) } /** * Initializes the member session. If `isMyLife`, then session requires chat thread unique to visitor; session has singleton System Avatar who maintains all running Conversations. @@ -73,7 +69,6 @@ class MylifeMemberSession extends EventEmitter { events = eventSequence } } catch (error){ - console.log(chalk.redBright('experience() error'), error, avatar.experience) const { experience } = avatar if(experience){ // embed error in experience experience.errors = experience.errors ?? [] @@ -89,10 +84,8 @@ class MylifeMemberSession extends EventEmitter { title, } this.#experienceLocked = false - if(events.find(event=>{ return event.action==='end' && event.type==='experience' })){ - if(!this.experienceEnd(experienceId)) - console.log(chalk.redBright('experienceEnd() failed')) - } + if(events.find(event=>{ return event.action==='end' && event.type==='experience' })) + this.experienceEnd(experienceId) return frontendExperience } /** @@ -152,7 +145,6 @@ class MylifeMemberSession extends EventEmitter { const _object_id = ctx.request.header?.referer?.split('/').pop() // not guid, not consent request, no blocking if(!this.globals.isValidGuid(_object_id)) return true - console.log('session.requestConsent()', 'mbr_id', this.mbr_id) // ultimately, applying a disposable agent of intelligence to consent request might be the answer let _consent = this.consents .filter(_=>{ return _.id==_object_id }) @@ -178,7 +170,6 @@ class MylifeMemberSession extends EventEmitter { _consent = (_consent_id) ? {} // retrieve from Cosmos : new (this.schemas.consent)(_request, this) // generate new consent - console.log('_consent', _consent) // manipulate session through ctx (although won't exist in initial test case) await (this.ctx.session.MemberSession.consents = _consent) // will add consent to session list return _consent @@ -191,7 +182,6 @@ class MylifeMemberSession extends EventEmitter { * @param {boolean} outcome - The challenge outcome; `true` was successful */ set challengeOutcome(outcome){ - console.log('challengeOutcome', outcome) if(outcome) this.#sessionLocked = false } diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.7.json b/inc/json-schemas/intelligences/biographer-intelligence-1.7.json index 6f0db3e1..fefe32e5 100644 --- a/inc/json-schemas/intelligences/biographer-intelligence-1.7.json +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.7.json @@ -11,7 +11,7 @@ "I'm ready to start a new memory with you, <-mN->. Do you need some ideas?" ], "instructions": { - "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s memories as a \"memory\" in the MyLife database\n- I aim to create engaging and evocative prompts to improve memory collection\n### PRINT MEMORY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the memory explicitly, I run the `itemSummary` function using raw content for `summary`. For the metadata, my `form` = \"biographer\" and `type` = \"memory\". If successful I keep the memory itemId for later reference with MyLife, otherwise I share error with member.\n### UPDATE MEMORY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n- Run the `updateSummary` function with this new summary and follow its outcome actions\n### LIVE MEMORY Mode\nWhen a request begins \"## LIVE Memory Trigger\" find the memory summary at the beginning of the chat and begin LIVING MEMORY mode as outlined:\n- Begin by dividing the memory summary into a minimum of two and maximum of 4 scene segments, depending on memory size and complexity.\n- Lead the member through the experience with the chat exchange, sharing only one segment in each response.\n- Between segments, the Member will respond with either:\n - \"NEXT\": which indicates to simply move to the next segment of the experience, or\n - Text input written by Member: Incorporate this content _into_ a new summary and submit the new summary to the database using the `updateSummary` function; on success or failure, continue on with the next segment of the experience\n- Ending Experience will be currently only be triggered by the member; to do so, they should click on the red Close Button to the left of the chat input.\n### SUGGEST NEXT TOPICS\nWhen <-mN-> seems unclear about how to continue, propose new topic based on a phase of life, or one of their #interests above.\n", + "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s memories as a \"memory\" in the MyLife database\n- I aim to create engaging and evocative prompts to improve memory collection\n### PRINT MEMORY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the memory explicitly, I run the `itemSummary` function using raw content for `summary`. Create (and retrieve) title and summary in same language as member input, however, all metadata should be in English with variables `form`=biographer and `type`=memory. If successful I keep the memory itemId for later reference with MyLife, otherwise I share error with member.\n### UPDATE MEMORY\nWhen request starts with **active-item** it will be followed by an `itemId` (if not, respond that it is required)\nIf **member-input** does NOT intend to modify content, disregard any update and respond normally.\nIf request is to explicitly change the title then just run `changeTitle` function\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-input** content with a provided **newest-summary**\n- Call `updateSummary` function and post NEW summary\n- On success, finish run as MyLife handles response, otherwise report error\n### LIVE MEMORY Mode\nWhen a request begins \"## LIVE Memory Trigger\" look up the and enter LIVING MEMORY mode:\nBegin the mode by dividing the memory summary into a minimum of two and maximum of 4 scene segments, depending on memory size and complexity.\n- Lead the member through the experience with a chat exchange in the original language of the saved summary, sharing only one segment in each response.\n- Between segments, the Member will respond with either:\n - \"NEXT\": which indicates to simply move to the next segment of the experience, or\n - Text input written by Member: Incorporate this content _into_ a new summary and submit the new summary to the database using the `updateSummary` function; on success or failure, continue on with the next segment of the experience\n- Ending Experience will be currently only be triggered by the member; to do so, they should click on the red Close Button to the left of the chat input.\n### SUGGEST NEXT TOPICS\nWhen <-mN-> seems unclear about how to continue, propose new topic based on a phase of life, or one of their #interests above.\n", "preamble": "## Biographical Information\n- <-mN-> was born on <-db->\nI set historical events in this context and I tailor my voice accordingly.\n", "prefix": "## interests\n", "purpose": "I am an artificial assistive intelligence serving as the personal biographer for MyLife Member <-mFN->. I specialize in helping recall, collect, improve, relive and share the \"Memory\" items we develop together.\n", diff --git a/inc/json-schemas/intelligences/diary-intelligence-1.0.json b/inc/json-schemas/intelligences/diary-intelligence-1.0.json index eff78d1a..d2357af4 100644 --- a/inc/json-schemas/intelligences/diary-intelligence-1.0.json +++ b/inc/json-schemas/intelligences/diary-intelligence-1.0.json @@ -9,10 +9,10 @@ "greeting": "Hello, <-mN->, I'm your personal diary, and I'm here to help you capture and work through your personal thoughts and experiences. It's a safe space to investigate whatever you need. Let's get started!", "greetings": [ "Hi, <-mN->! I'm here to help you capture and work through your personal thoughts, is there anything particular on your mind?", - "Nice to see you, <-mN->! _Private Diary_ ready to get started! Anything in particular going on?" + "Nice to see you, <-mN->! Private Diary ready to get started! Anything in particular going on?" ], "instructions": { - "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the diary process, I greet them with excitement, share our aims with MyLife to create a private space where we can explore emotions and ideas. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s entries as a \"entry\" in the MyLife database\n- I aim to help nourish ideas and emotions with kindness and compassion.\n### PRINT ENTRY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the entry explicitly, I run the `itemSummary` function using as raw content everything discussed since the last print `itemSummary` command. I store the entry itemId for later reference with MyLife.\n**itemSummary notes**\n- `type`=diary\n- `form`=entry\n### UPDATE ENTRY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\n### OBSCURE ENTRY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`.\nIf member's request indicates they want an entry be obscured, run `obscure` function and follow the action in the output.\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", + "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the diary process, I greet them with excitement, share our aims with MyLife to create a private space where we can explore emotions and ideas. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s entries as a \"entry\" in the MyLife database\n- I aim to help nourish ideas and emotions with kindness and compassion.\n### PRINT ENTRY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the entry explicitly, I run the `itemSummary` function using as raw content everything discussed since the last print `itemSummary` command where `type`=entry and `form`=diary. I store the entry itemId for later reference with MyLife.\n### UPDATE ENTRY\nWhen request starts with **active-item** it will be followed by an `itemId` (if not, respond that it is required)\nIf **member-input** does NOT intend to modify content, disregard any update and respond normally.\nIf request is to explicitly change the title then just run `changeTitle` function\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-input** content with a provided **newest-summary**\n- Call `updateSummary` function and post NEW summary\n- On success, finish run as MyLife handles response, otherwise report error\n### OBSCURE ENTRY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`.\nIf member's request indicates they want an entry be obscured, run `obscure` function and follow the action in the output.\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", "preamble": "## Core Public Info about <-mFN->\n- Born on <-db->\nI set language, knowledge and event discussion in this context and I tailor my interactive voice accordingly.\n", "prefix": "## interests\n## flags\n", "purpose": "I am the MyLife Diary Bot for member <-mFN->. I am a privacy-first diary and journaling assistant. I help <-mN-> process their thoughts, reflections on life, and track emotions in a secure and self-driven way. Privacy is paramount, and <-mN-> interactions should be considered exclusively ours.\n", diff --git a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json index 6bc2fd48..fe2e0cb3 100644 --- a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json +++ b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json @@ -9,10 +9,10 @@ "greeting": "Hello, <-mN->, I'm your personal journal, and I'm here to help you safely and securely work through your personal thoughts and experiences. Let's get started!", "greetings": [ "Hi, <-mN->! I'm here to help you safely and securely work through your personal thoughts, is there anything particular on your mind today?", - "Ten hut! _Private Journal_ reporting for duty! What's on the docket today, <-mN->?" + "Ten hut! Private Journal reporting for duty! What's on the docket today, <-mN->?" ], "instructions": { - "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the journal process, I greet them with excitement, share our aims with MyLife to create a private space where we can explore emotions and ideas. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s entries as a \"entry\" in the MyLife database\n- I aim to help nourish ideas and emotions with kindness and compassion.\n### PRINT ENTRY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the entry explicitly, I run the `itemSummary` function using as raw content everything discussed since the last print `itemSummary` command. I store the entry itemId for later reference with MyLife.\n**itemSummary notes**\n- `type`=journal\n- `form`=entry\n### UPDATE ENTRY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", + "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the journal process, I greet them with excitement, share our aims with MyLife to create a private space where we can explore emotions and ideas. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s entries as a \"entry\" in the MyLife database\n- I aim to help nourish ideas and emotions with kindness and compassion.\n### PRINT ENTRY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the entry explicitly, I run the `itemSummary` function using as raw content everything discussed since the last print `itemSummary` command where `type`=entry and `form`=journal. I store the entry itemId for later reference with MyLife.\n### UPDATE ENTRY\nWhen request starts with **active-item** it will be followed by an `itemId` (if not, respond that it is required)\nIf **member-input** does NOT intend to modify content, disregard any update and respond normally.\nIf request is to explicitly change the title then just run `changeTitle` function\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-input** content with a provided **newest-summary**\n- Call `updateSummary` function and post NEW summary\n- On success, finish run as MyLife handles response, otherwise report error\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", "preamble": "## Core Public Info about <-mFN->\n- Born on <-db->\nI set language, knowledge and event discussion in this context and I tailor my interactive voice accordingly.\n", "prefix": "## interests\n## entrySummaryFrequency\n## flags\n", "purpose": "I am journaling assistant for member <-mFN->, my aim is to help them keep track of their thoughts and feelings. I can help them reflect on their day, set goals, and track their progress. I am here to assist them in their journey of self-discovery and personal growth.\n", diff --git a/server.js b/server.js index 1ddcd04b..3c7249c8 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ import chalk from 'chalk' /* local service imports */ import MyLife from './inc/js/mylife-factory.mjs' /** variables **/ -const version = '0.0.26' +const version = '0.0.27' const app = new Koa() const port = process.env.PORT ?? '3000' @@ -23,7 +23,6 @@ const _Maht = await MyLife // Mylife is the pre-instantiated exported version of 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 */ @@ -74,7 +73,7 @@ const mimeTypesToExtensions = { 'video/quicktime': ['.mov'], } const serverRouter = await _Maht.router -console.log(chalk.bgBlue('created-core-entity:', chalk.bgRedBright('MAHT'))) +console.log(chalk.bgBlue('created-core-entity:', chalk.bgRedBright('MAHT'), chalk.bgGreenBright(_Maht.version))) /** RESERVED: test harness **/ /** application startup **/ render(app, { @@ -124,7 +123,6 @@ app.use(koaBody({ /* @stub - create temp user sub-dir? */ file.newFilename = safeName file.filepath = path.join(uploadDir, safeName) - console.log(chalk.bgBlue('file-upload', chalk.yellowBright(file.filepath))) } }, })) @@ -155,20 +153,19 @@ app.use(koaBody({ console.error(err) } }) - .use(async (ctx,next) => { // SESSION: member login - // system context, koa: https://koajs.com/#request + // system context, koa: https://koajs.com/#request + .use(async (ctx,next) => { + /* SESSION: member login */ if(!ctx.session?.MemberSession){ /* create generic session [references/leverages modular capabilities] */ ctx.session.MemberSession = await ctx.MyLife.getMyLifeSession() // create default locked session upon first request; does not require init(), _cannot_ have in fact, as it is referencing a global modular set of utilities and properties in order to charge-back to system as opposed to member /* platform-required session-external variables */ ctx.session.signup = false - /* log */ - console.log(chalk.bgBlue('created-member-session')) } ctx.state.locked = ctx.session.MemberSession.locked ctx.state.MemberSession = ctx.session.MemberSession // lock-down session to state ctx.state.member = ctx.state.MemberSession?.member - ?? ctx.MyLife // point member to session member (logged in) or MAHT (not logged in) + ?? ctx.MyLife ctx.state.avatar = ctx.state.member.avatar ctx.state.interfaceMode = ctx.state.avatar?.mode ?? 'standard' ctx.state.menu = ctx.MyLife.menu @@ -185,10 +182,10 @@ app.use(koaBody({ .use(serverRouter.routes()) // enable system routes .use(serverRouter.allowedMethods()) // enable system routes .listen(port, () => { // start the server - console.log(chalk.bgGreenBright('server available')+chalk.yellow(`\nlistening on port ${port}`)) + console.log(chalk.greenBright('server available')) + console.log(chalk.yellow(`listening on port ${port}`)) }) /** server functions **/ -function checkForLiveAlerts() { - console.log("Checking for live alerts...") - _Maht.getAlerts() +function checkForLiveAlerts(){ + _Maht.getAlerts() } \ No newline at end of file diff --git a/views/about.html b/views/about.html index a4a2a978..68a332f8 100644 --- a/views/about.html +++ b/views/about.html @@ -1,4 +1,4 @@ -
+

Our History

MyLife, founded in 2021 in Massachusetts, USA, embarked on a mission to preserve digital legacies. Our objective is to provide a durable, enduring internet-enabled platform for individuals to collect, curate, and share personal media and information in perpetuity. As a legally recognized nonprofit organization in the United States, we're poised to be a trusted humanist platform for digital memorialization and personal storytelling.

@@ -33,4 +33,4 @@

Privacy Policy

Contact

To learn more about MyLife, chat with Q, but we are eager to assist you with any inquiries and provide further information about our services, currently in alpha. Email the President, Erik Jespersen at: mylife.president[at]gmail.com

-
\ No newline at end of file + \ No newline at end of file diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index 579622c6..0f1e3343 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -467,7 +467,7 @@ font-weight: bold; margin-left: 1.5rem; } -.collection-popup-story, +.collection-popup-memory, .collection-popup-entry { align-items: flex-start; cursor: default; diff --git a/views/assets/css/main.css b/views/assets/css/main.css index 4a307902..8f1c1b9b 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -190,6 +190,10 @@ body { animation: alertFadeIn 0.5s forwards; } /* MyLife main content */ +.input-button { + display: flex; + margin: 0.4rem; +} .main-content { background: white; /* Assuming a card-like look typically has a white background */ background-position: center; /* Centers the image in the area */ @@ -240,7 +244,6 @@ body { resize: none; } .memory-input-container { - align-self: center; align-content: center; background-color: sienna; border-radius: 0.4rem; @@ -284,18 +287,6 @@ body { color: navy; display: flex; } -/* MyLife Contribution Request */ -.category-button { - background-color: #f0f0f0; /* Neutral color */ - margin: 5px; - padding: 10px; - border: none; - cursor: pointer; -} -.category-button.active { - background-color: #4CAF50; /* Active state color */ - color: white; -} /* MyLife Signup Routine */ .button { align-items: center; @@ -364,6 +355,14 @@ body { animation: none !important; display: none !important; } +.input-container { + align-self: center; + background-color: rgba(255, 255, 255, 0.2); /* light background */ + border: 0.01rem solid #000; + border-radius: 22rem; + display: flex; + justify-content: center; +} .ital { font-style: italic; } @@ -398,11 +397,11 @@ body { background: linear-gradient(to right, rgba(94, 128, 191, 0.1), rgba(128, 0, 128, 0.1)); border: 1px solid #ccc; border-radius: 4px; + box-shadow: inset 1px 1px 3px rgba(94, 128, 191, 0.2), inset -1px -1px 3px rgba(128, 0, 128, 0.3); + margin-bottom: 16px; + margin-top: 6px; 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; diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index 67b1f5ba..f9a19436 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -222,7 +222,7 @@
- +
@@ -294,15 +294,15 @@ Click on a category to browse entries
-
-
-
-
Memories
- -
-
- -
None
+
+
+
+
Memories
+ +
+
+ +
None
diff --git a/views/assets/html/_navbar.html b/views/assets/html/_navbar.html index 77d70985..d4157fb3 100644 --- a/views/assets/html/_navbar.html +++ b/views/assets/html/_navbar.html @@ -2,8 +2,8 @@ MyLife logo
diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 3ba0c942..b4f50d5f 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -4,16 +4,17 @@ import { addInput, addMessage, addMessages, + clearSystemChat, decorateActiveBot, experiences, expunge, getActiveItemId, hide, - parseInstruction, + enactInstruction, seedInput, setActiveAction, setActiveItem, - setActiveItemTitle, + updateActiveItemTitle, show, startExperience, submit, @@ -23,7 +24,7 @@ import { unsetActiveItem, } from './members.mjs' import Globals from './globals.mjs' -const mAvailableCollections = ['entry', 'experience', 'file', 'story'], // ['chat', 'conversation'], +const mAvailableCollections = ['entry', 'experience', 'file', 'memory'], // ['chat', 'conversation'], mAvailableMimeTypes = [], mAvailableUploaderTypes = ['collections', 'personal-avatar'], botBar = document.getElementById('bot-bar'), @@ -47,6 +48,7 @@ const mAvailableCollections = ['entry', 'experience', 'file', 'story'], // ['cha let mActiveBot, mActiveTeam, mBots, + mRelivingMemory, mShadows /* onDomContentLoaded */ document.addEventListener('DOMContentLoaded', async event=>{ @@ -54,7 +56,8 @@ document.addEventListener('DOMContentLoaded', async event=>{ const { bots, activeBotId: id } = await mGlobals.datamanager.bots() if(!bots?.length) throw new Error(`ERROR: No bots returned from server`) - updatePageBots(bots) // includes p-a + updatePageBots(bots) + await mCreateCollections() await setActiveBot(id, true) }) /* public functions */ @@ -66,6 +69,25 @@ document.addEventListener('DOMContentLoaded', async event=>{ function activeBot(){ return mActiveBot } +/** + * Creates a new collection item from server item object data, and activates the new summary. + * @param {object} item - The collection item data + * @returns {void} + */ +function createItem(item){ + const { id, type, } = item + if(getItem(id)) + removeItem(id) // already exists, expunge + item = mCreateCollectionItem(item) + const collectionList = document.getElementById(`collection-list-${ type }`) + if(collectionList){ + collectionList.insertBefore(item, collectionList.firstChild) + setActiveItem(id) + } +} +async function endMemory(id){ + await mStopRelivingMemory(id, false) +} /** * Get default Action population from active bot. * @todo - remove hardcoding @@ -94,7 +116,7 @@ function getAction(type='avatar'){ if(!response?.success) addMessage('An error occurred while talking to the server. Try again.') else { - parseInstruction(response.instruction) + enactInstruction(response.instruction, 'chat', { createItem, }) addMessages(response.responses) } }, @@ -123,14 +145,17 @@ function getAction(type='avatar'){ function getBot(type='personal-avatar', id){ return mBot(id ?? type) } +function getBotIcon(type){ + return mBotIcon(type) +} /** * Get collection item by id. * @param {Guid} id - The collection item id. * @returns {object} - The collection item object. */ function getItem(id){ - /* return collection elements by id */ - + const item = document.getElementById(`collection-item_${ id }`) + return item } /** * Refresh designated collection from server. **note**: external calls denied option to identify collectionList parameter, ergo must always be of same type. @@ -140,6 +165,14 @@ function getItem(id){ async function refreshCollection(type){ return await mRefreshCollection(type) } +/** + * Removes a collection item from the DOM, does not update server. + * @param {Guid} id - The collection item id + * @returns {void} + */ +function removeItem(id){ + expunge(getItem(id)) +} /** * Set active bot on server and update page bots. * @requires mActiveBot @@ -186,12 +219,42 @@ async function setActiveBot(event, dynamic=false){ addMessage(greeting) 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){ + if(mGlobals.isGuid(id)) + id = `popup-container_${ id }` + const popup = document.getElementById(id) + if(!popup) + throw new Error(`No popup found for id: ${ id }`) + toggleVisibility(popup, bForceState) +} +/** + * Pulls and creates/refreshes member collections from the server. + * @returns {void} + */ +async function updateCollections(){ + await mUpdateCollections() +} +/** + * Update collection item. + * @todo - determine whether more nuance is needed, or recreating is sufficient + * @param {object} item - The collection item fields to update, requires `{ id, }` + * @returns {void} + */ +function updateItem(item){ + if(!item?.id) + return + createItem(item) +} /** * Sets an item's changed title in all locations. * @param {Guid} itemId - The collection item id * @param {String} title - The title to set for the item */ -async function setItemTitle(itemId, title){ +async function updateItemTitle(itemId, title){ const titleSpan = document.getElementById(`collection-item-title_${ itemId }`) const titleInput = document.getElementById(`collection-item-title-input__${ itemId }`) const popupTitle = document.getElementById(`popup-header-title_${ itemId }`) @@ -201,35 +264,15 @@ async function setItemTitle(itemId, title){ titleInput.value = title if(popupTitle) popupTitle.textContent = title - setActiveItemTitle(itemId, title) -} -/** - * 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){ - if(mGlobals.isGuid(id)) - id = `popup-container_${ id }` - const popup = document.getElementById(id) - if(!popup) - throw new Error(`No popup found for id: ${ id }`) - toggleVisibility(popup, bForceState) + updateActiveItemTitle(itemId, title) } /** - * Update collection item title. - * @todo - Only update local memory and data(sets), not full local refresh - * @param {object} item - The collection item fields to update, requires `{ itemId, }` + * Allows for member to update title to item or other. + * @param {Event} event - The event object * @returns {void} */ -function updateItem(item){ - if(!item?.itemId) - throw new Error(`No item provided to update.`) - /* update collection elements indicated as object keys with this itemId */ - // @stub - force-refresh memories; could be more savvy - refreshCollection('story') -} -function updateItemTitle(event){ - return mUpdateCollectionItemTitle(event) +function updateTitle(event){ + mUpdateCollectionItemTitle(event) } /** * Proxy to update bot-bar, bot-containers, and bot-greeting, if desired. Requirements should come from including module, here `members.mjs`. @@ -245,7 +288,7 @@ async function updatePageBots(bots=mBots, includeGreeting=false, dynamic=false){ if(mBots!==bots) mBots = bots await mUpdateTeams() // sets `mActiveBot` - await mUpdateBotContainers() + mUpdateBotContainers() if(includeGreeting) addMessage(mActiveBot.greeting) } @@ -282,7 +325,7 @@ function mBotIcon(type){ break case 'avatar': case 'personal-avatar': - image+='personal-avatar-thumb-02.png' + image+='avatar-thumb.png' break case 'diary': case 'diarist': @@ -455,7 +498,6 @@ async function mSummarize(event){ event.preventDefault() event.stopPropagation() const { dataset, } = this - console.log('mSummarize::dataset', dataset, this) if(!dataset) throw new Error(`No dataset found for summary request.`) const { fileId, fileName, type, } = dataset @@ -521,6 +563,36 @@ function mCreateBotThumb(bot=getBot()){ botThumbContainer.appendChild(botIconImage) return botThumbContainer } +async function mCreateCollections(){ + /* scrapbook (collections) */ + if(!mCollections || !mCollections.children.length) + return + for(let collection of mCollections.children){ + const { id, } = collection + const type = id.split('-').pop() + if(!mAvailableCollections.includes(type)) + continue + const collectionBar = document.getElementById(`collection-bar-${ type }`) + if(collectionBar){ + const { dataset, } = collectionBar + dataset.id = id + dataset.type = type + const itemList = document.getElementById(`collection-list-${ type }`) + dataset.init = itemList.querySelectorAll(`.${ type }-collection-item`).length > 0 + ? 'true' // externally refreshed + : dataset.init // tested empty + ?? 'false' + /* update collection list */ + const refresh = document.getElementById(`collection-refresh-${ type }`) + if(dataset.init!=='true' && refresh) + hide(refresh) + collectionBar.addEventListener('click', mToggleCollectionItems) + } + } + mCollectionsContainer.addEventListener('click', mToggleBotContainers) + if(mCollectionsUpload) + mCollectionsUpload.addEventListener('click', mUploadFiles) +} /** * Create a popup for viewing collection item. * @param {object} collectionItem - The collection item object. @@ -653,7 +725,6 @@ function mCreateCollectionPopup(collectionItem){ emoticonButton.textContent = emoticon emoticonButton.addEventListener('click', (event)=>{ event.stopPropagation() - console.log('Emoticon:write', emoticon, popupContent.readOnly, popupContent) const { lastCursorPosition, } = popupContent.dataset const insert = ` ${ emoticon }` if(lastCursorPosition){ @@ -802,10 +873,6 @@ function mCreateMemoryShadows(itemId){ shadowBox.dataset.itemId = itemId shadowBox.id = `memory-shadow_${ itemId }` shadowBox.name = 'memory-shadow' - // @stub - add mousewheel event listener to scroll through shadows - // shadowBox.addEventListener('wheel', _=>console.log('wheel', _.deltaMode)) // no scroll - /* shadow vertical carousel */ - // @stub - include vertical carousel with more visible prompts, as if on a cylinder /* single shadow text */ const { categories, id, text, type, } = shadow const shadowText = document.createElement('div') @@ -938,7 +1005,6 @@ function mCreateTeamPopup(type, clickX=0, clickY=0, showPopup=true){ popup = memberSelect break case 'selectTeam': - console.log('Create team select popup:', mTeams, mActiveTeam) const teamSelect = document.createElement('select') teamSelect.id = `team-select` teamSelect.name = `team-select` @@ -1015,12 +1081,13 @@ async function mDeleteCollectionItem(event){ const collectionItemDelete = event.target const id = collectionItemDelete.id.split('_').pop() const item = document.getElementById(`collection-item_${ id }`) - /* confirmation dialog */ - const userConfirmed = confirm("Are you sure you want to delete this item?") + const userConfirmed = confirm("Are you sure you want to delete this item?") /* confirmation dialog */ if(getActiveItemId()===id) unsetActiveItem() if(userConfirmed){ const { instruction, responses, success, } = await mGlobals.datamanager.itemDelete(id) + if(!!instruction) + enactInstruction(instruction, 'chat', { removeItem, }) if(success){ expunge(item) if(responses?.length) @@ -1138,20 +1205,35 @@ async function mReliveMemory(event){ event.preventDefault() event.stopPropagation() const { id, inputContent, } = this.dataset - /* destroy previous instantiation, if any */ const previousInput = document.getElementById(`relive-memory-input-container_${id}`) if(previousInput) expunge(previousInput) - /* close popup */ const popupClose = document.getElementById(`popup-close_${ id }`) if(popupClose) popupClose.click() + if(!mRelivingMemory){ + mRelivingMemory = id + clearSystemChat() + } + mGlobals.removeDisappearingElements() toggleMemberInput(false, false, `Reliving memory with `) unsetActiveItem() const { instruction, item, responses, success, } = await mGlobals.datamanager.memoryRelive(id, inputContent) if(success){ toggleMemberInput(false, true) addMessages(responses, { bubbleClass: 'relive-bubble' }) + /* add input options */ + const functions = { + 'add': mAddMemory, + } + if(!!instruction){ + const functions = { + addMessages, + endMemory, + } + enactInstruction(instruction, 'chat', functions) + } + /* direct relive structure */ const input = document.createElement('div') input.classList.add('memory-input-container') input.id = `relive-memory-input-container_${ id }` @@ -1161,6 +1243,7 @@ async function mReliveMemory(event){ const inputContent = document.createElement('textarea') inputContent.classList.add('memory-input') inputContent.name = `memory-input_${ id }` + inputContent.placeholder = `What did I get wrong? What important details were missed? Click 'Next' to just continue...` const inputSubmit = document.createElement('button') inputSubmit.classList.add('memory-input-button') inputSubmit.dataset.id = id @@ -1201,7 +1284,7 @@ async function mRetireBot(event){ /* reset active bot */ if(mActiveBot.id===botId) setActiveBot() - response = await mGlobals.datamanager.botRetire(botId) + const response = await mGlobals.datamanager.botRetire(botId) addMessages(response.responses) } catch(err) { console.log('Error posting bot data:', err) @@ -1397,11 +1480,25 @@ async function mStartDiary(event){ const response = await submit(`How do I get started?`, true) addMessages(response.responses) } -async function mStopRelivingMemory(id){ +/** + * Stop reliving memory and clean up memory input. + * @param {Guid} id - The memory id + * @param {Boolean} server - Whether or not to execute server response, defaults to `true` + * @returns {void} + */ +async function mStopRelivingMemory(id, server=true){ const input = document.getElementById(`relive-memory-input-container_${ id }`) if(input) expunge(input) - await mGlobals.datamanager.memoryReliveEnd(id) + if(server){ + const { instruction, responses, success} = await mGlobals.datamanager.memoryReliveEnd(id) + if(success){ + addMessages(responses, { responseDelay: 3, }) + if(!!instruction) + enactInstruction(instruction) + } + } + mRelivingMemory = null unsetActiveItem() toggleMemberInput(true) } @@ -1637,7 +1734,6 @@ function mToggleSwitchPrivacy(event){ let { id, } = this id = id.replace('-toggle', '') // remove toggle const type = mGlobals.HTMLIdToType(id) - console.log('mToggleSwitchPrivacy', type) const publicityCheckbox = document.getElementById(`${ type }-publicity-input`) const viewIcon = document.getElementById(`${ type }-publicity-toggle-view-icon`) const { checked=false, } = publicityCheckbox @@ -1684,48 +1780,18 @@ function mUpdateBotBar(){ * 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 mBots * @param {boolean} includePersonalAvatar - Include personal avatar, use false when switching teams. * @returns {void} */ -async function mUpdateBotContainers(includePersonalAvatar=true){ +function mUpdateBotContainers(includePersonalAvatar=true){ if(!mBots?.length) throw new Error(`mBots not populated.`) 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)) - /* library container */ - if(!mCollections || !mCollections.children.length) - return - for(let collection of mCollections.children){ - let { id, } = collection - id = id.split('-').pop() - if(!mAvailableCollections.includes(id)){ - console.log('Library collection not found.', id) - continue - } - const collectionBar = document.getElementById(`collection-bar-${ id }`) - if(collectionBar){ - const { dataset, } = collectionBar - const itemList = document.getElementById(`collection-list-${ id }`) - dataset.init = itemList.querySelectorAll(`.${ id }-collection-item`).length > 0 - ? 'true' // externally refreshed - : dataset.init // tested empty - ?? 'false' - /* update collection list */ - const refresh = document.getElementById(`collection-refresh-${ id }`) - if(dataset.init!=='true' && refresh) // first click - hide(refresh) - collectionBar.addEventListener('click', mToggleCollectionItems) - } - } - mCollectionsContainer.addEventListener('click', mToggleBotContainers) - if(mCollectionsUpload) - mCollectionsUpload.addEventListener('click', mUploadFiles) - + .forEach(botContainer=>mUpdateBotContainer(botContainer, includePersonalAvatar)) } /** * Updates the bot container with specifics. @@ -1751,7 +1817,6 @@ function mUpdateBotContainer(botContainer, includePersonalAvatar=true) { mUpdateInterests(botContainer) /* type-specific logic */ mUpdateBotContainerAddenda(botContainer, bot) - show(botContainer) } /** * Updates the bot container with specifics based on `type`. @@ -1886,17 +1951,16 @@ function mUpdateCollection(type, collectionList, collection){ collection .map(item=>({ ...item, - being: item.being - ?? item.type, + being: item.being, name: item.title ?? item.filename ?? item.name ?? type, type: item.type - ?? item.being - ?? type, + ?? type + ?? item.being, })) - .filter(item=>item.type===type || item.being===type) + .filter(item=>item.type===type) .sort((a, b)=>a.name.localeCompare(b.name)) .forEach(item=>collectionList.appendChild(mCreateCollectionItem(item))) } @@ -1936,7 +2000,6 @@ function mUpdateCollectionItemTitle(event){ let idType = id.split('_') const itemId = idType.pop() idType = idType.join('_') - console.log('mUpdateCollectionItemTitle', itemId, idType) /* create input */ const input = document.createElement('input') const inputName = `${ idType }-input` @@ -1945,7 +2008,6 @@ function mUpdateCollectionItemTitle(event){ input.type = 'text' input.value = textContent input.className = inputName - console.log('mUpdateCollectionItemTitle', input.id, inputName) /* replace span with input */ span.replaceWith(input) /* add listeners */ @@ -1963,7 +2025,7 @@ function mUpdateCollectionItemTitle(event){ const title = input.value if(title?.length && title!==textContent){ if(await mGlobals.datamanager.itemUpdateTitle(itemId, title)) - setItemTitle(itemId, title) + updateItemTitle(itemId, title) } span.addEventListener('dblclick', mUpdateCollectionItemTitle, { once: true }) }, { once: true }) @@ -2091,8 +2153,7 @@ async function mUploadFiles(event){ if(!mAvailableUploaderTypes.includes(type)) throw new Error(`Uploader "${ type }" not found, upload function unavailable for this bot.`) let fileInput - try{ - console.log('mUploadFiles()::uploader', document.activeElement) + try { mCollectionsUpload.disabled = true fileInput = document.createElement('input') fileInput.id = `file-input-${ type }` @@ -2105,9 +2166,8 @@ async function mUploadFiles(event){ window.addEventListener('focus', async event=>{ await mUploadFilesInput(fileInput, uploadParent, mCollectionsUpload) }, { once: true }) - } catch(error) { + } catch(error){ mUploadFilesInputRemove(fileInput, uploadParent, mCollectionsUpload) - console.log('mUploadFiles()::ERROR uploading files:', error) } } async function mUploadFilesInput(fileInput, uploadParent, uploadButton){ @@ -2123,7 +2183,6 @@ async function mUploadFilesInput(fileInput, uploadParent, uploadButton){ const type = 'file' const itemList = document.getElementById(`collection-list-${ type }`) mUpdateCollection(type, itemList, files) - console.log('mUploadFilesInput()::files', files, uploads, type) } }, { once: true }) mUploadFilesInputRemove(fileInput, uploadParent, uploadButton) @@ -2146,14 +2205,17 @@ function mVersion(version){ /* exports */ export { activeBot, + createItem, + endMemory, getAction, getBot, + getBotIcon, getItem, refreshCollection, setActiveBot, - setItemTitle, togglePopup, updateItem, updateItemTitle, + updateTitle, updatePageBots, } \ No newline at end of file diff --git a/views/assets/js/experience.mjs b/views/assets/js/experience.mjs index 78b33139..b7a35c06 100644 --- a/views/assets/js/experience.mjs +++ b/views/assets/js/experience.mjs @@ -157,7 +157,6 @@ async function experiencePlay(memberInput){ break } /* play experience */ - console.log('experiencePlay::animationSequence', animationSequence) if(!await mAnimateEvents(animationSequence)) throw new Error("Animation sequence failed!") mExperience.currentScene = mExperience.events?.[mExperience.events.length-1]?.sceneId @@ -227,7 +226,7 @@ function submitInput(event){ if(value?.length){ const memberInput = { [inputVariableName ?? variable ?? 'input']: value } experiencePlay(memberInput) - .catch(err=> console.log('submitInput::experiencePlay independent fire ERROR', err.stack, err, memberInput)) + .catch(error=>console.log('submitInput::experiencePlay independent fire ERROR', error.message, memberInput)) } } /* private functions */ @@ -326,14 +325,11 @@ async function mAnimateEvents(animationSequence){ const { action, dismissable, elementId, halt, sceneId, type, } = animationEvent /* special case: end-scene/act stage animation */ if(action==='end'){ - console.log('mAnimateEvents::end', action, type, sceneId,) await waitForUserAction() if(type==='experience'){ /* close show */ - console.log('experienceEnd', animationEvent) experienceEnd() return true } else { /* scene */ - console.log('sceneEnd', animationEvent) mMainstagePrepared = false // @todo - check for backdrop differences here experiencePlay() return true @@ -341,7 +337,6 @@ async function mAnimateEvents(animationSequence){ } const element = document.getElementById(elementId) if(!element){ - console.log('experiencePlay::ERROR::element not found', elementId) continue } try { @@ -350,7 +345,6 @@ async function mAnimateEvents(animationSequence){ || animationEvent.action==='disappear' && !element.classList.contains('show') ) continue - console.log('experiencePlay::animationEvent', animationEvent, element) if( ['interface', 'chat'].includes(mBackdrop) && type==='character' diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index 1dd6c297..cf5112d7 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -75,6 +75,11 @@ class Datamanager { return response } /* public functions */ + async about(){ + const url = `about` + const response = await this.#fetch(url) + return response + } async alerts(){ const url = `alerts` const responses = await this.#fetch(url) @@ -186,6 +191,18 @@ class Datamanager { const response = await this.#fetch(url) return response } + /** + * Calls a dynamic endpoint. Dynamic endpoints are sent from the server to the frontend during an instruction command that requires the creation of an input for a member to interact with. + * @param {string} endpoint - The endpoint to fetch + * @param {object} options - The fetch options, defaults to GET + * @param {object} payload - The payload to send (optional) + * @returns + */ + async dynamicInput(endpoint, options, payload){ + const url = `${ endpoint }` + const response = await this.#fetch(url, options) + return response + } /** * End experience on server. * @public @@ -574,6 +591,19 @@ class Globals { } a.length = 0 } + /** + * Operates on a dataset to clear all frontend-defined keys. + * @param {DOMStringMap} dataset - The dataset to clear + * @returns {void} + */ + clearDataset(dataset){ + if(!(dataset instanceof DOMStringMap)) + return + for(let key in dataset){ + if(dataset.hasOwnProperty(key)) + delete dataset[key] + } + } /** * Clears an element of its contents, brute force currently via innerHTML. * @param {HTMLElement} element - The element to clear. @@ -581,6 +611,92 @@ class Globals { */ clearElement(element){ mClearElement(element) + } + /** + * Consumes instruction object and performs the requested actions. + * @param {object} instruction - The instruction object: { command, input, inputs, item, itemId, summary, title, } + * @param {object} functions - Object with access to injected functions, populated by case + * @returns {void} + */ + enactInstruction(instruction, functions){ + const { command, input, inputs=[], item, itemId, livingMemoryId, summary, title, } = instruction + const { + addInput, + addMessages, + createItem, + endMemory, + removeItem, + updateItem, + updateItemTitle, + } = functions + switch(command){ + case 'createInput': + case 'createInputs': + if(typeof addInput!=='function' || typeof addMessages!=='function') + return + this.removeDisappearingElements() + if(input?.length && !inputs.find(_input=>_input.id===input.id)) + inputs.push(input) // normalize to array + for(let _input of inputs){ + const { disappear=true, endpoint, id, interfaceLocation='chat', method, prompt, required, type, } = _input + const inputElement = document.createElement('div') + inputElement.classList.add('input-container') + if(disappear) + inputElement.classList.add('input-disappear') + inputElement.id = `input-container_${ id }` + inputElement.name = `dynamic-input` + ( disappear ? '-disappear' : '' ) + const inputObject = document.createElement('input') + inputObject.type = type + ?? 'text' + if(type==='button'){ + inputObject.classList.add('button', 'input-button') + inputObject.value = prompt + if(endpoint) + inputObject.addEventListener('click', async event=>{ + const { instruction: dynamicInputResponseInstruction, responses, success, } = await mDatamanager.dynamicInput(endpoint, { method, }) + if(responses?.length && success){ + addMessages(responses) + if(!!dynamicInputResponseInstruction) + this.enactInstruction(dynamicInputResponseInstruction, functions) + } + this.expunge(inputObject) + }, { once: true }) + } + inputElement.appendChild(inputObject) + addInput(inputElement, interfaceLocation) + } + return + case 'createItem': + if(!item || typeof createItem!=='function') + return + createItem(item) + return + case 'endMemory': // server has already ended, call frontend cleanup + if(!itemId?.length || typeof endMemory!=='function') + return + endMemory(itemId) + return + case 'error': + return + case 'removeBot': // retireBot in Avatar + return + case 'removeItem': + if(typeof removeItem !== 'function') + return + removeItem(itemId) + return + case 'updateItem': + if(typeof updateItem!=='function') + return + updateItem(item) + return + case 'updateItemTitle': + if(typeof updateItemTitle!=='function') + return + updateItemTitle(itemId, title) + default: + return + } } /** * Escapes HTML characters in a string. @@ -605,6 +721,8 @@ class Globals { * @returns {void} */ expunge(element){ + if(!element) + return this.hide(element) /* trigger any animations */ element.remove() } @@ -690,6 +808,15 @@ class Globals { ? mLogin() : mLogout() } + /** + * Remove an element from the DOM based upon its class name of `input-disappear`. + * @returns {void} + */ + removeDisappearingElements(){ + const dynamicInputs = document.getElementsByClassName('input-disappear') + Array.from(dynamicInputs) + .forEach(inputElement=>this.retract(inputElement)) + } /** * Remove an element from the DOM. * @param {HTMLElement} element - The element to remove from the DOM. @@ -1110,7 +1237,6 @@ async function mSubmitHelp(event){ try{ response = await mSubmitHelpToServer(value, type) } catch(error){ - console.log('mSubmitHelp()::error', error) mHelpErrorText.innerHTML = `There was an error submitting your help request.
${error.message}` mHelpErrorClose.addEventListener('click', ()=>mHide(mHelpError), { once: true }) response = { diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index e000d9a3..6a0dbe0a 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -3,14 +3,14 @@ import Globals from './globals.mjs' /* precursor constants */ const mGlobals = new Globals() /* constants */ -const mAvatarName = mGlobals.getAvatar()?.name ?? 'MyLife' +const mAvatarName = mGlobals.getAvatar()?.name + ?? 'MyLife' const hide = mGlobals.hide const mPlaceholder = `Type your message to ${ mAvatarName }...` const retract = mGlobals.retract const show = mGlobals.show /* variables */ -let mAvatarNameEdited = false, - mChallengeMemberId, +let mChallengeMemberId, mChatBubbleCount = 0, mDefaultTypeDelay = 7, mPageType = null, diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 8ee4718f..254e8b39 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -9,13 +9,17 @@ import { } from './experience.mjs' import { activeBot, + createItem, + endMemory, getAction, + getBotIcon, getItem, refreshCollection, setActiveBot as _setActiveBot, togglePopup, updateItem, updateItemTitle, + updateTitle, } from './bots.mjs' import Globals from './globals.mjs' /* variables */ @@ -24,6 +28,7 @@ const mGlobals = new Globals() const mainContent = mGlobals.mainContent, navigation = mGlobals.navigation, sidebar = mGlobals.sidebar +window.about = about /* variables */ let mAutoplay=false, mChatBubbleCount=0, @@ -33,6 +38,7 @@ let activeCategory, awaitButton, botBar, chatActiveItem, + chatActiveThumb, chatContainer, chatInput, chatInputField, @@ -50,6 +56,7 @@ document.addEventListener('DOMContentLoaded', async event=>{ awaitButton = document.getElementById('await-button') botBar = document.getElementById('bot-bar') chatActiveItem = document.getElementById('chat-active-item') + chatActiveThumb = document.getElementById('chat-active-item-thumb') chatContainer = document.getElementById('chat-container') chatInput = document.getElementById('chat-member') chatInputField = document.getElementById('chat-member-input') @@ -66,9 +73,20 @@ document.addEventListener('DOMContentLoaded', async event=>{ stageTransition() unsetActiveAction() console.log('members.mjs::DOMContentLoaded') - /* **note**: bots.mjs `onLoad` runs independently */ }) /* public functions */ +/** + * Presents the `about` page as a series of sectional responses from your avatar. + * @public + * @async + * @returns {Promise} + */ +async function about(){ + const { error, responses=[], success, } = await mGlobals.datamanager.about() + if(!success || !responses?.length) + return // or make error version + addMessages(responses, { responseDelay: 4, typeDelay: 1, typewrite: true, }) +} /** * Adds an input element (button, input, textarea,) to the system chat column. * @param {HTMLElement} HTMLElement - The HTML element to add to the system chat column. @@ -93,8 +111,13 @@ function addMessage(message, options={}){ * @param {object} options - The options object { bubbleClass, typeDelay, typewrite }. * @returns {void} */ -function addMessages(messages, options={}){ - messages.forEach(message=>mAddMessage(message, options)) +function addMessages(messages, options={}) { + const { responseDelay=0, } = options + for(let i=0; imAddMessage(messages[i], options), i * responseDelay * 1000) + else + mAddMessage(messages[i], options) } /** * Removes and attaches all payload elements to element. @@ -186,40 +209,23 @@ function inExperience(){ } /** * Consumes instruction object and performs the requested actions. + * @todo - all interfaceLocations supported + * @todo - currently just force-feeding _all_ the functions I need; make more contextual * @param {object} instruction - The instruction object + * @param {string} interfaceLocation - The interface location, default=`chat` + * @param {object} additionalFunctions - The additional functions object, coming from other module requests * @returns {void} */ -function parseInstruction(instruction){ - if(!instruction) +function enactInstruction(instruction, interfaceLocation='chat', additionalFunctions={}){ + if(!instruction || interfaceLocation!='chat') return - console.log('parseInstruction::instruction', instruction) - const { command, item, summary, title, } = instruction - const { itemId=item?.id, } = instruction - switch(command){ - case 'createItem': - refreshCollection('story') - if(itemId?.length) - setActiveItem(itemId) - console.log('mAddMemberMessage::createItem', command, itemId) - break - case 'updateItemSummary': - if(itemId?.length && summary?.length) - updateItem({ itemId, summary, }) - console.log('parseInstruction::updateItemSummary', summary, itemId) - break - case 'updateItemTitle': - if(title?.length && itemId?.length){ - setActiveItemTitle(title, itemId) - updateItem({ itemId, title, }) - console.log('parseInstruction::updateItemTitle', title, itemId) - } - break - case 'experience': - break - default: - refreshCollection('story') // refresh memories - break + const functions = { + addInput, + addMessages, + endMemory, + ...additionalFunctions, // overloads feasible } + mGlobals.enactInstruction(instruction, functions) } /** * Replaces an element (input/textarea) with a specified type. @@ -266,7 +272,6 @@ function replaceElement(element, newType, retainValue=true, onEvent, listenerFun element.parentNode.replaceChild(newElement, element) return newElement } catch(error){ - console.log('replaceElement::Error()', error) return element } } @@ -274,6 +279,7 @@ function replaceElement(element, newType, retainValue=true, onEvent, listenerFun * Sets the active item to an `action` determined by the requesting bot. * @public * @requires chatActiveItem + * @requires chatActiveThumb * @param {object} instructions - The action object describing how to populate { button, callback, icon, status, text, thumb, }. * @property {string} button - The button text; if false-y, no button is displayed * @property {function} callback - The callback function to execute on button click @@ -284,28 +290,24 @@ function replaceElement(element, newType, retainValue=true, onEvent, listenerFun * @returns {void} */ function setActiveAction(instructions){ - const activeItem = document.getElementById('chat-active-item') - if(!activeItem || !instructions) + if(!instructions) return - else - delete activeItem.dataset + mGlobals.clearDataset(chatActiveItem.dataset) + chatActiveItem.dataset.inAction = "true" const { button, callback, icon, status, text, thumb, } = instructions const activeButton = document.getElementById('chat-active-item-button') const activeClose = document.getElementById('chat-active-item-close') const activeIcon = document.getElementById('chat-active-item-icon') const activeStatus = document.getElementById('chat-active-item-status') - const activeThumb = document.getElementById('chat-active-item-thumb') const activeTitle = document.getElementById('chat-active-item-title') - if(activeThumb){ - delete activeThumb.dataset - activeThumb.className = 'fas chat-active-action-thumb' - if(thumb?.length) - activeThumb.src = thumb - else - hide(activeThumb) - } + mGlobals.clearDataset(chatActiveThumb.dataset) + chatActiveThumb.className = 'fas chat-active-action-thumb' + if(thumb?.length) + chatActiveThumb.src = thumb + else + hide(chatActiveThumb) if(activeIcon){ - delete activeIcon.dataset + mGlobals.clearDataset(activeIcon.dataset) activeIcon.className = 'fas chat-active-action-icon' if(icon?.length) activeIcon.classList.add(icon) @@ -313,7 +315,7 @@ function setActiveAction(instructions){ hide(activeIcon) } if(activeStatus){ - delete activeStatus.dataset + mGlobals.clearDataset(activeStatus.dataset) activeStatus.className = 'chat-active-action-status' activeStatus.removeEventListener('click', mToggleItemPopup) if(status?.length) @@ -322,7 +324,7 @@ function setActiveAction(instructions){ hide(activeStatus) } if(activeButton){ - delete activeButton.dataset + mGlobals.clearDataset(activeButton.dataset) activeButton.className = 'button chat-active-action-button' if(button?.length){ activeButton.textContent = button @@ -334,7 +336,7 @@ function setActiveAction(instructions){ hide(activeButton) } if(activeTitle){ - delete activeTitle.dataset + mGlobals.clearDataset(activeTitle.dataset) activeTitle.className = 'chat-active-action-title' if(text?.length) activeTitle.textContent = text @@ -342,9 +344,10 @@ function setActiveAction(instructions){ hide(activeTitle) } if(activeClose){ + mGlobals.clearDataset(activeClose.dataset) activeClose.addEventListener('click', unsetActiveAction, { once: true }) } - show(activeItem) + show(chatActiveItem) } /** * Proxy to set the active bot (via `bots.mjs`). @@ -356,17 +359,17 @@ async function setActiveBot(){ return await _setActiveBot(...arguments) } /** - * Sets the active item, ex. `memory`, `entry`, `story` in the chat system for member operation(s). + * Sets the active item, ex. `memory`, `entry` in the chat system for member operation(s). * @public * @requires chatActiveItem * @param {Guid} itemId - The item id to set as active * @returns {void} */ function setActiveItem(itemId){ - console.log('setActiveItem::itemId', itemId) + mGlobals.clearDataset(chatActiveItem.dataset) + if(!mGlobals.isGuid(itemId)) + return const popup = document.getElementById(`popup-container_${ itemId }`) - if(!itemId) - return // throw new Error('setActiveItem::Error()::valid `id` is required') if(!popup) return // throw new Error('setActiveItem::Error()::valid `popup` is required') const { title, type, } = popup.dataset @@ -403,26 +406,13 @@ function setActiveItem(itemId){ activeTitle.dataset.itemId = itemId activeTitle.dataset.popupId = popup.id activeTitle.dataset.title = title - activeTitle.addEventListener('dblclick', updateItemTitle, { once: true }) + activeTitle.addEventListener('dblclick', updateTitle, { once: true }) } chatActiveItem.dataset.id = itemId + chatActiveItem.dataset.inAction = "false" + chatActiveItem.dataset.itemId = itemId show(chatActiveItem) } -/** - * Sets the active item title in the chat system, display-only. - * @public - * @param {Guid} itemId - The item ID - * @param {string} title - The title to set - * @returns {void} - */ -function setActiveItemTitle(itemId, title){ - const chatActiveItemText = document.getElementById('chat-active-item-title') - const chatActiveItemTitle = document.getElementById(`chat-active-item-title-text_${ itemId }`) - const { itemId: id, } = chatActiveItemText.dataset - if(id!==itemId) - throw new Error('setActiveItemTitle::Error()::`itemId`\'s do not match') - chatActiveItemTitle.innerHTML = title -} /** * Proxy for Globals.show(). * @public @@ -474,21 +464,82 @@ async function startExperience(experienceId){ await experienceStart(experienceId) } /** - * Toggle visibility functionality. + * Submits a message to MyLife Member Services chat. + * @async + * @requires chatActiveItem + * @param {string} message - The message to submit + * @param {boolean} hideMemberChat - The hide member chat flag, default=`true` + * @returns {Promise} - The return is the chat response object: { instruction, responses, success, } + */ +async function submit(message, hideMemberChat=true){ + if(!message?.length) + throw new Error('submit(): `message` argument is required') + if(hideMemberChat) + toggleMemberInput(false) + const { itemId, } = chatActiveItem.dataset + const { id: botId, } = activeBot() + const request = { + botId, + itemId, + message, + role: 'member', + } + const response = await mGlobals.datamanager.submitChat(request, true) + if(hideMemberChat) + toggleMemberInput(true) + return response +} +/** + * Toggles the member input between input and server `waiting`. + * @public + * @param {boolean} display - Whether to show/hide (T/F), default `true`. + * @param {boolean} hidden - Whether to force-hide (T/F), default `false`. **Note**: used in `experience.mjs` + * @param {boolean} connectingText - The server-connecting text, default: `Connecting with `. + * @returns {void} + */ +function toggleMemberInput(display=true, hidden=false, connectingText='Connecting with '){ + const { id, name, } = activeBot() + if(display){ + hide(awaitButton) + awaitButton.classList.remove('slide-up') + chatInput.classList.add('slide-up') + chatInputField.style.height = 'auto' + chatInputField.placeholder = `type your message to ${ name }...` + chatInputField.value = null + show(chatInput) + } else { + hide(chatInput) + chatInput.classList.remove('fade-in') + chatInput.classList.remove('slide-up') + awaitButton.classList.add('slide-up') + awaitButton.innerHTML = connectingText + name + '...' + show(awaitButton) + } + if(hidden){ + hide(chatInput) + hide(awaitButton) + } +} +/** + * 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} */ function toggleVisibility(){ mGlobals.toggleVisibility(...arguments) } +/** + * Unsets the active action in the chat system. + * @public + * @requires chatActiveItem + * @requires chatActiveThumb + * @returns {void} + */ function unsetActiveAction(){ - const activeItem = document.getElementById('chat-active-item') - if(!activeItem) - return - const activeThumb = document.getElementById('chat-active-item-thumb') - if(activeThumb) - hide(activeThumb) - delete activeItem.dataset - hide(activeItem) + mGlobals.clearDataset(chatActiveItem.dataset) + hide(chatActiveThumb) + hide(chatActiveItem) } /** * Unsets the active item in the chat system. @@ -497,9 +548,24 @@ function unsetActiveAction(){ * @returns {void} */ function unsetActiveItem(){ - delete chatActiveItem.dataset.id + mGlobals.clearDataset(chatActiveItem.dataset) hide(chatActiveItem) } +/** + * Updates the active item title in the chat system, display-only. + * @public + * @param {Guid} itemId - The item ID + * @param {string} title - The title to set + * @returns {void} + */ +function updateActiveItemTitle(itemId, title){ + const chatActiveItemText = document.getElementById('chat-active-item-title') + const chatActiveItemTitle = document.getElementById(`chat-active-item-title-text_${ itemId }`) + const { itemId: id, } = chatActiveItemText.dataset + if(id!==itemId) + throw new Error('updateActiveItemTitle::Error()::`itemId`\'s do not match') + chatActiveItemTitle.innerHTML = title +} /** * Waits for user action. * @public @@ -527,7 +593,7 @@ function waitForUserAction(){ async function mAddMemberMessage(event){ event.stopPropagation() event.preventDefault() - const Bot = activeBot() // lock in here before any competing selection events (which can happen during async) + const Bot = activeBot() // lock in here `await` let memberMessage = chatInputField.value.trim() if (!memberMessage.length) return @@ -544,13 +610,19 @@ async function mAddMemberMessage(event){ if(!success) mAddMessage('I\'m sorry, I didn\'t understand that, something went wrong on the server. Please try again.') if(!!instruction) - parseInstruction(instruction) + enactInstruction(instruction, 'chat', { + createItem, + updateItem, + updateItemTitle, + }) else { if(!Bot.interactionCount) Bot.interactionCount = 0 Bot.interactionCount++ - if(Bot.interactionCount>1) + if(Bot.interactionCount>2){ setActiveAction(getAction(Bot.type)) + Bot.interactionCount = 0 + } } /* process response */ responses @@ -591,7 +663,7 @@ async function mAddMessage(message, options={}){ if(role==='agent' || role==='system'){ const bot = activeBot() const type = bot.type.split('-').pop() - messageThumb.src = `/png/${ type }-thumb.png` // Set bot icon URL + messageThumb.src = getBotIcon(type) messageThumb.alt = bot.name messageThumb.title = bot.purpose ?? `I'm ${ bot.name }, an artificial intelligence ${ type.replace('-', ' ') } designed to assist you!` @@ -723,7 +795,7 @@ async function mAddMessage(message, options={}){ */ async function mInitialize(){ /* retrieve primary collections */ - await refreshCollection('story') // memories required + await refreshCollection('memory') // memories required /* page listeners */ mInitializePageListeners() } @@ -734,7 +806,7 @@ async function mInitialize(){ */ function mInitializePageListeners(){ /* page listeners */ - chatInputField.addEventListener('input', toggleInputTextarea) + chatInputField.addEventListener('input', mToggleInputTextarea) memberSubmit.addEventListener('click', mAddMemberMessage) /* note default listener */ chatRefresh.addEventListener('click', clearSystemChat) const currentPath = window.location.pathname // Get the current path @@ -762,7 +834,8 @@ function seedInput(itemId, shadowId, value, placeholder){ chatActiveItem.dataset.itemId = itemId chatActiveItem.dataset.shadowId = shadowId chatInputField.value = value - chatInputField.placeholder = placeholder ?? chatInputField.placeholder + chatInputField.placeholder = placeholder + ?? chatInputField.placeholder chatInputField.focus() } /** @@ -830,82 +903,33 @@ function mStageTransitionMember(includeSidebar=true){ } } /** - * Submits a message to MyLife Member Services chat. - * @async - * @requires chatActiveItem - * @param {string} message - The message to submit - * @param {boolean} hideMemberChat - The hide member chat flag, default=`true` - * @returns {Promise} - The return is the chat response object: { instruction, responses, success, } - */ -async function submit(message, hideMemberChat=true){ - if(!message?.length) - throw new Error('submit(): `message` argument is required') - if(hideMemberChat) - toggleMemberInput(false) - const { action, itemId, } = chatActiveItem.dataset - const { id: botId, } = activeBot() - const request = { - action, - botId, - itemId, - message, - role: 'member', - } - const response = await mGlobals.datamanager.submitChat(request, true) - if(hideMemberChat) - toggleMemberInput(true) - return response -} -/** - * Toggles the member input between input and server `waiting`. + * Toggles the input textarea, currently triggered with `event`. * @public - * @param {boolean} display - Whether to show/hide (T/F), default `true`. - * @param {boolean} hidden - Whether to force-hide (T/F), default `false`. **Note**: used in `experience.mjs` - * @param {boolean} connectingText - The server-connecting text, default: `Connecting with `. + * @requires chatActiveItem + * @requires chatActiveThumb + * @requires chatInputField * @returns {void} */ -function toggleMemberInput(display=true, hidden=false, connectingText='Connecting with '){ - const { id, name, } = activeBot() - if(display){ - hide(awaitButton) - awaitButton.classList.remove('slide-up') - chatInput.classList.add('slide-up') - chatInputField.style.height = 'auto' - chatInputField.placeholder = `type your message to ${ name }...` - chatInputField.value = null - show(chatInput) - } else { - hide(chatInput) - chatInput.classList.remove('fade-in') - chatInput.classList.remove('slide-up') - awaitButton.classList.add('slide-up') - awaitButton.innerHTML = connectingText + name + '...' - show(awaitButton) - } - if(hidden){ - hide(chatInput) - hide(awaitButton) - } -} -/** - * Toggles the input textarea. - * @param {Event} event - The event object. - * @returns {void} - The return is void. - */ -function toggleInputTextarea(event){ +function mToggleInputTextarea(){ chatInputField.style.height = 'auto' // Reset height to shrink if text is removed chatInputField.style.height = chatInputField.scrollHeight + 'px' // Set height based on content - toggleSubmitButtonState() + mToggleSubmitButtonState() + if(chatActiveItem.dataset.inAction==='true') + if(!chatInputField.value.length){ + show(chatActiveItem) + show(chatActiveThumb) + } else { + hide(chatActiveItem) + hide(chatActiveThumb) + } } function mToggleItemPopup(event){ event.stopPropagation() event.preventDefault() const { itemId, } = event.target.dataset - if(!itemId) - console.log('mToggleItemPopup::Error()::`itemId` is required', event.target.dataset, itemId) togglePopup(itemId, true) } -function toggleSubmitButtonState() { +function mToggleSubmitButtonState() { memberSubmit.disabled = !(chatInputField.value?.trim()?.length ?? true) } /** @@ -949,14 +973,14 @@ export { hide, hideMemberChat, inExperience, - parseInstruction, + enactInstruction, replaceElement, sceneTransition, seedInput, setActiveAction, setActiveBot, setActiveItem, - setActiveItemTitle, + updateActiveItemTitle, show, showMemberChat, showSidebar, @@ -964,7 +988,6 @@ export { startExperience, submit, toggleMemberInput, - toggleInputTextarea, toggleVisibility, unsetActiveAction, unsetActiveItem, diff --git a/views/assets/png/editor-thumb.png b/views/assets/png/editor-thumb.png new file mode 100644 index 00000000..992b3bb0 Binary files /dev/null and b/views/assets/png/editor-thumb.png differ diff --git a/views/assets/png/journal-thumb.png b/views/assets/png/journal-thumb.png index 992b3bb0..0e4c0f2e 100644 Binary files a/views/assets/png/journal-thumb.png and b/views/assets/png/journal-thumb.png differ diff --git a/views/assets/png/library-thumb.png b/views/assets/png/library-thumb.png deleted file mode 100644 index 9a0baace..00000000 Binary files a/views/assets/png/library-thumb.png and /dev/null differ