fix: ask_user_choice Short-Poll, Freitext-Option, Stop-Kontext-Fix
- Long-Poll durch Short-Poll ersetzt (MCP-Server pollt jede Sekunde statt einer blockierenden HTTP-Anfrage)
- Expliziter Lambda-Rückgabetyp Task<IResult> für ASP.NET Minimal API Endpoints (behebt 200 empty body Bug)
- Freitext-Option bei Choice-Buttons ("Eigene Antwort…") für freie User-Eingabe
- Stop während ask_user_choice markiert Chain als awaiting_user_choice statt stopped (Kontext bleibt erhalten)
- ask_user_choice System-Prompt-Instruktion breiter formuliert (Quiz, Multiple-Choice etc.)
- Text wird jetzt immer in DB gespeichert, auch bei ask_user_choice
- Key-Check Model auf claude-haiku-4-5 geändert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e0a69e7a4
commit
92ecaf9dac
8 files changed
+184
-66
No files matched your search
@@ -24,16 +24,19 @@ public static class InternalEndpoint
|
|||||||
public static string InternalToken { get; } = Guid.NewGuid().ToString("N");
|
public static string InternalToken { get; } = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pending user choices – Key ist sessionId, Value wird completed wenn der User klickt.
|
/// Beantwortete User-Choices – Key ist sessionId, Value ist die gewählte Option.
|
||||||
|
/// MCP-Server pollt dieses Dictionary (Short-Poll statt Long-Poll).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ConcurrentDictionary<string, TaskCompletionSource<string>> PendingChoices = new();
|
public static readonly ConcurrentDictionary<string, string> ChoiceAnswers = new();
|
||||||
|
|
||||||
public static void MapInternalEndpoints(this IEndpointRouteBuilder app)
|
public static void MapInternalEndpoints(this IEndpointRouteBuilder app)
|
||||||
{
|
{
|
||||||
app.MapPost("/api/internal/remote-execute", HandleRemoteExecute);
|
app.MapPost("/api/internal/remote-execute", HandleRemoteExecute);
|
||||||
app.MapPost("/api/internal/ask-user-choice", HandleAskUserChoice);
|
// WICHTIG: Expliziter Rückgabetyp Task<IResult> nötig! Ohne ihn interpretiert ASP.NET den Lambda als
|
||||||
|
// RequestDelegate (Func<HttpContext,Task>) → IResult-Rückgabe wird ignoriert → 200 empty body.
|
||||||
|
app.MapPost("/api/internal/ask-user-choice", async Task<IResult> (HttpContext ctx) => await HandleAskUserChoice(ctx));
|
||||||
// choice-respond ist NICHT unter /api/internal/ weil es vom Frontend aufgerufen wird (normale Auth)
|
// choice-respond ist NICHT unter /api/internal/ weil es vom Frontend aufgerufen wird (normale Auth)
|
||||||
app.MapPost("/api/chat/choice-respond", HandleChoiceRespond);
|
app.MapPost("/api/chat/choice-respond", async Task<IResult> (HttpContext ctx) => await HandleChoiceRespond(ctx));
|
||||||
app.MapPost("/api/internal/memory-read", HandleMemoryRead);
|
app.MapPost("/api/internal/memory-read", HandleMemoryRead);
|
||||||
app.MapPost("/api/internal/memory-write", HandleMemoryWrite);
|
app.MapPost("/api/internal/memory-write", HandleMemoryWrite);
|
||||||
app.MapPost("/api/internal/memory-list", HandleMemoryList);
|
app.MapPost("/api/internal/memory-list", HandleMemoryList);
|
||||||
@@ -85,8 +88,8 @@ public static class InternalEndpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MCP-Server ruft diesen Endpoint auf und blockiert bis der User eine Auswahl trifft.
|
/// MCP-Server pollt diesen Endpoint bis der User eine Auswahl getroffen hat.
|
||||||
/// Der TCS wird vorher von AnthropicService erstellt (beim tool_use Event).
|
/// Gibt 200 mit { choice } zurück wenn beantwortet, 404 wenn noch ausstehend.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<IResult> HandleAskUserChoice(HttpContext ctx)
|
private static async Task<IResult> HandleAskUserChoice(HttpContext ctx)
|
||||||
{
|
{
|
||||||
@@ -105,34 +108,12 @@ public static class InternalEndpoint
|
|||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
return Results.BadRequest(new { error = "sessionId erforderlich" });
|
return Results.BadRequest(new { error = "sessionId erforderlich" });
|
||||||
|
|
||||||
// Warte auf den TCS der von AnthropicService erstellt wurde
|
// Prüfen ob bereits eine Antwort vorliegt (Short-Poll)
|
||||||
if (!PendingChoices.TryGetValue(sessionId, out var tcs))
|
if (ChoiceAnswers.TryRemove(sessionId, out var choice))
|
||||||
{
|
|
||||||
// TCS noch nicht erstellt (Race Condition) – kurz warten und nochmal probieren
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
await Task.Delay(500, ctx.RequestAborted);
|
|
||||||
if (PendingChoices.TryGetValue(sessionId, out tcs)) break;
|
|
||||||
}
|
|
||||||
if (tcs == null)
|
|
||||||
return Results.Json(new { error = "Keine ausstehende Auswahl für diese Session" }, statusCode: 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blockieren bis User antwortet (max 5 Minuten)
|
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted);
|
|
||||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var choice = await tcs.Task.WaitAsync(cts.Token);
|
|
||||||
PendingChoices.TryRemove(sessionId, out _);
|
|
||||||
return Results.Ok(new { choice });
|
return Results.Ok(new { choice });
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
// Noch keine Antwort – MCP-Server soll erneut pollen
|
||||||
{
|
return Results.Json(new { status = "pending" }, statusCode: 202);
|
||||||
PendingChoices.TryRemove(sessionId, out _);
|
|
||||||
return Results.Json(new { error = "Timeout – keine Auswahl getroffen" }, statusCode: 408);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -142,6 +123,7 @@ public static class InternalEndpoint
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Frontend ruft diesen Endpoint auf wenn der User einen Choice-Button klickt (CLI-Modus).
|
/// Frontend ruft diesen Endpoint auf wenn der User einen Choice-Button klickt (CLI-Modus).
|
||||||
|
/// Speichert die Antwort in ChoiceAnswers – MCP-Server holt sie beim nächsten Poll ab.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<IResult> HandleChoiceRespond(HttpContext ctx)
|
private static async Task<IResult> HandleChoiceRespond(HttpContext ctx)
|
||||||
{
|
{
|
||||||
@@ -162,13 +144,9 @@ public static class InternalEndpoint
|
|||||||
if (string.IsNullOrEmpty(choice))
|
if (string.IsNullOrEmpty(choice))
|
||||||
return Results.BadRequest(new { error = "choice erforderlich" });
|
return Results.BadRequest(new { error = "choice erforderlich" });
|
||||||
|
|
||||||
if (PendingChoices.TryGetValue(sessionId, out var tcs))
|
// Antwort speichern – MCP-Server holt sie beim nächsten Poll ab
|
||||||
{
|
ChoiceAnswers[sessionId] = choice;
|
||||||
tcs.TrySetResult(choice);
|
return Results.Ok(new { success = true });
|
||||||
return Results.Ok(new { success = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Results.NotFound(new { error = "Keine ausstehende Auswahl für diese Session" });
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace Gott.Services
|
|||||||
|
|
||||||
public static class AnthropicKeyChecker
|
public static class AnthropicKeyChecker
|
||||||
{
|
{
|
||||||
private const string CheckModel = "claude-sonnet-4-20250514";
|
private const string CheckModel = "claude-haiku-4-5"; //"claude-sonnet-4-20250514";
|
||||||
|
|
||||||
public static async Task<KeyCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
|
public static async Task<KeyCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@ public class AnthropicService
|
|||||||
var fullText = new StringBuilder();
|
var fullText = new StringBuilder();
|
||||||
var tempImagePaths = new List<string>();
|
var tempImagePaths = new List<string>();
|
||||||
string mcpConfigPath = null;
|
string mcpConfigPath = null;
|
||||||
|
bool isUserChoice = false;
|
||||||
|
|
||||||
var chain = new ChainRecord
|
var chain = new ChainRecord
|
||||||
{
|
{
|
||||||
@@ -1996,7 +1997,6 @@ public class AnthropicService
|
|||||||
prompt = $"<conversation_history>\n{historyContext}\n</conversation_history>\n\n<current_request>\n{prompt}\n</current_request>";
|
prompt = $"<conversation_history>\n{historyContext}\n</conversation_history>\n\n<current_request>\n{prompt}\n</current_request>";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isUserChoice = false;
|
|
||||||
string choiceToolCallId = null;
|
string choiceToolCallId = null;
|
||||||
|
|
||||||
var result = await _cliService.ChatStreamingAsync(
|
var result = await _cliService.ChatStreamingAsync(
|
||||||
@@ -2063,8 +2063,8 @@ public class AnthropicService
|
|||||||
foreach (var cOpt in cc.EnumerateArray())
|
foreach (var cOpt in cc.EnumerateArray())
|
||||||
cChoices.Add(cOpt.GetString() ?? "");
|
cChoices.Add(cOpt.GetString() ?? "");
|
||||||
|
|
||||||
// TCS erstellen bevor der MCP-Server den Endpoint aufruft
|
// Alte Antwort entfernen (falls vorhanden)
|
||||||
InternalEndpoint.PendingChoices[sessionId] = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
InternalEndpoint.ChoiceAnswers.TryRemove(sessionId, out _);
|
||||||
|
|
||||||
// SSE Event mit viaCli-Flag → Frontend zeigt Buttons + nutzt choice-respond Endpoint
|
// SSE Event mit viaCli-Flag → Frontend zeigt Buttons + nutzt choice-respond Endpoint
|
||||||
choiceToolCallId = evt.ToolId;
|
choiceToolCallId = evt.ToolId;
|
||||||
@@ -2133,8 +2133,8 @@ public class AnthropicService
|
|||||||
await SseHelper.TryWriteSse(response, "error", new { error = $"CLI-Fehler: {errorMsg}" }, sseCt);
|
await SseHelper.TryWriteSse(response, "error", new { error = $"CLI-Fehler: {errorMsg}" }, sseCt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Antwort in DB speichern (nur wenn nicht schon im UserChoice-Handler gespeichert)
|
// Antwort in DB speichern
|
||||||
if (!isUserChoice && fullText.Length > 0)
|
if (fullText.Length > 0)
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
@@ -2173,13 +2173,16 @@ public class AnthropicService
|
|||||||
catch (OperationCanceledException) when (workCt.IsCancellationRequested)
|
catch (OperationCanceledException) when (workCt.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Stop-Button gedrückt → CLI-Prozess wurde gestoppt
|
// Stop-Button gedrückt → CLI-Prozess wurde gestoppt
|
||||||
await FinalizeChain(chain, usage, 1, "stopped", sw);
|
// Wenn ask_user_choice aktiv war: als awaiting_user_choice markieren,
|
||||||
|
// damit die nächste Chain den Kontext (Frage + Optionen) behält.
|
||||||
|
var stoppedReason = isUserChoice ? "awaiting_user_choice" : "stopped";
|
||||||
|
await FinalizeChain(chain, usage, 1, stoppedReason, sw);
|
||||||
await SseHelper.TryWriteSse(response, "done", new
|
await SseHelper.TryWriteSse(response, "done", new
|
||||||
{
|
{
|
||||||
model = modelId,
|
model = modelId,
|
||||||
chainId,
|
chainId,
|
||||||
durationMs = sw.ElapsedMilliseconds,
|
durationMs = sw.ElapsedMilliseconds,
|
||||||
stopReason = "stopped",
|
stopReason = stoppedReason,
|
||||||
viaCli = true
|
viaCli = true
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder a
|
|||||||
{
|
{
|
||||||
Icon = "fa-question-circle",
|
Icon = "fa-question-circle",
|
||||||
Label = "Agent · Rückfragen (AllowUserChoice)",
|
Label = "Agent · Rückfragen (AllowUserChoice)",
|
||||||
Content = "## Rückfragen\nWenn du dir bei einer Aufgabe unsicher bist oder es mehrere sinnvolle Wege gibt, nutze das `ask_user_choice`-Tool um dem User eine Auswahlliste zu präsentieren. Nutze es immer, wenn eine Entscheidung vom User getroffen werden soll."
|
Content = "## Rückfragen\nWenn du dem User Optionen, Auswahlmöglichkeiten oder Fragen mit mehreren Antwortmöglichkeiten präsentierst, nutze IMMER das `ask_user_choice`-Tool (bzw. `mcp__gott-remote__ask_user_choice`). Das gilt für:\n- Rückfragen bei Unsicherheit oder mehreren sinnvollen Wegen\n- Quiz-Fragen, Multiple-Choice-Fragen\n- Jede Situation in der der User zwischen Optionen wählen soll\n\nNutze NIEMALS A/B/C/D-Aufzählungen oder nummerierte Listen als Ersatz – IMMER das Tool verwenden."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ def handle_memory_list(req_id, arguments):
|
|||||||
|
|
||||||
|
|
||||||
def handle_ask_user_choice(req_id, arguments):
|
def handle_ask_user_choice(req_id, arguments):
|
||||||
"""Stellt dem User eine Frage und wartet auf Antwort (long-poll)."""
|
"""Stellt dem User eine Frage und wartet auf Antwort (short-poll)."""
|
||||||
|
import time
|
||||||
question = arguments.get("question", "")
|
question = arguments.get("question", "")
|
||||||
choices = arguments.get("choices", [])
|
choices = arguments.get("choices", [])
|
||||||
if not question:
|
if not question:
|
||||||
@@ -225,23 +226,29 @@ def handle_ask_user_choice(req_id, arguments):
|
|||||||
if not choices or len(choices) < 2:
|
if not choices or len(choices) < 2:
|
||||||
return make_result(req_id, "Fehler: Mindestens 2 choices erforderlich", True)
|
return make_result(req_id, "Fehler: Mindestens 2 choices erforderlich", True)
|
||||||
|
|
||||||
# Long-Poll: Blockiert bis der User antwortet (max 5 Min)
|
# Short-Poll: Jede Sekunde beim Backend nachfragen ob der User geantwortet hat
|
||||||
# Der Backend-Endpoint /api/internal/ask-user-choice blockiert selbst
|
for i in range(300): # max 5 Minuten
|
||||||
result = api_call_long("ask-user-choice", {"sessionId": SESSION_ID})
|
try:
|
||||||
choice = result.get("choice", "")
|
result = api_call("ask-user-choice", {"sessionId": SESSION_ID})
|
||||||
return make_result(req_id, f"User hat gewählt: {choice}")
|
# 200 = Antwort vorhanden
|
||||||
|
if "choice" in result:
|
||||||
|
return make_result(req_id, f"User hat gewählt: {result['choice']}")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 202:
|
||||||
|
# 202 = noch ausstehend, weiter pollen
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
body = ""
|
||||||
|
try:
|
||||||
|
body = e.read().decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return make_result(req_id, f"API-Fehler {e.code}: {body}", True)
|
||||||
|
except Exception as e:
|
||||||
|
return make_result(req_id, f"Fehler: {str(e)}", True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return make_result(req_id, "Timeout – keine Auswahl getroffen (5 Minuten)", True)
|
||||||
def api_call_long(endpoint, payload):
|
|
||||||
"""API-Call mit langem Timeout (für Long-Poll Endpoints)."""
|
|
||||||
url = f"{API_URL}/api/internal/{endpoint}"
|
|
||||||
data = json.dumps(payload).encode("utf-8")
|
|
||||||
req = urllib.request.Request(url, data=data, method="POST")
|
|
||||||
req.add_header("Content-Type", "application/json")
|
|
||||||
if AUTH_TOKEN:
|
|
||||||
req.add_header("X-Internal-Token", AUTH_TOKEN)
|
|
||||||
with urllib.request.urlopen(req, timeout=330) as resp:
|
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
|
||||||
|
|
||||||
|
|
||||||
TOOL_HANDLERS = {
|
TOOL_HANDLERS = {
|
||||||
|
|||||||
@@ -1280,6 +1280,80 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Freitext-Option */
|
||||||
|
.choice-custom {
|
||||||
|
margin-top: var(--badge-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-toggle {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-toggle:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-row {
|
||||||
|
display: none;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--badge-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-open .choice-custom-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-open .choice-custom-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-input {
|
||||||
|
flex: 1;
|
||||||
|
border: var(--border-width) solid var(--f1-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: var(--f1-bg);
|
||||||
|
color: var(--f1-text);
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-send {
|
||||||
|
border: var(--border-width) solid var(--primary);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-send:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--f1-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-answered .choice-custom {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-answered .choice-custom.has-answer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-custom-answer {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--primary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-choice-block {
|
.inline-choice-block {
|
||||||
margin: var(--gap) 0;
|
margin: var(--gap) 0;
|
||||||
padding: var(--gap);
|
padding: var(--gap);
|
||||||
|
|||||||
@@ -215,6 +215,48 @@ function _createChoiceBubble(opts) {
|
|||||||
}
|
}
|
||||||
div.appendChild(buttonsDiv);
|
div.appendChild(buttonsDiv);
|
||||||
|
|
||||||
|
// Freitext-Option (nur wenn noch nicht beantwortet)
|
||||||
|
if (!answered) {
|
||||||
|
const customDiv = document.createElement('div');
|
||||||
|
customDiv.className = 'choice-custom';
|
||||||
|
|
||||||
|
const toggleLink = document.createElement('a');
|
||||||
|
toggleLink.className = 'choice-custom-toggle';
|
||||||
|
toggleLink.href = '#';
|
||||||
|
toggleLink.textContent = 'Eigene Antwort…';
|
||||||
|
toggleLink.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
customDiv.classList.add('choice-custom-open');
|
||||||
|
customDiv.querySelector('.choice-custom-input').focus();
|
||||||
|
});
|
||||||
|
customDiv.appendChild(toggleLink);
|
||||||
|
|
||||||
|
const inputRow = document.createElement('div');
|
||||||
|
inputRow.className = 'choice-custom-row';
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.className = 'choice-custom-input';
|
||||||
|
input.placeholder = 'Eigene Antwort eingeben…';
|
||||||
|
|
||||||
|
var sendCustom = function() {
|
||||||
|
var text = input.value.trim();
|
||||||
|
if (!text) return;
|
||||||
|
handleChoiceClick(opts.toolCallId, text, div);
|
||||||
|
};
|
||||||
|
input.addEventListener('keydown', function(e) { if (e.key === 'Enter') sendCustom(); });
|
||||||
|
|
||||||
|
var sendBtn = document.createElement('button');
|
||||||
|
sendBtn.className = 'choice-custom-send';
|
||||||
|
sendBtn.textContent = '↵';
|
||||||
|
sendBtn.addEventListener('click', sendCustom);
|
||||||
|
|
||||||
|
inputRow.appendChild(input);
|
||||||
|
inputRow.appendChild(sendBtn);
|
||||||
|
customDiv.appendChild(inputRow);
|
||||||
|
div.appendChild(customDiv);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.timestamp) _addTimestamp(div, opts.timestamp);
|
if (opts.timestamp) _addTimestamp(div, opts.timestamp);
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,14 +415,28 @@ async function sendMessageDirect(text) {
|
|||||||
async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
|
async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
|
||||||
// Buttons deaktivieren und gewählte Option markieren
|
// Buttons deaktivieren und gewählte Option markieren
|
||||||
const buttons = bubbleDiv.querySelectorAll('.choice-btn');
|
const buttons = bubbleDiv.querySelectorAll('.choice-btn');
|
||||||
|
var matchedButton = false;
|
||||||
buttons.forEach(btn => {
|
buttons.forEach(btn => {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
if (btn.textContent === choice) {
|
if (btn.textContent === choice) {
|
||||||
btn.classList.add('choice-selected');
|
btn.classList.add('choice-selected');
|
||||||
|
matchedButton = true;
|
||||||
} else {
|
} else {
|
||||||
btn.classList.add('choice-unselected');
|
btn.classList.add('choice-unselected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Freitext-Antwort: Keine der Buttons matched → Antwort als Text anzeigen
|
||||||
|
if (!matchedButton) {
|
||||||
|
var customDiv = bubbleDiv.querySelector('.choice-custom');
|
||||||
|
if (customDiv) {
|
||||||
|
customDiv.innerHTML = '';
|
||||||
|
customDiv.classList.add('has-answer');
|
||||||
|
var answerSpan = document.createElement('span');
|
||||||
|
answerSpan.className = 'choice-custom-answer';
|
||||||
|
answerSpan.textContent = '↳ ' + choice;
|
||||||
|
customDiv.appendChild(answerSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
bubbleDiv.classList.add('choice-answered');
|
bubbleDiv.classList.add('choice-answered');
|
||||||
|
|
||||||
// CLI-Modus: Antwort über internen Endpoint an MCP-Server weiterleiten (CLI läuft weiter)
|
// CLI-Modus: Antwort über internen Endpoint an MCP-Server weiterleiten (CLI läuft weiter)
|
||||||
|
|||||||
Reference in new issue
Block a user