From 3bd714960f24e7409720f3c095f487c8d4f878a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 19:39:39 +0000 Subject: [PATCH] Convert all setup prompts to whiptail, fix VRAM estimates, add model expectations Setup script changes: - All prompts now use whiptail dialogs with text fallback - Q1b (SSH), Q2 (storage), Q3 (Kiwix), Q4b (firewall), Q5 (models), Q6 (download), final confirm all converted - Model tier selection uses radiolist with recommended tier pre-selected - Custom model entry uses inputbox with current defaults pre-filled - Fix speed_label: now shows actual VRAM needed (file size + 2GB overhead) instead of misleading "fully in VRAM" for models that don't fit - qwen3.5-35b-a3b MoE already in tier list (was there, now with accurate VRAM estimate shown) README changes: - Add "Realistic expectations by model size" table - 35B MoE highlighted as sweet spot for small GPUs https://claude.ai/code/session_01PtYTPherSJaxDEVPgF6Nxu --- README.md | 23 +++ laptop_full_setup.sh | 326 +++++++++++++++++++++++++++++-------------- 2 files changed, 246 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 1723882..873ed4f 100644 --- a/README.md +++ b/README.md @@ -575,6 +575,29 @@ Local AI requires more manual workflow management but has real code awareness vi Channels are persistent chat rooms (like Slack/Discord channels) with multi-model support. They do NOT scope memories differently — memories are still global per user. Channels are useful for team collaboration, not memory isolation. +### Realistic expectations by model size + +Not all models can do all tasks. Here's what to actually expect: + +| Task | 4B (qwen3.5:4b) | 9B (qwen3.5:9b) | 35B MoE (qwen3.5-35b-a3b) | 14B+ dense | +|------|:---:|:---:|:---:|:---:| +| Answer simple questions | OK | Good | Good | Good | +| Explain existing code (with RAG) | OK | Good | Good | Good | +| Fix a simple bug (typo, off-by-one) | Maybe | Usually | Usually | Yes | +| Write a small utility function | Shaky | OK | Good | Good | +| Fix logic error across 2-3 functions | No | Maybe | Usually | Usually | +| Write a new feature (multiple files) | No | Shaky | Maybe | Maybe | +| Refactor with style consistency | No | No | Sometimes | Sometimes | +| Summarize a conversation for handoff | OK | Good | Good | Good | + +**The 35B MoE model (`qwen3.5-35b-a3b`) is the sweet spot for small GPUs.** It was trained as a 35B model but only activates 3B parameters per token. This means it has the *knowledge* of a 35B model with the VRAM footprint closer to a 4B. On a 6GB card it may fit (VRAM usage varies with context length and KV cache settings). + +**Bottom line for a 6GB GPU:** +- Use `qwen3.5:4b` for quick chat, explanations, and summarization +- Try `qwen3.5-35b-a3b` for code tasks — if it fits, it will be significantly better than 4B +- Use Claude Code for anything that requires reading/writing multiple files or complex reasoning +- The RAG server helps a lot — even a 4B model gives useful answers when it has the right code chunks in context + ### Fixing models that output code instead of natural language If your model (especially smaller ones like Qwen 3.5) responds with Python code blocks instead of plain English answers (as shown in the screenshot), this is a common behavior with code-optimized models. diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index eecccb0..009f767 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -217,15 +217,21 @@ fi SSH_IMPORT_IDS=() # list of "gh:username" or "lp:username" entries if ! $IS_UPDATE || [[ ! -f "$HOME/.ssh/authorized_keys" ]]; then - echo "" - echo -e " ${BOLD}[SSH] Import SSH public keys? (for passwordless SSH into this machine)${NC}" - echo " Pulls your public keys from GitHub or Launchpad and adds them to" - echo " ~/.ssh/authorized_keys using ssh-import-id." - echo "" - echo " Examples: gh:yourusername lp:yourlaunchpadid" - echo " Multiple: gh:alice lp:alice" - echo "" - read -rp " Usernames (or Enter to skip): " SSH_INPUT + if command -v whiptail &>/dev/null; then + SSH_INPUT=$(whiptail --title "SSH Key Import" \ + --inputbox "Import SSH public keys for passwordless SSH into this machine.\n\nExamples: gh:yourusername lp:yourlaunchpadid\nMultiple: gh:alice lp:alice\n\nLeave blank to skip." \ + 14 68 "" 3>&1 1>&2 2>&3) || SSH_INPUT="" + else + echo "" + echo -e " ${BOLD}[SSH] Import SSH public keys? (for passwordless SSH into this machine)${NC}" + echo " Pulls your public keys from GitHub or Launchpad and adds them to" + echo " ~/.ssh/authorized_keys using ssh-import-id." + echo "" + echo " Examples: gh:yourusername lp:yourlaunchpadid" + echo " Multiple: gh:alice lp:alice" + echo "" + read -rp " Usernames (or Enter to skip): " SSH_INPUT + fi if [[ -n "$SSH_INPUT" ]]; then read -ra SSH_IMPORT_IDS <<< "$SSH_INPUT" fi @@ -236,10 +242,6 @@ OLLAMA_STORAGE="volume" # "volume" = Docker named volume, else a host path OLLAMA_HOST_PATH="" if $INSTALL_AI; then - echo "" - echo -e " ${BOLD}[2/6] Where should Ollama models be stored?${NC}" - echo " (Models are large — 5-50GB each. A fast SSD or large HDD is ideal.)" - echo "" mapfile -t _MPTS < <( df -h --output=target,avail,fstype 2>/dev/null \ | awk 'NR>1 && $2~/[0-9]/ { @@ -248,37 +250,58 @@ if $INSTALL_AI; then if ((unit=="G" && num>=20) || unit=="T") print $0 }' | head -10 ) - echo " 0) Docker volume (default — /var/lib/docker/volumes/)" - for _i in "${!_MPTS[@]}"; do - printf " %d) %s\n" "$((_i+1))" "${_MPTS[$_i]}" - done - echo "" - read -rp " Choice [0]: " STORAGE_CHOICE - STORAGE_CHOICE="${STORAGE_CHOICE:-0}" - if [[ "$STORAGE_CHOICE" == "0" ]]; then - OLLAMA_STORAGE="volume" - elif [[ "$STORAGE_CHOICE" =~ ^[0-9]+$ ]] && (( STORAGE_CHOICE >= 1 && STORAGE_CHOICE <= ${#_MPTS[@]} )); then - _MP=$(awk '{print $1}' <<< "${_MPTS[$(( STORAGE_CHOICE-1 ))]}") - OLLAMA_HOST_PATH="$_MP/ollama-models" - OLLAMA_STORAGE="bind" - ok "Ollama models → $OLLAMA_HOST_PATH" + + if command -v whiptail &>/dev/null; then + WHIP_STORAGE=("volume" "Docker volume (default — /var/lib/docker/volumes/)" ON) + for _i in "${!_MPTS[@]}"; do + _mp_path=$(awk '{print $1}' <<< "${_MPTS[$_i]}") + _mp_avail=$(awk '{print $2}' <<< "${_MPTS[$_i]}") + WHIP_STORAGE+=("$_mp_path" "${_mp_path} (${_mp_avail} free)" OFF) + done + STORAGE_CHOICE=$(whiptail --title "Ollama Model Storage" \ + --radiolist "Models are large (5-50GB each). Choose a location.\nSPACE = select ENTER = confirm" \ + $((10 + ${#_MPTS[@]})) 72 $((1 + ${#_MPTS[@]})) \ + "${WHIP_STORAGE[@]}" 3>&1 1>&2 2>&3) || STORAGE_CHOICE="volume" + STORAGE_CHOICE="${STORAGE_CHOICE//\"/}" + if [[ "$STORAGE_CHOICE" == "volume" ]]; then + OLLAMA_STORAGE="volume" + else + OLLAMA_HOST_PATH="${STORAGE_CHOICE}/ollama-models" + OLLAMA_STORAGE="bind" + ok "Ollama models → $OLLAMA_HOST_PATH" + fi else - # Fallback: treat input as a literal path - OLLAMA_HOST_PATH="${STORAGE_CHOICE%/}" - [[ -z "$OLLAMA_HOST_PATH" ]] && die "No path entered." - OLLAMA_STORAGE="bind" + echo "" + echo -e " ${BOLD}[2/6] Where should Ollama models be stored?${NC}" + echo " (Models are large — 5-50GB each. A fast SSD or large HDD is ideal.)" + echo "" + echo " 0) Docker volume (default — /var/lib/docker/volumes/)" + for _i in "${!_MPTS[@]}"; do + printf " %d) %s\n" "$((_i+1))" "${_MPTS[$_i]}" + done + echo "" + read -rp " Choice [0]: " STORAGE_CHOICE + STORAGE_CHOICE="${STORAGE_CHOICE:-0}" + if [[ "$STORAGE_CHOICE" == "0" ]]; then + OLLAMA_STORAGE="volume" + elif [[ "$STORAGE_CHOICE" =~ ^[0-9]+$ ]] && (( STORAGE_CHOICE >= 1 && STORAGE_CHOICE <= ${#_MPTS[@]} )); then + _MP=$(awk '{print $1}' <<< "${_MPTS[$(( STORAGE_CHOICE-1 ))]}") + OLLAMA_HOST_PATH="$_MP/ollama-models" + OLLAMA_STORAGE="bind" + ok "Ollama models → $OLLAMA_HOST_PATH" + else + OLLAMA_HOST_PATH="${STORAGE_CHOICE%/}" + [[ -z "$OLLAMA_HOST_PATH" ]] && die "No path entered." + OLLAMA_STORAGE="bind" + fi fi - unset _MPTS _MP _i + unset _MPTS _MP _i WHIP_STORAGE fi # ── Q3: Storage for Kiwix ZIMs ──────────────────────────────────────────────── KIWIX_DIR="$BASE/kiwix" # default if $SVC_KIWIX; then - echo "" - echo -e " ${BOLD}[3/6] Where should Kiwix ZIM files be stored?${NC}" - echo " (ZIMs are large — Wikipedia alone is ~46GB. Total collection ~130GB.)" - echo "" mapfile -t _MPTS < <( df -h --output=target,avail,fstype 2>/dev/null \ | awk 'NR>1 && $2~/[0-9]/ { @@ -287,22 +310,44 @@ if $SVC_KIWIX; then if ((unit=="G" && num>=50) || unit=="T") print $0 }' | head -10 ) - echo " 0) Default: $KIWIX_DIR" - for _i in "${!_MPTS[@]}"; do - printf " %d) %s\n" "$((_i+1))" "${_MPTS[$_i]}" - done - echo "" - read -rp " Choice [0]: " KIWIX_CHOICE - KIWIX_CHOICE="${KIWIX_CHOICE:-0}" - if [[ "$KIWIX_CHOICE" != "0" ]] && [[ "$KIWIX_CHOICE" =~ ^[0-9]+$ ]] && (( KIWIX_CHOICE >= 1 && KIWIX_CHOICE <= ${#_MPTS[@]} )); then - _MP=$(awk '{print $1}' <<< "${_MPTS[$(( KIWIX_CHOICE-1 ))]}") - KIWIX_DIR="$_MP/kiwix" - ok "Kiwix ZIMs → $KIWIX_DIR" - elif [[ "$KIWIX_CHOICE" != "0" ]] && [[ -n "$KIWIX_CHOICE" ]]; then - # Fallback: treat as a literal path - KIWIX_DIR="${KIWIX_CHOICE%/}" + + if command -v whiptail &>/dev/null; then + WHIP_KIWIX=("default" "Default: $KIWIX_DIR" ON) + for _i in "${!_MPTS[@]}"; do + _mp_path=$(awk '{print $1}' <<< "${_MPTS[$_i]}") + _mp_avail=$(awk '{print $2}' <<< "${_MPTS[$_i]}") + WHIP_KIWIX+=("$_mp_path" "${_mp_path} (${_mp_avail} free)" OFF) + done + _KIWIX_SEL=$(whiptail --title "Kiwix ZIM Storage" \ + --radiolist "ZIMs are large — Wikipedia alone ~46GB, total ~130GB.\nSPACE = select ENTER = confirm" \ + $((10 + ${#_MPTS[@]})) 72 $((1 + ${#_MPTS[@]})) \ + "${WHIP_KIWIX[@]}" 3>&1 1>&2 2>&3) || _KIWIX_SEL="default" + _KIWIX_SEL="${_KIWIX_SEL//\"/}" + if [[ "$_KIWIX_SEL" != "default" ]]; then + KIWIX_DIR="${_KIWIX_SEL}/kiwix" + ok "Kiwix ZIMs → $KIWIX_DIR" + fi + else + echo "" + echo -e " ${BOLD}[3/6] Where should Kiwix ZIM files be stored?${NC}" + echo " (ZIMs are large — Wikipedia alone is ~46GB. Total collection ~130GB.)" + echo "" + echo " 0) Default: $KIWIX_DIR" + for _i in "${!_MPTS[@]}"; do + printf " %d) %s\n" "$((_i+1))" "${_MPTS[$_i]}" + done + echo "" + read -rp " Choice [0]: " KIWIX_CHOICE + KIWIX_CHOICE="${KIWIX_CHOICE:-0}" + if [[ "$KIWIX_CHOICE" != "0" ]] && [[ "$KIWIX_CHOICE" =~ ^[0-9]+$ ]] && (( KIWIX_CHOICE >= 1 && KIWIX_CHOICE <= ${#_MPTS[@]} )); then + _MP=$(awk '{print $1}' <<< "${_MPTS[$(( KIWIX_CHOICE-1 ))]}") + KIWIX_DIR="$_MP/kiwix" + ok "Kiwix ZIMs → $KIWIX_DIR" + elif [[ "$KIWIX_CHOICE" != "0" ]] && [[ -n "$KIWIX_CHOICE" ]]; then + KIWIX_DIR="${KIWIX_CHOICE%/}" + fi fi - unset _MPTS _MP _i _KIWIX_CHOICE + unset _MPTS _MP _i _KIWIX_CHOICE WHIP_KIWIX # ── Q4: Download ZIMs now? ───────────────────────────────────────────────── # ON if any matching ZIM file already exists in KIWIX_DIR @@ -447,11 +492,16 @@ fi # ── Q4b: Firewall (LAN subnet) ──────────────────────────────────────────────── LAN_SUBNET="192.168.1.0/24" if command -v ufw &>/dev/null && { [[ ! -f "$BASE/.ufw-done" ]] || $FORCE; }; then - echo "" - # Auto-detect likely subnet from current IP AUTO_SUBNET=$(echo "$LOCAL_IP" | awk -F. '{print $1"."$2"."$3".0/24"}') - echo -e " ${BOLD}[4b/6] Firewall — allow LAN access to services${NC}" - read -rp " LAN subnet [${AUTO_SUBNET}]: " LAN_INPUT + if command -v whiptail &>/dev/null; then + LAN_INPUT=$(whiptail --title "Firewall — LAN Access" \ + --inputbox "Allow LAN access to all services.\n\nYour detected subnet:" \ + 10 60 "$AUTO_SUBNET" 3>&1 1>&2 2>&3) || LAN_INPUT="" + else + echo "" + echo -e " ${BOLD}[4b/6] Firewall — allow LAN access to services${NC}" + read -rp " LAN subnet [${AUTO_SUBNET}]: " LAN_INPUT + fi LAN_SUBNET="${LAN_INPUT:-$AUTO_SUBNET}" [[ "$LAN_SUBNET" =~ /[0-9]+$ ]] || LAN_SUBNET="${LAN_SUBNET}/24" fi @@ -462,35 +512,48 @@ REASON_MODEL="" if $INSTALL_AI; then - # speed estimate based on Q4 model size vs available VRAM - # no hard limits — just honest labels so the user can choose + # speed estimate: model_file_size + ~1.5GB overhead for KV cache/CUDA vs VRAM + # Ollama has no hard limits — silently offloads to CPU when over VRAM speed_label() { - local mgb="$1" # approximate Q4 size in GB + local mgb="$1" # approximate Q4 file size in GB + local needed=$(( mgb + 2 )) # +2GB for KV cache, attention, CUDA overhead if [[ "$VRAM_GB" -eq 0 ]]; then printf "CPU only — very slow" - elif (( mgb <= VRAM_GB )); then - printf "✓ fast — fully in VRAM" - elif (( mgb <= VRAM_GB + 2 )); then - printf "~ good — fits with small overhang (~reading speed)" - elif (( mgb <= VRAM_GB + 8 )); then - printf "✗ slow — partial CPU offload" + elif (( needed <= VRAM_GB )); then + printf "✓ fast — fits in VRAM (~%dGB needed)" "$needed" + elif (( needed <= VRAM_GB + 2 )); then + printf "~ tight — ~%dGB needed, may spill to CPU" "$needed" + elif (( needed <= VRAM_GB + 8 )); then + printf "✗ slow — ~%dGB needed, partial CPU offload" "$needed" else - printf "✗ very slow — heavy CPU offload" + printf "✗ very slow — ~%dGB needed, heavy CPU offload" "$needed" fi } - echo "" - echo -e " ${BOLD}[5/6] Model selection${NC}" - echo " GPU: ${GPU_NAME:-None} (${VRAM_GB}GB VRAM)" - echo "" - echo " Origin preference:" - echo " 1) Western-only — Codestral (Mistral 🇫🇷) · Phi4 (Microsoft 🇺🇸) · Mistral 7B" - echo " 2) Performance-first — Qwen 3.5 (Feb 2026, top benchmarks, vision+code)" - echo " 3) Mixed — Western for chat, Qwen 3.5 for coding" - echo " 4) Custom — enter model names manually" - echo "" - read -rp " Choice [2]: " MODEL_PREF - MODEL_PREF="${MODEL_PREF:-2}" + if command -v whiptail &>/dev/null; then + MODEL_PREF=$(whiptail --title "Model Selection — ${GPU_NAME:-No GPU} (${VRAM_GB}GB VRAM)" \ + --radiolist "Choose model origin preference.\nSPACE = select ENTER = confirm" \ + 14 78 4 \ + "2" "Performance-first — Qwen 3.5 (top benchmarks, vision+code)" ON \ + "1" "Western-only — Codestral · Phi4 · Mistral 7B" OFF \ + "3" "Mixed — Western chat + Qwen 3.5 coding" OFF \ + "4" "Custom — enter model names manually" OFF \ + 3>&1 1>&2 2>&3) || MODEL_PREF="2" + MODEL_PREF="${MODEL_PREF//\"/}" + else + echo "" + echo -e " ${BOLD}[5/6] Model selection${NC}" + echo " GPU: ${GPU_NAME:-None} (${VRAM_GB}GB VRAM)" + echo "" + echo " Origin preference:" + echo " 1) Western-only — Codestral (Mistral) · Phi4 (Microsoft) · Mistral 7B" + echo " 2) Performance-first — Qwen 3.5 (Feb 2026, top benchmarks, vision+code)" + echo " 3) Mixed — Western for chat, Qwen 3.5 for coding" + echo " 4) Custom — enter model names manually" + echo "" + read -rp " Choice [2]: " MODEL_PREF + MODEL_PREF="${MODEL_PREF:-2}" + fi # Q4_K_M approximate weights in GB: 4B=2.5, 7B=4, 9B=5.5, 14B=9, 22B=13, 27B=17, 35B-MoE=12, 70B=41 @@ -554,21 +617,59 @@ if $INSTALL_AI; then if [[ "$MODEL_PREF" == "4" ]]; then # Custom — free-form entry - echo " Current defaults: fast=$FAST_MODEL chat=$CHAT_MODEL code=$CODE_MODEL" - echo " Press Enter on any line to keep the default shown." - echo "" - read -rp " Fast/chat model [$FAST_MODEL]: " _in; FAST_MODEL="${_in:-$FAST_MODEL}" - read -rp " Smart chat model [$CHAT_MODEL]: " _in; CHAT_MODEL="${_in:-$CHAT_MODEL}" - read -rp " Code model [$CODE_MODEL]: " _in; CODE_MODEL="${_in:-$CODE_MODEL}" - read -rp " Reasoning model (Enter to skip): " REASON_MODEL - else - read -rp " Choose tier [$REC_NUM]: " _TIER_INPUT - _TIER_INPUT="${_TIER_INPUT:-$REC_NUM}" - # Accept either a number (1-4) or the tier name directly - if [[ "$_TIER_INPUT" =~ ^[1-4]$ ]]; then - TIER_PICK="${_TIER_NAMES[$((_TIER_INPUT-1))]}" + if command -v whiptail &>/dev/null; then + _in=$(whiptail --title "Custom Models — Fast/Chat" \ + --inputbox "Fast/chat model (small, quick responses):" \ + 9 60 "$FAST_MODEL" 3>&1 1>&2 2>&3) && FAST_MODEL="${_in:-$FAST_MODEL}" + _in=$(whiptail --title "Custom Models — Smart Chat" \ + --inputbox "Smart chat model (main model for complex tasks):" \ + 9 60 "$CHAT_MODEL" 3>&1 1>&2 2>&3) && CHAT_MODEL="${_in:-$CHAT_MODEL}" + _in=$(whiptail --title "Custom Models — Code" \ + --inputbox "Code model:" \ + 9 60 "$CODE_MODEL" 3>&1 1>&2 2>&3) && CODE_MODEL="${_in:-$CODE_MODEL}" + REASON_MODEL=$(whiptail --title "Custom Models — Reasoning" \ + --inputbox "Reasoning model (leave blank to skip):" \ + 9 60 "" 3>&1 1>&2 2>&3) || REASON_MODEL="" else - TIER_PICK="$_TIER_INPUT" + echo " Current defaults: fast=$FAST_MODEL chat=$CHAT_MODEL code=$CODE_MODEL" + echo " Press Enter on any line to keep the default shown." + echo "" + read -rp " Fast/chat model [$FAST_MODEL]: " _in; FAST_MODEL="${_in:-$FAST_MODEL}" + read -rp " Smart chat model [$CHAT_MODEL]: " _in; CHAT_MODEL="${_in:-$CHAT_MODEL}" + read -rp " Code model [$CODE_MODEL]: " _in; CODE_MODEL="${_in:-$CODE_MODEL}" + read -rp " Reasoning model (Enter to skip): " REASON_MODEL + fi + else + if command -v whiptail &>/dev/null; then + # Build whiptail radiolist with recommended tier pre-selected + WHIP_TIERS=() + declare -A _TIER_LABELS + case "$MODEL_PREF" in + 1) _TIER_LABELS=([7B]="mistral:7b + codellama:7b" [14B]="phi4:14b + starcoder2:15b" [22B]="phi4:14b + codestral:22b" [70B]="llama3.3:70b + codestral:22b") + _TIER_SPEEDS=([7B]="$(speed_label 4)" [14B]="$(speed_label 9)" [22B]="$(speed_label 13)" [70B]="$(speed_label 41)") ;; + 2) _TIER_LABELS=([4B]="qwen3.5:4b (chat+code)" [9B]="qwen3.5:9b (chat+code)" [35B]="qwen3.5-35b-a3b (MoE, 3B active)" [27B]="qwen3.5:27b (dense)") + _TIER_SPEEDS=([4B]="$(speed_label 2)" [9B]="$(speed_label 5)" [35B]="$(speed_label 12)" [27B]="$(speed_label 17)") ;; + 3) _TIER_LABELS=([7B]="mistral:7b + qwen3.5:4b" [14B]="phi4:14b + qwen3.5:9b" [35B]="phi4:14b + qwen3.5-35b-a3b" [70B]="llama3.3:70b + qwen3.5-35b-a3b") + _TIER_SPEEDS=([7B]="$(speed_label 4)" [14B]="$(speed_label 9)" [35B]="$(speed_label 19)" [70B]="$(speed_label 41)") ;; + esac + for _tn in "${_TIER_NAMES[@]}"; do + _onoff="OFF"; [[ "$_tn" == "$REC_TIER" ]] && _onoff="ON" + WHIP_TIERS+=("$_tn" "${_TIER_LABELS[$_tn]} | ${_TIER_SPEEDS[$_tn]}" "$_onoff") + done + TIER_PICK=$(whiptail --title "Model Size — ${VRAM_GB}GB GPU" \ + --radiolist "Recommended tier pre-selected based on your GPU.\nSPACE = select ENTER = confirm" \ + 14 90 4 \ + "${WHIP_TIERS[@]}" 3>&1 1>&2 2>&3) || TIER_PICK="$REC_TIER" + TIER_PICK="${TIER_PICK//\"/}" + unset WHIP_TIERS _TIER_LABELS _TIER_SPEEDS + else + read -rp " Choose tier [$REC_NUM]: " _TIER_INPUT + _TIER_INPUT="${_TIER_INPUT:-$REC_NUM}" + if [[ "$_TIER_INPUT" =~ ^[1-4]$ ]]; then + TIER_PICK="${_TIER_NAMES[$((_TIER_INPUT-1))]}" + else + TIER_PICK="$_TIER_INPUT" + fi fi unset _TIER_NAMES _TIER_INPUT REC_NUM @@ -594,16 +695,29 @@ if $INSTALL_AI; then esac fi - echo "" - echo " Models selected:" - printf " %-16s %s\n" "Fast chat:" "$FAST_MODEL" - printf " %-16s %s\n" "Smart chat:" "$CHAT_MODEL" - printf " %-16s %s\n" "Code:" "$CODE_MODEL" - [[ -n "$REASON_MODEL" ]] && printf " %-16s %s\n" "Reasoning:" "$REASON_MODEL" - printf " %-16s %s\n" "Embed (RAG):" "$EMBED_MODEL" - echo "" - read -rp " Download these models now? [Y/n]: " DO_PULL - [[ "${DO_PULL,,}" != "n" ]] && PULL_MODELS=true + # Show selected models and ask about download + _MODEL_SUMMARY="Fast chat: $FAST_MODEL\nSmart chat: $CHAT_MODEL\nCode: $CODE_MODEL" + [[ -n "$REASON_MODEL" ]] && _MODEL_SUMMARY+="\nReasoning: $REASON_MODEL" + _MODEL_SUMMARY+="\nEmbed (RAG): $EMBED_MODEL" + + if command -v whiptail &>/dev/null; then + if whiptail --title "Download Models Now?" \ + --yesno "Models selected:\n\n$_MODEL_SUMMARY\n\nDownload these models now? (can take 10-40 min)" \ + 14 60 3>&1 1>&2 2>&3; then + PULL_MODELS=true + fi + else + echo "" + echo " Models selected:" + printf " %-16s %s\n" "Fast chat:" "$FAST_MODEL" + printf " %-16s %s\n" "Smart chat:" "$CHAT_MODEL" + printf " %-16s %s\n" "Code:" "$CODE_MODEL" + [[ -n "$REASON_MODEL" ]] && printf " %-16s %s\n" "Reasoning:" "$REASON_MODEL" + printf " %-16s %s\n" "Embed (RAG):" "$EMBED_MODEL" + echo "" + read -rp " Download these models now? [Y/n]: " DO_PULL + [[ "${DO_PULL,,}" != "n" ]] && PULL_MODELS=true + fi fi @@ -642,8 +756,14 @@ if [[ "$ZIM_CHOICE" == "2" ]]; then echo " ✓ Download ${_nzim} ZIM(s): $ZIM_PICKS" fi echo "" -read -rp " Proceed? [Y/n]: " CONFIRM -[[ "${CONFIRM,,}" == "n" ]] && echo "Aborted." && exit 0 +if command -v whiptail &>/dev/null; then + whiptail --title "Ready to Install" \ + --yesno "Everything above will be installed and configured.\n\nProceed?" \ + 9 50 3>&1 1>&2 2>&3 || { echo "Aborted."; exit 0; } +else + read -rp " Proceed? [Y/n]: " CONFIRM + [[ "${CONFIRM,,}" == "n" ]] && echo "Aborted." && exit 0 +fi echo "" # ── helper: write only if missing (or --force) ────────────────────────────────