Skip to content

Commit fcab72d

Browse files
skeptrunedevdensumesh
authored andcommitted
feat(dashboard): add demo page auto configurator
1 parent 4b54280 commit fcab72d

3 files changed

Lines changed: 207 additions & 20 deletions

File tree

frontends/dashboard/src/hooks/usePublicPageSettings.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ApiRoutes } from "../components/Routes";
1919
import { HeroPatterns } from "../pages/dataset/HeroPatterns";
2020
import { createInitializedContext } from "../utils/initialize";
2121
import {
22+
defaultOpenGraphMetadata,
2223
defaultPriceToolCallOptions,
2324
defaultRelevanceToolCallOptions,
2425
} from "../pages/dataset/PublicPageSettings";
@@ -92,6 +93,12 @@ export const { use: usePublicPage, provider: PublicPageProvider } =
9293
});
9394
}
9495

96+
if (!extraParams.openGraphMetadata) {
97+
setExtraParams("openGraphMetadata", {
98+
...defaultOpenGraphMetadata,
99+
});
100+
}
101+
95102
// manually set the array for rolemessages to simplify logic
96103
// context blocks until it's set
97104
if (
@@ -115,10 +122,6 @@ export const { use: usePublicPage, provider: PublicPageProvider } =
115122
}
116123
}
117124

118-
if (extraParams.inline == undefined || extraParams.inline == null) {
119-
setExtraParams("inline", true);
120-
}
121-
122125
if (
123126
extraParams.showResultHighlights === null ||
124127
extraParams.showResultHighlights === undefined
@@ -210,6 +213,7 @@ export const { use: usePublicPage, provider: PublicPageProvider } =
210213
placeholder: "Search...",
211214
defaultSearchMode: "chat",
212215
type: "ecommerce",
216+
inline: false,
213217
openLinksInNewTab: true,
214218
},
215219
},

frontends/dashboard/src/pages/dataset/PublicPageSettings.tsx

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-call */
12
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
23
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
3-
import { createEffect, createSignal, For, Show } from "solid-js";
4+
import { createEffect, createSignal, For, Show, useContext } from "solid-js";
45
import { CopyButton } from "../../components/CopyButton";
56
import { FaRegularCircleQuestion } from "solid-icons/fa";
67
import { JsonInput, MultiStringInput, Select, Tooltip } from "shared/ui";
@@ -16,11 +17,16 @@ import {
1617
} from "../../hooks/usePublicPageSettings";
1718
import { createStore } from "solid-js/store";
1819
import {
20+
$OpenApiTs,
21+
CrawlRequest,
1922
PriceToolCallOptions,
2023
PublicPageTabMessage,
2124
RelevanceToolCallOptions,
2225
} from "trieve-ts-sdk";
2326
import FilterSidebarBuilder from "../../components/FilterSidebarBuilder";
27+
import { DatasetContext } from "../../contexts/DatasetContext";
28+
import { useTrieve } from "../../hooks/useTrieve";
29+
import { UserContext } from "../../contexts/UserContext";
2430

