summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/SimpleMarkdown.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-02 21:46:36 +0000
committersoryu <soryu@soryu.co>2026-01-02 21:46:36 +0000
commite8ebf8f01101905bd9aec84aec94fd8854f8a030 (patch)
treef079b9f0a8b814f9a3bda6ac7f779b0ddb4b431a /makima/frontend/src/components/SimpleMarkdown.tsx
parent062ae51396e88a8998bb30e78381275d77e7c90e (diff)
downloadsoryu-e8ebf8f01101905bd9aec84aec94fd8854f8a030.tar.gz
soryu-e8ebf8f01101905bd9aec84aec94fd8854f8a030.zip
Update display of LLM edit panel
Diffstat (limited to 'makima/frontend/src/components/SimpleMarkdown.tsx')
-rw-r--r--makima/frontend/src/components/SimpleMarkdown.tsx173
1 files changed, 173 insertions, 0 deletions
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 (
+ <pre
+ key={partIndex}
+ className="bg-[#0a1525] border border-[rgba(117,170,252,0.2)] p-2 my-1 overflow-x-auto text-[#9bc3ff] text-[10px]"
+ >
+ <code>{code.trim()}</code>
+ </pre>
+ );
+ }
+
+ // 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 <div key={key} className="h-1" />;
+ }
+
+ // Headers
+ if (line.startsWith("### ")) {
+ return (
+ <div key={key} className="font-bold text-white/90 mt-2 mb-1">
+ {processInline(line.slice(4))}
+ </div>
+ );
+ }
+ if (line.startsWith("## ")) {
+ return (
+ <div key={key} className="font-bold text-white mt-2 mb-1">
+ {processInline(line.slice(3))}
+ </div>
+ );
+ }
+ if (line.startsWith("# ")) {
+ return (
+ <div key={key} className="font-bold text-white text-sm mt-2 mb-1">
+ {processInline(line.slice(2))}
+ </div>
+ );
+ }
+
+ // List items
+ if (line.match(/^[-*]\s/)) {
+ return (
+ <div key={key} className="flex gap-1">
+ <span className="text-[#555]">-</span>
+ <span>{processInline(line.slice(2))}</span>
+ </div>
+ );
+ }
+
+ // Numbered list items
+ if (line.match(/^\d+\.\s/)) {
+ const match = line.match(/^(\d+)\.\s(.*)$/);
+ if (match) {
+ return (
+ <div key={key} className="flex gap-1">
+ <span className="text-[#555]">{match[1]}.</span>
+ <span>{processInline(match[2])}</span>
+ </div>
+ );
+ }
+ }
+
+ // Regular paragraph
+ return <div key={key}>{processInline(line)}</div>;
+ });
+ });
+ }, [content]);
+
+ return <div className={`space-y-0.5 ${className}`}>{rendered}</div>;
+}
+
+/**
+ * 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(
+ <code
+ key={`code-${keyIndex++}`}
+ className="bg-[#0a1525] px-1 py-0.5 text-[#9bc3ff] text-[10px]"
+ >
+ {codeMatch[2]}
+ </code>
+ );
+ 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(<span key={`text-${keyIndex++}`}>{boldMatch[1]}</span>);
+ }
+ parts.push(
+ <strong key={`bold-${keyIndex++}`} className="text-white/90">
+ {boldMatch[2]}
+ </strong>
+ );
+ remaining = boldMatch[3];
+ continue;
+ }
+
+ // No more bold patterns
+ if (remaining) {
+ parts.push(<span key={`text-${keyIndex++}`}>{remaining}</span>);
+ }
+ break;
+ }
+
+ return parts;
+}