Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ worktrees/

# Graphify knowledge graph output
graphify-out/

# Impeccable
.impeccable/
135 changes: 93 additions & 42 deletions app/css/components/assistant-chat.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/* The editor's AI assistant chat panel.
Ported from Jiki's ChatPanel/Conversation/ChatMessages/ChatInput CSS modules,
remapped onto Exercism's design tokens. */
.c-assistant-chat {
@apply flex flex-col;
height: 100%;
Expand Down Expand Up @@ -74,14 +71,13 @@
object-fit: cover;
}

/* The assistant's avatar is Exercism's face on a light purple disc */
& .message.assistant .avatar {
background: theme(colors.lightPurple);
background: var(--backgroundColorF);
box-shadow: 0 0 0 1px var(--borderColor6);
& .c-icon {
width: 22px;
height: 22px;
filter: var(--purple-filter);
filter: var(--purple-light-white-dark-filter);
}
}

Expand All @@ -97,8 +93,8 @@
}

& .message.assistant .content {
background: theme(colors.lightPurple);
border: 1px solid theme(colors.lavender);
background: var(--backgroundColorF);
border: 1px solid var(--borderColor6);
}

/* --- Message content typography ---------------------------------- */
Expand All @@ -121,12 +117,12 @@
}
& .content a {
@apply font-medium underline;
color: theme(colors.purple);
color: var(--colorPurpleToBrightPurple);
}
& .content code {
@apply font-mono text-13 px-6 py-2 rounded-3;
background: var(--backgroundColorD);
color: theme(colors.purple);
color: var(--colorPurpleToBrightPurple);
}
& .content pre {
@apply p-12 rounded-8;
Expand All @@ -142,14 +138,22 @@
}

/* --- Thinking indicator ------------------------------------------ */
/* Dots are shorter than a line of text, so top alignment misplaces the avatar */
& .message.thinking-row {
@apply items-center;
& .avatar {
margin-top: 0;
}
}

