feat: add unified strategy project workflow

This commit is contained in:
Administrator committed 2026-09-09 15:40:29 +08:00
1 parent 67d12929fc
commit a26fd32eba
28 files changed
+3382 -191

No files matched your search

@@ -51,6 +51,10 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useTheme } from "@/contexts/ThemeContext";
import { useAgentsPaginated } from "@/core/agents";
import { getAPIClient } from "@/core/api";
@@ -69,6 +73,7 @@ import {
useToggleThreadPin,
} from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import {
bootstrapResumePath,
isBootstrapThread,
@@ -479,6 +484,7 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
) : (
filteredThreads.map((thread) => {
const isBoot = isBootstrapThread(thread);
const projectType = myProjectConversationTypeOfThread(thread);
const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread);
const isActive = href === pathname;
const pinned = isThreadPinned(thread);
@@ -505,6 +511,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
)}
aria-label="智能体创建会话"
/>
) : projectType ? (
<MyProjectThreadIcon type={projectType} />
) : (
<MessageSquare
className={cn(
@@ -516,9 +524,15 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
{pinned && (
<Pin className="text-muted-foreground/70 size-3 shrink-0" />
)}
<span className="truncate">
<span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))}
</span>
{projectType && (
<MyProjectThreadBadge
className="mr-4"
type={projectType}
/>
)}
{isActive && (
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
)}
@@ -4,7 +4,12 @@ import { MessageSquareIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useThreadsPage } from "@/core/threads/hooks";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import { pathOfThread, titleOfThread, isHiddenFromRecentChats, isBootstrapThread } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
@@ -45,6 +50,7 @@ export function AgentRecentChats({ agentId }: { agentId: string }) {
{threads.map((thread) => {
const href = pathOfThread(thread);
const isActive = href === pathname;
const projectType = myProjectConversationTypeOfThread(thread);
return (
<Link
key={thread.thread_id}
@@ -57,15 +63,20 @@ export function AgentRecentChats({ agentId }: { agentId: string }) {
: "text-muted-foreground hover:bg-accent hover:text-foreground",
)}
>
<MessageSquareIcon
className={cn(
"size-3.5 shrink-0",
isActive ? "text-primary" : "text-muted-foreground/70",
)}
/>
<span className="truncate">
{projectType ? (
<MyProjectThreadIcon type={projectType} />
) : (
<MessageSquareIcon
className={cn(
"size-3.5 shrink-0",
isActive ? "text-primary" : "text-muted-foreground/70",
)}
/>
)}
<span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, "新对话"))}
</span>
{projectType && <MyProjectThreadBadge type={projectType} />}
</Link>
);
})}
@@ -60,6 +60,10 @@ import {
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useAgentsPaginated, type Agent } from "@/core/agents";
import { getAPIClient } from "@/core/api";
import { useI18n } from "@/core/i18n/hooks";
@@ -75,6 +79,7 @@ import {
useToggleThreadPin,
} from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import {
isBootstrapThread,
isHiddenFromRecentChats,
@@ -483,6 +488,7 @@ export function GeneralQaSidebar() {
);
const isActive = href === pathname;
const isBoot = isBootstrapThread(thread);
const projectType = myProjectConversationTypeOfThread(thread);
const pinned = isThreadPinned(thread);
const favorited = isThreadFavorite(thread);
return (
@@ -507,6 +513,8 @@ export function GeneralQaSidebar() {
)}
aria-label="智能体创建会话"
/>
) : projectType ? (
<MyProjectThreadIcon type={projectType} />
) : (
<MessageSquare
className={cn(
@@ -518,9 +526,15 @@ export function GeneralQaSidebar() {
{pinned && (
<Pin className="size-3 shrink-0 text-muted-foreground/70" />
)}
<span className="truncate">
<span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))}
</span>
{projectType && (
<MyProjectThreadBadge
className="mr-4"
type={projectType}
/>
)}
{isActive && (
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
)}
@@ -25,6 +25,7 @@ export function ModelNameSelect({
className,
triggerClassName,
defaultLabel = "默认模型",
title = "选择用于本次操作的大模型(默认使用系统配置的模型)",
}: {
value: string | null;
onChange: (modelName: string | null) => void;
@@ -32,6 +33,7 @@ export function ModelNameSelect({
className?: string;
triggerClassName?: string;
defaultLabel?: string;
title?: string;
}) {
const { models } = useModels();
return (
@@ -42,7 +44,7 @@ export function ModelNameSelect({
>
<SelectTrigger
className={cn("h-8 gap-1.5 text-xs", triggerClassName, className)}
title="选择用于本次操作的大模型(默认使用系统配置的模型)"
title={title}
>
<SelectValue placeholder={defaultLabel} />
</SelectTrigger>
@@ -0,0 +1,51 @@
import { FolderKanban } from "lucide-react";
import {
MY_PROJECT_CONVERSATION_LABELS,
type MyProjectConversationType,
} from "@/core/threads/project-metadata";
import { cn } from "@/lib/utils";
const BADGE_STYLES: Record<MyProjectConversationType, string> = {
qa: "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-700/70 dark:bg-sky-950/55 dark:text-sky-300",
strategy:
"border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-700/70 dark:bg-violet-950/55 dark:text-violet-300",
solution:
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-700/70 dark:bg-emerald-950/55 dark:text-emerald-300",
};
export function MyProjectThreadIcon({
type,
className,
}: {
type: MyProjectConversationType;
className?: string;
}) {
return (
<FolderKanban
aria-label={`我的项目 · ${MY_PROJECT_CONVERSATION_LABELS[type]}`}
className={cn("size-3.5 shrink-0 text-violet-500", className)}
/>
);
}
export function MyProjectThreadBadge({
type,
className,
}: {
type: MyProjectConversationType;
className?: string;
}) {
return (
<span
className={cn(
"shrink-0 rounded border px-1 py-0.5 text-[9px] font-medium leading-none",
BADGE_STYLES[type],
className,
)}
title={`来自我的项目 · ${MY_PROJECT_CONVERSATION_LABELS[type]}`}
>
{MY_PROJECT_CONVERSATION_LABELS[type]}
</span>
);
}
@@ -49,7 +49,11 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { getAPIClient } from "@/core/api";
import { useI18n } from "@/core/i18n/hooks";
import {
@@ -63,7 +67,8 @@ import {
useToggleThreadFavorite,
useToggleThreadPin,
} from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import {
isBootstrapThread,
isHiddenFromRecentChats,
@@ -245,7 +250,8 @@ export function RecentChatList({
{shownThreads.map((thread) => {
const href = listPathOfThread(thread);
const isActive = href === pathname;
const isBoot = isBootstrapThread(thread);
const isBoot = isBootstrapThread(thread);
const projectType = myProjectConversationTypeOfThread(thread);
const pinned = isThreadPinned(thread);
const favorited = isThreadFavorite(thread);
return (
@@ -279,8 +285,10 @@ export function RecentChatList({
}
aria-label="智能体创建会话"
/>
) : (
compact && (
) : projectType ? (
<MyProjectThreadIcon type={projectType} />
) : (
compact && (
<MessageSquare
className={
"size-3.5 shrink-0 " +
@@ -294,9 +302,15 @@ export function RecentChatList({
{pinned && (
<Pin className="size-3 shrink-0 text-muted-foreground/70" />
)}
<span className="truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))}
</span>
<span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))}
</span>
{projectType && (
<MyProjectThreadBadge
className="mr-3"
type={projectType}
/>
)}
</Link>
</SidebarMenuButton>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
@@ -14,7 +14,7 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useI18n } from "@/core/i18n/hooks";
import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, Library, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, FolderKanban, Library, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
import { KnowledgeRagToggle } from "./knowledge-rag-toggle";
import { SidebarCollapseButton } from "./workspace-header";
@@ -65,6 +65,15 @@ export const WORKSPACE_COMPACT_NAV_ITEMS: WorkspaceCompactNavItem[] = [
path: "/page/workspace/chats",
match: (pathname) => pathname === "/page/workspace/chats",
},
{
id: "strategy-projects",
label: "我的项目",
icon: FolderKanban,
path: "/page/strategy/qa/projects",
match: (pathname) =>
pathname.startsWith("/page/strategy/qa/projects") ||
pathname.startsWith("/page/strategy/qa/strategy-selection"),
},
{
id: "workspace-library",
label: "资料库",