Spaces:
Build error
Build error
| FROM node:18-alpine AS base | |
| # Install dependencies only when needed | |
| FROM base AS deps | |
| RUN apk add --no-cache libc6-compat | |
| WORKDIR /app | |
| COPY package.json package-lock.json ./ | |
| RUN npm ci --only=production | |
| # Rebuild the source code only when needed | |
| FROM base AS builder | |
| WORKDIR /app | |
| COPY --from=deps /app/node_modules ./node_modules | |
| COPY . . | |
| ENV NEXT_TELEMETRY_DISABLED 1 | |
| RUN npm run build | |
| # Production image, copy all the files and run next | |
| FROM base AS runner | |
| WORKDIR /app | |
| ENV NODE_ENV production | |
| ENV NEXT_TELEMETRY_DISABLED 1 | |
| RUN addgroup --system --gid 1001 nodejs | |
| RUN adduser --system --uid 1001 nextjs | |
| COPY --from=builder /app/public ./public | |
| # Set the correct permission for prerender cache | |
| RUN mkdir .next | |
| RUN chown nextjs:nodejs .next | |
| # Automatically leverage output traces to reduce image size | |
| COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | |
| COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | |
| USER nextjs | |
| EXPOSE 7860 | |
| ENV PORT 7860 | |
| ENV HOSTNAME "0.0.0.0" | |
| CMD ["node", "server.js"] | |
| === package.json === | |
| { | |
| "name": "glm-4.6v-flash", | |
| "version": "0.1.0", | |
| "private": true, | |
| "scripts": { | |
| "dev": "next dev", | |
| "build": "next build", | |
| "start": "next start", | |
| "lint": "next lint" | |
| }, | |
| "dependencies": { | |
| "next": "14.1.0", | |
| "openai": "^4.28.0", | |
| "react": "^18.2.0", | |
| "react-dom": "^18.2.0" | |
| }, | |
| "devDependencies": { | |
| "@types/node": "^20.11.0", | |
| "@types/react": "^18.2.0", | |
| "@types/react-dom": "^18.2.0", | |
| "autoprefixer": "^10.4.16", | |
| "postcss": "^8.4.33", | |
| "tailwindcss": "^3.4.1", | |
| "typescript": "^5.3.3" | |
| } | |
| } | |
| === next.config.js === | |
| /** @type {import('next').NextConfig} */ | |
| const nextConfig = { | |
| experimental: { | |
| appDir: false, | |
| }, | |
| output: 'standalone', | |
| images: { | |
| domains: ['cdn.britannica.com', 'localhost'], | |
| }, | |
| } | |
| module.exports = nextConfig | |
| === pages/_app.js === | |
| import '../styles/globals.css' | |
| function MyApp({ Component, pageProps }) { | |
| return <Component {...pageProps} /> | |
| } | |
| export default MyApp | |
| === pages/index.js === | |
| import Head from 'next/head' | |
| import ChatInterface from '../components/ChatInterface' | |
| export default function Home() { | |
| return ( | |
| <> | |
| <Head> | |
| <title>GLM-4.6V-Flash</title> | |
| <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." /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| </Head> | |
| <main className="bg-gray-50 min-h-screen"> | |
| <div className="chat-container"> | |
| <header className="bg-blue-600 text-white p-4 shadow-md"> | |
| <div className="flex items-center justify-between"> | |
| <h1 className="text-xl font-bold">GLM-4.6V-Flash</h1> | |
| <a | |
| href="https://huggingface.co/spaces/akhaliq/anycoder" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-sm bg-white text-blue-600 px-3 py-1 rounded-full hover:bg-blue-50 transition-colors" | |
| > | |
| Built with anycoder | |
| </a> | |
| </div> | |
| </header> | |
| <ChatInterface /> | |
| </div> | |
| </main> | |
| </> | |
| ) | |
| } | |
| === postcss.config.js === | |
| module.exports = { | |
| plugins: { | |
| tailwindcss: {}, | |
| autoprefixer: {}, | |
| }, | |
| } | |
| === styles/globals.css === | |
| body { | |
| } | |
| } | |
| .chat-container { | |
| } | |
| .message-bubble { | |
| } | |
| .user-bubble { | |
| } | |
| .assistant-bubble { | |
| } | |
| } | |
| === tailwind.config.js === | |
| /** @type {import('tailwindcss').Config} */ | |
| module.exports = { | |
| content: [ | |
| './pages/**/*.{js,ts,jsx,tsx,mdx}', | |
| './components/**/*.{js,ts,jsx,tsx,mdx}', | |
| ], | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| animation: { | |
| 'pulse-dots': 'pulse-dots 1.4s infinite ease-in-out both', | |
| }, | |
| keyframes: { | |
| 'pulse-dots': { | |
| '0%, 80%, 100%': { transform: 'scale(0)', opacity: '0.5' }, | |
| '40%': { transform: 'scale(1)', opacity: '1' }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| plugins: [], | |
| } | |
| === pages/api/chat.js === | |
| import { OpenAI } from 'openai'; | |
| const openai = new OpenAI({ | |
| apiKey: process.env.OPENAI_API_KEY || 'sk-proj-12345', | |
| }); | |
| export default async function handler(req, res) { | |
| if (req.method !== 'POST') { | |
| return res.status(405).json({ error: 'Method not allowed' }); | |
| } | |
| try { | |
| const { messages, image } = req.body; | |
| const completion = await openai.chat.completions.create({ | |
| model: "gpt-4-vision-preview", | |
| messages: messages, | |
| max_tokens: 500, | |
| temperature: 0.7, | |
| }); | |
| const response = completion.choices[0]?.message?.content || "I'm sorry, I couldn't process that request."; | |
| res.status(200).json({ response }); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| res.status(500).json({ error: 'Failed to process request' }); | |
| } | |
| } | |
| === components/ChatInterface.jsx === | |
| import React, { useState, useRef, useEffect } from 'react'; | |
| export default function ChatInterface() { | |
| const [messages, setMessages] = useState([]); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [image, setImage] = useState(null); | |
| const messagesEndRef = useRef(null); | |
| const fileInputRef = useRef(null); | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| const handleImageUpload = (e) => { | |
| const file = e.target.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| setImage(reader.result); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }; | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!input.trim() && !image) return; | |
| const userMessage = { | |
| role: 'user', | |
| content: input, | |
| image: image, | |
| }; | |
| setMessages(prev => [...prev, userMessage]); | |
| setInput(''); | |
| setIsLoading(true); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| messages: [...messages, userMessage].map(msg => ({ | |
| role: msg.role, | |
| content: msg.content, | |
| })), | |
| image: image, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: data.response, | |
| }]); | |
| } else { | |
| throw new Error(data.error || 'Failed to get response'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: 'Sorry, I encountered an error. Please try again.', | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| setImage(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| } | |
| }; | |
| return ( | |
| <div className="flex-1 flex flex-col overflow-hidden"> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4"> | |
| {messages.length === 0 && ( | |
| <div className="text-center text-gray-500 mt-8"> | |
| <p className="text-lg">Welcome to GLM-4.6V-Flash</p> | |
| <p className="text-sm mt-2">Upload an image and ask me about it!</p> | |
| </div> | |
| )} | |
| {messages.map((message, index) => ( | |
| <div | |
| key={index} | |
| className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`} | |
| > | |
| <div className={`message-bubble ${message.role === 'user' ? 'user-bubble' : 'assistant-bubble'}`}> | |
| {message.image && ( | |
| <img | |
| src={message.image} | |
| alt="Uploaded" | |
| className="mb-2 rounded-lg max-w-full h-auto" | |
| style={{ maxHeight: '200px' }} | |
| /> | |
| )} | |
| <p className="whitespace-pre-wrap">{message.content}</p> | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="flex justify-start"> | |
| <div className="message-bubble assistant-bubble"> | |
| <div className="flex space-x-1"> | |
| <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots"></div> | |
| <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.2s' }}></div> | |
| <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.4s' }}></div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <div className="border-t bg-white p-4"> | |
| {image && ( | |
| <div className="mb-3 relative inline-block"> | |
| <img | |
| src={image} | |
| alt="Preview" | |
| className="rounded-lg max-w-full h-auto" | |
| style={{ maxHeight: '100px' }} | |
| /> | |
| <button | |
| onClick={() => { | |
| setImage(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| 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" | |
| > | |
| Γ | |
| </button> | |
| </div> | |
| )} | |
| <form onSubmit={handleSubmit} className="flex space-x-2"> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| onChange={handleImageUpload} | |
| accept="image/*" | |
| className="hidden" | |
| id="image-upload" | |
| /> | |
| <label | |
| htmlFor="image-upload" | |
| 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" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <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" /> | |
| </svg> | |
| </label> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| placeholder="Type your message..." | |
| 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" | |
| disabled={isLoading} | |
| /> | |
| <button | |
| type="submit" | |
| disabled={isLoading || (!input.trim() && !image)} | |
| 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" | |
| > | |
| {isLoading ? ( | |
| <div className="flex space-x-1"> | |
| <div className="w-1 h-4 bg-white rounded-full animate-pulse"></div> | |
| <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div> | |
| <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div> | |
| </div> | |
| ) : ( | |
| 'Send' | |
| )} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| ); | |
| } |