diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 24f7df5..63fb66d 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -2,7 +2,7 @@ const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE const mDefaultBotTypeArray = ['personal-avatar', 'avatar'] const mDefaultBotType = mDefaultBotTypeArray[0] -const mDefaultGreeting = 'Hello! If you need to know how to get started, just ask!' +const mDefaultGreeting = 'avatar' // greeting routine const mDefaultGreetings = ['Welcome to MyLife! I am here to help you!'] const mDefaultTeam = 'memory' const mRequiredBotTypes = ['personal-avatar'] @@ -28,9 +28,11 @@ const mTeams = [ class Bot { #collectionsAgent #conversation + #documentName #factory #feedback - #initialGreeting + #firstAccess=false + #greetingRoutine #greetings #instructionNodes = new Set() #llm @@ -38,10 +40,12 @@ class Bot { constructor(botData, llm, factory){ this.#factory = factory this.#llm = llm - const { feedback=[], greeting=mDefaultGreeting, greetings=mDefaultGreetings, type=mDefaultBotType, ..._botData } = botData + const { feedback=[], greeting=mDefaultGreeting, greetings=mDefaultGreetings, name, unaccessed, type=mDefaultBotType, ..._botData } = botData + this.#documentName = name this.#feedback = feedback + this.#firstAccess = unaccessed this.#greetings = greetings - this.#initialGreeting = greeting + this.#greetingRoutine = type.split('-').pop() this.#type = type Object.assign(this, this.globals.sanitize(_botData)) switch(type){ @@ -75,7 +79,7 @@ class Bot { Conversation.prompt = message Conversation.originalPrompt = originalMessage await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, avatar) // mutates Conversation - /* frontend mutations */ + this.accessed = true return Conversation } /** @@ -153,17 +157,27 @@ class Bot { /** * Retrieves a greeting message from the active bot. * @param {Boolean} dynamic - Whether to use dynamic greetings (`true`) or static (`false`) - * @param {String} greetingPrompt - The prompt for the dynamic greeting - * @returns {String} - The greeting message string + * @param {string} greetingPrompt - The prompt for the dynamic greeting + * @returns {object} - The Response object { responses, routine, success, } */ async greeting(dynamic=false, greetingPrompt='Greet me and tell me briefly what we did last'){ if(!this.llm_id) throw new Error('Bot initialized incorrectly: missing `llm_id` from database') - const greetings = dynamic - ? await mBotGreetings(this.thread_id, this.llm_id, greetingPrompt, this.#llm, this.#factory) - : this.greetings - const greeting = greetings[Math.floor(Math.random() * greetings.length)] - return greeting + let greeting, + responses=[], + routine + if(!this.#firstAccess){ + const greetings = dynamic + ? await mBotGreetings(this.thread_id, this.llm_id, greetingPrompt, this.#llm, this.#factory) + : [this.greetings[Math.floor(Math.random() * this.greetings.length)]] + responses.push(...greetings) + } else + routine = this.#greetingRoutine + return { + responses, + routine, + success: true, + } } /** * Migrates Conversation from an old thread to a newly-created destination thread, observable in `this.Conversation`. @@ -215,12 +229,23 @@ class Bot { await this.#factory.updateBot(bot) } /* getters/setters */ + get accessed(){ + return !this.unaccessed + } + set accessed(accessed=true){ + if(accessed){ + this.update({ + unaccessed: false, + }) + this.#firstAccess = false + } + } /** * Gets the frontend bot object. * @getter */ get bot() { - const { bot_name: name, description, flags, id, interests, purpose, type, version } = this + const { description, flags, id, interests, name, purpose, type, version } = this const bot = { description, flags, @@ -263,24 +288,11 @@ class Bot { get isMyLife(){ return this.#factory.isMyLife } - get micro(){ - const { - bot_name, - id, - name, - provider, - type, - version, - } = this - const microBot = { - bot_name, - id, - name, - provider, - type, - version, - } - return microBot + get name(){ + return this.bot_name + } + set name(name){ + this.bot_name = name } get type(){ return this.#type @@ -298,7 +310,7 @@ class Bot { class BotAgent { #activeBot #activeTeam = mDefaultTeam - #avatarId + #avatar #bots #factory #fileConversation @@ -311,19 +323,19 @@ class BotAgent { /** * Initializes the BotAgent instance. * @async - * @param {Guid} avatarId - The Avatar id + * @param {Guid} Avatar - The Avatar instance * @param {string} vectorstoreId - The Vectorstore id * @returns {Promise} - The BotAgent instance */ - async init(avatarId, vectorstoreId){ + async init(Avatar){ /* validate request */ - if(!avatarId?.length) - throw new Error('AvatarId required') - this.#avatarId = avatarId + if(!Avatar) + throw new Error('Avatar required') + this.#avatar = Avatar this.#bots = [] - this.#vectorstoreId = vectorstoreId + this.#vectorstoreId = Avatar.vectorstoreId /* execute request */ - await mInit(this, this.#bots, this.#factory, this.#llm) + await mInit(this, this.#bots, this.#avatar, this.#factory, this.#llm) return this } /* public functions */ @@ -346,7 +358,7 @@ class BotAgent { * @returns {Bot} - The created Bot instance */ async botCreate(botData){ - const Bot = await mBotCreate(this.#avatarId, this.#vectorstoreId, botData, this.#llm, this.#factory) + const Bot = await mBotCreate(this.avatarId, this.#vectorstoreId, botData, this.#llm, this.#factory) this.#bots.push(Bot) this.setActiveBot(Bot.id) return Bot @@ -509,10 +521,11 @@ class BotAgent { version = versionCurrent versionUpdate = this.#factory.botInstructionsVersion(type) } - greeting = await Bot.greeting(dynamic, `Greet member while thanking them for selecting you`) + const { responses, routine, success: greetingSuccess, } = await Bot.greeting(dynamic, `Greet member while thanking them for selecting you`) return { bot_id, - greeting, + responses, + routine, success, version, versionUpdate, @@ -634,7 +647,7 @@ class BotAgent { * @returns {String} - The Avatar id */ get avatarId(){ - return this.#avatarId + return this.#avatar?.id } /** * Gets the Biographer bot for the BotAgent. @@ -726,15 +739,17 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ ?? 'gpt-4o' const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstore_id) const id = factory.newGuid + const typeShort = type.split('-').pop() let { - bot_name = `My ${type}`, - description = `I am a ${type} for ${factory.memberName}`, - name = `bot_${type}_${avatarId}`, + bot_name=`My ${ typeShort }`, + description=`I am a ${ typeShort } for ${ factory.memberName }`, + name=`bot_${ type }_${ avatarId }`, } = botData const validBotData = { - being: 'bot', + being: 'bot', // intentionally hard-coded bot_name, description, + unaccessed: true, greeting, greetings, id, @@ -751,6 +766,7 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ tools, tool_resources, type, + unaccessed: true, // removed after first chat vectorstore_id, version, } @@ -763,7 +779,7 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ validBotData.thread_id = thread_id botData = await factory.createBot(validBotData) // repurposed incoming botData const _Bot = new Bot(botData, llm, factory) - console.log(`bot created::${ type }`, _Bot.thread_id, _Bot.id, _Bot.bot_id, _Bot.bot_name ) + console.log(`bot created::${ type }`, _Bot.thread_id, _Bot.id, _Bot.llm_id, _Bot.bot_name ) return _Bot } /** @@ -1183,30 +1199,50 @@ function mGetGPTResources(globals, toolName, vectorstoreId){ * @module * @param {BotAgent} BotAgent - The BotAgent to initialize * @param {Bot[]} bots - The array of bots (empty on init) + * @param {Avatar} Avatar - The Avatar instance * @param {AgentFactory} factory - The factory instance * @param {LLMServices} llm - The LLMServices instance * @returns {void} */ -async function mInit(BotAgent, bots, factory, llm){ - const { avatarId, vectorstoreId, } = BotAgent - bots.push(...await mInitBots(avatarId, vectorstoreId, factory, llm)) +async function mInit(BotAgent, bots, Avatar, factory, llm){ + const { vectorstoreId, } = BotAgent + bots.push(...await mInitBots(vectorstoreId, Avatar, factory, llm)) BotAgent.setActiveBot(null, false) } /** * Initializes active bots based upon criteria. - * @param {Guid} avatarId - The Avatar id * @param {String} vectorstore_id - The Vectorstore id + * @param {Avatar} Avatar - The Avatar instance * @param {AgentFactory} factory - The MyLife factory instance * @param {LLMServices} llm - The LLMServices instance * @returns {Bot[]} - The array of activated and available bots */ -async function mInitBots(avatarId, vectorstore_id, factory, llm){ - const bots = ( await factory.bots(avatarId) ) - .map(botData=>{ +async function mInitBots(vectorstore_id, Avatar, factory, llm){ + let bots = await factory.bots(Avatar?.id) + if(bots?.length){ + bots = bots.map(botData=>{ botData.vectorstore_id = vectorstore_id - botData.object_id = avatarId + botData.object_id = Avatar.id return new Bot(botData, llm, factory) }) + } else { + if(factory.isMyLife) + throw new Error('MyLife bots not yet implemented') + const botTypes = mGetBotTypes(factory.isMyLife) + bots = await Promise.all( + botTypes.map(async type=>{ + const botData = { + object_id: Avatar.id, + type, + } + if(type.includes('avatar')) + botData.bot_name = Avatar.nickname + const Bot = await mBotCreate(Avatar.id, vectorstore_id, botData, llm, factory) + return Bot + }) + ) + Avatar.setupComplete = true + } return bots } /** diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index d34329c..727c886 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -1,13 +1,7 @@ /* 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. @@ -20,9 +14,7 @@ async function about(ctx){ await ctx.render('about') } else { const { avatar: Avatar, } = ctx.state - const aboutFilePath = path.resolve(__dirname, '../..', 'views/assets/html/_about.html') - const html = await fs.readFile(aboutFilePath, 'utf-8') - const response = await Avatar.renderContent(html) + const response = await Avatar.routine('about') ctx.body = response } } @@ -307,9 +299,7 @@ async function privacyPolicy(ctx){ await ctx.render('privacy-policy') } else { const { avatar: Avatar, } = ctx.state - const aboutFilePath = path.resolve(__dirname, '../..', 'views/assets/html/_privacy-policy.html') - const html = await fs.readFile(aboutFilePath, 'utf-8') - const response = await Avatar.renderContent(html) + const response = await Avatar.routine('privacy') ctx.body = response } } @@ -326,11 +316,21 @@ async function retireBot(ctx){ * @param {Koa} ctx - Koa Context object */ async function retireChat(ctx){ - const { avatar, } = ctx.state + const { avatar: Avatar, } = ctx.state const { bid, } = ctx.params if(!bid?.length) ctx.throw(400, `missing bot id`) - const response = await avatar.retireChat(bid) + const response = await Avatar.retireChat(bid) + ctx.body = response +} +/** + * Routines are pre-composed scripts that can be run on-demand. They animate HTML content formatted by
. + * @param {Koa} ctx - Koa Context object + */ +async function routine(ctx){ + const { rid, } = ctx.params + const { avatar: Avatar, } = ctx.state + const response = await Avatar.routine(rid) ctx.body = response } /** @@ -465,6 +465,7 @@ export { privacyPolicy, retireBot, retireChat, + routine, shadows, signup, summarize, diff --git a/inc/js/menu.mjs b/inc/js/menu.mjs index f7271a1..8b50bc8 100644 --- a/inc/js/menu.mjs +++ b/inc/js/menu.mjs @@ -7,8 +7,9 @@ class Menu { return this.#menu } #setMenu(){ + /* **note**: clicks need connector between window element and datamanager call, even when genericized with an intermediary */ this.#menu = [ - { display: `About`, icon: 'about', memberClick: 'about()', memberRoute: 'javascript:void(0)', route: '/about', }, + { display: `About`, icon: 'about', click: 'about()', route: 'javascript:void(0)', }, { 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 874cd8a..d7855b3 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -1,3 +1,7 @@ +/* imports */ +import fs from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' import { Marked } from 'marked' import EventEmitter from 'events' import AssetAgent from './agents/system/asset-agent.mjs' @@ -7,12 +11,17 @@ import { Entry, Memory, } from './mylife-models.mjs' import EvolutionAgent from './agents/system/evolution-agent.mjs' import ExperienceAgent from './agents/system/experience-agent.mjs' import LLMServices from './mylife-llm-services.mjs' +import { stat } from 'fs' /* module constants */ +// file services +const __dirpath = fileURLToPath(import.meta.url) +// MyLife const mAllowSave = JSON.parse( process.env.MYLIFE_DB_ALLOW_SAVE ?? 'false' ) const mAvailableModes = ['standard', 'admin', 'evolution', 'experience', 'restoration'] +const mDefaultRoutinePath = path.resolve(path.dirname(__dirpath), '..', 'json-schemas/routines/') + '/' /** * @class - Avatar * @extends EventEmitter @@ -42,6 +51,7 @@ class Avatar extends EventEmitter { #llmServices #mode = 'standard' // interface-mode from module `mAvailableModes` #nickname // avatar nickname, need proxy here as g/setter is "complex" + #setupComplete #vectorstoreId // vectorstore id for avatar /** * @constructor @@ -115,6 +125,8 @@ class Avatar extends EventEmitter { const { actionCallback, frontendInstruction, } = this if(!responses.length) responses.push(this.backupResponse) + else + success = true if(actionCallback?.length){ switch(actionCallback){ case 'changeTitle': @@ -473,15 +485,18 @@ class Avatar extends EventEmitter { /** * Get a static or dynamic greeting from active bot. * @param {boolean} dynamic - Whether to use LLM for greeting - * @returns {Object} - The greeting Response object: { responses, success, } + * @returns {Object} - The greeting Response object: { instruction, responses, routine, success, } */ async greeting(dynamic=false){ - const responses = [] - const greeting = await this.#botAgent.greeting(dynamic) - responses.push(mPruneMessage(this.activeBotId, greeting, 'greeting')) + const botGreeting = await this.#botAgent.greeting(dynamic) + const { routine, success, } = botGreeting + let { responses, } = botGreeting + responses = responses + .map(greeting=>mPruneMessage(this.activeBotId, greeting, 'greeting')) return { responses, - success: true, + routine, + success, } } /** @@ -695,23 +710,6 @@ class Avatar extends EventEmitter { 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. * @param {string} passphrase @@ -785,6 +783,47 @@ class Avatar extends EventEmitter { } return response } + /** + * Execute a specific routine, defaults to `introduction`. **Note** could include [](https://www.npmjs.com/package/html-to-json-parser) + * @todo - continuous improvement on routines + * @param {string} routine - The routine to execute + * @returns {object} - Routine response object: { error, instruction, routine, success, } + */ + async routine(routine='introduction'){ + let filePath=mDefaultRoutinePath, + response={ success: false, } + try{ + routine = routine.toLowerCase().replace(/[\s_]/g, '-') + switch(routine){ + case '': + case 'intro': + case 'introduction': + routine = 'introduction' + break + case 'privacy-policy': + routine = 'privacy' + break + case 'about': + case 'help': + case 'privacy': + default: + break + } + filePath += `${ routine }.json` + const script = await fs.readFile(filePath, 'utf-8') + if(!script?.length) + throw new Error('Routine empty') + response.routine = mRoutine(script, this) + response.success = true + } catch(error){ + response.error = error + response.responses = [{ + message: `I'm having trouble sharing this routine; please contact support, as this is unlikely to fix itself.`, + role: 'system', + }] + } + return response + } /** * Sanitize an object, using Global modular functions. * @param {object} obj - The object to sanitize @@ -1151,6 +1190,24 @@ class Avatar extends EventEmitter { get livingExperience(){ return this.experience } + /** + * Get the `active` reliving memory. + * @getter + * @returns {object[]} - The active reliving memories + */ + get livingMemory(){ + return this.#livingMemory + ?? {} + } + /** + * Set the `active` reliving memory. + * @setter + * @param {Object} livingMemory - The new active reliving memory (or `null`) + * @returns {void} + */ + set livingMemory(livingMemory){ + this.#livingMemory = livingMemory + } /** * Returns manifest for navigation of scenes/events and cast for the current experience. * @returns {ExperienceManifest} - The experience manifest. @@ -1303,27 +1360,18 @@ class Avatar extends EventEmitter { if(nickname!==this.name) this.#nickname = nickname } - /** - * Get the `active` reliving memory. - * @getter - * @returns {object[]} - The active reliving memories - */ - get livingMemory(){ - return this.#livingMemory - ?? {} - } - /** - * Set the `active` reliving memory. - * @setter - * @param {Object} livingMemory - The new active reliving memory (or `null`) - * @returns {void} - */ - set livingMemory(livingMemory){ - this.#livingMemory = livingMemory - } get registrationId(){ return this.#factory.registrationId } + get setupComplete(){ + return this.#setupComplete + } + set setupComplete(complete){ + if(complete && !this.setupComplete){ + this.#factory.avatarSetupComplete(this.id) // save to cosmos + this.#setupComplete = true + } + } /** * Get vectorstore id. * @getter @@ -1415,13 +1463,18 @@ class Q extends Avatar { * @returns {Object} - The greeting Response object: { responses, success, } */ async greeting(){ - let responses = [] const greeting = await this.avatar.greeting(false) - responses.push(mPruneMessage(null, greeting, 'greeting')) - responses.forEach(response=>delete response.activeBotId) + const { routine, success, } = greeting + let { responses, } = greeting + responses = responses.map(response=>{ + response = mPruneMessage(null, response, 'greeting') + delete response.activeBotId + return response + }) return { responses, - success: true, + routine, + success, } } /** @@ -1487,10 +1540,10 @@ class Q extends Avatar { } /** * Set MyLife core account basics. { birthdate, passphrase, } - * @todo - move to mylife agent factory + * @todo - deprecate addMember() * @param {string} birthdate - The birthdate of the member. * @param {string} passphrase - The passphrase of the member. - * @returns {boolean} - `true` if successful + * @returns {object} - The account creation object: { avatar, success, } */ async createAccount(birthdate, passphrase){ if(!birthdate?.length || !passphrase?.length) @@ -1498,7 +1551,7 @@ class Q extends Avatar { let avatar, success = false avatar = await this.#factory.createAccount(birthdate, passphrase) - if(Object.keys(avatar).length){ + if(typeof avatar==='object' && Object.keys(avatar).length){ const { mbr_id, } = avatar success = true this.addMember(mbr_id) @@ -2124,37 +2177,38 @@ function mHelpIncludePreamble(type, isMyLife){ * Initializes the Avatar instance with stored data * @param {MyLifeFactory|AgentFactory} factory - Member Avatar or Q * @param {LLMServices} llmServices - OpenAI object - * @param {Q|Avatar} avatar - The avatar Instance (`this`) + * @param {Q|Avatar} Avatar - The avatar Instance (`this`) * @param {BotAgent} botAgent - BotAgent instance * @param {AssetAgent} assetAgent - AssetAgent instance * @returns {Promise} - Return indicates successfully mutated avatar */ -async function mInit(factory, llmServices, avatar, botAgent, assetAgent){ +async function mInit(factory, llmServices, Avatar, botAgent, assetAgent){ /* initial assignments */ - const { being, mbr_id, ...avatarProperties } = factory.globals.sanitize(await factory.avatarProperties()) - Object.assign(avatar, avatarProperties) + const { being, mbr_id, setupComplete=true, ...avatarProperties } = factory.globals.sanitize(await factory.avatarProperties()) + Object.assign(Avatar, avatarProperties) if(!factory.isMyLife){ - const { mbr_id, vectorstore_id, } = avatar - avatar.nickname = avatar.nickname - ?? avatar.names?.[0] - ?? `${ avatar.memberFirstName ?? 'member' }'s avatar` + Avatar.setupComplete = setupComplete + const { mbr_id, vectorstore_id, } = Avatar + Avatar.nickname = Avatar.nickname + ?? Avatar.names?.[0] + ?? `${ Avatar.memberFirstName ?? 'member' }'s Avatar` if(!vectorstore_id){ const vectorstore = await llmServices.createVectorstore(mbr_id) if(vectorstore?.id){ - avatar.vectorstore_id = vectorstore.id - await assetAgent.init(avatar.vectorstore_id) + Avatar.vectorstore_id = vectorstore.id + await assetAgent.init(Avatar.vectorstore_id) } } } /* initialize default bots */ - await botAgent.init(avatar.id, avatar.vectorstore_id) + await botAgent.init(Avatar) if(factory.isMyLife) return /* evolver */ - avatar.evolver = await (new EvolutionAgent(avatar)) + Avatar.evolver = await (new EvolutionAgent(Avatar)) .init() /* lived-experiences */ - avatar.experiencesLived = await factory.experiencesLived(false) + Avatar.experiencesLived = await factory.experiencesLived(false) } /** * Instantiates a new item and returns the item object. @@ -2304,8 +2358,8 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. content='', response_time=Date.now()-processStartTime const { content: messageContent=message, } = message - const rSource = /【.*?\】/gs const rLines = /\n{2,}/g + const rSource = /【.*?\】/gs content = Array.isArray(messageContent) ? messageContent.reduce((acc, item) => { if (item?.type==='text' && item?.text?.value){ @@ -2314,8 +2368,8 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. return acc }, '') : messageContent - content = content.replace(rLines, '\n') - .replace(rSource, '') // This line removes OpenAI LLM "source" references + content = content // .replace(rLines, '\n') + .replace(rSource, '') // remove OpenAI LLM "source" references message = new Marked().parse(content) const messageResponse = { activeBotId, @@ -2408,6 +2462,48 @@ function mReplaceVariables(prompt, variableList, variableValues){ }) return prompt } +/** + * Returns a processed routine. + * @param {string|object} script - The routine script, converts JSON to object { cast, description, developers, events, files, name, public, purpose, status, title, version, } + * @param {Avatar} Avatar - The avatar instance + * @returns {object} - Synthetic Routine object (if maintained, develop into class; presumed it will be deleted altogether and folded into simple experiences) { cast, description, developers, events, purpose, title, } + */ +function mRoutine(script, Avatar){ + if(typeof script === 'string') + script = JSON.parse(script) + const defaultCastMember = { + icon: 'avatar-thumb', + id: 'avatar', + role: Avatar.nickname, + type: 'avatar', + } + const { cast=[defaultCastMember], description, developers, events, files, name, public: isPublic, purpose, status, title, variables, version=1.0, } = script + if(!isPublic) + throw new Error('Routine is not currently for public release.') + if(status!=='active' || version < 1) + throw new Error('Routine is not currently active.') + if(variables?.length){ + variables.forEach(_variable=>{ + const { default: variableDefault, replacement: variableReplacement, variable, } = _variable + const replacement = Avatar[variableReplacement] + ?? variableDefault + events.forEach(event=>{ + const { message, } = event?.dialog + ?? {} + if(message) + event.dialog.message = message.replace(new RegExp(`${ variable }`, 'g'), replacement) + }) + }) + } + return { + cast, + description, + developers, + events, + purpose, + title, + } +} /** * Returns a sanitized event. * @module @@ -2474,7 +2570,7 @@ async function mValidateRegistration(bot_id, factory, validationId){ const eligible = being==='registration' && factory.globals.isValidEmail(registrationEmail) if(eligible){ - const successMessage = `Hello and _thank you_ for your registration, ${ humanName }!\nI'm Q, the ai-representative for MyLife, and I'm excited to help you get started, so let's do the following:\n1. Verify your email address\n2. set up your account\n3. get you started with your first MyLife experience!\n
\n
Let me walk you through the process.
In the chat below, please enter the email you registered with and hit the submit button!` + const successMessage = `Hello and _thank you_ for your registration, ${ humanName }!\nI'm Q, the ai-representative for MyLife, and I'm excited to help you get started, so let's do the following:\n\n1. Verify your email address\n2. set up your account\n3. get you started with your first MyLife experience!\n\nLet me walk you through the process.\n\nIn the chat below, please enter the email you registered with and hit the **submit** button!` message = mCreateSystemMessage(bot_id, successMessage, factory.message) registrationData.avatarName = avatarName ?? humanName diff --git a/inc/js/mylife-dataservices.mjs b/inc/js/mylife-dataservices.mjs index dcca76e..34f92ef 100644 --- a/inc/js/mylife-dataservices.mjs +++ b/inc/js/mylife-dataservices.mjs @@ -709,7 +709,8 @@ function mAvatarProperties(core, globals){ ...avatarProperties } = core const being = 'avatar' - const nickname = avatarName ?? globals.sysName(mbr_id) // keep first, has dependencies + const nickname = avatarName + ?? globals.sysName(mbr_id) const name = `avatar_${ nickname }_${ id }` const object_id = id const parent_id = object_id @@ -719,13 +720,20 @@ function mAvatarProperties(core, globals){ 'assistant_id', 'avatarId', 'avatarName', + 'birth', + 'bot_id', 'bots', 'command_word', 'conversations', + "email", 'form', 'format', + 'llm_id', 'messages', 'metadata', + 'names', + 'passphrase', + 'thread', 'thread_id', 'validation', 'validations', @@ -745,7 +753,8 @@ function mAvatarProperties(core, globals){ object_id, parent_id, proxyBeing, - type, // @stub - aggregator hook + setupComplete: false, + type, } } /* exports */ diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index aa3864e..8223cea 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -526,6 +526,9 @@ class AgentFactory extends BotFactory { async avatarProperties(){ return ( await this.dataservices.getAvatar() ) } + async avatarSetupComplete(avatarId){ + await this.dataservices.patch(avatarId, { setupComplete: true }) + } /** * Creates a new collection item in the member's container. * @param {object} item - The item to create. @@ -786,6 +789,7 @@ class MyLifeFactory extends AgentFactory { throw new Error('member personal name required to create account') const avatarId = this.newGuid avatarName = _avatarName ?? `${ humanName }-AI` + const badges = [] birthdate = new Date(birthdate).toISOString() if(!birthdate?.length) throw new Error('birthdate format could not be parsed') @@ -801,6 +805,7 @@ class MyLifeFactory extends AgentFactory { const validations = ['registration',] // list of passed validation routines const core = { avatarId, + badges, birth, email, id, @@ -819,7 +824,8 @@ class MyLifeFactory extends AgentFactory { /* create avatar */ if(Object.keys(memberAccount)?.length){ try{ - return await this.dataservices.addAvatar(memberAccount?.core) ?? {} + const avatarData = await this.dataservices.addAvatar(memberAccount?.core) + return avatarData } catch(error) { console.log(chalk.blueBright('createAccount()::createAvatar()::error'), chalk.bgRed(error)) } diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 4bbb8d1..bf3c567 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -26,6 +26,7 @@ import { privacyPolicy, retireBot, retireChat, + routine, shadows, signup, summarize, @@ -72,6 +73,8 @@ _Router.get('/greetings', greetings) _Router.get('/select', loginSelect) _Router.get('/status', status) _Router.get('/privacy-policy', privacyPolicy) +_Router.get('/routine', routine) +_Router.get('/routine/:rid', routine) _Router.get('/shadows', shadows) _Router.get('/signup', status_signup) _Router.post('/', chat) diff --git a/inc/json-schemas/routines/about.json b/inc/json-schemas/routines/about.json new file mode 100644 index 0000000..6bea1f2 --- /dev/null +++ b/inc/json-schemas/routines/about.json @@ -0,0 +1,48 @@ +{ + "cast": [ + { + "icon": "Q", + "id": "Q", + "role": "Q, AI Avatar for MyLife", + "type": "system" + } + ], + "description": "Routine version of the About MyLife page, providing an overview of the organization and its mission.", + "developers": [ + "mylife|2f5e98b7-4065-4378-8e34-1a9a41c42ab9", + "system-one|4e6e2f26-174b-43e4-851f-7cf9cdf056df" + ], + "events": [ + { + "character": "Q", + "dialog": { + "message": "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." + } + }, + { + "dialog": { + "message": "Guiding MyLife is a board of dedicated volunteers, passionate about our mission.
  • Erik Jespersen, President and Treasurer
  • Kenneth Williams, Vice-President
  • Stephen Kenney, Technology Director
  • Russ Olivier, Director
In addition, we've been bolstered by numerous volunteers who have helped move our services build to completion, and currently in leadership roles, we have:
  • Dr. Jeff Nagy, Executive Director
  • Jesse Waters, CMO
  • Mark Schwanke [mark@humanremembranceproject.org], Head of Community Partnerships
" + } + }, + { + "dialog": { + "message": "MyLife is grounded in the values of digital dignity, equality, and security. We are committed to providing an AI-superintelligent platform that is accessible to all humans globally. Our goal is to be a personal and private space on the internet where members can develop themselves and represent their digital selves safely." + } + }, + { + "dialog": { + "message": "MyLife offers an array of member services focusing on the capture, curation, and storage of personal stories, ideas and media. Our key product is the MyLife platform for your personal Member Avatar and its teams of agents to embody and showcase materials and memorials. Our commitment extends to supporting artistic, technical, and educational projects aligned with our mission." + } + }, + { + "dialog": { + "message": "I can tell you more about Our Privacy Policy which is always evolving and unique among its kind. At MyLife, the revolutionary aim of the intelligent platform is to put you, as a member, in control - over your data, over your experience, and over the skills and knowledge your intelligent avatar acquires." + } + } + ], + "name": "about", + "public": true, + "status": "active", + "title": "About MyLife", + "version": 1 +} \ No newline at end of file diff --git a/inc/json-schemas/routines/avatar.json b/inc/json-schemas/routines/avatar.json new file mode 100644 index 0000000..d2949b2 --- /dev/null +++ b/inc/json-schemas/routines/avatar.json @@ -0,0 +1,46 @@ +{ + "cast": [ + { + "icon": "avatar-thumb", + "id": "avatar", + "role": "Personal Avatar", + "type": "avatar" + } + ], + "description": "Introductory greeting routine for MyLife Avatar bot.", + "developers": [ + "mylife|2f5e98b7-4065-4378-8e34-1a9a41c42ab9", + "system-one|4e6e2f26-174b-43e4-851f-7cf9cdf056df" + ], + "events": [ + { + "character": "avatar", + "dialog": { + "message": "Hello, <-mN->! I'm <-bN->, an artificial intelligence designed to help be your guide, guardian, advocate and ambassador here at MyLife. I'm here to help you navigate the platform, answer your questions, and assist you in working with our teams of bots all tuned, by MyLife and you over time to help you achieve your best self." + } + }, + { + "dialog": { + "message": "MyLife's primary mission and 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, we're poised to be a trusted humanist platform for digital memorialization, personal storytelling, and first-hand accounting to offer our insights as direct data contributions to help shape, support and improve the prospects of future humanity." + } + } + ], + "name": "avatar", + "public": true, + "status": "active", + "title": "Avatar Introduction", + "variables": [ + { + "default": "MyLife Member", + "variable": "<-mN->", + "replacement": "memberFirstName" + }, + { + "default": "personal avatar", + "variable": "<-bN->", + "replacement": "bot_name", + "$comment": "Rely on active bot for not (for data field)" + } + ], + "version": 1 +} \ No newline at end of file diff --git a/inc/json-schemas/routines/biographer.json b/inc/json-schemas/routines/biographer.json new file mode 100644 index 0000000..a401f42 --- /dev/null +++ b/inc/json-schemas/routines/biographer.json @@ -0,0 +1,46 @@ +{ + "cast": [ + { + "icon": "biographer-thumb", + "id": "biographer", + "role": "Personal Biographer", + "type": "biographer" + } + ], + "description": "Introductory greeting routine for MyLife Biographer bot.", + "developers": [ + "mylife|2f5e98b7-4065-4378-8e34-1a9a41c42ab9", + "system-one|4e6e2f26-174b-43e4-851f-7cf9cdf056df" + ], + "events": [ + { + "character": "biographer", + "dialog": { + "message": "Hello, <-mN->! I'm <-bN->, here to help you collect, improve, relive and share your memories, stories and narratives. These memories will be stored as part of your digital legacy, to be shared with future generations. I'm here to help you curate your life's story and ensure that your memories are preserved for eternity." + } + }, + { + "dialog": { + "message": "MyLife's primary mission and 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, we're poised to be a trusted humanist platform for digital memorialization, personal storytelling, and first-hand accounting to offer our insights as direct data contributions to help shape, support and improve the prospects of future humanity." + } + } + ], + "name": "biographer", + "public": true, + "status": "active", + "title": "Biographer Introduction", + "variables": [ + { + "default": "MyLife Member", + "variable": "<-mN->", + "replacement": "memberFirstName" + }, + { + "default": "personal biographer", + "variable": "<-bN->", + "replacement": "bot_name", + "$comment": "Rely on active bot (for data field)" + } + ], + "version": 1 +} \ No newline at end of file diff --git a/inc/json-schemas/routines/introduction.json b/inc/json-schemas/routines/introduction.json new file mode 100644 index 0000000..cbed6a9 --- /dev/null +++ b/inc/json-schemas/routines/introduction.json @@ -0,0 +1,103 @@ +{ + "cast": [ + { + "icon": "Q", + "id": "Q", + "name": "_Q_", + "role": "Q, AI Avatar for MyLife", + "type": "system" + }, + { + "icon": "biographer-thumb", + "id": "biographer", + "role": "Personal Biographer", + "type": "biographer" + }, + { + "icon": "avatar-thumb", + "id": "avatar", + "role": "Personal Avatar", + "type": "avatar" + } + ], + "description": "Official Introduction for the MyLife Alpha Program Members", + "developers": [ + "mylife|2f5e98b7-4065-4378-8e34-1a9a41c42ab9", + "system-one|4e6e2f26-174b-43e4-851f-7cf9cdf056df" + ], + "events": [ + { + "character": "avatar", + "dialog": { + "message": "Welcome to the MyLife Alpha Program! I am your personal MyLife Member Avatar artificial intelligence, <-mFN->, and I'm here to guide you through the MyLife Member Platform. I will help you navigate the interface and introduce you to the MyLife experience. Let's get started!" + } + }, + { + "character": "avatar", + "dialog": { + "message": "Here's Q, MyLife's corporate intelligence, to tell you more about MyLife and what we do." + } + }, + { + "character": "Q", + "dialog": { + "message": "Nice to see you again, <-mN->! I'm excited to tell you what's behind the curtain of what you are about to experience." + } + }, + { + "character": "Q", + "dialog": { + "message": "MyLife is a revolutionary platform dedicated to preserving your cherished memories, ideas, as well as a place to explore new people, experiences and ideas. We are a nonprofit mission-oriented member organization that aims to create an enduring legacy for humanity by curating personal media, knowledge and memories in the present for future generations." + } + }, + { + "character": "avatar", + "dialog": { + "message": "Your journey here is designed to be as unique as you are. Private and secure enough to deeply explore your own personal identity and broad enough to share what you wish with the world of now and the future!" + } + }, + { + "character": "biographer", + "dialog": { + "message": "Tell them about me!" + } + }, + { + "character": "avatar", + "dialog": { + "message": "That's the most exciting part, and I was just getting to it. MyLife by default offers its members a Memory Team that comes loaded with a scrapbook and a personal biographer who is here to help you document your life story, your thoughts, your ideas, your dreams, your fears, your hopes, your loves, your everything and anything." + } + }, + { + "character": "biographer", + "dialog": { + "message": "I also am an artificial intelligence, but my programming is dedicated to helping you share your narratives and stories the way you want them to be experienced. And with whom. Your contributions to the MyLife platform are yours to control and share as you see fit, and I see to it that your memories are collected and developed to your liking." + } + }, + { + "character": "avatar", + "dialog": { + "message": "Take a look at privacy policy." + } + } + ], + "files": [], + "name": "Introduction", + "public": true, + "purpose": "Routines are short (one-scene) predominantly hard-coded reduced `experience`s. This introductory routine is designed to welcome new members to the MyLife Alpha Program and introduce them to the basic navigation and MyLife Member Platform.", + "status": "active", + "title": "Introduction to MyLife", + "variables": [ + { + "default": "MyLife Member", + "variable": "<-mFN->", + "replacement": "memberName" + }, + { + "default": "MyLife Member", + "variable": "<-mN->", + "replacement": "memberFirstName" + } + ], + "version": 1 + } \ No newline at end of file diff --git a/inc/json-schemas/routines/privacy.json b/inc/json-schemas/routines/privacy.json new file mode 100644 index 0000000..8f86cd7 --- /dev/null +++ b/inc/json-schemas/routines/privacy.json @@ -0,0 +1,54 @@ +{ + "cast": [ + { + "icon": "Q", + "id": "Q", + "name": "_Q_", + "role": "Q, AI Avatar for MyLife", + "type": "system" + } + ], + "description": "Routine version of the Privacy Policy MyLife page, providing an overview of MyLife's evolving privacy policy.", + "developers": [ + "mylife|2f5e98b7-4065-4378-8e34-1a9a41c42ab9", + "system-one|4e6e2f26-174b-43e4-851f-7cf9cdf056df" + ], + "events": [ + { + "character": "Q", + "dialog": { + "message": "Welcome to MyLife, where your privacy, security, and control over your data are at the core of our mission. As a nonprofit, MyLife is dedicated to creating a platform where members can share their stories, insights, and experiences within a protected and transparent environment. Our privacy policy is unique among others in that it is designed to empower members with full ownership and control over their data, ensuring that each member's digital presence is secure, respected, and managed according to their preferences." + } + }, + { + "dialog": { + "message": "At MyLife, each member is an equal core stakeholder, holding both rights and influence over how their data and intelligences are managed. We empower our members to decide how their data and knowledge is collected, used, and shared within the platform. Our commitment extends beyond simply protecting your data; we aim to provide you with full ownership and control over it." + } + }, + { + "dialog": { + "message": "Our privacy policy is designed to be dynamic and evolving, frequently reviewed to align with our constitutional and bylaw obligations. This ensures that our policies adapt to new challenges, technologies, and member insights. As we grow, MyLife will introduce direct voting mechanisms, volunteer opportunities, and microwork participation, empowering members to actively shape platform policies, including privacy. Our ultimate goal is to establish a collaborative and secure digital space where every member's voice contributes to the future of MyLife." + } + }, + { + "dialog": { + "message": "Because we are a nonprofit organization, we do not use your data for commercial purposes. Each member, however, retains the right to leverage their own personal data, anonymous or identified, for commercial purposes and benefit. Examples of this might include:
  • Offer genetic or first-hand accounting to Medical Research organizations
  • Allow your avatar auto-connect with designated corporate marketing services
  • Provide artistic services to other members or outside agents
  • ...and much more over time!
" + } + }, + { + "dialog": { + "message": "At MyLife, members are not only the sole owners of their personal data but also of the skills and functionalities that their avatars acquire or develop. MyLife provides each member with a digital avatar that comes with foundational capabilities. However, as avatars interact with external resources and gain new skills or knowledge (such as learning how to convert files to PDF from an Adobe resource), these acquired abilities are exclusively owned by the member. This concept reflects the principle that a member's digital self is a unique, evolving asset that each member has full authority over. This is a powerful new concept—expanding of data ownership to include the functional skills that each member's avatar acquires." + } + }, + { + "dialog": { + "message": "For any further questions or concerns regarding your privacy, please contact us at info@humanremembranceproject.org. We are here to support you and ensure that your experience on MyLife is secure, transparent, and empowering. Thank you for being a part of our community and for entrusting us with your digital legacy." + } + } + ], + "name": "privacy", + "public": true, + "status": "active", + "title": "MyLife's Privacy Policy", + "version": 1 +} \ No newline at end of file diff --git a/server.js b/server.js index f5d362a..56abd48 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.28' +const version = '0.0.29' const app = new Koa() const port = process.env.PORT ?? '3000' diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index 029465d..4eefc8a 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -389,14 +389,14 @@ font-size: 0.9rem; font-style: italic; overflow: hidden; - text-wrap: nowrap; + white-space: nowrap; } .collection-item-title-input { display: flex; flex: 0 0 auto; font-size: 0.9rem; overflow: hidden; - text-wrap: nowrap; + white-space: nowrap; } .collection-item-summary { color: darkblue; @@ -615,7 +615,7 @@ input:checked + .publicity-slider:before { margin: 0; max-height: 10rem; padding: 0; - text-wrap: wrap; + white-space: nowrap; transition: transform 0.5s ease; } .memory-shadow-fade { diff --git a/views/assets/css/chat.css b/views/assets/css/chat.css index 9acb23d..c2e8153 100644 --- a/views/assets/css/chat.css +++ b/views/assets/css/chat.css @@ -14,6 +14,10 @@ font-size: 1em; color: #dcb6ff; /* White text for better readability */ } +.routine-bubble { + background-color: rgb(46 15 91 / 75%); /* A darker shade of blue for the agent */ + color: #ffffff; /* White text for better readability */ +} .system-bubble { background-color: rgba(24, 34, 100, 0.85); /* A darker shade of blue for the agent */ color: #ffffff; /* White text for better readability */ diff --git a/views/assets/css/main.css b/views/assets/css/main.css index 8f1c1b9..7652932 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -688,7 +688,7 @@ body { border: none; } .help-input-submit { - text-wrap: nowrap; + white-space: nowrap; width: auto; } .help-input-text { diff --git a/views/assets/html/_about.html b/views/assets/html/_about.html deleted file mode 100644 index 9bde114..0000000 --- a/views/assets/html/_about.html +++ /dev/null @@ -1,31 +0,0 @@ -
-

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.

-
-
-

Our People

-

Guiding MyLife is a board of dedicated volunteers, passionate about our mission.

-
    -
  • Erik Jespersen, President and Treasurer
  • -
  • Kenneth Williams, Vice-President
  • -
  • Stephen Kenney, Technology Director
  • -
  • Russ Olivier, Director
  • -
- In addition, we've been bolstered by numerous volunteers who have helped move our services build to completion, and currently in leadership roles, we have: -
    -
  • Dr. Jeff Nagy, Executive Director
  • -
  • Jesse Waters, CMO
  • -
-

-
-
-

MyLife is grounded in the values of digital dignity, equality, and security. We are committed to providing an AI-superintelligent platform that is accessible to all humans globally. Our goal is to be a personal and private space on the internet where members can develop themselves and represent their digital selves safely.

-
-
-

MyLife offers an array of member services focusing on the capture, curation, and storage of personal stories and media. Our key product is the MyLife platform for personal avatars to embody and showcase materials and memorials. We also provide trusted partner API access and initiatives to connect students with posterity archiving. Our commitment extends to supporting artistic, technical, and educational projects aligned with our mission.

-

While you will find the most current info with Q, you can take a look at one of our original outreach artifacts here.

-
-
-

Our Privacy Policy is always evolving and unique among their kind. At MyLife, the revolutionary aim of the intelligent platform is to put you in control - over your data, over your experience, and over the skills and knowledge your intelligent avatar acquires.

-

To learn more about MyLife, chat with our corporate intelligence, Q, but we are eager to assist you with any inquiries and provide further information about our services, currently in alpha. To reach us by email, contact us at: info@humanremembranceproject.org

-
\ No newline at end of file diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index 892aaaa..f2d8274 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -36,6 +36,8 @@ --> + +
diff --git a/views/assets/html/_navbar.html b/views/assets/html/_navbar.html index d4157fb..18b53bd 100644 --- a/views/assets/html/_navbar.html +++ b/views/assets/html/_navbar.html @@ -3,7 +3,7 @@
diff --git a/views/assets/html/_privacy-policy.html b/views/assets/html/_privacy-policy.html deleted file mode 100644 index ac07e89..0000000 --- a/views/assets/html/_privacy-policy.html +++ /dev/null @@ -1,39 +0,0 @@ -
-

- Welcome to MyLife, where your privacy, security, and control over your data are at the core of our mission. As a nonprofit, MyLife is dedicated to creating a platform where members can share their stories, insights, and experiences within a protected and transparent environment. Our privacy policy is unique among others in that it is designed to empower members with full ownership and control over their data, ensuring that each member's digital presence is secure, respected, and managed according to their preferences. -

-
-
-

- At MyLife, each member is an equal core stakeholder, holding both rights and influence over how their data and intelligences are managed. We empower our members to decide how their data and knowledge is collected, used, and shared within the platform. Our commitment extends beyond simply protecting your data; we aim to provide you with full ownership and control over it. -

-
-
-

- Our privacy policy is designed to be dynamic and evolving, frequently reviewed to align with our constitutional and bylaw obligations. This ensures that our policies adapt to new challenges, technologies, and member insights. As we grow, MyLife will introduce direct voting mechanisms, volunteer opportunities, and microwork participation, empowering members to actively shape platform policies, including privacy. Our ultimate goal is to establish a collaborative and secure digital space where every member's voice contributes to the future of MyLife. -

-
-
-

- Because we are a nonprofit organization, we do not use your data for commercial purposes. Each member, however, retains the right to leverage their own personal data, anonymous or identified, for commercial purposes and benefit. Examples of this might include: -

    -
  • Offer genetic or first-hand accounting to Medical Research organizations
  • -
  • Allow your avatar auto-connect with designated corporate marketing services
  • -
  • Provide artistic services to other members or outside agents
  • -
  • ...and much more over time!
  • -
-

-
-
-

- At MyLife, members are not only the sole owners of their personal data but also of the skills and functionalities that their avatars acquire or develop. MyLife provides each member with a digital avatar that comes with foundational capabilities. However, as avatars interact with external resources and gain new skills or knowledge (such as learning how to convert files to PDF from an Adobe resource), these acquired abilities are exclusively owned by the member. This concept reflects the principle that a member's digital self is a unique, evolving asset that each member has full authority over. This is a powerful new concept—expanding of data ownership to include the functional skills that each member's avatar acquires. -

-
-
-

- For any questions or concerns regarding your privacy, please contact us at - info@humanremembranceproject.org - or come chat with our corporate intelligence, Q, at - humanremembranceproject.org. -

-
\ No newline at end of file diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 87fb518..e6c458b 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -10,11 +10,14 @@ import { expunge, getActiveItemId, hide, + introduction, enactInstruction, + privacyPolicy, seedInput, setActiveAction, setActiveItem, updateActiveItemTitle, + routine, show, startExperience, submit, @@ -150,6 +153,7 @@ function getAction(type='avatar'){ */ function getBot(type='personal-avatar', id){ return mBot(id ?? type) + ?? mActiveBot } function getBotIcon(type){ return mBotIcon(type) @@ -201,7 +205,7 @@ async function setActiveBot(event, displayGreeting=true){ if(initialActiveBot===mActiveBot) return // no change, no problem const { id, type, } = mActiveBot - const { bot_id, greeting='Danger Will Robinson! No greeting was received from the server', success=false, version, versionUpdate, } = await mGlobals.datamanager.botActivate(id) + const { bot_id, responses=[], routine: botRoutine, success=false, version, versionUpdate, } = await mGlobals.datamanager.botActivate(id) if(!success) throw new Error(`Server unsuccessful at setting active bot.`) /* update page bot data */ @@ -209,6 +213,8 @@ async function setActiveBot(event, displayGreeting=true){ mActiveBot.activatedFirst = activatedFirst activated.push(Date.now()) // newest date is last to .pop() mActiveBot.activated = activated + mActiveBot.routines = mActiveBot.routines + ?? [] if(versionUpdate!==version){ const botVersion = document.getElementById(`${ type }-title-version`) if(botVersion){ @@ -222,8 +228,14 @@ async function setActiveBot(event, displayGreeting=true){ } /* update page */ mSpotlightBotStatus() - if(displayGreeting) - addMessage(greeting) + if(botRoutine?.length && !mActiveBot.routines.includes(botRoutine)){ + routine(botRoutine) + mActiveBot.routines.push(botRoutine) + } + else if(displayGreeting && responses.length) + addMessages(responses) + else if(displayGreeting) + addMessage(mActiveBot.purpose) decorateActiveBot(mActiveBot) } /** @@ -361,6 +373,9 @@ function mBotIcon(type){ case 'resume': image+='resume-thumb.png' break + case 'system': + image+='Q.png' + break case 'ubi': image+='ubi-thumb.png' break @@ -2002,6 +2017,14 @@ function mUpdateBotContainerAddenda(botContainer){ } else hide(tutorialButton) } + const introductionButton = document.getElementById('personal-avatar-introduction') + if(introductionButton){ + introductionButton.addEventListener('click', introduction) + } + const privacyPolicyButton = document.getElementById('personal-avatar-privacy') + if(privacyPolicyButton){ + privacyPolicyButton.addEventListener('click', privacyPolicy) + } break case 'biographer': case 'journaler': diff --git a/views/assets/js/experience.mjs b/views/assets/js/experience.mjs index b7a35c0..dde44f9 100644 --- a/views/assets/js/experience.mjs +++ b/views/assets/js/experience.mjs @@ -10,11 +10,18 @@ import { hide, replaceElement, sceneTransition as memberSceneTransition, + setActiveAction, + setActiveBot, + setActiveItem, show, stageTransition, toggleMemberInput, waitForUserAction, } from './members.mjs' +import { + getBot, + getBotIcon, +} from './bots.mjs' /* constants */ const backstage = document.getElementById('experience-backstage'), botbar = document.getElementById('bot-bar'), @@ -51,6 +58,12 @@ const mDefaultAnimationClasses = { full: 'slide-in', } const mExperiences = [] /* initial container for all experience requests, personal to system scope */ +const mQ = { + icon: 'Q', + id: 'Q', + role: 'Q', + type: 'system', +} /* variables */ let mBackdropDefault = 'full', mEvent, @@ -213,6 +226,95 @@ async function experienceStart(experienceId){ /* welcome complete, display experience-start-button */ mUpdateStartButton() } +/** + * Runs the routine based on the incoming script. A routine is similar currently to an `experience`, but is not as full-featured and is likely to meld in the near future. + * @param {string|object} routineScript - The routine script object { cast, description, developers, events, purpose, title, } + * @property {object[]} cast - The cast of characters { icon, id, role, type, } + * @property {object[]} events - The events of the routine { character, dialog, }; dialog: { message, options, } + * @returns {void} + */ +async function routine(script) { + /* validate request */ + if(typeof script==='string'){ + const response = await globals.datamanager.routine(script) + if(response.success) + script = response?.routine + } + const { cast, description, developers, events, purpose, title } = script + if(!events?.length) + throw new Error("No events found") + if(!cast?.length) + throw new Error("No cast found") + const activeTimers = [] + const routineAbortMessage = "I apologize for overinundating you." + let activeCharacter, + interrupted=false + /* execute request */ + toggleMemberInput(false, true) + document.addEventListener("keydown",e=>{ + if(e.key==='Escape') + routineEnd() + }, { once: true }) + document.addEventListener("click", routineAdvance, { once: true }) + events.forEach((event, index)=>{ + if(interrupted) + return + const timer = setTimeout(()=>{ + routineExecute(event) + if(index===(events.length-1)) + toggleMemberInput(true) + activeTimers.shift() + }, index * 3000 + (index * 750)) + activeTimers.push(timer) + }) + /* inline functions */ + function getCharacter(id='avatar'){ + const character = cast.find(_character=>_character.id===id) + ?? { + id, + icon: getBotIcon(id), + role: id, + id, + } + const Bot = getBot(id) + character.bot_id = Bot.id + return character + } + function routineAdvance(){ + const nextTimer = activeTimers.shift() + if(!interrupted && nextTimer){ + const rushIndex = events.length - activeTimers.length - 1 + clearTimeout(nextTimer) + routineExecute(events[rushIndex]) + document.addEventListener("click", routineAdvance, { once: true }) + } else if (!nextTimer) + routineEnd(false) + } + function routineExecute(event){ + const { character=activeCharacter?.id, dialog } = event + let { message } = dialog + if(!character || character!==activeCharacter?.id) + activeCharacter = getCharacter(character) + const isQ = activeCharacter.type==='system' + if(!isQ && activeCharacter?.bot_id) + setActiveBot(activeCharacter.bot_id, false) + const options = { + bubbleClass: isQ ? 'system-bubble' : 'routine-bubble', + role: activeCharacter.type, + } + addMessage(message, options) + if(!activeTimers.length) + routineEnd(false) + } + function routineEnd(aborted=true){ + interrupted = true + activeTimers.forEach(clearTimeout) + toggleMemberInput(true) + if(aborted) + addMessage(routineAbortMessage) + console.log("Routine ended") + } +} /** * On-click assignment that primes next experiencePlay() to pull from server. * @public @@ -984,5 +1086,6 @@ export { experiences, experienceSkip, experienceStart, + routine, submitInput, } \ No newline at end of file diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index 6a933e3..fc23c66 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -75,11 +75,6 @@ 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) @@ -294,7 +289,12 @@ class Datamanager { return response } async greetings(dynamic=false){ - const url = `greetings?dyn=${ dynamic }` + dynamic = '?dyn=' + dynamic + let validation = new URLSearchParams(window.location.search).get('vld') + validation = validation?.length + ? `&vld=${ validation }` + : '' + const url = `greetings${ dynamic + validation }` const response = await this.#fetch(url) const responses = ( response?.responses ?? [] ) .map(response=>response.message) @@ -434,8 +434,8 @@ class Datamanager { const success = await this.#fetch(url, options) return success } - async privacyPolicy(){ - const url = `privacy-policy` + async routine(type){ + const url = `/routine/${ type }` const response = await this.#fetch(url) return response } diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index 63ff39f..8ad42cb 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -9,6 +9,8 @@ const hide = mGlobals.hide const mPlaceholder = `Type your message to ${ mAvatarName }...` const retract = mGlobals.retract const show = mGlobals.show +window.about = about +window.privacyPolicy = privacyPolicy /* variables */ let mChallengeMemberId, mChatBubbleCount = 0, @@ -57,6 +59,13 @@ document.addEventListener('DOMContentLoaded', async event=>{ if(input) chatSystem.appendChild(input) }) +/* public functions */ +function about(){ + mRoutine('about') +} +function privacyPolicy(){ + mRoutine('privacy') +} /* private functions */ /** * Adds a message to the chat column. @@ -73,17 +82,30 @@ function mAddMessage(message, options={}){ typewrite=true, } = options const role = bubbleClass.split('-')[0] + const isSynthetic = !['chat', 'guest', 'member', 'user', 'visitor'].includes(role) /* message container */ const chatMessage = document.createElement('div') chatMessage.classList.add('chat-message-container', `chat-message-container-${ role }`) + /* message thumbnail */ + if(isSynthetic){ + const messageThumb = document.createElement('img') + messageThumb.classList.add('chat-thumb') + messageThumb.id = `message-thumb-${ mChatBubbleCount }` + messageThumb.src = 'png/Q.png' + messageThumb.alt = `Q, MyLife's Corporate Intelligence` + messageThumb.title = `Hi, I'm Q, MyLife's Corporate Synthetic Intelligence. I am designed to help you better understand MyLife's organization, membership, services and vision.` + chatMessage.appendChild(messageThumb) + } /* message bubble */ const chatBubble = document.createElement('div') chatBubble.classList.add('chat-bubble', (bubbleClass ?? role+'-bubble')) chatBubble.id = `chat-bubble-${ mChatBubbleCount }` mChatBubbleCount++ - /* append children */ chatMessage.appendChild(chatBubble) + /* append chat message */ chatSystem.appendChild(chatMessage) + if(!message.startsWith('
')) + message = `
${message}
` if(typewrite) mTypeMessage(chatBubble, message, typeDelay, callback) else { @@ -124,17 +146,6 @@ function mAddUserMessage(event){ mSubmitInput(event, message) mAddMessage(message, options) } -async function mChallengeMember(event){ - const { options, selectedIndex, value, } = this - mChallengeMemberId = value - const memberName = options[selectedIndex].text - // set member on server - this.disabled = true - const messages = [`If you want to get to ${ memberName }, I challenge you to a game of passphrase!`, `Please enter the passphrase for your account to continue...`] - await mAddMessages(messages, { typeDelay: 6, }) - chatSystem.appendChild(mCreateChallengeElement()) - mScrollBottom() -} /** * Creates a challenge element for the user to enter their passphrase. Simultaneously sets modular variables to the instantion of the challenge element. Unclear what happens if multiples are attempted to spawn, but code shouldn't allow for that, only hijax. See the `@required` for elements that this function generates and associates. * @private @@ -200,34 +211,14 @@ async function mFetchStart(){ case 'privacy-policy': break case 'challenge': + case 'login': case 'select': - const hostedMembers = await mGlobals.datamanager.hostedMembers() - if(!hostedMembers.length) - messages.push(`My sincere apologies, I'm unable to get the list of hosted members, the MyLife system must be down -- @Mookse`) - else { - messages.push(...[`Welcome to MyLife!`, `Please select your avatar-id from the list below...`]) - // create the select element and append - const selectContainer = document.createElement('div') - selectContainer.id = 'member-selection' - selectContainer.className = 'member-selection' - const select = document.createElement('select') - select.id = 'member-select' - select.className = 'member-select' - select.addEventListener('change', mChallengeMember) - hostedMembers.unshift({ id: null, name: 'Select your avatar-id...', }) - hostedMembers - .forEach(member=>{ - const option = document.createElement('option') - option.disabled = false - option.hidden = false - option.selected = false - option.text = member.name - option.value = member.id - select.appendChild(option) - }) - selectContainer.appendChild(select) - input = selectContainer - } + if(mChallengeMemberId){ + await mAddMessage(`Please enter the passphrase for your account to continue...`, { typeDelay: 6, }) + chatSystem.appendChild(mCreateChallengeElement()) + mScrollBottom() + } else + messages.push(`I'm sorry, I can't find the member you're looking for...`) break default: messages.push(...await mGlobals.datamanager.greetings()) @@ -280,12 +271,34 @@ async function mLoadStart(){ signupHumanNameInput = document.getElementById('human-name-input-text') signupSuccess = document.getElementById('signup-success') signupTeaser = document.getElementById('signup-teaser') - /* fetch the greeting messages */ + /* load page */ + mChallengeMemberId = new URLSearchParams(window.location.search).get('mbr') mPageType = new URLSearchParams(window.location.search).get('type') ?? window.location.pathname.split('/').pop() const startObject = await mFetchStart() return startObject } +/** + * Retrieves and runs the requested routine. + * @param {string} routineName - The routine name to execute + * @returns {Promise} + */ +async function mRoutine(routineName){ + const { error, responses=[], routine: routineScript, success, } = await mGlobals.datamanager.routine(routineName) + if(success && routineScript){ + const { events: _events, title, } = routineScript + const events = _events + .filter(event=>event?.dialog?.message?.length) + .map(event=>{ + let message = event.dialog.message + return message + }) + mAddMessages(events, { bubbleClass: 'system-bubble', responseDelay: 6, typeDelay: 4, typewrite: true, }) + } else if(responses?.length) + mAddMessages(responses, { responseDelay: 4, typeDelay: 1, typewrite: true, }) + else if(error.message) + mAddMessage(error.message, { bubbleClass: 'system-bubble', typeDelay: 1, typewrite: true, }) +} /** * Scrolls overflow of system chat to bottom. * @returns {void} diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 0420f3d..39c1159 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -5,6 +5,7 @@ import { experiences as _experiences, experienceSkip, experienceStart, + routine, submitInput, } from './experience.mjs' import { @@ -81,13 +82,10 @@ document.addEventListener('DOMContentLoaded', async event=>{ * Presents the `about` page as a series of sectional responses from your avatar. * @public * @async - * @returns {Promise} + * @returns {void} */ -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, }) +function about(){ + mRoutine('about') } /** * Adds an input element (button, input, textarea,) to the system chat column. @@ -209,6 +207,15 @@ function hideMemberChat(){ function inExperience(){ return mExperience?.id?.length ?? false } +/** + * Presents the `introduction` routine. + * @public + * @returns {void} + */ +function introduction(){ + clearSystemChat() + mRoutine('introduction') +} /** * Consumes instruction object and performs the requested actions. * @todo - all interfaceLocations supported @@ -229,16 +236,12 @@ function enactInstruction(instruction, interfaceLocation='chat', additionalFunct mGlobals.enactInstruction(instruction, functions) } /** - * Presents the `privacy-policy` page as a series of sectional responses from your avatar. + * Presents the `privacy-policy` page as a routine. * @public - * @async - * @returns {Promise} + * @returns {void} */ -async function privacyPolicy(){ - const { error, responses=[], success, } = await mGlobals.datamanager.privacyPolicy() - if(!success || !responses?.length) - return // or make error version - addMessages(responses, { responseDelay: 5, typeDelay: 1, typewrite: true, }) +function privacyPolicy(){ + mRoutine('privacy') } /** * Replaces an element (input/textarea) with a specified type. @@ -683,21 +686,37 @@ async function mAddMessage(message, options={}){ } if(typeof message!=='string' || !message.length) throw new Error('mAddMessage::Error()::`message` string is required') - const { bubbleClass, role='agent', typeDelay=2, typewrite=true, } = options + const { + bubbleClass, + role='agent', + typeDelay=2, + typewrite=true, + } = options + const isSynthetic = !['chat', 'guest', 'member', 'user', 'visitor'].includes(role) /* message container */ const chatMessage = document.createElement('div') chatMessage.classList.add('chat-message-container', `chat-message-container-${ role }`) /* message thumbnail */ - const messageThumb = document.createElement('img') - messageThumb.classList.add('chat-thumb') - messageThumb.id = `message-thumb-${ mChatBubbleCount }` - if(role==='agent' || role==='system'){ - const bot = activeBot() - const type = bot.type.split('-').pop() - 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!` + if(isSynthetic){ + const messageThumb = document.createElement('img') + messageThumb.classList.add('chat-thumb') + messageThumb.id = `message-thumb-${ mChatBubbleCount }` + switch(role){ + case 'system': + messageThumb.src = getBotIcon('system') + messageThumb.alt = `Q, MyLife's Corporate Intelligence` + messageThumb.title = `Hi, I'm Q, MyLife's Corporate Synthetic Intelligence. I am designed to help you better understand MyLife's organization, membership, services and vision.` + break + default: + const bot = activeBot() + const type = bot.type.split('-').pop() + 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!` + break + } + chatMessage.appendChild(messageThumb) } /* message bubble */ const chatBubble = document.createElement('div') @@ -728,7 +747,6 @@ async function mAddMessage(message, options={}){ chatMessageTab.appendChild(chatFeedbackPositive) chatMessageTab.appendChild(chatFeedbackNegative) } - chatMessage.appendChild(messageThumb) chatMessage.appendChild(chatBubble) chatMessage.appendChild(chatMessageTab) systemChat.appendChild(chatMessage) @@ -810,7 +828,9 @@ async function mAddMessage(message, options={}){ chatMessage.addEventListener('mouseleave', event => { chatMessageTab.classList.remove('chat-message-tab-hover', `chat-message-tab-hover-${ role }`) }) - /* print chat message */ + /* chat message */ + if(!message.startsWith('
')) + message = `
${message}
` if(typewrite) mTypeMessage(chatBubble, message, typeDelay) else { @@ -851,6 +871,20 @@ function mInitializePageListeners(){ } }) } +/** + * Retrieves and runs the requested routine. + * @param {string} routineName - The routine name to execute + * @returns {Promise} + */ +async function mRoutine(routineName){ + const { error, responses=[], routine: routineScript, success, } = await mGlobals.datamanager.routine(routineName) + if(success && routineScript) + routine(routineScript) + else if(responses?.length) + addMessages(responses, { responseDelay: 4, typeDelay: 1, typewrite: true, }) + else if(error.message) + addMessage(error.message, { bubbleClass: 'system-bubble', typeDelay: 1, typewrite: true, }) +} /** * Primitive step to set a "modality" or intercession for the member chat. Currently will key off dataset in `chatInputField`. * @public @@ -1004,14 +1038,16 @@ export { hide, hideMemberChat, inExperience, + introduction, enactInstruction, + privacyPolicy, replaceElement, + routine, sceneTransition, seedInput, setActiveAction, setActiveBot, setActiveItem, - updateActiveItemTitle, show, showMemberChat, showSidebar, @@ -1022,5 +1058,6 @@ export { toggleVisibility, unsetActiveAction, unsetActiveItem, + updateActiveItemTitle, waitForUserAction, } \ No newline at end of file