@langgraph-js/ui 1.0.0 → 1.1.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-CBfok6qC.css +1 -0
- package/dist/assets/index-CCaE6qp1.js +192 -0
- package/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/chat/Chat.tsx +42 -4
- package/src/chat/FileUpload/index.ts +105 -0
- package/src/chat/chat.css +28 -1
- package/src/chat/components/FileList.css +116 -0
- package/src/chat/components/FileList.tsx +70 -0
- package/src/chat/components/HistoryList.tsx +2 -2
- package/src/chat/components/MessageAI.tsx +6 -2
- package/src/chat/components/MessageHuman.tsx +43 -3
- package/src/chat/components/MessageTool.tsx +1 -2
- package/src/chat/components/UsageMetadata.tsx +7 -1
- package/dist/assets/index-B6G4BLix.js +0 -164
- package/dist/assets/index-BAcH-2-3.css +0 -1
package/dist/index.html
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
height: 100%;
|
|
15
15
|
}
|
|
16
16
|
</style>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
18
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-CCaE6qp1.js"></script>
|
|
18
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CBfok6qC.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
|
21
21
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langgraph-js/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"react": "^19.0.0",
|
|
20
20
|
"react-dom": "^19.0.0",
|
|
21
21
|
"vite": "^6.2.0",
|
|
22
|
-
"
|
|
22
|
+
"react-markdown": "^10.1.0",
|
|
23
|
+
"remark-gfm": "^4.0.1",
|
|
24
|
+
"@langgraph-js/sdk": "1.1.6"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
27
|
"@types/react": "^19.0.10",
|
package/src/chat/Chat.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import "./chat.css";
|
|
3
3
|
import MessageHuman from "./components/MessageHuman";
|
|
4
4
|
import MessageAI from "./components/MessageAI";
|
|
@@ -6,7 +6,8 @@ import MessageTool from "./components/MessageTool";
|
|
|
6
6
|
import HistoryList from "./components/HistoryList";
|
|
7
7
|
import { ChatProvider, useChat } from "./context/ChatContext";
|
|
8
8
|
import { UsageMetadata } from "./components/UsageMetadata";
|
|
9
|
-
import { formatTime, formatTokens, getMessageContent } from "@langgraph-js/sdk";
|
|
9
|
+
import { formatTime, formatTokens, getMessageContent, Message } from "@langgraph-js/sdk";
|
|
10
|
+
import FileList from "./components/FileList";
|
|
10
11
|
|
|
11
12
|
const ChatMessages: React.FC = () => {
|
|
12
13
|
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse } = useChat();
|
|
@@ -38,15 +39,48 @@ const ChatMessages: React.FC = () => {
|
|
|
38
39
|
|
|
39
40
|
const ChatInput: React.FC = () => {
|
|
40
41
|
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client } = useChat();
|
|
42
|
+
const [extraParams, setExtraParams] = useState({});
|
|
43
|
+
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
|
44
|
+
|
|
45
|
+
const handleFileUploaded = (url: string) => {
|
|
46
|
+
setImageUrls((prev) => [...prev, url]);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sendMultiModalMessage = () => {
|
|
50
|
+
const content: Message[] = [
|
|
51
|
+
{
|
|
52
|
+
type: "human",
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: userInput,
|
|
57
|
+
},
|
|
58
|
+
...imageUrls.map((url) => ({
|
|
59
|
+
type: "image_url" as const,
|
|
60
|
+
image_url: { url },
|
|
61
|
+
})),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
sendMessage(content, {
|
|
67
|
+
extraParams,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 清空图片列表
|
|
71
|
+
setImageUrls([]);
|
|
72
|
+
};
|
|
73
|
+
|
|
41
74
|
const handleKeyPress = (event: React.KeyboardEvent) => {
|
|
42
75
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
43
76
|
event.preventDefault();
|
|
44
|
-
|
|
77
|
+
sendMultiModalMessage();
|
|
45
78
|
}
|
|
46
79
|
};
|
|
47
80
|
|
|
48
81
|
return (
|
|
49
82
|
<div className="chat-input">
|
|
83
|
+
<FileList onFileUploaded={handleFileUploaded} />
|
|
50
84
|
<div className="chat-input-header">
|
|
51
85
|
<select value={currentAgent} onChange={(e) => setCurrentAgent(e.target.value)}>
|
|
52
86
|
{client?.availableAssistants.map((i) => {
|
|
@@ -65,7 +99,11 @@ const ChatInput: React.FC = () => {
|
|
|
65
99
|
placeholder="输入消息..."
|
|
66
100
|
disabled={loading}
|
|
67
101
|
/>
|
|
68
|
-
<button
|
|
102
|
+
<button
|
|
103
|
+
className={`send-button ${loading ? "interrupt" : ""}`}
|
|
104
|
+
onClick={() => (loading ? stopGeneration() : sendMultiModalMessage())}
|
|
105
|
+
disabled={!loading && !userInput.trim() && imageUrls.length === 0}
|
|
106
|
+
>
|
|
69
107
|
{loading ? "中断" : "发送"}
|
|
70
108
|
</button>
|
|
71
109
|
</div>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Upload SDK - Base client for file upload services
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Base interfaces
|
|
6
|
+
interface FileUploadClientOptions {
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FileUploadOptions {
|
|
11
|
+
filename?: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FileUploadResponse {
|
|
16
|
+
status: string;
|
|
17
|
+
data?: {
|
|
18
|
+
url: string;
|
|
19
|
+
delete_url?: string;
|
|
20
|
+
expires_at?: string;
|
|
21
|
+
size?: number;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Abstract base class for file upload clients
|
|
28
|
+
abstract class FileUploadClient {
|
|
29
|
+
protected apiUrl: string;
|
|
30
|
+
|
|
31
|
+
constructor(options: FileUploadClientOptions = {}) {
|
|
32
|
+
this.apiUrl = options.apiUrl || "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected abstract getUploadEndpoint(): string;
|
|
36
|
+
protected abstract processResponse(response: FileUploadResponse): FileUploadResponse;
|
|
37
|
+
|
|
38
|
+
protected createFormData(file: File | Blob | string, filename?: string): FormData {
|
|
39
|
+
const formData = new FormData();
|
|
40
|
+
|
|
41
|
+
if (typeof file === "string") {
|
|
42
|
+
const blob = new Blob([file], { type: "text/plain" });
|
|
43
|
+
formData.append("file", blob, filename || "file.txt");
|
|
44
|
+
} else {
|
|
45
|
+
formData.append("file", file, filename || (file instanceof File ? file.name : "file"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return formData;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async upload(file: File | Blob | string, options: FileUploadOptions = {}): Promise<FileUploadResponse> {
|
|
52
|
+
const formData = this.createFormData(file, options.filename);
|
|
53
|
+
|
|
54
|
+
const fetchOptions: RequestInit = {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: formData,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (options.signal) {
|
|
60
|
+
fetchOptions.signal = options.signal;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(`${this.apiUrl}${this.getUploadEndpoint()}`, fetchOptions);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Upload failed with status: ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result = (await response.json()) as FileUploadResponse;
|
|
71
|
+
return this.processResponse(result);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`File upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* TmpFiles SDK - A client for uploading files to tmpfiles.org
|
|
80
|
+
*/
|
|
81
|
+
export class TmpFilesClient extends FileUploadClient {
|
|
82
|
+
constructor(options: FileUploadClientOptions = {}) {
|
|
83
|
+
super({
|
|
84
|
+
apiUrl: options.apiUrl || "https://tmpfiles.org/api/v1"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected getUploadEndpoint(): string {
|
|
89
|
+
return "/upload";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected processResponse(response: FileUploadResponse): FileUploadResponse {
|
|
93
|
+
if (response.data?.url) {
|
|
94
|
+
response.data.url = response.data.url.replace("https://tmpfiles.org/", "https://tmpfiles.org/dl/");
|
|
95
|
+
}
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Export types for external use
|
|
101
|
+
export type {
|
|
102
|
+
FileUploadClientOptions,
|
|
103
|
+
FileUploadOptions,
|
|
104
|
+
FileUploadResponse
|
|
105
|
+
};
|
package/src/chat/chat.css
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import url("https://unpkg.com/github-markdown-css/github-markdown.css");
|
|
2
|
+
|
|
1
3
|
.chat-container {
|
|
2
4
|
display: flex;
|
|
3
5
|
height: 100vh;
|
|
@@ -128,10 +130,31 @@
|
|
|
128
130
|
display: flex;
|
|
129
131
|
flex-direction: column;
|
|
130
132
|
gap: 0.5rem;
|
|
133
|
+
max-width: 100%;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
.message-text {
|
|
134
137
|
word-break: break-word;
|
|
138
|
+
line-height: 1.5;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.message-image {
|
|
142
|
+
margin: 0.5rem 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.message-image img {
|
|
146
|
+
max-width: 100%;
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.message-audio {
|
|
152
|
+
margin: 0.5rem 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.message-audio audio {
|
|
156
|
+
width: 100%;
|
|
157
|
+
max-width: 300px;
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
.message-meta {
|
|
@@ -178,7 +201,6 @@
|
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
.message.ai .message-content {
|
|
181
|
-
background-color: #f3f4f6;
|
|
182
204
|
color: #1f2937;
|
|
183
205
|
}
|
|
184
206
|
|
|
@@ -359,3 +381,8 @@
|
|
|
359
381
|
font-size: 14px;
|
|
360
382
|
text-align: center;
|
|
361
383
|
}
|
|
384
|
+
|
|
385
|
+
.markdown-body p {
|
|
386
|
+
text-align: left;
|
|
387
|
+
}
|
|
388
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
.file-list {
|
|
2
|
+
padding: 0.5rem;
|
|
3
|
+
background-color: #f9fafb;
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
margin-bottom: 1rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.file-list-header {
|
|
9
|
+
margin-bottom: 1rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.file-upload-button {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
width: 80px;
|
|
17
|
+
height: 80px;
|
|
18
|
+
background-color: #3b82f6;
|
|
19
|
+
color: white;
|
|
20
|
+
border-radius: 6px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
font-size: 2rem;
|
|
23
|
+
transition: background-color 0.2s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.file-upload-button:hover {
|
|
27
|
+
background-color: #2563eb;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.file-list-content {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-wrap: wrap;
|
|
33
|
+
gap: 0.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.file-item {
|
|
37
|
+
position: relative;
|
|
38
|
+
width: 80px;
|
|
39
|
+
height: 80px;
|
|
40
|
+
border-radius: 6px;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.file-preview {
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 100%;
|
|
47
|
+
object-fit: cover;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.file-info {
|
|
51
|
+
padding: 0.75rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.file-name {
|
|
55
|
+
display: block;
|
|
56
|
+
font-size: 0.875rem;
|
|
57
|
+
color: #374151;
|
|
58
|
+
margin-bottom: 0.5rem;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
text-overflow: ellipsis;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.file-actions {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 0.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.upload-button,
|
|
70
|
+
.remove-button {
|
|
71
|
+
flex: 1;
|
|
72
|
+
padding: 0.375rem 0.75rem;
|
|
73
|
+
border: none;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
font-size: 0.75rem;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: all 0.2s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.upload-button {
|
|
81
|
+
background-color: #3b82f6;
|
|
82
|
+
color: white;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.upload-button:hover {
|
|
86
|
+
background-color: #2563eb;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.remove-button {
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: 2px;
|
|
92
|
+
right: 2px;
|
|
93
|
+
width: 20px;
|
|
94
|
+
height: 20px;
|
|
95
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
96
|
+
color: white;
|
|
97
|
+
border: none;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
font-size: 16px;
|
|
101
|
+
line-height: 1;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
transition: background-color 0.2s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.remove-button:hover {
|
|
109
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.upload-button:disabled,
|
|
113
|
+
.remove-button:disabled {
|
|
114
|
+
opacity: 0.5;
|
|
115
|
+
cursor: not-allowed;
|
|
116
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { TmpFilesClient } from '../FileUpload';
|
|
3
|
+
import './FileList.css';
|
|
4
|
+
|
|
5
|
+
interface FileListProps {
|
|
6
|
+
onFileUploaded: (url: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const FileList: React.FC<FileListProps> = ({ onFileUploaded }) => {
|
|
10
|
+
const [files, setFiles] = useState<File[]>([]);
|
|
11
|
+
const client = new TmpFilesClient();
|
|
12
|
+
|
|
13
|
+
const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
14
|
+
const selectedFiles = Array.from(event.target.files || []);
|
|
15
|
+
const imageFiles = selectedFiles.filter(file => file.type.startsWith('image/'));
|
|
16
|
+
setFiles(prev => [...prev, ...imageFiles]);
|
|
17
|
+
|
|
18
|
+
for (const file of imageFiles) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await client.upload(file);
|
|
21
|
+
if (result.data?.url) {
|
|
22
|
+
onFileUploaded(result.data.url);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Upload failed:', error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 清空 input 值,允许重复选择相同文件
|
|
30
|
+
event.target.value = '';
|
|
31
|
+
}, [onFileUploaded]);
|
|
32
|
+
|
|
33
|
+
const removeFile = useCallback((index: number) => {
|
|
34
|
+
setFiles(prev => prev.filter((_, i) => i !== index));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="file-list">
|
|
39
|
+
<div className="file-list-content">
|
|
40
|
+
<label className="file-upload-button">
|
|
41
|
+
<span>+</span>
|
|
42
|
+
<input
|
|
43
|
+
type="file"
|
|
44
|
+
accept="image/*"
|
|
45
|
+
multiple
|
|
46
|
+
onChange={handleFileChange}
|
|
47
|
+
style={{ display: 'none' }}
|
|
48
|
+
/>
|
|
49
|
+
</label>
|
|
50
|
+
{files.map((file, index) => (
|
|
51
|
+
<div key={index} className="file-item">
|
|
52
|
+
<img
|
|
53
|
+
src={URL.createObjectURL(file)}
|
|
54
|
+
alt={file.name}
|
|
55
|
+
className="file-preview"
|
|
56
|
+
/>
|
|
57
|
+
<button
|
|
58
|
+
className="remove-button"
|
|
59
|
+
onClick={() => removeFile(index)}
|
|
60
|
+
>
|
|
61
|
+
×
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default FileList;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useChat } from "../context/ChatContext";
|
|
3
|
+
import { getHistoryContent } from "@langgraph-js/sdk";
|
|
3
4
|
|
|
4
5
|
interface HistoryListProps {
|
|
5
6
|
onClose: () => void;
|
|
@@ -8,7 +9,6 @@ interface HistoryListProps {
|
|
|
8
9
|
|
|
9
10
|
const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
|
|
10
11
|
const { historyList, currentChatId, refreshHistoryList, createNewChat, deleteHistoryChat, toHistoryChat } = useChat();
|
|
11
|
-
|
|
12
12
|
return (
|
|
13
13
|
<div className="history-list">
|
|
14
14
|
<div className="history-header">
|
|
@@ -42,7 +42,7 @@ const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
|
|
|
42
42
|
.map((thread) => (
|
|
43
43
|
<div className={`history-item ${thread.thread_id === currentChatId ? "active" : ""}`} key={thread.thread_id}>
|
|
44
44
|
<div className="history-info">
|
|
45
|
-
<div className="history-title">{thread
|
|
45
|
+
<div className="history-title">{getHistoryContent(thread)}</div>
|
|
46
46
|
<div className="history-meta">
|
|
47
47
|
<span className="history-time">{formatTime(new Date(thread.created_at))}</span>
|
|
48
48
|
<span className="history-status">{thread.status}</span>
|
|
@@ -2,6 +2,8 @@ import React from "react";
|
|
|
2
2
|
import { RenderMessage } from "@langgraph-js/sdk";
|
|
3
3
|
import { UsageMetadata } from "./UsageMetadata";
|
|
4
4
|
import { getMessageContent } from "@langgraph-js/sdk";
|
|
5
|
+
import Markdown from 'react-markdown'
|
|
6
|
+
import remarkGfm from 'remark-gfm'
|
|
5
7
|
interface MessageAIProps {
|
|
6
8
|
message: RenderMessage;
|
|
7
9
|
}
|
|
@@ -10,8 +12,10 @@ const MessageAI: React.FC<MessageAIProps> = ({ message }) => {
|
|
|
10
12
|
return (
|
|
11
13
|
<div className="message ai">
|
|
12
14
|
<div className="message-content">
|
|
13
|
-
<div className="message-text">
|
|
14
|
-
|
|
15
|
+
<div className="message-text markdown-body">
|
|
16
|
+
<Markdown remarkPlugins={[remarkGfm]}>{getMessageContent(message.content)}</Markdown>
|
|
17
|
+
</div>
|
|
18
|
+
<UsageMetadata response_metadata={message.response_metadata as any} usage_metadata={message.usage_metadata||{}} spend_time={message.spend_time} />
|
|
15
19
|
</div>
|
|
16
20
|
</div>
|
|
17
21
|
);
|
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
2
|
interface MessageHumanProps {
|
|
4
|
-
content: any;
|
|
3
|
+
content: string | any[];
|
|
5
4
|
}
|
|
6
5
|
|
|
7
6
|
const MessageHuman: React.FC<MessageHumanProps> = ({ content }) => {
|
|
7
|
+
const renderContent = () => {
|
|
8
|
+
if (typeof content === "string") {
|
|
9
|
+
return <div className="message-text">{content}</div>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(content)) {
|
|
13
|
+
return content.map((item, index) => {
|
|
14
|
+
switch (item.type) {
|
|
15
|
+
case "text":
|
|
16
|
+
return (
|
|
17
|
+
<div key={index} className="message-text">
|
|
18
|
+
{item.text}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
case "image_url":
|
|
22
|
+
return (
|
|
23
|
+
<div key={index} className="message-image">
|
|
24
|
+
<img src={item.image_url.url} alt="用户上传的图片" style={{ maxWidth: "200px", borderRadius: "4px" }} />
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
case "audio":
|
|
28
|
+
return (
|
|
29
|
+
<div key={index} className="message-audio">
|
|
30
|
+
<audio controls src={item.audio_url}>
|
|
31
|
+
您的浏览器不支持音频播放
|
|
32
|
+
</audio>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
default:
|
|
36
|
+
return (
|
|
37
|
+
<div key={index} className="message-text">
|
|
38
|
+
{JSON.stringify(item)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return <div className="message-text">{JSON.stringify(content)}</div>;
|
|
46
|
+
};
|
|
47
|
+
|
|
8
48
|
return (
|
|
9
49
|
<div className="message human">
|
|
10
|
-
<div className="message-content">{
|
|
50
|
+
<div className="message-content">{renderContent()}</div>
|
|
11
51
|
</div>
|
|
12
52
|
);
|
|
13
53
|
};
|
|
@@ -11,7 +11,6 @@ interface MessageToolProps {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageContent, formatTokens, isCollapsed, onToggleCollapse }) => {
|
|
14
|
-
console.log(message)
|
|
15
14
|
return (
|
|
16
15
|
<div className="message tool">
|
|
17
16
|
{message.name === "ask_user" && !message.additional_kwargs?.done && (
|
|
@@ -36,7 +35,7 @@ const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageCo
|
|
|
36
35
|
<div className="tool-content">
|
|
37
36
|
<div className="tool-input">{message.tool_input}</div>
|
|
38
37
|
<div className="tool-output">{getMessageContent(message.content)}</div>
|
|
39
|
-
{message.
|
|
38
|
+
<UsageMetadata response_metadata={message.response_metadata as any} usage_metadata={message.usage_metadata || {}} spend_time={message.spend_time} />
|
|
40
39
|
</div>
|
|
41
40
|
)}
|
|
42
41
|
</div>
|
|
@@ -4,10 +4,13 @@ interface UsageMetadataProps {
|
|
|
4
4
|
output_tokens: number;
|
|
5
5
|
total_tokens: number;
|
|
6
6
|
}>;
|
|
7
|
+
response_metadata?:{
|
|
8
|
+
model_name?: string;
|
|
9
|
+
}
|
|
7
10
|
spend_time?: number;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, spend_time }) => {
|
|
13
|
+
export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, spend_time ,response_metadata}) => {
|
|
11
14
|
const formatTokens = (tokens: number) => {
|
|
12
15
|
return tokens.toString();
|
|
13
16
|
};
|
|
@@ -28,6 +31,9 @@ export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, sp
|
|
|
28
31
|
{formatTokens(usage_metadata.total_tokens || 0)}
|
|
29
32
|
</span>
|
|
30
33
|
</div>
|
|
34
|
+
<div>
|
|
35
|
+
{response_metadata?.model_name}
|
|
36
|
+
</div>
|
|
31
37
|
<span className="message-time">{spend_time ? `${(spend_time / 1000).toFixed(2)}s` : ""}</span>
|
|
32
38
|
</div>
|
|
33
39
|
);
|