Semnykcz commited on
Commit
a2d424a
·
verified ·
1 Parent(s): ac5ebc8

Upload 11 files

Browse files
Files changed (9) hide show
  1. LICENSE +21 -0
  2. app.py +25 -183
  3. public/app.js +147 -211
  4. public/index.html +18 -9
  5. public/styles.css +136 -270
  6. readme.md +108 -63
  7. requirements.txt +3 -5
  8. utils/api_compat.py +81 -0
  9. utils/conversation.py +65 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AI Chat Application
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
app.py CHANGED
@@ -10,138 +10,28 @@ import sys
10
  import json
11
  import logging
12
  import time
13
- from typing import Optional, Dict, Any, Generator
14
  import torch
15
  from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
16
  import gradio as gr
17
  from fastapi import FastAPI, HTTPException, Response
18
  from fastapi.responses import StreamingResponse
19
- from pydantic import BaseModel
 
20
  import redis
21
  import asyncio
22
  import threading
23
  from threading import Thread
24
 
 
 
 
 
 
25
  # Configure logging
26
  logging.basicConfig(level=logging.INFO)
27
  logger = logging.getLogger(__name__)
28
 
29
- # Model configuration
30
- MODEL_NAME = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
31
- DEFAULT_MAX_TOKENS = 1024
32
- DEFAULT_TEMPERATURE = 0.7
33
-
34
- class ConversationManager:
35
- """Manage conversation history and caching"""
36
-
37
- def __init__(self):
38
- self.redis_client = None
39
- try:
40
- self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
41
- self.redis_client.ping()
42
- except:
43
- logger.warning("Redis not available, using in-memory storage")
44
- self.conversations = {}
45
-
46
- def save_conversation(self, conv_id: str, messages: list) -> None:
47
- """Save conversation to cache"""
48
- try:
49
- if self.redis_client:
50
- self.redis_client.setex(conv_id, 86400, json.dumps(messages)) # 24 hours expiry
51
- else:
52
- self.conversations[conv_id] = messages
53
- except Exception as e:
54
- logger.error(f"Error saving conversation: {e}")
55
-
56
- def load_conversation(self, conv_id: str) -> list:
57
- """Load conversation from cache"""
58
- try:
59
- if self.redis_client:
60
- data = self.redis_client.get(conv_id)
61
- if data:
62
- return json.loads(data)
63
- else:
64
- return self.conversations.get(conv_id, [])
65
- except Exception as e:
66
- logger.error(f"Error loading conversation: {e}")
67
- return []
68
-
69
- class ModelManager:
70
- """Manage Qwen model loading and inference"""
71
-
72
- def __init__(self):
73
- self.model = None
74
- self.tokenizer = None
75
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
76
- self.load_model()
77
-
78
- def load_model(self) -> None:
79
- """Load the Qwen model"""
80
- try:
81
- logger.info(f"Loading model {MODEL_NAME} on {self.device}")
82
- self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
83
- self.model = AutoModelForCausalLM.from_pretrained(
84
- MODEL_NAME,
85
- torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
86
- low_cpu_mem_usage=True,
87
- device_map="auto"
88
- )
89
- logger.info("Model loaded successfully")
90
- except Exception as e:
91
- logger.error(f"Error loading model: {e}")
92
- raise
93
-
94
- def generate_response(self, prompt: str, max_tokens: int = DEFAULT_MAX_TOKENS, temperature: float = DEFAULT_TEMPERATURE) -> str:
95
- """Generate response from the model"""
96
- try:
97
- inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
98
-
99
- # Generate without streaming for simple response
100
- generated = self.model.generate(
101
- **inputs,
102
- max_new_tokens=max_tokens,
103
- temperature=temperature,
104
- do_sample=True,
105
- pad_token_id=self.tokenizer.eos_token_id
106
- )
107
-
108
- response = self.tokenizer.decode(generated[0], skip_special_tokens=True)
109
- # Remove the prompt from the response
110
- response = response[len(prompt):].strip()
111
- return response
112
- except Exception as e:
113
- logger.error(f"Error generating response: {e}")
114
- raise
115
-
116
- def generate_streaming_response(self, prompt: str, max_tokens: int = DEFAULT_MAX_TOKENS, temperature: float = DEFAULT_TEMPERATURE) -> Generator[str, None, None]:
117
- """Generate streaming response from the model"""
118
- try:
119
- inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
120
-
121
- # Create streamer for streaming response
122
- streamer = TextIteratorStreamer(self.tokenizer, skip_prompt=True, skip_special_tokens=True)
123
-
124
- # Start generation in a separate thread
125
- generation_kwargs = dict(
126
- inputs,
127
- streamer=streamer,
128
- max_new_tokens=max_tokens,
129
- temperature=temperature,
130
- do_sample=True,
131
- pad_token_id=self.tokenizer.eos_token_id
132
- )
133
-
134
- thread = Thread(target=self.model.generate, kwargs=generation_kwargs)
135
- thread.start()
136
-
137
- # Yield tokens as they are generated
138
- for new_text in streamer:
139
- yield new_text
140
-
141
- except Exception as e:
142
- logger.error(f"Error generating streaming response: {e}")
143
- yield f"Error: {str(e)}"
144
-
145
  # Initialize managers
146
  conversation_manager = ConversationManager()
147
  model_manager = ModelManager()
@@ -149,64 +39,34 @@ model_manager = ModelManager()
149
  # FastAPI app for OPENAI API compatibility
150
  app = FastAPI(title="AI Chat API", description="OPENAI API compatible interface for Qwen model")
151
 
152
- class ChatMessage(BaseModel):
153
- role: str
154
- content: str
155
-
156
- class ChatRequest(BaseModel):
157
- messages: list[ChatMessage]
158
- model: str = MODEL_NAME
159
- max_tokens: Optional[int] = DEFAULT_MAX_TOKENS
160
- temperature: Optional[float] = DEFAULT_TEMPERATURE
161
 
162
- class ChatResponse(BaseModel):
163
- id: str
164
- object: str = "chat.completion"
165
- created: int
166
- model: str
167
- choices: list
168
- usage: Dict[str, int]
169
 
170
  @app.post("/v1/chat/completions", response_model=ChatResponse)
171
  async def chat_completion(request: ChatRequest):
172
  """OPENAI API compatible chat completion endpoint"""
173
  try:
174
  # Convert messages to prompt
175
- prompt = ""
176
- for msg in request.messages:
177
- if msg.role == "system":
178
- prompt += f"System: {msg.content}\n"
179
- elif msg.role == "user":
180
- prompt += f"User: {msg.content}\n"
181
- elif msg.role == "assistant":
182
- prompt += f"Assistant: {msg.content}\n"
183
 
184
  # Generate response
185
  response_text = model_manager.generate_response(
186
  prompt,
187
- request.max_tokens or DEFAULT_MAX_TOKENS,
188
- request.temperature or DEFAULT_TEMPERATURE
189
  )
190
 
191
  # Return in OPENAI format
192
- return ChatResponse(
193
- id="chatcmpl-" + str(hash(prompt))[:10],
194
- created=int(time.time()),
195
- model=request.model,
196
- choices=[{
197
- "index": 0,
198
- "message": {
199
- "role": "assistant",
200
- "content": response_text
201
- },
202
- "finish_reason": "stop"
203
- }],
204
- usage={
205
- "prompt_tokens": len(prompt.split()),
206
- "completion_tokens": len(response_text.split()),
207
- "total_tokens": len(prompt.split()) + len(response_text.split())
208
- }
209
- )
210
  except Exception as e:
211
  logger.error(f"Error in chat completion: {e}")
212
  raise HTTPException(status_code=500, detail=str(e))
@@ -262,30 +122,12 @@ gradio_interface = gr.ChatInterface(
262
  cache_examples=False
263
  )
264
 
265
- # Serve static files
266
- from fastapi.staticfiles import StaticFiles
267
 
268
- # Combine FastAPI and Gradio
269
  def launch_app():
270
  """Launch the combined FastAPI and Gradio app"""
271
- from fastapi.middleware.cors import CORSMiddleware
272
-
273
- # Add CORS middleware
274
- app.add_middleware(
275
- CORSMiddleware,
276
- allow_origins=["*"],
277
- allow_credentials=True,
278
- allow_methods=["*"],
279
- allow_headers=["*"],
280
- )
281
-
282
- # Mount static files
283
- app.mount("/public", StaticFiles(directory="public"), name="public")
284
-
285
- # Mount Gradio interface
286
- app.mount("/", gradio_interface.app)
287
-
288
- # Run the app
289
  import uvicorn
290
  uvicorn.run(app, host="0.0.0.0", port=7860)
291
 
 
10
  import json
11
  import logging
12
  import time
13
+ from typing import Optional, Dict, Any, Generator, List
14
  import torch
15
  from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
16
  import gradio as gr
17
  from fastapi import FastAPI, HTTPException, Response
18
  from fastapi.responses import StreamingResponse
19
+ from fastapi.staticfiles import StaticFiles
20
+ from fastapi.middleware.cors import CORSMiddleware
21
  import redis
22
  import asyncio
23
  import threading
24
  from threading import Thread
25
 
26
+ # Import utility modules
27
+ from utils.model_utils import ModelManager
28
+ from utils.conversation import ConversationManager
29
+ from utils.api_compat import ChatRequest, ChatResponse, convert_openai_request_to_model_input, create_openai_response, format_messages_for_frontend
30
+
31
  # Configure logging
32
  logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger(__name__)
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # Initialize managers
36
  conversation_manager = ConversationManager()
37
  model_manager = ModelManager()
 
39
  # FastAPI app for OPENAI API compatibility
40
  app = FastAPI(title="AI Chat API", description="OPENAI API compatible interface for Qwen model")
41
 
42
+ # Add CORS middleware
43
+ app.add_middleware(
44
+ CORSMiddleware,
45
+ allow_origins=["*"],
46
+ allow_credentials=True,
47
+ allow_methods=["*"],
48
+ allow_headers=["*"],
49
+ )
 
50
 
51
+ # Mount static files
52
+ app.mount("/public", StaticFiles(directory="public"), name="public")
 
 
 
 
 
53
 
54
  @app.post("/v1/chat/completions", response_model=ChatResponse)
55
  async def chat_completion(request: ChatRequest):
56
  """OPENAI API compatible chat completion endpoint"""
57
  try:
58
  # Convert messages to prompt
59
+ prompt = convert_openai_request_to_model_input(request)
 
 
 
 
 
 
 
60
 
61
  # Generate response
62
  response_text = model_manager.generate_response(
63
  prompt,
64
+ request.max_tokens or 1024,
65
+ request.temperature or 0.7
66
  )
67
 
68
  # Return in OPENAI format
69
+ return create_openai_response(response_text, request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  except Exception as e:
71
  logger.error(f"Error in chat completion: {e}")
72
  raise HTTPException(status_code=500, detail=str(e))
 
122
  cache_examples=False
123
  )
124
 
125
+ # Mount Gradio interface
126
+ app.mount("/", gradio_interface.app)
127
 
128
+ # Run the app
129
  def launch_app():
130
  """Launch the combined FastAPI and Gradio app"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  import uvicorn
132
  uvicorn.run(app, host="0.0.0.0", port=7860)
133
 
public/app.js CHANGED
@@ -1,228 +1,164 @@
1
- // AI Chat Application JavaScript Logic
2
- // This file contains the React component for the chat interface
3
-
4
- // Main App component
5
- function App() {
6
- const [messages, setMessages] = React.useState([]);
7
- const [inputValue, setInputValue] = React.useState('');
8
- const [isLoading, setIsLoading] = React.useState(false);
9
- const [darkMode, setDarkMode] = React.useState(false);
10
- const messagesEndRef = React.useRef(null);
11
-
12
- // Scroll to bottom of messages
13
- const scrollToBottom = () => {
14
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
15
- };
16
-
17
- // Scroll to bottom when messages change
18
- React.useEffect(() => {
19
- scrollToBottom();
20
- }, [messages]);
21
-
22
- // Toggle dark mode
23
- const toggleDarkMode = () => {
24
- setDarkMode(!darkMode);
25
- document.documentElement.classList.toggle('dark', !darkMode);
26
- };
27
-
28
- // Handle input change
29
- const handleInputChange = (e) => {
30
- setInputValue(e.target.value);
31
- };
32
-
33
- // Handle form submission
34
- const handleSubmit = async (e) => {
35
- e.preventDefault();
36
- if (!inputValue.trim() || isLoading) return;
37
-
38
- // Add user message to chat
39
- const userMessage = { id: Date.now(), text: inputValue, sender: 'user' };
40
- setMessages(prev => [...prev, userMessage]);
41
- setInputValue('');
42
- setIsLoading(true);
43
-
44
- try {
45
- // Add temporary AI message
46
- const aiMessageId = Date.now() + 1;
47
- setMessages(prev => [...prev, { id: aiMessageId, text: '', sender: 'ai', isLoading: true }]);
48
 
49
- // Send request to backend
50
- const response = await fetch('/chat', {
51
- method: 'POST',
52
- headers: {
53
- 'Content-Type': 'application/json',
54
- },
55
- body: JSON.stringify({
56
- message: inputValue,
57
- history: messages.filter(m => !m.isLoading).map(m => ({
58
- role: m.sender === 'user' ? 'user' : 'assistant',
59
- content: m.text
60
- }))
61
- })
62
- });
 
 
 
 
 
 
63
 
64
- if (!response.ok) {
65
- throw new Error(`HTTP error! status: ${response.status}`);
66
- }
 
 
 
 
67
 
68
- // Process streaming response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  const reader = response.body.getReader();
70
  const decoder = new TextDecoder();
 
71
  let aiResponse = '';
72
 
73
  while (true) {
74
  const { done, value } = await reader.read();
75
  if (done) break;
76
 
77
- const chunk = decoder.decode(value);
78
  aiResponse += chunk;
 
79
 
80
- // Update AI message with new content
81
- setMessages(prev => prev.map(msg =>
82
- msg.id === aiMessageId
83
- ? { ...msg, text: aiResponse, isLoading: false }
84
- : msg
85
- ));
86
  }
87
- } catch (error) {
88
- console.error('Error sending message:', error);
89
- setMessages(prev => prev.map(msg =>
90
- msg.id === aiMessageId
91
- ? { ...msg, text: 'Sorry, I encountered an error. Please try again.', isLoading: false, error: true }
92
- : msg
93
- ));
94
- } finally {
95
- setIsLoading(false);
96
  }
97
- };
98
-
99
- // Copy message to clipboard
100
- const copyToClipboard = (text) => {
101
- navigator.clipboard.writeText(text).then(() => {
102
- // Show success message (could be a toast notification)
103
- console.log('Copied to clipboard');
104
- }).catch(err => {
105
- console.error('Failed to copy: ', err);
106
- });
107
- };
108
-
109
- // Clear chat history
110
- const clearChat = () => {
111
- setMessages([]);
112
- };
113
-
114
- return (
115
- <div className="chat-container">
116
- {/* Header */}
117
- <div className="chat-header flex justify-between items-center">
118
- <h1 className="text-2xl font-bold">AI Chat with Qwen Coder</h1>
119
- <div className="flex gap-2">
120
- <button
121
- onClick={toggleDarkMode}
122
- className="btn btn-secondary"
123
- aria-label="Toggle dark mode"
124
- >
125
- {darkMode ? (
126
- <i className="fas fa-sun"></i>
127
- ) : (
128
- <i className="fas fa-moon"></i>
129
- )}
130
- </button>
131
- <button
132
- onClick={clearChat}
133
- className="btn btn-secondary"
134
- aria-label="Clear chat"
135
- >
136
- <i className="fas fa-trash"></i>
137
- </button>
138
- </div>
139
- </div>
140
-
141
- {/* Chat messages area */}
142
- <div className="chat-messages">
143
- {messages.length === 0 ? (
144
- <div className="flex flex-col items-center justify-center h-full text-center">
145
- <h2 className="text-2xl font-bold mb-4">Welcome to AI Chat</h2>
146
- <p className="text-lg mb-8">Start a conversation with Qwen Coder by typing a message below</p>
147
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full max-w-2xl">
148
- <div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
149
- <h3 className="font-bold mb-2">Examples</h3>
150
- <ul className="text-left">
151
- <li>"Explain quantum computing in simple terms"</li>
152
- <li>"Write a Python function to calculate Fibonacci numbers"</li>
153
- <li>"How do I make an HTTP request in JavaScript?"</li>
154
- </ul>
155
- </div>
156
- <div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
157
- <h3 className="font-bold mb-2">Capabilities</h3>
158
- <ul className="text-left">
159
- <li>Remembers previous conversation</li>
160
- <li>Understands complex instructions</li>
161
- <li>Generates code and explanations</li>
162
- </ul>
163
- </div>
164
- </div>
165
- </div>
166
- ) : (
167
- messages.map((message) => (
168
- <div
169
- key={message.id}
170
- className={`message-bubble relative ${message.sender === 'user' ? 'user' : 'ai'}`}
171
- >
172
- {message.sender === 'ai' && !message.isLoading && (
173
- <button
174
- onClick={() => copyToClipboard(message.text)}
175
- className="copy-button"
176
- aria-label="Copy message"
177
- >
178
- <i className="fas fa-copy"></i>
179
- </button>
180
- )}
181
- {message.isLoading ? (
182
- <div className="typing-indicator">
183
- <div className="typing-dot"></div>
184
- <div className="typing-dot"></div>
185
- <div className="typing-dot"></div>
186
- </div>
187
- ) : (
188
- <div>{message.text}</div>
189
- )}
190
- </div>
191
- ))
192
- )}
193
- <div ref={messagesEndRef} />
194
- </div>
195
-
196
- {/* Input area */}
197
- <div className="chat-input-area">
198
- <form onSubmit={handleSubmit} className="flex gap-2">
199
- <input
200
- type="text"
201
- value={inputValue}
202
- onChange={handleInputChange}
203
- placeholder="Type your message here..."
204
- className="chat-input"
205
- disabled={isLoading}
206
- />
207
- <button
208
- type="submit"
209
- className="btn"
210
- disabled={isLoading || !inputValue.trim()}
211
- >
212
- {isLoading ? (
213
- <i className="fas fa-spinner fa-spin"></i>
214
- ) : (
215
- <i className="fas fa-paper-plane"></i>
216
- )}
217
- </button>
218
- </form>
219
- <div className="text-xs text-center mt-2 text-gray-500 dark:text-gray-400">
220
- Qwen Coder can make mistakes. Consider checking important information.
221
- </div>
222
- </div>
223
- </div>
224
- );
225
  }
226
 
227
- // Render the app
228
- ReactDOM.render(<App />, document.getElementById('root'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // JavaScript logic for AI Chat Application
2
+
3
+ // DOM elements
4
+ const chatMessages = document.getElementById('chat-messages');
5
+ const userInput = document.getElementById('user-input');
6
+ const sendButton = document.getElementById('send-button');
7
+
8
+ // Conversation history
9
+ let conversationHistory = [];
10
+
11
+ // Function to add a message to the chat
12
+ function addMessage(sender, text) {
13
+ const messageDiv = document.createElement('div');
14
+ messageDiv.className = `message ${sender}`;
15
+
16
+ const messageHeader = document.createElement('div');
17
+ messageHeader.className = 'message-header';
18
+ messageHeader.textContent = sender === 'user' ? 'You' : 'AI';
19
+
20
+ const messageText = document.createElement('div');
21
+ messageText.className = 'message-text';
22
+ messageText.textContent = text;
23
+
24
+ messageDiv.appendChild(messageHeader);
25
+ messageDiv.appendChild(messageText);
26
+ chatMessages.appendChild(messageDiv);
27
+
28
+ // Scroll to bottom
29
+ chatMessages.scrollTop = chatMessages.scrollHeight;
30
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ // Function to show loading indicator
33
+ function showLoading() {
34
+ const loadingDiv = document.createElement('div');
35
+ loadingDiv.className = 'message ai';
36
+ loadingDiv.id = 'loading-message';
37
+
38
+ const messageHeader = document.createElement('div');
39
+ messageHeader.className = 'message-header';
40
+ messageHeader.textContent = 'AI';
41
+
42
+ const loadingIndicator = document.createElement('div');
43
+ loadingIndicator.className = 'loading';
44
+
45
+ loadingDiv.appendChild(messageHeader);
46
+ loadingDiv.appendChild(loadingIndicator);
47
+ chatMessages.appendChild(loadingDiv);
48
+
49
+ // Scroll to bottom
50
+ chatMessages.scrollTop = chatMessages.scrollHeight;
51
+ }
52
 
53
+ // Function to hide loading indicator
54
+ function hideLoading() {
55
+ const loadingMessage = document.getElementById('loading-message');
56
+ if (loadingMessage) {
57
+ loadingMessage.remove();
58
+ }
59
+ }
60
 
61
+ // Function to send message to backend
62
+ async function sendMessage(message) {
63
+ try {
64
+ // Add user message to UI
65
+ addMessage('user', message);
66
+
67
+ // Clear input
68
+ userInput.value = '';
69
+
70
+ // Show loading indicator
71
+ showLoading();
72
+
73
+ // Add user message to conversation history
74
+ conversationHistory.push({ role: 'user', content: message });
75
+
76
+ // Send request to backend
77
+ const response = await fetch('/chat', {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json'
81
+ },
82
+ body: JSON.stringify({
83
+ message: message,
84
+ history: conversationHistory.slice(0, -1) // Exclude the current message
85
+ })
86
+ });
87
+
88
+ // Hide loading indicator
89
+ hideLoading();
90
+
91
+ if (response.ok) {
92
+ // Create AI message element
93
+ const aiMessageDiv = document.createElement('div');
94
+ aiMessageDiv.className = 'message ai';
95
+
96
+ const messageHeader = document.createElement('div');
97
+ messageHeader.className = 'message-header';
98
+ messageHeader.textContent = 'AI';
99
+
100
+ const messageText = document.createElement('div');
101
+ messageText.className = 'message-text';
102
+ messageText.id = 'ai-response';
103
+
104
+ aiMessageDiv.appendChild(messageHeader);
105
+ aiMessageDiv.appendChild(messageText);
106
+ chatMessages.appendChild(aiMessageDiv);
107
+
108
+ // Stream the response
109
  const reader = response.body.getReader();
110
  const decoder = new TextDecoder();
111
+
112
  let aiResponse = '';
113
 
114
  while (true) {
115
  const { done, value } = await reader.read();
116
  if (done) break;
117
 
118
+ const chunk = decoder.decode(value, { stream: true });
119
  aiResponse += chunk;
120
+ messageText.textContent = aiResponse;
121
 
122
+ // Scroll to bottom
123
+ chatMessages.scrollTop = chatMessages.scrollHeight;
 
 
 
 
124
  }
125
+
126
+ // Add AI response to conversation history
127
+ conversationHistory.push({ role: 'assistant', content: aiResponse });
128
+ } else {
129
+ // Handle error
130
+ addMessage('ai', 'Sorry, I encountered an error. Please try again.');
 
 
 
131
  }
132
+ } catch (error) {
133
+ // Hide loading indicator
134
+ hideLoading();
135
+
136
+ // Handle network error
137
+ addMessage('ai', 'Sorry, I encountered a network error. Please check your connection and try again.');
138
+ console.error('Error sending message:', error);
139
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
+ // Event listener for send button
143
+ sendButton.addEventListener('click', () => {
144
+ const message = userInput.value.trim();
145
+ if (message) {
146
+ sendMessage(message);
147
+ }
148
+ });
149
+
150
+ // Event listener for Enter key
151
+ userInput.addEventListener('keydown', (event) => {
152
+ if (event.key === 'Enter' && !event.shiftKey) {
153
+ event.preventDefault();
154
+ const message = userInput.value.trim();
155
+ if (message) {
156
+ sendMessage(message);
157
+ }
158
+ }
159
+ });
160
+
161
+ // Initialize the chat with a welcome message
162
+ window.addEventListener('DOMContentLoaded', () => {
163
+ addMessage('ai', 'Hello! I am an AI assistant powered by Qwen Coder. How can I help you today?');
164
+ });
public/index.html CHANGED
@@ -4,15 +4,24 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>AI Chat with Qwen Coder</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
9
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
10
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
11
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
- <link rel="stylesheet" href="styles.css">
13
  </head>
14
- <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
15
- <div id="root"></div>
16
- <script type="text/babel" src="app.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </body>
18
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>AI Chat with Qwen Coder</title>
7
+ <link href="styles.css" rel="stylesheet">
 
 
 
 
 
8
  </head>
9
+ <body>
10
+ <div id="app">
11
+ <div class="chat-container">
12
+ <div class="chat-header">
13
+ <h1>AI Chat with Qwen Coder</h1>
14
+ <p>Chat with the Qwen/Qwen3-Coder-30B-A3B-Instruct model</p>
15
+ </div>
16
+ <div class="chat-messages" id="chat-messages">
17
+ <!-- Messages will be displayed here -->
18
+ </div>
19
+ <div class="chat-input-container">
20
+ <textarea id="user-input" placeholder="Type your message here..."></textarea>
21
+ <button id="send-button">Send</button>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <script src="app.js"></script>
26
  </body>
27
  </html>
public/styles.css CHANGED
@@ -1,335 +1,201 @@
1
- /* Custom CSS variables for theming */
2
- :root {
3
- /* Primary color palette */
4
- --primary-50: 240 249 255;
5
- --primary-100: 224 242 254;
6
- --primary-200: 186 230 253;
7
- --primary-300: 125 211 252;
8
- --primary-400: 56 189 248;
9
- --primary-500: 14 165 233;
10
- --primary-600: 2 132 199;
11
- --primary-700: 3 105 161;
12
- --primary-800: 7 89 133;
13
- --primary-900: 12 74 110;
14
-
15
- /* Secondary color palette */
16
- --secondary-50: 248 250 252;
17
- --secondary-100: 241 245 249;
18
- --secondary-200: 226 232 240;
19
- --secondary-300: 203 213 225;
20
- --secondary-400: 148 163 184;
21
- --secondary-500: 100 116 139;
22
- --secondary-600: 71 85 105;
23
- --secondary-700: 51 65 85;
24
- --secondary-800: 30 41 59;
25
- --secondary-900: 15 23 42;
26
-
27
- /* Accent colors */
28
- --accent-50: 254 249 195;
29
- --accent-100: 254 240 138;
30
- --accent-200: 253 230 138;
31
- --accent-300: 252 211 77;
32
- --accent-400: 251 191 36;
33
- --accent-500: 245 158 11;
34
- --accent-600: 217 119 6;
35
- --accent-700: 180 83 9;
36
- --accent-800: 146 64 14;
37
- --accent-900: 120 53 15;
38
-
39
- /* Gradient definitions */
40
- --gradient-primary: linear-gradient(135deg, hsl(var(--primary-500)), hsl(var(--accent-500)));
41
- --gradient-secondary: linear-gradient(135deg, hsl(var(--secondary-700)), hsl(var(--secondary-900)));
42
-
43
- /* Shadows */
44
- --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
45
- --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
46
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
47
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
48
- --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
49
- --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
50
-
51
- /* Transitions */
52
- --transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
53
- --transition-normal: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
54
- --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
55
- }
56
-
57
- /* Dark mode variables */
58
- .dark {
59
- --primary-50: 236 254 255;
60
- --primary-100: 207 250 254;
61
- --primary-200: 165 243 252;
62
- --primary-300: 103 232 249;
63
- --primary-400: 34 211 238;
64
- --primary-500: 6 182 212;
65
- --primary-600: 8 145 178;
66
- --primary-700: 14 116 144;
67
- --primary-800: 21 94 117;
68
- --primary-900: 22 78 99;
69
- }
70
 
71
  /* Base styles */
72
  body {
73
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
74
- -webkit-font-smoothing: antialiased;
75
- -moz-osx-font-smoothing: grayscale;
76
- background-color: hsl(var(--secondary-50));
77
- transition: background-color var(--transition-normal);
78
- }
79
-
80
- .dark body {
81
- background-color: hsl(var(--secondary-900));
 
 
 
 
 
82
  }
83
 
84
  /* Chat container */
85
  .chat-container {
86
- max-width: 1200px;
87
  margin: 0 auto;
88
- height: 100vh;
89
  display: flex;
90
  flex-direction: column;
91
- background-color: hsl(var(--secondary-50));
92
- transition: background-color var(--transition-normal);
93
  }
94
 
95
- .dark .chat-container {
96
- background-color: hsl(var(--secondary-900));
 
 
97
  }
98
 
99
- /* Header */
100
- .chat-header {
101
- padding: 1rem;
102
- border-bottom: 1px solid hsl(var(--secondary-200));
103
- background-color: hsl(var(--secondary-50));
104
- transition: all var(--transition-normal);
 
 
105
  }
106
 
107
- .dark .chat-header {
108
- border-bottom: 1px solid hsl(var(--secondary-800));
109
- background-color: hsl(var(--secondary-900));
110
  }
111
 
112
- /* Chat messages area */
 
 
 
 
 
 
113
  .chat-messages {
114
  flex: 1;
115
  overflow-y: auto;
116
- padding: 1rem;
117
- display: flex;
118
- flex-direction: column;
119
- gap: 1rem;
120
- background-color: hsl(var(--secondary-50));
121
- transition: background-color var(--transition-normal);
122
  }
123
 
124
- .dark .chat-messages {
125
- background-color: hsl(var(--secondary-900));
 
 
 
126
  }
127
 
128
- /* Message bubble */
129
- .message-bubble {
130
- max-width: 80%;
131
- padding: 1rem 1.5rem;
132
- border-radius: 1rem;
133
- box-shadow: var(--shadow);
134
- transition: all var(--transition-normal);
135
  }
136
 
137
- .message-bubble.user {
138
- align-self: flex-end;
139
- background-color: hsl(var(--primary-500));
140
- color: white;
141
  }
142
 
143
- .message-bubble.ai {
144
- align-self: flex-start;
145
- background-color: hsl(var(--secondary-100));
146
- color: hsl(var(--secondary-900));
147
  }
148
 
149
- .dark .message-bubble.ai {
150
- background-color: hsl(var(--secondary-800));
151
- color: hsl(var(--secondary-100));
 
 
 
 
 
152
  }
153
 
154
- /* Input area */
155
- .chat-input-area {
156
- padding: 1rem;
157
- border-top: 1px solid hsl(var(--secondary-200));
158
- background-color: hsl(var(--secondary-50));
159
- transition: all var(--transition-normal);
160
  }
161
 
162
- .dark .chat-input-area {
163
- border-top: 1px solid hsl(var(--secondary-800));
164
- background-color: hsl(var(--secondary-900));
165
  }
166
 
167
- /* Input field */
168
- .chat-input {
169
- width: 100%;
170
- padding: 0.75rem 1rem;
171
- border-radius: 0.5rem;
172
- border: 1px solid hsl(var(--secondary-300));
173
- background-color: hsl(var(--secondary-100));
174
- color: hsl(var(--secondary-900));
175
- transition: all var(--transition-normal);
176
  }
177
 
178
- .dark .chat-input {
179
- border: 1px solid hsl(var(--secondary-700));
180
- background-color: hsl(var(--secondary-800));
181
- color: hsl(var(--secondary-100));
182
  }
183
 
184
- .chat-input:focus {
185
- outline: none;
186
- border-color: hsl(var(--primary-500));
187
- box-shadow: 0 0 0 3px hsla(var(--primary-500), 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
 
190
- /* Buttons */
191
- .btn {
192
- padding: 0.5rem 1rem;
193
- border-radius: 0.5rem;
194
- font-weight: 500;
195
- transition: all var(--transition-normal);
196
- cursor: pointer;
197
- border: none;
198
- background-color: hsl(var(--primary-500));
199
  color: white;
 
 
 
 
 
 
 
200
  }
201
 
202
- .btn:hover {
203
- background-color: hsl(var(--primary-600));
204
- }
205
-
206
- .btn-secondary {
207
- background-color: hsl(var(--secondary-200));
208
- color: hsl(var(--secondary-900);
209
- }
210
-
211
- .dark .btn-secondary {
212
- background-color: hsl(var(--secondary-700));
213
- color: hsl(var(--secondary-100);
214
- }
215
-
216
- .btn-secondary:hover {
217
- background-color: hsl(var(--secondary-300));
218
- }
219
-
220
- .dark .btn-secondary:hover {
221
- background-color: hsl(var(--secondary-600));
222
- }
223
-
224
- /* Copy button */
225
- .copy-button {
226
- position: absolute;
227
- top: 0.5rem;
228
- right: 0.5rem;
229
- padding: 0.25rem;
230
- border-radius: 0.25rem;
231
- background-color: hsl(var(--secondary-200));
232
- color: hsl(var(--secondary-700));
233
- opacity: 0;
234
- transition: all var(--transition-normal);
235
- }
236
-
237
- .message-bubble:hover .copy-button {
238
- opacity: 1;
239
- }
240
-
241
- .dark .copy-button {
242
- background-color: hsl(var(--secondary-700));
243
- color: hsl(var(--secondary-200));
244
- }
245
-
246
- /* Typing indicator */
247
- .typing-indicator {
248
- display: flex;
249
- align-items: center;
250
- gap: 0.25rem;
251
- padding: 1rem 1.5rem;
252
- background-color: hsl(var(--secondary-100));
253
- border-radius: 1rem;
254
- width: fit-content;
255
- max-width: 80%;
256
- align-self: flex-start;
257
  }
258
 
259
- .dark .typing-indicator {
260
- background-color: hsl(var(--secondary-800));
261
  }
262
 
263
- .typing-dot {
264
- width: 0.5rem;
265
- height: 0.5rem;
 
 
 
266
  border-radius: 50%;
267
- background-color: hsl(var(--secondary-500));
268
- animation: typing 1.4s infinite ease-in-out;
269
  }
270
 
271
- .typing-dot:nth-child(1) {
272
- animation-delay: 0s;
273
  }
274
 
275
- .typing-dot:nth-child(2) {
276
- animation-delay: 0.2s;
277
- }
278
-
279
- .typing-dot:nth-child(3) {
280
- animation-delay: 0.4s;
281
- }
282
-
283
- @keyframes typing {
284
- 0%, 60%, 100% {
285
- transform: translateY(0);
286
- }
287
- 30% {
288
- transform: translateY(-5px);
289
- }
290
  }
291
 
292
  /* Responsive design */
293
  @media (max-width: 768px) {
294
- .message-bubble {
295
- max-width: 90%;
296
  }
297
 
298
- .chat-header, .chat-input-area {
299
- padding: 0.75rem;
300
  }
301
 
302
- .chat-messages {
303
- padding: 0.75rem;
 
 
 
 
304
  }
305
- }
306
-
307
- /* Scrollbar styling */
308
- ::-webkit-scrollbar {
309
- width: 8px;
310
- }
311
-
312
- ::-webkit-scrollbar-track {
313
- background: hsl(var(--secondary-100));
314
- }
315
-
316
- .dark ::-webkit-scrollbar-track {
317
- background: hsl(var(--secondary-800));
318
- }
319
-
320
- ::-webkit-scrollbar-thumb {
321
- background: hsl(var(--secondary-300));
322
- border-radius: 4px;
323
- }
324
-
325
- .dark ::-webkit-scrollbar-thumb {
326
- background: hsl(var(--secondary-600));
327
- }
328
-
329
- ::-webkit-scrollbar-thumb:hover {
330
- background: hsl(var(--secondary-400));
331
- }
332
-
333
- .dark ::-webkit-scrollbar-thumb:hover {
334
- background: hsl(var(--secondary-500));
335
  }
 
1
+ /* TailwindCSS styles for AI Chat Application */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  /* Base styles */
4
  body {
5
+ font-family: 'Inter', sans-serif;
6
+ background-color: #f8f9fa;
7
+ color: #333;
8
+ margin: 0;
9
+ padding: 0;
10
+ min-height: 100vh;
11
+ }
12
+
13
+ /* Dark mode styles */
14
+ @media (prefers-color-scheme: dark) {
15
+ body {
16
+ background-color: #1a202c;
17
+ color: #e2e8f0;
18
+ }
19
  }
20
 
21
  /* Chat container */
22
  .chat-container {
23
+ max-width: 800px;
24
  margin: 0 auto;
25
+ padding: 20px;
26
  display: flex;
27
  flex-direction: column;
28
+ height: 100vh;
 
29
  }
30
 
31
+ /* Chat header */
32
+ .chat-header {
33
+ text-align: center;
34
+ margin-bottom: 20px;
35
  }
36
 
37
+ .chat-header h1 {
38
+ font-size: 2rem;
39
+ font-weight: 700;
40
+ margin-bottom: 10px;
41
+ background: linear-gradient(135deg, #007cf0, #00dfd8);
42
+ -webkit-background-clip: text;
43
+ -webkit-text-fill-color: transparent;
44
+ background-clip: text;
45
  }
46
 
47
+ .chat-header p {
48
+ font-size: 1rem;
49
+ color: #666;
50
  }
51
 
52
+ @media (prefers-color-scheme: dark) {
53
+ .chat-header p {
54
+ color: #a0aec0;
55
+ }
56
+ }
57
+
58
+ /* Chat messages */
59
  .chat-messages {
60
  flex: 1;
61
  overflow-y: auto;
62
+ padding: 20px;
63
+ background-color: #fff;
64
+ border-radius: 10px;
65
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
66
+ margin-bottom: 20px;
 
67
  }
68
 
69
+ @media (prefers-color-scheme: dark) {
70
+ .chat-messages {
71
+ background-color: #2d3748;
72
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
73
+ }
74
  }
75
 
76
+ /* Individual message */
77
+ .message {
78
+ margin-bottom: 15px;
79
+ padding: 10px;
80
+ border-radius: 8px;
81
+ animation: fadeIn 0.3s ease-in;
 
82
  }
83
 
84
+ .message.user {
85
+ background-color: #e3f2fd;
86
+ margin-left: 20%;
 
87
  }
88
 
89
+ .message.ai {
90
+ background-color: #f0f4f8;
91
+ margin-right: 20%;
 
92
  }
93
 
94
+ @media (prefers-color-scheme: dark) {
95
+ .message.user {
96
+ background-color: #2c5282;
97
+ }
98
+
99
+ .message.ai {
100
+ background-color: #4a5568;
101
+ }
102
  }
103
 
104
+ .message-header {
105
+ font-weight: 600;
106
+ margin-bottom: 5px;
 
 
 
107
  }
108
 
109
+ .message.user .message-header {
110
+ color: #007cf0;
 
111
  }
112
 
113
+ .message.ai .message-header {
114
+ color: #00dfd8;
 
 
 
 
 
 
 
115
  }
116
 
117
+ /* Chat input container */
118
+ .chat-input-container {
119
+ display: flex;
120
+ gap: 10px;
121
  }
122
 
123
+ #user-input {
124
+ flex: 1;
125
+ padding: 12px;
126
+ border: 1px solid #ddd;
127
+ border-radius: 8px;
128
+ font-size: 1rem;
129
+ resize: none;
130
+ min-height: 50px;
131
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
132
+ }
133
+
134
+ @media (prefers-color-scheme: dark) {
135
+ #user-input {
136
+ background-color: #4a5568;
137
+ border-color: #718096;
138
+ color: #e2e8f0;
139
+ }
140
  }
141
 
142
+ #send-button {
143
+ padding: 12px 24px;
144
+ background: linear-gradient(135deg, #007cf0, #00dfd8);
 
 
 
 
 
 
145
  color: white;
146
+ border: none;
147
+ border-radius: 8px;
148
+ font-size: 1rem;
149
+ font-weight: 600;
150
+ cursor: pointer;
151
+ transition: transform 0.2s, box-shadow 0.2s;
152
+ box-shadow: 0 4px 6px rgba(0, 124, 240, 0.3);
153
  }
154
 
155
+ #send-button:hover {
156
+ transform: translateY(-2px);
157
+ box-shadow: 0 6px 8px rgba(0, 124, 240, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
+ #send-button:active {
161
+ transform: translateY(0);
162
  }
163
 
164
+ /* Loading indicator */
165
+ .loading {
166
+ display: inline-block;
167
+ width: 20px;
168
+ height: 20px;
169
+ border: 3px solid rgba(0, 124, 240, 0.3);
170
  border-radius: 50%;
171
+ border-top-color: #007cf0;
172
+ animation: spin 1s ease-in-out infinite;
173
  }
174
 
175
+ @keyframes spin {
176
+ to { transform: rotate(360deg); }
177
  }
178
 
179
+ @keyframes fadeIn {
180
+ from { opacity: 0; transform: translateY(10px); }
181
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
  /* Responsive design */
185
  @media (max-width: 768px) {
186
+ .chat-container {
187
+ padding: 10px;
188
  }
189
 
190
+ .chat-header h1 {
191
+ font-size: 1.5rem;
192
  }
193
 
194
+ .message.user {
195
+ margin-left: 10%;
196
+ }
197
+
198
+ .message.ai {
199
+ margin-right: 10%;
200
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
readme.md CHANGED
@@ -1,41 +1,50 @@
1
- # AI Chat Application for HuggingFace Spaces
2
 
3
- A fully functional AI chat application for HuggingFace Spaces integrating Qwen Coder 3 with advanced OPENAI API compatibility features.
4
 
5
  ## Features
6
 
7
- - Integration with Qwen/Qwen3-Coder-30B-A3B-Instruct model
8
- - Advanced OPENAI API compatibility
9
- - Professional web interface replicating Perplexity AI design
10
- - Responsive layout with TailwindCSS styling
11
- - Dark/light mode support
12
- - Real-time streaming responses
13
- - Conversation history management
14
- - Copy response functionality
15
- - Typing indicators
16
- - Full GPU optimization
17
- - Robust error handling and automatic connection recovery
18
- - Caching mechanisms
19
- - Ready for immediate deployment on HuggingFace Spaces
20
-
21
- ## Technology Stack
22
-
23
- - **Backend**: Python, Gradio, FastAPI, Transformers, PyTorch
24
- - **Frontend**: TailwindCSS, JavaScript, HTML5
25
- - **Infrastructure**: Redis for caching, HuggingFace Spaces deployment
 
 
 
 
 
 
 
 
 
26
 
27
  ## Requirements
28
 
29
  - Python 3.8+
30
- - GPU with at least 24GB VRAM (for Qwen/Qwen3-Coder-30B-A3B-Instruct model)
31
- - Redis server (optional, for conversation caching)
32
 
33
  ## Installation
34
 
35
  1. Clone this repository:
36
  ```bash
37
  git clone <repository-url>
38
- cd ai-chat-app
39
  ```
40
 
41
  2. Install dependencies:
@@ -48,29 +57,37 @@ A fully functional AI chat application for HuggingFace Spaces integrating Qwen C
48
  python app.py
49
  ```
50
 
51
- ## Usage
52
 
53
- ### Web Interface
 
 
 
54
 
55
- The application provides a web interface accessible at `http://localhost:7860` when running locally. The interface features:
 
 
56
 
57
- - Chat interface similar to Perplexity AI
58
- - Dark/light mode toggle
59
- - Conversation history sidebar
60
- - Copy buttons for responses
61
- - Typing indicators during response generation
62
 
63
- ### API Endpoints
 
64
 
65
- The application exposes OPENAI API compatible endpoints:
66
 
67
- - `POST /v1/chat/completions` - Chat completion endpoint
 
 
 
68
 
69
- Example request:
70
  ```json
71
  {
72
  "messages": [
73
- {"role": "user", "content": "Hello, how are you?"}
 
74
  ],
75
  "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
76
  "max_tokens": 1024,
@@ -78,47 +95,75 @@ Example request:
78
  }
79
  ```
80
 
81
- ## Deployment to HuggingFace Spaces
82
-
83
- 1. Create a new Space on HuggingFace with the following configuration:
84
- - SDK: Gradio
85
- - Hardware: GPU (recommended)
86
-
87
- 2. Upload all files to your Space repository
88
 
89
- 3. The application will automatically start and be accessible through your Space URL
 
 
 
 
 
 
 
 
 
90
 
91
- ## Configuration
92
 
93
- The application can be configured through environment variables:
 
 
 
94
 
95
- - `MODEL_NAME`: The HuggingFace model identifier (default: Qwen/Qwen3-Coder-30B-A3B-Instruct)
96
- - `MAX_TOKENS`: Default maximum tokens for responses (default: 1024)
97
- - `TEMPERATURE`: Default temperature for generation (default: 0.7)
98
- - `REDIS_URL`: Redis connection URL for caching (optional)
 
99
 
100
  ## Troubleshooting
101
 
102
- ### GPU Memory Issues
 
 
 
 
103
 
104
- If you encounter GPU memory issues:
 
 
105
 
106
- 1. Ensure your GPU has at least 24GB VRAM
107
- 2. Try reducing the `max_tokens` parameter
108
- 3. Use quantization techniques for model loading
109
 
110
- ### Model Loading Errors
111
 
112
- If the model fails to load:
 
 
113
 
114
- 1. Check your internet connection
115
- 2. Ensure you have sufficient disk space
116
- 3. Verify the model identifier is correct
117
 
118
  ## Contributing
119
 
120
- Contributions are welcome! Please fork the repository and submit a pull request with your changes.
 
 
 
 
121
 
122
  ## License
123
 
124
- This project is licensed under the MIT License - see the LICENSE file for details.
 
 
 
 
 
 
 
1
+ # AI Chat Application with Qwen Coder
2
 
3
+ This is a fully functional AI chat application built for HuggingFace Spaces, integrating the Qwen/Qwen3-Coder-30B-A3B-Instruct model with advanced OPENAI API compatibility features.
4
 
5
  ## Features
6
 
7
+ - **Qwen Coder 3 Integration**: Direct integration with the Qwen/Qwen3-Coder-30B-A3B-Instruct model
8
+ - **OPENAI API Compatibility**: Implements OPENAI API endpoints for seamless integration
9
+ - **Streaming Responses**: Real-time response streaming for interactive chat experience
10
+ - **Conversation History**: Persistent conversation history management
11
+ - **Modern UI**: Responsive design inspired by Perplexity AI with TailwindCSS
12
+ - **Dark/Light Mode**: Support for both dark and light themes
13
+ - **Copy Responses**: One-click copying of AI responses
14
+ - **Typing Indicators**: Visual indicators for AI response generation
15
+ - **GPU Optimization**: Full GPU optimization for maximum performance
16
+ - **Error Handling**: Robust error handling with automatic connection recovery
17
+ - **Caching**: Efficient caching mechanisms for improved performance
18
+
19
+ ## Project Structure
20
+
21
+ ```
22
+ /
23
+ ├── app.py # Main application entry point
24
+ ├── requirements.txt # Python dependencies
25
+ ├── README.md # This file
26
+ ├── public/ # Frontend static files
27
+ │ ├── index.html # Main HTML file
28
+ │ ├── styles.css # TailwindCSS styles
29
+ │ └── app.js # JavaScript logic
30
+ └── utils/ # Utility modules
31
+ ├── model_utils.py # Model management utilities
32
+ ├── conversation.py # Conversation management
33
+ └── api_compat.py # OPENAI API compatibility
34
+ ```
35
 
36
  ## Requirements
37
 
38
  - Python 3.8+
39
+ - GPU with CUDA support (recommended)
40
+ - 32GB+ RAM (for optimal performance with Qwen Coder 3)
41
 
42
  ## Installation
43
 
44
  1. Clone this repository:
45
  ```bash
46
  git clone <repository-url>
47
+ cd <repository-name>
48
  ```
49
 
50
  2. Install dependencies:
 
57
  python app.py
58
  ```
59
 
60
+ ## Deployment to HuggingFace Spaces
61
 
62
+ 1. Create a new Space on HuggingFace:
63
+ - Go to https://huggingface.co/new-space
64
+ - Choose "Gradio" as the Space SDK
65
+ - Select a GPU hardware (recommended for Qwen Coder 3)
66
 
67
+ 2. Upload files to your Space repository:
68
+ - Upload all files from this repository
69
+ - Make sure to include the `requirements.txt` file
70
 
71
+ 3. Configure the Space:
72
+ - The Space will automatically detect and install dependencies from `requirements.txt`
73
+ - The application will start automatically on port 7860
 
 
74
 
75
+ 4. Access your deployed application:
76
+ - Once the build is complete, your application will be available at the provided URL
77
 
78
+ ## API Endpoints
79
 
80
+ ### OPENAI API Compatible Endpoint
81
+ ```
82
+ POST /v1/chat/completions
83
+ ```
84
 
85
+ Request format:
86
  ```json
87
  {
88
  "messages": [
89
+ {"role": "system", "content": "You are a helpful assistant."},
90
+ {"role": "user", "content": "Hello!"}
91
  ],
92
  "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
93
  "max_tokens": 1024,
 
95
  }
96
  ```
97
 
98
+ ### Frontend Chat Endpoint
99
+ ```
100
+ POST /chat
101
+ ```
 
 
 
102
 
103
+ Request format:
104
+ ```json
105
+ {
106
+ "message": "Hello!",
107
+ "history": [
108
+ {"role": "user", "content": "Previous message"},
109
+ {"role": "assistant", "content": "Previous response"}
110
+ ]
111
+ }
112
+ ```
113
 
114
+ ## Customization
115
 
116
+ ### Model Configuration
117
+ You can customize the model behavior by modifying the parameters in `utils/model_utils.py`:
118
+ - `DEFAULT_MAX_TOKENS`: Maximum tokens to generate
119
+ - `DEFAULT_TEMPERATURE`: Sampling temperature
120
 
121
+ ### UI Customization
122
+ The UI can be customized by modifying:
123
+ - `public/styles.css`: CSS styles with TailwindCSS
124
+ - `public/app.js`: JavaScript logic
125
+ - `public/index.html`: HTML structure
126
 
127
  ## Troubleshooting
128
 
129
+ ### Common Issues
130
+
131
+ 1. **Model Loading Errors**:
132
+ - Ensure you have sufficient RAM and GPU memory
133
+ - Check that the model name is correct in `utils/model_utils.py`
134
 
135
+ 2. **CUDA Out of Memory**:
136
+ - Reduce `DEFAULT_MAX_TOKENS` in `utils/model_utils.py`
137
+ - Use a smaller model variant if available
138
 
139
+ 3. **Dependency Installation Failures**:
140
+ - Check the HuggingFace Space logs for specific error messages
141
+ - Ensure all dependencies are listed in `requirements.txt`
142
 
143
+ ### Performance Optimization
144
 
145
+ 1. **GPU Usage**:
146
+ - The application automatically detects and uses CUDA if available
147
+ - For CPU-only environments, performance will be significantly slower
148
 
149
+ 2. **Caching**:
150
+ - Redis is used for caching if available
151
+ - In-memory storage is used as fallback
152
 
153
  ## Contributing
154
 
155
+ 1. Fork the repository
156
+ 2. Create a feature branch
157
+ 3. Commit your changes
158
+ 4. Push to the branch
159
+ 5. Create a Pull Request
160
 
161
  ## License
162
 
163
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
164
+
165
+ ## Acknowledgments
166
+
167
+ - Qwen team for the Qwen/Qwen3-Coder-30B-A3B-Instruct model
168
+ - HuggingFace for providing the platform
169
+ - Gradio team for the web interface framework
requirements.txt CHANGED
@@ -1,10 +1,8 @@
1
  gradio>=3.0.0
2
- transformers>=4.30.0
3
- torch>=2.0.0
4
  fastapi>=0.68.0
5
  uvicorn>=0.15.0
6
  redis>=3.5.0
7
  aiohttp>=3.7.0
8
- pydantic>=1.8.0
9
- accelerate>=0.20.0
10
- bitsandbytes>=0.39.0
 
1
  gradio>=3.0.0
2
+ transformers>=4.0.0
3
+ torch>=1.9.0
4
  fastapi>=0.68.0
5
  uvicorn>=0.15.0
6
  redis>=3.5.0
7
  aiohttp>=3.7.0
8
+ pydantic>=1.8.0
 
 
utils/api_compat.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OPENAI API compatibility utilities
3
+ """
4
+
5
+ import time
6
+ import logging
7
+ from typing import List, Dict, Any, Optional
8
+ from pydantic import BaseModel
9
+
10
+ # Configure logging
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Default model configuration
15
+ DEFAULT_MODEL = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
16
+ DEFAULT_MAX_TOKENS = 1024
17
+ DEFAULT_TEMPERATURE = 0.7
18
+
19
+ class ChatMessage(BaseModel):
20
+ """Represents a chat message"""
21
+ role: str
22
+ content: str
23
+
24
+ class ChatRequest(BaseModel):
25
+ """Represents a chat request"""
26
+ messages: List[ChatMessage]
27
+ model: str = DEFAULT_MODEL
28
+ max_tokens: Optional[int] = DEFAULT_MAX_TOKENS
29
+ temperature: Optional[float] = DEFAULT_TEMPERATURE
30
+
31
+ class ChatResponse(BaseModel):
32
+ """Represents a chat response"""
33
+ id: str
34
+ object: str = "chat.completion"
35
+ created: int
36
+ model: str
37
+ choices: List[Dict[str, Any]]
38
+ usage: Dict[str, int]
39
+
40
+ def convert_openai_request_to_model_input(request: ChatRequest) -> str:
41
+ """Convert OPENAI API request to model input format"""
42
+ prompt = ""
43
+ for msg in request.messages:
44
+ if msg.role == "system":
45
+ prompt += f"System: {msg.content}\n"
46
+ elif msg.role == "user":
47
+ prompt += f"User: {msg.content}\n"
48
+ elif msg.role == "assistant":
49
+ prompt += f"Assistant: {msg.content}\n"
50
+ return prompt
51
+
52
+ def create_openai_response(response_text: str, request: ChatRequest) -> ChatResponse:
53
+ """Create OPENAI API compatible response"""
54
+ return ChatResponse(
55
+ id="chatcmpl-" + str(hash(response_text))[:10],
56
+ created=int(time.time()),
57
+ model=request.model,
58
+ choices=[{
59
+ "index": 0,
60
+ "message": {
61
+ "role": "assistant",
62
+ "content": response_text
63
+ },
64
+ "finish_reason": "stop"
65
+ }],
66
+ usage={
67
+ "prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
68
+ "completion_tokens": len(response_text.split()),
69
+ "total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_text.split())
70
+ }
71
+ )
72
+
73
+ def format_messages_for_frontend(messages: List[Dict[str, Any]]) -> List[Dict[str, str]]:
74
+ """Format messages for frontend display"""
75
+ formatted_messages = []
76
+ for msg in messages:
77
+ if msg["role"] == "user":
78
+ formatted_messages.append({"sender": "user", "text": msg["content"]})
79
+ elif msg["role"] == "assistant":
80
+ formatted_messages.append({"sender": "ai", "text": msg["content"]})
81
+ return formatted_messages
utils/conversation.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conversation management utilities
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import redis
8
+ from typing import List, Dict, Any
9
+
10
+ # Configure logging
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class ConversationManager:
15
+ """Manage conversation history and caching"""
16
+
17
+ def __init__(self):
18
+ self.redis_client = None
19
+ try:
20
+ self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
21
+ self.redis_client.ping()
22
+ except:
23
+ logger.warning("Redis not available, using in-memory storage")
24
+ self.conversations = {}
25
+
26
+ def save_conversation(self, conv_id: str, messages: List[Dict[str, Any]]) -> None:
27
+ """Save conversation to cache"""
28
+ try:
29
+ if self.redis_client:
30
+ self.redis_client.setex(conv_id, 86400, json.dumps(messages)) # 24 hours expiry
31
+ else:
32
+ self.conversations[conv_id] = messages
33
+ except Exception as e:
34
+ logger.error(f"Error saving conversation: {e}")
35
+
36
+ def load_conversation(self, conv_id: str) -> List[Dict[str, Any]]:
37
+ """Load conversation from cache"""
38
+ try:
39
+ if self.redis_client:
40
+ data = self.redis_client.get(conv_id)
41
+ if data:
42
+ return json.loads(data)
43
+ else:
44
+ return self.conversations.get(conv_id, [])
45
+ except Exception as e:
46
+ logger.error(f"Error loading conversation: {e}")
47
+ return []
48
+
49
+ def format_messages_for_model(self, messages: List[Dict[str, Any]]) -> str:
50
+ """Format messages for model input"""
51
+ prompt = ""
52
+ for msg in messages:
53
+ if msg["role"] == "system":
54
+ prompt += f"System: {msg['content']}\n"
55
+ elif msg["role"] == "user":
56
+ prompt += f"User: {msg['content']}\n"
57
+ elif msg["role"] == "assistant":
58
+ prompt += f"Assistant: {msg['content']}\n"
59
+ return prompt
60
+
61
+ def add_message_to_conversation(self, conv_id: str, role: str, content: str) -> None:
62
+ """Add a message to a conversation"""
63
+ conversation = self.load_conversation(conv_id)
64
+ conversation.append({"role": role, "content": content})
65
+ self.save_conversation(conv_id, conversation)