mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-20 15:25:50 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,38 +1,51 @@
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../../keybindings/useKeybinding.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||||
import { getExternalEditor } from '../../../utils/editor.js';
|
||||
import { toIDEDisplayName } from '../../../utils/ide.js';
|
||||
import { editPromptInEditor } from '../../../utils/promptEditor.js';
|
||||
import { Divider } from '../../design-system/Divider.js';
|
||||
import TextInput from '../../TextInput.js';
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
|
||||
import { PreviewBox } from './PreviewBox.js';
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
|
||||
import type { QuestionState } from './use-multiple-choice-state.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import {
|
||||
useKeybinding,
|
||||
useKeybindings,
|
||||
} from '../../../keybindings/useKeybinding.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
import { getExternalEditor } from '../../../utils/editor.js'
|
||||
import { toIDEDisplayName } from '../../../utils/ide.js'
|
||||
import { editPromptInEditor } from '../../../utils/promptEditor.js'
|
||||
import { Divider } from '../../design-system/Divider.js'
|
||||
import TextInput from '../../TextInput.js'
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
|
||||
import { PreviewBox } from './PreviewBox.js'
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
|
||||
import type { QuestionState } from './use-multiple-choice-state.js'
|
||||
|
||||
type Props = {
|
||||
question: Question;
|
||||
questions: Question[];
|
||||
currentQuestionIndex: number;
|
||||
answers: Record<string, string>;
|
||||
questionStates: Record<string, QuestionState>;
|
||||
hideSubmitTab?: boolean;
|
||||
minContentHeight?: number;
|
||||
minContentWidth?: number;
|
||||
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
|
||||
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
|
||||
onTextInputFocus: (isInInput: boolean) => void;
|
||||
onCancel: () => void;
|
||||
onTabPrev?: () => void;
|
||||
onTabNext?: () => void;
|
||||
onRespondToClaude: () => void;
|
||||
onFinishPlanInterview: () => void;
|
||||
};
|
||||
question: Question
|
||||
questions: Question[]
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, string>
|
||||
questionStates: Record<string, QuestionState>
|
||||
hideSubmitTab?: boolean
|
||||
minContentHeight?: number
|
||||
minContentWidth?: number
|
||||
onUpdateQuestionState: (
|
||||
questionText: string,
|
||||
updates: Partial<QuestionState>,
|
||||
isMultiSelect: boolean,
|
||||
) => void
|
||||
onAnswer: (
|
||||
questionText: string,
|
||||
label: string | string[],
|
||||
textInput?: string,
|
||||
shouldAdvance?: boolean,
|
||||
) => void
|
||||
onTextInputFocus: (isInInput: boolean) => void
|
||||
onCancel: () => void
|
||||
onTabPrev?: () => void
|
||||
onTabNext?: () => void
|
||||
onRespondToClaude: () => void
|
||||
onFinishPlanInterview: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A side-by-side question view for questions with preview content.
|
||||
@@ -54,188 +67,235 @@ export function PreviewQuestionView({
|
||||
onTabPrev,
|
||||
onTabNext,
|
||||
onRespondToClaude,
|
||||
onFinishPlanInterview
|
||||
onFinishPlanInterview,
|
||||
}: Props): React.ReactNode {
|
||||
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan';
|
||||
const [isFooterFocused, setIsFooterFocused] = useState(false);
|
||||
const [footerIndex, setFooterIndex] = useState(0);
|
||||
const [isInNotesInput, setIsInNotesInput] = useState(false);
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const editor = getExternalEditor();
|
||||
const editorName = editor ? toIDEDisplayName(editor) : null;
|
||||
const questionText = question.question;
|
||||
const questionState = questionStates[questionText];
|
||||
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'
|
||||
const [isFooterFocused, setIsFooterFocused] = useState(false)
|
||||
const [footerIndex, setFooterIndex] = useState(0)
|
||||
const [isInNotesInput, setIsInNotesInput] = useState(false)
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
|
||||
const editor = getExternalEditor()
|
||||
const editorName = editor ? toIDEDisplayName(editor) : null
|
||||
|
||||
const questionText = question.question
|
||||
const questionState = questionStates[questionText]
|
||||
|
||||
// Only real options — no "Other" for preview questions
|
||||
const allOptions = question.options;
|
||||
const allOptions = question.options
|
||||
|
||||
// Track which option is focused (for preview display)
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const [focusedIndex, setFocusedIndex] = useState(0)
|
||||
|
||||
// Reset focusedIndex when navigating to a different question
|
||||
const prevQuestionText = useRef(questionText);
|
||||
const prevQuestionText = useRef(questionText)
|
||||
if (prevQuestionText.current !== questionText) {
|
||||
prevQuestionText.current = questionText;
|
||||
const selected = questionState?.selectedValue as string | undefined;
|
||||
const idx = selected ? allOptions.findIndex(opt => opt.label === selected) : -1;
|
||||
setFocusedIndex(idx >= 0 ? idx : 0);
|
||||
prevQuestionText.current = questionText
|
||||
const selected = questionState?.selectedValue as string | undefined
|
||||
const idx = selected
|
||||
? allOptions.findIndex(opt => opt.label === selected)
|
||||
: -1
|
||||
setFocusedIndex(idx >= 0 ? idx : 0)
|
||||
}
|
||||
const focusedOption = allOptions[focusedIndex];
|
||||
const selectedValue = questionState?.selectedValue as string | undefined;
|
||||
const notesValue = questionState?.textInputValue || '';
|
||||
const handleSelectOption = useCallback((index: number) => {
|
||||
const option = allOptions[index];
|
||||
if (!option) return;
|
||||
setFocusedIndex(index);
|
||||
onUpdateQuestionState(questionText, {
|
||||
selectedValue: option.label
|
||||
}, false);
|
||||
onAnswer(questionText, option.label);
|
||||
}, [allOptions, questionText, onUpdateQuestionState, onAnswer]);
|
||||
const handleNavigate = useCallback((direction: 'up' | 'down' | number) => {
|
||||
if (isInNotesInput) return;
|
||||
let newIndex: number;
|
||||
if (typeof direction === 'number') {
|
||||
newIndex = direction;
|
||||
} else if (direction === 'up') {
|
||||
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex;
|
||||
} else {
|
||||
newIndex = focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex;
|
||||
}
|
||||
if (newIndex >= 0 && newIndex < allOptions.length) {
|
||||
setFocusedIndex(newIndex);
|
||||
}
|
||||
}, [focusedIndex, allOptions.length, isInNotesInput]);
|
||||
|
||||
const focusedOption = allOptions[focusedIndex]
|
||||
const selectedValue = questionState?.selectedValue as string | undefined
|
||||
const notesValue = questionState?.textInputValue || ''
|
||||
|
||||
const handleSelectOption = useCallback(
|
||||
(index: number) => {
|
||||
const option = allOptions[index]
|
||||
if (!option) return
|
||||
|
||||
setFocusedIndex(index)
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ selectedValue: option.label },
|
||||
false,
|
||||
)
|
||||
|
||||
onAnswer(questionText, option.label)
|
||||
},
|
||||
[allOptions, questionText, onUpdateQuestionState, onAnswer],
|
||||
)
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
(direction: 'up' | 'down' | number) => {
|
||||
if (isInNotesInput) return
|
||||
|
||||
let newIndex: number
|
||||
if (typeof direction === 'number') {
|
||||
newIndex = direction
|
||||
} else if (direction === 'up') {
|
||||
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex
|
||||
} else {
|
||||
newIndex =
|
||||
focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex
|
||||
}
|
||||
|
||||
if (newIndex >= 0 && newIndex < allOptions.length) {
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
},
|
||||
[focusedIndex, allOptions.length, isInNotesInput],
|
||||
)
|
||||
|
||||
// Handle ctrl+g to open external editor for notes
|
||||
useKeybinding('chat:externalEditor', async () => {
|
||||
const currentValue = questionState?.textInputValue || '';
|
||||
const result = await editPromptInEditor(currentValue);
|
||||
if (result.content !== null && result.content !== currentValue) {
|
||||
onUpdateQuestionState(questionText, {
|
||||
textInputValue: result.content
|
||||
}, false);
|
||||
}
|
||||
}, {
|
||||
context: 'Chat',
|
||||
isActive: isInNotesInput && !!editor
|
||||
});
|
||||
useKeybinding(
|
||||
'chat:externalEditor',
|
||||
async () => {
|
||||
const currentValue = questionState?.textInputValue || ''
|
||||
const result = await editPromptInEditor(currentValue)
|
||||
if (result.content !== null && result.content !== currentValue) {
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ textInputValue: result.content },
|
||||
false,
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Chat', isActive: isInNotesInput && !!editor },
|
||||
)
|
||||
|
||||
// Handle left/right arrow and tab for question navigation.
|
||||
// This must be in the child component (not just the parent) because child useInput
|
||||
// handlers register first on the event emitter and fire before parent handlers.
|
||||
// Without this, the parent's useKeybindings may not fire reliably depending on
|
||||
// listener ordering in the event emitter.
|
||||
useKeybindings({
|
||||
'tabs:previous': () => onTabPrev?.(),
|
||||
'tabs:next': () => onTabNext?.()
|
||||
}, {
|
||||
context: 'Tabs',
|
||||
isActive: !isInNotesInput && !isFooterFocused
|
||||
});
|
||||
useKeybindings(
|
||||
{
|
||||
'tabs:previous': () => onTabPrev?.(),
|
||||
'tabs:next': () => onTabNext?.(),
|
||||
},
|
||||
{ context: 'Tabs', isActive: !isInNotesInput && !isFooterFocused },
|
||||
)
|
||||
|
||||
// Re-submit the answer (plain label) when exiting notes input.
|
||||
// Notes are stored in questionStates and collected at submit time via annotations.
|
||||
const handleNotesExit = useCallback(() => {
|
||||
setIsInNotesInput(false);
|
||||
onTextInputFocus(false);
|
||||
setIsInNotesInput(false)
|
||||
onTextInputFocus(false)
|
||||
if (selectedValue) {
|
||||
onAnswer(questionText, selectedValue);
|
||||
onAnswer(questionText, selectedValue)
|
||||
}
|
||||
}, [selectedValue, questionText, onAnswer, onTextInputFocus]);
|
||||
}, [selectedValue, questionText, onAnswer, onTextInputFocus])
|
||||
|
||||
const handleDownFromPreview = useCallback(() => {
|
||||
setIsFooterFocused(true);
|
||||
}, []);
|
||||
setIsFooterFocused(true)
|
||||
}, [])
|
||||
|
||||
const handleUpFromFooter = useCallback(() => {
|
||||
setIsFooterFocused(false);
|
||||
}, []);
|
||||
setIsFooterFocused(false)
|
||||
}, [])
|
||||
|
||||
// Handle keyboard input for option/footer/notes navigation.
|
||||
// Always active — the handler routes internally based on isFooterFocused/isInNotesInput.
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (isFooterFocused) {
|
||||
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
if (footerIndex === 0) {
|
||||
handleUpFromFooter();
|
||||
} else {
|
||||
setFooterIndex(0);
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (isFooterFocused) {
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
if (footerIndex === 0) {
|
||||
handleUpFromFooter()
|
||||
} else {
|
||||
setFooterIndex(0)
|
||||
}
|
||||
return
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||||
e.preventDefault();
|
||||
if (isInPlanMode && footerIndex === 0) {
|
||||
setFooterIndex(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault();
|
||||
if (footerIndex === 0) {
|
||||
onRespondToClaude();
|
||||
} else {
|
||||
onFinishPlanInterview();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isInNotesInput) {
|
||||
// In notes input mode, handle escape to exit back to option navigation
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault();
|
||||
handleNotesExit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle option navigation (vertical)
|
||||
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
if (focusedIndex > 0) {
|
||||
handleNavigate('up');
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
if (isInPlanMode && footerIndex === 0) {
|
||||
setFooterIndex(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
if (footerIndex === 0) {
|
||||
onRespondToClaude()
|
||||
} else {
|
||||
onFinishPlanInterview()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||||
e.preventDefault();
|
||||
if (focusedIndex === allOptions.length - 1) {
|
||||
// At bottom of options, go to footer
|
||||
handleDownFromPreview();
|
||||
} else {
|
||||
handleNavigate('down');
|
||||
|
||||
if (isInNotesInput) {
|
||||
// In notes input mode, handle escape to exit back to option navigation
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault()
|
||||
handleNotesExit()
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if (e.key === 'return') {
|
||||
e.preventDefault();
|
||||
handleSelectOption(focusedIndex);
|
||||
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
|
||||
// Press 'n' to focus the notes input
|
||||
e.preventDefault();
|
||||
setIsInNotesInput(true);
|
||||
onTextInputFocus(true);
|
||||
} else if (e.key === 'escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
|
||||
e.preventDefault();
|
||||
const idx_0 = parseInt(e.key, 10) - 1;
|
||||
if (idx_0 < allOptions.length) {
|
||||
handleNavigate(idx_0);
|
||||
|
||||
// Handle option navigation (vertical)
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
if (focusedIndex > 0) {
|
||||
handleNavigate('up')
|
||||
}
|
||||
} else if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
if (focusedIndex === allOptions.length - 1) {
|
||||
// At bottom of options, go to footer
|
||||
handleDownFromPreview()
|
||||
} else {
|
||||
handleNavigate('down')
|
||||
}
|
||||
} else if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
handleSelectOption(focusedIndex)
|
||||
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
|
||||
// Press 'n' to focus the notes input
|
||||
e.preventDefault()
|
||||
setIsInNotesInput(true)
|
||||
onTextInputFocus(true)
|
||||
} else if (e.key === 'escape') {
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
|
||||
e.preventDefault()
|
||||
const idx = parseInt(e.key, 10) - 1
|
||||
if (idx < allOptions.length) {
|
||||
handleNavigate(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isFooterFocused, footerIndex, isInPlanMode, isInNotesInput, focusedIndex, allOptions.length, handleUpFromFooter, handleDownFromPreview, handleNavigate, handleSelectOption, handleNotesExit, onRespondToClaude, onFinishPlanInterview, onCancel, onTextInputFocus]);
|
||||
const previewContent = focusedOption?.preview || null;
|
||||
},
|
||||
[
|
||||
isFooterFocused,
|
||||
footerIndex,
|
||||
isInPlanMode,
|
||||
isInNotesInput,
|
||||
focusedIndex,
|
||||
allOptions.length,
|
||||
handleUpFromFooter,
|
||||
handleDownFromPreview,
|
||||
handleNavigate,
|
||||
handleSelectOption,
|
||||
handleNotesExit,
|
||||
onRespondToClaude,
|
||||
onFinishPlanInterview,
|
||||
onCancel,
|
||||
onTextInputFocus,
|
||||
],
|
||||
)
|
||||
|
||||
const previewContent = focusedOption?.preview || null
|
||||
|
||||
// The right panel's available width is terminal minus the left panel and gap.
|
||||
const LEFT_PANEL_WIDTH = 30;
|
||||
const GAP = 4;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP;
|
||||
const LEFT_PANEL_WIDTH = 30
|
||||
const GAP = 4
|
||||
const { columns } = useTerminalSize()
|
||||
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP
|
||||
|
||||
// Lines used within the content area that aren't preview content:
|
||||
// 1: marginTop on side-by-side box
|
||||
@@ -245,19 +305,34 @@ export function PreviewQuestionView({
|
||||
// 1: "Chat about this" line
|
||||
// 1: plan mode line (may or may not show)
|
||||
// 2: help text (marginTop=1 + text)
|
||||
const PREVIEW_OVERHEAD = 11;
|
||||
const PREVIEW_OVERHEAD = 11
|
||||
|
||||
// Compute the max lines available for preview content from the parent's
|
||||
// height budget to prevent terminal overflow. We do NOT pad shorter options
|
||||
// to match the tallest — the outer box's minHeight handles cross-question
|
||||
// layout consistency, and within-question shifts are acceptable.
|
||||
const previewMaxLines = useMemo(() => {
|
||||
return minContentHeight ? Math.max(1, minContentHeight - PREVIEW_OVERHEAD) : undefined;
|
||||
}, [minContentHeight]);
|
||||
return <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
||||
return minContentHeight
|
||||
? Math.max(1, minContentHeight - PREVIEW_OVERHEAD)
|
||||
: undefined
|
||||
}, [minContentHeight])
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Divider color="inactive" />
|
||||
<Box flexDirection="column" paddingTop={0}>
|
||||
<QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />
|
||||
<QuestionNavigationBar
|
||||
questions={questions}
|
||||
currentQuestionIndex={currentQuestionIndex}
|
||||
answers={answers}
|
||||
hideSubmitTab={hideSubmitTab}
|
||||
/>
|
||||
<PermissionRequestTitle title={question.question} color={'text'} />
|
||||
|
||||
<Box flexDirection="column" minHeight={minContentHeight}>
|
||||
@@ -265,33 +340,71 @@ export function PreviewQuestionView({
|
||||
<Box marginTop={1} flexDirection="row" gap={4}>
|
||||
{/* Left panel: vertical option list */}
|
||||
<Box flexDirection="column" width={30}>
|
||||
{allOptions.map((option_0, index_0) => {
|
||||
const isFocused = focusedIndex === index_0;
|
||||
const isSelected = selectedValue === option_0.label;
|
||||
return <Box key={option_0.label} flexDirection="row">
|
||||
{isFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||||
<Text dimColor> {index_0 + 1}.</Text>
|
||||
<Text color={isSelected ? 'success' : isFocused ? 'suggestion' : undefined} bold={isFocused}>
|
||||
{allOptions.map((option, index) => {
|
||||
const isFocused = focusedIndex === index
|
||||
const isSelected = selectedValue === option.label
|
||||
|
||||
return (
|
||||
<Box key={option.label} flexDirection="row">
|
||||
{isFocused ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text dimColor> {index + 1}.</Text>
|
||||
<Text
|
||||
color={
|
||||
isSelected
|
||||
? 'success'
|
||||
: isFocused
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
bold={isFocused}
|
||||
>
|
||||
{' '}
|
||||
{option_0.label}
|
||||
{option.label}
|
||||
</Text>
|
||||
{isSelected && <Text color="success"> {figures.tick}</Text>}
|
||||
</Box>;
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Right panel: preview + notes */}
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<PreviewBox content={previewContent || 'No preview available'} maxLines={previewMaxLines} minWidth={minContentWidth} maxWidth={previewMaxWidth} />
|
||||
<PreviewBox
|
||||
content={previewContent || 'No preview available'}
|
||||
maxLines={previewMaxLines}
|
||||
minWidth={minContentWidth}
|
||||
maxWidth={previewMaxWidth}
|
||||
/>
|
||||
<Box marginTop={1} flexDirection="row" gap={1}>
|
||||
<Text color="suggestion">Notes:</Text>
|
||||
{isInNotesInput ? <TextInput value={notesValue} placeholder="Add notes on this design…" onChange={value => {
|
||||
onUpdateQuestionState(questionText, {
|
||||
textInputValue: value
|
||||
}, false);
|
||||
}} onSubmit={handleNotesExit} onExit={handleNotesExit} focus={true} showCursor={true} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> : <Text dimColor italic>
|
||||
{isInNotesInput ? (
|
||||
<TextInput
|
||||
value={notesValue}
|
||||
placeholder="Add notes on this design…"
|
||||
onChange={value => {
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ textInputValue: value },
|
||||
false,
|
||||
)
|
||||
}}
|
||||
onSubmit={handleNotesExit}
|
||||
onExit={handleNotesExit}
|
||||
focus={true}
|
||||
showCursor={true}
|
||||
columns={60}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
/>
|
||||
) : (
|
||||
<Text dimColor italic>
|
||||
{notesValue || 'press n to add notes'}
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -300,28 +413,53 @@ export function PreviewQuestionView({
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Divider color="inactive" />
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||||
<Text color={isFooterFocused && footerIndex === 0 ? 'suggestion' : undefined}>
|
||||
{isFooterFocused && footerIndex === 0 ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
isFooterFocused && footerIndex === 0
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Chat about this
|
||||
</Text>
|
||||
</Box>
|
||||
{isInPlanMode && <Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||||
<Text color={isFooterFocused && footerIndex === 1 ? 'suggestion' : undefined}>
|
||||
{isInPlanMode && (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 1 ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
isFooterFocused && footerIndex === 1
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Skip interview and plan immediately
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color="inactive" dimColor>
|
||||
Enter to select · {figures.arrowUp}/{figures.arrowDown} to
|
||||
navigate · n to add notes
|
||||
{questions.length > 1 && <> · Tab to switch questions</>}
|
||||
{isInNotesInput && editorName && <> · ctrl+g to edit in {editorName}</>}{' '}
|
||||
{isInNotesInput && editorName && (
|
||||
<> · ctrl+g to edit in {editorName}</>
|
||||
)}{' '}
|
||||
· Esc to cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user