@@ -46,7 +46,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
4646import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
4747import { useFolders } from '@/hooks/queries/folders'
4848import { useWorkflows } from '@/hooks/queries/workflows'
49- import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
49+ import { useWorkspaceFileContent , useWorkspaceFiles } from '@/hooks/queries/workspace-files'
5050import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
5151import { useExecutionStore } from '@/stores/execution/store'
5252import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -84,11 +84,41 @@ export const ResourceContent = memo(function ResourceContent({
8484 genericResourceData,
8585} : ResourceContentProps ) {
8686 const streamFileName = streamingFile ?. fileName || 'file.md'
87+
88+ const isPatchStream = useMemo ( ( ) => {
89+ if ( ! streamingFile ) return false
90+ return / " o p e r a t i o n " \s * : \s * " p a t c h " / . test ( streamingFile . content )
91+ } , [ streamingFile ] )
92+
93+ const { data : allFiles = [ ] } = useWorkspaceFiles ( workspaceId )
94+ const activeFileRecord = useMemo ( ( ) => {
95+ if ( ! isPatchStream || resource . type !== 'file' ) return undefined
96+ return allFiles . find ( ( f ) => f . id === resource . id )
97+ } , [ isPatchStream , resource , allFiles ] )
98+
99+ const isSourceMime =
100+ activeFileRecord ?. type === 'text/x-pptxgenjs' ||
101+ activeFileRecord ?. type === 'text/x-docxjs' ||
102+ activeFileRecord ?. type === 'text/x-pdflibjs'
103+
104+ const { data : fetchedFileContent } = useWorkspaceFileContent (
105+ workspaceId ,
106+ activeFileRecord ?. id ?? '' ,
107+ activeFileRecord ?. key ?? '' ,
108+ isSourceMime
109+ )
110+
87111 const streamingExtractedContent = useMemo ( ( ) => {
88112 if ( ! streamingFile ) return undefined
89- const extracted = extractFileContent ( streamingFile . content )
113+ const raw = streamingFile . content
114+
115+ if ( isPatchStream && fetchedFileContent ) {
116+ return extractPatchPreview ( raw , fetchedFileContent )
117+ }
118+
119+ const extracted = extractFileContent ( raw )
90120 return extracted . length > 0 ? extracted : undefined
91- } , [ streamingFile ] )
121+ } , [ streamingFile , isPatchStream , fetchedFileContent ] )
92122 const syntheticFile = useMemo ( ( ) => {
93123 const ext = getFileExtension ( streamFileName )
94124 const SOURCE_MIME_MAP : Record < string , string > = {
@@ -564,3 +594,100 @@ function extractFileContent(raw: string): string {
564594 . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) => String . fromCharCode ( Number . parseInt ( hex , 16 ) ) )
565595 . replace ( / \\ \\ / g, '\\' )
566596}
597+
598+ function extractJsonString ( raw : string , key : string ) : string | undefined {
599+ const pattern = new RegExp ( `"${ key } "\\s*:\\s*"` )
600+ const m = pattern . exec ( raw )
601+ if ( ! m ) return undefined
602+ const start = m . index + m [ 0 ] . length
603+ let end = - 1
604+ for ( let i = start ; i < raw . length ; i ++ ) {
605+ if ( raw [ i ] === '\\' ) {
606+ i ++
607+ continue
608+ }
609+ if ( raw [ i ] === '"' ) {
610+ end = i
611+ break
612+ }
613+ }
614+ if ( end === - 1 ) return undefined
615+ return raw
616+ . slice ( start , end )
617+ . replace ( / \\ n / g, '\n' )
618+ . replace ( / \\ t / g, '\t' )
619+ . replace ( / \\ r / g, '\r' )
620+ . replace ( / \\ " / g, '"' )
621+ . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) => String . fromCharCode ( Number . parseInt ( hex , 16 ) ) )
622+ . replace ( / \\ \\ / g, '\\' )
623+ }
624+
625+ function findAnchorIndex ( lines : string [ ] , anchor : string , occurrence = 1 , afterIndex = - 1 ) : number {
626+ const trimmed = anchor . trim ( )
627+ let count = 0
628+ for ( let i = afterIndex + 1 ; i < lines . length ; i ++ ) {
629+ if ( lines [ i ] . trim ( ) === trimmed ) {
630+ count ++
631+ if ( count === occurrence ) return i
632+ }
633+ }
634+ return - 1
635+ }
636+
637+ function extractPatchPreview ( raw : string , existingContent : string ) : string | undefined {
638+ const mode = extractJsonString ( raw , 'mode' )
639+ if ( ! mode ) return undefined
640+
641+ const lines = existingContent . split ( '\n' )
642+ const occurrenceMatch = raw . match ( / " o c c u r r e n c e " \s * : \s * ( \d + ) / )
643+ const occurrence = occurrenceMatch ? Number . parseInt ( occurrenceMatch [ 1 ] , 10 ) : 1
644+
645+ if ( mode === 'replace_between' ) {
646+ const beforeAnchor = extractJsonString ( raw , 'before_anchor' )
647+ const afterAnchor = extractJsonString ( raw , 'after_anchor' )
648+ if ( ! beforeAnchor || ! afterAnchor ) return undefined
649+
650+ const beforeIdx = findAnchorIndex ( lines , beforeAnchor , occurrence )
651+ const afterIdx = findAnchorIndex ( lines , afterAnchor , occurrence , beforeIdx )
652+ if ( beforeIdx === - 1 || afterIdx === - 1 || afterIdx <= beforeIdx ) return undefined
653+
654+ const newContent = extractFileContent ( raw )
655+ const spliced = [
656+ ...lines . slice ( 0 , beforeIdx + 1 ) ,
657+ ...( newContent . length > 0 ? newContent . split ( '\n' ) : [ ] ) ,
658+ ...lines . slice ( afterIdx ) ,
659+ ]
660+ return spliced . join ( '\n' )
661+ }
662+
663+ if ( mode === 'insert_after' ) {
664+ const anchor = extractJsonString ( raw , 'anchor' )
665+ if ( ! anchor ) return undefined
666+
667+ const anchorIdx = findAnchorIndex ( lines , anchor , occurrence )
668+ if ( anchorIdx === - 1 ) return undefined
669+
670+ const newContent = extractFileContent ( raw )
671+ const spliced = [
672+ ...lines . slice ( 0 , anchorIdx + 1 ) ,
673+ ...( newContent . length > 0 ? newContent . split ( '\n' ) : [ ] ) ,
674+ ...lines . slice ( anchorIdx + 1 ) ,
675+ ]
676+ return spliced . join ( '\n' )
677+ }
678+
679+ if ( mode === 'delete_between' ) {
680+ const startAnchor = extractJsonString ( raw , 'start_anchor' )
681+ const endAnchor = extractJsonString ( raw , 'end_anchor' )
682+ if ( ! startAnchor || ! endAnchor ) return undefined
683+
684+ const startIdx = findAnchorIndex ( lines , startAnchor , occurrence )
685+ const endIdx = findAnchorIndex ( lines , endAnchor , occurrence , startIdx )
686+ if ( startIdx === - 1 || endIdx === - 1 || endIdx <= startIdx ) return undefined
687+
688+ const spliced = [ ...lines . slice ( 0 , startIdx ) , ...lines . slice ( endIdx ) ]
689+ return spliced . join ( '\n' )
690+ }
691+
692+ return undefined
693+ }
0 commit comments