mirror of
https://github.com/doolijb/serene-pub.git
synced 2026-04-26 10:31:13 +00:00
Fixes, readme updates
This commit is contained in:
parent
60ee83aaa7
commit
6882a96e57
46 changed files with 2661 additions and 371 deletions
|
|
@ -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
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
||||
# ===========================================
|
||||
|
|
|
|||
|
|
@ -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
228
README.md
|
|
@ -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
|
|||
| -------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
|
||||
|  |  |  |  |
|
||||
|
||||
### Ollama Manager
|
||||
|
||||
| Available Models | Downloads | Installed Models | Settings |
|
||||
| -------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
|  |  |  |  |
|
||||
|
||||
### 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>
|
||||
|
|
|
|||
|
|
@ -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 "========================================"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 "========================================"
|
||||
|
|
|
|||
1
drizzle/0010_calm_nightshade.sql
Normal file
1
drizzle/0010_calm_nightshade.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "system_settings" ADD COLUMN "show_home_page_banner" boolean DEFAULT true;
|
||||
2302
drizzle/meta/0010_snapshot.json
Normal file
2302
drizzle/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -71,6 +71,13 @@
|
|||
"when": 1754857930869,
|
||||
"tag": "0009_petite_mimic",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1755411731603,
|
||||
"tag": "0010_calm_nightshade",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
11
src/app.d.ts
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@
|
|||
let systemSettingsCtx: SystemSettingsCtx = $state({
|
||||
settings: {
|
||||
ollamaManagerEnabled: false,
|
||||
ollamaManagerBaseUrl: ""
|
||||
ollamaManagerBaseUrl: "",
|
||||
showAllCharacterFields: false,
|
||||
enableEasyCharacterCreation: true,
|
||||
enableEasyPersonaCreation: true,
|
||||
showHomePageBanner: true
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@
|
|||
<label class="font-semibold" for="stream">Stream</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="stream"
|
||||
name="stream"
|
||||
bind:checked={extraFields.stream}
|
||||
onchange={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
0
src/lib/client/sockets/typedSocket.ts
Normal file
0
src/lib/client/sockets/typedSocket.ts
Normal 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"
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ export class OpenAIChatAdapter extends BaseConnectionAdapter {
|
|||
messages = compiledPrompt.messages
|
||||
}
|
||||
|
||||
// Minimal params for debugging
|
||||
const params: ChatCompletionCreateParamsBase = {
|
||||
model,
|
||||
messages,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
0
src/lib/shared/sockets/types.ts
Normal file
0
src/lib/shared/sockets/types.ts
Normal file
|
|
@ -27,7 +27,9 @@
|
|||
|
||||
{#if socketsInitialized}
|
||||
<Layout>
|
||||
{@render children?.()}
|
||||
{#key page.route}
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
</Layout>
|
||||
{/if}
|
||||
{#if page.data?.isNewerReleaseAvailable && showUpdateBar}
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
BIN
static/screenshots/sidebar-ollama-manager-available.png
Normal file
BIN
static/screenshots/sidebar-ollama-manager-available.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
BIN
static/screenshots/sidebar-ollama-manager-downloads.png
Normal file
BIN
static/screenshots/sidebar-ollama-manager-downloads.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
static/screenshots/sidebar-ollama-manager-installed.png
Normal file
BIN
static/screenshots/sidebar-ollama-manager-installed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
BIN
static/screenshots/sidebar-ollama-manager-settings.png
Normal file
BIN
static/screenshots/sidebar-ollama-manager-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Loading…
Add table
Add a link
Reference in a new issue