DeL-TaiseiOzaki commited on
Commit
b7b8cc1
·
verified ·
1 Parent(s): f9e7819

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -181
app.py CHANGED
@@ -1,188 +1,212 @@
 
 
1
  import streamlit as st
2
  import tempfile
3
- import git
4
  from pathlib import Path
5
- from datetime import datetime
6
- from services.llm_service import LLMService
7
  from core.file_scanner import FileScanner, FileInfo
8
- from typing import List
9
-
10
-
11
- # ページ設定
12
- st.set_page_config(
13
- page_title="Repository Code Analysis",
14
- page_icon="🔍",
15
- layout="wide"
16
- )
17
-
18
- # ダークテーマの設定
19
- st.markdown("""
20
- <style>
21
- .stApp {
22
- background-color: #0e1117;
23
- color: #ffffff;
24
- }
25
- .chat-message {
26
- padding: 1rem;
27
- margin: 1rem 0;
28
- border-radius: 0.5rem;
29
- }
30
- .assistant-message {
31
- background-color: #1e2329;
32
- color: #ffffff;
33
- }
34
- .stButton button {
35
- background-color: #2ea44f;
36
- color: #ffffff;
37
- }
38
- .stTextArea textarea {
39
- background-color: #1e2329;
40
- color: #ffffff;
41
- }
42
- </style>
43
- """, unsafe_allow_html=True)
44
-
45
- def create_download_content(files: List[FileInfo]) -> str:
46
- content = "# スキャン結果\n\n"
47
- for file in files:
48
- content += f"## {file.path}\n"
49
- content += f"サイズ: {file.formatted_size}\n"
50
- content += f"エンコーディング: {file.encoding or '不明'}\n\n"
51
- if file.content:
52
- content += f"```{file.extension[1:] if file.extension else ''}\n"
53
- content += file.content
54
- content += "\n```\n\n"
55
- return content
56
-
57
- def clone_repository(repo_url: str) -> Path:
58
- """リポジトリをクローンして一時ディレクトリに保存"""
59
- temp_dir = Path(tempfile.mkdtemp())
60
- git.Repo.clone_from(repo_url, temp_dir)
61
- return temp_dir
62
 
 
63
  # セッション状態の初期化
