fix: improves table formatting in the playground and adds Jest tests (#8743)

* feat(tests): add Jest configuration and setup for testing environment

- Introduced Jest configuration file to set up testing environment with TypeScript support and JSDOM.
- Added setupTests.ts for global test configurations, including mocks for ResizeObserver and IntersectionObserver.
- Updated package.json and package-lock.json to include Jest and related dependencies.
- Implemented utility functions for processing markdown content, including handling tables and <think> tags.
- Added comprehensive tests for markdown utility functions to ensure proper functionality.

* refactor(makefile): separate frontend commands into a dedicated Makefile

- Removed frontend-related targets from the main Makefile and created a new Makefile.frontend to manage frontend-specific commands.
- Updated the main Makefile to include a reference to the new frontend Makefile and added a help message for frontend commands.
- This restructuring improves organization and clarity for managing backend and frontend build processes.
This commit is contained in:
Deon Sanchez 2025-06-30 15:13:04 -06:00 committed by GitHub
commit ff00eb582f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 5004 additions and 73 deletions

View file

@ -1,4 +1,4 @@
.PHONY: all init format_backend format_frontend format lint build build_frontend install_frontend run_frontend run_backend dev help tests coverage clean_python_cache clean_npm_cache clean_all
.PHONY: all init format_backend format lint build run_backend dev help tests coverage clean_python_cache clean_npm_cache clean_all
# Configurations
VERSION=$(shell grep "^version" pyproject.toml | sed 's/.*\"\(.*\)\"$$/\1/')
@ -45,6 +45,7 @@ help: ## show this help message
awk -F ':.*##' '{printf "\033[36mmake %s\033[0m: %s\n", $$1, $$2}' | \
column -c2 -t -s :
@echo '----'
@echo 'For frontend commands, run: make help_frontend'
######################
# INSTALL PROJECT
@ -58,22 +59,7 @@ install_backend: ## install the backend dependencies
@echo 'Installing backend dependencies'
@uv sync --frozen --extra "postgresql" $(EXTRA_ARGS)
install_frontend: ## install the frontend dependencies
@echo 'Installing frontend dependencies'
@cd src/frontend && npm install > /dev/null 2>&1
build_frontend: ## build the frontend static files
@echo '==== Starting frontend build ===='
@echo 'Current directory: $$(pwd)'
@echo 'Checking if src/frontend exists...'
@ls -la src/frontend || true
@echo 'Building frontend static files...'
@cd src/frontend && CI='' npm run build 2>&1 || { echo "\nBuild failed! Error output above ☝️"; exit 1; }
@echo 'Clearing destination directory...'
$(call CLEAR_DIRS,src/backend/base/langflow/frontend)
@echo 'Copying build files...'
@cp -r src/frontend/build/. src/backend/base/langflow/frontend
@echo '==== Frontend build complete ===='
init: check_tools ## initialize the project
@make install_backend
@ -189,9 +175,6 @@ format_backend: ## backend code formatters
@uv run ruff check . --fix
@uv run ruff format . --config pyproject.toml
format_frontend: ## frontend code formatters
@cd src/frontend && npm run format
format: format_backend format_frontend ## run code formatters
unsafe_fix:
@ -200,22 +183,7 @@ unsafe_fix:
lint: install_backend ## run linters
@uv run mypy --namespace-packages -p "langflow"
install_frontendci:
@cd src/frontend && npm ci > /dev/null 2>&1
install_frontendc:
@cd src/frontend && $(call CLEAR_DIRS,node_modules) && rm -f package-lock.json && npm install > /dev/null 2>&1
run_frontend: ## run the frontend
@-kill -9 `lsof -t -i:3000`
@cd src/frontend && npm start $(if $(FRONTEND_START_FLAGS),-- $(FRONTEND_START_FLAGS))
tests_frontend: ## run frontend tests
ifeq ($(UI), true)
@cd src/frontend && npx playwright test --ui --project=chromium
else
@cd src/frontend && npx playwright test --project=chromium
endif
run_cli: install_frontend install_backend build_frontend ## run the CLI
@echo 'Running the CLI'
@ -250,11 +218,7 @@ setup_devcontainer: ## set up the development container
setup_env: ## set up the environment
@sh ./scripts/setup/setup_env.sh
frontend: install_frontend ## run the frontend in development mode
make run_frontend
frontendc: install_frontendc
make run_frontend
backend: setup_env install_backend ## run the backend in development mode
@ -551,3 +515,10 @@ locust: ## run locust load tests (options: locust_users=10 locust_spawn_rate=1 l
--host $(locust_host) \
-f $$(basename "$(locust_file)"); \
fi
######################
# INCLUDE FRONTEND MAKEFILE
######################
# Include frontend-specific Makefile
include Makefile.frontend

206
Makefile.frontend Normal file
View file

@ -0,0 +1,206 @@
# Frontend-specific Makefile for Langflow
# This file contains all frontend-related targets
# Variables
FRONTEND_DIR = src/frontend
NPM = npm
.PHONY: install_frontend install_frontendci install_frontendc frontend_deps_check build_frontend run_frontend frontend frontendc format_frontend tests_frontend test_frontend test_frontend_watch test_frontend_coverage test_frontend_verbose test_frontend_ci test_frontend_clean test_frontend_file test_frontend_pattern test_frontend_snapshots test_frontend_config test_frontend_bail test_frontend_silent test_frontend_coverage_open help_frontend
######################
# FRONTEND DEPENDENCIES
######################
install_frontend: ## install the frontend dependencies
@echo 'Installing frontend dependencies'
@cd $(FRONTEND_DIR) && npm install > /dev/null 2>&1
install_frontendci:
@cd $(FRONTEND_DIR) && npm ci > /dev/null 2>&1
install_frontendc:
@cd $(FRONTEND_DIR) && $(call CLEAR_DIRS,node_modules) && rm -f package-lock.json && npm install > /dev/null 2>&1
# Check if frontend dependencies are installed
frontend_deps_check:
@if [ ! -d "$(FRONTEND_DIR)/node_modules" ]; then \
echo "Frontend dependencies not found. Installing..."; \
$(MAKE) install_frontend; \
fi
######################
# FRONTEND BUILD
######################
build_frontend: ## build the frontend static files
@echo '==== Starting frontend build ===='
@echo 'Current directory: $$(pwd)'
@echo 'Checking if $(FRONTEND_DIR) exists...'
@ls -la $(FRONTEND_DIR) || true
@echo 'Building frontend static files...'
@cd $(FRONTEND_DIR) && CI='' npm run build 2>&1 || { echo "\nBuild failed! Error output above ☝️"; exit 1; }
@echo 'Clearing destination directory...'
$(call CLEAR_DIRS,src/backend/base/langflow/frontend)
@echo 'Copying build files...'
@cp -r $(FRONTEND_DIR)/build/. src/backend/base/langflow/frontend
@echo '==== Frontend build complete ===='
######################
# FRONTEND DEVELOPMENT
######################
run_frontend: ## run the frontend
@-kill -9 `lsof -t -i:3000`
@cd $(FRONTEND_DIR) && npm start $(if $(FRONTEND_START_FLAGS),-- $(FRONTEND_START_FLAGS))
frontend: install_frontend ## run the frontend in development mode
make run_frontend
frontendc: install_frontendc
make run_frontend
######################
# FRONTEND CODE QUALITY
######################
format_frontend: ## frontend code formatters
@cd $(FRONTEND_DIR) && npm run format
######################
# FRONTEND E2E TESTS (PLAYWRIGHT)
######################
tests_frontend: ## run frontend tests
ifeq ($(UI), true)
@cd $(FRONTEND_DIR) && npx playwright test --ui --project=chromium
else
@cd $(FRONTEND_DIR) && npx playwright test --project=chromium
endif
######################
# FRONTEND UNIT TESTS (JEST)
######################
# Run all frontend Jest unit tests
test_frontend: frontend_deps_check ## run all frontend Jest unit tests
@echo "Running all frontend Jest unit tests..."
@cd $(FRONTEND_DIR) && $(NPM) test
# Run frontend tests in watch mode
test_frontend_watch: frontend_deps_check ## run frontend tests in watch mode
@echo "Running frontend tests in watch mode..."
@cd $(FRONTEND_DIR) && $(NPM) run test:watch
# Run frontend tests with coverage report
test_frontend_coverage: frontend_deps_check ## run frontend tests with coverage report
@echo "Running frontend tests with coverage report..."
@cd $(FRONTEND_DIR) && npx jest --coverage
# Run frontend tests with verbose output
test_frontend_verbose: frontend_deps_check ## run frontend tests with verbose output
@echo "Running frontend tests with verbose output..."
@cd $(FRONTEND_DIR) && npx jest --verbose
# Run frontend tests in CI mode (no watch, with coverage)
test_frontend_ci: frontend_deps_check ## run frontend tests in CI mode
@echo "Running frontend tests in CI mode..."
@cd $(FRONTEND_DIR) && npx jest --ci --coverage --watchAll=false
# Clean test cache and run tests
test_frontend_clean: frontend_deps_check ## clean test cache and run tests
@echo "Cleaning Jest cache and running tests..."
@cd $(FRONTEND_DIR) && npx jest --clearCache && npx jest
# Run tests for a specific file
test_frontend_file: frontend_deps_check ## run tests for a specific file (usage: make test_frontend_file path/to/test.ts)
$(eval file := $(word 2,$(MAKECMDGOALS)))
@if [ -z "$(file)" ]; then \
echo "Usage: make test_frontend_file path/to/test.ts"; \
exit 1; \
fi
@echo "Running tests for file: $(file)"
@cd $(FRONTEND_DIR) && npx jest $(file)
# Prevent make from treating the file argument as another target
%:
@:
# Run tests matching a pattern
test_frontend_pattern: frontend_deps_check ## run tests matching a pattern (usage: make test_frontend_pattern pattern)
$(eval pattern := $(word 2,$(MAKECMDGOALS)))
@if [ -z "$(pattern)" ]; then \
echo "Usage: make test_frontend_pattern pattern"; \
exit 1; \
fi
@echo "Running tests matching pattern: $(pattern)"
@cd $(FRONTEND_DIR) && npx jest --testNamePattern="$(pattern)"
# Update test snapshots
test_frontend_snapshots: frontend_deps_check ## update Jest snapshots
@echo "Updating Jest snapshots..."
@cd $(FRONTEND_DIR) && npx jest --updateSnapshot
# Show test configuration
test_frontend_config: ## show Jest configuration
@echo "Jest configuration:"
@cd $(FRONTEND_DIR) && npx jest --showConfig
# Run Jest tests with bail (stop on first failure)
test_frontend_bail: frontend_deps_check ## run tests with bail (stop on first failure)
@echo "Running Jest tests with bail (stop on first failure)..."
@cd $(FRONTEND_DIR) && npx jest --bail
# Run Jest tests silently (minimal output)
test_frontend_silent: frontend_deps_check ## run tests silently (minimal output)
@echo "Running Jest tests silently..."
@cd $(FRONTEND_DIR) && npx jest --silent
# Run Jest tests and open coverage report in browser
test_frontend_coverage_open: test_frontend_coverage ## run tests with coverage and open report in browser
@echo "Opening coverage report in browser..."
@if command -v open >/dev/null 2>&1; then \
open $(FRONTEND_DIR)/coverage/lcov-report/index.html; \
elif command -v xdg-open >/dev/null 2>&1; then \
xdg-open $(FRONTEND_DIR)/coverage/lcov-report/index.html; \
else \
echo "Coverage report generated at: $(FRONTEND_DIR)/coverage/lcov-report/index.html"; \
fi
######################
# FRONTEND HELP
######################
help_frontend: ## show frontend help
@echo "Frontend Commands:"
@echo ""
@echo "Dependencies:"
@echo " install_frontend - Install frontend dependencies"
@echo " install_frontendci - Install frontend dependencies with npm ci"
@echo " install_frontendc - Clean install frontend dependencies"
@echo ""
@echo "Build & Development:"
@echo " build_frontend - Build frontend static files"
@echo " run_frontend - Run the frontend development server"
@echo " frontend - Install dependencies and run frontend in dev mode"
@echo " frontendc - Clean install dependencies and run frontend"
@echo ""
@echo "Code Quality:"
@echo " format_frontend - Format frontend code"
@echo ""
@echo "Testing:"
@echo " tests_frontend - Run frontend Playwright e2e tests"
@echo " test_frontend - Run frontend Jest unit tests"
@echo " test_frontend_watch - Run unit tests in watch mode"
@echo " test_frontend_coverage - Run unit tests with coverage"
@echo " test_frontend_coverage_open - Run coverage and open report"
@echo " test_frontend_verbose - Run unit tests with verbose output"
@echo " test_frontend_ci - Run unit tests in CI mode"
@echo " test_frontend_clean - Clean cache and run unit tests"
@echo " test_frontend_bail - Run unit tests with bail"
@echo " test_frontend_silent - Run unit tests silently"
@echo ""
@echo "Targeted Testing:"
@echo " test_frontend_file path - Run tests for specific file"
@echo " test_frontend_pattern pattern - Run tests matching pattern"
@echo " test_frontend_snapshots - Update Jest snapshots"
@echo " test_frontend_config - Show Jest configuration"

View file

@ -0,0 +1,20 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
injectGlobals: true,
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
},
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
testMatch: [
"<rootDir>/src/**/__tests__/**/*.{ts,tsx}",
"<rootDir>/src/**/*.{test,spec}.{ts,tsx}",
],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
// Ignore node_modules except for packages that need transformation
transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$|@testing-library))"],
};

