Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
246c3be86b
commit
1b670a80d9
236
deploy_synology.sh
Executable file
236
deploy_synology.sh
Executable file
@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -z "${BASH_VERSION:-}" ]]; then
|
||||||
|
echo "Error: run this script with bash (example: bash deploy_synology.sh)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ManeshTrader quick deploy helper for Synology Docker host.
|
||||||
|
# Defaults are set for the target provided by the user.
|
||||||
|
|
||||||
|
REMOTE_USER="${REMOTE_USER:-mbrucedogs}"
|
||||||
|
REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}"
|
||||||
|
REMOTE_PORT="${REMOTE_PORT:-25}"
|
||||||
|
REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}"
|
||||||
|
SYNO_PASSWORD="${SYNO_PASSWORD:-}"
|
||||||
|
|
||||||
|
MODE="bind" # bind: upload + restart; image: upload + rebuild
|
||||||
|
NO_RESTART="0"
|
||||||
|
DRY_RUN="0"
|
||||||
|
|
||||||
|
DEFAULT_FILES=(
|
||||||
|
"web/src/web_core/ui/help_content.py"
|
||||||
|
"web/src/PRD.md"
|
||||||
|
"web/src/ONBOARDING.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
FILES=("${DEFAULT_FILES[@]}")
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./deploy_synology.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--mode bind|image Deploy mode (default: bind)
|
||||||
|
--files "a b c" Space-separated file list to upload
|
||||||
|
--recent-minutes N Upload files under web/src modified in last N minutes
|
||||||
|
--no-restart Upload only; skip remote docker restart/rebuild
|
||||||
|
--dry-run Print actions but do not execute
|
||||||
|
-h, --help Show help
|
||||||
|
|
||||||
|
Environment overrides:
|
||||||
|
REMOTE_USER, REMOTE_HOST, REMOTE_PORT, REMOTE_BASE, SYNO_PASSWORD, CONTAINER_NAME
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./deploy_synology.sh
|
||||||
|
./deploy_synology.sh --mode image
|
||||||
|
./deploy_synology.sh --recent-minutes 120
|
||||||
|
./deploy_synology.sh --files "web/src/web_core/ui/help_content.py"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
run_cmd() {
|
||||||
|
if [[ "$DRY_RUN" == "1" ]]; then
|
||||||
|
printf '[dry-run] %s\n' "$*"
|
||||||
|
else
|
||||||
|
eval "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
build_ssh_cmd() {
|
||||||
|
if [[ -n "${SYNO_PASSWORD:-}" ]]; then
|
||||||
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
|
echo "Error: SYNO_PASSWORD is set but sshpass is not installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "SSHPASS='%s' sshpass -e ssh -o StrictHostKeyChecking=accept-new -p '%s' '%s@%s'" \
|
||||||
|
"${SYNO_PASSWORD}" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}"
|
||||||
|
else
|
||||||
|
printf "ssh -p '%s' '%s@%s'" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
build_scp_cmd() {
|
||||||
|
if [[ -n "${SYNO_PASSWORD:-}" ]]; then
|
||||||
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
|
echo "Error: SYNO_PASSWORD is set but sshpass is not installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "SSHPASS='%s' sshpass -e scp -O -o StrictHostKeyChecking=accept-new -P '%s'" \
|
||||||
|
"${SYNO_PASSWORD}" "${REMOTE_PORT}"
|
||||||
|
else
|
||||||
|
printf "scp -O -P '%s'" "${REMOTE_PORT}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--mode)
|
||||||
|
MODE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--files)
|
||||||
|
read -r -a FILES <<< "${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--recent-minutes)
|
||||||
|
minutes="${2:-}"
|
||||||
|
if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Error: --recent-minutes requires an integer." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
FILES=()
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
[[ -z "$candidate" ]] && continue
|
||||||
|
FILES+=("$candidate")
|
||||||
|
done <<EOF
|
||||||
|
$(find web/src -type f -mmin "-$minutes" ! -name ".DS_Store" | sort)
|
||||||
|
EOF
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-restart)
|
||||||
|
NO_RESTART="1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN="1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$MODE" != "bind" && "$MODE" != "image" ]]; then
|
||||||
|
echo "Error: --mode must be 'bind' or 'image'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||||
|
echo "Error: No files selected for upload." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
echo "Error: Local file not found: $file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deploy target: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}"
|
||||||
|
echo "Remote base: ${REMOTE_BASE}"
|
||||||
|
echo "Mode: ${MODE}"
|
||||||
|
echo "Files:"
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
echo " - $file"
|
||||||
|
done
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
remote_path="${REMOTE_BASE}/${file}"
|
||||||
|
remote_dir="$(dirname "$remote_path")"
|
||||||
|
SSH_CMD="$(build_ssh_cmd)"
|
||||||
|
SCP_CMD="$(build_scp_cmd)"
|
||||||
|
run_cmd "${SSH_CMD} \"mkdir -p '${remote_dir}'\""
|
||||||
|
run_cmd "${SCP_CMD} '${file}' '${REMOTE_USER}@${REMOTE_HOST}:${remote_path}'"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$NO_RESTART" == "1" ]]; then
|
||||||
|
echo "Upload complete (restart/rebuild skipped)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MODE" == "bind" ]]; then
|
||||||
|
SSH_CMD="$(build_ssh_cmd)"
|
||||||
|
run_cmd "${SSH_CMD} '
|
||||||
|
set -e
|
||||||
|
cd \"${REMOTE_BASE}\"
|
||||||
|
DOCKER_BIN=\"\"
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
DOCKER_BIN=\"\$(command -v docker)\"
|
||||||
|
elif [[ -x /usr/local/bin/docker ]]; then
|
||||||
|
DOCKER_BIN=\"/usr/local/bin/docker\"
|
||||||
|
elif [[ -x /usr/bin/docker ]]; then
|
||||||
|
DOCKER_BIN=\"/usr/bin/docker\"
|
||||||
|
elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then
|
||||||
|
DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n \"\$DOCKER_BIN\" ]] && \"\$DOCKER_BIN\" compose version >/dev/null 2>&1; then
|
||||||
|
\"\$DOCKER_BIN\" compose restart
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose restart
|
||||||
|
elif [[ -n \"\$DOCKER_BIN\" ]]; then
|
||||||
|
if [[ -n \"${CONTAINER_NAME:-}\" ]]; then
|
||||||
|
\"\$DOCKER_BIN\" restart \"${CONTAINER_NAME}\"
|
||||||
|
else
|
||||||
|
ids=\$(\"\$DOCKER_BIN\" ps --filter \"name=maneshtrader\" -q)
|
||||||
|
if [[ -z \"\$ids\" ]]; then
|
||||||
|
echo \"No docker compose command found, and no running containers matched name=maneshtrader.\" >&2
|
||||||
|
echo \"Set CONTAINER_NAME=<your-container-name> and rerun.\" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
\"\$DOCKER_BIN\" restart \$ids
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo \"No docker or docker compose command found on remote host.\" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'"
|
||||||
|
echo "Upload + container restart complete."
|
||||||
|
else
|
||||||
|
SSH_CMD="$(build_ssh_cmd)"
|
||||||
|
run_cmd "${SSH_CMD} '
|
||||||
|
set -e
|
||||||
|
cd \"${REMOTE_BASE}\"
|
||||||
|
DOCKER_BIN=\"\"
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
DOCKER_BIN=\"\$(command -v docker)\"
|
||||||
|
elif [[ -x /usr/local/bin/docker ]]; then
|
||||||
|
DOCKER_BIN=\"/usr/local/bin/docker\"
|
||||||
|
elif [[ -x /usr/bin/docker ]]; then
|
||||||
|
DOCKER_BIN=\"/usr/bin/docker\"
|
||||||
|
elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then
|
||||||
|
DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n \"\$DOCKER_BIN\" ]] && \"\$DOCKER_BIN\" compose version >/dev/null 2>&1; then
|
||||||
|
\"\$DOCKER_BIN\" compose up -d --build
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose up -d --build
|
||||||
|
else
|
||||||
|
echo \"No docker compose command found on remote host.\" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'"
|
||||||
|
echo "Upload + image rebuild complete."
|
||||||
|
fi
|
||||||
@ -12,9 +12,10 @@ Trend logic:
|
|||||||
- Reversal requires 2 consecutive opposite real bars
|
- Reversal requires 2 consecutive opposite real bars
|
||||||
- Fake bars do not reverse trend
|
- Fake bars do not reverse trend
|
||||||
|
|
||||||
In-app help popup source:
|
In-app help popup:
|
||||||
- Primary: `web/src/web_core/help.html`
|
- Open from sidebar `Help / Quick Start`
|
||||||
- Fallback: this `web/src/ONBOARDING.md`
|
- Uses a multi-screen, child-friendly guide implemented in `web/src/web_core/ui/help_content.py`
|
||||||
|
- Includes dedicated screens for core setup, filters, training mode, backtest controls, and advanced panels
|
||||||
|
|
||||||
## 2) Quick Start (Recommended)
|
## 2) Quick Start (Recommended)
|
||||||
From project root:
|
From project root:
|
||||||
|
|||||||
@ -164,10 +164,11 @@ Gap handling (`hide_market_closed_gaps`):
|
|||||||
- For daily interval, weekend break removal is applied.
|
- For daily interval, weekend break removal is applied.
|
||||||
|
|
||||||
## 8. Help and Onboarding Behavior
|
## 8. Help and Onboarding Behavior
|
||||||
- Web-only fallback help entry exists in sidebar:
|
- Web-only help entry exists in sidebar:
|
||||||
- `Help / Quick Start`
|
- `Help / Quick Start`
|
||||||
- Content source: `web/src/web_core/help.html` (primary), `web/src/ONBOARDING.md` (fallback)
|
- Help appears in a dialog with multiple navigable screens (screen picker + previous/next).
|
||||||
- Help appears in a dialog.
|
- Help copy is intentionally beginner-friendly and explains each major sidebar control group, including detailed backtest controls and why each setting matters.
|
||||||
|
- The onboarding markdown remains project documentation; in-app help content is rendered from `web/src/web_core/ui/help_content.py`.
|
||||||
|
|
||||||
## 9. Outputs
|
## 9. Outputs
|
||||||
- Metrics:
|
- Metrics:
|
||||||
|
|||||||
@ -1,28 +1,237 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
|
_HELP_SCREENS: list[tuple[str, str]] = [
|
||||||
|
(
|
||||||
|
"Start Here",
|
||||||
|
"""
|
||||||
|
### What this app is for
|
||||||
|
Think of ManeshTrader like a **weather app for price candles**.
|
||||||
|
It does not place trades. It helps you answer:
|
||||||
|
- Is the market acting strong up, strong down, or messy?
|
||||||
|
- Did we get a real signal or just noise?
|
||||||
|
|
||||||
@st.cache_data(show_spinner=False)
|
### The 10-second idea
|
||||||
def load_help_content() -> tuple[str, bool]:
|
Each candle gets a label:
|
||||||
app_path = Path(__file__).resolve().parents[1] / "app.py"
|
- `real_bull`: a strong up break
|
||||||
help_html_paths = [
|
- `real_bear`: a strong down break
|
||||||
app_path.with_name("web_core").joinpath("help.html"),
|
- `fake`: noise inside the old range
|
||||||
app_path.with_name("help.html"),
|
|
||||||
|
Trend rule:
|
||||||
|
- 2 real up candles in a row => bullish trend
|
||||||
|
- 2 real down candles in a row => bearish trend
|
||||||
|
- Fake candles alone do not flip trend
|
||||||
|
|
||||||
|
### Why you should care
|
||||||
|
This turns a messy chart into simple states:
|
||||||
|
- **Go with up trend**
|
||||||
|
- **Go with down trend**
|
||||||
|
- **Wait when unclear**
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"How To Use",
|
||||||
|
"""
|
||||||
|
### A simple 5-step routine
|
||||||
|
1. Pick one symbol.
|
||||||
|
2. Use `1d` timeframe and `6mo` period.
|
||||||
|
3. Turn on `Show live decision guide` and `Show past behavior examples`.
|
||||||
|
4. Read trend + check latest examples.
|
||||||
|
5. Decide: follow trend, or stand aside if choppy.
|
||||||
|
|
||||||
|
### Important safety notes
|
||||||
|
- This app is **analysis-only**.
|
||||||
|
- It does **not** execute orders.
|
||||||
|
- Backtests and examples are educational.
|
||||||
|
- Never risk money you cannot afford to lose.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Main Setup",
|
||||||
|
"""
|
||||||
|
### Core controls (left sidebar)
|
||||||
|
These are your main knobs.
|
||||||
|
|
||||||
|
- `Symbol`: what you are looking at (`AAPL`, `BTC-USD`).
|
||||||
|
Why care: wrong symbol = wrong answer.
|
||||||
|
- `Timeframe`: candle size (`1d`, `4h`, `1h`, etc.).
|
||||||
|
Why care: smaller = noisier, bigger = smoother.
|
||||||
|
- `Period`: how much history to load.
|
||||||
|
Why care: too short can miss context, too long can feel slow.
|
||||||
|
- `Max bars`: cap on candles loaded.
|
||||||
|
Why care: helps performance and keeps chart readable.
|
||||||
|
- `Ignore potentially live last bar`: ignores the newest unfinished candle.
|
||||||
|
Why care: unfinished candles can lie before they close.
|
||||||
|
|
||||||
|
### Child-friendly default
|
||||||
|
Start with:
|
||||||
|
- `Timeframe = 1d`
|
||||||
|
- `Period = 6mo`
|
||||||
|
- `Ignore potentially live last bar = ON`
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Filters",
|
||||||
|
"""
|
||||||
|
### Classification Filters
|
||||||
|
These help you reduce fake signals.
|
||||||
|
|
||||||
|
- `Use previous body range (ignore wicks)`
|
||||||
|
Means: compare against candle body, not long spikes.
|
||||||
|
Why care: reduces wick tricks.
|
||||||
|
|
||||||
|
- `Enable volume filter`
|
||||||
|
Means: low-volume moves are treated as fake.
|
||||||
|
Why care: weak moves often fail.
|
||||||
|
|
||||||
|
- `Volume SMA window`
|
||||||
|
Means: how many candles used for average volume.
|
||||||
|
Why care: bigger window = steadier average.
|
||||||
|
|
||||||
|
- `Min volume / SMA multiplier`
|
||||||
|
Means: how much volume is required vs average.
|
||||||
|
Why care: higher value = stricter filter.
|
||||||
|
|
||||||
|
- `Gray out fake bars`
|
||||||
|
Means: noise bars fade visually.
|
||||||
|
Why care: easier to focus on important candles.
|
||||||
|
|
||||||
|
- `Hide market-closed gaps (stocks)`
|
||||||
|
Means: removes weekend/closed-market blank spaces.
|
||||||
|
Why care: cleaner chart shape.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Training Mode",
|
||||||
|
"""
|
||||||
|
### Training & Guidance controls
|
||||||
|
Use this section to learn the model.
|
||||||
|
|
||||||
|
- `Show live decision guide`
|
||||||
|
Why care: tells bias (long/short/neutral) in plain English.
|
||||||
|
|
||||||
|
- `Show past behavior examples`
|
||||||
|
Why care: shows historical examples of how signals played out.
|
||||||
|
|
||||||
|
- `Overlay example entries/exits on chart`
|
||||||
|
Why care: lets you see examples directly on candles.
|
||||||
|
|
||||||
|
- `Focus chart on selected example`
|
||||||
|
Why care: zooms to one example so you can study it.
|
||||||
|
|
||||||
|
- `Max training examples`
|
||||||
|
Why care: more examples = more history, but busier table.
|
||||||
|
|
||||||
|
- `Replay mode (hide future bars)` + `Replay bars shown`
|
||||||
|
Why care: practice without seeing the future first.
|
||||||
|
|
||||||
|
### Kid version
|
||||||
|
Replay mode is like covering answers on a worksheet,
|
||||||
|
then checking if your guess was right.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Monitoring + Advanced",
|
||||||
|
"""
|
||||||
|
### Monitoring
|
||||||
|
- `Auto-refresh`
|
||||||
|
Why care: keeps data fresh while you watch.
|
||||||
|
- `Refresh interval (seconds)`
|
||||||
|
Why care: lower is faster updates, but more load.
|
||||||
|
|
||||||
|
### Advanced Signals
|
||||||
|
- `Auto-run advanced panels (slower)`
|
||||||
|
Why care: always-updated extras, but heavier.
|
||||||
|
- `Run advanced panels now`
|
||||||
|
Why care: one-time run when you want speed normally.
|
||||||
|
- `Show multi-timeframe confirmation`
|
||||||
|
Why care: checks if 1h/4h/1d agree.
|
||||||
|
- `Regime filter`
|
||||||
|
Why care: warns when market is choppy (messy).
|
||||||
|
|
||||||
|
### Compare Symbols + Alerts
|
||||||
|
- `Enable compare symbols panel`
|
||||||
|
- `Compare symbols (comma separated)`
|
||||||
|
Why care: quick side-by-side trend check.
|
||||||
|
|
||||||
|
- `Enable alert rules`
|
||||||
|
- `Alert on bullish confirmations`
|
||||||
|
- `Alert on bearish confirmations`
|
||||||
|
- `Webhook URL (optional)`
|
||||||
|
Why care: get notified instead of watching charts all day.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Backtest Controls",
|
||||||
|
"""
|
||||||
|
### What a backtest is
|
||||||
|
A backtest asks: "If we used these rules in the past, what happened?"
|
||||||
|
It is practice data, not a promise.
|
||||||
|
|
||||||
|
### Every backtest control explained
|
||||||
|
- `Slippage (bps per side)`
|
||||||
|
Meaning: extra price loss when entering/exiting.
|
||||||
|
Why care: real fills are rarely perfect.
|
||||||
|
|
||||||
|
- `Fee (bps per side)`
|
||||||
|
Meaning: trading cost paid on entry and exit.
|
||||||
|
Why care: costs can erase small wins.
|
||||||
|
|
||||||
|
- `Stop loss (%)`
|
||||||
|
Meaning: emergency exit if price moves against you by this percent.
|
||||||
|
Why care: limits damage.
|
||||||
|
Note: `0` means OFF.
|
||||||
|
|
||||||
|
- `Take profit (%)`
|
||||||
|
Meaning: lock gains once profit reaches this percent.
|
||||||
|
Why care: prevents giving back wins.
|
||||||
|
Note: `0` means OFF.
|
||||||
|
|
||||||
|
- `Min hold bars`
|
||||||
|
Meaning: shortest time a trade must stay open.
|
||||||
|
Why care: avoids instant in-and-out noise trades.
|
||||||
|
|
||||||
|
- `Max hold bars`
|
||||||
|
Meaning: longest time a trade can stay open.
|
||||||
|
Why care: forces an exit if nothing happens.
|
||||||
|
|
||||||
|
### Simple example
|
||||||
|
If `Stop loss = 2%`, you are saying:
|
||||||
|
"If I am down 2%, I am out."
|
||||||
|
If `Take profit = 4%`, you are saying:
|
||||||
|
"If I am up 4%, I will lock it."
|
||||||
|
""",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
for help_html_path in help_html_paths:
|
|
||||||
if help_html_path.exists():
|
|
||||||
return help_html_path.read_text(encoding="utf-8"), True
|
|
||||||
|
|
||||||
onboarding_path = app_path.with_name("ONBOARDING.md")
|
|
||||||
if onboarding_path.exists():
|
|
||||||
return onboarding_path.read_text(encoding="utf-8"), False
|
|
||||||
return "Help content not found.", False
|
|
||||||
|
|
||||||
|
|
||||||
@st.dialog("Help & Quick Start", width="large")
|
@st.dialog("Help & Quick Start", width="large")
|
||||||
def help_dialog() -> None:
|
def help_dialog() -> None:
|
||||||
content, is_html = load_help_content()
|
total = len(_HELP_SCREENS)
|
||||||
st.markdown(content, unsafe_allow_html=is_html)
|
if "help_screen_index" not in st.session_state:
|
||||||
|
st.session_state["help_screen_index"] = 0
|
||||||
|
|
||||||
|
options = [title for title, _ in _HELP_SCREENS]
|
||||||
|
safe_index = min(max(int(st.session_state["help_screen_index"]), 0), total - 1)
|
||||||
|
st.session_state["help_screen_index"] = safe_index
|
||||||
|
|
||||||
|
picker_col, _ = st.columns([3, 2])
|
||||||
|
with picker_col:
|
||||||
|
selected_title = st.selectbox("Help Screen", options, index=safe_index)
|
||||||
|
selected_index = options.index(selected_title)
|
||||||
|
if selected_index != st.session_state["help_screen_index"]:
|
||||||
|
st.session_state["help_screen_index"] = selected_index
|
||||||
|
safe_index = selected_index
|
||||||
|
|
||||||
|
st.caption(f"Screen {safe_index + 1} of {total}")
|
||||||
|
st.markdown(_HELP_SCREENS[safe_index][1])
|
||||||
|
|
||||||
|
nav_col, _ = st.columns([2, 5])
|
||||||
|
prev_col, next_col = nav_col.columns(2)
|
||||||
|
with prev_col:
|
||||||
|
if st.button("Previous", disabled=safe_index == 0):
|
||||||
|
st.session_state["help_screen_index"] = max(safe_index - 1, 0)
|
||||||
|
with next_col:
|
||||||
|
if st.button("Next", disabled=safe_index >= total - 1):
|
||||||
|
st.session_state["help_screen_index"] = min(safe_index + 1, total - 1)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user