64
- if 'repo_content' not in st.session_state:
65
- st.session_state.repo_content = None
66
- if 'temp_dir' not in st.session_state:
67
- st.session_state.temp_dir = None
68
- if 'llm_service' not in st.session_state:
69
- try:
70
- st.session_state.llm_service = LLMService()
71
- except ValueError as e:
72
- st.error(str(e))
73
- st.stop()
74
-
75
- # メインのUIレイアウト
76
- st.title("🔍 リポジトリ解析・質問システム")
77
-
78
- # サイドバーでモデル選択
79
- with st.sidebar:
80
- available_models = []
81
-
82
- if st.session_state.llm_service.settings.anthropic_api_key:
83
- available_models.append("Claude")
84
- if st.session_state.llm_service.settings.openai_api_key:
85
- available_models.append("OpenAI")
86
-
87
- if available_models:
88
- model = st.radio(
89
- "使用するモデル",
90
- available_models,
91
- key="model_selection"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  )
93
- st.session_state.llm_service.switch_model(model.lower())
94
-
95
- st.divider()
96
- st.subheader("📌 使い方")
97
- st.markdown("""
98
- 1. GitHubリポジトリのURLを入力
99
- 2. スキャンを実行
100
- 3. コードについて質問(最大5ターンの会話が可能)
101
- """)
102
-
103
- st.subheader("🔍 スキャン対象")
104
- st.markdown("""
105
- - Python (.py)
106
- - JavaScript (.js)
107
- - Java (.java)
108
- - C/C++ (.c, .h, .cpp, .hpp)
109
- - その他の主要なプログラミング言語
110
- """)
111
-
112
- # URLの入力
113
- repo_url = st.text_input(
114
- "GitHubリポジトリのURLを入力",
115
- placeholder="https://github.com/username/repository.git"
116
- )
117
-
118
- # スキャン実行ボタン
119
- if st.button("スキャン開始", disabled=not repo_url):
120
- try:
121
- with st.spinner('リポジトリをクローン中...'):
122
- temp_dir = clone_repository(repo_url)
123
- st.session_state.temp_dir = temp_dir
124
-
125
- with st.spinner('ファイルをスキャン中...'):
126
- scanner = FileScanner(temp_dir)
127
- files = scanner.scan_files()
128
- st.session_state.repo_content = LLMService.format_code_content(files)
129
-
130
- st.success(f"スキャン完了: {len(files)}個のファイルを検出")
131
-
132
- # スキャン結果のダウンロードボタン
133
- scan_result = create_download_content(files)
134
- st.download_button(
135
- label="スキャン結果をダウンロード",
136
- data=scan_result,
137
- file_name=f"scan_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md",
138
- mime="text/markdown"
139
- )
140
-
141
- # 新しいスキャン時に会話履歴をクリア
142
- st.session_state.llm_service.clear_history()
143
-
144
- except Exception as e:
145
- st.error(f"エラーが発生しました: {str(e)}")
146
-
147
- # スキャン完了後の質問セクション
148
- if st.session_state.repo_content:
149
- st.divider()
150
- st.subheader("💭 コードについて質問する")
151
-
152
- # 会話履歴の表示(アシスタントの回答のみ)
153
- for message in st.session_state.llm_service.conversation_history:
154
- if message.role == "assistant":
155
- st.markdown(f'<div class="chat-message assistant-message">{message.content}</div>',
156
- unsafe_allow_html=True)
157
-
158
- query = st.text_area(
159
- "質問を入力してください",
160
- placeholder="例: このコードの主な機能は何ですか?"
161
- )
162
-
163
- col1, col2 = st.columns([1, 5])
164
- with col1:
165
- if st.button("履歴クリア"):
166
- st.session_state.llm_service.clear_history()
167
- st.rerun()
168
-
169
- with col2:
170
- if st.button("質問する", disabled=not query):
171
- with st.spinner('回答を生成中...'):
172
- response, error = st.session_state.llm_service.get_response(
173
- st.session_state.repo_content,
174
- query
175
- )
176
-
177
- if error:
178
- st.error(error)
179
- else:
180
- st.rerun()
181
-
182
- # セッション終了時のクリーンアップ
183
- if st.session_state.temp_dir and Path(st.session_state.temp_dir).exists():
184
- try:
185
- import shutil
186
- shutil.rmtree(st.session_state.temp_dir)
187
- except:
188
- pass
 
1
+ # app.py
2
+
3
  import streamlit as st
4
  import tempfile
5
+ import shutil
6
  from pathlib import Path
7
+ import git # GitPython
 
8
  from core.file_scanner import FileScanner, FileInfo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # =====================================
11
  # セッション状態の初期化
