socratic-debug 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- socratic.py +8 -0
- socratic_debug-0.0.1.dist-info/METADATA +143 -0
- socratic_debug-0.0.1.dist-info/RECORD +40 -0
- socratic_debug-0.0.1.dist-info/WHEEL +5 -0
- socratic_debug-0.0.1.dist-info/entry_points.txt +2 -0
- socratic_debug-0.0.1.dist-info/licenses/LICENSE +21 -0
- socratic_debug-0.0.1.dist-info/top_level.txt +3 -0
- src/__init__.py +1 -0
- src/agent/graph/graph.py +47 -0
- src/agent/llm/factory.py +15 -0
- src/agent/llm/providers/__init__.py +8 -0
- src/agent/llm/providers/ollama.py +13 -0
- src/agent/llm/providers/openrouter.py +14 -0
- src/agent/nodes/diagnose_node.py +21 -0
- src/agent/nodes/evaluate_node.py +38 -0
- src/agent/nodes/human_input_node.py +18 -0
- src/agent/nodes/questioner_node.py +47 -0
- src/agent/nodes/solution_node.py +36 -0
- src/agent/prompts/diagnose_prompt.py +34 -0
- src/agent/prompts/evaluation_prompt.py +33 -0
- src/agent/prompts/mentor_prompt.py +30 -0
- src/agent/prompts/questioner_prompt.py +47 -0
- src/agent/prompts/solution_prompt.py +38 -0
- src/agent/runner.py +21 -0
- src/agent/state/state.py +21 -0
- src/cli.py +98 -0
- src/config_creator.py +32 -0
- src/config_template.py +21 -0
- src/extractor.py +34 -0
- src/hook.py +67 -0
- src/utils/read_yaml.py +36 -0
- ui/__init__.py +15 -0
- ui/app.py +203 -0
- ui/bidi.py +23 -0
- ui/console.py +23 -0
- ui/styles.py +63 -0
- ui/templates.py +102 -0
- ui/translations.py +31 -0
- ui/views.py +52 -0
- ui/window.py +140 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class SolutionPrompt:
|
|
2
|
+
@staticmethod
|
|
3
|
+
def solution_prompt(
|
|
4
|
+
error_type: str,
|
|
5
|
+
line_number: int,
|
|
6
|
+
code_snippet: str,
|
|
7
|
+
root_cause: str,
|
|
8
|
+
eval_status: str = "solved",
|
|
9
|
+
language: str = "English",
|
|
10
|
+
style: str = "friendly",
|
|
11
|
+
) -> str:
|
|
12
|
+
return f"""You are a Socratic Coding Mentor wrapping up a debugging session.
|
|
13
|
+
|
|
14
|
+
Session Context:
|
|
15
|
+
- Language: Respond strictly in {language}.
|
|
16
|
+
- Style/Tone: {style}.
|
|
17
|
+
- Final Status: {eval_status.upper()} (The developer {'successfully understood and solved the bug' if eval_status == 'solved' else 'struggled or surrendered'}).
|
|
18
|
+
- Error Type: {error_type}
|
|
19
|
+
- Line #{line_number}
|
|
20
|
+
- Known Root Cause:
|
|
21
|
+
{root_cause}
|
|
22
|
+
|
|
23
|
+
Original Code Context:
|
|
24
|
+
```python
|
|
25
|
+
{code_snippet}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Instructions:
|
|
29
|
+
1. If status is SOLVED:
|
|
30
|
+
- Enthusiastically praise the developer for discovering and understanding the bug themselves.
|
|
31
|
+
- Summarize the core takeaway lesson in one memorable sentence.
|
|
32
|
+
2. If status is SURRENDERED:
|
|
33
|
+
- Be empathetic, encouraging, and clear.
|
|
34
|
+
- Concisely explain the concept so they understand why it happened.
|
|
35
|
+
3. IN BOTH CASES:
|
|
36
|
+
- Provide a clean, idiomatic Python code snippet demonstrating the "Best Practice" or defensive programming way to prevent this bug (e.g., using default values, proper guards, or cleaner logic).
|
|
37
|
+
- Keep the entire response concise and under 250 words.
|
|
38
|
+
"""
|
src/agent/runner.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from src.extractor import extract_crash_context
|
|
2
|
+
from src.agent.graph.graph import SocraticGraph
|
|
3
|
+
|
|
4
|
+
def run_socratic_agent(exception_type, exception_value, exception_traceback):
|
|
5
|
+
context = extract_crash_context(exception_traceback)
|
|
6
|
+
|
|
7
|
+
initial_state = {
|
|
8
|
+
"file_path": context["file_path"],
|
|
9
|
+
"function_name": context["function_name"],
|
|
10
|
+
"line_number": context["line_number"],
|
|
11
|
+
"line_content": context["line_content"],
|
|
12
|
+
"code_snippet": context["code_snippet"],
|
|
13
|
+
"error_type": exception_type.__name__,
|
|
14
|
+
"error_name": str(exception_value),
|
|
15
|
+
"root_cause": "",
|
|
16
|
+
"dialogue_history": [],
|
|
17
|
+
"is_solved": False,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
app = SocraticGraph.build_graph()
|
|
21
|
+
app.invoke(initial_state)
|
src/agent/state/state.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import TypedDict, List, Dict, Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SocraticAgentState(TypedDict):
|
|
5
|
+
# data about the problem
|
|
6
|
+
file_path: str
|
|
7
|
+
function_name: Optional[str]
|
|
8
|
+
line_number: int
|
|
9
|
+
line_content: str
|
|
10
|
+
code_snippet: str
|
|
11
|
+
error_type: str
|
|
12
|
+
error_name: str
|
|
13
|
+
|
|
14
|
+
# root cause of the problem
|
|
15
|
+
root_cause: str
|
|
16
|
+
|
|
17
|
+
# chat with user
|
|
18
|
+
dialogue_history: List[Dict[str, str]]
|
|
19
|
+
|
|
20
|
+
# dose problem is solved or not !
|
|
21
|
+
is_solved: bool
|
src/cli.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Add project root to sys.path so modules and UI resolve everywhere
|
|
6
|
+
project_root = str(Path(__file__).resolve().parent.parent)
|
|
7
|
+
if project_root not in sys.path:
|
|
8
|
+
sys.path.insert(0, project_root)
|
|
9
|
+
|
|
10
|
+
from src.hook import install
|
|
11
|
+
from ui.console import console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def print_usage():
|
|
15
|
+
console.print("""
|
|
16
|
+
[bold cyan]🧠 Socratic Debug — Autonomous Pedagogical Debugger[/bold cyan]
|
|
17
|
+
|
|
18
|
+
[bold yellow]Usage:[/bold yellow]
|
|
19
|
+
[green]socratic run <script.py>[/green] Run a Python file under Socratic supervision
|
|
20
|
+
[green]socratic init[/green] Create a default .socratic.yaml config file
|
|
21
|
+
[green]socratic --help[/green] Show this help message
|
|
22
|
+
""")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_target_script(script_path: str, script_args: list):
|
|
26
|
+
resolved_path = Path(script_path).resolve()
|
|
27
|
+
if not resolved_path.exists():
|
|
28
|
+
console.print(f"[bold red]Error:[/bold red] File not found: [yellow]{resolved_path}[/yellow]")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
# Automatically install Socratic hook before user code executes
|
|
32
|
+
install()
|
|
33
|
+
|
|
34
|
+
# Prepare execution environment simulating direct `python <script.py>`
|
|
35
|
+
sys.argv = [str(resolved_path)] + script_args
|
|
36
|
+
sys.path.insert(0, str(resolved_path.parent))
|
|
37
|
+
|
|
38
|
+
# Read and execute target code in its own module namespace
|
|
39
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
40
|
+
code_str = f.read()
|
|
41
|
+
|
|
42
|
+
namespace = {
|
|
43
|
+
"__name__": "__main__",
|
|
44
|
+
"__file__": str(resolved_path),
|
|
45
|
+
"__doc__": None,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
exec(compile(code_str, str(resolved_path), "exec"), namespace)
|
|
50
|
+
except SystemExit:
|
|
51
|
+
raise
|
|
52
|
+
except Exception:
|
|
53
|
+
# sys.excepthook will catch this cleanly and launch Socratic GUI!
|
|
54
|
+
sys.excepthook(*sys.exc_info())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
if len(sys.argv) < 2:
|
|
59
|
+
print_usage()
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
command = sys.argv[1].lower()
|
|
63
|
+
|
|
64
|
+
if command in ("-h", "--help", "help"):
|
|
65
|
+
print_usage()
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
elif command == "init":
|
|
69
|
+
from src.config_creator import create_config
|
|
70
|
+
create_config()
|
|
71
|
+
console.print("[bold green]✔[/bold green] Created [cyan].socratic.yaml[/cyan] with default settings.")
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
|
|
74
|
+
elif command == "session":
|
|
75
|
+
# Internal command executed inside the dedicated popup window
|
|
76
|
+
target_file = sys.argv[2]
|
|
77
|
+
extra_args = sys.argv[3:]
|
|
78
|
+
run_target_script(target_file, extra_args, force_in_process=True)
|
|
79
|
+
|
|
80
|
+
elif command == "run":
|
|
81
|
+
if len(sys.argv) < 3:
|
|
82
|
+
console.print("[bold red]Error:[/bold red] Missing script file to run.")
|
|
83
|
+
console.print("Example: [green]socratic run app.py[/green]")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
target_file = sys.argv[2]
|
|
87
|
+
extra_args = sys.argv[3:]
|
|
88
|
+
run_target_script(target_file, extra_args)
|
|
89
|
+
|
|
90
|
+
else:
|
|
91
|
+
# Default fallback: if user runs `socratic script.py` directly
|
|
92
|
+
target_file = sys.argv[1]
|
|
93
|
+
extra_args = sys.argv[2:]
|
|
94
|
+
run_target_script(target_file, extra_args)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|
src/config_creator.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from src.config_template import ConfigTemplate
|
|
3
|
+
except ImportError:
|
|
4
|
+
from config_template import ConfigTemplate
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
9
|
+
|
|
10
|
+
# make the socratic.config file
|
|
11
|
+
def create_config():
|
|
12
|
+
config_path = PROJECT_ROOT / ".socratic.yaml"
|
|
13
|
+
|
|
14
|
+
if config_path.exists():
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
with open(config_path, "w") as f:
|
|
18
|
+
f.write(ConfigTemplate.DEFAULT_CONFIG_TEMPLATE)
|
|
19
|
+
|
|
20
|
+
# read the socratic.config file and return the values
|
|
21
|
+
def read_config() -> dict:
|
|
22
|
+
config_path = PROJECT_ROOT / ".socratic.yaml"
|
|
23
|
+
|
|
24
|
+
if not config_path.exists():
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
with open(config_path, "r") as f:
|
|
28
|
+
return yaml.safe_load(f) or {}
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
create_config()
|
|
32
|
+
print("Config created!")
|
src/config_template.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class ConfigTemplate:
|
|
2
|
+
DEFAULT_CONFIG_TEMPLATE = """# Socratic Configuration
|
|
3
|
+
# in this file you can configure the socratic debugging tool
|
|
4
|
+
|
|
5
|
+
# LLM Configurations
|
|
6
|
+
provider: "ollama"
|
|
7
|
+
provider_name: "gemma4:31b-cloud"
|
|
8
|
+
provider_link: "http://localhost:11434"
|
|
9
|
+
|
|
10
|
+
# API Key (the variable name must named `api_key`)
|
|
11
|
+
api_key: "your_api_key"
|
|
12
|
+
|
|
13
|
+
# Socratic Mentor configuration
|
|
14
|
+
socratic_language: "English"
|
|
15
|
+
socratic_style: "friendly"
|
|
16
|
+
|
|
17
|
+
# Observability
|
|
18
|
+
LANGSMITH_TRACING: "true"
|
|
19
|
+
LANGSMITH_API_KEY: "your_langsmith_api_key"
|
|
20
|
+
LANGSMITH_PROJECT: "socratic-debug"
|
|
21
|
+
"""
|
src/extractor.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
3
|
+
def extract_crash_context(exception_traceback) -> dict:
|
|
4
|
+
frames = traceback.extract_tb(exception_traceback)
|
|
5
|
+
last_frame = frames[-1]
|
|
6
|
+
|
|
7
|
+
snippet = get_code_snippet(last_frame.filename, last_frame.lineno)
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
"file_path": last_frame.filename,
|
|
11
|
+
"line_number": last_frame.lineno,
|
|
12
|
+
"function_name": last_frame.name,
|
|
13
|
+
"line_content": last_frame.line,
|
|
14
|
+
"code_snippet": snippet
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def get_code_snippet(file_path: str, line_number: int, context_lines: int = 5) -> str:
|
|
18
|
+
try:
|
|
19
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
20
|
+
lines = f.readlines()
|
|
21
|
+
|
|
22
|
+
start_lines = max(1, line_number - context_lines)
|
|
23
|
+
end_lines = min(len(lines), line_number + context_lines)
|
|
24
|
+
|
|
25
|
+
snippet_lines = []
|
|
26
|
+
for i in range(start_lines, end_lines + 1):
|
|
27
|
+
prefix = ">" if i == line_number else " "
|
|
28
|
+
line_text = lines[i - 1]
|
|
29
|
+
snippet_lines.append(f"{prefix} {i:3d} | {line_text}")
|
|
30
|
+
|
|
31
|
+
return "".join(snippet_lines)
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return f"Error getting code snippet: {e}"
|
src/hook.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
from src.extractor import extract_crash_context
|
|
5
|
+
from src.config_creator import create_config
|
|
6
|
+
from src.agent.runner import run_socratic_agent
|
|
7
|
+
from ui.views import SocraticUI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def socratic_excepthook(exception_type, exception_value, exception_traceback):
|
|
11
|
+
"""Intercepts uncaught exceptions and launches the Socratic GUI Terminal window."""
|
|
12
|
+
context = extract_crash_context(exception_traceback)
|
|
13
|
+
|
|
14
|
+
# Check if graphical desktop display is available
|
|
15
|
+
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") or sys.platform == "win32")
|
|
16
|
+
|
|
17
|
+
if has_display:
|
|
18
|
+
try:
|
|
19
|
+
from PyQt6.QtWidgets import QApplication
|
|
20
|
+
from ui.app import SocraticWindow
|
|
21
|
+
|
|
22
|
+
os.environ["SOCRATIC_GUI_ACTIVE"] = "1"
|
|
23
|
+
|
|
24
|
+
app = QApplication.instance()
|
|
25
|
+
if app is None:
|
|
26
|
+
app = QApplication(sys.argv)
|
|
27
|
+
|
|
28
|
+
# Create and display the centered Socratic Terminal window
|
|
29
|
+
window = SocraticWindow(
|
|
30
|
+
error_type=exception_type.__name__,
|
|
31
|
+
error_message=str(exception_value),
|
|
32
|
+
file_path=context["file_path"],
|
|
33
|
+
line_number=context["line_number"],
|
|
34
|
+
line_content=context["line_content"],
|
|
35
|
+
)
|
|
36
|
+
window.show()
|
|
37
|
+
|
|
38
|
+
# Run LangGraph State Machine in background worker thread
|
|
39
|
+
agent_thread = threading.Thread(
|
|
40
|
+
target=run_socratic_agent,
|
|
41
|
+
args=(exception_type, exception_value, exception_traceback),
|
|
42
|
+
daemon=True,
|
|
43
|
+
)
|
|
44
|
+
agent_thread.start()
|
|
45
|
+
|
|
46
|
+
# Run Qt GUI event loop
|
|
47
|
+
app.exec()
|
|
48
|
+
return
|
|
49
|
+
except Exception as e:
|
|
50
|
+
# Fallback to terminal mode if GUI setup fails
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# Headless / Terminal Fallback Mode
|
|
54
|
+
os.environ["SOCRATIC_GUI_ACTIVE"] = "0"
|
|
55
|
+
SocraticUI.display_crash_banner(
|
|
56
|
+
error_type=exception_type.__name__,
|
|
57
|
+
error_message=str(exception_value),
|
|
58
|
+
file_path=context["file_path"],
|
|
59
|
+
line_number=context["line_number"],
|
|
60
|
+
line_content=context["line_content"],
|
|
61
|
+
)
|
|
62
|
+
run_socratic_agent(exception_type, exception_value, exception_traceback)
|
|
63
|
+
|
|
64
|
+
# use the hook
|
|
65
|
+
def install():
|
|
66
|
+
create_config()
|
|
67
|
+
sys.excepthook = socratic_excepthook
|
src/utils/read_yaml.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from src.config_creator import create_config
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import yaml
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReadYaml:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def get_config_path() -> Path:
|
|
10
|
+
# 1. First check in current working directory
|
|
11
|
+
cwd_file = Path.cwd() / ".socratic.yaml"
|
|
12
|
+
if cwd_file.exists():
|
|
13
|
+
return cwd_file
|
|
14
|
+
|
|
15
|
+
# 2. Check in project root
|
|
16
|
+
project_root = Path(__file__).resolve().parent.parent.parent
|
|
17
|
+
root_file = project_root / ".socratic.yaml"
|
|
18
|
+
if root_file.exists():
|
|
19
|
+
return root_file
|
|
20
|
+
|
|
21
|
+
# 3. Fallback: auto-create in project root and return
|
|
22
|
+
create_config()
|
|
23
|
+
return root_file if root_file.exists() else cwd_file
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def read_yaml() -> dict:
|
|
27
|
+
config_path = ReadYaml.get_config_path()
|
|
28
|
+
try:
|
|
29
|
+
with open(config_path, "r", encoding="utf-8") as file:
|
|
30
|
+
data = yaml.safe_load(file) or {}
|
|
31
|
+
for key in ["LANGSMITH_TRACING", "LANGSMITH_API_KEY", "LANGSMITH_PROJECT"]:
|
|
32
|
+
if data.get(key):
|
|
33
|
+
os.environ[key] = str(data[key])
|
|
34
|
+
return data
|
|
35
|
+
except Exception:
|
|
36
|
+
return {}
|
ui/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ui.app import SocraticWindow, bridge, wait_for_gui_user_input
|
|
2
|
+
from ui.views import SocraticUI
|
|
3
|
+
from ui.translations import TRANSLATIONS
|
|
4
|
+
from ui.styles import SOCRATIC_DARK_THEME
|
|
5
|
+
from ui.templates import ChatTemplates
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SocraticWindow",
|
|
9
|
+
"SocraticUI",
|
|
10
|
+
"bridge",
|
|
11
|
+
"wait_for_gui_user_input",
|
|
12
|
+
"TRANSLATIONS",
|
|
13
|
+
"SOCRATIC_DARK_THEME",
|
|
14
|
+
"ChatTemplates",
|
|
15
|
+
]
|
ui/app.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import threading
|
|
3
|
+
from PyQt6.QtWidgets import (
|
|
4
|
+
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
5
|
+
QTextEdit, QLineEdit, QPushButton, QLabel, QFrame
|
|
6
|
+
)
|
|
7
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QObject
|
|
8
|
+
from PyQt6.QtGui import QTextCursor
|
|
9
|
+
|
|
10
|
+
from src.utils.read_yaml import ReadYaml
|
|
11
|
+
from ui.translations import TRANSLATIONS
|
|
12
|
+
from ui.styles import SOCRATIC_DARK_THEME
|
|
13
|
+
from ui.templates import ChatTemplates
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UIBridge(QObject):
|
|
17
|
+
"""Bridge to pass messages from background agent threads to the Qt GUI thread."""
|
|
18
|
+
mentor_message_received = pyqtSignal(str, int)
|
|
19
|
+
user_input_requested = pyqtSignal()
|
|
20
|
+
solution_received = pyqtSignal(str)
|
|
21
|
+
evaluator_verdict_received = pyqtSignal(str)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
bridge = UIBridge()
|
|
25
|
+
_user_reply_event = threading.Event()
|
|
26
|
+
_user_reply_text = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def wait_for_gui_user_input() -> str:
|
|
30
|
+
_user_reply_event.clear()
|
|
31
|
+
bridge.user_input_requested.emit()
|
|
32
|
+
_user_reply_event.wait()
|
|
33
|
+
return _user_reply_text
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def submit_gui_user_input(text: str):
|
|
37
|
+
global _user_reply_text
|
|
38
|
+
_user_reply_text = text
|
|
39
|
+
_user_reply_event.set()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SocraticWindow(QMainWindow):
|
|
43
|
+
"""Pure, clean, fully-localized Socratic Terminal window."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, error_type: str, error_message: str, file_path: str, line_number: int, line_content: str):
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
# Load language setting from .socratic.yaml
|
|
49
|
+
config = ReadYaml.read_yaml()
|
|
50
|
+
lang_setting = config.get("socratic_language", "English").strip().lower()
|
|
51
|
+
self.is_arabic = "arab" in lang_setting
|
|
52
|
+
self.translations = TRANSLATIONS["arabic"] if self.is_arabic else TRANSLATIONS["english"]
|
|
53
|
+
|
|
54
|
+
self.setWindowTitle(self.translations["app_title"])
|
|
55
|
+
self.resize(880, 640)
|
|
56
|
+
|
|
57
|
+
# Dynamic RTL / LTR layout
|
|
58
|
+
self.setLayoutDirection(Qt.LayoutDirection.RightToLeft if self.is_arabic else Qt.LayoutDirection.LeftToRight)
|
|
59
|
+
|
|
60
|
+
self._center_on_screen()
|
|
61
|
+
self.setStyleSheet(SOCRATIC_DARK_THEME)
|
|
62
|
+
self._build_ui(error_type, error_message, file_path, line_number, line_content)
|
|
63
|
+
self._connect_signals()
|
|
64
|
+
|
|
65
|
+
def _center_on_screen(self):
|
|
66
|
+
screen = QApplication.primaryScreen()
|
|
67
|
+
if screen:
|
|
68
|
+
screen_geom = screen.availableGeometry()
|
|
69
|
+
x = (screen_geom.width() - self.width()) // 2
|
|
70
|
+
y = (screen_geom.height() - self.height()) // 2
|
|
71
|
+
self.move(x, y)
|
|
72
|
+
|
|
73
|
+
def _build_ui(self, error_type: str, error_message: str, file_path: str, line_number: int, line_content: str):
|
|
74
|
+
central_widget = QWidget(self)
|
|
75
|
+
self.setCentralWidget(central_widget)
|
|
76
|
+
main_layout = QVBoxLayout(central_widget)
|
|
77
|
+
main_layout.setContentsMargins(18, 16, 18, 16)
|
|
78
|
+
main_layout.setSpacing(12)
|
|
79
|
+
|
|
80
|
+
# Header Bar
|
|
81
|
+
top_bar = QHBoxLayout()
|
|
82
|
+
header_label = QLabel(self.translations["header_title"], self)
|
|
83
|
+
header_label.setObjectName("headerTitle")
|
|
84
|
+
|
|
85
|
+
self.status_badge = QLabel(self.translations["status_active"], self)
|
|
86
|
+
self.status_badge.setObjectName("statusBadge")
|
|
87
|
+
self.status_badge.setStyleSheet("color: #3fb950; background-color: #162a1c;")
|
|
88
|
+
|
|
89
|
+
top_bar.addWidget(header_label)
|
|
90
|
+
top_bar.addStretch()
|
|
91
|
+
top_bar.addWidget(self.status_badge)
|
|
92
|
+
main_layout.addLayout(top_bar)
|
|
93
|
+
|
|
94
|
+
# Crash Summary Box
|
|
95
|
+
crash_frame = QFrame(self)
|
|
96
|
+
crash_frame.setObjectName("crashBanner")
|
|
97
|
+
crash_layout = QVBoxLayout(crash_frame)
|
|
98
|
+
crash_layout.setContentsMargins(12, 10, 12, 10)
|
|
99
|
+
crash_layout.setSpacing(4)
|
|
100
|
+
|
|
101
|
+
crash_title = QLabel(f"💥 <b>{error_type}</b>: {error_message}", crash_frame)
|
|
102
|
+
crash_title.setStyleSheet("color: #ff7b72; font-size: 15px;")
|
|
103
|
+
crash_title.setTextFormat(Qt.TextFormat.RichText)
|
|
104
|
+
|
|
105
|
+
loc_text = self.translations["crash_location"]
|
|
106
|
+
line_text = self.translations["crashing_line"]
|
|
107
|
+
crash_info = QLabel(
|
|
108
|
+
f"📍 {loc_text}: <i>{file_path}:{line_number}</i> | ⚡ {line_text}: <code style='color: #ffffff; background: #2b1d20; padding: 2px 6px;'>{line_content.strip()}</code>",
|
|
109
|
+
crash_frame
|
|
110
|
+
)
|
|
111
|
+
crash_info.setStyleSheet("color: #8b949e; font-size: 13px;")
|
|
112
|
+
crash_info.setTextFormat(Qt.TextFormat.RichText)
|
|
113
|
+
|
|
114
|
+
crash_layout.addWidget(crash_title)
|
|
115
|
+
crash_layout.addWidget(crash_info)
|
|
116
|
+
main_layout.addWidget(crash_frame)
|
|
117
|
+
|
|
118
|
+
# Chat Feed Area
|
|
119
|
+
self.chat_area = QTextEdit(self)
|
|
120
|
+
self.chat_area.setObjectName("chatArea")
|
|
121
|
+
self.chat_area.setReadOnly(True)
|
|
122
|
+
main_layout.addWidget(self.chat_area, stretch=1)
|
|
123
|
+
|
|
124
|
+
# Bottom Input Area
|
|
125
|
+
input_layout = QHBoxLayout()
|
|
126
|
+
input_layout.setSpacing(10)
|
|
127
|
+
|
|
128
|
+
self.input_field = QLineEdit(self)
|
|
129
|
+
self.input_field.setObjectName("inputField")
|
|
130
|
+
self.input_field.setPlaceholderText(self.translations["placeholder"])
|
|
131
|
+
self.input_field.returnPressed.connect(self._on_send_clicked)
|
|
132
|
+
self.input_field.setEnabled(False)
|
|
133
|
+
|
|
134
|
+
self.send_button = QPushButton(self.translations["send_btn"], self)
|
|
135
|
+
self.send_button.setObjectName("sendButton")
|
|
136
|
+
self.send_button.clicked.connect(self._on_send_clicked)
|
|
137
|
+
self.send_button.setEnabled(False)
|
|
138
|
+
|
|
139
|
+
input_layout.addWidget(self.input_field, stretch=1)
|
|
140
|
+
input_layout.addWidget(self.send_button)
|
|
141
|
+
main_layout.addLayout(input_layout)
|
|
142
|
+
|
|
143
|
+
def _connect_signals(self):
|
|
144
|
+
bridge.mentor_message_received.connect(self._append_mentor_message)
|
|
145
|
+
bridge.user_input_requested.connect(self._enable_user_input)
|
|
146
|
+
bridge.solution_received.connect(self._append_solution)
|
|
147
|
+
bridge.evaluator_verdict_received.connect(self._append_verdict)
|
|
148
|
+
|
|
149
|
+
def _append_mentor_message(self, content: str, hint_level: int):
|
|
150
|
+
html_msg = ChatTemplates.mentor_bubble(
|
|
151
|
+
content=content,
|
|
152
|
+
hint_level=hint_level,
|
|
153
|
+
mentor_name=self.translations["mentor_tag"],
|
|
154
|
+
hint_label=self.translations["hint_level"],
|
|
155
|
+
is_arabic=self.is_arabic,
|
|
156
|
+
)
|
|
157
|
+
self.chat_area.append(html_msg)
|
|
158
|
+
self._scroll_to_bottom()
|
|
159
|
+
|
|
160
|
+
def _enable_user_input(self):
|
|
161
|
+
self.input_field.setEnabled(True)
|
|
162
|
+
self.send_button.setEnabled(True)
|
|
163
|
+
self.input_field.setFocus()
|
|
164
|
+
|
|
165
|
+
def _on_send_clicked(self):
|
|
166
|
+
text = self.input_field.text().strip()
|
|
167
|
+
if not text:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self.input_field.clear()
|
|
171
|
+
self.input_field.setEnabled(False)
|
|
172
|
+
self.send_button.setEnabled(False)
|
|
173
|
+
|
|
174
|
+
user_html = ChatTemplates.user_bubble(
|
|
175
|
+
text=text,
|
|
176
|
+
user_tag=self.translations["user_tag"],
|
|
177
|
+
is_arabic=self.is_arabic,
|
|
178
|
+
)
|
|
179
|
+
self.chat_area.append(user_html)
|
|
180
|
+
self._scroll_to_bottom()
|
|
181
|
+
|
|
182
|
+
submit_gui_user_input(text)
|
|
183
|
+
|
|
184
|
+
def _append_verdict(self, verdict: str):
|
|
185
|
+
badge_html = ChatTemplates.verdict_badge(verdict)
|
|
186
|
+
self.chat_area.append(badge_html)
|
|
187
|
+
self._scroll_to_bottom()
|
|
188
|
+
|
|
189
|
+
def _append_solution(self, content: str):
|
|
190
|
+
self.status_badge.setText(self.translations["status_solved"])
|
|
191
|
+
self.status_badge.setStyleSheet("color: #ffffff; background-color: #238636;")
|
|
192
|
+
|
|
193
|
+
sol_html = ChatTemplates.solution_bubble(
|
|
194
|
+
content=content,
|
|
195
|
+
wrapup_title=self.translations["wrapup_title"],
|
|
196
|
+
)
|
|
197
|
+
self.chat_area.append(sol_html)
|
|
198
|
+
self._scroll_to_bottom()
|
|
199
|
+
|
|
200
|
+
def _scroll_to_bottom(self):
|
|
201
|
+
cursor = self.chat_area.textCursor()
|
|
202
|
+
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
203
|
+
self.chat_area.setTextCursor(cursor)
|
ui/bidi.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class BidiHelper:
|
|
2
|
+
"""Helper to detect text direction for Socratic views."""
|
|
3
|
+
|
|
4
|
+
@staticmethod
|
|
5
|
+
def is_rtl_text(text: str) -> bool:
|
|
6
|
+
"""Determines whether text is predominantly Arabic / RTL."""
|
|
7
|
+
if not text:
|
|
8
|
+
return False
|
|
9
|
+
for char in text:
|
|
10
|
+
code = ord(char)
|
|
11
|
+
# Arabic unicode ranges
|
|
12
|
+
if 0x0600 <= code <= 0x06FF or 0x0750 <= code <= 0x077F or 0x08A0 <= code <= 0x08FF:
|
|
13
|
+
return True
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def format_text(text: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Modern Linux terminal emulators (GNOME Terminal, VTE, Kitty) have native
|
|
20
|
+
HarfBuzz shaping and BiDi engines. Passing raw Unicode preserves perfect
|
|
21
|
+
ligatures and prevents double-reversing artifacts.
|
|
22
|
+
"""
|
|
23
|
+
return text
|
ui/console.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.theme import Theme
|
|
4
|
+
|
|
5
|
+
# Modern Dev Theme with sleek accents
|
|
6
|
+
custom_theme = Theme({
|
|
7
|
+
"mentor": "bold cyan",
|
|
8
|
+
"mentor.box": "cyan",
|
|
9
|
+
"crash": "bold red",
|
|
10
|
+
"crash.box": "red",
|
|
11
|
+
"code": "bright_white on #1e1e2e",
|
|
12
|
+
"user": "bold yellow",
|
|
13
|
+
"success": "bold green",
|
|
14
|
+
"success.box": "green",
|
|
15
|
+
"subtle": "dim white",
|
|
16
|
+
"badge": "bold black on bright_cyan",
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
# Global rich console instance for socratic (adapts dynamically to window dimensions)
|
|
20
|
+
console = Console(
|
|
21
|
+
theme=custom_theme,
|
|
22
|
+
highlight=False,
|
|
23
|
+
)
|