akhaliq HF Staff commited on
Commit
34a952d
·
verified ·
1 Parent(s): 84184df

Upload components/ChatInterface.jsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. components/ChatInterface.jsx +163 -134
components/ChatInterface.jsx CHANGED
@@ -1,85 +1,45 @@
1
- import { useState, useRef, useEffect } from 'react'
2
- import MessageBubble from './MessageBubble'
3
- import ChatInput from './ChatInput'
4
- import TypingIndicator from './TypingIndicator'
5
 
6
  export default function ChatInterface() {
7
- const [messages, setMessages] = useState([
8
- {
9
- role: 'assistant',
10
- content: 'Hello! I can analyze images and answer questions about them. Upload an image and ask me anything!',
11
- },
12
- ])
13
- const [input, setInput] = useState('')
14
- const [uploadedImage, setUploadedImage] = useState(null)
15
- const [isLoading, setIsLoading] = useState(false)
16
- const [error, setError] = useState(null)
17
- const messagesEndRef = useRef(null)
18
 
19
  const scrollToBottom = () => {
20
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
21
- }
22
 
23
  useEffect(() => {
24
- scrollToBottom()
25
- }, [messages, isLoading])
26
-
27
- const handleImageUpload = (file) => {
28
- if (!file.type.startsWith('image/')) {
29
- setError('Please upload a valid image file')
30
- return
31
- }
32
-
33
- if (file.size > 10 * 1024 * 1024) {
34
- setError('Image size must be less than 10MB')
35
- return
36
  }
 
37
 
38
- const reader = new FileReader()
39
- reader.onload = (e) => {
40
- setUploadedImage({
41
- file,
42
- preview: e.target.result,
43
- base64: e.target.result.split(',')[1],
44
- })
45
- setError(null)
46
- }
47
- reader.readAsDataURL(file)
48
- }
49
-
50
- const handleSubmit = async () => {
51
- if (!input.trim() && !uploadedImage) {
52
- setError('Please enter a message or upload an image')
53
- return
54
- }
55
-
56
- setError(null)
57
- setIsLoading(true)
58
 
59
  const userMessage = {
60
  role: 'user',
61
- content: [],
62
- }
 
63
 
64
- if (input.trim()) {
65
- userMessage.content.push({
66
- type: 'text',
67
- text: input,
68
- })
69
- }
70
-
71
- if (uploadedImage) {
72
- userMessage.content.push({
73
- type: 'image_url',
74
- image_url: {
75
- url: `data:image/jpeg;base64,${uploadedImage.base64}`,
76
- },
77
- })
78
- }
79
-
80
- setMessages(prev => [...prev, userMessage])
81
- setInput('')
82
- setUploadedImage(null)
83
 
84
  try {
85
  const response = await fetch('/api/chat', {
@@ -88,81 +48,150 @@ export default function ChatInterface() {
88
  'Content-Type': 'application/json',
89
  },
90
  body: JSON.stringify({
91
- messages: [...messages, userMessage],
 
 
 
 
92
  }),
93
- })
94
-
95
- if (!response.ok) {
96
- throw new Error('Failed to get response from the AI')
 
 
 
 
 
 
 
97
  }
98
-
99
- const reader = response.body.getReader()
100
- const decoder = new TextDecoder()
101
- let accumulatedContent = ''
102
-
103
- setMessages(prev => [...prev, { role: 'assistant', content: '' }])
104
-
105
- while (true) {
106
- const { done, value } = await reader.read()
107
- if (done) break
108
-
109
- const chunk = decoder.decode(value, { stream: true })
110
- accumulatedContent += chunk
111
-
112
- setMessages(prev => {
113
- const newMessages = [...prev]
114
- const lastMessage = newMessages[newMessages.length - 1]
115
- if (lastMessage.role === 'assistant') {
116
- lastMessage.content = accumulatedContent
117
- }
118
- return newMessages
119
- })
120
- }
121
- } catch (err) {
122
- console.error('Chat error:', err)
123
- setError(err.message || 'An error occurred. Please try again.')
124
  } finally {
125
- setIsLoading(false)
126
- }
127
- }
128
-
129
- const handleKeyPress = (e) => {
130
- if (e.key === 'Enter' && !e.shiftKey) {
131
- e.preventDefault()
132
- handleSubmit()
133
  }
134
- }
135
 
136
  return (
137
- <div className="flex flex-col flex-1 overflow-hidden">
138
  <div className="flex-1 overflow-y-auto p-4 space-y-4">
 
 
 
 
 
 
 
139
  {messages.map((message, index) => (
140
- <MessageBubble
141
- key={index}
142
- message={message}
143
- isLast={index === messages.length - 1}
144
- />
 
 
 
 
 
 
 
 
 
 
 
145
  ))}
146
- {isLoading && <TypingIndicator />}
 
 
 
 
 
 
 
 
 
 
 
147
  <div ref={messagesEndRef} />
148
  </div>
149
 
150
- {error && (
151
- <div className="mx-4 mb-2 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
152
- {error}
153
- </div>
154
- )}
155
-
156
- <ChatInput
157
- input={input}
158
- setInput={setInput}
159
- uploadedImage={uploadedImage}
160
- setUploadedImage={setUploadedImage}
161
- onImageUpload={handleImageUpload}
162
- onSubmit={handleSubmit}
163
- onKeyPress={handleKeyPress}
164
- isLoading={isLoading}
165
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
- )
168
  }
 
1
+ import React, { useState, useRef, useEffect } from 'react';
 
 
 
2
 
