mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat(ui): Add beautiful startup UI with changelog integration
- Create comprehensive startup UI with ASCII art branding - Add What's New section with GitHub changelog integration - Implement update checking from PyPI - Show system status (Router, Node, API) indicators - Add quick start tips and command hints - Create inline startup notifications for commands - Support minimal UI mode with environment variables - Cache changelog data for offline usage - Smart display logic (show once per day for inline) - Version bump to 0.3.23 The UI provides a clean, informative startup experience similar to Claude Code, with automatic update notifications and helpful tips for new users.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""Hanzo - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime."""
|
||||
|
||||
__version__ = "0.3.2"
|
||||
__all__ = ["main", "cli"]
|
||||
__version__ = "0.3.23"
|
||||
__all__ = ["main", "cli", "__version__"]
|
||||
|
||||
from .cli import cli, main
|
||||
|
||||
@@ -25,9 +25,10 @@ from .commands import (
|
||||
)
|
||||
from .utils.output import console
|
||||
from .interactive.repl import HanzoREPL
|
||||
from .ui.startup import show_startup
|
||||
|
||||
# Version
|
||||
__version__ = "0.3.22"
|
||||
__version__ = "0.3.23"
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True)
|
||||
@@ -55,12 +56,13 @@ def cli(ctx, verbose: bool, json: bool, config: Optional[str]):
|
||||
|
||||
if os.environ.get("HANZO_COMPUTE_NODE") == "1":
|
||||
# Start as a compute node
|
||||
|
||||
asyncio.run(start_compute_node(ctx))
|
||||
else:
|
||||
# Show startup UI (unless in quiet mode)
|
||||
if not ctx.obj.get("quiet") and not os.environ.get("HANZO_NO_STARTUP"):
|
||||
show_startup(minimal=os.environ.get("HANZO_MINIMAL_UI") == "1")
|
||||
|
||||
# Enter interactive REPL mode
|
||||
console.print("[bold cyan]Hanzo AI - Interactive Mode[/bold cyan]")
|
||||
console.print("Type 'help' for commands, 'exit' to quit\n")
|
||||
try:
|
||||
repl = HanzoREPL(console=console)
|
||||
asyncio.run(repl.run())
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Chat command for interactive AI conversations."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import httpx
|
||||
from rich.markdown import Markdown
|
||||
from rich.panel import Panel
|
||||
from rich import box
|
||||
|
||||
from ..utils.output import console
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Hanzo UI components for CLI.
|
||||
"""
|
||||
|
||||
from .startup import show_startup, StartupUI
|
||||
from .inline_startup import show_inline_startup, show_status
|
||||
|
||||
__all__ = [
|
||||
"show_startup",
|
||||
"StartupUI",
|
||||
"show_inline_startup",
|
||||
"show_status"
|
||||
]
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Inline startup notifications for Hanzo commands.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class InlineStartup:
|
||||
"""Lightweight inline startup notifications."""
|
||||
|
||||
def __init__(self):
|
||||
self.config_dir = Path.home() / ".hanzo"
|
||||
self.last_shown_file = self.config_dir / ".last_inline_shown"
|
||||
self.show_interval = timedelta(hours=24) # Show once per day
|
||||
|
||||
def should_show(self) -> bool:
|
||||
"""Check if we should show inline startup."""
|
||||
# Check environment variable
|
||||
if os.environ.get("HANZO_NO_STARTUP") == "1":
|
||||
return False
|
||||
|
||||
# Check last shown time
|
||||
if self.last_shown_file.exists():
|
||||
try:
|
||||
last_shown = datetime.fromisoformat(
|
||||
self.last_shown_file.read_text().strip()
|
||||
)
|
||||
if datetime.now() - last_shown < self.show_interval:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def mark_shown(self):
|
||||
"""Mark inline startup as shown."""
|
||||
self.config_dir.mkdir(exist_ok=True)
|
||||
self.last_shown_file.write_text(datetime.now().isoformat())
|
||||
|
||||
def show_mini(self, command: str = None):
|
||||
"""Show mini inline startup."""
|
||||
if not self.should_show():
|
||||
return
|
||||
|
||||
# Build message
|
||||
message = Text()
|
||||
message.append("✨ ", style="yellow")
|
||||
message.append("Hanzo AI ", style="bold cyan")
|
||||
message.append("v0.3.23", style="green")
|
||||
|
||||
# Add what's new teaser
|
||||
message.append(" • ", style="dim")
|
||||
message.append("What's new: ", style="dim")
|
||||
message.append("Router management, improved docs", style="yellow dim")
|
||||
|
||||
# Show panel
|
||||
console.print(
|
||||
Panel(
|
||||
message,
|
||||
box=box.MINIMAL,
|
||||
border_style="cyan",
|
||||
padding=(0, 1)
|
||||
)
|
||||
)
|
||||
|
||||
self.mark_shown()
|
||||
|
||||
def show_command_hint(self, command: str):
|
||||
"""Show command-specific hints."""
|
||||
hints = {
|
||||
"chat": "💡 Tip: Use --model to change AI model, --router for local proxy",
|
||||
"node": "💡 Tip: Run 'hanzo node start' to enable local AI inference",
|
||||
"router": "💡 Tip: Router provides unified access to 100+ LLM providers",
|
||||
"repl": "💡 Tip: REPL combines Python with AI assistance",
|
||||
"agent": "💡 Tip: Agents can work in parallel with 'hanzo agent swarm'"
|
||||
}
|
||||
|
||||
hint = hints.get(command)
|
||||
if hint and os.environ.get("HANZO_SHOW_HINTS") != "0":
|
||||
console.print(f"[dim]{hint}[/dim]")
|
||||
|
||||
def show_status_bar(self):
|
||||
"""Show a compact status bar."""
|
||||
items = []
|
||||
|
||||
# Check router
|
||||
try:
|
||||
import httpx
|
||||
response = httpx.get("http://localhost:4000/health", timeout=0.5)
|
||||
if response.status_code == 200:
|
||||
items.append("[green]Router ✓[/green]")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check node
|
||||
try:
|
||||
import httpx
|
||||
response = httpx.get("http://localhost:8000/health", timeout=0.5)
|
||||
if response.status_code == 200:
|
||||
items.append("[green]Node ✓[/green]")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check API key
|
||||
if os.environ.get("HANZO_API_KEY"):
|
||||
items.append("[green]API ✓[/green]")
|
||||
else:
|
||||
items.append("[yellow]API ⚠[/yellow]")
|
||||
|
||||
if items:
|
||||
status = " • ".join(items)
|
||||
console.print(f"[dim]Status: {status}[/dim]")
|
||||
|
||||
|
||||
def show_inline_startup(command: str = None):
|
||||
"""Show inline startup notification."""
|
||||
startup = InlineStartup()
|
||||
startup.show_mini(command)
|
||||
if command:
|
||||
startup.show_command_hint(command)
|
||||
|
||||
|
||||
def show_status():
|
||||
"""Show compact status bar."""
|
||||
startup = InlineStartup()
|
||||
startup.show_status_bar()
|
||||
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Hanzo startup UI and changelog integration.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
import httpx
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
from rich.align import Align
|
||||
from rich.columns import Columns
|
||||
from rich.markdown import Markdown
|
||||
from rich import box
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class StartupUI:
|
||||
"""Clean startup UI for Hanzo with changelog integration."""
|
||||
|
||||
def __init__(self):
|
||||
self.config_dir = Path.home() / ".hanzo"
|
||||
self.config_file = self.config_dir / "config.json"
|
||||
self.changelog_cache = self.config_dir / "changelog_cache.json"
|
||||
self.last_shown_file = self.config_dir / ".last_shown_version"
|
||||
self.current_version = self._get_current_version()
|
||||
|
||||
def _get_current_version(self) -> str:
|
||||
"""Get current Hanzo version."""
|
||||
try:
|
||||
from hanzo import __version__
|
||||
return __version__
|
||||
except:
|
||||
return "0.3.23"
|
||||
|
||||
def _get_last_shown_version(self) -> Optional[str]:
|
||||
"""Get the last version shown to user."""
|
||||
if self.last_shown_file.exists():
|
||||
return self.last_shown_file.read_text().strip()
|
||||
return None
|
||||
|
||||
def _save_last_shown_version(self):
|
||||
"""Save current version as last shown."""
|
||||
self.config_dir.mkdir(exist_ok=True)
|
||||
self.last_shown_file.write_text(self.current_version)
|
||||
|
||||
def _fetch_changelog(self) -> List[Dict[str, Any]]:
|
||||
"""Fetch latest changelog from GitHub."""
|
||||
try:
|
||||
# Check cache first
|
||||
if self.changelog_cache.exists():
|
||||
cache_data = json.loads(self.changelog_cache.read_text())
|
||||
cache_time = datetime.fromisoformat(cache_data["timestamp"])
|
||||
if datetime.now() - cache_time < timedelta(hours=6):
|
||||
return cache_data["entries"]
|
||||
|
||||
# Fetch from GitHub
|
||||
response = httpx.get(
|
||||
"https://api.github.com/repos/hanzoai/python-sdk/releases",
|
||||
headers={"Accept": "application/vnd.github.v3+json"},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
releases = response.json()[:5] # Last 5 releases
|
||||
entries = []
|
||||
|
||||
for release in releases:
|
||||
entries.append({
|
||||
"version": release["tag_name"],
|
||||
"date": release["published_at"][:10],
|
||||
"highlights": self._parse_highlights(release["body"])
|
||||
})
|
||||
|
||||
# Cache the results
|
||||
cache_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"entries": entries
|
||||
}
|
||||
self.changelog_cache.write_text(json.dumps(cache_data))
|
||||
return entries
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to static changelog
|
||||
return self._get_static_changelog()
|
||||
|
||||
def _parse_highlights(self, body: str) -> List[str]:
|
||||
"""Parse release highlights from markdown."""
|
||||
if not body:
|
||||
return []
|
||||
|
||||
highlights = []
|
||||
lines = body.split("\n")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("- ") or line.startswith("* "):
|
||||
highlight = line[2:].strip()
|
||||
if len(highlight) > 80:
|
||||
highlight = highlight[:77] + "..."
|
||||
highlights.append(highlight)
|
||||
if len(highlights) >= 3:
|
||||
break
|
||||
|
||||
return highlights
|
||||
|
||||
def _get_static_changelog(self) -> List[Dict[str, Any]]:
|
||||
"""Get static changelog for offline mode."""
|
||||
return [
|
||||
{
|
||||
"version": "v0.3.23",
|
||||
"date": "2024-09-06",
|
||||
"highlights": [
|
||||
"✨ Added router management commands for LLM proxy control",
|
||||
"🎯 Renamed cluster to node for better clarity",
|
||||
"📚 Comprehensive documentation for all packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.3.22",
|
||||
"date": "2024-09-05",
|
||||
"highlights": [
|
||||
"🚀 Improved MCP tool performance with batch operations",
|
||||
"🔧 Fixed file permission handling in Windows",
|
||||
"💾 Added memory persistence for conversations"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
def _create_welcome_panel(self) -> Panel:
|
||||
"""Create the welcome panel with branding."""
|
||||
# ASCII art logo
|
||||
logo = """
|
||||
██╗ ██╗ █████╗ ███╗ ██╗███████╗ ██████╗
|
||||
██║ ██║██╔══██╗████╗ ██║╚══███╔╝██╔═══██╗
|
||||
███████║███████║██╔██╗ ██║ ███╔╝ ██║ ██║
|
||||
██╔══██║██╔══██║██║╚██╗██║ ███╔╝ ██║ ██║
|
||||
██║ ██║██║ ██║██║ ╚████║███████╗╚██████╔╝
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═════╝
|
||||
"""
|
||||
|
||||
# Create welcome text
|
||||
welcome = Text()
|
||||
welcome.append("Welcome to ", style="white")
|
||||
welcome.append("Hanzo AI", style="bold cyan")
|
||||
welcome.append(" • ", style="dim")
|
||||
welcome.append(f"v{self.current_version}", style="green")
|
||||
|
||||
# Add subtitle
|
||||
subtitle = Text("Your AI Infrastructure Platform", style="italic dim")
|
||||
|
||||
# Combine elements
|
||||
content = Align.center(
|
||||
Text.from_ansi(logo) + "\n" + welcome + "\n" + subtitle
|
||||
)
|
||||
|
||||
return Panel(
|
||||
content,
|
||||
box=box.DOUBLE,
|
||||
border_style="cyan",
|
||||
padding=(1, 2)
|
||||
)
|
||||
|
||||
def _create_whats_new_panel(self) -> Optional[Panel]:
|
||||
"""Create What's New panel with recent changes."""
|
||||
last_shown = self._get_last_shown_version()
|
||||
|
||||
# Only show if there's new content
|
||||
if last_shown == self.current_version:
|
||||
return None
|
||||
|
||||
changelog = self._fetch_changelog()
|
||||
if not changelog:
|
||||
return None
|
||||
|
||||
# Build content
|
||||
content = Text()
|
||||
content.append("🎉 What's New\n\n", style="bold yellow")
|
||||
|
||||
for entry in changelog[:2]: # Show last 2 versions
|
||||
content.append(f" {entry['version']}", style="bold cyan")
|
||||
content.append(f" ({entry['date']})\n", style="dim")
|
||||
|
||||
for highlight in entry['highlights'][:2]:
|
||||
content.append(f" • {highlight}\n", style="white")
|
||||
|
||||
content.append("\n")
|
||||
|
||||
return Panel(
|
||||
content,
|
||||
title="[yellow]Recent Updates[/yellow]",
|
||||
box=box.ROUNDED,
|
||||
border_style="yellow",
|
||||
padding=(0, 1)
|
||||
)
|
||||
|
||||
def _create_quick_start_panel(self) -> Panel:
|
||||
"""Create quick start tips panel."""
|
||||
tips = [
|
||||
("chat", "Start interactive AI chat"),
|
||||
("node start", "Run local AI node"),
|
||||
("router start", "Start LLM proxy"),
|
||||
("repl", "Interactive Python + AI"),
|
||||
("help", "Show all commands")
|
||||
]
|
||||
|
||||
# Create table
|
||||
table = Table(show_header=False, box=None, padding=(0, 2))
|
||||
table.add_column("Command", style="cyan")
|
||||
table.add_column("Description", style="dim")
|
||||
|
||||
for cmd, desc in tips:
|
||||
table.add_row(f"hanzo {cmd}", desc)
|
||||
|
||||
return Panel(
|
||||
table,
|
||||
title="[green]Quick Start[/green]",
|
||||
box=box.ROUNDED,
|
||||
border_style="green",
|
||||
padding=(0, 1)
|
||||
)
|
||||
|
||||
def _create_status_panel(self) -> Panel:
|
||||
"""Create status panel showing system state."""
|
||||
items = []
|
||||
|
||||
# Check router status
|
||||
try:
|
||||
response = httpx.get("http://localhost:4000/health", timeout=1)
|
||||
router_status = "🟢 Running" if response.status_code == 200 else "🔴 Offline"
|
||||
except:
|
||||
router_status = "⚫ Offline"
|
||||
|
||||
# Check node status
|
||||
try:
|
||||
response = httpx.get("http://localhost:8000/health", timeout=1)
|
||||
node_status = "🟢 Running" if response.status_code == 200 else "🔴 Offline"
|
||||
except:
|
||||
node_status = "⚫ Offline"
|
||||
|
||||
# Check API key
|
||||
api_key = os.getenv("HANZO_API_KEY")
|
||||
api_status = "🟢 Configured" if api_key else "🟡 Not Set"
|
||||
|
||||
# Build status text
|
||||
status = Text()
|
||||
status.append("Router: ", style="bold")
|
||||
status.append(f"{router_status} ", style="white")
|
||||
status.append("Node: ", style="bold")
|
||||
status.append(f"{node_status} ", style="white")
|
||||
status.append("API: ", style="bold")
|
||||
status.append(api_status, style="white")
|
||||
|
||||
return Panel(
|
||||
Align.center(status),
|
||||
box=box.ROUNDED,
|
||||
border_style="blue",
|
||||
padding=(0, 1)
|
||||
)
|
||||
|
||||
def _check_for_updates(self) -> Optional[str]:
|
||||
"""Check if updates are available."""
|
||||
try:
|
||||
response = httpx.get(
|
||||
"https://pypi.org/pypi/hanzo/json",
|
||||
timeout=3
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
latest = data["info"]["version"]
|
||||
if latest != self.current_version:
|
||||
return latest
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def show(self, minimal: bool = False):
|
||||
"""Display the startup UI."""
|
||||
console.clear()
|
||||
|
||||
if minimal:
|
||||
# Minimal mode - just show compact welcome
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold cyan]Hanzo AI[/bold cyan] • v{self.current_version} • [dim]Type [cyan]hanzo help[/cyan] for commands[/dim]",
|
||||
box=box.ROUNDED,
|
||||
padding=(0, 1)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Full startup UI
|
||||
panels = []
|
||||
|
||||
# Welcome panel
|
||||
welcome = self._create_welcome_panel()
|
||||
console.print(welcome)
|
||||
|
||||
# What's New (if applicable)
|
||||
whats_new = self._create_whats_new_panel()
|
||||
if whats_new:
|
||||
console.print(whats_new)
|
||||
self._save_last_shown_version()
|
||||
|
||||
# Quick start and status in columns
|
||||
quick_start = self._create_quick_start_panel()
|
||||
status = self._create_status_panel()
|
||||
|
||||
console.print(Columns([quick_start, status], equal=True, expand=True))
|
||||
|
||||
# Check for updates
|
||||
latest = self._check_for_updates()
|
||||
if latest:
|
||||
console.print(
|
||||
Panel(
|
||||
f"[yellow]📦 Update available:[/yellow] v{latest} → Run [cyan]pip install --upgrade hanzo[/cyan]",
|
||||
box=box.ROUNDED,
|
||||
border_style="yellow",
|
||||
padding=(0, 1)
|
||||
)
|
||||
)
|
||||
|
||||
# Footer
|
||||
console.print(
|
||||
Align.center(
|
||||
Text("Get started with ", style="dim") +
|
||||
Text("hanzo chat", style="bold cyan") +
|
||||
Text(" or view docs at ", style="dim") +
|
||||
Text("docs.hanzo.ai", style="blue underline")
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
|
||||
def show_startup(minimal: bool = False):
|
||||
"""Show the startup UI."""
|
||||
ui = StartupUI()
|
||||
ui.show(minimal=minimal)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
show_startup()
|
||||
Reference in New Issue
Block a user