Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Get ready in just a few steps:
* **19 AI providers supported:** Use local models (via Ollama), Claude, ChatGPT, Gemini, DeepSeek, Groq, and many more (with over 40 presets).
* **Run directly in FreeCAD:** F5 runs the entire macro, F9 runs only the selection. Pressing again aborts execution.
* **Generate macros from plain language (FC11–FC14):** Describe your macro in natural language and see the result immediately in an embedded **👁 3D preview**.
* **Whole project as context:** Pick a project folder and the AI gets a compact map of all folders and files — and pulls in individual files by itself when it needs them.
* **Unified error panel:** Translate errors to your preferred language in place, have the AI explain them, or auto-fix them.
* **🎤 Voice control (offline):** Navigate panels/actions and dictate text, fully **offline** (via Vosk), without sending data to external providers.
* **Flexible UI:** 10 freely arrangeable dock panels, interactive assistant, and a built-in searchable help.
Expand Down
15 changes: 15 additions & 0 deletions core/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ def lade_kontext() -> str:
def speichere_kontext(text: str):
App.ParamGet(PREF_KEY).SetString("ProjektKontext", text)

# ── Projektordner: ganzer Ordnerbaum als KI-Kontext ────────────────────────

def lade_projektordner() -> str:
return App.ParamGet(PREF_KEY).GetString("ProjektOrdner", "")

def speichere_projektordner(pfad: str) -> None:
App.ParamGet(PREF_KEY).SetString("ProjektOrdner", pfad or "")

def lade_projektkarte_aktiv() -> bool:
"""Ob die Projektkarte bei jedem KI-Aufruf mitgeschickt wird."""
return App.ParamGet(PREF_KEY).GetBool("ProjektKarteAktiv", True)

def speichere_projektkarte_aktiv(v: bool) -> None:
App.ParamGet(PREF_KEY).SetBool("ProjektKarteAktiv", bool(v))

