feat: add connection type selector to Setup Wizard

Step 1 of the wizard now offers a toggle between "Dedicated API"
(default) and "Connection Profile" before configuring the LLM.
Previously, Connection Profile was only available in the Settings
Modal after completing the wizard.

- Source toggle buttons with active/inactive styling
- Profile section: dropdown via CMRS.handleDropdown(), Test Connection
- Step 3 summary adapts to show profile name or provider/model
- Updated getting-started.md with both paths and new screenshot
- Updated CHANGELOG.md with improvements and tooltip fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
bal-spec 2026-03-18 22:12:39 -07:00
parent cb460e734f
commit 2fc514c0b2
6 changed files with 191 additions and 14 deletions

View file

@ -2,10 +2,16 @@
## 2.1.9
### Improvements
- **Setup Wizard connection type selector**: The wizard's Step 1 now lets you choose between **Dedicated API** (default) and **Connection Profile** before configuring the LLM connection. Previously, Connection Profile was only available in the Settings Modal after completing the wizard.
- **Documentation**: Added Connection Profile docs to providers.md, getting-started.md, README.md, and architecture.md. New screenshots for Settings Modal tabs and Connection Profile creation. Documented "Protect Recent Messages" feature in managing-memories.md.
### Bug Fixes
- **Fix message swipes counting toward extraction interval**: Swiping to a different AI response variant was incrementing the extraction counter as if a new message had been sent, causing extraction to trigger sooner than the configured interval. The `CHARACTER_MESSAGE_RENDERED` event handler now checks the `type` argument ST passes and returns early when `type === 'swipe'`. Fixes [#9](https://github.com/bal-spec/sillytavern-character-memory/issues/9).
- **Fix Settings and Troubleshooter modals cramped on mobile**: The left-nav layout used a fixed-width sidebar that consumed nearly half the popup width on narrow screens, leaving the content panel too narrow for readable text. In phone mode, the nav now switches to horizontal tabs above the content panel, giving it the full popup width.
- **Fix tooltip**: Injection Viewer button tooltip said "Toggle Injection Sidebar" — corrected to "Toggle Injection Viewer".
## 2.1.8

View file

@ -12,7 +12,14 @@ If the wizard didn't open automatically, click the **wand icon** (✦) in the Ch
CharMemory needs its own LLM connection — separate from your main chat. This keeps the extraction prompt clean and uncontaminated by chat personas, jailbreaks, or system prompts.
![Setup Wizard step 1 — provider dropdown, API key field, Connect button](../images/wizard-step1.png)
![Setup Wizard step 1 — connection type toggle, provider dropdown, API key field, Connect button](../images/wizard-step1.png)
The wizard offers two connection types at the top:
- **Dedicated API** (default) — connect directly to an API provider with its own key and model
- **Connection Profile** — reuse a saved SillyTavern connection (see [Providers → Connection Profiles](providers.md#connection-profiles))
### Dedicated API (default)
**1. Choose a provider** from the dropdown, e.g. **NanoGPT**. If you're not sure, **Pollinations** is free and requires no API key. See [Providers](providers.md) for a full list with model recommendations.
@ -26,10 +33,16 @@ CharMemory needs its own LLM connection — separate from your main chat. This k
> **Running an LLM locally?** Select **Local Server** from the provider dropdown. Enter your server URL (e.g., `http://localhost:11434/v1` for Ollama). No API key needed. See [Providers → Local Servers](providers.md#local-servers) for port numbers by backend.
> **Already have a connection configured in SillyTavern?** You can skip the wizard's provider setup and use a **Connection Profile** instead. After completing the wizard, open **Settings** (gear icon) → **Connection** → change **LLM Used for Extraction** to **Connection Profile** and select your saved profile. See [Providers → Connection Profiles](providers.md#connection-profiles).
> **If your provider is not listed** Many providers have an OpenAI compatible API endpoint. See if you can configure it that way.
### Connection Profile
If you already have a connection saved in SillyTavern's Connection Manager, click **Connection Profile** at the top to switch to that mode.
![Wizard step 1 with Connection Profile selected](../images/wizard-step1-profile.png)
Select your profile from the dropdown and click **Test Connection**. The profile's API, model, and credentials are used automatically — no separate setup needed. See [Providers → Connection Profiles](providers.md#connection-profiles) for how to create one.
![Wizard step 1 after successful connection — green checkmark, model selected](../images/wizard-step1-connected.png)
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

157
index.js
View file

@ -5040,6 +5040,9 @@ async function showSetupWizard(startStep = 1) {
// Step 1: LLM Connection
const noChatWarnStyle = getCharacterName() ? 'display:none;' : '';
const cmAvailable = isConnectionManagerAvailable();
const currentSource = s.source || EXTRACTION_SOURCE.PROVIDER;
const showProfile = currentSource === EXTRACTION_SOURCE.PROFILE;
const step1Html = `
<div class="charMemory_wizardStep" data-step="1">
<div id="cm_wiz_noChatWarn" class="charMemory_wizardCallout charMemory_wizardCallout--warn" style="${noChatWarnStyle}">
@ -5048,8 +5051,16 @@ async function showSetupWizard(startStep = 1) {
</div>
<div class="charMemory_wizardExplanation">
<strong>CharMemory</strong> automatically extracts structured memories from your roleplay chats and stores them so your characters can recall past events.
It needs access to an LLM to read your messages and create memory summaries. This can be any OpenAI-compatible provider.
It needs access to an LLM to read your messages and create memory summaries.
</div>
<div class="charMemory_modalFieldGroup">
<label><small>Connection type</small></label>
<div class="charMemory_wizSourceToggle">
<button type="button" class="menu_button charMemory_wizSourceBtn${!showProfile ? ' active' : ''}" data-source="provider">Dedicated API</button>
<button type="button" class="menu_button charMemory_wizSourceBtn${showProfile ? ' active' : ''}" data-source="profile" ${!cmAvailable ? 'disabled title="Enable the Connection Manager extension to use saved profiles"' : ''}>Connection Profile</button>
</div>
</div>
<div id="cm_wiz_providerSection" style="${showProfile ? 'display:none;' : ''}">
<div class="charMemory_modalFieldGroup">
<label><small>Provider</small></label>
<select id="cm_wiz_provider" class="text_pole">${providerOptions}</select>
@ -5087,6 +5098,20 @@ async function showSetupWizard(startStep = 1) {
</div>
<small id="cm_wiz_modelStatus" class="charMemory_helperText" style="display:none;"></small>
</div>
</div>
<div id="cm_wiz_profileSection" style="${showProfile ? '' : 'display:none;'}">
<div class="charMemory_modalFieldGroup">
<label><small>Connection Profile</small></label>
<select id="cm_wiz_profileSelect" class="text_pole">
<option value="">Select a Connection Profile</option>
</select>
<small class="charMemory_helperText">Uses credentials and settings from your saved SillyTavern connection profile.</small>
</div>
<div class="charMemory_modalFieldGroup">
<input type="button" id="cm_wiz_profileTest" class="menu_button charMemory_fullWidth" value="Test Connection" />
<small id="cm_wiz_profileTestStatus" class="charMemory_helperText" style="display:none;margin-bottom:6px;"></small>
</div>
</div>
<div class="charMemory_wizardNav">
<input type="button" id="cm_wiz_next1" class="menu_button" value="Next \u2192" disabled />
</div>
@ -5246,6 +5271,90 @@ async function showSetupWizard(startStep = 1) {
$wizard.find('#cm_wiz_modelRow').hide();
}
// --- Source toggle (Dedicated API vs Connection Profile) ---
$wizard.on('click', '.charMemory_wizSourceBtn', function () {
const source = $(this).data('source');
$wizard.find('.charMemory_wizSourceBtn').removeClass('active');
$(this).addClass('active');
const isProfile = source === 'profile';
$wizard.find('#cm_wiz_providerSection').toggle(!isProfile);
$wizard.find('#cm_wiz_profileSection').toggle(isProfile);
extension_settings[MODULE_NAME].source = isProfile ? EXTRACTION_SOURCE.PROFILE : EXTRACTION_SOURCE.PROVIDER;
saveSettingsDebounced();
// Reset connection state for the new source
wizConnectionOk = false;
$wizard.find('#cm_wiz_next1').prop('disabled', true);
$wizard.find('#cm_wiz_connectStatus, #cm_wiz_profileTestStatus').hide().text('');
});
// --- Connection Profile dropdown & test ---
if (cmAvailable) {
try {
const context = getContext();
const CMRS = context.ConnectionManagerRequestService;
if (CMRS) {
CMRS.handleDropdown(
'#cm_wiz_profileSelect',
s.selectedProfileId || '',
(profile) => {
extension_settings[MODULE_NAME].selectedProfileId = profile?.id || '';
saveSettingsDebounced();
},
);
}
} catch (err) {
console.warn(`${LOG_PREFIX} Failed to initialize wizard profile dropdown:`, err);
}
}
$wizard.on('click', '#cm_wiz_profileTest', async function () {
const profileId = extension_settings[MODULE_NAME].selectedProfileId;
const $status = $wizard.find('#cm_wiz_profileTestStatus');
const $btn = $(this);
if (!profileId) {
$status.text('Select a connection profile first.').css('color', '#e74c3c').show();
return;
}
$btn.prop('disabled', true).val('Testing...');
$status.text('Testing connection...').css('color', '').show();
try {
const context = getContext();
const CMRS = context.ConnectionManagerRequestService;
const profile = CMRS.getProfile(profileId);
const profileName = profile?.name || profileId;
const t0 = performance.now();
const result = await CMRS.sendRequest(
profileId,
[{ role: 'user', content: 'Respond with exactly: CHARMEMORY_TEST_OK' }],
20,
{ stream: false, extractData: true },
);
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
const reply = (result?.content || '').trim();
if (reply.includes('CHARMEMORY_TEST_OK')) {
$status.text(`\u2714 ${profileName} responded correctly (${elapsed}s)`).css('color', '#2ecc71').show();
} else {
$status.html(`\u2714 ${escapeHtml(profileName)} connected (${elapsed}s). It may still work for extraction.`).css('color', '#27ae60').show();
}
wizConnectionOk = true;
$wizard.find('#cm_wiz_next1').prop('disabled', false);
} catch (err) {
$status.text(`\u2718 ${err.message || 'Test failed'}`).css('color', '#e74c3c').show();
wizConnectionOk = false;
$wizard.find('#cm_wiz_next1').prop('disabled', true);
} finally {
$btn.prop('disabled', false).val('Test Connection');
}
});
$wizard.on('change', '#cm_wiz_provider', function () {
const key = String($(this).val());
extension_settings[MODULE_NAME].selectedProvider = key;
@ -5595,13 +5704,46 @@ async function showSetupWizard(startStep = 1) {
// --- Step 3: Review & Go ---
function initStep3() {
const source = extension_settings[MODULE_NAME].source;
const isProfile = source === EXTRACTION_SOURCE.PROFILE;
const pk = extension_settings[MODULE_NAME].selectedProvider;
const p = PROVIDER_PRESETS[pk] || {};
const ps = getProviderSettings(pk);
const modelName = ps.model || p.defaultModel || '(default)';
const modelShort = modelName.length > 40 ? modelName.slice(0, 40) + '\u2026' : modelName;
const interval = extension_settings[MODULE_NAME].interval || 20;
// Build connection summary rows based on source type
let connectionRows;
if (isProfile) {
let profileName = '(none selected)';
try {
const context = getContext();
const CMRS = context.ConnectionManagerRequestService;
const profile = CMRS?.getProfile(extension_settings[MODULE_NAME].selectedProfileId);
if (profile?.name) profileName = profile.name;
} catch { /* ignore */ }
connectionRows = `
<div class="charMemory_wizardSummaryRow">
<span class="label">Source</span>
<span>Connection Profile</span>
</div>
<div class="charMemory_wizardSummaryRow">
<span class="label">Profile</span>
<span>${escapeHtml(profileName)}</span>
</div>`;
} else {
const modelName = ps.model || p.defaultModel || '(default)';
const modelShort = modelName.length > 40 ? modelName.slice(0, 40) + '\u2026' : modelName;
connectionRows = `
<div class="charMemory_wizardSummaryRow">
<span class="label">Provider</span>
<span>${escapeHtml(p.name || pk)}</span>
</div>
<div class="charMemory_wizardSummaryRow">
<span class="label">Model</span>
<span>${escapeHtml(modelShort)}</span>
</div>`;
}
// VS summary from extension_settings.vectors
// Check DOM for VS extension UI to avoid false-positive when VS is disabled.
const vecSettings = extension_settings.vectors;
@ -5621,14 +5763,7 @@ async function showSetupWizard(startStep = 1) {
}
$wizard.find('#cm_wiz_summary').html(`
<div class="charMemory_wizardSummaryRow">
<span class="label">Provider</span>
<span>${escapeHtml(p.name || pk)}</span>
</div>
<div class="charMemory_wizardSummaryRow">
<span class="label">Model</span>
<span>${escapeHtml(modelShort)}</span>
</div>
${connectionRows}
<div class="charMemory_wizardSummaryRow">
<span class="label">Connection</span>
<span>${wizConnectionOk ? '<span style="color:#4a4;">\u2714 Connected</span>' : '<span style="color:#e8a33d;">\u26A0 Not tested</span>'}</span>

View file

@ -1602,6 +1602,29 @@ body.charMemory-phone-mode .charMemory_modalNavItem.active {
border-left: 3px solid var(--SmartThemeQuoteColor, #888);
}
.charMemory_wizSourceToggle {
display: flex;
gap: 6px;
}
.charMemory_wizSourceBtn {
flex: 1;
text-align: center;
opacity: 0.6;
border: 1px solid var(--SmartThemeBorderColor);
transition: opacity 0.15s, border-color 0.15s;
}
.charMemory_wizSourceBtn.active {
opacity: 1;
border-color: var(--SmartThemeQuoteColor);
}
.charMemory_wizSourceBtn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.charMemory_wizConnectRow {
display: flex;
align-items: flex-end;