File diff suppressed because it is too large Load diff

View file

@ -93,6 +93,8 @@
"dev:docker": "vite --host 0.0.0.0",
"start": "vite",
"build": "vite build",
"test": "jest",
"test:watch": "jest --watch",
"serve": "vite preview",
"format": "npx prettier --write \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
"check-format": "npx prettier --check \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
@ -118,6 +120,7 @@
},
"proxy": "http://localhost:7860",
"devDependencies": {
"@jest/types": "^30.0.1",
"@playwright/test": "^1.52.0",
"@swc/cli": "^0.5.2",
"@swc/core": "^1.6.1",
@ -135,6 +138,8 @@
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"eslint": "^9.5.0",
"jest": "^30.0.3",
"jest-environment-jsdom": "^30.0.2",
"postcss": "^8.4.38",
"prettier": "^3.3.2",
"prettier-plugin-organize-imports": "^3.2.4",
@ -142,8 +147,9 @@
"simple-git-hooks": "^2.11.1",
"tailwindcss": "^3.4.4",
"tailwindcss-dotted-background": "^1.1.0",
"ts-jest": "^29.4.0",
"typescript": "^5.4.5",
"ua-parser-js": "^1.0.38",
"vite": "^5.4.19"
}
}
}

View file

@ -1,4 +1,3 @@
import { ProfileIcon } from "@/components/core/appHeaderComponent/components/ProfileIcon";
import { ContentBlockDisplay } from "@/components/core/chatComponents/ContentBlockDisplay";
import { useUpdateMessage } from "@/controllers/API/queries/messages";
import { CustomProfileIcon } from "@/customization/components/custom-profile-icon";

