Skip to content

Commit 724dd66

Browse files
committed
tweak(ui): collapse questions
1 parent a94f564 commit 724dd66

2 files changed

Lines changed: 164 additions & 172 deletions

File tree

packages/app/src/pages/session/composer/session-question-dock.tsx

Lines changed: 164 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
33
import { Button } from "@opencode-ai/ui/button"
44
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
55
import { Icon } from "@opencode-ai/ui/icon"
6+
import { IconButton } from "@opencode-ai/ui/icon-button"
67
import { showToast } from "@opencode-ai/ui/toast"
78
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
89
import { useLanguage } from "@/context/language"
@@ -22,6 +23,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
2223
customOn: [] as boolean[],
2324
editing: false,
2425
sending: false,
26+
collapsed: false,
2527
})
2628

2729
let root: HTMLDivElement | undefined
@@ -31,6 +33,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
3133
const input = createMemo(() => store.custom[store.tab] ?? "")
3234
const on = createMemo(() => store.customOn[store.tab] === true)
3335
const multi = createMemo(() => question()?.multiple === true)
36+
const picked = createMemo(() => store.answers[store.tab]?.length ?? 0)
3437

3538
const summary = createMemo(() => {
3639
const n = Math.min(store.tab + 1, total())
@@ -39,6 +42,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
3942

4043
const last = createMemo(() => store.tab >= total() - 1)
4144

45+
const fold = () => setStore("collapsed", (value) => !value)
46+
4247
const customUpdate = (value: string, selected: boolean = on()) => {
4348
const prev = input().trim()
4449
const next = value.trim()
@@ -228,38 +233,44 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
228233
setStore("editing", false)
229234
}
230235

231-
const jump = (tab: number) => {
232-
if (store.sending) return
233-
setStore("tab", tab)
234-
setStore("editing", false)
235-
}
236-
237236
return (
238237
<DockPrompt
239238
kind="question"
240239
ref={(el) => (root = el)}
241240
header={
242-
<>
241+
<div
242+
data-action="session-question-toggle"
243+
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
244+
role="button"
245+
tabIndex={0}
246+
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
247+
onClick={fold}
248+
onKeyDown={(event) => {
249+
if (event.key !== "Enter" && event.key !== " ") return
250+
event.preventDefault()
251+
fold()
252+
}}
253+
>
243254
<div data-slot="question-header-title">{summary()}</div>
244-
<div data-slot="question-progress">
245-
<For each={questions()}>
246-
{(_, i) => (
247-
<button
248-
type="button"
249-
data-slot="question-progress-segment"
250-
data-active={i() === store.tab}
251-
data-answered={
252-
(store.answers[i()]?.length ?? 0) > 0 ||
253-
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
254-
}
255-
disabled={store.sending}
256-
onClick={() => jump(i())}
257-
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
258-
/>
259-
)}
260-
</For>
255+
<div class="ml-auto">
256+
<IconButton
257+
data-action="session-question-toggle-button"
258+
icon="chevron-down"
259+
size="normal"
260+
variant="ghost"
261+
classList={{ "rotate-180": store.collapsed }}
262+
onMouseDown={(event) => {
263+
event.preventDefault()
264+
event.stopPropagation()
265+
}}
266+
onClick={(event) => {
267+
event.stopPropagation()
268+
fold()
269+
}}
270+
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
271+
/>
261272
</div>
262-
</>
273+
</div>
263274
}
264275
footer={
265276
<>
@@ -279,56 +290,121 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
279290
</>
280291
}
281292
>
282-
<div data-slot="question-text">{question()?.question}</div>
283-
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
284-
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
293+
<div
294+
data-slot="question-text"
295+
class="cursor-default"
296+
classList={{
297+
"mb-6": store.collapsed && picked() === 0,
298+
}}
299+
role={store.collapsed ? "button" : undefined}
300+
tabIndex={store.collapsed ? 0 : undefined}
301+
onClick={fold}
302+
onKeyDown={(event) => {
303+
if (!store.collapsed) return
304+
if (event.key !== "Enter" && event.key !== " ") return
305+
event.preventDefault()
306+
fold()
307+
}}
308+
>
309+
{question()?.question}
310+
</div>
311+
<Show when={store.collapsed && picked() > 0}>
312+
<div data-slot="question-hint" class="cursor-default mb-6">
313+
{picked()} answer{picked() === 1 ? "" : "s"} selected
314+
</div>
285315
</Show>
286-
<div data-slot="question-options">
287-
<For each={options()}>
288-
{(opt, i) => {
289-
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
290-
return (
316+
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
317+
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
318+
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
319+
</Show>
320+
<div data-slot="question-options">
321+
<For each={options()}>
322+
{(opt, i) => {
323+
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
324+
return (
325+
<button
326+
data-slot="question-option"
327+
data-picked={picked()}
328+
role={multi() ? "checkbox" : "radio"}
329+
aria-checked={picked()}
330+
disabled={store.sending}
331+
onClick={() => selectOption(i())}
332+
>
333+
<span data-slot="question-option-check" aria-hidden="true">
334+
<span
335+
data-slot="question-option-box"
336+
data-type={multi() ? "checkbox" : "radio"}
337+
data-picked={picked()}
338+
>
339+
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
340+
<Icon name="check-small" size="small" />
341+
</Show>
342+
</span>
343+
</span>
344+
<span data-slot="question-option-main">
345+
<span data-slot="option-label">{opt.label}</span>
346+
<Show when={opt.description}>
347+
<span data-slot="option-description">{opt.description}</span>
348+
</Show>
349+
</span>
350+
</button>
351+
)
352+
}}
353+
</For>
354+
355+
<Show
356+
when={store.editing}
357+
fallback={
291358
<button
292359
data-slot="question-option"
293-
data-picked={picked()}
360+
data-custom="true"
361+
data-picked={on()}
294362
role={multi() ? "checkbox" : "radio"}
295-
aria-checked={picked()}
363+
aria-checked={on()}
296364
disabled={store.sending}
297-
onClick={() => selectOption(i())}
365+
onClick={customOpen}
298366
>
299-
<span data-slot="question-option-check" aria-hidden="true">
300-
<span
301-
data-slot="question-option-box"
302-
data-type={multi() ? "checkbox" : "radio"}
303-
data-picked={picked()}
304-
>
367+
<span
368+
data-slot="question-option-check"
369+
aria-hidden="true"
370+
onClick={(e) => {
371+
e.preventDefault()
372+
e.stopPropagation()
373+
customToggle()
374+
}}
375+
>
376+
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
305377
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
306378
<Icon name="check-small" size="small" />
307379
</Show>
308380
</span>
309381
</span>
310382
<span data-slot="question-option-main">
311-
<span data-slot="option-label">{opt.label}</span>
312-
<Show when={opt.description}>
313-
<span data-slot="option-description">{opt.description}</span>
314-
</Show>
383+
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
384+
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
315385
</span>
316386
</button>
317-
)
318-
}}
319-
</For>
320-
321-
<Show
322-
when={store.editing}
323-
fallback={
324-
<button
387+
}
388+
>
389+
<form
325390
data-slot="question-option"
326391
data-custom="true"
327392
data-picked={on()}
328393
role={multi() ? "checkbox" : "radio"}
329394
aria-checked={on()}
330-
disabled={store.sending}
331-
onClick={customOpen}
395+
onMouseDown={(e) => {
396+
if (store.sending) {
397+
e.preventDefault()
398+
return
399+
}
400+
if (e.target instanceof HTMLTextAreaElement) return
401+
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
402+
if (input instanceof HTMLTextAreaElement) input.focus()
403+
}}
404+
onSubmit={(e) => {
405+
e.preventDefault()
406+
commitCustom()
407+
}}
332408
>
333409
<span
334410
data-slot="question-option-check"
@@ -347,80 +423,39 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
347423
</span>
348424
<span data-slot="question-option-main">
349425
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
350-
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
351-
</span>
352-
</button>
353-
}
354-
>
355-
<form
356-
data-slot="question-option"
357-
data-custom="true"
358-
data-picked={on()}
359-
role={multi() ? "checkbox" : "radio"}
360-
aria-checked={on()}
361-
onMouseDown={(e) => {
362-
if (store.sending) {
363-
e.preventDefault()
364-
return
365-
}
366-
if (e.target instanceof HTMLTextAreaElement) return
367-
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
368-
if (input instanceof HTMLTextAreaElement) input.focus()
369-
}}
370-
onSubmit={(e) => {
371-
e.preventDefault()
372-
commitCustom()
373-
}}
374-
>
375-
<span
376-
data-slot="question-option-check"
377-
aria-hidden="true"
378-
onClick={(e) => {
379-
e.preventDefault()
380-
e.stopPropagation()
381-
customToggle()
382-
}}
383-
>
384-
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
385-
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
386-
<Icon name="check-small" size="small" />
387-
</Show>
388-
</span>
389-
</span>
390-
<span data-slot="question-option-main">
391-
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
392-
<textarea
393-
ref={(el) =>
394-
setTimeout(() => {
395-
el.focus()
396-
el.style.height = "0px"
397-
el.style.height = `${el.scrollHeight}px`
398-
}, 0)
399-
}
400-
data-slot="question-custom-input"
401-
placeholder={language.t("ui.question.custom.placeholder")}
402-
value={input()}
403-
rows={1}
404-
disabled={store.sending}
405-
onKeyDown={(e) => {
406-
if (e.key === "Escape") {
407-
e.preventDefault()
408-
setStore("editing", false)
409-
return
426+
<textarea
427+
ref={(el) =>
428+
setTimeout(() => {
429+
el.focus()
430+
el.style.height = "0px"
431+
el.style.height = `${el.scrollHeight}px`
432+
}, 0)
410433
}
411-
if (e.key !== "Enter" || e.shiftKey) return
412-
e.preventDefault()
413-
commitCustom()
414-
}}
415-
onInput={(e) => {
416-
customUpdate(e.currentTarget.value)
417-
e.currentTarget.style.height = "0px"
418-
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
419-
}}
420-
/>
421-
</span>
422-
</form>
423-
</Show>
434+
data-slot="question-custom-input"
435+
placeholder={language.t("ui.question.custom.placeholder")}
436+
value={input()}
437+
rows={1}
438+
disabled={store.sending}
439+
onKeyDown={(e) => {
440+
if (e.key === "Escape") {
441+
e.preventDefault()
442+
setStore("editing", false)
443+
return
444+
}
445+
if (e.key !== "Enter" || e.shiftKey) return
446+
e.preventDefault()
447+
commitCustom()
448+
}}
449+
onInput={(e) => {
450+
customUpdate(e.currentTarget.value)
451+
e.currentTarget.style.height = "0px"
452+
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
453+
}}
454+
/>
455+
</span>
456+
</form>
457+
</Show>
458+
</div>
424459
</div>
425460
</DockPrompt>
426461
)

0 commit comments

Comments
 (0)