feat: add AI research chatbot with confirmed business tools

This commit is contained in:
yuxuanhui
2026-09-07 23:02:55 +08:00
parent 3cd280d068
commit 79ab20b4ea
44 changed files with 4647 additions and 197 deletions
+93 -5
View File
@@ -23,6 +23,8 @@ import type { Account, Job } from "./types";
import { AccountPage } from "./pages/AccountPage";
import { AlphaPage } from "./pages/AlphaPage";
import { JobPanel } from "./components/JobPanel";
import { ChatPanel } from "./ai/ChatPanel";
import type { PageContext, UIAction } from "./ai/types";
export default function App() {
const [authenticated, setAuthenticated] = useState<boolean | null>(null);
@@ -34,6 +36,35 @@ export default function App() {
const [showJobs, setShowJobs] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [pollError, setPollError] = useState("");
const [chatOpen, setChatOpen] = useState(false);
const [chatWidth, setChatWidth] = useState(420);
const [viewport, setViewport] = useState(window.innerWidth);
const [alphaContext, setAlphaContext] = useState<PageContext>({
page: "alphas",
});
const [aiAction, setAIAction] = useState<UIAction | null>(null);
const chatOffset = viewport >= 1440 && chatOpen ? chatWidth : 0;
const focusBusiness = useCallback(() => {
if (window.innerWidth < 1440) setChatOpen(false);
}, []);
useEffect(() => {
const resize = () => setViewport(window.innerWidth);
window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize);
}, []);
useEffect(() => {
document.documentElement.style.setProperty(
"--chat-space",
`${chatOffset}px`,
);
document.documentElement.style.setProperty(
"--chat-width",
`${chatWidth}px`,
);
return () => {
document.documentElement.style.setProperty("--chat-space", "0px");
};
}, [chatOffset, chatWidth]);
const refresh = useCallback(async () => {
try {
const [nextAccount, nextJobs] = await Promise.all([
@@ -56,6 +87,8 @@ export default function App() {
setAuthenticated(false);
setAccount(null);
setJobs([]);
setChatOpen(false);
setShowJobs(false);
};
window.addEventListener("session-expired", expired);
const hash = () =>
@@ -103,6 +136,12 @@ export default function App() {
try {
await post("/auth/logout");
setAuthenticated(false);
setChatOpen(false);
setShowJobs(false);
setAccount(null);
setJobs([]);
setAIAction(null);
setAlphaContext({ page: "alphas" });
} catch (e) {
Toast.error((e as Error).message);
}
@@ -174,11 +213,21 @@ export default function App() {
<Button
theme="borderless"
icon={<IconBell />}
onClick={() => setShowJobs(true)}
onClick={() => {
focusBusiness();
setShowJobs(true);
}}
>
同步任务
</Button>
</Badge>
<Button
aria-label="切换研究助手"
theme={chatOpen ? "light" : "borderless"}
onClick={() => setChatOpen(!chatOpen)}
>
AI 助手
</Button>
<div className="top-divider" />
<Avatar size="small" color="purple">
{account?.display_name.slice(0, 1) || "研"}
@@ -201,25 +250,35 @@ export default function App() {
description={`后台状态暂时不可用:${pollError}`}
/>
)}
{page === "account" ? (
<div hidden={page !== "account"}>
<AccountPage
account={account}
onChange={actionDone}
onTask={taskCreated}
/>
) : (
</div>
<div hidden={page !== "alphas"}>
<AlphaPage
taskPanelOpen={showJobs}
account={account}
version={`${refreshKey}:${completedVersion}`}
onTask={taskCreated}
onAccount={() => changePage("account")}
active={page === "alphas"}
overlaySuspended={
page !== "alphas" || (viewport < 1440 && chatOpen)
}
chatOffset={chatOffset}
onContext={setAlphaContext}
action={aiAction}
onOverlay={focusBusiness}
/>
)}
</div>
</main>
</div>
<JobPanel
visible={showJobs}
visible={showJobs && !(viewport < 1440 && chatOpen)}
chatOffset={chatOffset}
jobs={jobs}
timezone={account?.timezone}
onClose={() => setShowJobs(false)}
@@ -229,6 +288,35 @@ export default function App() {
changePage("account");
}}
/>
{!chatOpen && (
<Button
className="ai-launcher"
aria-label="打开研究助手"
theme="solid"
onClick={() => setChatOpen(true)}
>
AI 研究助手
</Button>
)}
<ChatPanel
jobs={jobs}
open={chatOpen}
width={chatWidth}
onWidth={setChatWidth}
onClose={() => setChatOpen(false)}
context={page === "alphas" ? alphaContext : { page: "account" }}
timezone={account?.timezone}
onSettings={() => {
focusBusiness();
changePage("account");
}}
onChanged={actionDone}
onAction={(action) => {
focusBusiness();
changePage("alphas");
setAIAction(action);
}}
/>
</div>
)}
</LocaleProvider>