3131import threading
3232from typing import Any
3333
34- from concordia .utils import html as html_lib
35-
3634_DEFAULT_MIN_CHUNK_LENGTH = 50
3735
3836
@@ -554,62 +552,7 @@ def from_raw_log(cls, raw_log: list[Mapping[str, Any]]) -> SimulationLog:
554552
555553 return log
556554
557- def to_raw_log (self ) -> list [dict [str , Any ]]:
558- """Export the log in the old raw_log format for backward compatibility.
559-
560- This produces a list of dicts matching the format used by the old
561- logging system, enabling gradual migration.
562-
563- Returns:
564- List of log entries in the existing format.
565- """
566- raw_log : list [dict [str , Any ]] = []
567-
568- for step in self .get_steps ():
569- entries = self .get_entries_by_step (step )
570- if not entries :
571- continue
572-
573- entry_dict : dict [str , Any ] = {'Step' : step }
574- summaries = []
575-
576- for entry in entries :
577- summaries .append (entry .summary )
578-
579- if entry .entry_type == 'entity' :
580- key = f'Entity [{ entry .entity_name } ]'
581- else :
582- key = entry .entity_name
583-
584- reconstructed = self .reconstruct_value (entry .deduplicated_data )
585- if reconstructed .get ('value' ):
586- entry_dict [key ] = reconstructed ['value' ]
587-
588- entry_dict ['Summary' ] = next ((s for s in summaries if s ), f'Step { step } ' )
589- raw_log .append (entry_dict )
590-
591- return raw_log
592-
593555 def to_html (self , title : str = 'Simulation Log' ) -> str :
594- """Render the log to HTML matching the existing format.
595-
596- Uses entity memories and game master memories if they were attached
597- when the log was created from Simulation.play().
598-
599- Args:
600- title: Title for the HTML page.
601-
602- Returns:
603- Complete HTML string with tabs for GM log, entities, and memories.
604- """
605- return render_simulation_log_to_html (
606- simulation_log = self ,
607- entity_memories = self ._entity_memories or None ,
608- game_master_memories = self ._game_master_memories or None ,
609- title = title ,
610- )
611-
612- def to_dynamic_html (self , title : str = 'Simulation Log' ) -> str :
613556 """Render the log to HTML with dynamic JavaScript-based deduplication.
614557
615558 This method generates HTML that stores content once in a JavaScript
@@ -631,68 +574,6 @@ def to_dynamic_html(self, title: str = 'Simulation Log') -> str:
631574 )
632575
633576
634- def render_simulation_log_to_html (
635- simulation_log : SimulationLog ,
636- entity_memories : dict [str , list [str ]] | None = None ,
637- game_master_memories : list [str ] | None = None ,
638- player_scores : dict [str , Any ] | None = None ,
639- title : str = 'Simulation Log' ,
640- ) -> str :
641- """Render a SimulationLog to HTML matching the existing format.
642-
643- This function produces HTML output that is visually identical to the
644- existing logging system, ensuring a seamless transition.
645-
646- Args:
647- simulation_log: The structured log to render.
648- entity_memories: Optional dict of entity_name -> list of memory strings.
649- game_master_memories: Optional list of GM memory strings.
650- player_scores: Optional dict of player scores to show in summary.
651- title: Title for the HTML page.
652-
653- Returns:
654- Complete HTML string with tabs for GM log, entities, and memories.
655- """
656- # Convert to raw_log format for HTML rendering
657- raw_log = simulation_log .to_raw_log ()
658-
659- # Render main log
660- results_log = html_lib .PythonObjectToHTMLConverter (raw_log ).convert ()
661-
662- # Render entity memories
663- player_logs = []
664- player_log_names = []
665-
666- if entity_memories :
667- for entity_name , memories in entity_memories .items ():
668- player_html = html_lib .PythonObjectToHTMLConverter (memories ).convert ()
669- player_logs .append (player_html )
670- player_log_names .append (entity_name )
671-
672- # Render game master memories
673- if game_master_memories :
674- gm_html = html_lib .PythonObjectToHTMLConverter (
675- game_master_memories
676- ).convert ()
677- player_logs .append (gm_html )
678- player_log_names .append ('Game Master Memories' )
679-
680- # Build summary
681- summary = ''
682- if player_scores :
683- summary = f'Player Scores: { player_scores } '
684-
685- # Combine all pages
686- tabbed_html = html_lib .combine_html_pages (
687- [results_log , * player_logs ],
688- ['Game Master log' , * player_log_names ],
689- summary = summary ,
690- title = title ,
691- )
692-
693- return html_lib .finalise_html (tabbed_html )
694-
695-
696577def render_dynamic_html (
697578 simulation_log : SimulationLog ,
698579 entity_memories : dict [str , list [str ]] | None = None ,
@@ -702,9 +583,9 @@ def render_dynamic_html(
702583) -> str :
703584 """Render the log to HTML with dynamic JavaScript-based content composition.
704585
705- Unlike render_simulation_log_to_html, this function stores all unique content
706- once in a JavaScript data block and uses JavaScript to dynamically compose
707- the views. This achieves true deduplication in the HTML output.
586+ This function stores all unique content once in a JavaScript data block
587+ and uses JavaScript to dynamically compose the views. This achieves true
588+ deduplication in the HTML output.
708589
709590 Args:
710591 simulation_log: The log to render.
@@ -716,6 +597,7 @@ def render_dynamic_html(
716597 Returns:
717598 Complete HTML string with embedded data and dynamic rendering.
718599 """
600+
719601 # Build the content store data for JavaScript
720602 content_store_data = simulation_log .content_store .to_dict ()
721603
@@ -772,6 +654,12 @@ def render_dynamic_html(
772654details { margin: 5px 0; }
773655details summary { cursor: pointer; font-weight: bold; padding: 5px; background: #f0f0f0; border-radius: 4px; }
774656details[open] summary { background: #e0e0e0; }
657+ /* Step-level details get special styling with a colored left border */
658+ details.step-details { margin: 10px 0; border-left: 3px solid #667eea; padding-left: 10px; }
659+ details.step-details > summary { background: #e8e8ff; font-size: 14px; }
660+ details.step-details[open] > summary { background: #d8d8ff; }
661+ /* Content inside steps is indented */
662+ details.step-details > details { margin-left: 15px; }
775663.summary { padding: 10px; background: #e8f4f8; border-radius: 4px; margin-bottom: 15px; }
776664h1 { color: #333; margin-bottom: 5px; }
777665.subtitle { color: #666; margin-bottom: 20px; }
@@ -850,6 +738,7 @@ def render_dynamic_html(
850738
851739// Recursively render any Python object as collapsible HTML
852740// Mirrors PythonObjectToHTMLConverter logic
741+ // Handles _ref references by looking up content in CONTENT_STORE
853742function renderObject(obj) {
854743 if (obj === null || obj === undefined) {
855744 return '';
@@ -872,6 +761,15 @@ def render_dynamic_html(
872761 }
873762
874763 if (typeof obj === 'object') {
764+ // Handle _ref references - lookup in CONTENT_STORE
765+ if (obj._ref && Object.keys(obj).length === 1) {
766+ const content = CONTENT_STORE[obj._ref];
767+ if (content !== undefined) {
768+ return escapeHtml(content);
769+ }
770+ return '[ref:' + obj._ref + ']';
771+ }
772+
875773 // Determine summary from special keys (like PythonObjectToHTMLConverter)
876774 let summary = '';
877775 if (obj.date) {
@@ -885,12 +783,12 @@ def render_dynamic_html(
885783 summary = escapeHtml(obj.Name);
886784 } else if (obj.Key) {
887785 summary = escapeHtml(obj.Key);
786+ } else if (obj.Value !== undefined || obj.value !== undefined) {
787+ // Entity data with a "Value" key - use "Details" as summary
788+ summary = 'Details';
888789 } else {
889- // Use first key as summary
890- const keys = Object.keys(obj);
891- if (keys.length > 0) {
892- summary = escapeHtml(keys[0]);
893- }
790+ // For all other objects, use "Details" as a generic summary
791+ summary = 'Details';
894792 }
895793
896794 let html = '<details>';
@@ -946,50 +844,43 @@ def render_dynamic_html(
946844
947845 steps.forEach(step => {
948846 const entries = stepMap[step];
949- html += '<details open>';
950- html += '<summary><b>Step ' + step + '</b>';
951- // Add summary from first entry if available
847+
848+ // Build step summary from entries
849+ let stepSummary = 'Step ' + step;
952850 if (entries.length > 0 && entries[0].summary) {
953- html += ' --- ' + escapeHtml( entries[0].summary) ;
851+ stepSummary += ' --- ' + entries[0].summary;
954852 }
955- html += '</summary>';
853+
854+ html += '<details class="step-details" open>';
855+ html += '<summary><b>' + escapeHtml(stepSummary) + '</b></summary>';
956856
957857 entries.forEach(entry => {
958- html += '<div class="entry">';
959-
960- // If there's raw_value in metadata, render it recursively
961- if (entry.metadata && entry.metadata.raw_value) {
858+ // Create a label for this entry (like "Entity [name]" or component name)
859+ let entryLabel = entry.entity_name;
860+ if (entry.entry_type === 'entity') {
861+ entryLabel = 'Entity [' + entry.entity_name + ']';
862+ }
863+
864+ // If entry has deduplicated_data, render it as collapsible content
865+ if (entry.deduplicated_data && Object.keys(entry.deduplicated_data).length > 0) {
962866 html += '<details>';
963- html += '<summary><b>' + escapeHtml(entry.metadata.raw_key || entry.entity_name) + '</b></summary>';
964- html += renderObjectChildren(entry.metadata.raw_value);
867+ html += '<summary>' + escapeHtml(entryLabel) + '</summary>';
868+ // Render all the data in deduplicated_data recursively
869+ for (const [key, value] of Object.entries(entry.deduplicated_data)) {
870+ html += '<b><ul>' + escapeHtml(key) + '</b>';
871+ html += '<li>' + renderObject(value) + '</li></ul>';
872+ }
965873 html += '</details>';
966874 } else {
967- // Simple entry without nested data
968- html += '<span class="entry-entity">' + escapeHtml(entry.entity_name) + '</span>';
875+ // Simple entry with no data
876+ html += '<div class="entry">';
877+ html += '<span class="entry-entity">' + escapeHtml(entryLabel) + '</span>';
969878 html += ' <span class="entry-component">(' + escapeHtml(entry.component_name) + ')</span>';
970879 if (entry.summary) {
971880 html += '<div class="entry-summary">' + escapeHtml(entry.summary) + '</div>';
972881 }
973-
974- // Show prompt/response if available
975- const prompt = getContent(entry.prompt_id);
976- const response = getContent(entry.response_id);
977-
978- if (prompt || response) {
979- html += '<details><summary>Details</summary>';
980- if (prompt) {
981- html += '<div class="content-label">Prompt</div>';
982- html += '<div class="content-block">' + escapeHtml(prompt) + '</div>';
983- }
984- if (response) {
985- html += '<div class="content-label">Response</div>';
986- html += '<div class="content-block">' + escapeHtml(response) + '</div>';
987- }
988- html += '</details>';
989- }
882+ html += '</div>';
990883 }
991-
992- html += '</div>';
993884 });
994885
995886 html += '</details>';
0 commit comments