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
154 changes: 154 additions & 0 deletions frontend/app/element/ansiline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
export const ANSI_TAILWIND_MAP = {
// Reset and modifiers
0: "reset", // special: clear state
1: "font-bold",
2: "opacity-75",
3: "italic",
4: "underline",
8: "invisible",
9: "line-through",

// Foreground standard colors
30: "text-ansi-black",
31: "text-ansi-red",
32: "text-ansi-green",
33: "text-ansi-yellow",
34: "text-ansi-blue",
35: "text-ansi-magenta",
36: "text-ansi-cyan",
37: "text-ansi-white",

// Foreground bright colors
90: "text-ansi-brightblack",
91: "text-ansi-brightred",
92: "text-ansi-brightgreen",
93: "text-ansi-brightyellow",
94: "text-ansi-brightblue",
95: "text-ansi-brightmagenta",
96: "text-ansi-brightcyan",
97: "text-ansi-brightwhite",

// Background standard colors
40: "bg-ansi-black",
41: "bg-ansi-red",
42: "bg-ansi-green",
43: "bg-ansi-yellow",
44: "bg-ansi-blue",
45: "bg-ansi-magenta",
46: "bg-ansi-cyan",
47: "bg-ansi-white",

// Background bright colors
100: "bg-ansi-brightblack",
101: "bg-ansi-brightred",
102: "bg-ansi-brightgreen",
103: "bg-ansi-brightyellow",
104: "bg-ansi-brightblue",
105: "bg-ansi-brightmagenta",
106: "bg-ansi-brightcyan",
107: "bg-ansi-brightwhite",
};

type InternalStateType = {
modifiers: Set<string>;
textColor: string | null;
bgColor: string | null;
reverse: boolean;
};

type SegmentType = {
text: string;
classes: string;
};

const makeInitialState: () => InternalStateType = () => ({
modifiers: new Set<string>(),
textColor: null,
bgColor: null,
reverse: false,
});

const updateStateWithCodes = (state, codes) => {
codes.forEach((code) => {
if (code === 0) {
// Reset state
state.modifiers.clear();
state.textColor = null;
state.bgColor = null;
state.reverse = false;
return;
}
// Instead of swapping immediately, we set a flag
if (code === 7) {
state.reverse = true;
return;
}
const tailwindClass = ANSI_TAILWIND_MAP[code];
if (tailwindClass && tailwindClass !== "reset") {
if (tailwindClass.startsWith("text-")) {
state.textColor = tailwindClass;
} else if (tailwindClass.startsWith("bg-")) {
state.bgColor = tailwindClass;
} else {
state.modifiers.add(tailwindClass);
}
}
});
return state;
};
Comment on lines +71 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety and consider immutability.

The function has several potential improvements:

  1. Missing type annotations for parameters
  2. Direct state mutation could lead to bugs
  3. Redundant state return as it's mutated in-place

Consider this refactored version:

