@langgraph-js/ui 1.6.0 → 1.7.0
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.
- package/dist/assets/{index-7vem5Peg.css → index-C0dczJ0v.css} +1 -1
- package/dist/assets/index-JlVlMqZ-.js +248 -0
- package/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/chat/Chat.tsx +7 -6
- package/src/chat/components/JsonEditorPopup.tsx +158 -22
- package/src/chat/components/MessageBox.tsx +2 -3
- package/src/chat/components/MessageTool.tsx +37 -7
- package/src/chat/index.css +4 -0
- package/src/index.ts +1 -0
- package/dist/assets/index-CZ6k2dGe.js +0 -235
package/dist/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
padding: 0;
|
|
13
13
|
}
|
|
14
14
|
</style>
|
|
15
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-JlVlMqZ-.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C0dczJ0v.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="root" class="h-screen w-screen"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langgraph-js/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"minisearch": "^7.1.2",
|
|
26
26
|
"motion": "^12.16.0",
|
|
27
27
|
"nanostores": "^1.0.1",
|
|
28
|
+
"prism-react-renderer": "^2.4.1",
|
|
28
29
|
"react": "^19.0.0",
|
|
29
30
|
"react-dom": "^19.0.0",
|
|
30
31
|
"react-markdown": "^10.1.0",
|
|
31
32
|
"remark-gfm": "^4.0.1",
|
|
32
33
|
"unocss": "^66.1.3",
|
|
33
34
|
"vite": "^6.2.0",
|
|
34
|
-
"@langgraph-js/sdk": "1.
|
|
35
|
+
"@langgraph-js/sdk": "1.10.4"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/react": "^19.0.10",
|
package/src/chat/Chat.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { History, Network, LogOut, FileJson, Code } from "lucide-react";
|
|
|
14
14
|
import { ArtifactViewer } from "../artifacts/ArtifactViewer";
|
|
15
15
|
import "github-markdown-css/github-markdown.css";
|
|
16
16
|
import { ArtifactsProvider, useArtifacts } from "../artifacts/ArtifactsContext";
|
|
17
|
+
import "./index.css";
|
|
17
18
|
|
|
18
19
|
const ChatMessages: React.FC = () => {
|
|
19
20
|
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse, isFELocking } = useChat();
|
|
@@ -28,12 +29,11 @@ const ChatMessages: React.FC = () => {
|
|
|
28
29
|
const scrollPosition = container.scrollTop + container.clientHeight;
|
|
29
30
|
const scrollHeight = container.scrollHeight;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
return scrollHeight - scrollPosition <= container.clientHeight * 0.3;
|
|
32
|
+
return scrollHeight - scrollPosition <= 50;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const scrollToBottom = () => {
|
|
36
|
-
messagesEndRef.current?.scrollIntoView({ behavior: "
|
|
36
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
useEffect(() => {
|
|
@@ -66,7 +66,7 @@ const ChatMessages: React.FC = () => {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
const ChatInput: React.FC = () => {
|
|
69
|
-
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client } = useChat();
|
|
69
|
+
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client, currentChatId } = useChat();
|
|
70
70
|
const { extraParams } = useExtraParams();
|
|
71
71
|
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
|
72
72
|
|
|
@@ -148,8 +148,9 @@ const ChatInput: React.FC = () => {
|
|
|
148
148
|
{loading ? "中断" : "发送"}
|
|
149
149
|
</button>
|
|
150
150
|
</div>
|
|
151
|
-
<div className="flex border-b border-gray-200 mt-4">
|
|
151
|
+
<div className="flex border-b border-gray-200 mt-4 gap-2 justify-between">
|
|
152
152
|
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
153
|
+
<span className="text-sm text-gray-500">会话 ID: {currentChatId}</span>
|
|
153
154
|
</div>
|
|
154
155
|
</div>
|
|
155
156
|
);
|
|
@@ -162,7 +163,7 @@ const Chat: React.FC = () => {
|
|
|
162
163
|
const { showArtifact, setShowArtifact } = useArtifacts();
|
|
163
164
|
|
|
164
165
|
return (
|
|
165
|
-
<div className="flex h-full w-full overflow-hidden">
|
|
166
|
+
<div className="langgraph-chat-container flex h-full w-full overflow-hidden">
|
|
166
167
|
{showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
|
|
167
168
|
<div className="flex-1 flex flex-col overflow-auto">
|
|
168
169
|
<div className="flex items-center gap-2 p-4 border-b border-gray-200 justify-end h-16">
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
|
|
3
|
+
interface JsonPreset {
|
|
4
|
+
name: string;
|
|
5
|
+
data: object;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
interface JsonEditorPopupProps {
|
|
4
9
|
isOpen: boolean;
|
|
5
10
|
initialJson: object;
|
|
@@ -7,22 +12,102 @@ interface JsonEditorPopupProps {
|
|
|
7
12
|
onSave: (jsonData: object) => void;
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
const LOCAL_STORAGE_KEY = "json-editor-presets";
|
|
16
|
+
const ACTIVE_TAB_STORAGE_KEY = "json-editor-active-tab";
|
|
17
|
+
|
|
10
18
|
const JsonEditorPopup: React.FC<JsonEditorPopupProps> = ({ isOpen, initialJson, onClose, onSave }) => {
|
|
19
|
+
const [presets, setPresets] = useState<JsonPreset[]>([]);
|
|
20
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
11
21
|
const [jsonString, setJsonString] = useState("");
|
|
12
22
|
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
const [editingTab, setEditingTab] = useState<number | null>(null);
|
|
24
|
+
const [editingName, setEditingName] = useState("");
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (isOpen) {
|
|
28
|
+
let storedData: JsonPreset[] = [];
|
|
29
|
+
try {
|
|
30
|
+
const item = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
31
|
+
storedData = item ? JSON.parse(item) : [];
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error("Failed to parse presets from localStorage", e);
|
|
34
|
+
storedData = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (storedData.length === 0) {
|
|
38
|
+
storedData = [{ name: "Default", data: initialJson }];
|
|
39
|
+
}
|
|
40
|
+
setPresets(storedData);
|
|
41
|
+
|
|
42
|
+
let activeTabIndex = 0;
|
|
43
|
+
try {
|
|
44
|
+
const storedIndex = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY);
|
|
45
|
+
if (storedIndex) {
|
|
46
|
+
const parsedIndex = parseInt(storedIndex, 10);
|
|
47
|
+
if (parsedIndex >= 0 && parsedIndex < storedData.length) {
|
|
48
|
+
activeTabIndex = parsedIndex;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error("Failed to parse active tab from localStorage", e);
|
|
53
|
+
}
|
|
54
|
+
setActiveTab(activeTabIndex);
|
|
55
|
+
setError(null);
|
|
56
|
+
}
|
|
57
|
+
}, [isOpen, initialJson]);
|
|
13
58
|
|
|
14
59
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
60
|
+
if (isOpen && presets.length > 0 && presets[activeTab]) {
|
|
61
|
+
setJsonString(JSON.stringify(presets[activeTab].data, null, 2));
|
|
62
|
+
setError(null);
|
|
63
|
+
}
|
|
64
|
+
}, [activeTab, presets, isOpen]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (isOpen) {
|
|
68
|
+
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, activeTab.toString());
|
|
69
|
+
}
|
|
70
|
+
}, [activeTab, isOpen]);
|
|
18
71
|
|
|
19
72
|
if (!isOpen) {
|
|
20
73
|
return null;
|
|
21
74
|
}
|
|
22
75
|
|
|
76
|
+
const updatePresetsInStorage = (newPresets: JsonPreset[]) => {
|
|
77
|
+
setPresets(newPresets);
|
|
78
|
+
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newPresets));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleAddTab = () => {
|
|
82
|
+
const newPreset: JsonPreset = { name: `Preset ${presets.length + 1}`, data: {} };
|
|
83
|
+
const newPresets = [...presets, newPreset];
|
|
84
|
+
updatePresetsInStorage(newPresets);
|
|
85
|
+
setActiveTab(newPresets.length - 1);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleDeleteTab = (indexToDelete: number) => {
|
|
89
|
+
let newPresets = presets.filter((_, index) => index !== indexToDelete);
|
|
90
|
+
if (newPresets.length === 0) {
|
|
91
|
+
newPresets = [{ name: "Default", data: initialJson }];
|
|
92
|
+
}
|
|
93
|
+
updatePresetsInStorage(newPresets);
|
|
94
|
+
if (activeTab >= indexToDelete && activeTab > 0) {
|
|
95
|
+
setActiveTab(activeTab - 1);
|
|
96
|
+
} else if (activeTab >= newPresets.length) {
|
|
97
|
+
setActiveTab(newPresets.length - 1);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
23
101
|
const handleSave = () => {
|
|
24
102
|
try {
|
|
25
103
|
const parsedJson = JSON.parse(jsonString);
|
|
104
|
+
const updatedPresets = presets.map((preset, index) => {
|
|
105
|
+
if (index === activeTab) {
|
|
106
|
+
return { ...preset, data: parsedJson };
|
|
107
|
+
}
|
|
108
|
+
return preset;
|
|
109
|
+
});
|
|
110
|
+
updatePresetsInStorage(updatedPresets);
|
|
26
111
|
onSave(parsedJson);
|
|
27
112
|
onClose();
|
|
28
113
|
} catch (e) {
|
|
@@ -31,35 +116,86 @@ const JsonEditorPopup: React.FC<JsonEditorPopupProps> = ({ isOpen, initialJson,
|
|
|
31
116
|
}
|
|
32
117
|
};
|
|
33
118
|
|
|
119
|
+
const handleRename = (index: number) => {
|
|
120
|
+
if (editingName.trim() !== "") {
|
|
121
|
+
const newPresets = presets.map((p, i) => (i === index ? { ...p, name: editingName.trim() } : p));
|
|
122
|
+
updatePresetsInStorage(newPresets);
|
|
123
|
+
setEditingTab(null);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
34
127
|
return (
|
|
35
128
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
36
|
-
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4">
|
|
37
|
-
<div className="p-6">
|
|
38
|
-
<h2 className="text-xl font-semibold text-gray-900
|
|
129
|
+
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 flex flex-col" style={{ height: "80vh" }}>
|
|
130
|
+
<div className="p-6 border-b border-gray-200">
|
|
131
|
+
<h2 className="text-xl font-semibold text-gray-900">编辑 Extra Parameters</h2>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex-grow p-6 overflow-y-auto">
|
|
134
|
+
<div className="flex items-center border-b border-gray-200 mb-4">
|
|
135
|
+
{presets.map((preset, index) => (
|
|
136
|
+
<div
|
|
137
|
+
key={index}
|
|
138
|
+
className={`flex items-center px-4 py-2 border-b-2 cursor-pointer text-sm font-medium ${
|
|
139
|
+
activeTab === index ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
140
|
+
}`}
|
|
141
|
+
onDoubleClick={() => {
|
|
142
|
+
setEditingTab(index);
|
|
143
|
+
setEditingName(preset.name);
|
|
144
|
+
}}
|
|
145
|
+
onClick={() => setActiveTab(index)}
|
|
146
|
+
>
|
|
147
|
+
{editingTab === index ? (
|
|
148
|
+
<input
|
|
149
|
+
type="text"
|
|
150
|
+
value={editingName}
|
|
151
|
+
onChange={(e) => setEditingName(e.target.value)}
|
|
152
|
+
onBlur={() => handleRename(index)}
|
|
153
|
+
onKeyDown={(e) => e.key === "Enter" && handleRename(index)}
|
|
154
|
+
className="text-sm p-0 border-none focus:ring-0 bg-transparent"
|
|
155
|
+
autoFocus
|
|
156
|
+
/>
|
|
157
|
+
) : (
|
|
158
|
+
<span>{preset.name}</span>
|
|
159
|
+
)}
|
|
160
|
+
<button
|
|
161
|
+
onClick={(e) => {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
handleDeleteTab(index);
|
|
164
|
+
}}
|
|
165
|
+
className="ml-2 text-gray-400 hover:text-gray-600 w-4 h-4 flex items-center justify-center"
|
|
166
|
+
>
|
|
167
|
+
×
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
<button onClick={handleAddTab} className="ml-2 px-3 py-1 text-sm text-gray-500 hover:text-gray-700">
|
|
172
|
+
+
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
39
176
|
<textarea
|
|
40
177
|
value={jsonString}
|
|
41
178
|
onChange={(e) => {
|
|
42
179
|
setJsonString(e.target.value);
|
|
43
180
|
setError(null); // Clear error on edit
|
|
44
181
|
}}
|
|
45
|
-
|
|
46
|
-
className="w-full p-3 border border-gray-300 rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
182
|
+
className="w-full h-full p-3 border border-gray-300 rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
47
183
|
/>
|
|
48
184
|
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</
|
|
185
|
+
</div>
|
|
186
|
+
<div className="flex justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
|
187
|
+
<button
|
|
188
|
+
onClick={onClose}
|
|
189
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
190
|
+
>
|
|
191
|
+
取消
|
|
192
|
+
</button>
|
|
193
|
+
<button
|
|
194
|
+
onClick={handleSave}
|
|
195
|
+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
196
|
+
>
|
|
197
|
+
保存并使用
|
|
198
|
+
</button>
|
|
63
199
|
</div>
|
|
64
200
|
</div>
|
|
65
201
|
</div>
|
|
@@ -3,7 +3,6 @@ import MessageHuman from "./MessageHuman";
|
|
|
3
3
|
import MessageAI from "./MessageAI";
|
|
4
4
|
import MessageTool from "./MessageTool";
|
|
5
5
|
import { formatTokens, getMessageContent, LangGraphClient, RenderMessage } from "@langgraph-js/sdk";
|
|
6
|
-
import { motion } from "motion/react";
|
|
7
6
|
|
|
8
7
|
export const MessagesBox = ({
|
|
9
8
|
renderMessages,
|
|
@@ -19,7 +18,7 @@ export const MessagesBox = ({
|
|
|
19
18
|
return (
|
|
20
19
|
<div className="flex flex-col gap-4 w-full">
|
|
21
20
|
{renderMessages.map((message, index) => (
|
|
22
|
-
<
|
|
21
|
+
<div key={message.unique_id}>
|
|
23
22
|
{message.type === "human" ? (
|
|
24
23
|
<MessageHuman content={message.content} />
|
|
25
24
|
) : message.type === "tool" ? (
|
|
@@ -34,7 +33,7 @@ export const MessagesBox = ({
|
|
|
34
33
|
) : (
|
|
35
34
|
<MessageAI message={message} />
|
|
36
35
|
)}
|
|
37
|
-
</
|
|
36
|
+
</div>
|
|
38
37
|
))}
|
|
39
38
|
</div>
|
|
40
39
|
);
|
|
@@ -4,6 +4,9 @@ import { UsageMetadata } from "./UsageMetadata";
|
|
|
4
4
|
import { useChat } from "../context/ChatContext";
|
|
5
5
|
import Markdown from "react-markdown";
|
|
6
6
|
import remarkGfm from "remark-gfm";
|
|
7
|
+
import { Highlight, themes } from "prism-react-renderer";
|
|
8
|
+
|
|
9
|
+
const TOOL_COLORS = ["border-red-400", "border-blue-400", "border-green-500", "border-yellow-400", "border-purple-400", "border-pink-400", "border-indigo-400"];
|
|
7
10
|
|
|
8
11
|
interface MessageToolProps {
|
|
9
12
|
message: ToolMessage & RenderMessage;
|
|
@@ -14,16 +17,26 @@ interface MessageToolProps {
|
|
|
14
17
|
onToggleCollapse: () => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const getToolColorClass = (tool_name: string) => {
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (let i = 0; i < tool_name.length; i++) {
|
|
23
|
+
hash = tool_name.charCodeAt(i) + ((hash << 5) - hash);
|
|
24
|
+
}
|
|
25
|
+
const index = Math.abs(hash % TOOL_COLORS.length);
|
|
26
|
+
return TOOL_COLORS[index];
|
|
27
|
+
};
|
|
28
|
+
|
|
17
29
|
const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageContent, formatTokens, isCollapsed, onToggleCollapse }) => {
|
|
18
30
|
const { getToolUIRender } = useChat();
|
|
19
31
|
const render = getToolUIRender(message.name!);
|
|
32
|
+
const borderColorClass = getToolColorClass(message.name!);
|
|
20
33
|
return (
|
|
21
34
|
<div className="flex flex-col w-full">
|
|
22
35
|
{render ? (
|
|
23
36
|
(render(message) as JSX.Element)
|
|
24
37
|
) : (
|
|
25
|
-
<div className=
|
|
26
|
-
<div className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-
|
|
38
|
+
<div className={`flex flex-col w-full bg-white rounded-lg shadow-sm border-2 ${borderColorClass} overflow-hidden`}>
|
|
39
|
+
<div className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 transition-colors" onClick={onToggleCollapse}>
|
|
27
40
|
<div className="text-sm font-medium text-gray-700" onClick={() => console.log(message)}>
|
|
28
41
|
{message.node_name} | {message.name}
|
|
29
42
|
</div>
|
|
@@ -59,19 +72,24 @@ const Previewer = ({ content }: { content: string }) => {
|
|
|
59
72
|
};
|
|
60
73
|
const isJSON = content.startsWith("{") && content.endsWith("}") && validJSON();
|
|
61
74
|
const isMarkdown = content.includes("#") || content.includes("```") || content.includes("*");
|
|
62
|
-
const [jsonMode, setJsonMode] = useState(
|
|
75
|
+
const [jsonMode, setJsonMode] = useState(isJSON);
|
|
63
76
|
const [markdownMode, setMarkdownMode] = useState(false);
|
|
64
|
-
|
|
77
|
+
const copyToClipboard = () => {
|
|
78
|
+
navigator.clipboard.writeText(content);
|
|
79
|
+
};
|
|
65
80
|
return (
|
|
66
81
|
<div className={`flex flex-col`}>
|
|
67
82
|
<div className="flex gap-2 mb-2">
|
|
83
|
+
<button onClick={copyToClipboard} className="px-2 py-1 text-xs font-medium text-gray-600 bg-green-100 rounded hover:bg-green-200 transition-colors">
|
|
84
|
+
copy
|
|
85
|
+
</button>
|
|
68
86
|
{isJSON && (
|
|
69
|
-
<button onClick={() => setJsonMode(!jsonMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-
|
|
87
|
+
<button onClick={() => setJsonMode(!jsonMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-orange-100 rounded hover:bg-orange-200 transition-colors">
|
|
70
88
|
json
|
|
71
89
|
</button>
|
|
72
90
|
)}
|
|
73
91
|
{isMarkdown && (
|
|
74
|
-
<button onClick={() => setMarkdownMode(!markdownMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-
|
|
92
|
+
<button onClick={() => setMarkdownMode(!markdownMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-blue-100 rounded hover:bg-blue-200 transition-colors">
|
|
75
93
|
markdown
|
|
76
94
|
</button>
|
|
77
95
|
)}
|
|
@@ -79,7 +97,19 @@ const Previewer = ({ content }: { content: string }) => {
|
|
|
79
97
|
|
|
80
98
|
<div className="flex flex-col max-h-[300px] overflow-auto border border-gray-200 rounded p-2 w-full text-xs font-mono whitespace-pre-wrap">
|
|
81
99
|
{jsonMode && isJSON ? (
|
|
82
|
-
<
|
|
100
|
+
<Highlight code={JSON.stringify(JSON.parse(content), null, 2)} language="json" theme={themes.oneLight}>
|
|
101
|
+
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
|
102
|
+
<pre style={style}>
|
|
103
|
+
{tokens.map((line, i) => (
|
|
104
|
+
<div key={i} {...getLineProps({ line })}>
|
|
105
|
+
{line.map((token, key) => (
|
|
106
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</pre>
|
|
111
|
+
)}
|
|
112
|
+
</Highlight>
|
|
83
113
|
) : markdownMode && isMarkdown ? (
|
|
84
114
|
<div className="markdown-body">
|
|
85
115
|
<Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown>
|
package/src/index.ts
CHANGED