mirror of
https://github.com/doolijb/serene-pub.git
synced 2026-07-10 00:08:25 +00:00
Include node in builds, improve accessibility, fix char loops
This commit is contained in:
parent
33fbbdf110
commit
63e376d887
19 changed files with 588 additions and 270 deletions
71
.github/workflows/release.yml
vendored
71
.github/workflows/release.yml
vendored
|
|
@ -60,6 +60,77 @@ jobs:
|
|||
# Clean node_modules with modclean, preserving license files
|
||||
npx modclean --run --patterns="default:safe,default:caution,default:danger" --ignore="**/LICENSE,**/COPYING,**/NOTICE,**/README*,**/COPYRIGHT,**/AUTHORS,**/CONTRIBUTORS"
|
||||
|
||||
- name: Download Node.js runtime for target platform
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Downloading Node.js for ${{ matrix.target }}..."
|
||||
mkdir -p temp-node
|
||||
cd temp-node
|
||||
|
||||
case "${{ matrix.target }}" in
|
||||
linux-x64)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-linux-x64.tar.xz"
|
||||
curl -L -o node.tar.xz "$NODE_URL"
|
||||
tar -xf node.tar.xz
|
||||
cp node-v20.13.1-linux-x64/bin/node ../node
|
||||
;;
|
||||
linux-arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-linux-arm64.tar.xz"
|
||||
curl -L -o node.tar.xz "$NODE_URL"
|
||||
tar -xf node.tar.xz
|
||||
cp node-v20.13.1-linux-arm64/bin/node ../node
|
||||
;;
|
||||
linux-arm)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-linux-armv7l.tar.xz"
|
||||
curl -L -o node.tar.xz "$NODE_URL"
|
||||
tar -xf node.tar.xz
|
||||
cp node-v20.13.1-linux-armv7l/bin/node ../node
|
||||
;;
|
||||
linux-ppc64)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-linux-ppc64le.tar.xz"
|
||||
curl -L -o node.tar.xz "$NODE_URL"
|
||||
tar -xf node.tar.xz
|
||||
cp node-v20.13.1-linux-ppc64le/bin/node ../node
|
||||
;;
|
||||
macos-arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-darwin-arm64.tar.gz"
|
||||
curl -L -o node.tar.gz "$NODE_URL"
|
||||
tar -xzf node.tar.gz
|
||||
cp node-v20.13.1-darwin-arm64/bin/node ../node
|
||||
;;
|
||||
windows-x64)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-win-x64.zip"
|
||||
curl -L -o node.zip "$NODE_URL"
|
||||
unzip -q node.zip
|
||||
cp node-v20.13.1-win-x64/node.exe ../node.exe
|
||||
;;
|
||||
windows-arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-win-arm64.zip"
|
||||
curl -L -o node.zip "$NODE_URL"
|
||||
unzip -q node.zip
|
||||
cp node-v20.13.1-win-arm64/node.exe ../node.exe
|
||||
;;
|
||||
esac
|
||||
|
||||
cd ..
|
||||
rm -rf temp-node
|
||||
|
||||
# Verify the Node.js binary was downloaded
|
||||
if [[ "${{ matrix.target }}" == windows-* ]]; then
|
||||
if [ ! -f "node.exe" ]; then
|
||||
echo "ERROR: Failed to download node.exe"
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloaded node.exe ($(du -h node.exe | cut -f1))"
|
||||
else
|
||||
if [ ! -f "node" ]; then
|
||||
echo "ERROR: Failed to download node binary"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x node
|
||||
echo "Downloaded node binary ($(du -h node | cut -f1))"
|
||||
fi
|
||||
|
||||
- name: Bundle for ${{ matrix.target }}
|
||||
run: node scripts/bundle-dist.js ${{ matrix.target }}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ To run the app:
|
|||
|
||||
- If you get a permission error, run: chmod +x run.sh
|
||||
|
||||
3. On first launch, the app will automatically download and set up a local Node.js runtime if needed. No manual installation is required.
|
||||
3. Node.js runtime is included - no installation or download required! The application will start immediately.
|
||||
|
||||
4. Optional Configuration:
|
||||
4. To stop the application, press Ctrl+C in the terminal window.
|
||||
|
||||
5. Optional Configuration:
|
||||
- Copy ".env.example" to ".env" to customize settings
|
||||
- Edit ".env" to set custom data directory: SERENE_PUB_DATA_DIR=/path/to/data
|
||||
- Change server ports: SOCKETS_PORT=3001, PORT=3000
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
#!/bin/sh
|
||||
# Serene Pub Application Launcher
|
||||
# Licensed under AGPL-3.0 - See LICENSE file
|
||||
# Source: https://github.com/doolijb/serene-pub
|
||||
|
||||
DIR=$(dirname "$0")
|
||||
export NODE_ENV=production
|
||||
NODE_BIN="$DIR/node"
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-linux-x64.tar.xz"
|
||||
NODE_ARCHIVE="node-archive.linux.tar.xz"
|
||||
NODE_DIR="node-v20.13.1-linux-x64"
|
||||
NODE_BIN_PATH="$NODE_DIR/bin/node"
|
||||
APP_MAIN="$DIR/build/index.js"
|
||||
|
||||
# Load environment variables from .env file if present
|
||||
ENV_FILE="$DIR/.env"
|
||||
|
|
@ -14,18 +15,64 @@ if [ -f "$ENV_FILE" ]; then
|
|||
export $(grep -v '^#' "$ENV_FILE" | xargs -d '\n')
|
||||
fi
|
||||
|
||||
echo "========================================"
|
||||
echo "Serene Pub - AI Chat Application"
|
||||
echo "https://github.com/doolijb/serene-pub"
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
# Verify Node.js runtime exists
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
echo "Downloading Node.js..."
|
||||
curl -L -o "$NODE_ARCHIVE" "$NODE_URL"
|
||||
tar -xf "$NODE_ARCHIVE"
|
||||
cp "$NODE_BIN_PATH" "$NODE_BIN"
|
||||
rm -rf "$NODE_DIR"
|
||||
rm -f "$NODE_ARCHIVE"
|
||||
echo "ERROR: Node.js runtime not found at $NODE_BIN"
|
||||
echo "Please ensure all application files are present in this directory."
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify application files exist
|
||||
if [ ! -f "$APP_MAIN" ]; then
|
||||
echo "ERROR: Application file not found at $APP_MAIN"
|
||||
echo "Please ensure all application files are present in this directory."
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$NODE_BIN"
|
||||
|
||||
echo "Starting Serene Pub..."
|
||||
"$NODE_BIN" "$DIR/build/index.js" "$@"
|
||||
echo
|
||||
echo "The application will be available at:"
|
||||
echo " - http://localhost:3000"
|
||||
echo " - http://127.0.0.1:3000"
|
||||
echo
|
||||
echo "Press Ctrl+C to stop the application."
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
# Set up signal handling for graceful shutdown
|
||||
trap 'echo; echo "Shutting down Serene Pub..."; kill $NODE_PID 2>/dev/null; wait $NODE_PID 2>/dev/null; echo "Serene Pub stopped."; exit 0' INT TERM
|
||||
|
||||
# Start the application in background to handle signals
|
||||
"$NODE_BIN" "$APP_MAIN" "$@" &
|
||||
NODE_PID=$!
|
||||
|
||||
# Wait for the Node.js process
|
||||
wait $NODE_PID
|
||||
EXIT_CODE=$?
|
||||
|
||||
echo
|
||||
echo "========================================"
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "Serene Pub stopped normally."
|
||||
else
|
||||
echo "Serene Pub exited with code: $EXIT_CODE"
|
||||
echo "Check the output above for any error messages."
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
exit $EXIT_CODE
|
||||
read
|
||||
|
|
|
|||
|
|
@ -11,18 +11,20 @@ To run the app:
|
|||
3. Navigate to the extracted folder (e.g., serene-pub-<version>-macos):
|
||||
cd /path/to/serene-pub-<version>-macos
|
||||
|
||||
3. Run the app with:
|
||||
4. Run the app with:
|
||||
bash run.sh
|
||||
|
||||
4. On first launch, the app will automatically download and set up a local Node.js runtime if needed. No manual installation is required.
|
||||
5. Node.js runtime is included - no installation or download required! The application will start immediately.
|
||||
|
||||
5. Optional Configuration:
|
||||
6. To stop the application, press Ctrl+C in the terminal window.
|
||||
|
||||
7. Optional Configuration:
|
||||
- Copy ".env.example" to ".env" to customize settings
|
||||
- Edit ".env" to set custom data directory: SERENE_PUB_DATA_DIR=/path/to/data
|
||||
- Change server ports: SOCKETS_PORT=3001, PORT=3000
|
||||
- Disable auto-browser opening: SERENE_AUTO_OPEN=1
|
||||
|
||||
6. On your Mac, access Serene Pub from http://0.0.0.0:3000 or http://localhost:3000.
|
||||
8. On your Mac, access Serene Pub from http://localhost:3000.
|
||||
|
||||
Notes:
|
||||
- Serene Pub will attempt to automatically identify your local network IP address, allowing access across other devices (e.g. mobile phones.)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
#!/bin/sh
|
||||
# Serene Pub Application Launcher
|
||||
# Licensed under AGPL-3.0 - See LICENSE file
|
||||
# Source: https://github.com/doolijb/serene-pub
|
||||
|
||||
DIR=$(dirname "$0")
|
||||
export NODE_ENV=production
|
||||
NODE_BIN="$DIR/node"
|
||||
NODE_URL="https://nodejs.org/dist/v20.13.1/node-v20.13.1-darwin-x64.tar.gz"
|
||||
NODE_ARCHIVE="node-archive.macos.tar.gz"
|
||||
NODE_DIR="node-v20.13.1-darwin-x64"
|
||||
NODE_BIN_PATH="$NODE_DIR/bin/node"
|
||||
APP_MAIN="$DIR/build/index.js"
|
||||
|
||||
# Load environment variables from .env file if present
|
||||
ENV_FILE="$DIR/.env"
|
||||
|
|
@ -14,18 +15,64 @@ if [ -f "$ENV_FILE" ]; then
|
|||
export $(grep -v '^#' "$ENV_FILE" | xargs)
|
||||
fi
|
||||
|
||||
echo "========================================"
|
||||
echo "Serene Pub - AI Chat Application"
|
||||
echo "https://github.com/doolijb/serene-pub"
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
# Verify Node.js runtime exists
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
echo "Downloading Node.js..."
|
||||
curl -L -o "$NODE_ARCHIVE" "$NODE_URL"
|
||||
tar -xzf "$NODE_ARCHIVE"
|
||||
cp "$NODE_BIN_PATH" "$NODE_BIN"
|
||||
rm -rf "$NODE_DIR"
|
||||
rm -f "$NODE_ARCHIVE"
|
||||
echo "ERROR: Node.js runtime not found at $NODE_BIN"
|
||||
echo "Please ensure all application files are present in this directory."
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify application files exist
|
||||
if [ ! -f "$APP_MAIN" ]; then
|
||||
echo "ERROR: Application file not found at $APP_MAIN"
|
||||
echo "Please ensure all application files are present in this directory."
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$NODE_BIN"
|
||||
|
||||
echo "Starting Serene Pub..."
|
||||
"$NODE_BIN" "$DIR/build/index.js" "$@"
|
||||
echo
|
||||
echo "The application will be available at:"
|
||||
echo " - http://localhost:3000"
|
||||
echo " - http://127.0.0.1:3000"
|
||||
echo
|
||||
echo "Press Ctrl+C to stop the application."
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
# Set up signal handling for graceful shutdown
|
||||
trap 'echo; echo "Shutting down Serene Pub..."; kill $NODE_PID 2>/dev/null; wait $NODE_PID 2>/dev/null; echo "Serene Pub stopped."; exit 0' INT TERM
|
||||
|
||||
# Start the application in background to handle signals
|
||||
"$NODE_BIN" "$APP_MAIN" "$@" &
|
||||
NODE_PID=$!
|
||||
|
||||
# Wait for the Node.js process
|
||||
wait $NODE_PID
|
||||
EXIT_CODE=$?
|
||||
|
||||
echo
|
||||
echo "========================================"
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "Serene Pub stopped normally."
|
||||
else
|
||||
echo "Serene Pub exited with code: $EXIT_CODE"
|
||||
echo "Check the output above for any error messages."
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
exit $EXIT_CODE
|
||||
read
|
||||
|
|
|
|||
|
|
@ -5,19 +5,20 @@ Thank you for downloading Serene Pub.
|
|||
To run the app:
|
||||
|
||||
1. Extract the archive to your preferred location.
|
||||
2. Open the extracted folder (e.g., serene-pub-<version>-win).
|
||||
3. Double-click the "run.bat" file to start Serene Pub.
|
||||
2. Double-click the "run.cmd" file to start Serene Pub.
|
||||
|
||||
- The first launch will automatically download and set up Node.js if needed. No manual installation is required.
|
||||
- The launcher will automatically detect your Windows architecture (x64 or ARM64) and download the appropriate Node.js version.
|
||||
- Node.js runtime is included - no installation or download required!
|
||||
- The application will start immediately.
|
||||
|
||||
4. Optional Configuration:
|
||||
3. Optional Configuration:
|
||||
- Copy ".env.example" to ".env" to customize settings
|
||||
- Edit ".env" to set custom data directory: SERENE_PUB_DATA_DIR=C:\path\to\data
|
||||
- Change server ports: SOCKETS_PORT=3001, PORT=3000
|
||||
- Disable auto-browser opening: SERENE_AUTO_OPEN=1
|
||||
|
||||
5. On your Windows PC, access Serene Pub from http://0.0.0.0:3000 or http://localhost:3000.
|
||||
4. On your Windows PC, access Serene Pub from http://localhost:3000.
|
||||
|
||||
5. To stop the application, press Ctrl+C in the command window.
|
||||
|
||||
Notes:
|
||||
- Serene Pub will attempt to automatically identify your local network IP address, allowing access across other devices (e.g. mobile phones.)
|
||||
|
|
@ -25,9 +26,9 @@ Notes:
|
|||
- Look for the "IPv4 Address" under your active network adapter (e.g., 192.168.1.42). You can access Serene Pub from your phone at http://<your-ip>:3000
|
||||
|
||||
Troubleshooting:
|
||||
- If double-clicking "run.bat" does not start the app, you may need to run it as administrator
|
||||
- If Node.js fails to download, check your internet connection or firewall settings
|
||||
- Windows Defender or antivirus software might flag the batch file - this is normal for downloaded scripts
|
||||
- If double-clicking "run.cmd" does not start the app, you may need to run it as administrator
|
||||
- Ensure all files were extracted properly from the ZIP archive
|
||||
- This is open source software - you can review all code at https://github.com/doolijb/serene-pub
|
||||
|
||||
Uninstall:
|
||||
- To remove Serene Pub, simply delete the extracted folder. No files are installed outside this directory.
|
||||
|
|
|
|||
|
|
@ -1,132 +0,0 @@
|
|||
@echo off
|
||||
REM Serene Pub Launcher - Batch File Version
|
||||
REM This script downloads Node.js if needed and starts the application
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Set environment
|
||||
set NODE_ENV=production
|
||||
|
||||
REM Get the directory where this script is located
|
||||
set DIR=%~dp0
|
||||
set DIR=%DIR:~0,-1%
|
||||
|
||||
REM Node.js configuration - detect architecture
|
||||
set NODE_BIN=%DIR%\node.exe
|
||||
|
||||
REM Detect Windows architecture
|
||||
if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||
set NODE_ARCH=arm64
|
||||
set NODE_URL=https://nodejs.org/dist/v20.13.1/node-v20.13.1-win-arm64.zip
|
||||
set NODE_DIR=%DIR%\node-v20.13.1-win-arm64
|
||||
) else (
|
||||
set NODE_ARCH=x64
|
||||
set NODE_URL=https://nodejs.org/dist/v20.13.1/node-v20.13.1-win-x64.zip
|
||||
set NODE_DIR=%DIR%\node-v20.13.1-win-x64
|
||||
)
|
||||
|
||||
set NODE_ARCHIVE=%DIR%\node-archive.win.zip
|
||||
set NODE_BIN_PATH=%NODE_DIR%\node.exe
|
||||
|
||||
REM Application paths
|
||||
set APP_MAIN=%DIR%\build\index.js
|
||||
|
||||
REM Load environment variables from .env file if present
|
||||
set ENV_FILE=%DIR%\.env
|
||||
if exist "%ENV_FILE%" (
|
||||
echo Loading environment variables from .env file...
|
||||
for /f "usebackq tokens=1,2 delims==" %%a in ("%ENV_FILE%") do (
|
||||
set "line=%%a"
|
||||
if not "!line:~0,1!"=="#" (
|
||||
if not "%%a"=="" if not "%%b"=="" (
|
||||
set "%%a=%%b"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
echo === Serene Pub Launcher ===
|
||||
echo Checking Node.js installation...
|
||||
|
||||
REM Check if Node.js is already available
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo Node.js not found. Installing...
|
||||
call :InstallNodeJs
|
||||
if errorlevel 1 goto :error
|
||||
) else (
|
||||
echo Node.js found.
|
||||
)
|
||||
|
||||
REM Verify the main application file exists
|
||||
if not exist "%APP_MAIN%" (
|
||||
echo Error: Application file not found at %APP_MAIN%
|
||||
echo Please ensure all application files are present.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Start the application
|
||||
call :StartSerenePubl
|
||||
goto :end
|
||||
|
||||
:InstallNodeJs
|
||||
echo Downloading Node.js v20.13.1 for %NODE_ARCH%...
|
||||
|
||||
REM Use PowerShell for download (more reliable than curl/wget alternatives)
|
||||
powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference = 'SilentlyContinue'; try { Invoke-WebRequest -Uri '%NODE_URL%' -OutFile '%NODE_ARCHIVE%' -UserAgent 'SerenePublauncher/1.0 (Windows)' } catch { Write-Host 'Download failed:' $_.Exception.Message; exit 1 }}"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Error downloading Node.js. Please check your internet connection.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Extracting Node.js...
|
||||
powershell -Command "& {try { Expand-Archive -Path '%NODE_ARCHIVE%' -DestinationPath '%DIR%' -Force } catch { Write-Host 'Extraction failed:' $_.Exception.Message; exit 1 }}"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Error extracting Node.js archive.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Move node.exe to the expected location
|
||||
if exist "%NODE_BIN_PATH%" (
|
||||
copy "%NODE_BIN_PATH%" "%NODE_BIN%" >nul
|
||||
echo Node.js installed successfully.
|
||||
) else (
|
||||
echo Error: Node.js binary not found after extraction.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Cleanup
|
||||
if exist "%NODE_DIR%" rmdir /s /q "%NODE_DIR%"
|
||||
if exist "%NODE_ARCHIVE%" del "%NODE_ARCHIVE%"
|
||||
|
||||
exit /b 0
|
||||
|
||||
:StartSerenePubl
|
||||
echo Starting Serene Pub...
|
||||
echo Press Ctrl+C to stop the application.
|
||||
echo.
|
||||
|
||||
REM Start the Node.js application
|
||||
"%NODE_BIN%" "%APP_MAIN%"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Serene Pub exited with error code: %errorlevel%
|
||||
) else (
|
||||
echo Serene Pub stopped successfully.
|
||||
)
|
||||
|
||||
exit /b 0
|
||||
|
||||
:error
|
||||
echo An error occurred. Please try again.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:end
|
||||
echo.
|
||||
pause
|
||||
118
dist-assets/windows/run.cmd
Normal file
118
dist-assets/windows/run.cmd
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
@echo off
|
||||
REM Serene Pub Application Launcher
|
||||
REM Licensed under AGPL-3.0 - See LICENSE file
|
||||
REM Source: https://github.com/doolijb/serene-pub
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM === Configuration ===
|
||||
set NODE_ENV=production
|
||||
set DIR=%~dp0
|
||||
set DIR=%DIR:~0,-1%
|
||||
set NODE_BIN=%DIR%\node.exe
|
||||
set APP_MAIN=%DIR%\build\index.js
|
||||
|
||||
REM === Load Environment Variables ===
|
||||
set ENV_FILE=%DIR%\.env
|
||||
if exist "%ENV_FILE%" (
|
||||
echo Loading configuration from .env file...
|
||||
for /f "usebackq tokens=1,2 delims==" %%a in ("%ENV_FILE%") do (
|
||||
set "line=%%a"
|
||||
if not "!line:~0,1!"=="#" (
|
||||
if not "%%a"=="" if not "%%b"=="" (
|
||||
set "%%a=%%b"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
echo ========================================
|
||||
echo Serene Pub - AI Chat Application
|
||||
echo https://github.com/doolijb/serene-pub
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM === Verify Node.js Runtime ===
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo ERROR: Node.js runtime not found at %NODE_BIN%
|
||||
echo Please ensure all application files are present in this directory.
|
||||
goto :Error
|
||||
)
|
||||
|
||||
REM === Verify Application Files ===
|
||||
if not exist "%APP_MAIN%" (
|
||||
echo ERROR: Application file not found at %APP_MAIN%
|
||||
echo Please ensure all application files are present in this directory.
|
||||
goto :Error
|
||||
)
|
||||
|
||||
echo Starting Serene Pub...
|
||||
echo.
|
||||
echo The application will be available at:
|
||||
echo - http://localhost:3000
|
||||
echo - http://127.0.0.1:3000
|
||||
echo.
|
||||
echo Press Ctrl+C to stop the application.
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM === Start Application ===
|
||||
"%NODE_BIN%" "%APP_MAIN%"
|
||||
|
||||
REM === Application Exit Handling ===
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
echo.
|
||||
echo ========================================
|
||||
if %EXIT_CODE% equ 0 (
|
||||
echo Serene Pub stopped normally.
|
||||
) else (
|
||||
echo Serene Pub exited with code: %EXIT_CODE%
|
||||
echo Check the output above for any error messages.
|
||||
)
|
||||
echo.
|
||||
goto :End
|
||||
|
||||
:Error
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Setup failed. Please ensure:
|
||||
echo 1. All application files are present
|
||||
echo 2. Node.js runtime (node.exe) is included
|
||||
echo 3. Visit https://github.com/doolijb/serene-pub for help
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:End
|
||||
echo Press any key to exit...
|
||||
pause >nul
|
||||
exit /b %EXIT_CODE%
|
||||
"%NODE_BIN%" "%APP_MAIN%"
|
||||
|
||||
REM === Application Exit Handling ===
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
echo.
|
||||
echo ========================================
|
||||
if %EXIT_CODE% equ 0 (
|
||||
echo Serene Pub stopped normally.
|
||||
) else (
|
||||
echo Serene Pub exited with code: %EXIT_CODE%
|
||||
echo Check the output above for any error messages.
|
||||
)
|
||||
echo.
|
||||
goto :End
|
||||
|
||||
:Error
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Setup failed. Please try the following:
|
||||
echo 1. Check your internet connection
|
||||
echo 2. Temporarily disable antivirus/firewall
|
||||
echo 3. Run as administrator if needed
|
||||
echo 4. Visit https://github.com/doolijb/serene-pub for help
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:End
|
||||
echo Press any key to exit...
|
||||
pause >nul
|
||||
exit /b %EXIT_CODE%
|
||||
|
|
@ -281,6 +281,22 @@ if (!target) {
|
|||
fs.copyFileSync(instrFile, path.join(outDir, "INSTRUCTIONS.txt"))
|
||||
}
|
||||
|
||||
// Copy Node.js binary for the target platform
|
||||
const isWindows = target.platform === "win32"
|
||||
const nodeSrcName = isWindows ? "node.exe" : "node"
|
||||
const nodeSrcPath = path.resolve(__dirname, "..", nodeSrcName)
|
||||
const nodeDestPath = path.join(outDir, nodeSrcName)
|
||||
|
||||
if (fs.existsSync(nodeSrcPath)) {
|
||||
fs.copyFileSync(nodeSrcPath, nodeDestPath)
|
||||
if (!isWindows) {
|
||||
fs.chmodSync(nodeDestPath, 0o755)
|
||||
}
|
||||
console.log(`Copied Node.js binary: ${nodeSrcName}`)
|
||||
} else {
|
||||
console.warn(`Warning: Node.js binary not found at ${nodeSrcPath}`)
|
||||
}
|
||||
|
||||
// Copy all run files from dist-assets/<os>/
|
||||
const runFiles = fs
|
||||
.readdirSync(
|
||||
|
|
|
|||
|
|
@ -25,26 +25,34 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<header class="w-full">
|
||||
<header class="w-full" role="banner">
|
||||
<div
|
||||
class="bg-surface-100-900 bg-opacity-25 relative mx-auto flex w-full justify-between px-4 py-2 backdrop-blur"
|
||||
>
|
||||
<!-- Desktop left nav -->
|
||||
<div class="hidden flex-1 justify-start gap-2 lg:flex">
|
||||
<nav
|
||||
class="hidden flex-1 justify-start gap-2 lg:flex"
|
||||
aria-label="Left navigation"
|
||||
role="navigation"
|
||||
>
|
||||
{#each Object.entries(panelsCtx.leftNav) as [key, item]}
|
||||
{@const isOpen = panelsCtx.leftPanel === key}
|
||||
<button
|
||||
title={item.title}
|
||||
onclick={() => panelsCtx.openPanel({ key })}
|
||||
aria-pressed={isOpen}
|
||||
aria-label="Open {item.title} panel"
|
||||
type="button"
|
||||
>
|
||||
<item.icon
|
||||
class="{isOpen
|
||||
? 'text-primary-800-200'
|
||||
: ''} hover:text-primary-500 h-5 w-5 transition-colors"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Title (centered absolutely for desktop) -->
|
||||
<div
|
||||
|
|
@ -53,38 +61,49 @@
|
|||
<a
|
||||
class="text-foreground funnel-display pointer-events-auto text-xl font-bold tracking-tight whitespace-nowrap"
|
||||
href="/"
|
||||
aria-label="Serene Pub - Home"
|
||||
>
|
||||
Serene Pub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Desktop right nav -->
|
||||
<div class="hidden flex-1 justify-end gap-2 lg:flex">
|
||||
<nav
|
||||
class="hidden flex-1 justify-end gap-2 lg:flex"
|
||||
aria-label="Right navigation"
|
||||
role="navigation"
|
||||
>
|
||||
{#each Object.entries(panelsCtx.rightNav) as [key, item]}
|
||||
{@const isOpen = panelsCtx.rightPanel === key}
|
||||
<button
|
||||
class="btn-ghost"
|
||||
title={item.title}
|
||||
onclick={() => panelsCtx.openPanel({ key })}
|
||||
aria-pressed={isOpen}
|
||||
aria-label="Open {item.title} panel"
|
||||
type="button"
|
||||
>
|
||||
<item.icon
|
||||
class="{isOpen
|
||||
? 'text-primary-800-200'
|
||||
: ''} hover:text-primary-500 h-5 w-5 transition-colors"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-2 lg:hidden">
|
||||
<button
|
||||
class="btn preset-tonal"
|
||||
aria-label="Open menu"
|
||||
aria-label="Open navigation menu"
|
||||
onclick={() => {
|
||||
panelsCtx.isMobileMenuOpen = true
|
||||
}}
|
||||
type="button"
|
||||
aria-expanded={panelsCtx.isMobileMenuOpen}
|
||||
>
|
||||
<Icons.Menu class="text-foreground h-6 w-6" />
|
||||
<Icons.Menu class="text-foreground h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -243,12 +243,18 @@
|
|||
{#if !!userCtx.user}
|
||||
<div
|
||||
class="bg-surface-100-900 relative h-full max-h-[100dvh] w-full justify-between"
|
||||
role="application"
|
||||
aria-label="Serene Pub Chat Application"
|
||||
>
|
||||
<div
|
||||
class="relative flex h-svh max-w-full min-w-full flex-1 flex-col overflow-hidden lg:flex-row lg:gap-2"
|
||||
>
|
||||
<!-- Left Sidebar -->
|
||||
<aside class="desktop-sidebar">
|
||||
<aside
|
||||
class="desktop-sidebar"
|
||||
role="complementary"
|
||||
aria-label="Left navigation panel"
|
||||
>
|
||||
{#if panelsCtx.leftPanel}
|
||||
{@const title =
|
||||
panelsCtx.leftNav[panelsCtx.leftPanel]?.title ||
|
||||
|
|
@ -257,18 +263,23 @@
|
|||
class="bg-surface-50-950 me-2 flex h-full w-full flex-col overflow-y-auto rounded-r-lg"
|
||||
in:fly={{ x: -100, duration: 200 }}
|
||||
out:fly={{ x: -100, duration: 200 }}
|
||||
role="region"
|
||||
aria-labelledby="left-panel-title"
|
||||
>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<span
|
||||
<h2
|
||||
id="left-panel-title"
|
||||
class="text-foreground text-lg font-semibold capitalize"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
class="btn-ghost"
|
||||
onclick={() => closePanel({ panel: "left" })}
|
||||
aria-label="Close {title} panel"
|
||||
type="button"
|
||||
>
|
||||
<Icons.X class="text-foreground h-5 w-5" />
|
||||
<Icons.X class="text-foreground h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
|
|
@ -302,14 +313,22 @@
|
|||
{/if}
|
||||
</aside>
|
||||
<!-- Main Content -->
|
||||
<main class="flex h-full flex-col overflow-hidden">
|
||||
<main
|
||||
class="flex h-full flex-col overflow-hidden"
|
||||
role="main"
|
||||
aria-label="Main content area"
|
||||
>
|
||||
<Header />
|
||||
<div class="flex-1 overflow-auto">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</main>
|
||||
<!-- Right Sidebar -->
|
||||
<aside class="desktop-sidebar pt-1">
|
||||
<aside
|
||||
class="desktop-sidebar pt-1"
|
||||
role="complementary"
|
||||
aria-label="Right navigation panel"
|
||||
>
|
||||
{#if panelsCtx.rightPanel}
|
||||
{@const title =
|
||||
panelsCtx.rightNav[panelsCtx.rightPanel]?.title ||
|
||||
|
|
@ -318,21 +337,26 @@
|
|||
class="bg-surface-50-950 flex h-full w-full flex-col overflow-y-auto rounded-l-lg"
|
||||
in:fly={{ x: 100, duration: 200 }}
|
||||
out:fly={{ x: 100, duration: 200 }}
|
||||
role="region"
|
||||
aria-labelledby="right-panel-title"
|
||||
>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<span
|
||||
<h2
|
||||
id="right-panel-title"
|
||||
class="text-foreground text-lg font-semibold capitalize"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
class="btn-ghost"
|
||||
onclick={() => closePanel({ panel: "right" })}
|
||||
aria-label="Close {title} panel"
|
||||
type="button"
|
||||
>
|
||||
<Icons.X class="text-foreground h-5 w-5" />
|
||||
<Icons.X class="text-foreground h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<nav class="flex-1 overflow-y-auto">
|
||||
{#if panelsCtx.rightPanel === "personas"}
|
||||
<PersonasSidebar
|
||||
bind:onclose={panelsCtx.onRightPanelClose}
|
||||
|
|
@ -354,7 +378,7 @@
|
|||
bind:onclose={panelsCtx.onRightPanelClose}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
</aside>
|
||||
|
|
@ -366,6 +390,9 @@
|
|||
]?.title || panelsCtx.mobilePanel}
|
||||
<div
|
||||
class="bg-surface-100-900 fixed inset-0 z-[51] flex flex-col overflow-y-auto lg:hidden"
|
||||
role="dialog"
|
||||
aria-labelledby="mobile-panel-title"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
class="border-border flex items-center justify-between border-b p-4"
|
||||
|
|
|
|||
|
|
@ -24,12 +24,20 @@
|
|||
|
||||
<div
|
||||
class="card preset-tonal hover:preset-filled-surface-300-700 relative flex w-full gap-2 overflow-hidden rounded-lg py-2 pr-3 pl-2 {classes}"
|
||||
role="listitem"
|
||||
>
|
||||
<div class="relative flex min-w-0 flex-1 gap-2">
|
||||
{#if id !== undefined}
|
||||
<button {onclick} class="flex min-w-0 gap-2" title={contentTitle}>
|
||||
<button
|
||||
{onclick}
|
||||
class="flex min-w-0 gap-2"
|
||||
title={contentTitle}
|
||||
aria-label="Item {id}: {contentTitle}"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="text-muted-foreground my-auto h-fit w-8 flex-shrink-0 text-center text-xs"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{id}
|
||||
</span>
|
||||
|
|
@ -41,6 +49,8 @@
|
|||
{onclick}
|
||||
class="flex min-w-0 flex-1 gap-2"
|
||||
title={contentTitle}
|
||||
aria-label="{contentTitle}"
|
||||
type="button"
|
||||
>
|
||||
{@render content()}
|
||||
</button>
|
||||
|
|
@ -48,5 +58,11 @@
|
|||
{@render extraContent?.()}
|
||||
</div>
|
||||
</div>
|
||||
{@render controls?.()}
|
||||
<div
|
||||
class="controls"
|
||||
role="group"
|
||||
aria-label="Actions for {contentTitle}"
|
||||
>
|
||||
{@render controls?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,18 +66,26 @@
|
|||
value={tabGroup}
|
||||
{classes}
|
||||
onValueChange={(e) => (tabGroup = e.value as "compose" | "preview")}
|
||||
role="region"
|
||||
aria-label="Message composer"
|
||||
>
|
||||
{#snippet list()}
|
||||
<Tabs.Control value="compose" classes="min-h-[2.75em]">
|
||||
<span title="Compose"><Icons.Pen size="0.75em" /></span>
|
||||
<span title="Compose" aria-label="Compose tab">
|
||||
<Icons.Pen size="0.75em" aria-hidden="true" />
|
||||
</span>
|
||||
</Tabs.Control>
|
||||
<Tabs.Control value="preview" classes="min-h-[2.75em]">
|
||||
<span title="Preview"><Icons.Eye size="0.75em" /></span>
|
||||
<span title="Preview" aria-label="Preview tab">
|
||||
<Icons.Eye size="0.75em" aria-hidden="true" />
|
||||
</span>
|
||||
</Tabs.Control>
|
||||
{#if extraTabs}
|
||||
{#each extraTabs as tab}
|
||||
<Tabs.Control value={tab.value} classes="min-h-[2.75em]">
|
||||
<span title={tab.title}>{@render tab.control?.()}</span>
|
||||
<span title={tab.title} aria-label="{tab.title} tab">
|
||||
{@render tab.control?.()}
|
||||
</span>
|
||||
</Tabs.Control>
|
||||
{/each}
|
||||
{/if}
|
||||
|
|
@ -91,6 +99,8 @@
|
|||
title="Token Count"
|
||||
class="text-xs"
|
||||
class:text-error-500={contextExceeded}
|
||||
aria-label="Token count: {compiledPrompt.meta.tokenCounts.total} of {compiledPrompt.meta.tokenCounts.limit}"
|
||||
aria-live="polite"
|
||||
>
|
||||
{compiledPrompt.meta.tokenCounts.total} / {compiledPrompt
|
||||
.meta.tokenCounts.limit}
|
||||
|
|
@ -100,21 +110,40 @@
|
|||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex gap-4">
|
||||
{@render leftControls?.()}
|
||||
<div role="group" aria-label="Message controls">
|
||||
{@render leftControls?.()}
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Tabs.Panel value="compose">
|
||||
<label class="sr-only" for="message-input">
|
||||
Type your message here
|
||||
</label>
|
||||
<textarea
|
||||
id="message-input"
|
||||
class="input field-sizing-content flex-1 rounded-xl lg:min-h-[3.75em]"
|
||||
placeholder="Type a message..."
|
||||
bind:value={markdown}
|
||||
autocomplete="off"
|
||||
spellcheck="true"
|
||||
onkeydown={handleKeyDown}
|
||||
aria-describedby={contextExceeded ? "token-warning" : undefined}
|
||||
aria-invalid={contextExceeded}
|
||||
></textarea>
|
||||
{#if contextExceeded}
|
||||
<div
|
||||
id="token-warning"
|
||||
class="text-error-500 text-xs mt-1"
|
||||
role="alert"
|
||||
>
|
||||
Token limit exceeded. Message may be truncated.
|
||||
</div>
|
||||
{/if}
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="preview">
|
||||
<div
|
||||
class="card bg-surface-100-900 min-h-[4em] w-full rounded-lg p-2"
|
||||
role="region"
|
||||
aria-label="Message preview"
|
||||
>
|
||||
<div class="rendered-chat-message-content">
|
||||
{@html renderMarkdownWithQuotedText(markdown)}
|
||||
|
|
@ -124,12 +153,16 @@
|
|||
{#if extraTabs}
|
||||
{#each extraTabs as tab}
|
||||
<Tabs.Panel value={tab.value}>
|
||||
{@render tab.content?.()}
|
||||
<div role="region" aria-label="{tab.title} content">
|
||||
{@render tab.content?.()}
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{@render rightControls?.()}
|
||||
<div role="group" aria-label="Send controls">
|
||||
{@render rightControls?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -51,15 +51,21 @@
|
|||
imageClasses="object-cover"
|
||||
name={character.nickname || character.name!}
|
||||
>
|
||||
<Icons.User size={36} />
|
||||
<Icons.User size={36} aria-hidden="true" />
|
||||
</Avatar>
|
||||
<div class="relative flex min-w-0 flex-1 gap-2">
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<div class="truncate text-left font-semibold">
|
||||
<div
|
||||
class="truncate text-left font-semibold"
|
||||
id="character-name-{character.id}"
|
||||
>
|
||||
{character.nickname || character.name}
|
||||
</div>
|
||||
{#if character.description}
|
||||
<div class="text-muted-foreground line-clamp-2 text-left text-xs">
|
||||
<div
|
||||
class="text-muted-foreground line-clamp-2 text-left text-xs"
|
||||
id="character-desc-{character.id}"
|
||||
>
|
||||
{character.description}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -68,14 +74,16 @@
|
|||
{/snippet}
|
||||
{#snippet controls()}
|
||||
{#if showControls && (onEdit || onDelete)}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4" role="group" aria-labelledby="character-name-{character.id}">
|
||||
{#if onEdit}
|
||||
<button
|
||||
class="btn btn-sm text-primary-500 p-2"
|
||||
onclick={handleEditClick}
|
||||
title="Edit Character"
|
||||
aria-label="Edit {character.nickname || character.name}"
|
||||
type="button"
|
||||
>
|
||||
<Icons.Edit size={16} />
|
||||
<Icons.Edit size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
|
|
@ -83,8 +91,10 @@
|
|||
class="btn btn-sm text-error-500 p-2"
|
||||
onclick={handleDeleteClick}
|
||||
title="Delete Character"
|
||||
aria-label="Delete {character.nickname || character.name}"
|
||||
type="button"
|
||||
>
|
||||
<Icons.Trash2 size={16} />
|
||||
<Icons.Trash2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -61,45 +61,62 @@
|
|||
{onOpenChange}
|
||||
contentBase="card bg-surface-100-900 p-6 space-y-6 shadow-xl max-w-md"
|
||||
backdropClasses="backdrop-blur-sm"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
{#snippet content()}
|
||||
<header class="flex justify-between">
|
||||
<h2 class="h2">{title ? title : "Create new"}</h2>
|
||||
<h2 id="modal-title" class="h2">{title ? title : "Create new"}</h2>
|
||||
</header>
|
||||
<article class="space-y-4">
|
||||
{#if description}
|
||||
<p class="text-muted-foreground">{description}</p>
|
||||
<p id="modal-description" class="text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
<input
|
||||
bind:this={inputRef}
|
||||
bind:value={name}
|
||||
class="input w-full {validationErrors.name
|
||||
? 'border-red-500'
|
||||
: ''}"
|
||||
type="text"
|
||||
placeholder="Enter a name..."
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" && isValid) {
|
||||
if (validateForm()) {
|
||||
onConfirm(name)
|
||||
<div class="form-field">
|
||||
<label for="name-input" class="sr-only">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name-input"
|
||||
bind:this={inputRef}
|
||||
bind:value={name}
|
||||
class="input w-full {validationErrors.name
|
||||
? 'border-red-500'
|
||||
: ''}"
|
||||
type="text"
|
||||
placeholder="Enter a name..."
|
||||
aria-required="true"
|
||||
aria-invalid={!!validationErrors.name}
|
||||
aria-describedby={validationErrors.name ? "name-error" : undefined}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" && isValid) {
|
||||
if (validateForm()) {
|
||||
onConfirm(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
oninput={() => {
|
||||
if (validationErrors.name) {
|
||||
const { name, ...rest } = validationErrors
|
||||
validationErrors = rest
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if validationErrors.name}
|
||||
<p class="mt-1 text-sm text-red-500" role="alert">
|
||||
{validationErrors.name}
|
||||
</p>
|
||||
{/if}
|
||||
}}
|
||||
oninput={() => {
|
||||
if (validationErrors.name) {
|
||||
const { name, ...rest } = validationErrors
|
||||
validationErrors = rest
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if validationErrors.name}
|
||||
<p id="name-error" class="mt-1 text-sm text-red-500" role="alert">
|
||||
{validationErrors.name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
<footer class="flex justify-end gap-4">
|
||||
<button class="btn preset-filled-surface-500" onclick={onCancel}>
|
||||
<button
|
||||
class="btn preset-filled-surface-500"
|
||||
onclick={onCancel}
|
||||
type="button"
|
||||
aria-label="Cancel and close modal"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -110,6 +127,8 @@
|
|||
}
|
||||
}}
|
||||
disabled={!isValid}
|
||||
type="button"
|
||||
aria-label="Confirm and create new item"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -249,24 +249,28 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="text-foreground h-full p-4">
|
||||
<div class="text-foreground h-full p-4" role="region" aria-label="Characters management">
|
||||
{#if isCreating}
|
||||
<CharacterForm
|
||||
bind:isSafeToClose={characterFormHasChanges}
|
||||
closeForm={closeCharacterForm}
|
||||
bind:onCancel={onEditFormCancel}
|
||||
/>
|
||||
{:else if characterId}
|
||||
{#key characterId}
|
||||
<section aria-label="Create new character">
|
||||
<CharacterForm
|
||||
bind:isSafeToClose={characterFormHasChanges}
|
||||
{characterId}
|
||||
closeForm={closeCharacterForm}
|
||||
bind:onCancel={onEditFormCancel}
|
||||
/>
|
||||
</section>
|
||||
{:else if characterId}
|
||||
{#key characterId}
|
||||
<section aria-label="Edit character">
|
||||
<CharacterForm
|
||||
bind:isSafeToClose={characterFormHasChanges}
|
||||
{characterId}
|
||||
closeForm={closeCharacterForm}
|
||||
bind:onCancel={onEditFormCancel}
|
||||
/>
|
||||
</section>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="mb-2 flex gap-2">
|
||||
<div class="mb-2 flex gap-2" role="toolbar" aria-label="Character actions">
|
||||
<button
|
||||
class="btn btn-sm preset-filled-primary-500 {panelsCtx.digest
|
||||
.tutorial
|
||||
|
|
@ -274,38 +278,51 @@
|
|||
: ''}"
|
||||
onclick={handleCreateClick}
|
||||
title="Create New Character"
|
||||
aria-label="Create new character"
|
||||
type="button"
|
||||
>
|
||||
<Icons.Plus size={16} />
|
||||
<Icons.Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm preset-filled-primary-500"
|
||||
title="Import Character"
|
||||
onclick={handleImportClick}
|
||||
aria-label="Import character from file"
|
||||
type="button"
|
||||
>
|
||||
<Icons.Upload size={16} />
|
||||
<Icons.Upload size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm preset-filled-primary-500"
|
||||
title="Export Character"
|
||||
disabled
|
||||
aria-label="Export character (coming soon)"
|
||||
type="button"
|
||||
>
|
||||
<Icons.Download size={16} />
|
||||
<Icons.Download size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<label for="character-search" class="sr-only">
|
||||
Search characters
|
||||
</label>
|
||||
<input
|
||||
id="character-search"
|
||||
type="text"
|
||||
placeholder="Search characters, descriptions, tags..."
|
||||
class="input"
|
||||
bind:value={search}
|
||||
aria-label="Search characters by name, description, or tags"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2" role="list" aria-label="Characters list">
|
||||
{#if filteredCharacters.length === 0}
|
||||
<div
|
||||
class="text-muted-foreground relative w-100 py-8 text-center"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
No characters found.
|
||||
{search ? `No characters found matching "${search}".` : "No characters found."}
|
||||
</div>
|
||||
{:else}
|
||||
{#each filteredCharacters as c}
|
||||
|
|
@ -328,24 +345,31 @@
|
|||
onOpenChange={(e) => (showDeleteModal = e.open)}
|
||||
contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-dvw-sm border border-surface-300-700"
|
||||
backdropClasses="backdrop-blur-sm"
|
||||
role="alertdialog"
|
||||
aria-labelledby="delete-modal-title"
|
||||
aria-describedby="delete-modal-description"
|
||||
>
|
||||
{#snippet content()}
|
||||
<div class="p-6">
|
||||
<h2 class="mb-2 text-lg font-bold">Delete Character?</h2>
|
||||
<p class="mb-4">
|
||||
<h2 id="delete-modal-title" class="mb-2 text-lg font-bold">Delete Character?</h2>
|
||||
<p id="delete-modal-description" class="mb-4">
|
||||
Are you sure you want to delete this character? This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="flex justify-end gap-2" role="group" aria-label="Delete confirmation actions">
|
||||
<button
|
||||
class="btn preset-filled-surface-500"
|
||||
onclick={cancelDelete}
|
||||
type="button"
|
||||
aria-label="Cancel deletion"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn preset-filled-error-500"
|
||||
onclick={confirmDelete}
|
||||
type="button"
|
||||
aria-label="Confirm deletion"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
import HistoryEntryManager from "../lorebookForms/HistoryEntryManager.svelte"
|
||||
import { toaster } from "$lib/client/utils/toaster"
|
||||
import type { SpecV3 } from "@lenml/char-card-reader"
|
||||
import SidebarListItem from "../SidebarListItem.svelte"
|
||||
import LorebookListItem from "../listItems/LorebookListItem.svelte"
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -1660,21 +1660,20 @@ export async function getChatResponseOrder(
|
|||
// 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(
|
||||
{
|
||||
chatMessages: chat.chatMessages,
|
||||
chatCharacters: chat.chatCharacters
|
||||
.filter((cc) => cc.character !== null && cc.isActive)
|
||||
.sort(
|
||||
(a, b) => (a.position ?? 0) - (b.position ?? 0)
|
||||
) as any,
|
||||
chatPersonas: chat.chatPersonas.filter(
|
||||
(cp) => cp.persona !== null
|
||||
) as any
|
||||
},
|
||||
{ triggered: false }
|
||||
) || (sortedCharacterIds.length > 0 ? sortedCharacterIds[0] : null)
|
||||
const nextCharacterId = getNextCharacterTurn(
|
||||
{
|
||||
chatMessages: chat.chatMessages,
|
||||
chatCharacters: chat.chatCharacters
|
||||
.filter((cc) => cc.character !== null && cc.isActive)
|
||||
.sort(
|
||||
(a, b) => (a.position ?? 0) - (b.position ?? 0)
|
||||
) as any,
|
||||
chatPersonas: chat.chatPersonas.filter(
|
||||
(cp) => cp.persona !== null
|
||||
) as any
|
||||
},
|
||||
{ triggered: false }
|
||||
)
|
||||
|
||||
// console.log('Debug - Next character logic simplified:', { sortedCharacterIds, nextCharacterId })
|
||||
|
||||
|
|
|
|||
|
|
@ -114,11 +114,11 @@ export function getNextCharacterTurn(
|
|||
}
|
||||
}
|
||||
|
||||
// If all active characters have replied, start over with first active character
|
||||
// If all active characters have replied, stop the turn cycle (return null)
|
||||
console.log(
|
||||
"Debug - All characters have replied, cycling back to first"
|
||||
"Debug - All characters have replied, stopping turn cycle"
|
||||
)
|
||||
return activeCharacters[0]?.character.id || null
|
||||
return null
|
||||
} else {
|
||||
// For triggered: check if each character has replied since the last user message
|
||||
for (const cc of activeCharacters) {
|
||||
|
|
@ -134,10 +134,10 @@ export function getNextCharacterTurn(
|
|||
return cc.character.id
|
||||
}
|
||||
}
|
||||
// If all have replied, default to the first active character
|
||||
// If all have replied, stop the turn cycle (return null)
|
||||
console.log(
|
||||
"Debug - Triggered mode - All characters have replied, defaulting to first"
|
||||
"Debug - Triggered mode - All characters have replied, stopping turn cycle"
|
||||
)
|
||||
return activeCharacters[0]?.character.id || null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue