story-kit: merge 340_story_web_ui_rebuild_and_restart_button
This commit is contained in:
@@ -357,6 +357,10 @@ export const api = {
|
|||||||
getAllTokenUsage(baseUrl?: string) {
|
getAllTokenUsage(baseUrl?: string) {
|
||||||
return requestJson<AllTokenUsageResponse>("/token-usage", {}, baseUrl);
|
return requestJson<AllTokenUsageResponse>("/token-usage", {}, baseUrl);
|
||||||
},
|
},
|
||||||
|
/** Trigger a server rebuild and restart. */
|
||||||
|
rebuildAndRestart() {
|
||||||
|
return callMcpTool("rebuild_and_restart", {});
|
||||||
|
},
|
||||||
/** Approve a story in QA, moving it to merge. */
|
/** Approve a story in QA, moving it to merge. */
|
||||||
approveQa(storyId: string) {
|
approveQa(storyId: string) {
|
||||||
return callMcpTool("approve_qa", { story_id: storyId });
|
return callMcpTool("approve_qa", { story_id: storyId });
|
||||||
@@ -424,6 +428,7 @@ export class ChatWebSocket {
|
|||||||
level: string,
|
level: string,
|
||||||
message: string,
|
message: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
private onConnected?: () => void;
|
||||||
private connected = false;
|
private connected = false;
|
||||||
private closeTimer?: number;
|
private closeTimer?: number;
|
||||||
private wsPath = DEFAULT_WS_PATH;
|
private wsPath = DEFAULT_WS_PATH;
|
||||||
@@ -470,6 +475,7 @@ export class ChatWebSocket {
|
|||||||
this.socket.onopen = () => {
|
this.socket.onopen = () => {
|
||||||
this.reconnectDelay = 1000;
|
this.reconnectDelay = 1000;
|
||||||
this._startHeartbeat();
|
this._startHeartbeat();
|
||||||
|
this.onConnected?.();
|
||||||
};
|
};
|
||||||
this.socket.onmessage = (event) => {
|
this.socket.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
@@ -567,6 +573,7 @@ export class ChatWebSocket {
|
|||||||
onSideQuestionToken?: (content: string) => void;
|
onSideQuestionToken?: (content: string) => void;
|
||||||
onSideQuestionDone?: (response: string) => void;
|
onSideQuestionDone?: (response: string) => void;
|
||||||
onLogEntry?: (timestamp: string, level: string, message: string) => void;
|
onLogEntry?: (timestamp: string, level: string, message: string) => void;
|
||||||
|
onConnected?: () => void;
|
||||||
},
|
},
|
||||||
wsPath = DEFAULT_WS_PATH,
|
wsPath = DEFAULT_WS_PATH,
|
||||||
) {
|
) {
|
||||||
@@ -585,6 +592,7 @@ export class ChatWebSocket {
|
|||||||
this.onSideQuestionToken = handlers.onSideQuestionToken;
|
this.onSideQuestionToken = handlers.onSideQuestionToken;
|
||||||
this.onSideQuestionDone = handlers.onSideQuestionDone;
|
this.onSideQuestionDone = handlers.onSideQuestionDone;
|
||||||
this.onLogEntry = handlers.onLogEntry;
|
this.onLogEntry = handlers.onLogEntry;
|
||||||
|
this.onConnected = handlers.onConnected;
|
||||||
this.wsPath = wsPath;
|
this.wsPath = wsPath;
|
||||||
this.shouldReconnect = true;
|
this.shouldReconnect = true;
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
const [serverLogs, setServerLogs] = useState<LogEntry[]>([]);
|
const [serverLogs, setServerLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [wsConnected, setWsConnected] = useState(false);
|
||||||
// Ref so stale WebSocket callbacks can read the current queued messages
|
// Ref so stale WebSocket callbacks can read the current queued messages
|
||||||
const queuedMessagesRef = useRef<{ id: string; text: string }[]>([]);
|
const queuedMessagesRef = useRef<{ id: string; text: string }[]>([]);
|
||||||
const queueIdCounterRef = useRef(0);
|
const queueIdCounterRef = useRef(0);
|
||||||
@@ -457,6 +458,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
onLogEntry: (timestamp, level, message) => {
|
onLogEntry: (timestamp, level, message) => {
|
||||||
setServerLogs((prev) => [...prev, { timestamp, level, message }]);
|
setServerLogs((prev) => [...prev, { timestamp, level, message }]);
|
||||||
},
|
},
|
||||||
|
onConnected: () => {
|
||||||
|
setWsConnected(true);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -830,6 +834,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
}}
|
}}
|
||||||
enableTools={enableTools}
|
enableTools={enableTools}
|
||||||
onToggleTools={setEnableTools}
|
onToggleTools={setEnableTools}
|
||||||
|
wsConnected={wsConnected}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Two-column content area */}
|
{/* Two-column content area */}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { ChatHeader } from "./ChatHeader";
|
import { ChatHeader } from "./ChatHeader";
|
||||||
|
|
||||||
|
vi.mock("../api/client", () => ({
|
||||||
|
api: {
|
||||||
|
rebuildAndRestart: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface ChatHeaderProps {
|
interface ChatHeaderProps {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
onCloseProject: () => void;
|
onCloseProject: () => void;
|
||||||
@@ -14,6 +20,7 @@ interface ChatHeaderProps {
|
|||||||
onModelChange: (model: string) => void;
|
onModelChange: (model: string) => void;
|
||||||
enableTools: boolean;
|
enableTools: boolean;
|
||||||
onToggleTools: (enabled: boolean) => void;
|
onToggleTools: (enabled: boolean) => void;
|
||||||
|
wsConnected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeProps(overrides: Partial<ChatHeaderProps> = {}): ChatHeaderProps {
|
function makeProps(overrides: Partial<ChatHeaderProps> = {}): ChatHeaderProps {
|
||||||
@@ -29,6 +36,7 @@ function makeProps(overrides: Partial<ChatHeaderProps> = {}): ChatHeaderProps {
|
|||||||
onModelChange: vi.fn(),
|
onModelChange: vi.fn(),
|
||||||
enableTools: true,
|
enableTools: true,
|
||||||
onToggleTools: vi.fn(),
|
onToggleTools: vi.fn(),
|
||||||
|
wsConnected: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -211,4 +219,96 @@ describe("ChatHeader", () => {
|
|||||||
expect(sessionBtn.style.backgroundColor).toBe("rgb(47, 47, 47)");
|
expect(sessionBtn.style.backgroundColor).toBe("rgb(47, 47, 47)");
|
||||||
expect(sessionBtn.style.color).toBe("rgb(136, 136, 136)");
|
expect(sessionBtn.style.color).toBe("rgb(136, 136, 136)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Rebuild button ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it("renders rebuild button", () => {
|
||||||
|
render(<ChatHeader {...makeProps()} />);
|
||||||
|
expect(
|
||||||
|
screen.getByTitle("Rebuild and restart the server"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows confirmation dialog when rebuild button is clicked", () => {
|
||||||
|
render(<ChatHeader {...makeProps()} />);
|
||||||
|
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||||
|
expect(screen.getByText("Rebuild and restart?")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides confirmation dialog when cancel is clicked", () => {
|
||||||
|
render(<ChatHeader {...makeProps()} />);
|
||||||
|
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||||
|
fireEvent.click(screen.getByText("Cancel"));
|
||||||
|
expect(screen.queryByText("Rebuild and restart?")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls api.rebuildAndRestart and shows Building... when confirmed", async () => {
|
||||||
|
const { api } = await import("../api/client");
|
||||||
|
vi.mocked(api.rebuildAndRestart).mockReturnValue(new Promise(() => {}));
|
||||||
|
|
||||||
|
render(<ChatHeader {...makeProps()} />);
|
||||||
|
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||||
|
fireEvent.click(screen.getByText("Rebuild"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Building...")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(api.rebuildAndRestart).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows Reconnecting... when rebuild triggers a network error", async () => {
|
||||||
|
const { api } = await import("../api/client");
|
||||||
|
vi.mocked(api.rebuildAndRestart).mockRejectedValue(
|
||||||
|
new TypeError("Failed to fetch"),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<ChatHeader {...makeProps()} />);
|
||||||
|
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||||
|
fireEvent.click(screen.getByText("Rebuild"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Reconnecting...")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows error when rebuild returns a failure message", async () => {
|
||||||
|
const { api } = await import("../api/client");
|
||||||
|
vi.mocked(api.rebuildAndRestart).mockResolvedValue(
|
||||||
|
"error[E0308]: mismatched types",
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<ChatHeader {...makeProps()} />);
|
||||||
|
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||||
|
fireEvent.click(screen.getByText("Rebuild"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("⚠ Rebuild failed")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("error[E0308]: mismatched types"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears reconnecting state when wsConnected transitions to true", async () => {
|
||||||
|
const { api } = await import("../api/client");
|
||||||
|
vi.mocked(api.rebuildAndRestart).mockRejectedValue(
|
||||||
|
new TypeError("Failed to fetch"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<ChatHeader {...makeProps({ wsConnected: false })} />,
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||||
|
fireEvent.click(screen.getByText("Rebuild"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Reconnecting...")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(<ChatHeader {...makeProps({ wsConnected: true })} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("↺ Rebuild")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { api } from "../api/client";
|
||||||
|
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
function formatBuildTime(isoString: string): string {
|
function formatBuildTime(isoString: string): string {
|
||||||
const d = new Date(isoString);
|
const d = new Date(isoString);
|
||||||
const year = d.getUTCFullYear();
|
const year = d.getUTCFullYear();
|
||||||
@@ -26,6 +31,7 @@ interface ChatHeaderProps {
|
|||||||
onModelChange: (model: string) => void;
|
onModelChange: (model: string) => void;
|
||||||
enableTools: boolean;
|
enableTools: boolean;
|
||||||
onToggleTools: (enabled: boolean) => void;
|
onToggleTools: (enabled: boolean) => void;
|
||||||
|
wsConnected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContextEmoji = (percentage: number): string => {
|
const getContextEmoji = (percentage: number): string => {
|
||||||
@@ -34,6 +40,8 @@ const getContextEmoji = (percentage: number): string => {
|
|||||||
return "🟢";
|
return "🟢";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RebuildStatus = "idle" | "building" | "reconnecting" | "error";
|
||||||
|
|
||||||
export function ChatHeader({
|
export function ChatHeader({
|
||||||
projectPath,
|
projectPath,
|
||||||
onCloseProject,
|
onCloseProject,
|
||||||
@@ -46,10 +54,212 @@ export function ChatHeader({
|
|||||||
onModelChange,
|
onModelChange,
|
||||||
enableTools,
|
enableTools,
|
||||||
onToggleTools,
|
onToggleTools,
|
||||||
|
wsConnected,
|
||||||
}: ChatHeaderProps) {
|
}: ChatHeaderProps) {
|
||||||
const hasModelOptions = availableModels.length > 0 || claudeModels.length > 0;
|
const hasModelOptions = availableModels.length > 0 || claudeModels.length > 0;
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
|
const [rebuildStatus, setRebuildStatus] = useState<RebuildStatus>("idle");
|
||||||
|
const [rebuildError, setRebuildError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// When WS reconnects after a rebuild, clear the reconnecting status.
|
||||||
|
useEffect(() => {
|
||||||
|
if (rebuildStatus === "reconnecting" && wsConnected) {
|
||||||
|
setRebuildStatus("idle");
|
||||||
|
}
|
||||||
|
}, [wsConnected, rebuildStatus]);
|
||||||
|
|
||||||
|
function handleRebuildClick() {
|
||||||
|
setRebuildError(null);
|
||||||
|
setShowConfirm(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRebuildConfirm() {
|
||||||
|
setShowConfirm(false);
|
||||||
|
setRebuildStatus("building");
|
||||||
|
api
|
||||||
|
.rebuildAndRestart()
|
||||||
|
.then((result) => {
|
||||||
|
// Got a response = build failed (server still running).
|
||||||
|
setRebuildStatus("error");
|
||||||
|
setRebuildError(result || "Rebuild failed");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Network error = server is restarting (build succeeded).
|
||||||
|
setRebuildStatus("reconnecting");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRebuildCancel() {
|
||||||
|
setShowConfirm(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDismissError() {
|
||||||
|
setRebuildStatus("idle");
|
||||||
|
setRebuildError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rebuildButtonLabel =
|
||||||
|
rebuildStatus === "building"
|
||||||
|
? "Building..."
|
||||||
|
: rebuildStatus === "reconnecting"
|
||||||
|
? "Reconnecting..."
|
||||||
|
: rebuildStatus === "error"
|
||||||
|
? "⚠ Rebuild Failed"
|
||||||
|
: "↺ Rebuild";
|
||||||
|
|
||||||
|
const rebuildButtonDisabled =
|
||||||
|
rebuildStatus === "building" || rebuildStatus === "reconnecting";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{/* Confirmation dialog overlay */}
|
||||||
|
{showConfirm && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
background: "rgba(0,0,0,0.6)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#1e1e1e",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "24px",
|
||||||
|
maxWidth: "400px",
|
||||||
|
width: "90%",
|
||||||
|
color: "#ececec",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "1em",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Rebuild and restart?
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.85em",
|
||||||
|
color: "#aaa",
|
||||||
|
marginBottom: "20px",
|
||||||
|
lineHeight: "1.5",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This will run <code>cargo build</code> and replace the running
|
||||||
|
server. All agents will be stopped. The page will reconnect
|
||||||
|
automatically when the new server is ready.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRebuildCancel}
|
||||||
|
style={{
|
||||||
|
padding: "6px 16px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "1px solid #444",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#aaa",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.9em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRebuildConfirm}
|
||||||
|
style={{
|
||||||
|
padding: "6px 16px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "none",
|
||||||
|
background: "#c0392b",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.9em",
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error toast */}
|
||||||
|
{rebuildStatus === "error" && rebuildError && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "20px",
|
||||||
|
right: "20px",
|
||||||
|
background: "#3a1010",
|
||||||
|
border: "1px solid #c0392b",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "12px 16px",
|
||||||
|
maxWidth: "480px",
|
||||||
|
color: "#ececec",
|
||||||
|
zIndex: 1000,
|
||||||
|
fontSize: "0.85em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: "600", marginBottom: "4px" }}>
|
||||||
|
⚠ Rebuild failed
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
color: "#f08080",
|
||||||
|
maxHeight: "120px",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rebuildError}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDismissError}
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: "#aaa",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "1em",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "12px 24px",
|
padding: "12px 24px",
|
||||||
@@ -150,9 +360,65 @@ export function ChatHeader({
|
|||||||
}}
|
}}
|
||||||
title={`Context: ${contextUsage.used.toLocaleString()} / ${contextUsage.total.toLocaleString()} tokens (${contextUsage.percentage}%)`}
|
title={`Context: ${contextUsage.used.toLocaleString()} / ${contextUsage.total.toLocaleString()} tokens (${contextUsage.percentage}%)`}
|
||||||
>
|
>
|
||||||
{getContextEmoji(contextUsage.percentage)} {contextUsage.percentage}%
|
{getContextEmoji(contextUsage.percentage)} {contextUsage.percentage}
|
||||||
|
%
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRebuildClick}
|
||||||
|
disabled={rebuildButtonDisabled}
|
||||||
|
title="Rebuild and restart the server"
|
||||||
|
style={{
|
||||||
|
padding: "6px 12px",
|
||||||
|
borderRadius: "99px",
|
||||||
|
border: "none",
|
||||||
|
fontSize: "0.85em",
|
||||||
|
backgroundColor:
|
||||||
|
rebuildStatus === "error" ? "#5a1010" : "#2f2f2f",
|
||||||
|
color:
|
||||||
|
rebuildStatus === "error"
|
||||||
|
? "#f08080"
|
||||||
|
: rebuildButtonDisabled
|
||||||
|
? "#555"
|
||||||
|
: "#888",
|
||||||
|
cursor: rebuildButtonDisabled ? "not-allowed" : "pointer",
|
||||||
|
outline: "none",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
opacity: rebuildButtonDisabled ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
if (!rebuildButtonDisabled) {
|
||||||
|
e.currentTarget.style.backgroundColor = "#3f3f3f";
|
||||||
|
e.currentTarget.style.color = "#ccc";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
if (!rebuildButtonDisabled) {
|
||||||
|
e.currentTarget.style.backgroundColor =
|
||||||
|
rebuildStatus === "error" ? "#5a1010" : "#2f2f2f";
|
||||||
|
e.currentTarget.style.color =
|
||||||
|
rebuildStatus === "error" ? "#f08080" : "#888";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
if (!rebuildButtonDisabled) {
|
||||||
|
e.currentTarget.style.backgroundColor = "#3f3f3f";
|
||||||
|
e.currentTarget.style.color = "#ccc";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!rebuildButtonDisabled) {
|
||||||
|
e.currentTarget.style.backgroundColor =
|
||||||
|
rebuildStatus === "error" ? "#5a1010" : "#2f2f2f";
|
||||||
|
e.currentTarget.style.color =
|
||||||
|
rebuildStatus === "error" ? "#f08080" : "#888";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rebuildButtonLabel}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClearSession}
|
onClick={onClearSession}
|
||||||
@@ -274,5 +540,6 @@ export function ChatHeader({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user