Commit 99edb79c authored by Mahmoud Aglan's avatar Mahmoud Aglan

cool on mobile

parent c0e521e8
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -3,10 +3,11 @@ Son of Anton — Main FastAPI Application
"""
import os
import time
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
......@@ -21,6 +22,9 @@ from backend.routes.files_routes import router as files_router
from backend.routes.attachment_routes import router as attachment_router
from backend.services.bedrock_service import close_http_client
APP_VERSION = "2.1.0"
APP_BUILD_TIME = str(int(time.time()))
def _run_migrations():
"""Add new columns/tables to existing DB if they're missing."""
......@@ -54,7 +58,7 @@ async def lifespan(app: FastAPI):
Base.metadata.create_all(bind=engine)
_run_migrations()
seed_superadmin()
print("Son of Anton is online.")
print(f"Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.")
yield
await close_http_client()
print("Son of Anton shutting down.")
......@@ -63,7 +67,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Son of Anton",
description="Avatar of All Elements of Code",
version="2.0.0",
version=APP_VERSION,
lifespan=lifespan,
)
......@@ -75,6 +79,38 @@ app.add_middleware(
allow_headers=["*"],
)
@app.middleware("http")
async def add_cache_headers(request: Request, call_next):
response: Response = await call_next(request)
path = request.url.path
# API responses: never cache
if path.startswith("/api"):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
# Hashed assets (contain hash in filename): cache aggressively
elif path.startswith("/assets/") and any(c in path for c in [".js", ".css"]):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
# HTML and everything else: never cache
elif path.endswith(".html") or not path.startswith("/assets"):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
# Always add version header for debugging
response.headers["X-App-Version"] = APP_VERSION
response.headers["X-Build-Time"] = APP_BUILD_TIME
return response
# Version endpoint for frontend to check
@app.get("/api/version")
def get_version():
return {"version": APP_VERSION, "build": APP_BUILD_TIME}
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(chat_router, prefix="/api/chats", tags=["Chats"])
app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
......@@ -98,8 +134,15 @@ async def serve_frontend(full_path: str):
raise HTTPException(status_code=404, detail="Not found")
file_path = FRONTEND_DIR / full_path
if full_path and file_path.is_file():
return FileResponse(str(file_path))
resp = FileResponse(str(file_path))
# Don't cache non-hashed static files
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
return resp
index = FRONTEND_DIR / "index.html"
if index.is_file():
return FileResponse(str(index))
return {"message": "Son of Anton API is running. Frontend not built."}
resp = FileResponse(str(index))
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0"
return resp
return {"message": "Son of Anton API is running. Frontend not built."}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Son of Anton</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>" />
</head>
<body class="bg-anton-bg text-anton-text font-sans">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Son of Anton</title>
<!-- KILL BROWSER CACHE FOR THIS HTML -->
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<!-- PWA / Mobile -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="theme-color" content="#09090f" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet" />
<link rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>" />
</head>
<body class="bg-anton-bg text-anton-text font-sans overscroll-none">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
\ No newline at end of file
......@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { Routes, Route } from "react-router-dom";
import { useApp } from "./store";
import { getMe } from "./api";
import * as streamManager from "./streamManager";
import LoginPage from "./pages/LoginPage";
import ChatPage from "./pages/ChatPage";
import AdminPage from "./pages/AdminPage";
......@@ -11,6 +12,11 @@ export default function App() {
const { state, dispatch } = useApp();
const [authChecked, setAuthChecked] = useState(!state.token);
// Connect streamManager to store dispatch
useEffect(() => {
streamManager.setDispatch(dispatch);
}, [dispatch]);
useEffect(() => {
if (!state.token) {
setAuthChecked(true);
......@@ -34,7 +40,7 @@ export default function App() {
if (!authChecked) {
return (
<div className="h-full flex items-center justify-center bg-anton-bg">
<div className="h-dvh flex items-center justify-center bg-anton-bg">
<div className="flex flex-col items-center gap-4 animate-fade-in">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
<Flame size={32} className="text-white animate-pulse" />
......
This diff is collapsed.
This diff is collapsed.
......@@ -2,137 +2,287 @@
@tailwind components;
@tailwind utilities;
/* ── Globals ───────────────────────────────── */
* {
scrollbar-width: thin;
scrollbar-color: #2a2a3a #0a0a0f;
/* ═══════════════════════════════════════════ */
/* Use dvh for full-height on mobile */
/* ═══════════════════════════════════════════ */
:root {
--color-anton-bg: #09090f;
--color-anton-surface: #0f0f18;
--color-anton-card: #161622;
--color-anton-border: #1e1e30;
--color-anton-text: #e2e2f0;
--color-anton-muted: #6b6b8a;
--color-anton-accent: #e63946;
--color-anton-success: #2ecc71;
--color-anton-danger: #e74c3c;
/* safe area insets for notched phones */
--sat: env(safe-area-inset-top, 0px);
--sab: env(safe-area-inset-bottom, 0px);
--sal: env(safe-area-inset-left, 0px);
--sar: env(safe-area-inset-right, 0px);
}
*::-webkit-scrollbar {
width: 6px;
*,
*::before,
*::after {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
*::-webkit-scrollbar-track {
background: #0a0a0f;
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
*::-webkit-scrollbar-thumb {
background: #2a2a3a;
border-radius: 3px;
body {
margin: 0;
padding: 0;
background: var(--color-anton-bg);
color: var(--color-anton-text);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
overflow: hidden;
overscroll-behavior: none;
/* Prevent pull-to-refresh on mobile */
-webkit-overflow-scrolling: touch;
}
/* Fix iOS textarea zoom — font MUST be >= 16px */
textarea,
input,
select {
font-size: 16px !important;
}
@media (min-width: 640px) {
textarea,
input,
select {
font-size: 14px !important;
}
}
html,
body,
#root {
height: 100%;
margin: 0;
height: 100dvh;
width: 100vw;
overflow: hidden;
}
/* ── Markdown prose adjustments ────────────── */
.prose-anton h1,
.prose-anton h2,
.prose-anton h3 {
color: #f97316;
margin-top: 1em;
margin-bottom: 0.5em;
font-weight: 600;
/* ═══════════════════════════════════════════ */
/* Scrollbar styling */
/* ═══════════════════════════════════════════ */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.prose-anton h1 { font-size: 1.5rem; }
.prose-anton h2 { font-size: 1.25rem; }
.prose-anton h3 { font-size: 1.1rem; }
.prose-anton p {
margin-bottom: 0.75em;
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-anton-border);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-anton-muted);
}
/* Hide scrollbar on mobile for cleaner look */
@media (max-width: 640px) {
::-webkit-scrollbar {
width: 2px;
}
}
/* ═══════════════════════════════════════════ */
/* Animations */
/* ═══════════════════════════════════════════ */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.25s ease-out;
}
.thinking-pulse {
animation: pulse 1.5s ease-in-out infinite;
}
/* ═══════════════════════════════════════════ */
/* Prose (Markdown) styling */
/* ═══════════════════════════════════════════ */
.prose-anton {
line-height: 1.7;
word-break: break-word;
overflow-wrap: break-word;
}
.prose-anton p {
margin: 0.5em 0;
}
.prose-anton p:first-child {
margin-top: 0;
}
.prose-anton p:last-child {
margin-bottom: 0;
}
.prose-anton ul,
.prose-anton ol {
margin-left: 1.5em;
margin-bottom: 0.75em;
padding-left: 1.25em;
margin: 0.5em 0;
}
.prose-anton li {
margin-bottom: 0.25em;
margin: 0.15em 0;
}
.prose-anton ul { list-style-type: disc; }
.prose-anton ol { list-style-type: decimal; }
.prose-anton a {
color: #f97316;
text-decoration: underline;
.prose-anton code:not(pre code) {
background: var(--color-anton-border);
border-radius: 4px;
padding: 0.15em 0.35em;
font-family: "JetBrains Mono", monospace;
font-size: 0.85em;
color: #f0a0a0;
word-break: break-all;
}
.prose-anton blockquote {
border-left: 3px solid #f97316;
padding-left: 1em;
color: #8888a0;
margin: 0.75em 0;
border-left: 3px solid var(--color-anton-accent);
padding-left: 0.75em;
margin: 0.5em 0;
color: var(--color-anton-muted);
}
.prose-anton a {
color: var(--color-anton-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.prose-anton table {
border-collapse: collapse;
margin: 0.75em 0;
font-size: 0.85em;
width: 100%;
display: block;
overflow-x: auto;
}
.prose-anton th,
.prose-anton td {
border: 1px solid #2a2a3a;
padding: 0.4em 0.75em;
border: 1px solid var(--color-anton-border);
padding: 0.4em 0.6em;
text-align: left;
white-space: nowrap;
}
.prose-anton th {
background: #1a1a28;
background: var(--color-anton-card);
font-weight: 600;
}
.prose-anton code:not(pre code) {
background: #1a1a28;
padding: 0.15em 0.4em;
border-radius: 4px;
font-size: 0.9em;
font-family: "JetBrains Mono", monospace;
color: #f97316;
.prose-anton h1,
.prose-anton h2,
.prose-anton h3,
.prose-anton h4 {
font-weight: 600;
color: white;
margin: 0.75em 0 0.35em;
}
/* ── Thinking block animation ──────────────── */
@keyframes thinkPulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
.prose-anton h1 {
font-size: 1.35em;
}
.thinking-pulse {
animation: thinkPulse 2s ease-in-out infinite;
.prose-anton h2 {
font-size: 1.2em;
}
/* ── Slider custom styling ─────────────────── */
.prose-anton h3 {
font-size: 1.05em;
}
.prose-anton hr {
border: none;
border-top: 1px solid var(--color-anton-border);
margin: 1em 0;
}
/* ═══════════════════════════════════════════ */
/* Range input styling */
/* ═══════════════════════════════════════════ */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: transparent;
width: 100%;
}
input[type="range"]::-webkit-slider-track {
height: 4px;
border-radius: 2px;
background: #2a2a3a;
border-radius: 999px;
background: var(--color-anton-border);
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 16px;
width: 16px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #f97316;
margin-top: -6px;
background: var(--color-anton-accent);
cursor: pointer;
border: 2px solid var(--color-anton-bg);
}
input[type="range"]::-moz-range-track {
height: 4px;
border-radius: 2px;
background: #2a2a3a;
}
input[type="range"]::-moz-range-thumb {
height: 16px;
width: 16px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #f97316;
border: none;
background: var(--color-anton-accent);
cursor: pointer;
border: 2px solid var(--color-anton-bg);
}
/* ═══════════════════════════════════════════ */
/* Mobile-specific utilities */
/* ═══════════════════════════════════════════ */
@supports (height: 100dvh) {
.h-dvh {
height: 100dvh;
}
}
@supports not (height: 100dvh) {
.h-dvh {
height: 100vh;
}
}
/* Ensure touch targets are at least 44px */
@media (max-width: 640px) {
button,
a,
[role="button"] {
min-height: 44px;
min-width: 44px;
}
/* Exception for inline/tiny buttons */
.prose-anton button,
.text-\[11px\] button {
min-height: auto;
min-width: auto;
}
}
\ No newline at end of file
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { AppProvider } from "./store";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
......
import React, { useEffect, useCallback } from "react";
import React, { useEffect } from "react";
import { useApp } from "../store";
import { listChats } from "../api";
import Sidebar from "../components/Sidebar";
import ChatView from "../components/ChatView";
import { Flame, Paperclip, Layers, Zap } from "lucide-react";
import { Flame, MessageSquarePlus, Menu } from "lucide-react";
export default function ChatPage() {
const { state, dispatch } = useApp();
const loadChats = useCallback(async () => {
try {
const chats = await listChats(state.token);
dispatch({ type: "SET_CHATS", chats });
} catch { /* ignore */ }
}, [state.token, dispatch]);
useEffect(() => {
loadChats();
}, [loadChats]);
return (
<div className="h-full flex">
<Sidebar onRefresh={loadChats} />
<main className="flex-1 flex flex-col min-w-0">
{state.activeChatId ? (
<ChatView key={state.activeChatId} chatId={state.activeChatId} />
) : (
<EmptyState />
)}
</main>
</div>
);
}
(async () => {
try {
const chats = await listChats(state.token);
dispatch({ type: "SET_CHATS", chats });
} catch { }
})();
}, [state.token, dispatch]);
function EmptyState() {
return (
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center animate-fade-in max-w-lg">
<div className="inline-flex items-center justify-center w-24 h-24 rounded-3xl bg-gradient-to-br from-anton-accent/20 to-transparent border border-anton-accent/20 mb-6">
<Flame size={44} className="text-anton-accent" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">Son of Anton</h2>
<p className="text-anton-muted mb-6">
Avatar of All Elements of Code. Create a new chat to begin — but bring
real questions, not that first-result-of-Google garbage.
</p>
<div className="h-dvh flex overflow-hidden relative">
{/* Mobile overlay backdrop */}
{state.sidebarOpen && (
<div
className="fixed inset-0 z-30 bg-black/60 backdrop-blur-sm lg:hidden"
onClick={() => dispatch({ type: "CLOSE_SIDEBAR" })}
/>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-left">
<div className="bg-anton-surface border border-anton-border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center">
<Paperclip size={16} className="text-blue-400" />
</div>
<span className="text-sm font-medium text-white">File Upload</span>
</div>
<p className="text-xs text-anton-muted leading-relaxed">
Drop images, videos, PDFs, or code files directly into chat. AI describes and analyzes them.
</p>
</div>
{/* Sidebar — slides in on mobile, always visible on desktop */}
<div
className={`
fixed inset-y-0 left-0 z-40 w-72
transform transition-transform duration-300 ease-in-out
lg:relative lg:translate-x-0 lg:z-auto
${state.sidebarOpen ? "translate-x-0" : "-translate-x-full"}
`}
>
<Sidebar />
</div>
<div className="bg-anton-surface border border-anton-border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
<Layers size={16} className="text-purple-400" />
</div>
<span className="text-sm font-medium text-white">Parallel Chats</span>
</div>
<p className="text-xs text-anton-muted leading-relaxed">
Run multiple conversations simultaneously. Switch between them while they stream.
</p>
{/* Main content area */}
<div className="flex-1 flex flex-col min-w-0">
{/* Mobile top bar */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-anton-border bg-anton-surface lg:hidden">
<button
onClick={() => dispatch({ type: "TOGGLE_SIDEBAR" })}
className="p-2 -ml-2 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
>
<Menu size={22} />
</button>
<div className="flex items-center gap-2 flex-1 min-w-0">
<Flame size={18} className="text-anton-accent shrink-0" />
<span className="text-sm font-semibold text-white truncate">
{state.activeChatId
? state.chats.find((c) => c.id === state.activeChatId)?.title || "Chat"
: "Son of Anton"}
</span>
</div>
</div>
<div className="bg-anton-surface border border-anton-border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-green-500/20 flex items-center justify-center">
<Zap size={16} className="text-green-400" />
{/* Chat view or empty state */}
{state.activeChatId ? (
<ChatView chatId={state.activeChatId} />
) : (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center max-w-md">
<div className="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-6">
<Flame size={40} className="text-white" />
</div>
<span className="text-sm font-medium text-white">Full Code</span>
<h1 className="text-2xl font-bold text-white mb-2">Son of Anton</h1>
<p className="text-anton-muted text-sm mb-6 leading-relaxed">
Avatar of All Elements of Code. Select an existing chat from the sidebar
or create a new one to begin.
</p>
<button
onClick={() => dispatch({ type: "OPEN_SIDEBAR" })}
className="lg:hidden inline-flex items-center gap-2 px-5 py-2.5 bg-anton-accent text-white rounded-xl hover:opacity-90 transition active:scale-95 text-sm font-medium"
>
<MessageSquarePlus size={16} />
Open Chats
</button>
</div>
<p className="text-xs text-anton-muted leading-relaxed">
Production-ready code with syntax highlighting, download buttons, and ZIP export.
</p>
</div>
</div>
)}
</div>
</div>
);
......
import React, { createContext, useContext, useReducer, useEffect } from "react";
import { setDispatch } from "./streamManager";
const AppContext = createContext();
const AppContext = createContext(null);
const initialState = {
token: localStorage.getItem("token") || null,
user: JSON.parse(localStorage.getItem("user") || "null"),
user: null,
chats: [],
activeChatId: null,
sidebarOpen: true,
chatMessages: {},
activeStreams: {},
sidebarOpen: false, // mobile sidebar toggle
};
function reducer(state, action) {
switch (action.type) {
case "LOGIN": {
localStorage.setItem("token", action.token);
localStorage.setItem("user", JSON.stringify(action.user));
return { ...state, token: action.token, user: action.user };
}
case "SET_TOKEN":
if (action.token) localStorage.setItem("token", action.token);
else localStorage.removeItem("token");
localStorage.setItem("token", action.token);
return { ...state, token: action.token };
case "SET_USER":
localStorage.setItem("user", JSON.stringify(action.user));
return { ...state, user: action.user };
case "LOGOUT":
localStorage.removeItem("token");
localStorage.removeItem("user");
return { ...initialState, token: null, user: null };
return { ...initialState, token: null };
case "SET_CHATS":
return { ...state, chats: action.chats };
case "SET_ACTIVE_CHAT":
return { ...state, activeChatId: action.chatId, sidebarOpen: false };
case "ADD_CHAT":
return {
...state,
chats: [action.chat, ...state.chats],
activeChatId: action.chat.id,
sidebarOpen: false,
};
case "UPDATE_CHAT": {
......@@ -52,64 +46,52 @@ function reducer(state, action) {
return { ...state, chats: updated };
}
case "REMOVE_CHAT":
case "DELETE_CHAT": {
const chatId = action.chatId;
const filtered = state.chats.filter((c) => c.id !== chatId);
case "REMOVE_CHAT": {
const filtered = state.chats.filter((c) => c.id !== action.chatId);
const newMessages = { ...state.chatMessages };
delete newMessages[chatId];
const newStreams = { ...state.activeStreams };
delete newStreams[chatId];
delete newMessages[action.chatId];
return {
...state,
chats: filtered,
chatMessages: newMessages,
activeStreams: newStreams,
activeChatId:
state.activeChatId === chatId
? filtered[0]?.id || null
: state.activeChatId,
activeChatId: state.activeChatId === action.chatId ? null : state.activeChatId,
};
}
case "SET_ACTIVE_CHAT":
return { ...state, activeChatId: action.chatId };
case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen };
case "SET_MESSAGES":
return {
...state,
chatMessages: {
...state.chatMessages,
[action.chatId]: action.messages,
},
chatMessages: { ...state.chatMessages, [action.chatId]: action.messages },
};
case "ADD_MESSAGE":
case "ADD_MESSAGE": {
const prev = state.chatMessages[action.chatId] || [];
return {
...state,
chatMessages: {
...state.chatMessages,
[action.chatId]: [
...(state.chatMessages[action.chatId] || []),
action.message,
],
[action.chatId]: [...prev, action.message],
},
};
}
case "SET_STREAMING": {
case "SET_STREAMING":
if (action.streaming) {
return {
...state,
activeStreams: { ...state.activeStreams, [action.chatId]: true },
};
return { ...state, activeStreams: { ...state.activeStreams, [action.chatId]: true } };
} else {
const s = { ...state.activeStreams };
delete s[action.chatId];
return { ...state, activeStreams: s };
}
const next = { ...state.activeStreams };
delete next[action.chatId];
return { ...state, activeStreams: next };
}
case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen };
case "CLOSE_SIDEBAR":
return { ...state, sidebarOpen: false };
case "OPEN_SIDEBAR":
return { ...state, sidebarOpen: true };
default:
return state;
......@@ -118,11 +100,6 @@ function reducer(state, action) {
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
setDispatch(dispatch);
}, [dispatch]);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
......
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,jsx}"],
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
darkMode: "class",
theme: {
extend: {
colors: {
anton: {
bg: "#0a0a0f",
surface: "#12121a",
card: "#1a1a28",
border: "#2a2a3a",
accent: "#f97316",
accentDim: "#c2410c",
text: "#e4e4ef",
muted: "#8888a0",
user: "#1e293b",
assistant: "#15151f",
danger: "#ef4444",
success: "#22c55e",
},
"anton-bg": "#09090f",
"anton-surface": "#0f0f18",
"anton-card": "#161622",
"anton-border": "#1e1e30",
"anton-text": "#e2e2f0",
"anton-muted": "#6b6b8a",
"anton-accent": "#e63946",
"anton-success": "#2ecc71",
"anton-danger": "#e74c3c",
},
fontFamily: {
sans: ['"Inter"', "system-ui", "sans-serif"],
mono: ['"JetBrains Mono"', '"Fira Code"', "monospace"],
mono: ['"JetBrains Mono"', "monospace"],
},
animation: {
"pulse-slow": "pulse 3s cubic-bezier(0.4,0,0.6,1) infinite",
"fade-in": "fadeIn 0.3s ease-out",
height: {
dvh: "100dvh",
},
keyframes: {
fadeIn: {
"0%": { opacity: 0, transform: "translateY(8px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
},
minHeight: {
dvh: "100dvh",
},
screens: {
xs: "480px",
},
},
},
......
......@@ -5,11 +5,20 @@ export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:8000",
"/api": "http://localhost:80",
},
},
build: {
outDir: "dist",
// Content-hash all chunks so browsers fetch new versions
rollupOptions: {
output: {
entryFileNames: "assets/[name]-[hash].js",
chunkFileNames: "assets/[name]-[hash].js",
assetFileNames: "assets/[name]-[hash].[ext]",
},
},
// Generate a manifest for cache-busting verification
manifest: true,
sourcemap: false,
},
});
\ No newline at end of file
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