• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yext / search-ui-react / 9602058521

20 Jun 2024 05:49PM UTC coverage: 85.321% (-0.1%) from 85.425%
9602058521

push

github

web-flow
add generativeDirectAnswer support (#441)

J=CLIP-1226
TEST=auto,manual

added unit and visual regression test, ran and saw tests passed
added GDA to test-site App and verified that the appropriate gda content shows up and the UI looks as expected

1320 of 1723 branches covered (76.61%)

Branch coverage included in aggregate %.

38 of 43 new or added lines in 3 files covered. (88.37%)

1906 of 2058 relevant lines covered (92.61%)

146.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.57
/src/components/GenerativeDirectAnswer.tsx
1
import { 
6✔
2
  GenerativeDirectAnswerResponse, 
3
  useSearchActions, 
4
  useSearchState, 
5
  SearchTypeEnum,
6
  Result
7
} from '@yext/search-headless-react';
8
import { useComposedCssClasses } from '../hooks';
6✔
9
import { executeGenerativeDirectAnswer } from '../utils/search-operations';
6✔
10
import ReactMarkdown from "react-markdown";
6✔
11
import React from 'react';
6✔
12

13
/**
14
 * The CSS class interface used for {@link GenerativeDirectAnswer}.
15
 *
16
 * @public
17
 */
18
export interface GenerativeDirectAnswerCssClasses {
19
  container?: string,
20
  header?: string,
21
  answerText?: string,
22
  divider?: string,
23
  citationsContainer?: string,
24
  citation?: string,
25
  citationTitle?: string,
26
  citationSnippet?: string
27
}
28

29
const builtInCssClasses: Readonly<GenerativeDirectAnswerCssClasses> = {
6✔
30
  container: 'p-6 border border-gray-200 rounded-lg shadow-sm',
31
  header: 'text-xl',
32
  answerText: 'mt-4',
33
  divider: 'border-b border-gray-200 w-full pb-6 mb-6',
34
  citationsContainer: 'mt-4 flex overflow-x-auto gap-4',
35
  citation: 'p-4 border border-gray-200 rounded-lg shadow-sm bg-slate-100 flex flex-col grow-0 shrink-0 basis-64 text-sm text-neutral overflow-x-auto',
36
  citationTitle: 'font-bold',
37
  citationSnippet: 'line-clamp-2 text-ellipsis break-words'
38
};
39

40
/**
41
 * Props for {@link GenerativeDirectAnswer}.
42
 *
43
 * @public
44
 */
45
export interface GenerativeDirectAnswerProps {
46
  /** CSS classes for customizing the component styling. */
47
  customCssClasses?: GenerativeDirectAnswerCssClasses,
48
  /** The header for the answer section of the generative direct answer. */
49
  answerHeader?: string | JSX.Element,
50
  /** The header for the citations section of the generative direct answer. */
51
  citationsHeader?: string | JSX.Element,
52
  /** The component for citation card */
53
  CitationCard?: (props: CitationProps) => JSX.Element | null
54
}
55

56
/**
57
 * Displays the AI generated answer of a generative direct answer.
58
 *
59
 * @public
60
 *
61
 * @param props - {@link GenerativeDirectAnswerProps}
62
 * @returns A React element for the generative direct answer, or null if there is no generated answer
63
 */
64
export function GenerativeDirectAnswer({
6✔
65
  customCssClasses,
66
  answerHeader,
67
  citationsHeader,
68
  CitationCard
69
}: GenerativeDirectAnswerProps): JSX.Element | null {
70
  const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
1✔
71

72
  const isUniversal = useSearchState(state => state.meta.searchType) === SearchTypeEnum.Universal;
1✔
73
  const universalResults = useSearchState(state => state.universal);
1✔
74
  const verticalResults = useSearchState(state => state.vertical);
1✔
75

76
  const searchResults: Result[] | undefined = React.useMemo(() => {
1✔
77
    if (isUniversal) {
1!
78
      return universalResults.verticals?.flatMap(v => v.results);
3!
79
    } else {
NEW
80
      return verticalResults.results;
×
81
    }
82
  }, [isUniversal, universalResults, verticalResults]);
83

84
  const searchActions = useSearchActions();
1✔
85
  const gdaResponse = useSearchState(state => state.generativeDirectAnswer?.response);
1!
86
  const isLoading = useSearchState(state => state.generativeDirectAnswer?.isLoading);
1!
87
  
88
  React.useEffect(() => {
1✔
89
    if (!searchResults?.length) {
1!
NEW
90
      return;
×
91
    }
92
    executeGenerativeDirectAnswer(searchActions);
1✔
93
  }, [searchResults]);
94

95
  if (!searchResults?.length || isLoading || !gdaResponse || gdaResponse.resultStatus !== 'SUCCESS') {
1!
NEW
96
    return null;
×
97
  }
98

99
  return (
1✔
100
    <div className={cssClasses.container}>
101
      <Answer gdaResponse={gdaResponse} cssClasses={cssClasses} answerHeader={answerHeader}/>
102
      <div className={cssClasses.divider} />
103
      <Citations gdaResponse={gdaResponse} cssClasses={cssClasses} citationsHeader={citationsHeader} searchResults={searchResults} CitationCard={CitationCard}/>
104
    </div>
105
  );
106
}
107

108
interface AnswerProps {
109
  gdaResponse: GenerativeDirectAnswerResponse,
110
  cssClasses: GenerativeDirectAnswerCssClasses,
111
  answerHeader?: string | JSX.Element
112
}
113

114
/**
115
 * The answer section of the Generative Direct Answer.
116
 */
117
function Answer(props: AnswerProps) {
118
  const { 
119
    gdaResponse, 
120
    cssClasses,
121
    answerHeader = 'AI Generated Answer'
1✔
122
  } = props;
1✔
123
  return <>
1✔
124
    <div className={cssClasses.header}>
125
      {answerHeader}
126
    </div>
127
    <ReactMarkdown className={cssClasses.answerText} children={gdaResponse.directAnswer} />
128
  </>;
129
}
130

131
interface CitationsProps {
132
  gdaResponse: GenerativeDirectAnswerResponse,
133
  cssClasses: GenerativeDirectAnswerCssClasses,
134
  citationsHeader?: string | JSX.Element,
135
  searchResults: Result[],
136
  CitationCard?: (props: CitationProps) => JSX.Element | null
137
}
138

139
/**
140
 * The citations section of the Generative Direct Answer.
141
 */
142
function Citations(props: CitationsProps) {
143
  const { 
144
    gdaResponse, 
145
    cssClasses,
146
    citationsHeader = `Sources (${gdaResponse.citations.length})`,
1✔
147
    searchResults,
148
    CitationCard = Citation
1✔
149
  } = props;
1✔
150
  if (!gdaResponse.citations.length) {
1!
NEW
151
    return null;
×
152
  }
153
  return <>
1✔
154
    <div className={cssClasses.header}>
155
      {citationsHeader}
156
    </div>
157
    <div className={cssClasses.citationsContainer}>
158
      {gdaResponse.citations.map(
159
        citation => <CitationCard key={citation} searchResults={searchResults} citation={citation} cssClasses={cssClasses} />)}
2✔
160
    </div>
161
  </>;
162
}
163

164
/**
165
 * Props for citation card.
166
 *
167
 * @public
168
 */
169
export interface CitationProps {
170
  searchResults: Result[],
171
  citation: string,
172
  cssClasses: GenerativeDirectAnswerCssClasses
173
}
174

175
function Citation(props: CitationProps) {
176
  const {
177
    searchResults,
178
    citation,
179
    cssClasses
180
  } = props;
2✔
181
  const rawResult: Result | undefined = searchResults.find(r => r.rawData.uid === citation);
5✔
182
  if (!rawResult) {
2!
NEW
183
    return null;
×
184
  }
185

186
  return <div className={cssClasses.citation}>
2✔
187
    <div className={cssClasses.citationTitle}>{rawResult.rawData.name}</div>
188
    <div className={cssClasses.citationSnippet}>{rawResult.rawData.description}</div>
189
  </div>;
190
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc