@@ -7,6 +7,7 @@ import { SessionTabs } from "./SessionTabs";
77import { useIsMobile } from "../hooks/useIsMobile" ;
88import { dlog } from "../debug-log" ;
99import { randomUUID } from "../utils/uuid" ;
10+ import { E2eService } from "../e2e" ;
1011
1112type ChatWindowProps = {
1213 sendMessage : ( msg : WSMessage ) => void ;
@@ -282,11 +283,26 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
282283 }
283284 } , [ pendingImage ] ) ;
284285
285- const uploadImage = useCallback ( async ( file : File ) : Promise < string | null > => {
286- const formData = new FormData ( ) ;
287- formData . append ( "file" , file ) ;
286+ /**
287+ * Upload a file — if E2E is enabled, encrypts the binary before uploading.
288+ * Returns { url, mediaContextId? } or null on failure.
289+ */
290+ const uploadFile = useCallback ( async ( file : File , mediaContextId ?: string ) : Promise < { url : string } | null > => {
288291 const token = localStorage . getItem ( "botschat_token" ) ;
289292 try {
293+ let uploadBlob : Blob = file ;
294+
295+ // E2E: encrypt file content before uploading
296+ if ( E2eService . hasKey ( ) && mediaContextId ) {
297+ const arrayBuf = await file . arrayBuffer ( ) ;
298+ const plainBytes = new Uint8Array ( arrayBuf ) ;
299+ const { encrypted } = await E2eService . encryptMedia ( plainBytes , mediaContextId ) ;
300+ uploadBlob = new Blob ( [ encrypted . buffer . slice ( 0 ) as ArrayBuffer ] , { type : file . type } ) ;
301+ dlog . info ( "E2E" , `Encrypted media (${ plainBytes . length } bytes, ctx=${ mediaContextId . slice ( 0 , 8 ) } …)` ) ;
302+ }
303+
304+ const formData = new FormData ( ) ;
305+ formData . append ( "file" , uploadBlob , file . name ) ;
290306 const res = await fetch ( "/api/upload" , {
291307 method : "POST" ,
292308 headers : token ? { Authorization : `Bearer ${ token } ` } : { } ,
@@ -297,13 +313,12 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
297313 throw new Error ( ( err as { error ?: string } ) . error ?? `HTTP ${ res . status } ` ) ;
298314 }
299315 const data = await res . json ( ) as { url : string } ;
300- // Return absolute URL so OpenClaw on mini.local can fetch the image
301316 const absoluteUrl = data . url . startsWith ( "/" )
302317 ? `${ window . location . origin } ${ data . url } `
303318 : data . url ;
304- return absoluteUrl ;
319+ return { url : absoluteUrl } ;
305320 } catch ( err ) {
306- dlog . error ( "Upload" , `Image upload failed: ${ err } ` ) ;
321+ dlog . error ( "Upload" , `File upload failed: ${ err } ` ) ;
307322 return null ;
308323 }
309324 } , [ ] ) ;
@@ -366,6 +381,11 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
366381 const handleSend = async ( ) => {
367382 if ( ( ! input . trim ( ) && ! pendingImage ) || ! sessionKey ) return ;
368383
384+ // Warn if OpenClaw is offline (but don't block — connection may recover)
385+ if ( ! state . openclawConnected ) {
386+ dlog . warn ( "Chat" , "Sending while OpenClaw appears offline — message will be delivered when reconnected" ) ;
387+ }
388+
369389 // Prepend quoted message as Markdown blockquote
370390 const rawTrimmed = input . trim ( ) ;
371391 const trimmed = quotedMessage
@@ -381,19 +401,23 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
381401 setSkillVersion ( ( v ) => v + 1 ) ;
382402 }
383403
384- // Upload image if present
404+ // Generate message ID upfront so we can use it as E2E context for both text and media
405+ const msgId = randomUUID ( ) ;
406+
407+ // Upload file if present
385408 let mediaUrl : string | undefined ;
386409 if ( pendingImage ) {
387410 setImageUploading ( true ) ;
388- const url = await uploadImage ( pendingImage . file ) ;
411+ // Use "{msgId}:media" as E2E context for the binary — distinct from text context
412+ const result = await uploadFile ( pendingImage . file , `${ msgId } :media` ) ;
389413 setImageUploading ( false ) ;
390- if ( ! url ) return ; // Upload failed
391- mediaUrl = url ;
414+ if ( ! result ) return ; // Upload failed
415+ mediaUrl = result . url ;
392416 clearPendingImage ( ) ;
393417 }
394418
395419 const msg : ChatMessage = {
396- id : randomUUID ( ) ,
420+ id : msgId ,
397421 sender : "user" ,
398422 text : trimmed ,
399423 timestamp : Date . now ( ) ,
@@ -810,14 +834,14 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
810834 />
811835 < button
812836 onClick = { ( ) => fileInputRef . current ?. click ( ) }
813- className = "p-1.5 rounded hover:bg-[--bg-hover] transition-colors"
814- style = { { color : "var(--text-muted )" } }
815- title = "Upload image "
816- aria-label = "Upload image "
837+ className = "p-1.5 rounded hover:bg-[--bg-hover] transition-colors flex items-center gap-1 "
838+ style = { { color : "var(--text-secondary )" } }
839+ title = "Attach file "
840+ aria-label = "Attach file "
817841 disabled = { ! state . openclawConnected }
818842 >
819- < svg className = "w-4 h-4 " fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 1.5 } >
820- < path strokeLinecap = "round" strokeLinejoin = "round" d = "M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1 .5 1.5 0 002.25 6v13.5A1.5 1.5 0 003.75 21zm14.25-15.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0z " />
843+ < svg className = "w-5 h-5 " fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 1.5 } >
844+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1 .5 1.5 0 002.112 2.13 " />
821845 </ svg >
822846 </ button >
823847 </ div >
@@ -842,7 +866,7 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
842866 ) : (
843867 < button
844868 onClick = { handleSend }
845- disabled = { ( ! input . trim ( ) && ! pendingImage ) || ! state . openclawConnected }
869+ disabled = { ! input . trim ( ) && ! pendingImage }
846870 className = "px-3 py-1.5 rounded-sm text-caption font-bold text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
847871 style = { { background : "var(--bg-active)" } }
848872 >
@@ -954,6 +978,8 @@ function MessageRow({
954978 < MessageContent
955979 text = { msg . text }
956980 mediaUrl = { msg . mediaUrl }
981+ messageId = { msg . id }
982+ encrypted = { ! ! msg . mediaUrl && E2eService . hasKey ( ) }
957983 a2ui = { msg . a2ui }
958984 isStreaming = { msg . isStreaming }
959985 onAction = { onAction }
0 commit comments