2531
export const PublicPageSettingsPage = () => {
2632
return (
@@ -42,6 +48,13 @@ export const PublicPageSettingsPage = () => {
4248
);
4349
};
4450

51+
export const defaultOpenGraphMetadata = {
52+
title: "Trieve AI Sitesearch",
53+
description:
54+
"Trieve AI Sitesearch is ChatGPT for your website and content. Replicate the experience of a human sales associate with AI.",
55+
image: "",
56+
};
57+
4558
export const defaultRelevanceToolCallOptions: RelevanceToolCallOptions = {
4659
userMessageTextPrefix:
4760
"Be extra picky and detailed. Thoroughly examine all details of the query and product.",
@@ -95,6 +108,11 @@ const componentVersionOptions = [
95108
];
96109

97110
const PublicPageControls = () => {
111+
const [prospectiveCustomerUrl, setProspectiveCustomerUrl] = createSignal("");
112+
const [docColors, setDocColors] = createSignal<string[]>([]);
113+
const [loadingDefaultConfig, setLoadingDefaultConfig] = createSignal(false);
114+
const userContext = useContext(UserContext);
115+
const datasetContext = useContext(DatasetContext);
98116
const {
99117
extraParams,
100118
setExtraParams,
@@ -103,6 +121,138 @@ const PublicPageControls = () => {
103121
unpublishDataset,
104122
publicUrl,
105123
} = usePublicPage();
124+
const trieve = useTrieve();
125+
126+
const updateTrackingId = async (newTrackingId: string) => {
127+
const result = await trieve.fetch("/api/dataset", "put", {
128+
data: {
129+
dataset_id: datasetContext.datasetId(),
130+
new_tracking_id: newTrackingId,
131+
},
132+
organizationId: userContext.selectedOrg().id,
133+
});
134+
await userContext.invalidate();
135+
return result;
136+
};
137+
138+
const autoConfigure = async (url: string) => {
139+
setLoadingDefaultConfig(true);
140+
const domain = url.replace(/^(?:https?:\/\/)?(?:www\.)?/, "").split("/")[0];
141+
const domainParts = domain.split(".");
142+
const domainName = domainParts.slice(0, -1).join(".");
143+
void updateTrackingId(domainName);
144+
145+
const proxyUrl = `https://corsproxy.io/?url=${encodeURIComponent(url)}`;
146+
const pageResponse = await fetch(proxyUrl);
147+
const pageText = await pageResponse.text();
148+
const parser = new DOMParser();
149+
const doc = parser.parseFromString(pageText, "text/html");
150+
151+
const titleQuerysSelectors = [
152+
"title",
153+
"meta[property='og:title']",
154+
"meta[name='twitter:title']",
155+
];
156+
const title = titleQuerysSelectors
157+
.map((selector) => doc.querySelector(selector))
158+
.find((title) => title?.getAttribute("content") || title?.textContent);
159+
const titleText = title?.getAttribute("content") || title?.textContent;
160+
if (titleText) {
161+
setExtraParams("brandName", titleText);
162+
setExtraParams("forBrandName", titleText);
163+
setExtraParams("headingPrefix", `Demo For`);
164+
setExtraParams("openGraphMetadata", "title", titleText);
165+
}
166+
167+
const faviconQuerySelectors = [
168+
"link[rel='shortcut icon']",
169+
"link[rel='icon']",
170+
"link[rel='apple-touch-icon']",
171+
];
172+
const faviconHref = faviconQuerySelectors
173+
.map((selector) => doc.querySelector(selector))
174+
.find((link) => link?.getAttribute("href"))
175+
?.getAttribute("href");
176+
if (faviconHref) {
177+
const url = new URL(faviconHref, pageResponse.url);
178+
const faviconUrl = url.href.replace(/^\//, "");
179+
setExtraParams("navLogoImgSrcUrl", faviconUrl);
180+
setExtraParams("brandLogoImgSrcUrl", faviconUrl);
181+
}
182+
183+
const ogDescriptionQuerySelectors = [
184+
"meta[property='og:description']",
185+
"meta[name='twitter:description']",
186+
];
187+
const ogDescription = ogDescriptionQuerySelectors
188+
.map((selector) => doc.querySelector(selector))
189+
.find((description) => description?.getAttribute("content"))
190+
?.getAttribute("content");
191+
if (ogDescription) {
192+
setExtraParams("openGraphMetadata", "description", ogDescription);
193+
}
194+
195+
const ogImageQuerySelectors = [
196+
"meta[property='og:image']",
197+
"meta[name='twitter:image']",
198+
];
199+
const ogImage = ogImageQuerySelectors
200+
.map((selector) => doc.querySelector(selector))
201+
.find((image) => image?.getAttribute("content"))
202+
?.getAttribute("content");
203+
if (ogImage) {
204+
setExtraParams("openGraphMetadata", "image", ogImage);
205+
}
206+
207+
const hexColorRegex = /#([0-9A-Fa-f]{3,6})/g;
208+
const hexColors = Array.from(pageText.matchAll(hexColorRegex)).map(
209+
(match) => match[0],
210+
);
211+
const uniqueColors = new Set(hexColors);
212+
const range = 64;
213+
const sortedColors = Array.from(uniqueColors).sort((a, b) => {
214+
const [ar, ag, ab] = [
215+
Math.floor(parseInt(a.slice(1, 3), 16) / range),
216+
Math.floor(parseInt(a.slice(3, 5), 16) / range),
217+
Math.floor(parseInt(a.slice(5, 7), 16) / range),
218+
];
219+
const [br, bg, bb] = [
220+
Math.floor(parseInt(b.slice(1, 3), 16) / range),
221+
Math.floor(parseInt(b.slice(3, 5), 16) / range),
222+
Math.floor(parseInt(b.slice(5, 7), 16) / range),
223+
];
224+
return ar - br || ag - bg || ab - bb;
225+
});
226+
setDocColors(sortedColors);
227+
228+
setLoadingDefaultConfig(false);
229+
};
230+
231+
createEffect(() => {
232+
void (
233+
trieve.fetch<"eject">(
234+
`/api/crawl?limit=10&page=1` as keyof $OpenApiTs,
235+
"get",
236+
{
237+
datasetId: datasetContext.datasetId(),
238+
},
239+
) as Promise<CrawlRequest[]>
240+
).then((result) => {
241+
const lastUrl = result.length ? result[0].url : "";
242+
setProspectiveCustomerUrl(lastUrl);
243+
});
244+
245+
const handleKeyDown = (event: KeyboardEvent) => {
246+
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
247+
event.preventDefault();
248+
void publishDataset();
249+
}
250+
};
251+
window.addEventListener("keydown", handleKeyDown);
252+
return () => {
253+
window.removeEventListener("keydown", handleKeyDown);
254+
};
255+
});
106256

107257
return (
108258
<>
@@ -123,7 +273,26 @@ const PublicPageControls = () => {
123273
</div>
124274
</Show>
125275
<Show when={isPublic()}>
126-
<div class="mt-4 flex content-center items-center gap-1.5 gap-x-2.5">
276+
<div class="flex items-center space-x-2">
277+
<input
278+
placeholder="https://www.prospectivecustomer.com"
279+
value={prospectiveCustomerUrl()}
280+
onInput={(e) => {
281+
setProspectiveCustomerUrl(e.currentTarget.value);
282+
}}
283+
class="block w-full rounded border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
284+
/>
285+
<button
286+
onClick={() => {
287+
void autoConfigure(prospectiveCustomerUrl());
288+
}}
289+
disabled={loadingDefaultConfig()}
290+
class="inline-flex w-[200px] justify-center rounded-md bg-magenta-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-magenta-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-magenta-900 disabled:animate-pulse"
291+
>
292+
{loadingDefaultConfig() ? "Loading..." : "Auto Configure"}
293+
</button>
294+
</div>
295+
<div class="mb-6 py-2 flex content-center items-center gap-1.5 gap-x-2.5">
127296
<span class="font-medium">Published Url:</span>{" "}
128297
<a class="text-magenta-400" href={publicUrl()} target="_blank">
129298
{publicUrl()}
@@ -179,7 +348,18 @@ const PublicPageControls = () => {
179348
options={["light", "dark"]}
180349
/>
181350
</div>
182-
<div class="grow">
351+
<div class="grow max-w-[50%]">
352+
<For each={docColors()}>
353+
{(color) => (
354+
<button
355+
class="w-6 h-6 rounded-lg"
356+
style={{ "background-color": color }}
357+
onClick={() => {
358+
setExtraParams("brandColor", color);
359+
}}
360+
/>
361+
)}
362+
</For>
183363
<div class="flex items-center gap-1">
184364
<label class="block" for="">
185365
Brand Color
@@ -1864,14 +2044,9 @@ const HtmlEditor = (props: {
18642044

18652045
export const OgOptions = () => {
18662046
const { extraParams, setExtraParams } = usePublicPage();
1867-
const [defaultDetailOpen] = createSignal(
1868-
!!extraParams.openGraphMetadata?.title ||
1869-
!!extraParams.openGraphMetadata?.image ||
1870-
!!extraParams.openGraphMetadata?.description,
1871-
);
18722047

18732048
return (
1874-
<details class="my-4" open={defaultDetailOpen()}>
2049+
<details class="my-4">
18752050
<summary class="cursor-pointer text-sm font-medium">Open Graph</summary>
18762051
<div class="flex gap-4 pt-2">
18772052
<div class="grow">

frontends/shared/ui/MultiStringInput.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ interface MultiStringInputProps {
1313
disabled?: boolean;
1414
}
1515

16+
type TEntry = {
17+
value: string;
18+
id: string;
19+
};
20+
1621
const addBlankStringIfEmpty = (value: string[]) => {
1722
if (value.length === 0) {
1823
return [""];
@@ -21,13 +26,16 @@ const addBlankStringIfEmpty = (value: string[]) => {
2126
};
2227

2328
export const MultiStringInput = (props: MultiStringInputProps) => {
24-
const [proxyStore, setProxyStore] = createStore(
25-
// eslint-disable-next-line solid/reactivity
26-
addBlankStringIfEmpty(props.value).map((value) => ({
27-
value,
28-
id: Math.random().toString(36).slice(2),
29-
})),
30-
);
29+
const [proxyStore, setProxyStore] = createStore<TEntry[]>([]);
30+
31+
createEffect(() => {
32+
setProxyStore(
33+
addBlankStringIfEmpty(props.value).map((value) => ({
34+
value,
35+
id: Math.random().toString(36).slice(2),
36+
})),
37+
);
38+
});
3139

3240
createEffect(() => {
3341
props.onChange(

0 commit comments

Comments
 (0)