Skip to content

Commit 6f4cfcc

Browse files
committed
feat: improve rendering of generated files
Instead of rendering files one after each other, we should come up with a better editor-like representation. This is helpful as currently people need to scroll forever to e.g. get up to the error message, or when comparing between attempts.
1 parent d3baa3e commit 6f4cfcc

File tree

7 files changed

+406
-14
lines changed

7 files changed

+406
-14
lines changed

report-app/src/app/pages/report-viewer/report-viewer.html

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -535,19 +535,7 @@ <h4>Failed Tests</h4>
535535
}
536536

537537
<h4>Generated Code</h4>
538-
539-
@for (file of attempt.outputFiles; track file) {
540-
<strong>{{ file.filePath }}</strong>
541-
<div class="util-buttons button-group">
542-
<button class="text-button" (click)="copy(file.code)">
543-
Copy source code
544-
</button>
545-
<button class="text-button" (click)="format(file)">
546-
Format source code
547-
</button>
548-
</div>
549-
<app-code-viewer [code]="formatted().get(file) ?? file.code" />
550-
}
538+
<app-file-code-viewer [files]="attempt.outputFiles" />
551539
}
552540
</expansion-panel>
553541
}

report-app/src/app/pages/report-viewer/report-viewer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {ProviderLabel} from '../../shared/provider-label';
4545
import {AiAssistant} from '../../shared/ai-assistant/ai-assistant';
4646
import {LighthouseCategory} from './lighthouse-category';
4747
import {MultiSelect} from '../../shared/multi-select/multi-select';
48+
import {FileCodeViewer} from '../../shared/file-code-viewer/file-code-viewer';
4849

4950
const localReportRegex = /-l\d+$/;
5051

@@ -63,6 +64,7 @@ const localReportRegex = /-l\d+$/;
6364
AiAssistant,
6465
LighthouseCategory,
6566
MultiSelect,
67+
FileCodeViewer,
6668
],
6769
templateUrl: './report-viewer.html',
6870
styleUrls: ['./report-viewer.scss'],