-const updateStateWithCodes = (state, codes) => {
+const updateStateWithCodes = (state: InternalStateType, codes: number[]): InternalStateType => {
+  const newState = {
+    modifiers: new Set(state.modifiers),
+    textColor: state.textColor,
+    bgColor: state.bgColor,
+    reverse: state.reverse,
+  };
   codes.forEach((code) => {
     if (code === 0) {
-      state.modifiers.clear();
-      state.textColor = null;
-      state.bgColor = null;
-      state.reverse = false;
+      newState.modifiers.clear();
+      newState.textColor = null;
+      newState.bgColor = null;
+      newState.reverse = false;
       return;
     }
     if (code === 7) {
-      state.reverse = true;
+      newState.reverse = true;
       return;
     }
     const tailwindClass = ANSI_TAILWIND_MAP[code];
     if (tailwindClass && tailwindClass !== "reset") {
       if (tailwindClass.startsWith("text-")) {
-        state.textColor = tailwindClass;
+        newState.textColor = tailwindClass;
       } else if (tailwindClass.startsWith("bg-")) {
-        state.bgColor = tailwindClass;
+        newState.bgColor = tailwindClass;
       } else {
-        state.modifiers.add(tailwindClass);
+        newState.modifiers.add(tailwindClass);
       }
     }
   });
-  return state;
+  return newState;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateStateWithCodes = (state, codes) => {
codes.forEach((code) => {
if (code === 0) {
// Reset state
state.modifiers.clear();
state.textColor = null;
state.bgColor = null;
state.reverse = false;
return;
}
// Instead of swapping immediately, we set a flag
if (code === 7) {
state.reverse = true;
return;
}
const tailwindClass = ANSI_TAILWIND_MAP[code];
if (tailwindClass && tailwindClass !== "reset") {
if (tailwindClass.startsWith("text-")) {
state.textColor = tailwindClass;
} else if (tailwindClass.startsWith("bg-")) {
state.bgColor = tailwindClass;
} else {
state.modifiers.add(tailwindClass);
}
}
});
return state;
};
const updateStateWithCodes = (state: InternalStateType, codes: number[]): InternalStateType => {
const newState = {
modifiers: new Set(state.modifiers),
textColor: state.textColor,
bgColor: state.bgColor,
reverse: state.reverse,
};
codes.forEach((code) => {
if (code === 0) {
newState.modifiers.clear();
newState.textColor = null;
newState.bgColor = null;
newState.reverse = false;
return;
}
if (code === 7) {
newState.reverse = true;
return;
}
const tailwindClass = ANSI_TAILWIND_MAP[code];
if (tailwindClass && tailwindClass !== "reset") {
if (tailwindClass.startsWith("text-")) {
newState.textColor = tailwindClass;
} else if (tailwindClass.startsWith("bg-")) {
newState.bgColor = tailwindClass;
} else {
newState.modifiers.add(tailwindClass);
}
}
});
return newState;
};


const stateToClasses = (state: InternalStateType) => {
const classes = [];
classes.push(...Array.from(state.modifiers));

// Apply reverse: swap text and background colors if flag is set.
let textColor = state.textColor;
let bgColor = state.bgColor;
if (state.reverse) {
[textColor, bgColor] = [bgColor, textColor];
}
if (textColor) classes.push(textColor);
if (bgColor) classes.push(bgColor);

return classes.join(" ");
};

const ansiRegex = /\x1b\[([0-9;]+)m/g;

const AnsiLine = ({ line }) => {
const segments: SegmentType[] = [];
let lastIndex = 0;
let currentState = makeInitialState();

let match: RegExpExecArray;
while ((match = ansiRegex.exec(line)) !== null) {
if (match.index > lastIndex) {
segments.push({
text: line.substring(lastIndex, match.index),
classes: stateToClasses(currentState),
});
}
const codes = match[1].split(";").map(Number);
updateStateWithCodes(currentState, codes);
lastIndex = ansiRegex.lastIndex;
}

if (lastIndex < line.length) {
segments.push({
text: line.substring(lastIndex),
classes: stateToClasses(currentState),
});
}

return (
<div>
{segments.map((seg, idx) => (
<span key={idx} className={seg.classes}>
{seg.text}
</span>
))}
</div>
);
};

export default AnsiLine;
Comment on lines +118 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add prop types and optimize performance.

Several improvements could enhance the component:

  1. Missing prop types
  2. Array index as key could cause issues with React reconciliation
  3. Could benefit from memoization
+type AnsiLineProps = {
+  line: string;
+};

-const AnsiLine = ({ line }) => {
+const AnsiLine = ({ line }: AnsiLineProps) => {
   const segments: SegmentType[] = [];
   let lastIndex = 0;
   let currentState = makeInitialState();

   let match: RegExpExecArray;
   while ((match = ansiRegex.exec(line)) !== null) {
     if (match.index > lastIndex) {
       segments.push({
         text: line.substring(lastIndex, match.index),
         classes: stateToClasses(currentState),
       });
     }
     const codes = match[1].split(";").map(Number);
     currentState = updateStateWithCodes(currentState, codes);
     lastIndex = ansiRegex.lastIndex;
   }

   if (lastIndex < line.length) {
     segments.push({
       text: line.substring(lastIndex),
       classes: stateToClasses(currentState),
     });
   }

   return (
     <div>
       {segments.map((seg, idx) => (
-        <span key={idx} className={seg.classes}>
+        <span key={`${idx}-${seg.text}`} className={seg.classes}>
           {seg.text}
         </span>
       ))}
     </div>
   );
 };

+export default React.memo(AnsiLine);
-export default AnsiLine;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AnsiLine = ({ line }) => {
const segments: SegmentType[] = [];
let lastIndex = 0;
let currentState = makeInitialState();
let match: RegExpExecArray;
while ((match = ansiRegex.exec(line)) !== null) {
if (match.index > lastIndex) {
segments.push({
text: line.substring(lastIndex, match.index),
classes: stateToClasses(currentState),
});
}
const codes = match[1].split(";").map(Number);
updateStateWithCodes(currentState, codes);
lastIndex = ansiRegex.lastIndex;
}
if (lastIndex < line.length) {
segments.push({
text: line.substring(lastIndex),
classes: stateToClasses(currentState),
});
}
return (
<div>
{segments.map((seg, idx) => (
<span key={idx} className={seg.classes}>
{seg.text}
</span>
))}
</div>
);
};
export default AnsiLine;
type AnsiLineProps = {
line: string;
};
const AnsiLine = ({ line }: AnsiLineProps) => {
const segments: SegmentType[] = [];
let lastIndex = 0;
let currentState = makeInitialState();
let match: RegExpExecArray;
while ((match = ansiRegex.exec(line)) !== null) {
if (match.index > lastIndex) {
segments.push({
text: line.substring(lastIndex, match.index),
classes: stateToClasses(currentState),
});
}
const codes = match[1].split(";").map(Number);
currentState = updateStateWithCodes(currentState, codes);
lastIndex = ansiRegex.lastIndex;
}
if (lastIndex < line.length) {
segments.push({
text: line.substring(lastIndex),
classes: stateToClasses(currentState),
});
}
return (
<div>
{segments.map((seg, idx) => (
<span key={`${idx}-${seg.text}`} className={seg.classes}>
{seg.text}
</span>
))}
</div>
);
};
export default React.memo(AnsiLine);

18 changes: 18 additions & 0 deletions frontend/tailwindsetup.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,22 @@
--text-default: 14px;

--radius: 8px;

/* ANSI Colors (Default Dark Palette) */
--ansi-black: #757575;
--ansi-red: #cc685c;
--ansi-green: #76c266;
--ansi-yellow: #cbca9b;
--ansi-blue: #85aacb;
--ansi-magenta: #cc72ca;
--ansi-cyan: #74a7cb;
--ansi-white: #c1c1c1;
--ansi-brightblack: #727272;
--ansi-brightred: #cc9d97;
--ansi-brightgreen: #a3dd97;
--ansi-brightyellow: #cbcaaa;
--ansi-brightblue: #9ab6cb;
--ansi-brightmagenta: #cc8ecb;
--ansi-brightcyan: #b7b8cb;
--ansi-brightwhite: #f0f0f0;
}
Loading