Asish Karthikeya Gogineni commited on
Commit
31d220e
Β·
1 Parent(s): 77bf0e5

fix: Improve UI layout - VS Code style file tree, better buttons

Browse files

- File tree now uses arrows (β–Ά/β–Ό) for expand/collapse
- Tree indentation with lines (β”‚ β”œβ”€)
- Suggestion buttons in 2x2 grid with shorter text
- Much cleaner, less clumsy layout

Files changed (2) hide show
  1. components/file_explorer.py +95 -114
  2. components/multi_mode.py +21 -15
components/file_explorer.py CHANGED
@@ -1,27 +1,17 @@
1
  """
2
- File Explorer Component - Renders file tree sidebar for indexed files.
3
  """
4
  import streamlit as st
5
  import os
6
  from pathlib import Path
7
- from typing import Dict, List, Optional
8
 
9
 
10
  def build_file_tree(file_paths: List[str], base_path: str = "") -> Dict:
11
- """
12
- Build a nested dictionary representing the file tree from a list of file paths.
13
-
14
- Args:
15
- file_paths: List of file paths (relative or absolute)
16
- base_path: Base path to make paths relative to
17
-
18
- Returns:
19
- Nested dictionary representing folder structure
20
- """
21
  tree = {}
22
 
23
  for file_path in file_paths:
24
- # Make path relative if base_path is provided
25
  if base_path:
26
  try:
27
  rel_path = os.path.relpath(file_path, base_path)
@@ -30,135 +20,126 @@ def build_file_tree(file_paths: List[str], base_path: str = "") -> Dict:
30
  else:
31
  rel_path = file_path
32
 
33
- # Split path into parts
34
  parts = Path(rel_path).parts
35
-
36
- # Navigate/create tree structure
37
  current = tree
 
38
  for i, part in enumerate(parts):
39
  if i == len(parts) - 1:
40
- # It's a file
41
  current[part] = {"_type": "file", "_path": file_path}
42
  else:
43
- # It's a directory
44
  if part not in current:
45
- current[part] = {"_type": "dir"}
46
- current = current[part]
 
 
47
 
48
  return tree
49
 
50
 
51
  def get_file_icon(filename: str) -> str:
52
- """Get an appropriate icon for a file based on its extension."""
53
  ext = Path(filename).suffix.lower()
54
-
55
  icons = {
56
- ".py": "🐍",
57
- ".js": "πŸ“œ",
58
- ".ts": "πŸ“˜",
59
- ".jsx": "βš›οΈ",
60
- ".tsx": "βš›οΈ",
61
- ".html": "🌐",
62
- ".css": "🎨",
63
- ".json": "πŸ“‹",
64
- ".md": "πŸ“",
65
- ".txt": "πŸ“„",
66
- ".yaml": "βš™οΈ",
67
- ".yml": "βš™οΈ",
68
- ".toml": "βš™οΈ",
69
- ".sql": "πŸ—ƒοΈ",
70
- ".sh": "πŸ–₯️",
71
- ".bash": "πŸ–₯️",
72
- ".env": "πŸ”",
73
- ".gitignore": "🚫",
74
  }
75
-
76
  return icons.get(ext, "πŸ“„")
77
 
78
 
79
- def render_tree_node(name: str, node: Dict, path_prefix: str = "", depth: int = 0):
80
- """Recursively render a tree node (file or directory)."""
81
-
82
- if node.get("_type") == "file":
83
- # Render file
84
- icon = get_file_icon(name)
85
- file_path = node.get("_path", "")
86
-
87
- # Create button for file selection
88
- indent = " " * (depth * 4)
89
- if st.button(f"{icon} {name}", key=f"file_{file_path}", use_container_width=True):
90
- st.session_state.selected_file = file_path
91
- st.rerun()
92
- else:
93
- # Render directory
94
- dir_key = f"dir_{path_prefix}/{name}"
95
-
96
- # Check if directory is expanded
97
- if "expanded_dirs" not in st.session_state:
98
- st.session_state.expanded_dirs = set()
99
-
100
- is_expanded = dir_key in st.session_state.expanded_dirs
101
-
102
- # Toggle button for directory
103
- icon = "πŸ“‚" if is_expanded else "πŸ“"
104
- if st.button(f"{icon} {name}", key=dir_key, use_container_width=True):
105
- if is_expanded:
106
- st.session_state.expanded_dirs.discard(dir_key)
107
- else:
108
- st.session_state.expanded_dirs.add(dir_key)
109
- st.rerun()
110
-
111
- # Render children if expanded
112
- if is_expanded:
113
- # Get children (excluding metadata keys)
114
- children = {k: v for k, v in node.items() if not k.startswith("_")}
115
-
116
- # Sort: directories first, then files
117
- sorted_children = sorted(
118
- children.items(),
119
- key=lambda x: (x[1].get("_type") == "file", x[0].lower())
120
- )
121
-
122
- for child_name, child_node in sorted_children:
123
- with st.container():
124
- render_tree_node(
125
- child_name,
126
- child_node,
127
- f"{path_prefix}/{name}",
128
- depth + 1
129
- )
130
-
131
-
132
  def render_file_tree(indexed_files: List[str], base_path: str = ""):
