2023-07-20 19:32:15 +02:00
import {
characters ,
saveSettingsDebounced ,
this _chid ,
callPopup ,
menu _type ,
getCharacters ,
2023-08-18 22:13:15 +02:00
entitiesFilter ,
2023-07-20 19:32:15 +02:00
} from "../script.js" ;
2023-08-22 12:07:24 +02:00
import { FILTER _TYPES , FilterHelper } from "./filters.js" ;
2023-07-20 19:32:15 +02:00
2023-08-18 22:13:15 +02:00
import { groupCandidatesFilter , selected _group } from "./group-chats.js" ;
2023-08-17 13:19:20 +02:00
import { uuidv4 } from "./utils.js" ;
2023-07-20 19:32:15 +02:00
export {
tags ,
tag _map ,
loadTagsSettings ,
printTagFilters ,
getTagsList ,
appendTagToList ,
createTagMapFromList ,
renameTagKey ,
importTags ,
} ;
const CHARACTER _FILTER _SELECTOR = '#rm_characters_block .rm_tag_filter' ;
const GROUP _FILTER _SELECTOR = '#rm_group_chats_block .rm_tag_filter' ;
2023-08-18 22:13:15 +02:00
function getFilterHelper ( listSelector ) {
return $ ( listSelector ) . is ( GROUP _FILTER _SELECTOR ) ? groupCandidatesFilter : entitiesFilter ;
2023-07-20 19:32:15 +02:00
}
export const tag _filter _types = {
character : 0 ,
group _member : 1 ,
} ;
const ACTIONABLE _TAGS = {
FAV : { id : 1 , name : 'Show only favorites' , color : 'rgba(255, 255, 0, 0.5)' , action : applyFavFilter , icon : 'fa-solid fa-star' , class : 'filterByFavorites' } ,
GROUP : { id : 0 , name : 'Show only groups' , color : 'rgba(100, 100, 100, 0.5)' , action : filterByGroups , icon : 'fa-solid fa-users' , class : 'filterByGroups' } ,
HINT : { id : 3 , name : 'Show Tag List' , color : 'rgba(150, 100, 100, 0.5)' , action : onTagListHintClick , icon : 'fa-solid fa-tags' , class : 'showTagList' } ,
}
const InListActionable = {
VIEW : { id : 2 , name : 'Manage tags' , color : 'rgba(150, 100, 100, 0.5)' , action : onViewTagsListClick , icon : 'fa-solid fa-gear' } ,
}
const DEFAULT _TAGS = [
2023-08-22 12:07:24 +02:00
{ id : uuidv4 ( ) , name : "Plain Text" } ,
{ id : uuidv4 ( ) , name : "OpenAI" } ,
{ id : uuidv4 ( ) , name : "W++" } ,
{ id : uuidv4 ( ) , name : "Boostyle" } ,
{ id : uuidv4 ( ) , name : "PList" } ,
{ id : uuidv4 ( ) , name : "AliChat" } ,
2023-07-20 19:32:15 +02:00
] ;
let tags = [ ] ;
let tag _map = { } ;
2023-08-22 12:07:24 +02:00
/ * *
* Applies the favorite filter to the character list .
* @ param { FilterHelper } filterHelper Instance of FilterHelper class .
* /
2023-08-18 22:13:15 +02:00
function applyFavFilter ( filterHelper ) {
2023-07-20 19:32:15 +02:00
const isSelected = $ ( this ) . hasClass ( 'selected' ) ;
const displayFavoritesOnly = ! isSelected ;
$ ( this ) . toggleClass ( 'selected' , displayFavoritesOnly ) ;
2023-08-18 22:13:15 +02:00
filterHelper . setFilterData ( FILTER _TYPES . FAV , displayFavoritesOnly ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 12:07:24 +02:00
/ * *
* Applies the "is group" filter to the character list .
* @ param { FilterHelper } filterHelper Instance of FilterHelper class .
* /
2023-08-18 22:13:15 +02:00
function filterByGroups ( filterHelper ) {
2023-07-20 19:32:15 +02:00
const isSelected = $ ( this ) . hasClass ( 'selected' ) ;
const displayGroupsOnly = ! isSelected ;
$ ( this ) . toggleClass ( 'selected' , displayGroupsOnly ) ;
2023-08-18 22:13:15 +02:00
filterHelper . setFilterData ( FILTER _TYPES . GROUP , displayGroupsOnly ) ;
2023-07-20 19:32:15 +02:00
}
function loadTagsSettings ( settings ) {
tags = settings . tags !== undefined ? settings . tags : DEFAULT _TAGS ;
tag _map = settings . tag _map !== undefined ? settings . tag _map : Object . create ( null ) ;
}
function renameTagKey ( oldKey , newKey ) {
const value = tag _map [ oldKey ] ;
tag _map [ newKey ] = value || [ ] ;
delete tag _map [ oldKey ] ;
saveSettingsDebounced ( ) ;
}
function createTagMapFromList ( listElement , key ) {
const tagIds = [ ... ( $ ( listElement ) . find ( ".tag" ) . map ( ( _ , el ) => $ ( el ) . attr ( "id" ) ) ) ] ;
tag _map [ key ] = tagIds ;
saveSettingsDebounced ( ) ;
}
function getTagsList ( key ) {
if ( ! Array . isArray ( tag _map [ key ] ) ) {
tag _map [ key ] = [ ] ;
return [ ] ;
}
return tag _map [ key ]
. map ( x => tags . find ( y => y . id === x ) )
. filter ( x => x )
. sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
}
function getInlineListSelector ( ) {
if ( selected _group && menu _type === "group_edit" ) {
return ` .group_select[grid=" ${ selected _group } "] .tags ` ;
}
if ( this _chid && menu _type === "character_edit" ) {
return ` .character_select[chid=" ${ this _chid } "] .tags ` ;
}
return null ;
}
function getTagKey ( ) {
if ( selected _group && menu _type === "group_edit" ) {
return selected _group ;
}
if ( this _chid && menu _type === "character_edit" ) {
return characters [ this _chid ] . avatar ;
}
return null ;
}
function addTagToMap ( tagId ) {
const key = getTagKey ( ) ;
if ( ! key ) {
return ;
}
if ( ! Array . isArray ( tag _map [ key ] ) ) {
tag _map [ key ] = [ tagId ] ;
}
else {
tag _map [ key ] . push ( tagId ) ;
}
}
function removeTagFromMap ( tagId ) {
const key = getTagKey ( ) ;
if ( ! key ) {
return ;
}
if ( ! Array . isArray ( tag _map [ key ] ) ) {
tag _map [ key ] = [ ] ;
}
else {
const indexOf = tag _map [ key ] . indexOf ( tagId ) ;
tag _map [ key ] . splice ( indexOf , 1 ) ;
}
}
function findTag ( request , resolve , listSelector ) {
const skipIds = [ ... ( $ ( listSelector ) . find ( ".tag" ) . map ( ( _ , el ) => $ ( el ) . attr ( "id" ) ) ) ] ;
const haystack = tags . filter ( t => ! skipIds . includes ( t . id ) ) . map ( t => t . name ) . sort ( ) ;
const needle = request . term . toLowerCase ( ) ;
const hasExactMatch = haystack . findIndex ( x => x . toLowerCase ( ) == needle ) !== - 1 ;
const result = haystack . filter ( x => x . toLowerCase ( ) . includes ( needle ) ) ;
if ( request . term && ! hasExactMatch ) {
result . unshift ( request . term ) ;
}
resolve ( result ) ;
}
function selectTag ( event , ui , listSelector ) {
let tagName = ui . item . value ;
let tag = tags . find ( t => t . name === tagName ) ;
// create new tag if it doesn't exist
if ( ! tag ) {
tag = createNewTag ( tagName ) ;
}
// unfocus and clear the input
$ ( event . target ) . val ( "" ) . trigger ( 'blur' ) ;
// add tag to the UI and internal map
appendTagToList ( listSelector , tag , { removable : true } ) ;
appendTagToList ( getInlineListSelector ( ) , tag , { removable : false } ) ;
addTagToMap ( tag . id ) ;
saveSettingsDebounced ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
// need to return false to keep the input clear
return false ;
}
function getExistingTags ( new _tags ) {
let existing _tags = [ ] ;
for ( let tag of new _tags ) {
let foundTag = tags . find ( t => t . name . toLowerCase ( ) === tag . toLowerCase ( ) )
if ( foundTag ) {
existing _tags . push ( foundTag . name ) ;
}
}
return existing _tags
}
async function importTags ( imported _char ) {
let imported _tags = imported _char . tags . filter ( t => t !== "ROOT" && t !== "TAVERN" ) ;
let existingTags = await getExistingTags ( imported _tags ) ;
//make this case insensitive
let newTags = imported _tags . filter ( t => ! existingTags . some ( existingTag => existingTag . toLowerCase ( ) === t . toLowerCase ( ) ) ) ;
let selected _tags = "" ;
const existingTagsString = existingTags . length ? ( ': ' + existingTags . join ( ', ' ) ) : '' ;
if ( newTags . length === 0 ) {
await callPopup ( ` <h3>Importing Tags For ${ imported _char . name } </h3><p> ${ existingTags . length } existing tags have been found ${ existingTagsString } .</p> ` , 'text' ) ;
} else {
selected _tags = await callPopup ( ` <h3>Importing Tags For ${ imported _char . name } </h3><p> ${ existingTags . length } existing tags have been found ${ existingTagsString } .</p><p>The following ${ newTags . length } new tags will be imported.</p> ` , 'input' , newTags . join ( ', ' ) ) ;
}
selected _tags = existingTags . concat ( selected _tags . split ( ',' ) ) ;
selected _tags = selected _tags . map ( t => t . trim ( ) ) . filter ( t => t !== "" ) ;
//Anti-troll measure
if ( selected _tags . length > 15 ) {
selected _tags = selected _tags . slice ( 0 , 15 ) ;
}
for ( let tagName of selected _tags ) {
let tag = tags . find ( t => t . name === tagName ) ;
if ( ! tag ) {
tag = createNewTag ( tagName ) ;
}
if ( ! tag _map [ imported _char . avatar ] . includes ( tag . id ) ) {
tag _map [ imported _char . avatar ] . push ( tag . id ) ;
console . debug ( 'added tag to map' , tag , imported _char . name ) ;
}
} ;
saveSettingsDebounced ( ) ;
await getCharacters ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
// need to return false to keep the input clear
return false ;
}
function createNewTag ( tagName ) {
const tag = {
2023-08-22 12:07:24 +02:00
id : uuidv4 ( ) ,
2023-07-20 19:32:15 +02:00
name : tagName ,
color : '' ,
} ;
tags . push ( tag ) ;
return tag ;
}
function appendTagToList ( listElement , tag , { removable , selectable , action , isGeneralList } ) {
if ( ! listElement ) {
return ;
}
let tagElement = $ ( '#tag_template .tag' ) . clone ( ) ;
tagElement . attr ( 'id' , tag . id ) ;
tagElement . css ( 'color' , 'var(--SmartThemeBodyColor)' ) ;
tagElement . css ( 'background-color' , tag . color ) ;
tagElement . find ( '.tag_name' ) . text ( tag . name ) ;
const removeButton = tagElement . find ( ".tag_remove" ) ;
removable ? removeButton . show ( ) : removeButton . hide ( ) ;
if ( tag . class ) {
tagElement . addClass ( tag . class ) ;
}
if ( tag . icon ) {
tagElement . find ( '.tag_name' ) . text ( '' ) . attr ( 'title' , tag . name ) . addClass ( tag . icon ) ;
}
2023-08-19 20:08:35 +02:00
if ( tag . excluded && isGeneralList ) {
$ ( tagElement ) . addClass ( 'excluded' ) ;
2023-07-20 19:32:15 +02:00
}
if ( selectable ) {
2023-08-18 22:13:15 +02:00
tagElement . on ( 'click' , ( ) => onTagFilterClick . bind ( tagElement ) ( listElement ) ) ;
2023-07-20 19:32:15 +02:00
}
if ( action ) {
2023-08-18 22:13:15 +02:00
const filter = getFilterHelper ( $ ( listElement ) ) ;
tagElement . on ( 'click' , ( ) => action . bind ( tagElement ) ( filter ) ) ;
2023-07-20 19:32:15 +02:00
tagElement . addClass ( 'actionable' ) ;
}
if ( action && tag . id === 2 ) {
tagElement . addClass ( 'innerActionable hidden' ) ;
}
$ ( listElement ) . append ( tagElement ) ;
}
2023-08-18 22:13:15 +02:00
function onTagFilterClick ( listElement ) {
2023-07-20 19:32:15 +02:00
let excludeTag ;
if ( $ ( this ) . hasClass ( 'selected' ) ) {
$ ( this ) . removeClass ( 'selected' ) ;
$ ( this ) . addClass ( 'excluded' ) ;
excludeTag = true
}
else if ( $ ( this ) . hasClass ( 'excluded' ) ) {
$ ( this ) . removeClass ( 'excluded' ) ;
excludeTag = false ;
}
else {
$ ( this ) . addClass ( 'selected' ) ;
}
// Manual undefined check required for three-state boolean
if ( excludeTag !== undefined ) {
const tagId = $ ( this ) . attr ( 'id' ) ;
const existingTag = tags . find ( ( tag ) => tag . id === tagId ) ;
if ( existingTag ) {
existingTag . excluded = excludeTag ;
saveSettingsDebounced ( ) ;
}
}
2023-08-25 06:29:10 +02:00
runTagFilters ( listElement ) ;
}
function runTagFilters ( listElement ) {
2023-07-20 19:32:15 +02:00
const tagIds = [ ... ( $ ( listElement ) . find ( ".tag.selected:not(.actionable)" ) . map ( ( _ , el ) => $ ( el ) . attr ( "id" ) ) ) ] ;
const excludedTagIds = [ ... ( $ ( listElement ) . find ( ".tag.excluded:not(.actionable)" ) . map ( ( _ , el ) => $ ( el ) . attr ( "id" ) ) ) ] ;
2023-08-18 22:13:15 +02:00
const filterHelper = getFilterHelper ( $ ( listElement ) ) ;
filterHelper . setFilterData ( FILTER _TYPES . TAG , { excluded : excludedTagIds , selected : tagIds } ) ;
2023-07-20 19:32:15 +02:00
}
function printTagFilters ( type = tag _filter _types . character ) {
const FILTER _SELECTOR = type === tag _filter _types . character ? CHARACTER _FILTER _SELECTOR : GROUP _FILTER _SELECTOR ;
const selectedTagIds = [ ... ( $ ( FILTER _SELECTOR ) . find ( ".tag.selected" ) . map ( ( _ , el ) => $ ( el ) . attr ( "id" ) ) ) ] ;
$ ( FILTER _SELECTOR ) . empty ( ) ;
const characterTagIds = Object . values ( tag _map ) . flat ( ) ;
const tagsToDisplay = tags
. filter ( x => characterTagIds . includes ( x . id ) )
. sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
for ( const tag of Object . values ( ACTIONABLE _TAGS ) ) {
appendTagToList ( FILTER _SELECTOR , tag , { removable : false , selectable : false , action : tag . action , isGeneralList : true } ) ;
}
$ ( FILTER _SELECTOR ) . find ( '.actionable' ) . last ( ) . addClass ( 'margin-right-10px' ) ;
for ( const tag of Object . values ( InListActionable ) ) {
appendTagToList ( FILTER _SELECTOR , tag , { removable : false , selectable : false , action : tag . action , isGeneralList : true } ) ;
}
for ( const tag of tagsToDisplay ) {
appendTagToList ( FILTER _SELECTOR , tag , { removable : false , selectable : true , isGeneralList : true } ) ;
2023-08-25 06:29:10 +02:00
if ( tag . excluded ) {
runTagFilters ( FILTER _SELECTOR ) ;
}
2023-07-20 19:32:15 +02:00
}
for ( const tagId of selectedTagIds ) {
$ ( ` ${ FILTER _SELECTOR } .tag[id=" ${ tagId } "] ` ) . trigger ( 'click' ) ;
}
}
function onTagRemoveClick ( event ) {
event . stopPropagation ( ) ;
const tag = $ ( this ) . closest ( ".tag" ) ;
const tagId = tag . attr ( "id" ) ;
tag . remove ( ) ;
removeTagFromMap ( tagId ) ;
$ ( ` ${ getInlineListSelector ( ) } .tag[id=" ${ tagId } "] ` ) . remove ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
saveSettingsDebounced ( ) ;
}
function onTagInput ( event ) {
let val = $ ( this ) . val ( ) ;
if ( tags . find ( t => t . name === val ) ) return ;
$ ( this ) . autocomplete ( "search" , val ) ;
}
function onTagInputFocus ( ) {
$ ( this ) . autocomplete ( 'search' , $ ( this ) . val ( ) ) ;
}
function onCharacterCreateClick ( ) {
$ ( "#tagList" ) . empty ( ) ;
}
function onGroupCreateClick ( ) {
$ ( "#groupTagList" ) . empty ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
}
export function applyTagsOnCharacterSelect ( ) {
//clearTagsFilter();
const chid = Number ( $ ( this ) . attr ( 'chid' ) ) ;
const key = characters [ chid ] . avatar ;
const tags = getTagsList ( key ) ;
$ ( "#tagList" ) . empty ( ) ;
for ( const tag of tags ) {
appendTagToList ( "#tagList" , tag , { removable : true } ) ;
}
}
function applyTagsOnGroupSelect ( ) {
//clearTagsFilter();
const key = $ ( this ) . attr ( 'grid' ) ;
const tags = getTagsList ( key ) ;
$ ( "#groupTagList" ) . empty ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
for ( const tag of tags ) {
appendTagToList ( "#groupTagList" , tag , { removable : true } ) ;
}
}
function createTagInput ( inputSelector , listSelector ) {
$ ( inputSelector )
. autocomplete ( {
source : ( i , o ) => findTag ( i , o , listSelector ) ,
select : ( e , u ) => selectTag ( e , u , listSelector ) ,
minLength : 0 ,
} )
. focus ( onTagInputFocus ) ; // <== show tag list on click
}
function onViewTagsListClick ( ) {
$ ( '#dialogue_popup' ) . addClass ( 'large_dialogue_popup' ) ;
const list = document . createElement ( 'div' ) ;
const everything = Object . values ( tag _map ) . flat ( ) ;
$ ( list ) . append ( '<h3>Tags</h3><i>Click on the tag name to edit it.</i><br>' ) ;
$ ( list ) . append ( '<i>Click on color box to assign new color.</i><br><br>' ) ;
2023-08-28 00:50:32 +02:00
for ( const tag of tags . slice ( ) . sort ( ( a , b ) => a ? . name ? . toLowerCase ( ) ? . localeCompare ( b ? . name ? . toLowerCase ( ) ) ) ) {
2023-07-20 19:32:15 +02:00
const count = everything . filter ( x => x == tag . id ) . length ;
const template = $ ( '#tag_view_template .tag_view_item' ) . clone ( ) ;
template . attr ( 'id' , tag . id ) ;
template . find ( '.tag_view_counter_value' ) . text ( count ) ;
template . find ( '.tag_view_name' ) . text ( tag . name ) ;
template . find ( '.tag_view_name' ) . addClass ( 'tag' ) ;
template . find ( '.tag_view_name' ) . css ( 'background-color' , tag . color ) ;
const colorPickerId = tag . id + "-tag-color" ;
template . find ( '.tagColorPickerHolder' ) . html (
` <toolcool-color-picker id=" ${ colorPickerId } " color=" ${ tag . color } " class="tag-color"></toolcool-color-picker> `
) ;
template . find ( '.tag-color' ) . attr ( 'id' , colorPickerId ) ;
list . appendChild ( template . get ( 0 ) ) ;
setTimeout ( function ( ) {
document . querySelector ( ` .tag-color[id=" ${ colorPickerId } " ` ) . addEventListener ( 'change' , ( evt ) => {
onTagColorize ( evt ) ;
} ) ;
} , 100 ) ;
$ ( colorPickerId ) . color = tag . color ;
}
callPopup ( list . outerHTML , 'text' ) ;
}
function onTagDeleteClick ( ) {
if ( ! confirm ( "Are you sure?" ) ) {
return ;
}
const id = $ ( this ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
for ( const key of Object . keys ( tag _map ) ) {
tag _map [ key ] = tag _map [ key ] . filter ( x => x . id !== id ) ;
}
const index = tags . findIndex ( x => x . id === id ) ;
tags . splice ( index , 1 ) ;
$ ( ` .tag[id=" ${ id } "] ` ) . remove ( ) ;
$ ( ` .tag_view_item[id=" ${ id } "] ` ) . remove ( ) ;
saveSettingsDebounced ( ) ;
}
function onTagRenameInput ( ) {
const id = $ ( this ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
const newName = $ ( this ) . text ( ) ;
const tag = tags . find ( x => x . id === id ) ;
tag . name = newName ;
$ ( ` .tag[id=" ${ id } "] .tag_name ` ) . text ( newName ) ;
saveSettingsDebounced ( ) ;
}
function onTagColorize ( evt ) {
console . debug ( evt ) ;
const id = $ ( evt . target ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
const newColor = evt . detail . rgba ;
$ ( evt . target ) . parent ( ) . parent ( ) . find ( '.tag_view_name' ) . css ( 'background-color' , newColor ) ;
$ ( ` .tag[id=" ${ id } "] ` ) . css ( 'background-color' , newColor ) ;
const tag = tags . find ( x => x . id === id ) ;
tag . color = newColor ;
console . debug ( tag ) ;
saveSettingsDebounced ( ) ;
}
function onTagListHintClick ( ) {
console . log ( $ ( this ) ) ;
$ ( this ) . toggleClass ( 'selected' ) ;
$ ( this ) . siblings ( ".tag:not(.actionable)" ) . toggle ( 100 ) ;
$ ( this ) . siblings ( ".innerActionable" ) . toggleClass ( 'hidden' ) ;
}
$ ( document ) . ready ( ( ) => {
createTagInput ( '#tagInput' , '#tagList' ) ;
createTagInput ( '#groupTagInput' , '#groupTagList' ) ;
$ ( document ) . on ( "click" , "#rm_button_create" , onCharacterCreateClick ) ;
$ ( document ) . on ( "click" , "#rm_button_group_chats" , onGroupCreateClick ) ;
$ ( document ) . on ( "click" , ".character_select" , applyTagsOnCharacterSelect ) ;
$ ( document ) . on ( "click" , ".group_select" , applyTagsOnGroupSelect ) ;
$ ( document ) . on ( "click" , ".tag_remove" , onTagRemoveClick ) ;
$ ( document ) . on ( "input" , ".tag_input" , onTagInput ) ;
$ ( document ) . on ( "click" , ".tags_view" , onViewTagsListClick ) ;
$ ( document ) . on ( "click" , ".tag_delete" , onTagDeleteClick ) ;
$ ( document ) . on ( "input" , ".tag_view_name" , onTagRenameInput ) ;
} ) ;