Files
socweb/langgraph/src/components/MessageBubble.tsx
T
gongzhiyongandClaude Opus 4.6 251c6586f4
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 27s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 1m20s
feat: Phase 1 — ReAct loop + Markdown rendering + Docker deployment
Backend:
- Enterprise Agent refactored from single-round to ReAct multi-turn loop
- New agent.ts (LLM decision node) + tool-executor.ts (tool execution + Gen-UI)
- tool-defs.ts extracted for shared tool schemas
- MAX_ITERATIONS=6 safeguard against infinite loops

Frontend:
- MessageBubble: Markdown + code highlighting + LaTeX + tables
- ThemeToggle: light/dark/system theme cycling
- chart-result Gen-UI card: recharts bar/line/pie/area charts

Infrastructure:
- Docker Compose (lightweight): only LangGraph + Frontend, Azure cloud for PG/Redis/Blob
- Dockerfiles for dev (hot reload) and prod
- Makefile with dev/prod/down/logs commands
- Updated CLAUDE.md and agent definitions for LangGraph.js architecture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:29:56 +08:00

221 lines
5.8 KiB
TypeScript

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import {
oneDark,
oneLight,
} from "react-syntax-highlighter/dist/esm/styles/prism";
import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { cn } from "@/lib/utils";
import "katex/dist/katex.min.css";
interface MessageBubbleProps {
content: string;
role: "human" | "ai";
}
function CodeBlock({
language,
children,
}: {
language: string;
children: string;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(children).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
// Detect dark mode via document class
const isDark =
typeof document !== "undefined" &&
document.documentElement.classList.contains("dark");
return (
<div className="relative group my-2 rounded-lg overflow-hidden border border-border">
{/* Language label + copy button */}
<div className="flex items-center justify-between px-3 py-1.5 bg-muted border-b border-border">
<span className="text-xs font-mono text-muted-foreground">
{language || "text"}
</span>
<button
type="button"
onClick={handleCopy}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{copied ? (
<Check className="size-3.5 text-green-500" />
) : (
<Copy className="size-3.5" />
)}
{copied ? "已复制" : "复制"}
</button>
</div>
<SyntaxHighlighter
language={language || "text"}
style={isDark ? oneDark : oneLight}
customStyle={{
margin: 0,
borderRadius: 0,
fontSize: "0.75rem",
background: "transparent",
}}
PreTag="div"
>
{children}
</SyntaxHighlighter>
</div>
);
}
export default function MessageBubble({ content }: MessageBubbleProps) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Code blocks
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className ?? "");
const isInline = !match && !className;
const codeStr = String(children).replace(/\n$/, "");
if (isInline) {
return (
<code
className="bg-muted px-1 rounded text-xs font-mono"
{...props}
>
{children}
</code>
);
}
return (
<CodeBlock language={match ? match[1] : ""}>{codeStr}</CodeBlock>
);
},
// Tables
table({ children }) {
return (
<div className="overflow-x-auto my-2">
<table className="min-w-full text-xs border-collapse border border-border">
{children}
</table>
</div>
);
},
th({ children }) {
return (
<th className="border border-border px-3 py-1.5 bg-muted text-left font-medium text-foreground">
{children}
</th>
);
},
td({ children }) {
return (
<td className="border border-border px-3 py-1.5 text-foreground">
{children}
</td>
);
},
// Headings
h1({ children }) {
return (
<h1 className="text-base font-bold mt-3 mb-1 text-foreground">
{children}
</h1>
);
},
h2({ children }) {
return (
<h2 className="text-sm font-semibold mt-3 mb-1 text-foreground">
{children}
</h2>
);
},
h3({ children }) {
return (
<h3 className="text-sm font-medium mt-2 mb-1 text-foreground">
{children}
</h3>
);
},
// Lists
ul({ children }) {
return (
<ul className="list-disc list-inside space-y-0.5 my-1 text-foreground">
{children}
</ul>
);
},
ol({ children }) {
return (
<ol className="list-decimal list-inside space-y-0.5 my-1 text-foreground">
{children}
</ol>
);
},
// Paragraphs
p({ children }) {
return <p className="my-1 leading-relaxed text-foreground">{children}</p>;
},
// Links
a({ href, children }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn(
"underline underline-offset-2 text-foreground",
"hover:opacity-80 transition-opacity",
)}
>
{children}
</a>
);
},
// Blockquote
blockquote({ children }) {
return (
<blockquote className="border-l-2 border-border pl-3 my-2 text-muted-foreground italic">
{children}
</blockquote>
);
},
// Horizontal rule
hr() {
return <hr className="my-3 border-border" />;
},
// Strong / em
strong({ children }) {
return (
<strong className="font-semibold text-foreground">{children}</strong>
);
},
em({ children }) {
return <em className="italic text-foreground">{children}</em>;
},
}}
>
{content}
</ReactMarkdown>
);
}