report-app/src/app/shared/code-viewer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {AppColorMode} from '../services/app-color-mode';
3232
content: counter(step);
3333
counter-increment: step;
3434
width: 1rem;
35-
margin-right: 1.5rem;
35+
margin-right: 0.5rem;
3636
display: inline-block;
3737
text-align: right;
3838
color: rgba(115, 138, 148, 0.4);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<div class="editor-container">
2+
<div class="sidebar">
3+
<div class="sidebar-header">
4+
<span>Explorer</span>
5+
</div>
6+
<ul class="file-list">
7+
@for (node of flatTree(); track node.path) {
8+
@let isExpanded = node.isDirectory && node.isExpanded();
9+
@let iconOptions = { isDirectory: node.isDirectory, isExpanded: isExpanded };
10+
11+
@if (isNodeVisible(node)) {
12+
<li
13+
class="file-item"
14+
[class.selected]="!node.isDirectory && node === selectedFile()"
15+
[class.expanded]="isExpanded"
16+
(click)="toggleNode(node)"
17+
[style.padding-left]="'calc(' + (node.depth * 0.5) + 'rem + var(--padding-base))'"
18+
[title]="node.path"
19+
>
20+
@if (node.isDirectory) {
21+
<span class="material-symbols-outlined collapse-indicator">chevron_right</span>
22+
} @else {
23+
<span class="material-symbols-outlined collapse-indicator" style="visibility: hidden;"
24+
>chevron_right</span
25+
>
26+
}
27+
<span class="material-symbols-outlined">{{ getFileIcon(node.path, iconOptions) }}</span>
28+
<span class="file-name">{{ node.name }}</span>
29+
</li>
30+
}
31+
}
32+
</ul>
33+
</div>
34+
<div class="content">
35+
@if (selectedFile(); as file) {
36+
@let iconOptions = { isDirectory: false };
37+
<div class="tabs">
38+
<div class="tab" [title]="file.path">
39+
<span class="material-symbols-outlined">{{ getFileIcon(file.path, iconOptions) }}</span>
40+
<span>{{ file.name }}</span>
41+
</div>
42+
<button class="copy-button" (click)="copyCode()">
43+
<span class="material-symbols-outlined">content_copy</span>
44+
<span>Copy</span>
45+
</button>
46+
</div>
47+
<div class="code-container">
48+
<app-code-viewer [code]="file.file!.code" />
49+
</div>
50+
}
51+
</div>
52+
</div>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
:host {
2+
--bg-color: #1e1e1e;
3+
--sidebar-bg: #252526;
4+
--border-color: #333;
5+
--text-color: #cccccc;
6+
--accent-color: #37373d;
7+
--hover-bg: #2a2d2e;
8+
--padding-base: 1rem;
9+
10+
display: block;
11+
position: relative;
12+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13+
}
14+
15+
.editor-container {
16+
display: flex;
17+
height: 40rem;
18+
overflow: hidden;
19+
background-color: var(--bg-color);
20+
border-radius: 0.5rem;
21+
border: 1px solid var(--border-color);
22+
}
23+
24+
.sidebar {
25+
width: 15rem;
26+
max-width: 25rem;
27+
background-color: var(--sidebar-bg);
28+
display: flex;
29+
flex-direction: column;
30+
border-right: 1px solid var(--border-color);
31+
flex-shrink: 0;
32+
overflow: hidden;
33+
}
34+
35+
.sidebar-header {
36+
padding: calc(var(--padding-base) / 2) var(--padding-base);
37+
font-weight: 600;
38+
position: sticky;
39+
top: 0;
40+
background-color: var(--sidebar-bg);
41+
z-index: 1;
42+
text-transform: uppercase;
43+
font-size: 0.7rem;
44+
color: var(--text-color);
45+
}
46+
47+
.file-list {
48+
list-style: none;
49+
padding: 0;
50+
margin: 0;
51+
flex-grow: 1;
52+
overflow-y: auto;
53+
}
54+
55+
.file-item {
56+
display: flex;
57+
align-items: center;
58+
gap: 0.25rem;
59+
padding: 0.25rem var(--padding-base);
60+
cursor: pointer;
61+
transition: background-color 0.2s;
62+
color: var(--text-color);
63+
font-size: 0.85rem;
64+
white-space: nowrap;
65+
overflow: hidden;
66+
text-overflow: ellipsis;
67+
68+
&:hover {
69+
background-color: var(--hover-bg);
70+
}
71+
72+
&.selected {
73+
background-color: var(--accent-color);
74+
color: white;
75+
}
76+
}
77+
78+
.collapse-indicator {
79+
transition: transform 0.2s;
80+
}
81+
82+
.file-item.expanded .collapse-indicator {
83+
transform: rotate(90deg);
84+
}
85+
86+
.content {
87+
flex-grow: 1;
88+
overflow: hidden;
89+
display: flex;
90+
flex-direction: column;
91+
}
92+
93+
.tabs {
94+
display: flex;
95+
justify-content: space-between;
96+
align-items: center;
97+
background-color: var(--sidebar-bg);
98+
flex-shrink: 0;
99+
}
100+
101+
.tab {
102+
display: flex;
103+
align-items: center;
104+
gap: 0.5rem;
105+
padding: calc(var(--padding-base) / 2) var(--padding-base);
106+
background-color: var(--bg-color);
107+
color: var(--text-color);
108+
border-right: 1px solid var(--sidebar-bg);
109+
font-size: 0.85rem;
110+
white-space: nowrap;
111+
overflow: hidden;
112+
text-overflow: ellipsis;
113+
}
114+
115+
.copy-button {
116+
background-color: var(--border-color);
117+
color: var(--text-color);
118+
border: 1px solid #444;
119+
border-radius: 0.25rem;
120+
padding: 0.25rem 0.5rem;
121+
cursor: pointer;
122+
margin-right: 0.5rem;
123+
display: flex;
124+
align-items: center;
125+
gap: 0.25rem;
126+
127+
&:hover {
128+
background-color: #444;
129+
}
130+
}
131+
132+
.material-symbols-outlined {
133+
font-size: 1rem;
134+
}
135+
136+
.code-container {
137+
flex-grow: 1;
138+
overflow: auto;
139+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {Component, computed, inject, input, linkedSignal} from '@angular/core';
2+
import {LlmResponseFile} from '../../../../../runner/shared-interfaces';
3+
import {CodeViewer} from '../code-viewer';
4+
import {Clipboard} from '@angular/cdk/clipboard';
5+
import {FileTreeNode, TreeNode, buildFileTree} from './file-tree';
6+
7+
@Component({
8+
selector: 'app-file-code-viewer',
9+
templateUrl: './file-code-viewer.html',
10+
styleUrl: './file-code-viewer.scss',
11+
imports: [CodeViewer],
12+
})
13+
export class FileCodeViewer {
14+
private readonly clipboard = inject(Clipboard);
15+
readonly files = input.required<LlmResponseFile[]>();
16+
17+
private readonly fileTree = computed(() => buildFileTree(this.files()));
18+
19+
readonly flatTree = computed(() => {
20+
const tree = this.fileTree();
21+
const flatten = (nodes: TreeNode[]): TreeNode[] => {
22+
let flat: TreeNode[] = [];
23+
for (const node of nodes) {
24+
flat.push(node);
25+
if (node.isDirectory) {
26+
flat = flat.concat(flatten(node.children));
27+
}
28+
}
29+
return flat;
30+
};
31+
return flatten(tree);
32+
});
33+
34+
readonly selectedFile = linkedSignal<FileTreeNode | undefined>(() =>
35+
this.flatTree().find(f => !f.isDirectory),
36+
);
37+
38+
toggleNode(node: TreeNode): void {
39+
if (node.isDirectory) {
40+
node.isExpanded.update(e => !e);
41+
} else {
42+
this.selectedFile.set(node);
43+
}
44+
}
45+
46+
copyCode(): void {
47+
const fileNode = this.selectedFile();
48+
if (fileNode?.file) {
49+
if (!this.clipboard.copy(fileNode.file.code)) {
50+
alert('Failed to copy code to clipboard.');
51+
}
52+
}
53+
}
54+
55+
getFileIcon(filePath: string, options: {isDirectory: boolean; isExpanded?: boolean}): string {
56+
if (options.isDirectory) {
57+
return options.isExpanded ? 'folder_open' : 'folder';
58+
}
59+
const extension = filePath.split('.').pop();
60+
switch (extension) {
61+
case 'html':
62+
return 'html';
63+
case 'ts':
64+
return 'javascript';
65+
case 'css':
66+
return 'css';
67+
case 'scss':
68+
return 'css';
69+
default:
70+
return 'article';
71+
}
72+
}
73+
74+
isNodeVisible(node: TreeNode): boolean {
75+
let current = node.parent;
76+
while (current && current.path) {
77+
if (!current.isExpanded()) {
78+
return false;
79+
}
80+
current = current.parent;
81+
}
82+
return true;
83+
}
84+
}

0 commit comments

Comments
 (0)