& .thinking {
@apply flex items-center gap-8 text-15;
color: var(--textColor6);
& span {
width: 6px;
height: 6px;
border-radius: 50%;
background: theme(colors.purple);
background: var(--colorPurpleToBrightPurple);
animation: assistant-chat-pulse 1.4s ease-in-out infinite;
}
& span:nth-child(2) {
Expand All @@ -163,25 +167,25 @@
/* --- Notice bars (error / usage) --------------------------------- */
& .chat-status {
@apply flex items-center justify-between gap-12 px-32 py-8;
background: theme(colors.superLightOrange);
background: var(--backgroundColorCAlert);
border-top: 1px solid theme(colors.orange);
& .message-text {
@apply text-14;
color: var(--textColor2);
color: var(--textColorCAlert);
}
& button {
@apply text-14 font-medium underline;
color: theme(colors.purple);
color: var(--colorPurpleToBrightPurple);
}
}

& .chat-usage {
@apply px-32 py-8;
background: theme(colors.champagne);
background: var(--backgroundColorCAlert);
border-top: 1px solid theme(colors.yellowPrompt);
& .usage-text {
@apply text-14;
color: var(--textColor2);
color: var(--textColorCAlert);
}
&.at-cap .usage-text {
@apply font-medium;
Expand Down Expand Up @@ -222,7 +226,7 @@
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid theme(colors.gray);
border: 1px solid var(--borderColor6);
}
}

Expand All @@ -234,13 +238,13 @@
& .chat-input-field textarea {
@apply block w-100 p-12 rounded-8 text-15 leading-150;
background: var(--backgroundColorA);
border: 2px solid theme(colors.lavender);
border: 2px solid var(--borderColor6);
resize: none;
height: 96px;
transition: border-color 0.2s;
&:focus {
outline: none;
border-color: theme(colors.purple);
border-color: var(--colorPurpleToBrightPurple);
}
&::placeholder {
color: var(--textColor6);
Expand Down Expand Up @@ -280,12 +284,24 @@
}

/* --- Start state (no conversation yet) ---------------------------- */
/* Resizable pane: size off the container, not the viewport */
& .chat-start {
@apply flex items-center justify-center rounded-12;
@apply p-40;
container-type: inline-size;
container-name: chat-start;
flex: 1;
margin: 24px 32px 32px;
background: theme(colors.lightPurple);
/* Shrink to the scrolling tab panel rather than overflowing it */
min-height: 0;
/* `safe` keeps the top reachable when the panel is shorter than the card */
align-items: safe center;
margin: 16px;
padding: 32px 16px;
background: var(--backgroundColorF);

/* Spacing scale: bind an item to its label, group rows, break between groups */
--space-bind: 4px;
--space-group: 16px;
--space-break: 32px;
}

& .chat-start-content {
Expand All @@ -296,7 +312,8 @@
}

& .chat-start-avatar {
@apply flex items-center justify-center mx-auto mb-16;
@apply flex items-center justify-center mx-auto;
margin-bottom: var(--space-group);
width: 64px;
height: 64px;
border-radius: 50%;
Expand All @@ -314,44 +331,59 @@
}

& .chat-start-content h3 {
@apply text-h4 mb-8;
@apply text-h4;
margin-bottom: var(--space-bind);
text-wrap: balance;
color: var(--textColor1);
}

& .chat-start-description {
@apply text-p-base mx-auto mb-24 leading-170;
@apply text-p-base mx-auto;
/* Tighter than body leading so a wrapped sentence reads as one phrase */
line-height: 1.45;
margin-bottom: var(--space-break);
max-width: 460px;
text-wrap: pretty;
color: var(--textColor6);
/* Reserve the second line so the typewriter doesn't shift the input */
@container chat-start (max-width: 340px) {
min-height: calc(2 * 1.45em);
}
& .rotating-text {
@apply font-medium;
color: theme(colors.purple);
/* Keeps the phrase and its caret together on one line */
display: inline-block;
color: var(--colorPurpleToBrightPurple);
}
& .cursor {
display: inline-block;
width: 1px;
height: 1em;
margin-inline-start: 2px;
vertical-align: text-bottom;
background: theme(colors.purple);
background: var(--colorPurpleToBrightPurple);
animation: assistant-chat-caret 1s steps(2, start) infinite;
}
}

& .chat-start-input {
@apply relative mb-12;
@apply relative;
margin-bottom: var(--space-group);
& textarea {
@apply block w-100 rounded-12 text-16 leading-150;
padding: 14px 16px 56px 16px;
@apply block w-100 rounded-12 leading-150;
font-size: 16px;
/* Bottom padding clears the overlaid send button (16 + 40 + 8) */
padding: 16px 16px 64px;
background: var(--backgroundColorA);
border: 2px solid theme(colors.lavender);
border: 2px solid var(--borderColor6);
resize: none;
overflow: hidden;
min-height: 100px;
min-height: 128px;
transition: border-color 0.2s, height 0.1s ease;
animation: assistant-chat-glow 5s ease-in-out infinite;
&:focus {
outline: none;
border-color: theme(colors.purple);
border-color: var(--colorPurpleToBrightPurple);
}
&::placeholder {
color: var(--textColor6);
Expand All @@ -361,19 +393,32 @@

& .chat-start-send {
@apply absolute inline-flex items-center justify-center gap-6;
@apply rounded-8 text-16 font-medium;
bottom: 19px;
inset-inline-end: 14px;
@apply rounded-8 font-medium;
font-size: 16px;
bottom: 16px;
inset-inline-end: 16px;
padding: 8px 16px;
background: theme(colors.purpleHover);
color: theme(colors.purple);
/* A wrapped label would outgrow the padding the textarea reserves for it */
white-space: nowrap;
max-width: calc(100% - 32px);
overflow: hidden;
text-overflow: ellipsis;
background: var(--backgroundColorBtnSecondary);
color: var(--colorPurpleToBrightPurple);
opacity: 0.7;
cursor: default;
transition: all 0.3s ease;
& .c-icon {
width: 16px;
height: 16px;
filter: var(--purple-filter);
flex-shrink: 0;
/* Matches the label's --colorPurpleToBrightPurple in both themes */
filter: var(--purple-to-bright-purple-filter);
}
@container chat-start (max-width: 200px) {
& .c-icon {
display: none;
}
}
&.--active {
background: linear-gradient(
Expand All @@ -390,12 +435,18 @@
}
}

/* Tick is set inline, not as a flex row, so it flows with the copy when narrow */
& .chat-start-included {
@apply flex items-center justify-center gap-6 text-15;
font-size: 14px;
line-height: 1.4;
text-wrap: balance;
color: var(--textColor6);
& .c-icon {
width: 18px;
height: 18px;
display: inline-block;
width: 16px;
height: 16px;
margin-inline-end: 8px;
vertical-align: -3px;
filter: var(--textColor6-filter);
}
}
Expand Down
55 changes: 54 additions & 1 deletion app/css/pages/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
@apply uses-suspense;
}

/* `body` reserves a scrollbar gutter globally (see defaults.css) so pages
don't jump horizontally as the scrollbar comes and goes. The editor is
`position: fixed` at 100% height and never scrolls the body, so on this
page that rule can only ever draw an empty, unusable track down the right
edge. Reclaim the gutter here; every other page keeps the default. */
body:has(.c-react-wrapper-editor) {
overflow-y: hidden;
}

#page-editor {
@apply fixed flex flex-col;
width: 100%;
Expand Down Expand Up @@ -339,8 +348,13 @@
}
}

/* `auto`, not `scroll`: several of these panels (Results,
Feedback, the assistant's empty state) have nothing to scroll,
and `scroll` paints an empty track down their right edge
regardless. The panels that do overflow — Instructions, Get
help — still get their scrollbar. */
& > .--tab-panel {
overflow-y: scroll;
overflow-y: auto;
height: 100%;
}

Expand Down Expand Up @@ -377,6 +391,45 @@
}
}

/* The panel is a resizable pane, so its width is independent
of the viewport's — these size off the container, not the
screen. */
section.request-mentoring {
container-type: inline-size;
container-name: request-mentoring;

/* The illustration is a fixed 110px and doesn't shrink, so
beside the prose it leaves a column a couple of words wide.
Stack it above the copy instead: it keeps its full size and
the text gets the whole width. */
& .--intro {
@container request-mentoring (max-width: 460px) {
@apply flex-col items-start;

/* Lead with the illustration, then the copy. Left
aligned like the buttons and prose below it, so the
stacked layout keeps one edge throughout. */
& > .c-icon {
@apply order-first;
@apply ml-0 mt-0 mb-16;
}
}
}

/* Side by side the two don't fit: the "100% free" badge is
squeezed until its label breaks over three lines. Stack
them and each keeps its natural width. */
& .--cta {
@container request-mentoring (max-width: 420px) {
@apply flex-col items-start;

& > * + * {
@apply ml-0 mt-8;
}
}
}
}

section.feedback-pane {
@apply pb-16 px-24;

Expand Down
6 changes: 6 additions & 0 deletions app/javascript/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@ export default ({

<SplitPane
id="editor"
// Below this the right-hand panels stop being usable. The
// binding constraint is the feedback panel: its illustration is a
// fixed 110px plus a 48px margin and doesn't shrink, so the prose
// beside it gets `width - 209px`. 400 keeps that column around
// 190px; much less and it wraps to a couple of words a line.
rightMinWidth={400}
left={
<>
<FileEditorCodeMirror
Expand Down
Loading
Loading