12
+ # =====================================
13
+ if 'scanned_files' not in st.session_state:
14
+ st.session_state.scanned_files = [] # スキャンしたFileInfoリスト
15
+ if 'selected_files' not in st.session_state:
16
+ st.session_state.selected_files = set() # ユーザーが選択中のファイルパス (相対パス)
17
+ if 'cloned_repo_dir' not in st.session_state:
18
+ st.session_state.cloned_repo_dir = None # クローン先ディレクトリの絶対パス文字列
19
+
20
+ # =====================================
21
+ # タイトル等
22
+ # =====================================
23
+ st.title("Gitリポジトリ スキャナー")
24
+ st.markdown("**ディレクトリ構造をツリー表示し、ファイルを選んでMarkdownダウンロードできます**\n(**ワイドモード推奨**)")
25
+
26
+ # =====================================
27
+ # ツリー構造を生成する関数
28
+ # =====================================
29
+ def build_tree(paths):
30
+ """
31
+ 相対パス(Pathオブジェクト)のリストからツリー状のネスト構造を構築する。
32
+ 戻り値は {要素名 -> 子要素のdict or None} という入れ子の辞書。
33
+ """
34
+ tree = {}
35
+ for p in paths:
36
+ parts = p.parts
37
+ current = tree
38
+ for i, part in enumerate(parts):
39
+ if i == len(parts) - 1:
40
+ # ファイルやフォルダの末端
41
+ current[part] = None
42
+ else:
43
+ if part not in current:
44
+ current[part] = {}
45
+ if isinstance(current[part], dict):
46
+ current = current[part]
47
+ else:
48
+ # もしNoneだった場合(同名のファイル/フォルダがあるなど) → 無理やりdictに
49
+ current[part] = {}
50
+ current = current[part]
51
+ return tree
52
+
53
+ def format_tree(tree_dict, prefix=""):
54
+ """
55
+ build_tree()で作ったネスト構造をASCIIアートのツリー文字列にする。
56
+ """
57
+ lines = []
58
+ entries = sorted(tree_dict.keys())
59
+ for i, entry in enumerate(entries):
60
+ is_last = (i == len(entries) - 1)
61
+ marker = "└── " if is_last else "├── "
62
+ # 子要素がある(=dict)ならフォルダ、Noneならファイル
63
+ if isinstance(tree_dict[entry], dict):
64
+ # フォルダとして表示
65
+ lines.append(prefix + marker + entry + "/")
66
+ # 次の階層のプレフィックスを用意
67
+ extension = " " if is_last else "│ "
68
+ sub_prefix = prefix + extension
69
+ # 再帰的に生成
70
+ lines.extend(format_tree(tree_dict[entry], sub_prefix))
71
+ else:
72
+ # ファイルとして表示
73
+ lines.append(prefix + marker + entry)
74
+ return lines
75
+
76
+
77
+ # =====================================
78
+ # ユーザー入力
79
+ # =====================================
80
+ repo_url = st.text_input("GitリポジトリURL (例: https://github.com/username/repo.git)")
81
+
82
+ st.subheader("スキャン対象拡張子")
83
+ available_exts = [".py", ".js", ".ts", ".sh", ".md", ".txt", ".java", ".cpp"]
84
+ chosen_exts = []
85
+ for ext in available_exts:
86
+ default_checked = (ext in [".py", ".md"]) # デモ用に .py と .md を初期ON
87
+ if st.checkbox(ext, key=f"ext_{ext}", value=default_checked):
88
+ chosen_exts.append(ext)
89
+
90
+ # =====================================
91
+ # スキャン開始ボタン
92
+ # =====================================
93
+ if st.button("スキャン開始"):
94
+ if not repo_url.strip():
95
+ st.error("リポジトリURLを入力してください。")
96
+ else:
97
+ # 既にクローン済フォルダがあれば削除
98
+ if st.session_state.cloned_repo_dir and Path(st.session_state.cloned_repo_dir).exists():
99
+ shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
100
+
101
+ # 一時フォルダを作成してクローン
102
+ tmp_dir = tempfile.mkdtemp()
103
+ clone_path = Path(tmp_dir) / "cloned_repo"
104
+
105
+ try:
106
+ st.write(f"リポジトリをクローン中: {clone_path}")
107
+ git.Repo.clone_from(repo_url, clone_path)
108
+ st.session_state.cloned_repo_dir = str(clone_path)
109
+ except Exception as e:
110
+ st.error(f"クローン失敗: {e}")
111
+ st.session_state.cloned_repo_dir = None
112
+ st.session_state.scanned_files = []
113
+ st.stop()
114
+
115
+ # スキャン
116
+ scanner = FileScanner(base_dir=clone_path, target_extensions=set(chosen_exts))
117
+ found_files = scanner.scan_files()
118
+
119
+ st.session_state.scanned_files = found_files
120
+ st.session_state.selected_files = set()
121
+
122
+ st.success(f"スキャン完了: {len(found_files)}個のファイルを検出")
123
+
124
+ # =====================================
125
+ # クローン削除ボタン
126
+ # =====================================
127
+ if st.session_state.cloned_repo_dir:
128
+ if st.button("クローン済みデータを削除"):
129
+ shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
130
+ st.session_state.cloned_repo_dir = None
131
+ st.session_state.scanned_files = []
132
+ st.session_state.selected_files = set()
133
+ st.success("クローンしたディレクトリを削除しました")
134
+
135
+ # =====================================
136
+ # スキャン結果がある場合 → ツリー表示 + ファイル選択
137
+ # =====================================
138
+ if st.session_state.scanned_files:
139
+ base_path = Path(st.session_state.cloned_repo_dir)
140
+
141
+ # --- ツリーを作る ---
142
+ # scanned_files は「指定拡張子」だけ取得されているので、そのファイルパスのみでツリーを構築
143
+ rel_paths = [f.path.relative_to(base_path) for f in st.session_state.scanned_files]
144
+ tree_dict = build_tree(rel_paths)
145
+ tree_lines = format_tree(tree_dict)
146
+ ascii_tree = "\n".join(tree_lines)
147
+
148
+ st.write("## スキャン結果")
149
+ col_tree, col_files = st.columns([1, 2]) # 左:ツリー, 右:ファイル一覧
150
+
151
+ with col_tree:
152
+ st.markdown("**ディレクトリ構造 (指定拡張子のみ)**")
153
+ st.markdown(f"```\n{ascii_tree}\n```")
154
+
155
+ with col_files:
156
+ st.markdown("**ファイル一覧 (チェックボックス)**")
157
+
158
+ col_btn1, col_btn2 = st.columns(2)
159
+ with col_btn1:
160
+ if st.button("すべて選択"):
161
+ st.session_state.selected_files = set(rel_paths)
162
+ with col_btn2:
163
+ if st.button("すべて解除"):
164
+ st.session_state.selected_files = set()
165
+
166
+ for file_info in st.session_state.scanned_files:
167
+ rel_path = file_info.path.relative_to(base_path)
168
+ checked = rel_path in st.session_state.selected_files
169
+
170
+ new_checked = st.checkbox(
171
+ f"{rel_path} ({file_info.formatted_size})",
172
+ value=checked,
173
+ key=str(rel_path) # keyの重複回避
174
+ )
175
+ if new_checked:
176
+ st.session_state.selected_files.add(rel_path)
177
+ else:
178
+ st.session_state.selected_files.discard(rel_path)
179
+
180
+
181
+ # =====================================
182
+ # 選択ファイルをまとめてMarkdown化 & ダウンロード
183
+ # =====================================
184
+ def create_markdown_for_selected(files, selected_paths, base_dir: Path) -> str:
185
+ output = []
186
+ for f in files:
187
+ rel_path = f.path.relative_to(base_dir)
188
+ if rel_path in selected_paths:
189
+ output.append(f"## {rel_path}")
190
+ output.append("------------")
191
+ if f.content is not None:
192
+ output.append(f.content)
193
+ else:
194
+ output.append("# Failed to read content")
195
+ output.append("") # 空行
196
+ return "\n".join(output)
197
+
198
+ if st.session_state.scanned_files:
199
+ st.write("## 選択ファイルをダウンロード")
200
+ if st.button("選択ファイルをMarkdownとしてダウンロード(整形後,下にダウンロードボタンが出ます)"):
201
+ base_path = Path(st.session_state.cloned_repo_dir)
202
+ markdown_text = create_markdown_for_selected(
203
+ st.session_state.scanned_files,
204
+ st.session_state.selected_files,
205
+ base_path
206
+ )
207
+ st.download_button(
208
+ label="Markdownダウンロード",
209
+ data=markdown_text,
210
+ file_name="selected_files.md",
211
+ mime="text/markdown"
212
  )