@useatlas/react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +95 -0
  2. package/dist/chunk-2WFDP7G5.js +231 -0
  3. package/dist/chunk-2WFDP7G5.js.map +1 -0
  4. package/dist/chunk-44HBZYKP.js +224 -0
  5. package/dist/chunk-44HBZYKP.js.map +1 -0
  6. package/dist/chunk-5SEVKHS5.cjs +229 -0
  7. package/dist/chunk-5SEVKHS5.cjs.map +1 -0
  8. package/dist/chunk-UIRB6L36.cjs +249 -0
  9. package/dist/chunk-UIRB6L36.cjs.map +1 -0
  10. package/dist/hooks.cjs +251 -0
  11. package/dist/hooks.cjs.map +1 -0
  12. package/dist/hooks.d.cts +132 -0
  13. package/dist/hooks.d.ts +132 -0
  14. package/dist/hooks.js +237 -0
  15. package/dist/hooks.js.map +1 -0
  16. package/dist/index.cjs +2976 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +69 -0
  19. package/dist/index.d.ts +69 -0
  20. package/dist/index.js +2926 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/result-chart-NFAJ4IQ5.js +398 -0
  23. package/dist/result-chart-NFAJ4IQ5.js.map +1 -0
  24. package/dist/result-chart-YLCKBNV4.cjs +400 -0
  25. package/dist/result-chart-YLCKBNV4.cjs.map +1 -0
  26. package/dist/styles.css +59 -0
  27. package/dist/use-dark-mode-rFxawUv1.d.cts +123 -0
  28. package/dist/use-dark-mode-rFxawUv1.d.ts +123 -0
  29. package/dist/widget.css +2 -0
  30. package/dist/widget.js +445 -0
  31. package/package.json +113 -0
  32. package/src/components/__tests__/tool-renderers.test.tsx +239 -0
  33. package/src/components/actions/action-approval-card.tsx +296 -0
  34. package/src/components/actions/action-status-badge.tsx +50 -0
  35. package/src/components/admin/change-password-dialog.tsx +128 -0
  36. package/src/components/atlas-chat.tsx +656 -0
  37. package/src/components/chart/chart-detection.ts +318 -0
  38. package/src/components/chart/result-chart.tsx +590 -0
  39. package/src/components/chat/api-key-bar.tsx +66 -0
  40. package/src/components/chat/copy-button.tsx +25 -0
  41. package/src/components/chat/data-table.tsx +104 -0
  42. package/src/components/chat/error-banner.tsx +32 -0
  43. package/src/components/chat/explore-card.tsx +41 -0
  44. package/src/components/chat/follow-up-chips.tsx +29 -0
  45. package/src/components/chat/loading-card.tsx +10 -0
  46. package/src/components/chat/managed-auth-card.tsx +116 -0
  47. package/src/components/chat/markdown.tsx +146 -0
  48. package/src/components/chat/python-result-card.tsx +245 -0
  49. package/src/components/chat/sql-block.tsx +54 -0
  50. package/src/components/chat/sql-result-card.tsx +163 -0
  51. package/src/components/chat/starter-prompts.ts +6 -0
  52. package/src/components/chat/tool-part.tsx +106 -0
  53. package/src/components/chat/typing-indicator.tsx +22 -0
  54. package/src/components/conversations/conversation-item.tsx +135 -0
  55. package/src/components/conversations/conversation-list.tsx +69 -0
  56. package/src/components/conversations/conversation-sidebar.tsx +113 -0
  57. package/src/components/conversations/delete-confirmation.tsx +27 -0
  58. package/src/components/schema-explorer/schema-explorer.tsx +517 -0
  59. package/src/components/ui/alert-dialog.tsx +196 -0
  60. package/src/components/ui/badge.tsx +48 -0
  61. package/src/components/ui/button.tsx +64 -0
  62. package/src/components/ui/card.tsx +92 -0
  63. package/src/components/ui/dialog.tsx +158 -0
  64. package/src/components/ui/dropdown-menu.tsx +257 -0
  65. package/src/components/ui/input.tsx +21 -0
  66. package/src/components/ui/label.tsx +24 -0
  67. package/src/components/ui/scroll-area.tsx +62 -0
  68. package/src/components/ui/separator.tsx +28 -0
  69. package/src/components/ui/sheet.tsx +143 -0
  70. package/src/components/ui/table.tsx +116 -0
  71. package/src/components/ui/toggle-group.tsx +83 -0
  72. package/src/components/ui/toggle.tsx +47 -0
  73. package/src/context.tsx +85 -0
  74. package/src/env.d.ts +9 -0
  75. package/src/hooks/__tests__/provider.test.tsx +83 -0
  76. package/src/hooks/__tests__/use-atlas-auth.test.tsx +283 -0
  77. package/src/hooks/__tests__/use-atlas-chat.test.tsx +157 -0
  78. package/src/hooks/__tests__/use-atlas-conversations.test.tsx +159 -0
  79. package/src/hooks/__tests__/use-atlas-theme.test.tsx +56 -0
  80. package/src/hooks/index.ts +47 -0
  81. package/src/hooks/provider.tsx +77 -0
  82. package/src/hooks/theme-init-script.ts +17 -0
  83. package/src/hooks/use-atlas-auth.ts +131 -0
  84. package/src/hooks/use-atlas-chat.ts +102 -0
  85. package/src/hooks/use-atlas-conversations.ts +61 -0
  86. package/src/hooks/use-atlas-theme.ts +34 -0
  87. package/src/hooks/use-conversations.ts +189 -0
  88. package/src/hooks/use-dark-mode.ts +150 -0
  89. package/src/index.ts +36 -0
  90. package/src/lib/action-types.ts +11 -0
  91. package/src/lib/helpers.ts +198 -0
  92. package/src/lib/tool-renderer-types.ts +76 -0
  93. package/src/lib/types.ts +29 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/styles.css +59 -0
  96. package/src/test-setup.ts +55 -0
  97. package/src/widget-entry.ts +20 -0
  98. package/src/widget.css +12 -0
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { formatCell } from "../../lib/helpers";
5
+
6
+ export function DataTable({
7
+ columns,
8
+ rows,
9
+ maxRows = 10,
10
+ }: {
11
+ columns: string[];
12
+ rows: (Record<string, unknown> | unknown[])[];
13
+ maxRows?: number;
14
+ }) {
15
+ const [sortCol, setSortCol] = useState<number | null>(null);
16
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
17
+
18
+ const hasMore = rows.length > maxRows;
19
+
20
+ const cell = (row: Record<string, unknown> | unknown[], colIdx: number): unknown => {
21
+ if (Array.isArray(row)) return row[colIdx];
22
+ return (row as Record<string, unknown>)[columns[colIdx]];
23
+ };
24
+
25
+ const handleSort = (colIdx: number) => {
26
+ if (sortCol === colIdx) {
27
+ if (sortDir === "asc") {
28
+ setSortDir("desc");
29
+ } else {
30
+ setSortCol(null);
31
+ setSortDir("asc");
32
+ }
33
+ } else {
34
+ setSortCol(colIdx);
35
+ setSortDir("asc");
36
+ }
37
+ };
38
+
39
+ const sorted = sortCol !== null
40
+ ? [...rows].sort((a, b) => {
41
+ const av = cell(a, sortCol);
42
+ const bv = cell(b, sortCol);
43
+ if (av == null && bv == null) return 0;
44
+ if (av == null) return 1;
45
+ if (bv == null) return -1;
46
+ const aStr = String(av).trim();
47
+ const bStr = String(bv).trim();
48
+ if (aStr === "" && bStr === "") return 0;
49
+ if (aStr === "") return 1;
50
+ if (bStr === "") return -1;
51
+ const an = Number(aStr), bn = Number(bStr);
52
+ if (!isNaN(an) && !isNaN(bn)) {
53
+ return sortDir === "asc" ? an - bn : bn - an;
54
+ }
55
+ const cmp = aStr.localeCompare(bStr);
56
+ return sortDir === "asc" ? cmp : -cmp;
57
+ })
58
+ : rows;
59
+ const display = sorted.slice(0, maxRows);
60
+
61
+ return (
62
+ <div className="relative rounded-lg border border-zinc-200 dark:border-zinc-700">
63
+ <div className="overflow-x-auto [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-zinc-300 dark:[&::-webkit-scrollbar-thumb]:bg-zinc-600">
64
+ <table className="min-w-full text-xs">
65
+ <thead>
66
+ <tr className="border-b border-zinc-200 bg-zinc-100/80 dark:border-zinc-700 dark:bg-zinc-800/80">
67
+ {columns.map((col, i) => (
68
+ <th
69
+ key={i}
70
+ onClick={() => handleSort(i)}
71
+ className="group cursor-pointer select-none whitespace-nowrap px-3 py-2 text-left font-medium text-zinc-500 transition-colors hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
72
+ >
73
+ {col}
74
+ {sortCol === i
75
+ ? sortDir === "asc" ? " \u25B2" : " \u25BC"
76
+ : ""}
77
+ </th>
78
+ ))}
79
+ </tr>
80
+ </thead>
81
+ <tbody>
82
+ {display.map((row, i) => (
83
+ <tr
84
+ key={i}
85
+ className={i % 2 === 0 ? "bg-zinc-100/60 dark:bg-zinc-900/60" : "bg-zinc-50/30 dark:bg-zinc-900/30"}
86
+ >
87
+ {columns.map((_, j) => (
88
+ <td key={j} className="whitespace-nowrap px-3 py-1.5 text-zinc-700 dark:text-zinc-300">
89
+ {formatCell(cell(row, j))}
90
+ </td>
91
+ ))}
92
+ </tr>
93
+ ))}
94
+ </tbody>
95
+ </table>
96
+ </div>
97
+ {hasMore && (
98
+ <div className="border-t border-zinc-200 px-3 py-1.5 text-xs text-zinc-500 dark:border-zinc-700">
99
+ Showing {maxRows} of {rows.length} rows
100
+ </div>
101
+ )}
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import { parseChatError, type AuthMode } from "../../lib/types";
5
+
6
+ export function ErrorBanner({ error, authMode }: { error: Error; authMode: AuthMode }) {
7
+ const info = useMemo(() => parseChatError(error, authMode), [error, authMode]);
8
+ const [countdown, setCountdown] = useState(info.retryAfterSeconds ?? 0);
9
+
10
+ useEffect(() => {
11
+ if (!info.retryAfterSeconds) return;
12
+ setCountdown(info.retryAfterSeconds);
13
+ const interval = setInterval(() => {
14
+ setCountdown((prev) => {
15
+ if (prev <= 1) { clearInterval(interval); return 0; }
16
+ return prev - 1;
17
+ });
18
+ }, 1000);
19
+ return () => clearInterval(interval);
20
+ }, [info.retryAfterSeconds]);
21
+
22
+ const detail = info.retryAfterSeconds && countdown > 0
23
+ ? `Try again in ${countdown} second${countdown !== 1 ? "s" : ""}.`
24
+ : info.detail;
25
+
26
+ return (
27
+ <div className="mb-2 rounded-lg border border-red-300 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/20 dark:text-red-400 px-4 py-3 text-sm">
28
+ <p className="font-medium">{info.title}</p>
29
+ {detail && <p className="mt-1 text-xs opacity-80">{detail}</p>}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { getToolArgs, getToolResult, isToolComplete } from "../../lib/helpers";
5
+
6
+ export function ExploreCard({ part }: { part: unknown }) {
7
+ const args = getToolArgs(part);
8
+ const result = getToolResult(part);
9
+ const done = isToolComplete(part);
10
+ const [open, setOpen] = useState(false);
11
+
12
+ return (
13
+ <div className="my-2 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
14
+ <button
15
+ onClick={() => done && setOpen(!open)}
16
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60"
17
+ >
18
+ <span className="font-mono text-green-400">$</span>
19
+ <span className="flex-1 truncate font-mono text-zinc-700 dark:text-zinc-300">
20
+ {String(args.command ?? "")}
21
+ </span>
22
+ {done ? (
23
+ <span className="text-zinc-400 dark:text-zinc-600">{open ? "\u25BE" : "\u25B8"}</span>
24
+ ) : (
25
+ <span className="animate-pulse text-zinc-500">running...</span>
26
+ )}
27
+ </button>
28
+ {open && done && (
29
+ <div className="border-t border-zinc-100 bg-white px-3 py-2 dark:border-zinc-800 dark:bg-zinc-950">
30
+ <pre className="max-h-60 overflow-auto whitespace-pre-wrap font-mono text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
31
+ {result != null
32
+ ? typeof result === "string"
33
+ ? result
34
+ : JSON.stringify(result, null, 2)
35
+ : "(no output received)"}
36
+ </pre>
37
+ </div>
38
+ )}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+
5
+ export function FollowUpChips({
6
+ suggestions,
7
+ onSelect,
8
+ }: {
9
+ suggestions: string[];
10
+ onSelect: (text: string) => void;
11
+ }) {
12
+ if (suggestions.length === 0) return null;
13
+
14
+ return (
15
+ <div className="flex flex-wrap gap-2 pt-1">
16
+ {suggestions.map((s, i) => (
17
+ <Button
18
+ key={`${i}-${s}`}
19
+ variant="outline"
20
+ size="sm"
21
+ className="h-auto rounded-full px-3 py-1.5 text-xs font-normal text-zinc-600 dark:text-zinc-400"
22
+ onClick={() => onSelect(s)}
23
+ >
24
+ {s}
25
+ </Button>
26
+ ))}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export function LoadingCard({ label }: { label: string }) {
4
+ return (
5
+ <div className="my-2 flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">
6
+ <span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-zinc-300 border-t-zinc-600 dark:border-zinc-600 dark:border-t-zinc-300" />
7
+ {label}
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useAtlasConfig } from "../../context";
5
+
6
+ export function ManagedAuthCard() {
7
+ const { authClient } = useAtlasConfig();
8
+ const [view, setView] = useState<"login" | "signup">("login");
9
+ const [email, setEmail] = useState("");
10
+ const [password, setPassword] = useState("");
11
+ const [name, setName] = useState("");
12
+ const [error, setError] = useState("");
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ async function handleLogin(e: React.FormEvent) {
16
+ e.preventDefault();
17
+ setError("");
18
+ setLoading(true);
19
+ try {
20
+ const res = await authClient.signIn.email({ email, password });
21
+ if (res.error) setError(res.error.message ?? "Sign in failed");
22
+ } catch (err) {
23
+ console.error("Sign in error:", err);
24
+ setError(err instanceof TypeError ? "Unable to reach the server" : "Sign in failed");
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ }
29
+
30
+ async function handleSignup(e: React.FormEvent) {
31
+ e.preventDefault();
32
+ setError("");
33
+ setLoading(true);
34
+ try {
35
+ const res = await authClient.signUp.email({ email, password, name: name || email.split("@")[0] });
36
+ if (res.error) setError(res.error.message ?? "Sign up failed");
37
+ } catch (err) {
38
+ console.error("Sign up error:", err);
39
+ setError(err instanceof TypeError ? "Unable to reach the server" : "Sign up failed");
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ }
44
+
45
+ return (
46
+ <div className="flex h-full items-center justify-center">
47
+ <div className="w-full max-w-sm space-y-4 rounded-lg border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900">
48
+ <div className="text-center">
49
+ <h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
50
+ {view === "login" ? "Sign in to Atlas" : "Create an account"}
51
+ </h2>
52
+ <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
53
+ {view === "login" ? "Enter your credentials to continue" : "Set up your Atlas account"}
54
+ </p>
55
+ </div>
56
+
57
+ <form onSubmit={view === "login" ? handleLogin : handleSignup} className="space-y-3">
58
+ {view === "signup" && (
59
+ <input
60
+ type="text"
61
+ value={name}
62
+ onChange={(e) => setName(e.target.value)}
63
+ placeholder="Name (optional)"
64
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
65
+ />
66
+ )}
67
+ <input
68
+ type="email"
69
+ value={email}
70
+ onChange={(e) => setEmail(e.target.value)}
71
+ placeholder="Email"
72
+ required
73
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
74
+ />
75
+ <input
76
+ type="password"
77
+ value={password}
78
+ onChange={(e) => setPassword(e.target.value)}
79
+ placeholder="Password"
80
+ required
81
+ minLength={8}
82
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
83
+ />
84
+ {error && (
85
+ <p className="text-xs text-red-600 dark:text-red-400">{error}</p>
86
+ )}
87
+ <button
88
+ type="submit"
89
+ disabled={loading}
90
+ className="w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40"
91
+ >
92
+ {loading ? "..." : view === "login" ? "Sign in" : "Create account"}
93
+ </button>
94
+ </form>
95
+
96
+ <p className="text-center text-xs text-zinc-500 dark:text-zinc-400">
97
+ {view === "login" ? (
98
+ <>
99
+ No account?{" "}
100
+ <button onClick={() => { setView("signup"); setError(""); }} className="text-blue-600 hover:underline dark:text-blue-400">
101
+ Create one
102
+ </button>
103
+ </>
104
+ ) : (
105
+ <>
106
+ Already have an account?{" "}
107
+ <button onClick={() => { setView("login"); setError(""); }} className="text-blue-600 hover:underline dark:text-blue-400">
108
+ Sign in
109
+ </button>
110
+ </>
111
+ )}
112
+ </p>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { memo, useContext, useState, useEffect, type ReactNode } from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { DarkModeContext } from "../../hooks/use-dark-mode";
7
+
8
+ /* ------------------------------------------------------------------ */
9
+ /* Lazy-loaded syntax highlighter (~300KB) */
10
+ /* ------------------------------------------------------------------ */
11
+
12
+ type SyntaxHighlighterModule = typeof import("react-syntax-highlighter");
13
+ type StyleModule = typeof import("react-syntax-highlighter/dist/esm/styles/prism");
14
+
15
+ let _highlighterCache: { Prism: SyntaxHighlighterModule["Prism"]; oneDark: StyleModule["oneDark"]; oneLight: StyleModule["oneLight"] } | null = null;
16
+ let _loadFailed = false;
17
+
18
+ function LazyCodeBlock({ language, dark, children }: { language: string; dark: boolean; children: string }) {
19
+ const [mod, setMod] = useState(_highlighterCache);
20
+
21
+ useEffect(() => {
22
+ if (_highlighterCache || _loadFailed) return;
23
+ Promise.all([
24
+ import("react-syntax-highlighter"),
25
+ import("react-syntax-highlighter/dist/esm/styles/prism"),
26
+ ]).then(([sh, styles]) => {
27
+ _highlighterCache = { Prism: sh.Prism, oneDark: styles.oneDark, oneLight: styles.oneLight };
28
+ setMod(_highlighterCache);
29
+ }).catch((err) => {
30
+ console.warn("Syntax highlighter failed to load — code blocks will use plain text:", err);
31
+ _loadFailed = true;
32
+ });
33
+ }, []);
34
+
35
+ if (!mod) {
36
+ return (
37
+ <pre className="my-2 overflow-x-auto rounded-lg bg-zinc-100 p-3 text-xs dark:bg-zinc-800">
38
+ <code>{children}</code>
39
+ </pre>
40
+ );
41
+ }
42
+
43
+ return (
44
+ <mod.Prism
45
+ language={language}
46
+ style={dark ? mod.oneDark : mod.oneLight}
47
+ customStyle={CODE_BLOCK_STYLE}
48
+ >
49
+ {children}
50
+ </mod.Prism>
51
+ );
52
+ }
53
+
54
+ const CODE_BLOCK_STYLE = {
55
+ margin: "0.5rem 0",
56
+ borderRadius: "0.5rem",
57
+ fontSize: "0.75rem",
58
+ } as const;
59
+
60
+ /* ------------------------------------------------------------------ */
61
+ /* Static markdown renderers — hoisted outside component */
62
+ /* ------------------------------------------------------------------ */
63
+
64
+ const mdComponents = {
65
+ p: ({ children }: { children?: ReactNode }) => (
66
+ <p className="mb-3 leading-relaxed last:mb-0">{children}</p>
67
+ ),
68
+ h1: ({ children }: { children?: ReactNode }) => (
69
+ <h1 className="mb-2 mt-4 text-lg font-bold first:mt-0">{children}</h1>
70
+ ),
71
+ h2: ({ children }: { children?: ReactNode }) => (
72
+ <h2 className="mb-2 mt-3 text-base font-semibold first:mt-0">{children}</h2>
73
+ ),
74
+ h3: ({ children }: { children?: ReactNode }) => (
75
+ <h3 className="mb-1 mt-2 font-semibold first:mt-0">{children}</h3>
76
+ ),
77
+ ul: ({ children }: { children?: ReactNode }) => (
78
+ <ul className="mb-3 list-disc space-y-1 pl-4">{children}</ul>
79
+ ),
80
+ ol: ({ children }: { children?: ReactNode }) => (
81
+ <ol className="mb-3 list-decimal space-y-1 pl-4">{children}</ol>
82
+ ),
83
+ strong: ({ children }: { children?: ReactNode }) => (
84
+ <strong className="font-semibold text-zinc-900 dark:text-zinc-50">{children}</strong>
85
+ ),
86
+ blockquote: ({ children }: { children?: ReactNode }) => (
87
+ <blockquote className="my-2 border-l-2 border-zinc-300 pl-3 text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
88
+ {children}
89
+ </blockquote>
90
+ ),
91
+ table: ({ children }: { children?: ReactNode }) => (
92
+ <div className="my-3 overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700">
93
+ <table className="min-w-full text-sm">{children}</table>
94
+ </div>
95
+ ),
96
+ thead: ({ children }: { children?: ReactNode }) => (
97
+ <thead className="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/50">
98
+ {children}
99
+ </thead>
100
+ ),
101
+ tbody: ({ children }: { children?: ReactNode }) => (
102
+ <tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">{children}</tbody>
103
+ ),
104
+ tr: ({ children }: { children?: ReactNode }) => <tr>{children}</tr>,
105
+ th: ({ children }: { children?: ReactNode }) => (
106
+ <th className="px-3 py-2 text-left text-xs font-medium text-zinc-600 dark:text-zinc-300">
107
+ {children}
108
+ </th>
109
+ ),
110
+ td: ({ children }: { children?: ReactNode }) => (
111
+ <td className="px-3 py-2 text-zinc-700 dark:text-zinc-300">{children}</td>
112
+ ),
113
+ pre: ({ children }: { children?: ReactNode }) => <>{children}</>,
114
+ };
115
+
116
+ export const Markdown = memo(function Markdown({ content }: { content: string }) {
117
+ const dark = useContext(DarkModeContext);
118
+ return (
119
+ <ReactMarkdown
120
+ remarkPlugins={[remarkGfm]}
121
+ components={{
122
+ ...mdComponents,
123
+ code({ className, children, ...props }) {
124
+ const match = /language-(\w+)/.exec(className || "");
125
+ if (match) {
126
+ return (
127
+ <LazyCodeBlock language={match[1]} dark={dark}>
128
+ {String(children).replace(/\n$/, "")}
129
+ </LazyCodeBlock>
130
+ );
131
+ }
132
+ return (
133
+ <code
134
+ className="rounded bg-zinc-200/50 px-1.5 py-0.5 text-xs text-zinc-800 dark:bg-zinc-700/50 dark:text-zinc-200"
135
+ {...props}
136
+ >
137
+ {children}
138
+ </code>
139
+ );
140
+ },
141
+ }}
142
+ >
143
+ {content}
144
+ </ReactMarkdown>
145
+ );
146
+ });