|
6 | 6 | from pathlib import Path |
7 | 7 |
|
8 | 8 | import typer |
9 | | -from rich.console import Console |
| 9 | +from rich.console import Console, Group |
10 | 10 | from rich.panel import Panel |
11 | 11 | from rich.table import Table |
| 12 | +from rich.text import Text |
12 | 13 |
|
13 | 14 | from basic_memory.cli.app import app |
14 | 15 | from basic_memory.cli.auth import CLIAuth |
@@ -44,6 +45,17 @@ def format_path(path: str) -> str: |
44 | 45 | return path |
45 | 46 |
|
46 | 47 |
|
| 48 | +def make_bar(value: int, max_value: int, width: int = 40) -> Text: |
| 49 | + """Create a horizontal bar chart element using Unicode blocks.""" |
| 50 | + if max_value == 0: |
| 51 | + return Text("░" * width, style="dim") |
| 52 | + filled = max(1, round(value / max_value * width)) if value > 0 else 0 |
| 53 | + bar = Text() |
| 54 | + bar.append("█" * filled, style="cyan") |
| 55 | + bar.append("░" * (width - filled), style="dim") |
| 56 | + return bar |
| 57 | + |
| 58 | + |
47 | 59 | @project_app.command("list") |
48 | 60 | def list_projects( |
49 | 61 | local: bool = typer.Option(False, "--local", help="Force local routing for this command"), |
@@ -758,99 +770,113 @@ def display_project_info( |
758 | 770 | # Convert to JSON and print |
759 | 771 | print(json.dumps(info.model_dump(), indent=2, default=str)) |
760 | 772 | else: |
761 | | - # Project configuration section |
762 | | - console.print( |
763 | | - Panel( |
764 | | - f"Basic Memory version: [bold green]{info.system.version}[/bold green]\n" |
765 | | - f"[bold]Project:[/bold] {info.project_name}\n" |
766 | | - f"[bold]Path:[/bold] {info.project_path}\n" |
767 | | - f"[bold]Default Project:[/bold] {info.default_project}\n", |
768 | | - title="Basic Memory Project Info", |
769 | | - expand=False, |
| 773 | + # --- Left column: Knowledge Graph stats --- |
| 774 | + left = Table.grid(padding=(0, 2)) |
| 775 | + left.add_column("metric", style="cyan") |
| 776 | + left.add_column("value", style="green", justify="right") |
| 777 | + |
| 778 | + left.add_row("[bold]Knowledge Graph[/bold]", "") |
| 779 | + left.add_row("Entities", str(info.statistics.total_entities)) |
| 780 | + left.add_row("Observations", str(info.statistics.total_observations)) |
| 781 | + left.add_row("Relations", str(info.statistics.total_relations)) |
| 782 | + left.add_row("Unresolved", str(info.statistics.total_unresolved_relations)) |
| 783 | + left.add_row("Isolated", str(info.statistics.isolated_entities)) |
| 784 | + |
| 785 | + # --- Right column: Embeddings --- |
| 786 | + right = Table.grid(padding=(0, 2)) |
| 787 | + right.add_column("property", style="cyan") |
| 788 | + right.add_column("value", style="green") |
| 789 | + |
| 790 | + right.add_row("[bold]Embeddings[/bold]", "") |
| 791 | + if info.embedding_status: |
| 792 | + es = info.embedding_status |
| 793 | + if not es.semantic_search_enabled: |
| 794 | + right.add_row("[green]●[/green] Semantic Search", "Disabled") |
| 795 | + else: |
| 796 | + right.add_row("[green]●[/green] Semantic Search", "Enabled") |
| 797 | + if es.embedding_provider: |
| 798 | + right.add_row(" Provider", es.embedding_provider) |
| 799 | + if es.embedding_model: |
| 800 | + right.add_row(" Model", es.embedding_model) |
| 801 | + # Embedding coverage bar |
| 802 | + if es.total_indexed_entities > 0: |
| 803 | + coverage_bar = make_bar( |
| 804 | + es.total_entities_with_chunks, |
| 805 | + es.total_indexed_entities, |
| 806 | + width=20, |
| 807 | + ) |
| 808 | + count_text = Text( |
| 809 | + f" {es.total_entities_with_chunks}/{es.total_indexed_entities}", |
| 810 | + style="green", |
| 811 | + ) |
| 812 | + bar_with_count = Text.assemble(" Indexed ", coverage_bar, count_text) |
| 813 | + right.add_row(bar_with_count, "") |
| 814 | + right.add_row(" Chunks", str(es.total_chunks)) |
| 815 | + if es.reindex_recommended: |
| 816 | + right.add_row( |
| 817 | + "[yellow]●[/yellow] Status", |
| 818 | + "[yellow]Reindex recommended[/yellow]", |
| 819 | + ) |
| 820 | + if es.reindex_reason: |
| 821 | + right.add_row(" Reason", f"[yellow]{es.reindex_reason}[/yellow]") |
| 822 | + else: |
| 823 | + right.add_row("[green]●[/green] Status", "[green]Up to date[/green]") |
| 824 | + |
| 825 | + # --- Compose two-column layout (content-sized, NOT Layout) --- |
| 826 | + columns = Table.grid(padding=(0, 4), expand=False) |
| 827 | + columns.add_row(left, right) |
| 828 | + |
| 829 | + # --- Note Types bar chart (top 5 by count) --- |
| 830 | + bars_section = None |
| 831 | + if info.statistics.note_types: |
| 832 | + sorted_types = sorted( |
| 833 | + info.statistics.note_types.items(), key=lambda x: x[1], reverse=True |
| 834 | + ) |
| 835 | + top_types = sorted_types[:5] |
| 836 | + max_count = top_types[0][1] if top_types else 1 |
| 837 | + |
| 838 | + bars = Table.grid(padding=(0, 2), expand=False) |
| 839 | + bars.add_column("type", style="cyan", width=16, justify="right") |
| 840 | + bars.add_column("bar") |
| 841 | + bars.add_column("count", style="green", justify="right") |
| 842 | + |
| 843 | + for note_type, count in top_types: |
| 844 | + bars.add_row(note_type, make_bar(count, max_count), str(count)) |
| 845 | + |
| 846 | + remaining = len(sorted_types) - len(top_types) |
| 847 | + bars_section = Group( |
| 848 | + "[bold]Note Types[/bold]", |
| 849 | + bars, |
| 850 | + f"[dim]+{remaining} more types[/dim]" if remaining > 0 else "", |
770 | 851 | ) |
771 | | - ) |
772 | | - |
773 | | - # Statistics section |
774 | | - stats_table = Table(title="Statistics") |
775 | | - stats_table.add_column("Metric", style="cyan") |
776 | | - stats_table.add_column("Count", style="green") |
777 | | - |
778 | | - stats_table.add_row("Entities", str(info.statistics.total_entities)) |
779 | | - stats_table.add_row("Observations", str(info.statistics.total_observations)) |
780 | | - stats_table.add_row("Relations", str(info.statistics.total_relations)) |
781 | | - stats_table.add_row( |
782 | | - "Unresolved Relations", str(info.statistics.total_unresolved_relations) |
783 | | - ) |
784 | | - stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities)) |
785 | | - |
786 | | - console.print(stats_table) |
787 | 852 |
|
788 | | - # Note types |
789 | | - if info.statistics.note_types: |
790 | | - note_types_table = Table(title="Note Types") |
791 | | - note_types_table.add_column("Type", style="blue") |
792 | | - note_types_table.add_column("Count", style="green") |
793 | | - |
794 | | - for note_type, count in info.statistics.note_types.items(): |
795 | | - note_types_table.add_row(note_type, str(count)) |
796 | | - |
797 | | - console.print(note_types_table) |
798 | | - |
799 | | - # Most connected entities |
800 | | - if info.statistics.most_connected_entities: # pragma: no cover |
801 | | - connected_table = Table(title="Most Connected Entities") |
802 | | - connected_table.add_column("Title", style="blue") |
803 | | - connected_table.add_column("Permalink", style="cyan") |
804 | | - connected_table.add_column("Relations", style="green") |
805 | | - |
806 | | - for entity in info.statistics.most_connected_entities: |
807 | | - connected_table.add_row( |
808 | | - entity["title"], entity["permalink"], str(entity["relation_count"]) |
809 | | - ) |
810 | | - |
811 | | - console.print(connected_table) |
812 | | - |
813 | | - # Recent activity |
814 | | - if info.activity.recently_updated: # pragma: no cover |
815 | | - recent_table = Table(title="Recent Activity") |
816 | | - recent_table.add_column("Title", style="blue") |
817 | | - recent_table.add_column("Type", style="cyan") |
818 | | - recent_table.add_column("Last Updated", style="green") |
819 | | - |
820 | | - for entity in info.activity.recently_updated[:5]: # Show top 5 |
821 | | - updated_at = ( |
822 | | - datetime.fromisoformat(entity["updated_at"]) |
823 | | - if isinstance(entity["updated_at"], str) |
824 | | - else entity["updated_at"] |
825 | | - ) |
826 | | - recent_table.add_row( |
827 | | - entity["title"], |
828 | | - entity["note_type"], |
829 | | - updated_at.strftime("%Y-%m-%d %H:%M"), |
830 | | - ) |
831 | | - |
832 | | - console.print(recent_table) |
833 | | - |
834 | | - # Available projects |
835 | | - projects_table = Table(title="Available Projects") |
836 | | - projects_table.add_column("Name", style="blue") |
837 | | - projects_table.add_column("Path", style="cyan") |
838 | | - projects_table.add_column("Default", style="green") |
839 | | - |
840 | | - for name, proj_info in info.available_projects.items(): |
841 | | - is_default = name == info.default_project |
842 | | - project_path = proj_info["path"] |
843 | | - projects_table.add_row(name, project_path, "[X]" if is_default else "") |
844 | | - |
845 | | - console.print(projects_table) |
846 | | - |
847 | | - # Timestamp |
| 853 | + # --- Footer --- |
848 | 854 | current_time = ( |
849 | 855 | datetime.fromisoformat(str(info.system.timestamp)) |
850 | 856 | if isinstance(info.system.timestamp, str) |
851 | 857 | else info.system.timestamp |
852 | 858 | ) |
853 | | - console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]") |
| 859 | + footer = ( |
| 860 | + f"[dim]{format_path(info.project_path)} " |
| 861 | + f"default: {info.default_project} " |
| 862 | + f"{current_time.strftime('%Y-%m-%d %H:%M')}[/dim]" |
| 863 | + ) |
| 864 | + |
| 865 | + # --- Assemble dashboard --- |
| 866 | + parts: list = [columns, ""] |
| 867 | + if bars_section: |
| 868 | + parts.extend([bars_section, ""]) |
| 869 | + parts.append(footer) |
| 870 | + body = Group(*parts) |
| 871 | + |
| 872 | + console.print( |
| 873 | + Panel( |
| 874 | + body, |
| 875 | + title=f"[bold]{info.project_name}[/bold]", |
| 876 | + subtitle=f"Basic Memory {info.system.version}", |
| 877 | + expand=False, |
| 878 | + ) |
| 879 | + ) |
854 | 880 |
|
855 | 881 | except typer.Exit: |
856 | 882 | raise |
|
0 commit comments