akhaliq's picture
akhaliq HF Staff
🎨 Redesign from AnyCoder
4f76458 verified
raw
history blame
12.1 kB
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 ===
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-gray-50 text-gray-900 antialiased;
}
}
@layer components {
.chat-container {
@apply flex flex-col h-screen max-w-4xl mx-auto bg-white shadow-lg;
}
.message-bubble {
@apply rounded-2xl px-4 py-3 max-w-xs md:max-w-md break-words;
}
.user-bubble {
@apply bg-blue-600 text-white ml-auto;
}
.assistant-bubble {
@apply bg-gray-200 text-gray-800 mr-auto;
}
}
=== 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>
);
}