Spaces:
Running
Running
add image model support
Browse files- backend_api.py +42 -19
- frontend/src/app/page.tsx +24 -5
- frontend/src/components/ChatInterface.tsx +91 -3
- frontend/src/types/index.ts +3 -0
backend_api.py
CHANGED
|
@@ -98,19 +98,20 @@ def get_cached_client(model_id: str, provider: str = "auto"):
|
|
| 98 |
|
| 99 |
# Define models and languages here to avoid importing Gradio UI
|
| 100 |
AVAILABLE_MODELS = [
|
| 101 |
-
{"name": "
|
| 102 |
-
{"name": "DeepSeek
|
| 103 |
-
{"name": "
|
| 104 |
-
{"name": "
|
| 105 |
-
{"name": "
|
| 106 |
-
{"name": "
|
| 107 |
-
{"name": "GPT-5.1
|
| 108 |
-
{"name": "GPT-5.1
|
| 109 |
-
{"name": "
|
| 110 |
-
{"name": "Claude-
|
| 111 |
-
{"name": "Claude-
|
| 112 |
-
{"name": "
|
| 113 |
-
{"name": "
|
|
|
|
| 114 |
]
|
| 115 |
|
| 116 |
# Cache model lookup for faster access (built after AVAILABLE_MODELS is defined)
|
|
@@ -197,12 +198,13 @@ async def startup_event():
|
|
| 197 |
class CodeGenerationRequest(BaseModel):
|
| 198 |
query: str
|
| 199 |
language: str = "html"
|
| 200 |
-
model_id: str = "
|
| 201 |
provider: str = "auto"
|
| 202 |
history: List[List[str]] = []
|
| 203 |
agent_mode: bool = False
|
| 204 |
existing_repo_id: Optional[str] = None # For auto-deploy to update existing space
|
| 205 |
skip_auto_deploy: bool = False # Skip auto-deploy (for PR creation)
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
class DeploymentRequest(BaseModel):
|
|
@@ -779,11 +781,32 @@ async def generate_code(
|
|
| 779 |
actual_model_id = get_real_model_id(selected_model_id)
|
| 780 |
|
| 781 |
# Prepare messages (optimized - no string concatenation in hot path)
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
|
| 788 |
# Stream the response
|
| 789 |
try:
|
|
|
|
| 98 |
|
| 99 |
# Define models and languages here to avoid importing Gradio UI
|
| 100 |
AVAILABLE_MODELS = [
|
| 101 |
+
{"name": "GLM-4.6V 👁️", "id": "zai-org/GLM-4.6V:zai-org", "description": "GLM-4.6V vision model - supports image uploads for visual understanding (Default)", "supports_images": True},
|
| 102 |
+
{"name": "DeepSeek V3.2", "id": "deepseek-ai/DeepSeek-V3.2-Exp", "description": "DeepSeek V3.2 Experimental - Fast model for code generation via HuggingFace Router with Novita provider", "supports_images": False},
|
| 103 |
+
{"name": "DeepSeek R1", "id": "deepseek-ai/DeepSeek-R1-0528", "description": "DeepSeek R1 model for code generation", "supports_images": False},
|
| 104 |
+
{"name": "Gemini 3.0 Pro", "id": "gemini-3.0-pro", "description": "Google Gemini 3.0 Pro via Poe with advanced reasoning", "supports_images": False},
|
| 105 |
+
{"name": "Grok 4.1 Fast", "id": "x-ai/grok-4.1-fast", "description": "Grok 4.1 Fast model via OpenRouter (20 req/min on free tier)", "supports_images": False},
|
| 106 |
+
{"name": "MiniMax M2", "id": "MiniMaxAI/MiniMax-M2", "description": "MiniMax M2 model via HuggingFace InferenceClient with Novita provider", "supports_images": False},
|
| 107 |
+
{"name": "GPT-5.1", "id": "gpt-5.1", "description": "OpenAI GPT-5.1 model via Poe for advanced code generation and general tasks", "supports_images": False},
|
| 108 |
+
{"name": "GPT-5.1 Instant", "id": "gpt-5.1-instant", "description": "OpenAI GPT-5.1 Instant model via Poe for fast responses", "supports_images": False},
|
| 109 |
+
{"name": "GPT-5.1 Codex", "id": "gpt-5.1-codex", "description": "OpenAI GPT-5.1 Codex model via Poe optimized for code generation", "supports_images": False},
|
| 110 |
+
{"name": "Claude-Opus-4.5", "id": "claude-opus-4.5", "description": "Anthropic Claude Opus 4.5 via Poe (OpenAI-compatible)", "supports_images": False},
|
| 111 |
+
{"name": "Claude-Sonnet-4.5", "id": "claude-sonnet-4.5", "description": "Anthropic Claude Sonnet 4.5 via Poe (OpenAI-compatible)", "supports_images": False},
|
| 112 |
+
{"name": "Claude-Haiku-4.5", "id": "claude-haiku-4.5", "description": "Anthropic Claude Haiku 4.5 via Poe (OpenAI-compatible)", "supports_images": False},
|
| 113 |
+
{"name": "Kimi K2 Thinking", "id": "moonshotai/Kimi-K2-Thinking", "description": "Moonshot Kimi K2 Thinking model via HuggingFace with Together AI provider", "supports_images": False},
|
| 114 |
+
{"name": "GLM-4.6", "id": "zai-org/GLM-4.6", "description": "GLM-4.6 model via HuggingFace with Cerebras provider", "supports_images": False},
|
| 115 |
]
|
| 116 |
|
| 117 |
# Cache model lookup for faster access (built after AVAILABLE_MODELS is defined)
|
|
|
|
| 198 |
class CodeGenerationRequest(BaseModel):
|
| 199 |
query: str
|
| 200 |
language: str = "html"
|
| 201 |
+
model_id: str = "zai-org/GLM-4.6V:zai-org"
|
| 202 |
provider: str = "auto"
|
| 203 |
history: List[List[str]] = []
|
| 204 |
agent_mode: bool = False
|
| 205 |
existing_repo_id: Optional[str] = None # For auto-deploy to update existing space
|
| 206 |
skip_auto_deploy: bool = False # Skip auto-deploy (for PR creation)
|
| 207 |
+
image_url: Optional[str] = None # For vision models like GLM-4.6V
|
| 208 |
|
| 209 |
|
| 210 |
class DeploymentRequest(BaseModel):
|
|
|
|
| 781 |
actual_model_id = get_real_model_id(selected_model_id)
|
| 782 |
|
| 783 |
# Prepare messages (optimized - no string concatenation in hot path)
|
| 784 |
+
# Check if this is a vision model and we have an image
|
| 785 |
+
if request.image_url and selected_model_id == "zai-org/GLM-4.6V:zai-org":
|
| 786 |
+
# Vision model with image - use multi-modal format
|
| 787 |
+
user_content = [
|
| 788 |
+
{
|
| 789 |
+
"type": "text",
|
| 790 |
+
"text": f"Generate a {language} application: {query}"
|
| 791 |
+
},
|
| 792 |
+
{
|
| 793 |
+
"type": "image_url",
|
| 794 |
+
"image_url": {
|
| 795 |
+
"url": request.image_url
|
| 796 |
+
}
|
| 797 |
+
}
|
| 798 |
+
]
|
| 799 |
+
messages = [
|
| 800 |
+
{"role": "system", "content": system_prompt},
|
| 801 |
+
{"role": "user", "content": user_content}
|
| 802 |
+
]
|
| 803 |
+
else:
|
| 804 |
+
# Regular text-only model
|
| 805 |
+
user_content = f"Generate a {language} application: {query}"
|
| 806 |
+
messages = [
|
| 807 |
+
{"role": "system", "content": system_prompt},
|
| 808 |
+
{"role": "user", "content": user_content}
|
| 809 |
+
]
|
| 810 |
|
| 811 |
# Stream the response
|
| 812 |
try:
|
frontend/src/app/page.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import CodeEditor from '@/components/CodeEditor';
|
|
| 9 |
import ControlPanel from '@/components/ControlPanel';
|
| 10 |
import { apiClient } from '@/lib/api';
|
| 11 |
import { isAuthenticated as checkIsAuthenticated, getStoredToken } from '@/lib/auth';
|
| 12 |
-
import type { Message, Language, CodeGenerationRequest } from '@/types';
|
| 13 |
|
| 14 |
export default function Home() {
|
| 15 |
// Initialize messages as empty array (will load from localStorage in useEffect)
|
|
@@ -17,7 +17,8 @@ export default function Home() {
|
|
| 17 |
|
| 18 |
const [generatedCode, setGeneratedCode] = useState('');
|
| 19 |
const [selectedLanguage, setSelectedLanguage] = useState<Language>('html');
|
| 20 |
-
const [selectedModel, setSelectedModel] = useState('
|
|
|
|
| 21 |
const [isGenerating, setIsGenerating] = useState(false);
|
| 22 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
| 23 |
const [currentRepoId, setCurrentRepoId] = useState<string | null>(null); // Track imported/deployed space
|
|
@@ -43,14 +44,29 @@ export default function Home() {
|
|
| 43 |
console.log('[App] 🔵 currentRepoId changed to:', currentRepoId);
|
| 44 |
}, [currentRepoId]);
|
| 45 |
|
| 46 |
-
// Clear cache on app startup to ensure fresh data
|
| 47 |
useEffect(() => {
|
| 48 |
if (typeof window !== 'undefined') {
|
| 49 |
console.log('[Cache] Clearing models and languages cache on app startup');
|
| 50 |
localStorage.removeItem('anycoder_models');
|
| 51 |
localStorage.removeItem('anycoder_languages');
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
}, []); // Run once on mount
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
// Load messages from localStorage on mount (client-side only to avoid hydration issues)
|
| 56 |
useEffect(() => {
|
|
@@ -239,7 +255,7 @@ export default function Home() {
|
|
| 239 |
}
|
| 240 |
};
|
| 241 |
|
| 242 |
-
const handleSendMessage = async (message: string, overrideLanguage?: Language, overrideModel?: string, overrideRepoId?: string, shouldCreatePR?: boolean) => {
|
| 243 |
if (!isAuthenticated) {
|
| 244 |
alert('Please sign in with HuggingFace first! Click the "Sign in with Hugging Face" button in the header.');
|
| 245 |
return;
|
|
@@ -277,6 +293,7 @@ export default function Home() {
|
|
| 277 |
role: 'user',
|
| 278 |
content: message,
|
| 279 |
timestamp: new Date().toISOString(),
|
|
|
|
| 280 |
};
|
| 281 |
setMessages((prev) => [...prev, userMessage]);
|
| 282 |
setIsGenerating(true);
|
|
@@ -303,6 +320,7 @@ export default function Home() {
|
|
| 303 |
agent_mode: false,
|
| 304 |
existing_repo_id: effectiveRepoId, // Pass duplicated/imported space ID for auto-deploy
|
| 305 |
skip_auto_deploy: !!shouldCreatePR, // Skip auto-deploy if creating PR
|
|
|
|
| 306 |
};
|
| 307 |
|
| 308 |
const assistantMessage: Message = {
|
|
@@ -798,7 +816,7 @@ export default function Home() {
|
|
| 798 |
|
| 799 |
// Send the message with the selected language and model
|
| 800 |
// Don't pass repoId to handleSendMessage when creating PR (we want to generate code first, then create PR)
|
| 801 |
-
await handleSendMessage(prompt, language, modelId, shouldCreatePR ? undefined : repoId, shouldCreatePR);
|
| 802 |
};
|
| 803 |
|
| 804 |
// Resize handlers for chat sidebar (desktop only)
|
|
@@ -906,6 +924,7 @@ export default function Home() {
|
|
| 906 |
onSendMessage={handleSendMessage}
|
| 907 |
isGenerating={isGenerating}
|
| 908 |
isAuthenticated={isAuthenticated}
|
|
|
|
| 909 |
/>
|
| 910 |
</div>
|
| 911 |
</div>
|
|
|
|
| 9 |
import ControlPanel from '@/components/ControlPanel';
|
| 10 |
import { apiClient } from '@/lib/api';
|
| 11 |
import { isAuthenticated as checkIsAuthenticated, getStoredToken } from '@/lib/auth';
|
| 12 |
+
import type { Message, Language, CodeGenerationRequest, Model } from '@/types';
|
| 13 |
|
| 14 |
export default function Home() {
|
| 15 |
// Initialize messages as empty array (will load from localStorage in useEffect)
|
|
|
|
| 17 |
|
| 18 |
const [generatedCode, setGeneratedCode] = useState('');
|
| 19 |
const [selectedLanguage, setSelectedLanguage] = useState<Language>('html');
|
| 20 |
+
const [selectedModel, setSelectedModel] = useState('zai-org/GLM-4.6V:zai-org');
|
| 21 |
+
const [models, setModels] = useState<Model[]>([]);
|
| 22 |
const [isGenerating, setIsGenerating] = useState(false);
|
| 23 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
| 24 |
const [currentRepoId, setCurrentRepoId] = useState<string | null>(null); // Track imported/deployed space
|
|
|
|
| 44 |
console.log('[App] 🔵 currentRepoId changed to:', currentRepoId);
|
| 45 |
}, [currentRepoId]);
|
| 46 |
|
| 47 |
+
// Clear cache on app startup to ensure fresh data and load models
|
| 48 |
useEffect(() => {
|
| 49 |
if (typeof window !== 'undefined') {
|
| 50 |
console.log('[Cache] Clearing models and languages cache on app startup');
|
| 51 |
localStorage.removeItem('anycoder_models');
|
| 52 |
localStorage.removeItem('anycoder_languages');
|
| 53 |
+
|
| 54 |
+
// Load models for checking image support
|
| 55 |
+
loadModels();
|
| 56 |
}
|
| 57 |
}, []); // Run once on mount
|
| 58 |
+
|
| 59 |
+
const loadModels = async () => {
|
| 60 |
+
try {
|
| 61 |
+
const modelsList = await apiClient.getModels();
|
| 62 |
+
setModels(modelsList);
|
| 63 |
+
} catch (error) {
|
| 64 |
+
console.error('Failed to load models:', error);
|
| 65 |
+
}
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
// Check if current model supports images
|
| 69 |
+
const currentModelSupportsImages = models.find(m => m.id === selectedModel)?.supports_images || false;
|
| 70 |
|
| 71 |
// Load messages from localStorage on mount (client-side only to avoid hydration issues)
|
| 72 |
useEffect(() => {
|
|
|
|
| 255 |
}
|
| 256 |
};
|
| 257 |
|
| 258 |
+
const handleSendMessage = async (message: string, imageUrl?: string, overrideLanguage?: Language, overrideModel?: string, overrideRepoId?: string, shouldCreatePR?: boolean) => {
|
| 259 |
if (!isAuthenticated) {
|
| 260 |
alert('Please sign in with HuggingFace first! Click the "Sign in with Hugging Face" button in the header.');
|
| 261 |
return;
|
|
|
|
| 293 |
role: 'user',
|
| 294 |
content: message,
|
| 295 |
timestamp: new Date().toISOString(),
|
| 296 |
+
image_url: imageUrl,
|
| 297 |
};
|
| 298 |
setMessages((prev) => [...prev, userMessage]);
|
| 299 |
setIsGenerating(true);
|
|
|
|
| 320 |
agent_mode: false,
|
| 321 |
existing_repo_id: effectiveRepoId, // Pass duplicated/imported space ID for auto-deploy
|
| 322 |
skip_auto_deploy: !!shouldCreatePR, // Skip auto-deploy if creating PR
|
| 323 |
+
image_url: imageUrl, // For vision models like GLM-4.6V
|
| 324 |
};
|
| 325 |
|
| 326 |
const assistantMessage: Message = {
|
|
|
|
| 816 |
|
| 817 |
// Send the message with the selected language and model
|
| 818 |
// Don't pass repoId to handleSendMessage when creating PR (we want to generate code first, then create PR)
|
| 819 |
+
await handleSendMessage(prompt, undefined, language, modelId, shouldCreatePR ? undefined : repoId, shouldCreatePR);
|
| 820 |
};
|
| 821 |
|
| 822 |
// Resize handlers for chat sidebar (desktop only)
|
|
|
|
| 924 |
onSendMessage={handleSendMessage}
|
| 925 |
isGenerating={isGenerating}
|
| 926 |
isAuthenticated={isAuthenticated}
|
| 927 |
+
supportsImages={currentModelSupportsImages}
|
| 928 |
/>
|
| 929 |
</div>
|
| 930 |
</div>
|
frontend/src/components/ChatInterface.tsx
CHANGED
|
@@ -4,16 +4,21 @@ import { useState, useRef, useEffect } from 'react';
|
|
| 4 |
import type { Message } from '@/types';
|
| 5 |
import ReactMarkdown from 'react-markdown';
|
| 6 |
import remarkGfm from 'remark-gfm';
|
|
|
|
| 7 |
|
| 8 |
interface ChatInterfaceProps {
|
| 9 |
messages: Message[];
|
| 10 |
-
onSendMessage: (message: string) => void;
|
| 11 |
isGenerating: boolean;
|
| 12 |
isAuthenticated?: boolean;
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
-
export default function ChatInterface({ messages, onSendMessage, isGenerating, isAuthenticated = false }: ChatInterfaceProps) {
|
| 16 |
const [input, setInput] = useState('');
|
|
|
|
|
|
|
|
|
|
| 17 |
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 18 |
|
| 19 |
const scrollToBottom = () => {
|
|
@@ -27,8 +32,32 @@ export default function ChatInterface({ messages, onSendMessage, isGenerating, i
|
|
| 27 |
const handleSubmit = (e: React.FormEvent) => {
|
| 28 |
e.preventDefault();
|
| 29 |
if (input.trim() && !isGenerating) {
|
| 30 |
-
onSendMessage(input);
|
| 31 |
setInput('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
};
|
| 34 |
|
|
@@ -63,6 +92,18 @@ export default function ChatInterface({ messages, onSendMessage, isGenerating, i
|
|
| 63 |
: 'bg-[#2d2d2f] text-[#f5f5f7]'
|
| 64 |
}`}
|
| 65 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
<div className="text-sm leading-relaxed">
|
| 67 |
{message.role === 'assistant' ? (
|
| 68 |
<ReactMarkdown
|
|
@@ -92,6 +133,27 @@ export default function ChatInterface({ messages, onSendMessage, isGenerating, i
|
|
| 92 |
|
| 93 |
{/* Input */}
|
| 94 |
<div className="border-t border-[#424245]/30 p-3 bg-[#000000]">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
| 96 |
<input
|
| 97 |
type="text"
|
|
@@ -101,6 +163,32 @@ export default function ChatInterface({ messages, onSendMessage, isGenerating, i
|
|
| 101 |
disabled={isGenerating || !isAuthenticated}
|
| 102 |
className="flex-1 px-4 py-2.5 bg-[#2d2d2f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-full focus:outline-none focus:border-[#424245] disabled:opacity-40 disabled:cursor-not-allowed placeholder-[#86868b]"
|
| 103 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
<button
|
| 105 |
type="submit"
|
| 106 |
disabled={isGenerating || !input.trim() || !isAuthenticated}
|
|
|
|
| 4 |
import type { Message } from '@/types';
|
| 5 |
import ReactMarkdown from 'react-markdown';
|
| 6 |
import remarkGfm from 'remark-gfm';
|
| 7 |
+
import Image from 'next/image';
|
| 8 |
|
| 9 |
interface ChatInterfaceProps {
|
| 10 |
messages: Message[];
|
| 11 |
+
onSendMessage: (message: string, imageUrl?: string) => void;
|
| 12 |
isGenerating: boolean;
|
| 13 |
isAuthenticated?: boolean;
|
| 14 |
+
supportsImages?: boolean;
|
| 15 |
}
|
| 16 |
|
| 17 |
+
export default function ChatInterface({ messages, onSendMessage, isGenerating, isAuthenticated = false, supportsImages = false }: ChatInterfaceProps) {
|
| 18 |
const [input, setInput] = useState('');
|
| 19 |
+
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
| 20 |
+
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
| 21 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 22 |
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 23 |
|
| 24 |
const scrollToBottom = () => {
|
|
|
|
| 32 |
const handleSubmit = (e: React.FormEvent) => {
|
| 33 |
e.preventDefault();
|
| 34 |
if (input.trim() && !isGenerating) {
|
| 35 |
+
onSendMessage(input, uploadedImageUrl || undefined);
|
| 36 |
setInput('');
|
| 37 |
+
setUploadedImageUrl(null);
|
| 38 |
+
setUploadedImageFile(null);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 43 |
+
const file = e.target.files?.[0];
|
| 44 |
+
if (file) {
|
| 45 |
+
// Create a data URL for the image
|
| 46 |
+
const reader = new FileReader();
|
| 47 |
+
reader.onload = (event) => {
|
| 48 |
+
const imageUrl = event.target?.result as string;
|
| 49 |
+
setUploadedImageUrl(imageUrl);
|
| 50 |
+
setUploadedImageFile(file);
|
| 51 |
+
};
|
| 52 |
+
reader.readAsDataURL(file);
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
const removeImage = () => {
|
| 57 |
+
setUploadedImageUrl(null);
|
| 58 |
+
setUploadedImageFile(null);
|
| 59 |
+
if (fileInputRef.current) {
|
| 60 |
+
fileInputRef.current.value = '';
|
| 61 |
}
|
| 62 |
};
|
| 63 |
|
|
|
|
| 92 |
: 'bg-[#2d2d2f] text-[#f5f5f7]'
|
| 93 |
}`}
|
| 94 |
>
|
| 95 |
+
{message.image_url && message.role === 'user' && (
|
| 96 |
+
<div className="mb-2">
|
| 97 |
+
<Image
|
| 98 |
+
src={message.image_url}
|
| 99 |
+
alt="Uploaded image"
|
| 100 |
+
width={200}
|
| 101 |
+
height={200}
|
| 102 |
+
className="rounded-lg object-cover max-w-full h-auto"
|
| 103 |
+
unoptimized
|
| 104 |
+
/>
|
| 105 |
+
</div>
|
| 106 |
+
)}
|
| 107 |
<div className="text-sm leading-relaxed">
|
| 108 |
{message.role === 'assistant' ? (
|
| 109 |
<ReactMarkdown
|
|
|
|
| 133 |
|
| 134 |
{/* Input */}
|
| 135 |
<div className="border-t border-[#424245]/30 p-3 bg-[#000000]">
|
| 136 |
+
{/* Image Preview */}
|
| 137 |
+
{uploadedImageUrl && (
|
| 138 |
+
<div className="mb-2 relative inline-block">
|
| 139 |
+
<Image
|
| 140 |
+
src={uploadedImageUrl}
|
| 141 |
+
alt="Upload preview"
|
| 142 |
+
width={120}
|
| 143 |
+
height={120}
|
| 144 |
+
className="rounded-lg object-cover"
|
| 145 |
+
unoptimized
|
| 146 |
+
/>
|
| 147 |
+
<button
|
| 148 |
+
type="button"
|
| 149 |
+
onClick={removeImage}
|
| 150 |
+
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all flex items-center justify-center text-xs font-bold"
|
| 151 |
+
>
|
| 152 |
+
×
|
| 153 |
+
</button>
|
| 154 |
+
</div>
|
| 155 |
+
)}
|
| 156 |
+
|
| 157 |
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
| 158 |
<input
|
| 159 |
type="text"
|
|
|
|
| 163 |
disabled={isGenerating || !isAuthenticated}
|
| 164 |
className="flex-1 px-4 py-2.5 bg-[#2d2d2f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-full focus:outline-none focus:border-[#424245] disabled:opacity-40 disabled:cursor-not-allowed placeholder-[#86868b]"
|
| 165 |
/>
|
| 166 |
+
|
| 167 |
+
{/* Image Upload Button (only show if model supports images) */}
|
| 168 |
+
{supportsImages && (
|
| 169 |
+
<>
|
| 170 |
+
<input
|
| 171 |
+
ref={fileInputRef}
|
| 172 |
+
type="file"
|
| 173 |
+
accept="image/*"
|
| 174 |
+
onChange={handleImageUpload}
|
| 175 |
+
className="hidden"
|
| 176 |
+
disabled={isGenerating || !isAuthenticated}
|
| 177 |
+
/>
|
| 178 |
+
<button
|
| 179 |
+
type="button"
|
| 180 |
+
onClick={() => fileInputRef.current?.click()}
|
| 181 |
+
disabled={isGenerating || !isAuthenticated}
|
| 182 |
+
className="p-2.5 bg-[#2d2d2f] text-[#f5f5f7] rounded-full hover:bg-[#424245] disabled:opacity-40 disabled:cursor-not-allowed transition-all active:scale-95 flex-shrink-0"
|
| 183 |
+
title="Upload image"
|
| 184 |
+
>
|
| 185 |
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 186 |
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
| 187 |
+
</svg>
|
| 188 |
+
</button>
|
| 189 |
+
</>
|
| 190 |
+
)}
|
| 191 |
+
|
| 192 |
<button
|
| 193 |
type="submit"
|
| 194 |
disabled={isGenerating || !input.trim() || !isAuthenticated}
|
frontend/src/types/index.ts
CHANGED
|
@@ -4,12 +4,14 @@ export interface Model {
|
|
| 4 |
name: string;
|
| 5 |
id: string;
|
| 6 |
description: string;
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
export interface Message {
|
| 10 |
role: 'user' | 'assistant' | 'system';
|
| 11 |
content: string;
|
| 12 |
timestamp?: string;
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
export interface CodeGenerationRequest {
|
|
@@ -21,6 +23,7 @@ export interface CodeGenerationRequest {
|
|
| 21 |
agent_mode: boolean;
|
| 22 |
existing_repo_id?: string; // For auto-deploy to update existing space
|
| 23 |
skip_auto_deploy?: boolean; // Skip auto-deploy (for PR creation)
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
export interface CodeGenerationResponse {
|
|
|
|
| 4 |
name: string;
|
| 5 |
id: string;
|
| 6 |
description: string;
|
| 7 |
+
supports_images?: boolean;
|
| 8 |
}
|
| 9 |
|
| 10 |
export interface Message {
|
| 11 |
role: 'user' | 'assistant' | 'system';
|
| 12 |
content: string;
|
| 13 |
timestamp?: string;
|
| 14 |
+
image_url?: string; // For vision models
|
| 15 |
}
|
| 16 |
|
| 17 |
export interface CodeGenerationRequest {
|
|
|
|
| 23 |
agent_mode: boolean;
|
| 24 |
existing_repo_id?: string; // For auto-deploy to update existing space
|
| 25 |
skip_auto_deploy?: boolean; // Skip auto-deploy (for PR creation)
|
| 26 |
+
image_url?: string; // For vision models like GLM-4.6V
|
| 27 |
}
|
| 28 |
|
| 29 |
export interface CodeGenerationResponse {
|