From e8ebf8f01101905bd9aec84aec94fd8854f8a030 Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 2 Jan 2026 21:46:36 +0000 Subject: Update display of LLM edit panel --- makima/frontend/src/components/SimpleMarkdown.tsx | 173 ++++++++++++++++++++++ makima/frontend/src/components/files/CliInput.tsx | 3 +- 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 makima/frontend/src/components/SimpleMarkdown.tsx (limited to 'makima/frontend/src/components') diff --git a/makima/frontend/src/components/SimpleMarkdown.tsx b/makima/frontend/src/components/SimpleMarkdown.tsx new file mode 100644 index 0000000..4f09644 --- /dev/null +++ b/makima/frontend/src/components/SimpleMarkdown.tsx @@ -0,0 +1,173 @@ +import { useMemo } from "react"; + +interface SimpleMarkdownProps { + content: string; + className?: string; +} + +/** + * A simplistic markdown renderer that handles: + * - Newlines (paragraphs) + * - Bold (**text**) + * - Inline code (`code`) + * - Code blocks (```code```) + * - Headers (# ## ###) + * - Lists (- item) + */ +export function SimpleMarkdown({ content, className = "" }: SimpleMarkdownProps) { + const rendered = useMemo(() => { + if (!content) return null; + + // Split by code blocks first to handle them separately + const parts = content.split(/(```[\s\S]*?```)/g); + + return parts.map((part, partIndex) => { + // Handle code blocks + if (part.startsWith("```") && part.endsWith("```")) { + const code = part.slice(3, -3).replace(/^\w+\n/, ""); // Remove language hint + return ( +
+            {code.trim()}
+          
+ ); + } + + // Split by newlines and process each line + const lines = part.split("\n"); + + return lines.map((line, lineIndex) => { + const key = `${partIndex}-${lineIndex}`; + + // Skip empty lines but add spacing + if (!line.trim()) { + return
; + } + + // Headers + if (line.startsWith("### ")) { + return ( +
+ {processInline(line.slice(4))} +
+ ); + } + if (line.startsWith("## ")) { + return ( +
+ {processInline(line.slice(3))} +
+ ); + } + if (line.startsWith("# ")) { + return ( +
+ {processInline(line.slice(2))} +
+ ); + } + + // List items + if (line.match(/^[-*]\s/)) { + return ( +
+ - + {processInline(line.slice(2))} +
+ ); + } + + // Numbered list items + if (line.match(/^\d+\.\s/)) { + const match = line.match(/^(\d+)\.\s(.*)$/); + if (match) { + return ( +
+ {match[1]}. + {processInline(match[2])} +
+ ); + } + } + + // Regular paragraph + return
{processInline(line)}
; + }); + }); + }, [content]); + + return
{rendered}
; +} + +/** + * Process inline markdown: bold, inline code + */ +function processInline(text: string): React.ReactNode { + if (!text) return null; + + // Split by inline code and bold patterns + const parts: React.ReactNode[] = []; + let remaining = text; + let keyIndex = 0; + + while (remaining.length > 0) { + // Check for inline code first + const codeMatch = remaining.match(/^(.*?)`([^`]+)`(.*)$/); + if (codeMatch) { + if (codeMatch[1]) { + parts.push(...processInlineBold(codeMatch[1], keyIndex++)); + } + parts.push( + + {codeMatch[2]} + + ); + remaining = codeMatch[3]; + continue; + } + + // No more inline code, process bold in remaining text + parts.push(...processInlineBold(remaining, keyIndex)); + break; + } + + return parts.length === 1 ? parts[0] : parts; +} + +/** + * Process bold text (**text**) + */ +function processInlineBold(text: string, startKey: number): React.ReactNode[] { + const parts: React.ReactNode[] = []; + let remaining = text; + let keyIndex = startKey; + + while (remaining.length > 0) { + const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/); + if (boldMatch) { + if (boldMatch[1]) { + parts.push({boldMatch[1]}); + } + parts.push( + + {boldMatch[2]} + + ); + remaining = boldMatch[3]; + continue; + } + + // No more bold patterns + if (remaining) { + parts.push({remaining}); + } + break; + } + + return parts; +} diff --git a/makima/frontend/src/components/files/CliInput.tsx b/makima/frontend/src/components/files/CliInput.tsx index 1dcc884..0ac840a 100644 --- a/makima/frontend/src/components/files/CliInput.tsx +++ b/makima/frontend/src/components/files/CliInput.tsx @@ -1,5 +1,6 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { chatWithFile, type BodyElement, type LlmModel } from "../../lib/api"; +import { SimpleMarkdown } from "../SimpleMarkdown"; interface CliInputProps { fileId: string; @@ -114,7 +115,7 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) { )} {msg.type === "assistant" && (
-
{msg.content}
+ {msg.toolCalls && msg.toolCalls.length > 0 && (
{msg.toolCalls.map((tc, i) => ( -- cgit v1.2.3