View file

@ -1,4 +1,5 @@
import { EMPTY_OUTPUT_SEND_MESSAGE } from "@/constants/constants";
import { preprocessChatMessage } from "@/utils/markdownUtils";
import { cn } from "@/utils/utils";
import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
@ -14,14 +15,6 @@ type MarkdownFieldProps = {
isAudioMessage?: boolean;
};
// Function to replace <think> tags with a placeholder before markdown processing
const preprocessChatMessage = (text: string): string => {
// Replace <think> tags with `<span class="think-tag">think:</span>`
return text
.replace(/<think>/g, "`<think>`")
.replace(/<\/think>/g, "`</think>`");
};
export const MarkdownField = ({
chat,
isEmpty,
@ -29,7 +22,7 @@ export const MarkdownField = ({
editedFlag,
isAudioMessage,
}: MarkdownFieldProps) => {
// Process the chat message to handle <think> tags
// Process the chat message to handle <think> tags and clean up tables
const processedChatMessage = preprocessChatMessage(chatMessage);
return (

View file

@ -0,0 +1,62 @@
// Jest setup file for testing environment
import "@testing-library/jest-dom";
// Mock ResizeObserver if not available in test environment
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
// Mock IntersectionObserver if not available in test environment
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
// Mock window.matchMedia for components that use it
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Suppress console.error and console.warn in tests unless explicitly needed
const originalError = console.error;
const originalWarn = console.warn;
beforeAll(() => {
console.error = (...args) => {
if (
typeof args[0] === "string" &&
args[0].includes("Warning: ReactDOM.render is deprecated")
) {
return;
}
originalError.call(console, ...args);
};
console.warn = (...args) => {
if (
typeof args[0] === "string" &&
args[0].includes("componentWillReceiveProps has been renamed")
) {
return;
}
originalWarn.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
console.warn = originalWarn;
});

View file

@ -0,0 +1,215 @@
import {
cleanupTableEmptyCells,
isMarkdownTable,
preprocessChatMessage,
} from "../markdownUtils";
describe("markdownUtils", () => {
describe("isMarkdownTable", () => {
it("should return true for valid markdown table", () => {
const table = `| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |`;
expect(isMarkdownTable(table)).toBe(true);
});
it("should return true for table with alignment", () => {
const table = `| Left | Center | Right |
|:-----|:------:|------:|
| L1 | C1 | R1 |`;
expect(isMarkdownTable(table)).toBe(true);
});
it("should return false for non-table content", () => {
expect(isMarkdownTable("Just some text")).toBe(false);
expect(isMarkdownTable("# Header\nSome content")).toBe(false);
});
it("should return false for empty or null input", () => {
expect(isMarkdownTable("")).toBe(false);
expect(isMarkdownTable(" ")).toBe(false);
});
it("should return false for table without separator", () => {
const invalidTable = `| Header 1 | Header 2 |
| Cell 1 | Cell 2 |`;
expect(isMarkdownTable(invalidTable)).toBe(false);
});
it("should return true for table with extra whitespace", () => {
const table = ` | Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 | `;
expect(isMarkdownTable(table)).toBe(true);
});
});
describe("cleanupTableEmptyCells", () => {
it("should remove completely empty rows", () => {
const table = `| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| | |
| Cell 3 | Cell 4 |`;
const expected = `| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |`;
expect(cleanupTableEmptyCells(table)).toBe(expected);
});
it("should keep rows with at least one non-empty cell", () => {
const table = `| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | |
| | Cell 2 |`;
const expected = `| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | |
| | Cell 2 |`;
expect(cleanupTableEmptyCells(table)).toBe(expected);
});
it("should preserve separator rows", () => {
const table = `| Header 1 | Header 2 |
|----------|----------|
| | |`;
const expected = `| Header 1 | Header 2 |
|----------|----------|`;
expect(cleanupTableEmptyCells(table)).toBe(expected);
});
it("should handle mixed content with tables", () => {
const content = `Some text before
| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| | |
Some text after`;
const expected = `Some text before
| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
Some text after`;
expect(cleanupTableEmptyCells(content)).toBe(expected);
});
it("should handle non-table content unchanged", () => {
const content = `# Header
This is just regular text.
No tables here.`;
expect(cleanupTableEmptyCells(content)).toBe(content);
});
it("should handle empty input", () => {
expect(cleanupTableEmptyCells("")).toBe("");
expect(cleanupTableEmptyCells(" \n \n ")).toBe(" \n \n ");
});
it("should handle table with alignment separators", () => {
const table = `| Left | Center | Right |
|:-----|:------:|------:|
| L1 | C1 | R1 |
| | | |`;
const expected = `| Left | Center | Right |
|:-----|:------:|------:|
| L1 | C1 | R1 |`;
expect(cleanupTableEmptyCells(table)).toBe(expected);
});
});
describe("preprocessChatMessage", () => {
it("should replace <think> tags with backticks", () => {
const message = "Before <think>thinking</think> after";
const expected = "Before `<think>`thinking`</think>` after";
expect(preprocessChatMessage(message)).toBe(expected);
});
it("should handle multiple <think> tags", () => {
const message = "<think>first</think> and <think>second</think>";
const expected = "`<think>`first`</think>` and `<think>`second`</think>`";
expect(preprocessChatMessage(message)).toBe(expected);
});
it("should clean up tables when present", () => {
const message = `<think>analyzing</think>
| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| | |`;
const result = preprocessChatMessage(message);
// Should replace think tags
expect(result).toContain("`<think>`analyzing`</think>`");
// Should remove empty table row
expect(result).not.toContain("| | |");
// Should keep good content
expect(result).toContain("| Cell 1 | Cell 2 |");
});
it("should handle messages without tables", () => {
const message = "<think>pondering</think> Just some regular text";
const expected = "`<think>`pondering`</think>` Just some regular text";
expect(preprocessChatMessage(message)).toBe(expected);
});
it("should handle messages without think tags", () => {
const message = `| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| | |`;
const result = preprocessChatMessage(message);
expect(result).not.toContain("| | |");
expect(result).toContain("| Cell 1 | Cell 2 |");
});
it("should handle empty messages", () => {
expect(preprocessChatMessage("")).toBe("");
expect(preprocessChatMessage(" ")).toBe(" ");
});
it("should handle complex nested scenarios", () => {
const message = `<think>Let me create a table</think>
| Name | Status | Notes |
|------|--------|-------|
| John | Active | Good |
| | | |
| Jane | Active | |
<think>Done</think>`;
const result = preprocessChatMessage(message);
// Think tags should be replaced
expect(result).toContain("`<think>`Let me create a table`</think>`");
expect(result).toContain("`<think>`Done`</think>`");
// Empty row should be removed
expect(result).not.toContain("| | | |");
// Partial row should be kept
expect(result).toContain("| Jane | Active | |");
});
});
});

View file

@ -0,0 +1,52 @@
/**
* Utility functions for processing markdown content, particularly tables
*/
/**
* Detects if the given text contains a markdown table
*/
export const isMarkdownTable = (text: string): boolean => {
if (!text?.trim()) return false;
// Single regex to detect markdown table with header separator
return /\|.*\|.*\n\s*\|[\s\-:]+\|/m.test(text);
};
/**
* Removes completely empty rows from markdown tables
*/
export const cleanupTableEmptyCells = (text: string): string => {
return text
.split("\n")
.filter((line) => {
const trimmed = line.trim();
// Keep non-table lines
if (!trimmed.includes("|")) return true;
// Keep separator rows (contain only |, -, :, spaces)
if (/^\|[\s\-:]+\|$/.test(trimmed)) return true;
// For data rows, check if any cell has content
const cells = trimmed.split("|").slice(1, -1); // Remove delimiter cells
return cells.some((cell) => cell.trim() !== "");
})
.join("\n");
};
/**
* Preprocesses chat messages by handling <think> tags and cleaning up tables
*/
export const preprocessChatMessage = (text: string): string => {
// Handle <think> tags
let processed = text
.replace(/<think>/g, "`<think>`")
.replace(/<\/think>/g, "`</think>`");
// Clean up tables if present
if (isMarkdownTable(processed)) {
processed = cleanupTableEmptyCells(processed);
}
return processed;
};