133
- """
134
- Render the file tree sidebar.
135
-
136
- Args:
137
- indexed_files: List of indexed file paths
138
- base_path: Base path to make paths relative to
139
- """
140
- st.markdown("### πŸ“ Files")
141
 
142
  if not indexed_files:
143
- st.caption("No files indexed yet")
144
  return
145
 
146
- st.caption(f"{len(indexed_files)} files indexed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- # Build tree structure
149
  tree = build_file_tree(indexed_files, base_path)
150
 
151
- # Sort root level: directories first, then files
152
- sorted_root = sorted(
153
- tree.items(),
154
- key=lambda x: (x[1].get("_type") == "file", x[0].lower())
155
- )
156
 
157
- # Render tree
158
- for name, node in sorted_root:
159
- render_tree_node(name, node)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
 
162
  def get_indexed_files_from_session() -> List[str]:
163
- """Get the list of indexed files from session state."""
164
  return st.session_state.get("indexed_files", [])
 
1
  """
2
+ File Explorer Component - VS Code style file tree.
3
  """
4
  import streamlit as st
5
  import os
6
  from pathlib import Path
7
+ from typing import Dict, List
8
 
9
 
10
  def build_file_tree(file_paths: List[str], base_path: str = "") -> Dict:
11
+ """Build a nested dictionary representing the file tree."""
 
 
 
 
 
 
 
 
 
12
  tree = {}
13
 
14
  for file_path in file_paths:
 
15
  if base_path:
16
  try:
17
  rel_path = os.path.relpath(file_path, base_path)
 
20
  else:
21
  rel_path = file_path
22
 
 
23
  parts = Path(rel_path).parts
 
 
24
  current = tree
25
+
26
  for i, part in enumerate(parts):
27
  if i == len(parts) - 1:
 
28
  current[part] = {"_type": "file", "_path": file_path}
29
  else:
 
30
  if part not in current:
31
+ current[part] = {"_type": "dir", "_children": {}}
32
+ current = current.get(part, {}).get("_children", current.get(part, {}))
33
+ if "_children" not in current:
34
+ current = current
35
 
36
  return tree
37
 
38
 
39
  def get_file_icon(filename: str) -> str:
40
+ """Get icon for file based on extension."""
41
  ext = Path(filename).suffix.lower()
 
42
  icons = {
43
+ ".py": "🐍", ".js": "πŸ“œ", ".ts": "πŸ“˜", ".jsx": "βš›οΈ", ".tsx": "βš›οΈ",
44
+ ".html": "🌐", ".css": "🎨", ".json": "πŸ“‹", ".md": "πŸ“",
45
+ ".yaml": "βš™οΈ", ".yml": "βš™οΈ", ".toml": "βš™οΈ", ".sql": "πŸ—ƒοΈ",
46
+ ".env": "πŸ”", ".gitignore": "🚫", ".txt": "πŸ“„",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
 
48
  return icons.get(ext, "πŸ“„")
49
 
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def render_file_tree(indexed_files: List[str], base_path: str = ""):
52
+ """Render VS Code style file tree."""
 
 
 
 
 
 
 
53
 
54
  if not indexed_files:
55
+ st.caption("No files indexed")
56
  return
57
 
58
+ # Custom CSS for tree styling
59
+ st.markdown("""
60
+ <style>
61
+ .tree-item {
62
+ padding: 2px 0;
63
+ cursor: pointer;
64
+ font-size: 13px;
65
+ font-family: 'Segoe UI', sans-serif;
66
+ color: #ccc;
67
+ white-space: nowrap;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ }
71
+ .tree-item:hover {
72
+ background: rgba(255,255,255,0.1);
73
+ }
74
+ .tree-dir {
75
+ font-weight: 500;
76
+ }
77
+ .tree-file {
78
+ font-weight: 400;
79
+ }
80
+ .tree-selected {
81
+ background: rgba(56, 189, 248, 0.2) !important;
82
+ color: #38bdf8;
83
+ }
84
+ </style>
85
+ """, unsafe_allow_html=True)
86
+
87
+ st.markdown(f"**πŸ“ Files** ({len(indexed_files)})")
88
 
89
+ # Build and render tree
90
  tree = build_file_tree(indexed_files, base_path)
91
 
92
+ # Initialize expanded state
93
+ if "tree_expanded" not in st.session_state:
94
+ st.session_state.tree_expanded = set()
 
 
95
 
96
+ # Render tree items
97
+ render_tree_items(tree, 0)
98
+
99
+
100
+ def render_tree_items(tree: Dict, depth: int):
101
+ """Render tree items with proper indentation."""
102
+
103
+ # Sort: directories first, then files
104
+ items = [(k, v) for k, v in tree.items() if not k.startswith("_")]
105
+ sorted_items = sorted(items, key=lambda x: (x[1].get("_type") == "file", x[0].lower()))
106
+
107
+ for name, node in sorted_items:
108
+ is_file = node.get("_type") == "file"
109
+ indent = "β”‚ " * depth
110
+
111
+ if is_file:
112
+ # File item
113
+ file_path = node.get("_path", "")
114
+ icon = get_file_icon(name)
115
+ is_selected = st.session_state.get("selected_file") == file_path
116
+
117
+ # Compact button
118
+ btn_label = f"{indent}β”œβ”€ {icon} {name}"
119
+ if st.button(btn_label, key=f"tree_{file_path}", use_container_width=True,
120
+ type="primary" if is_selected else "secondary"):
121
+ st.session_state.selected_file = file_path
122
+ st.rerun()
123
+ else:
124
+ # Directory item
125
+ dir_key = f"dir_{depth}_{name}"
126
+ is_expanded = dir_key in st.session_state.tree_expanded
127
+ arrow = "β–Ό" if is_expanded else "β–Ά"
128
+
129
+ btn_label = f"{indent}{arrow} πŸ“ {name}"
130
+ if st.button(btn_label, key=dir_key, use_container_width=True, type="secondary"):
131
+ if is_expanded:
132
+ st.session_state.tree_expanded.discard(dir_key)
133
+ else:
134
+ st.session_state.tree_expanded.add(dir_key)
135
+ st.rerun()
136
+
137
+ # Render children if expanded
138
+ if is_expanded:
139
+ children = {k: v for k, v in node.items() if not k.startswith("_")}
140
+ render_tree_items(children, depth + 1)
141
 
142
 
143
  def get_indexed_files_from_session() -> List[str]:
144
+ """Get indexed files from session state."""
145
  return st.session_state.get("indexed_files", [])
components/multi_mode.py CHANGED
@@ -64,26 +64,32 @@ def render_chat_mode(chat_engine):
64
  chat_engine: ChatEngine instance
65
  """
66
  st.markdown("### πŸ’¬ Chat with Your Codebase")
67
- st.caption("πŸ“š Explain code & workflows | πŸ”§ Generate & modify code | ✨ Develop new features | πŸ” Fast indexed retrieval")
68
 
69
  # Show suggested prompts if no history
70
  if not st.session_state.get("messages", []):
71
- st.markdown("#### πŸ’‘ Try asking:")
72
 
73
- suggestions = [
74
- "How does the main module work?",
75
- "Explain key functions and workflows",
76
- "Generate a new utility function",
77
- "Suggest improvements for the code",
78
- "List all modules and their purpose"
79
- ]
 
 
 
80
 
81
- cols = st.columns(len(suggestions))
82
- for i, suggestion in enumerate(suggestions):
83
- with cols[i]:
84
- if st.button(suggestion, key=f"suggest_{i}", use_container_width=True):
85
- st.session_state.pending_prompt = suggestion
86
- st.rerun()
 
 
 
 
87
 
88
  # Return True to continue with normal chat flow
89
  return True
 
64
  chat_engine: ChatEngine instance
65
  """
66
  st.markdown("### πŸ’¬ Chat with Your Codebase")
 
67
 
68
  # Show suggested prompts if no history
69
  if not st.session_state.get("messages", []):
70
+ st.markdown("πŸ’‘ **Try asking:**")
71
 
72
+ # Row 1 - 2 suggestions
73
+ col1, col2 = st.columns(2)
74
+ with col1:
75
+ if st.button("πŸ” Explain project structure", key="suggest_0", use_container_width=True):
76
+ st.session_state.pending_prompt = "Explain the project structure and main components"
77
+ st.rerun()
78
+ with col2:
79
+ if st.button("πŸ“ List all functions", key="suggest_1", use_container_width=True):
80
+ st.session_state.pending_prompt = "List all the main functions and their purpose"
81
+ st.rerun()
82
 
83
+ # Row 2 - 2 suggestions
84
+ col3, col4 = st.columns(2)
85
+ with col3:
86
+ if st.button("⚑ Generate code", key="suggest_2", use_container_width=True):
87
+ st.session_state.pending_prompt = "Generate a new utility function for this project"
88
+ st.rerun()
89
+ with col4:
90
+ if st.button("πŸ”§ Suggest improvements", key="suggest_3", use_container_width=True):
91
+ st.session_state.pending_prompt = "What improvements would you suggest for this code?"
92
+ st.rerun()
93
 
94
  # Return True to continue with normal chat flow
95
  return True