Commit 10eb3289 authored by Mahmoud Aglan's avatar Mahmoud Aglan

kokowawa

parent 459309cd
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -3,7 +3,9 @@ Code extraction and file download helpers. ...@@ -3,7 +3,9 @@ Code extraction and file download helpers.
""" """
import io import io
import re
import zipfile import zipfile
from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import APIRouter from fastapi import APIRouter
...@@ -16,6 +18,7 @@ router = APIRouter() ...@@ -16,6 +18,7 @@ router = APIRouter()
class ExtractBody(BaseModel): class ExtractBody(BaseModel):
markdown: str markdown: str
title: Optional[str] = None
@router.post("/extract") @router.post("/extract")
...@@ -30,20 +33,31 @@ def download_zip(body: ExtractBody): ...@@ -30,20 +33,31 @@ def download_zip(body: ExtractBody):
if not blocks: if not blocks:
return {"error": "No code blocks found"} return {"error": "No code blocks found"}
# Keep LAST occurrence of each filename (latest version wins)
file_map: dict[str, str] = {}
for b in blocks:
file_map[b["filename"]] = b["code"]
if not file_map:
return {"error": "No code blocks found"}
# Build a safe zip filename from chat title
raw_title = (body.title or "").strip()
if not raw_title or raw_title == "New Chat":
safe_title = "code"
else:
safe_title = re.sub(r'[^\w\s-]', '', raw_title).strip()
safe_title = re.sub(r'[\s]+', '-', safe_title)[:60] or "code"
zip_filename = f"{safe_title}.zip"
buf = io.BytesIO() buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
seen = set() for name, code in file_map.items():
for b in blocks: zf.writestr(name, code)
name = b["filename"]
if name in seen:
base, ext = name.rsplit(".", 1) if "." in name else (name, "txt")
name = f"{base}_{len(seen)}.{ext}"
seen.add(name)
zf.writestr(name, b["code"])
buf.seek(0) buf.seek(0)
return StreamingResponse( return StreamingResponse(
buf, buf,
media_type="application/zip", media_type="application/zip",
headers={"Content-Disposition": "attachment; filename=son-of-anton-code.zip"}, headers={"Content-Disposition": f'attachment; filename="{zip_filename}"'},
) )
\ No newline at end of file
...@@ -193,11 +193,11 @@ export const adminListChats = (token) => ...@@ -193,11 +193,11 @@ export const adminListChats = (token) =>
// Code Download // Code Download
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
export async function downloadZip(token, markdown) { export async function downloadZip(token, markdown, chatTitle) {
const res = await fetch(`${BASE}/files/download-zip`, { const res = await fetch(`${BASE}/files/download-zip`, {
method: "POST", method: "POST",
headers: headers(token), headers: headers(token),
body: JSON.stringify({ markdown }), body: JSON.stringify({ markdown, title: chatTitle || null }),
}); });
if (!res.ok) throw new Error("Download failed"); if (!res.ok) throw new Error("Download failed");
const ct = res.headers.get("content-type") || ""; const ct = res.headers.get("content-type") || "";
...@@ -206,7 +206,13 @@ export async function downloadZip(token, markdown) { ...@@ -206,7 +206,13 @@ export async function downloadZip(token, markdown) {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = "son-of-anton-code.zip"; // Derive filename from chat title, fallback to generic
const raw = (chatTitle || "").trim();
const safeName =
raw && raw !== "New Chat"
? raw.replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "code"
: "code";
a.download = `${safeName}.zip`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} else { } else {
......
...@@ -333,18 +333,16 @@ export default function ChatView({ chatId }) { ...@@ -333,18 +333,16 @@ export default function ChatView({ chatId }) {
<div className="flex items-end gap-1.5"> <div className="flex items-end gap-1.5">
<button <button
onClick={toggleSettings} onClick={toggleSettings}
className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${ className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${showSettings ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white hover:bg-anton-card active:bg-anton-card"
showSettings ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white hover:bg-anton-card active:bg-anton-card" }`}
}`}
> >
<Settings2 size={18} /> <Settings2 size={18} />
</button> </button>
<button <button
onClick={() => fileRef.current?.click()} onClick={() => fileRef.current?.click()}
className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${ 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 active:bg-anton-card"
pendingFiles.length ? "bg-green-500/20 text-green-400" : "text-anton-muted hover:text-white hover:bg-anton-card active:bg-anton-card" }`}
}`}
title="Attach files" title="Attach files"
> >
<Paperclip size={18} /> <Paperclip size={18} />
...@@ -407,7 +405,7 @@ export default function ChatView({ chatId }) { ...@@ -407,7 +405,7 @@ export default function ChatView({ chatId }) {
<button <button
onClick={async () => { onClick={async () => {
const all = messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n"); const all = messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n");
if (all) try { await downloadZip(state.token, all); } catch { /* */ } if (all) try { await downloadZip(state.token, all, currentChat?.title); } catch { /* */ }
}} }}
className="ml-auto hover:text-anton-accent transition" className="ml-auto hover:text-anton-accent transition"
> >
......
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