@schlessera/brain-ui-react 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/components/chat/ask-user-card.d.ts +12 -0
- package/dist/components/chat/ask-user-card.d.ts.map +1 -0
- package/dist/components/chat/ask-user-card.js +185 -0
- package/dist/components/chat/ask-user-card.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +35 -0
- package/dist/components/chat/brain-markdown.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown.js +363 -0
- package/dist/components/chat/brain-markdown.js.map +1 -0
- package/dist/components/chat/chat-page.d.ts +2 -0
- package/dist/components/chat/chat-page.d.ts.map +1 -0
- package/dist/components/chat/chat-page.js +442 -0
- package/dist/components/chat/chat-page.js.map +1 -0
- package/dist/components/chat/command-palette.d.ts +5 -0
- package/dist/components/chat/command-palette.d.ts.map +1 -0
- package/dist/components/chat/command-palette.js +18 -0
- package/dist/components/chat/command-palette.js.map +1 -0
- package/dist/components/chat/markdown-content.d.ts +4 -0
- package/dist/components/chat/markdown-content.d.ts.map +1 -0
- package/dist/components/chat/markdown-content.js +6 -0
- package/dist/components/chat/markdown-content.js.map +1 -0
- package/dist/components/chat/message-bubble.d.ts +9 -0
- package/dist/components/chat/message-bubble.d.ts.map +1 -0
- package/dist/components/chat/message-bubble.js +129 -0
- package/dist/components/chat/message-bubble.js.map +1 -0
- package/dist/components/chat/message-share.d.ts +10 -0
- package/dist/components/chat/message-share.d.ts.map +1 -0
- package/dist/components/chat/message-share.js +65 -0
- package/dist/components/chat/message-share.js.map +1 -0
- package/dist/components/chat/renderers/claude-tools.d.ts +3 -0
- package/dist/components/chat/renderers/claude-tools.d.ts.map +1 -0
- package/dist/components/chat/renderers/claude-tools.js +38 -0
- package/dist/components/chat/renderers/claude-tools.js.map +1 -0
- package/dist/components/chat/renderers/generic.d.ts +9 -0
- package/dist/components/chat/renderers/generic.d.ts.map +1 -0
- package/dist/components/chat/renderers/generic.js +68 -0
- package/dist/components/chat/renderers/generic.js.map +1 -0
- package/dist/components/chat/renderers/index.d.ts +5 -0
- package/dist/components/chat/renderers/index.d.ts.map +1 -0
- package/dist/components/chat/renderers/index.js +17 -0
- package/dist/components/chat/renderers/index.js.map +1 -0
- package/dist/components/chat/risk-hints.d.ts +20 -0
- package/dist/components/chat/risk-hints.d.ts.map +1 -0
- package/dist/components/chat/risk-hints.js +81 -0
- package/dist/components/chat/risk-hints.js.map +1 -0
- package/dist/components/chat/session-drawer.d.ts +6 -0
- package/dist/components/chat/session-drawer.d.ts.map +1 -0
- package/dist/components/chat/session-drawer.js +96 -0
- package/dist/components/chat/session-drawer.js.map +1 -0
- package/dist/components/chat/share-block.d.ts +9 -0
- package/dist/components/chat/share-block.d.ts.map +1 -0
- package/dist/components/chat/share-block.js +70 -0
- package/dist/components/chat/share-block.js.map +1 -0
- package/dist/components/chat/tool-call-timeline.d.ts +8 -0
- package/dist/components/chat/tool-call-timeline.d.ts.map +1 -0
- package/dist/components/chat/tool-call-timeline.js +100 -0
- package/dist/components/chat/tool-call-timeline.js.map +1 -0
- package/dist/components/chat/tool-views.d.ts +116 -0
- package/dist/components/chat/tool-views.d.ts.map +1 -0
- package/dist/components/chat/tool-views.js +628 -0
- package/dist/components/chat/tool-views.js.map +1 -0
- package/dist/components/chat/welcome-state.d.ts +4 -0
- package/dist/components/chat/welcome-state.d.ts.map +1 -0
- package/dist/components/chat/welcome-state.js +10 -0
- package/dist/components/chat/welcome-state.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts +5 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -0
- package/dist/components/connectivity/connection-gate.js +42 -0
- package/dist/components/connectivity/connection-gate.js.map +1 -0
- package/dist/components/connectivity/login-screen.d.ts +9 -0
- package/dist/components/connectivity/login-screen.d.ts.map +1 -0
- package/dist/components/connectivity/login-screen.js +99 -0
- package/dist/components/connectivity/login-screen.js.map +1 -0
- package/dist/components/files/file-panel.d.ts +5 -0
- package/dist/components/files/file-panel.d.ts.map +1 -0
- package/dist/components/files/file-panel.js +46 -0
- package/dist/components/files/file-panel.js.map +1 -0
- package/dist/components/files/file-tree.d.ts +2 -0
- package/dist/components/files/file-tree.d.ts.map +1 -0
- package/dist/components/files/file-tree.js +63 -0
- package/dist/components/files/file-tree.js.map +1 -0
- package/dist/components/files/file-viewer-binary.d.ts +5 -0
- package/dist/components/files/file-viewer-binary.d.ts.map +1 -0
- package/dist/components/files/file-viewer-binary.js +20 -0
- package/dist/components/files/file-viewer-binary.js.map +1 -0
- package/dist/components/files/file-viewer-html.d.ts +4 -0
- package/dist/components/files/file-viewer-html.d.ts.map +1 -0
- package/dist/components/files/file-viewer-html.js +5 -0
- package/dist/components/files/file-viewer-html.js.map +1 -0
- package/dist/components/files/file-viewer-markdown.d.ts +4 -0
- package/dist/components/files/file-viewer-markdown.d.ts.map +1 -0
- package/dist/components/files/file-viewer-markdown.js +10 -0
- package/dist/components/files/file-viewer-markdown.js.map +1 -0
- package/dist/components/files/file-viewer-raw.d.ts +5 -0
- package/dist/components/files/file-viewer-raw.d.ts.map +1 -0
- package/dist/components/files/file-viewer-raw.js +15 -0
- package/dist/components/files/file-viewer-raw.js.map +1 -0
- package/dist/components/files/file-viewer.d.ts +2 -0
- package/dist/components/files/file-viewer.d.ts.map +1 -0
- package/dist/components/files/file-viewer.js +175 -0
- package/dist/components/files/file-viewer.js.map +1 -0
- package/dist/components/files/frontmatter-panel.d.ts +5 -0
- package/dist/components/files/frontmatter-panel.d.ts.map +1 -0
- package/dist/components/files/frontmatter-panel.js +12 -0
- package/dist/components/files/frontmatter-panel.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts +5 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -0
- package/dist/components/layout/app-shell.js +10 -0
- package/dist/components/layout/app-shell.js.map +1 -0
- package/dist/components/layout/mobile-tab-bar.d.ts +2 -0
- package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -0
- package/dist/components/layout/mobile-tab-bar.js +16 -0
- package/dist/components/layout/mobile-tab-bar.js.map +1 -0
- package/dist/components/layout/side-rail.d.ts +2 -0
- package/dist/components/layout/side-rail.d.ts.map +1 -0
- package/dist/components/layout/side-rail.js +26 -0
- package/dist/components/layout/side-rail.js.map +1 -0
- package/dist/components/layout/slide-panel.d.ts +9 -0
- package/dist/components/layout/slide-panel.d.ts.map +1 -0
- package/dist/components/layout/slide-panel.js +18 -0
- package/dist/components/layout/slide-panel.js.map +1 -0
- package/dist/components/quick-actions/streaming-modal.d.ts +8 -0
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/streaming-modal.js +92 -0
- package/dist/components/quick-actions/streaming-modal.js.map +1 -0
- package/dist/components/quick-actions/whatsup-modal.d.ts +5 -0
- package/dist/components/quick-actions/whatsup-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/whatsup-modal.js +82 -0
- package/dist/components/quick-actions/whatsup-modal.js.map +1 -0
- package/dist/components/settings/passkey-panel.d.ts +10 -0
- package/dist/components/settings/passkey-panel.d.ts.map +1 -0
- package/dist/components/settings/passkey-panel.js +118 -0
- package/dist/components/settings/passkey-panel.js.map +1 -0
- package/dist/components/share/share-menu.d.ts +23 -0
- package/dist/components/share/share-menu.d.ts.map +1 -0
- package/dist/components/share/share-menu.js +58 -0
- package/dist/components/share/share-menu.js.map +1 -0
- package/dist/components/voice/dictation-sheet.d.ts +6 -0
- package/dist/components/voice/dictation-sheet.d.ts.map +1 -0
- package/dist/components/voice/dictation-sheet.js +91 -0
- package/dist/components/voice/dictation-sheet.js.map +1 -0
- package/dist/components/voice/mic-button.d.ts +6 -0
- package/dist/components/voice/mic-button.d.ts.map +1 -0
- package/dist/components/voice/mic-button.js +9 -0
- package/dist/components/voice/mic-button.js.map +1 -0
- package/dist/components/voice/review-card.d.ts +8 -0
- package/dist/components/voice/review-card.d.ts.map +1 -0
- package/dist/components/voice/review-card.js +8 -0
- package/dist/components/voice/review-card.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -0
- package/dist/hooks/use-vpn-status.d.ts +2 -0
- package/dist/hooks/use-vpn-status.d.ts.map +1 -0
- package/dist/hooks/use-vpn-status.js +71 -0
- package/dist/hooks/use-vpn-status.js.map +1 -0
- package/dist/hooks/use-websocket.d.ts +8 -0
- package/dist/hooks/use-websocket.d.ts.map +1 -0
- package/dist/hooks/use-websocket.js +366 -0
- package/dist/hooks/use-websocket.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +115 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +70 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/backend.d.ts +7 -0
- package/dist/lib/backend.d.ts.map +1 -0
- package/dist/lib/backend.js +33 -0
- package/dist/lib/backend.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +21 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +88 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/image-attachments.d.ts +36 -0
- package/dist/lib/image-attachments.d.ts.map +1 -0
- package/dist/lib/image-attachments.js +175 -0
- package/dist/lib/image-attachments.js.map +1 -0
- package/dist/lib/passkeys.d.ts +18 -0
- package/dist/lib/passkeys.d.ts.map +1 -0
- package/dist/lib/passkeys.js +36 -0
- package/dist/lib/passkeys.js.map +1 -0
- package/dist/lib/share.d.ts +53 -0
- package/dist/lib/share.d.ts.map +1 -0
- package/dist/lib/share.js +144 -0
- package/dist/lib/share.js.map +1 -0
- package/dist/lib/strip-markdown.d.ts +10 -0
- package/dist/lib/strip-markdown.d.ts.map +1 -0
- package/dist/lib/strip-markdown.js +37 -0
- package/dist/lib/strip-markdown.js.map +1 -0
- package/dist/lib/tool-names.d.ts +17 -0
- package/dist/lib/tool-names.d.ts.map +1 -0
- package/dist/lib/tool-names.js +23 -0
- package/dist/lib/tool-names.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/ws-client.d.ts +26 -0
- package/dist/lib/ws-client.d.ts.map +1 -0
- package/dist/lib/ws-client.js +93 -0
- package/dist/lib/ws-client.js.map +1 -0
- package/dist/stores/chat-store.d.ts +131 -0
- package/dist/stores/chat-store.d.ts.map +1 -0
- package/dist/stores/chat-store.js +376 -0
- package/dist/stores/chat-store.js.map +1 -0
- package/dist/stores/connection-store.d.ts +11 -0
- package/dist/stores/connection-store.d.ts.map +1 -0
- package/dist/stores/connection-store.js +8 -0
- package/dist/stores/connection-store.js.map +1 -0
- package/dist/stores/file-store.d.ts +53 -0
- package/dist/stores/file-store.d.ts.map +1 -0
- package/dist/stores/file-store.js +284 -0
- package/dist/stores/file-store.js.map +1 -0
- package/dist/stores/provider-store.d.ts +24 -0
- package/dist/stores/provider-store.d.ts.map +1 -0
- package/dist/stores/provider-store.js +56 -0
- package/dist/stores/provider-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +21 -0
- package/dist/stores/ui-store.d.ts.map +1 -0
- package/dist/stores/ui-store.js +23 -0
- package/dist/stores/ui-store.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/voice/asr-clients.d.ts +2 -0
- package/dist/voice/asr-clients.d.ts.map +1 -0
- package/dist/voice/asr-clients.js +33 -0
- package/dist/voice/asr-clients.js.map +1 -0
- package/dist/voice/asr-deepgram.d.ts +38 -0
- package/dist/voice/asr-deepgram.d.ts.map +1 -0
- package/dist/voice/asr-deepgram.js +307 -0
- package/dist/voice/asr-deepgram.js.map +1 -0
- package/dist/voice/asr-webspeech.d.ts +23 -0
- package/dist/voice/asr-webspeech.d.ts.map +1 -0
- package/dist/voice/asr-webspeech.js +93 -0
- package/dist/voice/asr-webspeech.js.map +1 -0
- package/dist/voice/use-dictation.d.ts +6 -0
- package/dist/voice/use-dictation.d.ts.map +1 -0
- package/dist/voice/use-dictation.js +174 -0
- package/dist/voice/use-dictation.js.map +1 -0
- package/dist/voice/voice-store.d.ts +26 -0
- package/dist/voice/voice-store.d.ts.map +1 -0
- package/dist/voice/voice-store.js +27 -0
- package/dist/voice/voice-store.js.map +1 -0
- package/package.json +71 -0
- package/src/components/chat/ask-user-card.tsx +451 -0
- package/src/components/chat/brain-markdown.tsx +547 -0
- package/src/components/chat/chat-page.tsx +820 -0
- package/src/components/chat/command-palette.tsx +68 -0
- package/src/components/chat/markdown-content.tsx +5 -0
- package/src/components/chat/message-bubble.tsx +362 -0
- package/src/components/chat/message-share.ts +78 -0
- package/src/components/chat/renderers/claude-tools.tsx +53 -0
- package/src/components/chat/renderers/generic.tsx +85 -0
- package/src/components/chat/renderers/index.ts +19 -0
- package/src/components/chat/risk-hints.ts +95 -0
- package/src/components/chat/session-drawer.tsx +213 -0
- package/src/components/chat/share-block.tsx +128 -0
- package/src/components/chat/tool-call-timeline.tsx +322 -0
- package/src/components/chat/tool-views.tsx +898 -0
- package/src/components/chat/welcome-state.tsx +105 -0
- package/src/components/connectivity/connection-gate.tsx +137 -0
- package/src/components/connectivity/login-screen.tsx +169 -0
- package/src/components/files/file-panel.tsx +116 -0
- package/src/components/files/file-tree.tsx +143 -0
- package/src/components/files/file-viewer-binary.tsx +63 -0
- package/src/components/files/file-viewer-html.tsx +10 -0
- package/src/components/files/file-viewer-markdown.tsx +14 -0
- package/src/components/files/file-viewer-raw.tsx +44 -0
- package/src/components/files/file-viewer.tsx +290 -0
- package/src/components/files/frontmatter-panel.tsx +78 -0
- package/src/components/layout/app-shell.tsx +25 -0
- package/src/components/layout/mobile-tab-bar.tsx +54 -0
- package/src/components/layout/side-rail.tsx +128 -0
- package/src/components/layout/slide-panel.tsx +64 -0
- package/src/components/quick-actions/streaming-modal.tsx +168 -0
- package/src/components/quick-actions/whatsup-modal.tsx +149 -0
- package/src/components/settings/passkey-panel.tsx +281 -0
- package/src/components/share/share-menu.tsx +121 -0
- package/src/components/voice/dictation-sheet.tsx +217 -0
- package/src/components/voice/mic-button.tsx +31 -0
- package/src/components/voice/review-card.tsx +66 -0
- package/src/config.ts +27 -0
- package/src/hooks/use-vpn-status.ts +75 -0
- package/src/hooks/use-websocket.ts +403 -0
- package/src/index.ts +46 -0
- package/src/lib/api-client.ts +173 -0
- package/src/lib/backend.ts +35 -0
- package/src/lib/frontmatter.ts +99 -0
- package/src/lib/image-attachments.ts +221 -0
- package/src/lib/passkeys.ts +50 -0
- package/src/lib/share.ts +167 -0
- package/src/lib/strip-markdown.ts +38 -0
- package/src/lib/tool-names.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/ws-client.ts +109 -0
- package/src/stores/chat-store.ts +619 -0
- package/src/stores/connection-store.ts +23 -0
- package/src/stores/file-store.ts +344 -0
- package/src/stores/provider-store.ts +85 -0
- package/src/stores/ui-store.ts +48 -0
- package/src/styles.css +4 -0
- package/src/theme.css +425 -0
- package/src/voice/asr-clients.ts +45 -0
- package/src/voice/asr-deepgram.ts +325 -0
- package/src/voice/asr-webspeech.ts +135 -0
- package/src/voice/use-dictation.ts +187 -0
- package/src/voice/voice-store.ts +55 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { uiConfig } from "../../config.js";
|
|
2
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Check,
|
|
5
|
+
X,
|
|
6
|
+
MessageCircleQuestion,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
ChevronUp,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import type {
|
|
11
|
+
AskUserQuestion,
|
|
12
|
+
AskUserOption,
|
|
13
|
+
AskUserAnnotation,
|
|
14
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
15
|
+
import { BrainMarkdown } from "./brain-markdown.js";
|
|
16
|
+
import { cn } from "../../lib/utils.js";
|
|
17
|
+
|
|
18
|
+
const OTHER_LABEL = "Other";
|
|
19
|
+
|
|
20
|
+
interface PerQuestionState {
|
|
21
|
+
/** Picked option labels (single-select keeps only the latest). */
|
|
22
|
+
selected: string[];
|
|
23
|
+
/** Free-text content when "Other" is selected. */
|
|
24
|
+
otherText: string;
|
|
25
|
+
/** Currently focused option index — drives the preview pane. */
|
|
26
|
+
focusedIndex: number | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultState(): PerQuestionState {
|
|
30
|
+
return { selected: [], otherText: "", focusedIndex: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function AskUserCard({
|
|
34
|
+
requestId,
|
|
35
|
+
questions,
|
|
36
|
+
answered,
|
|
37
|
+
cancelled,
|
|
38
|
+
live = false,
|
|
39
|
+
onSubmit,
|
|
40
|
+
onCancel,
|
|
41
|
+
}: {
|
|
42
|
+
requestId: string;
|
|
43
|
+
questions: AskUserQuestion[];
|
|
44
|
+
answered?: Record<string, string>;
|
|
45
|
+
cancelled?: boolean;
|
|
46
|
+
/** Parent message still streaming — keeps an expanded card open until it ends. */
|
|
47
|
+
live?: boolean;
|
|
48
|
+
onSubmit: (
|
|
49
|
+
requestId: string,
|
|
50
|
+
answers: Record<string, string>,
|
|
51
|
+
annotations?: Record<string, AskUserAnnotation>
|
|
52
|
+
) => void;
|
|
53
|
+
onCancel: (requestId: string) => void;
|
|
54
|
+
}) {
|
|
55
|
+
|
|
56
|
+
// Independent per-question state
|
|
57
|
+
const [state, setState] = useState<PerQuestionState[]>(() =>
|
|
58
|
+
questions.map(() => defaultState())
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Once answered/dismissed, the card rolls up to a one-line summary that
|
|
62
|
+
// scrolls away with the rest of the turn. It stays open while unanswered
|
|
63
|
+
// (it's an active prompt) and re-collapses when the streaming turn ends.
|
|
64
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
65
|
+
const prevLive = useRef(live);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (prevLive.current && !live) setCollapsed(true);
|
|
68
|
+
prevLive.current = live;
|
|
69
|
+
}, [live]);
|
|
70
|
+
|
|
71
|
+
// Re-initialise if the question set changes (new requestId arriving)
|
|
72
|
+
const lastRequestId = useRef(requestId);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (lastRequestId.current !== requestId) {
|
|
75
|
+
lastRequestId.current = requestId;
|
|
76
|
+
setState(questions.map(() => defaultState()));
|
|
77
|
+
}
|
|
78
|
+
}, [requestId, questions]);
|
|
79
|
+
|
|
80
|
+
// If we already have answers (from prior submit), force-fill state for display
|
|
81
|
+
const lockedAnswers = answered ?? null;
|
|
82
|
+
const isLocked = !!lockedAnswers || !!cancelled;
|
|
83
|
+
|
|
84
|
+
const filledState = useMemo<PerQuestionState[]>(() => {
|
|
85
|
+
if (!lockedAnswers) return state;
|
|
86
|
+
return questions.map((q) => {
|
|
87
|
+
const raw = lockedAnswers[q.question] ?? "";
|
|
88
|
+
const labels = q.multiSelect
|
|
89
|
+
? raw
|
|
90
|
+
.split(",")
|
|
91
|
+
.map((s) => s.trim())
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
: raw
|
|
94
|
+
? [raw]
|
|
95
|
+
: [];
|
|
96
|
+
// If the saved label isn't a known option, treat as "Other"
|
|
97
|
+
const knownLabels = new Set(q.options.map((o) => o.label));
|
|
98
|
+
const selected = labels.filter((l) => knownLabels.has(l));
|
|
99
|
+
const otherLabels = labels.filter((l) => !knownLabels.has(l));
|
|
100
|
+
const finalSelected = otherLabels.length
|
|
101
|
+
? [...selected, OTHER_LABEL]
|
|
102
|
+
: selected;
|
|
103
|
+
return {
|
|
104
|
+
selected: finalSelected,
|
|
105
|
+
otherText: otherLabels.join(", "),
|
|
106
|
+
focusedIndex: null,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}, [lockedAnswers, questions, state]);
|
|
110
|
+
|
|
111
|
+
// Collapsed once locked: a compact, expandable summary that behaves like a
|
|
112
|
+
// finished tool-timeline row instead of a full card pinned in place.
|
|
113
|
+
if (isLocked && collapsed) {
|
|
114
|
+
return (
|
|
115
|
+
<AskUserSummaryRow
|
|
116
|
+
questions={questions}
|
|
117
|
+
answers={lockedAnswers}
|
|
118
|
+
cancelled={!!cancelled}
|
|
119
|
+
onExpand={() => setCollapsed(false)}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toggleOption(qi: number, label: string) {
|
|
125
|
+
if (isLocked) return;
|
|
126
|
+
setState((prev) => {
|
|
127
|
+
const next = prev.map((s, i) => {
|
|
128
|
+
if (i !== qi) return s;
|
|
129
|
+
const q = questions[qi];
|
|
130
|
+
if (q.multiSelect) {
|
|
131
|
+
const has = s.selected.includes(label);
|
|
132
|
+
return {
|
|
133
|
+
...s,
|
|
134
|
+
selected: has
|
|
135
|
+
? s.selected.filter((l) => l !== label)
|
|
136
|
+
: [...s.selected, label],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...s,
|
|
141
|
+
selected: s.selected[0] === label ? [] : [label],
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
return next;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setFocus(qi: number, idx: number | null) {
|
|
149
|
+
if (isLocked) return;
|
|
150
|
+
setState((prev) =>
|
|
151
|
+
prev.map((s, i) => (i === qi ? { ...s, focusedIndex: idx } : s))
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setOtherText(qi: number, text: string) {
|
|
156
|
+
if (isLocked) return;
|
|
157
|
+
setState((prev) =>
|
|
158
|
+
prev.map((s, i) => (i === qi ? { ...s, otherText: text } : s))
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const canSubmit = !isLocked && questions.every((_q, i) => {
|
|
163
|
+
const s = state[i];
|
|
164
|
+
if (s.selected.length === 0) return false;
|
|
165
|
+
if (s.selected.includes(OTHER_LABEL) && !s.otherText.trim()) return false;
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function handleSubmit() {
|
|
170
|
+
if (!canSubmit) return;
|
|
171
|
+
const answers: Record<string, string> = {};
|
|
172
|
+
const annotations: Record<string, AskUserAnnotation> = {};
|
|
173
|
+
for (let i = 0; i < questions.length; i++) {
|
|
174
|
+
const q = questions[i];
|
|
175
|
+
const s = state[i];
|
|
176
|
+
const labels: string[] = [];
|
|
177
|
+
let usedOther = false;
|
|
178
|
+
for (const sel of s.selected) {
|
|
179
|
+
if (sel === OTHER_LABEL) {
|
|
180
|
+
if (s.otherText.trim()) labels.push(s.otherText.trim());
|
|
181
|
+
usedOther = true;
|
|
182
|
+
} else {
|
|
183
|
+
labels.push(sel);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
answers[q.question] = q.multiSelect ? labels.join(", ") : (labels[0] ?? "");
|
|
187
|
+
|
|
188
|
+
// Capture preview from a single non-other selection for annotations
|
|
189
|
+
if (!usedOther && !q.multiSelect && s.selected.length === 1) {
|
|
190
|
+
const picked = q.options.find((o) => o.label === s.selected[0]);
|
|
191
|
+
if (picked?.preview) {
|
|
192
|
+
annotations[q.question] = { preview: picked.preview };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
onSubmit(
|
|
197
|
+
requestId,
|
|
198
|
+
answers,
|
|
199
|
+
Object.keys(annotations).length ? annotations : undefined
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleCancel() {
|
|
204
|
+
onCancel(requestId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="rounded-xl border border-accent/30 bg-surface px-4 py-4 shadow-[0_0_0_1px_rgba(91,181,162,0.08)]">
|
|
209
|
+
<div className="mb-3 flex items-center gap-2 text-xs uppercase tracking-wider text-accent/80">
|
|
210
|
+
<MessageCircleQuestion className="h-3.5 w-3.5" />
|
|
211
|
+
<span className="font-[family-name:var(--font-mono)]">
|
|
212
|
+
{isLocked
|
|
213
|
+
? cancelled
|
|
214
|
+
? "Dismissed"
|
|
215
|
+
: "You answered"
|
|
216
|
+
: `${uiConfig.assistantName} needs your input`}
|
|
217
|
+
</span>
|
|
218
|
+
{isLocked && (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={() => setCollapsed(true)}
|
|
222
|
+
className="ml-auto flex items-center gap-1 text-[11px] normal-case tracking-normal text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
|
223
|
+
>
|
|
224
|
+
<ChevronUp className="h-3 w-3" />
|
|
225
|
+
Collapse
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="space-y-5">
|
|
231
|
+
{questions.map((q, qi) => (
|
|
232
|
+
<QuestionBlock
|
|
233
|
+
key={qi}
|
|
234
|
+
question={q}
|
|
235
|
+
state={filledState[qi]}
|
|
236
|
+
locked={isLocked}
|
|
237
|
+
onToggleOption={(label) => toggleOption(qi, label)}
|
|
238
|
+
onFocus={(idx) => setFocus(qi, idx)}
|
|
239
|
+
onOtherChange={(t) => setOtherText(qi, t)}
|
|
240
|
+
/>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{!isLocked && (
|
|
245
|
+
<div className="mt-4 flex items-center justify-end gap-2">
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={handleCancel}
|
|
249
|
+
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
|
|
250
|
+
>
|
|
251
|
+
<X className="h-3.5 w-3.5" />
|
|
252
|
+
Dismiss
|
|
253
|
+
</button>
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
onClick={handleSubmit}
|
|
257
|
+
disabled={!canSubmit}
|
|
258
|
+
className={cn(
|
|
259
|
+
"flex items-center gap-1.5 rounded-lg px-4 py-1.5 text-xs font-medium transition-all",
|
|
260
|
+
canSubmit
|
|
261
|
+
? "bg-accent text-accent-foreground hover:brightness-110"
|
|
262
|
+
: "bg-surface-raised text-muted-foreground/40 cursor-not-allowed"
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
<Check className="h-3.5 w-3.5" />
|
|
266
|
+
Submit
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** One-line, expandable stand-in for an answered/dismissed question. */
|
|
275
|
+
function AskUserSummaryRow({
|
|
276
|
+
questions,
|
|
277
|
+
answers,
|
|
278
|
+
cancelled,
|
|
279
|
+
onExpand,
|
|
280
|
+
}: {
|
|
281
|
+
questions: AskUserQuestion[];
|
|
282
|
+
answers: Record<string, string> | null;
|
|
283
|
+
cancelled: boolean;
|
|
284
|
+
onExpand: () => void;
|
|
285
|
+
}) {
|
|
286
|
+
const detail = cancelled
|
|
287
|
+
? "Dismissed"
|
|
288
|
+
: questions
|
|
289
|
+
.map((q) => {
|
|
290
|
+
const a = answers?.[q.question];
|
|
291
|
+
return a ? `${q.header}: ${a}` : q.header;
|
|
292
|
+
})
|
|
293
|
+
.join(" · ");
|
|
294
|
+
return (
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
onClick={onExpand}
|
|
298
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground/70 transition-colors hover:text-foreground"
|
|
299
|
+
>
|
|
300
|
+
<MessageCircleQuestion className="h-3.5 w-3.5 shrink-0 text-accent/70" />
|
|
301
|
+
<span className="min-w-0 truncate font-[family-name:var(--font-mono)]">
|
|
302
|
+
{!cancelled && <span className="text-accent/70">Answered · </span>}
|
|
303
|
+
{detail}
|
|
304
|
+
</span>
|
|
305
|
+
<ChevronRight className="h-3 w-3 shrink-0" />
|
|
306
|
+
</button>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function QuestionBlock({
|
|
311
|
+
question,
|
|
312
|
+
state,
|
|
313
|
+
locked,
|
|
314
|
+
onToggleOption,
|
|
315
|
+
onFocus,
|
|
316
|
+
onOtherChange,
|
|
317
|
+
}: {
|
|
318
|
+
question: AskUserQuestion;
|
|
319
|
+
state: PerQuestionState;
|
|
320
|
+
locked: boolean;
|
|
321
|
+
onToggleOption: (label: string) => void;
|
|
322
|
+
onFocus: (idx: number | null) => void;
|
|
323
|
+
onOtherChange: (text: string) => void;
|
|
324
|
+
}) {
|
|
325
|
+
const optionsWithOther: Array<AskUserOption & { isOther?: boolean }> = [
|
|
326
|
+
...question.options,
|
|
327
|
+
{ label: OTHER_LABEL, description: "Provide a custom answer.", isOther: true },
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
const focusedOption =
|
|
331
|
+
state.focusedIndex !== null ? optionsWithOther[state.focusedIndex] : null;
|
|
332
|
+
const previewToShow =
|
|
333
|
+
focusedOption?.preview ??
|
|
334
|
+
(state.selected.length === 1 && !state.selected.includes(OTHER_LABEL)
|
|
335
|
+
? question.options.find((o) => o.label === state.selected[0])?.preview
|
|
336
|
+
: undefined);
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div>
|
|
340
|
+
<div className="mb-2 flex items-center gap-2">
|
|
341
|
+
<span className="rounded-md border border-accent/30 bg-accent/10 px-2 py-0.5 font-[family-name:var(--font-mono)] text-[10px] uppercase tracking-wider text-accent">
|
|
342
|
+
{question.header}
|
|
343
|
+
</span>
|
|
344
|
+
{question.multiSelect && (
|
|
345
|
+
<span className="text-[10px] uppercase tracking-wider text-muted-foreground/60">
|
|
346
|
+
multi-select
|
|
347
|
+
</span>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
<div className="mb-3 text-sm leading-relaxed text-foreground">
|
|
351
|
+
{question.question}
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<div className="grid gap-2 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
|
355
|
+
<ul className="space-y-1.5">
|
|
356
|
+
{optionsWithOther.map((opt, idx) => {
|
|
357
|
+
const selected = state.selected.includes(opt.label);
|
|
358
|
+
return (
|
|
359
|
+
<li key={opt.label}>
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
onClick={() => onToggleOption(opt.label)}
|
|
363
|
+
onMouseEnter={() => onFocus(idx)}
|
|
364
|
+
onFocus={() => onFocus(idx)}
|
|
365
|
+
disabled={locked}
|
|
366
|
+
className={cn(
|
|
367
|
+
"group flex w-full items-start gap-2 rounded-lg border px-3 py-2 text-left transition-colors",
|
|
368
|
+
selected
|
|
369
|
+
? "border-accent/60 bg-accent/10"
|
|
370
|
+
: "border-border bg-background hover:bg-surface-raised",
|
|
371
|
+
locked && "cursor-default opacity-90"
|
|
372
|
+
)}
|
|
373
|
+
>
|
|
374
|
+
<span
|
|
375
|
+
className={cn(
|
|
376
|
+
"mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border",
|
|
377
|
+
question.multiSelect ? "rounded-sm" : "rounded-full",
|
|
378
|
+
selected
|
|
379
|
+
? "border-accent bg-accent text-accent-foreground"
|
|
380
|
+
: "border-border bg-background"
|
|
381
|
+
)}
|
|
382
|
+
>
|
|
383
|
+
{selected && <Check className="h-3 w-3" />}
|
|
384
|
+
</span>
|
|
385
|
+
<span className="min-w-0 flex-1">
|
|
386
|
+
<span
|
|
387
|
+
className={cn(
|
|
388
|
+
"block text-sm font-medium",
|
|
389
|
+
opt.isOther
|
|
390
|
+
? "italic text-muted-foreground group-hover:text-foreground"
|
|
391
|
+
: "text-foreground"
|
|
392
|
+
)}
|
|
393
|
+
>
|
|
394
|
+
{opt.label}
|
|
395
|
+
</span>
|
|
396
|
+
{opt.description && (
|
|
397
|
+
<span className="block text-xs leading-snug text-muted-foreground">
|
|
398
|
+
{opt.description}
|
|
399
|
+
</span>
|
|
400
|
+
)}
|
|
401
|
+
</span>
|
|
402
|
+
</button>
|
|
403
|
+
{opt.isOther && state.selected.includes(OTHER_LABEL) && (
|
|
404
|
+
<textarea
|
|
405
|
+
value={state.otherText}
|
|
406
|
+
onChange={(e) => onOtherChange(e.target.value)}
|
|
407
|
+
disabled={locked}
|
|
408
|
+
placeholder="Type your answer..."
|
|
409
|
+
rows={2}
|
|
410
|
+
className="mt-1.5 w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground/40 focus:border-accent/60 focus:outline-none"
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
</li>
|
|
414
|
+
);
|
|
415
|
+
})}
|
|
416
|
+
</ul>
|
|
417
|
+
|
|
418
|
+
{previewToShow && (
|
|
419
|
+
<div className="hidden min-w-0 md:block">
|
|
420
|
+
<div className="rounded-lg border border-border bg-background p-3">
|
|
421
|
+
<div className="mb-1 text-[10px] uppercase tracking-wider text-muted-foreground/60 font-[family-name:var(--font-mono)]">
|
|
422
|
+
Preview
|
|
423
|
+
</div>
|
|
424
|
+
<BrainMarkdown
|
|
425
|
+
content={previewToShow}
|
|
426
|
+
className="brain-prose text-xs"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{/* Mobile preview drops below the options when a preview exists */}
|
|
434
|
+
{previewToShow && (
|
|
435
|
+
<div className="mt-2 md:hidden">
|
|
436
|
+
<details className="rounded-lg border border-border bg-background">
|
|
437
|
+
<summary className="cursor-pointer px-3 py-2 text-[11px] uppercase tracking-wider text-muted-foreground">
|
|
438
|
+
Preview
|
|
439
|
+
</summary>
|
|
440
|
+
<div className="border-t border-border px-3 py-2">
|
|
441
|
+
<BrainMarkdown
|
|
442
|
+
content={previewToShow}
|
|
443
|
+
className="brain-prose text-xs"
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
</details>
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|