Merge remote-tracking branch 'origin/master' into ji
This commit is contained in:
commit
a800baf8ae
107 files changed
+13645
-16203
No files matched your search
@@ -72,6 +72,12 @@ const menuItems: MenuItem[] = [
|
||||
icon: HelpCircle,
|
||||
children: [
|
||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||
{
|
||||
id: "qa-roundtable",
|
||||
label: "圆桌规划",
|
||||
icon: Bot,
|
||||
path: "/page/roundtable/planning",
|
||||
},
|
||||
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
|
||||
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
|
||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||
@@ -99,6 +105,12 @@ const menuItems: MenuItem[] = [
|
||||
icon: List,
|
||||
path: "/page/strategy/light-app/notes",
|
||||
},
|
||||
{
|
||||
id: "light-app-note-qa",
|
||||
label: "笔记问答",
|
||||
icon: MessageSquareCode,
|
||||
path: "/page/strategy/light-app/note-qa",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: "light-app-square", label: "应用广场", icon: Layout },
|
||||
|
||||
@@ -397,53 +397,53 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
@@ -503,99 +503,99 @@ const sidebarMenuButtonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
@@ -686,37 +686,37 @@ function SidebarMenuSubItem({
|
||||
);
|
||||
}
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive = false, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive = false, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
|
||||
@@ -526,7 +526,7 @@ interface CoTReasoningStep extends GenericCoTStep<"reasoning"> {
|
||||
interface CoTToolCallStep extends GenericCoTStep<"toolCall"> {
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
result?: string;
|
||||
result?: string | Record<string, unknown>;
|
||||
}
|
||||
|
||||
type CoTStep = CoTReasoningStep | CoTToolCallStep;
|
||||
@@ -558,12 +558,22 @@ function convertToSteps(messages: Message[]): CoTStep[] {
|
||||
};
|
||||
const toolCallId = tool_call.id;
|
||||
if (toolCallId) {
|
||||
const toolCallResult = findToolCallResult(toolCallId, messages);
|
||||
if (toolCallResult) {
|
||||
try {
|
||||
const json = JSON.parse(toolCallResult);
|
||||
step.result = json;
|
||||
} catch {
|
||||
const toolCallResult = findToolCallResult(
|
||||
toolCallId,
|
||||
messages,
|
||||
tool_call.name,
|
||||
);
|
||||
if (toolCallResult !== undefined) {
|
||||
if (typeof toolCallResult === "string") {
|
||||
try {
|
||||
step.result = JSON.parse(toolCallResult) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
} catch {
|
||||
step.result = toolCallResult;
|
||||
}
|
||||
} else {
|
||||
step.result = toolCallResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import type { Subtask } from "@/core/tasks";
|
||||
import { useUpdateSubtask } from "@/core/tasks/context";
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification";
|
||||
|
||||
import { ArtifactFileList } from "../artifacts/artifact-file-list";
|
||||
import { StreamingIndicator } from "../streaming-indicator";
|
||||
@@ -115,7 +116,9 @@ export function MessageList({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmitAllClarifications = useCallback(async () => {
|
||||
const handleSubmitAllClarifications = useCallback(async (
|
||||
selectionOverrides?: Map<string, ClarificationSelection>,
|
||||
) => {
|
||||
if (pendingClarificationIds.length === 0 || !onClarificationSubmit) {
|
||||
return;
|
||||
}
|
||||
@@ -124,7 +127,7 @@ export function MessageList({
|
||||
const id = pendingClarificationIds[index]!;
|
||||
const message = messages.find((msg) => msg.id === id);
|
||||
const payload = message ? getClarificationPayload(message) : null;
|
||||
const selection = clarificationSelections.get(id) ?? {
|
||||
const selection = selectionOverrides?.get(id) ?? clarificationSelections.get(id) ?? {
|
||||
selectedIds: [],
|
||||
custom: "",
|
||||
};
|
||||
@@ -244,7 +247,12 @@ export function MessageList({
|
||||
}
|
||||
submitting={submittingClarification}
|
||||
canSubmit={hasAnyClarificationAnswer}
|
||||
onSubmitAll={handleSubmitAllClarifications}
|
||||
onSubmitAll={(override) => {
|
||||
const overrides = override
|
||||
? new Map([[messageId, override]])
|
||||
: undefined;
|
||||
void handleSubmitAllClarifications(overrides);
|
||||
}}
|
||||
/>
|
||||
<MessageTokenUsageList
|
||||
enabled={tokenUsageEnabled}
|
||||
@@ -435,6 +443,7 @@ type ClarificationPayload = {
|
||||
clarification_type?: string;
|
||||
options?: ClarificationOption[];
|
||||
allow_custom?: boolean;
|
||||
allow_multiple?: boolean;
|
||||
};
|
||||
|
||||
type ClarificationSelection = {
|
||||
@@ -641,7 +650,7 @@ function ClarificationCard({
|
||||
onSelectionChange: (next: ClarificationSelection) => void;
|
||||
submitting: boolean;
|
||||
canSubmit: boolean;
|
||||
onSubmitAll: () => void | Promise<void>;
|
||||
onSubmitAll: (override?: ClarificationSelection) => void | Promise<void>;
|
||||
}) {
|
||||
const payload = getClarificationPayload(message);
|
||||
|
||||
@@ -665,11 +674,21 @@ function ClarificationCard({
|
||||
|
||||
const disabled = !isPending || submitting;
|
||||
const multiPending = (pendingCount ?? 0) > 1;
|
||||
const allowMultiple = resolveAllowMultiple(
|
||||
payload.allow_multiple,
|
||||
payload.clarification_type,
|
||||
);
|
||||
|
||||
function toggleOption(optionId: string) {
|
||||
function handleOptionClick(optionId: string) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (!allowMultiple) {
|
||||
const next = { selectedIds: [optionId], custom: "" };
|
||||
onSelectionChange(next);
|
||||
void onSubmitAll(next);
|
||||
return;
|
||||
}
|
||||
const next = selection.selectedIds.includes(optionId)
|
||||
? selection.selectedIds.filter((id) => id !== optionId)
|
||||
: [...selection.selectedIds, optionId];
|
||||
@@ -707,7 +726,7 @@ function ClarificationCard({
|
||||
{options.length > 0 ? (
|
||||
<div className="mt-4 grid gap-2">
|
||||
<div className="text-muted-foreground text-xs">
|
||||
可多选(再次点击取消选择)
|
||||
{allowMultiple ? "可多选(再次点击取消选择)" : "请选择一项(点击即发送)"}
|
||||
</div>
|
||||
{options.map((option) => {
|
||||
const optionId = option.id || option.label;
|
||||
@@ -716,10 +735,10 @@ function ClarificationCard({
|
||||
<Button
|
||||
key={optionId}
|
||||
type="button"
|
||||
variant={isSelected ? "default" : "outline"}
|
||||
variant={isSelected && allowMultiple ? "default" : "outline"}
|
||||
className="h-auto justify-start whitespace-normal px-3 py-2 text-left"
|
||||
disabled={disabled}
|
||||
onClick={() => toggleOption(optionId)}
|
||||
onClick={() => handleOptionClick(optionId)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col items-start gap-1">
|
||||
<span>{option.label}</span>
|
||||
@@ -736,7 +755,7 @@ function ClarificationCard({
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{isSelected ? (
|
||||
{isSelected && allowMultiple ? (
|
||||
<CheckIcon className="ml-2 h-4 w-4 shrink-0" />
|
||||
) : null}
|
||||
</Button>
|
||||
@@ -761,10 +780,10 @@ function ClarificationCard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPending ? (
|
||||
{isPending && (allowMultiple || payload.allow_custom !== false) ? (
|
||||
isLastPending ? (
|
||||
<div className="mt-3 flex items-center justify-end gap-2">
|
||||
{multiPending ? (
|
||||
{multiPending && allowMultiple ? (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
将一起提交以上 {pendingCount} 个问题的回答
|
||||
</span>
|
||||
@@ -776,14 +795,14 @@ function ClarificationCard({
|
||||
onClick={() => void onSubmitAll()}
|
||||
>
|
||||
<SendIcon className="h-4 w-4" />
|
||||
{submitting ? "发送中…" : multiPending ? "全部发送" : "发送回答"}
|
||||
{submitting ? "发送中…" : multiPending && allowMultiple ? "全部发送" : "发送回答"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
) : allowMultiple ? (
|
||||
<div className="text-muted-foreground mt-3 text-right text-xs">
|
||||
选择后等待最后一题统一提交
|
||||
</div>
|
||||
)
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronDown,
|
||||
Download,
|
||||
FileJson,
|
||||
FileText,
|
||||
@@ -63,6 +64,15 @@ export function RecentChatList() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [collapsed, setCollapsed] = useState(
|
||||
() => localStorage.getItem("sidebar:recent-chats-collapsed") === "true",
|
||||
);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
const next = !collapsed;
|
||||
setCollapsed(next);
|
||||
localStorage.setItem("sidebar:recent-chats-collapsed", String(next));
|
||||
};
|
||||
const { thread_id: threadIdFromPath, agent_id: agentIdFromPath } =
|
||||
useParams<{
|
||||
thread_id: string;
|
||||
@@ -161,12 +171,24 @@ export function RecentChatList() {
|
||||
return (
|
||||
<>
|
||||
<SidebarGroup className="px-2 pt-3 mt-2 border-t border-sidebar-border/60">
|
||||
<SidebarGroupLabel className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase">
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
|
||||
? t.sidebar.recentChats
|
||||
: t.sidebar.demoChats}
|
||||
<SidebarGroupLabel
|
||||
className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase cursor-pointer select-none flex items-center justify-between hover:text-muted-foreground transition-colors"
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<span>
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
|
||||
? t.sidebar.recentChats
|
||||
: t.sidebar.demoChats}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 shrink-0 transition-transform duration-200 ${collapsed ? "-rotate-90" : ""}`}
|
||||
/>
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
|
||||
<SidebarGroupContent
|
||||
className={`group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 overflow-hidden transition-all duration-200 ${
|
||||
collapsed ? "max-h-0 opacity-0 pointer-events-none" : "max-h-[800px] opacity-100"
|
||||
}`}
|
||||
>
|
||||
<SidebarMenu>
|
||||
<div className="flex w-full flex-col gap-0.5">
|
||||
{visibleThreads.map((thread) => {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
formatRelative,
|
||||
STATUS_LABELS,
|
||||
} from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
|
||||
import { useAIWritingSessions } from "@/open-canvas/hooks/useAIWritingSessions";
|
||||
import type { AIWritingSessionSummary } from "@/open-canvas/api/ai-writing-sessions";
|
||||
|
||||
const AI_WRITING_PATH = "/page/workspace/ai-writing";
|
||||
|
||||
function openSession(
|
||||
session: AIWritingSessionSummary,
|
||||
isOnPage: boolean,
|
||||
router: ReturnType<typeof useRouter>,
|
||||
) {
|
||||
if (isOnPage) {
|
||||
// 已在 AI 写作页面,直接通过自定义事件通知面板
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("ai-writing:open-session", { detail: session }),
|
||||
);
|
||||
} else {
|
||||
// 不在该页面,把 session 存入 sessionStorage 后跳转
|
||||
sessionStorage.setItem("ai-writing:pending-session", JSON.stringify(session));
|
||||
router.push(AI_WRITING_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
export function RecentWritingList() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { sessions, loading, refresh } = useAIWritingSessions();
|
||||
const [collapsed, setCollapsed] = useState(
|
||||
() => localStorage.getItem("sidebar:recent-writing-collapsed") === "true",
|
||||
);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
const next = !collapsed;
|
||||
setCollapsed(next);
|
||||
localStorage.setItem("sidebar:recent-writing-collapsed", String(next));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
if (!loading && sessions.length === 0) return null;
|
||||
|
||||
const isOnPage = pathname === AI_WRITING_PATH;
|
||||
|
||||
return (
|
||||
<SidebarGroup className="px-2 pt-3 mt-0 border-t border-sidebar-border/60">
|
||||
<SidebarGroupLabel
|
||||
className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase cursor-pointer select-none flex items-center justify-between hover:text-muted-foreground transition-colors"
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<span>最近写作</span>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 shrink-0 transition-transform duration-200 ${collapsed ? "-rotate-90" : ""}`}
|
||||
/>
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent
|
||||
className={`group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 overflow-hidden transition-all duration-200 ${
|
||||
collapsed ? "max-h-0 opacity-0 pointer-events-none" : "max-h-[600px] opacity-100"
|
||||
}`}
|
||||
>
|
||||
<SidebarMenu>
|
||||
<div className="flex w-full flex-col gap-0.5">
|
||||
{loading && sessions.length === 0 ? (
|
||||
<p className="px-3 py-1 text-[12px] text-muted-foreground">加载中…</p>
|
||||
) : (
|
||||
sessions.slice(0, 10).map((session) => {
|
||||
const { label, color } = STATUS_LABELS[session.status] ?? {
|
||||
label: session.status,
|
||||
color: "text-muted-foreground",
|
||||
};
|
||||
return (
|
||||
<SidebarMenuItem key={session.id} className="group/side-menu-item">
|
||||
<SidebarMenuButton
|
||||
className={
|
||||
"rounded-lg h-auto py-1.5 px-3 text-muted-foreground " +
|
||||
"hover:text-foreground hover:bg-sidebar-accent/60"
|
||||
}
|
||||
onClick={() => openSession(session, isOnPage, router)}
|
||||
>
|
||||
<div className="flex flex-col items-start min-w-0 w-full">
|
||||
<span className="text-[13px] truncate w-full leading-snug">
|
||||
{session.title}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<span className={cn("text-[10px] font-medium", color)}>
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">·</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{formatRelative(session.updated_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { MonitorSmartphoneIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useMemo, type ComponentType, type SVGProps } from "react";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -27,18 +28,12 @@ const languageOptions: { value: Locale; label: string }[] = [
|
||||
|
||||
export function AppearanceSettingsPage() {
|
||||
const { t, locale, changeLocale } = useI18n();
|
||||
const { theme, setTheme, systemTheme } = useTheme();
|
||||
const currentTheme = (theme ?? "system") as "system" | "light" | "dark";
|
||||
const { theme, setTheme } = useTheme();
|
||||
const currentTheme = theme;
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
|
||||
const themeOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "system",
|
||||
label: t.settings.appearance.system,
|
||||
description: t.settings.appearance.systemDescription,
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
{
|
||||
id: "light",
|
||||
label: t.settings.appearance.light,
|
||||
@@ -57,8 +52,6 @@ export function AppearanceSettingsPage() {
|
||||
t.settings.appearance.darkDescription,
|
||||
t.settings.appearance.light,
|
||||
t.settings.appearance.lightDescription,
|
||||
t.settings.appearance.system,
|
||||
t.settings.appearance.systemDescription,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -68,7 +61,7 @@ export function AppearanceSettingsPage() {
|
||||
title={t.settings.appearance.themeTitle}
|
||||
description={t.settings.appearance.themeDescription}
|
||||
>
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
<div className="grid gap-3 lg:grid-cols-2">
|
||||
{themeOptions.map((option) => (
|
||||
<ThemePreviewCard
|
||||
key={option.id}
|
||||
@@ -76,8 +69,7 @@ export function AppearanceSettingsPage() {
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
active={currentTheme === option.id}
|
||||
mode={option.id as "system" | "light" | "dark"}
|
||||
systemTheme={systemTheme}
|
||||
mode={option.id as "light" | "dark"}
|
||||
onSelect={(value) => setTheme(value)}
|
||||
/>
|
||||
))}
|
||||
@@ -159,19 +151,16 @@ function ThemePreviewCard({
|
||||
description,
|
||||
active,
|
||||
mode,
|
||||
systemTheme,
|
||||
onSelect,
|
||||
}: {
|
||||
icon: ComponentType<SVGProps<SVGSVGElement>>;
|
||||
label: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
mode: "system" | "light" | "dark";
|
||||
systemTheme?: string;
|
||||
onSelect: (mode: "system" | "light" | "dark") => void;
|
||||
mode: "light" | "dark";
|
||||
onSelect: (mode: "light" | "dark") => void;
|
||||
}) {
|
||||
const previewMode =
|
||||
mode === "system" ? (systemTheme === "dark" ? "dark" : "light") : mode;
|
||||
const previewMode = mode;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
// import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react";
|
||||
import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, SparklesIcon } from "lucide-react";
|
||||
import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, SparklesIcon, Users } from "lucide-react";
|
||||
|
||||
import { SidebarCollapseButton } from "./workspace-header";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -119,6 +119,18 @@ export function WorkspaceNavChatList({
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={pathname.startsWith("/page/workspace/roundtable/planning")}
|
||||
asChild
|
||||
className={NAV_ITEM_CLASS}
|
||||
>
|
||||
<Link href="/page/workspace/roundtable/planning">
|
||||
<Users />
|
||||
<span>多智能体规划</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={pathname === "/page/workspace/scheduled/tasks"}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { RecentChatList } from "./recent-chat-list";
|
||||
import { RecentWritingList } from "./recent-writing-list";
|
||||
import { WorkspaceHeader } from "./workspace-header";
|
||||
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
||||
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||
@@ -45,6 +46,7 @@ export function WorkspaceSidebar({
|
||||
<SidebarContent>
|
||||
<WorkspaceNavChatList showCollapseTrigger={compactChrome} />
|
||||
{isSidebarOpen && <RecentChatList />}
|
||||
{isSidebarOpen && <RecentWritingList />}
|
||||
</SidebarContent>
|
||||
{!compactChrome && (
|
||||
<SidebarFooter>
|
||||
|
||||
Reference in new issue
Block a user