tweak(ui): collapse questions
parent
a94f564ff0
commit
724dd665ec
|
|
@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
@ -22,6 +23,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||||
customOn: [] as boolean[],
|
customOn: [] as boolean[],
|
||||||
editing: false,
|
editing: false,
|
||||||
sending: false,
|
sending: false,
|
||||||
|
collapsed: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
let root: HTMLDivElement | undefined
|
let root: HTMLDivElement | undefined
|
||||||
|
|
@ -31,6 +33,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||||
const on = createMemo(() => store.customOn[store.tab] === true)
|
const on = createMemo(() => store.customOn[store.tab] === true)
|
||||||
const multi = createMemo(() => question()?.multiple === true)
|
const multi = createMemo(() => question()?.multiple === true)
|
||||||
|
const picked = createMemo(() => store.answers[store.tab]?.length ?? 0)
|
||||||
|
|
||||||
const summary = createMemo(() => {
|
const summary = createMemo(() => {
|
||||||
const n = Math.min(store.tab + 1, total())
|
const n = Math.min(store.tab + 1, total())
|
||||||
|
|
@ -39,6 +42,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||||
|
|
||||||
const last = createMemo(() => store.tab >= total() - 1)
|
const last = createMemo(() => store.tab >= total() - 1)
|
||||||
|
|
||||||
|
const fold = () => setStore("collapsed", (value) => !value)
|
||||||
|
|
||||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
const customUpdate = (value: string, selected: boolean = on()) => {
|
||||||
const prev = input().trim()
|
const prev = input().trim()
|
||||||
const next = value.trim()
|
const next = value.trim()
|
||||||
|
|
@ -228,38 +233,44 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jump = (tab: number) => {
|
|
||||||
if (store.sending) return
|
|
||||||
setStore("tab", tab)
|
|
||||||
setStore("editing", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DockPrompt
|
<DockPrompt
|
||||||
kind="question"
|
kind="question"
|
||||||
ref={(el) => (root = el)}
|
ref={(el) => (root = el)}
|
||||||
header={
|
header={
|
||||||
<>
|
<div
|
||||||
|
data-action="session-question-toggle"
|
||||||
|
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
|
||||||
|
onClick={fold}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
fold()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div data-slot="question-header-title">{summary()}</div>
|
<div data-slot="question-header-title">{summary()}</div>
|
||||||
<div data-slot="question-progress">
|
<div class="ml-auto">
|
||||||
<For each={questions()}>
|
<IconButton
|
||||||
{(_, i) => (
|
data-action="session-question-toggle-button"
|
||||||
<button
|
icon="chevron-down"
|
||||||
type="button"
|
size="normal"
|
||||||
data-slot="question-progress-segment"
|
variant="ghost"
|
||||||
data-active={i() === store.tab}
|
classList={{ "rotate-180": store.collapsed }}
|
||||||
data-answered={
|
onMouseDown={(event) => {
|
||||||
(store.answers[i()]?.length ?? 0) > 0 ||
|
event.preventDefault()
|
||||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
event.stopPropagation()
|
||||||
}
|
}}
|
||||||
disabled={store.sending}
|
onClick={(event) => {
|
||||||
onClick={() => jump(i())}
|
event.stopPropagation()
|
||||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
fold()
|
||||||
/>
|
}}
|
||||||
)}
|
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
|
||||||
</For>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
|
|
@ -279,56 +290,121 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div data-slot="question-text">{question()?.question}</div>
|
<div
|
||||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
data-slot="question-text"
|
||||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
class="cursor-default"
|
||||||
|
classList={{
|
||||||
|
"mb-6": store.collapsed && picked() === 0,
|
||||||
|
}}
|
||||||
|
role={store.collapsed ? "button" : undefined}
|
||||||
|
tabIndex={store.collapsed ? 0 : undefined}
|
||||||
|
onClick={fold}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!store.collapsed) return
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
fold()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question()?.question}
|
||||||
|
</div>
|
||||||
|
<Show when={store.collapsed && picked() > 0}>
|
||||||
|
<div data-slot="question-hint" class="cursor-default mb-6">
|
||||||
|
{picked()} answer{picked() === 1 ? "" : "s"} selected
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div data-slot="question-options">
|
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
|
||||||
<For each={options()}>
|
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||||
{(opt, i) => {
|
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
</Show>
|
||||||
return (
|
<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()}
|
||||||
|
role={multi() ? "checkbox" : "radio"}
|
||||||
|
aria-checked={picked()}
|
||||||
|
disabled={store.sending}
|
||||||
|
onClick={() => selectOption(i())}
|
||||||
|
>
|
||||||
|
<span data-slot="question-option-check" aria-hidden="true">
|
||||||
|
<span
|
||||||
|
data-slot="question-option-box"
|
||||||
|
data-type={multi() ? "checkbox" : "radio"}
|
||||||
|
data-picked={picked()}
|
||||||
|
>
|
||||||
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{opt.label}</span>
|
||||||
|
<Show when={opt.description}>
|
||||||
|
<span data-slot="option-description">{opt.description}</span>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={store.editing}
|
||||||
|
fallback={
|
||||||
<button
|
<button
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-picked={picked()}
|
data-custom="true"
|
||||||
|
data-picked={on()}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={picked()}
|
aria-checked={on()}
|
||||||
disabled={store.sending}
|
disabled={store.sending}
|
||||||
onClick={() => selectOption(i())}
|
onClick={customOpen}
|
||||||
>
|
>
|
||||||
<span data-slot="question-option-check" aria-hidden="true">
|
<span
|
||||||
<span
|
data-slot="question-option-check"
|
||||||
data-slot="question-option-box"
|
aria-hidden="true"
|
||||||
data-type={multi() ? "checkbox" : "radio"}
|
onClick={(e) => {
|
||||||
data-picked={picked()}
|
e.preventDefault()
|
||||||
>
|
e.stopPropagation()
|
||||||
|
customToggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
<Icon name="check-small" size="small" />
|
<Icon name="check-small" size="small" />
|
||||||
</Show>
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{opt.label}</span>
|
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
<Show when={opt.description}>
|
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||||
<span data-slot="option-description">{opt.description}</span>
|
|
||||||
</Show>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
}
|
||||||
}}
|
>
|
||||||
</For>
|
<form
|
||||||
|
|
||||||
<Show
|
|
||||||
when={store.editing}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-custom="true"
|
data-custom="true"
|
||||||
data-picked={on()}
|
data-picked={on()}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={on()}
|
aria-checked={on()}
|
||||||
disabled={store.sending}
|
onMouseDown={(e) => {
|
||||||
onClick={customOpen}
|
if (store.sending) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.target instanceof HTMLTextAreaElement) return
|
||||||
|
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||||
|
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||||
|
}}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
data-slot="question-option-check"
|
data-slot="question-option-check"
|
||||||
|
|
@ -347,80 +423,39 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||||
</span>
|
</span>
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
<textarea
|
||||||
</span>
|
ref={(el) =>
|
||||||
</button>
|
setTimeout(() => {
|
||||||
}
|
el.focus()
|
||||||
>
|
el.style.height = "0px"
|
||||||
<form
|
el.style.height = `${el.scrollHeight}px`
|
||||||
data-slot="question-option"
|
}, 0)
|
||||||
data-custom="true"
|
|
||||||
data-picked={on()}
|
|
||||||
role={multi() ? "checkbox" : "radio"}
|
|
||||||
aria-checked={on()}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
if (store.sending) {
|
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.target instanceof HTMLTextAreaElement) return
|
|
||||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
|
||||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
|
||||||
}}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-slot="question-option-check"
|
|
||||||
aria-hidden="true"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
|
||||||
<Icon name="check-small" size="small" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
|
||||||
<textarea
|
|
||||||
ref={(el) =>
|
|
||||||
setTimeout(() => {
|
|
||||||
el.focus()
|
|
||||||
el.style.height = "0px"
|
|
||||||
el.style.height = `${el.scrollHeight}px`
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
data-slot="question-custom-input"
|
|
||||||
placeholder={language.t("ui.question.custom.placeholder")}
|
|
||||||
value={input()}
|
|
||||||
rows={1}
|
|
||||||
disabled={store.sending}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
setStore("editing", false)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (e.key !== "Enter" || e.shiftKey) return
|
data-slot="question-custom-input"
|
||||||
e.preventDefault()
|
placeholder={language.t("ui.question.custom.placeholder")}
|
||||||
commitCustom()
|
value={input()}
|
||||||
}}
|
rows={1}
|
||||||
onInput={(e) => {
|
disabled={store.sending}
|
||||||
customUpdate(e.currentTarget.value)
|
onKeyDown={(e) => {
|
||||||
e.currentTarget.style.height = "0px"
|
if (e.key === "Escape") {
|
||||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
e.preventDefault()
|
||||||
}}
|
setStore("editing", false)
|
||||||
/>
|
return
|
||||||
</span>
|
}
|
||||||
</form>
|
if (e.key !== "Enter" || e.shiftKey) return
|
||||||
</Show>
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
customUpdate(e.currentTarget.value)
|
||||||
|
e.currentTarget.style.height = "0px"
|
||||||
|
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DockPrompt>
|
</DockPrompt>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -808,49 +808,6 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="question-progress"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="question-progress-segment"] {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 999px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
touch-action: manipulation;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
width: 16px;
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background-color: var(--icon-weak-base);
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-active="true"]::after {
|
|
||||||
background-color: var(--icon-strong-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-answered="true"]::after {
|
|
||||||
background-color: var(--icon-interactive-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="question-content"] {
|
[data-slot="question-content"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue