Commit c324caf4 authored by Administrator's avatar Administrator

Update frontend/src/components/ChatView.jsx via Son of Anton

parent 57750e01
......@@ -3,7 +3,7 @@ import { useApp } from "../store";
import { getMessages, downloadZip, listKnowledgeBases, updateChat, uploadAttachments, gitlabListRepos, gitlabCommitSingle, refreshRepoContext, exportPptx, exportDocx } from "../api";
import * as streamManager from "../streamManager";
import MessageBubble from "./MessageBubble";
import { Send, Square, Settings2, X, Brain, BookOpen, Paperclip, FileText, Loader2, Upload, Film, Image as ImageIcon, FileCode, GitBranch, RefreshCw, Globe, Presentation, FileOutput, Wand2, ChevronDown } from "lucide-react";
import { Send, Square, Settings2, X, Brain, BookOpen, Paperclip, FileText, Loader2, Upload, Film, Image as ImageIcon, FileCode, GitBranch, RefreshCw, Globe, Presentation, FileOutput, Wand2, ChevronDown, Paintbrush } from "lucide-react";
const MODELS = [
{ id: "eu.anthropic.claude-opus-4-6-v1", label: "Opus 4.6" },
......@@ -13,6 +13,23 @@ const TYPE_ICONS = { image: ImageIcon, video: Film, document: FileText, text: Fi
const TYPE_COLORS = { image: "border-blue-500/40 bg-blue-500/10", video: "border-purple-500/40 bg-purple-500/10", document: "border-amber-500/40 bg-amber-500/10", text: "border-green-500/40 bg-green-500/10" };
const TYPE_ICON_COLORS = { image: "text-blue-400", video: "text-purple-400", document: "text-amber-400", text: "text-green-400" };
const UI_DESIGN_PREFIX = `[UI DESIGN MODE] You are now a world-class UI/UX designer. Generate COMPLETE, SELF-CONTAINED HTML files that render beautifully in a browser preview.
CRITICAL RULES:
- Output a SINGLE HTML file using the format \`\`\`html:design.html with ALL CSS and JS embedded inline
- Include <meta name="viewport" content="width=device-width, initial-scale=1.0">
- Use modern CSS (flexbox, grid, custom properties, transitions, animations)
- Make it fully responsive (mobile-first)
- Use realistic, professional content (NOT "Lorem ipsum")
- Include hover states, focus states, and micro-interactions
- Use a cohesive color palette and typography (Google Fonts via CDN OK)
- If the design uses Tailwind-like utilities, include the Tailwind CDN script
- For multi-screen apps, generate separate HTML files per screen
- The output MUST look production-quality and polished
- Include subtle shadows, gradients, and modern design patterns
`;
function classifyFile(f) { const ext = (f.name || "").split(".").pop().toLowerCase(); const mime = f.type || ""; if (mime.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(ext)) return "image"; if (mime.startsWith("video/") || ["mp4", "mov", "avi", "mkv", "webm"].includes(ext)) return "video"; if (mime === "application/pdf" || ext === "pdf") return "document"; return "text"; }
function fmtSize(b) { if (!b) return "0B"; if (b < 1024) return b + "B"; if (b < 1048576) return (b / 1024).toFixed(0) + "KB"; return (b / 1048576).toFixed(1) + "MB"; }
......@@ -31,6 +48,7 @@ export default function ChatView({ chatId }) {
const [selectedKbId, setSelectedKbId] = useState(currentChat?.knowledge_base_id || null);
const [selectedRepoId, setSelectedRepoId] = useState(currentChat?.linked_repo_id || null);
const [webSearch, setWebSearch] = useState(false);
const [uiDesign, setUiDesign] = useState(false);
const [kbs, setKbs] = useState([]);
const [repos, setRepos] = useState([]);
const [pendingFiles, setPendingFiles] = useState([]);
......@@ -57,15 +75,19 @@ export default function ChatView({ chatId }) {
function removePending(i) { setPendingFiles(prev => { if (prev[i]?.preview) URL.revokeObjectURL(prev[i].preview); return prev.filter((_, j) => j !== i); }); }
const handleSend = useCallback(async () => {
const content = input.trim(); if ((!content && !pendingFiles.length) || streamData.streaming) return;
const text = content || "Please analyze the attached file(s).";
const raw = input.trim(); if ((!raw && !pendingFiles.length) || streamData.streaming) return;
const text = raw || "Please analyze the attached file(s).";
// Prepend UI design instructions if mode is active
const content = uiDesign ? UI_DESIGN_PREFIX + text : text;
let attIds = [], uploaded = [];
if (pendingFiles.length) { setUploading(true); try { const res = await uploadAttachments(state.token, chatId, pendingFiles.map(p => p.file)); uploaded = (res.attachments || []).filter(a => !a.error); attIds = uploaded.map(a => a.id); } catch { setUploading(false); return; } setUploading(false); }
dispatch({ type: "ADD_MESSAGE", chatId, message: { id: `tmp-${Date.now()}`, role: "user", content: text, created_at: new Date().toISOString(), attachments: uploaded } });
setInput(""); pendingFiles.forEach(p => { if (p.preview) URL.revokeObjectURL(p.preview); }); setPendingFiles([]); autoScroll.current = true;
if (inputRef.current) inputRef.current.style.height = "auto";
streamManager.startStream({ token: state.token, chatId, body: { content: text, model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId, attachment_ids: attIds, web_search: webSearch } });
}, [input, pendingFiles, streamData.streaming, state.token, chatId, model, maxTokens, reasoningBudget, selectedKbId, webSearch, dispatch]);
streamManager.startStream({ token: state.token, chatId, body: { content, model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId, attachment_ids: attIds, web_search: webSearch } });
}, [input, pendingFiles, streamData.streaming, state.token, chatId, model, maxTokens, reasoningBudget, selectedKbId, webSearch, uiDesign, dispatch]);
function handleKeyDown(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }
function handlePaste(e) { const items = Array.from(e.clipboardData?.items || []).filter(i => i.kind === "file"); if (!items.length) return; e.preventDefault(); addFiles(items.map(i => i.getAsFile()).filter(Boolean)); }
......@@ -103,13 +125,23 @@ export default function ChatView({ chatId }) {
</div>
)}
{/* UI Design Mode Banner */}
{uiDesign && (
<div className="px-3 py-1.5 bg-blue-500/10 border-b border-blue-500/20 flex items-center gap-2 text-xs">
<Paintbrush size={12} className="text-blue-400" />
<span className="text-blue-300 font-medium">UI Design Mode</span>
<span className="text-blue-300/60">— Generating previewable HTML designs</span>
<button onClick={() => setUiDesign(false)} className="ml-auto text-blue-400/60 hover:text-blue-300 transition"><X size={12} /></button>
</div>
)}
<div ref={scrollRef} onScroll={onScroll} className="flex-1 overflow-y-auto overscroll-contain px-3 sm:px-4 py-3 sm:py-4 space-y-3">
{messages.map(m => <MessageBubble key={m.id} message={m} token={state.token} linkedRepo={linkedRepo} onCommit={handleCommitFromChat} chatId={chatId} />)}
{streaming && (streamData.thinking || streamData.text) && <MessageBubble message={{ id: "streaming", role: "assistant", content: streamData.text, thinking_content: streamData.thinking || null, attachments: [] }} isStreaming isThinking={streamData.isThinking} token={state.token} />}
{streaming && !streamData.text && !streamData.thinking && (
<div className="flex items-center gap-2 px-3 py-3 animate-fade-in">
<div className="flex gap-1">{[0, 150, 300].map(d => <span key={d} className="w-1.5 h-1.5 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: d + "ms" }} />)}</div>
<span className="text-anton-muted text-sm">{webSearch ? "Searching web & thinking…" : linkedRepo ? "Loading codebase & thinking…" : "Thinking…"}</span>
<span className="text-anton-muted text-sm">{uiDesign ? "Designing UI…" : webSearch ? "Searching web & thinking…" : linkedRepo ? "Loading codebase & thinking…" : "Thinking…"}</span>
</div>
)}
</div>
......@@ -145,14 +177,23 @@ export default function ChatView({ chatId }) {
{/* Tools menu button */}
<div className="relative">
<button onClick={() => { setShowTools(!showTools); setShowSettings(false); }} className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${showTools ? "bg-blue-500/20 text-blue-400" : webSearch ? "bg-green-500/20 text-green-400" : "text-anton-muted hover:text-white hover:bg-anton-card"}`} title="Tools"><Wand2 size={18} /></button>
<button onClick={() => { setShowTools(!showTools); setShowSettings(false); }} className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${showTools ? "bg-blue-500/20 text-blue-400" : (webSearch || uiDesign) ? "bg-green-500/20 text-green-400" : "text-anton-muted hover:text-white hover:bg-anton-card"}`} title="Tools"><Wand2 size={18} /></button>
{showTools && (
<div className="absolute bottom-full left-0 mb-2 w-56 bg-anton-card border border-anton-border rounded-xl shadow-2xl p-2 space-y-1 animate-fade-in z-30">
<p className="text-[10px] text-anton-muted px-2 py-1 uppercase tracking-wider font-semibold">Tools</p>
{/* UI Design Mode Toggle */}
<button onClick={() => { setUiDesign(!uiDesign); }} className={`w-full flex items-center justify-between gap-2 px-3 py-2.5 rounded-lg text-sm transition ${uiDesign ? "bg-blue-500/15 text-blue-400" : "text-white hover:bg-anton-bg"}`}>
<span className="flex items-center gap-2"><Paintbrush size={15} /> UI Design</span>
<div className={`w-8 h-4.5 rounded-full transition-colors ${uiDesign ? "bg-blue-500" : "bg-anton-border"}`}><div className={`w-3.5 h-3.5 rounded-full bg-white shadow transform transition-transform mt-0.5 ${uiDesign ? "translate-x-4 ml-0.5" : "translate-x-0.5"}`} /></div>
</button>
{/* Web Search Toggle */}
<button onClick={() => { setWebSearch(!webSearch); }} className={`w-full flex items-center justify-between gap-2 px-3 py-2.5 rounded-lg text-sm transition ${webSearch ? "bg-green-500/15 text-green-400" : "text-white hover:bg-anton-bg"}`}>
<span className="flex items-center gap-2"><Globe size={15} /> Web Search</span>
<div className={`w-8 h-4.5 rounded-full transition-colors ${webSearch ? "bg-green-500" : "bg-anton-border"}`}><div className={`w-3.5 h-3.5 rounded-full bg-white shadow transform transition-transform mt-0.5 ${webSearch ? "translate-x-4 ml-0.5" : "translate-x-0.5"}`} /></div>
</button>
<hr className="border-anton-border" />
<button onClick={() => handleExport("pptx")} disabled={!lastAssistantContent || !!exporting} className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg text-sm text-white hover:bg-anton-bg transition disabled:opacity-30 disabled:cursor-not-allowed">
{exporting === "pptx" ? <Loader2 size={15} className="animate-spin" /> : <Presentation size={15} className="text-orange-400" />} Download PPTX
......@@ -167,7 +208,7 @@ export default function ChatView({ chatId }) {
<button onClick={() => fileRef.current?.click()} className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${pendingFiles.length ? "bg-green-500/20 text-green-400" : "text-anton-muted hover:text-white hover:bg-anton-card"}`} title="Attach files"><Paperclip size={18} /></button>
<input ref={fileRef} type="file" multiple className="hidden" accept="image/*,video/*,.pdf,.txt,.md,.py,.js,.ts,.jsx,.tsx,.cs,.java,.cpp,.c,.h,.go,.rs,.rb,.php,.html,.css,.json,.yaml,.yml,.xml,.toml,.csv,.sql,.sh,.swift,.kt,.lua,.gd,.dart,.vue,.svelte,.log" onChange={e => { addFiles(Array.from(e.target.files || [])); e.target.value = ""; }} />
<div className="flex-1 min-w-0">
<textarea ref={inputRef} value={input} onChange={e => setInput(e.target.value)} onKeyDown={handleKeyDown} onPaste={handlePaste} placeholder={webSearch ? "Search the web & ask…" : pendingFiles.length ? "Add a message…" : linkedRepo ? `Ask about ${linkedRepo.name}…` : "Ask anything…"} rows={1} style={{ maxHeight: "120px" }} className="w-full bg-anton-card border border-anton-border rounded-xl px-3 py-2.5 text-white resize-none focus:outline-none focus:border-anton-accent transition leading-snug" onInput={e => { e.target.style.height = "auto"; e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px"; }} />
<textarea ref={inputRef} value={input} onChange={e => setInput(e.target.value)} onKeyDown={handleKeyDown} onPaste={handlePaste} placeholder={uiDesign ? "Describe a UI design… (e.g. a modern dashboard with dark theme)" : webSearch ? "Search the web & ask…" : pendingFiles.length ? "Add a message…" : linkedRepo ? `Ask about ${linkedRepo.name}…` : "Ask anything…"} rows={1} style={{ maxHeight: "120px" }} className="w-full bg-anton-card border border-anton-border rounded-xl px-3 py-2.5 text-white resize-none focus:outline-none focus:border-anton-accent transition leading-snug" onInput={e => { e.target.style.height = "auto"; e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px"; }} />
</div>
{streaming ? (
<button onClick={() => streamManager.abortStream(chatId)} className="p-2.5 rounded-xl bg-anton-danger text-white hover:opacity-80 transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center"><Square size={18} /></button>
......@@ -182,6 +223,7 @@ export default function ChatView({ chatId }) {
<span>{MODELS.find(m => m.id === model)?.label}</span>
<span></span><span>{maxTokens.toLocaleString()} tok</span>
{reasoningBudget > 0 && <><span></span><span className="text-purple-400">🧠 {reasoningBudget.toLocaleString()}</span></>}
{uiDesign && <><span></span><span className="text-blue-400">🎨 UI Design</span></>}
{webSearch && <><span></span><span className="text-green-400">🌐 Web</span></>}
{selectedKbId && <><span></span><span className="text-green-400">📚 RAG</span></>}
{linkedRepo && <><span></span><span className="text-orange-400">🔀 {linkedRepo.name}</span></>}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment