|
| 1 | +"""UI and logic inkeep chat component.""" |
| 2 | + |
| 3 | +import reflex as rx |
| 4 | +from reflex.utils.imports import ImportVar |
| 5 | +from reflex.vars import Var |
| 6 | + |
| 7 | + |
| 8 | +class InkeepSearchBar(rx.NoSSRComponent): |
| 9 | + tag = "InkeepSearchBar" |
| 10 | + library = "@inkeep/cxkit-react@0.5.55" |
| 11 | + |
| 12 | + |
| 13 | +class Search(rx.el.Div): |
| 14 | + def add_imports(self): |
| 15 | + """Add the imports for the component.""" |
| 16 | + return { |
| 17 | + "react": {ImportVar(tag="useContext")}, |
| 18 | + "$/utils/context": {ImportVar(tag="ColorModeContext")}, |
| 19 | + } |
| 20 | + |
| 21 | + def add_hooks(self): |
| 22 | + """Add the hooks for the component.""" |
| 23 | + return [ |
| 24 | + "const { resolvedColorMode } = useContext(ColorModeContext)", |
| 25 | + """ |
| 26 | +const escalationParams = { |
| 27 | + type: "object", |
| 28 | + properties: { |
| 29 | + explanation: { |
| 30 | + type: "string", |
| 31 | + description: "A brief few word justification of why a specific confidence level was chosen.", |
| 32 | + }, |
| 33 | + answerConfidence: { |
| 34 | + anyOf: [ |
| 35 | + { |
| 36 | + type: "string", |
| 37 | + const: "very_confident", |
| 38 | + description: `\n The AI Assistant provided a complete and direct answer to all parts of the User Question.\n The answer fully resolved the issue without requiring any further action from the User.\n Every part of the answer was cited from the information sources.\n The assistant did not ask for more information or provide options requiring User action.\n This is the highest Answer Confidence level and should be used sparingly.\n `, |
| 39 | + }, |
| 40 | + { |
| 41 | + type: "string", |
| 42 | + const: "somewhat_confident", |
| 43 | + description: `\n The AI Assistant provided a complete and direct answer to the User Question, but the answer contained minor caveats or uncertainties. \n \n Examples:\n • The AI Assistant asked follow-up questions to the User\n • The AI Assistant requested additional information from the User\n • The AI Assistant suggested uncertainty in the answer\n • The AI Assistant answered the question but mentioned potential exceptions\n `, |
| 44 | + }, |
| 45 | + { |
| 46 | + type: "string", |
| 47 | + const: "not_confident", |
| 48 | + description: `\n The AI Assistant tried to answer the User Question but did not fully resolve it.\n The assistant provided options requiring further action from the User, asked for more information, showed uncertainty,\n suggested the user contact support or provided contact information, or provided an indirect or incomplete answer.\n This is the most common Answer Confidence level.\n \n Examples:\n • The AI Assistant provided a general answer not directly related to the User Question\n • The AI Assistant said to reach out to support or provided an email address or contact information\n • The AI Assistant provided options that require further action from the User to resolve the issue\n `, |
| 49 | + }, |
| 50 | + { |
| 51 | + type: "string", |
| 52 | + const: "no_sources", |
| 53 | + description: `\n The AI Assistant did not use or cite any sources from the information sources to answer the User Question.\n `, |
| 54 | + }, |
| 55 | + { |
| 56 | + type: "string", |
| 57 | + const: "other", |
| 58 | + description: `\n The User Question is unclear or unrelated to the subject matter.\n `, |
| 59 | + }, |
| 60 | + ], |
| 61 | + description: "A measure of how confidently the AI Assistant completely and directly answered the User Question.", |
| 62 | + }, |
| 63 | + }, |
| 64 | + required: ["explanation", "answerConfidence"], |
| 65 | + additionalProperties: false, |
| 66 | +}; |
| 67 | +const searchBarProps = { |
| 68 | + baseSettings: { |
| 69 | + apiKey: '6299820854cd95d0a6e55a502d5bae06549e62360e7805a6', |
| 70 | + customIcons: {search: {custom: "/icons/search.svg"}}, |
| 71 | + organizationDisplayName: 'Reflex', |
| 72 | + primaryBrandColor: '#6E56CF', |
| 73 | + transformSource: (source) => { |
| 74 | + const urlPatterns = { |
| 75 | + blog: 'reflex.dev/blog', |
| 76 | + library: 'reflex.dev/docs/library', |
| 77 | + apiRef: 'reflex.dev/docs/api-reference', |
| 78 | + docs: 'reflex.dev/docs', |
| 79 | + } |
| 80 | +
|
| 81 | + function matchUrl(pattern) { |
| 82 | + return source.url.includes(pattern) |
| 83 | + } |
| 84 | +
|
| 85 | + function getBreadcrumbs() { |
| 86 | + if (matchUrl(urlPatterns.blog)) { |
| 87 | + return ['Blogs', ...source.breadcrumbs.slice(1)] |
| 88 | + } |
| 89 | + if (matchUrl(urlPatterns.library)) { |
| 90 | + return ['Components', ...source.breadcrumbs.slice(1)] |
| 91 | + } |
| 92 | + if (matchUrl(urlPatterns.apiRef)) { |
| 93 | + return ['API Reference'] |
| 94 | + } |
| 95 | + if (matchUrl(urlPatterns.docs)) { |
| 96 | + return ['Docs', ...source.breadcrumbs.slice(1)] |
| 97 | + } |
| 98 | + return source.breadcrumbs |
| 99 | + } |
| 100 | +
|
| 101 | + const breadcrumbs = getBreadcrumbs() |
| 102 | +
|
| 103 | + function getTabs() { |
| 104 | + const tabMap = { |
| 105 | + [urlPatterns.blog]: 'Blogs', |
| 106 | + [urlPatterns.library]: 'Components', |
| 107 | + [urlPatterns.apiRef]: 'API Reference', |
| 108 | + [urlPatterns.docs]: 'Docs', |
| 109 | + } |
| 110 | +
|
| 111 | + for (const [pattern, tab] of Object.entries(tabMap)) { |
| 112 | + if (matchUrl(pattern)) { |
| 113 | + return [ |
| 114 | + ...(source.tabs ?? []), |
| 115 | + // If the first breadcrumb is the same as the tab, use the remaining breadcrumbs |
| 116 | + // This is only if you don't want breadcrumbs to include current tab, e.g. just "Blog Post" instead of "Blogs > Blog Post" in the Blogs tab |
| 117 | + // The tab type accepts a string or an object with a breadcrumbs property i.e. breadcrumbs shown for this source in that tab |
| 118 | + [ |
| 119 | + tab, |
| 120 | + { breadcrumbs: breadcrumbs[0] === tab ? breadcrumbs.slice(1) : breadcrumbs }, |
| 121 | + ], |
| 122 | + ] |
| 123 | + } |
| 124 | + } |
| 125 | + return source.tabs |
| 126 | + } |
| 127 | +
|
| 128 | + return { |
| 129 | + ...source, |
| 130 | + tabs: getTabs(), |
| 131 | + breadcrumbs, |
| 132 | + } |
| 133 | + }, |
| 134 | + colorMode: { |
| 135 | + forcedColorMode: resolvedColorMode, // options: 'light' or dark' |
| 136 | + }, |
| 137 | + theme: { |
| 138 | + // Add inline styles using the recommended approach from the docs |
| 139 | + styles: [ |
| 140 | + { |
| 141 | + key: "custom-theme", |
| 142 | + type: "style", |
| 143 | + value: ` |
| 144 | + [data-theme='light'] .ikp-search-bar__button, |
| 145 | + [data-theme='dark'] .ikp-search-bar__button { |
| 146 | + display: flex; |
| 147 | + max-height: 32px; |
| 148 | + min-height: 32px; |
| 149 | + padding: 6px; |
| 150 | + min-width: 256px; |
| 151 | + justify-content: space-between; |
| 152 | + align-items: center; |
| 153 | + border-radius: 10px; |
| 154 | + border: 1px solid var(--c-slate-5, #E0E1E6); |
| 155 | + background: var(--c-slate-1); |
| 156 | + /* Small */ |
| 157 | + font-family: "Instrument Sans"; |
| 158 | + font-size: 14px; |
| 159 | + font-style: normal; |
| 160 | + font-weight: 500; |
| 161 | + line-height: 20px; |
| 162 | + /* 142.857% */ |
| 163 | + letter-spacing: -0.0125em; |
| 164 | + color: var(--c-slate-9, #8B8D98); |
| 165 | + /* Shadow/Large */ |
| 166 | + box-shadow: 0px 24px 12px 0px rgba(28, 32, 36, 0.02), 0px 8px 8px 0px rgba(28, 32, 36, 0.02), 0px 2px 6px 0px rgba(28, 32, 36, 0.02); |
| 167 | + transition: background-color 0.1s linear, width 0s; |
| 168 | + } |
| 169 | +
|
| 170 | + [data-theme='light'] .ikp-search-bar__container, |
| 171 | + [data-theme='dark'] .ikp-search-bar__container { |
| 172 | + display: flex; |
| 173 | + justify-content: center; |
| 174 | + align-items: center; |
| 175 | + } |
| 176 | + |
| 177 | + [data-theme='light'] .ikp-search-bar__button:hover, |
| 178 | + [data-theme='dark'] .ikp-search-bar__button:hover { |
| 179 | + background-color: var(--c-slate-3, #F0F0F3); |
| 180 | + } |
| 181 | + |
| 182 | + [data-theme='dark'] .ikp-modal__overlay { |
| 183 | + background: rgba(18, 17, 19, 0.50); |
| 184 | + backdrop-filter: blur(20px); |
| 185 | + } |
| 186 | + |
| 187 | + @media (max-width: 80em) { |
| 188 | + [data-theme='light'] .ikp-search-bar__button, |
| 189 | + [data-theme='dark'] .ikp-search-bar__button { |
| 190 | + padding: 2px 12px; |
| 191 | + display: block; |
| 192 | + height: 32px; |
| 193 | + min-height: 32px; |
| 194 | + width: 32px; |
| 195 | + max-width: 6em; |
| 196 | + min-width: 0px; |
| 197 | + } |
| 198 | + |
| 199 | + .ikp-search-bar__button { |
| 200 | + align-items: center; |
| 201 | + justify-content: center; |
| 202 | + } |
| 203 | + |
| 204 | + .ikp-search-bar__kbd-wrapper, |
| 205 | + .ikp-search-bar__text { |
| 206 | + display: none; |
| 207 | + } |
| 208 | + |
| 209 | + .ikp-search-bar__icon { |
| 210 | + padding: 0; |
| 211 | + margin-right: 2px; |
| 212 | + } |
| 213 | + |
| 214 | + .ikp-search-bar__content-wrapper { |
| 215 | + justify-content: center; |
| 216 | + } |
| 217 | + } |
| 218 | + |
| 219 | + .ikp-search-bar__icon { |
| 220 | + display: flex; |
| 221 | + } |
| 222 | + |
| 223 | + .ikp-search-bar__icon svg { |
| 224 | + width: auto; |
| 225 | + } |
| 226 | + |
| 227 | + .ikp-search-bar__kbd-wrapper { |
| 228 | + padding: 0px 8px; |
| 229 | + justify-content: center; |
| 230 | + align-items: center; |
| 231 | + border-radius: 4px; |
| 232 | + box-shadow: none; |
| 233 | + color: var(--c-slate-9, #8B8D98); |
| 234 | + font-family: "Instrument Sans"; |
| 235 | + --ikp-colors-transparent: var(--c-slate-3, #FCFCFD); |
| 236 | + background: var(--c-slate-3, #F0F0F3) !important; |
| 237 | + font-size: 12px; |
| 238 | + font-style: normal; |
| 239 | + font-weight: 500; |
| 240 | + line-height: 20px; |
| 241 | + /* 166.667% */ |
| 242 | + letter-spacing: -0.09px; |
| 243 | + } |
| 244 | + |
| 245 | + .ikp-search-bar__text, |
| 246 | + .ikp-search-bar__icon { |
| 247 | + color: var(--c-slate-9, #8B8D98); |
| 248 | + font-weight: 500; |
| 249 | + } |
| 250 | + `, |
| 251 | + }, |
| 252 | + { |
| 253 | + key: "google-fonts-instrument", |
| 254 | + type: "link", |
| 255 | + value: "https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;700&display=swap", |
| 256 | + }, |
| 257 | + ], |
| 258 | + } |
| 259 | + }, |
| 260 | + searchSettings: { // optional InkeepSearchSettings |
| 261 | + tabs: ['All', 'Docs', 'Components', 'API Reference', 'Blogs', 'GitHub', 'Forums'].map((t) => [ |
| 262 | + t, |
| 263 | + { isAlwaysVisible: true }, |
| 264 | + ]), |
| 265 | + placeholder: 'Search', |
| 266 | + }, |
| 267 | + aiChatSettings: { // optional typeof InkeepAIChatSettings |
| 268 | + exampleQuestions: [ |
| 269 | + 'How does Reflex work?', |
| 270 | + 'What types of apps can I build with Reflex?', |
| 271 | + 'Where can I deploy my apps?', |
| 272 | + ], |
| 273 | + getTools: () => [ |
| 274 | + { |
| 275 | + type: "function", |
| 276 | + function: { |
| 277 | + name: "provideAnswerConfidence", |
| 278 | + description: "Determine how confident the AI assistant was and whether or not to escalate to humans.", |
| 279 | + parameters: escalationParams, |
| 280 | + }, |
| 281 | + renderMessageButtons: ({ args }) => { |
| 282 | + const confidence = args.answerConfidence; |
| 283 | + if (["not_confident", "no_sources", "other"].includes(confidence)) { |
| 284 | + return [ |
| 285 | + { |
| 286 | + label: "Contact Support", |
| 287 | + action: { |
| 288 | + 'type': 'open_form', |
| 289 | + }, |
| 290 | + } |
| 291 | + ]; |
| 292 | + } |
| 293 | + return []; |
| 294 | + }, |
| 295 | + }, |
| 296 | + ], |
| 297 | +
|
| 298 | + }, |
| 299 | +};""", |
| 300 | + ] |
| 301 | + |
| 302 | + @classmethod |
| 303 | + def create(cls): |
| 304 | + """Create the search component.""" |
| 305 | + return super().create( |
| 306 | + InkeepSearchBar.create( |
| 307 | + special_props=[Var("{...searchBarProps}")], |
| 308 | + ) |
| 309 | + ) |
| 310 | + |
| 311 | + |
| 312 | +inkeep = Search.create |
0 commit comments