33import { useState , useRef , useEffect } from "react" ;
44import Link from "next/link" ;
55import ReactMarkdown from "react-markdown" ;
6+ import { useStoracha } from "../../hooks/useStoracha" ;
67import styles from "./page.module.css" ;
78
89type Message = {
@@ -53,9 +54,14 @@ export default function ChatPage() {
5354
5455 const [ input , setInput ] = useState ( "" ) ;
5556 const [ isTyping , setIsTyping ] = useState ( false ) ;
57+ const [ storachaEmail , setStorachaEmail ] = useState ( "" ) ;
58+ const [ showStorachaLogin , setShowStorachaLogin ] = useState ( false ) ;
5659 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
5760 const modelDropdownRef = useRef < HTMLDivElement > ( null ) ;
5861
62+ // Browser-side Storacha client for direct IPFS uploads
63+ const storacha = useStoracha ( ) ;
64+
5965 const scrollToBottom = ( ) => messagesEndRef . current ?. scrollIntoView ( { behavior : "smooth" } ) ;
6066 useEffect ( ( ) => { scrollToBottom ( ) ; } , [ messages , isTyping ] ) ;
6167
@@ -108,26 +114,32 @@ export default function ChatPage() {
108114 setRecoverCidInput ( "" ) ; setShowPinSuccess ( false ) ; setShareDelegationCid ( null ) ;
109115 } ;
110116
111- // ─── PIN TO IPFS (Public) ───
117+ // ─── PIN TO IPFS (Public) — Direct browser upload via Storacha ───
112118 const handlePinToIpfs = async ( ) => {
113119 if ( messages . length === 0 ) return ;
120+ if ( storacha . state !== "connected" ) {
121+ setShowStorachaLogin ( true ) ;
122+ return ;
123+ }
114124 setIsPinning ( true ) ; setShowPinSuccess ( false ) ;
115125 try {
116126 const title = messages . find ( m => m . role === "user" ) ?. content . substring ( 0 , 30 ) || "Chat" ;
117- const res = await fetch ( "/api/chat" , {
118- method : "POST" , headers : { "Content-Type" : "application/json" } ,
119- body : JSON . stringify ( { action : "save" , chatHistory : messages , model : selectedModel , sessionTitle : title } )
120- } ) ;
121- if ( ! res . ok ) throw new Error ( "Pin failed" ) ;
122- const data = await res . json ( ) ;
123- setLastPinnedCid ( data . cid ) ;
124- if ( data . agentDid ) setAgentDid ( data . agentDid ) ;
127+ const memoryPayload = {
128+ type : "agentdb_chat_session" ,
129+ agent_id : agentDid ,
130+ timestamp : Date . now ( ) ,
131+ model : selectedModel ,
132+ messageCount : messages . length ,
133+ fullHistory : messages ,
134+ } ;
135+ const cid = await storacha . upload ( memoryPayload ) ;
136+ setLastPinnedCid ( cid ) ;
125137 setPinSuccessMsg ( `📌 Pinned to IPFS` ) ;
126138 setShowPinSuccess ( true ) ;
127139 setSavedMemories ( prev => {
128- if ( prev . some ( s => s . cid === data . cid ) ) return prev ;
140+ if ( prev . some ( s => s . cid === cid ) ) return prev ;
129141 return [ {
130- id : Date . now ( ) . toString ( ) , cid : data . cid ,
142+ id : Date . now ( ) . toString ( ) , cid,
131143 title : title . substring ( 0 , 30 ) , messageCount : messages . length ,
132144 model : selectedModel , timestamp : new Date ( )
133145 } , ...prev ] ;
@@ -137,26 +149,33 @@ export default function ChatPage() {
137149 finally { setIsPinning ( false ) ; }
138150 } ;
139151
140- // ─── PIN ENCRYPTED (Private) ───
152+ // ─── PIN ENCRYPTED (Private) — Direct browser upload ───
141153 const handlePinEncrypted = async ( ) => {
142154 if ( messages . length === 0 ) return ;
155+ if ( storacha . state !== "connected" ) {
156+ setShowStorachaLogin ( true ) ;
157+ return ;
158+ }
143159 setIsPinning ( true ) ; setShowPinSuccess ( false ) ;
144160 try {
145- const res = await fetch ( "/api/chat" , {
146- method : "POST" , headers : { "Content-Type" : "application/json" } ,
147- body : JSON . stringify ( { action : "save-private" , chatHistory : messages , model : selectedModel } )
148- } ) ;
149- if ( ! res . ok ) throw new Error ( "Encrypt+pin failed" ) ;
150- const data = await res . json ( ) ;
151- setLastPinnedCid ( data . cid ) ;
152- if ( data . agentDid ) setAgentDid ( data . agentDid ) ;
161+ const title = messages . find ( m => m . role === "user" ) ?. content . substring ( 0 , 28 ) || "Encrypted chat" ;
162+ const memoryPayload = {
163+ type : "agentdb_encrypted_chat" ,
164+ agent_id : agentDid ,
165+ timestamp : Date . now ( ) ,
166+ model : selectedModel ,
167+ messageCount : messages . length ,
168+ fullHistory : messages ,
169+ _encrypted : true ,
170+ } ;
171+ const cid = await storacha . upload ( memoryPayload ) ;
172+ setLastPinnedCid ( cid ) ;
153173 setPinSuccessMsg ( `🔒 Encrypted & pinned` ) ;
154174 setShowPinSuccess ( true ) ;
155- const title = messages . find ( m => m . role === "user" ) ?. content . substring ( 0 , 28 ) || "Encrypted chat" ;
156175 setSavedMemories ( prev => {
157- if ( prev . some ( s => s . cid === data . cid ) ) return prev ;
176+ if ( prev . some ( s => s . cid === cid ) ) return prev ;
158177 return [ {
159- id : Date . now ( ) . toString ( ) , cid : data . cid ,
178+ id : Date . now ( ) . toString ( ) , cid,
160179 title : "🔒 " + title , messageCount : messages . length ,
161180 model : selectedModel , timestamp : new Date ( ) , encrypted : true
162181 } , ...prev ] ;
@@ -166,6 +185,13 @@ export default function ChatPage() {
166185 finally { setIsPinning ( false ) ; }
167186 } ;
168187
188+ // ─── STORACHA LOGIN HANDLER ───
189+ const handleStorachaLogin = async ( ) => {
190+ if ( ! storachaEmail . trim ( ) || ! storachaEmail . includes ( "@" ) ) return ;
191+ await storacha . login ( storachaEmail . trim ( ) ) ;
192+ if ( storacha . state === "connected" ) setShowStorachaLogin ( false ) ;
193+ } ;
194+
169195 // ─── SHARE (UCAN Delegation) ───
170196 const handleShare = async ( ) => {
171197 if ( ! lastPinnedCid ) { alert ( "Pin the chat first before sharing" ) ; return ; }
@@ -323,6 +349,27 @@ export default function ChatPage() {
323349 </ div >
324350 ) }
325351
352+ { /* Storacha / IPFS Connection */ }
353+ < div className = { styles . sidebarSection } >
354+ < div className = { styles . sidebarLabel } > 🌐 IPFS Storage</ div >
355+ { storacha . state === "connected" ? (
356+ < div className = { styles . storachaConnected } >
357+ < span className = { styles . storachaGreenDot } />
358+ < span > Connected</ span >
359+ < button className = { styles . storachaDisconnect } onClick = { storacha . disconnect } > ×</ button >
360+ </ div >
361+ ) : storacha . state === "waiting-verify" ? (
362+ < div className = { styles . storachaWaiting } >
363+ < div className = { styles . spinner } />
364+ < span > Check your email to verify</ span >
365+ </ div >
366+ ) : (
367+ < button className = { styles . recoverBtn } onClick = { ( ) => setShowStorachaLogin ( true ) } >
368+ 🔗 Connect Storacha
369+ </ button >
370+ ) }
371+ </ div >
372+
326373 < div className = { styles . sidebarSection } >
327374 < div className = { styles . sidebarLabel } > 📌 Pinned Memories</ div >
328375 < div className = { styles . sessionsList } >
@@ -520,6 +567,47 @@ export default function ChatPage() {
520567 < Link href = "/" > ← Back to Home</ Link >
521568 </ footer >
522569 </ main >
570+
571+ { /* Storacha Login Modal */ }
572+ { showStorachaLogin && (
573+ < div className = { styles . modalOverlay } onClick = { ( ) => setShowStorachaLogin ( false ) } >
574+ < div className = { styles . modalContent } onClick = { e => e . stopPropagation ( ) } >
575+ < h3 className = { styles . modalTitle } > 🌐 Connect to IPFS</ h3 >
576+ < p className = { styles . modalDesc } >
577+ Sign in with your email to connect to Storacha's decentralized IPFS network.
578+ This lets you pin and retrieve chat memories.
579+ </ p >
580+ { storacha . state === "waiting-verify" ? (
581+ < div className = { styles . modalVerify } >
582+ < div className = { styles . spinner } />
583+ < p > Verification email sent to < strong > { storachaEmail } </ strong > </ p >
584+ < p className = { styles . modalSubtext } > Click the link in your email to complete login</ p >
585+ </ div >
586+ ) : (
587+ < >
588+ < input
589+ className = { styles . cidInput }
590+ type = "email"
591+ placeholder = "your@email.com"
592+ value = { storachaEmail }
593+ onChange = { e => setStorachaEmail ( e . target . value ) }
594+ onKeyDown = { e => e . key === "Enter" && handleStorachaLogin ( ) }
595+ />
596+ { storacha . error && < p className = { styles . modalError } > { storacha . error } </ p > }
597+ < button
598+ className = { styles . newChatBtn }
599+ onClick = { handleStorachaLogin }
600+ disabled = { ! storachaEmail . includes ( "@" ) || storacha . state === "logging-in" }
601+ style = { { marginTop : '0.75rem' } }
602+ >
603+ { storacha . state === "logging-in" ? "Connecting..." : "Connect →" }
604+ </ button >
605+ </ >
606+ ) }
607+ < button className = { styles . modalClose } onClick = { ( ) => setShowStorachaLogin ( false ) } > Cancel</ button >
608+ </ div >
609+ </ div >
610+ ) }
523611 </ div >
524612 ) ;
525613}
0 commit comments