From 673a5dbd23dc049b196bbaa9a7d7205f6521887d Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 16 Jun 2026 21:52:10 +0900 Subject: [PATCH] #371 --- .../controllers/gekanator_games_controller.rb | 13 +- frontend/src/pages/GekanatorPage.tsx | 667 +++++++++++------- 2 files changed, 413 insertions(+), 267 deletions(-) diff --git a/backend/app/controllers/gekanator_games_controller.rb b/backend/app/controllers/gekanator_games_controller.rb index d4c4d3b..9144ed6 100644 --- a/backend/app/controllers/gekanator_games_controller.rb +++ b/backend/app/controllers/gekanator_games_controller.rb @@ -192,7 +192,10 @@ class GekanatorGamesController < ApplicationController answer_value = answer['answer'].to_s next if answer_value.blank? || answer_value == 'unknown' - question = accepted_questions[answer['question_id'].to_s] + question_id = game_answer_question_id(answer) + next if question_id.blank? + + question = accepted_questions[question_id.to_s] next unless learnable_game_answer_question?(question) example = @@ -251,6 +254,7 @@ class GekanatorGamesController < ApplicationController end def learnable_game_answer_question? question + return false if question.nil? return true if question.kind == 'post_similarity' return false unless question.kind == 'tag' @@ -258,4 +262,11 @@ class GekanatorGamesController < ApplicationController key = condition[:key].to_s !key.start_with?('nico:') end + + def game_answer_question_id answer + answer['question_id'] || + answer[:question_id] || + answer['questionId'] || + answer[:questionId] + end end diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 598a4ae..cdce7a0 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -110,6 +110,7 @@ type StoredGekanatorGame = { savedGameId: number | null learnedExampleCount?: number | null gameSeed?: string + questionSuggestionEntryMode?: 'search' | 'new' questionSuggestionSearch?: string questionSuggestionSelectedId?: number | null questionSuggestion: string @@ -141,6 +142,10 @@ type QuestionBuildMode = | 'split' | 'confirmation' +type QuestionSuggestionEntryMode = + | 'search' + | 'new' + type MascotState = | 'idle' | 'thinking_far' @@ -256,6 +261,7 @@ const normalizeStoredGame = (game: StoredGekanatorGame): StoredGekanatorGame => winningRunTargetId: game.winningRunTargetId ?? null, winningRunStartAnswerCount: game.winningRunStartAnswerCount ?? null, learnedExampleCount: game.learnedExampleCount ?? null, + questionSuggestionEntryMode: game.questionSuggestionEntryMode ?? 'search', questionSuggestionSearch: game.questionSuggestionSearch ?? '', questionSuggestionSelectedId: game.questionSuggestionSelectedId ?? null, askedQuestionBank: game.askedQuestionBank?.map (question => ({ @@ -1125,17 +1131,31 @@ const applyQuestionAnswerDeltaToScores = ({ if (!(questionUsesPostSimilarityGraphForScoring (question))) return + // `post_similarities` is the propagation graph. Directly matched posts + // get only the base delta; only non-direct neighbors get `base delta * cos`. + // When several matched posts point at the same neighbor, keep the largest + // absolute propagated contribution instead of summing all of them. + const propagatedDeltaByPostId = new Map () matched.forEach (postId => { const post = materialIndex.postById.get (postId) post?.postSimilarityEdges?.forEach (edge => { if (!Number.isFinite (edge.cos) || edge.cos <= 0) return + if (matched.has (edge.targetPostId)) + return - nextScores.set ( - edge.targetPostId, - (nextScores.get (edge.targetPostId) ?? 0) + baseDelta * edge.cos) + const propagatedDelta = baseDelta * edge.cos + const current = propagatedDeltaByPostId.get (edge.targetPostId) + if (current === undefined || Math.abs (propagatedDelta) > Math.abs (current)) + propagatedDeltaByPostId.set (edge.targetPostId, propagatedDelta) }) }) + + propagatedDeltaByPostId.forEach ((propagatedDelta, postId) => { + nextScores.set ( + postId, + (nextScores.get (postId) ?? 0) + propagatedDelta) + }) } const buildIndexedQuestion = ( @@ -1294,7 +1314,7 @@ const buildQuestionsForCandidateIds = ( .forEach (([term]) => { addQuestion (buildIndexedQuestion ({ condition: { type: 'title-contains', text: String (term) }, - text: `題名に「${ term }」が含まれる?`, + text: `タイトルに「${ term }」が含まれる?`, kind: 'title', priorityWeight: .99, materialIndex })) @@ -1313,7 +1333,7 @@ const buildQuestionsForCandidateIds = ( if (asciiCount > 0 && asciiCount < total) addQuestion (buildIndexedQuestion ({ condition: { type: 'title-has-ascii' }, - text: '題名に英数字が混じっている?', + text: 'タイトルに英数字が混じっている?', kind: 'title', priorityWeight: .68, materialIndex })) @@ -1367,7 +1387,7 @@ const buildQuestionsForCandidateIds = ( .forEach (term => { addQuestion (buildIndexedQuestion ({ condition: { type: 'title-contains', text: term }, - text: `題名に「${ term }」が含まれる?`, + text: `タイトルに「${ term }」が含まれる?`, kind: 'title', priorityWeight: .99, materialIndex })) @@ -1386,7 +1406,7 @@ const buildQuestionsForCandidateIds = ( if (materialIndex.titleAsciiPostIds.has (targetPostId)) addQuestion (buildIndexedQuestion ({ condition: { type: 'title-has-ascii' }, - text: '題名に英数字が混じっている?', + text: 'タイトルに英数字が混じっている?', kind: 'title', priorityWeight: .68, materialIndex })) @@ -1415,6 +1435,8 @@ const candidatePostsForState = ({ recoveredCandidatePosts: Map }): Post[] => { const dynamicMatchIndex = new Map> () + const answerAllowsHardFilter = (answer: GekanatorAnswerValue): boolean => + answer === 'yes' || answer === 'no' return posts.filter (post => { if (rejectedPostIds.has (post.id)) @@ -1427,7 +1449,7 @@ const candidatePostsForState = ({ return true if (softenedQuestionIds.has (answer.questionId)) return true - if (!(answer.answer === 'yes' || answer.answer === 'no')) + if (!(answerAllowsHardFilter (answer.answer))) return true const question = questionById.get (answer.questionId) @@ -2717,6 +2739,7 @@ const GekanatorBackdrop: FC<{ const marqueeTransform = useMotionTemplate`translate(${ x }%, ${ y }%)` const [activeDirection, setActiveDirection] = useState (nextDirection) const activeDirectionRef = useRef (activeDirection) + const guessAnimationControlsRef = useRef[]> ([]) const flipTimerRef = useRef (null) const [displayedBackdropMode, setDisplayedBackdropMode] = useState<'normal' | 'winning_run' | 'guess'> (backdropMode) @@ -2731,15 +2754,30 @@ const GekanatorBackdrop: FC<{ const [flipVisualSeed, setFlipVisualSeed] = useState (visualSeed) const [isFlippingTiles, setIsFlippingTiles] = useState (false) const [isCrossfading, setIsCrossfading] = useState (false) - const renderedSettings = settingsForMode (displayedBackdropMode) - const renderedTileCount = - renderedSettings.columns * renderedSettings.rows - const renderedScale = scaleForMode (displayedBackdropMode, displayedWinningRunCount) - const isGuessPresentation = - phase === 'guess' || backdropMode === 'guess' || displayedBackdropMode === 'guess' + + const isLeavingGuessBackdrop = displayedBackdropMode === 'guess' && backdropMode !== 'guess' + + const renderBackdropMode = isLeavingGuessBackdrop ? backdropMode : displayedBackdropMode + + const renderWinningRunCount = (isLeavingGuessBackdrop + ? winningRunQuestionCount + : displayedWinningRunCount) + + const renderThumbnails = isLeavingGuessBackdrop ? nextThumbnails : displayedThumbnails + + const renderIsCrossfading = isCrossfading && !(isLeavingGuessBackdrop) + + const renderedSettings = settingsForMode (renderBackdropMode) + const renderedTileCount = renderedSettings.columns * renderedSettings.rows + const renderedScale = scaleForMode (renderBackdropMode, renderWinningRunCount) + + const isGuessPresentation = backdropMode === 'guess' const crossfadeDuration = motionMode === 'calm' ? .95 : .75 useEffect (() => { + guessAnimationControlsRef.current.forEach (control => control.stop ()) + guessAnimationControlsRef.current = [] + if (motionMode === 'off') return @@ -2752,9 +2790,11 @@ const GekanatorBackdrop: FC<{ const controls = [ animate (x, 0, { duration, ease }), animate (y, 0, { duration, ease })] + guessAnimationControlsRef.current = controls return () => { controls.forEach (control => control.stop ()) + guessAnimationControlsRef.current = [] } }, [isGuessPresentation, motionMode, visualSeed, x, y]) @@ -2771,13 +2811,21 @@ const GekanatorBackdrop: FC<{ if (motionMode === 'off' || nextThumbnails.length === 0) { + guessAnimationControlsRef.current.forEach (control => control.stop ()) + guessAnimationControlsRef.current = [] x.set (0) y.set (0) return } if (isGuessPresentation) - return + { + guessAnimationControlsRef.current.forEach (control => control.stop ()) + guessAnimationControlsRef.current = [] + x.set (0) + y.set (0) + return + } const speed = 33.333333 / marqueeDuration let animationFrame: number @@ -2795,8 +2843,7 @@ const GekanatorBackdrop: FC<{ animationFrame = window.requestAnimationFrame (tick) return () => window.cancelAnimationFrame (animationFrame) - }, [ - x, + }, [x, y, marqueeDuration, motionMode, @@ -2809,67 +2856,86 @@ const GekanatorBackdrop: FC<{ setActiveDirection (nextDirection) } - if (flipTimerRef.current !== null) { - window.clearTimeout (flipTimerRef.current) - flipTimerRef.current = null - } + if (flipTimerRef.current !== null) + { + window.clearTimeout (flipTimerRef.current) + flipTimerRef.current = null + } - if (motionMode === 'off') { - applyDirection () - setIsFlippingTiles (false) - setIsCrossfading (false) - setFlipVisualSeed (visualSeed) - return - } + if (motionMode === 'off') + { + applyDirection () + setIsFlippingTiles (false) + setIsCrossfading (false) + setFlipVisualSeed (visualSeed) + return + } - if (backdropMode === 'guess' && guessThumbnail) { - setIsFlippingTiles (false) - setIsCrossfading (false) - setDisplayedBackdropMode ('guess') - setDisplayedWinningRunCount (winningRunQuestionCount) - setDisplayedThumbnails (nextThumbnails) - setFromThumbnails (nextThumbnails) - setToThumbnails (nextThumbnails) - setFlipVisualSeed (visualSeed) - return - } + if (backdropMode === 'guess' && guessThumbnail) + { + setIsFlippingTiles (false) + setIsCrossfading (false) + setDisplayedBackdropMode ('guess') + setDisplayedWinningRunCount (winningRunQuestionCount) + setDisplayedThumbnails (nextThumbnails) + setFromThumbnails (nextThumbnails) + setToThumbnails (nextThumbnails) + setFlipVisualSeed (visualSeed) + return + } - if ( - displayedBackdropMode === 'winning_run' - && backdropMode === 'winning_run' - ) { - applyDirection () - setDisplayedBackdropMode ('winning_run') - setDisplayedWinningRunCount (winningRunQuestionCount) - setDisplayedThumbnails (nextThumbnails) - setFromThumbnails (nextThumbnails) - setToThumbnails (nextThumbnails) - setIsFlippingTiles (false) - setIsCrossfading (false) - setFlipVisualSeed (visualSeed) - return - } + if (displayedBackdropMode === 'winning_run' + && backdropMode === 'winning_run') + { + applyDirection () + setDisplayedBackdropMode ('winning_run') + setDisplayedWinningRunCount (winningRunQuestionCount) + setDisplayedThumbnails (nextThumbnails) + setFromThumbnails (nextThumbnails) + setToThumbnails (nextThumbnails) + setIsFlippingTiles (false) + setIsCrossfading (false) + setFlipVisualSeed (visualSeed) + return + } - if (nextThumbnails.length === 0) { - applyDirection () - setIsFlippingTiles (false) - setIsCrossfading (false) - setFlipVisualSeed (visualSeed) - return - } + if (nextThumbnails.length === 0) + { + applyDirection () + setIsFlippingTiles (false) + setIsCrossfading (false) + setFlipVisualSeed (visualSeed) + return + } + + if (displayedBackdropMode === 'guess' && backdropMode !== 'guess') + { + applyDirection () + x.set (0) + y.set (0) + setIsFlippingTiles (false) + setIsCrossfading (false) + setDisplayedBackdropMode (backdropMode) + setDisplayedWinningRunCount (winningRunQuestionCount) + setDisplayedThumbnails (nextThumbnails) + setFromThumbnails (nextThumbnails) + setToThumbnails (nextThumbnails) + setFlipVisualSeed (visualSeed) + return + } const sameTiles = displayedThumbnails.length === nextThumbnails.length - && displayedThumbnails.every ( - (thumbnail, index) => thumbnail === nextThumbnails[index]) - if (sameTiles && flipVisualSeed === visualSeed) { - if ( - activeDirection.x !== nextDirection.x - || activeDirection.y !== nextDirection.y - ) - applyDirection () - return - } + && displayedThumbnails.every ((thumbnail, index) => thumbnail === nextThumbnails[index]) + + if (sameTiles && flipVisualSeed === visualSeed) + { + if (activeDirection.x !== nextDirection.x + || activeDirection.y !== nextDirection.y) + applyDirection () + + return + } const currentThumbnails = displayedThumbnails.length > 0 ? displayedThumbnails : nextThumbnails @@ -2895,13 +2961,13 @@ const GekanatorBackdrop: FC<{ }, (shouldCrossfade ? crossfadeDuration : tileFlipDuration) * 1000) return () => { - if (flipTimerRef.current !== null) { - window.clearTimeout (flipTimerRef.current) - flipTimerRef.current = null - } + if (flipTimerRef.current !== null) + { + window.clearTimeout (flipTimerRef.current) + flipTimerRef.current = null + } } - }, [ - motionMode, + }, [motionMode, backdropMode, displayedBackdropMode, guessThumbnail, @@ -2917,118 +2983,120 @@ const GekanatorBackdrop: FC<{ x, y]) - const renderTileSet = ({ - mode, - thumbnails, - settings, - tileCount, - scale, - opacity, - withFlip, - }: { - mode: 'normal' | 'winning_run' | 'guess' - thumbnails: string[] - settings: { columns: number; rows: number; opacity: number } - tileCount: number - scale: number - opacity?: number - withFlip?: boolean - }) => ( - { + const guessModeFlg = mode === 'guess' + + return ( + - {Array.from ({ length: 9 }, (_, duplicate) => { - const column = duplicate % 3 - const row = Math.floor (duplicate / 3) + x: guessModeFlg ? guessFocusOffset.x : '0%', + y: guessModeFlg ? guessFocusOffset.y : '0%' }} + transition={guessModeFlg + ? { duration: 0 } + : (mode === 'winning_run' + ? { duration: crossfadeDuration, ease: [.16, 1, .3, 1] } + : { duration: .2 })}> + {Array.from ({ length: 9 }, (_, duplicate) => { + const column = duplicate % 3 + const row = Math.floor (duplicate / 3) - return ( - - {Array.from ({ length: tileCount }, (_, index) => { - const currentThumbnail = - thumbnails[index % Math.max (thumbnails.length, 1)] - const frontThumbnail = - withFlip - ? fromThumbnails[index % Math.max (fromThumbnails.length, 1)] - : currentThumbnail - const backThumbnail = - withFlip - ? toThumbnails[index % Math.max (toThumbnails.length, 1)] - : currentThumbnail - const thumbnail = currentThumbnail - if (!(thumbnail) || !(frontThumbnail) || !(backThumbnail)) - return null + return ( + + {Array.from ({ length: tileCount }, (_, index) => { + const currentThumbnail = + thumbnails[index % Math.max (thumbnails.length, 1)] + const frontThumbnail = + withFlip + ? fromThumbnails[index % Math.max (fromThumbnails.length, 1)] + : currentThumbnail + const backThumbnail = + withFlip + ? toThumbnails[index % Math.max (toThumbnails.length, 1)] + : currentThumbnail + const thumbnail = currentThumbnail + if (!(thumbnail) || !(frontThumbnail) || !(backThumbnail)) + return null - return ( - - {(mode !== 'normal' || !(withFlip)) - ? ( - ) - : ( - + return ( + + {(mode !== 'normal' || !(withFlip)) + ? ( - - )} - ) - })} - ) - })} - ) + style={{ opacity: settings.opacity }}/>) + : ( + + + + )} + ) + })} + ) + })} + ) + } if (motionMode === 'off' || nextThumbnails.length === 0) return ( @@ -3046,15 +3114,16 @@ const GekanatorBackdrop: FC<{ height: 'calc(max(100vw, 100vh) * 3)' }}> - {isCrossfading + {renderIsCrossfading ? ( <> {renderTileSet ({ mode: displayedBackdropMode, thumbnails: fromThumbnails, - settings: renderedSettings, - tileCount: renderedTileCount, - scale: renderedScale, + settings: settingsForMode (displayedBackdropMode), + tileCount: (settingsForMode (displayedBackdropMode).columns + * settingsForMode (displayedBackdropMode).rows), + scale: scaleForMode (displayedBackdropMode, displayedWinningRunCount), opacity: 0 })} {renderTileSet ({ mode: backdropMode, @@ -3064,13 +3133,14 @@ const GekanatorBackdrop: FC<{ scale: scaleForMode (backdropMode, winningRunQuestionCount), opacity: 1 })} ) - : renderTileSet ({ - mode: displayedBackdropMode, - thumbnails: displayedThumbnails, - settings: renderedSettings, - tileCount: renderedTileCount, - scale: renderedScale, - withFlip: isFlippingTiles })} + : ( + renderTileSet ({ + mode: renderBackdropMode, + thumbnails: renderThumbnails, + settings: renderedSettings, + tileCount: renderedTileCount, + scale: renderedScale, + withFlip: isFlippingTiles && !(isLeavingGuessBackdrop) }))} @@ -3150,6 +3220,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { storedGame?.savedGameId ?? null) const [learnedExampleCount, setLearnedExampleCount] = useState ( storedGame?.learnedExampleCount ?? null) + const [questionSuggestionEntryMode, setQuestionSuggestionEntryMode] = + useState ( + storedGame?.questionSuggestionEntryMode ?? 'search') const [questionSuggestionSearch, setQuestionSuggestionSearch] = useState ( storedGame?.questionSuggestionSearch ?? '') const [questionSuggestionSelectedId, setQuestionSuggestionSelectedId] = @@ -3190,11 +3263,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { [posts, acceptedQuestions]) useEffect (() => { - if ( - posts.length === 0 + if (posts.length === 0 || storedAskedQuestionBankIds.length === 0 - || !(acceptedQuestionsFetched) - ) + || !(acceptedQuestionsFetched)) return const questionById = new Map ( @@ -3207,8 +3278,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { .map (questionId => questionById.get (questionId)) .filter ((question): question is GekanatorQuestion => question !== undefined)) setStoredAskedQuestionBankIds ([]) - }, [ - posts, + }, [posts, storedAskedQuestionBankIds, acceptedQuestionsFetched, askedQuestionBank, @@ -3250,6 +3320,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { savedGameId, learnedExampleCount, gameSeed, + questionSuggestionEntryMode, questionSuggestionSearch, questionSuggestionSelectedId, questionSuggestion, @@ -3293,6 +3364,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { savedGameId, learnedExampleCount, gameSeed, + questionSuggestionEntryMode, questionSuggestionSearch, questionSuggestionSelectedId, questionSuggestion, @@ -3355,6 +3427,28 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { question => question.recordId === questionSuggestionSelectedId) ?? null, [acceptedQuestions, questionSuggestionSelectedId]) + const canSubmitQuestionSuggestion = useMemo (() => { + if (!(canPersistGame) || reviewCorrectPostId === null) + return false + + if (questionSuggestionEntryMode === 'search') + return selectedSuggestedQuestion !== null + + return questionSuggestion.trim () !== '' + }, [ + canPersistGame, + reviewCorrectPostId, + questionSuggestionEntryMode, + selectedSuggestedQuestion, + questionSuggestion]) + const canShowNewQuestionSuggestionButton = useMemo (() => { + return questionSuggestionEntryMode === 'search' + && questionSuggestionSearch.trim () !== '' + && searchableSuggestedQuestions.length === 0 + }, [ + questionSuggestionEntryMode, + questionSuggestionSearch, + searchableSuggestedQuestions.length]) const recentFirstQuestionPenaltyById = useMemo (() => { const penalties = new Map () @@ -3494,6 +3588,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { onSuccess: async data => { await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () }) setQuestionSuggestionCount (data.count) + setQuestionSuggestionEntryMode ('search') setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) setQuestionSuggestion ('') @@ -3544,6 +3639,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setSavedGameId (null) setLearnedExampleCount (null) setGameSeed (createGameSeed ()) + setQuestionSuggestionEntryMode ('search') setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) setQuestionSuggestion ('') @@ -3876,6 +3972,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setSaved (false) setSavedGameId (null) setLearnedExampleCount (null) + setQuestionSuggestionEntryMode ('search') setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) setReviewGuessedPostId (guessedPostId) @@ -3895,6 +3992,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setSaved (false) setSavedGameId (null) setLearnedExampleCount (null) + setQuestionSuggestionEntryMode ('search') setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) setSelectingCorrectPost (false) @@ -3925,6 +4023,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { } const saveAndReset = () => { + if (saveMutation.isError) + { + reset () + return + } + if (!(canPersistGame)) { reset () @@ -3948,11 +4052,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const submitQuestionSuggestion = () => { const questionText = questionSuggestion.trim () const selectedQuestion = selectedSuggestedQuestion - if ( - !(canPersistGame) - || (selectedQuestion === null && !(questionText)) - || questionSuggestionMutation.isPending - ) + if (!(canSubmitQuestionSuggestion) || questionSuggestionMutation.isPending) return saveReviewedResult (gekanatorGameId => { @@ -4092,6 +4192,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setSaved (false) setSavedGameId (null) setLearnedExampleCount (null) + setQuestionSuggestionEntryMode ('search') setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) resetExtraQuestionState () @@ -4105,6 +4206,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setSaved (false) setSavedGameId (null) setLearnedExampleCount (null) + setQuestionSuggestionEntryMode ('search') setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) resetExtraQuestionState () @@ -4798,62 +4900,101 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {

質問追加

-

まず既存質問を探してください。

+

+ {questionSuggestionEntryMode === 'search' + ? 'まず既存質問を探してください。' + : '新しい質問を追加します。'} +

- - {searchableSuggestedQuestions.length > 0 && ( + {questionSuggestionEntryMode === 'search' + ? ( + <> + + {searchableSuggestedQuestions.length > 0 && ( +
+
既存質問候補
+
+ {searchableSuggestedQuestions.map (question => ( + ))} +
+
)} + {selectedSuggestedQuestion && ( +

+ 既存質問を選択中: {selectedSuggestedQuestion.text} +

)} + {canShowNewQuestionSuggestionButton && ( +
+

+ 適切な質問がない場合は新規追加してください。 +

+
+ +
+
)} + ) + : (
-
既存質問候補
-
- {searchableSuggestedQuestions.map (question => ( - ))} +
新規質問
+