feat: ask_user_choice Tool – interaktive Auswahlmöglichkeiten im Chat

Neues Tool 'ask_user_choice' analog zum AskUserQuestion aus dem Claude Agent SDK.
Claude kann dem User Multiple-Choice-Fragen stellen, wenn es mehrere sinnvolle
Wege gibt. Der User wählt per Button, Claude arbeitet mit der Antwort weiter.

Backend:
- ToolService: ask_user_choice Tool-Definition (question + choices[2-5])
- AnthropicService/ProcessStream: Tool wird NICHT ausgeführt, sondern als
  SSE-Event 'user_choice' an den Client gesendet. stopReason wird auf
  'awaiting_user_choice' gesetzt (mit Guard gegen Überschreibung durch
  message_delta)
- ChatEndpoint: Erkennung von Choice-Responses (choiceToolCallId + choiceResult),
  tool_result wird in die korrekte Chain gespeichert (wo der tool_use liegt),
  toolChoice wird auf 'auto' gesetzt damit Claude frei antworten kann
- ChatService: FindChainWithToolCall() zum Auffinden der richtigen Chain,
  BuildHistoryFromDb() erweitert um Choice-Chain-Erkennung (wenn letzte Chain
  leer/nur tool_result → vorletzte Chain vollständig rekonstruieren)
- ChatModels: ChatRequest um ChoiceToolCallId + ChoiceResult erweitert

Frontend:
- api.js: apiChatChoice() für Choice-Responses
- chat.js: handleStreamEvent() als shared Handler extrahiert (sendMessage +
  handleChoiceClick nutzen denselben Code), handleChoiceClick() startet neuen
  Stream nach Button-Klick
- ui.js: addChoiceBubble() (live/interaktiv) + addChoiceBubbleStatic() (History)
- history.js: Choice-Bubbles in _renderMessagesIntoFragment + renderMessages,
  Voraus-Suche nach zugehörigem tool_result, unbeantwortet = klickbare Buttons
- chat.css: Styling für Choice-Bubbles (Primary-Color-Outline-Buttons, Hover,
  Selected/Unselected States)

Robustheit:
- Backend-Fallback wenn Claude choices als String statt Array sendet
- Frontend-Guard: Array.isArray(choices) an allen 4 Render-Stellen
- Page-Refresh mit offener Choice → aktive Buttons werden korrekt gerendert
This commit is contained in:
Meleki committed 2026-03-07 11:30:55 +01:00
1 parent 72347ed2f5
commit 5f3e308975
10 files changed
+628 -140

No files matched your search

+69
View File
@@ -1134,3 +1134,72 @@
.review-iterations-row input[type="number"] {
width: 5em;
}
/* ── Choice Bubbles (ask_user_choice) ── */
.choice-bubble {
align-self: flex-start;
background: var(--f1-bg);
border: var(--border-width) solid var(--f1-border);
border-left: 3px solid var(--primary);
max-width: 80%;
display: flex;
flex-direction: column;
gap: var(--box-spacing);
}
.choice-question {
font-size: 0.95em;
}
.choice-question p {
margin: 0;
}
.choice-buttons {
display: flex;
flex-wrap: wrap;
gap: var(--badge-spacing);
}
.choice-btn {
padding: 0.45em 1em;
border: var(--border-width) solid var(--primary);
border-radius: var(--border-radius);
background: transparent;
color: var(--primary);
font-size: 0.9em;
cursor: pointer;
transition: all 0.15s ease;
font-family: inherit;
}
.choice-btn:hover:not(:disabled) {
background: var(--primary);
color: var(--primary-text);
}
.choice-btn:active:not(:disabled) {
transform: scale(0.97);
}
.choice-btn:disabled {
cursor: default;
}
.choice-btn.choice-selected {
background: var(--primary);
color: var(--primary-text);
border-color: var(--primary);
font-weight: 600;
}
.choice-btn.choice-unselected {
opacity: 0.4;
border-color: var(--f1-border);
color: var(--muted);
}
.choice-answered .choice-question {
opacity: 0.7;
}