diff --git a/backend/routes/export.py b/backend/routes/export.py index 8eb9405..35b7846 100644 --- a/backend/routes/export.py +++ b/backend/routes/export.py @@ -72,7 +72,7 @@ async def export_messages(req: ExportMessagesRequest): if req.format == ExportFormat.json: content = json.dumps({"messages": messages, "exported_at": ts}, indent=2, ensure_ascii=False) media = "application/json" - filename = f"localmind_messages_{ts.replace(' ', '_')}.json" + filename = f"localmind_messages_{ts.replace(' ', '_').replace(':', '-')}.json" elif req.format == ExportFormat.markdown: lines = ["# LocalMind – Exported Messages\n", f"*Exported: {ts}*\n\n---\n"] @@ -84,7 +84,7 @@ async def export_messages(req: ExportMessagesRequest): lines.append("\n---\n") content = "\n".join(lines) media = "text/markdown" - filename = f"localmind_messages_{ts.replace(' ', '_')}.md" + filename = f"localmind_messages_{ts.replace(' ', '_').replace(':', '-')}.md" else: lines = ["LocalMind Export — Selected Messages", f"Exported: {ts}", "=" * 50, ""] @@ -93,7 +93,7 @@ async def export_messages(req: ExportMessagesRequest): lines += [f"[{role}]", m["content"], ""] content = "\n".join(lines) media = "text/plain" - filename = f"localmind_messages_{ts.replace(' ', '_')}.txt" + filename = f"localmind_messages_{ts.replace(' ', '_').replace(':', '-')}.txt" return Response( content=content.encode("utf-8"), diff --git a/frontend/src/components/ChatWindow.jsx b/frontend/src/components/ChatWindow.jsx index 2faadb0..90d32ab 100644 --- a/frontend/src/components/ChatWindow.jsx +++ b/frontend/src/components/ChatWindow.jsx @@ -1,82 +1,23 @@ import { useState, useRef, useEffect } from "react"; import { exportSession } from "../utils/api"; -import { AppLogoIcon, CloseIcon, FileIcon, LockIcon, PlusCircleIcon, TemplateIcon } from "./Icons"; -import CodeBlockWithCopy from "./CodeBlockWithCopy"; -import PromptTemplateDialog from "./PromptTemplateDialog"; +import { AppLogoIcon, FileIcon, LockIcon } from "./Icons"; export default function ChatWindow({ messages, loading, onSend, sessionId }) { const [input, setInput] = useState(""); const bottomRef = useRef(null); const textareaRef = useRef(null); - // NEW: state for selected messages and export format - const [selectedMessages, setSelectedMessages] = useState([]); - const [exportFormat, setExportFormat] = useState("markdown"); - useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - // Close plus menu on outside click - useEffect(() => { - function handleClickOutside(e) { - if (plusMenuRef.current && !plusMenuRef.current.contains(e.target)) { - setShowPlusMenu(false); - } - } - if (showPlusMenu) document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [showPlusMenu]); - - function handleSelectTemplate(template) { - setSelectedTemplate(template); - setShowTemplateDialog(false); - setShowPlusMenu(false); - setTimeout(() => textareaRef.current?.focus(), 0); - } - - // Parse code blocks for copy button - function parseMessageWithCodeBlocks(content) { - if (!content) return [{ type: "text", content: "" }]; - const parts = []; - const regex = /```(\w*)\n([\s\S]*?)```/g; - let lastIndex = 0; - let match; - while ((match = regex.exec(content)) !== null) { - if (match.index > lastIndex) { - parts.push({ type: "text", content: content.slice(lastIndex, match.index) }); - } - parts.push({ - type: "code", - language: match[1] || "text", - code: match[2].trim() - }); - lastIndex = match.index + match[0].length; - } - if (lastIndex < content.length) { - parts.push({ type: "text", content: content.slice(lastIndex) }); - } - if (parts.length === 0) { - parts.push({ type: "text", content }); - } - return parts; - } - function send() { - if ((!input.trim() && !selectedTemplate) || loading) return; - const message = selectedTemplate - ? `${selectedTemplate.prompt}\n\n${input.trim()}`.trim() - : input.trim(); - onSend(message); + if (!input.trim() || loading) return; + onSend(input.trim()); setInput(""); - if (textareaRef.current) { - textareaRef.current.style.height = "auto"; - } + if (textareaRef.current) { textareaRef.current.style.height = "auto"; } } function handleKey(e) { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - send(); - } + if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } } function autoResize(e) { @@ -84,56 +25,6 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) { e.target.style.height = Math.min(e.target.scrollHeight, 160) + "px"; } - // Message selection and export - const toggleSelectMessage = (msgId) => { - setSelectedMessages(prev => - prev.includes(msgId) ? prev.filter(id => id !== msgId) : [...prev, msgId] - ); - }; - - const handleExportSelected = async () => { - if (selectedMessages.length === 0) return; - try { - const response = await fetch("/api/export/messages", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message_ids: selectedMessages, format: exportFormat }), - }); - if (!response.ok) throw new Error("Export failed"); - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `localmind_export.${exportFormat === "markdown" ? "md" : exportFormat}`; - a.click(); - URL.revokeObjectURL(url); - } catch (err) { - console.error(err); - alert("Failed to export messages"); - } - }; - - const exportSingleMessage = async (msgId) => { - try { - const response = await fetch("/api/export/messages", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message_ids: [msgId], format: exportFormat }), - }); - if (!response.ok) throw new Error("Export failed"); - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `localmind_message_${msgId}.${exportFormat === "markdown" ? "md" : exportFormat}`; - a.click(); - URL.revokeObjectURL(url); - } catch (err) { - console.error(err); - alert("Failed to export message"); - } - }; - const SUGGESTIONS = [ "Summarize the uploaded document", "What are the key points?", @@ -143,67 +34,31 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) { return (
- {/* Export bar – existing for whole session + new selection bar */} + {/* Export bar */} {messages.length > 0 && (
- {["markdown", "json", "txt"].map(f => ( - ))}
)} - {/* Export selection bar */} - {selectedMessages.length > 0 && ( -
- {selectedMessages.length} message(s) selected -
- - - -
-
- )} - {/* Messages */}
{messages.length === 0 && (
- +

LocalMind is ready

100% private · runs offline · no cloud

{SUGGESTIONS.map(s => ( - ))} @@ -213,15 +68,6 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) { {messages.map((msg, i) => (
- {/* Checkbox for selection */} -
- toggleSelectMessage(msg.id)} - className="w-4 h-4 rounded border-gray-600 bg-gray-800 text-purple-600 focus:ring-purple-500 focus:ring-1" - /> -
{msg.role === "assistant" && (
@@ -230,34 +76,17 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) { {msg.streaming && typing...}
)} -
- {msg.role === "user" ? ( - <> - {msg.content} - {msg.streaming && } - - ) : ( - <> - {parseMessageWithCodeBlocks(msg.content).map((part, idx) => ( - part.type === "code" ? ( - - ) : ( -
{part.content}
- ) - ))} - {msg.streaming && } - - )} + ${msg.role === "user" + ? "bg-purple-700 text-white rounded-br-sm" + : "bg-gray-800 text-gray-100 rounded-bl-sm border border-gray-700"}`}> + {msg.content} + {msg.streaming && }
- {msg.sources?.length > 0 && (
- {msg.sources.map((s, idx) => ( - + {msg.sources.map((s,i) => ( + {s} @@ -267,27 +96,8 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) {
)} {msg.role === "user" && ( -
+
You - {/* Per-message export button */} - -
- )} - {msg.role === "assistant" && ( -
-
)}
@@ -302,12 +112,9 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) { LocalMind
- {[0, 1, 2].map(i => ( -
+ {[0,1,2].map(i => ( +
))}
@@ -316,7 +123,7 @@ export default function ChatWindow({ messages, loading, onSend, sessionId }) {
- {/* Input Form Footer */} + {/* Input */}