1
- import { useEffect , useState } from "react" ;
1
+ import React , { useEffect , useState } from "react" ;
2
2
import {
3
3
reactExtension ,
4
4
BlockStack ,
@@ -10,9 +10,10 @@ import {
10
10
Banner ,
11
11
TextArea ,
12
12
Icon ,
13
+ Box ,
13
14
} from "@shopify/ui-extensions-react/admin" ;
14
- import { TrieveProvider } from "./TrieveProvider" ;
15
- import { useChunkExtraContent } from "./useChunkExtraContent " ;
15
+ import { TrieveProvider , useTrieve } from "./TrieveProvider" ;
16
+ import { ChunkMetadata } from "trieve-ts-sdk " ;
16
17
17
18
const TARGET = "admin.product-details.block.render" ;
18
19
@@ -32,69 +33,297 @@ export default reactExtension(TARGET, () => (
32
33
33
34
function App ( ) {
34
35
const { data } = useApi ( TARGET ) ;
35
- const productId = data . selected [ 0 ] . id ;
36
- const simplifiedProductId = extractShopifyProductId ( productId ) ;
37
- const [ content , setContent ] = useState ( "" ) ;
38
- const [ isSaving , setIsSaving ] = useState ( false ) ;
36
+ const productId = extractShopifyProductId ( data . selected [ 0 ] . id ) ;
37
+ const [ content , setContent ] = useState < ChunkMetadata [ ] > ( [ ] ) ;
38
+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
39
39
const [ showSuccess , setShowSuccess ] = useState ( false ) ;
40
+ const trieve = useTrieve ( ) ;
41
+ const [ extraContent , setExtraContent ] = useState < ChunkMetadata [ ] > ( [ ] ) ;
42
+ const [ aiLoading , setAILoading ] = useState ( false ) ;
40
43
41
- const {
42
- extraContent,
43
- updateContent,
44
- generateAIDescription,
45
- loading,
46
- aiLoading,
47
- } = useChunkExtraContent ( simplifiedProductId ) ;
44
+ // Fetch current product extra content chunk
45
+ const getData = async ( ) => {
46
+ if ( ! productId ) {
47
+ console . info ( "Tried to fetch product content without id" ) ;
48
+ return ;
49
+ }
50
+ try {
51
+ const result = await trieve . scroll ( {
52
+ filters : {
53
+ "should" : [
54
+ {
55
+ "field" : "tag_set" ,
56
+ "match_all" : [
57
+ `${ productId } -pdp-content`
58
+ ] ,
59
+ } ,
60
+ {
61
+ "tracking_ids" : [
62
+ `${ productId } -pdp-content`
63
+ ]
64
+ }
65
+ ]
66
+ } ,
67
+ page_size : 20
68
+ } ) ;
69
+ if ( ! result ) {
70
+ setExtraContent ( [ ] ) ;
71
+ return ;
72
+ }
73
+ setExtraContent ( result . chunks ) ;
74
+ } catch {
75
+ setExtraContent ( [ ] ) ;
76
+ }
77
+ } ;
78
+
79
+ const generateAIDescription = async ( ) => {
80
+ if ( ! productId ) {
81
+ console . info ( "Tried to generate AI description without id" ) ;
82
+ return ;
83
+ }
84
+ setAILoading ( true ) ;
85
+ const topic = await trieve . createTopic ( {
86
+ owner_id : "shopify-enrich-content-block" ,
87
+ first_user_message : "Describe this product" ,
88
+ name : "Shopify Enrich Content Block" ,
89
+ } ) ;
90
+
91
+ const message = await trieve . createMessage ( {
92
+ topic_id : topic . id ,
93
+ new_message_content :
94
+ "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" ,
95
+ use_group_search : true ,
96
+ filters : {
97
+ must : [
98
+ {
99
+ field : "group_tracking_ids" ,
100
+ match_all : [ productId ] ,
101
+ } ,
102
+ ] ,
103
+ } ,
104
+ llm_options : {
105
+ stream_response : false ,
106
+ } ,
107
+ } ) ;
108
+
109
+ const response = message . split ( "||" ) . at ( 1 ) ;
110
+ if ( ! response ) {
111
+ console . error ( "No response from AI" ) ;
112
+ return ;
113
+ }
114
+
115
+ const chunk = await trieve . createChunk ( {
116
+ chunk_html : response ,
117
+ tag_set : [ `${ productId } -pdp-content` ] ,
118
+ group_tracking_ids : [ productId ] ,
119
+ } ) ;
120
+ // Check if chunk.chunk_metadata is a list
121
+ if ( ! Array . isArray ( chunk . chunk_metadata ) ) {
122
+ setExtraContent ( ( prev ) => {
123
+ return [
124
+ chunk . chunk_metadata ,
125
+ ...prev ,
126
+ ]
127
+ } ) ;
128
+ setShowSuccess ( true ) ;
129
+ }
130
+ setAILoading ( false ) ;
131
+ } ;
132
+
133
+ useEffect ( ( ) => {
134
+ if ( ! productId ) {
135
+ return ;
136
+ }
137
+ getData ( ) ;
138
+ } , [ productId ] ) ;
139
+
140
+ const [ indexBeingEdited , setIndexBeingEdited ] = useState < number | null > ( null ) ;
48
141
49
142
useEffect ( ( ) => {
50
143
if ( extraContent ) {
51
144
setContent ( extraContent ) ;
52
145
}
53
146
} , [ extraContent ] ) ;
54
147
55
- const handleSave = async ( ) => {
56
- setIsSaving ( true ) ;
57
- try {
58
- await updateContent ( content ) ;
59
- setShowSuccess ( true ) ;
60
- } finally {
61
- setIsSaving ( false ) ;
148
+
149
+ const upsertContent = ( chunk : ChunkMetadata ) => {
150
+ setIndexBeingEdited ( null ) ;
151
+ if ( chunk . id != "" ) {
152
+ trieve . updateChunk ( {
153
+ chunk_id : chunk . id ,
154
+ chunk_html : chunk . chunk_html
155
+ } )
156
+ } else if ( productId ) {
157
+ trieve . createChunk ( {
158
+ chunk_html : chunk . chunk_html ,
159
+ tag_set : [ `${ productId } -pdp-content` ] ,
160
+ group_tracking_ids : [ productId ] ,
161
+ } )
62
162
}
63
163
} ;
64
164
65
165
return (
66
- < AdminBlock title = "Enrich Content " >
166
+ < AdminBlock title = "AI Context " >
67
167
< BlockStack gap = "base" >
68
168
{ showSuccess && (
69
169
< Banner tone = "success" onDismiss = { ( ) => setShowSuccess ( false ) } >
70
170
Content saved successfully
71
171
</ Banner >
72
172
) }
73
- < BlockStack >
74
- < InlineStack inlineAlignment = "space-between" blockAlignment = "end" >
75
- < Text > Extra Content</ Text >
173
+ < InlineStack inlineAlignment = "space-between" blockAlignment = "center" >
174
+ < Box
175
+ inlineSize = "80%" >
176
+ < Text > Product context for the AI</ Text >
177
+ </ Box >
178
+ < InlineStack
179
+ blockAlignment = "center"
180
+ inlineAlignment = "end"
181
+ gap = "base base"
182
+ >
76
183
< Button
77
184
disabled = { aiLoading }
78
- variant = "tertiary"
79
185
onPress = { generateAIDescription }
80
186
>
81
- < InlineStack blockAlignment = "center" gap = "small small" >
187
+ < InlineStack blockAlignment = "center" >
82
188
< Icon name = "WandMinor" />
83
- { aiLoading ? "Generating..." : "Generate AI Description" }
189
+ { aiLoading ? "Generating..." : "Generate AI Context" }
190
+ </ InlineStack >
191
+ </ Button >
192
+
193
+ < Button
194
+ onPress = { ( ) => {
195
+ setExtraContent ( ( prev ) => [
196
+ {
197
+ id : "" ,
198
+ chunk_html : "" ,
199
+ tag_set : [ `${ productId } -pdp-content` ] ,
200
+ group_tracking_ids : [ productId ] ,
201
+ created_at : "" ,
202
+ updated_at : "" ,
203
+ dataset_id : "" ,
204
+ weight : 1 // Just to make the lsp stop
205
+ } ,
206
+ ...prev ,
207
+ ] ) ;
208
+ setIndexBeingEdited ( 0 ) ;
209
+ setCurrentPage ( 1 ) ;
210
+ } }
211
+ >
212
+ < InlineStack blockAlignment = "center" >
213
+ < Icon name = "PlusMinor" />
214
+ Add Context
84
215
</ InlineStack >
85
216
</ Button >
86
217
</ InlineStack >
87
- < TextArea
88
- rows = { 4 }
89
- disabled = { loading }
90
- label = ""
91
- value = { content }
92
- onChange = { setContent }
93
- />
94
- </ BlockStack >
95
- < InlineStack gap = "base" inlineAlignment = "end" >
96
- < Button onPress = { handleSave } disabled = { isSaving } >
97
- { isSaving ? "Saving..." : "Save Content" }
218
+ </ InlineStack >
219
+ < Box >
220
+ { content . map ( ( chunk , index ) => {
221
+ if ( index != ( currentPage - 1 ) ) {
222
+ return null ;
223
+ }
224
+
225
+ return (
226
+ < Box key = { index } padding = "base small" >
227
+ < InlineStack
228
+ blockAlignment = "center"
229
+ inlineAlignment = "space-between"
230
+ inlineSize = "100%"
231
+ gap = "large"
232
+ >
233
+ < Box
234
+ inlineSize = { `${ index === indexBeingEdited ? "100%" : "75%" } ` }
235
+ >
236
+ { index === indexBeingEdited ? (
237
+ < TextArea
238
+ rows = { 4 }
239
+ label = ""
240
+ value = { chunk . chunk_html ?? "" }
241
+ onChange = { ( value ) => {
242
+ // updateContent(index, value);
243
+ setContent ( ( prevContent ) => prevContent . map ( ( prevChunk ) => prevChunk . id == chunk . id
244
+ ? { ...prevChunk , chunk_html : value }
245
+ : prevChunk
246
+ ) )
247
+ } }
248
+ />
249
+ ) : (
250
+ < Text > { chunk . chunk_html } </ Text >
251
+ ) }
252
+ </ Box >
253
+ < Box inlineSize = "25%" >
254
+ < InlineStack
255
+ inlineSize = "100%"
256
+ inlineAlignment = "end"
257
+ blockAlignment = "center"
258
+ >
259
+ { index === indexBeingEdited ? (
260
+ < >
261
+ < Button
262
+ onClick = { ( ) => {
263
+ upsertContent ( chunk ) ;
264
+ } }
265
+ variant = "primary"
266
+ >
267
+ < Text > Finish</ Text >
268
+ </ Button >
269
+ </ >
270
+ ) : (
271
+ < >
272
+ < Button
273
+ onClick = { ( ) => {
274
+ setIndexBeingEdited ( index ) ;
275
+ } }
276
+ variant = "tertiary"
277
+ >
278
+ < Icon name = "EditMinor" />
279
+ </ Button >
280
+ < Button
281
+ onClick = { ( ) => {
282
+ trieve . deleteChunkById ( {
283
+ chunkId : chunk . id
284
+ } ) ;
285
+ setContent ( ( prevContent ) => prevContent . filter ( ( prevChunk ) => prevChunk . id != chunk . id
286
+ ) )
287
+ if ( index === content . length - 1 ) {
288
+ setCurrentPage ( ( prev ) => prev - 1 ) ;
289
+ }
290
+ } }
291
+ variant = "tertiary"
292
+ >
293
+ < Icon name = "DeleteMinor" />
294
+ </ Button >
295
+ </ >
296
+ ) }
297
+ </ InlineStack >
298
+ </ Box >
299
+ </ InlineStack >
300
+ </ Box >
301
+ ) ;
302
+ } ) }
303
+ </ Box >
304
+ < InlineStack
305
+ paddingBlockStart = "large"
306
+ blockAlignment = "center"
307
+ inlineAlignment = "center"
308
+ >
309
+ < Button
310
+ onPress = { ( ) => setCurrentPage ( ( prev ) => prev - 1 ) }
311
+ disabled = { currentPage === 1 }
312
+ >
313
+ < Icon name = "ChevronLeftMinor" />
314
+ </ Button >
315
+ < InlineStack
316
+ inlineSize = { 50 }
317
+ blockAlignment = "center"
318
+ inlineAlignment = "center"
319
+ >
320
+ < Text > { currentPage } / { content . length } </ Text >
321
+ </ InlineStack >
322
+ < Button
323
+ onPress = { ( ) => setCurrentPage ( ( prev ) => prev + 1 ) }
324
+ disabled = { currentPage >= content . length }
325
+ >
326
+ < Icon name = "ChevronRightMinor" />
98
327
</ Button >
99
328
</ InlineStack >
100
329
</ BlockStack >
0 commit comments