diff --git a/frontend/src/pages/integration/TeamAnalysisResultPage.tsx b/frontend/src/pages/integration/TeamAnalysisResultPage.tsx index d13796f..170c8f4 100644 --- a/frontend/src/pages/integration/TeamAnalysisResultPage.tsx +++ b/frontend/src/pages/integration/TeamAnalysisResultPage.tsx @@ -44,10 +44,10 @@ import { FiShare2, } from 'react-icons/fi' import { Link, useNavigate } from 'react-router-dom' -import MessageText from '../../components/slack/MessageText' import ErrorBoundary from '../../components/common/ErrorBoundary' import { useAnalysisData } from '../../hooks' import { ServiceResource } from '../../lib/integrationService' +import { renderPlainText, extractSectionContent, isObviouslyNotJson } from '../../utils/textRenderer' interface ChannelAnalysisListProps { title: string @@ -73,148 +73,6 @@ const ChannelAnalysisList: FC = ({ emptyMessage = 'No information available.', }) => { const navigate = useNavigate() - const renderPlainText = ( - text: string | unknown, - workspaceUuid: string | undefined - ) => { - const textStr = typeof text === 'string' ? text : String(text || '') - if (!textStr || textStr.trim().length === 0) { - return No content available - } - - let cleanedText = textStr - - if (/^\s*\{\s*\}\s*$/.test(cleanedText)) { - return No content available - } - - cleanedText = cleanedText.replace(/\\n/g, '\n') - - const isLikelyPlainText = - /^[A-Za-z]/.test(cleanedText.trim()) && - !cleanedText.includes('```json') && - !(cleanedText.trim().startsWith('{') && cleanedText.trim().endsWith('}')) - - if (isLikelyPlainText) { - return ( - - {cleanedText.split('\n').map((paragraph, index) => ( - - {paragraph.trim() ? ( - - ) : ( - - )} - - ))} - - ) - } - - if ( - cleanedText.includes('{') && - cleanedText.includes('}') && - cleanedText.includes('"') - ) { - try { - const contentMatch = cleanedText.match(/"[^"]+"\s*:\s*"([^"]*)"/) - if (contentMatch && contentMatch[1]) { - cleanedText = contentMatch[1].replace(/\\n/g, '\n') - } else { - cleanedText = cleanedText - .replace(/[{}"]/g, '') // Remove braces and quotes - .replace(/[\w_]+\s*:/g, '') // Remove field names - .replace(/,\s*/g, '\n') // Replace commas with newlines - .trim() - } - } catch (e) { - console.warn('Error cleaning text content:', e) - } - } - - const hasMarkdownHeaders = /^#+\s+.+$/m.test(cleanedText) - - return ( - - {cleanedText.split('\n').map((paragraph, index) => { - if (!paragraph.trim()) { - return - } - - if (hasMarkdownHeaders && /^(#+)\s+(.+)$/.test(paragraph)) { - const match = paragraph.match(/^(#+)\s+(.+)$/) - if (match) { - const level = match[1].length - const headerText = match[2] - - const isTabHeader = [ - 'Summary', - 'Topics', - 'Contributors', - 'Highlights', - ].some((tab) => - headerText.toLowerCase().includes(tab.toLowerCase()) - ) - - if (isTabHeader) { - return // Skip this header - } - - const size = level === 1 ? 'lg' : level === 2 ? 'md' : 'sm' - return ( - - {headerText} - - ) - } - } - - if ( - paragraph.trim().startsWith('- ') || - paragraph.trim().startsWith('* ') - ) { - return ( - - - • - - - - - - ) - } - - return ( - - - - ) - })} - - ) - } const filteredAnalyses = reportResult?.resource_analyses && @@ -490,153 +348,7 @@ Generated using Toban Contribution Viewer with ${modelUsed} }) } - /** - * Render plain text with proper formatting and support for markdown-like syntax - */ - const renderPlainText = (text: string | unknown, workspace_uuid: string) => { - const textStr = typeof text === 'string' ? text : String(text || '') - if (!textStr || textStr.trim().length === 0) { - return No content available - } - let cleanedText = textStr - - if (/^\s*\{\s*\}\s*$/.test(cleanedText)) { - return No content available - } - - cleanedText = cleanedText.replace(/\\n/g, '\n') - - const isLikelyPlainText = - /^[A-Za-z]/.test(cleanedText.trim()) && - !cleanedText.includes('```json') && - !(cleanedText.trim().startsWith('{') && cleanedText.trim().endsWith('}')) - - if (isLikelyPlainText) { - console.log('Content appears to be plain text, rendering directly') - return ( - - {cleanedText.split('\n').map((paragraph, index) => ( - - {paragraph.trim() ? ( - - ) : ( - - )} - - ))} - - ) - } - - if ( - cleanedText.includes('{') && - cleanedText.includes('}') && - cleanedText.includes('"') - ) { - try { - const contentMatch = cleanedText.match(/"[^"]+"\s*:\s*"([^"]*)"/) - if (contentMatch && contentMatch[1]) { - cleanedText = contentMatch[1].replace(/\\n/g, '\n') - } else { - cleanedText = cleanedText - .replace(/[{}"]/g, '') // Remove braces and quotes - .replace(/[\w_]+\s*:/g, '') // Remove field names - .replace(/,\s*/g, '\n') // Replace commas with newlines - .trim() - } - } catch (e) { - console.warn('Error cleaning text content:', e) - } - } - - const hasMarkdownHeaders = /^#+\s+.+$/m.test(cleanedText) - - return ( - - {cleanedText.split('\n').map((paragraph, index) => { - if (!paragraph.trim()) { - return - } - - if (hasMarkdownHeaders && /^(#+)\s+(.+)$/.test(paragraph)) { - const match = paragraph.match(/^(#+)\s+(.+)$/) - if (match) { - const level = match[1].length - const headerText = match[2] - - const isTabHeader = [ - 'Summary', - 'Topics', - 'Contributors', - 'Highlights', - ].some((tab) => - headerText.toLowerCase().includes(tab.toLowerCase()) - ) - - if (isTabHeader) { - return // Skip this header - } - - const size = level === 1 ? 'lg' : level === 2 ? 'md' : 'sm' - return ( - - {headerText} - - ) - } - } - - if ( - paragraph.trim().startsWith('- ') || - paragraph.trim().startsWith('* ') - ) { - return ( - - - • - - - - - - ) - } - - return ( - - - - ) - })} - - ) - } - - const isObviouslyNotJson = (str: string) => { - return !str.includes('{') && !str.includes('"') && !str.includes(':') - } const fixedAnalysis = analysis ? { @@ -667,14 +379,6 @@ Generated using Toban Contribution Viewer with ${modelUsed} fixedAnalysis.contributor_insights } - const extractSectionContent = (text: string, sectionName: string) => { - const regex = new RegExp( - `#+\\s*${sectionName}\\s*\\n([\\s\\S]*?)(?=#+\\s*|$)`, - 'i' - ) - const match = text.match(regex) - return match ? match[1].trim() : '' - } if (analysis && fixedAnalysis) { if ( diff --git a/frontend/src/utils/textRenderer.ts b/frontend/src/utils/textRenderer.ts deleted file mode 100644 index b195cdf..0000000 --- a/frontend/src/utils/textRenderer.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Utility functions for rendering text content - */ - -/** - * Renders plain text with proper formatting - */ -export const renderPlainText = (text: string): string => { - // Implementation will go here - return text -} diff --git a/frontend/src/utils/textRenderer.tsx b/frontend/src/utils/textRenderer.tsx new file mode 100644 index 0000000..64774aa --- /dev/null +++ b/frontend/src/utils/textRenderer.tsx @@ -0,0 +1,179 @@ +/** + * Utility functions for rendering text content + */ +import React from 'react' +import { Box, Heading, Text } from '@chakra-ui/react' +import MessageText from '../components/slack/MessageText' + +/** + * Check if a string is obviously not in JSON format + * @param str The string to check + * @returns True if the string is obviously not JSON + */ +export const isObviouslyNotJson = (str: string): boolean => { + return !str.includes('{') && !str.includes('"') && !str.includes(':') +} + +/** + * Extract content for a specific section from markdown-formatted text + * @param text The markdown text to extract from + * @param sectionName The name of the section to extract + * @returns The extracted section content + */ +export const extractSectionContent = (text: string, sectionName: string): string => { + const regex = new RegExp( + `#+\\s*${sectionName}\\s*\\n([\\s\\S]*?)(?=#+\\s*|$)`, + 'i' + ) + const match = text.match(regex) + return match ? match[1].trim() : '' +} + +/** + * Render plain text with proper formatting and support for markdown-like syntax + * @param text The text to render + * @param workspaceUuid The workspace UUID for resolving mentions + * @returns Rendered React elements + */ +export const renderPlainText = ( + text: string | unknown, + workspaceUuid: string | undefined +): React.ReactNode => { + const textStr = typeof text === 'string' ? text : String(text || '') + if (!textStr || textStr.trim().length === 0) { + return No content available + } + + let cleanedText = textStr + + if (/^\s*\{\s*\}\s*$/.test(cleanedText)) { + return No content available + } + + cleanedText = cleanedText.replace(/\\n/g, '\n') + + const isLikelyPlainText = + /^[A-Za-z]/.test(cleanedText.trim()) && + !cleanedText.includes('```json') && + !(cleanedText.trim().startsWith('{') && cleanedText.trim().endsWith('}')) + + if (isLikelyPlainText) { + return ( + + {cleanedText.split('\n').map((paragraph, index) => ( + + {paragraph.trim() ? ( + + ) : ( + + )} + + ))} + + ) + } + + if ( + cleanedText.includes('{') && + cleanedText.includes('}') && + cleanedText.includes('"') + ) { + try { + const contentMatch = cleanedText.match(/"[^"]+"\s*:\s*"([^"]*)"/) + if (contentMatch && contentMatch[1]) { + cleanedText = contentMatch[1].replace(/\\n/g, '\n') + } else { + cleanedText = cleanedText + .replace(/[{}"]/g, '') // Remove braces and quotes + .replace(/[\w_]+\s*:/g, '') // Remove field names + .replace(/,\s*/g, '\n') // Replace commas with newlines + .trim() + } + } catch (e) { + console.warn('Error cleaning text content:', e) + } + } + + const hasMarkdownHeaders = /^#+\s+.+$/m.test(cleanedText) + + return ( + + {cleanedText.split('\n').map((paragraph, index) => { + if (!paragraph.trim()) { + return + } + + if (hasMarkdownHeaders && /^(#+)\s+(.+)$/.test(paragraph)) { + const match = paragraph.match(/^(#+)\s+(.+)$/) + if (match) { + const level = match[1].length + const headerText = match[2] + + const isTabHeader = [ + 'Summary', + 'Topics', + 'Contributors', + 'Highlights', + ].some((tab) => + headerText.toLowerCase().includes(tab.toLowerCase()) + ) + + if (isTabHeader) { + return // Skip this header + } + + const size = level === 1 ? 'lg' : level === 2 ? 'md' : 'sm' + return ( + + {headerText} + + ) + } + } + + if ( + paragraph.trim().startsWith('- ') || + paragraph.trim().startsWith('* ') + ) { + return ( + + + • + + + + + + ) + } + + return ( + + + + ) + })} + + ) +}