akhaliq HF Staff commited on
Commit
4f76458
Β·
verified Β·
1 Parent(s): 34a952d

🎨 Redesign from AnyCoder

Browse files

This Pull Request contains a redesigned version of the app with:

- ✨ Modern, mobile-friendly design
- 🎯 Minimal, clean components
- πŸ“± Responsive layout
- πŸš€ Improved user experience

Generated by [AnyCoder](https://huggingface.co/spaces/akhaliq/anycoder)

Files changed (1) hide show
  1. app.py +423 -0
app.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+
8
+ COPY package.json package-lock.json ./
9
+ RUN npm ci --only=production
10
+
11
+ # Rebuild the source code only when needed
12
+ FROM base AS builder
13
+ WORKDIR /app
14
+ COPY --from=deps /app/node_modules ./node_modules
15
+ COPY . .
16
+
17
+ ENV NEXT_TELEMETRY_DISABLED 1
18
+ RUN npm run build
19
+
20
+ # Production image, copy all the files and run next
21
+ FROM base AS runner
22
+ WORKDIR /app
23
+
24
+ ENV NODE_ENV production
25
+ ENV NEXT_TELEMETRY_DISABLED 1
26
+
27
+ RUN addgroup --system --gid 1001 nodejs
28
+ RUN adduser --system --uid 1001 nextjs
29
+
30
+ COPY --from=builder /app/public ./public
31
+
32
+ # Set the correct permission for prerender cache
33
+ RUN mkdir .next
34
+ RUN chown nextjs:nodejs .next
35
+
36
+ # Automatically leverage output traces to reduce image size
37
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
38
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
39
+
40
+ USER nextjs
41
+
42
+ EXPOSE 7860
43
+
44
+ ENV PORT 7860
45
+ ENV HOSTNAME "0.0.0.0"
46
+
47
+ CMD ["node", "server.js"]
48
+
49
+ === package.json ===
50
+ {
51
+ "name": "glm-4.6v-flash",
52
+ "version": "0.1.0",
53
+ "private": true,
54
+ "scripts": {
55
+ "dev": "next dev",
56
+ "build": "next build",
57
+ "start": "next start",
58
+ "lint": "next lint"
59
+ },
60
+ "dependencies": {
61
+ "next": "14.1.0",
62
+ "openai": "^4.28.0",
63
+ "react": "^18.2.0",
64
+ "react-dom": "^18.2.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^20.11.0",
68
+ "@types/react": "^18.2.0",
69
+ "@types/react-dom": "^18.2.0",
70
+ "autoprefixer": "^10.4.16",
71
+ "postcss": "^8.4.33",
72
+ "tailwindcss": "^3.4.1",
73
+ "typescript": "^5.3.3"
74
+ }
75
+ }
76
+
77
+ === next.config.js ===
78
+ /** @type {import('next').NextConfig} */
79
+ const nextConfig = {
80
+ experimental: {
81
+ appDir: false,
82
+ },
83
+ output: 'standalone',
84
+ images: {
85
+ domains: ['cdn.britannica.com', 'localhost'],
86
+ },
87
+ }
88
+
89
+ module.exports = nextConfig
90
+
91
+ === pages/_app.js ===
92
+ import '../styles/globals.css'
93
+
94
+ function MyApp({ Component, pageProps }) {
95
+ return <Component {...pageProps} />
96
+ }
97
+
98
+ export default MyApp
99
+
100
+ === pages/index.js ===
101
+ import Head from 'next/head'
102
+ import ChatInterface from '../components/ChatInterface'
103
+
104
+ export default function Home() {
105
+ return (
106
+ <>
107
+ <Head>
108
+ <title>GLM-4.6V-Flash</title>
109
+ <meta name="description" content="GLM-4.6V series model includes two versions: GLM-4.6V (106B), a foundation model designed for cloud and high-performance cluster scenarios, and GLM-4.6V-Flash (9B), a lightweight model optimized for local deployment and low-latency applications." />
110
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
111
+ </Head>
112
+ <main className="bg-gray-50 min-h-screen">
113
+ <div className="chat-container">
114
+ <header className="bg-blue-600 text-white p-4 shadow-md">
115
+ <div className="flex items-center justify-between">
116
+ <h1 className="text-xl font-bold">GLM-4.6V-Flash</h1>
117
+ <a
118
+ href="https://huggingface.co/spaces/akhaliq/anycoder"
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ className="text-sm bg-white text-blue-600 px-3 py-1 rounded-full hover:bg-blue-50 transition-colors"
122
+ >
123
+ Built with anycoder
124
+ </a>
125
+ </div>
126
+ </header>
127
+ <ChatInterface />
128
+ </div>
129
+ </main>
130
+ </>
131
+ )
132
+ }
133
+
134
+ === postcss.config.js ===
135
+ module.exports = {
136
+ plugins: {
137
+ tailwindcss: {},
138
+ autoprefixer: {},
139
+ },
140
+ }
141
+
142
+ === styles/globals.css ===
143
+ @tailwind base;
144
+ @tailwind components;
145
+ @tailwind utilities;
146
+
147
+ @layer base {
148
+ body {
149
+ @apply bg-gray-50 text-gray-900 antialiased;
150
+ }
151
+ }
152
+
153
+ @layer components {
154
+ .chat-container {
155
+ @apply flex flex-col h-screen max-w-4xl mx-auto bg-white shadow-lg;
156
+ }
157
+
158
+ .message-bubble {
159
+ @apply rounded-2xl px-4 py-3 max-w-xs md:max-w-md break-words;
160
+ }
161
+
162
+ .user-bubble {
163
+ @apply bg-blue-600 text-white ml-auto;
164
+ }
165
+
166
+ .assistant-bubble {
167
+ @apply bg-gray-200 text-gray-800 mr-auto;
168
+ }
169
+ }
170
+
171
+ === tailwind.config.js ===
172
+ /** @type {import('tailwindcss').Config} */
173
+ module.exports = {
174
+ content: [
175
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
176
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
177
+ ],
178
+ darkMode: 'class',
179
+ theme: {
180
+ extend: {
181
+ animation: {
182
+ 'pulse-dots': 'pulse-dots 1.4s infinite ease-in-out both',
183
+ },
184
+ keyframes: {
185
+ 'pulse-dots': {
186
+ '0%, 80%, 100%': { transform: 'scale(0)', opacity: '0.5' },
187
+ '40%': { transform: 'scale(1)', opacity: '1' },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ plugins: [],
193
+ }
194
+
195
+ === pages/api/chat.js ===
196
+ import { OpenAI } from 'openai';
197
+
198
+ const openai = new OpenAI({
199
+ apiKey: process.env.OPENAI_API_KEY || 'sk-proj-12345',
200
+ });
201
+
202
+ export default async function handler(req, res) {
203
+ if (req.method !== 'POST') {
204
+ return res.status(405).json({ error: 'Method not allowed' });
205
+ }
206
+
207
+ try {
208
+ const { messages, image } = req.body;
209
+
210
+ const completion = await openai.chat.completions.create({
211
+ model: "gpt-4-vision-preview",
212
+ messages: messages,
213
+ max_tokens: 500,
214
+ temperature: 0.7,
215
+ });
216
+
217
+ const response = completion.choices[0]?.message?.content || "I'm sorry, I couldn't process that request.";
218
+
219
+ res.status(200).json({ response });
220
+ } catch (error) {
221
+ console.error('Error:', error);
222
+ res.status(500).json({ error: 'Failed to process request' });
223
+ }
224
+ }
225
+
226
+ === components/ChatInterface.jsx ===
227
+ import React, { useState, useRef, useEffect } from 'react';
228
+
229
+ export default function ChatInterface() {
230
+ const [messages, setMessages] = useState([]);
231
+ const [input, setInput] = useState('');
232
+ const [isLoading, setIsLoading] = useState(false);
233
+ const [image, setImage] = useState(null);
234
+ const messagesEndRef = useRef(null);
235
+ const fileInputRef = useRef(null);
236
+
237
+ const scrollToBottom = () => {
238
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
239
+ };
240
+
241
+ useEffect(() => {
242
+ scrollToBottom();
243
+ }, [messages]);
244
+
245
+ const handleImageUpload = (e) => {
246
+ const file = e.target.files[0];
247
+ if (file && file.type.startsWith('image/')) {
248
+ const reader = new FileReader();
249
+ reader.onloadend = () => {
250
+ setImage(reader.result);
251
+ };
252
+ reader.readAsDataURL(file);
253
+ }
254
+ };
255
+
256
+ const handleSubmit = async (e) => {
257
+ e.preventDefault();
258
+ if (!input.trim() && !image) return;
259
+
260
+ const userMessage = {
261
+ role: 'user',
262
+ content: input,
263
+ image: image,
264
+ };
265
+
266
+ setMessages(prev => [...prev, userMessage]);
267
+ setInput('');
268
+ setIsLoading(true);
269
+
270
+ try {
271
+ const response = await fetch('/api/chat', {
272
+ method: 'POST',
273
+ headers: {
274
+ 'Content-Type': 'application/json',
275
+ },
276
+ body: JSON.stringify({
277
+ messages: [...messages, userMessage].map(msg => ({
278
+ role: msg.role,
279
+ content: msg.content,
280
+ })),
281
+ image: image,
282
+ }),
283
+ });
284
+
285
+ const data = await response.json();
286
+
287
+ if (response.ok) {
288
+ setMessages(prev => [...prev, {
289
+ role: 'assistant',
290
+ content: data.response,
291
+ }]);
292
+ } else {
293
+ throw new Error(data.error || 'Failed to get response');
294
+ }
295
+ } catch (error) {
296
+ console.error('Error:', error);
297
+ setMessages(prev => [...prev, {
298
+ role: 'assistant',
299
+ content: 'Sorry, I encountered an error. Please try again.',
300
+ }]);
301
+ } finally {
302
+ setIsLoading(false);
303
+ setImage(null);
304
+ if (fileInputRef.current) {
305
+ fileInputRef.current.value = '';
306
+ }
307
+ }
308
+ };
309
+
310
+ return (
311
+ <div className="flex-1 flex flex-col overflow-hidden">
312
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
313
+ {messages.length === 0 && (
314
+ <div className="text-center text-gray-500 mt-8">
315
+ <p className="text-lg">Welcome to GLM-4.6V-Flash</p>
316
+ <p className="text-sm mt-2">Upload an image and ask me about it!</p>
317
+ </div>
318
+ )}
319
+
320
+ {messages.map((message, index) => (
321
+ <div
322
+ key={index}
323
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
324
+ >
325
+ <div className={`message-bubble ${message.role === 'user' ? 'user-bubble' : 'assistant-bubble'}`}>
326
+ {message.image && (
327
+ <img
328
+ src={message.image}
329
+ alt="Uploaded"
330
+ className="mb-2 rounded-lg max-w-full h-auto"
331
+ style={{ maxHeight: '200px' }}
332
+ />
333
+ )}
334
+ <p className="whitespace-pre-wrap">{message.content}</p>
335
+ </div>
336
+ </div>
337
+ ))}
338
+
339
+ {isLoading && (
340
+ <div className="flex justify-start">
341
+ <div className="message-bubble assistant-bubble">
342
+ <div className="flex space-x-1">
343
+ <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots"></div>
344
+ <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.2s' }}></div>
345
+ <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.4s' }}></div>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ )}
350
+ <div ref={messagesEndRef} />
351
+ </div>
352
+
353
+ <div className="border-t bg-white p-4">
354
+ {image && (
355
+ <div className="mb-3 relative inline-block">
356
+ <img
357
+ src={image}
358
+ alt="Preview"
359
+ className="rounded-lg max-w-full h-auto"
360
+ style={{ maxHeight: '100px' }}
361
+ />
362
+ <button
363
+ onClick={() => {
364
+ setImage(null);
365
+ if (fileInputRef.current) {
366
+ fileInputRef.current.value = '';
367
+ }
368
+
369
+ 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"
370
+ >
371
+ Γ—
372
+ </button>
373
+ </div>
374
+ )}
375
+
376
+ <form onSubmit={handleSubmit} className="flex space-x-2">
377
+ <input
378
+ type="file"
379
+ ref={fileInputRef}
380
+ onChange={handleImageUpload}
381
+ accept="image/*"
382
+ className="hidden"
383
+ id="image-upload"
384
+ />
385
+
386
+ <label
387
+ htmlFor="image-upload"
388
+ 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"
389
+ >
390
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
391
+ <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" />
392
+ </svg>
393
+ </label>
394
+
395
+ <input
396
+ type="text"
397
+ value={input}
398
+ onChange={(e) => setInput(e.target.value)}
399
+ placeholder="Type your message..."
400
+ 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"
401
+ disabled={isLoading}
402
+ />
403
+
404
+ <button
405
+ type="submit"
406
+ disabled={isLoading || (!input.trim() && !image)}
407
+ 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"
408
+ >
409
+ {isLoading ? (
410
+ <div className="flex space-x-1">
411
+ <div className="w-1 h-4 bg-white rounded-full animate-pulse"></div>
412
+ <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
413
+ <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
414
+ </div>
415
+ ) : (
416
+ 'Send'
417
+ )}
418
+ </button>
419
+ </form>
420
+ </div>
421
+ </div>
422
+ );
423
+ }