Fixes, readme updates

This commit is contained in:
Jody Doolittle 2025-08-17 13:18:45 -07:00
parent 60ee83aaa7
commit 6882a96e57
46 changed files with 2661 additions and 371 deletions

View file

@ -1,4 +1,4 @@
# Serene Pub 0.4.0 - Development Configuration
# Serene Pub 0.4.1 - Development Configuration
# Copy this file to .env to customize development settings
# ===========================================
@ -34,6 +34,6 @@
# ===========================================
# DEVELOPMENT OPTIONS
# ===========================================
# Automatically open browser on startup (default: true)
# Set to false to disable automatic browser opening
# SERENE_AUTO_OPEN=false
# Automatically open browser on startup (default: enabled)
# Set to 1 to disable automatic browser opening
# SERENE_AUTO_OPEN=1

View file

@ -148,7 +148,7 @@ jobs:
exit 1
fi
cat > "$DIST_DIR/.env.example" << 'EOF'
# Serene Pub 0.4.0 - Production Configuration
# Serene Pub 0.4.1 - Production Configuration
# Copy this file to .env in the same directory to customize settings
# ===========================================

View file

@ -1,6 +1,6 @@
# Serene Pub - AI Assistant Reference
**Version**: 0.4.0
**Version**: 0.4.1
**License**: AGPL-3.0
**Author**: Jody Doolittle
**Repository**: https://github.com/doolijb/serene-pub

228
README.md
View file

