refactor(web): React 最佳实践优化 — memo/callback + 目录边界 + 路由增强
- useLogger: useMemo + JSON.stringify 替代 useState 派生 - useIsDark: effectiveTheme 替代 token 色值比较 - useCurrentProject: layouts/ 提升到 shared/hooks/ - ConsoleShell: locale useMemo 缓存 - ConsoleOutlet: 添加 Suspense 边界 - routes: 添加 layout 级 errorElement - Table 组件: operationColumn useMemo + useCallback - ChatPanel: footer 合并为 useCallback, props 传入模型数据 - ChatPage: textModels/conversations useMemo 缓存
This commit is contained in:
@@ -52,17 +52,18 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si
|
|||||||
|
|
||||||
### 共享 Hooks
|
### 共享 Hooks
|
||||||
|
|
||||||
| Hook | 路径 | 说明 |
|
| Hook | 路径 | 说明 |
|
||||||
| ----------------------- | --------------------------------------- | ----------------------------------------- |
|
| ----------------------- | --------------------------------------- | ---------------------------------------------------------- |
|
||||||
| `use-meta.ts` | `shared/hooks/use-meta.ts` | `/api/meta`(30s 轮询,5s staleTime) |
|
| `use-meta.ts` | `shared/hooks/use-meta.ts` | `/api/meta`(30s 轮询,5s staleTime) |
|
||||||
| `use-providers.ts` | `shared/hooks/use-providers.ts` | 供应商 CRUD + test connection |
|
| `use-providers.ts` | `shared/hooks/use-providers.ts` | 供应商 CRUD + test connection |
|
||||||
| `use-models.ts` | `shared/hooks/use-models.ts` | 模型 CRUD + test connection |
|
| `use-models.ts` | `shared/hooks/use-models.ts` | 模型 CRUD + test connection |
|
||||||
| `use-projects.ts` | `shared/hooks/use-projects.ts` | 项目 CRUD + archive/restore |
|
| `use-projects.ts` | `shared/hooks/use-projects.ts` | 项目 CRUD + archive/restore |
|
||||||
| `use-conversations.ts` | `shared/hooks/use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) |
|
| `use-conversations.ts` | `shared/hooks/use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) |
|
||||||
| `use-logger` | `shared/hooks/use-logger.ts` | Logger hook(组件内使用) |
|
| `use-logger` | `shared/hooks/use-logger.ts` | Logger hook(组件内使用) |
|
||||||
| `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 |
|
| `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 |
|
||||||
| `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 |
|
| `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 |
|
||||||
| `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 |
|
| `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 |
|
||||||
|
| `use-current-project` | `shared/hooks/use-current-project.ts` | 当前工作台项目 + ProjectContext(需在 ProjectProvider 内) |
|
||||||
|
|
||||||
### 共享工具函数
|
### 共享工具函数
|
||||||
|
|
||||||
|
|||||||
@@ -11,27 +11,51 @@
|
|||||||
"source": "ant-design/antd-skill",
|
"source": "ant-design/antd-skill",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/ant-design/SKILL.md",
|
"skillPath": "skills/ant-design/SKILL.md",
|
||||||
"computedHash": "4d0447d48fced080b2825ecc0fb4d7ca836c8015882899c643acca0b864d5179"
|
"computedHash": "096d4ac9513e43030f960aab49b50168a3d5eb35be86926ac6e96e5998ea9466"
|
||||||
},
|
},
|
||||||
"antd": {
|
"antd": {
|
||||||
"source": "ant-design/antd-skill",
|
"source": "ant-design/antd-skill",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/antd/SKILL.md",
|
"skillPath": "skills/antd/SKILL.md",
|
||||||
"computedHash": "4295010f09f85855cab9e9de9ec7f96c14541474b4f3f9d6ef89006430931b94"
|
"computedHash": "5e26c8042060bb811118927b5daf637af7929a00fa973dd8f5f804f3ba6e2bf2"
|
||||||
|
},
|
||||||
|
"react-router-data-mode": {
|
||||||
|
"source": "remix-run/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/react-router-data-mode/SKILL.md",
|
||||||
|
"computedHash": "76e3e0f70ff47b743bd90999e676515221e25fd7ee89cd9e5b340417b1a601e2"
|
||||||
|
},
|
||||||
|
"react-router-declarative-mode": {
|
||||||
|
"source": "remix-run/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/react-router-declarative-mode/SKILL.md",
|
||||||
|
"computedHash": "d7ebbf1ede90809618f02cb3b3d37b9871cdd6c88a81cf338e63de50a0df6a42"
|
||||||
|
},
|
||||||
|
"react-router-framework-mode": {
|
||||||
|
"source": "remix-run/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/react-router-framework-mode/SKILL.md",
|
||||||
|
"computedHash": "26c5bdac2f686c47eb4c4b48b6cb52401cde1dc833e6d26408ddfb22ea83c5ca"
|
||||||
|
},
|
||||||
|
"vercel-react-best-practices": {
|
||||||
|
"source": "vercel-labs/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/react-best-practices/SKILL.md",
|
||||||
|
"computedHash": "ca7b0c0c6e5f2750043f7f0cd72d16ac4e2abc48f9b5500d047a4b77a2506212"
|
||||||
},
|
},
|
||||||
"x-components": {
|
"x-components": {
|
||||||
"source": "ant-design/x",
|
"source": "ant-design/x",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "packages/x-skill/skills/x-components/SKILL.md",
|
"skillPath": "packages/x-skill/skills/x-components/SKILL.md",
|
||||||
"computedHash": "ebc195a3a5020b6d4f4533adf2e0af33253919f0c704947e727f877aba23a4c2"
|
"computedHash": "efb7661cadf8a35fae32ce9a6b261b82ee8c8a2bb76303b333ff166163c0a729"
|
||||||
},
|
},
|
||||||
"x-markdown": {
|
"x-markdown": {
|
||||||
"source": "ant-design/x",
|
"source": "ant-design/x",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "packages/x-skill/skills/x-markdown/SKILL.md",
|
"skillPath": "packages/x-skill/skills/x-markdown/SKILL.md",
|
||||||
"computedHash": "2d26b8eda1692929e99a8b6163ef8b206f1f096a4a84507b50dbe836a7ec041e"
|
"computedHash": "441c281e8537e4aebbc6db5dce0b12c170df916f81782f33f3c8f66dd3f17b17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { DeleteOutlined, MoreOutlined } from "@ant-design/icons";
|
|||||||
import { Conversations } from "@ant-design/x";
|
import { Conversations } from "@ant-design/x";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { App, Spin } from "antd";
|
import { App, Spin } from "antd";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import type { Conversation } from "../../../shared/api";
|
import type { Conversation } from "../../../shared/api";
|
||||||
|
|
||||||
import { useCurrentProject } from "../../layouts/workbench-layout/useCurrentProject";
|
|
||||||
import { createConversation, deleteConversation, fetchConversations } from "../../shared/hooks/use-conversations";
|
import { createConversation, deleteConversation, fetchConversations } from "../../shared/hooks/use-conversations";
|
||||||
|
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
||||||
import { useModelList } from "../../shared/hooks/use-models";
|
import { useModelList } from "../../shared/hooks/use-models";
|
||||||
import { ChatPanel } from "./ChatPanel";
|
import { ChatPanel } from "./ChatPanel";
|
||||||
|
|
||||||
@@ -25,8 +25,13 @@ export function ChatPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: modelsData } = useModelList({ pageSize: 200 });
|
const { data: modelsData } = useModelList({ pageSize: 200 });
|
||||||
const textModels = (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text"));
|
|
||||||
const defaultModelId = textModels[0]?.id;
|
const textModels = useMemo(
|
||||||
|
() => (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text")),
|
||||||
|
[modelsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultModelId = textModels[0]?.id ?? null;
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (id: string) => deleteConversation(project.id, id),
|
mutationFn: (id: string) => deleteConversation(project.id, id),
|
||||||
@@ -39,10 +44,10 @@ export function ChatPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversations = (data?.items ?? []).map((c: Conversation) => ({
|
const conversations = useMemo(
|
||||||
key: c.id,
|
() => (data?.items ?? []).map((c: Conversation) => ({ key: c.id, label: c.title })),
|
||||||
label: c.title,
|
[data],
|
||||||
}));
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-chat-page">
|
<div className="app-chat-page">
|
||||||
@@ -54,7 +59,7 @@ export function ChatPage() {
|
|||||||
activeKey={activeConversationId ?? ""}
|
activeKey={activeConversationId ?? ""}
|
||||||
creation={{
|
creation={{
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
void createConversation(project.id, defaultModelId)
|
void createConversation(project.id, defaultModelId ?? undefined)
|
||||||
.then((conv) => {
|
.then((conv) => {
|
||||||
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
|
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
|
||||||
setActiveConversationId(conv.id);
|
setActiveConversationId(conv.id);
|
||||||
@@ -85,8 +90,10 @@ export function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
<ChatPanel
|
<ChatPanel
|
||||||
conversationId={activeConversationId}
|
conversationId={activeConversationId}
|
||||||
|
defaultModelId={defaultModelId}
|
||||||
onConversationCreated={setActiveConversationId}
|
onConversationCreated={setActiveConversationId}
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
|
textModels={textModels}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Sender } from "@ant-design/x";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { DefaultChatTransport, type UIMessage } from "ai";
|
import { DefaultChatTransport, type UIMessage } from "ai";
|
||||||
import { App, Button, Card, Divider, Flex, Input, Select, Spin, Typography } from "antd";
|
import { App, Button, Card, Divider, Flex, Input, Select, Spin, Typography } from "antd";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createConversation,
|
createConversation,
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
updateConversation,
|
updateConversation,
|
||||||
} from "../../shared/hooks/use-conversations";
|
} from "../../shared/hooks/use-conversations";
|
||||||
import { useLogger } from "../../shared/hooks/use-logger";
|
import { useLogger } from "../../shared/hooks/use-logger";
|
||||||
import { useModelList } from "../../shared/hooks/use-models";
|
|
||||||
import { ChatScrollArea } from "./ChatScrollArea";
|
import { ChatScrollArea } from "./ChatScrollArea";
|
||||||
import { ReasoningPart } from "./parts/ReasoningPart";
|
import { ReasoningPart } from "./parts/ReasoningPart";
|
||||||
import { TextPart } from "./parts/TextPart";
|
import { TextPart } from "./parts/TextPart";
|
||||||
@@ -21,11 +20,19 @@ import { ToolPart } from "./parts/ToolPart";
|
|||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
conversationId: null | string;
|
conversationId: null | string;
|
||||||
|
defaultModelId: null | string;
|
||||||
onConversationCreated: (id: string) => void;
|
onConversationCreated: (id: string) => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
textModels: Array<{ id: string; name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPanel({ conversationId, onConversationCreated, projectId }: ChatPanelProps) {
|
export function ChatPanel({
|
||||||
|
conversationId,
|
||||||
|
defaultModelId: _defaultModelId,
|
||||||
|
onConversationCreated,
|
||||||
|
projectId,
|
||||||
|
textModels,
|
||||||
|
}: ChatPanelProps) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const logger = useLogger({ component: "ChatPanel", page: "workbench" });
|
const logger = useLogger({ component: "ChatPanel", page: "workbench" });
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -37,12 +44,6 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
|
|||||||
const fetchRef = useRef(fetchMessages);
|
const fetchRef = useRef(fetchMessages);
|
||||||
const skipHistoryLoadRef = useRef<null | string>(null);
|
const skipHistoryLoadRef = useRef<null | string>(null);
|
||||||
|
|
||||||
const { data: modelsData } = useModelList({ pageSize: 200 });
|
|
||||||
const textModels = useMemo(
|
|
||||||
() => (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text")),
|
|
||||||
[modelsData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const modelOptions = useMemo(() => textModels.map((m) => ({ label: m.name, value: m.id })), [textModels]);
|
const modelOptions = useMemo(() => textModels.map((m) => ({ label: m.name, value: m.id })), [textModels]);
|
||||||
|
|
||||||
const { messages, regenerate, sendMessage, setMessages, status, stop } = useChat({
|
const { messages, regenerate, sendMessage, setMessages, status, stop } = useChat({
|
||||||
@@ -178,6 +179,33 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
|
|||||||
[sendMessage, conversationId, projectId, onConversationCreated, message, queryClient, displayModelId, logger],
|
[sendMessage, conversationId, projectId, onConversationCreated, message, queryClient, displayModelId, logger],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderSenderFooter = useCallback(
|
||||||
|
(actionNode: ReactNode) => (
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Select
|
||||||
|
className="chat-model-select"
|
||||||
|
disabled={isLoading}
|
||||||
|
onChange={handleModelChange}
|
||||||
|
options={modelOptions}
|
||||||
|
placeholder="选择模型"
|
||||||
|
value={displayModelId}
|
||||||
|
/>
|
||||||
|
<Flex align="center">
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
{actionNode}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
[isLoading, handleModelChange, modelOptions, displayModelId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStop = useCallback(() => {
|
||||||
|
void stop().catch((err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
logger.warn("停止聊天失败", { error: msg });
|
||||||
|
});
|
||||||
|
}, [stop, logger]);
|
||||||
|
|
||||||
const extractText = useCallback((msg: UIMessage) => {
|
const extractText = useCallback((msg: UIMessage) => {
|
||||||
return msg.parts
|
return msg.parts
|
||||||
.filter((p) => p.type === "text")
|
.filter((p) => p.type === "text")
|
||||||
@@ -304,29 +332,9 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
|
|||||||
<Sender
|
<Sender
|
||||||
autoSize={{ maxRows: 3, minRows: 1 }}
|
autoSize={{ maxRows: 3, minRows: 1 }}
|
||||||
classNames={{ root: "chat-sender-box" }}
|
classNames={{ root: "chat-sender-box" }}
|
||||||
footer={(actionNode) => (
|
footer={renderSenderFooter}
|
||||||
<Flex align="center" justify="space-between">
|
|
||||||
<Select
|
|
||||||
className="chat-model-select"
|
|
||||||
disabled={isLoading}
|
|
||||||
onChange={handleModelChange}
|
|
||||||
options={modelOptions}
|
|
||||||
placeholder="选择模型"
|
|
||||||
value={displayModelId}
|
|
||||||
/>
|
|
||||||
<Flex align="center">
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
{actionNode}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onCancel={() => {
|
onCancel={handleStop}
|
||||||
void stop().catch((err: unknown) => {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
logger.warn("停止聊天失败", { error: msg });
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
onSubmit={handleSenderSubmit}
|
onSubmit={handleSenderSubmit}
|
||||||
placeholder="输入消息..."
|
placeholder="输入消息..."
|
||||||
@@ -397,29 +405,9 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
|
|||||||
<Sender
|
<Sender
|
||||||
autoSize={{ maxRows: 3, minRows: 1 }}
|
autoSize={{ maxRows: 3, minRows: 1 }}
|
||||||
classNames={{ root: "chat-sender-box" }}
|
classNames={{ root: "chat-sender-box" }}
|
||||||
footer={(actionNode) => (
|
footer={renderSenderFooter}
|
||||||
<Flex align="center" justify="space-between">
|
|
||||||
<Select
|
|
||||||
className="chat-model-select"
|
|
||||||
disabled={isLoading}
|
|
||||||
onChange={handleModelChange}
|
|
||||||
options={modelOptions}
|
|
||||||
placeholder="选择模型"
|
|
||||||
value={displayModelId}
|
|
||||||
/>
|
|
||||||
<Flex align="center">
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
{actionNode}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onCancel={() => {
|
onCancel={handleStop}
|
||||||
void stop().catch((err: unknown) => {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
logger.warn("停止聊天失败", { error: msg });
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
onSubmit={handleSenderSubmit}
|
onSubmit={handleSenderSubmit}
|
||||||
placeholder="输入消息..."
|
placeholder="输入消息..."
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ColumnsType } from "antd/es/table";
|
import type { TableColumnsType } from "antd";
|
||||||
|
|
||||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||||
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
import type { Model, ModelListResponse, ProviderOption } from "../../../../shared/api";
|
import type { Model, ModelListResponse, ProviderOption } from "../../../../shared/api";
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ function getProviderName(providerId: string, providers: ProviderOption[]): strin
|
|||||||
return providers.find((p) => p.id === providerId)?.name ?? providerId;
|
return providers.find((p) => p.id === providerId)?.name ?? providerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS: ColumnsType<Model> = [
|
const COLUMNS: TableColumnsType<Model> = [
|
||||||
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
|
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
|
||||||
{
|
{
|
||||||
dataIndex: "providerId",
|
dataIndex: "providerId",
|
||||||
@@ -65,49 +66,61 @@ export function ModelTable({
|
|||||||
}: ModelTableProps) {
|
}: ModelTableProps) {
|
||||||
const { message } = AntApp.useApp();
|
const { message } = AntApp.useApp();
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async (id: string) => {
|
||||||
await onDelete(id);
|
try {
|
||||||
message.success("模型已删除");
|
await onDelete(id);
|
||||||
} catch (err: unknown) {
|
message.success("模型已删除");
|
||||||
message.error((err as Error).message);
|
} catch (err: unknown) {
|
||||||
}
|
message.error((err as Error).message);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
const columnsWithProvider: ColumnsType<Model> = COLUMNS.map((col) =>
|
[onDelete, message],
|
||||||
"dataIndex" in col && col.dataIndex === "providerId"
|
|
||||||
? {
|
|
||||||
...col,
|
|
||||||
render: (_value: unknown, record: Model) => getProviderName(record.providerId, providers),
|
|
||||||
}
|
|
||||||
: col,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const operationColumn: ColumnsType<Model>[number] = {
|
const columnsWithProvider = useMemo<TableColumnsType<Model>>(
|
||||||
dataIndex: "op",
|
() =>
|
||||||
render: (_value: unknown, record: Model) => (
|
COLUMNS.map((col) =>
|
||||||
<Space size="small">
|
"dataIndex" in col && col.dataIndex === "providerId"
|
||||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
? {
|
||||||
编辑
|
...col,
|
||||||
</Button>
|
render: (_value: unknown, record: Model) => getProviderName(record.providerId, providers),
|
||||||
<Popconfirm
|
}
|
||||||
description="此操作不可恢复。"
|
: col,
|
||||||
onConfirm={() => void handleDelete(record.id)}
|
),
|
||||||
title="确认删除此模型?"
|
[providers],
|
||||||
>
|
);
|
||||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
|
||||||
删除
|
const operationColumn = useMemo<TableColumnsType<Model>[number]>(
|
||||||
|
() => ({
|
||||||
|
dataIndex: "op",
|
||||||
|
render: (_value: unknown, record: Model) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||||
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
<Popconfirm
|
||||||
</Space>
|
description="此操作不可恢复。"
|
||||||
),
|
onConfirm={() => void handleDelete(record.id)}
|
||||||
title: "操作",
|
title="确认删除此模型?"
|
||||||
width: 180,
|
>
|
||||||
};
|
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
title: "操作",
|
||||||
|
width: 180,
|
||||||
|
}),
|
||||||
|
[onEdit, handleDelete],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => [...columnsWithProvider, operationColumn], [columnsWithProvider, operationColumn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
columns={[...columnsWithProvider, operationColumn]}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ColumnsType } from "antd/es/table";
|
import type { TableColumnsType } from "antd";
|
||||||
|
|
||||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||||
import { App as AntApp, Button, Popconfirm, Space, Table } from "antd";
|
import { App as AntApp, Button, Popconfirm, Space, Table } from "antd";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
import type { Provider, ProviderListResponse } from "../../../../shared/api";
|
import type { Provider, ProviderListResponse } from "../../../../shared/api";
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ const TYPE_LABELS: Record<Provider["type"], string> = {
|
|||||||
"openai-compatible": "OpenAI 兼容",
|
"openai-compatible": "OpenAI 兼容",
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMNS: ColumnsType<Provider> = [
|
const COLUMNS: TableColumnsType<Provider> = [
|
||||||
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
|
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
|
||||||
{
|
{
|
||||||
dataIndex: "type",
|
dataIndex: "type",
|
||||||
@@ -35,40 +36,48 @@ const COLUMNS: ColumnsType<Provider> = [
|
|||||||
export function ProviderTable({ data, loading, onDelete, onEdit, onPageChange, page, pageSize }: ProviderTableProps) {
|
export function ProviderTable({ data, loading, onDelete, onEdit, onPageChange, page, pageSize }: ProviderTableProps) {
|
||||||
const { message } = AntApp.useApp();
|
const { message } = AntApp.useApp();
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async (id: string) => {
|
||||||
await onDelete(id);
|
try {
|
||||||
message.success("供应商已删除");
|
await onDelete(id);
|
||||||
} catch (err: unknown) {
|
message.success("供应商已删除");
|
||||||
message.error((err as Error).message);
|
} catch (err: unknown) {
|
||||||
}
|
message.error((err as Error).message);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[onDelete, message],
|
||||||
|
);
|
||||||
|
|
||||||
const operationColumn: ColumnsType<Provider>[number] = {
|
const operationColumn = useMemo<TableColumnsType<Provider>[number]>(
|
||||||
dataIndex: "op",
|
() => ({
|
||||||
render: (_value: unknown, record: Provider) => (
|
dataIndex: "op",
|
||||||
<Space size="small">
|
render: (_value: unknown, record: Provider) => (
|
||||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
<Space size="small">
|
||||||
编辑
|
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||||
</Button>
|
编辑
|
||||||
<Popconfirm
|
|
||||||
description="该供应商下存在模型时无法删除,请先删除或迁移相关模型。"
|
|
||||||
onConfirm={() => void handleDelete(record.id)}
|
|
||||||
title="确认删除此供应商?"
|
|
||||||
>
|
|
||||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
|
||||||
删除
|
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
<Popconfirm
|
||||||
</Space>
|
description="该供应商下存在模型时无法删除,请先删除或迁移相关模型。"
|
||||||
),
|
onConfirm={() => void handleDelete(record.id)}
|
||||||
title: "操作",
|
title="确认删除此供应商?"
|
||||||
width: 180,
|
>
|
||||||
};
|
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
title: "操作",
|
||||||
|
width: 180,
|
||||||
|
}),
|
||||||
|
[onEdit, handleDelete],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => [...COLUMNS, operationColumn], [operationColumn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
columns={[...COLUMNS, operationColumn]}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ColumnsType } from "antd/es/table";
|
import type { TableColumnsType } from "antd";
|
||||||
|
|
||||||
import { DeleteOutlined, EditOutlined, InboxOutlined, LoginOutlined, RedoOutlined } from "@ant-design/icons";
|
import { DeleteOutlined, EditOutlined, InboxOutlined, LoginOutlined, RedoOutlined } from "@ant-design/icons";
|
||||||
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import type { Project, ProjectListResponse, ProjectStatus } from "../../../../shared/api";
|
import type { Project, ProjectListResponse, ProjectStatus } from "../../../../shared/api";
|
||||||
@@ -19,7 +20,7 @@ interface ProjectTableProps {
|
|||||||
status: ProjectStatus;
|
status: ProjectStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS: ColumnsType<Project> = [
|
const COLUMNS: TableColumnsType<Project> = [
|
||||||
{ dataIndex: "name", ellipsis: true, title: "名称", width: 140 },
|
{ dataIndex: "name", ellipsis: true, title: "名称", width: 140 },
|
||||||
{ dataIndex: "description", ellipsis: true, title: "描述" },
|
{ dataIndex: "description", ellipsis: true, title: "描述" },
|
||||||
{
|
{
|
||||||
@@ -65,88 +66,102 @@ export function ProjectTable({
|
|||||||
const { message } = AntApp.useApp();
|
const { message } = AntApp.useApp();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleArchive = async (id: string) => {
|
const handleArchive = useCallback(
|
||||||
try {
|
async (id: string) => {
|
||||||
await onArchive(id);
|
try {
|
||||||
message.success("项目已归档");
|
await onArchive(id);
|
||||||
} catch (err: unknown) {
|
message.success("项目已归档");
|
||||||
message.error((err as Error).message);
|
} catch (err: unknown) {
|
||||||
}
|
message.error((err as Error).message);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[onArchive, message],
|
||||||
|
);
|
||||||
|
|
||||||
const handleRestore = async (id: string) => {
|
const handleRestore = useCallback(
|
||||||
try {
|
async (id: string) => {
|
||||||
await onRestore(id);
|
try {
|
||||||
message.success("项目已恢复");
|
await onRestore(id);
|
||||||
} catch (err: unknown) {
|
message.success("项目已恢复");
|
||||||
message.error((err as Error).message);
|
} catch (err: unknown) {
|
||||||
}
|
message.error((err as Error).message);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[onRestore, message],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async (id: string) => {
|
||||||
await onDelete(id);
|
try {
|
||||||
message.success("项目已永久删除");
|
await onDelete(id);
|
||||||
} catch (err: unknown) {
|
message.success("项目已永久删除");
|
||||||
message.error((err as Error).message);
|
} catch (err: unknown) {
|
||||||
}
|
message.error((err as Error).message);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[onDelete, message],
|
||||||
|
);
|
||||||
|
|
||||||
const operationColumn: ColumnsType<Project>[number] = {
|
const operationColumn = useMemo<TableColumnsType<Project>[number]>(
|
||||||
dataIndex: "op",
|
() => ({
|
||||||
render: (_value, record: Project) => {
|
dataIndex: "op",
|
||||||
if (record.status === "active") {
|
render: (_value, record: Project) => {
|
||||||
|
if (record.status === "active") {
|
||||||
|
return (
|
||||||
|
<Space size="small">
|
||||||
|
<Button
|
||||||
|
icon={<LoginOutlined />}
|
||||||
|
onClick={() => void navigate(`/workbench/${record.id}`)}
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
>
|
||||||
|
工作台
|
||||||
|
</Button>
|
||||||
|
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
description="归档后项目将变为只读。"
|
||||||
|
onConfirm={() => void handleArchive(record.id)}
|
||||||
|
title="确认归档此项目?"
|
||||||
|
>
|
||||||
|
<Button color="orange" icon={<InboxOutlined />} size="small" variant="link">
|
||||||
|
归档
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
<Button
|
<Popconfirm onConfirm={() => void handleRestore(record.id)} title="确认恢复此项目?">
|
||||||
icon={<LoginOutlined />}
|
<Button icon={<RedoOutlined />} size="small" type="link">
|
||||||
onClick={() => void navigate(`/workbench/${record.id}`)}
|
恢复
|
||||||
size="small"
|
</Button>
|
||||||
type="link"
|
</Popconfirm>
|
||||||
>
|
|
||||||
工作台
|
|
||||||
</Button>
|
|
||||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
description="归档后项目将变为只读。"
|
description="此操作不可恢复。"
|
||||||
onConfirm={() => void handleArchive(record.id)}
|
onConfirm={() => void handleDelete(record.id)}
|
||||||
title="确认归档此项目?"
|
title="确认永久删除此项目?"
|
||||||
>
|
>
|
||||||
<Button color="orange" icon={<InboxOutlined />} size="small" variant="link">
|
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||||
归档
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
return (
|
title: "操作",
|
||||||
<Space size="small">
|
width: status === "active" ? 260 : 160,
|
||||||
<Popconfirm onConfirm={() => void handleRestore(record.id)} title="确认恢复此项目?">
|
}),
|
||||||
<Button icon={<RedoOutlined />} size="small" type="link">
|
[navigate, onEdit, handleArchive, handleRestore, handleDelete, status],
|
||||||
恢复
|
);
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
const columns = useMemo(() => [...COLUMNS, operationColumn], [operationColumn]);
|
||||||
<Popconfirm
|
|
||||||
description="此操作不可恢复。"
|
|
||||||
onConfirm={() => void handleDelete(record.id)}
|
|
||||||
title="确认永久删除此项目?"
|
|
||||||
>
|
|
||||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
title: "操作",
|
|
||||||
width: status === "active" ? 260 : 160,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
columns={[...COLUMNS, operationColumn]}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type ReactNode } from "react";
|
|||||||
|
|
||||||
import type { Project } from "../../../shared/api";
|
import type { Project } from "../../../shared/api";
|
||||||
|
|
||||||
import { ProjectContext } from "./ProjectContextValue";
|
import { ProjectContext } from "../../shared/hooks/use-current-project";
|
||||||
|
|
||||||
export function ProjectProvider({ children, project }: { children: ReactNode; project: Project }) {
|
export function ProjectProvider({ children, project }: { children: ReactNode; project: Project }) {
|
||||||
return <ProjectContext.Provider value={project}>{children}</ProjectContext.Provider>;
|
return <ProjectContext.Provider value={project}>{children}</ProjectContext.Provider>;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
import type { Project } from "../../../shared/api";
|
|
||||||
|
|
||||||
export const ProjectContext = createContext<null | Project>(null);
|
|
||||||
@@ -5,9 +5,9 @@ import { useNavigate } from "react-router";
|
|||||||
import type { Project } from "../../../shared/api";
|
import type { Project } from "../../../shared/api";
|
||||||
|
|
||||||
import { ConsoleShell } from "../../shared/components/ConsoleShell/ConsoleShell";
|
import { ConsoleShell } from "../../shared/components/ConsoleShell/ConsoleShell";
|
||||||
|
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
||||||
import { ProjectProvider } from "./ProjectContext";
|
import { ProjectProvider } from "./ProjectContext";
|
||||||
import { getWorkbenchMenuItems } from "./routes";
|
import { getWorkbenchMenuItems } from "./routes";
|
||||||
import { useCurrentProject } from "./useCurrentProject";
|
|
||||||
|
|
||||||
interface WorkbenchConsoleLayoutProps {
|
interface WorkbenchConsoleLayoutProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import { NotFoundPage } from "./features/not-found";
|
|||||||
import { ProjectsPage } from "./features/projects";
|
import { ProjectsPage } from "./features/projects";
|
||||||
import { AdminConsoleLayout } from "./layouts/admin-layout/AdminConsoleLayout";
|
import { AdminConsoleLayout } from "./layouts/admin-layout/AdminConsoleLayout";
|
||||||
import { WorkbenchProjectGate } from "./layouts/workbench-layout/WorkbenchProjectGate";
|
import { WorkbenchProjectGate } from "./layouts/workbench-layout/WorkbenchProjectGate";
|
||||||
|
import { RouteError } from "./shared/components/RouteError";
|
||||||
|
|
||||||
export function AppRoutes() {
|
export function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<AdminConsoleLayout />}>
|
<Route element={<AdminConsoleLayout />} errorElement={<RouteError />}>
|
||||||
<Route element={<DashboardPage />} path="/" />
|
<Route element={<DashboardPage />} path="/" />
|
||||||
<Route element={<ProjectsPage />} path="/projects" />
|
<Route element={<ProjectsPage />} path="/projects" />
|
||||||
<Route element={<ModelsPage />} path="/models" />
|
<Route element={<ModelsPage />} path="/models" />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<WorkbenchProjectGate />} path="/workbench/:projectId">
|
<Route element={<WorkbenchProjectGate />} errorElement={<RouteError />} path="/workbench/:projectId">
|
||||||
<Route element={<ChatPage />} path="" />
|
<Route element={<ChatPage />} path="" />
|
||||||
<Route element={<ChatPage />} path="chat" />
|
<Route element={<ChatPage />} path="chat" />
|
||||||
<Route element={<InboxPage />} path="inbox" />
|
<Route element={<InboxPage />} path="inbox" />
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
|
import { Spin } from "antd";
|
||||||
|
import { Suspense } from "react";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
export function ConsoleOutlet() {
|
export function ConsoleOutlet() {
|
||||||
return <Outlet />;
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="app-loading">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { XProvider } from "@ant-design/x";
|
|||||||
import zhCN_X from "@ant-design/x/locale/zh_CN";
|
import zhCN_X from "@ant-design/x/locale/zh_CN";
|
||||||
import { App as AntApp, Layout, Segmented, theme } from "antd";
|
import { App as AntApp, Layout, Segmented, theme } from "antd";
|
||||||
import zhCN from "antd/locale/zh_CN";
|
import zhCN from "antd/locale/zh_CN";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import type { ConsoleShellProps } from "./types";
|
import type { ConsoleShellProps } from "./types";
|
||||||
|
|
||||||
@@ -28,9 +29,10 @@ export function ConsoleShell({ headerExtra, menuItems, title }: ConsoleShellProp
|
|||||||
|
|
||||||
const versionDisplay = meta?.version ? `v${meta.version}` : null;
|
const versionDisplay = meta?.version ? `v${meta.version}` : null;
|
||||||
const themeAlgorithm = effectiveTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
const themeAlgorithm = effectiveTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||||
|
const locale = useMemo(() => ({ ...zhCN, ...zhCN_X }), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<XProvider locale={{ ...zhCN, ...zhCN_X }} theme={{ algorithm: themeAlgorithm }}>
|
<XProvider locale={locale} theme={{ algorithm: themeAlgorithm }}>
|
||||||
<AntApp>
|
<AntApp>
|
||||||
<Layout className="app-layout">
|
<Layout className="app-layout">
|
||||||
<Header className="app-header">
|
<Header className="app-header">
|
||||||
|
|||||||
29
src/web/shared/components/RouteError.tsx
Normal file
29
src/web/shared/components/RouteError.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Alert, Button } from "antd";
|
||||||
|
import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router";
|
||||||
|
|
||||||
|
export function RouteError() {
|
||||||
|
const error = useRouteError();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const message = isRouteErrorResponse(error)
|
||||||
|
? `${error.status} ${error.statusText}`
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "未知错误";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-loading">
|
||||||
|
<Alert
|
||||||
|
action={
|
||||||
|
<Button onClick={() => void navigate("/")} size="small" type="primary">
|
||||||
|
返回首页
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
description={message}
|
||||||
|
showIcon
|
||||||
|
title="页面加载出错"
|
||||||
|
type="error"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
import type { Project } from "../../../shared/api";
|
import type { Project } from "../../../shared/api";
|
||||||
|
|
||||||
import { ProjectContext } from "./ProjectContextValue";
|
export const ProjectContext = createContext<null | Project>(null);
|
||||||
|
|
||||||
export function useCurrentProject(): Project {
|
export function useCurrentProject(): Project {
|
||||||
const project = useContext(ProjectContext);
|
const project = useContext(ProjectContext);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { theme } from "antd";
|
import { useThemePreference } from "./use-theme-preference";
|
||||||
|
|
||||||
export function useIsDark(): boolean {
|
export function useIsDark(): boolean {
|
||||||
const { token } = theme.useToken();
|
const { effectiveTheme } = useThemePreference();
|
||||||
return token.colorBgBase === "#000";
|
return effectiveTheme === "dark";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { App } from "antd";
|
import { App } from "antd";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import type { Logger } from "../utils/logger";
|
import type { Logger } from "../utils/logger";
|
||||||
|
|
||||||
@@ -7,19 +7,12 @@ import { AntdMessageSink, ConsoleSink, createDefaultLogger } from "../utils/logg
|
|||||||
|
|
||||||
export function useLogger(bindings?: Record<string, unknown>): Logger {
|
export function useLogger(bindings?: Record<string, unknown>): Logger {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [stableJson, setStableJson] = useState(() => JSON.stringify(bindings ?? {}));
|
const bindingsKey = JSON.stringify(bindings ?? {});
|
||||||
const [stableBindings, setStableBindings] = useState(() => bindings);
|
|
||||||
const currentJson = JSON.stringify(bindings ?? {});
|
|
||||||
|
|
||||||
if (currentJson !== stableJson) {
|
|
||||||
setStableJson(currentJson);
|
|
||||||
setStableBindings(bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const isProduction = !!import.meta.env["PROD"];
|
const isProduction = !!import.meta.env["PROD"];
|
||||||
const base = createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
|
const base = createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
|
||||||
if (!stableBindings || Object.keys(stableBindings).length === 0) return base;
|
if (bindingsKey === "{}") return base;
|
||||||
return base.child(stableBindings);
|
return base.child(JSON.parse(bindingsKey) as Record<string, unknown>);
|
||||||
}, [message, stableBindings]);
|
}, [message, bindingsKey]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ describe("ChatPanel", () => {
|
|||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
createElement(ChatPanel, {
|
createElement(ChatPanel, {
|
||||||
conversationId: null,
|
conversationId: null,
|
||||||
|
defaultModelId: "model-1",
|
||||||
onConversationCreated: noop,
|
onConversationCreated: noop,
|
||||||
projectId: PROJECT_ID,
|
projectId: PROJECT_ID,
|
||||||
|
textModels: [TEXT_MODEL],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,8 +104,10 @@ describe("ChatPanel", () => {
|
|||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
createElement(ChatPanel, {
|
createElement(ChatPanel, {
|
||||||
conversationId: null,
|
conversationId: null,
|
||||||
|
defaultModelId: "model-1",
|
||||||
onConversationCreated: onCreated,
|
onConversationCreated: onCreated,
|
||||||
projectId: PROJECT_ID,
|
projectId: PROJECT_ID,
|
||||||
|
textModels: [TEXT_MODEL],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -133,8 +137,10 @@ describe("ChatPanel", () => {
|
|||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
createElement(ChatPanel, {
|
createElement(ChatPanel, {
|
||||||
conversationId: null,
|
conversationId: null,
|
||||||
|
defaultModelId: "model-1",
|
||||||
onConversationCreated: noop,
|
onConversationCreated: noop,
|
||||||
projectId: PROJECT_ID,
|
projectId: PROJECT_ID,
|
||||||
|
textModels: [TEXT_MODEL],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -154,8 +160,10 @@ describe("ChatPanel", () => {
|
|||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
createElement(ChatPanel, {
|
createElement(ChatPanel, {
|
||||||
conversationId: "conv-1",
|
conversationId: "conv-1",
|
||||||
|
defaultModelId: "model-1",
|
||||||
onConversationCreated: noop,
|
onConversationCreated: noop,
|
||||||
projectId: PROJECT_ID,
|
projectId: PROJECT_ID,
|
||||||
|
textModels: [TEXT_MODEL],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user