diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css
index 27925e7..bf919da 100644
--- a/wwwroot/css/chat.css
+++ b/wwwroot/css/chat.css
@@ -439,23 +439,82 @@
/* Unified Modus-Toggle: Aus / Plan / Fusion / MoA (ein einziger #planModeBtn) */
#planModeBtn.plan-mode-active {
color: var(--primary);
- background: color-mix(in srgb, var(--primary) 15%, transparent);
- border-radius: var(--border-radius);
+ background: color-mix(in srgb, var(--primary) 25%, transparent);
}
#planModeBtn.panel-mode-fusion {
color: var(--primary);
- background: color-mix(in srgb, var(--primary) 15%, transparent);
- border-radius: var(--border-radius);
+ background: color-mix(in srgb, var(--primary) 25%, transparent);
}
#planModeBtn.panel-mode-moa {
color: var(--primary);
- background: color-mix(in srgb, var(--primary) 28%, transparent);
- border-radius: var(--border-radius);
+ background: color-mix(in srgb, var(--primary) 25%, transparent);
}
#planModeBtn.ultracode-active {
- color: var(--info);
- background: color-mix(in srgb, var(--info) 18%, transparent);
+ color: var(--primary);
+ background: color-mix(in srgb, var(--primary) 25%, transparent);
+}
+
+/* Modus-Menü (Dropdown: Aus / Plan / Ultracode / Fusion / MoA) */
+
+.composite-menu {
+ position: fixed;
+ z-index: 10000;
+ background: var(--f2-bg);
+ border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
+ min-width: 16em;
+ padding-block: calc(var(--badge-spacing) / 2);
+}
+
+.composite-menu-item {
+ display: flex;
+ align-items: center;
+ gap: var(--badge-spacing);
+ padding: var(--menu-item-padding);
+ background: none;
+ border: none;
+ border-radius: unset;
+ text-align: left;
+ white-space: nowrap;
+ cursor: pointer;
+ color: var(--text);
+}
+
+.composite-menu-item:hover {
+ background: var(--f3-bg);
+ border: none;
+}
+
+.composite-menu-item.active {
+ color: var(--primary);
+}
+
+.composite-menu-icon {
+ width: 1.3em;
+ text-align: center;
+ flex-shrink: 0;
+}
+
+.composite-menu-text {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+}
+
+.composite-menu-desc {
+ font-size: var(--font-size);
+ opacity: var(--semi-transparent);
+}
+
+.composite-menu-check {
+ margin-left: auto;
+ flex-shrink: 0;
+ color: var(--primary);
}
/* Emoji Picker */
diff --git a/wwwroot/index.html b/wwwroot/index.html
index fcee1e1..20b0268 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -458,7 +458,7 @@
-
+
diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js
index 68329bf..9566eb0 100644
--- a/wwwroot/js/chat.js
+++ b/wwwroot/js/chat.js
@@ -217,65 +217,70 @@ function handleStreamEvent(eventType, data) {
}
}
-// ── Unified Modus-Toggle (Aus / Plan / Fusion / MoA) ──
+// ── Unified Modus-Menü (Aus / Plan / Ultracode / Fusion / MoA) ──
//
-// Ein einziger Button (#planModeBtn) schaltet zyklisch durch alle Betriebsmodi:
-// Aus → Plan-Modus → Fusion (Panel) → MoA (Panel) → Aus
+// Der Button (#planModeBtn) öffnet ein Dropdown-Menü (Muster wie der Emoji-Picker), aus dem
+// der Betriebsmodus direkt gewählt wird. Der Button zeigt weiterhin das Symbol des aktiven
+// Modus; im Menü ist der aktive Eintrag markiert.
//
-// Plan-Modus ist transient (wird bei Session-Wechsel zurückgesetzt, nie in der Session
-// persistiert). Fusion/MoA werden in der Session persistiert (apiUpdateSession) und
-// server-seitig aus ctx.MultiModelMode gelesen – daher reicht ein PATCH.
+// Plan-Modus und Ultracode sind transient (pro Request, nicht in der Session persistiert) und
+// laufen ohne Panel → beim Wählen wird multiModelMode der Session auf 'off' gesetzt, damit kein
+// zuvor gewähltes Fusion/MoA-Panel nebenherläuft (das Menü erlaubt direkte Sprünge, anders als
+// der frühere Zyklus, der immer durch 'off' lief). Fusion/MoA werden in der Session persistiert
+// und server-seitig aus ctx.MultiModelMode gelesen.
//
-// _planModeActive bleibt als Compat-Variable (sendMessage + modals.js lesen sie direkt).
+// _planModeActive / _ultracodeActive bleiben als Compat-Variablen (sendMessage + modals.js lesen
+// sie direkt).
+
+// Metadaten je Modus – eine Quelle für Button-Render UND Menü-Aufbau: Reihenfolge im Menü,
+// Icon (Button + Menü), Label, Button-Aktiv-Klasse, Input-Placeholder, kurze Beschreibung.
+const _COMPOSITE_MODES = [
+ { mode: 'off', icon: 'fa-ban', label: 'Aus', btnClass: null, placeholder: 'Nachricht...', desc: 'Normaler Einzel-Agent' },
+ { mode: 'plan', icon: 'fa-map', label: 'Plan-Modus', btnClass: 'plan-mode-active', placeholder: 'Planen...', desc: 'Nur analysieren & planen (read-only)' },
+ { mode: 'ultracode', icon: 'fa-sitemap', label: 'Ultracode', btnClass: 'ultracode-active', placeholder: 'Ultracode – als Workflow...', desc: 'Dynamischer Workflow (mehrere Subagenten)' },
+ { mode: 'fusion', icon: 'fa-users', label: 'Fusion', btnClass: 'panel-mode-fusion', placeholder: 'Nachricht...', desc: 'Panel: Antworten fusionieren' },
+ { mode: 'moa', icon: 'fa-clone', label: 'MoA', btnClass: 'panel-mode-moa', placeholder: 'Nachricht...', desc: 'Panel: Mixture-of-Agents' }
+];
let _compositeMode = 'off'; // 'off' | 'plan' | 'ultracode' | 'fusion' | 'moa'
let _compositeHasAgents = false;
-// Compat-Variablen – werden durch cycleCompositeMode / initCompositeMode synchron gehalten
+// Compat-Variablen – werden durch setCompositeMode / initCompositeMode synchron gehalten
let _planModeActive = false; // true genau wenn _compositeMode === 'plan'
let _ultracodeActive = false; // true genau wenn _compositeMode === 'ultracode' (transient, pro Request)
-const _COMPOSITE_ORDER = ['off', 'plan', 'ultracode', 'fusion', 'moa'];
+// Offenes Modus-Dropdown + Outside-Click-Handler (wie beim Emoji-Picker).
+let _compositeMenuEl = null;
+let _compositeMenuCloseHandler = null;
-// Serialisierte Modus-Persistenz. Schnelles Durchklicken (off→plan→fusion→moa) feuert sonst
-// ÜBERLAPPENDE PATCHes, deren Commit-Reihenfolge nicht garantiert ist → der zuletzt committete
-// Zwischenwert 'fusion' kann 'moa' überschreiben. Und ein direkt danach gesendeter Chat-Request
-// lädt die Session, BEVOR 'moa' committed ist → das Backend liest den alten Modus (Fusion → 1 Runde
-// statt MoA). Die Kette garantiert (a) Reihenfolge (letzter Klick gewinnt) und (b) dass sendMessage
-// das offene PATCH abwarten kann. Die Kette rejected nie (Fehler werden hier geschluckt).
+// Serialisierte Modus-Persistenz. Schnell hintereinander gewählte Modi feuern sonst ÜBERLAPPENDE
+// PATCHes, deren Commit-Reihenfolge nicht garantiert ist → ein Zwischenwert kann den letzten
+// überschreiben. Und ein direkt danach gesendeter Chat-Request lädt die Session, BEVOR der Modus
+// committed ist → das Backend liest den alten Modus. Die Kette garantiert (a) Reihenfolge (letzte
+// Wahl gewinnt) und (b) dass sendMessage das offene PATCH abwarten kann. Sie rejected nie (Fehler
+// werden hier geschluckt).
let _modePersist = Promise.resolve();
+function _compositeMeta(mode) {
+ for (var i = 0; i < _COMPOSITE_MODES.length; i++) {
+ if (_COMPOSITE_MODES[i].mode === mode) return _COMPOSITE_MODES[i];
+ }
+ return _COMPOSITE_MODES[0];
+}
+
function _renderCompositeBtn() {
var btn = $('#planModeBtn');
if (!btn) return;
btn.classList.remove('plan-mode-active', 'ultracode-active', 'panel-mode-fusion', 'panel-mode-moa');
+ var meta = _compositeMeta(_compositeMode);
var icon = btn.querySelector('i');
+ if (icon) icon.className = 'fa ' + meta.icon;
+ if (meta.btnClass) btn.classList.add(meta.btnClass);
+ btn.title = _compositeMode === 'off'
+ ? 'Modus wählen (aktuell: Aus)'
+ : 'Modus: ' + meta.label + ' · klicken zum Wechseln';
var input = $('#input');
- if (_compositeMode === 'plan') {
- btn.classList.add('plan-mode-active');
- if (icon) icon.className = 'fa fa-map';
- btn.title = 'Plan-Modus aktiv · klicken → Ultracode';
- if (input) input.placeholder = 'Planen...';
- } else if (_compositeMode === 'ultracode') {
- btn.classList.add('ultracode-active');
- if (icon) icon.className = 'fa fa-sitemap';
- btn.title = 'Ultracode aktiv (dynamischer Workflow) · klicken → Fusion';
- if (input) input.placeholder = 'Ultracode – als Workflow...';
- } else if (_compositeMode === 'fusion') {
- btn.classList.add('panel-mode-fusion');
- if (icon) icon.className = 'fa fa-users';
- btn.title = 'Fusion aktiv · klicken → MoA';
- if (input) input.placeholder = 'Nachricht...';
- } else if (_compositeMode === 'moa') {
- btn.classList.add('panel-mode-moa');
- if (icon) icon.className = 'fa fa-clone';
- btn.title = 'MoA aktiv · klicken → Aus';
- if (input) input.placeholder = 'Nachricht...';
- } else {
- if (icon) icon.className = 'fa fa-map';
- btn.title = 'Plan-Modus / Panel umschalten';
- if (input) input.placeholder = 'Nachricht...';
- }
+ if (input) input.placeholder = meta.placeholder;
}
// Initialisiert den Button aus der geladenen Session (bei jedem Chat-Wechsel aufrufen).
@@ -288,46 +293,106 @@ function initCompositeMode(panelMode, hasAgents) {
_renderCompositeBtn();
}
-// Schaltet Aus → Plan → Fusion → MoA → Aus durch.
-// Fusion/MoA werden sofort in die Session persistiert; Plan-Modus ist transient (kein Server-Call).
-async function cycleCompositeMode() {
- var idx = _COMPOSITE_ORDER.indexOf(_compositeMode);
- _compositeMode = _COMPOSITE_ORDER[(idx + 1) % _COMPOSITE_ORDER.length];
- _planModeActive = (_compositeMode === 'plan');
- _ultracodeActive = (_compositeMode === 'ultracode');
+// Setzt den Modus direkt (aus dem Menü gewählt).
+// Persistiert das EFFEKTIVE Panel der Session (off/fusion/moa): für plan/ultracode also 'off',
+// damit kein zuvor gewähltes Panel nebenherläuft. Plan/Ultracode selbst sind transient (Request-Flags).
+async function setCompositeMode(mode) {
+ if (mode === _compositeMode) return; // schon aktiv → nichts tun
+ if (_compositeMeta(mode).mode !== mode) return; // unbekannter Modus → ignorieren
+
+ _compositeMode = mode;
+ _planModeActive = (mode === 'plan');
+ _ultracodeActive = (mode === 'ultracode');
_renderCompositeBtn();
- // Nur Fusion/MoA/Off brauchen einen Server-Call (Plan + Ultracode sind transient pro Request).
- // Auf die serialisierte Kette legen, damit aufeinanderfolgende Klicks in Reihenfolge committen
- // (letzter Klick gewinnt) und sendMessage das offene PATCH abwarten kann.
- if (_compositeMode !== 'plan' && _compositeMode !== 'ultracode') {
- var sid = getCurrentSessionId();
- if (sid) {
- var targetMode = _compositeMode;
- _modePersist = _modePersist
- .then(function () { return apiUpdateSession(sid, { multiModelMode: targetMode }); })
- .catch(function (e) {
- console.error('Modus speichern fehlgeschlagen:', e);
- if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
- });
- }
+ // Effektiver Panel-Modus: nur Fusion/MoA aktivieren ein Panel, alles andere 'off'.
+ // Auf die serialisierte Kette legen, damit aufeinanderfolgende Wahlen in Reihenfolge committen
+ // (letzte gewinnt) und sendMessage das offene PATCH abwarten kann.
+ var panelMode = (mode === 'fusion' || mode === 'moa') ? mode : 'off';
+ var sid = getCurrentSessionId();
+ if (sid) {
+ _modePersist = _modePersist
+ .then(function () { return apiUpdateSession(sid, { multiModelMode: panelMode }); })
+ .catch(function (e) {
+ console.error('Modus speichern fehlgeschlagen:', e);
+ if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
+ });
}
if (typeof showToast === 'function') {
- if (_compositeMode === 'off') {
+ if (mode === 'off') {
showToast('Modus: Aus', 'info');
- } else if (_compositeMode === 'plan') {
+ } else if (mode === 'plan') {
showToast('Plan-Modus aktiv', 'info');
- } else if (_compositeMode === 'ultracode') {
+ } else if (mode === 'ultracode') {
showToast('Ultracode aktiv – diese Anfrage läuft als dynamischer Workflow (mehr Tokens/Zeit)', 'info');
} else if (!_compositeHasAgents) {
- showToast('Panel: ' + (_compositeMode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv – aber keine Panel-Agenten konfiguriert (im Session-Bearbeiten festlegen)', 'info');
+ showToast('Panel: ' + (mode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv – aber keine Panel-Agenten konfiguriert (im Session-Bearbeiten festlegen)', 'info');
} else {
- showToast('Panel: ' + (_compositeMode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv', 'success');
+ showToast('Panel: ' + (mode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv', 'success');
}
}
}
+// ── Modus-Dropdown-Menü (Muster wie der Emoji-Picker) ──
+
+function _buildCompositeMenu() {
+ var menu = document.createElement('div');
+ menu.className = 'composite-menu';
+ _COMPOSITE_MODES.forEach(function (m) {
+ var active = (m.mode === _compositeMode);
+ var item = document.createElement('button');
+ item.className = 'composite-menu-item' + (active ? ' active' : '');
+ item.innerHTML =
+ '' +
+ '' +
+ (active ? '' : '');
+ item.addEventListener('click', function (e) {
+ e.stopPropagation();
+ closeCompositeMenu();
+ setCompositeMode(m.mode);
+ });
+ menu.appendChild(item);
+ });
+ return menu;
+}
+
+function toggleCompositeMenu(anchor) {
+ if (_compositeMenuEl) { closeCompositeMenu(); return; }
+
+ var menu = _buildCompositeMenu();
+ document.body.appendChild(menu);
+ _compositeMenuEl = menu;
+
+ // Positionierung: über dem Button (Input-Leiste ist unten), linksbündig zum Button.
+ var rect = anchor.getBoundingClientRect();
+ var h = menu.offsetHeight;
+ var w = menu.offsetWidth;
+ var top = rect.top - h - 6;
+ var left = rect.left;
+ if (top < 8) top = rect.bottom + 6; // kein Platz oben → unten
+ if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8; // rechts abgeschnitten
+ if (left < 8) left = 8;
+ menu.style.top = top + 'px';
+ menu.style.left = left + 'px';
+
+ _compositeMenuCloseHandler = function (e) {
+ if (!menu.contains(e.target) && e.target !== anchor && !anchor.contains(e.target)) closeCompositeMenu();
+ };
+ setTimeout(function () { document.addEventListener('click', _compositeMenuCloseHandler); }, 0);
+}
+
+function closeCompositeMenu() {
+ if (_compositeMenuEl) { _compositeMenuEl.remove(); _compositeMenuEl = null; }
+ if (_compositeMenuCloseHandler) {
+ document.removeEventListener('click', _compositeMenuCloseHandler);
+ _compositeMenuCloseHandler = null;
+ }
+}
+
// ── Chat State ──
let isStreaming = false;
diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js
index cff1597..aaf4f6c 100644
--- a/wwwroot/js/pages/chat-page.js
+++ b/wwwroot/js/pages/chat-page.js
@@ -572,12 +572,16 @@ registerPage('chat', {
});
}
- // Unified Modus-Toggle (Aus / Plan / Fusion / MoA) – aus der Session initialisieren
+ // Unified Modus-Menü (Aus / Plan / Ultracode / Fusion / MoA) – aus der Session initialisieren.
+ // Klick öffnet ein Dropdown (wie der Emoji-Picker), aus dem der Modus direkt gewählt wird.
const compositeModeBtn = document.getElementById('planModeBtn');
if (compositeModeBtn) {
const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim());
initCompositeMode(session.multiModelMode, hasPanelAgents);
- compositeModeBtn.addEventListener('click', cycleCompositeMode);
+ compositeModeBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ toggleCompositeMenu(compositeModeBtn);
+ });
}
// File Upload