Skip to content

feature: add multiple context blocks for context #3896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clients/search-component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"react-two-thumb-input-range": "^1.0.7",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.0.2",
"trieve-ts-sdk": "^0.0.102"
"trieve-ts-sdk": "^0.0.104"
},
"peerDependencies": {
"react": "^18.3.1 || ^19.0.0-rc",
Expand Down
8 changes: 6 additions & 2 deletions clients/search-component/src/utils/hooks/chat-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -801,12 +801,16 @@ function ChatProvider({ children }: { children: React.ReactNode }) {
[]
).splice(0, 1)
: undefined;
const descriptionOfFirstChunk = `Title: ${(firstChunk?.metadata as any)["title"]}\nDescription: ${firstChunk?.chunk_html}\nPrice: ${firstChunk?.num_value}`;
const jsonOfFirstChunk = {
title: (firstChunk?.metadata as any)["title"],
Description: firstChunk?.chunk_html,
price: firstChunk?.num_value,
};
return retryOperation(async () => {
const relevanceToolCallResp =
await trieveSDK.getToolCallFunctionParams(
{
user_message_text: `${props.relevanceToolCallOptions?.userMessageTextPrefix ?? defaultRelevanceToolCallOptions.userMessageTextPrefix} Determine the relevance of the below product for this query that user sent:\n\n${questionProp || currentQuestion}.\n\nHere are the details of the product you need to rank the relevance of:\n\n${descriptionOfFirstChunk}`,
user_message_text: `Rank the relevance of this product given the following query: ${questionProp || currentQuestion}. Here are the details of the product you need to rank the relevance of:\n\n${JSON.stringify(jsonOfFirstChunk)}. ${props.relevanceToolCallOptions?.userMessageTextPrefix ?? defaultRelevanceToolCallOptions.userMessageTextPrefix}`,
image_urls: imageUrls,
tool_function: {
name: "determine_relevance",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TextField,
} from "@shopify/ui-extensions-react/admin";
import { SuggestedQueriesResponse, TrieveSDK } from "trieve-ts-sdk";
import React from "react";

export type TrieveKey = {
id?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import {
reactExtension,
BlockStack,
Expand All @@ -10,9 +10,10 @@ import {
Banner,
TextArea,
Icon,
Box,
} from "@shopify/ui-extensions-react/admin";
import { TrieveProvider } from "./TrieveProvider";
import { useChunkExtraContent } from "./useChunkExtraContent";
import { TrieveProvider, useTrieve } from "./TrieveProvider";
import { ChunkMetadata } from "trieve-ts-sdk";

const TARGET = "admin.product-details.block.render";

Expand All @@ -32,69 +33,297 @@ export default reactExtension(TARGET, () => (

function App() {
const { data } = useApi(TARGET);
const productId = data.selected[0].id;
const simplifiedProductId = extractShopifyProductId(productId);
const [content, setContent] = useState("");
const [isSaving, setIsSaving] = useState(false);
const productId = extractShopifyProductId(data.selected[0].id);
const [content, setContent] = useState<ChunkMetadata[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [showSuccess, setShowSuccess] = useState(false);
const trieve = useTrieve();
const [extraContent, setExtraContent] = useState<ChunkMetadata[]>([]);
const [aiLoading, setAILoading] = useState(false);

const {
extraContent,
updateContent,
generateAIDescription,
loading,
aiLoading,
} = useChunkExtraContent(simplifiedProductId);
// Fetch current product extra content chunk
const getData = async () => {
if (!productId) {
console.info("Tried to fetch product content without id");
return;
}
try {
const result = await trieve.scroll({
filters: {
"should": [
{
"field": "tag_set",
"match_all": [
`${productId}-pdp-content`
],
},
{
"tracking_ids": [
`${productId}-pdp-content`
]
}
]
},
page_size: 20
});
if (!result) {
setExtraContent([]);
return;
}
setExtraContent(result.chunks);
} catch {
setExtraContent([]);
}
};

const generateAIDescription = async () => {
if (!productId) {
console.info("Tried to generate AI description without id");
return;
}
setAILoading(true);
const topic = await trieve.createTopic({
owner_id: "shopify-enrich-content-block",
first_user_message: "Describe this product",
name: "Shopify Enrich Content Block",
});

const message = await trieve.createMessage({
topic_id: topic.id,
new_message_content:
"Describe this product to add extra context to an LLM. Generate a description for an online shop. Keep it to 3 sentences maximum. Do not include an introduction or welcome message",
use_group_search: true,
filters: {
must: [
{
field: "group_tracking_ids",
match_all: [productId],
},
],
},
llm_options: {
stream_response: false,
},
});

const response = message.split("||").at(1);
if (!response) {
console.error("No response from AI");
return;
}

const chunk = await trieve.createChunk({
chunk_html: response,
tag_set: [`${productId}-pdp-content`],
group_tracking_ids: [productId],
});
// Check if chunk.chunk_metadata is a list
if (!Array.isArray(chunk.chunk_metadata)) {
setExtraContent((prev) => {
return [
chunk.chunk_metadata,
...prev,
]
});
setShowSuccess(true);
}
setAILoading(false);
};

useEffect(() => {
if (!productId) {
return;
}
getData();
}, [productId]);

const [indexBeingEdited, setIndexBeingEdited] = useState<number | null>(null);

useEffect(() => {
if (extraContent) {
setContent(extraContent);
}
}, [extraContent]);

const handleSave = async () => {
setIsSaving(true);
try {
await updateContent(content);
setShowSuccess(true);
} finally {
setIsSaving(false);

const upsertContent = (chunk: ChunkMetadata) => {
setIndexBeingEdited(null);
if (chunk.id != "") {
trieve.updateChunk({
chunk_id: chunk.id,
chunk_html: chunk.chunk_html
})
} else if (productId) {
trieve.createChunk({
chunk_html: chunk.chunk_html,
tag_set: [`${productId}-pdp-content`],
group_tracking_ids: [productId],
})
}
};

return (
<AdminBlock title="Enrich Content">
<AdminBlock title="AI Context">
<BlockStack gap="base">
{showSuccess && (
<Banner tone="success" onDismiss={() => setShowSuccess(false)}>
Content saved successfully
</Banner>
)}
<BlockStack>
<InlineStack inlineAlignment="space-between" blockAlignment="end">
<Text>Extra Content</Text>
<InlineStack inlineAlignment="space-between" blockAlignment="center">
<Box
inlineSize="80%">
<Text>Product context for the AI</Text>
</Box>
<InlineStack
blockAlignment="center"
inlineAlignment="end"
gap="base base"
>
<Button
disabled={aiLoading}
variant="tertiary"
onPress={generateAIDescription}
>
<InlineStack blockAlignment="center" gap="small small">
<InlineStack blockAlignment="center">
<Icon name="WandMinor" />
{aiLoading ? "Generating..." : "Generate AI Description"}
{aiLoading ? "Generating..." : "Generate AI Context"}
</InlineStack>
</Button>

<Button
onPress={() => {
setExtraContent((prev) => [
{
id: "",
chunk_html: "",
tag_set: [`${productId}-pdp-content`],
group_tracking_ids: [productId],
created_at: "",
updated_at: "",
dataset_id: "",
weight: 1 // Just to make the lsp stop
},
...prev,
]);
setIndexBeingEdited(0);
setCurrentPage(1);
}}
>
<InlineStack blockAlignment="center">
<Icon name="PlusMinor" />
Add Context
</InlineStack>
</Button>
</InlineStack>
<TextArea
rows={4}
disabled={loading}
label=""
value={content}
onChange={setContent}
/>
</BlockStack>
<InlineStack gap="base" inlineAlignment="end">
<Button onPress={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Save Content"}
</InlineStack>
<Box>
{content.map((chunk, index) => {
if (index != (currentPage - 1)) {
return null;
}

return (
<Box key={index} padding="base small">
<InlineStack
blockAlignment="center"
inlineAlignment="space-between"
inlineSize="100%"
gap="large"
>
<Box
inlineSize={`${index === indexBeingEdited ? "100%" : "75%"}`}
>
{index === indexBeingEdited ? (
<TextArea
rows={4}
label=""
value={chunk.chunk_html ?? ""}
onChange={(value) => {
// updateContent(index, value);
setContent((prevContent) => prevContent.map((prevChunk) => prevChunk.id == chunk.id
? { ...prevChunk, chunk_html: value }
: prevChunk
))
}}
/>
) : (
<Text>{chunk.chunk_html}</Text>
)}
</Box>
<Box inlineSize="25%">
<InlineStack
inlineSize="100%"
inlineAlignment="end"
blockAlignment="center"
>
{index === indexBeingEdited ? (
<>
<Button
onClick={() => {
upsertContent(chunk);
}}
variant="primary"
>
<Text>Finish</Text>
</Button>
</>
) : (
<>
<Button
onClick={() => {
setIndexBeingEdited(index);
}}
variant="tertiary"
>
<Icon name="EditMinor" />
</Button>
<Button
onClick={() => {
trieve.deleteChunkById({
chunkId: chunk.id
});
setContent((prevContent) => prevContent.filter((prevChunk) => prevChunk.id != chunk.id
))
if (index === content.length - 1) {
setCurrentPage((prev) => prev - 1);
}
}}
variant="tertiary"
>
<Icon name="DeleteMinor" />
</Button>
</>
)}
</InlineStack>
</Box>
</InlineStack>
</Box>
);
})}
</Box>
<InlineStack
paddingBlockStart="large"
blockAlignment="center"
inlineAlignment="center"
>
<Button
onPress={() => setCurrentPage((prev) => prev - 1)}
disabled={currentPage === 1}
>
<Icon name="ChevronLeftMinor" />
</Button>
<InlineStack
inlineSize={50}
blockAlignment="center"
inlineAlignment="center"
>
<Text>{currentPage} / {content.length}</Text>
</InlineStack>
<Button
onPress={() => setCurrentPage((prev) => prev + 1)}
disabled={currentPage >= content.length}
>
<Icon name="ChevronRightMinor" />
</Button>
</InlineStack>
</BlockStack>
Expand Down
Loading