Skip to content

Commit 89b6a34

Browse files
committed
feat(test-results-bar): split logs and autofix into two panes
Refactor test results bar to use separate left and right panes for logs and autofix tabs when screen width permits. This improves UI by allowing simultaneous display of logs and autofix information on wider screens. Remove toggle for full autofix logs and show only summary and explanation for successful autofix requests to simplify the interface. Adjust layout in pane component for better flex behavior and consistent scrolling experience. Add fade transition for pane changes and update tab availability logic based on user role, active step, and screen size for better UX.
1 parent 2a55eb0 commit 89b6a34

File tree

10 files changed

+508
-137
lines changed

10 files changed

+508
-137
lines changed

app/components/course-page/test-results-bar/autofix-section/autofix-result.hbs

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,6 @@
1818
<span class="font-mono text-xs text-white">Hints generated</span>
1919
{{/if}}
2020
</div>
21-
{{#unless (eq @autofixRequest.status "in_progress")}}
22-
<div
23-
class="text-gray-400 underline text-xs px-3 py-2 inline-flex hover:text-gray-300"
24-
{{on "click" (fn (mut this.shouldShowFullLog) (not this.shouldShowFullLog))}}
25-
role="button"
26-
>
27-
{{#if this.shouldShowFullLog}}
28-
Show summary
29-
{{else}}
30-
Show Full Log
31-
{{/if}}
32-
</div>
33-
{{/unless}}
3421
</div>
3522

3623
{{#if (eq @autofixRequest.status "in_progress")}}
@@ -47,15 +34,11 @@
4734
</div>
4835
</div>
4936
{{else}}
50-
{{#if this.shouldShowFullLog}}
51-
<div class="bg-gray-800 border border-gray-700 rounded-sm grow overflow-y-auto">
52-
<div class="p-3">
53-
<pre class="font-mono text-xs text-white whitespace-pre-wrap"><code>{{ansi-to-html @autofixRequest.logs}}</code></pre>
54-
</div>
55-
</div>
56-
{{else if (eq @autofixRequest.status "success")}}
37+
{{#if (eq @autofixRequest.status "success")}}
5738
<div class="grow overflow-y-auto">
5839
<div class="prose prose-sm dark:prose-invert has-prism-highlighting" {{highlight-code-blocks @autofixRequest.explanationMarkdown}}>
40+
<h3>Hint: {{@autofixRequest.summary}}</h3>
41+
5942
{{markdown-to-html @autofixRequest.explanationMarkdown}}
6043
</div>
6144

@@ -65,13 +48,6 @@
6548
Share Feedback
6649
</TertiaryButton>
6750
</FeedbackButton>
68-
69-
<a
70-
href="https://docs.codecrafters.io/experimental/ai-hints"
71-
target="_blank"
72-
rel="noopener noreferrer"
73-
class="text-sm text-gray-400 underline"
74-
>Learn more</a>.
7551
</div>
7652

7753
<BlurredOverlay @isBlurred={{this.diffIsBlurred}} @overlayClass="bg-gray-900/20">
Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,3 @@
11
<div class="pb-3 flex flex-col" ...attributes data-test-autofix-section>
2-
{{#if this.lastSubmission}}
3-
{{#if this.lastAutofixRequest}}
4-
<CoursePage::TestResultsBar::AutofixSection::AutofixResult @autofixRequest={{this.lastAutofixRequest}} class="overflow-y-auto grow" />
5-
{{else}}
6-
<AlertWithIcon class="mb-3">
7-
<p>
8-
We're testing out a new feature called "AI Hints". This is currently only available on stage 2.
9-
<a href="https://docs.codecrafters.io/experimental/ai-hints" target="_blank" rel="noopener noreferrer">Learn more</a>.
10-
</p>
11-
</AlertWithIcon>
12-
13-
<CoursePage::TestResultsBar::AutofixSection::CreateAutofixPrompt @submission={{this.lastSubmission}} />
14-
{{/if}}
15-
{{else}}
16-
{{! TODO: Review copy here }}
17-
<div class="py-16 flex items-center justify-center grow">
18-
<div class="text-gray-500 text-sm">
19-
{{#if (eq @activeStep.type "CourseStageStep")}}
20-
{{#if (eq @activeStep @currentStep)}}
21-
After you've run tests for this stage once, you'll be able to use the "AI Hints" feature here to help automatically fix any issues.
22-
{{else}}
23-
After you've run tests for the
24-
<LinkTo
25-
class="font-semibold text-gray-400 underline hover:text-gray-300"
26-
@route={{@activeStep.routeParams.route}}
27-
@models={{@activeStep.routeParams.models}}
28-
>{{@activeStep.title}}</LinkTo>
29-
stage, you'll be able to use the "AI Hints" feature here to help automatically fix any issues.
30-
{{/if}}
31-
{{else}}
32-
After you've run tests on your code, you'll be able to use the "AI Hints" feature here to help automatically fix any issues.
33-
{{/if}}
34-
</div>
35-
</div>
36-
{{/if}}
2+
<CoursePage::TestResultsBar::AutofixSection::AutofixResult @autofixRequest={{@autofixRequest}} class="overflow-y-auto grow" />
373
</div>

app/components/course-page/test-results-bar/autofix-section/index.ts

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,15 @@
1-
import type Store from '@ember-data/store';
2-
import { service } from '@ember/service';
31
import Component from '@glimmer/component';
4-
import { StepDefinition } from 'codecrafters-frontend/utils/course-page-step-list';
5-
import type CourseStageStep from 'codecrafters-frontend/utils/course-page-step-list/course-stage-step';
62
import type AutofixRequestModel from 'codecrafters-frontend/models/autofix-request';
7-
import type RepositoryModel from 'codecrafters-frontend/models/repository';
8-
import { tracked } from '@glimmer/tracking';
9-
import fieldComparator from 'codecrafters-frontend/utils/field-comparator';
103

114
interface Signature {
125
Element: HTMLDivElement;
136

147
Args: {
15-
activeStep: StepDefinition;
16-
currentStep: StepDefinition;
17-
repository: RepositoryModel;
8+
autofixRequest: AutofixRequestModel;
189
};
1910
}
2011

21-
export default class AutofixSection extends Component<Signature> {
22-
@service declare store: Store;
23-
@tracked autofixCreationError: string | null = null;
24-
25-
get activeCourseStage() {
26-
if (this.args.activeStep.type === 'CourseStageStep') {
27-
return this.activeStepAsCourseStageStep.courseStage;
28-
} else {
29-
return null;
30-
}
31-
}
32-
33-
get activeStepAsCourseStageStep() {
34-
return this.args.activeStep as CourseStageStep;
35-
}
36-
37-
get lastAutofixRequest(): AutofixRequestModel | null {
38-
if (!this.lastSubmission) {
39-
return null;
40-
}
41-
42-
return (
43-
this.lastSubmission.autofixRequests
44-
.filter((item) => !item.isNew)
45-
.filter((item) => !item.creatorTypeIsSystem)
46-
.sort(fieldComparator('createdAt'))
47-
.at(-1) || null
48-
);
49-
}
50-
51-
get lastSubmission() {
52-
if (!this.activeCourseStage) {
53-
return null;
54-
}
55-
56-
if (this.args.repository.lastSubmission.courseStage === this.activeCourseStage) {
57-
return this.args.repository.lastSubmission;
58-
} else {
59-
return null;
60-
}
61-
}
62-
}
12+
export default class AutofixSection extends Component<Signature> {}
6313

6414
declare module '@glint/environment-ember-loose/registry' {
6515
export default interface Registry {

app/components/course-page/test-results-bar/index.hbs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,43 @@
1818
{{/if}}
1919

2020
<div
21-
class="relative w-full flex flex-col grow overflow-auto {{unless this.isResizing 'transition-[height] duration-200 ease-out'}}"
21+
class="relative w-full flex flex-col grow {{unless this.isResizing 'transition-[height] duration-200 ease-out'}}"
2222
style={{this.expandedContainerStyle}}
2323
data-test-contents
2424
>
2525
{{#if this.isExpanded}}
26-
<div class="flex flex-col grow border-b border-gray-800 px-6">
26+
{{! min-h-0 ensures div doesn't outgrow container !}}
27+
<div class="flex gap-6 grow border-b border-gray-800 px-6 min-h-0">
2728
<CoursePage::TestResultsBar::Pane
28-
@activeTabSlug={{this.activeTabSlug}}
29-
@onActiveTabSlugChange={{fn (mut this.activeTabSlug)}}
30-
@availableTabSlugs={{this.availableTabSlugs}}
29+
@activeTabSlug={{this.activeTabSlugForLeftPane}}
30+
@onActiveTabSlugChange={{fn (mut this.activeTabSlugForLeftPane)}}
31+
@availableTabSlugs={{this.availableTabSlugsForLeftPane}}
32+
class="transition-width duration-200 ease-out {{if this.shouldShowRightPane 'w-1/2' 'w-full'}} grow"
3133
>
32-
{{#if (eq this.activeTabSlug "logs")}}
34+
{{#if (eq this.activeTabSlugForLeftPane "logs")}}
3335
<CoursePage::TestResultsBar::LogsSection
3436
@activeStep={{@activeStep}}
3537
@currentStep={{@currentStep}}
3638
@repository={{@repository}}
37-
class="grow overflow-y-auto"
38-
/>
39-
{{else if (eq this.activeTabSlug "autofix")}}
40-
<CoursePage::TestResultsBar::AutofixSection
41-
@activeStep={{@activeStep}}
42-
@currentStep={{@currentStep}}
43-
@repository={{@repository}}
44-
class="grow overflow-y-auto"
39+
class="grow min-h-0"
4540
/>
41+
{{else if (eq this.activeTabSlugForLeftPane "autofix")}}
42+
{{! Extra if convinces glint that autofixRequest is not null }}
43+
{{#if this.autofixRequestForActiveStep}}
44+
<CoursePage::TestResultsBar::AutofixSection @autofixRequest={{this.autofixRequestForActiveStep}} class="grow min-h-0" />
45+
{{/if}}
46+
{{/if}}
47+
</CoursePage::TestResultsBar::Pane>
48+
49+
<CoursePage::TestResultsBar::Pane
50+
@activeTabSlug={{this.activeTabSlugForRightPane}}
51+
@onActiveTabSlugChange={{fn (mut this.activeTabSlugForRightPane)}}
52+
@availableTabSlugs={{this.availableTabSlugsForRightPane}}
53+
class="transition-[width,opacity] duration-200 ease-out {{if this.shouldShowRightPane 'w-1/2 opacity-100' 'w-[0] opacity-0'}} grow"
54+
>
55+
{{! Extra if convinces glint that autofixRequest is not null }}
56+
{{#if this.autofixRequestForActiveStep}}
57+
<CoursePage::TestResultsBar::AutofixSection @autofixRequest={{this.autofixRequestForActiveStep}} class="grow min-h-0" />
4658
{{/if}}
4759
</CoursePage::TestResultsBar::Pane>
4860
</div>

app/components/course-page/test-results-bar/index.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type AuthenticatorService from 'codecrafters-frontend/services/authentica
99
import type CoursePageStateService from 'codecrafters-frontend/services/course-page-state';
1010
import type CourseStageStep from 'codecrafters-frontend/utils/course-page-step-list/course-stage-step';
1111
import type RepositoryModel from 'codecrafters-frontend/models/repository';
12+
import fade from 'ember-animated/transitions/fade';
13+
import type AutofixRequestModel from 'codecrafters-frontend/models/autofix-request';
1214

1315
interface Signature {
1416
Element: HTMLDivElement;
@@ -20,32 +22,71 @@ interface Signature {
2022
};
2123
}
2224

25+
interface ScreenService {
26+
width: number;
27+
}
28+
2329
export default class TestResultsBar extends Component<Signature> {
24-
@service declare coursePageState: CoursePageStateService;
30+
transition = fade;
31+
2532
@service declare authenticator: AuthenticatorService;
26-
@tracked activeTabSlug = 'logs'; // 'logs' | 'autofix'
33+
@service declare coursePageState: CoursePageStateService;
34+
@service declare screen: ScreenService;
35+
36+
@tracked activeTabSlugForLeftPane = 'logs'; // 'logs' | 'autofix'
37+
@tracked activeTabSlugForRightPane = 'autofix';
2738
@tracked bottomSectionElement: HTMLDivElement | null = null;
2839
@tracked expandedContainerHeight = '75vh';
2940
@tracked isResizing = false;
3041

42+
get autofixRequestForActiveStep(): AutofixRequestModel | null {
43+
if (this.args.activeStep.type !== 'CourseStageStep') {
44+
return null;
45+
}
46+
47+
const courseStageStep = this.args.activeStep as CourseStageStep;
48+
49+
return (
50+
this.args.repository.lastSubmission?.autofixRequests
51+
.filter((request) => !request.creatorTypeIsStaff)
52+
.filter((request) => request.submission === this.args.repository.lastSubmission)
53+
.filter((request) => request.submission.courseStage === courseStageStep.courseStage)
54+
.at(-1) || null
55+
);
56+
}
57+
3158
get availableTabSlugs() {
3259
if (this.args.activeStep.type === 'CourseStageStep') {
3360
const courseStageStep = this.args.activeStep as CourseStageStep;
3461

3562
if (courseStageStep.courseStage.isFirst) {
3663
return ['logs'];
3764
} else {
38-
if (this.authenticator.currentUser?.isStaff) {
65+
if (this.authenticator.currentUser?.isStaff && this.autofixRequestForActiveStep) {
3966
return ['logs', 'autofix'];
4067
} else {
4168
return ['logs'];
4269
}
70+
71+
// return ['logs'];
4372
}
4473
} else {
4574
return ['logs'];
4675
}
4776
}
4877

78+
get availableTabSlugsForLeftPane() {
79+
return this.availableTabSlugs.filter((slug) => !this.availableTabSlugsForRightPane.includes(slug));
80+
}
81+
82+
get availableTabSlugsForRightPane() {
83+
if (this.screen.width > 1024 && this.availableTabSlugs.length > 1) {
84+
return ['autofix'];
85+
} else {
86+
return [];
87+
}
88+
}
89+
4990
get expandedContainerStyle() {
5091
if (this.isExpanded) {
5192
return htmlSafe(`height: ${this.expandedContainerHeight}`);
@@ -62,6 +103,10 @@ export default class TestResultsBar extends Component<Signature> {
62103
return this.coursePageState.testResultsBarIsExpanded;
63104
}
64105

106+
get shouldShowRightPane() {
107+
return this.availableTabSlugsForRightPane.length > 0;
108+
}
109+
65110
@action
66111
handleCollapseButtonClick() {
67112
this.coursePageState.testResultsBarIsExpanded = false;

app/components/course-page/test-results-bar/pane.hbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<div ...attributes>
2-
<ul class="flex items-center gap-2 overflow-x-auto mt-3 mb-3">
1+
<div class="flex flex-col" ...attributes>
2+
<ul class="flex items-center gap-2 overflow-x-auto mt-3 mb-3 shrink-0">
33
{{#each this.tabs as |tab|}}
44
<TabHeader
55
@size="small"

app/models/autofix-request.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ export default class AutofixRequestModel extends Model {
66
@belongsTo('submission', { async: false, inverse: 'autofixRequests' }) declare submission: SubmissionModel;
77
@belongsTo('repository', { async: false, inverse: 'autofixRequests' }) declare repository: RepositoryModel;
88

9-
@attr('string') declare creatorType: 'user' | 'system';
9+
@attr('string') declare creatorType: 'user' | 'system' | 'staff';
1010
@attr() declare changedFiles: { diff: string; filename: string }[]; // free-form JSON
1111
@attr('date') declare createdAt: Date;
1212
@attr('string') declare explanationMarkdown: string;
1313
@attr('string') declare logstreamId: string; // For streaming logs when status is in_progress
1414
@attr('string') declare logsBase64: string; // Base64-encoded logs
1515
@attr('number') declare resultDelayInMilliseconds: number | null;
1616
@attr('string') declare status: string; // 'in_progress' | 'success' | 'failure' | 'error'
17+
@attr('string') declare summary: string;
1718

18-
get creatorTypeIsSystem() {
19-
return this.creatorType === 'system';
19+
get creatorTypeIsStaff() {
20+
return this.creatorType === 'staff';
2021
}
2122

2223
get logs(): string {

mirage/handlers/fake-submission-logs.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ export default function (server) {
55
if (request.queryParams.type === 'success') {
66
return new Response(200, {}, '\x1b[92m[stage-1] passed\x1b[0m\n\x1b[92m[stage-2] passed\x1b[0m');
77
} else {
8-
return new Response(200, {}, '\x1b[91m[stage-1] failure\x1b[0m\n\x1b[91m[stage-2] failure\x1b[0m');
8+
// Generate 20 lines of colored failure output for stages 1-20
9+
const lines = [];
10+
11+
for (let i = 1; i <= 20; i++) {
12+
lines.push(`\x1b[91m[stage-${i}] failure\x1b[0m`);
13+
}
14+
15+
return new Response(200, {}, lines.join('\n'));
916
}
1017
});
1118
}

0 commit comments

Comments
 (0)