feat: proje temizleme ve bildirim deneyimini iyilestir
This commit is contained in:
@@ -4,12 +4,13 @@ import SessionToolbar from "./components/SessionToolbar.jsx";
|
||||
import ChatStream from "./components/ChatStream.jsx";
|
||||
import PromptComposer from "./components/PromptComposer.jsx";
|
||||
import TeamBoard from "./components/TeamBoard.jsx";
|
||||
import ToastStack from "./components/ToastStack.jsx";
|
||||
import { useSocket } from "./hooks/useSocket.js";
|
||||
import { useSession } from "./hooks/useSession.js";
|
||||
|
||||
export default function App() {
|
||||
const { socket, connected } = useSocket();
|
||||
const { session, chat, error, startSession, stopSession, sendPrompt, selectProject, clearError } = useSession(socket);
|
||||
const { session, chat, startSession, clearProject, sendPrompt, selectProject, clearError, toasts, dismissToast } = useSession(socket);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const autoStartedRef = useRef(false);
|
||||
|
||||
@@ -57,8 +58,6 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
<ShellFrame>
|
||||
{error ? <div className="error-banner">{error}</div> : null}
|
||||
|
||||
<div className="console-grid">
|
||||
<div className="console-grid__side">
|
||||
<TeamBoard chat={chat} />
|
||||
@@ -71,7 +70,7 @@ export default function App() {
|
||||
<SessionToolbar
|
||||
session={session}
|
||||
busy={busy}
|
||||
onStop={() => runAction(stopSession)}
|
||||
onClearProject={() => runAction(clearProject)}
|
||||
onSelectProject={() => runAction(selectProject)}
|
||||
/>
|
||||
}
|
||||
@@ -82,6 +81,8 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToastStack toasts={toasts} onDismiss={dismissToast} />
|
||||
</ShellFrame>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import PixelButton from "./PixelButton.jsx";
|
||||
|
||||
export default function SessionToolbar({ session, busy, onStop, onSelectProject }) {
|
||||
const isRunning = session.status === "running";
|
||||
|
||||
export default function SessionToolbar({ session, busy, onClearProject, onSelectProject }) {
|
||||
return (
|
||||
<div className="session-toolbar session-toolbar--inline">
|
||||
<PixelButton tone="red" disabled={busy || (!isRunning && session.status !== "starting")} onClick={onStop}>
|
||||
Stop Session
|
||||
<PixelButton tone="red" disabled={busy || !session.currentProjectPath} onClick={onClearProject}>
|
||||
Clean Project
|
||||
</PixelButton>
|
||||
<PixelButton tone="amber" disabled={busy} onClick={onSelectProject}>
|
||||
Select Project
|
||||
|
||||
22
web/src/components/ToastStack.jsx
Normal file
22
web/src/components/ToastStack.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function ToastStack({ toasts = [], onDismiss }) {
|
||||
if (!toasts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toast-stack" aria-live="polite" aria-atomic="true">
|
||||
{toasts.map((toast) => (
|
||||
<button
|
||||
key={toast.id}
|
||||
type="button"
|
||||
className={`toast toast--${toast.tone || "error"}`}
|
||||
onClick={() => onDismiss?.(toast.id)}
|
||||
title="Dismiss notification"
|
||||
>
|
||||
<span className="toast__label">{toast.tone === "error" ? "ALERT" : "NOTICE"}</span>
|
||||
<span className="toast__message">{toast.message}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,13 +17,30 @@ export function useSession(socket) {
|
||||
const [session, setSession] = useState(initialState);
|
||||
const [chat, setChat] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
function pushToast(message, tone = "error") {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
setToasts((current) => [...current, { id, message, tone }].slice(-4));
|
||||
|
||||
window.setTimeout(() => {
|
||||
setToasts((current) => current.filter((toast) => toast.id !== id));
|
||||
}, 4200);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleState = (value) => setSession(value);
|
||||
const handleChunk = ({ chunk }) => setChat((current) => current + chunk);
|
||||
const handleSnapshot = ({ content }) => setChat(content ?? "");
|
||||
const handleReset = () => setChat("");
|
||||
const handleError = ({ message }) => setError(message);
|
||||
const handleError = ({ message }) => {
|
||||
setError(message);
|
||||
pushToast(message, "error");
|
||||
};
|
||||
|
||||
socket.on("session:state", handleState);
|
||||
socket.on("chat:chunk", handleChunk);
|
||||
@@ -46,6 +63,7 @@ export function useSession(socket) {
|
||||
if (!response?.ok) {
|
||||
const message = response?.error ?? "Unknown socket error";
|
||||
setError(message);
|
||||
pushToast(message, "error");
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
@@ -60,7 +78,9 @@ export function useSession(socket) {
|
||||
session,
|
||||
chat,
|
||||
error,
|
||||
toasts,
|
||||
clearError: () => setError(""),
|
||||
dismissToast: (id) => setToasts((current) => current.filter((toast) => toast.id !== id)),
|
||||
startSession: () => emitWithAck("session:start"),
|
||||
stopSession: () => emitWithAck("session:stop"),
|
||||
activateTeam: () => emitWithAck("team:activate"),
|
||||
@@ -78,6 +98,27 @@ export function useSession(socket) {
|
||||
if (!response.ok || !payload.ok) {
|
||||
const message = payload.error ?? "Project selection failed";
|
||||
setError(message);
|
||||
pushToast(message, "error");
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
setError("");
|
||||
return payload;
|
||||
},
|
||||
clearProject: async () => {
|
||||
const response = await fetch("/api/project/clear", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok || !payload.ok) {
|
||||
const message = payload.error ?? "Project cleanup failed";
|
||||
setError(message);
|
||||
pushToast(message, "error");
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,47 @@
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.toast-stack {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
z-index: 5;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: min(420px, calc(100vw - 64px));
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
text-align: left;
|
||||
border: 3px solid var(--border-dark);
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.05), var(--shadow-panel);
|
||||
background: linear-gradient(180deg, rgba(60, 22, 22, 0.98), rgba(18, 8, 8, 0.98));
|
||||
color: #ffd9d9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast--info {
|
||||
background: linear-gradient(180deg, rgba(18, 52, 55, 0.98), rgba(8, 17, 20, 0.98));
|
||||
color: #d8feff;
|
||||
}
|
||||
|
||||
.toast__label {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
|
||||
.toast__message {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.console-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
|
||||
@@ -233,6 +274,16 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-panel .panel-frame__body {
|
||||
height: 62vh;
|
||||
min-height: 62vh;
|
||||
}
|
||||
|
||||
.chat-stream,
|
||||
.team-card__body {
|
||||
min-height: 420px;
|
||||
@@ -244,6 +295,12 @@
|
||||
linear-gradient(180deg, rgba(4, 8, 5, 0.88) 0%, rgba(8, 12, 9, 0.92) 100%);
|
||||
}
|
||||
|
||||
.chat-panel .chat-stream {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.chat-stream pre,
|
||||
.team-message pre {
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user