diff --git a/public/img/minus-solid.svg b/public/img/minus-solid.svg new file mode 100644 index 000000000..f3a20d000 --- /dev/null +++ b/public/img/minus-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/plus-solid.svg b/public/img/plus-solid.svg new file mode 100644 index 000000000..637f19613 --- /dev/null +++ b/public/img/plus-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/user-group-solid.svg b/public/img/user-group-solid.svg new file mode 100644 index 000000000..b9c841345 --- /dev/null +++ b/public/img/user-group-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html index 4d7d29f6f..508a5587b 100644 --- a/public/index.html +++ b/public/index.html @@ -62,6 +62,8 @@ var mesStr = ''; var generatedPromtCache = ''; var characters = []; + let groups = []; + let selected_group = null; var this_chid; var backgrounds = []; var default_avatar = 'img/fluffy.png'; @@ -72,6 +74,7 @@ const system_message_types = { HELP: 'help', WELCOME: 'welcome', + GROUP: 'group', }; const system_messages = { @@ -83,6 +86,14 @@ "is_name":true, "mes": "Hi there! The following chat formatting commands are supported in TavernAI:

Need more help? Visit our wiki – TavernAI Wiki!

" }, + 'group': { + "name": systemUserName, + "force_avatar": "img/chloe.png", + "is_user":false, + "is_system": true, + "is_name": true, + "mes": "Group chat created. Say 'Hi' to lovely people!" + } }; const world_info_position = { @@ -106,6 +117,7 @@ var timerSaveEdit; var timerWorldSave; + var timerGroupSave; var durationSaveEdit = 200; //animation right menu var animation_rm_duration = 200; @@ -433,11 +445,16 @@ $("#rm_print_characters_block").prepend('
'+item.name+'
'); //console.log(item.name); }); - - + for (let group of groups) { + const template = $('#group_list_template .group_select').clone(); + template.data('id', group.id); + template.find('.avatar img').attr('src', group.avatar_url); + template.find('.ch_name').html(group.name); + $('#rm_print_characters_block').prepend(template); + } } async function getCharacters() { - + await getGroups(); const response = await fetch("/getcharacters", { method: "POST", headers: { @@ -1491,7 +1508,7 @@ //maybe a way to simulate this would be to disable the eventListener for people iOS. $("#send_textarea").keydown(function (e) { - if(e.which === 13 && e.shiftKey && is_send_press == false) { + if(e.which === 13 && !e.shiftKey && is_send_press == false) { is_send_press = true; e.preventDefault(); Generate(); @@ -1520,6 +1537,7 @@ $( "#rm_ch_create_block" ).css("display", "none"); $( "#rm_info_block" ).css("display", "none"); + $("#rm_group_chats_block").css("display", "none"); $( "#rm_button_characters" ).children("h2").css(deselected_button_style); $( "#rm_button_settings" ).children("h2").css(selected_button_style); @@ -1541,6 +1559,280 @@ selected_button = 'character_edit'; select_selected_character(this_chid); }); + $(document).on('click', '.group_select', async function() { + const id = $(this).data('id'); + selected_button = 'group_chats'; + + if (selected_group !== id){ + if(!is_send_press){ + selected_group = id; + this_edit_mes_id = undefined; + clearChat(); + chat.length = 0; + await getGroupChat(id); + } + } + + select_group_chats(id); + }); + $("#rm_button_group_chats").click(function() { + selected_button = 'group_chats'; + select_group_chats(); + }); + $( "#rm_button_back_from_group" ).click(function() { + selected_button = 'characters'; + select_rm_characters(); + }); + $('#rm_group_filter').on('input', function() { + const searchValue = $(this).val().trim().toLowerCase(); + + if (!searchValue) { + $("#rm_group_add_members .group_member").show(); + } else { + $("#rm_group_add_members .group_member").each(function () { + $(this).children('.ch_name').text().toLowerCase().includes(searchValue) + ? $(this).show() + : $(this).hide(); + }); + } + }); + $('#rm_group_submit').click(async function() { + let name = $('#rm_group_chat_name').val(); + const members = $('#rm_group_members .group_member').map((_, x) => $(x).data('id')).toArray(); + + if (!name) { + name = `Chat with ${members.join(', ')}`; + } + + // placeholder + const avatar_url = '/img/five.png'; + + const createGroupResponse = await fetch('/creategroup', { + method: 'POST', + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + body: JSON.stringify({ name: name, members: members, avatar_url: avatar_url }), + }); + + if (createGroupResponse.ok) { + const createGroupData = await createGroupResponse.json(); + const id = createGroupData.id; + + await getCharacters(); + $('#rm_info_avatar').html(''); + var avatar = $('#avatar_div_div').clone(); + avatar.find('img').attr('src', avatar_url); + $('#rm_info_avatar').append(avatar); + $('#rm_info_block').transition({ opacity: 0 ,duration: 0}); + select_rm_info("Group chat created"); + $('#rm_info_block').transition({ opacity: 1.0 ,duration: 2000}); + } + }); + async function getGroupChat(id) { + const response = await fetch('/getgroupchat', { + method: 'POST', + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + body: JSON.stringify({ id: id }), + }); + + if (response.ok) { + const data = await response.json(); + if(Array.isArray(data) && data.length) { + for (let key of data) { + chat.push(key); + } + printMessages(); + } + else { + sendSystemMessage(system_message_types.GROUP); + } + + await saveGroupChat(id); + } + } + async function saveGroupChat(id) { + const response = await fetch('/savegroupchat', { + method: 'POST', + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + body: JSON.stringify({id: id, chat: [...chat]}) + }); + } + async function getGroups() { + const response = await fetch('/getgroups', { + method: 'POST', + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + }); + + if (response.ok) { + const data = await response.json(); + groups = data.sort((a,b) => a.id - b.id); + } + } + async function deleteGroup(id) { + const response = await fetch('/deletegroup', { + method: 'POST', + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + body: JSON.stringify({id : id}), + }); + + if (response.ok) { + await getCharacters(); + $('#rm_info_avatar').html(''); + $('#rm_info_block').transition({ opacity: 0 ,duration: 0}); + select_rm_info("Group deleted!"); + $('#rm_info_block').transition({ opacity: 1.0 ,duration: 2000}); + } + + } + async function editGroup(id, immediately) { + const group = groups.find(x => x.id == id); + + if (!group) { + return; + } + + async function _save() { + const response = await fetch('/editgroup', { + method: 'POST', + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + body: JSON.stringify(group), + }); + } + + if (immediately) { + return await _save(); + } + + clearTimeout(timerGroupSave); + timerGroupSave = setTimeout(async () => await _save(), durationSaveEdit); + } + + function select_group_chats(chat_id) { + menu_type = 'group_chats'; + const group = chat_id && groups.find(x => x.id == chat_id); + const groupName = group?.name ?? ''; + + $('#rm_group_chat_name').val(groupName); + $('#rm_group_chat_name').off(); + $('#rm_group_chat_name').on('input', async function() { + if (chat_id) { + group.name = $(this).val(); + await editGroup(chat_id); + } + }); + $('#rm_group_filter').val('').trigger('input'); + $("#rm_group_chats_block").css("display", "flex"); + $('#rm_group_chats_block').css('opacity', 0.0); + $('#rm_group_chats_block').transition({ + opacity: 1.0, + duration: animation_rm_duration, + easing: animation_rm_easing, + complete: function() { } + }); + + $("#rm_ch_create_block").css("display", "none"); + $("#rm_characters_block" ).css("display", "none"); + + async function memberClickHandler() { + const id = $(this).data('id'); + const isDelete = !!($(this).closest('#rm_group_members').length); + const template = $(this).clone(); + template.data('id', id); + template.click(memberClickHandler); + + if (isDelete) { + template.find('.plus').show(); + template.find('.minus').hide(); + $('#rm_group_add_members').prepend(template); + } else { + template.find('.plus').hide(); + template.find('.minus').show(); + $('#rm_group_members').prepend(template); + } + + if (group) { + if (isDelete) { + const index = group.members.findIndex(x => x === id); + if (index !== -1) { + group.members.splice(index, 1); + } + } else { + group.members.push(id); + } + await editGroup(chat_id); + } + + $(this).remove(); + const groupHasMembers = !!$('#rm_group_members').children().length; + $("#rm_group_submit").prop('disabled', !groupHasMembers); + } + + // render characters list + $('#rm_group_add_members').empty(); + $('#rm_group_members').empty(); + for (let character of characters) { + const avatar = character.avatar != 'none' ? `characters/${character.avatar}#${Date.now()}` : default_avatar; + const template = $('#group_member_template .group_member').clone(); + template.data('id', character.name); + template.find('.avatar img').attr('src', avatar); + template.find('.ch_name').html(character.name); + template.click(memberClickHandler); + + if (group && Array.isArray(group.members) && group.members.includes(character.name)) { + template.find('.plus').hide(); + template.find('.minus').show(); + $('#rm_group_members').append(template); + } else { + template.find('.plus').show(); + template.find('.minus').hide(); + $('#rm_group_add_members').append(template); + } + } + + const groupHasMembers = !!$('#rm_group_members').children().length; + $("#rm_group_submit").prop('disabled', !groupHasMembers); + + // bottom buttons + if (chat_id) { + $('#rm_group_submit').hide(); + $('#rm_group_delete').show(); + } else { + $('#rm_group_submit').show(); + $('#rm_group_delete').hide(); + } + + $('#rm_group_delete').off(); + $('#rm_group_delete').on('click', function() { + popup_type = 'del_group'; + $('#dialogue_popup').data('group_id', chat_id); + callPopup('

Delete the group?

'); + }); + + // top bar + if (group) { + var display_name = groupName; + $("#rm_button_selected_ch").children("h2").css(deselected_button_style); + $("#rm_button_selected_ch").children("h2").text(''); + } + } + function select_rm_create(){ menu_type = 'create'; if(selected_button == 'create'){ @@ -1553,6 +1845,7 @@ $( "#rm_characters_block" ).css("display", "none"); $( "#rm_api_block" ).css("display", "none"); $( "#rm_ch_create_block" ).css("display", "block"); + $("#rm_group_chats_block").css("display", "none"); $('#rm_ch_create_block').css('opacity',0.0); $('#rm_ch_create_block').transition({ @@ -1606,6 +1899,7 @@ $( "#rm_api_block" ).css("display", "none"); $( "#rm_ch_create_block" ).css("display", "none"); $( "#rm_info_block" ).css("display", "none"); + $("#rm_group_chats_block").css("display", "none"); $( "#rm_button_characters" ).children("h2").css(selected_button_style); $( "#rm_button_settings" ).children("h2").css(deselected_button_style); @@ -1615,6 +1909,7 @@ $( "#rm_characters_block" ).css("display", "none"); $( "#rm_api_block" ).css("display", "none"); $( "#rm_ch_create_block" ).css("display", "none"); + $("#rm_group_chats_block").css("display", "none"); $( "#rm_info_block" ).css("display", "flex"); $("#rm_info_text").html('

'+text+'

'); @@ -1671,6 +1966,7 @@ $("#form_create").attr("actiontype", "editcharacter"); } $(document).on('click', '.character_select', function(){ + selected_group = null; if(this_chid !== $(this).attr("chid")){ if(!is_send_press){ this_edit_mes_id = undefined; @@ -1869,6 +2165,13 @@ if (popup_type === 'del_world' && world_info) { deleteWorldInfo(world_info); } + if (popup_type === 'del_group') { + const groupId = $('#dialogue_popup').data('group_id'); + + if (groupId) { + deleteGroup(groupId); + } + } if(popup_type == 'new_chat' && this_chid != undefined && menu_type != "create"){//Fix it; New chat doesn't create while open create character menu clearChat(); chat.length = 0; @@ -1901,6 +2204,7 @@ $("#dialogue_popup_ok").text("Yes"); break; case 'del_world': + case 'del_group': default: $("#dialogue_popup_ok").css("background-color", "#791b31"); $("#dialogue_popup_ok").text("Delete"); @@ -4005,6 +4309,54 @@ +
+
+
+

+
+ +
+
+

Add Members

+ +
+ +
+

Members

+ +
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+ +
 
+ +
+

API

@@ -4162,6 +4514,7 @@

+New Character

+Import

+

+New Group

diff --git a/public/style.css b/public/style.css index 60205f39d..9b9ea9358 100644 --- a/public/style.css +++ b/public/style.css @@ -756,7 +756,7 @@ display: none; padding-left: 0.75em; } -#character_search_bar::-webkit-search-cancel-button { +input[type=search]::-webkit-search-cancel-button { -webkit-appearance: none; height: 1em; width: 1em; @@ -769,7 +769,7 @@ display: none; cursor: pointer; } -#character_search_bar:focus::-webkit-search-cancel-button { +input[type=search]:focus::-webkit-search-cancel-button { opacity: .3; pointer-events: all; } @@ -2486,3 +2486,153 @@ body { -webkit-transition: width 0s ease; transition: width 0s ease; } + +/* GROUP CHATS */ + +#rm_group_top_bar { + display: flex; + flex-direction: row; + align-items: flex-start; + width: 100%; +} +#rm_button_group_chats{ + cursor: pointer; + display: inline-block; +} +#rm_button_group_chats h2{ + margin-top: auto; + margin-bottom: auto; + font-size: 16px; + color: rgb(188, 193, 200, 1); + border: 1px solid #333; + background-color: rgba(0,0,0,0.3); + padding:6px; + border-radius: 10px; +} +#rm_group_chats_block { + display: none; + height: 100%; + flex-direction: column; + align-items: flex-start; +} +#rm_group_chat_name { + width: 90%; +} +#rm_group_buttons { + display: flex; + flex-direction: row; + width: 100%; +} +#rm_group_buttons > input { + font-size: 16px; + cursor: pointer; + user-select: none; +} +#rm_group_buttons > input:disabled { + filter: brightness(0.3); + cursor: unset; +} +#rm_group_members, #rm_group_add_members { + margin-top: 0.25rem; + margin-bottom: 0.25rem; + width: 100%; + flex: 1; + overflow: auto; +} +#rm_group_buttons_expander { + flex-grow: 1; +} +#rm_group_delete { + color: rgb(190, 0, 0); +} +#rm_group_members:empty { + width: 100%; +} +#rm_group_members:empty::before { + content: 'Group is empty'; + font-size: 1rem; + font-weight: bolder; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + opacity: 0.8; +} +#rm_group_add_members:empty { + width: 100%; +} +#rm_group_add_members_header { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; +} +#rm_group_add_members_header input { + flex-grow: 1; + width: 100%; + margin-left: 1rem; +} +#rm_group_add_members:empty::before { + content: 'No characters available'; + font-size: 1rem; + font-weight: bolder; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + opacity: 0.8; +} +.group_member_icon { + width: 25px; + height: 25px; + margin: 0 10px; +} +.group_member_icon img { + filter: invert(1); +} +.group_member { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + padding: 5px; + border-radius: 10px; + cursor:pointer; +} +.group_member .ch_name { + flex-grow: 1; + margin-left: 10px; +} +.group_member:hover{ + background-color: #ffffff11; +} +#group_member_template { + display: none !important; +} +#group_list_template { + display: none !important; +} +.group_select { + display: flex; + flex-direction: row; + align-items: center; + padding: 5px; + border-radius: 10px; + cursor: pointer; +} +.group_select:hover { + background-color: #ffffff11; +} +.group_select .group_icon { + width: 20px; + height: 20px; + margin: 0 10px; +} +.group_select .ch_name { + flex-grow: 1; +} +.group_select .group_icon img { + filter: invert(1); +} \ No newline at end of file diff --git a/server.js b/server.js index ee7235886..4840eaf06 100644 --- a/server.js +++ b/server.js @@ -61,7 +61,12 @@ if (is_colab && process.env.googledrive == 2){ const jsonParser = express.json({limit: '100mb'}); const urlencodedParser = express.urlencoded({extended: true, limit: '100mb'}); const baseRequestArgs = { headers: { "Content-Type": "application/json" } }; -const directories = { worlds: 'public/worlds/', avatars: 'public/User Avatars' }; +const directories = { + worlds: 'public/worlds/', + avatars: 'public/User Avatars', + groups: 'public/groups/', + groupChats: 'public/group chats', +}; // CSRF Protection // const doubleCsrf = require('csrf-csrf').doubleCsrf; @@ -1294,6 +1299,112 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { } }); +app.post('/getgroups', jsonParser, (_, response) => { + const groups = []; + + if (!fs.existsSync(directories.groups)) { + fs.mkdirSync(directories.groups); + } + + const files = fs.readdirSync(directories.groups); + files.forEach(function(file) { + const fileContents = fs.readFileSync(path.join(directories.groups, file), 'utf8'); + const group = JSON.parse(fileContents); + groups.push(group); + }); + + return response.send(groups); +}); + +app.post('/creategroup', jsonParser, (request, response) => { + if (!request.body) { + return response.sendStatus(400); + } + + const id = Date.now(); + const chatMetadata = { id: id, name: request.body.name ?? 'New Group', members: request.body.members ?? [], avatar_url: request.body.avatar_url }; + const pathToFile = path.join(directories.groups, `${id}.json`); + const fileData = JSON.stringify(chatMetadata); + + if (!fs.existsSync(directories.groups)) { + fs.mkdirSync(directories.groups); + } + + fs.writeFileSync(pathToFile, fileData); + return response.send(chatMetadata); +}); + +app.post('/editgroup', jsonParser, (request, response) => { + if (!request.body || !request.body.id) { + return response.sendStatus(400); + } + + const id = request.body.id; + const pathToFile = path.join(directories.groups, `${id}.json`); + const fileData = JSON.stringify(request.body); + + fs.writeFileSync(pathToFile, fileData); + return response.send({ok: true}); +}); + +app.post('/getgroupchat', jsonParser, (request, response) => { + if (!request.body || !request.body.id) { + return response.sendStatus(400); + } + + const id = request.body.id; + const pathToFile = path.join(directories.groupChats, `${id}.jsonl`); + + if (fs.existsSync(pathToFile)) { + const data = fs.readFileSync(pathToFile, 'utf8'); + const lines = data.split('\n'); + + // Iterate through the array of strings and parse each line as JSON + const jsonData = lines.map(JSON.parse); + return response.send(jsonData); + } else { + return response.send([]); + } +}); + +app.post('/savegroupchat', jsonParser, (request, response) => { + if (!request.body || !request.body.id) { + return response.sendStatus(400); + } + + const id = request.body.id; + const pathToFile = path.join(directories.groupChats, `${id}.jsonl`); + + if (!fs.existsSync(directories.groupChats)) { + fs.mkdirSync(directories.groupChats); + } + + let chat_data = request.body.chat; + let jsonlData = chat_data.map(JSON.stringify).join('\n'); + fs.writeFileSync(pathToFile, jsonlData, 'utf8'); + return response.send({ok: true}); +}); + +app.post('/deletegroup', jsonParser, async (request, response) => { + if (!request.body || !request.body.id) { + return response.sendStatus(400); + } + + const id = request.body.id; + const pathToGroup = path.join(directories.groups, `${id}.json`); + const pathToChat = path.join(directories.groupChats, `${id}.jsonl`); + + if (fs.existsSync(pathToGroup)) { + fs.rmSync(pathToGroup); + } + + if (fs.existsSync(pathToChat)) { + fs.rmSync(pathToChat); + } + + return response.send({ok: true}); +}); + // ** REST CLIENT ASYNC WRAPPERS ** function deleteAsync(url, args) { return new Promise((resolve, reject) => {