feat: Add 'divider' and 'group'/'groupEnd' type toggle (#856)

# PR Checklist
- [ ] Have you checked if it works normally in all models? *Ignore this
if it doesn't use models.*
- [x] Have you checked if it works normally in all web, local, and node
hosted versions? If it doesn't, have you blocked it in those versions?
- [x] Have you added type definitions?

# Description

## Dividers

| Before | After |
| --- | --- |

|![image](https://github.com/user-attachments/assets/9d6281fe-3be6-4b51-8ca1-dab20c666945)
|
![image](https://github.com/user-attachments/assets/3cec282c-29ed-4a9e-af03-9ee1f1ed4fab)|

Instead of pseudo-dividers with unwanted checkboxes, this allows
creators to define a "divider" *toggle*.


![image](https://github.com/user-attachments/assets/e22c9dda-673f-4981-9c5d-3810089e57b4)

Dividers are created by keyless `=name=divider`, or keyless valueless
`==divider`.

```
=My Module=divider
==divider
```

Multiple dividers with equal label will merge into one to prevent too
many dividers from multiple modules covering everything up.


![image](https://github.com/user-attachments/assets/7fc41a8f-02f2-4937-8d4c-ede0f41c8ac3)

```
// these won't merge
==divider
=K=divider
// these will merge
==divider
==divider
```

## Groups


![image](https://github.com/user-attachments/assets/fc3be4de-8e23-404e-b7f3-2b9d80ffc4de)

Groups are opened by keyless `=name=group` and closed by keyless
valueless `==groupEnd`. Nesting is not supported.

| Opened groups | MobileGUI |
|---|---|
|
![image](https://github.com/user-attachments/assets/8ec05169-07bd-4e20-bf04-47b1736059b5)
|
![image](https://github.com/user-attachments/assets/aa19be6c-0951-43a5-8175-4c96ade2913d)
|
This commit is contained in:
kwaroran
2025-05-25 18:21:11 +09:00
committed by GitHub
2 changed files with 94 additions and 24 deletions

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { getModuleToggles } from "src/ts/process/modules";
import { DBState, MobileGUI } from "src/ts/stores.svelte";
import { parseToggleSyntax } from "src/ts/util";
import CheckInput from "../UI/GUI/CheckInput.svelte";
import { parseToggleSyntax, type sidebarToggle, type sidebarToggleGroup } from "src/ts/util";
import { language } from "src/lang";
import type { character, groupChat } from "src/ts/storage/database.svelte";
import Arcodion from '../UI/Arcodion.svelte'
import CheckInput from "../UI/GUI/CheckInput.svelte";
import SelectInput from "../UI/GUI/SelectInput.svelte";
import OptionInput from "../UI/GUI/OptionInput.svelte";
import TextInput from "../UI/GUI/TextInput.svelte";
@@ -16,16 +17,38 @@
let { chara = $bindable(), noContainer }: Props = $props();
let parsedKv = $derived(parseToggleSyntax(DBState.db.customPromptTemplateToggle + getModuleToggles()))
let groupedToggles = $derived.by(() => {
const ungrouped = parseToggleSyntax(DBState.db.customPromptTemplateToggle + getModuleToggles())
let groupOpen = false
// group toggles together between group ... groupEnd
return ungrouped.reduce<sidebarToggle[]>((acc, toggle) => {
if (toggle.type === 'group') {
groupOpen = true
acc.push(toggle)
} else if (toggle.type === 'groupEnd') {
groupOpen = false
} else if (groupOpen) {
(acc.at(-1) as sidebarToggleGroup).children.push(toggle)
} else {
acc.push(toggle)
}
return acc
}, [])
})
</script>
{#snippet toggles(reverse: boolean = false)}
{#each parsedKv as toggle}
{#if toggle.type === 'select'}
<div class="flex gap-2 mt-2 items-center" class:flex-row-reverse={!reverse} class:justify-end={!reverse}>
{#snippet toggles(items: sidebarToggle[], reverse: boolean = false)}
{#each items as toggle, index}
{#if toggle.type === 'group' && toggle.children.length > 0}
<div class="w-full">
<Arcodion styled name={toggle.value}>
{@render toggles((toggle as sidebarToggleGroup).children, reverse)}
</Arcodion>
</div>
{:else if toggle.type === 'select'}
<div class="w-full flex gap-2 mt-2 items-center" class:justify-end={$MobileGUI} >
<span>{toggle.value}</span>
<SelectInput className="w-32" bind:value={DBState.db.globalChatVariables[`toggle_${toggle.key}`]}>
{#each toggle.options as option, i}
<OptionInput value={i.toString()}>{option}</OptionInput>
@@ -33,12 +56,23 @@
</SelectInput>
</div>
{:else if toggle.type === 'text'}
<div class="flex gap-2 mt-2 items-center" class:flex-row-reverse={!reverse} class:justify-end={!reverse}>
<div class="w-full flex gap-2 mt-2 items-center" class:justify-end={$MobileGUI}>
<span>{toggle.value}</span>
<TextInput className="w-32" bind:value={DBState.db.globalChatVariables[`toggle_${toggle.key}`]} />
</div>
{:else if toggle.type === 'divider'}
{@const prevToggle = groupedToggles[index - 1]}
<!-- Prevent multiple dividers appearing in a row -->
{#if index === 0 || prevToggle.type !== 'divider' || prevToggle.value !== toggle.value}
<div class="w-full min-h-5 flex gap-2 mt-2 items-center" class:justify-end={!reverse}>
{#if toggle.value}
<span>{toggle.value}</span>
{/if}
<hr class="border-t border-darkborderc m-0 min-w-32 flex-grow" />
</div>
{/if}
{:else}
<div class="flex mt-2 items-center">
<div class="w-full flex mt-2 items-center" class:justify-end={$MobileGUI}>
<CheckInput check={DBState.db.globalChatVariables[`toggle_${toggle.key}`] === '1'} reverse={reverse} name={toggle.value} onChange={() => {
DBState.db.globalChatVariables[`toggle_${toggle.key}`] = DBState.db.globalChatVariables[`toggle_${toggle.key}`] === '1' ? '0' : '1'
}} />
@@ -47,12 +81,12 @@
{/each}
{/snippet}
{#if !noContainer && parsedKv.length > 4}
{#if !noContainer && groupedToggles.length > 4}
<div class="h-48 border-darkborderc p-2 border rounded flex flex-col items-start mt-2 overflow-y-auto">
<div class="flex mt-2 items-center w-full" class:justify-end={$MobileGUI}>
<CheckInput bind:check={DBState.db.jailbreakToggle} name={language.jailbreakToggle} reverse />
</div>
{@render toggles(true)}
{@render toggles(groupedToggles, true)}
{#if DBState.db.supaModelType !== 'none' || DBState.db.hanuraiEnable || DBState.db.hypaV3}
<div class="flex mt-2 items-center w-full" class:justify-end={$MobileGUI}>
<CheckInput bind:check={chara.supaMemory} reverse name={DBState.db.hypaV3 ? language.ToggleHypaMemory : DBState.db.hanuraiEnable ? language.hanuraiMemory : DBState.db.hypaMemory ? language.ToggleHypaMemory : language.ToggleSuperMemory}/>
@@ -63,7 +97,7 @@
<div class="flex mt-2 items-center">
<CheckInput bind:check={DBState.db.jailbreakToggle} name={language.jailbreakToggle}/>
</div>
{@render toggles()}
{@render toggles(groupedToggles)}
{#if DBState.db.supaModelType !== 'none' || DBState.db.hanuraiEnable || DBState.db.hypaV3}
<div class="flex mt-2 items-center">
<CheckInput bind:check={chara.supaMemory} name={DBState.db.hypaV3 ? language.ToggleHypaMemory : DBState.db.hanuraiEnable ? language.hanuraiMemory : DBState.db.hypaMemory ? language.ToggleHypaMemory : language.ToggleSuperMemory}/>

View File

@@ -1009,33 +1009,69 @@ export function parseKeyValue(template:string){
}
}
export type sidebarToggleGroup = {
key?:string,
value?:string,
type:'group',
children:sidebarToggle[]
}
export type sidebarToggleGroupEnd = {
key?:string,
value?:string,
type:'groupEnd',
}
export type sidebarToggle =
| sidebarToggleGroup
| sidebarToggleGroupEnd
| {
key?:string,
value?:string,
type:'divider',
}
| {
key:string,
value:string,
type:'select',
options:string[]
}
| {
key:string,
value:string,
type:'text'|undefined,
options?:string[]
}
export function parseToggleSyntax(template:string){
try {
console.log(template)
if(!template){
return []
}
const keyValue:{
key:string,
value:string,
type?:string,
options?:string[]
}[] = []
const keyValue:sidebarToggle[] = []
const splited = template.split('\n')
for(const line of splited){
const [key, value, type, option] = line.split('=')
if(key && value){
if(type === 'group' || type === 'groupEnd' || type === 'divider'){
keyValue.push({
key, value, type, options: option ? option.split(',') : []
key,
value,
type,
children: []
})
} else if((key && value)){
keyValue.push({
key,
value,
type: type === 'select' || type === 'text' ? type : undefined,
options: option?.split(',') ?? []
})
}
}
console.log(keyValue)
return keyValue
} catch (error) {
console.error(error)