@ -5,37 +5,37 @@
> **⚠️ Serene Pub is in alpha! Expect bugs and rapid changes. This project is under heavy development.**
<p align="center">
<b><a href="https://github.com/doolijb/serene-pub">Repo</a>
<a href="https://github.com/doolijb/serene-pub/wiki/Home">Wiki</a>
<a href="https://github.com/doolijb/serene-pub/releases">Downloads</a>
<a href="https://github.com/doolijb/serene-pub/issues">Issues</a>
<a href="https://discord.gg/3kUx3MDcSa">Discord</a>
<a href="https://buymeacoffee.com/serenepub">Buy Me a Coffee</a></b>
<b><a href="https://github.com/doolijb/serene-pub/wiki">📚 Documentation</a>
<a href="https://github.com/doolijb/serene-pub/releases">⬇️ Downloads</a>
<a href="https://github.com/doolijb/serene-pub/issues">🐛 Issues</a>
<a href="https://discord.gg/3kUx3MDcSa">💬 Discord</a>
<a href="https://buymeacoffee.com/serenepub">☕ Buy Me a Coffee</a></b>
</p>
---
## Table of Contents
- [Why Serene Pub?](#-why-serene-pub)
- [Screenshots](#-screenshots)
- [Features](#-features)
- [Quick Start / Download](#-quick-start)
- [Installation & Setup](#installation--setup)
- [Documentation & Help](#-documentation--help)
- [Planned Features](#-planned-features)
- [Considered Features](#-considered-features)
- [How to Update](#-how-to-update)
- [Contributing](#-contributing)
- [License](#-license)
- [Special Thanks](#-special-thanks)
# 🦊 Serene Pub
**Modern, Open Source AI Roleplay Chat**
Serene Pub is a brand new, open source chat application for immersive AI roleplay and creative conversations. Designed for simplicity, speed, and beautiful usability, Serene Pub brings your characters and worlds to life—on your terms, with your data, and your favorite AI models.
## 📚 **[Full Documentation & Setup Guide](https://github.com/doolijb/serene-pub/wiki)**
**For detailed installation instructions, configuration guides, and tutorials, visit our [Wiki](https://github.com/doolijb/serene-pub/wiki).**
---
## Table of Contents
- [Why Serene Pub?](#-why-serene-pub)
- [Screenshots](#-screenshots)
- [Features](#-features)
- [Quick Start](#-quick-start)
- [Documentation](#-documentation)
- [Contributing](#-contributing)
- [License](#-license)
---
## ✨ Why Serene Pub?
@ -76,6 +76,12 @@ Serene Pub is a brand new, open source chat application for immersive AI rolepla
| -------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
| ![](static/screenshots/lorebooks-character-bindings.png) | ![](static/screenshots/lorebooks-character-lore.png) | ![](static/screenshots/lorebooks-history.png) | ![](static/screenshots/lorebooks-world-lore.png) |
### Ollama Manager
| Available Models | Downloads | Installed Models | Settings |
| -------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------ |
| ![](static/screenshots/sidebar-ollama-manager-available.png) | ![](static/screenshots/sidebar-ollama-manager-downloads.png) | ![](static/screenshots/sidebar-ollama-manager-installed.png) | ![](static/screenshots/sidebar-ollama-manager-settings.png) |
### Mobile Experience
| Chat | Connections | Edit Character |
@ -91,9 +97,11 @@ Serene Pub is a brand new, open source chat application for immersive AI rolepla
## 🚀 Features
- **AI Model Agnostic:** Connect to OpenAI, Ollama, Llama.cpp, and more
- **Ollama Manager:** Built-in UI to easily manage, download, and activate Ollama models
- **Character & Persona Management:** Import, create, and edit with rich metadata and avatars
- **Lorebooks+:** Organize world lore, character lore, and history for deep roleplay
- **Group Chats:** Multi-character chats for immersive group roleplay and dynamic storylines
- **Tags:** Easily organize and filter chats, characters, personas, and lorebooks with customizable tags
- **Chat & Context Tools:**
- Auto character response
- Edit/delete messages
@ -105,6 +113,7 @@ Serene Pub is a brand new, open source chat application for immersive AI rolepla
- **Context Templates:** Handlebar-based, customizable prompt formats
- **Mobile-First Design:** Fully responsive, works great on phones and tablets
- **Themes & Dark Mode:** 20+ themes, instant switching, and accessibility options
- **Accessibility & Screen Reader Support:** Experimental support for screen readers and assistive technologies (in progress)
- **Portable & Secure:** Embedded database, no cloud required, runs anywhere
- **Silly Tavern Compatibility:** Import/export character cards and avatars
- **Open Source & Extensible:** AGPL-3.0, modular adapters, easy to hack
@ -117,14 +126,14 @@ Serene Pub is a brand new, open source chat application for immersive AI rolepla
Linux, MacOS and Windows are supported!
1. [Download the latest release](https://github.com/doolijb/serene-pub/releases) for your OS
1. **[Download the latest release](https://github.com/doolijb/serene-pub/releases)** for your OS
2. Extract the archive anywhere
3. Read the included `INSTRUCTIONS.txt` for your platform
4. Run the launcher script (`run.sh`/`run.cmd`)
5. Open [http://localhost:3000](http://localhost:3000) in your browser
6. Add your first AI connection and start chatting!
### Source Code
### From Source
#### Requirements
@ -138,172 +147,38 @@ Linux, MacOS and Windows are supported!
3. `npm run dev` to start the dev server, or `npm run dev:host`
4. Visit [http://localhost:5173](http://localhost:5173)
---
## 📚 Documentation & Help
### 🧩 Context Configuration
Serene Pub uses Handlebars-style templates to build highly customizable prompts. Templates can include dynamic information like date, world lore, and structured history. Example:
````hbs
{{#systemBlock}}
Instructions: """
{{#if currentDate}}
The current date in the story is {{{currentDate}}}.
{{/if}}
{{{instructions}}}
""" Assistant Characters (AI-controlled): ```json
{{{characters}}}
``` User Characters (player-controlled): ```json
{{{personas}}}
``` Scenario: """ {{{scenario}}} """
{{#if worldLore}}
World lore: ```json
{{{worldLore}}}
```
{{/if}}
{{#if history}}
Story history: ```json
{{{history}}}
```
{{/if}}
{{#if exampleDialogue}}
{{{exampleDialogue}}}
{{/if}}
{{/systemBlock}}
{{#each chatMessages}}
{{#if (eq role "assistant")}}
{{#assistantBlock}}
{{{name}}}: {{{message}}}
{{/assistantBlock}}
{{/if}}
{{#if (eq role "user")}}
{{#userBlock}}
{{{name}}}: {{{message}}}
{{/userBlock}}
{{/if}}
{{/each}}
{{#if postHistoryInstructions}}
{{#systemBlock}}
{{{postHistoryInstructions}}}
{{/systemBlock}}
{{/if}}
````
### 📝 Prompt Configuration
Prompt Configurations define the tone and behavior of the AI's responses. These instructions are sent alongside your message history and context blocks.
Example:
```text
Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions. Keep the story immersive and engaging.
```
### 👤 Personas
Personas are user-controlled characters. Give your persona a name, description, and avatar to represent yourself in chats. Personas let you roleplay as different characters, each with their own style and background.
### 🤖 Characters
Characters are AI-controlled participants in your chats. Each character can have a name, description, avatar, personality, and greeting.
- **Greeting:** The greeting is the first message a character inserts into the chat. It helps ground the AI and provides an example of how the character should behave. You can add additional greetings or group-only greetings for more variety.
- **Importing:** Character cards can be imported from Silly Tavern or your favorite character card website, making it easy to bring your favorite personalities into Serene Pub.
### 📖 Lorebooks+
Lorebooks+ are advanced worldbuilding and context management tools that let you deeply enrich your roleplay experience. With Lorebooks+, you can:
- **Character Bindings:** Hotswappable characters linked in the lorebook. Dynamically update character and persona names in lorebook entries, so the right information is injected into the prompt for the current cast of your chat.
- **World Lore:** Store and organize facts, rules, and background information about your world, setting, or universe. World lore can be automatically included in relevant chats.
- _Imported lorebooks/world books are inserted into "world lore" for easy access and editing._
- **Character Lore:** Linked to character bindings and extends the character profiles. Maintain detailed backstories, traits, and secrets for each character, ensuring the AI stays consistent and in-character.
- **History:** Track and inject important story events, chat history, or evolving facts as the narrative progresses. _History entries are experimental and may change in future releases._ This helps maintain continuity and depth in long-running stories.
To use a lorebook, select it when creating or editing a chat. Lorebook entries are triggered by keywords, but may also be pinned to ensure they are always present in the prompt. Vectorization will be added in the future as a replacement for keyword engineering, making context injection smarter and more flexible.
Lorebooks+ make it easy to manage complex worlds, keep characters consistent, and ensure the AI always has the right context for immersive storytelling.
### 💬 Chats
Create a chat by adding one or more characters and at least one persona. You can:
- Optionally add a scenario to tell the AI what the current objective is. In non-group chats, this overrides character scenarios.
- Enable or disable characters from responding automatically in group chats for more control over the conversation flow.
- Optionally select a lorebook to inject world, character, or historical lore into your chat for richer context and storytelling.
### 🏷️ Tags (Coming in 0.4.0)
Tags are a planned feature for the 0.4.0 release. Tags will help you organize and filter your content, making it easier to manage complex stories and worlds.
### 📂 Data Location
Your data is saved locally in your OS-specific app directory:
- **macOS:** `~/Library/Application Support/SerenePub`
- **Windows:** `%LOCALAPPDATA%\SerenePub`
- **Linux:** `~/.local/share/SerenePub`
### ⚡ Troubleshooting
- Adjust "Context Tokens" in the Sampling tab.
- Configure a more accurate tokenizer in the Connections sidebar.
- Use the "View Prompt Statistics" modal to preview your final prompt.
* Save your configuration to apply changes.
* Default templates marked with `*` cannot be edited directly; clone to customize.
- Ensure you are editing a cloned configuration, not the defaults.
### 🔌 Connections
Serene Pub supports a variety of AI model connections, both local and cloud-based. For a full list of currently supported (or in development) connection options, see the [Supported Connections Issue](https://github.com/doolijb/serene-pub/issues/10).
- **Connection Types:** Some adapters support chat APIs, completion APIs, or both. You can mix and match connections to suit your needs. When using completion APIs (Serene Pub precompiles the entire prompt), you can select from available prompt formats to best match your model's requirements.
- **Token Counter:** Each connection uses a token counter to estimate the number of tokens in your chat history, character definitions, and prompt. This helps manage context size and avoid exceeding model limits.
- **Setup:** Add and configure connections in the app sidebar. Test connections and refresh available models directly from the UI.
**Need help?** Check out our **[Setup Guide](https://github.com/doolijb/serene-pub/wiki/Installation-&-Setup)** in the wiki.
---
## 🗺️ Planned Features
## <20> Documentation
### **[Complete Documentation Available in our Wiki](https://github.com/doolijb/serene-pub/wiki)**
**Popular Wiki Pages:**
- **[Installation & Setup](https://github.com/doolijb/serene-pub/wiki/Installation-&-Setup)** - Detailed setup instructions for all platforms
- **[Connections](https://github.com/doolijb/serene-pub/wiki/Connections)** - How to connect to AI models (OpenAI, Ollama, LM Studio, etc.)
- **[Characters & Personas](https://github.com/doolijb/serene-pub/wiki/Characters-&-Personas)** - Creating and managing your roleplay characters
- **[Lorebooks](https://github.com/doolijb/serene-pub/wiki/Lorebooks)** - Advanced worldbuilding and context management
- **[Context Templates](https://github.com/doolijb/serene-pub/wiki/Context-Templates)** - Customizing AI prompts with Handlebars
- **[Troubleshooting](https://github.com/doolijb/serene-pub/wiki/Troubleshooting)** - Common issues and solutions
### 🗺️ Planned Features
- 🏷️ Tags (coming in 0.4.0)
- 🧠 Vectorization
- 🔌 More API connection types
- 🤖 Assistant Chat: Ask AI questions about Serene Pub and get suggestions to improve your characters, personas, and lore
- 🦙 Ollama Manager UI: Manage, download, and update Ollama models directly from the app
- 🖼️ Image generation
## 💡 Considered Features
### 💡 Considered Features
- 👥 Multi-user logins & multi-user group chats
- 📝 Chat summarizing
- 🖼️ User/chat backgrounds
- 📖 Story narration/system instructions
- 🦯 Screen reader support
- 📅 Lorebooks+ features: custom calendars, "eras" historical categories, and more
- 🕹️ Text adventure & narrator modes
- 🖼️ Image generation
---
## 🔄 How to Update
**Updating from 0.2.x, 0.3.x:**
- Download the latest version and extract to your desired location and run. Your data will automatically be copied from the old database to the new, more powerful database.
**Updating to future versions:**
- Download the latest version and extract to your desired location and run (it doesn't matter where). Run the application. Any database migrations will be performed automatically.
---
@ -311,6 +186,8 @@ Serene Pub supports a variety of AI model connections, both local and cloud-base
Serene Pub is community-driven! Bug fixes, features, and feedback are welcome. Please [open an issue](https://github.com/doolijb/serene-pub/issues) or [start a discussion](https://github.com/doolijb/serene-pub/discussions) before submitting large changes.
**For development setup and contribution guidelines, see our [Contributing Guide](https://github.com/doolijb/serene-pub/wiki/Contributing).**
---
## 🛡️ License
@ -325,4 +202,7 @@ Special thanks to **crazyaphro** for Q/A, **M3d4r** for editing the Wiki, and **
---
<p align="center"><b>Serene Pub — Play more, tweak less. 100% open source.</b></p>
<p align="center">
<b>Serene Pub — Play more, tweak less. 100% open source.</b><br>
<b>📚 <a href="https://github.com/doolijb/serene-pub/wiki">Read the full documentation</a></b>
</p>

View file

@ -12,7 +12,15 @@ APP_MAIN="$DIR/build/index.js"
ENV_FILE="$DIR/.env"
if [ -f "$ENV_FILE" ]; then
echo "Loading environment variables from .env file..."
export $(grep -v '^#' "$ENV_FILE" | xargs -d '\n')
# Use a more portable way to load environment variables
while IFS='=' read -r key value; do
# Skip comments and empty lines
case "$key" in
'#'*|'') continue ;;
esac
# Export the variable, removing any surrounding quotes
export "$key"="$(echo "$value" | sed 's/^["'\'']\|["'\'']$//g')"
done < "$ENV_FILE"
fi
echo "========================================"

View file

@ -9,9 +9,9 @@
<key>CFBundleName</key>
<string>Serene Pub</string>
<key>CFBundleVersion</key>
<string>0.4.0</string>
<string>0.4.1</string>
<key>CFBundleShortVersionString</key>
<string>0.4.0</string>
<string>0.4.1</string>
<key>CFBundleIconFile</key>
<string>favicon.icns</string>
<key>CFBundlePackageType</key>

View file

@ -12,7 +12,15 @@ APP_MAIN="$DIR/build/index.js"
ENV_FILE="$DIR/.env"
if [ -f "$ENV_FILE" ]; then
echo "Loading environment variables from .env file..."
export $(grep -v '^#' "$ENV_FILE" | xargs)
# Use a more portable way to load environment variables
while IFS='=' read -r key value; do
# Skip comments and empty lines
case "$key" in
'#'*|'') continue ;;
esac
# Export the variable, removing any surrounding quotes
export "$key"="$(echo "$value" | sed 's/^["'\'']\|["'\'']$//g')"
done < "$ENV_FILE"
fi
echo "========================================"

View file

@ -0,0 +1 @@
ALTER TABLE "system_settings" ADD COLUMN "show_home_page_banner" boolean DEFAULT true;

File diff suppressed because it is too large Load diff

View file

@ -71,6 +71,13 @@
"when": 1754857930869,
"tag": "0009_petite_mimic",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1755411731603,
"tag": "0010_calm_nightshade",
"breakpoints": true
}
]
}

View file

@ -1,7 +1,7 @@
{
"name": "serene-pub",
"private": true,
"version": "0.4.0-alpha",
"version": "0.4.1-alpha",
"type": "module",
"license": "AGPL-3.0",
"bin": "build/index.js",

View file

@ -185,9 +185,9 @@ async function createMacOSExecutable(platformDir, config) {
<key>CFBundleName</key>
<string>Serene Pub</string>
<key>CFBundleVersion</key>
<string>0.4.0</string>
<string>0.4.1</string>
<key>CFBundleShortVersionString</key>
<string>0.4.0</string>
<string>0.4.1</string>
<key>CFBundleIconFile</key>
<string>favicon.icns</string>
<key>CFBundlePackageType</key>

View file

@ -13,17 +13,6 @@ console.log("🔧 Customizing server build output...")
let content = fs.readFileSync(buildFile, "utf8")
// Debug: Check what patterns exist in the file
console.log("Looking for patterns in the file...")
console.log(
'Contains "Listening on file descriptor":',
content.includes("Listening on file descriptor")
)
console.log(
'Contains "Listening on ${path":',
content.includes("Listening on ${path")
)
// Replace console.log messages
let replacements = 0
@ -106,7 +95,7 @@ content = content.replace(
console.log(\`\`);
// Auto-open browser if SERENE_AUTO_OPEN is not disabled
if (process.env.SERENE_AUTO_OPEN !== 'false') {
if (process.env.SERENE_AUTO_OPEN !== '1' && process.env.SERENE_AUTO_OPEN !== 'true') {
setTimeout(() => {
import('open').then(({ default: open }) => {
open(\`http://localhost:\${port}\`);
@ -117,7 +106,7 @@ content = content.replace(
});
}, 1000);
} else {
console.log(\` Auto-open browser disabled (SERENE_AUTO_OPEN=false)\`);
console.log(\` Auto-open browser disabled (SERENE_AUTO_OPEN=\${process.env.SERENE_AUTO_OPEN})\`);
}
}`
)

11
src/app.d.ts vendored
View file

@ -82,6 +82,7 @@ declare global {
showAllCharacterFields: boolean
enableEasyCharacterCreation: boolean
enableEasyPersonaCreation: boolean
showHomePageBanner: boolean
}
}
@ -137,6 +138,7 @@ declare global {
showAllCharacterFields: boolean
enableEasyCharacterCreation: boolean
enableEasyPersonaCreation: boolean
showHomePageBanner: boolean
}
}
}
@ -547,6 +549,15 @@ declare global {
enabled?: boolean
}
}
namespace UpdateShowHomePageBanner {
interface Call {
enabled: boolean
}
interface Response {
success: boolean
enabled?: boolean
}
}
// --- WEIGHTS ---
namespace DeleteSamplingConfig {
interface Call {

View file

@ -64,7 +64,11 @@
let systemSettingsCtx: SystemSettingsCtx = $state({
settings: {
ollamaManagerEnabled: false,
ollamaManagerBaseUrl: ""
ollamaManagerBaseUrl: "",
showAllCharacterFields: false,
enableEasyCharacterCreation: true,
enableEasyPersonaCreation: true,
showHomePageBanner: true
}
})

View file

@ -572,7 +572,7 @@
<div class="flex flex-col gap-1">
<span title="Toggle Character Active">
<Switch
name="Toggle Character Active"
name="toggle-character-active-{c.id}"
controlWidth="w-9"
controlActive="preset-filled-success-500"
controlDisabled="preset-filled-surface-500"
@ -580,6 +580,7 @@
checked={isActive}
onCheckedChange={(e) =>
toggleCharacterActive(e, c)}
aria-label="Toggle character {c.name} active status"
>
{#snippet inactiveChild()}<Icons.Meh
size="20"

View file

@ -547,41 +547,44 @@
</summary>
<div class="mt-2 flex flex-col gap-2">
<div class="flex w-full justify-between">
<span>Use Regex</span>
<label for="useRegex-{entry.id}">Use Regex</label>
<Switch
name="useRegex"
label="Use Regex"
name="useRegex-{entry.id}"
checked={entry.useRegex || false}
onCheckedChange={(e) =>
(entry.useRegex = e.checked)}
aria-labelledby="useRegex-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Case Sensitive</span>
<label for="caseSensitive-{entry.id}">Case Sensitive</label>
<Switch
name="caseSensitive"
name="caseSensitive-{entry.id}"
checked={entry.caseSensitive}
onCheckedChange={(e) =>
(entry.caseSensitive =
e.checked)}
aria-labelledby="caseSensitive-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Pinned</span>
<label for="constant-{entry.id}">Pinned</label>
<Switch
name="constant"
name="constant-{entry.id}"
checked={entry.constant}
onCheckedChange={(e) =>
(entry.constant = e.checked)}
aria-labelledby="constant-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Enabled</span>
<label for="enabled-{entry.id}">Enabled</label>
<Switch
name="enabled"
name="enabled-{entry.id}"
checked={entry.enabled}
onCheckedChange={(e) =>
(entry.enabled = e.checked)}
aria-labelledby="enabled-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">

View file

@ -542,41 +542,44 @@
</summary>
<div class="mt-2 flex flex-col gap-2">
<div class="flex w-full justify-between">
<span>Use Regex</span>
<label for="useRegex-{entry.id}">Use Regex</label>
<Switch
name="useRegex"
label="Use Regex"
name="useRegex-{entry.id}"
checked={entry.useRegex || false}
onCheckedChange={(e) =>
(entry.useRegex = e.checked)}
aria-labelledby="useRegex-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Case Sensitive</span>
<label for="caseSensitive-{entry.id}">Case Sensitive</label>
<Switch
name="caseSensitive"
name="caseSensitive-{entry.id}"
checked={entry.caseSensitive}
onCheckedChange={(e) =>
(entry.caseSensitive =
e.checked)}
aria-labelledby="caseSensitive-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Pinned</span>
<label for="constant-{entry.id}">Pinned</label>
<Switch
name="constant"
name="constant-{entry.id}"
checked={entry.constant}
onCheckedChange={(e) =>
(entry.constant = e.checked)}
aria-labelledby="constant-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Enabled</span>
<label for="enabled-{entry.id}">Enabled</label>
<Switch
name="enabled"
name="enabled-{entry.id}"
checked={entry.enabled}
onCheckedChange={(e) =>
(entry.enabled = e.checked)}
aria-labelledby="enabled-{entry.id}"
/>
</div>
</div>

View file

@ -25,7 +25,7 @@
const DefaultWorldEntry: InsertWorldLoreEntry = {
name: "",
content: "",
keys: [],
keys: "",
useRegex: false,
caseSensitive: false,
constant: false,
@ -495,41 +495,44 @@
</summary>
<div class="mt-2 flex flex-col gap-2">
<div class="flex w-full justify-between">
<span>Use Regex</span>
<label for="useRegex-{entry.id}">Use Regex</label>
<Switch
name="useRegex"
label="Use Regex"
name="useRegex-{entry.id}"
checked={entry.useRegex || false}
onCheckedChange={(e) =>
(entry.useRegex = e.checked)}
aria-labelledby="useRegex-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Case Sensitive</span>
<label for="caseSensitive-{entry.id}">Case Sensitive</label>
<Switch
name="caseSensitive"
name="caseSensitive-{entry.id}"
checked={entry.caseSensitive}
onCheckedChange={(e) =>
(entry.caseSensitive =
e.checked)}
aria-labelledby="caseSensitive-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Pinned</span>
<label for="constant-{entry.id}">Pinned</label>
<Switch
name="constant"
name="constant-{entry.id}"
checked={entry.constant}
onCheckedChange={(e) =>
(entry.constant = e.checked)}
aria-labelledby="constant-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">
<span>Enabled</span>
<label for="enabled-{entry.id}">Enabled</label>
<Switch
name="enabled"
name="enabled-{entry.id}"
checked={entry.enabled}
onCheckedChange={(e) =>
(entry.enabled = e.checked)}
aria-labelledby="enabled-{entry.id}"
/>
</div>
<div class="flex w-full justify-between">

View file

@ -110,7 +110,7 @@
const enabledKey = key + "Enabled"
return (
key !== "isImmutable" &&
(sampling![enabledKey] === undefined || sampling![enabledKey])
((sampling as any)?.[enabledKey] === undefined || (sampling as any)?.[enabledKey])
)
}
@ -139,6 +139,7 @@
$state([])
function handleSelectChange(e: Event) {
if (!socket) return
socket.emit("setUserActiveSamplingConfig", {
id: (e.target as HTMLSelectElement).value
})
@ -148,6 +149,7 @@
showNewNameModal = true
}
function handleNewNameConfirm(name: string) {
if (!socket) return
const newSamplingConfig = { ...sampling }
delete newSamplingConfig.id
delete newSamplingConfig.isImmutable
@ -182,7 +184,8 @@
}
function handleUpdate() {
if (sampling!.isImmutable) {
if (!socket || !sampling) return
if (sampling.isImmutable) {
toaster.error({
title: "Cannot Save",
description: "Cannot save immutable sampling configuration."
@ -194,11 +197,14 @@
}
function handleReset() {
sampling = { ...originalSamplingConfig }
if (originalSamplingConfig) {
sampling = { ...originalSamplingConfig }
}
}
function handleDelete() {
if (sampling!.isImmutable) {
if (!socket || !sampling) return
if (sampling.isImmutable) {
toaster.error({
title: "Cannot Delete",
description: "Cannot delete immutable sampling configuration."
@ -209,6 +215,7 @@
}
function confirmDelete() {
if (!socket) return
socket.emit("deleteSamplingConfig", {
id: userCtx.user.activeSamplingConfigId
})
@ -254,6 +261,8 @@
onMount(() => {
onclose = handleOnClose
if (!socket) return
socket.on("sampling", (message: Sockets.SamplingConfig.Response) => {
sampling = { ...message.sampling }
originalSamplingConfig = { ...message.sampling }
@ -267,7 +276,7 @@
!userCtx.user.activeSamplingConfigId &&
samplingConfigsList.length > 0
) {
socket.emit("setUserActiveSamplingConfig", {
socket?.emit("setUserActiveSamplingConfig", {
id: samplingConfigsList[0].id
})
}
@ -287,7 +296,7 @@
)
socket.on(
"createSamplingConfig",
(message: Sockets.CreateSamplingConfig.Response) => {
(message: Sockets.SamplingConfig.Response) => {
toaster.success({ title: "Sampling Config Created" })
}
)
@ -297,11 +306,12 @@
})
onDestroy(() => {
socket.off("sampling")
socket.off("samplingConfigsList")
socket.off("deleteSamplingConfig")
socket.off("updateSamplingConfig")
socket.off("createSamplingConfig")
if (!socket) return
socket.removeAllListeners("sampling")
socket.removeAllListeners("samplingConfigsList")
socket.removeAllListeners("deleteSamplingConfig")
socket.removeAllListeners("updateSamplingConfig")
socket.removeAllListeners("createSamplingConfig")
})
</script>
@ -326,12 +336,19 @@
{#if meta.type === "number" || meta.type === "boolean"}
<label
class="hover:bg-muted flex items-center gap-2 rounded p-2 transition"
for="{key}Enabled"
>
<input
id="{key}Enabled"
type="checkbox"
bind:checked={sampling[key + "Enabled"]!}
checked={(sampling as any)?.[key + "Enabled"] ?? false}
onchange={(e) => {
if (sampling) {
(sampling as any)[key + "Enabled"] = (e.target as HTMLInputElement).checked
}
}}
class="accent-primary"
disabled={sampling[key + "Enabled"] ===
disabled={(sampling as any)?.[key + "Enabled"] ===
undefined}
/>
<span class="font-medium">{meta.label}</span>
@ -446,7 +463,12 @@
max={getFieldMax(key)}
step={meta.step}
id={key}
bind:value={sampling![key]}
value={(sampling as any)?.[key] ?? 0}
oninput={(e) => {
if (sampling) {
(sampling as any)[key] = parseFloat((e.target as HTMLInputElement).value)
}
}}
class="accent-primary w-full"
/>
<div
@ -464,7 +486,12 @@
min={meta.min}
max={getFieldMax(key)}
step={meta.step}
bind:value={sampling![key]}
value={(sampling as any)?.[key] ?? 0}
oninput={(e) => {
if (sampling) {
(sampling as any)[key] = parseFloat((e.target as HTMLInputElement).value)
}
}}
id={key + "-manual"}
class="border-primary input w-16 rounded border"
onblur={() => (editingField = null)}
@ -487,7 +514,7 @@
)
}}
>
{sampling![key]}
{(sampling as any)?.[key]}
</button>
{/if}
<span
@ -538,14 +565,24 @@
<input
type="checkbox"
id={key}
bind:checked={sampling[key]}
checked={(sampling as any)?.[key] ?? false}
onchange={(e) => {
if (sampling) {
(sampling as any)[key] = (e.target as HTMLInputElement).checked
}
}}
class="accent-primary"
/>
{:else}
<input
type="text"
id={key}
bind:value={sampling[key]}
value={(sampling as any)?.[key] ?? ""}
oninput={(e) => {
if (sampling) {
(sampling as any)[key] = (e.target as HTMLInputElement).value
}
}}
class="input"
/>
{/if}

View file

@ -79,6 +79,13 @@
socket.emit("updateEasyPersonaCreation", res)
}
async function onShowHomePageBannerClick(event: { checked: boolean }) {
const res: Sockets.UpdateShowHomePageBanner.Call = {
enabled: event.checked
}
socket.emit("updateShowHomePageBanner", res)
}
onMount(() => {
onclose = async () => {
return true
@ -177,6 +184,16 @@
Easy Persona Creation
</label>
</div>
<div class="flex gap-2">
<Switch
name="show-home-page-banner"
checked={systemSettingsCtx.settings.showHomePageBanner}
onCheckedChange={onShowHomePageBannerClick}
></Switch>
<label for="show-home-page-banner" class="font-semibold">
Show Home Page Banner
</label>
</div>
</div>
<div

View file

@ -270,6 +270,7 @@
<label class="font-semibold" for="stream">Stream</label>
<input
type="checkbox"
id="stream"
name="stream"
bind:checked={extraFields.stream}
onchange={() => {

View file

@ -271,6 +271,7 @@
checked={ollamaFields.useChat}
onCheckedChange={(e) =>
(ollamaFields!.useChat = e.checked)}
aria-labelledby="useChat"
/>
</div>
<div class="flex items-center justify-between gap-4">
@ -280,6 +281,7 @@
checked={ollamaFields.stream}
onCheckedChange={(e) =>
(ollamaFields!.stream = e.checked)}
aria-labelledby="stream"
/>
</div>
<div class="flex items-center justify-between gap-4">
@ -289,6 +291,7 @@
checked={ollamaFields.think}
onCheckedChange={(e) =>
(ollamaFields!.think = e.checked)}
aria-labelledby="think"
/>
</div>
</section>

View file

@ -294,6 +294,7 @@
checked={openAIFields.stream}
onCheckedChange={(e) =>
(openAIFields!.stream = e.checked)}
aria-labelledby="stream"
/>
</div>
<div class="flex items-center justify-between gap-4">
@ -305,6 +306,7 @@
checked={openAIFields.prerenderPrompt}
onCheckedChange={(e) =>
(openAIFields!.prerenderPrompt = e.checked)}
aria-labelledby="prerenderPrompt"
/>
</div>
</section>

View file

View file

@ -377,7 +377,6 @@ async function listModels(
if (res && Array.isArray(res.models)) {
return { models: res.models }
} else {
// console.log("Ollama listModels response:", res)
return {
models: [],
error: "Unexpected response format from Ollama API"
@ -401,7 +400,6 @@ async function testConnection(
if (res && Array.isArray(res.models)) {
return { ok: true }
} else {
// console.log("Ollama testConnection response:", res)
return {
ok: false,
error: "Unexpected response format from Ollama API"

View file

@ -83,7 +83,6 @@ export class OpenAIChatAdapter extends BaseConnectionAdapter {
messages = compiledPrompt.messages
}
// Minimal params for debugging
const params: ChatCompletionCreateParamsBase = {
model,
messages,

View file

@ -180,7 +180,7 @@ export function compareVersions(a: string, b: string): -1 | 0 | 1 {
}
async function runMigrations() {
// TODO: Update this in 0.4.0 to perform pg backups. Not needed for 0.3.0
// TODO: Update this in 0.4.1 to perform pg backups. Not needed for 0.3.0
await migrate(db, {
migrationsFolder: dbConfig.migrationsDir

View file

@ -755,5 +755,6 @@ export const systemSettings = pgTable("system_settings", {
.default(true),
enableEasyPersonaCreation: boolean("enable_easy_persona_creation")
.notNull()
.default(true)
.default(true),
showHomePageBanner: boolean("show_home_page_banner").default(true),
})

View file

@ -3,7 +3,7 @@ import { and, eq } from "drizzle-orm"
import * as schema from "$lib/server/db/schema"
import * as fsPromises from "fs/promises"
import { getCharacterDataDir, handleCharacterAvatarUpload } from "../utils"
import { CharacterCard } from "@lenml/char-card-reader"
import { CharacterCard, type SpecV3 } from "@lenml/char-card-reader"
import { fileTypeFromBuffer } from "file-type"
// Helper function to process tags for character creation/update
@ -266,7 +266,9 @@ export async function characterCardImport(
card = await CharacterCard.from_file(buffer)
}
const v3Data = card.toSpecV3().data
const v3Data: SpecV3.CharacterCardV3["data"] = card.toSpecV3().data
const creationDate =
v3Data.creation_date && !isNaN(Number(v3Data.creation_date))
? new Date(Number(v3Data.creation_date)).toISOString()
@ -299,6 +301,14 @@ export async function characterCardImport(
.values(data)
.returning()
// Handle tags
const tagsNames: string[] = v3Data.tags || []
if (tagsNames.length > 0) {
await processCharacterTags(character.id, tagsNames)
}
// Extract file extension and check if it's a supported image type
let ext = ""
if (typeof message.file === "string") {

View file

@ -1683,7 +1683,6 @@ export async function getChatResponseOrder(
message: Sockets.GetChatResponseOrder.Call,
emitToUser: (event: string, data: any) => void
) {
// console.log('Debug - getChatResponseOrder called for chatId:', message.chatId)
const userId = 1 // Replace with actual user id
const chat = await getPromptChatFromDb(message.chatId, userId)
@ -1712,8 +1711,6 @@ export async function getChatResponseOrder(
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
.map((cc) => cc.character!.id)
// console.log('Debug - Characters for next turn:', { totalCharacters: chat.chatCharacters.length, activeCharacters: activeCharacters.length, charactersToUse: charactersToUse.length, sortedCharacterIds })
// Use getNextCharacterTurn to determine who should respond next based on message history
const nextCharacterId = getNextCharacterTurn(
{
@ -1730,15 +1727,11 @@ export async function getChatResponseOrder(
{ triggered: false }
)
// console.log('Debug - Next character logic simplified:', { sortedCharacterIds, nextCharacterId })
const res: Sockets.GetChatResponseOrder.Response = {
chatId: message.chatId,
characterIds: sortedCharacterIds,
nextCharacterId
}
// console.log('Debug - Sending response:', { chatId: res.chatId, characterIdsCount: res.characterIds.length, nextCharacterId: res.nextCharacterId })
emitToUser("getChatResponseOrder", res)
}

View file

@ -83,6 +83,7 @@ import {
updateShowAllCharacterFields,
updateEasyCharacterCreation,
updateEasyPersonaCreation,
updateShowHomePageBanner,
systemSettings
} from "./systemSettings"
import {
@ -159,6 +160,7 @@ export function connectSockets(io: {
register(socket, updateShowAllCharacterFields, emitToUser)
register(socket, updateEasyCharacterCreation, emitToUser)
register(socket, updateEasyPersonaCreation, emitToUser)
register(socket, updateShowHomePageBanner, emitToUser)
// Characters
register(socket, characterList, emitToUser)

View file

@ -178,7 +178,6 @@ export async function updateLorebook(
message: Sockets.UpdateLorebook.Call,
emitToUser: (event: string, data: any) => void
) {
console.log("Updating lorebook with message:", message)
try {
const userId = 1 // TODO: Replace with actual user ID from socket data
const tags = message.lorebook.tags || []
@ -505,7 +504,10 @@ export async function createWorldLoreEntry(
const data: InsertWorldLoreEntry = message.worldLoreEntry
data.name = data.name.trim()
data.content = data.content?.trim() || ""
// data.keys = data.keys
// Convert keys to string if it's an array (frontend might send array)
data.keys = Array.isArray(data.keys)
? data.keys.join(", ")
: (data.keys || "")
// Get next available position for the lore entry
const existingBook = await db.query.lorebooks.findFirst({
@ -572,7 +574,6 @@ export async function createWorldLoreEntry(
}
async function syncLorebookBindings({ lorebookId }: { lorebookId: number }) {
console.log("Syncing lorebook bindings for lorebookId:", lorebookId)
const queries: (() => Promise<any>)[] = []
// Query all lorebook bindings for the given lorebook
const existingBindings = await db.query.lorebookBindings.findMany({
@ -689,7 +690,10 @@ export async function updateWorldLoreEntry(
const data: SelectWorldLoreEntry = { ...message.worldLoreEntry }
data.name = data.name!.trim()
data.content = data.content!.trim()
// data.keys = data.keys
// Convert keys to string if it's an array (frontend might send array)
data.keys = Array.isArray(data.keys)
? data.keys.join(", ")
: (data.keys || "")
const [updatedEntry] = await db
.update(schema.worldLoreEntries)
@ -1437,13 +1441,10 @@ export async function lorebookImport(
emitToUser: (event: string, data: any) => void
) {
try {
console.log("Importing lorebook data:", message.lorebookData)
let charId: number | undefined = message.characterId
let char: Partial<SelectCharacter> | undefined = undefined
let card = CharacterBook.from_json(message.lorebookData)
console.log("Importing lorebook data:", card)
if (!card) {
return socket.emit("error", { error: "No lorebook data provided." })
}
@ -1493,7 +1494,6 @@ export async function lorebookImport(
} else if ((entry.priority || 1) > 3) {
entry.priority = 3
}
// console.log("Importing lore entry:", JSON.stringify(entry))
queries.push(
db.insert(schema.worldLoreEntries).values({
name: entry.name || entry.comment || "Imported Entry",

View file

@ -462,8 +462,6 @@ export async function ollamaSearchAvailableModels(
const data = await response.json()
// console.log("OllamaDB response:", data)
// Transform ollamadb.dev response to our format
models = (data.models || []).map((model: any) => ({
name: model.model_identifier || model.model_name,

View file

@ -143,3 +143,31 @@ export async function updateEasyPersonaCreation(
emitToUser("error", res)
}
}
export async function updateShowHomePageBanner(
socket: any,
message: Sockets.UpdateShowHomePageBanner.Call,
emitToUser: (event: string, data: any) => void
) {
try {
await db
.update(schema.systemSettings)
.set({
showHomePageBanner: message.enabled
})
.where(eq(schema.systemSettings.id, 1))
const res: Sockets.UpdateShowHomePageBanner.Response = {
success: true,
enabled: message.enabled
}
emitToUser("updateShowHomePageBanner", res)
await systemSettings(socket, {}, emitToUser) // Refresh system settings after update
} catch (error: any) {
console.error("Update show home page banner error:", error)
const res = {
error: "Failed to update show home page banner setting"
}
emitToUser("error", res)
}
}

View file

@ -8,39 +8,11 @@ export function getNextCharacterTurn(
opts: { triggered?: boolean } = {}
): number | null {
const { triggered = false } = opts
console.log("Debug - getNextCharacterTurn called with:", {
charactersLength: chat.chatCharacters?.length,
personasLength: chat.chatPersonas?.length,
triggered
})
if (!chat.chatCharacters?.length || !chat.chatPersonas?.length) {
console.log(
"Debug - getNextCharacterTurn early return: no characters or personas"
)
return null
}
// Validate input data
if (!chat.chatCharacters?.every((cc) => cc.character?.id)) {
console.error(
"Debug - Invalid character data detected:",
chat.chatCharacters?.map((cc) => ({
hasCharacter: !!cc.character,
characterId: cc.character?.id
}))
)
}
// Ensure positions are consistent
const positions = chat.chatCharacters
.map((cc) => cc.position)
.filter((p) => p !== null && p !== undefined)
const hasDuplicatePositions = positions.length !== new Set(positions).size
if (hasDuplicatePositions) {
console.warn("Debug - Duplicate positions detected:", positions)
}
// Sort ALL characters by position first with normalization, then filter active ones while preserving order
const allCharactersSorted = chat.chatCharacters
.slice()
@ -52,28 +24,8 @@ export function getNextCharacterTurn(
const activeCharacters = allCharactersSorted.filter((cc) => cc.isActive)
console.log("Debug - Active characters filtering:", {
totalCharacters: allCharactersSorted.length,
activeCharacters: activeCharacters.length,
activeCharacterIds: activeCharacters.map((cc) => cc.character.id)
})
console.log("Debug - Character positions and activity:", {
characters: activeCharacters.map((cc) => ({
id: cc.character.id,
name: cc.character.name,
position: cc.position,
normalizedPosition: cc.normalizedPosition,
isActive: cc.isActive
})),
poolSize: 0, // Will be set after pool calculation
lastPersonaIdx: -1, // Will be set after calculation
triggered
})
// If no active characters, return null
if (activeCharacters.length === 0) {
console.log("Debug - getNextCharacterTurn: no active characters")
return null
}
@ -92,12 +44,6 @@ export function getNextCharacterTurn(
// Pool of messages since the last persona message (exclusive)
const pool = chat.chatMessages.slice(lastPersonaIdx + 1)
console.log("Debug - Message pool analysis:", {
lastPersonaIdx,
poolSize: pool.length,
totalMessages: chat.chatMessages.length
})
if (!triggered) {
// Round-robin: find first active character who hasn't replied since last user message
for (const cc of activeCharacters) {
@ -106,18 +52,12 @@ export function getNextCharacterTurn(
msg.role === "assistant" &&
msg.characterId === cc.character.id
)
console.log(
`Debug - Checking character ${cc.character.name} (ID: ${cc.character.id}): hasMessage=${hasMessage}`
)
if (!hasMessage) {
return cc.character.id
}
}
// If all active characters have replied, stop the turn cycle (return null)
console.log(
"Debug - All characters have replied, stopping turn cycle"
)
return null
} else {
// For triggered: check if each character has replied since the last user message
@ -127,17 +67,44 @@ export function getNextCharacterTurn(
msg.role === "assistant" &&
msg.characterId === cc.character.id
)
console.log(
`Debug - Triggered mode - Checking character ${cc.character.name} (ID: ${cc.character.id}): hasRecentReply=${hasRecentReply}`
)
if (!hasRecentReply) {
return cc.character.id
}
}
// If all have replied, stop the turn cycle (return null)
console.log(
"Debug - Triggered mode - All characters have replied, stopping turn cycle"
)
// If all have replied in the normal flow, select the character with the oldest most recent reply
// (furthest removed from their latest response)
// Find the most recent message for each character in the pool
let oldestRecentCharacter: { id: number; lastMessageIndex: number } | null = null
for (const cc of activeCharacters) {
// Find the most recent message from this character in the pool
let lastMessageIndex = -1
for (let i = pool.length - 1; i >= 0; i--) {
const msg = pool[i]
if (msg.role === "assistant" && msg.characterId === cc.character.id) {
lastMessageIndex = i
break // Found the most recent message from this character
}
}
// If this character has a message in the pool, compare their most recent message
if (lastMessageIndex >= 0) {
if (!oldestRecentCharacter || lastMessageIndex < oldestRecentCharacter.lastMessageIndex) {
oldestRecentCharacter = {
id: cc.character.id,
lastMessageIndex: lastMessageIndex
}
}
}
}
if (oldestRecentCharacter) {
return oldestRecentCharacter.id
}
// Fallback: if for some reason no character is found with a recent reply, return null
return null
}
}

View file

@ -163,12 +163,8 @@ export class ContentInfillEngine {
// Store for use in template context building
this.currentInterpolationContext = interpolationContext
// Debug timing
const debugTimings: any[] = []
// Main processing loop
while (!state.completed) {
const iterStart = Date.now()
state.iterationCount++
// Process content for current priority level
@ -222,14 +218,6 @@ export class ContentInfillEngine {
}
}
const iterEnd = Date.now()
debugTimings.push({
priority: state.priority,
chatMessagesCount: state.chatMessages.length,
totalTokens: state.totalTokens,
iterationMs: iterEnd - iterStart
})
// Safety check to prevent infinite loops
if (state.iterationCount > 1000) {
console.error(

View file

@ -754,8 +754,6 @@ export class PromptBuilder {
historyTotal = lorebook.historyEntries.length
}
// console.log("Prompt messages:" + JSON.stringify(renderedMessages))
// Default: return as before
return {
prompt: renderedPrompt,

View file

View file

@ -27,7 +27,9 @@
{#if socketsInitialized}
<Layout>
{@render children?.()}
{#key page.route}
{@render children?.()}
{/key}
</Layout>
{/if}
{#if page.data?.isNewerReleaseAvailable && showUpdateBar}

View file

@ -116,6 +116,8 @@
// Quick setup functions
function handleQuickSetup() {
if (!socket) return
// Auto-set the default configs if not already set
if (!userCtx.user?.activeSamplingConfig) {
socket.emit("setUserActiveSamplingConfig", { id: 1 }) // Default
@ -140,18 +142,23 @@
}
function connectToOllamaModel(modelName: string) {
if (!socket) return
socket.emit("ollamaConnectModel", { modelName: modelName })
}
function checkOllamaConnection() {
if (!socket) return
socket.emit("ollamaVersion", {})
}
function refreshOllamaModels() {
if (!socket) return
socket.emit("ollamaModelsList", {})
}
function createSamplePersona() {
if (!socket) return
const samplePersona = {
name: "You",
description:
@ -171,8 +178,16 @@
closeWizard()
}
function toggleBanner() {
const res: Sockets.UpdateShowHomePageBanner.Call = {
enabled: false
}
socket.emit("updateShowHomePageBanner", res)
}
// Listen for socket events
onMount(() => {
socket.on("characterList", (msg: Sockets.CharacterList.Response) => {
characters = msg.characterList || []
})
@ -256,10 +271,6 @@
// Handle successful chat creation
socket.on("createChat", (res: any) => {
if (res.chat) {
toaster.success({
title: "Chat Created!",
description: "Your chat is ready. Opening chat panel..."
})
// Close wizard if it's open
if (showWizard) {
closeWizard()
@ -298,13 +309,24 @@
<div
class="flex flex-1 flex-col items-center justify-center gap-4 px-2 md:px-0"
>
<img
src={themeCtx.mode === "dark"
? "logo-w-text-dark.png"
: "logo-w-text.png"}
alt="Serene Pub Logo"
class="bg-primary-500/25 w-full rounded-xl"
/>
{#if systemSettingsCtx.settings.showHomePageBanner}
<div class="relative w-full">
<img
src={themeCtx.mode === "dark"
? "logo-w-text-dark.png"
: "logo-w-text.png"}
alt="Serene Pub Logo"
class="bg-primary-500/25 w-full rounded-xl"
/>
<button
class="text-primary-800 hover:text-primary-900 dark:text-primary-200 hover:dark:text-primary-100 absolute right-2 top-2 text-xl leading-none font-bold bg-black/20 hover:bg-black/30 rounded-full w-6 h-6 flex items-center justify-center"
onclick={toggleBanner}
title="Hide banner"
>
×
</button>
</div>
{/if}
<!-- Alpha disclaimer card below the logo -->
<div
@ -762,7 +784,7 @@
<button
class="btn preset-filled-primary-500"
onclick={() => {
if (selectedOllamaModel) {
if (selectedOllamaModel && socket) {
// Manual connection creation
const newConnection = {
name: `Ollama - ${selectedOllamaModel}`,

View file

@ -184,16 +184,20 @@
}
$effect(() => {
const _chatId = page.params.id
if (_chatId) {
chatId = Number.parseInt(_chatId)
// React to chatId changes (which is derived from page.params.id)
if (chatId) {
// Reset state when switching chats
chat = undefined // Clear current chat data
pagination = undefined
chatResponseOrder = undefined
draftCompiledPrompt = undefined
editChatMessage = undefined
newMessage = ""
isInitialLoad = true
lastSeenMessageId = null
lastSeenMessageContent = ""
loadingOlderMessages = false
socket.emit("chat", { id: chatId, limit: 25, offset: 0 })
// console.log('Debug - Emitting getChatResponseOrder for chatId:', chatId)
socket.emit("getChatResponseOrder", { chatId })
}
})
@ -472,7 +476,7 @@
onMount(() => {
socket.on("chat", (msg: Sockets.Chat.Response) => {
if (msg.chat.id === Number.parseInt(page.params.id)) {
if (msg.chat.id === chatId) {
if (chat && loadingOlderMessages) {
// Merge older messages (avoiding duplicates)
const existingIds = new Set(

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB