Spaces:
Running
Running
fixing layout issue and rendering issues
Browse files- app/components/FlutterRunner.tsx +4 -6
- app/components/GeminiChat.tsx +6 -2
- app/components/LaTeXEditor.tsx +100 -24
- app/components/Messages.tsx +12 -2
- app/globals.css +19 -0
app/components/FlutterRunner.tsx
CHANGED
|
@@ -244,6 +244,7 @@ export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode }:
|
|
| 244 |
a.download = activeFileName || 'main.dart'
|
| 245 |
a.click()
|
| 246 |
URL.revokeObjectURL(url)
|
|
|
|
| 247 |
|
| 248 |
const handleCopyCode = () => {
|
| 249 |
navigator.clipboard.writeText(code).then(() => {
|
|
@@ -252,7 +253,6 @@ export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode }:
|
|
| 252 |
console.error("Failed to copy:", err)
|
| 253 |
})
|
| 254 |
}
|
| 255 |
-
}
|
| 256 |
|
| 257 |
return (
|
| 258 |
<Window
|
|
@@ -319,7 +319,6 @@ export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode }:
|
|
| 319 |
</div>
|
| 320 |
|
| 321 |
<div className="flex items-center gap-2">
|
| 322 |
-
<button
|
| 323 |
<button
|
| 324 |
onClick={handleCopyCode}
|
| 325 |
className="flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs font-medium transition-colors"
|
|
@@ -328,6 +327,7 @@ export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode }:
|
|
| 328 |
<Copy size={14} weight="bold" />
|
| 329 |
Copy Code
|
| 330 |
</button>
|
|
|
|
| 331 |
onClick={handleDownload}
|
| 332 |
className="p-1.5 text-gray-400 hover:text-white hover:bg-[#3e3e42] rounded transition-colors"
|
| 333 |
title="Download Code"
|
|
@@ -406,16 +406,14 @@ export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode }:
|
|
| 406 |
<div className="flex-1 overflow-y-auto py-2">
|
| 407 |
<div className="px-2">
|
| 408 |
<div className="flex items-center gap-1 py-1 px-2 text-sm text-gray-300 hover:bg-[#2a2d2e] rounded cursor-pointer">
|
| 409 |
-
<CaretDown
|
| 410 |
-
Copy size={12} weight="bold" />
|
| 411 |
<span className="font-bold">Flutter App</span>
|
| 412 |
</div>
|
| 413 |
<div className="pl-4">
|
| 414 |
{files.map(file => (
|
| 415 |
<div key={file.id}>
|
| 416 |
<div className="flex items-center gap-2 py-1 px-2 text-sm hover:bg-[#2a2d2e] rounded cursor-pointer text-blue-400">
|
| 417 |
-
<CaretDown
|
| 418 |
-
Copy size={12} weight="bold" />
|
| 419 |
{file.name}
|
| 420 |
</div>
|
| 421 |
{file.children?.map(child => (
|
|
|
|
| 244 |
a.download = activeFileName || 'main.dart'
|
| 245 |
a.click()
|
| 246 |
URL.revokeObjectURL(url)
|
| 247 |
+
}
|
| 248 |
|
| 249 |
const handleCopyCode = () => {
|
| 250 |
navigator.clipboard.writeText(code).then(() => {
|
|
|
|
| 253 |
console.error("Failed to copy:", err)
|
| 254 |
})
|
| 255 |
}
|
|
|
|
| 256 |
|
| 257 |
return (
|
| 258 |
<Window
|
|
|
|
| 319 |
</div>
|
| 320 |
|
| 321 |
<div className="flex items-center gap-2">
|
|
|
|
| 322 |
<button
|
| 323 |
onClick={handleCopyCode}
|
| 324 |
className="flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs font-medium transition-colors"
|
|
|
|
| 327 |
<Copy size={14} weight="bold" />
|
| 328 |
Copy Code
|
| 329 |
</button>
|
| 330 |
+
<button
|
| 331 |
onClick={handleDownload}
|
| 332 |
className="p-1.5 text-gray-400 hover:text-white hover:bg-[#3e3e42] rounded transition-colors"
|
| 333 |
title="Download Code"
|
|
|
|
| 406 |
<div className="flex-1 overflow-y-auto py-2">
|
| 407 |
<div className="px-2">
|
| 408 |
<div className="flex items-center gap-1 py-1 px-2 text-sm text-gray-300 hover:bg-[#2a2d2e] rounded cursor-pointer">
|
| 409 |
+
<CaretDown size={12} weight="bold" />
|
|
|
|
| 410 |
<span className="font-bold">Flutter App</span>
|
| 411 |
</div>
|
| 412 |
<div className="pl-4">
|
| 413 |
{files.map(file => (
|
| 414 |
<div key={file.id}>
|
| 415 |
<div className="flex items-center gap-2 py-1 px-2 text-sm hover:bg-[#2a2d2e] rounded cursor-pointer text-blue-400">
|
| 416 |
+
<CaretDown size={12} weight="bold" />
|
|
|
|
| 417 |
{file.name}
|
| 418 |
</div>
|
| 419 |
{file.children?.map(child => (
|
app/components/GeminiChat.tsx
CHANGED
|
@@ -229,7 +229,7 @@ export function GeminiChat({ onClose, onMinimize, onMaximize, onFocus, zIndex }:
|
|
| 229 |
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
| 230 |
>
|
| 231 |
<div
|
| 232 |
-
className={`max-w-[80%] ${message.role === 'user'
|
| 233 |
? 'bg-blue-600 text-white rounded-2xl rounded-tr-none'
|
| 234 |
: 'bg-gray-100 text-gray-800 rounded-2xl rounded-tl-none'
|
| 235 |
} px-4 py-2 text-sm`}
|
|
@@ -281,8 +281,12 @@ export function GeminiChat({ onClose, onMinimize, onMaximize, onFocus, zIndex }:
|
|
| 281 |
value={input}
|
| 282 |
onChange={(e) => setInput(e.target.value)}
|
| 283 |
onKeyDown={handleKeyPress}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
placeholder="Ask Gemini..."
|
| 285 |
-
className="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 transition-all"
|
| 286 |
disabled={isLoading}
|
| 287 |
/>
|
| 288 |
<button
|
|
|
|
| 229 |
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
| 230 |
>
|
| 231 |
<div
|
| 232 |
+
className={`max-w-[80%] select-text ${message.role === 'user'
|
| 233 |
? 'bg-blue-600 text-white rounded-2xl rounded-tr-none'
|
| 234 |
: 'bg-gray-100 text-gray-800 rounded-2xl rounded-tl-none'
|
| 235 |
} px-4 py-2 text-sm`}
|
|
|
|
| 281 |
value={input}
|
| 282 |
onChange={(e) => setInput(e.target.value)}
|
| 283 |
onKeyDown={handleKeyPress}
|
| 284 |
+
onPaste={(e) => {
|
| 285 |
+
// Allow paste - default behavior
|
| 286 |
+
e.stopPropagation()
|
| 287 |
+
}}
|
| 288 |
placeholder="Ask Gemini..."
|
| 289 |
+
className="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 transition-all select-text"
|
| 290 |
disabled={isLoading}
|
| 291 |
/>
|
| 292 |
<button
|
app/components/LaTeXEditor.tsx
CHANGED
|
@@ -203,39 +203,115 @@ export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }:
|
|
| 203 |
useEffect(() => {
|
| 204 |
if (previewRef.current) {
|
| 205 |
try {
|
| 206 |
-
//
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
.replace(/\\
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
previewRef.current.innerHTML = ''
|
| 225 |
|
| 226 |
parts.forEach(part => {
|
| 227 |
-
if (part.startsWith('\\[') || part.startsWith('$')) {
|
| 228 |
-
|
| 229 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
try {
|
| 231 |
-
katex.render(math, span, {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
previewRef.current?.appendChild(span)
|
| 233 |
} catch (e) {
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
}
|
| 236 |
-
} else {
|
|
|
|
| 237 |
const div = document.createElement('div')
|
| 238 |
div.innerHTML = part
|
|
|
|
| 239 |
previewRef.current?.appendChild(div)
|
| 240 |
}
|
| 241 |
})
|
|
|
|
| 203 |
useEffect(() => {
|
| 204 |
if (previewRef.current) {
|
| 205 |
try {
|
| 206 |
+
// Extract title, author, and date from LaTeX preamble
|
| 207 |
+
let title = 'Untitled Document'
|
| 208 |
+
let author = 'Anonymous'
|
| 209 |
+
let date = new Date().toLocaleDateString()
|
| 210 |
+
|
| 211 |
+
const titleMatch = code.match(/\\title\{([^}]+)\}/)
|
| 212 |
+
const authorMatch = code.match(/\\author\{([^}]+)\}/)
|
| 213 |
+
const dateMatch = code.match(/\\date\{([^}]+)\}/)
|
| 214 |
+
|
| 215 |
+
if (titleMatch) title = titleMatch[1]
|
| 216 |
+
if (authorMatch) author = authorMatch[1]
|
| 217 |
+
if (dateMatch) {
|
| 218 |
+
date = dateMatch[1].replace(/\\today/g, new Date().toLocaleDateString())
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// Process the document content
|
| 222 |
+
let processedText = code
|
| 223 |
+
// Remove preamble
|
| 224 |
+
.replace(/\\documentclass\{[^}]+\}/g, '')
|
| 225 |
+
.replace(/\\usepackage\{[^}]+\}/g, '')
|
| 226 |
+
.replace(/\\title\{[^}]+\}/g, '')
|
| 227 |
+
.replace(/\\author\{[^}]+\}/g, '')
|
| 228 |
+
.replace(/\\date\{[^}]+\}/g, '')
|
| 229 |
+
.replace(/\\begin\{document\}/g, '')
|
| 230 |
+
.replace(/\\end\{document\}/g, '')
|
| 231 |
+
|
| 232 |
+
// Handle title
|
| 233 |
+
.replace(/\\maketitle/g, `
|
| 234 |
+
<div style="text-align: center; margin-bottom: 2em; padding-bottom: 1em; border-bottom: 1px solid #ccc;">
|
| 235 |
+
<h1 style="font-size: 2em; margin: 0.5em 0;">${title}</h1>
|
| 236 |
+
<p style="margin: 0.3em 0; font-size: 1.1em;">${author}</p>
|
| 237 |
+
<p style="margin: 0.3em 0; color: #666;">${date}</p>
|
| 238 |
+
</div>
|
| 239 |
+
`)
|
| 240 |
+
|
| 241 |
+
// Sections and structure
|
| 242 |
+
.replace(/\\section\{([^}]+)\}/g, '<h2 style="font-size: 1.5em; margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;">$1</h2>')
|
| 243 |
+
.replace(/\\subsection\{([^}]+)\}/g, '<h3 style="font-size: 1.2em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold;">$1</h3>')
|
| 244 |
+
.replace(/\\subsubsection\{([^}]+)\}/g, '<h4 style="font-size: 1em; margin-top: 0.8em; margin-bottom: 0.3em; font-weight: bold;">$1</h4>')
|
| 245 |
+
.replace(/\\paragraph\{([^}]+)\}/g, '<p style="font-weight: bold; margin-top: 0.5em;">$1</p>')
|
| 246 |
+
|
| 247 |
+
// Text formatting
|
| 248 |
+
.replace(/\\textbf\{([^}]+)\}/g, '<strong>$1</strong>')
|
| 249 |
+
.replace(/\\textit\{([^}]+)\}/g, '<em>$1</em>')
|
| 250 |
+
.replace(/\\underline\{([^}]+)\}/g, '<u>$1</u>')
|
| 251 |
+
.replace(/\\emph\{([^}]+)\}/g, '<em>$1</em>')
|
| 252 |
+
.replace(/\\texttt\{([^}]+)\}/g, '<code style="font-family: monospace; background: #f5f5f5; padding: 2px 4px; border-radius: 3px;">$1</code>')
|
| 253 |
+
|
| 254 |
+
// Lists
|
| 255 |
+
.replace(/\\begin\{itemize\}/g, '<ul style="margin: 0.5em 0; padding-left: 1.5em;">')
|
| 256 |
+
.replace(/\\end\{itemize\}/g, '</ul>')
|
| 257 |
+
.replace(/\\begin\{enumerate\}/g, '<ol style="margin: 0.5em 0; padding-left: 1.5em;">')
|
| 258 |
+
.replace(/\\end\{enumerate\}/g, '</ol>')
|
| 259 |
+
.replace(/\\item\s+/g, '<li style="margin: 0.3em 0;">')
|
| 260 |
+
|
| 261 |
+
// Paragraphs and line breaks
|
| 262 |
+
.replace(/\\\\/g, '<br />')
|
| 263 |
+
.replace(/\\newline/g, '<br />')
|
| 264 |
+
.replace(/\\par\b/g, '</p><p>')
|
| 265 |
+
|
| 266 |
+
// Clean up extra whitespace
|
| 267 |
+
.replace(/\n\n+/g, '</p><p style="margin: 0.5em 0;">')
|
| 268 |
+
|
| 269 |
+
// Split by math delimiters to render KaTeX (improved regex for display and inline math)
|
| 270 |
+
const parts = processedText.split(/(\\\[[\s\S]*?\\\]|\$\$[\s\S]*?\$\$|\$[^$]+\$|\\\([\s\S]*?\\\))/)
|
| 271 |
previewRef.current.innerHTML = ''
|
| 272 |
|
| 273 |
parts.forEach(part => {
|
| 274 |
+
if (part.startsWith('\\[') || part.startsWith('$$') || part.startsWith('$') || part.startsWith('\\(')) {
|
| 275 |
+
// Determine if it's display math or inline math
|
| 276 |
+
const isDisplayMath = part.startsWith('\\[') || part.startsWith('$$')
|
| 277 |
+
|
| 278 |
+
// Extract the math content
|
| 279 |
+
let math = part
|
| 280 |
+
if (part.startsWith('\\[') && part.endsWith('\\]')) {
|
| 281 |
+
math = part.slice(2, -2)
|
| 282 |
+
} else if (part.startsWith('$$') && part.endsWith('$$')) {
|
| 283 |
+
math = part.slice(2, -2)
|
| 284 |
+
} else if (part.startsWith('$') && part.endsWith('$')) {
|
| 285 |
+
math = part.slice(1, -1)
|
| 286 |
+
} else if (part.startsWith('\\(') && part.endsWith('\\)')) {
|
| 287 |
+
math = part.slice(2, -2)
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
const span = document.createElement(isDisplayMath ? 'div' : 'span')
|
| 291 |
+
if (isDisplayMath) {
|
| 292 |
+
span.style.textAlign = 'center'
|
| 293 |
+
span.style.margin = '1em 0'
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
try {
|
| 297 |
+
katex.render(math, span, {
|
| 298 |
+
throwOnError: false,
|
| 299 |
+
displayMode: isDisplayMath,
|
| 300 |
+
strict: false
|
| 301 |
+
})
|
| 302 |
previewRef.current?.appendChild(span)
|
| 303 |
} catch (e) {
|
| 304 |
+
// If KaTeX fails, show the raw math
|
| 305 |
+
const errorSpan = document.createElement('span')
|
| 306 |
+
errorSpan.style.color = '#cc0000'
|
| 307 |
+
errorSpan.textContent = part
|
| 308 |
+
previewRef.current?.appendChild(errorSpan)
|
| 309 |
}
|
| 310 |
+
} else if (part.trim()) {
|
| 311 |
+
// Only add non-empty parts
|
| 312 |
const div = document.createElement('div')
|
| 313 |
div.innerHTML = part
|
| 314 |
+
div.style.margin = '0.5em 0'
|
| 315 |
previewRef.current?.appendChild(div)
|
| 316 |
}
|
| 317 |
})
|
app/components/Messages.tsx
CHANGED
|
@@ -215,7 +215,7 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
|
|
| 215 |
)}
|
| 216 |
<div
|
| 217 |
className={`
|
| 218 |
-
max-w-[70%] px-3 py-1.5 text-[13px] leading-relaxed break-words relative group
|
| 219 |
${isMe
|
| 220 |
? 'bg-[#0A84FF] text-white rounded-2xl rounded-br-sm message-bubble-sent'
|
| 221 |
: 'bg-[#3a3a3a] text-gray-100 rounded-2xl rounded-bl-sm message-bubble-received'}
|
|
@@ -268,10 +268,20 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
|
|
| 268 |
type="text"
|
| 269 |
value={inputText}
|
| 270 |
onChange={(e) => setInputText(e.target.value)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
placeholder="iMessage"
|
| 272 |
maxLength={200}
|
| 273 |
disabled={isLoading}
|
| 274 |
-
className="w-full bg-[#2a2a2a] border border-white/10 rounded-full py-1.5 pl-4 pr-10 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-gray-500 transition-colors"
|
| 275 |
/>
|
| 276 |
<button
|
| 277 |
type="submit"
|
|
|
|
| 215 |
)}
|
| 216 |
<div
|
| 217 |
className={`
|
| 218 |
+
max-w-[70%] px-3 py-1.5 text-[13px] leading-relaxed break-words relative group select-text
|
| 219 |
${isMe
|
| 220 |
? 'bg-[#0A84FF] text-white rounded-2xl rounded-br-sm message-bubble-sent'
|
| 221 |
: 'bg-[#3a3a3a] text-gray-100 rounded-2xl rounded-bl-sm message-bubble-received'}
|
|
|
|
| 268 |
type="text"
|
| 269 |
value={inputText}
|
| 270 |
onChange={(e) => setInputText(e.target.value)}
|
| 271 |
+
onKeyDown={(e) => {
|
| 272 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 273 |
+
e.preventDefault()
|
| 274 |
+
handleSend(e)
|
| 275 |
+
}
|
| 276 |
+
}}
|
| 277 |
+
onPaste={(e) => {
|
| 278 |
+
// Allow paste - default behavior
|
| 279 |
+
e.stopPropagation()
|
| 280 |
+
}}
|
| 281 |
placeholder="iMessage"
|
| 282 |
maxLength={200}
|
| 283 |
disabled={isLoading}
|
| 284 |
+
className="w-full bg-[#2a2a2a] border border-white/10 rounded-full py-1.5 pl-4 pr-10 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-gray-500 transition-colors select-text"
|
| 285 |
/>
|
| 286 |
<button
|
| 287 |
type="submit"
|
app/globals.css
CHANGED
|
@@ -269,6 +269,25 @@
|
|
| 269 |
content: '+';
|
| 270 |
}
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
/* Custom Scrollbar */
|
| 273 |
::-webkit-scrollbar {
|
| 274 |
width: 10px;
|
|
|
|
| 269 |
content: '+';
|
| 270 |
}
|
| 271 |
|
| 272 |
+
/* Text Selection Styling */
|
| 273 |
+
::selection {
|
| 274 |
+
background: rgba(59, 130, 246, 0.3);
|
| 275 |
+
color: inherit;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
::-moz-selection {
|
| 279 |
+
background: rgba(59, 130, 246, 0.3);
|
| 280 |
+
color: inherit;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
/* Enable text selection for specific elements */
|
| 284 |
+
.select-text {
|
| 285 |
+
user-select: text !important;
|
| 286 |
+
-webkit-user-select: text !important;
|
| 287 |
+
-moz-user-select: text !important;
|
| 288 |
+
cursor: text;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
/* Custom Scrollbar */
|
| 292 |
::-webkit-scrollbar {
|
| 293 |
width: 10px;
|