_KEY_NAMES = {
"anthropic": "ApiKeyAnthropic",
"openai": "ApiKeyOpenAI",
Expand Down
26 changes: 26 additions & 0 deletions data/hilfe_texte.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,32 @@
mit dem Fehler — dort 🔍 Übersetzen, 🐛 KI erklärt
oder 🔧 KI korrigieren.

Projektordner (ganzer Ordnerbaum als Kontext):
🤖 KI-Panel → ▶ 📌 Projekt-Kontext aufklappen
→ 📁 Wählen … → Ordner deines Projekts auswählen

Mitgeschickt wird nur eine ÜBERSICHT, kein Quelltext:
der Ordnerbaum plus alle Klassen und Funktionen der
Python-Dateien. Das kostet meist nur ein paar hundert
Token, egal wie groß das Projekt ist.
Bei Ollama wird automatisch die sparsame Variante
(nur Ordnerbaum) verwendet.

Braucht die KI den Inhalt einer Datei, fordert sie ihn
selbst an — die Datei wird automatisch nachgeladen und
die Anfrage läuft weiter (max. 2 Runden pro Frage).
Im Statusfeld steht dann „📂 Lade nach …".

Der Haken „Projektübersicht bei jeder Anfrage
mitschicken" schaltet das Ganze ab, ohne dass du den
Ordner verlierst. ✕ entfernt den Ordner.

Ausgelassen werden: __pycache__, .git, node_modules,
venv, build, dist sowie Dateien über 200 KB und alles,
was kein Textformat ist.
Es wird ausschließlich gelesen — nie geschrieben, und
nie außerhalb des gewählten Ordners.

Chat-Verlauf:
Bleibt zwischen Fragen erhalten (Folgefragen möglich).
Ab 5.000 Zeichen wird der älteste Teil automatisch
Expand Down
36 changes: 36 additions & 0 deletions docs/ki-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,42 @@ can 🔍 Translate, 🐛 have the AI explain it, or 🔧 AI-fix it.
```
On the next FreeCAD start, simply load the `.json` file and continue seamlessly.

## Project folder as context
By default the AI only sees the file you have open. To give it a whole project — several
folders and files — pick a project folder:

```
🤖 AI panel → ▶ 📌 Project context → 📁 Choose …
```

What is sent is a **map, not source code**: the folder tree plus every class and function of
the Python files in it. This usually costs only a few hundred tokens no matter how large the
project is. For Ollama the frugal variant (folder tree only) is used automatically, so the
small context window of local models is not blown.

When the AI needs the actual content of a file, it asks for it itself:

```
#DATEI: core/params.py → the file is loaded and the request continues
#SUCHE: Placement → matches across the project are supplied
```

The file is fetched automatically and the request re-runs with that content (max. 2 rounds per
question, max. 3 files per round). The status bar shows `📂 Lade nach …` while this happens.
Because this uses plain text markers instead of a provider-specific tool API, it works with all
19 providers — including local Ollama models without tool support.

The same three operations are also available as manual buttons in the **🔧 Tools panel**
(`projekt_dateien_auflisten`, `projekt_datei_lesen`, `projekt_suchen`).

| Detail | Behaviour |
|--------|-----------|
| Checkbox "Projektübersicht … mitschicken" | Turns the map off without losing the folder |
| ✕ button | Clears the project folder |
| Skipped | `__pycache__`, `.git`, `node_modules`, `venv`, `build`, `dist`, files > 200 KB, non-text files |
| Limits | 400 files, 12,000 characters of map |
| Access | Read-only, and never outside the chosen folder |

## Using the chat history
The chat history is kept between questions. Follow-up questions build on previous answers.
After 5,000 characters the oldest part is automatically compressed (summarised).
Expand Down
76 changes: 76 additions & 0 deletions editor/builders/dock_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
lade_auto_einfuegen, speichere_auto_einfuegen,
lade_thinking_modus, speichere_thinking_modus,
lade_plan_modus, speichere_plan_modus,
lade_projektordner, speichere_projektordner,
lade_projektkarte_aktiv, speichere_projektkarte_aktiv,
lade_api_key, SYSTEM_PROMPT_VORLAGEN)
from ui.barrierefreiheit import BarrierefreiheitPanel
from editor.panel import FreecadHelferPanel
Expand Down Expand Up @@ -608,6 +610,80 @@ def _mindesthoehe(widget, zeilen: int) -> None:
lambda: speichere_kontext(editor._kontext.toPlainText()))
_kontext_sek.addWidget(editor._kontext)

# ── Projektordner: ganzer Ordnerbaum als KI-Kontext ────────────────────
_ordner_zeile = QtWidgets.QHBoxLayout()
_ordner_zeile.setSpacing(theme.DOCK_KI_RAHMEN_ABST)

editor._projekt_pfad_feld = QtWidgets.QLineEdit(lade_projektordner())
editor._projekt_pfad_feld.setReadOnly(True)
editor._projekt_pfad_feld.setPlaceholderText(
"Kein Projektordner gewählt – nur die offene Datei geht an die KI")
editor._projekt_pfad_feld.setToolTip(
"Gewählter Projektordner. Die KI bekommt eine Übersicht aller\n"
"Dateien darin und kann einzelne Dateien gezielt nachfordern.")
_ordner_zeile.addWidget(editor._projekt_pfad_feld, stretch=1)

_btn_ordner = QtWidgets.QPushButton("📁 Wählen …")
_btn_ordner.setToolTip("Projektordner auswählen")
_ordner_zeile.addWidget(_btn_ordner)

_btn_ordner_weg = QtWidgets.QPushButton("✕")
_btn_ordner_weg.setFixedWidth(theme.DOCK_ICON_BTN_BREITE)
_btn_ordner_weg.setToolTip("Projektordner nicht mehr mitschicken")
_ordner_zeile.addWidget(_btn_ordner_weg)
_kontext_sek.addLayout(_ordner_zeile)

editor._projekt_karte_box = QtWidgets.QCheckBox(
"Projektübersicht bei jeder Anfrage mitschicken")
editor._projekt_karte_box.setChecked(lade_projektkarte_aktiv())
editor._projekt_karte_box.setToolTip(
"Sendet Ordnerbaum und Klassen-/Funktionsübersicht mit —\n"
"keinen Quelltext. Kostet nur wenige hundert Token.")
editor._projekt_karte_box.toggled.connect(speichere_projektkarte_aktiv)
_kontext_sek.addWidget(editor._projekt_karte_box)

editor._projekt_info_lbl = QtWidgets.QLabel("")
editor._projekt_info_lbl.setWordWrap(True)
_kontext_sek.addWidget(editor._projekt_info_lbl)

def _projekt_info_aktualisieren():
pfad = editor._projekt_pfad_feld.text().strip()
if not pfad:
editor._projekt_info_lbl.setText("")
return
try:
from editor.ki.projekt_kontext import projekt_statistik
anzahl, tokens = projekt_statistik(pfad)
except Exception as exc:
editor._projekt_info_lbl.setText(f"⚠ Ordner nicht lesbar: {exc}")
return
if not anzahl:
editor._projekt_info_lbl.setText(
"⚠ Keine lesbaren Textdateien in diesem Ordner gefunden")
else:
editor._projekt_info_lbl.setText(
f"📂 {anzahl} Datei(en) · Übersicht ≈ {tokens} Token je Anfrage")

def _projekt_ordner_waehlen():
start = editor._projekt_pfad_feld.text().strip() or ""
pfad = QtWidgets.QFileDialog.getExistingDirectory(
editor, "Projektordner wählen", start)
if not pfad:
return
editor._projekt_pfad_feld.setText(pfad)
speichere_projektordner(pfad)
_projekt_info_aktualisieren()

def _projekt_ordner_loeschen():
editor._projekt_pfad_feld.clear()
speichere_projektordner("")
_projekt_info_aktualisieren()

_btn_ordner.clicked.connect(_projekt_ordner_waehlen)
_btn_ordner_weg.clicked.connect(_projekt_ordner_loeschen)
# Beim Start nicht sofort durchsuchen — das Panel soll zügig aufgehen
QtCore.QTimer.singleShot(1500, _projekt_info_aktualisieren)

ki_layout.addWidget(_input_w, stretch=1)
ki_layout.addWidget(_kontext_sek)
editor._dock_ki = editor._make_dock("🤖 KI", "dock_ki", _L, ki_widget)
Expand Down
36 changes: 36 additions & 0 deletions editor/ki/ki_anfrage.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ def _relevante_api_hints(frage: str, max_hints: int = 5) -> str:
return "\n".join(zeilen)


def _projektkarte_block(kompakt: bool = False) -> str:
"""Projektkarte des gewählten Ordners — "" wenn keiner gesetzt/aktiv ist."""
try:
from core.params import lade_projektordner, lade_projektkarte_aktiv
if not lade_projektkarte_aktiv():
return ""
from editor.ki.projekt_kontext import karte_als_prompt_block
return karte_als_prompt_block(lade_projektordner(), kompakt=kompakt)
except Exception:
return ""


class KIAnfrage:
"""Baut KI-Prompts auf und startet Worker-Threads."""

Expand Down Expand Up @@ -266,6 +278,21 @@ def ki_fragen(self):
self._c._status.setText(
f"🤖 Warte auf ersten Token … | Verlauf: {verlauf_kb} KB")

# Projektordner: kompakte Karte des ganzen Ordnerbaums (ohne Quelltext).
# Für Ollama nur der Ordnerbaum — die Struktur-Übersicht sprengt sonst
# das kleine Kontextfenster lokaler Modelle.
projekt_block = _projektkarte_block(
kompakt=self._c._src_box.currentText().startswith("Ollama"))
# Nachlade-Protokoll nur dort anbieten, wo die Folgeanfrage auch läuft
nachlade_hinweis = ""
if projekt_block and not (ist_nl_modus or ist_pd_modus
or ist_sw_modus or ist_tc_modus):
from editor.ki.projekt_nachladen import ANFORDERUNGS_HINWEIS
nachlade_hinweis = f"{ANFORDERUNGS_HINWEIS}\n"
self._c._projekt_nachlade_runde = 0
# Nur nachladen, wenn die KI das Protokoll auch erklärt bekommen hat
self._c._projekt_nachlade_erlaubt = bool(nachlade_hinweis)

# Sitemap: nur Struktur-Übersicht, kein voller Code-Text
sitemap = erstelle_code_sitemap(self._c._editor.toPlainText())
sitemap_block = (
Expand All @@ -285,13 +312,15 @@ def ki_fragen(self):

if code_quelle == "nur_frage":
full_prompt = (
f"{projekt_block}{nachlade_hinweis}"
f"{sitemap_block}"
f"{prompt}{_hints_section}\n\n"
f"{modus_prefix}"
)
else:
herkunft = "Markierter Block" if code_quelle == "suchfeld" else "Editor-Inhalt"
full_prompt = (
f"{projekt_block}{nachlade_hinweis}"
f"{sitemap_block}"
f"Aufgabe: {prompt}\n\n"
f"{herkunft}:\n```python\n{code}\n```"
Expand Down Expand Up @@ -391,6 +420,11 @@ def ki_fragen(self):
except Exception:
pass

# Projektordner-Übersicht auch für FC11 (ohne Nachlade-Protokoll —
# FC11 darf ausschließlich Code zurückgeben)
if projekt_block:
fc11_prompt = fc11_prompt + "\n" + projekt_block

# Skills: passende Skill-Dateien anhand von Stichwörtern anhängen
try:
import re as _re, os as _os
Expand Down Expand Up @@ -527,6 +561,8 @@ def ki_fehler_erklaeren(self):

aktueller_code = self._c._editor.toPlainText()
code_ausschnitt = extrahiere_fehler_kontext(aktueller_code, fehler_text)
# Fehler-Erklärung läuft ohne Projektkarte — also auch ohne Nachladen
self._c._projekt_nachlade_erlaubt = False

if code_ausschnitt:
system = (
Expand Down
12 changes: 12 additions & 0 deletions editor/ki/ki_chunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ def on_stream_done(self):
self._c._btn_ki.setEnabled(True)
return

# Projektordner: hat die KI Dateien angefordert, werden sie nachgeladen
# und die Anfrage läuft automatisch weiter (nur im normalen Chat-Modus).
if not (getattr(self._c, "_nl_antwort_aktiv", False)
or getattr(self._c, "_tc_modus_aktiv", False)
or getattr(self._c, "_sw_modus_aktiv", False)):
try:
from editor.ki.projekt_nachladen import nachladen_falls_angefordert
if nachladen_falls_angefordert(self._c, full):
return
except Exception:
pass

if getattr(self._c, "_nl_antwort_aktiv", False):
self._c._nl_antwort_aktiv = False
clean = extrahiere_code_aus_nl_antwort(clean)
Expand Down
Loading
Loading