diff --git a/docs/development/frontend.md b/docs/development/frontend.md index e8fcde0..4c07c92 100644 --- a/docs/development/frontend.md +++ b/docs/development/frontend.md @@ -52,17 +52,18 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si ### 共享 Hooks -| Hook | 路径 | 说明 | -| ----------------------- | --------------------------------------- | ----------------------------------------- | -| `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-models.ts` | `shared/hooks/use-models.ts` | 模型 CRUD + test connection | -| `use-projects.ts` | `shared/hooks/use-projects.ts` | 项目 CRUD + archive/restore | -| `use-conversations.ts` | `shared/hooks/use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) | -| `use-logger` | `shared/hooks/use-logger.ts` | Logger hook(组件内使用) | -| `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 | -| `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 | -| `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 | +| Hook | 路径 | 说明 | +| ----------------------- | --------------------------------------- | ---------------------------------------------------------- | +| `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-models.ts` | `shared/hooks/use-models.ts` | 模型 CRUD + test connection | +| `use-projects.ts` | `shared/hooks/use-projects.ts` | 项目 CRUD + archive/restore | +| `use-conversations.ts` | `shared/hooks/use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) | +| `use-logger` | `shared/hooks/use-logger.ts` | Logger hook(组件内使用) | +| `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 | +| `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 | +| `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 | +| `use-current-project` | `shared/hooks/use-current-project.ts` | 当前工作台项目 + ProjectContext(需在 ProjectProvider 内) | ### 共享工具函数 diff --git a/skills-lock.json b/skills-lock.json index 89040ed..9d401b1 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -11,27 +11,51 @@ "source": "ant-design/antd-skill", "sourceType": "github", "skillPath": "skills/ant-design/SKILL.md", - "computedHash": "4d0447d48fced080b2825ecc0fb4d7ca836c8015882899c643acca0b864d5179" + "computedHash": "096d4ac9513e43030f960aab49b50168a3d5eb35be86926ac6e96e5998ea9466" }, "antd": { "source": "ant-design/antd-skill", "sourceType": "github", "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": { "source": "ant-design/x", "ref": "main", "sourceType": "github", "skillPath": "packages/x-skill/skills/x-components/SKILL.md", - "computedHash": "ebc195a3a5020b6d4f4533adf2e0af33253919f0c704947e727f877aba23a4c2" + "computedHash": "efb7661cadf8a35fae32ce9a6b261b82ee8c8a2bb76303b333ff166163c0a729" }, "x-markdown": { "source": "ant-design/x", "ref": "main", "sourceType": "github", "skillPath": "packages/x-skill/skills/x-markdown/SKILL.md", - "computedHash": "2d26b8eda1692929e99a8b6163ef8b206f1f096a4a84507b50dbe836a7ec041e" + "computedHash": "441c281e8537e4aebbc6db5dce0b12c170df916f81782f33f3c8f66dd3f17b17" } } } diff --git a/src/web/features/chat/ChatPage.tsx b/src/web/features/chat/ChatPage.tsx index c302c99..bcae78f 100644 --- a/src/web/features/chat/ChatPage.tsx +++ b/src/web/features/chat/ChatPage.tsx @@ -2,12 +2,12 @@ import { DeleteOutlined, MoreOutlined } from "@ant-design/icons"; import { Conversations } from "@ant-design/x"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { App, Spin } from "antd"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import type { Conversation } from "../../../shared/api"; -import { useCurrentProject } from "../../layouts/workbench-layout/useCurrentProject"; 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 { ChatPanel } from "./ChatPanel"; @@ -25,8 +25,13 @@ export function ChatPage() { }); 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({ mutationFn: (id: string) => deleteConversation(project.id, id), @@ -39,10 +44,10 @@ export function ChatPage() { }, }); - const conversations = (data?.items ?? []).map((c: Conversation) => ({ - key: c.id, - label: c.title, - })); + const conversations = useMemo( + () => (data?.items ?? []).map((c: Conversation) => ({ key: c.id, label: c.title })), + [data], + ); return (
@@ -54,7 +59,7 @@ export function ChatPage() { activeKey={activeConversationId ?? ""} creation={{ onClick: () => { - void createConversation(project.id, defaultModelId) + void createConversation(project.id, defaultModelId ?? undefined) .then((conv) => { void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY }); setActiveConversationId(conv.id); @@ -85,8 +90,10 @@ export function ChatPage() {
); diff --git a/src/web/features/chat/ChatPanel.tsx b/src/web/features/chat/ChatPanel.tsx index c620977..8293297 100644 --- a/src/web/features/chat/ChatPanel.tsx +++ b/src/web/features/chat/ChatPanel.tsx @@ -4,7 +4,7 @@ import { Sender } from "@ant-design/x"; import { useQueryClient } from "@tanstack/react-query"; import { DefaultChatTransport, type UIMessage } from "ai"; 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 { createConversation, @@ -13,7 +13,6 @@ import { updateConversation, } from "../../shared/hooks/use-conversations"; import { useLogger } from "../../shared/hooks/use-logger"; -import { useModelList } from "../../shared/hooks/use-models"; import { ChatScrollArea } from "./ChatScrollArea"; import { ReasoningPart } from "./parts/ReasoningPart"; import { TextPart } from "./parts/TextPart"; @@ -21,11 +20,19 @@ import { ToolPart } from "./parts/ToolPart"; interface ChatPanelProps { conversationId: null | string; + defaultModelId: null | string; onConversationCreated: (id: string) => void; 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 logger = useLogger({ component: "ChatPanel", page: "workbench" }); const queryClient = useQueryClient(); @@ -37,12 +44,6 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }: const fetchRef = useRef(fetchMessages); const skipHistoryLoadRef = useRef(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 { 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], ); + const renderSenderFooter = useCallback( + (actionNode: ReactNode) => ( + + - - - {actionNode} - - - )} + footer={renderSenderFooter} loading={isLoading} - onCancel={() => { - void stop().catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - logger.warn("停止聊天失败", { error: msg }); - }); - }} + onCancel={handleStop} onChange={setInput} onSubmit={handleSenderSubmit} placeholder="输入消息..." @@ -397,29 +405,9 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }: ( - -