@@ -38,7 +38,7 @@ import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components
3838import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
3939import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
4040import { useWorkflows } from '@/hooks/queries/workflows'
41- import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
41+ import { useWorkspaceFileContent , useWorkspaceFiles } from '@/hooks/queries/workspace-files'
4242import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
4343import { useExecutionStore } from '@/stores/execution/store'
4444import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -76,11 +76,41 @@ export const ResourceContent = memo(function ResourceContent({
7676 genericResourceData,
7777} : ResourceContentProps ) {
7878 const streamFileName = streamingFile ?. fileName || 'file.md'
79+
80+ const isPatchStream = useMemo ( ( ) => {
81+ if ( ! streamingFile ) return false
82+ return / " o p e r a t i o n " \s * : \s * " p a t c h " / . test ( streamingFile . content )
83+ } , [ streamingFile ] )
84+
85+ const { data : allFiles = [ ] } = useWorkspaceFiles ( workspaceId )
86+ const activeFileRecord = useMemo ( ( ) => {
87+ if ( ! isPatchStream || resource . type !== 'file' ) return undefined
88+ return allFiles . find ( ( f ) => f . id === resource . id )
89+ } , [ isPatchStream , resource , allFiles ] )
90+
91+ const isSourceMime =
92+ activeFileRecord ?. type === 'text/x-pptxgenjs' ||
93+ activeFileRecord ?. type === 'text/x-docxjs' ||
94+ activeFileRecord ?. type === 'text/x-pdflibjs'
95+
96+ const { data : fetchedFileContent } = useWorkspaceFileContent (
97+ workspaceId ,
98+ activeFileRecord ?. id ?? '' ,
99+ activeFileRecord ?. key ?? '' ,
100+ isSourceMime
101+ )
102+
79103 const streamingExtractedContent = useMemo ( ( ) => {
80104 if ( ! streamingFile ) return undefined
81- const extracted = extractFileContent ( streamingFile . content )
105+ const raw = streamingFile . content
106+
107+ if ( isPatchStream && fetchedFileContent ) {
108+ return extractPatchPreview ( raw , fetchedFileContent )
109+ }
110+
111+ const extracted = extractFileContent ( raw )
82112 return extracted . length > 0 ? extracted : undefined
83- } , [ streamingFile ] )
113+ } , [ streamingFile , isPatchStream , fetchedFileContent ] )
84114 const syntheticFile = useMemo ( ( ) => {
85115 const ext = getFileExtension ( streamFileName )
86116 const SOURCE_MIME_MAP : Record < string , string > = {
@@ -486,3 +516,100 @@ function extractFileContent(raw: string): string {
486516 . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) => String . fromCharCode ( Number . parseInt ( hex , 16 ) ) )
487517 . replace ( / \\ \\ / g, '\\' )
488518}
519+
520+ function extractJsonString ( raw : string , key : string ) : string | undefined {
521+ const pattern = new RegExp ( `"${ key } "\\s*:\\s*"` )
522+ const m = pattern . exec ( raw )
523+ if ( ! m ) return undefined
524+ const start = m . index + m [ 0 ] . length
525+ let end = - 1
526+ for ( let i = start ; i < raw . length ; i ++ ) {
527+ if ( raw [ i ] === '\\' ) {
528+ i ++
529+ continue
530+ }
531+ if ( raw [ i ] === '"' ) {
532+ end = i
533+ break
534+ }
535+ }
536+ if ( end === - 1 ) return undefined
537+ return raw
538+ . slice ( start , end )
539+ . replace ( / \\ n / g, '\n' )
540+ . replace ( / \\ t / g, '\t' )
541+ . replace ( / \\ r / g, '\r' )
542+ . replace ( / \\ " / g, '"' )
543+ . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) => String . fromCharCode ( Number . parseInt ( hex , 16 ) ) )
544+ . replace ( / \\ \\ / g, '\\' )
545+ }
546+
547+ function findAnchorIndex ( lines : string [ ] , anchor : string , occurrence = 1 , afterIndex = - 1 ) : number {
548+ const trimmed = anchor . trim ( )
549+ let count = 0
550+ for ( let i = afterIndex + 1 ; i < lines . length ; i ++ ) {
551+ if ( lines [ i ] . trim ( ) === trimmed ) {
552+ count ++
553+ if ( count === occurrence ) return i
554+ }
555+ }
556+ return - 1
557+ }
558+
559+ function extractPatchPreview ( raw : string , existingContent : string ) : string | undefined {
560+ const mode = extractJsonString ( raw , 'mode' )
561+ if ( ! mode ) return undefined
562+
563+ const lines = existingContent . split ( '\n' )
564+ const occurrenceMatch = raw . match ( / " o c c u r r e n c e " \s * : \s * ( \d + ) / )
565+ const occurrence = occurrenceMatch ? Number . parseInt ( occurrenceMatch [ 1 ] , 10 ) : 1
566+
567+ if ( mode === 'replace_between' ) {
568+ const beforeAnchor = extractJsonString ( raw , 'before_anchor' )
569+ const afterAnchor = extractJsonString ( raw , 'after_anchor' )
570+ if ( ! beforeAnchor || ! afterAnchor ) return undefined
571+
572+ const beforeIdx = findAnchorIndex ( lines , beforeAnchor , occurrence )
573+ const afterIdx = findAnchorIndex ( lines , afterAnchor , occurrence , beforeIdx )
574+ if ( beforeIdx === - 1 || afterIdx === - 1 || afterIdx <= beforeIdx ) return undefined
575+
576+ const newContent = extractFileContent ( raw )
577+ const spliced = [
578+ ...lines . slice ( 0 , beforeIdx + 1 ) ,
579+ ...( newContent . length > 0 ? newContent . split ( '\n' ) : [ ] ) ,
580+ ...lines . slice ( afterIdx ) ,
581+ ]
582+ return spliced . join ( '\n' )
583+ }
584+
585+ if ( mode === 'insert_after' ) {
586+ const anchor = extractJsonString ( raw , 'anchor' )
587+ if ( ! anchor ) return undefined
588+
589+ const anchorIdx = findAnchorIndex ( lines , anchor , occurrence )
590+ if ( anchorIdx === - 1 ) return undefined
591+
592+ const newContent = extractFileContent ( raw )
593+ const spliced = [
594+ ...lines . slice ( 0 , anchorIdx + 1 ) ,
595+ ...( newContent . length > 0 ? newContent . split ( '\n' ) : [ ] ) ,
596+ ...lines . slice ( anchorIdx + 1 ) ,
597+ ]
598+ return spliced . join ( '\n' )
599+ }
600+
601+ if ( mode === 'delete_between' ) {
602+ const startAnchor = extractJsonString ( raw , 'start_anchor' )
603+ const endAnchor = extractJsonString ( raw , 'end_anchor' )
604+ if ( ! startAnchor || ! endAnchor ) return undefined
605+
606+ const startIdx = findAnchorIndex ( lines , startAnchor , occurrence )
607+ const endIdx = findAnchorIndex ( lines , endAnchor , occurrence , startIdx )
608+ if ( startIdx === - 1 || endIdx === - 1 || endIdx <= startIdx ) return undefined
609+
610+ const spliced = [ ...lines . slice ( 0 , startIdx ) , ...lines . slice ( endIdx ) ]
611+ return spliced . join ( '\n' )
612+ }
613+
614+ return undefined
615+ }
0 commit comments