| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- import { For, Show, createMemo, type Component } from "solid-js"
- import { createStore } from "solid-js/store"
- import { Button } from "@opencode-ai/ui/button"
- import { Icon } from "@opencode-ai/ui/icon"
- import { showToast } from "@opencode-ai/ui/toast"
- import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
- import { useLanguage } from "@/context/language"
- import { useSDK } from "@/context/sdk"
- export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
- const sdk = useSDK()
- const language = useLanguage()
- const questions = createMemo(() => props.request.questions)
- const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
- const [store, setStore] = createStore({
- tab: 0,
- answers: [] as QuestionAnswer[],
- custom: [] as string[],
- editing: false,
- sending: false,
- })
- const question = createMemo(() => questions()[store.tab])
- const confirm = createMemo(() => !single() && store.tab === questions().length)
- const options = createMemo(() => question()?.options ?? [])
- const input = createMemo(() => store.custom[store.tab] ?? "")
- const multi = createMemo(() => question()?.multiple === true)
- const customPicked = createMemo(() => {
- const value = input()
- if (!value) return false
- return store.answers[store.tab]?.includes(value) ?? false
- })
- const fail = (err: unknown) => {
- const message = err instanceof Error ? err.message : String(err)
- showToast({ title: language.t("common.requestFailed"), description: message })
- }
- const reply = (answers: QuestionAnswer[]) => {
- if (store.sending) return
- setStore("sending", true)
- sdk.client.question
- .reply({ requestID: props.request.id, answers })
- .catch(fail)
- .finally(() => setStore("sending", false))
- }
- const reject = () => {
- if (store.sending) return
- setStore("sending", true)
- sdk.client.question
- .reject({ requestID: props.request.id })
- .catch(fail)
- .finally(() => setStore("sending", false))
- }
- const submit = () => {
- reply(questions().map((_, i) => store.answers[i] ?? []))
- }
- const pick = (answer: string, custom: boolean = false) => {
- const answers = [...store.answers]
- answers[store.tab] = [answer]
- setStore("answers", answers)
- if (custom) {
- const inputs = [...store.custom]
- inputs[store.tab] = answer
- setStore("custom", inputs)
- }
- if (single()) {
- reply([[answer]])
- return
- }
- setStore("tab", store.tab + 1)
- }
- const toggle = (answer: string) => {
- const existing = store.answers[store.tab] ?? []
- const next = [...existing]
- const index = next.indexOf(answer)
- if (index === -1) next.push(answer)
- if (index !== -1) next.splice(index, 1)
- const answers = [...store.answers]
- answers[store.tab] = next
- setStore("answers", answers)
- }
- const selectTab = (index: number) => {
- setStore("tab", index)
- setStore("editing", false)
- }
- const selectOption = (optIndex: number) => {
- if (store.sending) return
- if (optIndex === options().length) {
- setStore("editing", true)
- return
- }
- const opt = options()[optIndex]
- if (!opt) return
- if (multi()) {
- toggle(opt.label)
- return
- }
- pick(opt.label)
- }
- const handleCustomSubmit = (e: Event) => {
- e.preventDefault()
- if (store.sending) return
- const value = input().trim()
- if (!value) {
- setStore("editing", false)
- return
- }
- if (multi()) {
- const existing = store.answers[store.tab] ?? []
- const next = [...existing]
- if (!next.includes(value)) next.push(value)
- const answers = [...store.answers]
- answers[store.tab] = next
- setStore("answers", answers)
- setStore("editing", false)
- return
- }
- pick(value, true)
- setStore("editing", false)
- }
- return (
- <div data-component="question-prompt">
- <Show when={!single()}>
- <div data-slot="question-tabs">
- <For each={questions()}>
- {(q, index) => {
- const active = () => index() === store.tab
- const answered = () => (store.answers[index()]?.length ?? 0) > 0
- return (
- <button
- data-slot="question-tab"
- data-active={active()}
- data-answered={answered()}
- disabled={store.sending}
- onClick={() => selectTab(index())}
- >
- {q.header}
- </button>
- )
- }}
- </For>
- <button
- data-slot="question-tab"
- data-active={confirm()}
- disabled={store.sending}
- onClick={() => selectTab(questions().length)}
- >
- {language.t("ui.common.confirm")}
- </button>
- </div>
- </Show>
- <Show when={!confirm()}>
- <div data-slot="question-content">
- <div data-slot="question-text">
- {question()?.question}
- {multi() ? " " + language.t("ui.question.multiHint") : ""}
- </div>
- <div data-slot="question-options">
- <For each={options()}>
- {(opt, i) => {
- const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
- return (
- <button
- data-slot="question-option"
- data-picked={picked()}
- disabled={store.sending}
- onClick={() => selectOption(i())}
- >
- <span data-slot="option-label">{opt.label}</span>
- <Show when={opt.description}>
- <span data-slot="option-description">{opt.description}</span>
- </Show>
- <Show when={picked()}>
- <Icon name="check-small" size="normal" />
- </Show>
- </button>
- )
- }}
- </For>
- <button
- data-slot="question-option"
- data-picked={customPicked()}
- disabled={store.sending}
- onClick={() => selectOption(options().length)}
- >
- <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
- <Show when={!store.editing && input()}>
- <span data-slot="option-description">{input()}</span>
- </Show>
- <Show when={customPicked()}>
- <Icon name="check-small" size="normal" />
- </Show>
- </button>
- <Show when={store.editing}>
- <form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
- <input
- ref={(el) => setTimeout(() => el.focus(), 0)}
- type="text"
- data-slot="custom-input"
- placeholder={language.t("ui.question.custom.placeholder")}
- value={input()}
- disabled={store.sending}
- onInput={(e) => {
- const inputs = [...store.custom]
- inputs[store.tab] = e.currentTarget.value
- setStore("custom", inputs)
- }}
- />
- <Button type="submit" variant="primary" size="small" disabled={store.sending}>
- {multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
- </Button>
- <Button
- type="button"
- variant="ghost"
- size="small"
- disabled={store.sending}
- onClick={() => setStore("editing", false)}
- >
- {language.t("ui.common.cancel")}
- </Button>
- </form>
- </Show>
- </div>
- </div>
- </Show>
- <Show when={confirm()}>
- <div data-slot="question-review">
- <div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
- <For each={questions()}>
- {(q, index) => {
- const value = () => store.answers[index()]?.join(", ") ?? ""
- const answered = () => Boolean(value())
- return (
- <div data-slot="review-item">
- <span data-slot="review-label">{q.question}</span>
- <span data-slot="review-value" data-answered={answered()}>
- {answered() ? value() : language.t("ui.question.review.notAnswered")}
- </span>
- </div>
- )
- }}
- </For>
- </div>
- </Show>
- <div data-slot="question-actions">
- <Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
- {language.t("ui.common.dismiss")}
- </Button>
- <Show when={!single()}>
- <Show when={confirm()}>
- <Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
- {language.t("ui.common.submit")}
- </Button>
- </Show>
- <Show when={!confirm() && multi()}>
- <Button
- variant="secondary"
- size="small"
- onClick={() => selectTab(store.tab + 1)}
- disabled={store.sending || (store.answers[store.tab]?.length ?? 0) === 0}
- >
- {language.t("ui.common.next")}
- </Button>
- </Show>
- </Show>
- </div>
- </div>
- )
- }
|