3
  export default function ChatInterface() {
4
+ const [messages, setMessages] = useState([]);
5
+ const [input, setInput] = useState('');
6
+ const [isLoading, setIsLoading] = useState(false);
7
+ const [image, setImage] = useState(null);
8
+ const messagesEndRef = useRef(null);
9
+ const fileInputRef = useRef(null);
 
 
 
 
 
10
 
11
  const scrollToBottom = () => {
12
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
13
+ };
14
 
15
  useEffect(() => {
16
+ scrollToBottom();
17
+ }, [messages]);
18
+
19
+ const handleImageUpload = (e) => {
20
+ const file = e.target.files[0];
21
+ if (file && file.type.startsWith('image/')) {
22
+ const reader = new FileReader();
23
+ reader.onloadend = () => {
24
+ setImage(reader.result);
25
+ };
26
+ reader.readAsDataURL(file);
 
27
  }
28
+ };
29
 
30
+ const handleSubmit = async (e) => {
31
+ e.preventDefault();
32
+ if (!input.trim() && !image) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  const userMessage = {
35
  role: 'user',
36
+ content: input,
37
+ image: image,
38
+ };
39
 
40
+ setMessages(prev => [...prev, userMessage]);
41
+ setInput('');
42
+ setIsLoading(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  try {
45
  const response = await fetch('/api/chat', {
 
48
  'Content-Type': 'application/json',
49
  },
50
  body: JSON.stringify({
51
+ messages: [...messages, userMessage].map(msg => ({
52
+ role: msg.role,
53
+ content: msg.content,
54
+ })),
55
+ image: image,
56
  }),
57
+ });
58
+
59
+ const data = await response.json();
60
+
61
+ if (response.ok) {
62
+ setMessages(prev => [...prev, {
63
+ role: 'assistant',
64
+ content: data.response,
65
+ }]);
66
+ } else {
67
+ throw new Error(data.error || 'Failed to get response');
68
  }
69
+ } catch (error) {
70
+ console.error('Error:', error);
71
+ setMessages(prev => [...prev, {
72
+ role: 'assistant',
73
+ content: 'Sorry, I encountered an error. Please try again.',
74
+ }]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  } finally {
76
+ setIsLoading(false);
77
+ setImage(null);
78
+ if (fileInputRef.current) {
79
+ fileInputRef.current.value = '';
80
+ }
 
 
 
81
  }
82
+ };
83
 
84
  return (
85
+ <div className="flex-1 flex flex-col overflow-hidden">
86
  <div className="flex-1 overflow-y-auto p-4 space-y-4">
87
+ {messages.length === 0 && (
88
+ <div className="text-center text-gray-500 mt-8">
89
+ <p className="text-lg">Welcome to GLM-4.6V-Flash</p>
90
+ <p className="text-sm mt-2">Upload an image and ask me about it!</p>
91
+ </div>
92
+ )}
93
+
94
  {messages.map((message, index) => (
95
+ <div
96
+ key={index}
97
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
98
+ >
99
+ <div className={`message-bubble ${message.role === 'user' ? 'user-bubble' : 'assistant-bubble'}`}>
100
+ {message.image && (
101
+ <img
102
+ src={message.image}
103
+ alt="Uploaded"
104
+ className="mb-2 rounded-lg max-w-full h-auto"
105
+ style={{ maxHeight: '200px' }}
106
+ />
107
+ )}
108
+ <p className="whitespace-pre-wrap">{message.content}</p>
109
+ </div>
110
+ </div>
111
  ))}
112
+
113
+ {isLoading && (
114
+ <div className="flex justify-start">
115
+ <div className="message-bubble assistant-bubble">
116
+ <div className="flex space-x-1">
117
+ <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots"></div>
118
+ <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.2s' }}></div>
119
+ <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.4s' }}></div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ )}
124
  <div ref={messagesEndRef} />
125
  </div>
126
 
127
+ <div className="border-t bg-white p-4">
128
+ {image && (
129
+ <div className="mb-3 relative inline-block">
130
+ <img
131
+ src={image}
132
+ alt="Preview"
133
+ className="rounded-lg max-w-full h-auto"
134
+ style={{ maxHeight: '100px' }}
135
+ />
136
+ <button
137
+ onClick={() => {
138
+ setImage(null);
139
+ if (fileInputRef.current) {
140
+ fileInputRef.current.value = '';
141
+ }
142
+
143
+ className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-600 transition-colors"
144
+ >
145
+ ×
146
+ </button>
147
+ </div>
148
+ )}
149
+
150
+ <form onSubmit={handleSubmit} className="flex space-x-2">
151
+ <input
152
+ type="file"
153
+ ref={fileInputRef}
154
+ onChange={handleImageUpload}
155
+ accept="image/*"
156
+ className="hidden"
157
+ id="image-upload"
158
+ />
159
+
160
+ <label
161
+ htmlFor="image-upload"
162
+ className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors cursor-pointer flex items-center justify-center"
163
+ >
164
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
165
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
166
+ </svg>
167
+ </label>
168
+
169
+ <input
170
+ type="text"
171
+ value={input}
172
+ onChange={(e) => setInput(e.target.value)}
173
+ placeholder="Type your message..."
174
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
175
+ disabled={isLoading}
176
+ />
177
+
178
+ <button
179
+ type="submit"
180
+ disabled={isLoading || (!input.trim() && !image)}
181
+ className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
182
+ >
183
+ {isLoading ? (
184
+ <div className="flex space-x-1">
185
+ <div className="w-1 h-4 bg-white rounded-full animate-pulse"></div>
186
+ <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
187
+ <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
188
+ </div>
189
+ ) : (
190
+ 'Send'
191
+ )}
192
+ </button>
193
+ </form>
194
+ </div>
195
  </div>
196
+ );
197
  }