1+ import { invariant } from '@epic-web/invariant'
12import { useTransition } from 'react'
23import {
34 ErrorBoundary ,
45 useErrorBoundary ,
56 type FallbackProps ,
67} from 'react-error-boundary'
7- import { useMcpUiInit , navigateToLink } from '#app/utils/mcp.ts'
8+ import { useRevalidator } from 'react-router'
9+ import { z } from 'zod'
10+ import {
11+ useMcpUiInit ,
12+ navigateToLink ,
13+ callTool ,
14+ sendPrompt ,
15+ } from '#app/utils/mcp.ts'
16+ import { useDoubleCheck } from '#app/utils/misc.ts'
817import { type Route } from './+types/journal-viewer.tsx'
918
1019export async function loader ( { context } : Route . LoaderArgs ) {
@@ -18,7 +27,7 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
1827 useMcpUiInit ( )
1928
2029 return (
21- < div className = "bg-background min -h-screen p-4" >
30+ < div className = "bg-background max -h-[800px] overflow-y-auto p-4" >
2231 < div className = "mx-auto max-w-4xl" >
2332 < div className = "bg-card mb-6 rounded-xl p-6 shadow-lg" >
2433 < h1 className = "text-foreground mb-2 text-3xl font-bold" >
@@ -68,10 +77,12 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
6877 </ span >
6978 </ div >
7079
71- < div className = "mt-4" >
80+ < div className = "mt-4 flex gap-2 " >
7281 < button className = "text-primary text-sm font-medium hover:underline" >
7382 View Details
7483 </ button >
84+ < SummarizeEntryButton entry = { entry } />
85+ < DeleteEntryButton entry = { entry } />
7586 </ div >
7687 </ div >
7788 </ div >
@@ -96,10 +107,10 @@ function XPostLinkError({ error, resetErrorBoundary }: FallbackProps) {
96107 return (
97108 < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
98109 < p className = "text-sm font-medium" > Failed to post on X</ p >
99- < p className = "text-xs text- destructive/80" > { error . message } </ p >
110+ < p className = "text-destructive/80 text-xs " > { error . message } </ p >
100111 < button
101112 onClick = { resetErrorBoundary }
102- className = "mt-2 text-xs text-destructive hover:underline cursor-pointer "
113+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
103114 >
104115 Try again
105116 </ button >
@@ -128,12 +139,161 @@ function XPostLinkImpl({ entryCount }: { entryCount: number }) {
128139 < button
129140 onClick = { handlePostOnX }
130141 disabled = { isPending }
131- className = "bg-black text-white px-4 py-2 rounded-lg hover:bg-gray-800 disabled:bg-gray-400 disabled: cursor-not-allowed transition-colors flex items-center gap-2 cursor-pointer "
142+ className = "flex cursor-pointer items-center gap-2 rounded-lg bg-black px-4 py-2 text-white transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-400 "
132143 >
133- < svg className = "w -5 h -5" fill = "currentColor" viewBox = "0 0 24 24" >
144+ < svg className = "h -5 w -5" fill = "currentColor" viewBox = "0 0 24 24" >
134145 < path d = "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
135146 </ svg >
136147 { isPending ? 'Posting...' : 'Post' }
137148 </ button >
138149 )
139150}
151+
152+ function DeleteEntryButton ( {
153+ entry,
154+ } : {
155+ entry : { id : number ; title : string }
156+ } ) {
157+ return (
158+ < ErrorBoundary FallbackComponent = { DeleteEntryError } >
159+ < DeleteEntryButtonImpl entry = { entry } />
160+ </ ErrorBoundary >
161+ )
162+ }
163+
164+ function DeleteEntryError ( { error, resetErrorBoundary } : FallbackProps ) {
165+ return (
166+ < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
167+ < p className = "text-sm font-medium" > Failed to delete entry</ p >
168+ < p className = "text-destructive/80 text-xs" > { error . message } </ p >
169+ < button
170+ onClick = { resetErrorBoundary }
171+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
172+ >
173+ Try again
174+ </ button >
175+ </ div >
176+ )
177+ }
178+
179+ function DeleteEntryButtonImpl ( {
180+ entry,
181+ } : {
182+ entry : { id : number ; title : string }
183+ } ) {
184+ const [ isPending , startTransition ] = useTransition ( )
185+ const { doubleCheck, getButtonProps } = useDoubleCheck ( )
186+ const { showBoundary } = useErrorBoundary ( )
187+ const revalidator = useRevalidator ( )
188+
189+ const handleDelete = ( ) => {
190+ startTransition ( async ( ) => {
191+ try {
192+ await callTool ( 'delete_entry' , { id : entry . id } )
193+ await revalidator . revalidate ( )
194+ } catch ( err ) {
195+ showBoundary ( err )
196+ }
197+ } )
198+ }
199+
200+ return (
201+ < button
202+ { ...getButtonProps ( {
203+ onClick : doubleCheck ? handleDelete : undefined ,
204+ disabled : isPending ,
205+ className : `text-sm font-medium px-3 py-1.5 rounded-md border transition-colors ${
206+ doubleCheck
207+ ? 'bg-destructive text-destructive-foreground border-destructive hover:bg-destructive/90'
208+ : 'text-destructive border-destructive/20 hover:bg-destructive/10 hover:border-destructive/40'
209+ } ${ isPending ? 'opacity-50 cursor-not-allowed' : '' } `,
210+ } ) }
211+ >
212+ { isPending ? 'Deleting...' : doubleCheck ? `Confirm?` : 'Delete' }
213+ </ button >
214+ )
215+ }
216+
217+ function SummarizeEntryButton ( {
218+ entry,
219+ } : {
220+ entry : { id : number ; title : string }
221+ } ) {
222+ return (
223+ < ErrorBoundary FallbackComponent = { SummarizeEntryError } >
224+ < SummarizeEntryButtonImpl entry = { entry } />
225+ </ ErrorBoundary >
226+ )
227+ }
228+
229+ function SummarizeEntryError ( { error, resetErrorBoundary } : FallbackProps ) {
230+ return (
231+ < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
232+ < p className = "text-sm font-medium" > Failed to summarize entry</ p >
233+ < p className = "text-destructive/80 text-xs" > { error . message } </ p >
234+ < button
235+ onClick = { resetErrorBoundary }
236+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
237+ >
238+ Try again
239+ </ button >
240+ </ div >
241+ )
242+ }
243+
244+ function SummarizeEntryButtonImpl ( {
245+ entry,
246+ } : {
247+ entry : { id : number ; title : string }
248+ } ) {
249+ const [ isPending , startTransition ] = useTransition ( )
250+ const { showBoundary } = useErrorBoundary ( )
251+
252+ const handleSummarize = ( ) => {
253+ startTransition ( async ( ) => {
254+ try {
255+ // Get the full entry content first
256+ const fullEntry = await callTool ( 'get_entry' , { id : entry . id } )
257+ console . log ( { fullEntry } )
258+ invariant ( fullEntry , 'Failed to retrieve entry content' )
259+ const entrySchema = z . object ( {
260+ title : z . string ( ) ,
261+ content : z . string ( ) ,
262+ mood : z . string ( ) . optional ( ) ,
263+ location : z . string ( ) . optional ( ) ,
264+ weather : z . string ( ) . optional ( ) ,
265+ tags : z
266+ . array ( z . object ( { id : z . number ( ) , name : z . string ( ) } ) )
267+ . optional ( ) ,
268+ } )
269+ const parsedEntry = entrySchema . parse ( fullEntry )
270+
271+ // Create a prompt requesting a summary
272+ const prompt = `Please provide a concise summary of this journal entry:
273+
274+ Title: ${ parsedEntry . title }
275+ Content: ${ parsedEntry . content }
276+ Mood: ${ parsedEntry . mood || 'Not specified' }
277+ Location: ${ parsedEntry . location || 'Not specified' }
278+ Weather: ${ parsedEntry . weather || 'Not specified' }
279+ Tags: ${ parsedEntry . tags ?. map ( ( t : { name : string } ) => t . name ) . join ( ', ' ) || 'None' }
280+
281+ Please provide a brief, insightful summary of this entry.`
282+
283+ await sendPrompt ( prompt )
284+ } catch ( err ) {
285+ showBoundary ( err )
286+ }
287+ } )
288+ }
289+
290+ return (
291+ < button
292+ onClick = { handleSummarize }
293+ disabled = { isPending }
294+ className = "text-primary text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-50"
295+ >
296+ { isPending ? 'Summarizing...' : 'Summarize' }
297+ </ button >
298+ )
299+ }
0 commit comments