webui: +sketch-tool-card-multiple-choice
This is just the frontend part of the multiple-choice tool.
Since the actual tool call isn't implemented yet, the only
way to exercise this code is via the demo page.
Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/webui/src/web-components/demo/sketch-tool-card.demo.html b/webui/src/web-components/demo/sketch-tool-card.demo.html
index f8ba308..ba53164 100644
--- a/webui/src/web-components/demo/sketch-tool-card.demo.html
+++ b/webui/src/web-components/demo/sketch-tool-card.demo.html
@@ -8,6 +8,41 @@
<script>
const toolCalls = [
{
+ name: "multiple-choice",
+ input: JSON.stringify({
+ question: "What is your favorite programming language?",
+ choices: [
+ "JavaScript",
+ "TypeScript",
+ "Python",
+ "Go",
+ "Rust",
+ "Java",
+ "C#",
+ "C++",
+ ],
+ }),
+ result_message: {
+ type: "tool",
+ tool_result: JSON.stringify({
+ selected: "Go",
+ }),
+ },
+ },
+ {
+ name: "multiple-choice",
+ input: JSON.stringify({
+ question: "Which feature would you like to implement next?",
+ choices: [
+ "Dark mode",
+ "User profiles",
+ "Social sharing",
+ "Analytics dashboard",
+ ],
+ }),
+ // No result yet, showing the choices without a selection
+ },
+ {
name: "bash",
input: JSON.stringify({
command:
@@ -238,6 +273,11 @@
case "title":
toolCardEl = document.createElement("sketch-tool-card-title");
break;
+ case "multiple-choice":
+ toolCardEl = document.createElement(
+ "sketch-tool-card-multiple-choice",
+ );
+ break;
}
toolCardEl.toolCall = toolCall;
toolCardEl.open = true;
diff --git a/webui/src/web-components/sketch-tool-card.ts b/webui/src/web-components/sketch-tool-card.ts
index dbb09ae..ed058a2 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -574,6 +574,200 @@
}
}
+@customElement("sketch-tool-card-multiple-choice")
+export class SketchToolCardMultipleChoice extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ @property()
+ selectedOption: string | number | null = null;
+
+ static styles = css`
+ .options-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: 10px 0;
+ }
+
+ .option {
+ display: inline-flex;
+ align-items: center;
+ padding: 8px 12px;
+ border-radius: 4px;
+ background-color: #f5f5f5;
+ cursor: pointer;
+ transition: all 0.2s;
+ border: 1px solid transparent;
+ user-select: none;
+ }
+
+ .option:hover {
+ background-color: #e0e0e0;
+ border-color: #ccc;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .option:active {
+ transform: translateY(0);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ background-color: #d5d5d5;
+ }
+
+ .option.selected {
+ background-color: #e3f2fd;
+ border-color: #2196f3;
+ border-width: 1px;
+ border-style: solid;
+ }
+
+ .option-index {
+ font-size: 0.8em;
+ opacity: 0.7;
+ margin-right: 6px;
+ }
+
+ .option-label {
+ font-family: sans-serif;
+ }
+
+ .option-checkmark {
+ margin-left: 6px;
+ color: #2196f3;
+ }
+
+ .summary-text {
+ font-style: italic;
+ }
+
+ .summary-text strong {
+ font-style: normal;
+ color: #2196f3;
+ font-weight: 600;
+ }
+
+ p {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+ }
+ `;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.updateSelectedOption();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ updated(changedProps) {
+ if (changedProps.has("toolCall")) {
+ this.updateSelectedOption();
+ }
+ }
+
+ updateSelectedOption() {
+ // Get selected option from result if available
+ if (this.toolCall?.result_message?.tool_result) {
+ try {
+ this.selectedOption = JSON.parse(
+ this.toolCall.result_message.tool_result,
+ ).selected;
+ } catch (e) {
+ console.error("Error parsing result:", e);
+ this.selectedOption = this.toolCall.result_message.tool_result;
+ }
+ } else {
+ this.selectedOption = null;
+ }
+ }
+
+ handleOptionClick(choice) {
+ // If this option is already selected, unselect it (toggle behavior)
+ if (this.selectedOption === choice) {
+ this.selectedOption = null;
+ } else {
+ // Otherwise, select the clicked option
+ this.selectedOption = choice;
+ }
+
+ // Dispatch a custom event that can be listened to by parent components
+ const event = new CustomEvent("option-selected", {
+ detail: { selected: this.selectedOption },
+ bubbles: true,
+ composed: true,
+ });
+ this.dispatchEvent(event);
+ }
+
+ render() {
+ // Parse the input to get choices if available
+ let choices = [];
+ let question = "";
+ try {
+ const inputData = JSON.parse(this.toolCall?.input || "{}");
+ choices = inputData.choices || [];
+ question = inputData.question || "Please select an option:";
+ } catch (e) {
+ console.error("Error parsing multiple-choice input:", e);
+ }
+
+ // Determine what to show in the summary slot
+ const summaryContent =
+ this.selectedOption !== null
+ ? html`<span class="summary-text"
+ >${question}: <strong>${this.selectedOption}</strong></span
+ >`
+ : html`<span class="summary-text">${question}</span>`;
+
+ return html` <sketch-tool-card
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ >
+ <span slot="summary">${summaryContent}</span>
+ <div slot="input">
+ <p>${question}</p>
+ <div class="options-container">
+ ${choices.map((choice, index) => {
+ const isSelected =
+ this.selectedOption !== null &&
+ (this.selectedOption === choice || this.selectedOption === index);
+ return html`
+ <div
+ class="option ${isSelected ? "selected" : ""}"
+ @click=${() => this.handleOptionClick(choice)}
+ >
+ <span class="option-index">${index + 1}</span>
+ <span class="option-label">${choice}</span>
+ ${isSelected
+ ? html`<span class="option-checkmark">✓</span>`
+ : ""}
+ </div>
+ `;
+ })}
+ </div>
+ </div>
+ <div slot="result">
+ ${this.toolCall?.result_message && this.selectedOption
+ ? html`<p>Selected: <strong>${this.selectedOption}</strong></p>`
+ : ""}
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
@customElement("sketch-tool-card-generic")
export class SketchToolCardGeneric extends LitElement {
@property()
@@ -626,5 +820,6 @@
"sketch-tool-card-patch": SketchToolCardPatch;
"sketch-tool-card-think": SketchToolCardThink;
"sketch-tool-card-title": SketchToolCardTitle;
+ "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
}
}